From dde22b17a9f5886e4beadd4893ef44dff9638cd4 Mon Sep 17 00:00:00 2001 From: Norbert Preining Date: Wed, 6 Jan 2021 14:50:52 +0000 Subject: [PATCH] Import kwin_5.20.5.orig.tar.xz [dgit import orig kwin_5.20.5.orig.tar.xz] --- 3rdparty/xcursor.c | 978 ++++ 3rdparty/xcursor.h | 76 + CMakeLists.txt | 870 +++ HACKING.md | 98 + KWinDBusInterfaceConfig.cmake.in | 6 + LICENSES/BSD-2-Clause.txt | 22 + LICENSES/BSD-3-Clause.txt | 26 + LICENSES/GPL-2.0-only.txt | 319 ++ LICENSES/GPL-2.0-or-later.txt | 319 ++ LICENSES/GPL-3.0-only.txt | 625 +++ LICENSES/GPL-3.0-or-later.txt | 625 +++ LICENSES/LGPL-2.0-only.txt | 446 ++ LICENSES/LGPL-2.0-or-later.txt | 446 ++ LICENSES/LGPL-2.1-only.txt | 467 ++ LICENSES/LGPL-3.0-only.txt | 163 + LICENSES/LicenseRef-KDE-Accepted-GPL.txt | 12 + LICENSES/LicenseRef-KDE-Accepted-LGPL.txt | 12 + LICENSES/MIT.txt | 19 + Mainpage.dox | 19 + Messages.sh | 3 + README.md | 48 + TESTING.md | 33 + abstract_client.cpp | 3525 ++++++++++++ abstract_client.h | 1371 +++++ abstract_opengl_context_attribute_builder.cpp | 29 + abstract_opengl_context_attribute_builder.h | 115 + abstract_output.cpp | 106 + abstract_output.h | 179 + abstract_wayland_output.cpp | 357 ++ abstract_wayland_output.h | 176 + activation.cpp | 881 +++ activities.cpp | 211 + activities.h | 118 + appmenu.cpp | 123 + appmenu.h | 64 + atoms.cpp | 91 + atoms.h | 98 + autotests/CMakeLists.txt | 426 ++ autotests/abstract_client.h | 1 + autotests/drm/CMakeLists.txt | 26 + autotests/drm/mock_drm.cpp | 67 + autotests/drm/mock_drm.h | 21 + autotests/drm/objecttest.cpp | 208 + autotests/fakeeffectplugin.cpp | 38 + autotests/fakeeffectplugin.json | 13 + autotests/fakeeffectplugin_version.cpp | 39 + autotests/fakeeffectplugin_version.json | 13 + autotests/integration/CMakeLists.txt | 114 + autotests/integration/activation_test.cpp | 573 ++ autotests/integration/activities_test.cpp | 151 + .../integration/buffer_size_change_test.cpp | 115 + .../colorcorrect_nightcolor_test.cpp | 328 ++ .../data/anim-data-delete-effect/effect.js | 14 + autotests/integration/data/example.desktop | 3 + .../data/rules/maximize-vert-apply-initial | 13 + autotests/integration/dbus_interface_test.cpp | 378 ++ autotests/integration/debug_console_test.cpp | 503 ++ .../integration/decoration_input_test.cpp | 795 +++ .../integration/desktop_window_x11_test.cpp | 166 + .../dont_crash_aurorae_destroy_deco.cpp | 146 + .../dont_crash_cancel_animation.cpp | 112 + .../dont_crash_cursor_physical_size_empty.cpp | 99 + .../integration/dont_crash_empty_deco.cpp | 111 + autotests/integration/dont_crash_glxgears.cpp | 95 + .../integration/dont_crash_no_border.cpp | 112 + .../dont_crash_reinitialize_compositor.cpp | 156 + .../dont_crash_useractions_menu.cpp | 104 + autotests/integration/effects/CMakeLists.txt | 12 + .../desktop_switching_animation_test.cpp | 150 + .../effects/maximize_animation_test.cpp | 189 + .../effects/minimize_animation_test.cpp | 185 + .../popup_open_close_animation_test.cpp | 267 + .../effects/scripted_effects_test.cpp | 780 +++ .../effects/scripts/animationTest.js | 12 + .../effects/scripts/animationTestMulti.js | 24 + .../effects/scripts/completeTest.js | 19 + .../effects/scripts/effectContext.js | 6 + .../effects/scripts/effectsHandler.js | 16 + .../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 | 18 + .../scripts/redirectAnimateTerminateTest.js | 18 + .../scripts/redirectSetDontTerminateTest.js | 19 + .../scripts/redirectSetTerminateTest.js | 19 + .../effects/scripts/screenEdgeTest.js | 3 + .../effects/scripts/screenEdgeTouchTest.js | 3 + .../effects/scripts/shortcutsTest.js | 3 + .../integration/effects/scripts/ungrabTest.js | 15 + .../effects/slidingpopups_test.cpp | 367 ++ .../toplevel_open_close_animation_test.cpp | 213 + .../integration/effects/translucency_test.cpp | 237 + .../effects/windowgeometry_test.cpp | 86 + .../integration/effects/wobbly_shade_test.cpp | 185 + autotests/integration/fakes/CMakeLists.txt | 2 + .../fakes/org.kde.kdecoration2/CMakeLists.txt | 15 + .../fakedecoration_with_shadows.cpp | 61 + .../fakedecoration_with_shadows.json | 15 + .../integration/generic_scene_opengl_test.cpp | 108 + .../integration/generic_scene_opengl_test.h | 29 + .../integration/globalshortcuts_test.cpp | 370 ++ autotests/integration/helper/CMakeLists.txt | 11 + autotests/integration/helper/copy.cpp | 60 + autotests/integration/helper/kill.cpp | 35 + autotests/integration/helper/paste.cpp | 57 + .../integration/idle_inhibition_test.cpp | 359 ++ .../integration/input_stacking_order.cpp | 166 + autotests/integration/internal_window.cpp | 815 +++ .../integration/keyboard_layout_test.cpp | 544 ++ .../keymap_creation_failure_test.cpp | 90 + autotests/integration/kwin_wayland_test.cpp | 181 + autotests/integration/kwin_wayland_test.h | 378 ++ autotests/integration/kwinbindings_test.cpp | 254 + .../integration/layershellv1client_test.cpp | 584 ++ autotests/integration/lockscreen.cpp | 773 +++ autotests/integration/maximize_test.cpp | 446 ++ .../modifier_only_shortcut_test.cpp | 375 ++ .../integration/move_resize_window_test.cpp | 1148 ++++ .../integration/no_global_shortcuts_test.cpp | 274 + .../integration/no_xdg_runtime_dir_test.cpp | 37 + autotests/integration/placement_test.cpp | 349 ++ autotests/integration/plasma_surface_test.cpp | 413 ++ autotests/integration/plasmawindow_test.cpp | 319 ++ autotests/integration/platformcursor.cpp | 60 + .../integration/pointer_constraints_test.cpp | 372 ++ autotests/integration/pointer_input.cpp | 1625 ++++++ .../protocols/wlr-layer-shell-unstable-v1.xml | 325 ++ autotests/integration/quick_tiling_test.cpp | 885 +++ .../integration/scene_opengl_es_test.cpp | 19 + .../integration/scene_opengl_shadow_test.cpp | 852 +++ autotests/integration/scene_opengl_test.cpp | 19 + .../scene_qpainter_shadow_test.cpp | 769 +++ autotests/integration/scene_qpainter_test.cpp | 366 ++ autotests/integration/screen_changes_test.cpp | 188 + .../screenedge_client_show_test.cpp | 283 + .../integration/scripting/CMakeLists.txt | 2 + .../scripting/minimizeall_test.cpp | 158 + .../integration/scripting/screenedge_test.cpp | 287 + .../scripting/scripts/screenedge.js | 1 + .../scripting/scripts/screenedgetouch.qml | 10 + .../scripting/scripts/screenedgeunregister.js | 12 + .../scripting/scripts/touchScreenedge.js | 1 + autotests/integration/shade_test.cpp | 130 + .../integration/showing_desktop_test.cpp | 115 + autotests/integration/stacking_order_test.cpp | 897 +++ autotests/integration/struts_test.cpp | 954 ++++ autotests/integration/tabbox_test.cpp | 244 + autotests/integration/test_helpers.cpp | 911 +++ autotests/integration/touch_input_test.cpp | 272 + autotests/integration/transient_placement.cpp | 355 ++ .../integration/virtual_desktop_test.cpp | 287 + .../integration/virtualkeyboard_test.cpp | 154 + autotests/integration/window_rules_test.cpp | 238 + .../integration/window_selection_test.cpp | 546 ++ autotests/integration/x11_client_test.cpp | 1111 ++++ .../integration/xdgshellclient_rules_test.cpp | 4216 ++++++++++++++ autotests/integration/xdgshellclient_test.cpp | 1596 ++++++ autotests/integration/xwayland_input_test.cpp | 302 + .../integration/xwayland_selections_test.cpp | 172 + .../integration/xwaylandserver_crash_test.cpp | 139 + .../xwaylandserver_restart_test.cpp | 122 + autotests/libinput/CMakeLists.txt | 85 + autotests/libinput/context_test.cpp | 81 + autotests/libinput/device_test.cpp | 2417 ++++++++ autotests/libinput/gesture_event_test.cpp | 203 + autotests/libinput/input_event_test.cpp | 179 + autotests/libinput/key_event_test.cpp | 105 + autotests/libinput/mock_libinput.cpp | 925 ++++ autotests/libinput/mock_libinput.h | 158 + autotests/libinput/mock_udev.cpp | 26 + autotests/libinput/mock_udev.h | 17 + autotests/libinput/pointer_event_test.cpp | 221 + autotests/libinput/switch_event_test.cpp | 88 + autotests/libinput/touch_event_test.cpp | 134 + autotests/libkwineffects/CMakeLists.txt | 20 + .../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-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 + .../glplatform/intel-sandybridge-mobile-3.3 | 19 + .../data/glplatform/intel-skylake-gt2-3.0 | 19 + .../data/glplatform/llvmpipe-10.0 | 22 + .../data/glplatform/llvmpipe-3.0 | 22 + .../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 + .../qualcomm-adreno-330-libhybris-gles-3.0 | 16 + .../libkwineffects/data/glplatform/virgl-3.1 | 22 + .../libkwineffects/kwinglplatformtest.cpp | 272 + autotests/libkwineffects/mock_gl.cpp | 59 + autotests/libkwineffects/mock_gl.h | 28 + autotests/libkwineffects/timelinetest.cpp | 403 ++ .../libkwineffects/windowquadlisttest.cpp | 210 + autotests/libxrenderutils/CMakeLists.txt | 13 + .../libxrenderutils/blendpicture_test.cpp | 50 + autotests/mock_abstract_client.cpp | 106 + autotests/mock_abstract_client.h | 59 + autotests/mock_effectshandler.cpp | 27 + autotests/mock_effectshandler.h | 280 + autotests/mock_screens.cpp | 87 + autotests/mock_screens.h | 42 + autotests/mock_workspace.cpp | 79 + autotests/mock_workspace.h | 72 + autotests/mock_x11client.cpp | 27 + autotests/mock_x11client.h | 32 + autotests/onscreennotificationtest.cpp | 125 + autotests/onscreennotificationtest.h | 24 + .../opengl_context_attribute_builder_test.cpp | 431 ++ autotests/tabbox/CMakeLists.txt | 100 + autotests/tabbox/mock_tabboxclient.cpp | 26 + autotests/tabbox/mock_tabboxclient.h | 63 + autotests/tabbox/mock_tabboxhandler.cpp | 111 + autotests/tabbox/mock_tabboxhandler.h | 99 + autotests/tabbox/test_desktopchain.cpp | 252 + autotests/tabbox/test_tabbox_clientmodel.cpp | 83 + autotests/tabbox/test_tabbox_clientmodel.h | 42 + autotests/tabbox/test_tabbox_config.cpp | 74 + autotests/tabbox/test_tabbox_handler.cpp | 52 + autotests/test_builtin_effectloader.cpp | 556 ++ autotests/test_client_machine.cpp | 146 + autotests/test_gbm_surface.cpp | 108 + autotests/test_gestures.cpp | 604 ++ autotests/test_plugin_effectloader.cpp | 413 ++ autotests/test_screen_edges.cpp | 1086 ++++ autotests/test_screen_paint_data.cpp | 276 + autotests/test_screens.cpp | 345 ++ autotests/test_scripted_effectloader.cpp | 464 ++ autotests/test_virtual_desktops.cpp | 642 +++ autotests/test_virtualkeyboard_dbus.cpp | 131 + autotests/test_window_paint_data.cpp | 557 ++ autotests/test_x11_timestamp_update.cpp | 129 + autotests/test_xcb_size_hints.cpp | 366 ++ autotests/test_xcb_window.cpp | 202 + autotests/test_xcb_wrapper.cpp | 520 ++ autotests/test_xkb.cpp | 502 ++ autotests/testutils.h | 52 + autotests/workspace.h | 1 + autotests/x11client.h | 1 + client_machine.cpp | 231 + client_machine.h | 106 + cmake/modules/COPYING-CMAKE-SCRIPTS | 22 + cmake/modules/FindFontconfig.cmake | 89 + cmake/modules/FindLibcap.cmake | 38 + cmake/modules/FindLibdrm.cmake | 105 + cmake/modules/FindLibinput.cmake | 104 + cmake/modules/FindUDev.cmake | 28 + cmake/modules/FindXKB.cmake | 89 + cmake/modules/FindXwayland.cmake | 13 + cmake/modules/Findepoll.cmake | 59 + cmake/modules/Findepoxy.cmake | 34 + cmake/modules/Findgbm.cmake | 104 + cmake/modules/Findhwdata.cmake | 25 + cmake/modules/Findlibhybris.cmake | 164 + colorcorrection/clockskewnotifier.cpp | 78 + colorcorrection/clockskewnotifier.h | 62 + colorcorrection/clockskewnotifierengine.cpp | 29 + .../clockskewnotifierengine_linux.cpp | 62 + .../clockskewnotifierengine_linux.h | 32 + colorcorrection/clockskewnotifierengine_p.h | 28 + colorcorrection/colorcorrect_settings.kcfg | 57 + colorcorrection/colorcorrect_settings.kcfgc | 8 + colorcorrection/colorcorrectdbusinterface.cpp | 312 ++ colorcorrection/colorcorrectdbusinterface.h | 156 + colorcorrection/constants.h | 278 + colorcorrection/manager.cpp | 947 ++++ colorcorrection/manager.h | 320 ++ colorcorrection/suncalc.cpp | 163 + colorcorrection/suncalc.h | 36 + composite.cpp | 1046 ++++ composite.h | 263 + config-kwin.h.cmake | 46 + cursor.cpp | 494 ++ cursor.h | 353 ++ data/CMakeLists.txt | 14 + data/icons/16-apps-kwin.png | Bin 0 -> 380 bytes data/icons/32-apps-kwin.png | Bin 0 -> 611 bytes data/icons/48-apps-kwin.png | Bin 0 -> 877 bytes data/icons/CMakeLists.txt | 11 + data/icons/sc-apps-kwin.svgz | Bin 0 -> 3106 bytes data/org_kde_kwin.categories | 25 + data/update_default_rules.cpp | 58 + dbusinterface.cpp | 511 ++ dbusinterface.h | 243 + debug_console.cpp | 1562 ++++++ debug_console.h | 185 + debug_console.ui | 432 ++ decorations/decoratedclient.cpp | 335 ++ decorations/decoratedclient.h | 114 + decorations/decorationbridge.cpp | 323 ++ decorations/decorationbridge.h | 93 + decorations/decorationpalette.cpp | 127 + decorations/decorationpalette.h | 59 + decorations/decorationrenderer.cpp | 101 + decorations/decorationrenderer.h | 76 + decorations/decorations_logging.cpp | 10 + decorations/decorations_logging.h | 15 + decorations/settings.cpp | 193 + decorations/settings.h | 60 + deleted.cpp | 301 + deleted.h | 239 + dmabuftexture.cpp | 28 + dmabuftexture.h | 31 + doc/CMakeLists.txt | 7 + doc/coding-conventions.md | 86 + doc/desktop/CMakeLists.txt | 2 + doc/desktop/index.docbook | 98 + doc/kwindecoration/CMakeLists.txt | 2 + doc/kwindecoration/button.png | Bin 0 -> 27081 bytes doc/kwindecoration/configure.png | Bin 0 -> 384 bytes doc/kwindecoration/decoration.png | Bin 0 -> 30586 bytes doc/kwindecoration/index.docbook | 142 + doc/kwindecoration/main.png | Bin 0 -> 36500 bytes doc/kwineffects/CMakeLists.txt | 2 + doc/kwineffects/configure-effects.png | Bin 0 -> 512 bytes doc/kwineffects/dialog-information.png | Bin 0 -> 745 bytes doc/kwineffects/index.docbook | 85 + doc/kwineffects/video.png | Bin 0 -> 375 bytes doc/kwinscreenedges/CMakeLists.txt | 2 + doc/kwinscreenedges/index.docbook | 63 + doc/kwintabbox/CMakeLists.txt | 2 + doc/kwintabbox/index.docbook | 102 + doc/windowbehaviour/CMakeLists.txt | 2 + doc/windowbehaviour/index.docbook | 672 +++ doc/windowspecific/CMakeLists.txt | 2 + doc/windowspecific/Face-smile.png | Bin 0 -> 1233 bytes doc/windowspecific/akgregator-info.png | Bin 0 -> 46542 bytes doc/windowspecific/akregator-attributes.png | Bin 0 -> 68855 bytes doc/windowspecific/akregator-fav.png | Bin 0 -> 157267 bytes doc/windowspecific/config-win-behavior.png | Bin 0 -> 36233 bytes doc/windowspecific/emacs-attribute.png | Bin 0 -> 71059 bytes doc/windowspecific/emacs-info.png | Bin 0 -> 34767 bytes .../focus-stealing-pop2top-attribute.png | Bin 0 -> 58619 bytes doc/windowspecific/index.docbook | 1020 ++++ doc/windowspecific/knotes-attribute.png | Bin 0 -> 35453 bytes doc/windowspecific/knotes-info.png | Bin 0 -> 21435 bytes doc/windowspecific/kopete-attribute-2.png | Bin 0 -> 49387 bytes doc/windowspecific/kopete-chat-attribute.png | Bin 0 -> 50398 bytes doc/windowspecific/kopete-chat-info.png | Bin 0 -> 32735 bytes doc/windowspecific/kopete-info.png | Bin 0 -> 31642 bytes doc/windowspecific/kwin-detect-window.png | Bin 0 -> 27744 bytes doc/windowspecific/kwin-kopete-rules.png | Bin 0 -> 47435 bytes doc/windowspecific/kwin-rule-editor.png | Bin 0 -> 40361 bytes .../kwin-rules-main-n-akregator.png | Bin 0 -> 49262 bytes doc/windowspecific/kwin-rules-main.png | Bin 0 -> 48580 bytes doc/windowspecific/kwin-rules-ordering.png | Bin 0 -> 29609 bytes doc/windowspecific/kwin-window-attributes.png | Bin 0 -> 61505 bytes doc/windowspecific/kwin-window-matching.png | Bin 0 -> 53362 bytes doc/windowspecific/pager-4-desktops.png | Bin 0 -> 11817 bytes .../tbird-compose-attribute.png | Bin 0 -> 59885 bytes doc/windowspecific/tbird-compose-info.png | Bin 0 -> 36961 bytes doc/windowspecific/tbird-main-attribute.png | Bin 0 -> 73055 bytes doc/windowspecific/tbird-main-info.png | Bin 0 -> 36343 bytes .../tbird-reminder-attribute-2.png | Bin 0 -> 52173 bytes doc/windowspecific/tbird-reminder-info.png | Bin 0 -> 36154 bytes doc/windowspecific/window-matching-emacs.png | Bin 0 -> 55919 bytes doc/windowspecific/window-matching-init.png | Bin 0 -> 47718 bytes doc/windowspecific/window-matching-knotes.png | Bin 0 -> 37378 bytes .../window-matching-kopete-chat.png | Bin 0 -> 52380 bytes doc/windowspecific/window-matching-kopete.png | Bin 0 -> 50418 bytes .../window-matching-ready-akregator.png | Bin 0 -> 50090 bytes .../window-matching-tbird-compose.png | Bin 0 -> 50248 bytes .../window-matching-tbird-main.png | Bin 0 -> 55370 bytes .../window-matching-tbird-reminder.png | Bin 0 -> 55508 bytes effectloader.cpp | 547 ++ effectloader.h | 366 ++ effects.cpp | 2407 ++++++++ effects.h | 663 +++ effects/CMakeLists.txt | 223 + effects/Messages.sh | 4 + effects/backgroundcontrast/.directory | 3 + effects/backgroundcontrast/CMakeLists.txt | 8 + .../backgroundcontrast.kdev4 | 3 + effects/backgroundcontrast/contrast.cpp | 520 ++ effects/backgroundcontrast/contrast.h | 91 + effects/backgroundcontrast/contrastshader.cpp | 198 + effects/backgroundcontrast/contrastshader.h | 63 + effects/blur/CMakeLists.txt | 23 + effects/blur/blur.cpp | 818 +++ effects/blur/blur.h | 134 + effects/blur/blur.kcfg | 15 + effects/blur/blur_config.cpp | 49 + effects/blur/blur_config.desktop | 89 + effects/blur/blur_config.h | 33 + effects/blur/blur_config.ui | 160 + effects/blur/blurconfig.kcfgc | 5 + effects/blur/blurshader.cpp | 432 ++ effects/blur/blurshader.h | 103 + effects/colorpicker/colorpicker.cpp | 120 + effects/colorpicker/colorpicker.h | 54 + effects/coverswitch/CMakeLists.txt | 26 + effects/coverswitch/coverswitch.cpp | 994 ++++ effects/coverswitch/coverswitch.h | 155 + effects/coverswitch/coverswitch.kcfg | 42 + effects/coverswitch/coverswitch_config.cpp | 56 + .../coverswitch/coverswitch_config.desktop | 76 + effects/coverswitch/coverswitch_config.h | 43 + effects/coverswitch/coverswitch_config.ui | 307 + effects/coverswitch/coverswitchconfig.kcfgc | 5 + .../shaders/1.10/coverswitch-reflection.glsl | 9 + .../shaders/1.40/coverswitch-reflection.glsl | 12 + effects/cube/CMakeLists.txt | 32 + effects/cube/cube.cpp | 1739 ++++++ effects/cube/cube.h | 252 + effects/cube/cube.kcfg | 68 + effects/cube/cube_config.cpp | 110 + effects/cube/cube_config.desktop | 83 + effects/cube/cube_config.h | 46 + effects/cube/cube_config.ui | 569 ++ effects/cube/cube_inside.h | 29 + effects/cube/cube_proxy.cpp | 36 + effects/cube/cube_proxy.h | 34 + effects/cube/cubeconfig.kcfgc | 6 + effects/cube/data/1.10/cube-cap.glsl | 29 + effects/cube/data/1.10/cube-reflection.glsl | 8 + effects/cube/data/1.10/cylinder.vert | 35 + effects/cube/data/1.10/sphere.vert | 41 + effects/cube/data/1.40/cube-cap.glsl | 32 + effects/cube/data/1.40/cube-reflection.glsl | 11 + effects/cube/data/1.40/cylinder.vert | 36 + effects/cube/data/1.40/sphere.vert | 42 + effects/cube/data/cubecap.png | Bin 0 -> 48777 bytes effects/cubeslide/CMakeLists.txt | 24 + effects/cubeslide/cubeslide.cpp | 660 +++ effects/cubeslide/cubeslide.h | 105 + effects/cubeslide/cubeslide.kcfg | 25 + effects/cubeslide/cubeslide_config.cpp | 58 + effects/cubeslide/cubeslide_config.desktop | 74 + effects/cubeslide/cubeslide_config.h | 43 + effects/cubeslide/cubeslide_config.ui | 105 + effects/cubeslide/cubeslideconfig.kcfgc | 5 + effects/desktopgrid/CMakeLists.txt | 31 + effects/desktopgrid/desktopgrid.cpp | 1414 +++++ effects/desktopgrid/desktopgrid.h | 161 + effects/desktopgrid/desktopgrid.kcfg | 32 + effects/desktopgrid/desktopgrid_config.cpp | 129 + .../desktopgrid/desktopgrid_config.desktop | 85 + effects/desktopgrid/desktopgrid_config.h | 51 + effects/desktopgrid/desktopgrid_config.ui | 250 + effects/desktopgrid/desktopgridconfig.kcfgc | 5 + effects/desktopgrid/main.qml | 26 + .../package/contents/code/main.js | 153 + effects/dialogparent/package/metadata.desktop | 159 + effects/diminactive/CMakeLists.txt | 23 + effects/diminactive/diminactive.cpp | 409 ++ effects/diminactive/diminactive.h | 129 + effects/diminactive/diminactive.kcfg | 27 + effects/diminactive/diminactive_config.cpp | 54 + .../diminactive/diminactive_config.desktop | 81 + effects/diminactive/diminactive_config.h | 37 + effects/diminactive/diminactive_config.ui | 83 + effects/diminactive/diminactiveconfig.kcfgc | 5 + .../dimscreen/package/contents/code/main.js | 227 + effects/dimscreen/package/metadata.desktop | 145 + effects/effect_builtins.cpp | 748 +++ effects/effect_builtins.h | 96 + .../eyeonscreen/package/contents/code/main.js | 152 + effects/eyeonscreen/package/metadata.desktop | 73 + effects/fade/CMakeLists.txt | 8 + effects/fade/package/contents/code/main.js | 106 + effects/fade/package/contents/config/main.xml | 20 + effects/fade/package/metadata.desktop | 163 + effects/fadedesktop/CMakeLists.txt | 8 + .../fadedesktop/package/contents/code/main.js | 125 + effects/fadedesktop/package/metadata.desktop | 148 + .../package/contents/code/main.js | 140 + effects/fadingpopups/package/metadata.desktop | 83 + effects/fallapart/CMakeLists.txt | 7 + effects/fallapart/fallapart.cpp | 192 + effects/fallapart/fallapart.h | 56 + effects/fallapart/fallapart.kcfg | 14 + effects/fallapart/fallapartconfig.kcfgc | 5 + effects/flipswitch/CMakeLists.txt | 24 + effects/flipswitch/flipswitch.cpp | 979 ++++ effects/flipswitch/flipswitch.h | 154 + effects/flipswitch/flipswitch.kcfg | 30 + effects/flipswitch/flipswitch_config.cpp | 86 + effects/flipswitch/flipswitch_config.desktop | 77 + effects/flipswitch/flipswitch_config.h | 46 + effects/flipswitch/flipswitch_config.ui | 238 + effects/flipswitch/flipswitchconfig.kcfgc | 5 + .../frozenapp/package/contents/code/main.js | 118 + effects/frozenapp/package/metadata.desktop | 99 + .../package/contents/code/fullscreen.js | 90 + effects/fullscreen/package/metadata.desktop | 62 + effects/glide/CMakeLists.txt | 24 + effects/glide/glide.cpp | 326 ++ effects/glide/glide.h | 145 + effects/glide/glide.kcfg | 40 + effects/glide/glide_config.cpp | 48 + effects/glide/glide_config.desktop | 69 + effects/glide/glide_config.h | 34 + effects/glide/glide_config.ui | 260 + effects/glide/glideconfig.kcfgc | 5 + effects/highlightwindow/CMakeLists.txt | 7 + effects/highlightwindow/highlightwindow.cpp | 300 + effects/highlightwindow/highlightwindow.h | 76 + effects/invert/CMakeLists.txt | 25 + effects/invert/data/1.10/invert.frag | 22 + effects/invert/data/1.40/invert.frag | 25 + effects/invert/invert.cpp | 140 + effects/invert/invert.h | 64 + effects/invert/invert_config.cpp | 96 + effects/invert/invert_config.desktop | 90 + effects/invert/invert_config.h | 38 + effects/kscreen/CMakeLists.txt | 10 + effects/kscreen/kscreen.cpp | 181 + effects/kscreen/kscreen.h | 55 + effects/kscreen/kscreen.kcfg | 12 + effects/kscreen/kscreenconfig.kcfgc | 5 + effects/kwineffect.desktop | 107 + effects/logging.cpp | 10 + effects/login/package/contents/code/main.js | 79 + .../login/package/contents/config/main.xml | 12 + effects/login/package/contents/ui/config.ui | 38 + effects/login/package/metadata.desktop | 173 + effects/logout/package/contents/code/main.js | 71 + effects/logout/package/metadata.desktop | 86 + effects/lookingglass/CMakeLists.txt | 27 + .../lookingglass/data/1.10/lookingglass.frag | 25 + .../lookingglass/data/1.40/lookingglass.frag | 28 + effects/lookingglass/lookingglass.cpp | 246 + effects/lookingglass/lookingglass.h | 72 + effects/lookingglass/lookingglass.kcfg | 12 + effects/lookingglass/lookingglass_config.cpp | 108 + .../lookingglass/lookingglass_config.desktop | 83 + effects/lookingglass/lookingglass_config.h | 46 + effects/lookingglass/lookingglass_config.ui | 59 + effects/lookingglass/lookingglassconfig.kcfgc | 5 + effects/magiclamp/CMakeLists.txt | 23 + effects/magiclamp/magiclamp.cpp | 363 ++ effects/magiclamp/magiclamp.h | 58 + effects/magiclamp/magiclamp.kcfg | 12 + effects/magiclamp/magiclamp_config.cpp | 60 + effects/magiclamp/magiclamp_config.desktop | 80 + effects/magiclamp/magiclamp_config.h | 43 + effects/magiclamp/magiclamp_config.ui | 53 + effects/magiclamp/magiclampconfig.kcfgc | 5 + effects/magnifier/CMakeLists.txt | 24 + effects/magnifier/magnifier.cpp | 331 ++ effects/magnifier/magnifier.h | 71 + effects/magnifier/magnifier.kcfg | 18 + effects/magnifier/magnifier_config.cpp | 109 + effects/magnifier/magnifier_config.desktop | 90 + effects/magnifier/magnifier_config.h | 46 + effects/magnifier/magnifier_config.ui | 106 + effects/magnifier/magnifierconfig.kcfgc | 5 + .../package/contents/code/maximize.js | 90 + effects/maximize/package/metadata.desktop | 116 + .../package/contents/code/morphingpopups.js | 125 + .../morphingpopups/package/metadata.desktop | 95 + effects/mouseclick/CMakeLists.txt | 25 + effects/mouseclick/mouseclick.cpp | 378 ++ effects/mouseclick/mouseclick.h | 174 + effects/mouseclick/mouseclick.kcfg | 34 + effects/mouseclick/mouseclick_config.cpp | 83 + effects/mouseclick/mouseclick_config.desktop | 57 + effects/mouseclick/mouseclick_config.h | 45 + effects/mouseclick/mouseclick_config.ui | 282 + effects/mouseclick/mouseclickconfig.kcfgc | 5 + effects/mousemark/CMakeLists.txt | 25 + effects/mousemark/mousemark.cpp | 283 + effects/mousemark/mousemark.h | 65 + effects/mousemark/mousemark.kcfg | 15 + effects/mousemark/mousemark_config.cpp | 97 + effects/mousemark/mousemark_config.desktop | 85 + effects/mousemark/mousemark_config.h | 45 + effects/mousemark/mousemark_config.ui | 110 + effects/mousemark/mousemarkconfig.kcfgc | 5 + effects/presentwindows/CMakeLists.txt | 29 + effects/presentwindows/main.qml | 18 + effects/presentwindows/presentwindows.cpp | 2007 +++++++ effects/presentwindows/presentwindows.h | 323 ++ effects/presentwindows/presentwindows.kcfg | 59 + .../presentwindows/presentwindows_config.cpp | 107 + .../presentwindows_config.desktop | 86 + .../presentwindows/presentwindows_config.h | 46 + .../presentwindows/presentwindows_config.ui | 492 ++ .../presentwindows/presentwindows_proxy.cpp | 36 + effects/presentwindows/presentwindows_proxy.h | 35 + .../presentwindows/presentwindowsconfig.kcfgc | 6 + effects/resize/CMakeLists.txt | 23 + effects/resize/resize.cpp | 166 + effects/resize/resize.h | 62 + effects/resize/resize.kcfg | 15 + effects/resize/resize_config.cpp | 59 + effects/resize/resize_config.desktop | 76 + effects/resize/resize_config.h | 43 + effects/resize/resize_config.ui | 45 + effects/resize/resizeconfig.kcfgc | 5 + effects/scale/package/contents/code/main.js | 169 + .../scale/package/contents/config/main.xml | 28 + effects/scale/package/contents/ui/config.ui | 93 + effects/scale/package/metadata.desktop | 88 + effects/screenedge/CMakeLists.txt | 7 + effects/screenedge/screenedgeeffect.cpp | 362 ++ effects/screenedge/screenedgeeffect.h | 68 + effects/screenshot/CMakeLists.txt | 8 + effects/screenshot/screenshot.cpp | 801 +++ effects/screenshot/screenshot.h | 170 + .../sessionquit/package/contents/code/main.js | 32 + effects/sessionquit/package/metadata.desktop | 77 + effects/shaders.qrc | 23 + effects/sheet/CMakeLists.txt | 8 + effects/sheet/sheet.cpp | 219 + effects/sheet/sheet.h | 74 + effects/sheet/sheet.kcfg | 12 + effects/sheet/sheetconfig.kcfgc | 5 + effects/showfps/CMakeLists.txt | 24 + effects/showfps/showfps.cpp | 540 ++ effects/showfps/showfps.h | 98 + effects/showfps/showfps.kcfg | 28 + effects/showfps/showfps_config.cpp | 56 + effects/showfps/showfps_config.desktop | 86 + effects/showfps/showfps_config.h | 36 + effects/showfps/showfps_config.ui | 175 + effects/showfps/showfpsconfig.kcfgc | 5 + effects/showpaint/CMakeLists.txt | 24 + effects/showpaint/showpaint.cpp | 142 + effects/showpaint/showpaint.h | 45 + effects/showpaint/showpaint_config.cpp | 76 + effects/showpaint/showpaint_config.desktop | 75 + effects/showpaint/showpaint_config.h | 35 + effects/showpaint/showpaint_config.ui | 39 + effects/slide/CMakeLists.txt | 23 + effects/slide/slide.cpp | 447 ++ effects/slide/slide.h | 131 + effects/slide/slide.kcfg | 25 + effects/slide/slide_config.cpp | 52 + effects/slide/slide_config.desktop | 74 + effects/slide/slide_config.h | 36 + effects/slide/slide_config.ui | 133 + effects/slide/slideconfig.kcfgc | 5 + effects/slideback/CMakeLists.txt | 7 + effects/slideback/slideback.cpp | 330 ++ effects/slideback/slideback.h | 69 + effects/slidingpopups/CMakeLists.txt | 7 + effects/slidingpopups/slidingpopups.cpp | 533 ++ effects/slidingpopups/slidingpopups.h | 107 + effects/slidingpopups/slidingpopups.kcfg | 17 + .../slidingpopups/slidingpopupsconfig.kcfgc | 5 + effects/snaphelper/CMakeLists.txt | 7 + effects/snaphelper/snaphelper.cpp | 324 ++ effects/snaphelper/snaphelper.h | 55 + effects/squash/package/contents/code/main.js | 155 + effects/squash/package/metadata.desktop | 83 + effects/startupfeedback/CMakeLists.txt | 7 + .../data/1.10/blinking-startup-fragment.glsl | 13 + .../data/1.40/blinking-startup-fragment.glsl | 16 + effects/startupfeedback/startupfeedback.cpp | 388 ++ effects/startupfeedback/startupfeedback.h | 84 + effects/thumbnailaside/CMakeLists.txt | 24 + effects/thumbnailaside/thumbnailaside.cpp | 185 + effects/thumbnailaside/thumbnailaside.h | 80 + effects/thumbnailaside/thumbnailaside.kcfg | 21 + .../thumbnailaside/thumbnailaside_config.cpp | 90 + .../thumbnailaside_config.desktop | 83 + .../thumbnailaside/thumbnailaside_config.h | 45 + .../thumbnailaside/thumbnailaside_config.ui | 138 + .../thumbnailaside/thumbnailasideconfig.kcfgc | 5 + effects/touchpoints/touchpoints.cpp | 314 ++ effects/touchpoints/touchpoints.h | 88 + effects/trackmouse/CMakeLists.txt | 29 + effects/trackmouse/data/tm_inner.png | Bin 0 -> 1247 bytes effects/trackmouse/data/tm_outer.png | Bin 0 -> 1311 bytes effects/trackmouse/trackmouse.cpp | 301 + effects/trackmouse/trackmouse.h | 76 + effects/trackmouse/trackmouse.kcfg | 21 + effects/trackmouse/trackmouse_config.cpp | 113 + effects/trackmouse/trackmouse_config.desktop | 86 + effects/trackmouse/trackmouse_config.h | 50 + effects/trackmouse/trackmouse_config.ui | 104 + effects/trackmouse/trackmouseconfig.kcfgc | 5 + .../package/contents/code/main.js | 219 + .../package/contents/config/main.xml | 36 + .../package/contents/ui/config.ui | 473 ++ effects/translucency/package/metadata.desktop | 172 + .../package/contents/code/main.js | 192 + .../windowaperture/package/metadata.desktop | 87 + effects/windowgeometry/CMakeLists.txt | 24 + effects/windowgeometry/windowgeometry.cpp | 229 + effects/windowgeometry/windowgeometry.h | 62 + effects/windowgeometry/windowgeometry.kcfg | 15 + .../windowgeometry/windowgeometry_config.cpp | 84 + .../windowgeometry_config.desktop | 64 + .../windowgeometry/windowgeometry_config.h | 46 + .../windowgeometry/windowgeometry_config.ui | 47 + .../windowgeometry/windowgeometryconfig.kcfgc | 5 + effects/wobblywindows/CMakeLists.txt | 23 + effects/wobblywindows/wobblywindows.cpp | 1093 ++++ effects/wobblywindows/wobblywindows.h | 192 + effects/wobblywindows/wobblywindows.kcfg | 57 + .../wobblywindows/wobblywindows_config.cpp | 109 + .../wobblywindows_config.desktop | 82 + effects/wobblywindows/wobblywindows_config.h | 41 + effects/wobblywindows/wobblywindows_config.ui | 373 ++ .../wobblywindows/wobblywindowsconfig.kcfgc | 5 + effects/zoom/CMakeLists.txt | 24 + effects/zoom/accessibilityintegration.cpp | 99 + effects/zoom/accessibilityintegration.h | 46 + effects/zoom/zoom.cpp | 529 ++ effects/zoom/zoom.h | 126 + effects/zoom/zoom.kcfg | 33 + effects/zoom/zoom_config.cpp | 144 + effects/zoom/zoom_config.desktop | 93 + effects/zoom/zoom_config.h | 46 + effects/zoom/zoom_config.ui | 192 + effects/zoom/zoomconfig.kcfgc | 5 + egl_context_attribute_builder.cpp | 71 + egl_context_attribute_builder.h | 28 + events.cpp | 1403 +++++ fixqopengl.h | 26 + focuschain.cpp | 248 + focuschain.h | 242 + geometrytip.cpp | 64 + geometrytip.h | 33 + gestures.cpp | 207 + gestures.h | 210 + globalshortcuts.cpp | 301 + globalshortcuts.h | 181 + group.cpp | 125 + group.h | 86 + helpers/CMakeLists.txt | 1 + helpers/killer/CMakeLists.txt | 15 + helpers/killer/killer.cpp | 116 + idle_inhibition.cpp | 110 + idle_inhibition.h | 56 + input.cpp | 2766 +++++++++ input.h | 527 ++ input_event.cpp | 70 + input_event.h | 193 + input_event_spy.cpp | 141 + input_event_spy.h | 87 + inputpanelv1client.cpp | 129 + inputpanelv1client.h | 58 + inputpanelv1integration.cpp | 33 + inputpanelv1integration.h | 29 + internal_client.cpp | 563 ++ internal_client.h | 99 + kcmkwin/CMakeLists.txt | 15 + kcmkwin/common/CMakeLists.txt | 32 + kcmkwin/common/Messages.sh | 2 + kcmkwin/common/effectsmodel.cpp | 671 +++ kcmkwin/common/effectsmodel.h | 264 + kcmkwin/kwincompositing/CMakeLists.txt | 36 + kcmkwin/kwincompositing/Messages.sh | 3 + kcmkwin/kwincompositing/compositing.ui | 307 + .../kwincompositing/kwincompositing.desktop | 138 + .../kwincompositing_setting.kcfg | 74 + .../kwincompositing_setting.kcfgc | 5 + kcmkwin/kwincompositing/main.cpp | 294 + kcmkwin/kwindecoration/CMakeLists.txt | 33 + kcmkwin/kwindecoration/Messages.sh | 2 + .../declarative-plugin/CMakeLists.txt | 26 + .../declarative-plugin/buttonsmodel.cpp | 187 + .../declarative-plugin/buttonsmodel.h | 52 + .../declarative-plugin/plugin.cpp | 39 + .../declarative-plugin/plugin.h | 27 + .../declarative-plugin/previewbridge.cpp | 245 + .../declarative-plugin/previewbridge.h | 127 + .../declarative-plugin/previewbutton.cpp | 135 + .../declarative-plugin/previewbutton.h | 73 + .../declarative-plugin/previewclient.cpp | 456 ++ .../declarative-plugin/previewclient.h | 201 + .../declarative-plugin/previewitem.cpp | 454 ++ .../declarative-plugin/previewitem.h | 90 + .../declarative-plugin/previewsettings.cpp | 265 + .../declarative-plugin/previewsettings.h | 149 + .../kwindecoration/declarative-plugin/qmldir | 2 + kcmkwin/kwindecoration/decorationmodel.cpp | 192 + kcmkwin/kwindecoration/decorationmodel.h | 62 + kcmkwin/kwindecoration/kcm.cpp | 260 + kcmkwin/kwindecoration/kcm.h | 105 + kcmkwin/kwindecoration/kwindecoration.desktop | 156 + .../kwindecorationsettings.kcfg | 54 + .../kwindecorationsettings.kcfgc | 7 + .../package/contents/ui/ButtonGroup.qml | 62 + .../package/contents/ui/Buttons.qml | 236 + .../package/contents/ui/Themes.qml | 109 + .../package/contents/ui/main.qml | 155 + .../kwindecoration/package/metadata.desktop | 110 + kcmkwin/kwindecoration/utils.cpp | 109 + kcmkwin/kwindecoration/utils.h | 29 + .../kwindecoration/window-decorations.knsrc | 67 + kcmkwin/kwindesktop/CMakeLists.txt | 38 + kcmkwin/kwindesktop/Messages.sh | 2 + kcmkwin/kwindesktop/animationsmodel.cpp | 154 + kcmkwin/kwindesktop/animationsmodel.h | 61 + kcmkwin/kwindesktop/desktopsmodel.cpp | 665 +++ kcmkwin/kwindesktop/desktopsmodel.h | 123 + .../kcm_kwin_virtualdesktops.desktop | 157 + .../kwindesktop/package/contents/ui/main.qml | 264 + kcmkwin/kwindesktop/package/metadata.desktop | 106 + kcmkwin/kwindesktop/virtualdesktops.cpp | 164 + kcmkwin/kwindesktop/virtualdesktops.h | 59 + .../kwindesktop/virtualdesktopssettings.kcfg | 29 + .../kwindesktop/virtualdesktopssettings.kcfgc | 6 + kcmkwin/kwineffects/CMakeLists.txt | 35 + kcmkwin/kwineffects/Messages.sh | 2 + .../kwineffects/effectsfilterproxymodel.cpp | 93 + kcmkwin/kwineffects/effectsfilterproxymodel.h | 51 + kcmkwin/kwineffects/kcm.cpp | 113 + kcmkwin/kwineffects/kcm.h | 47 + kcmkwin/kwineffects/kcm_kwin_effects.desktop | 129 + kcmkwin/kwineffects/kwineffect.knsrc | 49 + .../package/contents/ui/Effect.qml | 117 + .../kwineffects/package/contents/ui/Video.qml | 43 + .../kwineffects/package/contents/ui/main.qml | 131 + kcmkwin/kwineffects/package/metadata.desktop | 107 + kcmkwin/kwinoptions/AUTHORS | 12 + kcmkwin/kwinoptions/CMakeLists.txt | 39 + kcmkwin/kwinoptions/ChangeLog | 51 + kcmkwin/kwinoptions/Messages.sh | 3 + kcmkwin/kwinoptions/actions.ui | 539 ++ kcmkwin/kwinoptions/advanced.ui | 156 + kcmkwin/kwinoptions/focus.ui | 298 + kcmkwin/kwinoptions/kwinactions.desktop | 122 + kcmkwin/kwinoptions/kwinadvanced.desktop | 106 + kcmkwin/kwinoptions/kwinfocus.desktop | 107 + kcmkwin/kwinoptions/kwinmoving.desktop | 119 + kcmkwin/kwinoptions/kwinoptions.desktop | 181 + .../kwinoptions_kdeglobals_settings.kcfg | 15 + .../kwinoptions_kdeglobals_settings.kcfgc | 6 + kcmkwin/kwinoptions/kwinoptions_settings.kcfg | 368 ++ .../kwinoptions/kwinoptions_settings.kcfgc | 6 + kcmkwin/kwinoptions/main.cpp | 235 + kcmkwin/kwinoptions/main.h | 79 + kcmkwin/kwinoptions/mouse.cpp | 106 + kcmkwin/kwinoptions/mouse.h | 83 + kcmkwin/kwinoptions/mouse.ui | 740 +++ kcmkwin/kwinoptions/moving.ui | 170 + kcmkwin/kwinoptions/windows.cpp | 323 ++ kcmkwin/kwinoptions/windows.h | 126 + kcmkwin/kwinrules/CMakeLists.txt | 57 + kcmkwin/kwinrules/Messages.sh | 2 + kcmkwin/kwinrules/kcm_kwinrules.desktop | 169 + kcmkwin/kwinrules/kcmrules.cpp | 240 + kcmkwin/kwinrules/kcmrules.h | 61 + kcmkwin/kwinrules/kwinsrc.cpp | 33 + kcmkwin/kwinrules/main.cpp | 241 + kcmkwin/kwinrules/optionsmodel.cpp | 196 + kcmkwin/kwinrules/optionsmodel.h | 101 + .../package/contents/ui/FileDialogLoader.qml | 45 + .../package/contents/ui/OptionsComboBox.qml | 94 + .../package/contents/ui/RuleItemDelegate.qml | 98 + .../package/contents/ui/RulesEditor.qml | 270 + .../package/contents/ui/RulesList.qml | 243 + .../package/contents/ui/ValueEditor.qml | 189 + kcmkwin/kwinrules/package/metadata.desktop | 125 + kcmkwin/kwinrules/rulebookmodel.cpp | 173 + kcmkwin/kwinrules/rulebookmodel.h | 50 + kcmkwin/kwinrules/ruleitem.cpp | 226 + kcmkwin/kwinrules/ruleitem.h | 115 + kcmkwin/kwinrules/rulesdialog.cpp | 74 + kcmkwin/kwinrules/rulesdialog.h | 41 + kcmkwin/kwinrules/rulesmodel.cpp | 877 +++ kcmkwin/kwinrules/rulesmodel.h | 121 + kcmkwin/kwinscreenedges/CMakeLists.txt | 39 + kcmkwin/kwinscreenedges/Messages.sh | 4 + kcmkwin/kwinscreenedges/kwinscreenedge.cpp | 222 + kcmkwin/kwinscreenedges/kwinscreenedge.h | 75 + .../kwinscreenedgeconfigform.cpp | 104 + .../kwinscreenedgeconfigform.h | 63 + .../kwinscreenedges/kwinscreenedges.desktop | 157 + .../kwinscreenedgescriptsettings.kcfg | 14 + .../kwinscreenedgescriptsettings.kcfgc | 7 + .../kwinscreenedgesettings.kcfg | 88 + .../kwinscreenedgesettings.kcfgc | 7 + .../kwinscreenedges/kwintouchscreen.desktop | 127 + .../kwintouchscreenedgeconfigform.cpp | 34 + .../kwintouchscreenedgeconfigform.h | 41 + .../kwintouchscreenscriptsettings.kcfg | 14 + .../kwintouchscreenscriptsettings.kcfgc | 7 + .../kwintouchscreensettings.kcfg | 56 + .../kwintouchscreensettings.kcfgc | 7 + kcmkwin/kwinscreenedges/main.cpp | 363 ++ kcmkwin/kwinscreenedges/main.h | 78 + kcmkwin/kwinscreenedges/main.ui | 331 ++ kcmkwin/kwinscreenedges/monitor.cpp | 313 ++ kcmkwin/kwinscreenedges/monitor.h | 104 + .../kwinscreenedges/screenpreviewwidget.cpp | 149 + kcmkwin/kwinscreenedges/screenpreviewwidget.h | 44 + kcmkwin/kwinscreenedges/touch.cpp | 340 ++ kcmkwin/kwinscreenedges/touch.h | 78 + kcmkwin/kwinscreenedges/touch.ui | 72 + kcmkwin/kwinscripts/CMakeLists.txt | 28 + kcmkwin/kwinscripts/Messages.sh | 4 + kcmkwin/kwinscripts/kwinscripts.desktop | 166 + kcmkwin/kwinscripts/kwinscripts.knsrc | 48 + kcmkwin/kwinscripts/main.cpp | 14 + kcmkwin/kwinscripts/module.cpp | 178 + kcmkwin/kwinscripts/module.h | 61 + kcmkwin/kwinscripts/module.ui | 96 + kcmkwin/kwinscripts/version.h.cmake | 7 + kcmkwin/kwintabbox/CMakeLists.txt | 43 + kcmkwin/kwintabbox/Messages.sh | 4 + kcmkwin/kwintabbox/kwinpluginssettings.kcfg | 18 + kcmkwin/kwintabbox/kwinpluginssettings.kcfgc | 7 + .../kwintabbox/kwinswitcheffectsettings.kcfg | 17 + .../kwintabbox/kwinswitcheffectsettings.kcfgc | 6 + kcmkwin/kwintabbox/kwinswitcher.knsrc | 49 + kcmkwin/kwintabbox/kwintabbox.desktop | 163 + kcmkwin/kwintabbox/kwintabboxconfigform.cpp | 396 ++ kcmkwin/kwintabbox/kwintabboxconfigform.h | 122 + kcmkwin/kwintabbox/kwintabboxsettings.kcfg | 41 + kcmkwin/kwintabbox/kwintabboxsettings.kcfgc | 7 + kcmkwin/kwintabbox/layoutpreview.cpp | 254 + kcmkwin/kwintabbox/layoutpreview.h | 142 + kcmkwin/kwintabbox/main.cpp | 478 ++ kcmkwin/kwintabbox/main.h | 76 + kcmkwin/kwintabbox/main.ui | 699 +++ kcmkwin/kwintabbox/thumbnailitem.cpp | 208 + kcmkwin/kwintabbox/thumbnailitem.h | 97 + kcmkwin/kwintabbox/thumbnails/dolphin.png | Bin 0 -> 26991 bytes kcmkwin/kwintabbox/thumbnails/kmail.png | Bin 0 -> 38069 bytes kcmkwin/kwintabbox/thumbnails/konqueror.png | Bin 0 -> 57484 bytes .../kwintabbox/thumbnails/systemsettings.png | Bin 0 -> 23682 bytes kconf_update/CMakeLists.txt | 11 + kconf_update/kwin-5.16-auto-bordersize.sh | 24 + kconf_update/kwin-5.18-move-animspeed.py | 14 + kconf_update/kwin.upd | 39 + kconf_update/kwinrules-5.19-placement.pl | 22 + kconf_update/kwinrules.upd | 7 + keyboard_input.cpp | 246 + keyboard_input.h | 93 + keyboard_layout.cpp | 332 ++ keyboard_layout.h | 102 + keyboard_layout_switching.cpp | 371 ++ keyboard_layout_switching.h | 138 + keyboard_repeat.cpp | 62 + keyboard_repeat.h | 45 + killwindow.cpp | 50 + killwindow.h | 30 + kwin.kcfg | 317 ++ kwin.notifyrc | 339 ++ kwinbindings.cpp | 157 + layers.cpp | 847 +++ layershellv1client.cpp | 276 + layershellv1client.h | 68 + layershellv1integration.cpp | 216 + layershellv1integration.h | 37 + libinput/connection.cpp | 815 +++ libinput/connection.h | 175 + libinput/context.cpp | 174 + libinput/context.h | 69 + libinput/device.cpp | 541 ++ libinput/device.h | 586 ++ libinput/events.cpp | 386 ++ libinput/events.h | 367 ++ libinput/libinput_logging.cpp | 10 + libinput/libinput_logging.h | 15 + libkwineffects/CMakeLists.txt | 128 + libkwineffects/Mainpage.dox | 22 + libkwineffects/Messages.sh | 2 + libkwineffects/anidata.cpp | 125 + libkwineffects/anidata_p.h | 97 + libkwineffects/kwinanimationeffect.cpp | 1046 ++++ libkwineffects/kwinanimationeffect.h | 408 ++ libkwineffects/kwinconfig.h.cmake | 26 + libkwineffects/kwineffectquickview.cpp | 380 ++ libkwineffects/kwineffectquickview.h | 168 + libkwineffects/kwineffects.cpp | 1930 +++++++ libkwineffects/kwineffects.h | 4014 ++++++++++++++ libkwineffects/kwineglimagetexture.cpp | 36 + libkwineffects/kwineglimagetexture.h | 34 + libkwineffects/kwinglobals.h | 217 + libkwineffects/kwinglplatform.cpp | 1264 +++++ libkwineffects/kwinglplatform.h | 436 ++ libkwineffects/kwingltexture.cpp | 682 +++ libkwineffects/kwingltexture.h | 150 + libkwineffects/kwingltexture_p.h | 81 + libkwineffects/kwinglutils.cpp | 2308 ++++++++ libkwineffects/kwinglutils.h | 818 +++ libkwineffects/kwinglutils_funcs.cpp | 91 + libkwineffects/kwinglutils_funcs.h | 49 + libkwineffects/kwinxrenderutils.cpp | 282 + libkwineffects/kwinxrenderutils.h | 183 + libkwineffects/logging.cpp | 12 + libkwineffects/logging_p.h | 19 + linux_dmabuf.cpp | 77 + linux_dmabuf.h | 63 + logind.cpp | 455 ++ logind.h | 102 + logo.png | Bin 0 -> 4200 bytes main.cpp | 457 ++ main.h | 299 + main_wayland.cpp | 703 +++ main_wayland.h | 69 + main_x11.cpp | 487 ++ main_x11.h | 47 + modifier_only_shortcuts.cpp | 94 + modifier_only_shortcuts.h | 45 + moving_client_x11_filter.cpp | 51 + moving_client_x11_filter.h | 27 + netinfo.cpp | 295 + netinfo.h | 83 + onscreennotification.cpp | 233 + onscreennotification.h | 80 + options.cpp | 1103 ++++ options.h | 927 ++++ org.kde.KWin.Session.xml | 24 + org.kde.KWin.VirtualDesktopManager.xml | 50 + org.kde.KWin.xml | 49 + org.kde.kappmenu.xml | 28 + org.kde.kwin.ColorCorrect.xml | 116 + org.kde.kwin.Compositing.xml | 19 + org.kde.kwin.Effects.xml | 43 + osd.cpp | 68 + osd.h | 31 + outline.cpp | 178 + outline.h | 183 + outputscreens.cpp | 119 + outputscreens.h | 49 + overlaywindow.cpp | 21 + overlaywindow.h | 41 + placement.cpp | 985 ++++ placement.h | 99 + plasma-kwin_wayland.service.in | 7 + plasma-kwin_x11.service.in | 8 + platform.cpp | 581 ++ platform.h | 573 ++ platformsupport/CMakeLists.txt | 1 + platformsupport/scenes/CMakeLists.txt | 2 + platformsupport/scenes/opengl/CMakeLists.txt | 24 + .../scenes/opengl/abstract_egl_backend.cpp | 686 +++ .../scenes/opengl/abstract_egl_backend.h | 119 + platformsupport/scenes/opengl/backend.cpp | 119 + platformsupport/scenes/opengl/backend.h | 348 ++ platformsupport/scenes/opengl/drm_fourcc.h | 421 ++ platformsupport/scenes/opengl/egl_dmabuf.cpp | 443 ++ platformsupport/scenes/opengl/egl_dmabuf.h | 93 + platformsupport/scenes/opengl/kwineglext.h | 65 + .../scenes/opengl/swap_profiler.cpp | 46 + platformsupport/scenes/opengl/swap_profiler.h | 42 + platformsupport/scenes/opengl/texture.cpp | 69 + platformsupport/scenes/opengl/texture.h | 59 + .../scenes/qpainter/CMakeLists.txt | 16 + platformsupport/scenes/qpainter/backend.cpp | 57 + platformsupport/scenes/qpainter/backend.h | 97 + plugins/CMakeLists.txt | 11 + plugins/idletime/CMakeLists.txt | 18 + plugins/idletime/kwin.json | 3 + plugins/idletime/poller.cpp | 123 + plugins/idletime/poller.h | 56 + plugins/kdecorations/CMakeLists.txt | 2 + plugins/kdecorations/Messages.sh | 4 + plugins/kdecorations/aurorae/AUTHORS | 1 + plugins/kdecorations/aurorae/CMakeLists.txt | 4 + plugins/kdecorations/aurorae/README | 6 + plugins/kdecorations/aurorae/TODO | 3 + .../kdecorations/aurorae/src/CMakeLists.txt | 71 + plugins/kdecorations/aurorae/src/aurorae.cpp | 749 +++ plugins/kdecorations/aurorae/src/aurorae.h | 119 + plugins/kdecorations/aurorae/src/aurorae.json | 22 + .../kdecorations/aurorae/src/aurorae.knsrc | 46 + .../kdecorations/aurorae/src/colorhelper.cpp | 52 + .../kdecorations/aurorae/src/colorhelper.h | 230 + .../aurorae/src/decorationoptions.cpp | 258 + .../aurorae/src/decorationoptions.h | 307 + .../aurorae/src/decorationplugin.cpp | 18 + .../aurorae/src/decorationplugin.h | 18 + .../aurorae/src/kwindecoration.desktop | 62 + .../aurorae/src/lib/auroraetheme.cpp | 499 ++ .../aurorae/src/lib/auroraetheme.h | 217 + .../aurorae/src/lib/themeconfig.cpp | 190 + .../aurorae/src/lib/themeconfig.h | 402 ++ .../aurorae/src/qml/AppMenuButton.qml | 18 + .../aurorae/src/qml/AuroraeButton.qml | 204 + .../aurorae/src/qml/AuroraeButtonGroup.qml | 49 + .../aurorae/src/qml/AuroraeMaximizeButton.qml | 58 + .../aurorae/src/qml/ButtonGroup.qml | 90 + .../aurorae/src/qml/Decoration.qml | 25 + .../aurorae/src/qml/DecorationButton.qml | 114 + .../aurorae/src/qml/MenuButton.qml | 70 + .../kdecorations/aurorae/src/qml/aurorae.qml | 220 + plugins/kdecorations/aurorae/src/qml/qmldir | 8 + .../kdecorations/aurorae/theme-description | 163 + .../aurorae/themes/CMakeLists.txt | 1 + .../aurorae/themes/plastik/CMakeLists.txt | 10 + .../themes/plastik/code/CMakeLists.txt | 11 + .../themes/plastik/code/plastikbutton.cpp | 459 ++ .../themes/plastik/code/plastikbutton.h | 73 + .../themes/plastik/code/plastikplugin.cpp | 21 + .../themes/plastik/code/plastikplugin.h | 20 + .../aurorae/themes/plastik/code/qmldir | 2 + .../plastik/package/contents/config/main.xml | 25 + .../package/contents/ui/PlastikButton.qml | 149 + .../plastik/package/contents/ui/config.ui | 88 + .../plastik/package/contents/ui/main.qml | 414 ++ .../themes/plastik/package/metadata.desktop | 151 + plugins/kglobalaccel/CMakeLists.txt | 17 + plugins/kglobalaccel/kglobalaccel_plugin.cpp | 47 + plugins/kglobalaccel/kglobalaccel_plugin.h | 37 + plugins/kglobalaccel/kwin.json | 3 + plugins/kpackage/CMakeLists.txt | 5 + plugins/kpackage/aurorae/CMakeLists.txt | 17 + plugins/kpackage/aurorae/aurorae.cpp | 72 + plugins/kpackage/aurorae/aurorae.h | 20 + .../kwin-packagestructure-aurorae.desktop | 49 + plugins/kpackage/decoration/CMakeLists.txt | 17 + plugins/kpackage/decoration/decoration.cpp | 49 + plugins/kpackage/decoration/decoration.h | 20 + .../kwin-packagestructure-decoration.desktop | 50 + plugins/kpackage/effect/CMakeLists.txt | 16 + plugins/kpackage/effect/effect.cpp | 50 + plugins/kpackage/effect/effect.h | 20 + .../kwin-packagestructure-effect.desktop | 44 + plugins/kpackage/scripts/CMakeLists.txt | 16 + .../kwin-packagestructure-scripts.desktop | 50 + plugins/kpackage/scripts/scripts.cpp | 49 + plugins/kpackage/scripts/scripts.h | 20 + .../kpackage/windowswitcher/CMakeLists.txt | 16 + ...in-packagestructure-windowswitcher.desktop | 50 + .../windowswitcher/windowswitcher.cpp | 49 + .../kpackage/windowswitcher/windowswitcher.h | 20 + plugins/platforms/CMakeLists.txt | 12 + plugins/platforms/drm/CMakeLists.txt | 46 + plugins/platforms/drm/drm.json | 84 + plugins/platforms/drm/drm_backend.cpp | 829 +++ plugins/platforms/drm/drm_backend.h | 196 + plugins/platforms/drm/drm_buffer.cpp | 97 + plugins/platforms/drm/drm_buffer.h | 77 + plugins/platforms/drm/drm_buffer_gbm.cpp | 58 + plugins/platforms/drm/drm_buffer_gbm.h | 56 + .../platforms/drm/drm_inputeventfilter.cpp | 104 + plugins/platforms/drm/drm_inputeventfilter.h | 45 + plugins/platforms/drm/drm_object.cpp | 180 + plugins/platforms/drm/drm_object.h | 141 + .../platforms/drm/drm_object_connector.cpp | 70 + plugins/platforms/drm/drm_object_connector.h | 46 + plugins/platforms/drm/drm_object_crtc.cpp | 122 + plugins/platforms/drm/drm_object_crtc.h | 74 + plugins/platforms/drm/drm_object_plane.cpp | 179 + plugins/platforms/drm/drm_object_plane.h | 114 + plugins/platforms/drm/drm_output.cpp | 1045 ++++ plugins/platforms/drm/drm_output.h | 179 + plugins/platforms/drm/drm_pointer.h | 147 + plugins/platforms/drm/edid.cpp | 206 + plugins/platforms/drm/edid.h | 68 + plugins/platforms/drm/egl_gbm_backend.cpp | 656 +++ plugins/platforms/drm/egl_gbm_backend.h | 115 + plugins/platforms/drm/egl_stream_backend.cpp | 677 +++ plugins/platforms/drm/egl_stream_backend.h | 104 + plugins/platforms/drm/gbm_dmabuf.cpp | 71 + plugins/platforms/drm/gbm_dmabuf.h | 38 + plugins/platforms/drm/gbm_surface.cpp | 44 + plugins/platforms/drm/gbm_surface.h | 44 + plugins/platforms/drm/logging.cpp | 10 + plugins/platforms/drm/logging.h | 15 + .../drm/scene_qpainter_drm_backend.cpp | 135 + .../drm/scene_qpainter_drm_backend.h | 49 + plugins/platforms/drm/screens_drm.cpp | 35 + plugins/platforms/drm/screens_drm.h | 31 + plugins/platforms/fbdev/CMakeLists.txt | 16 + plugins/platforms/fbdev/fb_backend.cpp | 274 + plugins/platforms/fbdev/fb_backend.h | 108 + plugins/platforms/fbdev/fbdev.json | 84 + plugins/platforms/fbdev/logging.cpp | 10 + plugins/platforms/fbdev/logging.h | 15 + .../fbdev/scene_qpainter_fb_backend.cpp | 95 + .../fbdev/scene_qpainter_fb_backend.h | 51 + plugins/platforms/hwcomposer/CMakeLists.txt | 26 + .../hwcomposer/egl_hwcomposer_backend.cpp | 176 + .../hwcomposer/egl_hwcomposer_backend.h | 55 + plugins/platforms/hwcomposer/hwcomposer.json | 83 + .../hwcomposer/hwcomposer_backend.cpp | 538 ++ .../platforms/hwcomposer/hwcomposer_backend.h | 155 + plugins/platforms/hwcomposer/logging.cpp | 10 + plugins/platforms/hwcomposer/logging.h | 15 + .../hwcomposer/screens_hwcomposer.cpp | 23 + .../platforms/hwcomposer/screens_hwcomposer.h | 30 + plugins/platforms/virtual/CMakeLists.txt | 22 + plugins/platforms/virtual/egl_gbm_backend.cpp | 238 + plugins/platforms/virtual/egl_gbm_backend.h | 63 + .../scene_qpainter_virtual_backend.cpp | 78 + .../virtual/scene_qpainter_virtual_backend.h | 47 + plugins/platforms/virtual/screens_virtual.cpp | 42 + plugins/platforms/virtual/screens_virtual.h | 34 + plugins/platforms/virtual/virtual.json | 84 + plugins/platforms/virtual/virtual_backend.cpp | 133 + plugins/platforms/virtual/virtual_backend.h | 68 + plugins/platforms/virtual/virtual_output.cpp | 44 + plugins/platforms/virtual/virtual_output.h | 51 + plugins/platforms/wayland/CMakeLists.txt | 27 + .../platforms/wayland/egl_wayland_backend.cpp | 457 ++ .../platforms/wayland/egl_wayland_backend.h | 124 + plugins/platforms/wayland/logging.cpp | 10 + plugins/platforms/wayland/logging.h | 15 + .../scene_qpainter_wayland_backend.cpp | 199 + .../wayland/scene_qpainter_wayland_backend.h | 91 + plugins/platforms/wayland/wayland.json | 83 + plugins/platforms/wayland/wayland_backend.cpp | 843 +++ plugins/platforms/wayland/wayland_backend.h | 273 + plugins/platforms/wayland/wayland_output.cpp | 172 + plugins/platforms/wayland/wayland_output.h | 111 + plugins/platforms/x11/CMakeLists.txt | 5 + plugins/platforms/x11/common/CMakeLists.txt | 9 + .../platforms/x11/common/eglonxbackend.cpp | 533 ++ plugins/platforms/x11/common/eglonxbackend.h | 97 + .../platforms/x11/common/ge_event_mem_mover.h | 44 + .../platforms/x11/standalone/CMakeLists.txt | 44 + plugins/platforms/x11/standalone/edge.cpp | 133 + plugins/platforms/x11/standalone/edge.h | 68 + .../effects_mouse_interception_x11_filter.cpp | 96 + .../effects_mouse_interception_x11_filter.h | 32 + .../platforms/x11/standalone/effects_x11.cpp | 113 + .../platforms/x11/standalone/effects_x11.h | 47 + .../glx_context_attribute_builder.cpp | 42 + .../glx_context_attribute_builder.h | 21 + .../platforms/x11/standalone/glxbackend.cpp | 961 ++++ plugins/platforms/x11/standalone/glxbackend.h | 139 + plugins/platforms/x11/standalone/logging.cpp | 10 + plugins/platforms/x11/standalone/logging.h | 15 + .../x11/standalone/non_composited_outline.cpp | 139 + .../x11/standalone/non_composited_outline.h | 48 + .../x11/standalone/overlaywindow_x11.cpp | 190 + .../x11/standalone/overlaywindow_x11.h | 46 + .../x11/standalone/screenedges_filter.cpp | 54 + .../x11/standalone/screenedges_filter.h | 26 + .../x11/standalone/screens_xrandr.cpp | 95 + .../platforms/x11/standalone/screens_xrandr.h | 40 + .../x11/standalone/windowselector.cpp | 262 + .../platforms/x11/standalone/windowselector.h | 59 + plugins/platforms/x11/standalone/x11.json | 81 + .../standalone/x11_decoration_renderer.cpp | 98 + .../x11/standalone/x11_decoration_renderer.h | 44 + .../platforms/x11/standalone/x11_output.cpp | 95 + plugins/platforms/x11/standalone/x11_output.h | 67 + .../platforms/x11/standalone/x11_platform.cpp | 540 ++ .../platforms/x11/standalone/x11_platform.h | 98 + .../platforms/x11/standalone/x11cursor.cpp | 186 + plugins/platforms/x11/standalone/x11cursor.h | 74 + .../standalone/xfixes_cursor_event_filter.cpp | 29 + .../standalone/xfixes_cursor_event_filter.h | 30 + .../x11/standalone/xinputintegration.cpp | 276 + .../x11/standalone/xinputintegration.h | 58 + plugins/platforms/x11/windowed/CMakeLists.txt | 22 + .../x11/windowed/egl_x11_backend.cpp | 110 + .../platforms/x11/windowed/egl_x11_backend.h | 46 + plugins/platforms/x11/windowed/logging.cpp | 10 + plugins/platforms/x11/windowed/logging.h | 15 + .../windowed/scene_qpainter_x11_backend.cpp | 93 + .../x11/windowed/scene_qpainter_x11_backend.h | 54 + plugins/platforms/x11/windowed/x11.json | 83 + .../x11/windowed/x11windowed_backend.cpp | 534 ++ .../x11/windowed/x11windowed_backend.h | 114 + .../x11/windowed/x11windowed_output.cpp | 157 + .../x11/windowed/x11windowed_output.h | 76 + plugins/qpa/CMakeLists.txt | 40 + plugins/qpa/backingstore.cpp | 92 + plugins/qpa/backingstore.h | 42 + plugins/qpa/eglhelpers.cpp | 105 + plugins/qpa/eglhelpers.h | 29 + plugins/qpa/eglplatformcontext.cpp | 268 + plugins/qpa/eglplatformcontext.h | 51 + plugins/qpa/integration.cpp | 159 + plugins/qpa/integration.h | 58 + plugins/qpa/kwin.json | 3 + plugins/qpa/main.cpp | 37 + plugins/qpa/offscreensurface.cpp | 71 + plugins/qpa/offscreensurface.h | 42 + plugins/qpa/platformcursor.cpp | 42 + plugins/qpa/platformcursor.h | 32 + plugins/qpa/screen.cpp | 68 + plugins/qpa/screen.h | 43 + plugins/qpa/window.cpp | 195 + plugins/qpa/window.h | 68 + plugins/scenes/CMakeLists.txt | 5 + plugins/scenes/opengl/CMakeLists.txt | 32 + plugins/scenes/opengl/lanczosfilter.cpp | 414 ++ plugins/scenes/opengl/lanczosfilter.h | 65 + plugins/scenes/opengl/opengl.json | 80 + plugins/scenes/opengl/resources.qrc | 6 + plugins/scenes/opengl/scene_opengl.cpp | 2766 +++++++++ plugins/scenes/opengl/scene_opengl.h | 339 ++ .../opengl/shaders/1.10/lanczos-fragment.glsl | 16 + .../opengl/shaders/1.40/lanczos-fragment.glsl | 19 + plugins/scenes/qpainter/CMakeLists.txt | 15 + plugins/scenes/qpainter/qpainter.json | 82 + plugins/scenes/qpainter/scene_qpainter.cpp | 877 +++ plugins/scenes/qpainter/scene_qpainter.h | 195 + plugins/scenes/xrender/CMakeLists.txt | 27 + plugins/scenes/xrender/scene_xrender.cpp | 1345 +++++ plugins/scenes/xrender/scene_xrender.h | 351 ++ plugins/scenes/xrender/xrender.json | 82 + plugins/windowsystem/CMakeLists.txt | 17 + plugins/windowsystem/kwindowsystem.json | 3 + plugins/windowsystem/plugin.cpp | 38 + plugins/windowsystem/plugin.h | 24 + plugins/windowsystem/windoweffects.cpp | 140 + plugins/windowsystem/windoweffects.h | 35 + plugins/windowsystem/windowshadow.cpp | 81 + plugins/windowsystem/windowshadow.h | 28 + plugins/windowsystem/windowsystem.cpp | 314 ++ plugins/windowsystem/windowsystem.h | 71 + po/af/kcm_kwindecoration.po | 227 + po/af/kcm_kwinrules.po | 875 +++ po/af/kcmkwm.po | 1427 +++++ po/af/kwin.po | 2624 +++++++++ po/af/kwin_clients.po | 132 + po/ar/kcm-kwin-scripts.po | 91 + po/ar/kcm_kwin_virtualdesktops.po | 135 + po/ar/kcm_kwindecoration.po | 250 + po/ar/kcm_kwinrules.po | 869 +++ po/ar/kcm_kwintabbox.po | 223 + po/ar/kcmkwincompositing.po | 230 + po/ar/kcmkwinscreenedges.po | 238 + po/ar/kcmkwm.po | 1331 +++++ po/ar/kwin.po | 2639 +++++++++ po/ar/kwin_clients.po | 130 + po/ar/kwin_effects.po | 2185 ++++++++ po/ar/kwin_scripting.po | 114 + po/as/kwin.po | 2621 +++++++++ po/ast/kcm-kwin-scripts.po | 92 + po/ast/kcm_kwin_effects.po | 98 + po/ast/kcm_kwin_virtualdesktops.po | 131 + po/ast/kcmkwincompositing.po | 221 + po/ast/kwin_effects.po | 2133 +++++++ po/az/kcm-kwin-scripts.po | 92 + po/az/kcm_kwin_effects.po | 98 + po/az/kcm_kwin_virtualdesktops.po | 131 + po/az/kcm_kwindecoration.po | 214 + po/az/kcm_kwinrules.po | 873 +++ po/az/kcm_kwintabbox.po | 228 + po/az/kcmkwincommon.po | 81 + po/az/kcmkwincompositing.po | 235 + po/az/kcmkwinscreenedges.po | 237 + po/az/kcmkwm.po | 1389 +++++ po/az/kwin.po | 2585 +++++++++ po/az/kwin_clients.po | 127 + po/az/kwin_effects.po | 2155 ++++++++ po/az/kwin_scripting.po | 117 + po/az/kwin_scripts.po | 61 + po/be/kcm_kwin_virtualdesktops.po | 133 + po/be/kcm_kwindecoration.po | 231 + po/be/kcm_kwinrules.po | 851 +++ po/be/kcmkwincompositing.po | 220 + po/be/kcmkwm.po | 1275 +++++ po/be/kwin.po | 2622 +++++++++ po/be/kwin_clients.po | 131 + po/be/kwin_effects.po | 2181 ++++++++ po/be@latin/kwin.po | 2630 +++++++++ po/bg/kcm_kwin_virtualdesktops.po | 131 + po/bg/kcm_kwindecoration.po | 237 + po/bg/kcm_kwinrules.po | 874 +++ po/bg/kcm_kwintabbox.po | 235 + po/bg/kcmkwincompositing.po | 231 + po/bg/kcmkwinscreenedges.po | 234 + po/bg/kcmkwm.po | 1425 +++++ po/bg/kwin.po | 2628 +++++++++ po/bg/kwin_clients.po | 141 + po/bg/kwin_effects.po | 2193 ++++++++ po/bn/kcmkwm.po | 1378 +++++ po/bn/kwin.po | 2613 +++++++++ po/bn/kwin_effects.po | 2201 ++++++++ po/bn_IN/kcm_kwin_virtualdesktops.po | 131 + po/bn_IN/kcm_kwindecoration.po | 228 + po/bn_IN/kcm_kwinrules.po | 857 +++ po/bn_IN/kcmkwm.po | 1288 +++++ po/bn_IN/kwin.po | 2619 +++++++++ po/bn_IN/kwin_clients.po | 129 + po/bn_IN/kwin_effects.po | 2189 ++++++++ po/br/kcm_kwindecoration.po | 225 + po/br/kcm_kwinrules.po | 839 +++ po/br/kcmkwm.po | 1282 +++++ po/br/kwin.po | 2604 +++++++++ po/br/kwin_clients.po | 126 + po/bs/kcm-kwin-scripts.po | 94 + po/bs/kcm_kwin_virtualdesktops.po | 189 + po/bs/kcm_kwindecoration.po | 243 + po/bs/kcm_kwinrules.po | 1140 ++++ po/bs/kcm_kwintabbox.po | 229 + po/bs/kcmkwincompositing.po | 239 + po/bs/kcmkwinscreenedges.po | 270 + po/bs/kcmkwm.po | 1497 +++++ po/bs/kwin.po | 2633 +++++++++ po/bs/kwin_clients.po | 605 ++ po/bs/kwin_effects.po | 2318 ++++++++ po/bs/kwin_scripting.po | 116 + po/ca/docs/kcontrol/desktop/index.docbook | 156 + po/ca/docs/kcontrol/kwindecoration/button.png | Bin 0 -> 40217 bytes .../kcontrol/kwindecoration/decoration.png | Bin 0 -> 65246 bytes .../kcontrol/kwindecoration/index.docbook | 169 + po/ca/docs/kcontrol/kwindecoration/main.png | Bin 0 -> 61358 bytes po/ca/docs/kcontrol/kwineffects/index.docbook | 117 + .../kcontrol/kwinscreenedges/index.docbook | 80 + po/ca/docs/kcontrol/kwintabbox/index.docbook | 157 + .../kcontrol/windowbehaviour/index.docbook | 795 +++ .../kcontrol/windowspecific/index.docbook | 2198 ++++++++ po/ca/kcm-kwin-scripts.po | 96 + po/ca/kcm_kwin_effects.po | 102 + po/ca/kcm_kwin_virtualdesktops.po | 135 + po/ca/kcm_kwindecoration.po | 220 + po/ca/kcm_kwinrules.po | 875 +++ po/ca/kcm_kwintabbox.po | 233 + po/ca/kcmkwincommon.po | 85 + po/ca/kcmkwincompositing.po | 238 + po/ca/kcmkwinscreenedges.po | 243 + po/ca/kcmkwm.po | 1408 +++++ po/ca/kwin.po | 2600 +++++++++ po/ca/kwin_clients.po | 134 + po/ca/kwin_effects.po | 2168 ++++++++ po/ca/kwin_scripting.po | 123 + po/ca/kwin_scripts.po | 65 + po/ca@valencia/kcm-kwin-scripts.po | 96 + po/ca@valencia/kcm_kwin_effects.po | 108 + po/ca@valencia/kcm_kwin_virtualdesktops.po | 135 + po/ca@valencia/kcm_kwindecoration.po | 221 + po/ca@valencia/kcm_kwinrules.po | 915 +++ po/ca@valencia/kcm_kwintabbox.po | 234 + po/ca@valencia/kcmkwincommon.po | 85 + po/ca@valencia/kcmkwincompositing.po | 239 + po/ca@valencia/kcmkwinscreenedges.po | 254 + po/ca@valencia/kcmkwm.po | 1513 +++++ po/ca@valencia/kwin.po | 2654 +++++++++ po/ca@valencia/kwin_clients.po | 134 + po/ca@valencia/kwin_effects.po | 2178 ++++++++ po/ca@valencia/kwin_scripting.po | 123 + po/ca@valencia/kwin_scripts.po | 65 + po/cs/kcm-kwin-scripts.po | 93 + po/cs/kcm_kwin_effects.po | 96 + po/cs/kcm_kwin_virtualdesktops.po | 130 + po/cs/kcm_kwindecoration.po | 216 + po/cs/kcm_kwinrules.po | 856 +++ po/cs/kcm_kwintabbox.po | 227 + po/cs/kcmkwincommon.po | 86 + po/cs/kcmkwincompositing.po | 230 + po/cs/kcmkwinscreenedges.po | 236 + po/cs/kcmkwm.po | 1297 +++++ po/cs/kwin.po | 2561 +++++++++ po/cs/kwin_clients.po | 128 + po/cs/kwin_effects.po | 2147 +++++++ po/cs/kwin_scripting.po | 113 + po/cs/kwin_scripts.po | 61 + po/csb/kcm_kwin_virtualdesktops.po | 133 + po/csb/kwin.po | 2626 +++++++++ po/csb/kwin_clients.po | 137 + po/csb/kwin_effects.po | 2217 ++++++++ po/cy/kcm_kwindecoration.po | 225 + po/cy/kcm_kwinrules.po | 846 +++ po/cy/kcmkwm.po | 1384 +++++ po/cy/kwin.po | 2621 +++++++++ po/cy/kwin_clients.po | 125 + po/da/kcm-kwin-scripts.po | 92 + po/da/kcm_kwin_effects.po | 98 + po/da/kcm_kwin_virtualdesktops.po | 133 + po/da/kcm_kwindecoration.po | 217 + po/da/kcm_kwinrules.po | 904 +++ po/da/kcm_kwintabbox.po | 228 + po/da/kcmkwincommon.po | 81 + po/da/kcmkwincompositing.po | 234 + po/da/kcmkwinscreenedges.po | 237 + po/da/kcmkwm.po | 1443 +++++ po/da/kwin.po | 2615 +++++++++ po/da/kwin_clients.po | 127 + po/da/kwin_effects.po | 2174 ++++++++ po/da/kwin_scripting.po | 112 + po/da/kwin_scripts.po | 61 + po/de/docs/kcontrol/desktop/index.docbook | 184 + .../kcontrol/kwindecoration/index.docbook | 181 + po/de/docs/kcontrol/kwineffects/index.docbook | 131 + .../kcontrol/kwinscreenedges/index.docbook | 94 + po/de/docs/kcontrol/kwintabbox/index.docbook | 169 + .../kcontrol/windowbehaviour/index.docbook | 799 +++ .../kcontrol/windowspecific/index.docbook | 2214 ++++++++ po/de/kcm-kwin-scripts.po | 90 + po/de/kcm_kwin_effects.po | 98 + po/de/kcm_kwin_virtualdesktops.po | 133 + po/de/kcm_kwindecoration.po | 216 + po/de/kcm_kwinrules.po | 859 +++ po/de/kcm_kwintabbox.po | 230 + po/de/kcmkwincommon.po | 81 + po/de/kcmkwincompositing.po | 237 + po/de/kcmkwinscreenedges.po | 237 + po/de/kcmkwm.po | 1474 +++++ po/de/kwin.po | 2904 ++++++++++ po/de/kwin_clients.po | 131 + po/de/kwin_effects.po | 2178 ++++++++ po/de/kwin_scripting.po | 115 + po/de/kwin_scripts.po | 58 + po/el/kcm-kwin-scripts.po | 95 + po/el/kcm_kwin_virtualdesktops.po | 134 + po/el/kcm_kwindecoration.po | 255 + po/el/kcm_kwinrules.po | 922 +++ po/el/kcm_kwintabbox.po | 231 + po/el/kcmkwincompositing.po | 245 + po/el/kcmkwinscreenedges.po | 250 + po/el/kcmkwm.po | 1532 +++++ po/el/kwin.po | 2692 +++++++++ po/el/kwin_clients.po | 135 + po/el/kwin_effects.po | 2212 ++++++++ po/el/kwin_scripting.po | 119 + po/el/kwin_scripts.po | 61 + po/en_GB/kcm-kwin-scripts.po | 92 + po/en_GB/kcm_kwin_effects.po | 98 + po/en_GB/kcm_kwin_virtualdesktops.po | 134 + po/en_GB/kcm_kwindecoration.po | 216 + po/en_GB/kcm_kwinrules.po | 867 +++ po/en_GB/kcm_kwintabbox.po | 226 + po/en_GB/kcmkwincommon.po | 81 + po/en_GB/kcmkwincompositing.po | 234 + po/en_GB/kcmkwinscreenedges.po | 239 + po/en_GB/kcmkwm.po | 1382 +++++ po/en_GB/kwin.po | 2581 +++++++++ po/en_GB/kwin_clients.po | 127 + po/en_GB/kwin_effects.po | 2154 ++++++++ po/en_GB/kwin_scripting.po | 112 + po/en_GB/kwin_scripts.po | 61 + po/eo/kcm_kwin_virtualdesktops.po | 130 + po/eo/kcm_kwindecoration.po | 241 + po/eo/kcm_kwinrules.po | 873 +++ po/eo/kcm_kwintabbox.po | 235 + po/eo/kcmkwincompositing.po | 223 + po/eo/kcmkwinscreenedges.po | 248 + po/eo/kcmkwm.po | 1402 +++++ po/eo/kwin.po | 2645 +++++++++ po/eo/kwin_clients.po | 131 + po/eo/kwin_effects.po | 2230 ++++++++ po/es/docs/kcontrol/desktop/index.docbook | 198 + po/es/kcm-kwin-scripts.po | 92 + po/es/kcm_kwin_effects.po | 100 + po/es/kcm_kwin_virtualdesktops.po | 135 + po/es/kcm_kwindecoration.po | 224 + po/es/kcm_kwinrules.po | 879 +++ po/es/kcm_kwintabbox.po | 234 + po/es/kcmkwincommon.po | 83 + po/es/kcmkwincompositing.po | 242 + po/es/kcmkwinscreenedges.po | 243 + po/es/kcmkwm.po | 1417 +++++ po/es/kwin.po | 2613 +++++++++ po/es/kwin_clients.po | 134 + po/es/kwin_effects.po | 2170 ++++++++ po/es/kwin_scripting.po | 118 + po/es/kwin_scripts.po | 63 + po/et/kcm-kwin-scripts.po | 93 + po/et/kcm_kwin_effects.po | 98 + po/et/kcm_kwin_virtualdesktops.po | 132 + po/et/kcm_kwindecoration.po | 215 + po/et/kcm_kwinrules.po | 863 +++ po/et/kcm_kwintabbox.po | 224 + po/et/kcmkwincommon.po | 81 + po/et/kcmkwincompositing.po | 234 + po/et/kcmkwinscreenedges.po | 237 + po/et/kcmkwm.po | 1389 +++++ po/et/kwin.po | 2579 +++++++++ po/et/kwin_clients.po | 126 + po/et/kwin_effects.po | 2151 +++++++ po/et/kwin_scripting.po | 111 + po/et/kwin_scripts.po | 61 + po/eu/kcm-kwin-scripts.po | 96 + po/eu/kcm_kwin_effects.po | 101 + po/eu/kcm_kwin_virtualdesktops.po | 139 + po/eu/kcm_kwindecoration.po | 226 + po/eu/kcm_kwinrules.po | 884 +++ po/eu/kcm_kwintabbox.po | 234 + po/eu/kcmkwincommon.po | 84 + po/eu/kcmkwincompositing.po | 240 + po/eu/kcmkwinscreenedges.po | 287 + po/eu/kcmkwm.po | 1399 +++++ po/eu/kwin.po | 2603 +++++++++ po/eu/kwin_clients.po | 133 + po/eu/kwin_effects.po | 2171 ++++++++ po/eu/kwin_scripting.po | 117 + po/eu/kwin_scripts.po | 64 + po/fa/kcm-kwin-scripts.po | 93 + po/fa/kcm_kwin_virtualdesktops.po | 129 + po/fa/kcm_kwindecoration.po | 230 + po/fa/kcm_kwinrules.po | 870 +++ po/fa/kcm_kwintabbox.po | 222 + po/fa/kcmkwincompositing.po | 212 + po/fa/kcmkwinscreenedges.po | 229 + po/fa/kcmkwm.po | 1421 +++++ po/fa/kwin.po | 2628 +++++++++ po/fa/kwin_clients.po | 134 + po/fa/kwin_effects.po | 2126 +++++++ po/fi/kcm-kwin-scripts.po | 94 + po/fi/kcm_kwin_effects.po | 96 + po/fi/kcm_kwin_virtualdesktops.po | 240 + po/fi/kcm_kwindecoration.po | 379 ++ po/fi/kcm_kwinrules.po | 977 ++++ po/fi/kcm_kwintabbox.po | 233 + po/fi/kcmkwincommon.po | 95 + po/fi/kcmkwincompositing.po | 923 ++++ po/fi/kcmkwinscreenedges.po | 243 + po/fi/kcmkwm.po | 1405 +++++ po/fi/kwin.po | 2818 ++++++++++ po/fi/kwin_clients.po | 137 + po/fi/kwin_effects.po | 2202 ++++++++ po/fi/kwin_scripting.po | 118 + po/fi/kwin_scripts.po | 61 + po/fr/docs/kcontrol/desktop/index.docbook | 156 + .../kcontrol/kwinscreenedges/index.docbook | 80 + po/fr/docs/kcontrol/kwintabbox/index.docbook | 157 + .../kcontrol/windowspecific/index.docbook | 2199 ++++++++ po/fr/kcm-kwin-scripts.po | 102 + po/fr/kcm_kwin_effects.po | 98 + po/fr/kcm_kwin_virtualdesktops.po | 136 + po/fr/kcm_kwindecoration.po | 229 + po/fr/kcm_kwinrules.po | 907 +++ po/fr/kcm_kwintabbox.po | 234 + po/fr/kcmkwincommon.po | 91 + po/fr/kcmkwincompositing.po | 366 ++ po/fr/kcmkwinscreenedges.po | 249 + po/fr/kcmkwm.po | 1436 +++++ po/fr/kwin.po | 2625 +++++++++ po/fr/kwin_clients.po | 139 + po/fr/kwin_effects.po | 2196 ++++++++ po/fr/kwin_scripting.po | 119 + po/fr/kwin_scripts.po | 64 + po/fy/kcm_kwin_virtualdesktops.po | 130 + po/fy/kcm_kwindecoration.po | 238 + po/fy/kcm_kwinrules.po | 878 +++ po/fy/kcmkwincompositing.po | 226 + po/fy/kcmkwinscreenedges.po | 250 + po/fy/kcmkwm.po | 1658 ++++++ po/fy/kwin.po | 2621 +++++++++ po/fy/kwin_clients.po | 142 + po/fy/kwin_effects.po | 2214 ++++++++ po/ga/kcm-kwin-scripts.po | 92 + po/ga/kcm_kwin_virtualdesktops.po | 133 + po/ga/kcm_kwindecoration.po | 241 + po/ga/kcm_kwinrules.po | 886 +++ po/ga/kcm_kwintabbox.po | 225 + po/ga/kcmkwincompositing.po | 231 + po/ga/kcmkwinscreenedges.po | 252 + po/ga/kcmkwm.po | 1783 ++++++ po/ga/kwin.po | 2619 +++++++++ po/ga/kwin_clients.po | 683 +++ po/ga/kwin_effects.po | 2185 ++++++++ po/gl/kcm-kwin-scripts.po | 92 + po/gl/kcm_kwin_effects.po | 98 + po/gl/kcm_kwin_virtualdesktops.po | 136 + po/gl/kcm_kwindecoration.po | 223 + po/gl/kcm_kwinrules.po | 907 +++ po/gl/kcm_kwintabbox.po | 230 + po/gl/kcmkwincommon.po | 80 + po/gl/kcmkwincompositing.po | 242 + po/gl/kcmkwinscreenedges.po | 241 + po/gl/kcmkwm.po | 1456 +++++ po/gl/kwin.po | 2646 +++++++++ po/gl/kwin_clients.po | 132 + po/gl/kwin_effects.po | 2168 ++++++++ po/gl/kwin_scripting.po | 119 + po/gl/kwin_scripts.po | 61 + po/gu/kcm_kwin_virtualdesktops.po | 130 + po/gu/kcm_kwindecoration.po | 237 + po/gu/kcm_kwinrules.po | 850 +++ po/gu/kcm_kwintabbox.po | 231 + po/gu/kcmkwincompositing.po | 226 + po/gu/kcmkwinscreenedges.po | 235 + po/gu/kcmkwm.po | 1265 +++++ po/gu/kwin.po | 2601 +++++++++ po/gu/kwin_clients.po | 132 + po/gu/kwin_effects.po | 2212 ++++++++ po/he/kcm-kwin-scripts.po | 95 + po/he/kcm_kwin_virtualdesktops.po | 134 + po/he/kcm_kwindecoration.po | 254 + po/he/kcm_kwinrules.po | 878 +++ po/he/kcm_kwintabbox.po | 230 + po/he/kcmkwincompositing.po | 220 + po/he/kcmkwinscreenedges.po | 239 + po/he/kcmkwm.po | 1487 +++++ po/he/kwin.po | 2633 +++++++++ po/he/kwin_clients.po | 128 + po/he/kwin_effects.po | 2160 ++++++++ po/he/kwin_scripting.po | 106 + po/he/kwin_scripts.po | 58 + po/hi/kcm_kwin_virtualdesktops.po | 130 + po/hi/kcm_kwindecoration.po | 230 + po/hi/kcm_kwinrules.po | 864 +++ po/hi/kcm_kwintabbox.po | 234 + po/hi/kcmkwincompositing.po | 220 + po/hi/kcmkwinscreenedges.po | 235 + po/hi/kcmkwm.po | 1346 +++++ po/hi/kwin.po | 2633 +++++++++ po/hi/kwin_clients.po | 139 + po/hi/kwin_effects.po | 2232 ++++++++ po/hne/kcm_kwin_virtualdesktops.po | 131 + po/hne/kcm_kwindecoration.po | 231 + po/hne/kcm_kwinrules.po | 865 +++ po/hne/kcmkwincompositing.po | 227 + po/hne/kcmkwm.po | 1341 +++++ po/hne/kwin.po | 2628 +++++++++ po/hne/kwin_clients.po | 134 + po/hne/kwin_effects.po | 2203 ++++++++ po/hr/kcm_kwin_virtualdesktops.po | 136 + po/hr/kcm_kwindecoration.po | 248 + po/hr/kcm_kwinrules.po | 893 +++ po/hr/kcm_kwintabbox.po | 245 + po/hr/kcmkwincompositing.po | 237 + po/hr/kcmkwinscreenedges.po | 254 + po/hr/kcmkwm.po | 1429 +++++ po/hr/kwin.po | 2627 +++++++++ po/hr/kwin_clients.po | 141 + po/hr/kwin_effects.po | 2222 ++++++++ po/hsb/kcm_kwin_virtualdesktops.po | 132 + po/hsb/kcm_kwindecoration.po | 230 + po/hsb/kcm_kwinrules.po | 827 +++ po/hsb/kcmkwincompositing.po | 226 + po/hsb/kcmkwm.po | 1312 +++++ po/hsb/kwin.po | 2617 +++++++++ po/hsb/kwin_clients.po | 125 + po/hsb/kwin_effects.po | 2187 ++++++++ po/hu/kcm-kwin-scripts.po | 93 + po/hu/kcm_kwin_effects.po | 97 + po/hu/kcm_kwin_virtualdesktops.po | 133 + po/hu/kcm_kwindecoration.po | 216 + po/hu/kcm_kwinrules.po | 902 +++ po/hu/kcm_kwintabbox.po | 229 + po/hu/kcmkwincommon.po | 81 + po/hu/kcmkwincompositing.po | 236 + po/hu/kcmkwinscreenedges.po | 232 + po/hu/kcmkwm.po | 1450 +++++ po/hu/kwin.po | 2648 +++++++++ po/hu/kwin_clients.po | 128 + po/hu/kwin_effects.po | 2200 ++++++++ po/hu/kwin_scripting.po | 115 + po/hu/kwin_scripts.po | 61 + po/ia/kcm-kwin-scripts.po | 92 + po/ia/kcm_kwin_effects.po | 98 + po/ia/kcm_kwin_virtualdesktops.po | 127 + po/ia/kcm_kwindecoration.po | 214 + po/ia/kcm_kwinrules.po | 850 +++ po/ia/kcm_kwintabbox.po | 228 + po/ia/kcmkwincommon.po | 81 + po/ia/kcmkwincompositing.po | 216 + po/ia/kcmkwinscreenedges.po | 239 + po/ia/kcmkwm.po | 1369 +++++ po/ia/kwin.po | 2581 +++++++++ po/ia/kwin_clients.po | 127 + po/ia/kwin_effects.po | 2162 ++++++++ po/ia/kwin_scripting.po | 116 + po/ia/kwin_scripts.po | 61 + po/id/docs/kcontrol/desktop/index.docbook | 170 + .../kcontrol/kwindecoration/index.docbook | 183 + po/id/docs/kcontrol/kwineffects/index.docbook | 134 + .../kcontrol/kwinscreenedges/index.docbook | 92 + po/id/docs/kcontrol/kwintabbox/index.docbook | 171 + .../kcontrol/windowbehaviour/index.docbook | 811 +++ .../kcontrol/windowspecific/index.docbook | 2214 ++++++++ po/id/kcm-kwin-scripts.po | 91 + po/id/kcm_kwin_effects.po | 98 + po/id/kcm_kwin_virtualdesktops.po | 131 + po/id/kcm_kwindecoration.po | 215 + po/id/kcm_kwinrules.po | 905 +++ po/id/kcm_kwintabbox.po | 227 + po/id/kcmkwincommon.po | 82 + po/id/kcmkwincompositing.po | 238 + po/id/kcmkwinscreenedges.po | 239 + po/id/kcmkwm.po | 1393 +++++ po/id/kwin.po | 2621 +++++++++ po/id/kwin_clients.po | 133 + po/id/kwin_effects.po | 2168 ++++++++ po/id/kwin_scripting.po | 110 + po/id/kwin_scripts.po | 60 + po/is/kcm_kwin_virtualdesktops.po | 133 + po/is/kcm_kwindecoration.po | 244 + po/is/kcm_kwinrules.po | 885 +++ po/is/kcm_kwintabbox.po | 243 + po/is/kcmkwincompositing.po | 229 + po/is/kcmkwinscreenedges.po | 252 + po/is/kcmkwm.po | 1428 +++++ po/is/kwin.po | 2624 +++++++++ po/is/kwin_clients.po | 137 + po/is/kwin_effects.po | 2217 ++++++++ po/it/docs/kcontrol/desktop/index.docbook | 178 + .../kcontrol/kwindecoration/index.docbook | 201 + po/it/docs/kcontrol/kwineffects/index.docbook | 131 + .../kcontrol/kwinscreenedges/index.docbook | 94 + po/it/docs/kcontrol/kwintabbox/index.docbook | 173 + .../kcontrol/windowbehaviour/index.docbook | 815 +++ .../kcontrol/windowspecific/index.docbook | 2204 ++++++++ po/it/kcm-kwin-scripts.po | 93 + po/it/kcm_kwin_effects.po | 98 + po/it/kcm_kwin_virtualdesktops.po | 247 + po/it/kcm_kwindecoration.po | 311 ++ po/it/kcm_kwinrules.po | 875 +++ po/it/kcm_kwintabbox.po | 230 + po/it/kcmkwincommon.po | 85 + po/it/kcmkwincompositing.po | 241 + po/it/kcmkwinscreenedges.po | 241 + po/it/kcmkwm.po | 1407 +++++ po/it/kwin.po | 2608 +++++++++ po/it/kwin_clients.po | 130 + po/it/kwin_effects.po | 2172 ++++++++ po/it/kwin_scripting.po | 116 + po/it/kwin_scripts.po | 62 + po/ja/kcm-kwin-scripts.po | 87 + po/ja/kcm_kwin_effects.po | 91 + po/ja/kcm_kwin_virtualdesktops.po | 126 + po/ja/kcm_kwindecoration.po | 225 + po/ja/kcm_kwinrules.po | 1002 ++++ po/ja/kcm_kwintabbox.po | 230 + po/ja/kcmkwincommon.po | 86 + po/ja/kcmkwincompositing.po | 220 + po/ja/kcmkwinscreenedges.po | 240 + po/ja/kcmkwm.po | 1459 +++++ po/ja/kwin.po | 2625 +++++++++ po/ja/kwin_clients.po | 132 + po/ja/kwin_effects.po | 2204 ++++++++ po/ja/kwin_scripting.po | 106 + po/ja/kwin_scripts.po | 58 + po/kk/kcm-kwin-scripts.po | 91 + po/kk/kcm_kwin_virtualdesktops.po | 132 + po/kk/kcm_kwindecoration.po | 244 + po/kk/kcm_kwinrules.po | 904 +++ po/kk/kcm_kwintabbox.po | 225 + po/kk/kcmkwincompositing.po | 242 + po/kk/kcmkwinscreenedges.po | 247 + po/kk/kcmkwm.po | 1504 +++++ po/kk/kwin.po | 2657 +++++++++ po/kk/kwin_clients.po | 141 + po/kk/kwin_effects.po | 2198 ++++++++ po/kk/kwin_scripting.po | 115 + po/km/kcm-kwin-scripts.po | 95 + po/km/kcm_kwin_virtualdesktops.po | 131 + po/km/kcm_kwindecoration.po | 237 + po/km/kcm_kwinrules.po | 1227 ++++ po/km/kcm_kwintabbox.po | 226 + po/km/kcmkwincompositing.po | 233 + po/km/kcmkwinscreenedges.po | 243 + po/km/kcmkwm.po | 1393 +++++ po/km/kwin.po | 2614 +++++++++ po/km/kwin_clients.po | 593 ++ po/km/kwin_effects.po | 2203 ++++++++ po/kn/kcm_kwin_virtualdesktops.po | 132 + po/kn/kcm_kwindecoration.po | 232 + po/kn/kcm_kwinrules.po | 873 +++ po/kn/kcm_kwintabbox.po | 241 + po/kn/kcmkwincompositing.po | 226 + po/kn/kcmkwinscreenedges.po | 242 + po/kn/kcmkwm.po | 1421 +++++ po/kn/kwin.po | 2623 +++++++++ po/kn/kwin_clients.po | 129 + po/kn/kwin_effects.po | 2216 ++++++++ po/ko/kcm-kwin-scripts.po | 92 + po/ko/kcm_kwin_effects.po | 98 + po/ko/kcm_kwin_virtualdesktops.po | 128 + po/ko/kcm_kwindecoration.po | 214 + po/ko/kcm_kwinrules.po | 859 +++ po/ko/kcm_kwintabbox.po | 224 + po/ko/kcmkwincommon.po | 82 + po/ko/kcmkwincompositing.po | 231 + po/ko/kcmkwinscreenedges.po | 229 + po/ko/kcmkwm.po | 1356 +++++ po/ko/kwin.po | 2574 +++++++++ po/ko/kwin_clients.po | 128 + po/ko/kwin_effects.po | 2146 +++++++ po/ko/kwin_scripting.po | 110 + po/ko/kwin_scripts.po | 61 + po/ku/kcm_kwin_virtualdesktops.po | 131 + po/ku/kcm_kwindecoration.po | 231 + po/ku/kcm_kwinrules.po | 853 +++ po/ku/kcmkwincompositing.po | 225 + po/ku/kcmkwm.po | 1339 +++++ po/ku/kwin.po | 2610 +++++++++ po/ku/kwin_clients.po | 135 + po/ku/kwin_effects.po | 2180 ++++++++ po/lt/kcm-kwin-scripts.po | 95 + po/lt/kcm_kwin_effects.po | 100 + po/lt/kcm_kwin_virtualdesktops.po | 137 + po/lt/kcm_kwindecoration.po | 218 + po/lt/kcm_kwinrules.po | 883 +++ po/lt/kcm_kwintabbox.po | 232 + po/lt/kcmkwincommon.po | 83 + po/lt/kcmkwincompositing.po | 239 + po/lt/kcmkwinscreenedges.po | 239 + po/lt/kcmkwm.po | 1394 +++++ po/lt/kwin.po | 2591 +++++++++ po/lt/kwin_clients.po | 130 + po/lt/kwin_effects.po | 2176 ++++++++ po/lt/kwin_scripting.po | 120 + po/lt/kwin_scripts.po | 63 + po/lv/kcm_kwin_virtualdesktops.po | 137 + po/lv/kcm_kwindecoration.po | 241 + po/lv/kcm_kwinrules.po | 914 +++ po/lv/kcm_kwintabbox.po | 242 + po/lv/kcmkwincompositing.po | 237 + po/lv/kcmkwinscreenedges.po | 245 + po/lv/kcmkwm.po | 1421 +++++ po/lv/kwin.po | 2631 +++++++++ po/lv/kwin_clients.po | 131 + po/lv/kwin_effects.po | 2183 ++++++++ po/mai/kcm_kwin_virtualdesktops.po | 131 + po/mai/kcm_kwindecoration.po | 234 + po/mai/kcm_kwinrules.po | 868 +++ po/mai/kcm_kwintabbox.po | 229 + po/mai/kcmkwincompositing.po | 228 + po/mai/kcmkwinscreenedges.po | 238 + po/mai/kcmkwm.po | 1336 +++++ po/mai/kwin.po | 2624 +++++++++ po/mai/kwin_clients.po | 143 + po/mai/kwin_effects.po | 2209 ++++++++ po/mk/kcm_kwin_virtualdesktops.po | 134 + po/mk/kcm_kwindecoration.po | 231 + po/mk/kcm_kwinrules.po | 878 +++ po/mk/kcmkwincompositing.po | 227 + po/mk/kcmkwm.po | 1420 +++++ po/mk/kwin.po | 2622 +++++++++ po/mk/kwin_clients.po | 136 + po/mk/kwin_effects.po | 2207 ++++++++ po/ml/kcm-kwin-scripts.po | 91 + po/ml/kcm_kwin_effects.po | 95 + po/ml/kcm_kwin_virtualdesktops.po | 128 + po/ml/kcm_kwindecoration.po | 214 + po/ml/kcm_kwinrules.po | 842 +++ po/ml/kcm_kwintabbox.po | 223 + po/ml/kcmkwincommon.po | 82 + po/ml/kcmkwincompositing.po | 211 + po/ml/kcmkwinscreenedges.po | 235 + po/ml/kcmkwm.po | 1285 +++++ po/ml/kwin.po | 2551 +++++++++ po/ml/kwin_clients.po | 128 + po/ml/kwin_effects.po | 2134 +++++++ po/ml/kwin_scripting.po | 110 + po/ml/kwin_scripts.po | 62 + po/mr/kcm-kwin-scripts.po | 96 + po/mr/kcm_kwin_virtualdesktops.po | 132 + po/mr/kcm_kwindecoration.po | 246 + po/mr/kcm_kwinrules.po | 874 +++ po/mr/kcm_kwintabbox.po | 225 + po/mr/kcmkwincompositing.po | 235 + po/mr/kcmkwinscreenedges.po | 250 + po/mr/kcmkwm.po | 1315 +++++ po/mr/kwin.po | 2602 +++++++++ po/mr/kwin_clients.po | 132 + po/mr/kwin_effects.po | 2193 ++++++++ po/mr/kwin_scripting.po | 109 + po/ms/kcm_kwin_virtualdesktops.po | 131 + po/ms/kcm_kwindecoration.po | 237 + po/ms/kcm_kwinrules.po | 874 +++ po/ms/kcm_kwintabbox.po | 233 + po/ms/kcmkwincompositing.po | 224 + po/ms/kcmkwinscreenedges.po | 236 + po/ms/kcmkwm.po | 1417 +++++ po/ms/kwin.po | 2622 +++++++++ po/ms/kwin_clients.po | 132 + po/ms/kwin_effects.po | 2153 ++++++++ po/nb/kcm-kwin-scripts.po | 92 + po/nb/kcm_kwin_virtualdesktops.po | 129 + po/nb/kcm_kwindecoration.po | 216 + po/nb/kcm_kwinrules.po | 854 +++ po/nb/kcm_kwintabbox.po | 229 + po/nb/kcmkwincommon.po | 83 + po/nb/kcmkwincompositing.po | 229 + po/nb/kcmkwinscreenedges.po | 234 + po/nb/kcmkwm.po | 1303 +++++ po/nb/kwin.po | 2567 +++++++++ po/nb/kwin_clients.po | 130 + po/nb/kwin_effects.po | 2144 +++++++ po/nb/kwin_scripting.po | 114 + po/nds/kcm-kwin-scripts.po | 94 + po/nds/kcm_kwin_virtualdesktops.po | 132 + po/nds/kcm_kwindecoration.po | 248 + po/nds/kcm_kwinrules.po | 907 +++ po/nds/kcm_kwintabbox.po | 231 + po/nds/kcmkwincompositing.po | 235 + po/nds/kcmkwinscreenedges.po | 243 + po/nds/kcmkwm.po | 1499 +++++ po/nds/kwin.po | 2662 +++++++++ po/nds/kwin_clients.po | 133 + po/nds/kwin_effects.po | 2206 ++++++++ po/nds/kwin_scripting.po | 116 + po/ne/kcm_kwin_virtualdesktops.po | 129 + po/ne/kcm_kwindecoration.po | 230 + po/ne/kcm_kwinrules.po | 874 +++ po/ne/kcmkwincompositing.po | 221 + po/ne/kcmkwm.po | 1416 +++++ po/ne/kwin.po | 2638 +++++++++ po/ne/kwin_clients.po | 136 + po/nl/docs/kcontrol/desktop/index.docbook | 170 + .../kcontrol/kwindecoration/index.docbook | 167 + po/nl/docs/kcontrol/kwineffects/index.docbook | 117 + .../kcontrol/kwinscreenedges/index.docbook | 80 + po/nl/docs/kcontrol/kwintabbox/index.docbook | 157 + .../kcontrol/windowbehaviour/index.docbook | 795 +++ .../kcontrol/windowspecific/index.docbook | 2194 ++++++++ po/nl/kcm-kwin-scripts.po | 93 + po/nl/kcm_kwin_effects.po | 98 + po/nl/kcm_kwin_virtualdesktops.po | 133 + po/nl/kcm_kwindecoration.po | 225 + po/nl/kcm_kwinrules.po | 881 +++ po/nl/kcm_kwintabbox.po | 229 + po/nl/kcmkwincommon.po | 81 + po/nl/kcmkwincompositing.po | 243 + po/nl/kcmkwinscreenedges.po | 243 + po/nl/kcmkwm.po | 1408 +++++ po/nl/kwin.po | 2599 +++++++++ po/nl/kwin_clients.po | 131 + po/nl/kwin_effects.po | 2172 ++++++++ po/nl/kwin_scripting.po | 113 + po/nl/kwin_scripts.po | 61 + po/nn/kcm-kwin-scripts.po | 94 + po/nn/kcm_kwin_effects.po | 100 + po/nn/kcm_kwin_virtualdesktops.po | 134 + po/nn/kcm_kwindecoration.po | 218 + po/nn/kcm_kwinrules.po | 873 +++ po/nn/kcm_kwintabbox.po | 231 + po/nn/kcmkwincommon.po | 86 + po/nn/kcmkwincompositing.po | 238 + po/nn/kcmkwinscreenedges.po | 236 + po/nn/kcmkwm.po | 1384 +++++ po/nn/kwin.po | 2589 +++++++++ po/nn/kwin_clients.po | 131 + po/nn/kwin_effects.po | 2159 ++++++++ po/nn/kwin_scripting.po | 116 + po/nn/kwin_scripts.po | 63 + po/oc/kcm_kwin_virtualdesktops.po | 130 + po/oc/kcm_kwindecoration.po | 225 + po/oc/kcm_kwinrules.po | 823 +++ po/oc/kcmkwincompositing.po | 218 + po/oc/kcmkwm.po | 1256 +++++ po/oc/kwin.po | 2560 +++++++++ po/oc/kwin_clients.po | 132 + po/oc/kwin_effects.po | 2151 +++++++ po/pa/kcm-kwin-scripts.po | 92 + po/pa/kcm_kwin_virtualdesktops.po | 128 + po/pa/kcm_kwindecoration.po | 241 + po/pa/kcm_kwinrules.po | 869 +++ po/pa/kcm_kwintabbox.po | 223 + po/pa/kcmkwincompositing.po | 231 + po/pa/kcmkwinscreenedges.po | 236 + po/pa/kcmkwm.po | 1312 +++++ po/pa/kwin.po | 2615 +++++++++ po/pa/kwin_clients.po | 132 + po/pa/kwin_effects.po | 2180 ++++++++ po/pa/kwin_scripting.po | 109 + po/pl/kcm-kwin-scripts.po | 93 + po/pl/kcm_kwin_effects.po | 97 + po/pl/kcm_kwin_virtualdesktops.po | 137 + po/pl/kcm_kwindecoration.po | 222 + po/pl/kcm_kwinrules.po | 874 +++ po/pl/kcm_kwintabbox.po | 231 + po/pl/kcmkwincommon.po | 82 + po/pl/kcmkwincompositing.po | 240 + po/pl/kcmkwinscreenedges.po | 237 + po/pl/kcmkwm.po | 1402 +++++ po/pl/kwin.po | 2604 +++++++++ po/pl/kwin_clients.po | 130 + po/pl/kwin_effects.po | 2170 ++++++++ po/pl/kwin_scripting.po | 114 + po/pl/kwin_scripts.po | 62 + po/pt/docs/kcontrol/desktop/index.docbook | 170 + .../kcontrol/kwindecoration/index.docbook | 183 + po/pt/docs/kcontrol/kwineffects/index.docbook | 134 + .../kcontrol/kwinscreenedges/index.docbook | 94 + po/pt/docs/kcontrol/kwintabbox/index.docbook | 171 + .../kcontrol/windowbehaviour/index.docbook | 837 +++ .../kcontrol/windowspecific/index.docbook | 2227 ++++++++ po/pt/kcm-kwin-scripts.po | 88 + po/pt/kcm_kwin_effects.po | 99 + po/pt/kcm_kwin_virtualdesktops.po | 127 + po/pt/kcm_kwindecoration.po | 212 + po/pt/kcm_kwinrules.po | 869 +++ po/pt/kcm_kwintabbox.po | 224 + po/pt/kcmkwincommon.po | 77 + po/pt/kcmkwincompositing.po | 235 + po/pt/kcmkwinscreenedges.po | 233 + po/pt/kcmkwm.po | 1406 +++++ po/pt/kwin.po | 2603 +++++++++ po/pt/kwin_clients.po | 129 + po/pt/kwin_effects.po | 2164 ++++++++ po/pt/kwin_scripting.po | 119 + po/pt/kwin_scripts.po | 57 + po/pt_BR/docs/kcontrol/desktop/index.docbook | 170 + .../kcontrol/kwindecoration/configure.png | Bin 0 -> 582 bytes .../kcontrol/kwindecoration/index.docbook | 183 + .../kcontrol/kwinscreenedges/index.docbook | 94 + .../docs/kcontrol/kwintabbox/index.docbook | 171 + .../kcontrol/windowbehaviour/index.docbook | 837 +++ .../kcontrol/windowspecific/index.docbook | 2225 ++++++++ po/pt_BR/kcm-kwin-scripts.po | 94 + po/pt_BR/kcm_kwin_effects.po | 100 + po/pt_BR/kcm_kwin_virtualdesktops.po | 134 + po/pt_BR/kcm_kwindecoration.po | 222 + po/pt_BR/kcm_kwinrules.po | 1061 ++++ po/pt_BR/kcm_kwintabbox.po | 232 + po/pt_BR/kcmkwincommon.po | 82 + po/pt_BR/kcmkwincompositing.po | 239 + po/pt_BR/kcmkwinscreenedges.po | 240 + po/pt_BR/kcmkwm.po | 1406 +++++ po/pt_BR/kwin.po | 2611 +++++++++ po/pt_BR/kwin_clients.po | 135 + po/pt_BR/kwin_effects.po | 2167 ++++++++ po/pt_BR/kwin_scripting.po | 119 + po/pt_BR/kwin_scripts.po | 62 + po/ro/kcm-kwin-scripts.po | 94 + po/ro/kcm_kwin_effects.po | 99 + po/ro/kcm_kwin_virtualdesktops.po | 138 + po/ro/kcm_kwindecoration.po | 218 + po/ro/kcm_kwinrules.po | 874 +++ po/ro/kcm_kwintabbox.po | 230 + po/ro/kcmkwincommon.po | 82 + po/ro/kcmkwincompositing.po | 241 + po/ro/kcmkwinscreenedges.po | 241 + po/ro/kcmkwm.po | 1360 +++++ po/ro/kwin.po | 2581 +++++++++ po/ro/kwin_clients.po | 134 + po/ro/kwin_effects.po | 2170 ++++++++ po/ro/kwin_scripting.po | 117 + po/ro/kwin_scripts.po | 62 + po/ru/docs/kcontrol/desktop/index.docbook | 188 + po/ru/docs/kcontrol/kwindecoration/button.png | Bin 0 -> 34437 bytes .../kcontrol/kwindecoration/configure.png | Bin 0 -> 483 bytes .../kcontrol/kwindecoration/decoration.png | Bin 0 -> 29903 bytes .../kcontrol/kwindecoration/index.docbook | 197 + po/ru/docs/kcontrol/kwindecoration/main.png | Bin 0 -> 53065 bytes po/ru/docs/kcontrol/kwineffects/index.docbook | 134 + .../kcontrol/kwinscreenedges/index.docbook | 94 + po/ru/docs/kcontrol/kwintabbox/index.docbook | 183 + .../kcontrol/windowbehaviour/index.docbook | 841 +++ .../windowspecific/akgregator-info.png | Bin 0 -> 807311 bytes .../windowspecific/akregator-attributes.png | Bin 0 -> 78261 bytes .../kcontrol/windowspecific/akregator-fav.png | Bin 0 -> 54037 bytes .../windowspecific/config-win-behavior.png | Bin 0 -> 51629 bytes .../windowspecific/emacs-attribute.png | Bin 0 -> 77149 bytes .../kcontrol/windowspecific/emacs-info.png | Bin 0 -> 835866 bytes .../focus-stealing-pop2top-attribute.png | Bin 0 -> 64469 bytes .../windowspecific/knotes-attribute.png | Bin 0 -> 42443 bytes .../kcontrol/windowspecific/knotes-info.png | Bin 0 -> 34092 bytes .../windowspecific/kopete-attribute-2.png | Bin 0 -> 42618 bytes .../windowspecific/kopete-chat-attribute.png | Bin 0 -> 42460 bytes .../windowspecific/kopete-chat-info.png | Bin 0 -> 33630 bytes .../kcontrol/windowspecific/kopete-info.png | Bin 0 -> 33230 bytes .../windowspecific/kwin-detect-window.png | Bin 0 -> 34194 bytes .../windowspecific/kwin-kopete-rules.png | Bin 0 -> 34830 bytes .../windowspecific/kwin-rule-editor.png | Bin 0 -> 46851 bytes .../kwin-rules-main-n-akregator.png | Bin 0 -> 33970 bytes .../windowspecific/kwin-rules-main.png | Bin 0 -> 35382 bytes .../windowspecific/kwin-rules-ordering.png | Bin 0 -> 35771 bytes .../windowspecific/kwin-window-attributes.png | Bin 0 -> 79639 bytes .../windowspecific/kwin-window-matching.png | Bin 0 -> 46851 bytes .../tbird-compose-attribute.png | Bin 0 -> 64438 bytes .../windowspecific/tbird-compose-info.png | Bin 0 -> 828004 bytes .../windowspecific/tbird-main-attribute.png | Bin 0 -> 77587 bytes .../windowspecific/tbird-main-info.png | Bin 0 -> 826785 bytes .../tbird-reminder-attribute-2.png | Bin 0 -> 43435 bytes .../windowspecific/tbird-reminder-info.png | Bin 0 -> 816667 bytes .../windowspecific/window-matching-emacs.png | Bin 0 -> 51975 bytes .../windowspecific/window-matching-init.png | Bin 0 -> 46859 bytes .../windowspecific/window-matching-knotes.png | Bin 0 -> 47837 bytes .../window-matching-kopete-chat.png | Bin 0 -> 49628 bytes .../windowspecific/window-matching-kopete.png | Bin 0 -> 47908 bytes .../window-matching-ready-akregator.png | Bin 0 -> 48337 bytes .../window-matching-tbird-compose.png | Bin 0 -> 50376 bytes .../window-matching-tbird-main.png | Bin 0 -> 49064 bytes .../window-matching-tbird-reminder.png | Bin 0 -> 48901 bytes po/ru/kcm-kwin-scripts.po | 99 + po/ru/kcm_kwin_effects.po | 98 + po/ru/kcm_kwin_virtualdesktops.po | 143 + po/ru/kcm_kwindecoration.po | 230 + po/ru/kcm_kwinrules.po | 1064 ++++ po/ru/kcm_kwintabbox.po | 233 + po/ru/kcmkwincommon.po | 83 + po/ru/kcmkwincompositing.po | 573 ++ po/ru/kcmkwinscreenedges.po | 245 + po/ru/kcmkwm.po | 1406 +++++ po/ru/kwin.po | 2746 +++++++++ po/ru/kwin_clients.po | 137 + po/ru/kwin_effects.po | 2177 ++++++++ po/ru/kwin_scripting.po | 119 + po/ru/kwin_scripts.po | 62 + po/se/kcm_kwin_virtualdesktops.po | 129 + po/se/kcm_kwindecoration.po | 214 + po/se/kcm_kwinrules.po | 814 +++ po/se/kcmkwincommon.po | 83 + po/se/kcmkwincompositing.po | 217 + po/se/kcmkwm.po | 1256 +++++ po/se/kwin.po | 2548 +++++++++ po/se/kwin_clients.po | 125 + po/si/kcm_kwin_virtualdesktops.po | 129 + po/si/kcm_kwindecoration.po | 229 + po/si/kcm_kwinrules.po | 870 +++ po/si/kcm_kwintabbox.po | 238 + po/si/kcmkwincompositing.po | 231 + po/si/kcmkwinscreenedges.po | 242 + po/si/kcmkwm.po | 1409 +++++ po/si/kwin.po | 2619 +++++++++ po/si/kwin_clients.po | 134 + po/si/kwin_effects.po | 2211 ++++++++ po/sk/kcm-kwin-scripts.po | 93 + po/sk/kcm_kwin_effects.po | 95 + po/sk/kcm_kwin_virtualdesktops.po | 133 + po/sk/kcm_kwindecoration.po | 218 + po/sk/kcm_kwinrules.po | 872 +++ po/sk/kcm_kwintabbox.po | 227 + po/sk/kcmkwincommon.po | 79 + po/sk/kcmkwincompositing.po | 235 + po/sk/kcmkwinscreenedges.po | 234 + po/sk/kcmkwm.po | 1431 +++++ po/sk/kwin.po | 2583 +++++++++ po/sk/kwin_clients.po | 129 + po/sk/kwin_effects.po | 2161 ++++++++ po/sk/kwin_scripting.po | 109 + po/sk/kwin_scripts.po | 59 + po/sl/kcm-kwin-scripts.po | 97 + po/sl/kcm_kwin_effects.po | 99 + po/sl/kcm_kwin_virtualdesktops.po | 139 + po/sl/kcm_kwindecoration.po | 225 + po/sl/kcm_kwinrules.po | 871 +++ po/sl/kcm_kwintabbox.po | 230 + po/sl/kcmkwincommon.po | 82 + po/sl/kcmkwincompositing.po | 238 + po/sl/kcmkwinscreenedges.po | 240 + po/sl/kcmkwm.po | 1399 +++++ po/sl/kwin.po | 2591 +++++++++ po/sl/kwin_clients.po | 132 + po/sl/kwin_effects.po | 2163 ++++++++ po/sl/kwin_scripting.po | 112 + po/sl/kwin_scripts.po | 62 + po/sq/kcm_kwin_virtualdesktops.po | 131 + po/sq/kcm_kwindecoration.po | 221 + po/sq/kcm_kwinrules.po | 834 +++ po/sq/kcmkwincompositing.po | 223 + po/sq/kcmkwinscreenedges.po | 236 + po/sq/kcmkwm.po | 1263 +++++ po/sq/kwin.po | 2572 +++++++++ po/sq/kwin_clients.po | 127 + po/sq/kwin_effects.po | 2177 ++++++++ po/sr/docs/kcontrol/desktop/index.docbook | 182 + po/sr/kcm-kwin-scripts.po | 89 + po/sr/kcm_kwin_virtualdesktops.po | 134 + po/sr/kcm_kwindecoration.po | 246 + po/sr/kcm_kwinrules.po | 925 ++++ po/sr/kcm_kwintabbox.po | 245 + po/sr/kcmkwincompositing.po | 243 + po/sr/kcmkwinscreenedges.po | 255 + po/sr/kcmkwm.po | 1464 +++++ po/sr/kwin.po | 2754 +++++++++ po/sr/kwin_clients.po | 138 + po/sr/kwin_effects.po | 2245 ++++++++ po/sr/kwin_scripting.po | 111 + po/sr/kwin_scripts.po | 64 + po/sr@ijekavian/kcm-kwin-scripts.po | 89 + po/sr@ijekavian/kcm_kwin_virtualdesktops.po | 134 + po/sr@ijekavian/kcm_kwindecoration.po | 246 + po/sr@ijekavian/kcm_kwinrules.po | 925 ++++ po/sr@ijekavian/kcm_kwintabbox.po | 245 + po/sr@ijekavian/kcmkwincompositing.po | 243 + po/sr@ijekavian/kcmkwinscreenedges.po | 255 + po/sr@ijekavian/kcmkwm.po | 1464 +++++ po/sr@ijekavian/kwin.po | 2754 +++++++++ po/sr@ijekavian/kwin_clients.po | 138 + po/sr@ijekavian/kwin_effects.po | 2245 ++++++++ po/sr@ijekavian/kwin_scripting.po | 111 + po/sr@ijekavian/kwin_scripts.po | 64 + po/sr@ijekavianlatin/kcm-kwin-scripts.po | 89 + .../kcm_kwin_virtualdesktops.po | 134 + po/sr@ijekavianlatin/kcm_kwindecoration.po | 246 + po/sr@ijekavianlatin/kcm_kwinrules.po | 925 ++++ po/sr@ijekavianlatin/kcm_kwintabbox.po | 245 + po/sr@ijekavianlatin/kcmkwincompositing.po | 243 + po/sr@ijekavianlatin/kcmkwinscreenedges.po | 255 + po/sr@ijekavianlatin/kcmkwm.po | 1466 +++++ po/sr@ijekavianlatin/kwin.po | 2755 +++++++++ po/sr@ijekavianlatin/kwin_clients.po | 138 + po/sr@ijekavianlatin/kwin_effects.po | 2246 ++++++++ po/sr@ijekavianlatin/kwin_scripting.po | 111 + po/sr@ijekavianlatin/kwin_scripts.po | 64 + .../docs/kcontrol/desktop/index.docbook | 182 + po/sr@latin/kcm-kwin-scripts.po | 89 + po/sr@latin/kcm_kwin_virtualdesktops.po | 134 + po/sr@latin/kcm_kwindecoration.po | 246 + po/sr@latin/kcm_kwinrules.po | 925 ++++ po/sr@latin/kcm_kwintabbox.po | 245 + po/sr@latin/kcmkwincompositing.po | 243 + po/sr@latin/kcmkwinscreenedges.po | 255 + po/sr@latin/kcmkwm.po | 1466 +++++ po/sr@latin/kwin.po | 2755 +++++++++ po/sr@latin/kwin_clients.po | 138 + po/sr@latin/kwin_effects.po | 2245 ++++++++ po/sr@latin/kwin_scripting.po | 111 + po/sr@latin/kwin_scripts.po | 64 + po/sv/kcm-kwin-scripts.po | 92 + po/sv/kcm_kwin_effects.po | 98 + po/sv/kcm_kwin_virtualdesktops.po | 132 + po/sv/kcm_kwindecoration.po | 216 + po/sv/kcm_kwinrules.po | 865 +++ po/sv/kcm_kwintabbox.po | 225 + po/sv/kcmkwincommon.po | 81 + po/sv/kcmkwincompositing.po | 236 + po/sv/kcmkwinscreenedges.po | 236 + po/sv/kcmkwm.po | 1391 +++++ po/sv/kwin.po | 2583 +++++++++ po/sv/kwin_clients.po | 128 + po/sv/kwin_effects.po | 2155 ++++++++ po/sv/kwin_scripting.po | 113 + po/sv/kwin_scripts.po | 61 + po/ta/kcm_kwin_virtualdesktops.po | 131 + po/ta/kcm_kwindecoration.po | 228 + po/ta/kcm_kwinrules.po | 867 +++ po/ta/kcmkwincompositing.po | 221 + po/ta/kcmkwm.po | 1374 +++++ po/ta/kwin.po | 2621 +++++++++ po/ta/kwin_clients.po | 127 + po/ta/kwin_effects.po | 2225 ++++++++ po/te/kcm_kwin_virtualdesktops.po | 130 + po/te/kcm_kwindecoration.po | 228 + po/te/kcm_kwinrules.po | 864 +++ po/te/kcmkwincompositing.po | 237 + po/te/kcmkwm.po | 1339 +++++ po/te/kwin.po | 2565 +++++++++ po/te/kwin_clients.po | 129 + po/te/kwin_effects.po | 2160 ++++++++ po/tg/kcm_kwin_virtualdesktops.po | 127 + po/tg/kcm_kwindecoration.po | 210 + po/tg/kcm_kwinrules.po | 825 +++ po/tg/kcmkwincommon.po | 81 + po/tg/kcmkwincompositing.po | 215 + po/tg/kcmkwinscreenedges.po | 229 + po/tg/kcmkwm.po | 1229 ++++ po/tg/kwin.po | 2547 +++++++++ po/tg/kwin_clients.po | 123 + po/tg/kwin_effects.po | 2130 +++++++ po/th/kcm_kwin_virtualdesktops.po | 131 + po/th/kcm_kwindecoration.po | 238 + po/th/kcm_kwinrules.po | 873 +++ po/th/kcm_kwintabbox.po | 239 + po/th/kcmkwincompositing.po | 227 + po/th/kcmkwinscreenedges.po | 245 + po/th/kcmkwm.po | 1404 +++++ po/th/kwin.po | 2628 +++++++++ po/th/kwin_clients.po | 140 + po/th/kwin_effects.po | 2204 ++++++++ po/tr/kcm-kwin-scripts.po | 93 + po/tr/kcm_kwin_virtualdesktops.po | 134 + po/tr/kcm_kwindecoration.po | 248 + po/tr/kcm_kwinrules.po | 909 +++ po/tr/kcm_kwintabbox.po | 231 + po/tr/kcmkwincompositing.po | 239 + po/tr/kcmkwinscreenedges.po | 246 + po/tr/kcmkwm.po | 1505 +++++ po/tr/kwin.po | 2669 +++++++++ po/tr/kwin_clients.po | 134 + po/tr/kwin_effects.po | 2211 ++++++++ po/tr/kwin_scripting.po | 115 + po/tr/kwin_scripts.po | 61 + po/ug/kcm-kwin-scripts.po | 90 + po/ug/kcm_kwin_virtualdesktops.po | 128 + po/ug/kcm_kwindecoration.po | 240 + po/ug/kcm_kwinrules.po | 862 +++ po/ug/kcm_kwintabbox.po | 222 + po/ug/kcmkwincompositing.po | 223 + po/ug/kcmkwinscreenedges.po | 234 + po/ug/kcmkwm.po | 1301 +++++ po/ug/kwin.po | 2601 +++++++++ po/ug/kwin_clients.po | 134 + po/ug/kwin_effects.po | 2198 ++++++++ po/ug/kwin_scripting.po | 109 + po/uk/docs/kcontrol/desktop/index.docbook | 170 + po/uk/docs/kcontrol/kwindecoration/button.png | Bin 0 -> 19312 bytes .../kcontrol/kwindecoration/decoration.png | Bin 0 -> 16065 bytes .../kcontrol/kwindecoration/index.docbook | 181 + po/uk/docs/kcontrol/kwindecoration/main.png | Bin 0 -> 17984 bytes po/uk/docs/kcontrol/kwineffects/index.docbook | 131 + .../kcontrol/kwinscreenedges/index.docbook | 94 + po/uk/docs/kcontrol/kwintabbox/index.docbook | 171 + .../kcontrol/windowbehaviour/index.docbook | 809 +++ .../kcontrol/windowspecific/index.docbook | 2225 ++++++++ po/uk/kcm-kwin-scripts.po | 95 + po/uk/kcm_kwin_effects.po | 101 + po/uk/kcm_kwin_virtualdesktops.po | 138 + po/uk/kcm_kwindecoration.po | 220 + po/uk/kcm_kwinrules.po | 882 +++ po/uk/kcm_kwintabbox.po | 229 + po/uk/kcmkwincommon.po | 84 + po/uk/kcmkwincompositing.po | 242 + po/uk/kcmkwinscreenedges.po | 237 + po/uk/kcmkwm.po | 1399 +++++ po/uk/kwin.po | 2597 +++++++++ po/uk/kwin_clients.po | 133 + po/uk/kwin_effects.po | 2171 ++++++++ po/uk/kwin_scripting.po | 116 + po/uk/kwin_scripts.po | 64 + po/uz/kcm_kwindecoration.po | 229 + po/uz/kcm_kwinrules.po | 845 +++ po/uz/kcmkwm.po | 1336 +++++ po/uz/kwin.po | 2615 +++++++++ po/uz/kwin_clients.po | 130 + po/uz@cyrillic/kcm_kwindecoration.po | 229 + po/uz@cyrillic/kcm_kwinrules.po | 845 +++ po/uz@cyrillic/kcmkwm.po | 1334 +++++ po/uz@cyrillic/kwin.po | 2615 +++++++++ po/uz@cyrillic/kwin_clients.po | 130 + po/vi/kcm_kwindecoration.po | 230 + po/vi/kcm_kwinrules.po | 886 +++ po/vi/kcmkwm.po | 1434 +++++ po/vi/kwin.po | 2622 +++++++++ po/vi/kwin_clients.po | 133 + po/wa/kcm_kwin_virtualdesktops.po | 130 + po/wa/kcm_kwindecoration.po | 240 + po/wa/kcm_kwinrules.po | 871 +++ po/wa/kcm_kwintabbox.po | 240 + po/wa/kcmkwincompositing.po | 225 + po/wa/kcmkwinscreenedges.po | 247 + po/wa/kcmkwm.po | 1396 +++++ po/wa/kwin.po | 2615 +++++++++ po/wa/kwin_clients.po | 137 + po/wa/kwin_effects.po | 2205 ++++++++ po/xh/kcm_kwindecoration.po | 216 + po/xh/kcmkwm.po | 1354 +++++ po/xh/kwin.po | 2574 +++++++++ po/zh_CN/kcm-kwin-scripts.po | 98 + po/zh_CN/kcm_kwin_effects.po | 96 + po/zh_CN/kcm_kwin_virtualdesktops.po | 136 + po/zh_CN/kcm_kwindecoration.po | 225 + po/zh_CN/kcm_kwinrules.po | 855 +++ po/zh_CN/kcm_kwintabbox.po | 229 + po/zh_CN/kcmkwincommon.po | 86 + po/zh_CN/kcmkwincompositing.po | 232 + po/zh_CN/kcmkwinscreenedges.po | 240 + po/zh_CN/kcmkwm.po | 1335 +++++ po/zh_CN/kwin.po | 2573 +++++++++ po/zh_CN/kwin_clients.po | 130 + po/zh_CN/kwin_effects.po | 2151 +++++++ po/zh_CN/kwin_scripting.po | 114 + po/zh_CN/kwin_scripts.po | 65 + po/zh_TW/kcm-kwin-scripts.po | 94 + po/zh_TW/kcm_kwin_effects.po | 98 + po/zh_TW/kcm_kwin_virtualdesktops.po | 130 + po/zh_TW/kcm_kwindecoration.po | 216 + po/zh_TW/kcm_kwinrules.po | 854 +++ po/zh_TW/kcm_kwintabbox.po | 225 + po/zh_TW/kcmkwincommon.po | 81 + po/zh_TW/kcmkwincompositing.po | 231 + po/zh_TW/kcmkwinscreenedges.po | 232 + po/zh_TW/kcmkwm.po | 1308 +++++ po/zh_TW/kwin.po | 2574 +++++++++ po/zh_TW/kwin_clients.po | 130 + po/zh_TW/kwin_effects.po | 2154 ++++++++ po/zh_TW/kwin_scripting.po | 110 + po/zh_TW/kwin_scripts.po | 61 + pointer_input.cpp | 1475 +++++ pointer_input.h | 284 + popup_input_filter.cpp | 74 + popup_input_filter.h | 35 + qml/CMakeLists.txt | 3 + .../plasma/dummydata/osd.qml | 7 + qml/onscreennotification/plasma/main.qml | 33 + qml/outline/plasma/outline.qml | 121 + rootinfo_filter.cpp | 34 + rootinfo_filter.h | 31 + rulebooksettings.cpp | 97 + rulebooksettings.h | 39 + rulebooksettingsbase.kcfg | 13 + rulebooksettingsbase.kcfgc | 5 + rules.cpp | 1111 ++++ rules.h | 381 ++ rulesettings.kcfg | 435 ++ rulesettings.kcfgc | 7 + scene.cpp | 1376 +++++ scene.h | 756 +++ screencast/pipewirecore.cpp | 113 + screencast/pipewirecore.h | 46 + screencast/pipewirestream.cpp | 478 ++ screencast/pipewirestream.h | 101 + screencast/screencastmanager.cpp | 147 + screencast/screencastmanager.h | 36 + screenedge.cpp | 1494 +++++ screenedge.h | 580 ++ screenlockerwatcher.cpp | 127 + screenlockerwatcher.h | 52 + screens.cpp | 231 + screens.h | 235 + scripting/CMakeLists.txt | 11 + scripting/Messages.sh | 2 + scripting/dbuscall.cpp | 42 + scripting/dbuscall.h | 136 + scripting/documentation-effect-global.xml | 179 + scripting/documentation-global.xml | 144 + scripting/genericscriptedconfig.cpp | 165 + scripting/genericscriptedconfig.h | 88 + scripting/genericscriptedconfig.json | 7 + scripting/kwinscript.desktop | 68 + scripting/meta.cpp | 245 + scripting/meta.h | 124 + scripting/screenedgeitem.cpp | 107 + scripting/screenedgeitem.h | 115 + scripting/scriptedeffect.cpp | 867 +++ scripting/scriptedeffect.h | 163 + scripting/scripting.cpp | 888 +++ scripting/scripting.h | 421 ++ scripting/scripting_logging.cpp | 10 + scripting/scripting_logging.h | 15 + scripting/scripting_model.cpp | 908 +++ scripting/scripting_model.h | 376 ++ scripting/scriptingutils.cpp | 35 + scripting/scriptingutils.h | 358 ++ scripting/timer.cpp | 31 + scripting/workspace_wrapper.cpp | 396 ++ scripting/workspace_wrapper.h | 384 ++ scripts/CMakeLists.txt | 14 + scripts/Messages.sh | 4 + scripts/desktopchangeosd/contents/ui/main.qml | 26 + scripts/desktopchangeosd/contents/ui/osd.qml | 299 + scripts/desktopchangeosd/metadata.desktop | 113 + scripts/minimizeall/contents/code/main.js | 72 + scripts/minimizeall/metadata.desktop | 98 + .../contents/code/main.js | 23 + .../synchronizeskipswitcher/metadata.desktop | 109 + scripts/videowall/contents/code/main.js | 35 + scripts/videowall/contents/config/main.xml | 22 + scripts/videowall/contents/ui/config.ui | 148 + scripts/videowall/metadata.desktop | 113 + service_utils.cpp | 21 + service_utils.h | 66 + settings.kcfgc | 7 + shadow.cpp | 490 ++ shadow.h | 181 + shortcutdialog.ui | 104 + sm.cpp | 383 ++ sm.h | 93 + subsurfacemonitor.cpp | 92 + subsurfacemonitor.h | 79 + syncalarmx11filter.cpp | 36 + syncalarmx11filter.h | 25 + tabbox/CMakeLists.txt | 3 + tabbox/clientmodel.cpp | 257 + tabbox/clientmodel.h | 103 + tabbox/desktopchain.cpp | 131 + tabbox/desktopchain.h | 137 + tabbox/desktopmodel.cpp | 176 + tabbox/desktopmodel.h | 79 + tabbox/kwindesktopswitcher.desktop | 60 + tabbox/kwinwindowswitcher.desktop | 62 + tabbox/switcheritem.cpp | 104 + tabbox/switcheritem.h | 107 + tabbox/tabbox.cpp | 1524 +++++ tabbox/tabbox.h | 363 ++ tabbox/tabbox_logging.cpp | 10 + tabbox/tabbox_logging.h | 15 + tabbox/tabboxconfig.cpp | 198 + tabbox/tabboxconfig.h | 315 ++ tabbox/tabboxhandler.cpp | 644 +++ tabbox/tabboxhandler.h | 391 ++ tabbox/x11_filter.cpp | 151 + tabbox/x11_filter.h | 38 + tablet_input.cpp | 155 + tablet_input.h | 80 + tabletmodemanager.cpp | 159 + tabletmodemanager.h | 47 + tests/CMakeLists.txt | 40 + tests/cursorhotspottest.cpp | 143 + tests/inputmethodstest.qml | 73 + tests/libinputtest.cpp | 103 + tests/normalhintsbasesizetest.cpp | 103 + tests/pointerconstraintstest.cpp | 403 ++ tests/pointerconstraintstest.h | 164 + tests/pointerconstraintstest.qml | 205 + tests/pointergesturestest.cpp | 154 + tests/pointergesturestest.qml | 16 + tests/screenedgeshowtest.cpp | 347 ++ tests/unmapdestroytest.qml | 72 + tests/x11shadowreader.cpp | 116 + thumbnailitem.cpp | 209 + thumbnailitem.h | 139 + toplevel.cpp | 830 +++ toplevel.h | 1068 ++++ touch_hide_cursor_spy.cpp | 54 + touch_hide_cursor_spy.h | 29 + touch_input.cpp | 222 + touch_input.h | 95 + udev.cpp | 286 + udev.h | 89 + unmanaged.cpp | 199 + unmanaged.h | 58 + useractions.cpp | 1690 ++++++ useractions.h | 252 + utils.cpp | 201 + utils.h | 199 + virtual_terminal.cpp | 215 + virtual_terminal.h | 50 + virtualdesktops.cpp | 912 +++ virtualdesktops.h | 716 +++ virtualdesktopsdbustypes.cpp | 59 + virtualdesktopsdbustypes.h | 36 + virtualkeyboard.cpp | 374 ++ virtualkeyboard.h | 66 + virtualkeyboard_dbus.cpp | 26 + virtualkeyboard_dbus.h | 51 + was_user_interaction_x11_filter.cpp | 27 + was_user_interaction_x11_filter.h | 26 + wayland_server.cpp | 887 +++ wayland_server.h | 313 ++ waylandclient.cpp | 537 ++ waylandclient.h | 95 + waylandshellintegration.cpp | 17 + waylandshellintegration.h | 25 + window_property_notify_x11_filter.cpp | 40 + window_property_notify_x11_filter.h | 31 + workspace.cpp | 2799 ++++++++++ workspace.h | 810 +++ x11client.cpp | 4920 +++++++++++++++++ x11client.h | 634 +++ x11eventfilter.cpp | 50 + x11eventfilter.h | 76 + xcbutils.cpp | 614 ++ xcbutils.h | 1883 +++++++ xcursortheme.cpp | 142 + xcursortheme.h | 126 + xdgshellclient.cpp | 2004 +++++++ xdgshellclient.h | 253 + xdgshellintegration.cpp | 77 + xdgshellintegration.h | 33 + xkb.cpp | 574 ++ xkb.h | 165 + xkb_qt_mapping.h | 311 ++ xwaylandclient.cpp | 26 + xwaylandclient.h | 28 + xwl/clipboard.cpp | 201 + xwl/clipboard.h | 62 + xwl/databridge.cpp | 105 + xwl/databridge.h | 89 + xwl/dnd.cpp | 219 + xwl/dnd.h | 82 + xwl/drag.cpp | 76 + xwl/drag.h | 58 + xwl/drag_wl.cpp | 435 ++ xwl/drag_wl.h | 155 + xwl/drag_x.cpp | 538 ++ xwl/drag_x.h | 158 + xwl/selection.cpp | 351 ++ xwl/selection.h | 133 + xwl/selection_source.cpp | 312 ++ xwl/selection_source.h | 164 + xwl/transfer.cpp | 583 ++ xwl/transfer.h | 219 + xwl/xwayland.cpp | 396 ++ xwl/xwayland.h | 116 + xwl/xwayland_interface.cpp | 32 + xwl/xwayland_interface.h | 60 + 2655 files changed, 1034558 insertions(+) create mode 100644 3rdparty/xcursor.c create mode 100644 3rdparty/xcursor.h create mode 100644 CMakeLists.txt create mode 100644 HACKING.md create mode 100644 KWinDBusInterfaceConfig.cmake.in create mode 100644 LICENSES/BSD-2-Clause.txt create mode 100644 LICENSES/BSD-3-Clause.txt create mode 100644 LICENSES/GPL-2.0-only.txt create mode 100644 LICENSES/GPL-2.0-or-later.txt create mode 100644 LICENSES/GPL-3.0-only.txt create mode 100644 LICENSES/GPL-3.0-or-later.txt create mode 100644 LICENSES/LGPL-2.0-only.txt create mode 100644 LICENSES/LGPL-2.0-or-later.txt create mode 100644 LICENSES/LGPL-2.1-only.txt create mode 100644 LICENSES/LGPL-3.0-only.txt create mode 100644 LICENSES/LicenseRef-KDE-Accepted-GPL.txt create mode 100644 LICENSES/LicenseRef-KDE-Accepted-LGPL.txt create mode 100644 LICENSES/MIT.txt create mode 100644 Mainpage.dox create mode 100644 Messages.sh create mode 100644 README.md create mode 100644 TESTING.md create mode 100644 abstract_client.cpp create mode 100644 abstract_client.h create mode 100644 abstract_opengl_context_attribute_builder.cpp create mode 100644 abstract_opengl_context_attribute_builder.h create mode 100644 abstract_output.cpp create mode 100644 abstract_output.h create mode 100644 abstract_wayland_output.cpp create mode 100644 abstract_wayland_output.h create mode 100644 activation.cpp create mode 100644 activities.cpp create mode 100644 activities.h create mode 100644 appmenu.cpp create mode 100644 appmenu.h create mode 100644 atoms.cpp create mode 100644 atoms.h create mode 100644 autotests/CMakeLists.txt create mode 100644 autotests/abstract_client.h create mode 100644 autotests/drm/CMakeLists.txt create mode 100644 autotests/drm/mock_drm.cpp create mode 100644 autotests/drm/mock_drm.h create mode 100644 autotests/drm/objecttest.cpp create mode 100644 autotests/fakeeffectplugin.cpp create mode 100644 autotests/fakeeffectplugin.json create mode 100644 autotests/fakeeffectplugin_version.cpp create mode 100644 autotests/fakeeffectplugin_version.json create mode 100644 autotests/integration/CMakeLists.txt create mode 100644 autotests/integration/activation_test.cpp create mode 100644 autotests/integration/activities_test.cpp create mode 100644 autotests/integration/buffer_size_change_test.cpp create mode 100644 autotests/integration/colorcorrect_nightcolor_test.cpp create mode 100644 autotests/integration/data/anim-data-delete-effect/effect.js create mode 100644 autotests/integration/data/example.desktop create mode 100644 autotests/integration/data/rules/maximize-vert-apply-initial create mode 100644 autotests/integration/dbus_interface_test.cpp create mode 100644 autotests/integration/debug_console_test.cpp create mode 100644 autotests/integration/decoration_input_test.cpp create mode 100644 autotests/integration/desktop_window_x11_test.cpp create mode 100644 autotests/integration/dont_crash_aurorae_destroy_deco.cpp create mode 100644 autotests/integration/dont_crash_cancel_animation.cpp create mode 100644 autotests/integration/dont_crash_cursor_physical_size_empty.cpp create mode 100644 autotests/integration/dont_crash_empty_deco.cpp create mode 100644 autotests/integration/dont_crash_glxgears.cpp create mode 100644 autotests/integration/dont_crash_no_border.cpp create mode 100644 autotests/integration/dont_crash_reinitialize_compositor.cpp create mode 100644 autotests/integration/dont_crash_useractions_menu.cpp create mode 100644 autotests/integration/effects/CMakeLists.txt create mode 100644 autotests/integration/effects/desktop_switching_animation_test.cpp create mode 100644 autotests/integration/effects/maximize_animation_test.cpp create mode 100644 autotests/integration/effects/minimize_animation_test.cpp create mode 100644 autotests/integration/effects/popup_open_close_animation_test.cpp create mode 100644 autotests/integration/effects/scripted_effects_test.cpp create mode 100644 autotests/integration/effects/scripts/animationTest.js create mode 100644 autotests/integration/effects/scripts/animationTestMulti.js create mode 100644 autotests/integration/effects/scripts/completeTest.js create mode 100644 autotests/integration/effects/scripts/effectContext.js create mode 100644 autotests/integration/effects/scripts/effectsHandler.js create mode 100644 autotests/integration/effects/scripts/fullScreenEffectTest.js create mode 100644 autotests/integration/effects/scripts/fullScreenEffectTestGlobal.js create mode 100644 autotests/integration/effects/scripts/fullScreenEffectTestMulti.js create mode 100644 autotests/integration/effects/scripts/grabAlreadyGrabbedWindowForcedTest_owner.js create mode 100644 autotests/integration/effects/scripts/grabAlreadyGrabbedWindowForcedTest_thief.js create mode 100644 autotests/integration/effects/scripts/grabAlreadyGrabbedWindowTest_grabber.js create mode 100644 autotests/integration/effects/scripts/grabAlreadyGrabbedWindowTest_owner.js create mode 100644 autotests/integration/effects/scripts/grabTest.js create mode 100644 autotests/integration/effects/scripts/keepAliveTest.js create mode 100644 autotests/integration/effects/scripts/keepAliveTestDontKeep.js create mode 100644 autotests/integration/effects/scripts/redirectAnimateDontTerminateTest.js create mode 100644 autotests/integration/effects/scripts/redirectAnimateTerminateTest.js create mode 100644 autotests/integration/effects/scripts/redirectSetDontTerminateTest.js create mode 100644 autotests/integration/effects/scripts/redirectSetTerminateTest.js create mode 100644 autotests/integration/effects/scripts/screenEdgeTest.js create mode 100644 autotests/integration/effects/scripts/screenEdgeTouchTest.js create mode 100644 autotests/integration/effects/scripts/shortcutsTest.js create mode 100644 autotests/integration/effects/scripts/ungrabTest.js create mode 100644 autotests/integration/effects/slidingpopups_test.cpp create mode 100644 autotests/integration/effects/toplevel_open_close_animation_test.cpp create mode 100644 autotests/integration/effects/translucency_test.cpp create mode 100644 autotests/integration/effects/windowgeometry_test.cpp create mode 100644 autotests/integration/effects/wobbly_shade_test.cpp create mode 100644 autotests/integration/fakes/CMakeLists.txt create mode 100644 autotests/integration/fakes/org.kde.kdecoration2/CMakeLists.txt create mode 100644 autotests/integration/fakes/org.kde.kdecoration2/fakedecoration_with_shadows.cpp create mode 100644 autotests/integration/fakes/org.kde.kdecoration2/fakedecoration_with_shadows.json create mode 100644 autotests/integration/generic_scene_opengl_test.cpp create mode 100644 autotests/integration/generic_scene_opengl_test.h create mode 100644 autotests/integration/globalshortcuts_test.cpp create mode 100644 autotests/integration/helper/CMakeLists.txt create mode 100644 autotests/integration/helper/copy.cpp create mode 100644 autotests/integration/helper/kill.cpp create mode 100644 autotests/integration/helper/paste.cpp create mode 100644 autotests/integration/idle_inhibition_test.cpp create mode 100644 autotests/integration/input_stacking_order.cpp create mode 100644 autotests/integration/internal_window.cpp create mode 100644 autotests/integration/keyboard_layout_test.cpp create mode 100644 autotests/integration/keymap_creation_failure_test.cpp create mode 100644 autotests/integration/kwin_wayland_test.cpp create mode 100644 autotests/integration/kwin_wayland_test.h create mode 100644 autotests/integration/kwinbindings_test.cpp create mode 100644 autotests/integration/layershellv1client_test.cpp create mode 100644 autotests/integration/lockscreen.cpp create mode 100644 autotests/integration/maximize_test.cpp create mode 100644 autotests/integration/modifier_only_shortcut_test.cpp create mode 100644 autotests/integration/move_resize_window_test.cpp create mode 100644 autotests/integration/no_global_shortcuts_test.cpp create mode 100644 autotests/integration/no_xdg_runtime_dir_test.cpp create mode 100644 autotests/integration/placement_test.cpp create mode 100644 autotests/integration/plasma_surface_test.cpp create mode 100644 autotests/integration/plasmawindow_test.cpp create mode 100644 autotests/integration/platformcursor.cpp create mode 100644 autotests/integration/pointer_constraints_test.cpp create mode 100644 autotests/integration/pointer_input.cpp create mode 100644 autotests/integration/protocols/wlr-layer-shell-unstable-v1.xml create mode 100644 autotests/integration/quick_tiling_test.cpp create mode 100644 autotests/integration/scene_opengl_es_test.cpp create mode 100644 autotests/integration/scene_opengl_shadow_test.cpp create mode 100644 autotests/integration/scene_opengl_test.cpp create mode 100644 autotests/integration/scene_qpainter_shadow_test.cpp create mode 100644 autotests/integration/scene_qpainter_test.cpp create mode 100644 autotests/integration/screen_changes_test.cpp create mode 100644 autotests/integration/screenedge_client_show_test.cpp create mode 100644 autotests/integration/scripting/CMakeLists.txt create mode 100644 autotests/integration/scripting/minimizeall_test.cpp create mode 100644 autotests/integration/scripting/screenedge_test.cpp create mode 100644 autotests/integration/scripting/scripts/screenedge.js create mode 100644 autotests/integration/scripting/scripts/screenedgetouch.qml create mode 100644 autotests/integration/scripting/scripts/screenedgeunregister.js create mode 100644 autotests/integration/scripting/scripts/touchScreenedge.js create mode 100644 autotests/integration/shade_test.cpp create mode 100644 autotests/integration/showing_desktop_test.cpp create mode 100644 autotests/integration/stacking_order_test.cpp create mode 100644 autotests/integration/struts_test.cpp create mode 100644 autotests/integration/tabbox_test.cpp create mode 100644 autotests/integration/test_helpers.cpp create mode 100644 autotests/integration/touch_input_test.cpp create mode 100644 autotests/integration/transient_placement.cpp create mode 100644 autotests/integration/virtual_desktop_test.cpp create mode 100644 autotests/integration/virtualkeyboard_test.cpp create mode 100644 autotests/integration/window_rules_test.cpp create mode 100644 autotests/integration/window_selection_test.cpp create mode 100644 autotests/integration/x11_client_test.cpp create mode 100644 autotests/integration/xdgshellclient_rules_test.cpp create mode 100644 autotests/integration/xdgshellclient_test.cpp create mode 100644 autotests/integration/xwayland_input_test.cpp create mode 100644 autotests/integration/xwayland_selections_test.cpp create mode 100644 autotests/integration/xwaylandserver_crash_test.cpp create mode 100644 autotests/integration/xwaylandserver_restart_test.cpp create mode 100644 autotests/libinput/CMakeLists.txt create mode 100644 autotests/libinput/context_test.cpp create mode 100644 autotests/libinput/device_test.cpp create mode 100644 autotests/libinput/gesture_event_test.cpp create mode 100644 autotests/libinput/input_event_test.cpp create mode 100644 autotests/libinput/key_event_test.cpp create mode 100644 autotests/libinput/mock_libinput.cpp create mode 100644 autotests/libinput/mock_libinput.h create mode 100644 autotests/libinput/mock_udev.cpp create mode 100644 autotests/libinput/mock_udev.h create mode 100644 autotests/libinput/pointer_event_test.cpp create mode 100644 autotests/libinput/switch_event_test.cpp create mode 100644 autotests/libinput/touch_event_test.cpp create mode 100644 autotests/libkwineffects/CMakeLists.txt create mode 100644 autotests/libkwineffects/data/glplatform/amd-catalyst-radeonhd-7700M-3.1.13399 create mode 100644 autotests/libkwineffects/data/glplatform/amd-gallium-bonaire-3.0 create mode 100644 autotests/libkwineffects/data/glplatform/amd-gallium-cayman-gles-3.0 create mode 100644 autotests/libkwineffects/data/glplatform/amd-gallium-hawaii-3.0 create mode 100644 autotests/libkwineffects/data/glplatform/amd-gallium-navi-4.5 create mode 100644 autotests/libkwineffects/data/glplatform/amd-gallium-radeon-r9-290-4.5 create mode 100644 autotests/libkwineffects/data/glplatform/amd-gallium-radeon-rx-480-series-4.5 create mode 100644 autotests/libkwineffects/data/glplatform/amd-gallium-radeon-rx-550-series-3.1 create mode 100644 autotests/libkwineffects/data/glplatform/amd-gallium-radeon-rx-580-series-4.5 create mode 100644 autotests/libkwineffects/data/glplatform/amd-gallium-radeon-rx-vega-56-4.5 create mode 100644 autotests/libkwineffects/data/glplatform/amd-gallium-radeon-rx-vega-64-4.5 create mode 100644 autotests/libkwineffects/data/glplatform/amd-gallium-redwood-3.0 create mode 100644 autotests/libkwineffects/data/glplatform/amd-gallium-tonga-4.1 create mode 100644 autotests/libkwineffects/data/glplatform/intel-broadwell-gt2-3.3 create mode 100644 autotests/libkwineffects/data/glplatform/intel-haswell-mobile-3.3 create mode 100644 autotests/libkwineffects/data/glplatform/intel-ivybridge-desktop-3.0 create mode 100644 autotests/libkwineffects/data/glplatform/intel-ivybridge-desktop-3.3 create mode 100644 autotests/libkwineffects/data/glplatform/intel-ivybridge-mobile-3.3 create mode 100644 autotests/libkwineffects/data/glplatform/intel-sandybridge-mobile-3.3 create mode 100644 autotests/libkwineffects/data/glplatform/intel-skylake-gt2-3.0 create mode 100644 autotests/libkwineffects/data/glplatform/llvmpipe-10.0 create mode 100644 autotests/libkwineffects/data/glplatform/llvmpipe-3.0 create mode 100644 autotests/libkwineffects/data/glplatform/llvmpipe-5.0 create mode 100644 autotests/libkwineffects/data/glplatform/nvidia-geforce-gtx-560-4.5 create mode 100644 autotests/libkwineffects/data/glplatform/nvidia-geforce-gtx-660-3.1 create mode 100644 autotests/libkwineffects/data/glplatform/nvidia-geforce-gtx-950-4.5 create mode 100644 autotests/libkwineffects/data/glplatform/nvidia-geforce-gtx-970-3.1 create mode 100644 autotests/libkwineffects/data/glplatform/nvidia-geforce-gtx-970M-3.1 create mode 100644 autotests/libkwineffects/data/glplatform/nvidia-geforce-gtx-980-3.1 create mode 100644 autotests/libkwineffects/data/glplatform/qualcomm-adreno-330-libhybris-gles-3.0 create mode 100644 autotests/libkwineffects/data/glplatform/virgl-3.1 create mode 100644 autotests/libkwineffects/kwinglplatformtest.cpp create mode 100644 autotests/libkwineffects/mock_gl.cpp create mode 100644 autotests/libkwineffects/mock_gl.h create mode 100644 autotests/libkwineffects/timelinetest.cpp create mode 100644 autotests/libkwineffects/windowquadlisttest.cpp create mode 100644 autotests/libxrenderutils/CMakeLists.txt create mode 100644 autotests/libxrenderutils/blendpicture_test.cpp create mode 100644 autotests/mock_abstract_client.cpp create mode 100644 autotests/mock_abstract_client.h create mode 100644 autotests/mock_effectshandler.cpp create mode 100644 autotests/mock_effectshandler.h create mode 100644 autotests/mock_screens.cpp create mode 100644 autotests/mock_screens.h create mode 100644 autotests/mock_workspace.cpp create mode 100644 autotests/mock_workspace.h create mode 100644 autotests/mock_x11client.cpp create mode 100644 autotests/mock_x11client.h create mode 100644 autotests/onscreennotificationtest.cpp create mode 100644 autotests/onscreennotificationtest.h create mode 100644 autotests/opengl_context_attribute_builder_test.cpp create mode 100644 autotests/tabbox/CMakeLists.txt create mode 100644 autotests/tabbox/mock_tabboxclient.cpp create mode 100644 autotests/tabbox/mock_tabboxclient.h create mode 100644 autotests/tabbox/mock_tabboxhandler.cpp create mode 100644 autotests/tabbox/mock_tabboxhandler.h create mode 100644 autotests/tabbox/test_desktopchain.cpp create mode 100644 autotests/tabbox/test_tabbox_clientmodel.cpp create mode 100644 autotests/tabbox/test_tabbox_clientmodel.h create mode 100644 autotests/tabbox/test_tabbox_config.cpp create mode 100644 autotests/tabbox/test_tabbox_handler.cpp create mode 100644 autotests/test_builtin_effectloader.cpp create mode 100644 autotests/test_client_machine.cpp create mode 100644 autotests/test_gbm_surface.cpp create mode 100644 autotests/test_gestures.cpp create mode 100644 autotests/test_plugin_effectloader.cpp create mode 100644 autotests/test_screen_edges.cpp create mode 100644 autotests/test_screen_paint_data.cpp create mode 100644 autotests/test_screens.cpp create mode 100644 autotests/test_scripted_effectloader.cpp create mode 100644 autotests/test_virtual_desktops.cpp create mode 100644 autotests/test_virtualkeyboard_dbus.cpp create mode 100644 autotests/test_window_paint_data.cpp create mode 100644 autotests/test_x11_timestamp_update.cpp create mode 100644 autotests/test_xcb_size_hints.cpp create mode 100644 autotests/test_xcb_window.cpp create mode 100644 autotests/test_xcb_wrapper.cpp create mode 100644 autotests/test_xkb.cpp create mode 100644 autotests/testutils.h create mode 100644 autotests/workspace.h create mode 100644 autotests/x11client.h create mode 100644 client_machine.cpp create mode 100644 client_machine.h create mode 100644 cmake/modules/COPYING-CMAKE-SCRIPTS create mode 100644 cmake/modules/FindFontconfig.cmake create mode 100644 cmake/modules/FindLibcap.cmake create mode 100644 cmake/modules/FindLibdrm.cmake create mode 100644 cmake/modules/FindLibinput.cmake create mode 100644 cmake/modules/FindUDev.cmake create mode 100644 cmake/modules/FindXKB.cmake create mode 100644 cmake/modules/FindXwayland.cmake create mode 100644 cmake/modules/Findepoll.cmake create mode 100644 cmake/modules/Findepoxy.cmake create mode 100644 cmake/modules/Findgbm.cmake create mode 100644 cmake/modules/Findhwdata.cmake create mode 100644 cmake/modules/Findlibhybris.cmake create mode 100644 colorcorrection/clockskewnotifier.cpp create mode 100644 colorcorrection/clockskewnotifier.h create mode 100644 colorcorrection/clockskewnotifierengine.cpp create mode 100644 colorcorrection/clockskewnotifierengine_linux.cpp create mode 100644 colorcorrection/clockskewnotifierengine_linux.h create mode 100644 colorcorrection/clockskewnotifierengine_p.h create mode 100644 colorcorrection/colorcorrect_settings.kcfg create mode 100644 colorcorrection/colorcorrect_settings.kcfgc create mode 100644 colorcorrection/colorcorrectdbusinterface.cpp create mode 100644 colorcorrection/colorcorrectdbusinterface.h create mode 100644 colorcorrection/constants.h create mode 100644 colorcorrection/manager.cpp create mode 100644 colorcorrection/manager.h create mode 100644 colorcorrection/suncalc.cpp create mode 100644 colorcorrection/suncalc.h create mode 100644 composite.cpp create mode 100644 composite.h create mode 100644 config-kwin.h.cmake create mode 100644 cursor.cpp create mode 100644 cursor.h create mode 100644 data/CMakeLists.txt create mode 100644 data/icons/16-apps-kwin.png create mode 100644 data/icons/32-apps-kwin.png create mode 100644 data/icons/48-apps-kwin.png create mode 100644 data/icons/CMakeLists.txt create mode 100644 data/icons/sc-apps-kwin.svgz create mode 100644 data/org_kde_kwin.categories create mode 100644 data/update_default_rules.cpp create mode 100644 dbusinterface.cpp create mode 100644 dbusinterface.h create mode 100644 debug_console.cpp create mode 100644 debug_console.h create mode 100644 debug_console.ui create mode 100644 decorations/decoratedclient.cpp create mode 100644 decorations/decoratedclient.h create mode 100644 decorations/decorationbridge.cpp create mode 100644 decorations/decorationbridge.h create mode 100644 decorations/decorationpalette.cpp create mode 100644 decorations/decorationpalette.h create mode 100644 decorations/decorationrenderer.cpp create mode 100644 decorations/decorationrenderer.h create mode 100644 decorations/decorations_logging.cpp create mode 100644 decorations/decorations_logging.h create mode 100644 decorations/settings.cpp create mode 100644 decorations/settings.h create mode 100644 deleted.cpp create mode 100644 deleted.h create mode 100644 dmabuftexture.cpp create mode 100644 dmabuftexture.h create mode 100644 doc/CMakeLists.txt create mode 100644 doc/coding-conventions.md create mode 100644 doc/desktop/CMakeLists.txt create mode 100644 doc/desktop/index.docbook create mode 100644 doc/kwindecoration/CMakeLists.txt create mode 100644 doc/kwindecoration/button.png create mode 100644 doc/kwindecoration/configure.png create mode 100644 doc/kwindecoration/decoration.png create mode 100644 doc/kwindecoration/index.docbook create mode 100644 doc/kwindecoration/main.png create mode 100644 doc/kwineffects/CMakeLists.txt create mode 100644 doc/kwineffects/configure-effects.png create mode 100644 doc/kwineffects/dialog-information.png create mode 100644 doc/kwineffects/index.docbook create mode 100644 doc/kwineffects/video.png create mode 100644 doc/kwinscreenedges/CMakeLists.txt create mode 100644 doc/kwinscreenedges/index.docbook create mode 100644 doc/kwintabbox/CMakeLists.txt create mode 100644 doc/kwintabbox/index.docbook create mode 100644 doc/windowbehaviour/CMakeLists.txt create mode 100644 doc/windowbehaviour/index.docbook create mode 100644 doc/windowspecific/CMakeLists.txt create mode 100644 doc/windowspecific/Face-smile.png create mode 100644 doc/windowspecific/akgregator-info.png create mode 100644 doc/windowspecific/akregator-attributes.png create mode 100644 doc/windowspecific/akregator-fav.png create mode 100644 doc/windowspecific/config-win-behavior.png create mode 100644 doc/windowspecific/emacs-attribute.png create mode 100644 doc/windowspecific/emacs-info.png create mode 100644 doc/windowspecific/focus-stealing-pop2top-attribute.png create mode 100644 doc/windowspecific/index.docbook create mode 100644 doc/windowspecific/knotes-attribute.png create mode 100644 doc/windowspecific/knotes-info.png create mode 100644 doc/windowspecific/kopete-attribute-2.png create mode 100644 doc/windowspecific/kopete-chat-attribute.png create mode 100644 doc/windowspecific/kopete-chat-info.png create mode 100644 doc/windowspecific/kopete-info.png create mode 100644 doc/windowspecific/kwin-detect-window.png create mode 100644 doc/windowspecific/kwin-kopete-rules.png create mode 100644 doc/windowspecific/kwin-rule-editor.png create mode 100644 doc/windowspecific/kwin-rules-main-n-akregator.png create mode 100644 doc/windowspecific/kwin-rules-main.png create mode 100644 doc/windowspecific/kwin-rules-ordering.png create mode 100644 doc/windowspecific/kwin-window-attributes.png create mode 100644 doc/windowspecific/kwin-window-matching.png create mode 100644 doc/windowspecific/pager-4-desktops.png create mode 100644 doc/windowspecific/tbird-compose-attribute.png create mode 100644 doc/windowspecific/tbird-compose-info.png create mode 100644 doc/windowspecific/tbird-main-attribute.png create mode 100644 doc/windowspecific/tbird-main-info.png create mode 100644 doc/windowspecific/tbird-reminder-attribute-2.png create mode 100644 doc/windowspecific/tbird-reminder-info.png create mode 100644 doc/windowspecific/window-matching-emacs.png create mode 100644 doc/windowspecific/window-matching-init.png create mode 100644 doc/windowspecific/window-matching-knotes.png create mode 100644 doc/windowspecific/window-matching-kopete-chat.png create mode 100644 doc/windowspecific/window-matching-kopete.png create mode 100644 doc/windowspecific/window-matching-ready-akregator.png create mode 100644 doc/windowspecific/window-matching-tbird-compose.png create mode 100644 doc/windowspecific/window-matching-tbird-main.png create mode 100644 doc/windowspecific/window-matching-tbird-reminder.png create mode 100644 effectloader.cpp create mode 100644 effectloader.h create mode 100644 effects.cpp create mode 100644 effects.h create mode 100644 effects/CMakeLists.txt create mode 100644 effects/Messages.sh create mode 100644 effects/backgroundcontrast/.directory create mode 100644 effects/backgroundcontrast/CMakeLists.txt create mode 100644 effects/backgroundcontrast/backgroundcontrast.kdev4 create mode 100644 effects/backgroundcontrast/contrast.cpp create mode 100644 effects/backgroundcontrast/contrast.h create mode 100644 effects/backgroundcontrast/contrastshader.cpp create mode 100644 effects/backgroundcontrast/contrastshader.h create mode 100644 effects/blur/CMakeLists.txt create mode 100644 effects/blur/blur.cpp create mode 100644 effects/blur/blur.h create mode 100644 effects/blur/blur.kcfg create mode 100644 effects/blur/blur_config.cpp create mode 100644 effects/blur/blur_config.desktop create mode 100644 effects/blur/blur_config.h create mode 100644 effects/blur/blur_config.ui create mode 100644 effects/blur/blurconfig.kcfgc create mode 100644 effects/blur/blurshader.cpp create mode 100644 effects/blur/blurshader.h create mode 100644 effects/colorpicker/colorpicker.cpp create mode 100644 effects/colorpicker/colorpicker.h create mode 100644 effects/coverswitch/CMakeLists.txt create mode 100644 effects/coverswitch/coverswitch.cpp create mode 100644 effects/coverswitch/coverswitch.h create mode 100644 effects/coverswitch/coverswitch.kcfg create mode 100644 effects/coverswitch/coverswitch_config.cpp create mode 100644 effects/coverswitch/coverswitch_config.desktop create mode 100644 effects/coverswitch/coverswitch_config.h create mode 100644 effects/coverswitch/coverswitch_config.ui create mode 100644 effects/coverswitch/coverswitchconfig.kcfgc create mode 100644 effects/coverswitch/shaders/1.10/coverswitch-reflection.glsl create mode 100644 effects/coverswitch/shaders/1.40/coverswitch-reflection.glsl create mode 100644 effects/cube/CMakeLists.txt create mode 100644 effects/cube/cube.cpp create mode 100644 effects/cube/cube.h create mode 100644 effects/cube/cube.kcfg create mode 100644 effects/cube/cube_config.cpp create mode 100644 effects/cube/cube_config.desktop create mode 100644 effects/cube/cube_config.h create mode 100644 effects/cube/cube_config.ui create mode 100644 effects/cube/cube_inside.h create mode 100644 effects/cube/cube_proxy.cpp create mode 100644 effects/cube/cube_proxy.h create mode 100644 effects/cube/cubeconfig.kcfgc create mode 100644 effects/cube/data/1.10/cube-cap.glsl create mode 100644 effects/cube/data/1.10/cube-reflection.glsl create mode 100644 effects/cube/data/1.10/cylinder.vert create mode 100644 effects/cube/data/1.10/sphere.vert create mode 100644 effects/cube/data/1.40/cube-cap.glsl create mode 100644 effects/cube/data/1.40/cube-reflection.glsl create mode 100644 effects/cube/data/1.40/cylinder.vert create mode 100644 effects/cube/data/1.40/sphere.vert create mode 100644 effects/cube/data/cubecap.png create mode 100644 effects/cubeslide/CMakeLists.txt create mode 100644 effects/cubeslide/cubeslide.cpp create mode 100644 effects/cubeslide/cubeslide.h create mode 100644 effects/cubeslide/cubeslide.kcfg create mode 100644 effects/cubeslide/cubeslide_config.cpp create mode 100644 effects/cubeslide/cubeslide_config.desktop create mode 100644 effects/cubeslide/cubeslide_config.h create mode 100644 effects/cubeslide/cubeslide_config.ui create mode 100644 effects/cubeslide/cubeslideconfig.kcfgc create mode 100644 effects/desktopgrid/CMakeLists.txt create mode 100644 effects/desktopgrid/desktopgrid.cpp create mode 100644 effects/desktopgrid/desktopgrid.h create mode 100644 effects/desktopgrid/desktopgrid.kcfg create mode 100644 effects/desktopgrid/desktopgrid_config.cpp create mode 100644 effects/desktopgrid/desktopgrid_config.desktop create mode 100644 effects/desktopgrid/desktopgrid_config.h create mode 100644 effects/desktopgrid/desktopgrid_config.ui create mode 100644 effects/desktopgrid/desktopgridconfig.kcfgc create mode 100644 effects/desktopgrid/main.qml create mode 100644 effects/dialogparent/package/contents/code/main.js create mode 100644 effects/dialogparent/package/metadata.desktop create mode 100644 effects/diminactive/CMakeLists.txt create mode 100644 effects/diminactive/diminactive.cpp create mode 100644 effects/diminactive/diminactive.h create mode 100644 effects/diminactive/diminactive.kcfg create mode 100644 effects/diminactive/diminactive_config.cpp create mode 100644 effects/diminactive/diminactive_config.desktop create mode 100644 effects/diminactive/diminactive_config.h create mode 100644 effects/diminactive/diminactive_config.ui create mode 100644 effects/diminactive/diminactiveconfig.kcfgc create mode 100644 effects/dimscreen/package/contents/code/main.js create mode 100644 effects/dimscreen/package/metadata.desktop create mode 100644 effects/effect_builtins.cpp create mode 100644 effects/effect_builtins.h create mode 100644 effects/eyeonscreen/package/contents/code/main.js create mode 100644 effects/eyeonscreen/package/metadata.desktop create mode 100644 effects/fade/CMakeLists.txt create mode 100644 effects/fade/package/contents/code/main.js create mode 100644 effects/fade/package/contents/config/main.xml create mode 100644 effects/fade/package/metadata.desktop create mode 100644 effects/fadedesktop/CMakeLists.txt create mode 100644 effects/fadedesktop/package/contents/code/main.js create mode 100644 effects/fadedesktop/package/metadata.desktop create mode 100644 effects/fadingpopups/package/contents/code/main.js create mode 100644 effects/fadingpopups/package/metadata.desktop create mode 100644 effects/fallapart/CMakeLists.txt create mode 100644 effects/fallapart/fallapart.cpp create mode 100644 effects/fallapart/fallapart.h create mode 100644 effects/fallapart/fallapart.kcfg create mode 100644 effects/fallapart/fallapartconfig.kcfgc create mode 100644 effects/flipswitch/CMakeLists.txt create mode 100644 effects/flipswitch/flipswitch.cpp create mode 100644 effects/flipswitch/flipswitch.h create mode 100644 effects/flipswitch/flipswitch.kcfg create mode 100644 effects/flipswitch/flipswitch_config.cpp create mode 100644 effects/flipswitch/flipswitch_config.desktop create mode 100644 effects/flipswitch/flipswitch_config.h create mode 100644 effects/flipswitch/flipswitch_config.ui create mode 100644 effects/flipswitch/flipswitchconfig.kcfgc create mode 100644 effects/frozenapp/package/contents/code/main.js create mode 100644 effects/frozenapp/package/metadata.desktop create mode 100644 effects/fullscreen/package/contents/code/fullscreen.js create mode 100644 effects/fullscreen/package/metadata.desktop create mode 100644 effects/glide/CMakeLists.txt create mode 100644 effects/glide/glide.cpp create mode 100644 effects/glide/glide.h create mode 100644 effects/glide/glide.kcfg create mode 100644 effects/glide/glide_config.cpp create mode 100644 effects/glide/glide_config.desktop create mode 100644 effects/glide/glide_config.h create mode 100644 effects/glide/glide_config.ui create mode 100644 effects/glide/glideconfig.kcfgc create mode 100644 effects/highlightwindow/CMakeLists.txt create mode 100644 effects/highlightwindow/highlightwindow.cpp create mode 100644 effects/highlightwindow/highlightwindow.h create mode 100644 effects/invert/CMakeLists.txt create mode 100644 effects/invert/data/1.10/invert.frag create mode 100644 effects/invert/data/1.40/invert.frag create mode 100644 effects/invert/invert.cpp create mode 100644 effects/invert/invert.h create mode 100644 effects/invert/invert_config.cpp create mode 100644 effects/invert/invert_config.desktop create mode 100644 effects/invert/invert_config.h create mode 100644 effects/kscreen/CMakeLists.txt create mode 100644 effects/kscreen/kscreen.cpp create mode 100644 effects/kscreen/kscreen.h create mode 100644 effects/kscreen/kscreen.kcfg create mode 100644 effects/kscreen/kscreenconfig.kcfgc create mode 100644 effects/kwineffect.desktop create mode 100644 effects/logging.cpp create mode 100644 effects/login/package/contents/code/main.js create mode 100644 effects/login/package/contents/config/main.xml create mode 100644 effects/login/package/contents/ui/config.ui create mode 100644 effects/login/package/metadata.desktop create mode 100644 effects/logout/package/contents/code/main.js create mode 100644 effects/logout/package/metadata.desktop create mode 100644 effects/lookingglass/CMakeLists.txt create mode 100644 effects/lookingglass/data/1.10/lookingglass.frag create mode 100644 effects/lookingglass/data/1.40/lookingglass.frag create mode 100644 effects/lookingglass/lookingglass.cpp create mode 100644 effects/lookingglass/lookingglass.h create mode 100644 effects/lookingglass/lookingglass.kcfg create mode 100644 effects/lookingglass/lookingglass_config.cpp create mode 100644 effects/lookingglass/lookingglass_config.desktop create mode 100644 effects/lookingglass/lookingglass_config.h create mode 100644 effects/lookingglass/lookingglass_config.ui create mode 100644 effects/lookingglass/lookingglassconfig.kcfgc create mode 100644 effects/magiclamp/CMakeLists.txt create mode 100644 effects/magiclamp/magiclamp.cpp create mode 100644 effects/magiclamp/magiclamp.h create mode 100644 effects/magiclamp/magiclamp.kcfg create mode 100644 effects/magiclamp/magiclamp_config.cpp create mode 100644 effects/magiclamp/magiclamp_config.desktop create mode 100644 effects/magiclamp/magiclamp_config.h create mode 100644 effects/magiclamp/magiclamp_config.ui create mode 100644 effects/magiclamp/magiclampconfig.kcfgc create mode 100644 effects/magnifier/CMakeLists.txt create mode 100644 effects/magnifier/magnifier.cpp create mode 100644 effects/magnifier/magnifier.h create mode 100644 effects/magnifier/magnifier.kcfg create mode 100644 effects/magnifier/magnifier_config.cpp create mode 100644 effects/magnifier/magnifier_config.desktop create mode 100644 effects/magnifier/magnifier_config.h create mode 100644 effects/magnifier/magnifier_config.ui create mode 100644 effects/magnifier/magnifierconfig.kcfgc create mode 100644 effects/maximize/package/contents/code/maximize.js create mode 100644 effects/maximize/package/metadata.desktop create mode 100644 effects/morphingpopups/package/contents/code/morphingpopups.js create mode 100644 effects/morphingpopups/package/metadata.desktop create mode 100644 effects/mouseclick/CMakeLists.txt create mode 100644 effects/mouseclick/mouseclick.cpp create mode 100644 effects/mouseclick/mouseclick.h create mode 100644 effects/mouseclick/mouseclick.kcfg create mode 100644 effects/mouseclick/mouseclick_config.cpp create mode 100644 effects/mouseclick/mouseclick_config.desktop create mode 100644 effects/mouseclick/mouseclick_config.h create mode 100644 effects/mouseclick/mouseclick_config.ui create mode 100644 effects/mouseclick/mouseclickconfig.kcfgc create mode 100644 effects/mousemark/CMakeLists.txt create mode 100644 effects/mousemark/mousemark.cpp create mode 100644 effects/mousemark/mousemark.h create mode 100644 effects/mousemark/mousemark.kcfg create mode 100644 effects/mousemark/mousemark_config.cpp create mode 100644 effects/mousemark/mousemark_config.desktop create mode 100644 effects/mousemark/mousemark_config.h create mode 100644 effects/mousemark/mousemark_config.ui create mode 100644 effects/mousemark/mousemarkconfig.kcfgc create mode 100644 effects/presentwindows/CMakeLists.txt create mode 100644 effects/presentwindows/main.qml create mode 100644 effects/presentwindows/presentwindows.cpp create mode 100644 effects/presentwindows/presentwindows.h create mode 100644 effects/presentwindows/presentwindows.kcfg create mode 100644 effects/presentwindows/presentwindows_config.cpp create mode 100644 effects/presentwindows/presentwindows_config.desktop create mode 100644 effects/presentwindows/presentwindows_config.h create mode 100644 effects/presentwindows/presentwindows_config.ui create mode 100644 effects/presentwindows/presentwindows_proxy.cpp create mode 100644 effects/presentwindows/presentwindows_proxy.h create mode 100644 effects/presentwindows/presentwindowsconfig.kcfgc create mode 100644 effects/resize/CMakeLists.txt create mode 100644 effects/resize/resize.cpp create mode 100644 effects/resize/resize.h create mode 100644 effects/resize/resize.kcfg create mode 100644 effects/resize/resize_config.cpp create mode 100644 effects/resize/resize_config.desktop create mode 100644 effects/resize/resize_config.h create mode 100644 effects/resize/resize_config.ui create mode 100644 effects/resize/resizeconfig.kcfgc create mode 100644 effects/scale/package/contents/code/main.js create mode 100644 effects/scale/package/contents/config/main.xml create mode 100644 effects/scale/package/contents/ui/config.ui create mode 100644 effects/scale/package/metadata.desktop create mode 100644 effects/screenedge/CMakeLists.txt create mode 100644 effects/screenedge/screenedgeeffect.cpp create mode 100644 effects/screenedge/screenedgeeffect.h create mode 100644 effects/screenshot/CMakeLists.txt create mode 100644 effects/screenshot/screenshot.cpp create mode 100644 effects/screenshot/screenshot.h create mode 100644 effects/sessionquit/package/contents/code/main.js create mode 100644 effects/sessionquit/package/metadata.desktop create mode 100644 effects/shaders.qrc create mode 100644 effects/sheet/CMakeLists.txt create mode 100644 effects/sheet/sheet.cpp create mode 100644 effects/sheet/sheet.h create mode 100644 effects/sheet/sheet.kcfg create mode 100644 effects/sheet/sheetconfig.kcfgc create mode 100644 effects/showfps/CMakeLists.txt create mode 100644 effects/showfps/showfps.cpp create mode 100644 effects/showfps/showfps.h create mode 100644 effects/showfps/showfps.kcfg create mode 100644 effects/showfps/showfps_config.cpp create mode 100644 effects/showfps/showfps_config.desktop create mode 100644 effects/showfps/showfps_config.h create mode 100644 effects/showfps/showfps_config.ui create mode 100644 effects/showfps/showfpsconfig.kcfgc create mode 100644 effects/showpaint/CMakeLists.txt create mode 100644 effects/showpaint/showpaint.cpp create mode 100644 effects/showpaint/showpaint.h create mode 100644 effects/showpaint/showpaint_config.cpp create mode 100644 effects/showpaint/showpaint_config.desktop create mode 100644 effects/showpaint/showpaint_config.h create mode 100644 effects/showpaint/showpaint_config.ui create mode 100644 effects/slide/CMakeLists.txt create mode 100644 effects/slide/slide.cpp create mode 100644 effects/slide/slide.h create mode 100644 effects/slide/slide.kcfg create mode 100644 effects/slide/slide_config.cpp create mode 100644 effects/slide/slide_config.desktop create mode 100644 effects/slide/slide_config.h create mode 100644 effects/slide/slide_config.ui create mode 100644 effects/slide/slideconfig.kcfgc create mode 100644 effects/slideback/CMakeLists.txt create mode 100644 effects/slideback/slideback.cpp create mode 100644 effects/slideback/slideback.h create mode 100644 effects/slidingpopups/CMakeLists.txt create mode 100644 effects/slidingpopups/slidingpopups.cpp create mode 100644 effects/slidingpopups/slidingpopups.h create mode 100644 effects/slidingpopups/slidingpopups.kcfg create mode 100644 effects/slidingpopups/slidingpopupsconfig.kcfgc create mode 100644 effects/snaphelper/CMakeLists.txt create mode 100644 effects/snaphelper/snaphelper.cpp create mode 100644 effects/snaphelper/snaphelper.h create mode 100644 effects/squash/package/contents/code/main.js create mode 100644 effects/squash/package/metadata.desktop create mode 100644 effects/startupfeedback/CMakeLists.txt create mode 100644 effects/startupfeedback/data/1.10/blinking-startup-fragment.glsl create mode 100644 effects/startupfeedback/data/1.40/blinking-startup-fragment.glsl create mode 100644 effects/startupfeedback/startupfeedback.cpp create mode 100644 effects/startupfeedback/startupfeedback.h create mode 100644 effects/thumbnailaside/CMakeLists.txt create mode 100644 effects/thumbnailaside/thumbnailaside.cpp create mode 100644 effects/thumbnailaside/thumbnailaside.h create mode 100644 effects/thumbnailaside/thumbnailaside.kcfg create mode 100644 effects/thumbnailaside/thumbnailaside_config.cpp create mode 100644 effects/thumbnailaside/thumbnailaside_config.desktop create mode 100644 effects/thumbnailaside/thumbnailaside_config.h create mode 100644 effects/thumbnailaside/thumbnailaside_config.ui create mode 100644 effects/thumbnailaside/thumbnailasideconfig.kcfgc create mode 100644 effects/touchpoints/touchpoints.cpp create mode 100644 effects/touchpoints/touchpoints.h create mode 100644 effects/trackmouse/CMakeLists.txt create mode 100644 effects/trackmouse/data/tm_inner.png create mode 100644 effects/trackmouse/data/tm_outer.png create mode 100644 effects/trackmouse/trackmouse.cpp create mode 100644 effects/trackmouse/trackmouse.h create mode 100644 effects/trackmouse/trackmouse.kcfg create mode 100644 effects/trackmouse/trackmouse_config.cpp create mode 100644 effects/trackmouse/trackmouse_config.desktop create mode 100644 effects/trackmouse/trackmouse_config.h create mode 100644 effects/trackmouse/trackmouse_config.ui create mode 100644 effects/trackmouse/trackmouseconfig.kcfgc create mode 100644 effects/translucency/package/contents/code/main.js create mode 100644 effects/translucency/package/contents/config/main.xml create mode 100644 effects/translucency/package/contents/ui/config.ui create mode 100644 effects/translucency/package/metadata.desktop create mode 100644 effects/windowaperture/package/contents/code/main.js create mode 100644 effects/windowaperture/package/metadata.desktop create mode 100644 effects/windowgeometry/CMakeLists.txt create mode 100644 effects/windowgeometry/windowgeometry.cpp create mode 100644 effects/windowgeometry/windowgeometry.h create mode 100644 effects/windowgeometry/windowgeometry.kcfg create mode 100644 effects/windowgeometry/windowgeometry_config.cpp create mode 100644 effects/windowgeometry/windowgeometry_config.desktop create mode 100644 effects/windowgeometry/windowgeometry_config.h create mode 100644 effects/windowgeometry/windowgeometry_config.ui create mode 100644 effects/windowgeometry/windowgeometryconfig.kcfgc create mode 100644 effects/wobblywindows/CMakeLists.txt create mode 100644 effects/wobblywindows/wobblywindows.cpp create mode 100644 effects/wobblywindows/wobblywindows.h create mode 100644 effects/wobblywindows/wobblywindows.kcfg create mode 100644 effects/wobblywindows/wobblywindows_config.cpp create mode 100644 effects/wobblywindows/wobblywindows_config.desktop create mode 100644 effects/wobblywindows/wobblywindows_config.h create mode 100644 effects/wobblywindows/wobblywindows_config.ui create mode 100644 effects/wobblywindows/wobblywindowsconfig.kcfgc create mode 100644 effects/zoom/CMakeLists.txt create mode 100644 effects/zoom/accessibilityintegration.cpp create mode 100644 effects/zoom/accessibilityintegration.h create mode 100644 effects/zoom/zoom.cpp create mode 100644 effects/zoom/zoom.h create mode 100644 effects/zoom/zoom.kcfg create mode 100644 effects/zoom/zoom_config.cpp create mode 100644 effects/zoom/zoom_config.desktop create mode 100644 effects/zoom/zoom_config.h create mode 100644 effects/zoom/zoom_config.ui create mode 100644 effects/zoom/zoomconfig.kcfgc create mode 100644 egl_context_attribute_builder.cpp create mode 100644 egl_context_attribute_builder.h create mode 100644 events.cpp create mode 100644 fixqopengl.h create mode 100644 focuschain.cpp create mode 100644 focuschain.h create mode 100644 geometrytip.cpp create mode 100644 geometrytip.h create mode 100644 gestures.cpp create mode 100644 gestures.h create mode 100644 globalshortcuts.cpp create mode 100644 globalshortcuts.h create mode 100644 group.cpp create mode 100644 group.h create mode 100644 helpers/CMakeLists.txt create mode 100644 helpers/killer/CMakeLists.txt create mode 100644 helpers/killer/killer.cpp create mode 100644 idle_inhibition.cpp create mode 100644 idle_inhibition.h create mode 100644 input.cpp create mode 100644 input.h create mode 100644 input_event.cpp create mode 100644 input_event.h create mode 100644 input_event_spy.cpp create mode 100644 input_event_spy.h create mode 100644 inputpanelv1client.cpp create mode 100644 inputpanelv1client.h create mode 100644 inputpanelv1integration.cpp create mode 100644 inputpanelv1integration.h create mode 100644 internal_client.cpp create mode 100644 internal_client.h create mode 100644 kcmkwin/CMakeLists.txt create mode 100644 kcmkwin/common/CMakeLists.txt create mode 100644 kcmkwin/common/Messages.sh create mode 100644 kcmkwin/common/effectsmodel.cpp create mode 100644 kcmkwin/common/effectsmodel.h create mode 100644 kcmkwin/kwincompositing/CMakeLists.txt create mode 100644 kcmkwin/kwincompositing/Messages.sh create mode 100644 kcmkwin/kwincompositing/compositing.ui create mode 100644 kcmkwin/kwincompositing/kwincompositing.desktop create mode 100644 kcmkwin/kwincompositing/kwincompositing_setting.kcfg create mode 100644 kcmkwin/kwincompositing/kwincompositing_setting.kcfgc create mode 100644 kcmkwin/kwincompositing/main.cpp create mode 100644 kcmkwin/kwindecoration/CMakeLists.txt create mode 100644 kcmkwin/kwindecoration/Messages.sh create mode 100644 kcmkwin/kwindecoration/declarative-plugin/CMakeLists.txt create mode 100644 kcmkwin/kwindecoration/declarative-plugin/buttonsmodel.cpp create mode 100644 kcmkwin/kwindecoration/declarative-plugin/buttonsmodel.h create mode 100644 kcmkwin/kwindecoration/declarative-plugin/plugin.cpp create mode 100644 kcmkwin/kwindecoration/declarative-plugin/plugin.h create mode 100644 kcmkwin/kwindecoration/declarative-plugin/previewbridge.cpp create mode 100644 kcmkwin/kwindecoration/declarative-plugin/previewbridge.h create mode 100644 kcmkwin/kwindecoration/declarative-plugin/previewbutton.cpp create mode 100644 kcmkwin/kwindecoration/declarative-plugin/previewbutton.h create mode 100644 kcmkwin/kwindecoration/declarative-plugin/previewclient.cpp create mode 100644 kcmkwin/kwindecoration/declarative-plugin/previewclient.h create mode 100644 kcmkwin/kwindecoration/declarative-plugin/previewitem.cpp create mode 100644 kcmkwin/kwindecoration/declarative-plugin/previewitem.h create mode 100644 kcmkwin/kwindecoration/declarative-plugin/previewsettings.cpp create mode 100644 kcmkwin/kwindecoration/declarative-plugin/previewsettings.h create mode 100644 kcmkwin/kwindecoration/declarative-plugin/qmldir create mode 100644 kcmkwin/kwindecoration/decorationmodel.cpp create mode 100644 kcmkwin/kwindecoration/decorationmodel.h create mode 100644 kcmkwin/kwindecoration/kcm.cpp create mode 100644 kcmkwin/kwindecoration/kcm.h create mode 100644 kcmkwin/kwindecoration/kwindecoration.desktop create mode 100644 kcmkwin/kwindecoration/kwindecorationsettings.kcfg create mode 100644 kcmkwin/kwindecoration/kwindecorationsettings.kcfgc create mode 100644 kcmkwin/kwindecoration/package/contents/ui/ButtonGroup.qml create mode 100644 kcmkwin/kwindecoration/package/contents/ui/Buttons.qml create mode 100644 kcmkwin/kwindecoration/package/contents/ui/Themes.qml create mode 100644 kcmkwin/kwindecoration/package/contents/ui/main.qml create mode 100644 kcmkwin/kwindecoration/package/metadata.desktop create mode 100644 kcmkwin/kwindecoration/utils.cpp create mode 100644 kcmkwin/kwindecoration/utils.h create mode 100644 kcmkwin/kwindecoration/window-decorations.knsrc create mode 100644 kcmkwin/kwindesktop/CMakeLists.txt create mode 100644 kcmkwin/kwindesktop/Messages.sh create mode 100644 kcmkwin/kwindesktop/animationsmodel.cpp create mode 100644 kcmkwin/kwindesktop/animationsmodel.h create mode 100644 kcmkwin/kwindesktop/desktopsmodel.cpp create mode 100644 kcmkwin/kwindesktop/desktopsmodel.h create mode 100644 kcmkwin/kwindesktop/kcm_kwin_virtualdesktops.desktop create mode 100644 kcmkwin/kwindesktop/package/contents/ui/main.qml create mode 100644 kcmkwin/kwindesktop/package/metadata.desktop create mode 100644 kcmkwin/kwindesktop/virtualdesktops.cpp create mode 100644 kcmkwin/kwindesktop/virtualdesktops.h create mode 100644 kcmkwin/kwindesktop/virtualdesktopssettings.kcfg create mode 100644 kcmkwin/kwindesktop/virtualdesktopssettings.kcfgc create mode 100644 kcmkwin/kwineffects/CMakeLists.txt create mode 100644 kcmkwin/kwineffects/Messages.sh create mode 100644 kcmkwin/kwineffects/effectsfilterproxymodel.cpp create mode 100644 kcmkwin/kwineffects/effectsfilterproxymodel.h create mode 100644 kcmkwin/kwineffects/kcm.cpp create mode 100644 kcmkwin/kwineffects/kcm.h create mode 100644 kcmkwin/kwineffects/kcm_kwin_effects.desktop create mode 100644 kcmkwin/kwineffects/kwineffect.knsrc create mode 100644 kcmkwin/kwineffects/package/contents/ui/Effect.qml create mode 100644 kcmkwin/kwineffects/package/contents/ui/Video.qml create mode 100644 kcmkwin/kwineffects/package/contents/ui/main.qml create mode 100644 kcmkwin/kwineffects/package/metadata.desktop create mode 100644 kcmkwin/kwinoptions/AUTHORS create mode 100644 kcmkwin/kwinoptions/CMakeLists.txt create mode 100644 kcmkwin/kwinoptions/ChangeLog create mode 100644 kcmkwin/kwinoptions/Messages.sh create mode 100644 kcmkwin/kwinoptions/actions.ui create mode 100644 kcmkwin/kwinoptions/advanced.ui create mode 100644 kcmkwin/kwinoptions/focus.ui create mode 100644 kcmkwin/kwinoptions/kwinactions.desktop create mode 100644 kcmkwin/kwinoptions/kwinadvanced.desktop create mode 100644 kcmkwin/kwinoptions/kwinfocus.desktop create mode 100644 kcmkwin/kwinoptions/kwinmoving.desktop create mode 100644 kcmkwin/kwinoptions/kwinoptions.desktop create mode 100644 kcmkwin/kwinoptions/kwinoptions_kdeglobals_settings.kcfg create mode 100644 kcmkwin/kwinoptions/kwinoptions_kdeglobals_settings.kcfgc create mode 100644 kcmkwin/kwinoptions/kwinoptions_settings.kcfg create mode 100644 kcmkwin/kwinoptions/kwinoptions_settings.kcfgc create mode 100644 kcmkwin/kwinoptions/main.cpp create mode 100644 kcmkwin/kwinoptions/main.h create mode 100644 kcmkwin/kwinoptions/mouse.cpp create mode 100644 kcmkwin/kwinoptions/mouse.h create mode 100644 kcmkwin/kwinoptions/mouse.ui create mode 100644 kcmkwin/kwinoptions/moving.ui create mode 100644 kcmkwin/kwinoptions/windows.cpp create mode 100644 kcmkwin/kwinoptions/windows.h create mode 100644 kcmkwin/kwinrules/CMakeLists.txt create mode 100644 kcmkwin/kwinrules/Messages.sh create mode 100644 kcmkwin/kwinrules/kcm_kwinrules.desktop create mode 100644 kcmkwin/kwinrules/kcmrules.cpp create mode 100644 kcmkwin/kwinrules/kcmrules.h create mode 100644 kcmkwin/kwinrules/kwinsrc.cpp create mode 100644 kcmkwin/kwinrules/main.cpp create mode 100644 kcmkwin/kwinrules/optionsmodel.cpp create mode 100644 kcmkwin/kwinrules/optionsmodel.h create mode 100644 kcmkwin/kwinrules/package/contents/ui/FileDialogLoader.qml create mode 100644 kcmkwin/kwinrules/package/contents/ui/OptionsComboBox.qml create mode 100644 kcmkwin/kwinrules/package/contents/ui/RuleItemDelegate.qml create mode 100644 kcmkwin/kwinrules/package/contents/ui/RulesEditor.qml create mode 100644 kcmkwin/kwinrules/package/contents/ui/RulesList.qml create mode 100644 kcmkwin/kwinrules/package/contents/ui/ValueEditor.qml create mode 100644 kcmkwin/kwinrules/package/metadata.desktop create mode 100644 kcmkwin/kwinrules/rulebookmodel.cpp create mode 100644 kcmkwin/kwinrules/rulebookmodel.h create mode 100644 kcmkwin/kwinrules/ruleitem.cpp create mode 100644 kcmkwin/kwinrules/ruleitem.h create mode 100644 kcmkwin/kwinrules/rulesdialog.cpp create mode 100644 kcmkwin/kwinrules/rulesdialog.h create mode 100644 kcmkwin/kwinrules/rulesmodel.cpp create mode 100644 kcmkwin/kwinrules/rulesmodel.h create mode 100644 kcmkwin/kwinscreenedges/CMakeLists.txt create mode 100644 kcmkwin/kwinscreenedges/Messages.sh create mode 100644 kcmkwin/kwinscreenedges/kwinscreenedge.cpp create mode 100644 kcmkwin/kwinscreenedges/kwinscreenedge.h create mode 100644 kcmkwin/kwinscreenedges/kwinscreenedgeconfigform.cpp create mode 100644 kcmkwin/kwinscreenedges/kwinscreenedgeconfigform.h create mode 100644 kcmkwin/kwinscreenedges/kwinscreenedges.desktop create mode 100644 kcmkwin/kwinscreenedges/kwinscreenedgescriptsettings.kcfg create mode 100644 kcmkwin/kwinscreenedges/kwinscreenedgescriptsettings.kcfgc create mode 100644 kcmkwin/kwinscreenedges/kwinscreenedgesettings.kcfg create mode 100644 kcmkwin/kwinscreenedges/kwinscreenedgesettings.kcfgc create mode 100644 kcmkwin/kwinscreenedges/kwintouchscreen.desktop create mode 100644 kcmkwin/kwinscreenedges/kwintouchscreenedgeconfigform.cpp create mode 100644 kcmkwin/kwinscreenedges/kwintouchscreenedgeconfigform.h create mode 100644 kcmkwin/kwinscreenedges/kwintouchscreenscriptsettings.kcfg create mode 100644 kcmkwin/kwinscreenedges/kwintouchscreenscriptsettings.kcfgc create mode 100644 kcmkwin/kwinscreenedges/kwintouchscreensettings.kcfg create mode 100644 kcmkwin/kwinscreenedges/kwintouchscreensettings.kcfgc create mode 100644 kcmkwin/kwinscreenedges/main.cpp create mode 100644 kcmkwin/kwinscreenedges/main.h create mode 100644 kcmkwin/kwinscreenedges/main.ui create mode 100644 kcmkwin/kwinscreenedges/monitor.cpp create mode 100644 kcmkwin/kwinscreenedges/monitor.h create mode 100644 kcmkwin/kwinscreenedges/screenpreviewwidget.cpp create mode 100644 kcmkwin/kwinscreenedges/screenpreviewwidget.h create mode 100644 kcmkwin/kwinscreenedges/touch.cpp create mode 100644 kcmkwin/kwinscreenedges/touch.h create mode 100644 kcmkwin/kwinscreenedges/touch.ui create mode 100644 kcmkwin/kwinscripts/CMakeLists.txt create mode 100755 kcmkwin/kwinscripts/Messages.sh create mode 100644 kcmkwin/kwinscripts/kwinscripts.desktop create mode 100644 kcmkwin/kwinscripts/kwinscripts.knsrc create mode 100644 kcmkwin/kwinscripts/main.cpp create mode 100644 kcmkwin/kwinscripts/module.cpp create mode 100644 kcmkwin/kwinscripts/module.h create mode 100644 kcmkwin/kwinscripts/module.ui create mode 100644 kcmkwin/kwinscripts/version.h.cmake create mode 100644 kcmkwin/kwintabbox/CMakeLists.txt create mode 100644 kcmkwin/kwintabbox/Messages.sh create mode 100644 kcmkwin/kwintabbox/kwinpluginssettings.kcfg create mode 100644 kcmkwin/kwintabbox/kwinpluginssettings.kcfgc create mode 100644 kcmkwin/kwintabbox/kwinswitcheffectsettings.kcfg create mode 100644 kcmkwin/kwintabbox/kwinswitcheffectsettings.kcfgc create mode 100644 kcmkwin/kwintabbox/kwinswitcher.knsrc create mode 100644 kcmkwin/kwintabbox/kwintabbox.desktop create mode 100644 kcmkwin/kwintabbox/kwintabboxconfigform.cpp create mode 100644 kcmkwin/kwintabbox/kwintabboxconfigform.h create mode 100644 kcmkwin/kwintabbox/kwintabboxsettings.kcfg create mode 100644 kcmkwin/kwintabbox/kwintabboxsettings.kcfgc create mode 100644 kcmkwin/kwintabbox/layoutpreview.cpp create mode 100644 kcmkwin/kwintabbox/layoutpreview.h create mode 100644 kcmkwin/kwintabbox/main.cpp create mode 100644 kcmkwin/kwintabbox/main.h create mode 100644 kcmkwin/kwintabbox/main.ui create mode 100644 kcmkwin/kwintabbox/thumbnailitem.cpp create mode 100644 kcmkwin/kwintabbox/thumbnailitem.h create mode 100644 kcmkwin/kwintabbox/thumbnails/dolphin.png create mode 100644 kcmkwin/kwintabbox/thumbnails/kmail.png create mode 100644 kcmkwin/kwintabbox/thumbnails/konqueror.png create mode 100644 kcmkwin/kwintabbox/thumbnails/systemsettings.png create mode 100644 kconf_update/CMakeLists.txt create mode 100644 kconf_update/kwin-5.16-auto-bordersize.sh create mode 100644 kconf_update/kwin-5.18-move-animspeed.py create mode 100644 kconf_update/kwin.upd create mode 100644 kconf_update/kwinrules-5.19-placement.pl create mode 100644 kconf_update/kwinrules.upd create mode 100644 keyboard_input.cpp create mode 100644 keyboard_input.h create mode 100644 keyboard_layout.cpp create mode 100644 keyboard_layout.h create mode 100644 keyboard_layout_switching.cpp create mode 100644 keyboard_layout_switching.h create mode 100644 keyboard_repeat.cpp create mode 100644 keyboard_repeat.h create mode 100644 killwindow.cpp create mode 100644 killwindow.h create mode 100644 kwin.kcfg create mode 100644 kwin.notifyrc create mode 100644 kwinbindings.cpp create mode 100644 layers.cpp create mode 100644 layershellv1client.cpp create mode 100644 layershellv1client.h create mode 100644 layershellv1integration.cpp create mode 100644 layershellv1integration.h create mode 100644 libinput/connection.cpp create mode 100644 libinput/connection.h create mode 100644 libinput/context.cpp create mode 100644 libinput/context.h create mode 100644 libinput/device.cpp create mode 100644 libinput/device.h create mode 100644 libinput/events.cpp create mode 100644 libinput/events.h create mode 100644 libinput/libinput_logging.cpp create mode 100644 libinput/libinput_logging.h create mode 100644 libkwineffects/CMakeLists.txt create mode 100644 libkwineffects/Mainpage.dox create mode 100644 libkwineffects/Messages.sh create mode 100644 libkwineffects/anidata.cpp create mode 100644 libkwineffects/anidata_p.h create mode 100644 libkwineffects/kwinanimationeffect.cpp create mode 100644 libkwineffects/kwinanimationeffect.h create mode 100644 libkwineffects/kwinconfig.h.cmake create mode 100644 libkwineffects/kwineffectquickview.cpp create mode 100644 libkwineffects/kwineffectquickview.h create mode 100644 libkwineffects/kwineffects.cpp create mode 100644 libkwineffects/kwineffects.h create mode 100644 libkwineffects/kwineglimagetexture.cpp create mode 100644 libkwineffects/kwineglimagetexture.h create mode 100644 libkwineffects/kwinglobals.h create mode 100644 libkwineffects/kwinglplatform.cpp create mode 100644 libkwineffects/kwinglplatform.h create mode 100644 libkwineffects/kwingltexture.cpp create mode 100644 libkwineffects/kwingltexture.h create mode 100644 libkwineffects/kwingltexture_p.h create mode 100644 libkwineffects/kwinglutils.cpp create mode 100644 libkwineffects/kwinglutils.h create mode 100644 libkwineffects/kwinglutils_funcs.cpp create mode 100644 libkwineffects/kwinglutils_funcs.h create mode 100644 libkwineffects/kwinxrenderutils.cpp create mode 100644 libkwineffects/kwinxrenderutils.h create mode 100644 libkwineffects/logging.cpp create mode 100644 libkwineffects/logging_p.h create mode 100644 linux_dmabuf.cpp create mode 100644 linux_dmabuf.h create mode 100644 logind.cpp create mode 100644 logind.h create mode 100644 logo.png create mode 100644 main.cpp create mode 100644 main.h create mode 100644 main_wayland.cpp create mode 100644 main_wayland.h create mode 100644 main_x11.cpp create mode 100644 main_x11.h create mode 100644 modifier_only_shortcuts.cpp create mode 100644 modifier_only_shortcuts.h create mode 100644 moving_client_x11_filter.cpp create mode 100644 moving_client_x11_filter.h create mode 100644 netinfo.cpp create mode 100644 netinfo.h create mode 100644 onscreennotification.cpp create mode 100644 onscreennotification.h create mode 100644 options.cpp create mode 100644 options.h create mode 100644 org.kde.KWin.Session.xml create mode 100644 org.kde.KWin.VirtualDesktopManager.xml create mode 100644 org.kde.KWin.xml create mode 100644 org.kde.kappmenu.xml create mode 100644 org.kde.kwin.ColorCorrect.xml create mode 100644 org.kde.kwin.Compositing.xml create mode 100644 org.kde.kwin.Effects.xml create mode 100644 osd.cpp create mode 100644 osd.h create mode 100644 outline.cpp create mode 100644 outline.h create mode 100644 outputscreens.cpp create mode 100644 outputscreens.h create mode 100644 overlaywindow.cpp create mode 100644 overlaywindow.h create mode 100644 placement.cpp create mode 100644 placement.h create mode 100644 plasma-kwin_wayland.service.in create mode 100644 plasma-kwin_x11.service.in create mode 100644 platform.cpp create mode 100644 platform.h create mode 100644 platformsupport/CMakeLists.txt create mode 100644 platformsupport/scenes/CMakeLists.txt create mode 100644 platformsupport/scenes/opengl/CMakeLists.txt create mode 100644 platformsupport/scenes/opengl/abstract_egl_backend.cpp create mode 100644 platformsupport/scenes/opengl/abstract_egl_backend.h create mode 100644 platformsupport/scenes/opengl/backend.cpp create mode 100644 platformsupport/scenes/opengl/backend.h create mode 100644 platformsupport/scenes/opengl/drm_fourcc.h create mode 100644 platformsupport/scenes/opengl/egl_dmabuf.cpp create mode 100644 platformsupport/scenes/opengl/egl_dmabuf.h create mode 100644 platformsupport/scenes/opengl/kwineglext.h create mode 100644 platformsupport/scenes/opengl/swap_profiler.cpp create mode 100644 platformsupport/scenes/opengl/swap_profiler.h create mode 100644 platformsupport/scenes/opengl/texture.cpp create mode 100644 platformsupport/scenes/opengl/texture.h create mode 100644 platformsupport/scenes/qpainter/CMakeLists.txt create mode 100644 platformsupport/scenes/qpainter/backend.cpp create mode 100644 platformsupport/scenes/qpainter/backend.h create mode 100644 plugins/CMakeLists.txt create mode 100644 plugins/idletime/CMakeLists.txt create mode 100644 plugins/idletime/kwin.json create mode 100644 plugins/idletime/poller.cpp create mode 100644 plugins/idletime/poller.h create mode 100644 plugins/kdecorations/CMakeLists.txt create mode 100644 plugins/kdecorations/Messages.sh create mode 100644 plugins/kdecorations/aurorae/AUTHORS create mode 100644 plugins/kdecorations/aurorae/CMakeLists.txt create mode 100644 plugins/kdecorations/aurorae/README create mode 100644 plugins/kdecorations/aurorae/TODO create mode 100644 plugins/kdecorations/aurorae/src/CMakeLists.txt create mode 100644 plugins/kdecorations/aurorae/src/aurorae.cpp create mode 100644 plugins/kdecorations/aurorae/src/aurorae.h create mode 100644 plugins/kdecorations/aurorae/src/aurorae.json create mode 100644 plugins/kdecorations/aurorae/src/aurorae.knsrc create mode 100644 plugins/kdecorations/aurorae/src/colorhelper.cpp create mode 100644 plugins/kdecorations/aurorae/src/colorhelper.h create mode 100644 plugins/kdecorations/aurorae/src/decorationoptions.cpp create mode 100644 plugins/kdecorations/aurorae/src/decorationoptions.h create mode 100644 plugins/kdecorations/aurorae/src/decorationplugin.cpp create mode 100644 plugins/kdecorations/aurorae/src/decorationplugin.h create mode 100644 plugins/kdecorations/aurorae/src/kwindecoration.desktop create mode 100644 plugins/kdecorations/aurorae/src/lib/auroraetheme.cpp create mode 100644 plugins/kdecorations/aurorae/src/lib/auroraetheme.h create mode 100644 plugins/kdecorations/aurorae/src/lib/themeconfig.cpp create mode 100644 plugins/kdecorations/aurorae/src/lib/themeconfig.h create mode 100644 plugins/kdecorations/aurorae/src/qml/AppMenuButton.qml create mode 100644 plugins/kdecorations/aurorae/src/qml/AuroraeButton.qml create mode 100644 plugins/kdecorations/aurorae/src/qml/AuroraeButtonGroup.qml create mode 100644 plugins/kdecorations/aurorae/src/qml/AuroraeMaximizeButton.qml create mode 100644 plugins/kdecorations/aurorae/src/qml/ButtonGroup.qml create mode 100644 plugins/kdecorations/aurorae/src/qml/Decoration.qml create mode 100644 plugins/kdecorations/aurorae/src/qml/DecorationButton.qml create mode 100644 plugins/kdecorations/aurorae/src/qml/MenuButton.qml create mode 100644 plugins/kdecorations/aurorae/src/qml/aurorae.qml create mode 100644 plugins/kdecorations/aurorae/src/qml/qmldir create mode 100644 plugins/kdecorations/aurorae/theme-description create mode 100644 plugins/kdecorations/aurorae/themes/CMakeLists.txt create mode 100644 plugins/kdecorations/aurorae/themes/plastik/CMakeLists.txt create mode 100644 plugins/kdecorations/aurorae/themes/plastik/code/CMakeLists.txt create mode 100644 plugins/kdecorations/aurorae/themes/plastik/code/plastikbutton.cpp create mode 100644 plugins/kdecorations/aurorae/themes/plastik/code/plastikbutton.h create mode 100644 plugins/kdecorations/aurorae/themes/plastik/code/plastikplugin.cpp create mode 100644 plugins/kdecorations/aurorae/themes/plastik/code/plastikplugin.h create mode 100644 plugins/kdecorations/aurorae/themes/plastik/code/qmldir create mode 100644 plugins/kdecorations/aurorae/themes/plastik/package/contents/config/main.xml create mode 100644 plugins/kdecorations/aurorae/themes/plastik/package/contents/ui/PlastikButton.qml create mode 100644 plugins/kdecorations/aurorae/themes/plastik/package/contents/ui/config.ui create mode 100644 plugins/kdecorations/aurorae/themes/plastik/package/contents/ui/main.qml create mode 100644 plugins/kdecorations/aurorae/themes/plastik/package/metadata.desktop create mode 100644 plugins/kglobalaccel/CMakeLists.txt create mode 100644 plugins/kglobalaccel/kglobalaccel_plugin.cpp create mode 100644 plugins/kglobalaccel/kglobalaccel_plugin.h create mode 100644 plugins/kglobalaccel/kwin.json create mode 100644 plugins/kpackage/CMakeLists.txt create mode 100644 plugins/kpackage/aurorae/CMakeLists.txt create mode 100644 plugins/kpackage/aurorae/aurorae.cpp create mode 100644 plugins/kpackage/aurorae/aurorae.h create mode 100644 plugins/kpackage/aurorae/kwin-packagestructure-aurorae.desktop create mode 100644 plugins/kpackage/decoration/CMakeLists.txt create mode 100644 plugins/kpackage/decoration/decoration.cpp create mode 100644 plugins/kpackage/decoration/decoration.h create mode 100644 plugins/kpackage/decoration/kwin-packagestructure-decoration.desktop create mode 100644 plugins/kpackage/effect/CMakeLists.txt create mode 100644 plugins/kpackage/effect/effect.cpp create mode 100644 plugins/kpackage/effect/effect.h create mode 100644 plugins/kpackage/effect/kwin-packagestructure-effect.desktop create mode 100644 plugins/kpackage/scripts/CMakeLists.txt create mode 100644 plugins/kpackage/scripts/kwin-packagestructure-scripts.desktop create mode 100644 plugins/kpackage/scripts/scripts.cpp create mode 100644 plugins/kpackage/scripts/scripts.h create mode 100644 plugins/kpackage/windowswitcher/CMakeLists.txt create mode 100644 plugins/kpackage/windowswitcher/kwin-packagestructure-windowswitcher.desktop create mode 100644 plugins/kpackage/windowswitcher/windowswitcher.cpp create mode 100644 plugins/kpackage/windowswitcher/windowswitcher.h create mode 100644 plugins/platforms/CMakeLists.txt create mode 100644 plugins/platforms/drm/CMakeLists.txt create mode 100644 plugins/platforms/drm/drm.json create mode 100644 plugins/platforms/drm/drm_backend.cpp create mode 100644 plugins/platforms/drm/drm_backend.h create mode 100644 plugins/platforms/drm/drm_buffer.cpp create mode 100644 plugins/platforms/drm/drm_buffer.h create mode 100644 plugins/platforms/drm/drm_buffer_gbm.cpp create mode 100644 plugins/platforms/drm/drm_buffer_gbm.h create mode 100644 plugins/platforms/drm/drm_inputeventfilter.cpp create mode 100644 plugins/platforms/drm/drm_inputeventfilter.h create mode 100644 plugins/platforms/drm/drm_object.cpp create mode 100644 plugins/platforms/drm/drm_object.h create mode 100644 plugins/platforms/drm/drm_object_connector.cpp create mode 100644 plugins/platforms/drm/drm_object_connector.h create mode 100644 plugins/platforms/drm/drm_object_crtc.cpp create mode 100644 plugins/platforms/drm/drm_object_crtc.h create mode 100644 plugins/platforms/drm/drm_object_plane.cpp create mode 100644 plugins/platforms/drm/drm_object_plane.h create mode 100644 plugins/platforms/drm/drm_output.cpp create mode 100644 plugins/platforms/drm/drm_output.h create mode 100644 plugins/platforms/drm/drm_pointer.h create mode 100644 plugins/platforms/drm/edid.cpp create mode 100644 plugins/platforms/drm/edid.h create mode 100644 plugins/platforms/drm/egl_gbm_backend.cpp create mode 100644 plugins/platforms/drm/egl_gbm_backend.h create mode 100644 plugins/platforms/drm/egl_stream_backend.cpp create mode 100644 plugins/platforms/drm/egl_stream_backend.h create mode 100644 plugins/platforms/drm/gbm_dmabuf.cpp create mode 100644 plugins/platforms/drm/gbm_dmabuf.h create mode 100644 plugins/platforms/drm/gbm_surface.cpp create mode 100644 plugins/platforms/drm/gbm_surface.h create mode 100644 plugins/platforms/drm/logging.cpp create mode 100644 plugins/platforms/drm/logging.h create mode 100644 plugins/platforms/drm/scene_qpainter_drm_backend.cpp create mode 100644 plugins/platforms/drm/scene_qpainter_drm_backend.h create mode 100644 plugins/platforms/drm/screens_drm.cpp create mode 100644 plugins/platforms/drm/screens_drm.h create mode 100644 plugins/platforms/fbdev/CMakeLists.txt create mode 100644 plugins/platforms/fbdev/fb_backend.cpp create mode 100644 plugins/platforms/fbdev/fb_backend.h create mode 100644 plugins/platforms/fbdev/fbdev.json create mode 100644 plugins/platforms/fbdev/logging.cpp create mode 100644 plugins/platforms/fbdev/logging.h create mode 100644 plugins/platforms/fbdev/scene_qpainter_fb_backend.cpp create mode 100644 plugins/platforms/fbdev/scene_qpainter_fb_backend.h create mode 100644 plugins/platforms/hwcomposer/CMakeLists.txt create mode 100644 plugins/platforms/hwcomposer/egl_hwcomposer_backend.cpp create mode 100644 plugins/platforms/hwcomposer/egl_hwcomposer_backend.h create mode 100644 plugins/platforms/hwcomposer/hwcomposer.json create mode 100644 plugins/platforms/hwcomposer/hwcomposer_backend.cpp create mode 100644 plugins/platforms/hwcomposer/hwcomposer_backend.h create mode 100644 plugins/platforms/hwcomposer/logging.cpp create mode 100644 plugins/platforms/hwcomposer/logging.h create mode 100644 plugins/platforms/hwcomposer/screens_hwcomposer.cpp create mode 100644 plugins/platforms/hwcomposer/screens_hwcomposer.h create mode 100644 plugins/platforms/virtual/CMakeLists.txt create mode 100644 plugins/platforms/virtual/egl_gbm_backend.cpp create mode 100644 plugins/platforms/virtual/egl_gbm_backend.h create mode 100644 plugins/platforms/virtual/scene_qpainter_virtual_backend.cpp create mode 100644 plugins/platforms/virtual/scene_qpainter_virtual_backend.h create mode 100644 plugins/platforms/virtual/screens_virtual.cpp create mode 100644 plugins/platforms/virtual/screens_virtual.h create mode 100644 plugins/platforms/virtual/virtual.json create mode 100644 plugins/platforms/virtual/virtual_backend.cpp create mode 100644 plugins/platforms/virtual/virtual_backend.h create mode 100644 plugins/platforms/virtual/virtual_output.cpp create mode 100644 plugins/platforms/virtual/virtual_output.h create mode 100644 plugins/platforms/wayland/CMakeLists.txt create mode 100644 plugins/platforms/wayland/egl_wayland_backend.cpp create mode 100644 plugins/platforms/wayland/egl_wayland_backend.h create mode 100644 plugins/platforms/wayland/logging.cpp create mode 100644 plugins/platforms/wayland/logging.h create mode 100644 plugins/platforms/wayland/scene_qpainter_wayland_backend.cpp create mode 100644 plugins/platforms/wayland/scene_qpainter_wayland_backend.h create mode 100644 plugins/platforms/wayland/wayland.json create mode 100644 plugins/platforms/wayland/wayland_backend.cpp create mode 100644 plugins/platforms/wayland/wayland_backend.h create mode 100644 plugins/platforms/wayland/wayland_output.cpp create mode 100644 plugins/platforms/wayland/wayland_output.h create mode 100644 plugins/platforms/x11/CMakeLists.txt create mode 100644 plugins/platforms/x11/common/CMakeLists.txt create mode 100644 plugins/platforms/x11/common/eglonxbackend.cpp create mode 100644 plugins/platforms/x11/common/eglonxbackend.h create mode 100644 plugins/platforms/x11/common/ge_event_mem_mover.h create mode 100644 plugins/platforms/x11/standalone/CMakeLists.txt create mode 100644 plugins/platforms/x11/standalone/edge.cpp create mode 100644 plugins/platforms/x11/standalone/edge.h create mode 100644 plugins/platforms/x11/standalone/effects_mouse_interception_x11_filter.cpp create mode 100644 plugins/platforms/x11/standalone/effects_mouse_interception_x11_filter.h create mode 100644 plugins/platforms/x11/standalone/effects_x11.cpp create mode 100644 plugins/platforms/x11/standalone/effects_x11.h create mode 100644 plugins/platforms/x11/standalone/glx_context_attribute_builder.cpp create mode 100644 plugins/platforms/x11/standalone/glx_context_attribute_builder.h create mode 100644 plugins/platforms/x11/standalone/glxbackend.cpp create mode 100644 plugins/platforms/x11/standalone/glxbackend.h create mode 100644 plugins/platforms/x11/standalone/logging.cpp create mode 100644 plugins/platforms/x11/standalone/logging.h create mode 100644 plugins/platforms/x11/standalone/non_composited_outline.cpp create mode 100644 plugins/platforms/x11/standalone/non_composited_outline.h create mode 100644 plugins/platforms/x11/standalone/overlaywindow_x11.cpp create mode 100644 plugins/platforms/x11/standalone/overlaywindow_x11.h create mode 100644 plugins/platforms/x11/standalone/screenedges_filter.cpp create mode 100644 plugins/platforms/x11/standalone/screenedges_filter.h create mode 100644 plugins/platforms/x11/standalone/screens_xrandr.cpp create mode 100644 plugins/platforms/x11/standalone/screens_xrandr.h create mode 100644 plugins/platforms/x11/standalone/windowselector.cpp create mode 100644 plugins/platforms/x11/standalone/windowselector.h create mode 100644 plugins/platforms/x11/standalone/x11.json create mode 100644 plugins/platforms/x11/standalone/x11_decoration_renderer.cpp create mode 100644 plugins/platforms/x11/standalone/x11_decoration_renderer.h create mode 100644 plugins/platforms/x11/standalone/x11_output.cpp create mode 100644 plugins/platforms/x11/standalone/x11_output.h create mode 100644 plugins/platforms/x11/standalone/x11_platform.cpp create mode 100644 plugins/platforms/x11/standalone/x11_platform.h create mode 100644 plugins/platforms/x11/standalone/x11cursor.cpp create mode 100644 plugins/platforms/x11/standalone/x11cursor.h create mode 100644 plugins/platforms/x11/standalone/xfixes_cursor_event_filter.cpp create mode 100644 plugins/platforms/x11/standalone/xfixes_cursor_event_filter.h create mode 100644 plugins/platforms/x11/standalone/xinputintegration.cpp create mode 100644 plugins/platforms/x11/standalone/xinputintegration.h create mode 100644 plugins/platforms/x11/windowed/CMakeLists.txt create mode 100644 plugins/platforms/x11/windowed/egl_x11_backend.cpp create mode 100644 plugins/platforms/x11/windowed/egl_x11_backend.h create mode 100644 plugins/platforms/x11/windowed/logging.cpp create mode 100644 plugins/platforms/x11/windowed/logging.h create mode 100644 plugins/platforms/x11/windowed/scene_qpainter_x11_backend.cpp create mode 100644 plugins/platforms/x11/windowed/scene_qpainter_x11_backend.h create mode 100644 plugins/platforms/x11/windowed/x11.json create mode 100644 plugins/platforms/x11/windowed/x11windowed_backend.cpp create mode 100644 plugins/platforms/x11/windowed/x11windowed_backend.h create mode 100644 plugins/platforms/x11/windowed/x11windowed_output.cpp create mode 100644 plugins/platforms/x11/windowed/x11windowed_output.h create mode 100644 plugins/qpa/CMakeLists.txt create mode 100644 plugins/qpa/backingstore.cpp create mode 100644 plugins/qpa/backingstore.h create mode 100644 plugins/qpa/eglhelpers.cpp create mode 100644 plugins/qpa/eglhelpers.h create mode 100644 plugins/qpa/eglplatformcontext.cpp create mode 100644 plugins/qpa/eglplatformcontext.h create mode 100644 plugins/qpa/integration.cpp create mode 100644 plugins/qpa/integration.h create mode 100644 plugins/qpa/kwin.json create mode 100644 plugins/qpa/main.cpp create mode 100644 plugins/qpa/offscreensurface.cpp create mode 100644 plugins/qpa/offscreensurface.h create mode 100644 plugins/qpa/platformcursor.cpp create mode 100644 plugins/qpa/platformcursor.h create mode 100644 plugins/qpa/screen.cpp create mode 100644 plugins/qpa/screen.h create mode 100644 plugins/qpa/window.cpp create mode 100644 plugins/qpa/window.h create mode 100644 plugins/scenes/CMakeLists.txt create mode 100644 plugins/scenes/opengl/CMakeLists.txt create mode 100644 plugins/scenes/opengl/lanczosfilter.cpp create mode 100644 plugins/scenes/opengl/lanczosfilter.h create mode 100644 plugins/scenes/opengl/opengl.json create mode 100644 plugins/scenes/opengl/resources.qrc create mode 100644 plugins/scenes/opengl/scene_opengl.cpp create mode 100644 plugins/scenes/opengl/scene_opengl.h create mode 100644 plugins/scenes/opengl/shaders/1.10/lanczos-fragment.glsl create mode 100644 plugins/scenes/opengl/shaders/1.40/lanczos-fragment.glsl create mode 100644 plugins/scenes/qpainter/CMakeLists.txt create mode 100644 plugins/scenes/qpainter/qpainter.json create mode 100644 plugins/scenes/qpainter/scene_qpainter.cpp create mode 100644 plugins/scenes/qpainter/scene_qpainter.h create mode 100644 plugins/scenes/xrender/CMakeLists.txt create mode 100644 plugins/scenes/xrender/scene_xrender.cpp create mode 100644 plugins/scenes/xrender/scene_xrender.h create mode 100644 plugins/scenes/xrender/xrender.json create mode 100644 plugins/windowsystem/CMakeLists.txt create mode 100644 plugins/windowsystem/kwindowsystem.json create mode 100644 plugins/windowsystem/plugin.cpp create mode 100644 plugins/windowsystem/plugin.h create mode 100644 plugins/windowsystem/windoweffects.cpp create mode 100644 plugins/windowsystem/windoweffects.h create mode 100644 plugins/windowsystem/windowshadow.cpp create mode 100644 plugins/windowsystem/windowshadow.h create mode 100644 plugins/windowsystem/windowsystem.cpp create mode 100644 plugins/windowsystem/windowsystem.h create mode 100644 po/af/kcm_kwindecoration.po create mode 100644 po/af/kcm_kwinrules.po create mode 100644 po/af/kcmkwm.po create mode 100644 po/af/kwin.po create mode 100644 po/af/kwin_clients.po create mode 100644 po/ar/kcm-kwin-scripts.po create mode 100644 po/ar/kcm_kwin_virtualdesktops.po create mode 100644 po/ar/kcm_kwindecoration.po create mode 100644 po/ar/kcm_kwinrules.po create mode 100644 po/ar/kcm_kwintabbox.po create mode 100644 po/ar/kcmkwincompositing.po create mode 100644 po/ar/kcmkwinscreenedges.po create mode 100644 po/ar/kcmkwm.po create mode 100644 po/ar/kwin.po create mode 100644 po/ar/kwin_clients.po create mode 100644 po/ar/kwin_effects.po create mode 100644 po/ar/kwin_scripting.po create mode 100644 po/as/kwin.po create mode 100644 po/ast/kcm-kwin-scripts.po create mode 100644 po/ast/kcm_kwin_effects.po create mode 100644 po/ast/kcm_kwin_virtualdesktops.po create mode 100644 po/ast/kcmkwincompositing.po create mode 100644 po/ast/kwin_effects.po create mode 100644 po/az/kcm-kwin-scripts.po create mode 100644 po/az/kcm_kwin_effects.po create mode 100644 po/az/kcm_kwin_virtualdesktops.po create mode 100644 po/az/kcm_kwindecoration.po create mode 100644 po/az/kcm_kwinrules.po create mode 100644 po/az/kcm_kwintabbox.po create mode 100644 po/az/kcmkwincommon.po create mode 100644 po/az/kcmkwincompositing.po create mode 100644 po/az/kcmkwinscreenedges.po create mode 100644 po/az/kcmkwm.po create mode 100644 po/az/kwin.po create mode 100644 po/az/kwin_clients.po create mode 100644 po/az/kwin_effects.po create mode 100644 po/az/kwin_scripting.po create mode 100644 po/az/kwin_scripts.po create mode 100644 po/be/kcm_kwin_virtualdesktops.po create mode 100644 po/be/kcm_kwindecoration.po create mode 100644 po/be/kcm_kwinrules.po create mode 100644 po/be/kcmkwincompositing.po create mode 100644 po/be/kcmkwm.po create mode 100644 po/be/kwin.po create mode 100644 po/be/kwin_clients.po create mode 100644 po/be/kwin_effects.po create mode 100644 po/be@latin/kwin.po create mode 100644 po/bg/kcm_kwin_virtualdesktops.po create mode 100644 po/bg/kcm_kwindecoration.po create mode 100644 po/bg/kcm_kwinrules.po create mode 100644 po/bg/kcm_kwintabbox.po create mode 100644 po/bg/kcmkwincompositing.po create mode 100644 po/bg/kcmkwinscreenedges.po create mode 100644 po/bg/kcmkwm.po create mode 100644 po/bg/kwin.po create mode 100644 po/bg/kwin_clients.po create mode 100644 po/bg/kwin_effects.po create mode 100644 po/bn/kcmkwm.po create mode 100644 po/bn/kwin.po create mode 100644 po/bn/kwin_effects.po create mode 100644 po/bn_IN/kcm_kwin_virtualdesktops.po create mode 100644 po/bn_IN/kcm_kwindecoration.po create mode 100644 po/bn_IN/kcm_kwinrules.po create mode 100644 po/bn_IN/kcmkwm.po create mode 100644 po/bn_IN/kwin.po create mode 100644 po/bn_IN/kwin_clients.po create mode 100644 po/bn_IN/kwin_effects.po create mode 100644 po/br/kcm_kwindecoration.po create mode 100644 po/br/kcm_kwinrules.po create mode 100644 po/br/kcmkwm.po create mode 100644 po/br/kwin.po create mode 100644 po/br/kwin_clients.po create mode 100644 po/bs/kcm-kwin-scripts.po create mode 100644 po/bs/kcm_kwin_virtualdesktops.po create mode 100644 po/bs/kcm_kwindecoration.po create mode 100644 po/bs/kcm_kwinrules.po create mode 100644 po/bs/kcm_kwintabbox.po create mode 100644 po/bs/kcmkwincompositing.po create mode 100644 po/bs/kcmkwinscreenedges.po create mode 100644 po/bs/kcmkwm.po create mode 100644 po/bs/kwin.po create mode 100644 po/bs/kwin_clients.po create mode 100644 po/bs/kwin_effects.po create mode 100644 po/bs/kwin_scripting.po create mode 100644 po/ca/docs/kcontrol/desktop/index.docbook create mode 100644 po/ca/docs/kcontrol/kwindecoration/button.png create mode 100644 po/ca/docs/kcontrol/kwindecoration/decoration.png create mode 100644 po/ca/docs/kcontrol/kwindecoration/index.docbook create mode 100644 po/ca/docs/kcontrol/kwindecoration/main.png create mode 100644 po/ca/docs/kcontrol/kwineffects/index.docbook create mode 100644 po/ca/docs/kcontrol/kwinscreenedges/index.docbook create mode 100644 po/ca/docs/kcontrol/kwintabbox/index.docbook create mode 100644 po/ca/docs/kcontrol/windowbehaviour/index.docbook create mode 100644 po/ca/docs/kcontrol/windowspecific/index.docbook create mode 100644 po/ca/kcm-kwin-scripts.po create mode 100644 po/ca/kcm_kwin_effects.po create mode 100644 po/ca/kcm_kwin_virtualdesktops.po create mode 100644 po/ca/kcm_kwindecoration.po create mode 100644 po/ca/kcm_kwinrules.po create mode 100644 po/ca/kcm_kwintabbox.po create mode 100644 po/ca/kcmkwincommon.po create mode 100644 po/ca/kcmkwincompositing.po create mode 100644 po/ca/kcmkwinscreenedges.po create mode 100644 po/ca/kcmkwm.po create mode 100644 po/ca/kwin.po create mode 100644 po/ca/kwin_clients.po create mode 100644 po/ca/kwin_effects.po create mode 100644 po/ca/kwin_scripting.po create mode 100644 po/ca/kwin_scripts.po create mode 100644 po/ca@valencia/kcm-kwin-scripts.po create mode 100644 po/ca@valencia/kcm_kwin_effects.po create mode 100644 po/ca@valencia/kcm_kwin_virtualdesktops.po create mode 100644 po/ca@valencia/kcm_kwindecoration.po create mode 100644 po/ca@valencia/kcm_kwinrules.po create mode 100644 po/ca@valencia/kcm_kwintabbox.po create mode 100644 po/ca@valencia/kcmkwincommon.po create mode 100644 po/ca@valencia/kcmkwincompositing.po create mode 100644 po/ca@valencia/kcmkwinscreenedges.po create mode 100644 po/ca@valencia/kcmkwm.po create mode 100644 po/ca@valencia/kwin.po create mode 100644 po/ca@valencia/kwin_clients.po create mode 100644 po/ca@valencia/kwin_effects.po create mode 100644 po/ca@valencia/kwin_scripting.po create mode 100644 po/ca@valencia/kwin_scripts.po create mode 100644 po/cs/kcm-kwin-scripts.po create mode 100644 po/cs/kcm_kwin_effects.po create mode 100644 po/cs/kcm_kwin_virtualdesktops.po create mode 100644 po/cs/kcm_kwindecoration.po create mode 100644 po/cs/kcm_kwinrules.po create mode 100644 po/cs/kcm_kwintabbox.po create mode 100644 po/cs/kcmkwincommon.po create mode 100644 po/cs/kcmkwincompositing.po create mode 100644 po/cs/kcmkwinscreenedges.po create mode 100644 po/cs/kcmkwm.po create mode 100644 po/cs/kwin.po create mode 100644 po/cs/kwin_clients.po create mode 100644 po/cs/kwin_effects.po create mode 100644 po/cs/kwin_scripting.po create mode 100644 po/cs/kwin_scripts.po create mode 100644 po/csb/kcm_kwin_virtualdesktops.po create mode 100644 po/csb/kwin.po create mode 100644 po/csb/kwin_clients.po create mode 100644 po/csb/kwin_effects.po create mode 100644 po/cy/kcm_kwindecoration.po create mode 100644 po/cy/kcm_kwinrules.po create mode 100644 po/cy/kcmkwm.po create mode 100644 po/cy/kwin.po create mode 100644 po/cy/kwin_clients.po create mode 100644 po/da/kcm-kwin-scripts.po create mode 100644 po/da/kcm_kwin_effects.po create mode 100644 po/da/kcm_kwin_virtualdesktops.po create mode 100644 po/da/kcm_kwindecoration.po create mode 100644 po/da/kcm_kwinrules.po create mode 100644 po/da/kcm_kwintabbox.po create mode 100644 po/da/kcmkwincommon.po create mode 100644 po/da/kcmkwincompositing.po create mode 100644 po/da/kcmkwinscreenedges.po create mode 100644 po/da/kcmkwm.po create mode 100644 po/da/kwin.po create mode 100644 po/da/kwin_clients.po create mode 100644 po/da/kwin_effects.po create mode 100644 po/da/kwin_scripting.po create mode 100644 po/da/kwin_scripts.po create mode 100644 po/de/docs/kcontrol/desktop/index.docbook create mode 100644 po/de/docs/kcontrol/kwindecoration/index.docbook create mode 100644 po/de/docs/kcontrol/kwineffects/index.docbook create mode 100644 po/de/docs/kcontrol/kwinscreenedges/index.docbook create mode 100644 po/de/docs/kcontrol/kwintabbox/index.docbook create mode 100644 po/de/docs/kcontrol/windowbehaviour/index.docbook create mode 100644 po/de/docs/kcontrol/windowspecific/index.docbook create mode 100644 po/de/kcm-kwin-scripts.po create mode 100644 po/de/kcm_kwin_effects.po create mode 100644 po/de/kcm_kwin_virtualdesktops.po create mode 100644 po/de/kcm_kwindecoration.po create mode 100644 po/de/kcm_kwinrules.po create mode 100644 po/de/kcm_kwintabbox.po create mode 100644 po/de/kcmkwincommon.po create mode 100644 po/de/kcmkwincompositing.po create mode 100644 po/de/kcmkwinscreenedges.po create mode 100644 po/de/kcmkwm.po create mode 100644 po/de/kwin.po create mode 100644 po/de/kwin_clients.po create mode 100644 po/de/kwin_effects.po create mode 100644 po/de/kwin_scripting.po create mode 100644 po/de/kwin_scripts.po create mode 100644 po/el/kcm-kwin-scripts.po create mode 100644 po/el/kcm_kwin_virtualdesktops.po create mode 100644 po/el/kcm_kwindecoration.po create mode 100644 po/el/kcm_kwinrules.po create mode 100644 po/el/kcm_kwintabbox.po create mode 100644 po/el/kcmkwincompositing.po create mode 100644 po/el/kcmkwinscreenedges.po create mode 100644 po/el/kcmkwm.po create mode 100644 po/el/kwin.po create mode 100644 po/el/kwin_clients.po create mode 100644 po/el/kwin_effects.po create mode 100644 po/el/kwin_scripting.po create mode 100644 po/el/kwin_scripts.po create mode 100644 po/en_GB/kcm-kwin-scripts.po create mode 100644 po/en_GB/kcm_kwin_effects.po create mode 100644 po/en_GB/kcm_kwin_virtualdesktops.po create mode 100644 po/en_GB/kcm_kwindecoration.po create mode 100644 po/en_GB/kcm_kwinrules.po create mode 100644 po/en_GB/kcm_kwintabbox.po create mode 100644 po/en_GB/kcmkwincommon.po create mode 100644 po/en_GB/kcmkwincompositing.po create mode 100644 po/en_GB/kcmkwinscreenedges.po create mode 100644 po/en_GB/kcmkwm.po create mode 100644 po/en_GB/kwin.po create mode 100644 po/en_GB/kwin_clients.po create mode 100644 po/en_GB/kwin_effects.po create mode 100644 po/en_GB/kwin_scripting.po create mode 100644 po/en_GB/kwin_scripts.po create mode 100644 po/eo/kcm_kwin_virtualdesktops.po create mode 100644 po/eo/kcm_kwindecoration.po create mode 100644 po/eo/kcm_kwinrules.po create mode 100644 po/eo/kcm_kwintabbox.po create mode 100644 po/eo/kcmkwincompositing.po create mode 100644 po/eo/kcmkwinscreenedges.po create mode 100644 po/eo/kcmkwm.po create mode 100644 po/eo/kwin.po create mode 100644 po/eo/kwin_clients.po create mode 100644 po/eo/kwin_effects.po create mode 100644 po/es/docs/kcontrol/desktop/index.docbook create mode 100644 po/es/kcm-kwin-scripts.po create mode 100644 po/es/kcm_kwin_effects.po create mode 100644 po/es/kcm_kwin_virtualdesktops.po create mode 100644 po/es/kcm_kwindecoration.po create mode 100644 po/es/kcm_kwinrules.po create mode 100644 po/es/kcm_kwintabbox.po create mode 100644 po/es/kcmkwincommon.po create mode 100644 po/es/kcmkwincompositing.po create mode 100644 po/es/kcmkwinscreenedges.po create mode 100644 po/es/kcmkwm.po create mode 100644 po/es/kwin.po create mode 100644 po/es/kwin_clients.po create mode 100644 po/es/kwin_effects.po create mode 100644 po/es/kwin_scripting.po create mode 100644 po/es/kwin_scripts.po create mode 100644 po/et/kcm-kwin-scripts.po create mode 100644 po/et/kcm_kwin_effects.po create mode 100644 po/et/kcm_kwin_virtualdesktops.po create mode 100644 po/et/kcm_kwindecoration.po create mode 100644 po/et/kcm_kwinrules.po create mode 100644 po/et/kcm_kwintabbox.po create mode 100644 po/et/kcmkwincommon.po create mode 100644 po/et/kcmkwincompositing.po create mode 100644 po/et/kcmkwinscreenedges.po create mode 100644 po/et/kcmkwm.po create mode 100644 po/et/kwin.po create mode 100644 po/et/kwin_clients.po create mode 100644 po/et/kwin_effects.po create mode 100644 po/et/kwin_scripting.po create mode 100644 po/et/kwin_scripts.po create mode 100644 po/eu/kcm-kwin-scripts.po create mode 100644 po/eu/kcm_kwin_effects.po create mode 100644 po/eu/kcm_kwin_virtualdesktops.po create mode 100644 po/eu/kcm_kwindecoration.po create mode 100644 po/eu/kcm_kwinrules.po create mode 100644 po/eu/kcm_kwintabbox.po create mode 100644 po/eu/kcmkwincommon.po create mode 100644 po/eu/kcmkwincompositing.po create mode 100644 po/eu/kcmkwinscreenedges.po create mode 100644 po/eu/kcmkwm.po create mode 100644 po/eu/kwin.po create mode 100644 po/eu/kwin_clients.po create mode 100644 po/eu/kwin_effects.po create mode 100644 po/eu/kwin_scripting.po create mode 100644 po/eu/kwin_scripts.po create mode 100644 po/fa/kcm-kwin-scripts.po create mode 100644 po/fa/kcm_kwin_virtualdesktops.po create mode 100644 po/fa/kcm_kwindecoration.po create mode 100644 po/fa/kcm_kwinrules.po create mode 100644 po/fa/kcm_kwintabbox.po create mode 100644 po/fa/kcmkwincompositing.po create mode 100644 po/fa/kcmkwinscreenedges.po create mode 100644 po/fa/kcmkwm.po create mode 100644 po/fa/kwin.po create mode 100644 po/fa/kwin_clients.po create mode 100644 po/fa/kwin_effects.po create mode 100644 po/fi/kcm-kwin-scripts.po create mode 100644 po/fi/kcm_kwin_effects.po create mode 100644 po/fi/kcm_kwin_virtualdesktops.po create mode 100644 po/fi/kcm_kwindecoration.po create mode 100644 po/fi/kcm_kwinrules.po create mode 100644 po/fi/kcm_kwintabbox.po create mode 100644 po/fi/kcmkwincommon.po create mode 100644 po/fi/kcmkwincompositing.po create mode 100644 po/fi/kcmkwinscreenedges.po create mode 100644 po/fi/kcmkwm.po create mode 100644 po/fi/kwin.po create mode 100644 po/fi/kwin_clients.po create mode 100644 po/fi/kwin_effects.po create mode 100644 po/fi/kwin_scripting.po create mode 100644 po/fi/kwin_scripts.po create mode 100644 po/fr/docs/kcontrol/desktop/index.docbook create mode 100644 po/fr/docs/kcontrol/kwinscreenedges/index.docbook create mode 100644 po/fr/docs/kcontrol/kwintabbox/index.docbook create mode 100644 po/fr/docs/kcontrol/windowspecific/index.docbook create mode 100644 po/fr/kcm-kwin-scripts.po create mode 100644 po/fr/kcm_kwin_effects.po create mode 100644 po/fr/kcm_kwin_virtualdesktops.po create mode 100644 po/fr/kcm_kwindecoration.po create mode 100644 po/fr/kcm_kwinrules.po create mode 100644 po/fr/kcm_kwintabbox.po create mode 100644 po/fr/kcmkwincommon.po create mode 100644 po/fr/kcmkwincompositing.po create mode 100644 po/fr/kcmkwinscreenedges.po create mode 100644 po/fr/kcmkwm.po create mode 100644 po/fr/kwin.po create mode 100644 po/fr/kwin_clients.po create mode 100644 po/fr/kwin_effects.po create mode 100644 po/fr/kwin_scripting.po create mode 100644 po/fr/kwin_scripts.po create mode 100644 po/fy/kcm_kwin_virtualdesktops.po create mode 100644 po/fy/kcm_kwindecoration.po create mode 100644 po/fy/kcm_kwinrules.po create mode 100644 po/fy/kcmkwincompositing.po create mode 100644 po/fy/kcmkwinscreenedges.po create mode 100644 po/fy/kcmkwm.po create mode 100644 po/fy/kwin.po create mode 100644 po/fy/kwin_clients.po create mode 100644 po/fy/kwin_effects.po create mode 100644 po/ga/kcm-kwin-scripts.po create mode 100644 po/ga/kcm_kwin_virtualdesktops.po create mode 100644 po/ga/kcm_kwindecoration.po create mode 100644 po/ga/kcm_kwinrules.po create mode 100644 po/ga/kcm_kwintabbox.po create mode 100644 po/ga/kcmkwincompositing.po create mode 100644 po/ga/kcmkwinscreenedges.po create mode 100644 po/ga/kcmkwm.po create mode 100644 po/ga/kwin.po create mode 100644 po/ga/kwin_clients.po create mode 100644 po/ga/kwin_effects.po create mode 100644 po/gl/kcm-kwin-scripts.po create mode 100644 po/gl/kcm_kwin_effects.po create mode 100644 po/gl/kcm_kwin_virtualdesktops.po create mode 100644 po/gl/kcm_kwindecoration.po create mode 100644 po/gl/kcm_kwinrules.po create mode 100644 po/gl/kcm_kwintabbox.po create mode 100644 po/gl/kcmkwincommon.po create mode 100644 po/gl/kcmkwincompositing.po create mode 100644 po/gl/kcmkwinscreenedges.po create mode 100644 po/gl/kcmkwm.po create mode 100644 po/gl/kwin.po create mode 100644 po/gl/kwin_clients.po create mode 100644 po/gl/kwin_effects.po create mode 100644 po/gl/kwin_scripting.po create mode 100644 po/gl/kwin_scripts.po create mode 100644 po/gu/kcm_kwin_virtualdesktops.po create mode 100644 po/gu/kcm_kwindecoration.po create mode 100644 po/gu/kcm_kwinrules.po create mode 100644 po/gu/kcm_kwintabbox.po create mode 100644 po/gu/kcmkwincompositing.po create mode 100644 po/gu/kcmkwinscreenedges.po create mode 100644 po/gu/kcmkwm.po create mode 100644 po/gu/kwin.po create mode 100644 po/gu/kwin_clients.po create mode 100644 po/gu/kwin_effects.po create mode 100644 po/he/kcm-kwin-scripts.po create mode 100644 po/he/kcm_kwin_virtualdesktops.po create mode 100644 po/he/kcm_kwindecoration.po create mode 100644 po/he/kcm_kwinrules.po create mode 100644 po/he/kcm_kwintabbox.po create mode 100644 po/he/kcmkwincompositing.po create mode 100644 po/he/kcmkwinscreenedges.po create mode 100644 po/he/kcmkwm.po create mode 100644 po/he/kwin.po create mode 100644 po/he/kwin_clients.po create mode 100644 po/he/kwin_effects.po create mode 100644 po/he/kwin_scripting.po create mode 100644 po/he/kwin_scripts.po create mode 100644 po/hi/kcm_kwin_virtualdesktops.po create mode 100644 po/hi/kcm_kwindecoration.po create mode 100644 po/hi/kcm_kwinrules.po create mode 100644 po/hi/kcm_kwintabbox.po create mode 100644 po/hi/kcmkwincompositing.po create mode 100644 po/hi/kcmkwinscreenedges.po create mode 100644 po/hi/kcmkwm.po create mode 100644 po/hi/kwin.po create mode 100644 po/hi/kwin_clients.po create mode 100644 po/hi/kwin_effects.po create mode 100644 po/hne/kcm_kwin_virtualdesktops.po create mode 100644 po/hne/kcm_kwindecoration.po create mode 100644 po/hne/kcm_kwinrules.po create mode 100644 po/hne/kcmkwincompositing.po create mode 100644 po/hne/kcmkwm.po create mode 100644 po/hne/kwin.po create mode 100644 po/hne/kwin_clients.po create mode 100644 po/hne/kwin_effects.po create mode 100644 po/hr/kcm_kwin_virtualdesktops.po create mode 100644 po/hr/kcm_kwindecoration.po create mode 100644 po/hr/kcm_kwinrules.po create mode 100644 po/hr/kcm_kwintabbox.po create mode 100644 po/hr/kcmkwincompositing.po create mode 100644 po/hr/kcmkwinscreenedges.po create mode 100644 po/hr/kcmkwm.po create mode 100644 po/hr/kwin.po create mode 100644 po/hr/kwin_clients.po create mode 100644 po/hr/kwin_effects.po create mode 100644 po/hsb/kcm_kwin_virtualdesktops.po create mode 100644 po/hsb/kcm_kwindecoration.po create mode 100644 po/hsb/kcm_kwinrules.po create mode 100644 po/hsb/kcmkwincompositing.po create mode 100644 po/hsb/kcmkwm.po create mode 100644 po/hsb/kwin.po create mode 100644 po/hsb/kwin_clients.po create mode 100644 po/hsb/kwin_effects.po create mode 100644 po/hu/kcm-kwin-scripts.po create mode 100644 po/hu/kcm_kwin_effects.po create mode 100644 po/hu/kcm_kwin_virtualdesktops.po create mode 100644 po/hu/kcm_kwindecoration.po create mode 100644 po/hu/kcm_kwinrules.po create mode 100644 po/hu/kcm_kwintabbox.po create mode 100644 po/hu/kcmkwincommon.po create mode 100644 po/hu/kcmkwincompositing.po create mode 100644 po/hu/kcmkwinscreenedges.po create mode 100644 po/hu/kcmkwm.po create mode 100644 po/hu/kwin.po create mode 100644 po/hu/kwin_clients.po create mode 100644 po/hu/kwin_effects.po create mode 100644 po/hu/kwin_scripting.po create mode 100644 po/hu/kwin_scripts.po create mode 100644 po/ia/kcm-kwin-scripts.po create mode 100644 po/ia/kcm_kwin_effects.po create mode 100644 po/ia/kcm_kwin_virtualdesktops.po create mode 100644 po/ia/kcm_kwindecoration.po create mode 100644 po/ia/kcm_kwinrules.po create mode 100644 po/ia/kcm_kwintabbox.po create mode 100644 po/ia/kcmkwincommon.po create mode 100644 po/ia/kcmkwincompositing.po create mode 100644 po/ia/kcmkwinscreenedges.po create mode 100644 po/ia/kcmkwm.po create mode 100644 po/ia/kwin.po create mode 100644 po/ia/kwin_clients.po create mode 100644 po/ia/kwin_effects.po create mode 100644 po/ia/kwin_scripting.po create mode 100644 po/ia/kwin_scripts.po create mode 100644 po/id/docs/kcontrol/desktop/index.docbook create mode 100644 po/id/docs/kcontrol/kwindecoration/index.docbook create mode 100644 po/id/docs/kcontrol/kwineffects/index.docbook create mode 100644 po/id/docs/kcontrol/kwinscreenedges/index.docbook create mode 100644 po/id/docs/kcontrol/kwintabbox/index.docbook create mode 100644 po/id/docs/kcontrol/windowbehaviour/index.docbook create mode 100644 po/id/docs/kcontrol/windowspecific/index.docbook create mode 100644 po/id/kcm-kwin-scripts.po create mode 100644 po/id/kcm_kwin_effects.po create mode 100644 po/id/kcm_kwin_virtualdesktops.po create mode 100644 po/id/kcm_kwindecoration.po create mode 100644 po/id/kcm_kwinrules.po create mode 100644 po/id/kcm_kwintabbox.po create mode 100644 po/id/kcmkwincommon.po create mode 100644 po/id/kcmkwincompositing.po create mode 100644 po/id/kcmkwinscreenedges.po create mode 100644 po/id/kcmkwm.po create mode 100644 po/id/kwin.po create mode 100644 po/id/kwin_clients.po create mode 100644 po/id/kwin_effects.po create mode 100644 po/id/kwin_scripting.po create mode 100644 po/id/kwin_scripts.po create mode 100644 po/is/kcm_kwin_virtualdesktops.po create mode 100644 po/is/kcm_kwindecoration.po create mode 100644 po/is/kcm_kwinrules.po create mode 100644 po/is/kcm_kwintabbox.po create mode 100644 po/is/kcmkwincompositing.po create mode 100644 po/is/kcmkwinscreenedges.po create mode 100644 po/is/kcmkwm.po create mode 100644 po/is/kwin.po create mode 100644 po/is/kwin_clients.po create mode 100644 po/is/kwin_effects.po create mode 100644 po/it/docs/kcontrol/desktop/index.docbook create mode 100644 po/it/docs/kcontrol/kwindecoration/index.docbook create mode 100644 po/it/docs/kcontrol/kwineffects/index.docbook create mode 100644 po/it/docs/kcontrol/kwinscreenedges/index.docbook create mode 100644 po/it/docs/kcontrol/kwintabbox/index.docbook create mode 100644 po/it/docs/kcontrol/windowbehaviour/index.docbook create mode 100644 po/it/docs/kcontrol/windowspecific/index.docbook create mode 100644 po/it/kcm-kwin-scripts.po create mode 100644 po/it/kcm_kwin_effects.po create mode 100644 po/it/kcm_kwin_virtualdesktops.po create mode 100644 po/it/kcm_kwindecoration.po create mode 100644 po/it/kcm_kwinrules.po create mode 100644 po/it/kcm_kwintabbox.po create mode 100644 po/it/kcmkwincommon.po create mode 100644 po/it/kcmkwincompositing.po create mode 100644 po/it/kcmkwinscreenedges.po create mode 100644 po/it/kcmkwm.po create mode 100644 po/it/kwin.po create mode 100644 po/it/kwin_clients.po create mode 100644 po/it/kwin_effects.po create mode 100644 po/it/kwin_scripting.po create mode 100644 po/it/kwin_scripts.po create mode 100644 po/ja/kcm-kwin-scripts.po create mode 100644 po/ja/kcm_kwin_effects.po create mode 100644 po/ja/kcm_kwin_virtualdesktops.po create mode 100644 po/ja/kcm_kwindecoration.po create mode 100644 po/ja/kcm_kwinrules.po create mode 100644 po/ja/kcm_kwintabbox.po create mode 100644 po/ja/kcmkwincommon.po create mode 100644 po/ja/kcmkwincompositing.po create mode 100644 po/ja/kcmkwinscreenedges.po create mode 100644 po/ja/kcmkwm.po create mode 100644 po/ja/kwin.po create mode 100644 po/ja/kwin_clients.po create mode 100644 po/ja/kwin_effects.po create mode 100644 po/ja/kwin_scripting.po create mode 100644 po/ja/kwin_scripts.po create mode 100644 po/kk/kcm-kwin-scripts.po create mode 100644 po/kk/kcm_kwin_virtualdesktops.po create mode 100644 po/kk/kcm_kwindecoration.po create mode 100644 po/kk/kcm_kwinrules.po create mode 100644 po/kk/kcm_kwintabbox.po create mode 100644 po/kk/kcmkwincompositing.po create mode 100644 po/kk/kcmkwinscreenedges.po create mode 100644 po/kk/kcmkwm.po create mode 100644 po/kk/kwin.po create mode 100644 po/kk/kwin_clients.po create mode 100644 po/kk/kwin_effects.po create mode 100644 po/kk/kwin_scripting.po create mode 100644 po/km/kcm-kwin-scripts.po create mode 100644 po/km/kcm_kwin_virtualdesktops.po create mode 100644 po/km/kcm_kwindecoration.po create mode 100644 po/km/kcm_kwinrules.po create mode 100644 po/km/kcm_kwintabbox.po create mode 100644 po/km/kcmkwincompositing.po create mode 100644 po/km/kcmkwinscreenedges.po create mode 100644 po/km/kcmkwm.po create mode 100644 po/km/kwin.po create mode 100644 po/km/kwin_clients.po create mode 100644 po/km/kwin_effects.po create mode 100644 po/kn/kcm_kwin_virtualdesktops.po create mode 100644 po/kn/kcm_kwindecoration.po create mode 100644 po/kn/kcm_kwinrules.po create mode 100644 po/kn/kcm_kwintabbox.po create mode 100644 po/kn/kcmkwincompositing.po create mode 100644 po/kn/kcmkwinscreenedges.po create mode 100644 po/kn/kcmkwm.po create mode 100644 po/kn/kwin.po create mode 100644 po/kn/kwin_clients.po create mode 100644 po/kn/kwin_effects.po create mode 100644 po/ko/kcm-kwin-scripts.po create mode 100644 po/ko/kcm_kwin_effects.po create mode 100644 po/ko/kcm_kwin_virtualdesktops.po create mode 100644 po/ko/kcm_kwindecoration.po create mode 100644 po/ko/kcm_kwinrules.po create mode 100644 po/ko/kcm_kwintabbox.po create mode 100644 po/ko/kcmkwincommon.po create mode 100644 po/ko/kcmkwincompositing.po create mode 100644 po/ko/kcmkwinscreenedges.po create mode 100644 po/ko/kcmkwm.po create mode 100644 po/ko/kwin.po create mode 100644 po/ko/kwin_clients.po create mode 100644 po/ko/kwin_effects.po create mode 100644 po/ko/kwin_scripting.po create mode 100644 po/ko/kwin_scripts.po create mode 100644 po/ku/kcm_kwin_virtualdesktops.po create mode 100644 po/ku/kcm_kwindecoration.po create mode 100644 po/ku/kcm_kwinrules.po create mode 100644 po/ku/kcmkwincompositing.po create mode 100644 po/ku/kcmkwm.po create mode 100644 po/ku/kwin.po create mode 100644 po/ku/kwin_clients.po create mode 100644 po/ku/kwin_effects.po create mode 100644 po/lt/kcm-kwin-scripts.po create mode 100644 po/lt/kcm_kwin_effects.po create mode 100644 po/lt/kcm_kwin_virtualdesktops.po create mode 100644 po/lt/kcm_kwindecoration.po create mode 100644 po/lt/kcm_kwinrules.po create mode 100644 po/lt/kcm_kwintabbox.po create mode 100644 po/lt/kcmkwincommon.po create mode 100644 po/lt/kcmkwincompositing.po create mode 100644 po/lt/kcmkwinscreenedges.po create mode 100644 po/lt/kcmkwm.po create mode 100644 po/lt/kwin.po create mode 100644 po/lt/kwin_clients.po create mode 100644 po/lt/kwin_effects.po create mode 100644 po/lt/kwin_scripting.po create mode 100644 po/lt/kwin_scripts.po create mode 100644 po/lv/kcm_kwin_virtualdesktops.po create mode 100644 po/lv/kcm_kwindecoration.po create mode 100644 po/lv/kcm_kwinrules.po create mode 100644 po/lv/kcm_kwintabbox.po create mode 100644 po/lv/kcmkwincompositing.po create mode 100644 po/lv/kcmkwinscreenedges.po create mode 100644 po/lv/kcmkwm.po create mode 100644 po/lv/kwin.po create mode 100644 po/lv/kwin_clients.po create mode 100644 po/lv/kwin_effects.po create mode 100644 po/mai/kcm_kwin_virtualdesktops.po create mode 100644 po/mai/kcm_kwindecoration.po create mode 100644 po/mai/kcm_kwinrules.po create mode 100644 po/mai/kcm_kwintabbox.po create mode 100644 po/mai/kcmkwincompositing.po create mode 100644 po/mai/kcmkwinscreenedges.po create mode 100644 po/mai/kcmkwm.po create mode 100644 po/mai/kwin.po create mode 100644 po/mai/kwin_clients.po create mode 100644 po/mai/kwin_effects.po create mode 100644 po/mk/kcm_kwin_virtualdesktops.po create mode 100644 po/mk/kcm_kwindecoration.po create mode 100644 po/mk/kcm_kwinrules.po create mode 100644 po/mk/kcmkwincompositing.po create mode 100644 po/mk/kcmkwm.po create mode 100644 po/mk/kwin.po create mode 100644 po/mk/kwin_clients.po create mode 100644 po/mk/kwin_effects.po create mode 100644 po/ml/kcm-kwin-scripts.po create mode 100644 po/ml/kcm_kwin_effects.po create mode 100644 po/ml/kcm_kwin_virtualdesktops.po create mode 100644 po/ml/kcm_kwindecoration.po create mode 100644 po/ml/kcm_kwinrules.po create mode 100644 po/ml/kcm_kwintabbox.po create mode 100644 po/ml/kcmkwincommon.po create mode 100644 po/ml/kcmkwincompositing.po create mode 100644 po/ml/kcmkwinscreenedges.po create mode 100644 po/ml/kcmkwm.po create mode 100644 po/ml/kwin.po create mode 100644 po/ml/kwin_clients.po create mode 100644 po/ml/kwin_effects.po create mode 100644 po/ml/kwin_scripting.po create mode 100644 po/ml/kwin_scripts.po create mode 100644 po/mr/kcm-kwin-scripts.po create mode 100644 po/mr/kcm_kwin_virtualdesktops.po create mode 100644 po/mr/kcm_kwindecoration.po create mode 100644 po/mr/kcm_kwinrules.po create mode 100644 po/mr/kcm_kwintabbox.po create mode 100644 po/mr/kcmkwincompositing.po create mode 100644 po/mr/kcmkwinscreenedges.po create mode 100644 po/mr/kcmkwm.po create mode 100644 po/mr/kwin.po create mode 100644 po/mr/kwin_clients.po create mode 100644 po/mr/kwin_effects.po create mode 100644 po/mr/kwin_scripting.po create mode 100644 po/ms/kcm_kwin_virtualdesktops.po create mode 100644 po/ms/kcm_kwindecoration.po create mode 100644 po/ms/kcm_kwinrules.po create mode 100644 po/ms/kcm_kwintabbox.po create mode 100644 po/ms/kcmkwincompositing.po create mode 100644 po/ms/kcmkwinscreenedges.po create mode 100644 po/ms/kcmkwm.po create mode 100644 po/ms/kwin.po create mode 100644 po/ms/kwin_clients.po create mode 100644 po/ms/kwin_effects.po create mode 100644 po/nb/kcm-kwin-scripts.po create mode 100644 po/nb/kcm_kwin_virtualdesktops.po create mode 100644 po/nb/kcm_kwindecoration.po create mode 100644 po/nb/kcm_kwinrules.po create mode 100644 po/nb/kcm_kwintabbox.po create mode 100644 po/nb/kcmkwincommon.po create mode 100644 po/nb/kcmkwincompositing.po create mode 100644 po/nb/kcmkwinscreenedges.po create mode 100644 po/nb/kcmkwm.po create mode 100644 po/nb/kwin.po create mode 100644 po/nb/kwin_clients.po create mode 100644 po/nb/kwin_effects.po create mode 100644 po/nb/kwin_scripting.po create mode 100644 po/nds/kcm-kwin-scripts.po create mode 100644 po/nds/kcm_kwin_virtualdesktops.po create mode 100644 po/nds/kcm_kwindecoration.po create mode 100644 po/nds/kcm_kwinrules.po create mode 100644 po/nds/kcm_kwintabbox.po create mode 100644 po/nds/kcmkwincompositing.po create mode 100644 po/nds/kcmkwinscreenedges.po create mode 100644 po/nds/kcmkwm.po create mode 100644 po/nds/kwin.po create mode 100644 po/nds/kwin_clients.po create mode 100644 po/nds/kwin_effects.po create mode 100644 po/nds/kwin_scripting.po create mode 100644 po/ne/kcm_kwin_virtualdesktops.po create mode 100644 po/ne/kcm_kwindecoration.po create mode 100644 po/ne/kcm_kwinrules.po create mode 100644 po/ne/kcmkwincompositing.po create mode 100644 po/ne/kcmkwm.po create mode 100644 po/ne/kwin.po create mode 100644 po/ne/kwin_clients.po create mode 100644 po/nl/docs/kcontrol/desktop/index.docbook create mode 100644 po/nl/docs/kcontrol/kwindecoration/index.docbook create mode 100644 po/nl/docs/kcontrol/kwineffects/index.docbook create mode 100644 po/nl/docs/kcontrol/kwinscreenedges/index.docbook create mode 100644 po/nl/docs/kcontrol/kwintabbox/index.docbook create mode 100644 po/nl/docs/kcontrol/windowbehaviour/index.docbook create mode 100644 po/nl/docs/kcontrol/windowspecific/index.docbook create mode 100644 po/nl/kcm-kwin-scripts.po create mode 100644 po/nl/kcm_kwin_effects.po create mode 100644 po/nl/kcm_kwin_virtualdesktops.po create mode 100644 po/nl/kcm_kwindecoration.po create mode 100644 po/nl/kcm_kwinrules.po create mode 100644 po/nl/kcm_kwintabbox.po create mode 100644 po/nl/kcmkwincommon.po create mode 100644 po/nl/kcmkwincompositing.po create mode 100644 po/nl/kcmkwinscreenedges.po create mode 100644 po/nl/kcmkwm.po create mode 100644 po/nl/kwin.po create mode 100644 po/nl/kwin_clients.po create mode 100644 po/nl/kwin_effects.po create mode 100644 po/nl/kwin_scripting.po create mode 100644 po/nl/kwin_scripts.po create mode 100644 po/nn/kcm-kwin-scripts.po create mode 100644 po/nn/kcm_kwin_effects.po create mode 100644 po/nn/kcm_kwin_virtualdesktops.po create mode 100644 po/nn/kcm_kwindecoration.po create mode 100644 po/nn/kcm_kwinrules.po create mode 100644 po/nn/kcm_kwintabbox.po create mode 100644 po/nn/kcmkwincommon.po create mode 100644 po/nn/kcmkwincompositing.po create mode 100644 po/nn/kcmkwinscreenedges.po create mode 100644 po/nn/kcmkwm.po create mode 100644 po/nn/kwin.po create mode 100644 po/nn/kwin_clients.po create mode 100644 po/nn/kwin_effects.po create mode 100644 po/nn/kwin_scripting.po create mode 100644 po/nn/kwin_scripts.po create mode 100644 po/oc/kcm_kwin_virtualdesktops.po create mode 100644 po/oc/kcm_kwindecoration.po create mode 100644 po/oc/kcm_kwinrules.po create mode 100644 po/oc/kcmkwincompositing.po create mode 100644 po/oc/kcmkwm.po create mode 100644 po/oc/kwin.po create mode 100644 po/oc/kwin_clients.po create mode 100644 po/oc/kwin_effects.po create mode 100644 po/pa/kcm-kwin-scripts.po create mode 100644 po/pa/kcm_kwin_virtualdesktops.po create mode 100644 po/pa/kcm_kwindecoration.po create mode 100644 po/pa/kcm_kwinrules.po create mode 100644 po/pa/kcm_kwintabbox.po create mode 100644 po/pa/kcmkwincompositing.po create mode 100644 po/pa/kcmkwinscreenedges.po create mode 100644 po/pa/kcmkwm.po create mode 100644 po/pa/kwin.po create mode 100644 po/pa/kwin_clients.po create mode 100644 po/pa/kwin_effects.po create mode 100644 po/pa/kwin_scripting.po create mode 100644 po/pl/kcm-kwin-scripts.po create mode 100644 po/pl/kcm_kwin_effects.po create mode 100644 po/pl/kcm_kwin_virtualdesktops.po create mode 100644 po/pl/kcm_kwindecoration.po create mode 100644 po/pl/kcm_kwinrules.po create mode 100644 po/pl/kcm_kwintabbox.po create mode 100644 po/pl/kcmkwincommon.po create mode 100644 po/pl/kcmkwincompositing.po create mode 100644 po/pl/kcmkwinscreenedges.po create mode 100644 po/pl/kcmkwm.po create mode 100644 po/pl/kwin.po create mode 100644 po/pl/kwin_clients.po create mode 100644 po/pl/kwin_effects.po create mode 100644 po/pl/kwin_scripting.po create mode 100644 po/pl/kwin_scripts.po create mode 100644 po/pt/docs/kcontrol/desktop/index.docbook create mode 100644 po/pt/docs/kcontrol/kwindecoration/index.docbook create mode 100644 po/pt/docs/kcontrol/kwineffects/index.docbook create mode 100644 po/pt/docs/kcontrol/kwinscreenedges/index.docbook create mode 100644 po/pt/docs/kcontrol/kwintabbox/index.docbook create mode 100644 po/pt/docs/kcontrol/windowbehaviour/index.docbook create mode 100644 po/pt/docs/kcontrol/windowspecific/index.docbook create mode 100644 po/pt/kcm-kwin-scripts.po create mode 100644 po/pt/kcm_kwin_effects.po create mode 100644 po/pt/kcm_kwin_virtualdesktops.po create mode 100644 po/pt/kcm_kwindecoration.po create mode 100644 po/pt/kcm_kwinrules.po create mode 100644 po/pt/kcm_kwintabbox.po create mode 100644 po/pt/kcmkwincommon.po create mode 100644 po/pt/kcmkwincompositing.po create mode 100644 po/pt/kcmkwinscreenedges.po create mode 100644 po/pt/kcmkwm.po create mode 100644 po/pt/kwin.po create mode 100644 po/pt/kwin_clients.po create mode 100644 po/pt/kwin_effects.po create mode 100644 po/pt/kwin_scripting.po create mode 100644 po/pt/kwin_scripts.po create mode 100644 po/pt_BR/docs/kcontrol/desktop/index.docbook create mode 100644 po/pt_BR/docs/kcontrol/kwindecoration/configure.png create mode 100644 po/pt_BR/docs/kcontrol/kwindecoration/index.docbook create mode 100644 po/pt_BR/docs/kcontrol/kwinscreenedges/index.docbook create mode 100644 po/pt_BR/docs/kcontrol/kwintabbox/index.docbook create mode 100644 po/pt_BR/docs/kcontrol/windowbehaviour/index.docbook create mode 100644 po/pt_BR/docs/kcontrol/windowspecific/index.docbook create mode 100644 po/pt_BR/kcm-kwin-scripts.po create mode 100644 po/pt_BR/kcm_kwin_effects.po create mode 100644 po/pt_BR/kcm_kwin_virtualdesktops.po create mode 100644 po/pt_BR/kcm_kwindecoration.po create mode 100644 po/pt_BR/kcm_kwinrules.po create mode 100644 po/pt_BR/kcm_kwintabbox.po create mode 100644 po/pt_BR/kcmkwincommon.po create mode 100644 po/pt_BR/kcmkwincompositing.po create mode 100644 po/pt_BR/kcmkwinscreenedges.po create mode 100644 po/pt_BR/kcmkwm.po create mode 100644 po/pt_BR/kwin.po create mode 100644 po/pt_BR/kwin_clients.po create mode 100644 po/pt_BR/kwin_effects.po create mode 100644 po/pt_BR/kwin_scripting.po create mode 100644 po/pt_BR/kwin_scripts.po create mode 100644 po/ro/kcm-kwin-scripts.po create mode 100644 po/ro/kcm_kwin_effects.po create mode 100644 po/ro/kcm_kwin_virtualdesktops.po create mode 100644 po/ro/kcm_kwindecoration.po create mode 100644 po/ro/kcm_kwinrules.po create mode 100644 po/ro/kcm_kwintabbox.po create mode 100644 po/ro/kcmkwincommon.po create mode 100644 po/ro/kcmkwincompositing.po create mode 100644 po/ro/kcmkwinscreenedges.po create mode 100644 po/ro/kcmkwm.po create mode 100644 po/ro/kwin.po create mode 100644 po/ro/kwin_clients.po create mode 100644 po/ro/kwin_effects.po create mode 100644 po/ro/kwin_scripting.po create mode 100644 po/ro/kwin_scripts.po create mode 100644 po/ru/docs/kcontrol/desktop/index.docbook create mode 100644 po/ru/docs/kcontrol/kwindecoration/button.png create mode 100644 po/ru/docs/kcontrol/kwindecoration/configure.png create mode 100644 po/ru/docs/kcontrol/kwindecoration/decoration.png create mode 100644 po/ru/docs/kcontrol/kwindecoration/index.docbook create mode 100644 po/ru/docs/kcontrol/kwindecoration/main.png create mode 100644 po/ru/docs/kcontrol/kwineffects/index.docbook create mode 100644 po/ru/docs/kcontrol/kwinscreenedges/index.docbook create mode 100644 po/ru/docs/kcontrol/kwintabbox/index.docbook create mode 100644 po/ru/docs/kcontrol/windowbehaviour/index.docbook create mode 100644 po/ru/docs/kcontrol/windowspecific/akgregator-info.png create mode 100644 po/ru/docs/kcontrol/windowspecific/akregator-attributes.png create mode 100644 po/ru/docs/kcontrol/windowspecific/akregator-fav.png create mode 100644 po/ru/docs/kcontrol/windowspecific/config-win-behavior.png create mode 100644 po/ru/docs/kcontrol/windowspecific/emacs-attribute.png create mode 100644 po/ru/docs/kcontrol/windowspecific/emacs-info.png create mode 100644 po/ru/docs/kcontrol/windowspecific/focus-stealing-pop2top-attribute.png create mode 100644 po/ru/docs/kcontrol/windowspecific/knotes-attribute.png create mode 100644 po/ru/docs/kcontrol/windowspecific/knotes-info.png create mode 100644 po/ru/docs/kcontrol/windowspecific/kopete-attribute-2.png create mode 100644 po/ru/docs/kcontrol/windowspecific/kopete-chat-attribute.png create mode 100644 po/ru/docs/kcontrol/windowspecific/kopete-chat-info.png create mode 100644 po/ru/docs/kcontrol/windowspecific/kopete-info.png create mode 100644 po/ru/docs/kcontrol/windowspecific/kwin-detect-window.png create mode 100644 po/ru/docs/kcontrol/windowspecific/kwin-kopete-rules.png create mode 100644 po/ru/docs/kcontrol/windowspecific/kwin-rule-editor.png create mode 100644 po/ru/docs/kcontrol/windowspecific/kwin-rules-main-n-akregator.png create mode 100644 po/ru/docs/kcontrol/windowspecific/kwin-rules-main.png create mode 100644 po/ru/docs/kcontrol/windowspecific/kwin-rules-ordering.png create mode 100644 po/ru/docs/kcontrol/windowspecific/kwin-window-attributes.png create mode 100644 po/ru/docs/kcontrol/windowspecific/kwin-window-matching.png create mode 100644 po/ru/docs/kcontrol/windowspecific/tbird-compose-attribute.png create mode 100644 po/ru/docs/kcontrol/windowspecific/tbird-compose-info.png create mode 100644 po/ru/docs/kcontrol/windowspecific/tbird-main-attribute.png create mode 100644 po/ru/docs/kcontrol/windowspecific/tbird-main-info.png create mode 100644 po/ru/docs/kcontrol/windowspecific/tbird-reminder-attribute-2.png create mode 100644 po/ru/docs/kcontrol/windowspecific/tbird-reminder-info.png create mode 100644 po/ru/docs/kcontrol/windowspecific/window-matching-emacs.png create mode 100644 po/ru/docs/kcontrol/windowspecific/window-matching-init.png create mode 100644 po/ru/docs/kcontrol/windowspecific/window-matching-knotes.png create mode 100644 po/ru/docs/kcontrol/windowspecific/window-matching-kopete-chat.png create mode 100644 po/ru/docs/kcontrol/windowspecific/window-matching-kopete.png create mode 100644 po/ru/docs/kcontrol/windowspecific/window-matching-ready-akregator.png create mode 100644 po/ru/docs/kcontrol/windowspecific/window-matching-tbird-compose.png create mode 100644 po/ru/docs/kcontrol/windowspecific/window-matching-tbird-main.png create mode 100644 po/ru/docs/kcontrol/windowspecific/window-matching-tbird-reminder.png create mode 100644 po/ru/kcm-kwin-scripts.po create mode 100644 po/ru/kcm_kwin_effects.po create mode 100644 po/ru/kcm_kwin_virtualdesktops.po create mode 100644 po/ru/kcm_kwindecoration.po create mode 100644 po/ru/kcm_kwinrules.po create mode 100644 po/ru/kcm_kwintabbox.po create mode 100644 po/ru/kcmkwincommon.po create mode 100644 po/ru/kcmkwincompositing.po create mode 100644 po/ru/kcmkwinscreenedges.po create mode 100644 po/ru/kcmkwm.po create mode 100644 po/ru/kwin.po create mode 100644 po/ru/kwin_clients.po create mode 100644 po/ru/kwin_effects.po create mode 100644 po/ru/kwin_scripting.po create mode 100644 po/ru/kwin_scripts.po create mode 100644 po/se/kcm_kwin_virtualdesktops.po create mode 100644 po/se/kcm_kwindecoration.po create mode 100644 po/se/kcm_kwinrules.po create mode 100644 po/se/kcmkwincommon.po create mode 100644 po/se/kcmkwincompositing.po create mode 100644 po/se/kcmkwm.po create mode 100644 po/se/kwin.po create mode 100644 po/se/kwin_clients.po create mode 100644 po/si/kcm_kwin_virtualdesktops.po create mode 100644 po/si/kcm_kwindecoration.po create mode 100644 po/si/kcm_kwinrules.po create mode 100644 po/si/kcm_kwintabbox.po create mode 100644 po/si/kcmkwincompositing.po create mode 100644 po/si/kcmkwinscreenedges.po create mode 100644 po/si/kcmkwm.po create mode 100644 po/si/kwin.po create mode 100644 po/si/kwin_clients.po create mode 100644 po/si/kwin_effects.po create mode 100644 po/sk/kcm-kwin-scripts.po create mode 100644 po/sk/kcm_kwin_effects.po create mode 100644 po/sk/kcm_kwin_virtualdesktops.po create mode 100644 po/sk/kcm_kwindecoration.po create mode 100644 po/sk/kcm_kwinrules.po create mode 100644 po/sk/kcm_kwintabbox.po create mode 100644 po/sk/kcmkwincommon.po create mode 100644 po/sk/kcmkwincompositing.po create mode 100644 po/sk/kcmkwinscreenedges.po create mode 100644 po/sk/kcmkwm.po create mode 100644 po/sk/kwin.po create mode 100644 po/sk/kwin_clients.po create mode 100644 po/sk/kwin_effects.po create mode 100644 po/sk/kwin_scripting.po create mode 100644 po/sk/kwin_scripts.po create mode 100644 po/sl/kcm-kwin-scripts.po create mode 100644 po/sl/kcm_kwin_effects.po create mode 100644 po/sl/kcm_kwin_virtualdesktops.po create mode 100644 po/sl/kcm_kwindecoration.po create mode 100644 po/sl/kcm_kwinrules.po create mode 100644 po/sl/kcm_kwintabbox.po create mode 100644 po/sl/kcmkwincommon.po create mode 100644 po/sl/kcmkwincompositing.po create mode 100644 po/sl/kcmkwinscreenedges.po create mode 100644 po/sl/kcmkwm.po create mode 100644 po/sl/kwin.po create mode 100644 po/sl/kwin_clients.po create mode 100644 po/sl/kwin_effects.po create mode 100644 po/sl/kwin_scripting.po create mode 100644 po/sl/kwin_scripts.po create mode 100644 po/sq/kcm_kwin_virtualdesktops.po create mode 100644 po/sq/kcm_kwindecoration.po create mode 100644 po/sq/kcm_kwinrules.po create mode 100644 po/sq/kcmkwincompositing.po create mode 100644 po/sq/kcmkwinscreenedges.po create mode 100644 po/sq/kcmkwm.po create mode 100644 po/sq/kwin.po create mode 100644 po/sq/kwin_clients.po create mode 100644 po/sq/kwin_effects.po create mode 100644 po/sr/docs/kcontrol/desktop/index.docbook create mode 100644 po/sr/kcm-kwin-scripts.po create mode 100644 po/sr/kcm_kwin_virtualdesktops.po create mode 100644 po/sr/kcm_kwindecoration.po create mode 100644 po/sr/kcm_kwinrules.po create mode 100644 po/sr/kcm_kwintabbox.po create mode 100644 po/sr/kcmkwincompositing.po create mode 100644 po/sr/kcmkwinscreenedges.po create mode 100644 po/sr/kcmkwm.po create mode 100644 po/sr/kwin.po create mode 100644 po/sr/kwin_clients.po create mode 100644 po/sr/kwin_effects.po create mode 100644 po/sr/kwin_scripting.po create mode 100644 po/sr/kwin_scripts.po create mode 100644 po/sr@ijekavian/kcm-kwin-scripts.po create mode 100644 po/sr@ijekavian/kcm_kwin_virtualdesktops.po create mode 100644 po/sr@ijekavian/kcm_kwindecoration.po create mode 100644 po/sr@ijekavian/kcm_kwinrules.po create mode 100644 po/sr@ijekavian/kcm_kwintabbox.po create mode 100644 po/sr@ijekavian/kcmkwincompositing.po create mode 100644 po/sr@ijekavian/kcmkwinscreenedges.po create mode 100644 po/sr@ijekavian/kcmkwm.po create mode 100644 po/sr@ijekavian/kwin.po create mode 100644 po/sr@ijekavian/kwin_clients.po create mode 100644 po/sr@ijekavian/kwin_effects.po create mode 100644 po/sr@ijekavian/kwin_scripting.po create mode 100644 po/sr@ijekavian/kwin_scripts.po create mode 100644 po/sr@ijekavianlatin/kcm-kwin-scripts.po create mode 100644 po/sr@ijekavianlatin/kcm_kwin_virtualdesktops.po create mode 100644 po/sr@ijekavianlatin/kcm_kwindecoration.po create mode 100644 po/sr@ijekavianlatin/kcm_kwinrules.po create mode 100644 po/sr@ijekavianlatin/kcm_kwintabbox.po create mode 100644 po/sr@ijekavianlatin/kcmkwincompositing.po create mode 100644 po/sr@ijekavianlatin/kcmkwinscreenedges.po create mode 100644 po/sr@ijekavianlatin/kcmkwm.po create mode 100644 po/sr@ijekavianlatin/kwin.po create mode 100644 po/sr@ijekavianlatin/kwin_clients.po create mode 100644 po/sr@ijekavianlatin/kwin_effects.po create mode 100644 po/sr@ijekavianlatin/kwin_scripting.po create mode 100644 po/sr@ijekavianlatin/kwin_scripts.po create mode 100644 po/sr@latin/docs/kcontrol/desktop/index.docbook create mode 100644 po/sr@latin/kcm-kwin-scripts.po create mode 100644 po/sr@latin/kcm_kwin_virtualdesktops.po create mode 100644 po/sr@latin/kcm_kwindecoration.po create mode 100644 po/sr@latin/kcm_kwinrules.po create mode 100644 po/sr@latin/kcm_kwintabbox.po create mode 100644 po/sr@latin/kcmkwincompositing.po create mode 100644 po/sr@latin/kcmkwinscreenedges.po create mode 100644 po/sr@latin/kcmkwm.po create mode 100644 po/sr@latin/kwin.po create mode 100644 po/sr@latin/kwin_clients.po create mode 100644 po/sr@latin/kwin_effects.po create mode 100644 po/sr@latin/kwin_scripting.po create mode 100644 po/sr@latin/kwin_scripts.po create mode 100644 po/sv/kcm-kwin-scripts.po create mode 100644 po/sv/kcm_kwin_effects.po create mode 100644 po/sv/kcm_kwin_virtualdesktops.po create mode 100644 po/sv/kcm_kwindecoration.po create mode 100644 po/sv/kcm_kwinrules.po create mode 100644 po/sv/kcm_kwintabbox.po create mode 100644 po/sv/kcmkwincommon.po create mode 100644 po/sv/kcmkwincompositing.po create mode 100644 po/sv/kcmkwinscreenedges.po create mode 100644 po/sv/kcmkwm.po create mode 100644 po/sv/kwin.po create mode 100644 po/sv/kwin_clients.po create mode 100644 po/sv/kwin_effects.po create mode 100644 po/sv/kwin_scripting.po create mode 100644 po/sv/kwin_scripts.po create mode 100644 po/ta/kcm_kwin_virtualdesktops.po create mode 100644 po/ta/kcm_kwindecoration.po create mode 100644 po/ta/kcm_kwinrules.po create mode 100644 po/ta/kcmkwincompositing.po create mode 100644 po/ta/kcmkwm.po create mode 100644 po/ta/kwin.po create mode 100644 po/ta/kwin_clients.po create mode 100644 po/ta/kwin_effects.po create mode 100644 po/te/kcm_kwin_virtualdesktops.po create mode 100644 po/te/kcm_kwindecoration.po create mode 100644 po/te/kcm_kwinrules.po create mode 100644 po/te/kcmkwincompositing.po create mode 100644 po/te/kcmkwm.po create mode 100644 po/te/kwin.po create mode 100644 po/te/kwin_clients.po create mode 100644 po/te/kwin_effects.po create mode 100644 po/tg/kcm_kwin_virtualdesktops.po create mode 100644 po/tg/kcm_kwindecoration.po create mode 100644 po/tg/kcm_kwinrules.po create mode 100644 po/tg/kcmkwincommon.po create mode 100644 po/tg/kcmkwincompositing.po create mode 100644 po/tg/kcmkwinscreenedges.po create mode 100644 po/tg/kcmkwm.po create mode 100644 po/tg/kwin.po create mode 100644 po/tg/kwin_clients.po create mode 100644 po/tg/kwin_effects.po create mode 100644 po/th/kcm_kwin_virtualdesktops.po create mode 100644 po/th/kcm_kwindecoration.po create mode 100644 po/th/kcm_kwinrules.po create mode 100644 po/th/kcm_kwintabbox.po create mode 100644 po/th/kcmkwincompositing.po create mode 100644 po/th/kcmkwinscreenedges.po create mode 100644 po/th/kcmkwm.po create mode 100644 po/th/kwin.po create mode 100644 po/th/kwin_clients.po create mode 100644 po/th/kwin_effects.po create mode 100644 po/tr/kcm-kwin-scripts.po create mode 100644 po/tr/kcm_kwin_virtualdesktops.po create mode 100644 po/tr/kcm_kwindecoration.po create mode 100644 po/tr/kcm_kwinrules.po create mode 100644 po/tr/kcm_kwintabbox.po create mode 100644 po/tr/kcmkwincompositing.po create mode 100644 po/tr/kcmkwinscreenedges.po create mode 100644 po/tr/kcmkwm.po create mode 100644 po/tr/kwin.po create mode 100644 po/tr/kwin_clients.po create mode 100644 po/tr/kwin_effects.po create mode 100644 po/tr/kwin_scripting.po create mode 100644 po/tr/kwin_scripts.po create mode 100644 po/ug/kcm-kwin-scripts.po create mode 100644 po/ug/kcm_kwin_virtualdesktops.po create mode 100644 po/ug/kcm_kwindecoration.po create mode 100644 po/ug/kcm_kwinrules.po create mode 100644 po/ug/kcm_kwintabbox.po create mode 100644 po/ug/kcmkwincompositing.po create mode 100644 po/ug/kcmkwinscreenedges.po create mode 100644 po/ug/kcmkwm.po create mode 100644 po/ug/kwin.po create mode 100644 po/ug/kwin_clients.po create mode 100644 po/ug/kwin_effects.po create mode 100644 po/ug/kwin_scripting.po create mode 100644 po/uk/docs/kcontrol/desktop/index.docbook create mode 100644 po/uk/docs/kcontrol/kwindecoration/button.png create mode 100644 po/uk/docs/kcontrol/kwindecoration/decoration.png create mode 100644 po/uk/docs/kcontrol/kwindecoration/index.docbook create mode 100644 po/uk/docs/kcontrol/kwindecoration/main.png create mode 100644 po/uk/docs/kcontrol/kwineffects/index.docbook create mode 100644 po/uk/docs/kcontrol/kwinscreenedges/index.docbook create mode 100644 po/uk/docs/kcontrol/kwintabbox/index.docbook create mode 100644 po/uk/docs/kcontrol/windowbehaviour/index.docbook create mode 100644 po/uk/docs/kcontrol/windowspecific/index.docbook create mode 100644 po/uk/kcm-kwin-scripts.po create mode 100644 po/uk/kcm_kwin_effects.po create mode 100644 po/uk/kcm_kwin_virtualdesktops.po create mode 100644 po/uk/kcm_kwindecoration.po create mode 100644 po/uk/kcm_kwinrules.po create mode 100644 po/uk/kcm_kwintabbox.po create mode 100644 po/uk/kcmkwincommon.po create mode 100644 po/uk/kcmkwincompositing.po create mode 100644 po/uk/kcmkwinscreenedges.po create mode 100644 po/uk/kcmkwm.po create mode 100644 po/uk/kwin.po create mode 100644 po/uk/kwin_clients.po create mode 100644 po/uk/kwin_effects.po create mode 100644 po/uk/kwin_scripting.po create mode 100644 po/uk/kwin_scripts.po create mode 100644 po/uz/kcm_kwindecoration.po create mode 100644 po/uz/kcm_kwinrules.po create mode 100644 po/uz/kcmkwm.po create mode 100644 po/uz/kwin.po create mode 100644 po/uz/kwin_clients.po create mode 100644 po/uz@cyrillic/kcm_kwindecoration.po create mode 100644 po/uz@cyrillic/kcm_kwinrules.po create mode 100644 po/uz@cyrillic/kcmkwm.po create mode 100644 po/uz@cyrillic/kwin.po create mode 100644 po/uz@cyrillic/kwin_clients.po create mode 100644 po/vi/kcm_kwindecoration.po create mode 100644 po/vi/kcm_kwinrules.po create mode 100644 po/vi/kcmkwm.po create mode 100644 po/vi/kwin.po create mode 100644 po/vi/kwin_clients.po create mode 100644 po/wa/kcm_kwin_virtualdesktops.po create mode 100644 po/wa/kcm_kwindecoration.po create mode 100644 po/wa/kcm_kwinrules.po create mode 100644 po/wa/kcm_kwintabbox.po create mode 100644 po/wa/kcmkwincompositing.po create mode 100644 po/wa/kcmkwinscreenedges.po create mode 100644 po/wa/kcmkwm.po create mode 100644 po/wa/kwin.po create mode 100644 po/wa/kwin_clients.po create mode 100644 po/wa/kwin_effects.po create mode 100644 po/xh/kcm_kwindecoration.po create mode 100644 po/xh/kcmkwm.po create mode 100644 po/xh/kwin.po create mode 100644 po/zh_CN/kcm-kwin-scripts.po create mode 100644 po/zh_CN/kcm_kwin_effects.po create mode 100644 po/zh_CN/kcm_kwin_virtualdesktops.po create mode 100644 po/zh_CN/kcm_kwindecoration.po create mode 100644 po/zh_CN/kcm_kwinrules.po create mode 100644 po/zh_CN/kcm_kwintabbox.po create mode 100644 po/zh_CN/kcmkwincommon.po create mode 100644 po/zh_CN/kcmkwincompositing.po create mode 100644 po/zh_CN/kcmkwinscreenedges.po create mode 100644 po/zh_CN/kcmkwm.po create mode 100644 po/zh_CN/kwin.po create mode 100644 po/zh_CN/kwin_clients.po create mode 100644 po/zh_CN/kwin_effects.po create mode 100644 po/zh_CN/kwin_scripting.po create mode 100644 po/zh_CN/kwin_scripts.po create mode 100644 po/zh_TW/kcm-kwin-scripts.po create mode 100644 po/zh_TW/kcm_kwin_effects.po create mode 100644 po/zh_TW/kcm_kwin_virtualdesktops.po create mode 100644 po/zh_TW/kcm_kwindecoration.po create mode 100644 po/zh_TW/kcm_kwinrules.po create mode 100644 po/zh_TW/kcm_kwintabbox.po create mode 100644 po/zh_TW/kcmkwincommon.po create mode 100644 po/zh_TW/kcmkwincompositing.po create mode 100644 po/zh_TW/kcmkwinscreenedges.po create mode 100644 po/zh_TW/kcmkwm.po create mode 100644 po/zh_TW/kwin.po create mode 100644 po/zh_TW/kwin_clients.po create mode 100644 po/zh_TW/kwin_effects.po create mode 100644 po/zh_TW/kwin_scripting.po create mode 100644 po/zh_TW/kwin_scripts.po create mode 100644 pointer_input.cpp create mode 100644 pointer_input.h create mode 100644 popup_input_filter.cpp create mode 100644 popup_input_filter.h create mode 100644 qml/CMakeLists.txt create mode 100644 qml/onscreennotification/plasma/dummydata/osd.qml create mode 100644 qml/onscreennotification/plasma/main.qml create mode 100644 qml/outline/plasma/outline.qml create mode 100644 rootinfo_filter.cpp create mode 100644 rootinfo_filter.h create mode 100644 rulebooksettings.cpp create mode 100644 rulebooksettings.h create mode 100644 rulebooksettingsbase.kcfg create mode 100644 rulebooksettingsbase.kcfgc create mode 100644 rules.cpp create mode 100644 rules.h create mode 100644 rulesettings.kcfg create mode 100644 rulesettings.kcfgc create mode 100644 scene.cpp create mode 100644 scene.h create mode 100644 screencast/pipewirecore.cpp create mode 100644 screencast/pipewirecore.h create mode 100644 screencast/pipewirestream.cpp create mode 100644 screencast/pipewirestream.h create mode 100644 screencast/screencastmanager.cpp create mode 100644 screencast/screencastmanager.h create mode 100644 screenedge.cpp create mode 100644 screenedge.h create mode 100644 screenlockerwatcher.cpp create mode 100644 screenlockerwatcher.h create mode 100644 screens.cpp create mode 100644 screens.h create mode 100644 scripting/CMakeLists.txt create mode 100644 scripting/Messages.sh create mode 100644 scripting/dbuscall.cpp create mode 100644 scripting/dbuscall.h create mode 100644 scripting/documentation-effect-global.xml create mode 100644 scripting/documentation-global.xml create mode 100644 scripting/genericscriptedconfig.cpp create mode 100644 scripting/genericscriptedconfig.h create mode 100644 scripting/genericscriptedconfig.json create mode 100644 scripting/kwinscript.desktop create mode 100644 scripting/meta.cpp create mode 100644 scripting/meta.h create mode 100644 scripting/screenedgeitem.cpp create mode 100644 scripting/screenedgeitem.h create mode 100644 scripting/scriptedeffect.cpp create mode 100644 scripting/scriptedeffect.h create mode 100644 scripting/scripting.cpp create mode 100644 scripting/scripting.h create mode 100644 scripting/scripting_logging.cpp create mode 100644 scripting/scripting_logging.h create mode 100644 scripting/scripting_model.cpp create mode 100644 scripting/scripting_model.h create mode 100644 scripting/scriptingutils.cpp create mode 100644 scripting/scriptingutils.h create mode 100644 scripting/timer.cpp create mode 100644 scripting/workspace_wrapper.cpp create mode 100644 scripting/workspace_wrapper.h create mode 100644 scripts/CMakeLists.txt create mode 100644 scripts/Messages.sh create mode 100644 scripts/desktopchangeosd/contents/ui/main.qml create mode 100644 scripts/desktopchangeosd/contents/ui/osd.qml create mode 100644 scripts/desktopchangeosd/metadata.desktop create mode 100644 scripts/minimizeall/contents/code/main.js create mode 100644 scripts/minimizeall/metadata.desktop create mode 100644 scripts/synchronizeskipswitcher/contents/code/main.js create mode 100644 scripts/synchronizeskipswitcher/metadata.desktop create mode 100644 scripts/videowall/contents/code/main.js create mode 100644 scripts/videowall/contents/config/main.xml create mode 100644 scripts/videowall/contents/ui/config.ui create mode 100644 scripts/videowall/metadata.desktop create mode 100644 service_utils.cpp create mode 100644 service_utils.h create mode 100644 settings.kcfgc create mode 100644 shadow.cpp create mode 100644 shadow.h create mode 100644 shortcutdialog.ui create mode 100644 sm.cpp create mode 100644 sm.h create mode 100644 subsurfacemonitor.cpp create mode 100644 subsurfacemonitor.h create mode 100644 syncalarmx11filter.cpp create mode 100644 syncalarmx11filter.h create mode 100644 tabbox/CMakeLists.txt create mode 100644 tabbox/clientmodel.cpp create mode 100644 tabbox/clientmodel.h create mode 100644 tabbox/desktopchain.cpp create mode 100644 tabbox/desktopchain.h create mode 100644 tabbox/desktopmodel.cpp create mode 100644 tabbox/desktopmodel.h create mode 100644 tabbox/kwindesktopswitcher.desktop create mode 100644 tabbox/kwinwindowswitcher.desktop create mode 100644 tabbox/switcheritem.cpp create mode 100644 tabbox/switcheritem.h create mode 100644 tabbox/tabbox.cpp create mode 100644 tabbox/tabbox.h create mode 100644 tabbox/tabbox_logging.cpp create mode 100644 tabbox/tabbox_logging.h create mode 100644 tabbox/tabboxconfig.cpp create mode 100644 tabbox/tabboxconfig.h create mode 100644 tabbox/tabboxhandler.cpp create mode 100644 tabbox/tabboxhandler.h create mode 100644 tabbox/x11_filter.cpp create mode 100644 tabbox/x11_filter.h create mode 100644 tablet_input.cpp create mode 100644 tablet_input.h create mode 100644 tabletmodemanager.cpp create mode 100644 tabletmodemanager.h create mode 100644 tests/CMakeLists.txt create mode 100644 tests/cursorhotspottest.cpp create mode 100644 tests/inputmethodstest.qml create mode 100644 tests/libinputtest.cpp create mode 100644 tests/normalhintsbasesizetest.cpp create mode 100644 tests/pointerconstraintstest.cpp create mode 100644 tests/pointerconstraintstest.h create mode 100644 tests/pointerconstraintstest.qml create mode 100644 tests/pointergesturestest.cpp create mode 100644 tests/pointergesturestest.qml create mode 100644 tests/screenedgeshowtest.cpp create mode 100644 tests/unmapdestroytest.qml create mode 100644 tests/x11shadowreader.cpp create mode 100644 thumbnailitem.cpp create mode 100644 thumbnailitem.h create mode 100644 toplevel.cpp create mode 100644 toplevel.h create mode 100644 touch_hide_cursor_spy.cpp create mode 100644 touch_hide_cursor_spy.h create mode 100644 touch_input.cpp create mode 100644 touch_input.h create mode 100644 udev.cpp create mode 100644 udev.h create mode 100644 unmanaged.cpp create mode 100644 unmanaged.h create mode 100644 useractions.cpp create mode 100644 useractions.h create mode 100644 utils.cpp create mode 100644 utils.h create mode 100644 virtual_terminal.cpp create mode 100644 virtual_terminal.h create mode 100644 virtualdesktops.cpp create mode 100644 virtualdesktops.h create mode 100644 virtualdesktopsdbustypes.cpp create mode 100644 virtualdesktopsdbustypes.h create mode 100644 virtualkeyboard.cpp create mode 100644 virtualkeyboard.h create mode 100644 virtualkeyboard_dbus.cpp create mode 100644 virtualkeyboard_dbus.h create mode 100644 was_user_interaction_x11_filter.cpp create mode 100644 was_user_interaction_x11_filter.h create mode 100644 wayland_server.cpp create mode 100644 wayland_server.h create mode 100644 waylandclient.cpp create mode 100644 waylandclient.h create mode 100644 waylandshellintegration.cpp create mode 100644 waylandshellintegration.h create mode 100644 window_property_notify_x11_filter.cpp create mode 100644 window_property_notify_x11_filter.h create mode 100644 workspace.cpp create mode 100644 workspace.h create mode 100644 x11client.cpp create mode 100644 x11client.h create mode 100644 x11eventfilter.cpp create mode 100644 x11eventfilter.h create mode 100644 xcbutils.cpp create mode 100644 xcbutils.h create mode 100644 xcursortheme.cpp create mode 100644 xcursortheme.h create mode 100644 xdgshellclient.cpp create mode 100644 xdgshellclient.h create mode 100644 xdgshellintegration.cpp create mode 100644 xdgshellintegration.h create mode 100644 xkb.cpp create mode 100644 xkb.h create mode 100644 xkb_qt_mapping.h create mode 100644 xwaylandclient.cpp create mode 100644 xwaylandclient.h create mode 100644 xwl/clipboard.cpp create mode 100644 xwl/clipboard.h create mode 100644 xwl/databridge.cpp create mode 100644 xwl/databridge.h create mode 100644 xwl/dnd.cpp create mode 100644 xwl/dnd.h create mode 100644 xwl/drag.cpp create mode 100644 xwl/drag.h create mode 100644 xwl/drag_wl.cpp create mode 100644 xwl/drag_wl.h create mode 100644 xwl/drag_x.cpp create mode 100644 xwl/drag_x.h create mode 100644 xwl/selection.cpp create mode 100644 xwl/selection.h create mode 100644 xwl/selection_source.cpp create mode 100644 xwl/selection_source.h create mode 100644 xwl/transfer.cpp create mode 100644 xwl/transfer.h create mode 100644 xwl/xwayland.cpp create mode 100644 xwl/xwayland.h create mode 100644 xwl/xwayland_interface.cpp create mode 100644 xwl/xwayland_interface.h diff --git a/3rdparty/xcursor.c b/3rdparty/xcursor.c new file mode 100644 index 0000000..1f1360f --- /dev/null +++ b/3rdparty/xcursor.c @@ -0,0 +1,978 @@ +/* + * Copyright © 2002 Keith Packard + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice (including the + * next paragraph) shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "xcursor.h" +#include +#include +#include +#include + +/* + * From libXcursor/include/X11/extensions/Xcursor.h + */ + +#define XcursorTrue 1 +#define XcursorFalse 0 + +/* + * Cursor files start with a header. The header + * contains a magic number, a version number and a + * table of contents which has type and offset information + * for the remaining tables in the file. + * + * File minor versions increment for compatible changes + * File major versions increment for incompatible changes (never, we hope) + * + * Chunks of the same type are always upward compatible. Incompatible + * changes are made with new chunk types; the old data can remain under + * the old type. Upward compatible changes can add header data as the + * header lengths are specified in the file. + * + * File: + * FileHeader + * LISTofChunk + * + * FileHeader: + * CARD32 magic magic number + * CARD32 header bytes in file header + * CARD32 version file version + * CARD32 ntoc number of toc entries + * LISTofFileToc toc table of contents + * + * FileToc: + * CARD32 type entry type + * CARD32 subtype entry subtype (size for images) + * CARD32 position absolute file position + */ + +#define XCURSOR_MAGIC 0x72756358 /* "Xcur" LSBFirst */ + +/* + * Current Xcursor version number. Will be substituted by configure + * from the version in the libXcursor configure.ac file. + */ + +#define XCURSOR_LIB_MAJOR 1 +#define XCURSOR_LIB_MINOR 1 +#define XCURSOR_LIB_REVISION 13 +#define XCURSOR_LIB_VERSION ((XCURSOR_LIB_MAJOR * 10000) + \ + (XCURSOR_LIB_MINOR * 100) + \ + (XCURSOR_LIB_REVISION)) + +/* + * This version number is stored in cursor files; changes to the + * file format require updating this version number + */ +#define XCURSOR_FILE_MAJOR 1 +#define XCURSOR_FILE_MINOR 0 +#define XCURSOR_FILE_VERSION ((XCURSOR_FILE_MAJOR << 16) | (XCURSOR_FILE_MINOR)) +#define XCURSOR_FILE_HEADER_LEN (4 * 4) +#define XCURSOR_FILE_TOC_LEN (3 * 4) + +typedef struct _XcursorFileToc { + XcursorUInt type; /* chunk type */ + XcursorUInt subtype; /* subtype (size for images) */ + XcursorUInt position; /* absolute position in file */ +} XcursorFileToc; + +typedef struct _XcursorFileHeader { + XcursorUInt magic; /* magic number */ + XcursorUInt header; /* byte length of header */ + XcursorUInt version; /* file version number */ + XcursorUInt ntoc; /* number of toc entries */ + XcursorFileToc *tocs; /* table of contents */ +} XcursorFileHeader; + +/* + * The rest of the file is a list of chunks, each tagged by type + * and version. + * + * Chunk: + * ChunkHeader + * + * + * + * ChunkHeader: + * CARD32 header bytes in chunk header + type header + * CARD32 type chunk type + * CARD32 subtype chunk subtype + * CARD32 version chunk type version + */ + +#define XCURSOR_CHUNK_HEADER_LEN (4 * 4) + +typedef struct _XcursorChunkHeader { + XcursorUInt header; /* bytes in chunk header */ + XcursorUInt type; /* chunk type */ + XcursorUInt subtype; /* chunk subtype (size for images) */ + XcursorUInt version; /* version of this type */ +} XcursorChunkHeader; + +/* + * Here's a list of the known chunk types + */ + +/* + * Comments consist of a 4-byte length field followed by + * UTF-8 encoded text + * + * Comment: + * ChunkHeader header chunk header + * CARD32 length bytes in text + * LISTofCARD8 text UTF-8 encoded text + */ + +#define XCURSOR_COMMENT_TYPE 0xfffe0001 +#define XCURSOR_COMMENT_VERSION 1 +#define XCURSOR_COMMENT_HEADER_LEN (XCURSOR_CHUNK_HEADER_LEN + (1 *4)) +#define XCURSOR_COMMENT_COPYRIGHT 1 +#define XCURSOR_COMMENT_LICENSE 2 +#define XCURSOR_COMMENT_OTHER 3 +#define XCURSOR_COMMENT_MAX_LEN 0x100000 + +typedef struct _XcursorComment { + XcursorUInt version; + XcursorUInt comment_type; + char *comment; +} XcursorComment; + +/* + * Each cursor image occupies a separate image chunk. + * The length of the image header follows the chunk header + * so that future versions can extend the header without + * breaking older applications + * + * Image: + * ChunkHeader header chunk header + * CARD32 width actual width + * CARD32 height actual height + * CARD32 xhot hot spot x + * CARD32 yhot hot spot y + * CARD32 delay animation delay + * LISTofCARD32 pixels ARGB pixels + */ + +#define XCURSOR_IMAGE_TYPE 0xfffd0002 +#define XCURSOR_IMAGE_VERSION 1 +#define XCURSOR_IMAGE_HEADER_LEN (XCURSOR_CHUNK_HEADER_LEN + (5*4)) +#define XCURSOR_IMAGE_MAX_SIZE 0x7fff /* 32767x32767 max cursor size */ + +typedef struct _XcursorFile XcursorFile; + +struct _XcursorFile { + void *closure; + int (*read) (XcursorFile *file, unsigned char *buf, int len); + int (*write) (XcursorFile *file, unsigned char *buf, int len); + int (*seek) (XcursorFile *file, long offset, int whence); +}; + +typedef struct _XcursorComments { + int ncomment; /* number of comments */ + XcursorComment **comments; /* array of XcursorComment pointers */ +} XcursorComments; + +/* + * From libXcursor/src/file.c + */ + +static XcursorImage * +XcursorImageCreate (int width, int height) +{ + XcursorImage *image; + + if (width < 0 || height < 0) + return NULL; + if (width > XCURSOR_IMAGE_MAX_SIZE || height > XCURSOR_IMAGE_MAX_SIZE) + return NULL; + + image = malloc (sizeof (XcursorImage) + + width * height * sizeof (XcursorPixel)); + if (!image) + return NULL; + image->version = XCURSOR_IMAGE_VERSION; + image->pixels = (XcursorPixel *) (image + 1); + image->size = width > height ? width : height; + image->width = width; + image->height = height; + image->delay = 0; + return image; +} + +static void +XcursorImageDestroy (XcursorImage *image) +{ + free (image); +} + +static XcursorImages * +XcursorImagesCreate (int size) +{ + XcursorImages *images; + + images = malloc (sizeof (XcursorImages) + + size * sizeof (XcursorImage *)); + if (!images) + return NULL; + images->nimage = 0; + images->images = (XcursorImage **) (images + 1); + images->name = NULL; + return images; +} + +void +XcursorImagesDestroy (XcursorImages *images) +{ + int n; + + if (!images) + return; + + for (n = 0; n < images->nimage; n++) + XcursorImageDestroy (images->images[n]); + if (images->name) + free (images->name); + free (images); +} + +static void +XcursorImagesSetName (XcursorImages *images, const char *name) +{ + char *new; + + if (!images || !name) + return; + + new = malloc (strlen (name) + 1); + + if (!new) + return; + + strcpy (new, name); + if (images->name) + free (images->name); + images->name = new; +} + +static XcursorBool +_XcursorReadUInt (XcursorFile *file, XcursorUInt *u) +{ + unsigned char bytes[4]; + + if (!file || !u) + return XcursorFalse; + + if ((*file->read) (file, bytes, 4) != 4) + return XcursorFalse; + + *u = ((XcursorUInt)(bytes[0]) << 0) | + ((XcursorUInt)(bytes[1]) << 8) | + ((XcursorUInt)(bytes[2]) << 16) | + ((XcursorUInt)(bytes[3]) << 24); + return XcursorTrue; +} + +static void +_XcursorFileHeaderDestroy (XcursorFileHeader *fileHeader) +{ + free (fileHeader); +} + +static XcursorFileHeader * +_XcursorFileHeaderCreate (int ntoc) +{ + XcursorFileHeader *fileHeader; + + if (ntoc > 0x10000) + return NULL; + fileHeader = malloc (sizeof (XcursorFileHeader) + + ntoc * sizeof (XcursorFileToc)); + if (!fileHeader) + return NULL; + fileHeader->magic = XCURSOR_MAGIC; + fileHeader->header = XCURSOR_FILE_HEADER_LEN; + fileHeader->version = XCURSOR_FILE_VERSION; + fileHeader->ntoc = ntoc; + fileHeader->tocs = (XcursorFileToc *) (fileHeader + 1); + return fileHeader; +} + +static XcursorFileHeader * +_XcursorReadFileHeader (XcursorFile *file) +{ + XcursorFileHeader head, *fileHeader; + XcursorUInt skip; + unsigned int n; + + if (!file) + return NULL; + + if (!_XcursorReadUInt (file, &head.magic)) + return NULL; + if (head.magic != XCURSOR_MAGIC) + return NULL; + if (!_XcursorReadUInt (file, &head.header)) + return NULL; + if (!_XcursorReadUInt (file, &head.version)) + return NULL; + if (!_XcursorReadUInt (file, &head.ntoc)) + return NULL; + skip = head.header - XCURSOR_FILE_HEADER_LEN; + if (skip) + if ((*file->seek) (file, skip, SEEK_CUR) == EOF) + return NULL; + fileHeader = _XcursorFileHeaderCreate (head.ntoc); + if (!fileHeader) + return NULL; + fileHeader->magic = head.magic; + fileHeader->header = head.header; + fileHeader->version = head.version; + fileHeader->ntoc = head.ntoc; + for (n = 0; n < fileHeader->ntoc; n++) + { + if (!_XcursorReadUInt (file, &fileHeader->tocs[n].type)) + break; + if (!_XcursorReadUInt (file, &fileHeader->tocs[n].subtype)) + break; + if (!_XcursorReadUInt (file, &fileHeader->tocs[n].position)) + break; + } + if (n != fileHeader->ntoc) + { + _XcursorFileHeaderDestroy (fileHeader); + return NULL; + } + return fileHeader; +} + +static XcursorBool +_XcursorSeekToToc (XcursorFile *file, + XcursorFileHeader *fileHeader, + int toc) +{ + if (!file || !fileHeader || \ + (*file->seek) (file, fileHeader->tocs[toc].position, SEEK_SET) == EOF) + return XcursorFalse; + return XcursorTrue; +} + +static XcursorBool +_XcursorFileReadChunkHeader (XcursorFile *file, + XcursorFileHeader *fileHeader, + int toc, + XcursorChunkHeader *chunkHeader) +{ + if (!file || !fileHeader || !chunkHeader) + return XcursorFalse; + if (!_XcursorSeekToToc (file, fileHeader, toc)) + return XcursorFalse; + if (!_XcursorReadUInt (file, &chunkHeader->header)) + return XcursorFalse; + if (!_XcursorReadUInt (file, &chunkHeader->type)) + return XcursorFalse; + if (!_XcursorReadUInt (file, &chunkHeader->subtype)) + return XcursorFalse; + if (!_XcursorReadUInt (file, &chunkHeader->version)) + return XcursorFalse; + /* sanity check */ + if (chunkHeader->type != fileHeader->tocs[toc].type || + chunkHeader->subtype != fileHeader->tocs[toc].subtype) + return XcursorFalse; + return XcursorTrue; +} + +#define dist(a,b) ((a) > (b) ? (a) - (b) : (b) - (a)) + +static XcursorDim +_XcursorFindBestSize (XcursorFileHeader *fileHeader, + XcursorDim size, + int *nsizesp) +{ + unsigned int n; + int nsizes = 0; + XcursorDim bestSize = 0; + XcursorDim thisSize; + + if (!fileHeader || !nsizesp) + return 0; + + for (n = 0; n < fileHeader->ntoc; n++) + { + if (fileHeader->tocs[n].type != XCURSOR_IMAGE_TYPE) + continue; + thisSize = fileHeader->tocs[n].subtype; + if (!bestSize || dist (thisSize, size) < dist (bestSize, size)) + { + bestSize = thisSize; + nsizes = 1; + } + else if (thisSize == bestSize) + nsizes++; + } + *nsizesp = nsizes; + return bestSize; +} + +static int +_XcursorFindImageToc (XcursorFileHeader *fileHeader, + XcursorDim size, + int count) +{ + unsigned int toc; + XcursorDim thisSize; + + if (!fileHeader) + return 0; + + for (toc = 0; toc < fileHeader->ntoc; toc++) + { + if (fileHeader->tocs[toc].type != XCURSOR_IMAGE_TYPE) + continue; + thisSize = fileHeader->tocs[toc].subtype; + if (thisSize != size) + continue; + if (!count) + break; + count--; + } + if (toc == fileHeader->ntoc) + return -1; + return toc; +} + +static XcursorImage * +_XcursorReadImage (XcursorFile *file, + XcursorFileHeader *fileHeader, + int toc) +{ + XcursorChunkHeader chunkHeader; + XcursorImage head; + XcursorImage *image; + int n; + XcursorPixel *p; + + if (!file || !fileHeader) + return NULL; + + if (!_XcursorFileReadChunkHeader (file, fileHeader, toc, &chunkHeader)) + return NULL; + if (!_XcursorReadUInt (file, &head.width)) + return NULL; + if (!_XcursorReadUInt (file, &head.height)) + return NULL; + if (!_XcursorReadUInt (file, &head.xhot)) + return NULL; + if (!_XcursorReadUInt (file, &head.yhot)) + return NULL; + if (!_XcursorReadUInt (file, &head.delay)) + return NULL; + /* sanity check data */ + if (head.width > XCURSOR_IMAGE_MAX_SIZE || + head.height > XCURSOR_IMAGE_MAX_SIZE) + return NULL; + if (head.width == 0 || head.height == 0) + return NULL; + if (head.xhot > head.width || head.yhot > head.height) + return NULL; + + /* Create the image and initialize it */ + image = XcursorImageCreate (head.width, head.height); + if (image == NULL) + return NULL; + if (chunkHeader.version < image->version) + image->version = chunkHeader.version; + image->size = chunkHeader.subtype; + image->xhot = head.xhot; + image->yhot = head.yhot; + image->delay = head.delay; + n = image->width * image->height; + p = image->pixels; + while (n--) + { + if (!_XcursorReadUInt (file, p)) + { + XcursorImageDestroy (image); + return NULL; + } + p++; + } + return image; +} + +static XcursorImages * +XcursorXcFileLoadImages (XcursorFile *file, int size) +{ + XcursorFileHeader *fileHeader; + XcursorDim bestSize; + int nsize; + XcursorImages *images; + int n; + int toc; + + if (!file || size < 0) + return NULL; + fileHeader = _XcursorReadFileHeader (file); + if (!fileHeader) + return NULL; + bestSize = _XcursorFindBestSize (fileHeader, (XcursorDim) size, &nsize); + if (!bestSize) + { + _XcursorFileHeaderDestroy (fileHeader); + return NULL; + } + images = XcursorImagesCreate (nsize); + if (!images) + { + _XcursorFileHeaderDestroy (fileHeader); + return NULL; + } + for (n = 0; n < nsize; n++) + { + toc = _XcursorFindImageToc (fileHeader, bestSize, n); + if (toc < 0) + break; + images->images[images->nimage] = _XcursorReadImage (file, fileHeader, + toc); + if (!images->images[images->nimage]) + break; + images->nimage++; + } + _XcursorFileHeaderDestroy (fileHeader); + if (images->nimage != nsize) + { + XcursorImagesDestroy (images); + images = NULL; + } + return images; +} + +static int +_XcursorStdioFileRead (XcursorFile *file, unsigned char *buf, int len) +{ + FILE *f = file->closure; + return fread (buf, 1, len, f); +} + +static int +_XcursorStdioFileWrite (XcursorFile *file, unsigned char *buf, int len) +{ + FILE *f = file->closure; + return fwrite (buf, 1, len, f); +} + +static int +_XcursorStdioFileSeek (XcursorFile *file, long offset, int whence) +{ + FILE *f = file->closure; + return fseek (f, offset, whence); +} + +static void +_XcursorStdioFileInitialize (FILE *stdfile, XcursorFile *file) +{ + file->closure = stdfile; + file->read = _XcursorStdioFileRead; + file->write = _XcursorStdioFileWrite; + file->seek = _XcursorStdioFileSeek; +} + +static XcursorImages * +XcursorFileLoadImages (FILE *file, int size) +{ + XcursorFile f; + + if (!file) + return NULL; + + _XcursorStdioFileInitialize (file, &f); + return XcursorXcFileLoadImages (&f, size); +} + +/* + * From libXcursor/src/library.c + */ + +#ifndef ICONDIR +#define ICONDIR "/usr/X11R6/lib/X11/icons" +#endif + +#ifndef XCURSORPATH +#define XCURSORPATH "~/.icons:/usr/share/icons:/usr/share/pixmaps:~/.cursors:/usr/share/cursors/xorg-x11:"ICONDIR +#endif + +static const char * +XcursorLibraryPath (void) +{ + static const char *path; + + if (!path) + { + path = getenv ("XCURSOR_PATH"); + if (!path) + path = XCURSORPATH; + } + return path; +} + +static void +_XcursorAddPathElt (char *path, const char *elt, int len) +{ + int pathlen = strlen (path); + + /* append / if the path doesn't currently have one */ + if (path[0] == '\0' || path[pathlen - 1] != '/') + { + strcat (path, "/"); + pathlen++; + } + if (len == -1) + len = strlen (elt); + /* strip leading slashes */ + while (len && elt[0] == '/') + { + elt++; + len--; + } + strncpy (path + pathlen, elt, len); + path[pathlen + len] = '\0'; +} + +static char * +_XcursorBuildThemeDir (const char *dir, const char *theme) +{ + const char *colon; + const char *tcolon; + char *full; + char *home; + int dirlen; + int homelen; + int themelen; + int len; + + if (!dir || !theme) + return NULL; + + colon = strchr (dir, ':'); + if (!colon) + colon = dir + strlen (dir); + + dirlen = colon - dir; + + tcolon = strchr (theme, ':'); + if (!tcolon) + tcolon = theme + strlen (theme); + + themelen = tcolon - theme; + + home = NULL; + homelen = 0; + if (*dir == '~') + { + home = getenv ("HOME"); + if (!home) + return NULL; + homelen = strlen (home); + dir++; + dirlen--; + } + + /* + * add space for any needed directory separators, one per component, + * and one for the trailing null + */ + len = 1 + homelen + 1 + dirlen + 1 + themelen + 1; + + full = malloc (len); + if (!full) + return NULL; + full[0] = '\0'; + + if (home) + _XcursorAddPathElt (full, home, -1); + _XcursorAddPathElt (full, dir, dirlen); + _XcursorAddPathElt (full, theme, themelen); + return full; +} + +static char * +_XcursorBuildFullname (const char *dir, const char *subdir, const char *file) +{ + char *full; + + if (!dir || !subdir || !file) + return NULL; + + full = malloc (strlen (dir) + 1 + strlen (subdir) + 1 + strlen (file) + 1); + if (!full) + return NULL; + full[0] = '\0'; + _XcursorAddPathElt (full, dir, -1); + _XcursorAddPathElt (full, subdir, -1); + _XcursorAddPathElt (full, file, -1); + return full; +} + +static const char * +_XcursorNextPath (const char *path) +{ + char *colon = strchr (path, ':'); + + if (!colon) + return NULL; + return colon + 1; +} + +#define XcursorWhite(c) ((c) == ' ' || (c) == '\t' || (c) == '\n') +#define XcursorSep(c) ((c) == ';' || (c) == ',') + +static char * +_XcursorThemeInherits (const char *full) +{ + char line[8192]; + char *result = NULL; + FILE *f; + + if (!full) + return NULL; + + f = fopen (full, "r"); + if (f) + { + while (fgets (line, sizeof (line), f)) + { + if (!strncmp (line, "Inherits", 8)) + { + char *l = line + 8; + char *r; + while (*l == ' ') l++; + if (*l != '=') continue; + l++; + while (*l == ' ') l++; + result = malloc (strlen (l) + 1); + if (result) + { + r = result; + while (*l) + { + while (XcursorSep(*l) || XcursorWhite (*l)) l++; + if (!*l) + break; + if (r != result) + *r++ = ':'; + while (*l && !XcursorWhite(*l) && + !XcursorSep(*l)) + *r++ = *l++; + } + *r++ = '\0'; + } + break; + } + } + fclose (f); + } + return result; +} + +static FILE * +XcursorScanTheme (const char *theme, const char *name) +{ + FILE *f = NULL; + char *full; + char *dir; + const char *path; + char *inherits = NULL; + const char *i; + + if (!theme || !name) + return NULL; + + /* + * Scan this theme + */ + for (path = XcursorLibraryPath (); + path && f == NULL; + path = _XcursorNextPath (path)) + { + dir = _XcursorBuildThemeDir (path, theme); + if (dir) + { + full = _XcursorBuildFullname (dir, "cursors", name); + if (full) + { + f = fopen (full, "r"); + free (full); + } + if (!f && !inherits) + { + full = _XcursorBuildFullname (dir, "", "index.theme"); + if (full) + { + inherits = _XcursorThemeInherits (full); + free (full); + } + } + free (dir); + } + } + /* + * Recurse to scan inherited themes + */ + for (i = inherits; i && f == NULL; i = _XcursorNextPath (i)) + f = XcursorScanTheme (i, name); + if (inherits != NULL) + free (inherits); + return f; +} + +XcursorImages * +XcursorLibraryLoadImages (const char *file, const char *theme, int size) +{ + FILE *f = NULL; + XcursorImages *images = NULL; + + if (!file) + return NULL; + + if (theme) + f = XcursorScanTheme (theme, file); + if (!f) + f = XcursorScanTheme ("default", file); + if (f) + { + images = XcursorFileLoadImages (f, size); + if (images) + XcursorImagesSetName (images, file); + fclose (f); + } + return images; +} + +static void +load_all_cursors_from_dir(const char *path, int size, + void (*load_callback)(XcursorImages *, void *), + void *user_data) +{ + FILE *f; + DIR *dir = opendir(path); + struct dirent *ent; + char *full; + XcursorImages *images; + + if (!dir) + return; + + for(ent = readdir(dir); ent; ent = readdir(dir)) { +#ifdef _DIRENT_HAVE_D_TYPE + if (ent->d_type != DT_UNKNOWN && + (ent->d_type != DT_REG && ent->d_type != DT_LNK)) + continue; +#endif + + full = _XcursorBuildFullname(path, "", ent->d_name); + if (!full) + continue; + + f = fopen(full, "r"); + if (!f) { + free(full); + continue; + } + + images = XcursorFileLoadImages(f, size); + + if (images) { + XcursorImagesSetName(images, ent->d_name); + load_callback(images, user_data); + } + + fclose (f); + free(full); + } + + closedir(dir); +} + +/** Load all the cursor of a theme + * + * This function loads all the cursor images of a given theme and its + * inherited themes. Each cursor is loaded into an XcursorImages object + * which is passed to the caller's load callback. If a cursor appears + * more than once across all the inherited themes, the load callback + * will be called multiple times, with possibly different XcursorImages + * object which have the same name. The user is expected to destroy the + * XcursorImages objects passed to the callback with + * XcursorImagesDestroy(). + * + * \param theme The name of theme that should be loaded + * \param size The desired size of the cursor images + * \param load_callback A callback function that will be called + * for each cursor loaded. The first parameter is the XcursorImages + * object representing the loaded cursor and the second is a pointer + * to data provided by the user. + * \param user_data The data that should be passed to the load callback + */ +void +xcursor_load_theme(const char *theme, int size, + void (*load_callback)(XcursorImages *, void *), + void *user_data) +{ + char *full, *dir; + char *inherits = NULL; + const char *path, *i; + + if (!theme) + theme = "default"; + + for (path = XcursorLibraryPath(); + path; + path = _XcursorNextPath(path)) { + dir = _XcursorBuildThemeDir(path, theme); + if (!dir) + continue; + + full = _XcursorBuildFullname(dir, "cursors", ""); + + if (full) { + load_all_cursors_from_dir(full, size, load_callback, + user_data); + free(full); + } + + if (!inherits) { + full = _XcursorBuildFullname(dir, "", "index.theme"); + if (full) { + inherits = _XcursorThemeInherits(full); + free(full); + } + } + + free(dir); + } + + for (i = inherits; i; i = _XcursorNextPath(i)) + xcursor_load_theme(i, size, load_callback, user_data); + + if (inherits) + free(inherits); +} diff --git a/3rdparty/xcursor.h b/3rdparty/xcursor.h new file mode 100644 index 0000000..eca6db1 --- /dev/null +++ b/3rdparty/xcursor.h @@ -0,0 +1,76 @@ +/* + * Copyright © 2002 Keith Packard + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice (including the + * next paragraph) shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef XCURSOR_H +#define XCURSOR_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +typedef int XcursorBool; +typedef uint32_t XcursorUInt; + +typedef XcursorUInt XcursorDim; +typedef XcursorUInt XcursorPixel; + +typedef struct _XcursorImage { + XcursorUInt version; /* version of the image data */ + XcursorDim size; /* nominal size for matching */ + XcursorDim width; /* actual width */ + XcursorDim height; /* actual height */ + XcursorDim xhot; /* hot spot x (must be inside image) */ + XcursorDim yhot; /* hot spot y (must be inside image) */ + XcursorUInt delay; /* animation delay to next frame (ms) */ + XcursorPixel *pixels; /* pointer to pixels */ +} XcursorImage; + +/* + * Other data structures exposed by the library API + */ +typedef struct _XcursorImages { + int nimage; /* number of images */ + XcursorImage **images; /* array of XcursorImage pointers */ + char *name; /* name used to load images */ +} XcursorImages; + +XcursorImages * +XcursorLibraryLoadImages (const char *file, const char *theme, int size); + +void +XcursorImagesDestroy (XcursorImages *images); + +void +xcursor_load_theme(const char *theme, int size, + void (*load_callback)(XcursorImages *, void *), + void *user_data); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..ec42855 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,870 @@ +cmake_minimum_required(VERSION 3.1 FATAL_ERROR) + +project(KWin) +set(PROJECT_VERSION "5.20.5") +set(PROJECT_VERSION_MAJOR 5) + +set(QT_MIN_VERSION "5.15.0") +set(KF5_MIN_VERSION "5.74") + +set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules ${CMAKE_MODULE_PATH}) + +find_package(ECM 5.38 REQUIRED NO_MODULE) + +include(FeatureSummary) +include(WriteBasicConfigVersionFile) +include(GenerateExportHeader) + +# where to look first for cmake modules, before ${CMAKE_ROOT}/Modules/ is checked +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH} ) + +find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS + Concurrent + Core + DBus + Quick + QuickWidgets + Script + Sensors + UiTools + Widgets + X11Extras +) + +find_package(Qt5Test ${QT_MIN_VERSION} CONFIG QUIET) +set_package_properties(Qt5Test PROPERTIES + PURPOSE "Required for tests" + TYPE OPTIONAL +) +add_feature_info("Qt5Test" Qt5Test_FOUND "Required for building tests") +if (NOT Qt5Test_FOUND) + set(BUILD_TESTING OFF CACHE BOOL "Build the testing tree.") +endif() + +include(KDEInstallDirs) +include(KDECMakeSettings) +include(KDECompilerSettings NO_POLICY_SCOPE) +include(KDEClangFormat) + +include(ECMInstallIcons) +include(ECMOptionalAddSubdirectory) +include(ECMConfiguredInstall) + +add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0 -DQT_USE_QSTRINGBUILDER -DQT_NO_URL_CAST_FROM_STRING) + +set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(Qt5Multimedia QUIET) +set_package_properties(Qt5Multimedia PROPERTIES + PURPOSE "Runtime-only dependency for effect video playback" + TYPE RUNTIME +) + +# required frameworks by Core +find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS + Config + ConfigWidgets + CoreAddons + Crash + GlobalAccel + I18n + IconThemes + IdleTime + Notifications + Package + Plasma + Wayland + WidgetsAddons + WindowSystem +) +# required frameworks by config modules +find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS + Completion + Declarative + KCMUtils + KIO + NewStuff + Service + TextWidgets + XmlGui +) + +find_package(Threads) +set_package_properties(Threads PROPERTIES + PURPOSE "Needed for VirtualTerminal support in KWin Wayland" + TYPE REQUIRED +) + +# optional frameworks +find_package(KF5Activities ${KF5_MIN_VERSION} CONFIG) +set_package_properties(KF5Activities PROPERTIES + PURPOSE "Enable building of KWin with kactivities support" + TYPE OPTIONAL +) +add_feature_info("KF5Activities" KF5Activities_FOUND "Enable building of KWin with kactivities support") + +find_package(KF5DocTools ${KF5_MIN_VERSION} CONFIG) +set_package_properties(KF5DocTools PROPERTIES + PURPOSE "Enable building documentation" + TYPE OPTIONAL +) +add_feature_info("KF5DocTools" KF5DocTools_FOUND "Enable building documentation") + +find_package(KF5Kirigami2 ${KF5_MIN_VERSION} CONFIG) +set_package_properties(KF5Kirigami2 PROPERTIES + DESCRIPTION "A QtQuick based components set" + PURPOSE "Required at runtime for Virtual desktop KCM and the virtual keyboard" + TYPE RUNTIME +) + +find_package(KDecoration2 5.18.0 CONFIG REQUIRED) + +find_package(KScreenLocker CONFIG REQUIRED) +set_package_properties(KScreenLocker PROPERTIES + TYPE REQUIRED + PURPOSE "For screenlocker integration in kwin_wayland" +) + +find_package(KWaylandServer CONFIG REQUIRED) +set_package_properties(KWaylandServer PROPERTIES + TYPE REQUIRED + PURPOSE "For Wayland integration" +) + +find_package(Breeze 5.9.0 CONFIG) +set_package_properties(Breeze PROPERTIES + TYPE OPTIONAL + PURPOSE "For setting the default window decoration plugin" +) +if (${Breeze_FOUND}) + if (${BREEZE_WITH_KDECORATION}) + set(HAVE_BREEZE_DECO true) + else() + set(HAVE_BREEZE_DECO FALSE) + endif() +else() + set(HAVE_BREEZE_DECO FALSE) +endif() +add_feature_info("Breeze-Decoration" HAVE_BREEZE_DECO "Default decoration plugin Breeze") + +find_package(EGL) +set_package_properties(EGL PROPERTIES + TYPE RUNTIME + PURPOSE "Required to build KWin with EGL support" +) + +find_package(epoxy) +set_package_properties(epoxy PROPERTIES + DESCRIPTION "libepoxy" + URL "https://github.com/anholt/libepoxy" + TYPE REQUIRED + PURPOSE "OpenGL dispatch library" +) + +set(HAVE_DL_LIBRARY FALSE) +if (epoxy_HAS_GLX) + find_library(DL_LIBRARY dl) + if (DL_LIBRARY) + set(HAVE_DL_LIBRARY TRUE) + endif() +endif() + +find_package(Wayland 1.2 OPTIONAL_COMPONENTS Egl) +set_package_properties(Wayland PROPERTIES + TYPE REQUIRED + PURPOSE "Required for building KWin with Wayland support" +) +add_feature_info("Wayland::EGL" Wayland_Egl_FOUND "Enable building of Wayland backend.") +set(HAVE_WAYLAND_EGL FALSE) +if (Wayland_Egl_FOUND) + set(HAVE_WAYLAND_EGL TRUE) +endif() + +find_package(XKB 0.7.0) +set_package_properties(XKB PROPERTIES + TYPE REQUIRED + PURPOSE "Required for building KWin with Wayland support" +) + +find_package(Libinput 1.9) +set_package_properties(Libinput PROPERTIES TYPE REQUIRED PURPOSE "Required for input handling on Wayland.") + +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.62) +set_package_properties(Libdrm PROPERTIES TYPE OPTIONAL PURPOSE "Required for drm output on Wayland.") +set(HAVE_DRM FALSE) +if (Libdrm_FOUND) + set(HAVE_DRM TRUE) +endif() + +find_package(gbm) +set_package_properties(gbm PROPERTIES TYPE OPTIONAL PURPOSE "Required for egl output of drm backend.") +set(HAVE_GBM FALSE) +if (HAVE_DRM AND gbm_FOUND) + set(HAVE_GBM TRUE) +endif() + +option(KWIN_BUILD_EGL_STREAM_BACKEND "Enable building of EGLStream based DRM backend" ON) +if (HAVE_DRM AND KWIN_BUILD_EGL_STREAM_BACKEND) + set(HAVE_EGL_STREAMS TRUE) +endif() + +find_package(libhybris) +set_package_properties(libhybris PROPERTIES TYPE OPTIONAL PURPOSE "Required for libhybris backend") +set(HAVE_LIBHYBRIS ${libhybris_FOUND}) + +find_package(X11) +set_package_properties(X11 PROPERTIES + DESCRIPTION "X11 libraries" + URL "https://www.x.org" + TYPE REQUIRED +) +add_feature_info("XInput" X11_Xinput_FOUND "Required for poll-free mouse cursor updates") +set(HAVE_X11_XINPUT ${X11_Xinput_FOUND}) + +# All the required XCB components +find_package(XCB 1.10 REQUIRED COMPONENTS + COMPOSITE + CURSOR + DAMAGE + GLX + ICCCM + IMAGE + KEYSYMS + RANDR + RENDER + SHAPE + SHM + SYNC + XCB + XFIXES +) +set_package_properties(XCB PROPERTIES TYPE REQUIRED) + +# and the optional XCB dependencies +if (XCB_ICCCM_VERSION VERSION_LESS "0.4") + set(XCB_ICCCM_FOUND FALSE) +endif() +add_feature_info("XCB-ICCCM" XCB_ICCCM_FOUND "Required for building test applications for KWin") + +find_package(X11_XCB) +set_package_properties(X11_XCB PROPERTIES + PURPOSE "Required for building X11 windowed backend of kwin_wayland" + TYPE OPTIONAL +) + +# dependencies for QPA plugin +find_package(Qt5FontDatabaseSupport REQUIRED) +find_package(Qt5ThemeSupport REQUIRED) +find_package(Qt5EventDispatcherSupport REQUIRED) + +find_package(Freetype REQUIRED) +set_package_properties(Freetype PROPERTIES + DESCRIPTION "A font rendering engine" + URL "https://www.freetype.org" + TYPE REQUIRED + PURPOSE "Needed for KWin's QPA plugin." +) +find_package(Fontconfig REQUIRED) +set_package_properties(Fontconfig PROPERTIES + TYPE REQUIRED + PURPOSE "Needed for KWin's QPA plugin." +) + +find_package(Xwayland) +set_package_properties(Xwayland PROPERTIES + URL "https://x.org" + DESCRIPTION "Xwayland X server" + TYPE RUNTIME + PURPOSE "Needed for running kwin_wayland" +) + +find_package(Libcap) +set_package_properties(Libcap PROPERTIES + TYPE OPTIONAL + PURPOSE "Needed for running kwin_wayland with real-time scheduling policy" +) +set(HAVE_LIBCAP ${Libcap_FOUND}) + +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(QAccessibilityClient CONFIG) +set_package_properties(QAccessibilityClient PROPERTIES + URL "https://www.kde.org" + DESCRIPTION "KDE client-side accessibility library" + TYPE OPTIONAL + PURPOSE "Required to enable accessibility features" +) +set(HAVE_ACCESSIBILITY ${QAccessibilityClient_FOUND}) + +if(CMAKE_SYSTEM MATCHES "FreeBSD") + find_package(epoll) + set_package_properties(epoll PROPERTIES DESCRIPTION "I/O event notification facility" + TYPE REQUIRED + PURPOSE "Needed for running kwin_wayland" + ) +endif() + +include(ECMQMLModules) +ecm_find_qmlmodule(QtQuick 2.3) +ecm_find_qmlmodule(QtQuick.Controls 1.2) +ecm_find_qmlmodule(QtQuick.Layouts 1.3) +ecm_find_qmlmodule(QtQuick.VirtualKeyboard 2.1) +ecm_find_qmlmodule(QtQuick.Window 2.1) +ecm_find_qmlmodule(QtMultimedia 5.0) +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) + +########### configure tests ############### +include(CMakeDependentOption) + +option(KWIN_BUILD_DECORATIONS "Enable building of KWin decorations." ON) +option(KWIN_BUILD_KCMS "Enable building of KWin configuration modules." ON) +option(KWIN_BUILD_TABBOX "Enable building of KWin Tabbox functionality" ON) +option(KWIN_BUILD_XRENDER_COMPOSITING "Enable building of KWin with XRender Compositing support" ON) +cmake_dependent_option(KWIN_BUILD_ACTIVITIES "Enable building of KWin with kactivities support" ON "KF5Activities_FOUND" OFF) + +# Binary name of KWin +set(KWIN_NAME "kwin") +set(KWIN_INTERNAL_NAME_X11 "kwin_x11") +set(KWIN_INTERNAL_NAME_WAYLAND "kwin_wayland") + +# KWIN_HAVE_XRENDER_COMPOSITING - whether XRender-based compositing support is available: may be disabled +if (KWIN_BUILD_XRENDER_COMPOSITING) + set(KWIN_HAVE_XRENDER_COMPOSITING 1) +endif() + +include_directories(${XKB_INCLUDE_DIR}) + +include_directories(${epoxy_INCLUDE_DIR}) +set(HAVE_EPOXY_GLX ${epoxy_HAS_GLX}) + +# for things that are also used by kwin libraries +configure_file(libkwineffects/kwinconfig.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/libkwineffects/kwinconfig.h ) +# for kwin internal things +set(HAVE_X11_XCB ${X11_XCB_FOUND}) + +include(CheckIncludeFile) +include(CheckIncludeFiles) +include(CheckSymbolExists) +check_include_files(unistd.h HAVE_UNISTD_H) +check_include_files(malloc.h HAVE_MALLOC_H) + +check_include_file("sys/prctl.h" HAVE_SYS_PRCTL_H) +check_symbol_exists(PR_SET_DUMPABLE "sys/prctl.h" HAVE_PR_SET_DUMPABLE) +check_symbol_exists(PR_SET_PDEATHSIG "sys/prctl.h" HAVE_PR_SET_PDEATHSIG) +check_include_file("sys/procctl.h" HAVE_SYS_PROCCTL_H) +check_symbol_exists(PROC_TRACE_CTL "sys/procctl.h" HAVE_PROC_TRACE_CTL) +if (HAVE_PR_SET_DUMPABLE OR HAVE_PROC_TRACE_CTL) + set(CAN_DISABLE_PTRACE TRUE) +endif() +add_feature_info("prctl/procctl tracing control" + CAN_DISABLE_PTRACE + "Required for disallowing ptrace on kwin_wayland process") + +check_include_file("sys/sysmacros.h" HAVE_SYS_SYSMACROS_H) + +check_include_file("linux/vt.h" HAVE_LINUX_VT_H) +add_feature_info("linux/vt.h" + HAVE_LINUX_VT_H + "Required for virtual terminal support under wayland") +check_include_file("linux/fb.h" HAVE_LINUX_FB_H) +add_feature_info("linux/fb.h" + HAVE_LINUX_FB_H + "Required for the fbdev backend") + +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") + + +pkg_check_modules(PipeWire IMPORTED_TARGET libpipewire-0.3) +add_feature_info(PipeWire PipeWire_FOUND "Required for Wayland screencasting") + +configure_file(config-kwin.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-kwin.h) + +########### global ############### +set(kwin_effects_dbus_xml ${CMAKE_CURRENT_SOURCE_DIR}/org.kde.kwin.Effects.xml) +qt5_add_dbus_interface(effects_interface_SRCS ${kwin_effects_dbus_xml} kwineffects_interface) +add_library(KWinEffectsInterface STATIC ${effects_interface_SRCS}) +target_link_libraries(KWinEffectsInterface Qt5::DBus) + +include_directories(BEFORE + ${CMAKE_CURRENT_BINARY_DIR}/libkwineffects + ${CMAKE_CURRENT_BINARY_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/libkwineffects + ${CMAKE_CURRENT_SOURCE_DIR}/effects + ${CMAKE_CURRENT_SOURCE_DIR}/tabbox + ${CMAKE_CURRENT_SOURCE_DIR}/platformsupport +) + +add_subdirectory(libkwineffects) +if (KWIN_BUILD_KCMS) + add_subdirectory(kcmkwin) +endif() + +add_subdirectory(data) + +add_subdirectory(effects) +add_subdirectory(scripts) +add_subdirectory(tabbox) +add_subdirectory(scripting) +add_subdirectory(helpers) + +########### next target ############### + +set(kwin_SRCS + 3rdparty/xcursor.c + abstract_client.cpp + abstract_opengl_context_attribute_builder.cpp + abstract_output.cpp + abstract_wayland_output.cpp + activation.cpp + appmenu.cpp + atoms.cpp + client_machine.cpp + colorcorrection/clockskewnotifier.cpp + colorcorrection/clockskewnotifierengine.cpp + colorcorrection/colorcorrectdbusinterface.cpp + colorcorrection/manager.cpp + colorcorrection/suncalc.cpp + composite.cpp + cursor.cpp + dbusinterface.cpp + debug_console.cpp + decorations/decoratedclient.cpp + decorations/decorationbridge.cpp + decorations/decorationpalette.cpp + decorations/decorationrenderer.cpp + decorations/decorations_logging.cpp + decorations/settings.cpp + deleted.cpp + dmabuftexture.cpp + effectloader.cpp + effects.cpp + egl_context_attribute_builder.cpp + events.cpp + focuschain.cpp + geometrytip.cpp + gestures.cpp + globalshortcuts.cpp + group.cpp + idle_inhibition.cpp + input.cpp + input_event.cpp + input_event_spy.cpp + inputpanelv1client.cpp + inputpanelv1integration.cpp + internal_client.cpp + keyboard_input.cpp + keyboard_layout.cpp + keyboard_layout_switching.cpp + keyboard_repeat.cpp + killwindow.cpp + layers.cpp + layershellv1client.cpp + layershellv1integration.cpp + libinput/connection.cpp + libinput/context.cpp + libinput/device.cpp + libinput/events.cpp + libinput/libinput_logging.cpp + linux_dmabuf.cpp + logind.cpp + main.cpp + modifier_only_shortcuts.cpp + moving_client_x11_filter.cpp + netinfo.cpp + onscreennotification.cpp + options.cpp + osd.cpp + outline.cpp + outputscreens.cpp + overlaywindow.cpp + placement.cpp + platform.cpp + pointer_input.cpp + popup_input_filter.cpp + rootinfo_filter.cpp + rulebooksettings.cpp + rules.cpp + scene.cpp + screenedge.cpp + screenlockerwatcher.cpp + screens.cpp + scripting/dbuscall.cpp + scripting/meta.cpp + scripting/screenedgeitem.cpp + scripting/scriptedeffect.cpp + scripting/scripting.cpp + scripting/scripting_logging.cpp + scripting/scripting_model.cpp + scripting/scriptingutils.cpp + scripting/timer.cpp + scripting/workspace_wrapper.cpp + shadow.cpp + sm.cpp + subsurfacemonitor.cpp + syncalarmx11filter.cpp + tablet_input.cpp + thumbnailitem.cpp + toplevel.cpp + touch_hide_cursor_spy.cpp + touch_input.cpp + udev.cpp + unmanaged.cpp + useractions.cpp + utils.cpp + virtualdesktops.cpp + virtualdesktopsdbustypes.cpp + virtualkeyboard.cpp + virtualkeyboard_dbus.cpp + was_user_interaction_x11_filter.cpp + wayland_server.cpp + waylandclient.cpp + waylandshellintegration.cpp + window_property_notify_x11_filter.cpp + workspace.cpp + x11client.cpp + x11eventfilter.cpp + xcbutils.cpp + xcursortheme.cpp + xdgshellclient.cpp + xdgshellintegration.cpp + xkb.cpp + xwaylandclient.cpp + xwl/xwayland_interface.cpp +) + +if (CMAKE_SYSTEM_NAME MATCHES "Linux") + set(kwin_SRCS + ${kwin_SRCS} + colorcorrection/clockskewnotifierengine_linux.cpp + ) +endif() + +include(ECMQtDeclareLoggingCategory) +ecm_qt_declare_logging_category(kwin_SRCS + HEADER + colorcorrect_logging.h + IDENTIFIER + KWIN_COLORCORRECTION + CATEGORY_NAME + kwin_colorcorrection + DEFAULT_SEVERITY + Critical +) + +if (KWIN_BUILD_TABBOX) + include_directories(${Qt5Gui_PRIVATE_INCLUDE_DIRS}) + set(kwin_SRCS ${kwin_SRCS} + tabbox/clientmodel.cpp + tabbox/desktopchain.cpp + tabbox/desktopmodel.cpp + tabbox/switcheritem.cpp + tabbox/tabbox.cpp + tabbox/tabbox_logging.cpp + tabbox/tabboxconfig.cpp + tabbox/tabboxhandler.cpp + tabbox/x11_filter.cpp + ) +endif() + +if (KWIN_BUILD_ACTIVITIES) + set(kwin_SRCS ${kwin_SRCS} + activities.cpp + ) +endif() + +if (HAVE_LINUX_VT_H) + set(kwin_SRCS ${kwin_SRCS} + virtual_terminal.cpp + ) + set(KWIN_TTY_PREFIX "/dev/tty") +endif() + +if(CMAKE_SYSTEM MATCHES "FreeBSD") + # We know it has epoll, so supports VT as well + set(kwin_SRCS ${kwin_SRCS} + virtual_terminal.cpp + ) + set(KWIN_TTY_PREFIX "/dev/ttyv") +endif() +if(KWIN_TTY_PREFIX) + set_source_files_properties(virtual_terminal.cpp PROPERTIES COMPILE_DEFINITIONS KWIN_TTY_PREFIX="${KWIN_TTY_PREFIX}") +endif() + +kconfig_add_kcfg_files(kwin_SRCS settings.kcfgc) +kconfig_add_kcfg_files(kwin_SRCS colorcorrection/colorcorrect_settings.kcfgc) +kconfig_add_kcfg_files(kwin_SRCS rulesettings.kcfgc) +kconfig_add_kcfg_files(kwin_SRCS rulebooksettingsbase.kcfgc) + +qt5_add_dbus_adaptor(kwin_SRCS org.kde.KWin.xml dbusinterface.h KWin::DBusInterface) +qt5_add_dbus_adaptor(kwin_SRCS org.kde.kwin.Compositing.xml dbusinterface.h KWin::CompositorDBusInterface) +qt5_add_dbus_adaptor(kwin_SRCS org.kde.kwin.ColorCorrect.xml colorcorrection/colorcorrectdbusinterface.h KWin::ColorCorrect::ColorCorrectDBusInterface) +qt5_add_dbus_adaptor(kwin_SRCS ${kwin_effects_dbus_xml} effects.h KWin::EffectsHandlerImpl) +qt5_add_dbus_adaptor(kwin_SRCS org.kde.KWin.VirtualDesktopManager.xml dbusinterface.h KWin::VirtualDesktopManagerDBusInterface) +qt5_add_dbus_adaptor(kwin_SRCS org.kde.KWin.Session.xml sm.h KWin::SessionManager) + +qt5_add_dbus_interface(kwin_SRCS ${KSCREENLOCKER_DBUS_INTERFACES_DIR}/kf5_org.freedesktop.ScreenSaver.xml screenlocker_interface) +qt5_add_dbus_interface(kwin_SRCS ${KSCREENLOCKER_DBUS_INTERFACES_DIR}/org.kde.screensaver.xml kscreenlocker_interface) +qt5_add_dbus_interface(kwin_SRCS org.kde.kappmenu.xml appmenu_interface) + +ki18n_wrap_ui(kwin_SRCS + debug_console.ui + shortcutdialog.ui +) + +########### target link libraries ############### + +set(kwin_OWN_LIBS + kwineffects + kwin4_effect_builtins +) + +set(kwin_QT_LIBS + Qt5::Concurrent + Qt5::DBus + Qt5::Quick + Qt5::Script + Qt5::Sensors +) + +set(kwin_KDE_LIBS + KF5::ConfigCore + KF5::ConfigWidgets + KF5::CoreAddons + KF5::GlobalAccel + KF5::GlobalAccelPrivate + KF5::I18n + KF5::Notifications + KF5::Package + KF5::Plasma + KF5::QuickAddons + KF5::WindowSystem + + KDecoration2::KDecoration + KDecoration2::KDecoration2Private + + PW::KScreenLocker +) + +set(kwin_XLIB_LIBS + ${X11_ICE_LIB} + ${X11_SM_LIB} + ${X11_X11_LIB} +) + +set(kwin_XCB_LIBS + XCB::COMPOSITE + XCB::DAMAGE + XCB::GLX + XCB::ICCCM + XCB::KEYSYMS + XCB::RANDR + XCB::RENDER + XCB::SHAPE + XCB::SHM + XCB::SYNC + XCB::XCB + XCB::XFIXES +) + +set(kwin_WAYLAND_LIBS + KF5::WaylandClient + Plasma::KWaylandServer + XKB::XKB + ${CMAKE_THREAD_LIBS_INIT} +) + +if (KWIN_BUILD_ACTIVITIES) + set(kwin_KDE_LIBS ${kwin_KDE_LIBS} KF5::Activities) +endif() + +set(kwinLibs + ${kwin_OWN_LIBS} + ${kwin_QT_LIBS} + ${kwin_KDE_LIBS} + ${kwin_XLIB_LIBS} + ${kwin_XCB_LIBS} + ${kwin_WAYLAND_LIBS} + ${UDEV_LIBS} + Libinput::Libinput +) + +add_library(kwin SHARED ${kwin_SRCS}) +if (Libinput_VERSION_STRING VERSION_GREATER 1.14) + target_compile_definitions(kwin PRIVATE -DLIBINPUT_HAS_TOTEM) +endif () + +set_target_properties(kwin PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR} +) + +target_link_libraries(kwin ${kwinLibs} kwinglutils ${epoxy_LIBRARY}) + +generate_export_header(kwin EXPORT_FILE_NAME kwin_export.h) + +if(CMAKE_SYSTEM MATCHES "FreeBSD") + # epoll is required, includes live under ${LOCALBASE}, separate library + target_include_directories(kwin PUBLIC ${epoll_INCLUDE_DIRS}) + target_link_libraries(kwin ${kwinLibs} ${epoll_LIBRARIES}) +endif() + +add_executable(kwin_x11 main_x11.cpp) +target_link_libraries(kwin_x11 kwin KF5::Crash Qt5::X11Extras) + +install(TARGETS kwin ${INSTALL_TARGETS_DEFAULT_ARGS} LIBRARY NAMELINK_SKIP) +install(TARGETS kwin_x11 ${INSTALL_TARGETS_DEFAULT_ARGS}) + +set(kwin_XWAYLAND_SRCS + ${CMAKE_CURRENT_SOURCE_DIR}/xwl/clipboard.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/xwl/databridge.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/xwl/dnd.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/xwl/drag.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/xwl/drag_wl.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/xwl/drag_x.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/xwl/selection.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/xwl/selection_source.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/xwl/transfer.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/xwl/xwayland.cpp +) +include(ECMQtDeclareLoggingCategory) +ecm_qt_declare_logging_category(kwin_XWAYLAND_SRCS + HEADER + xwayland_logging.h + IDENTIFIER + KWIN_XWL + CATEGORY_NAME + kwin_xwl + DEFAULT_SEVERITY + Warning +) + +set(kwin_WAYLAND_SRCS + main_wayland.cpp + tabletmodemanager.cpp +) +ecm_qt_declare_logging_category(kwin_WAYLAND_SRCS + HEADER + kwinscreencast_logging.h + IDENTIFIER + KWIN_SCREENCAST + CATEGORY_NAME + kwin_screencast + DEFAULT_SEVERITY + Warning +) + +add_executable(kwin_wayland ${kwin_WAYLAND_SRCS} ${kwin_XWAYLAND_SRCS}) + + +if (PipeWire_FOUND) + target_sources(kwin_wayland + PRIVATE + screencast/screencastmanager.cpp + screencast/pipewirecore.cpp + screencast/pipewirestream.cpp) + target_link_libraries(kwin_wayland PkgConfig::PipeWire) +endif() + +target_link_libraries(kwin_wayland + kwin + KF5::Crash +) +if (HAVE_LIBCAP) + target_link_libraries(kwin_wayland ${Libcap_LIBRARIES}) +endif() + +install(TARGETS kwin_wayland ${INSTALL_TARGETS_DEFAULT_ARGS}) +if (HAVE_LIBCAP) + install( + CODE "execute_process( + COMMAND + ${SETCAP_EXECUTABLE} + CAP_SYS_NICE=+ep + \$ENV{DESTDIR}${CMAKE_INSTALL_FULL_BINDIR}/kwin_wayland)" + ) +endif() + +add_subdirectory(platformsupport) +add_subdirectory(plugins) + +########### install files ############### + +install(FILES kwin.kcfg DESTINATION ${KCFG_INSTALL_DIR} RENAME ${KWIN_NAME}.kcfg) +install(FILES colorcorrection/colorcorrect_settings.kcfg DESTINATION ${KCFG_INSTALL_DIR} RENAME ${KWIN_NAME}_colorcorrect.kcfg) +install(FILES kwin.notifyrc DESTINATION ${KNOTIFYRC_INSTALL_DIR} RENAME ${KWIN_NAME}.notifyrc) +install( + FILES + org.kde.KWin.VirtualDesktopManager.xml + org.kde.KWin.xml + org.kde.kwin.ColorCorrect.xml + org.kde.kwin.Compositing.xml + org.kde.kwin.Effects.xml + DESTINATION + ${KDE_INSTALL_DBUSINTERFACEDIR} +) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/kwin_export.h DESTINATION ${INCLUDE_INSTALL_DIR} COMPONENT Devel) + +# Install the KWin/Script service type +install(FILES scripting/kwinscript.desktop DESTINATION ${SERVICETYPES_INSTALL_DIR}) + +add_subdirectory(qml) + +if (BUILD_TESTING) + find_package(WaylandProtocols REQUIRED) + find_package(QtWaylandScanner ${QT_MIN_VERSION} REQUIRED) + find_package(Wayland REQUIRED COMPONENTS Client) + + add_subdirectory(autotests) + add_subdirectory(tests) +endif() + +if (KF5DocTools_FOUND) + add_subdirectory(doc) +endif() + +add_subdirectory(kconf_update) + +# 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}) + +feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) + +include(CMakePackageConfigHelpers) +set(CMAKECONFIG_INSTALL_DIR "${CMAKECONFIG_INSTALL_PREFIX}/KWinDBusInterface") +configure_package_config_file(KWinDBusInterfaceConfig.cmake.in + "${CMAKE_CURRENT_BINARY_DIR}/KWinDBusInterfaceConfig.cmake" + PATH_VARS KDE_INSTALL_DBUSINTERFACEDIR + INSTALL_DESTINATION ${CMAKECONFIG_INSTALL_DIR}) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/KWinDBusInterfaceConfig.cmake + DESTINATION ${CMAKECONFIG_INSTALL_DIR}) + +ecm_install_configured_files(INPUT plasma-kwin_x11.service.in plasma-kwin_wayland.service.in @ONLY + DESTINATION ${SYSTEMD_USER_UNIT_INSTALL_DIR}) + +find_package(KF5I18n CONFIG REQUIRED) +ki18n_install(po) + + find_package(KF5DocTools CONFIG) + if(KF5DocTools_FOUND) + kdoctools_install(po) + endif() diff --git a/HACKING.md b/HACKING.md new file mode 100644 index 0000000..4aace2c --- /dev/null +++ b/HACKING.md @@ -0,0 +1,98 @@ +# Quick building + +KWin uses CMake. This means that KWin can be build in a normal cmake-style out of source tree. + + mkdir build + cd build + cmake ../ + make + +# Dependencies + +All of KWin's dependencies are found through CMake. CMake will report what is missing. The dependencies can be installed using system packages. For the master branch it is possible that system packages are not up to date. KWin master sometimes depends from the master branch of some KDE frameworks (mostly KWayland and KWindowSystems) and other components inside Plasma (KDecoration). KWin never depends on unreleased third party components. In such a case it is required to build these components also from master. Alternatively some distributions provide daily build packages which can also be used instead. Stable branches never depend on unstable components. On Debian based distributions the easiest way to install all build dependencies is + + sudo apt build-dep kwin-wayland + +# Running the build KWin +KWin can be executed directly from the build directory. All binaries are put into a subdirectory bin. From there KWin and its tests can be started. + +## Nested KWin/Wayland +The best way to test changes in KWin is through using the nested KWin Wayland in a running X11 or Wayland session. + +To start a nested KWin Wayland use: + + cd build + cd bin + QT_PLUGIN_PATH=`pwd` dbus-run-session ./kwin_wayland --xwayland --socket=wayland-1 + +The socket option is not required if KWin is started from an X11 session. On Wayland of course a socket not matching the session's socket must be chosen. To show windows in the nested KWin adjust the environment variables DISPLAY (for X11 windows) and WAYLAND_DISPLAY (for Wayland windows). Alternatively it's possible to pass applications to launch as command line arguments to kwin_wayland command. E.g. + + QT_PLUGIN_PATH=`pwd` dbus-run-session ./kwin_wayland --xwayland --socket=wayland-1 konsole + +Will start a konsole in the nested KWin. + +### Why adjusting QT_PLUGIN_PATH? + +Qt's plugin path needs to point to the build directory so that KWin can load the plugins from the build directory instead of using the system installed plugins which would normally be preferred. + +### Why start a dedicated DBus session? + +KWin interacts with kglobalaccel, so starting KWin without a dedicated DBus session would steal all global shortcuts from the running session. KGlobalaccel is just a very prominent example, there are further DBus interaction problems without a dedicated DBus session. + +## DRM platform + +The nested setup only works for the X11 and Wayland platform plugins. Changes in the DRM platform plugin or libinput cannot be tested in a nested setup. To test these, change to a tty, login and start kwin_wayland with the same command as for nested. KWin automatically picks the DRM platform as neither DISPLAY nor WAYLAND_DISPLAY environment variables should be defined. + +## KWin/X11 + +KWin for the X11 windowing system cannot be tested with a nested Wayland setup. Instead the common way is to run KWin and replace the existing window manager of the X session: + + cd build + cd bin + QT_PLUGIN_PATH=`pwd` ./kwin_x11 --replace + +In this case also the current DBus session should be used and dbus-run-session should not be used. Of course it's only possible to start kwin_x11 in an X session. On Wayland kwin_x11 will refuse to start. + +### Xephyr + +It is possible to run kwin_x11 in a Xephyr window, but this is rather limited and especially the OpenGL compositor cannot really be tested. For X11 it's better to replace the running session. On Wayland using Xephyr is better than nothing. + +## Containers +While it is possible to run KWin through container technologies such as docker this is not recommended. KWin needs to interact with the actual hardware such as the OpenGL drivers, input devices, etc. Getting this setup is possible, but complicated. With containers one can only achieve a nested setup and this requires passing through the socket of the host's windowing system, device files for graphics stack, etc. + +# Attaching a debugger + +Debugging KWin is challenging as KWin is drawing the screen. If you gdb into a running kwin_x11 or kwin_wayland from your current session, it's probably the last thing you'll do in the session. The session hard locks the moment the debugger is attached to the process. Due to that never attach a debugger from your running session. + +It is possible to attach gdb from another tty, but that is only a solution for X11. On Wayland one would not be able to switch back to the tty once a breakpoint is hit as KWin is responsible for tty switching. + +Overall the only sensible solution for attaching gdb is from another system through ssh. + +## Better ways of debugging +As attaching gdb to a running session is not a satisfying solution it is better to run nested KWin Wayland in gdb. E.g. + + cd build + cd bin + QT_PLUGIN_PATH=`pwd` dbus-run-session gdb --args ./kwin_wayland --xwayland --socket=wayland-1 + +Another solution is using KWin's extensive test suite and run the appropriate test binary through gdb. + +# Automatic tests +KWin's test suite is explained in document [TESTING](TESTING.md). + +# Contributing patches + +KWin uses [KDE's phabricator instance](https://phabricator.kde.org) for code review. Patches can be uploaded automatically using the tool arcanist. A possible workflow could look like: + + git checkout -b my-feature-branch + git add ... + git commit + arc diff + +More complete documentation can be found in [KDE's wiki](https://community.kde.org/Infrastructure/Phabricator). Please add "#KWin" as reviewers. Please run KWin's automated test suite prior to uploading a patch to ensure that the change does not break existing code. + +# Coding conventions +KWin's coding conventions are explained in document [coding-conventions.md](doc/coding-conventions.md). + +# Coding style +KWin code follows the [Frameworks coding style](https://techbase.kde.org/Policies/Frameworks_Coding_Style). diff --git a/KWinDBusInterfaceConfig.cmake.in b/KWinDBusInterfaceConfig.cmake.in new file mode 100644 index 0000000..1a3960b --- /dev/null +++ b/KWinDBusInterfaceConfig.cmake.in @@ -0,0 +1,6 @@ +@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_WAYLAND_BIN_PATH "@CMAKE_INSTALL_FULL_BINDIR@/kwin_wayland") diff --git a/LICENSES/BSD-2-Clause.txt b/LICENSES/BSD-2-Clause.txt new file mode 100644 index 0000000..2d2bab1 --- /dev/null +++ b/LICENSES/BSD-2-Clause.txt @@ -0,0 +1,22 @@ +Copyright (c) . All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/LICENSES/BSD-3-Clause.txt b/LICENSES/BSD-3-Clause.txt new file mode 100644 index 0000000..0741db7 --- /dev/null +++ b/LICENSES/BSD-3-Clause.txt @@ -0,0 +1,26 @@ +Copyright (c) . All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/LICENSES/GPL-2.0-only.txt b/LICENSES/GPL-2.0-only.txt new file mode 100644 index 0000000..0f3d641 --- /dev/null +++ b/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/LICENSES/GPL-2.0-or-later.txt b/LICENSES/GPL-2.0-or-later.txt new file mode 100644 index 0000000..1d80ac3 --- /dev/null +++ b/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/LICENSES/GPL-3.0-only.txt b/LICENSES/GPL-3.0-only.txt new file mode 100644 index 0000000..e142a52 --- /dev/null +++ b/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/LICENSES/GPL-3.0-or-later.txt b/LICENSES/GPL-3.0-or-later.txt new file mode 100644 index 0000000..e142a52 --- /dev/null +++ b/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/LICENSES/LGPL-2.0-only.txt b/LICENSES/LGPL-2.0-only.txt new file mode 100644 index 0000000..5c96471 --- /dev/null +++ b/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/LICENSES/LGPL-2.0-or-later.txt b/LICENSES/LGPL-2.0-or-later.txt new file mode 100644 index 0000000..5c96471 --- /dev/null +++ b/LICENSES/LGPL-2.0-or-later.txt @@ -0,0 +1,446 @@ +GNU LIBRARY GENERAL PUBLIC LICENSE + +Version 2, June 1991 Copyright (C) 1991 Free Software Foundation, Inc. + +51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +[This is the first released version of the library GPL. It is numbered 2 because +it goes with version 2 of the ordinary GPL.] + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public Licenses are intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. + +This license, the Library General Public License, applies to some specially +designated Free Software Foundation software, and to any other libraries whose +authors decide to use it. You can use it for your libraries, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for this service if you +wish), that you receive source code or can get it if you want it, that you +can change the software or use pieces of it in new free programs; and that +you know you can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to +deny you these rights or to ask you to surrender the rights. These restrictions +translate to certain responsibilities for you if you distribute copies of +the library, or if you modify it. + +For example, if you distribute copies of the library, whether gratis or for +a fee, you must give the recipients all the rights that we gave you. You must +make sure that they, too, receive or can get the source code. If you link +a program with the library, you must provide complete object files to the +recipients so that they can relink them with the library, after making changes +to the library and recompiling it. And you must show them these terms so they +know their rights. + +Our method of protecting your rights has two steps: (1) copyright the library, +and (2) offer you this license which gives you legal permission to copy, distribute +and/or modify the library. + +Also, for each distributor's protection, we want to make certain that everyone +understands that there is no warranty for this free library. If the library +is modified by someone else and passed on, we want its recipients to know +that what they have is not the original version, so that any problems introduced +by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We +wish to avoid the danger that companies distributing free software will individually +obtain patent licenses, thus in effect transforming the program into proprietary +software. To prevent this, we have made it clear that any patent must be licensed +for everyone's free use or not licensed at all. + +Most GNU software, including some libraries, is covered by the ordinary GNU +General Public License, which was designed for utility programs. This license, +the GNU Library General Public License, applies to certain designated libraries. +This license is quite different from the ordinary one; be sure to read it +in full, and don't assume that anything in it is the same as in the ordinary +license. + +The reason we have a separate public license for some libraries is that they +blur the distinction we usually make between modifying or adding to a program +and simply using it. Linking a program with a library, without changing the +library, is in some sense simply using the library, and is analogous to running +a utility program or application program. However, in a textual and legal +sense, the linked executable is a combined work, a derivative of the original +library, and the ordinary General Public License treats it as such. + +Because of this blurred distinction, using the ordinary General Public License +for libraries did not effectively promote software sharing, because most developers +did not use the libraries. We concluded that weaker conditions might promote +sharing better. + +However, unrestricted linking of non-free programs would deprive the users +of those programs of all benefit from the free status of the libraries themselves. +This Library General Public License is intended to permit developers of non-free +programs to use free libraries, while preserving your freedom as a user of +such programs to change the free libraries that are incorporated in them. +(We have not seen how to achieve this as regards changes in header files, +but we have achieved it as regards changes in the actual functions of the +Library.) The hope is that this will lead to faster development of free libraries. + +The precise terms and conditions for copying, distribution and modification +follow. Pay close attention to the difference between a "work based on the +library" and a "work that uses the library". The former contains code derived +from the library, while the latter only works together with the library. + +Note that it is possible for a library to be covered by the ordinary General +Public License rather than by this special one. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License Agreement applies to any software library which contains a +notice placed by the copyright holder or other authorized party saying it +may be distributed under the terms of this Library General Public License +(also called "this License"). Each licensee is addressed as "you". + +A "library" means a collection of software functions and/or data prepared +so as to be conveniently linked with application programs (which use some +of those functions and data) to form executables. + +The "Library", below, refers to any such software library or work which has +been distributed under these terms. A "work based on the Library" means either +the Library or any derivative work under copyright law: that is to say, a +work containing the Library or a portion of it, either verbatim or with modifications +and/or translated straightforwardly into another language. (Hereinafter, translation +is included without limitation in the term "modification".) + +"Source code" for a work means the preferred form of the work for making modifications +to it. For a library, complete source code means all the source code for all +modules it contains, plus any associated interface definition files, plus +the scripts used to control compilation and installation of the library. + +Activities other than copying, distribution and modification are not covered +by this License; they are outside its scope. The act of running a program +using the Library is not restricted, and output from such a program is covered +only if its contents constitute a work based on the Library (independent of +the use of the Library in a tool for writing it). Whether that is true depends +on what the Library does and what the program that uses the Library does. + +1. You may copy and distribute verbatim copies of the Library's complete source +code as you receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice and disclaimer +of warranty; keep intact all the notices that refer to this License and to +the absence of any warranty; and distribute a copy of this License along with +the Library. + +You may charge a fee for the physical act of transferring a copy, and you +may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Library or any portion of it, +thus forming a work based on the Library, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all +of these conditions: + + a) The modified work must itself be a software library. + +b) You must cause the files modified to carry prominent notices stating that +you changed the files and the date of any change. + +c) You must cause the whole of the work to be licensed at no charge to all +third parties under the terms of this License. + +d) If a facility in the modified Library refers to a function or a table of +data to be supplied by an application program that uses the facility, other +than as an argument passed when the facility is invoked, then you must make +a good faith effort to ensure that, in the event an application does not supply +such function or table, the facility still operates, and performs whatever +part of its purpose remains meaningful. + +(For example, a function in a library to compute square roots has a purpose +that is entirely well-defined independent of the application. Therefore, Subsection +2d requires that any application-supplied function or table used by this function +must be optional: if the application does not supply it, the square root function +must still compute square roots.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Library, and can be reasonably +considered independent and separate works in themselves, then this License, +and its terms, do not apply to those sections when you distribute them as +separate works. But when you distribute the same sections as part of a whole +which is a work based on the Library, the distribution of the whole must be +on the terms of this License, whose permissions for other licensees extend +to the entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise +the right to control the distribution of derivative or collective works based +on the Library. + +In addition, mere aggregation of another work not based on the Library with +the Library (or with a work based on the Library) on a volume of a storage +or distribution medium does not bring the other work under the scope of this +License. + +3. You may opt to apply the terms of the ordinary GNU General Public License +instead of this License to a given copy of the Library. To do this, you must +alter all the notices that refer to this License, so that they refer to the +ordinary GNU General Public License, version 2, instead of to this License. +(If a newer version than version 2 of the ordinary GNU General Public License +has appeared, then you can specify that version instead if you wish.) Do not +make any other change in these notices. + +Once this change is made in a given copy, it is irreversible for that copy, +so the ordinary GNU General Public License applies to all subsequent copies +and derivative works made from that copy. + +This option is useful when you wish to copy part of the code of the Library +into a program that is not a library. + +4. You may copy and distribute the Library (or a portion or derivative of +it, under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you accompany it with the complete corresponding +machine-readable source code, which must be distributed under the terms of +Sections 1 and 2 above on a medium customarily used for software interchange. + +If distribution of object code is made by offering access to copy from a designated +place, then offering equivalent access to copy the source code from the same +place satisfies the requirement to distribute the source code, even though +third parties are not compelled to copy the source along with the object code. + +5. A program that contains no derivative of any portion of the Library, but +is designed to work with the Library by being compiled or linked with it, +is called a "work that uses the Library". Such a work, in isolation, is not +a derivative work of the Library, and therefore falls outside the scope of +this License. + +However, linking a "work that uses the Library" with the Library creates an +executable that is a derivative of the Library (because it contains portions +of the Library), rather than a "work that uses the library". The executable +is therefore covered by this License. Section 6 states terms for distribution +of such executables. + +When a "work that uses the Library" uses material from a header file that +is part of the Library, the object code for the work may be a derivative work +of the Library even though the source code is not. Whether this is true is +especially significant if the work can be linked without the Library, or if +the work is itself a library. The threshold for this to be true is not precisely +defined by law. + +If such an object file uses only numerical parameters, data structure layouts +and accessors, and small macros and small inline functions (ten lines or less +in length), then the use of the object file is unrestricted, regardless of +whether it is legally a derivative work. (Executables containing this object +code plus portions of the Library will still fall under Section 6.) + +Otherwise, if the work is a derivative of the Library, you may distribute +the object code for the work under the terms of Section 6. Any executables +containing that work also fall under Section 6, whether or not they are linked +directly with the Library itself. + +6. As an exception to the Sections above, you may also compile or link a "work +that uses the Library" with the Library to produce a work containing portions +of the Library, and distribute that work under terms of your choice, provided +that the terms permit modification of the work for the customer's own use +and reverse engineering for debugging such modifications. + +You must give prominent notice with each copy of the work that the Library +is used in it and that the Library and its use are covered by this License. +You must supply a copy of this License. If the work during execution displays +copyright notices, you must include the copyright notice for the Library among +them, as well as a reference directing the user to the copy of this License. +Also, you must do one of these things: + +a) Accompany the work with the complete corresponding machine-readable source +code for the Library including whatever changes were used in the work (which +must be distributed under Sections 1 and 2 above); and, if the work is an +executable linked with the Library, with the complete machine-readable "work +that uses the Library", as object code and/or source code, so that the user +can modify the Library and then relink to produce a modified executable containing +the modified Library. (It is understood that the user who changes the contents +of definitions files in the Library will not necessarily be able to recompile +the application to use the modified definitions.) + +b) Accompany the work with a written offer, valid for at least three years, +to give the same user the materials specified in Subsection 6a, above, for +a charge no more than the cost of performing this distribution. + +c) If distribution of the work is made by offering access to copy from a designated +place, offer equivalent access to copy the above specified materials from +the same place. + +d) Verify that the user has already received a copy of these materials or +that you have already sent this user a copy. + +For an executable, the required form of the "work that uses the Library" must +include any data and utility programs needed for reproducing the executable +from it. However, as a special exception, the source code distributed need +not include anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the operating +system on which the executable runs, unless that component itself accompanies +the executable. + +It may happen that this requirement contradicts the license restrictions of +other proprietary libraries that do not normally accompany the operating system. +Such a contradiction means you cannot use both them and the Library together +in an executable that you distribute. + +7. You may place library facilities that are a work based on the Library side-by-side +in a single library together with other library facilities not covered by +this License, and distribute such a combined library, provided that the separate +distribution of the work based on the Library and of the other library facilities +is otherwise permitted, and provided that you do these two things: + +a) Accompany the combined library with a copy of the same work based on the +Library, uncombined with any other library facilities. This must be distributed +under the terms of the Sections above. + +b) Give prominent notice with the combined library of the fact that part of +it is a work based on the Library, and explaining where to find the accompanying +uncombined form of the same work. + +8. You may not copy, modify, sublicense, link with, or distribute the Library +except as expressly provided under this License. Any attempt otherwise to +copy, modify, sublicense, link with, or distribute the Library is void, and +will automatically terminate your rights under this License. However, parties +who have received copies, or rights, from you under this License will not +have their licenses terminated so long as such parties remain in full compliance. + +9. You are not required to accept this License, since you have not signed +it. However, nothing else grants you permission to modify or distribute the +Library or its derivative works. These actions are prohibited by law if you +do not accept this License. Therefore, by modifying or distributing the Library +(or any work based on the Library), you indicate your acceptance of this License +to do so, and all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + +10. Each time you redistribute the Library (or any work based on the Library), +the recipient automatically receives a license from the original licensor +to copy, distribute, link with or modify the Library subject to these terms +and conditions. You may not impose any further restrictions on the recipients' +exercise of the rights granted herein. You are not responsible for enforcing +compliance by third parties to this License. + +11. If, as a consequence of a court judgment or allegation of patent infringement +or for any other reason (not limited to patent issues), conditions are imposed +on you (whether by court order, agreement or otherwise) that contradict the +conditions of this License, they do not excuse you from the conditions of +this License. If you cannot distribute so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, then as +a consequence you may not distribute the Library at all. For example, if a +patent license would not permit royalty-free redistribution of the Library +by all those who receive copies directly or indirectly through you, then the +only way you could satisfy both it and this License would be to refrain entirely +from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents +or other property right claims or to contest validity of any such claims; +this section has the sole purpose of protecting the integrity of the free +software distribution system which is implemented by public license practices. +Many people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose +that choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +12. If the distribution and/or use of the Library is restricted in certain +countries either by patents or by copyrighted interfaces, the original copyright +holder who places the Library under this License may add an explicit geographical +distribution limitation excluding those countries, so that distribution is +permitted only in or among countries not thus excluded. In such case, this +License incorporates the limitation as if written in the body of this License. + +13. The Free Software Foundation may publish revised and/or new versions of +the Library General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to address +new problems or concerns. + +Each version is given a distinguishing version number. If the Library specifies +a version number of this License which applies to it and "any later version", +you have the option of following the terms and conditions either of that version +or of any later version published by the Free Software Foundation. If the +Library does not specify a license version number, you may choose any version +ever published by the Free Software Foundation. + +14. If you wish to incorporate parts of the Library into other free programs +whose distribution conditions are incompatible with these, write to the author +to ask for permission. For software which is copyrighted by the Free Software +Foundation, write to the Free Software Foundation; we sometimes make exceptions +for this. Our decision will be guided by the two goals of preserving the free +status of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + +15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY +"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE +OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE +THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE +OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA +OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES +OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH +HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Libraries + +If you develop a new library, and you want it to be of the greatest possible +use to the public, we recommend making it free software that everyone can +redistribute and change. You can do so by permitting redistribution under +these terms (or, alternatively, under the terms of the ordinary General Public +License). + +To apply these terms, attach the following notices to the library. It is safest +to attach them to the start of each source file to most effectively convey +the exclusion of warranty; and each file should have at least the "copyright" +line and a pointer to where the full notice is found. + +one line to give the library's name and an idea of what it does. + +Copyright (C) year name of author + +This library is free software; you can redistribute it and/or modify it under +the terms of the GNU Library General Public License as published by the Free +Software Foundation; either version 2 of the License, or (at your option) +any later version. + +This library is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more +details. + +You should have received a copy of the GNU Library General Public License +along with this library; if not, write to the Free Software Foundation, Inc., +51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the library, if necessary. Here +is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in + +the library `Frob' (a library for tweaking knobs) written + +by James Random Hacker. + +signature of Ty Coon, 1 April 1990 + +Ty Coon, President of Vice + +That's all there is to it! diff --git a/LICENSES/LGPL-2.1-only.txt b/LICENSES/LGPL-2.1-only.txt new file mode 100644 index 0000000..130dffb --- /dev/null +++ b/LICENSES/LGPL-2.1-only.txt @@ -0,0 +1,467 @@ +GNU LESSER GENERAL PUBLIC LICENSE + +Version 2.1, February 1999 + +Copyright (C) 1991, 1999 Free Software Foundation, Inc. + +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts as the +successor of the GNU Library Public License, version 2, hence the version +number 2.1.] + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public Licenses are intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. + +This license, the Lesser General Public License, applies to some specially +designated software packages--typically libraries--of the Free Software Foundation +and other authors who decide to use it. You can use it too, but we suggest +you first think carefully about whether this license or the ordinary General +Public License is the better strategy to use in any particular case, based +on the explanations below. + +When we speak of free software, we are referring to freedom of use, not price. +Our General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for this service if you +wish); that you receive source code or can get it if you want it; that you +can change the software and use pieces of it in new free programs; and that +you are informed that you can do these things. + +To protect your rights, we need to make restrictions that forbid distributors +to deny you these rights or to ask you to surrender these rights. These restrictions +translate to certain responsibilities for you if you distribute copies of +the library or if you modify it. + +For example, if you distribute copies of the library, whether gratis or for +a fee, you must give the recipients all the rights that we gave you. You must +make sure that they, too, receive or can get the source code. If you link +other code with the library, you must provide complete object files to the +recipients, so that they can relink them with the library after making changes +to the library and recompiling it. And you must show them these terms so they +know their rights. + +We protect your rights with a two-step method: (1) we copyright the library, +and (2) we offer you this license, which gives you legal permission to copy, +distribute and/or modify the library. + +To protect each distributor, we want to make it very clear that there is no +warranty for the free library. Also, if the library is modified by someone +else and passed on, the recipients should know that what they have is not +the original version, so that the original author's reputation will not be +affected by problems that might be introduced by others. + +Finally, software patents pose a constant threat to the existence of any free +program. We wish to make sure that a company cannot effectively restrict the +users of a free program by obtaining a restrictive license from a patent holder. +Therefore, we insist that any patent license obtained for a version of the +library must be consistent with the full freedom of use specified in this +license. + +Most GNU software, including some libraries, is covered by the ordinary GNU +General Public License. This license, the GNU Lesser General Public License, +applies to certain designated libraries, and is quite different from the ordinary +General Public License. We use this license for certain libraries in order +to permit linking those libraries into non-free programs. + +When a program is linked with a library, whether statically or using a shared +library, the combination of the two is legally speaking a combined work, a +derivative of the original library. The ordinary General Public License therefore +permits such linking only if the entire combination fits its criteria of freedom. +The Lesser General Public License permits more lax criteria for linking other +code with the library. + +We call this license the "Lesser" General Public License because it does Less +to protect the user's freedom than the ordinary General Public License. It +also provides other free software developers Less of an advantage over competing +non-free programs. These disadvantages are the reason we use the ordinary +General Public License for many libraries. However, the Lesser license provides +advantages in certain special circumstances. + +For example, on rare occasions, there may be a special need to encourage the +widest possible use of a certain library, so that it becomes a de-facto standard. +To achieve this, non-free programs must be allowed to use the library. A more +frequent case is that a free library does the same job as widely used non-free +libraries. In this case, there is little to gain by limiting the free library +to free software only, so we use the Lesser General Public License. + +In other cases, permission to use a particular library in non-free programs +enables a greater number of people to use a large body of free software. For +example, permission to use the GNU C Library in non-free programs enables +many more people to use the whole GNU operating system, as well as its variant, +the GNU/Linux operating system. + +Although the Lesser General Public License is Less protective of the users' +freedom, it does ensure that the user of a program that is linked with the +Library has the freedom and the wherewithal to run that program using a modified +version of the Library. + +The precise terms and conditions for copying, distribution and modification +follow. Pay close attention to the difference between a "work based on the +library" and a "work that uses the library". The former contains code derived +from the library, whereas the latter must be combined with the library in +order to run. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License Agreement applies to any software library or other program +which contains a notice placed by the copyright holder or other authorized +party saying it may be distributed under the terms of this Lesser General +Public License (also called "this License"). Each licensee is addressed as +"you". + +A "library" means a collection of software functions and/or data prepared +so as to be conveniently linked with application programs (which use some +of those functions and data) to form executables. + +The "Library", below, refers to any such software library or work which has +been distributed under these terms. A "work based on the Library" means either +the Library or any derivative work under copyright law: that is to say, a +work containing the Library or a portion of it, either verbatim or with modifications +and/or translated straightforwardly into another language. (Hereinafter, translation +is included without limitation in the term "modification".) + +"Source code" for a work means the preferred form of the work for making modifications +to it. For a library, complete source code means all the source code for all +modules it contains, plus any associated interface definition files, plus +the scripts used to control compilation and installation of the library. + +Activities other than copying, distribution and modification are not covered +by this License; they are outside its scope. The act of running a program +using the Library is not restricted, and output from such a program is covered +only if its contents constitute a work based on the Library (independent of +the use of the Library in a tool for writing it). Whether that is true depends +on what the Library does and what the program that uses the Library does. + +1. You may copy and distribute verbatim copies of the Library's complete source +code as you receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice and disclaimer +of warranty; keep intact all the notices that refer to this License and to +the absence of any warranty; and distribute a copy of this License along with +the Library. + +You may charge a fee for the physical act of transferring a copy, and you +may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Library or any portion of it, +thus forming a work based on the Library, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all +of these conditions: + + a) The modified work must itself be a software library. + +b) You must cause the files modified to carry prominent notices stating that +you changed the files and the date of any change. + +c) You must cause the whole of the work to be licensed at no charge to all +third parties under the terms of this License. + +d) If a facility in the modified Library refers to a function or a table of +data to be supplied by an application program that uses the facility, other +than as an argument passed when the facility is invoked, then you must make +a good faith effort to ensure that, in the event an application does not supply +such function or table, the facility still operates, and performs whatever +part of its purpose remains meaningful. + +(For example, a function in a library to compute square roots has a purpose +that is entirely well-defined independent of the application. Therefore, Subsection +2d requires that any application-supplied function or table used by this function +must be optional: if the application does not supply it, the square root function +must still compute square roots.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Library, and can be reasonably +considered independent and separate works in themselves, then this License, +and its terms, do not apply to those sections when you distribute them as +separate works. But when you distribute the same sections as part of a whole +which is a work based on the Library, the distribution of the whole must be +on the terms of this License, whose permissions for other licensees extend +to the entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise +the right to control the distribution of derivative or collective works based +on the Library. + +In addition, mere aggregation of another work not based on the Library with +the Library (or with a work based on the Library) on a volume of a storage +or distribution medium does not bring the other work under the scope of this +License. + +3. You may opt to apply the terms of the ordinary GNU General Public License +instead of this License to a given copy of the Library. To do this, you must +alter all the notices that refer to this License, so that they refer to the +ordinary GNU General Public License, version 2, instead of to this License. +(If a newer version than version 2 of the ordinary GNU General Public License +has appeared, then you can specify that version instead if you wish.) Do not +make any other change in these notices. + +Once this change is made in a given copy, it is irreversible for that copy, +so the ordinary GNU General Public License applies to all subsequent copies +and derivative works made from that copy. + +This option is useful when you wish to copy part of the code of the Library +into a program that is not a library. + +4. You may copy and distribute the Library (or a portion or derivative of +it, under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you accompany it with the complete corresponding +machine-readable source code, which must be distributed under the terms of +Sections 1 and 2 above on a medium customarily used for software interchange. + +If distribution of object code is made by offering access to copy from a designated +place, then offering equivalent access to copy the source code from the same +place satisfies the requirement to distribute the source code, even though +third parties are not compelled to copy the source along with the object code. + +5. A program that contains no derivative of any portion of the Library, but +is designed to work with the Library by being compiled or linked with it, +is called a "work that uses the Library". Such a work, in isolation, is not +a derivative work of the Library, and therefore falls outside the scope of +this License. + +However, linking a "work that uses the Library" with the Library creates an +executable that is a derivative of the Library (because it contains portions +of the Library), rather than a "work that uses the library". The executable +is therefore covered by this License. Section 6 states terms for distribution +of such executables. + +When a "work that uses the Library" uses material from a header file that +is part of the Library, the object code for the work may be a derivative work +of the Library even though the source code is not. Whether this is true is +especially significant if the work can be linked without the Library, or if +the work is itself a library. The threshold for this to be true is not precisely +defined by law. + +If such an object file uses only numerical parameters, data structure layouts +and accessors, and small macros and small inline functions (ten lines or less +in length), then the use of the object file is unrestricted, regardless of +whether it is legally a derivative work. (Executables containing this object +code plus portions of the Library will still fall under Section 6.) + +Otherwise, if the work is a derivative of the Library, you may distribute +the object code for the work under the terms of Section 6. Any executables +containing that work also fall under Section 6, whether or not they are linked +directly with the Library itself. + +6. As an exception to the Sections above, you may also combine or link a "work +that uses the Library" with the Library to produce a work containing portions +of the Library, and distribute that work under terms of your choice, provided +that the terms permit modification of the work for the customer's own use +and reverse engineering for debugging such modifications. + +You must give prominent notice with each copy of the work that the Library +is used in it and that the Library and its use are covered by this License. +You must supply a copy of this License. If the work during execution displays +copyright notices, you must include the copyright notice for the Library among +them, as well as a reference directing the user to the copy of this License. +Also, you must do one of these things: + +a) Accompany the work with the complete corresponding machine-readable source +code for the Library including whatever changes were used in the work (which +must be distributed under Sections 1 and 2 above); and, if the work is an +executable linked with the Library, with the complete machine-readable "work +that uses the Library", as object code and/or source code, so that the user +can modify the Library and then relink to produce a modified executable containing +the modified Library. (It is understood that the user who changes the contents +of definitions files in the Library will not necessarily be able to recompile +the application to use the modified definitions.) + +b) Use a suitable shared library mechanism for linking with the Library. A +suitable mechanism is one that (1) uses at run time a copy of the library +already present on the user's computer system, rather than copying library +functions into the executable, and (2) will operate properly with a modified +version of the library, if the user installs one, as long as the modified +version is interface-compatible with the version that the work was made with. + +c) Accompany the work with a written offer, valid for at least three years, +to give the same user the materials specified in Subsection 6a, above, for +a charge no more than the cost of performing this distribution. + +d) If distribution of the work is made by offering access to copy from a designated +place, offer equivalent access to copy the above specified materials from +the same place. + +e) Verify that the user has already received a copy of these materials or +that you have already sent this user a copy. + +For an executable, the required form of the "work that uses the Library" must +include any data and utility programs needed for reproducing the executable +from it. However, as a special exception, the materials to be distributed +need not include anything that is normally distributed (in either source or +binary form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component itself +accompanies the executable. + +It may happen that this requirement contradicts the license restrictions of +other proprietary libraries that do not normally accompany the operating system. +Such a contradiction means you cannot use both them and the Library together +in an executable that you distribute. + +7. You may place library facilities that are a work based on the Library side-by-side +in a single library together with other library facilities not covered by +this License, and distribute such a combined library, provided that the separate +distribution of the work based on the Library and of the other library facilities +is otherwise permitted, and provided that you do these two things: + +a) Accompany the combined library with a copy of the same work based on the +Library, uncombined with any other library facilities. This must be distributed +under the terms of the Sections above. + +b) Give prominent notice with the combined library of the fact that part of +it is a work based on the Library, and explaining where to find the accompanying +uncombined form of the same work. + +8. You may not copy, modify, sublicense, link with, or distribute the Library +except as expressly provided under this License. Any attempt otherwise to +copy, modify, sublicense, link with, or distribute the Library is void, and +will automatically terminate your rights under this License. However, parties +who have received copies, or rights, from you under this License will not +have their licenses terminated so long as such parties remain in full compliance. + +9. You are not required to accept this License, since you have not signed +it. However, nothing else grants you permission to modify or distribute the +Library or its derivative works. These actions are prohibited by law if you +do not accept this License. Therefore, by modifying or distributing the Library +(or any work based on the Library), you indicate your acceptance of this License +to do so, and all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + +10. Each time you redistribute the Library (or any work based on the Library), +the recipient automatically receives a license from the original licensor +to copy, distribute, link with or modify the Library subject to these terms +and conditions. You may not impose any further restrictions on the recipients' +exercise of the rights granted herein. You are not responsible for enforcing +compliance by third parties with this License. + +11. If, as a consequence of a court judgment or allegation of patent infringement +or for any other reason (not limited to patent issues), conditions are imposed +on you (whether by court order, agreement or otherwise) that contradict the +conditions of this License, they do not excuse you from the conditions of +this License. If you cannot distribute so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, then as +a consequence you may not distribute the Library at all. For example, if a +patent license would not permit royalty-free redistribution of the Library +by all those who receive copies directly or indirectly through you, then the +only way you could satisfy both it and this License would be to refrain entirely +from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents +or other property right claims or to contest validity of any such claims; +this section has the sole purpose of protecting the integrity of the free +software distribution system which is implemented by public license practices. +Many people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose +that choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +12. If the distribution and/or use of the Library is restricted in certain +countries either by patents or by copyrighted interfaces, the original copyright +holder who places the Library under this License may add an explicit geographical +distribution limitation excluding those countries, so that distribution is +permitted only in or among countries not thus excluded. In such case, this +License incorporates the limitation as if written in the body of this License. + +13. The Free Software Foundation may publish revised and/or new versions of +the Lesser General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to address +new problems or concerns. + +Each version is given a distinguishing version number. If the Library specifies +a version number of this License which applies to it and "any later version", +you have the option of following the terms and conditions either of that version +or of any later version published by the Free Software Foundation. If the +Library does not specify a license version number, you may choose any version +ever published by the Free Software Foundation. + +14. If you wish to incorporate parts of the Library into other free programs +whose distribution conditions are incompatible with these, write to the author +to ask for permission. For software which is copyrighted by the Free Software +Foundation, write to the Free Software Foundation; we sometimes make exceptions +for this. Our decision will be guided by the two goals of preserving the free +status of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + +15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY +"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE +OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE +THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE +OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA +OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES +OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH +HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Libraries + +If you develop a new library, and you want it to be of the greatest possible +use to the public, we recommend making it free software that everyone can +redistribute and change. You can do so by permitting redistribution under +these terms (or, alternatively, under the terms of the ordinary General Public +License). + +To apply these terms, attach the following notices to the library. It is safest +to attach them to the start of each source file to most effectively convey +the exclusion of warranty; and each file should have at least the "copyright" +line and a pointer to where the full notice is found. + +< one line to give the library's name and an idea of what it does. > + +Copyright (C) < year > < name of author > + +This library is free software; you can redistribute it and/or modify it under +the terms of the GNU Lesser General Public License as published by the Free +Software Foundation; either version 2.1 of the License, or (at your option) +any later version. + +This library is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +details. + +You should have received a copy of the GNU Lesser General Public License along +with this library; if not, write to the Free Software Foundation, Inc., 51 +Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information +on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the library, if necessary. Here +is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in + +the library `Frob' (a library for tweaking knobs) written + +by James Random Hacker. + +< signature of Ty Coon > , 1 April 1990 + +Ty Coon, President of Vice + +That's all there is to it! diff --git a/LICENSES/LGPL-3.0-only.txt b/LICENSES/LGPL-3.0-only.txt new file mode 100644 index 0000000..bd405af --- /dev/null +++ b/LICENSES/LGPL-3.0-only.txt @@ -0,0 +1,163 @@ +GNU LESSER GENERAL PUBLIC LICENSE + +Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +This version of the GNU Lesser General Public License incorporates the terms +and conditions of version 3 of the GNU General Public License, supplemented +by the additional permissions listed below. + + 0. Additional Definitions. + + + +As used herein, "this License" refers to version 3 of the GNU Lesser General +Public License, and the "GNU GPL" refers to version 3 of the GNU General Public +License. + + + +"The Library" refers to a covered work governed by this License, other than +an Application or a Combined Work as defined below. + + + +An "Application" is any work that makes use of an interface provided by the +Library, but which is not otherwise based on the Library. Defining a subclass +of a class defined by the Library is deemed a mode of using an interface provided +by the Library. + + + +A "Combined Work" is a work produced by combining or linking an Application +with the Library. The particular version of the Library with which the Combined +Work was made is also called the "Linked Version". + + + +The "Minimal Corresponding Source" for a Combined Work means the Corresponding +Source for the Combined Work, excluding any source code for portions of the +Combined Work that, considered in isolation, are based on the Application, +and not on the Linked Version. + + + +The "Corresponding Application Code" for a Combined Work means the object +code and/or source code for the Application, including any data and utility +programs needed for reproducing the Combined Work from the Application, but +excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + +You may convey a covered work under sections 3 and 4 of this License without +being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + +If you modify a copy of the Library, and, in your modifications, a facility +refers to a function or data to be supplied by an Application that uses the +facility (other than as an argument passed when the facility is invoked), +then you may convey a copy of the modified version: + +a) under this License, provided that you make a good faith effort to ensure +that, in the event an Application does not supply the function or data, the +facility still operates, and performs whatever part of its purpose remains +meaningful, or + +b) under the GNU GPL, with none of the additional permissions of this License +applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + +The object code form of an Application may incorporate material from a header +file that is part of the Library. You may convey such object code under terms +of your choice, provided that, if the incorporated material is not limited +to numerical parameters, data structure layouts and accessors, or small macros, +inline functions and templates (ten or fewer lines in length), you do both +of the following: + +a) Give prominent notice with each copy of the object code that the Library +is used in it and that the Library and its use are covered by this License. + +b) Accompany the object code with a copy of the GNU GPL and this license document. + + 4. Combined Works. + +You may convey a Combined Work under terms of your choice that, taken together, +effectively do not restrict modification of the portions of the Library contained +in the Combined Work and reverse engineering for debugging such modifications, +if you also do each of the following: + +a) Give prominent notice with each copy of the Combined Work that the Library +is used in it and that the Library and its use are covered by this License. + +b) Accompany the Combined Work with a copy of the GNU GPL and this license +document. + +c) For a Combined Work that displays copyright notices during execution, include +the copyright notice for the Library among these notices, as well as a reference +directing the user to the copies of the GNU GPL and this license document. + + d) Do one of the following: + +0) Convey the Minimal Corresponding Source under the terms of this License, +and the Corresponding Application Code in a form suitable for, and under terms +that permit, the user to recombine or relink the Application with a modified +version of the Linked Version to produce a modified Combined Work, in the +manner specified by section 6 of the GNU GPL for conveying Corresponding Source. + +1) Use a suitable shared library mechanism for linking with the Library. A +suitable mechanism is one that (a) uses at run time a copy of the Library +already present on the user's computer system, and (b) will operate properly +with a modified version of the Library that is interface-compatible with the +Linked Version. + +e) Provide Installation Information, but only if you would otherwise be required +to provide such information under section 6 of the GNU GPL, and only to the +extent that such information is necessary to install and execute a modified +version of the Combined Work produced by recombining or relinking the Application +with a modified version of the Linked Version. (If you use option 4d0, the +Installation Information must accompany the Minimal Corresponding Source and +Corresponding Application Code. If you use option 4d1, you must provide the +Installation Information in the manner specified by section 6 of the GNU GPL +for conveying Corresponding Source.) + + 5. Combined Libraries. + +You may place library facilities that are a work based on the Library side +by side in a single library together with other library facilities that are +not Applications and are not covered by this License, and convey such a combined +library under terms of your choice, if you do both of the following: + +a) Accompany the combined library with a copy of the same work based on the +Library, uncombined with any other library facilities, conveyed under the +terms of this License. + +b) Give prominent notice with the combined library that part of it is a work +based on the Library, and explaining where to find the accompanying uncombined +form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + +The Free Software Foundation may publish revised and/or new versions of the +GNU Lesser General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to address +new problems or concerns. + +Each version is given a distinguishing version number. If the Library as you +received it specifies that a certain numbered version of the GNU Lesser General +Public License "or any later version" applies to it, you have the option of +following the terms and conditions either of that published version or of +any later version published by the Free Software Foundation. If the Library +as you received it does not specify a version number of the GNU Lesser General +Public License, you may choose any version of the GNU Lesser General Public +License ever published by the Free Software Foundation. + +If the Library as you received it specifies that a proxy can decide whether +future versions of the GNU Lesser General Public License shall apply, that +proxy's public statement of acceptance of any version is permanent authorization +for you to choose that version for the Library. diff --git a/LICENSES/LicenseRef-KDE-Accepted-GPL.txt b/LICENSES/LicenseRef-KDE-Accepted-GPL.txt new file mode 100644 index 0000000..60a2dff --- /dev/null +++ b/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/LICENSES/LicenseRef-KDE-Accepted-LGPL.txt b/LICENSES/LicenseRef-KDE-Accepted-LGPL.txt new file mode 100644 index 0000000..232b3c5 --- /dev/null +++ b/LICENSES/LicenseRef-KDE-Accepted-LGPL.txt @@ -0,0 +1,12 @@ +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the license or (at your option) any later version +that is accepted by the membership of KDE e.V. (or its successor +approved by the membership of KDE e.V.), which shall act as a +proxy as defined in Section 6 of version 3 of the license. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. diff --git a/LICENSES/MIT.txt b/LICENSES/MIT.txt new file mode 100644 index 0000000..204b93d --- /dev/null +++ b/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/Mainpage.dox b/Mainpage.dox new file mode 100644 index 0000000..c0d4e69 --- /dev/null +++ b/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/Messages.sh b/Messages.sh new file mode 100644 index 0000000..f4d4382 --- /dev/null +++ b/Messages.sh @@ -0,0 +1,3 @@ +#! /usr/bin/env bash +$EXTRACTRC *.kcfg *.ui >> rc.cpp +$XGETTEXT *.h *.cpp colorcorrection/*.cpp helpers/killer/*.cpp plugins/scenes/opengl/*.cpp tabbox/*.cpp scripting/*.cpp -o $podir/kwin.pot diff --git a/README.md b/README.md new file mode 100644 index 0000000..595e307 --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# KWin + +KWin is an easy to use, but flexible, composited Window Manager for Xorg windowing systems (Wayland, X11) 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 window manager (c.f. openbox, i3) 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 without compositing or for X11 network transparency, though both are possible. + +# Contacting KWin development team + + * mailing list: [kwin@kde.org](https://mail.kde.org/mailman/listinfo/kwin) + * IRC: #kwin on freenode + +# 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). + +# Developing on KWin +Please refer to [hacking documentation](HACKING.md) for how to build and start KWin. Further information about KWin's test suite can be found in [TESTING.md](TESTING.md). + +## 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 (xrandr) + * 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. \ No newline at end of file diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..3465588 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,33 @@ +# Testing in KWin +KWin provides a unit and integration test suit 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. + +# Running the test suite +The test suite can be run from the build directory. Best is to do: + + cd path/to/build/directory + 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. To prevent side effects with the running session it is recommended to start a dedicated dbus session: + + 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/abstract_client.cpp b/abstract_client.cpp new file mode 100644 index 0000000..2984f67 --- /dev/null +++ b/abstract_client.cpp @@ -0,0 +1,3525 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "abstract_client.h" + +#include "appmenu.h" +#include "decorations/decoratedclient.h" +#include "decorations/decorationpalette.h" +#include "decorations/decorationbridge.h" +#include "cursor.h" +#include "effects.h" +#include "focuschain.h" +#include "outline.h" +#include "screens.h" +#ifdef KWIN_BUILD_TABBOX +#include "tabbox.h" +#endif +#include "screenedge.h" +#include "useractions.h" +#include "workspace.h" + +#include "wayland_server.h" +#include + +#include + +#include + +#include +#include +#include + +namespace KWin +{ + +static inline int sign(int v) +{ + return (v > 0) - (v < 0); +} + +QHash> AbstractClient::s_palettes; +std::shared_ptr AbstractClient::s_defaultPalette; + +AbstractClient::AbstractClient() + : Toplevel() +#ifdef KWIN_BUILD_TABBOX + , m_tabBoxClient(QSharedPointer(new TabBox::TabBoxClientImpl(this))) +#endif + , m_colorScheme(QStringLiteral("kdeglobals")) +{ + connect(this, &AbstractClient::clientStartUserMovedResized, this, &AbstractClient::moveResizedChanged); + connect(this, &AbstractClient::clientFinishUserMovedResized, this, &AbstractClient::moveResizedChanged); + connect(this, &AbstractClient::clientStartUserMovedResized, this, &AbstractClient::removeCheckScreenConnection); + connect(this, &AbstractClient::clientFinishUserMovedResized, this, &AbstractClient::setupCheckScreenConnection); + + connect(this, &AbstractClient::paletteChanged, this, &AbstractClient::triggerDecorationRepaint); + + connect(Decoration::DecorationBridge::self(), &QObject::destroyed, this, &AbstractClient::destroyDecoration); + + // If the user manually moved the window, don't restore it after the keyboard closes + connect(this, &AbstractClient::clientFinishUserMovedResized, this, [this] () { + m_keyboardGeometryRestore = QRect(); + }); + connect(this, qOverload(&AbstractClient::clientMaximizedStateChanged), this, [this] () { + m_keyboardGeometryRestore = QRect(); + }); + connect(this, &AbstractClient::fullScreenChanged, this, [this] () { + m_keyboardGeometryRestore = QRect(); + }); + + // replace on-screen-display on size changes + connect(this, &AbstractClient::frameGeometryChanged, this, + [this] (Toplevel *c, const QRect &old) { + Q_UNUSED(c) + if (isOnScreenDisplay() && !frameGeometry().isEmpty() && old.size() != frameGeometry().size() && !isInitialPositionSet()) { + GeometryUpdatesBlocker blocker(this); + const QRect area = workspace()->clientArea(PlacementArea, Screens::self()->current(), desktop()); + Placement::self()->place(this, area); + setGeometryRestore(frameGeometry()); + } + } + ); + + connect(this, &AbstractClient::paddingChanged, this, [this]() { + m_visibleRectBeforeGeometryUpdate = visibleRect(); + }); + + connect(ApplicationMenu::self(), &ApplicationMenu::applicationMenuEnabledChanged, this, [this] { + emit hasApplicationMenuChanged(hasApplicationMenu()); + }); +} + +AbstractClient::~AbstractClient() +{ + Q_ASSERT(m_blockGeometryUpdates == 0); + Q_ASSERT(m_decoration.decoration == nullptr); +} + +void AbstractClient::updateMouseGrab() +{ +} + +bool AbstractClient::belongToSameApplication(const AbstractClient *c1, const AbstractClient *c2, SameApplicationChecks checks) +{ + return c1->belongsToSameApplication(c2, checks); +} + +bool AbstractClient::isTransient() const +{ + return false; +} + +void AbstractClient::setClientShown(bool shown) +{ + Q_UNUSED(shown) +} + +xcb_timestamp_t AbstractClient::userTime() const +{ + return XCB_TIME_CURRENT_TIME; +} + +void AbstractClient::setSkipSwitcher(bool set) +{ + set = rules()->checkSkipSwitcher(set); + if (set == skipSwitcher()) + return; + m_skipSwitcher = set; + doSetSkipSwitcher(); + updateWindowRules(Rules::SkipSwitcher); + emit skipSwitcherChanged(); +} + +void AbstractClient::setSkipPager(bool b) +{ + b = rules()->checkSkipPager(b); + if (b == skipPager()) + return; + m_skipPager = b; + doSetSkipPager(); + updateWindowRules(Rules::SkipPager); + emit skipPagerChanged(); +} + +void AbstractClient::doSetSkipPager() +{ +} + +void AbstractClient::setSkipTaskbar(bool b) +{ + int was_wants_tab_focus = wantsTabFocus(); + if (b == skipTaskbar()) + return; + m_skipTaskbar = b; + doSetSkipTaskbar(); + updateWindowRules(Rules::SkipTaskbar); + if (was_wants_tab_focus != wantsTabFocus()) { + FocusChain::self()->update(this, isActive() ? FocusChain::MakeFirst : FocusChain::Update); + } + emit skipTaskbarChanged(); +} + +void AbstractClient::setOriginalSkipTaskbar(bool b) +{ + m_originalSkipTaskbar = rules()->checkSkipTaskbar(b); + setSkipTaskbar(m_originalSkipTaskbar); +} + +void AbstractClient::doSetSkipTaskbar() +{ + +} + +void AbstractClient::doSetSkipSwitcher() +{ + +} + +void AbstractClient::setIcon(const QIcon &icon) +{ + m_icon = icon; + emit iconChanged(); +} + +void AbstractClient::setActive(bool act) +{ + if (isZombie()) { + return; + } + if (m_active == act) { + return; + } + m_active = act; + const int ruledOpacity = m_active + ? rules()->checkOpacityActive(qRound(opacity() * 100.0)) + : rules()->checkOpacityInactive(qRound(opacity() * 100.0)); + setOpacity(ruledOpacity / 100.0); + workspace()->setActiveClient(act ? this : nullptr); + + if (!m_active) + cancelAutoRaise(); + + if (!m_active && shadeMode() == ShadeActivated) + setShade(ShadeNormal); + + StackingUpdatesBlocker blocker(workspace()); + workspace()->updateClientLayer(this); // active windows may get different layer + auto mainclients = mainClients(); + for (auto it = mainclients.constBegin(); + it != mainclients.constEnd(); + ++it) + if ((*it)->isFullScreen()) // fullscreens go high even if their transient is active + workspace()->updateClientLayer(*it); + + doSetActive(); + emit activeChanged(); + updateMouseGrab(); +} + +void AbstractClient::doSetActive() +{ +} + +bool AbstractClient::isZombie() const +{ + return m_zombie; +} + +void AbstractClient::markAsZombie() +{ + Q_ASSERT(!m_zombie); + m_zombie = true; + addWorkspaceRepaint(visibleRect()); +} + +Layer AbstractClient::layer() const +{ + if (m_layer == UnknownLayer) + const_cast< AbstractClient* >(this)->m_layer = belongsToLayer(); + return m_layer; +} + +void AbstractClient::updateLayer() +{ + if (layer() == belongsToLayer()) + return; + StackingUpdatesBlocker blocker(workspace()); + invalidateLayer(); // invalidate, will be updated when doing restacking + for (auto it = transients().constBegin(), + end = transients().constEnd(); it != end; ++it) + (*it)->updateLayer(); +} + +void AbstractClient::placeIn(const QRect &area) +{ + // TODO: Get rid of this method eventually. We need to call setGeometryRestore() because + // checkWorkspacePosition() operates on geometryRestore() and because of quick tiling. + Placement::self()->place(this, area); + setGeometryRestore(frameGeometry()); +} + +void AbstractClient::invalidateLayer() +{ + m_layer = UnknownLayer; +} + +Layer AbstractClient::belongsToLayer() const +{ + // NOTICE while showingDesktop, desktops move to the AboveLayer + // (interchangeable w/ eg. yakuake etc. which will at first remain visible) + // and the docks move into the NotificationLayer (which is between Above- and + // ActiveLayer, so that active fullscreen windows will still cover everything) + // Since the desktop is also activated, nothing should be in the ActiveLayer, though + if (isInternal()) + return UnmanagedLayer; + if (isLockScreen()) + return UnmanagedLayer; + if (isInputMethod()) + return UnmanagedLayer; + if (isDesktop()) + return workspace()->showingDesktop() ? AboveLayer : DesktopLayer; + if (isSplash()) // no damn annoying splashscreens + return NormalLayer; // getting in the way of everything else + if (isDock()) { + if (workspace()->showingDesktop()) + return NotificationLayer; + return layerForDock(); + } + if (isOnScreenDisplay()) + return OnScreenDisplayLayer; + if (isNotification()) + return NotificationLayer; + if (isCriticalNotification()) + return CriticalNotificationLayer; + if (workspace()->showingDesktop() && belongsToDesktop()) { + return AboveLayer; + } + if (keepBelow()) + return BelowLayer; + if (isActiveFullScreen()) + return ActiveLayer; + if (keepAbove()) + return AboveLayer; + + return NormalLayer; +} + +bool AbstractClient::belongsToDesktop() const +{ + return false; +} + +Layer AbstractClient::layerForDock() const +{ + // slight hack for the 'allow window to cover panel' Kicker setting + // don't move keepbelow docks below normal window, but only to the same + // layer, so that both may be raised to cover the other + if (keepBelow()) + return NormalLayer; + if (keepAbove()) // slight hack for the autohiding panels + return AboveLayer; + return DockLayer; +} + +void AbstractClient::setKeepAbove(bool b) +{ + b = rules()->checkKeepAbove(b); + if (b && !rules()->checkKeepBelow(false)) + setKeepBelow(false); + if (b == keepAbove()) { + return; + } + m_keepAbove = b; + doSetKeepAbove(); + workspace()->updateClientLayer(this); + updateWindowRules(Rules::Above); + + emit keepAboveChanged(m_keepAbove); +} + +void AbstractClient::doSetKeepAbove() +{ +} + +void AbstractClient::setKeepBelow(bool b) +{ + b = rules()->checkKeepBelow(b); + if (b && !rules()->checkKeepAbove(false)) + setKeepAbove(false); + if (b == keepBelow()) { + return; + } + m_keepBelow = b; + doSetKeepBelow(); + workspace()->updateClientLayer(this); + updateWindowRules(Rules::Below); + + emit keepBelowChanged(m_keepBelow); +} + +void AbstractClient::doSetKeepBelow() +{ +} + +void AbstractClient::startAutoRaise() +{ + delete m_autoRaiseTimer; + m_autoRaiseTimer = new QTimer(this); + connect(m_autoRaiseTimer, &QTimer::timeout, this, &AbstractClient::autoRaise); + m_autoRaiseTimer->setSingleShot(true); + m_autoRaiseTimer->start(options->autoRaiseInterval()); +} + +void AbstractClient::cancelAutoRaise() +{ + delete m_autoRaiseTimer; + m_autoRaiseTimer = nullptr; +} + +void AbstractClient::autoRaise() +{ + workspace()->raiseClient(this); + cancelAutoRaise(); +} + +bool AbstractClient::isMostRecentlyRaised() const +{ + // The last toplevel in the unconstrained stacking order is the most recently raised one. + return workspace()->topClientOnDesktop(VirtualDesktopManager::self()->current(), -1, true, false) == this; +} + +bool AbstractClient::wantsTabFocus() const +{ + return (isNormalWindow() || isDialog()) && wantsInput(); +} + +bool AbstractClient::isSpecialWindow() const +{ + // TODO + return isDesktop() || isDock() || isSplash() || isToolbar() || isNotification() || isOnScreenDisplay() || isCriticalNotification(); +} + +void AbstractClient::demandAttention(bool set) +{ + if (isActive()) + set = false; + if (m_demandsAttention == set) + return; + m_demandsAttention = set; + doSetDemandsAttention(); + workspace()->clientAttentionChanged(this, set); + emit demandsAttentionChanged(); +} + +void AbstractClient::doSetDemandsAttention() +{ +} + +void AbstractClient::setDesktop(int desktop) +{ + const int numberOfDesktops = VirtualDesktopManager::self()->count(); + if (desktop != NET::OnAllDesktops) // Do range check + desktop = qMax(1, qMin(numberOfDesktops, desktop)); + desktop = qMin(numberOfDesktops, rules()->checkDesktop(desktop)); + + QVector desktops; + if (desktop != NET::OnAllDesktops) { + desktops << VirtualDesktopManager::self()->desktopForX11Id(desktop); + } + setDesktops(desktops); +} + +void AbstractClient::setDesktops(QVector desktops) +{ + //on x11 we can have only one desktop at a time + if (kwinApp()->operationMode() == Application::OperationModeX11 && desktops.size() > 1) { + desktops = QVector({desktops.last()}); + } + + if (desktops == m_desktops) { + return; + } + + int was_desk = AbstractClient::desktop(); + const bool wasOnCurrentDesktop = isOnCurrentDesktop() && was_desk >= 0; + + m_desktops = desktops; + + if (windowManagementInterface()) { + if (m_desktops.isEmpty()) { + windowManagementInterface()->setOnAllDesktops(true); + } else { + windowManagementInterface()->setOnAllDesktops(false); + auto currentDesktops = windowManagementInterface()->plasmaVirtualDesktops(); + for (auto desktop: m_desktops) { + if (!currentDesktops.contains(desktop->id())) { + windowManagementInterface()->addPlasmaVirtualDesktop(desktop->id()); + } else { + currentDesktops.removeOne(desktop->id()); + } + } + for (auto desktopId: currentDesktops) { + windowManagementInterface()->removePlasmaVirtualDesktop(desktopId); + } + } + } + if (info) { + info->setDesktop(desktop()); + } + + if ((was_desk == NET::OnAllDesktops) != (desktop() == NET::OnAllDesktops)) { + // onAllDesktops changed + workspace()->updateOnAllDesktopsOfTransients(this); + } + + auto transients_stacking_order = workspace()->ensureStackingOrder(transients()); + for (auto it = transients_stacking_order.constBegin(); + it != transients_stacking_order.constEnd(); + ++it) + (*it)->setDesktops(desktops); + + if (isModal()) // if a modal dialog is moved, move the mainwindow with it as otherwise + // the (just moved) modal dialog will confusingly return to the mainwindow with + // the next desktop change + { + foreach (AbstractClient * c2, mainClients()) + c2->setDesktops(desktops); + } + + doSetDesktop(); + + FocusChain::self()->update(this, FocusChain::MakeFirst); + updateWindowRules(Rules::Desktop); + + emit desktopChanged(); + if (wasOnCurrentDesktop != isOnCurrentDesktop()) + emit desktopPresenceChanged(this, was_desk); + emit x11DesktopIdsChanged(); +} + +void AbstractClient::doSetDesktop() +{ +} + +void AbstractClient::enterDesktop(VirtualDesktop *virtualDesktop) +{ + if (m_desktops.contains(virtualDesktop)) { + return; + } + auto desktops = m_desktops; + desktops.append(virtualDesktop); + setDesktops(desktops); +} + +void AbstractClient::leaveDesktop(VirtualDesktop *virtualDesktop) +{ + QVector currentDesktops; + if (m_desktops.isEmpty()) { + currentDesktops = VirtualDesktopManager::self()->desktops(); + } else { + currentDesktops = m_desktops; + } + + if (!currentDesktops.contains(virtualDesktop)) { + return; + } + auto desktops = currentDesktops; + desktops.removeOne(virtualDesktop); + setDesktops(desktops); +} + +void AbstractClient::setOnAllDesktops(bool b) +{ + if ((b && isOnAllDesktops()) || + (!b && !isOnAllDesktops())) + return; + if (b) + setDesktop(NET::OnAllDesktops); + else + setDesktop(VirtualDesktopManager::self()->current()); +} + +QVector AbstractClient::x11DesktopIds() const +{ + const auto desks = desktops(); + QVector x11Ids; + x11Ids.reserve(desks.count()); + std::transform(desks.constBegin(), desks.constEnd(), + std::back_inserter(x11Ids), + [] (const VirtualDesktop *vd) { + return vd->x11DesktopNumber(); + } + ); + return x11Ids; +} + +ShadeMode AbstractClient::shadeMode() const +{ + return m_shadeMode; +} + +bool AbstractClient::isShadeable() const +{ + return false; +} + +void AbstractClient::setShade(bool set) +{ + set ? setShade(ShadeNormal) : setShade(ShadeNone); +} + +void AbstractClient::setShade(ShadeMode mode) +{ + if (!isShadeable()) + return; + if (mode == ShadeHover && isMove()) + return; // causes geometry breaks and is probably nasty + if (isSpecialWindow() || noBorder()) + mode = ShadeNone; + + mode = rules()->checkShade(mode); + if (m_shadeMode == mode) + return; + + const bool wasShade = isShade(); + const ShadeMode previousShadeMode = shadeMode(); + m_shadeMode = mode; + + if (wasShade == isShade()) { + // Decoration may want to update after e.g. hover-shade changes + emit shadeChanged(); + return; // No real change in shaded state + } + + Q_ASSERT(isDecorated()); + GeometryUpdatesBlocker blocker(this); + + doSetShade(previousShadeMode); + + discardWindowPixmap(); + updateWindowRules(Rules::Shade); + + emit shadeChanged(); +} + +void AbstractClient::doSetShade(ShadeMode previousShadeMode) +{ + Q_UNUSED(previousShadeMode) +} + +void AbstractClient::shadeHover() +{ + setShade(ShadeHover); + cancelShadeHoverTimer(); +} + +void AbstractClient::shadeUnhover() +{ + setShade(ShadeNormal); + cancelShadeHoverTimer(); +} + +void AbstractClient::startShadeHoverTimer() +{ + if (!isShade()) + return; + m_shadeHoverTimer = new QTimer(this); + connect(m_shadeHoverTimer, &QTimer::timeout, this, &AbstractClient::shadeHover); + m_shadeHoverTimer->setSingleShot(true); + m_shadeHoverTimer->start(options->shadeHoverInterval()); +} + +void AbstractClient::startShadeUnhoverTimer() +{ + if (m_shadeMode == ShadeHover && !isMoveResize() && !isMoveResizePointerButtonDown()) { + m_shadeHoverTimer = new QTimer(this); + connect(m_shadeHoverTimer, &QTimer::timeout, this, &AbstractClient::shadeUnhover); + m_shadeHoverTimer->setSingleShot(true); + m_shadeHoverTimer->start(options->shadeHoverInterval()); + } +} + +void AbstractClient::cancelShadeHoverTimer() +{ + delete m_shadeHoverTimer; + m_shadeHoverTimer = nullptr; +} + +void AbstractClient::toggleShade() +{ + // If the mode is ShadeHover or ShadeActive, cancel shade too. + setShade(shadeMode() == ShadeNone ? ShadeNormal : ShadeNone); +} + +AbstractClient::Position AbstractClient::titlebarPosition() const +{ + // TODO: still needed, remove? + return PositionTop; +} + +bool AbstractClient::titlebarPositionUnderMouse() const +{ + if (!isDecorated()) { + return false; + } + const auto sectionUnderMouse = decoration()->sectionUnderMouse(); + if (sectionUnderMouse == Qt::TitleBarArea) { + return true; + } + // check other sections based on titlebarPosition + switch (titlebarPosition()) { + case AbstractClient::PositionTop: + return (sectionUnderMouse == Qt::TopLeftSection || sectionUnderMouse == Qt::TopSection || sectionUnderMouse == Qt::TopRightSection); + case AbstractClient::PositionLeft: + return (sectionUnderMouse == Qt::TopLeftSection || sectionUnderMouse == Qt::LeftSection || sectionUnderMouse == Qt::BottomLeftSection); + case AbstractClient::PositionRight: + return (sectionUnderMouse == Qt::BottomRightSection || sectionUnderMouse == Qt::RightSection || sectionUnderMouse == Qt::TopRightSection); + case AbstractClient::PositionBottom: + return (sectionUnderMouse == Qt::BottomLeftSection || sectionUnderMouse == Qt::BottomSection || sectionUnderMouse == Qt::BottomRightSection); + default: + // nothing + return false; + } +} + +void AbstractClient::setMinimized(bool set) +{ + set ? minimize() : unminimize(); +} + +void AbstractClient::minimize(bool avoid_animation) +{ + if (!isMinimizable() || isMinimized()) + return; + + m_minimized = true; + doMinimize(); + + updateWindowRules(Rules::Minimize); + // TODO: merge signal with s_minimized + addWorkspaceRepaint(visibleRect()); + emit clientMinimized(this, !avoid_animation); + emit minimizedChanged(); +} + +void AbstractClient::unminimize(bool avoid_animation) +{ + if (!isMinimized()) + return; + + if (rules()->checkMinimize(false)) { + return; + } + + m_minimized = false; + doMinimize(); + + updateWindowRules(Rules::Minimize); + emit clientUnminimized(this, !avoid_animation); + emit minimizedChanged(); +} + +void AbstractClient::doMinimize() +{ +} + +QPalette AbstractClient::palette() const +{ + if (!m_palette) { + return QPalette(); + } + return m_palette->palette(); +} + +const Decoration::DecorationPalette *AbstractClient::decorationPalette() const +{ + return m_palette.get(); +} + +QString AbstractClient::preferredColorScheme() const +{ + return rules()->checkDecoColor(QString()); +} + +QString AbstractClient::colorScheme() const +{ + return m_colorScheme; +} + +void AbstractClient::setColorScheme(const QString &colorScheme) +{ + QString requestedColorScheme = colorScheme; + if (requestedColorScheme.isEmpty()) { + requestedColorScheme = QStringLiteral("kdeglobals"); + } + + if (!m_palette || m_colorScheme != requestedColorScheme) { + m_colorScheme = requestedColorScheme; + + if (m_palette) { + disconnect(m_palette.get(), &Decoration::DecorationPalette::changed, this, &AbstractClient::handlePaletteChange); + } + + auto it = s_palettes.find(m_colorScheme); + + if (it == s_palettes.end() || it->expired()) { + m_palette = std::make_shared(m_colorScheme); + if (m_palette->isValid()) { + s_palettes[m_colorScheme] = m_palette; + } else { + if (!s_defaultPalette) { + s_defaultPalette = std::make_shared(QStringLiteral("kdeglobals")); + s_palettes[QStringLiteral("kdeglobals")] = s_defaultPalette; + } + + m_palette = s_defaultPalette; + } + + if (m_colorScheme == QStringLiteral("kdeglobals")) { + s_defaultPalette = m_palette; + } + } else { + m_palette = it->lock(); + } + + connect(m_palette.get(), &Decoration::DecorationPalette::changed, this, &AbstractClient::handlePaletteChange); + + emit paletteChanged(palette()); + emit colorSchemeChanged(); + } +} + +void AbstractClient::updateColorScheme() +{ + setColorScheme(preferredColorScheme()); +} + +void AbstractClient::handlePaletteChange() +{ + emit paletteChanged(palette()); +} + +void AbstractClient::keepInArea(QRect area, bool partial) +{ + if (partial) { + // increase the area so that can have only 100 pixels in the area + area.setLeft(qMin(area.left() - width() + 100, area.left())); + area.setTop(qMin(area.top() - height() + 100, area.top())); + area.setRight(qMax(area.right() + width() - 100, area.right())); + area.setBottom(qMax(area.bottom() + height() - 100, area.bottom())); + } + if (!partial) { + // resize to fit into area + if (area.width() < width() || area.height() < height()) + resizeWithChecks(size().boundedTo(area.size())); + } + int tx = x(), ty = y(); + if (frameGeometry().right() > area.right() && width() <= area.width()) + tx = area.right() - width() + 1; + if (frameGeometry().bottom() > area.bottom() && height() <= area.height()) + ty = area.bottom() - height() + 1; + if (!area.contains(frameGeometry().topLeft())) { + if (tx < area.x()) + tx = area.x(); + if (ty < area.y()) + ty = area.y(); + } + if (tx != x() || ty != y()) + move(tx, ty); +} + +/** + * Returns the maximum client size, not the maximum frame size. + */ +QSize AbstractClient::maxSize() const +{ + return rules()->checkMaxSize(QSize(INT_MAX, INT_MAX)); +} + +/** + * Returns the minimum client size, not the minimum frame size. + */ +QSize AbstractClient::minSize() const +{ + return rules()->checkMinSize(QSize(0, 0)); +} + +void AbstractClient::blockGeometryUpdates(bool block) +{ + if (block) { + if (m_blockGeometryUpdates == 0) + m_pendingGeometryUpdate = PendingGeometryNone; + ++m_blockGeometryUpdates; + } else { + if (--m_blockGeometryUpdates == 0) { + if (m_pendingGeometryUpdate != PendingGeometryNone) { + if (isShade()) + setFrameGeometry(QRect(pos(), adjustedSize()), NormalGeometrySet); + else + setFrameGeometry(frameGeometry(), NormalGeometrySet); + m_pendingGeometryUpdate = PendingGeometryNone; + } + } + } +} + +void AbstractClient::maximize(MaximizeMode m) +{ + setMaximize(m & MaximizeVertical, m & MaximizeHorizontal); +} + +void AbstractClient::setMaximize(bool vertically, bool horizontally) +{ + // changeMaximize() flips the state, so change from set->flip + const MaximizeMode oldMode = requestedMaximizeMode(); + changeMaximize( + oldMode & MaximizeHorizontal ? !horizontally : horizontally, + oldMode & MaximizeVertical ? !vertically : vertically, + false); + const MaximizeMode newMode = maximizeMode(); + if (oldMode != newMode) { + emit clientMaximizedStateChanged(this, newMode); + emit clientMaximizedStateChanged(this, vertically, horizontally); + } +} + +void AbstractClient::move(int x, int y, ForceGeometry_t force) +{ + // resuming geometry updates is handled only in setGeometry() + Q_ASSERT(pendingGeometryUpdate() == PendingGeometryNone || areGeometryUpdatesBlocked()); + QPoint p(x, y); + if (!areGeometryUpdatesBlocked() && p != rules()->checkPosition(p)) { + qCDebug(KWIN_CORE) << "forced position fail:" << p << ":" << rules()->checkPosition(p); + } + if (force == NormalGeometrySet && m_frameGeometry.topLeft() == p) + return; + m_frameGeometry.moveTopLeft(p); + if (areGeometryUpdatesBlocked()) { + if (pendingGeometryUpdate() == PendingGeometryForced) + {} // maximum, nothing needed + else if (force == ForceGeometrySet) + setPendingGeometryUpdate(PendingGeometryForced); + else + setPendingGeometryUpdate(PendingGeometryNormal); + return; + } + const QRect oldBufferGeometry = bufferGeometryBeforeUpdateBlocking(); + const QRect oldClientGeometry = clientGeometryBeforeUpdateBlocking(); + const QRect oldFrameGeometry = frameGeometryBeforeUpdateBlocking(); + doMove(x, y); + updateGeometryBeforeUpdateBlocking(); + updateWindowRules(Rules::Position); + screens()->setCurrent(this); + workspace()->updateStackingOrder(); + // client itself is not damaged + emit bufferGeometryChanged(this, oldBufferGeometry); + emit clientGeometryChanged(this, oldClientGeometry); + emit frameGeometryChanged(this, oldFrameGeometry); + addRepaintDuringGeometryUpdates(); +} + +bool AbstractClient::startMoveResize() +{ + Q_ASSERT(!isMoveResize()); + Q_ASSERT(QWidget::keyboardGrabber() == nullptr); + Q_ASSERT(QWidget::mouseGrabber() == nullptr); + stopDelayedMoveResize(); + if (QApplication::activePopupWidget() != nullptr) + return false; // popups have grab + if (isFullScreen() && (screens()->count() < 2 || !isMovableAcrossScreens())) + return false; + if (!doStartMoveResize()) { + return false; + } + + invalidateDecorationDoubleClickTimer(); + + setMoveResize(true); + workspace()->setMoveResizeClient(this); + + const Position mode = moveResizePointerMode(); + if (mode != PositionCenter) { // means "isResize()" but moveResizeMode = true is set below + if (maximizeMode() == MaximizeFull) { // partial is cond. reset in finishMoveResize + setGeometryRestore(frameGeometry()); // "restore" to current geometry + setMaximize(false, false); + } + } + + if (quickTileMode() != QuickTileMode(QuickTileFlag::None) && mode != PositionCenter) { // Cannot use isResize() yet + // Exit quick tile mode when the user attempts to resize a tiled window + updateQuickTileMode(QuickTileFlag::None); // Do so without restoring original geometry + setGeometryRestore(frameGeometry()); + doSetQuickTileMode(); + emit quickTileModeChanged(); + } + + updateHaveResizeEffect(); + updateInitialMoveResizeGeometry(); + checkUnrestrictedMoveResize(); + emit clientStartUserMovedResized(this); + if (ScreenEdges::self()->isDesktopSwitchingMovingClients()) + ScreenEdges::self()->reserveDesktopSwitching(true, Qt::Vertical|Qt::Horizontal); + return true; +} + +void AbstractClient::finishMoveResize(bool cancel) +{ + GeometryUpdatesBlocker blocker(this); + const bool wasResize = isResize(); // store across leaveMoveResize + leaveMoveResize(); + + doFinishMoveResize(); + + if (cancel) + setFrameGeometry(initialMoveResizeGeometry()); + else { + const QRect &moveResizeGeom = moveResizeGeometry(); + if (wasResize) { + const bool restoreH = maximizeMode() == MaximizeHorizontal && + moveResizeGeom.width() != initialMoveResizeGeometry().width(); + const bool restoreV = maximizeMode() == MaximizeVertical && + moveResizeGeom.height() != initialMoveResizeGeometry().height(); + if (restoreH || restoreV) { + changeMaximize(restoreH, restoreV, false); + } + } + setFrameGeometry(moveResizeGeom); + } + checkScreen(); // needs to be done because clientFinishUserMovedResized has not yet re-activated online alignment + if (screen() != moveResizeStartScreen()) { + workspace()->sendClientToScreen(this, screen()); // checks rule validity + if (maximizeMode() != MaximizeRestore) + checkWorkspacePosition(); + } + + if (isElectricBorderMaximizing()) { + setQuickTileMode(electricBorderMode()); + setElectricBorderMaximizing(false); + } else if (!cancel) { + QRect geom_restore = geometryRestore(); + if (!(maximizeMode() & MaximizeHorizontal)) { + geom_restore.setX(frameGeometry().x()); + geom_restore.setWidth(frameGeometry().width()); + } + if (!(maximizeMode() & MaximizeVertical)) { + geom_restore.setY(frameGeometry().y()); + geom_restore.setHeight(frameGeometry().height()); + } + setGeometryRestore(geom_restore); + } +// FRAME update(); + + emit clientFinishUserMovedResized(this); +} + +// This function checks if it actually makes sense to perform a restricted move/resize. +// If e.g. the titlebar is already outside of the workarea, there's no point in performing +// a restricted move resize, because then e.g. resize would also move the window (#74555). +// NOTE: Most of it is duplicated from handleMoveResize(). +void AbstractClient::checkUnrestrictedMoveResize() +{ + if (isUnrestrictedMoveResize()) + return; + const QRect &moveResizeGeom = moveResizeGeometry(); + QRect desktopArea = workspace()->clientArea(WorkArea, moveResizeGeom.center(), desktop()); + int left_marge, right_marge, top_marge, bottom_marge, titlebar_marge; + // restricted move/resize - keep at least part of the titlebar always visible + // how much must remain visible when moved away in that direction + left_marge = qMin(100 + borderRight(), moveResizeGeom.width()); + right_marge = qMin(100 + borderLeft(), moveResizeGeom.width()); + // width/height change with opaque resizing, use the initial ones + titlebar_marge = initialMoveResizeGeometry().height(); + top_marge = borderBottom(); + bottom_marge = borderTop(); + if (isResize()) { + if (moveResizeGeom.bottom() < desktopArea.top() + top_marge) + setUnrestrictedMoveResize(true); + if (moveResizeGeom.top() > desktopArea.bottom() - bottom_marge) + setUnrestrictedMoveResize(true); + if (moveResizeGeom.right() < desktopArea.left() + left_marge) + setUnrestrictedMoveResize(true); + if (moveResizeGeom.left() > desktopArea.right() - right_marge) + setUnrestrictedMoveResize(true); + if (!isUnrestrictedMoveResize() && moveResizeGeom.top() < desktopArea.top()) // titlebar mustn't go out + setUnrestrictedMoveResize(true); + } + if (isMove()) { + if (moveResizeGeom.bottom() < desktopArea.top() + titlebar_marge - 1) + setUnrestrictedMoveResize(true); + // no need to check top_marge, titlebar_marge already handles it + if (moveResizeGeom.top() > desktopArea.bottom() - bottom_marge + 1) // titlebar mustn't go out + setUnrestrictedMoveResize(true); + if (moveResizeGeom.right() < desktopArea.left() + left_marge) + setUnrestrictedMoveResize(true); + if (moveResizeGeom.left() > desktopArea.right() - right_marge) + setUnrestrictedMoveResize(true); + } +} + +// When the user pressed mouse on the titlebar, don't activate move immediately, +// since it may be just a click. Activate instead after a delay. Move used to be +// activated only after moving by several pixels, but that looks bad. +void AbstractClient::startDelayedMoveResize() +{ + Q_ASSERT(!m_moveResize.delayedTimer); + m_moveResize.delayedTimer = new QTimer(this); + m_moveResize.delayedTimer->setSingleShot(true); + connect(m_moveResize.delayedTimer, &QTimer::timeout, this, + [this]() { + Q_ASSERT(isMoveResizePointerButtonDown()); + if (!startMoveResize()) { + setMoveResizePointerButtonDown(false); + } + updateCursor(); + stopDelayedMoveResize(); + } + ); + m_moveResize.delayedTimer->start(QApplication::startDragTime()); +} + +void AbstractClient::stopDelayedMoveResize() +{ + delete m_moveResize.delayedTimer; + m_moveResize.delayedTimer = nullptr; +} + +void AbstractClient::updateMoveResize(const QPointF ¤tGlobalCursor) +{ + handleMoveResize(pos(), currentGlobalCursor.toPoint()); +} + +void AbstractClient::handleMoveResize(const QPoint &local, const QPoint &global) +{ + const QRect oldGeo = frameGeometry(); + handleMoveResize(local.x(), local.y(), global.x(), global.y()); + if (!isFullScreen() && isMove()) { + if (quickTileMode() != QuickTileMode(QuickTileFlag::None) && oldGeo != frameGeometry()) { + GeometryUpdatesBlocker blocker(this); + setQuickTileMode(QuickTileFlag::None); + const QRect &geom_restore = geometryRestore(); + setMoveOffset(QPoint(double(moveOffset().x()) / double(oldGeo.width()) * double(geom_restore.width()), + double(moveOffset().y()) / double(oldGeo.height()) * double(geom_restore.height()))); + if (rules()->checkMaximize(MaximizeRestore) == MaximizeRestore) + setMoveResizeGeometry(geom_restore); + handleMoveResize(local.x(), local.y(), global.x(), global.y()); // fix position + } else if (quickTileMode() == QuickTileMode(QuickTileFlag::None) && isResizable()) { + checkQuickTilingMaximizationZones(global.x(), global.y()); + } + } +} + +void AbstractClient::handleMoveResize(int x, int y, int x_root, int y_root) +{ + if (isWaitingForMoveResizeSync()) + return; // we're still waiting for the client or the timeout + + const Position mode = moveResizePointerMode(); + if ((mode == PositionCenter && !isMovableAcrossScreens()) + || (mode != PositionCenter && (isShade() || !isResizable()))) + return; + + if (!isMoveResize()) { + QPoint p(QPoint(x/* - padding_left*/, y/* - padding_top*/) - moveOffset()); + if (p.manhattanLength() >= QApplication::startDragDistance()) { + if (!startMoveResize()) { + setMoveResizePointerButtonDown(false); + updateCursor(); + return; + } + updateCursor(); + } else + return; + } + + // ShadeHover or ShadeActive, ShadeNormal was already avoided above + if (mode != PositionCenter && shadeMode() != ShadeNone) + setShade(ShadeNone); + + QPoint globalPos(x_root, y_root); + // these two points limit the geometry rectangle, i.e. if bottomleft resizing is done, + // the bottomleft corner should be at is at (topleft.x(), bottomright().y()) + QPoint topleft = globalPos - moveOffset(); + QPoint bottomright = globalPos + invertedMoveOffset(); + QRect previousMoveResizeGeom = moveResizeGeometry(); + + // TODO move whole group when moving its leader or when the leader is not mapped? + + auto titleBarRect = [this](bool &transposed, int &requiredPixels) -> QRect { + const QRect &moveResizeGeom = moveResizeGeometry(); + QRect r(moveResizeGeom); + r.moveTopLeft(QPoint(0,0)); + switch (titlebarPosition()) { + default: + case PositionTop: + r.setHeight(borderTop()); + break; + case PositionLeft: + r.setWidth(borderLeft()); + transposed = true; + break; + case PositionBottom: + r.setTop(r.bottom() - borderBottom()); + break; + case PositionRight: + r.setLeft(r.right() - borderRight()); + transposed = true; + break; + } + // When doing a restricted move we must always keep 100px of the titlebar + // visible to allow the user to be able to move it again. + requiredPixels = qMin(100 * (transposed ? r.width() : r.height()), + moveResizeGeom.width() * moveResizeGeom.height()); + return r; + }; + + bool update = false; + if (isResize()) { + QRect orig = initialMoveResizeGeometry(); + SizeMode sizeMode = SizeModeAny; + auto calculateMoveResizeGeom = [this, &topleft, &bottomright, &orig, &sizeMode, &mode]() { + switch(mode) { + case PositionTopLeft: + setMoveResizeGeometry(QRect(topleft, orig.bottomRight())); + break; + case PositionBottomRight: + setMoveResizeGeometry(QRect(orig.topLeft(), bottomright)); + break; + case PositionBottomLeft: + setMoveResizeGeometry(QRect(QPoint(topleft.x(), orig.y()), QPoint(orig.right(), bottomright.y()))); + break; + case PositionTopRight: + setMoveResizeGeometry(QRect(QPoint(orig.x(), topleft.y()), QPoint(bottomright.x(), orig.bottom()))); + break; + case PositionTop: + setMoveResizeGeometry(QRect(QPoint(orig.left(), topleft.y()), orig.bottomRight())); + sizeMode = SizeModeFixedH; // try not to affect height + break; + case PositionBottom: + setMoveResizeGeometry(QRect(orig.topLeft(), QPoint(orig.right(), bottomright.y()))); + sizeMode = SizeModeFixedH; + break; + case PositionLeft: + setMoveResizeGeometry(QRect(QPoint(topleft.x(), orig.top()), orig.bottomRight())); + sizeMode = SizeModeFixedW; + break; + case PositionRight: + setMoveResizeGeometry(QRect(orig.topLeft(), QPoint(bottomright.x(), orig.bottom()))); + sizeMode = SizeModeFixedW; + break; + case PositionCenter: + default: + abort(); + break; + } + }; + + // first resize (without checking constrains), then snap, then check bounds, then check constrains + calculateMoveResizeGeom(); + // adjust new size to snap to other windows/borders + setMoveResizeGeometry(workspace()->adjustClientSize(this, moveResizeGeometry(), mode)); + + if (!isUnrestrictedMoveResize()) { + // Make sure the titlebar isn't behind a restricted area. We don't need to restrict + // the other directions. If not visible enough, move the window to the closest valid + // point. We bruteforce this by slowly moving the window back to its previous position + QRegion availableArea(workspace()->clientArea(FullArea, -1, 0)); // On the screen + availableArea -= workspace()->restrictedMoveArea(desktop()); // Strut areas + bool transposed = false; + int requiredPixels; + QRect bTitleRect = titleBarRect(transposed, requiredPixels); + int lastVisiblePixels = -1; + QRect lastTry = moveResizeGeometry(); + bool titleFailed = false; + for (;;) { + const QRect titleRect(bTitleRect.translated(moveResizeGeometry().topLeft())); + int visiblePixels = 0; + int realVisiblePixels = 0; + for (const QRect &rect : availableArea) { + const QRect r = rect & titleRect; + realVisiblePixels += r.width() * r.height(); + if ((transposed && r.width() == titleRect.width()) || // Only the full size regions... + (!transposed && r.height() == titleRect.height())) // ...prevents long slim areas + visiblePixels += r.width() * r.height(); + } + + if (visiblePixels >= requiredPixels) + break; // We have reached a valid position + + if (realVisiblePixels <= lastVisiblePixels) { + if (titleFailed && realVisiblePixels < lastVisiblePixels) + break; // we won't become better + else { + if (!titleFailed) + setMoveResizeGeometry(lastTry); + titleFailed = true; + } + } + lastVisiblePixels = realVisiblePixels; + QRect moveResizeGeom = moveResizeGeometry(); + lastTry = moveResizeGeom; + + // Not visible enough, move the window to the closest valid point. We bruteforce + // this by slowly moving the window back to its previous position. + // The geometry changes at up to two edges, the one with the title (if) shall take + // precedence. The opposing edge has no impact on visiblePixels and only one of + // the adjacent can alter at a time, ie. it's enough to ignore adjacent edges + // if the title edge altered + bool leftChanged = previousMoveResizeGeom.left() != moveResizeGeom.left(); + bool rightChanged = previousMoveResizeGeom.right() != moveResizeGeom.right(); + bool topChanged = previousMoveResizeGeom.top() != moveResizeGeom.top(); + bool btmChanged = previousMoveResizeGeom.bottom() != moveResizeGeom.bottom(); + auto fixChangedState = [titleFailed](bool &major, bool &counter, bool &ad1, bool &ad2) { + counter = false; + if (titleFailed) + major = false; + if (major) + ad1 = ad2 = false; + }; + switch (titlebarPosition()) { + default: + case PositionTop: + fixChangedState(topChanged, btmChanged, leftChanged, rightChanged); + break; + case PositionLeft: + fixChangedState(leftChanged, rightChanged, topChanged, btmChanged); + break; + case PositionBottom: + fixChangedState(btmChanged, topChanged, leftChanged, rightChanged); + break; + case PositionRight: + fixChangedState(rightChanged, leftChanged, topChanged, btmChanged); + break; + } + if (topChanged) + moveResizeGeom.setTop(moveResizeGeom.y() + sign(previousMoveResizeGeom.y() - moveResizeGeom.y())); + else if (leftChanged) + moveResizeGeom.setLeft(moveResizeGeom.x() + sign(previousMoveResizeGeom.x() - moveResizeGeom.x())); + else if (btmChanged) + moveResizeGeom.setBottom(moveResizeGeom.bottom() + sign(previousMoveResizeGeom.bottom() - moveResizeGeom.bottom())); + else if (rightChanged) + moveResizeGeom.setRight(moveResizeGeom.right() + sign(previousMoveResizeGeom.right() - moveResizeGeom.right())); + else + break; // no position changed - that's certainly not good + setMoveResizeGeometry(moveResizeGeom); + } + } + + // Always obey size hints, even when in "unrestricted" mode + QSize size = constrainFrameSize(moveResizeGeometry().size(), sizeMode); + // the new topleft and bottomright corners (after checking size constrains), if they'll be needed + topleft = QPoint(moveResizeGeometry().right() - size.width() + 1, moveResizeGeometry().bottom() - size.height() + 1); + bottomright = QPoint(moveResizeGeometry().left() + size.width() - 1, moveResizeGeometry().top() + size.height() - 1); + orig = moveResizeGeometry(); + + // if aspect ratios are specified, both dimensions may change. + // Therefore grow to the right/bottom if needed. + // TODO it should probably obey gravity rather than always using right/bottom ? + if (sizeMode == SizeModeFixedH) + orig.setRight(bottomright.x()); + else if (sizeMode == SizeModeFixedW) + orig.setBottom(bottomright.y()); + + calculateMoveResizeGeom(); + + if (moveResizeGeometry().size() != previousMoveResizeGeom.size()) + update = true; + } else if (isMove()) { + Q_ASSERT(mode == PositionCenter); + if (!isMovable()) { // isMovableAcrossScreens() must have been true to get here + // Special moving of maximized windows on Xinerama screens + int screen = screens()->number(globalPos); + if (isFullScreen()) + setMoveResizeGeometry(workspace()->clientArea(FullScreenArea, screen, 0)); + else { + QRect moveResizeGeom = workspace()->clientArea(MaximizeArea, screen, 0); + QSize adjSize = constrainFrameSize(moveResizeGeom.size(), SizeModeMax); + if (adjSize != moveResizeGeom.size()) { + QRect r(moveResizeGeom); + moveResizeGeom.setSize(adjSize); + moveResizeGeom.moveCenter(r.center()); + } + setMoveResizeGeometry(moveResizeGeom); + } + } else { + // first move, then snap, then check bounds + QRect moveResizeGeom = moveResizeGeometry(); + moveResizeGeom.moveTopLeft(topleft); + moveResizeGeom.moveTopLeft(workspace()->adjustClientPosition(this, moveResizeGeom.topLeft(), + isUnrestrictedMoveResize())); + setMoveResizeGeometry(moveResizeGeom); + + if (!isUnrestrictedMoveResize()) { + const QRegion strut = workspace()->restrictedMoveArea(desktop()); // Strut areas + QRegion availableArea(workspace()->clientArea(FullArea, -1, 0)); // On the screen + availableArea -= strut; // Strut areas + bool transposed = false; + int requiredPixels; + QRect bTitleRect = titleBarRect(transposed, requiredPixels); + for (;;) { + QRect moveResizeGeom = moveResizeGeometry(); + const QRect titleRect(bTitleRect.translated(moveResizeGeom.topLeft())); + int visiblePixels = 0; + for (const QRect &rect : availableArea) { + const QRect r = rect & titleRect; + if ((transposed && r.width() == titleRect.width()) || // Only the full size regions... + (!transposed && r.height() == titleRect.height())) // ...prevents long slim areas + visiblePixels += r.width() * r.height(); + } + if (visiblePixels >= requiredPixels) + break; // We have reached a valid position + + // (esp.) if there're more screens with different struts (panels) it the titlebar + // will be movable outside the movearea (covering one of the panels) until it + // crosses the panel "too much" (not enough visiblePixels) and then stucks because + // it's usually only pushed by 1px to either direction + // so we first check whether we intersect suc strut and move the window below it + // immediately (it's still possible to hit the visiblePixels >= titlebarArea break + // by moving the window slightly downwards, but it won't stuck) + // see bug #274466 + // and bug #301805 for why we can't just match the titlearea against the screen + if (screens()->count() > 1) { // optimization + // TODO: could be useful on partial screen struts (half-width panels etc.) + int newTitleTop = -1; + for (const QRect &r : strut) { + if (r.top() == 0 && r.width() > r.height() && // "top panel" + r.intersects(moveResizeGeom) && moveResizeGeom.top() < r.bottom()) { + newTitleTop = r.bottom() + 1; + break; + } + } + if (newTitleTop > -1) { + moveResizeGeom.moveTop(newTitleTop); // invalid position, possibly on screen change + setMoveResizeGeometry(moveResizeGeom); + break; + } + } + + int dx = sign(previousMoveResizeGeom.x() - moveResizeGeom.x()), + dy = sign(previousMoveResizeGeom.y() - moveResizeGeom.y()); + if (visiblePixels && dx) // means there's no full width cap -> favor horizontally + dy = 0; + else if (dy) + dx = 0; + + // Move it back + moveResizeGeom.translate(dx, dy); + setMoveResizeGeometry(moveResizeGeom); + + if (moveResizeGeom == previousMoveResizeGeom) { + break; // Prevent lockup + } + } + } + } + if (moveResizeGeometry().topLeft() != previousMoveResizeGeom.topLeft()) + update = true; + } else + abort(); + + if (!update) + return; + + if (isResize() && !haveResizeEffect()) { + doResizeSync(); + } else + performMoveResize(); + + if (isMove()) { + ScreenEdges::self()->check(globalPos, QDateTime::fromMSecsSinceEpoch(xTime(), Qt::UTC)); + } +} + +void AbstractClient::performMoveResize() +{ + const QRect &moveResizeGeom = moveResizeGeometry(); + if (isMove() || (isResize() && !haveResizeEffect())) { + setFrameGeometry(moveResizeGeom); + } + doPerformMoveResize(); + positionGeometryTip(); + emit clientStepUserMovedResized(this, moveResizeGeom); +} + +StrutRect AbstractClient::strutRect(StrutArea area) const +{ + Q_UNUSED(area) + return StrutRect(); +} + +StrutRects AbstractClient::strutRects() const +{ + StrutRects region; + region += strutRect(StrutAreaTop); + region += strutRect(StrutAreaRight); + region += strutRect(StrutAreaBottom); + region += strutRect(StrutAreaLeft); + return region; +} + +bool AbstractClient::hasStrut() const +{ + return false; +} + +void AbstractClient::setupWindowManagementInterface() +{ + if (m_windowManagementInterface) { + // already setup + return; + } + if (!waylandServer() || !surface()) { + return; + } + if (!waylandServer()->windowManagement()) { + return; + } + using namespace KWaylandServer; + auto w = waylandServer()->windowManagement()->createWindow(waylandServer()->windowManagement(), internalId()); + w->setTitle(caption()); + w->setVirtualDesktop(isOnAllDesktops() ? 0 : desktop() - 1); + w->setActive(isActive()); + w->setFullscreen(isFullScreen()); + w->setKeepAbove(keepAbove()); + w->setKeepBelow(keepBelow()); + w->setMaximized(maximizeMode() == KWin::MaximizeFull); + w->setMinimized(isMinimized()); + w->setOnAllDesktops(isOnAllDesktops()); + w->setDemandsAttention(isDemandingAttention()); + w->setCloseable(isCloseable()); + w->setMaximizeable(isMaximizable()); + w->setMinimizeable(isMinimizable()); + w->setFullscreenable(isFullScreenable()); + w->setApplicationMenuPaths(applicationMenuServiceName(), applicationMenuObjectPath()); + w->setIcon(icon()); + auto updateAppId = [this, w] { + w->setAppId(QString::fromUtf8(m_desktopFileName.isEmpty() ? resourceClass() : m_desktopFileName)); + }; + updateAppId(); + w->setSkipTaskbar(skipTaskbar()); + w->setSkipSwitcher(skipSwitcher()); + w->setPid(pid()); + w->setShadeable(isShadeable()); + w->setShaded(isShade()); + w->setResizable(isResizable()); + w->setMovable(isMovable()); + w->setVirtualDesktopChangeable(true); // FIXME Matches X11Client::actionSupported(), but both should be implemented. + w->setParentWindow(transientFor() ? transientFor()->windowManagementInterface() : nullptr); + w->setGeometry(frameGeometry()); + connect(this, &AbstractClient::skipTaskbarChanged, w, + [w, this] { + w->setSkipTaskbar(skipTaskbar()); + } + ); + connect(this, &AbstractClient::skipSwitcherChanged, w, + [w, this] { + w->setSkipSwitcher(skipSwitcher()); + } + ); + connect(this, &AbstractClient::captionChanged, w, [w, this] { w->setTitle(caption()); }); + + connect(this, &AbstractClient::activeChanged, w, [w, this] { w->setActive(isActive()); }); + connect(this, &AbstractClient::fullScreenChanged, w, [w, this] { w->setFullscreen(isFullScreen()); }); + connect(this, &AbstractClient::keepAboveChanged, w, &PlasmaWindowInterface::setKeepAbove); + connect(this, &AbstractClient::keepBelowChanged, w, &PlasmaWindowInterface::setKeepBelow); + connect(this, &AbstractClient::minimizedChanged, w, [w, this] { w->setMinimized(isMinimized()); }); + connect(this, static_cast(&AbstractClient::clientMaximizedStateChanged), w, + [w] (KWin::AbstractClient *c, MaximizeMode mode) { + Q_UNUSED(c); + w->setMaximized(mode == KWin::MaximizeFull); + } + ); + connect(this, &AbstractClient::demandsAttentionChanged, w, [w, this] { w->setDemandsAttention(isDemandingAttention()); }); + connect(this, &AbstractClient::iconChanged, w, + [w, this] { + w->setIcon(icon()); + } + ); + connect(this, &AbstractClient::windowClassChanged, w, updateAppId); + connect(this, &AbstractClient::desktopFileNameChanged, w, updateAppId); + connect(this, &AbstractClient::shadeChanged, w, [w, this] { w->setShaded(isShade()); }); + connect(this, &AbstractClient::transientChanged, w, + [w, this] { + w->setParentWindow(transientFor() ? transientFor()->windowManagementInterface() : nullptr); + } + ); + connect(this, &AbstractClient::frameGeometryChanged, w, + [w, this] { + w->setGeometry(frameGeometry()); + } + ); + connect(this, &AbstractClient::applicationMenuChanged, w, + [w, this] { + w->setApplicationMenuPaths(applicationMenuServiceName(), applicationMenuObjectPath()); + } + ); + connect(w, &PlasmaWindowInterface::closeRequested, this, [this] { closeWindow(); }); + connect(w, &PlasmaWindowInterface::moveRequested, this, + [this] { + Cursors::self()->mouse()->setPos(frameGeometry().center()); + performMouseCommand(Options::MouseMove, Cursors::self()->mouse()->pos()); + } + ); + connect(w, &PlasmaWindowInterface::resizeRequested, this, + [this] { + Cursors::self()->mouse()->setPos(frameGeometry().bottomRight()); + performMouseCommand(Options::MouseResize, Cursors::self()->mouse()->pos()); + } + ); + connect(w, &PlasmaWindowInterface::virtualDesktopRequested, this, + [this] (quint32 desktop) { + workspace()->sendClientToDesktop(this, desktop + 1, true); + } + ); + connect(w, &PlasmaWindowInterface::fullscreenRequested, this, + [this] (bool set) { + setFullScreen(set, false); + } + ); + connect(w, &PlasmaWindowInterface::minimizedRequested, this, + [this] (bool set) { + if (set) { + minimize(); + } else { + unminimize(); + } + } + ); + connect(w, &PlasmaWindowInterface::maximizedRequested, this, + [this] (bool set) { + maximize(set ? MaximizeFull : MaximizeRestore); + } + ); + connect(w, &PlasmaWindowInterface::keepAboveRequested, this, + [this] (bool set) { + setKeepAbove(set); + } + ); + connect(w, &PlasmaWindowInterface::keepBelowRequested, this, + [this] (bool set) { + setKeepBelow(set); + } + ); + connect(w, &PlasmaWindowInterface::demandsAttentionRequested, this, + [this] (bool set) { + demandAttention(set); + } + ); + connect(w, &PlasmaWindowInterface::activeRequested, this, + [this] (bool set) { + if (set) { + workspace()->activateClient(this, true); + } + } + ); + connect(w, &PlasmaWindowInterface::shadedRequested, this, + [this] (bool set) { + setShade(set); + } + ); + + for (const auto vd : m_desktops) { + w->addPlasmaVirtualDesktop(vd->id()); + } + + //this is only for the legacy + connect(this, &AbstractClient::desktopChanged, w, + [w, this] { + if (isOnAllDesktops()) { + w->setOnAllDesktops(true); + return; + } + w->setVirtualDesktop(desktop() - 1); + w->setOnAllDesktops(false); + } + ); + + //Plasma Virtual desktop management + //show/hide when the window enters/exits from desktop + connect(w, &PlasmaWindowInterface::enterPlasmaVirtualDesktopRequested, this, + [this] (const QString &desktopId) { + VirtualDesktop *vd = VirtualDesktopManager::self()->desktopForId(desktopId.toUtf8()); + if (vd) { + enterDesktop(vd); + } + } + ); + connect(w, &PlasmaWindowInterface::enterNewPlasmaVirtualDesktopRequested, this, + [this] () { + VirtualDesktopManager::self()->setCount(VirtualDesktopManager::self()->count() + 1); + enterDesktop(VirtualDesktopManager::self()->desktops().last()); + } + ); + connect(w, &PlasmaWindowInterface::leavePlasmaVirtualDesktopRequested, this, + [this] (const QString &desktopId) { + VirtualDesktop *vd = VirtualDesktopManager::self()->desktopForId(desktopId.toUtf8()); + if (vd) { + leaveDesktop(vd); + } + } + ); + + m_windowManagementInterface = w; +} + +void AbstractClient::destroyWindowManagementInterface() +{ + if (m_windowManagementInterface) { + m_windowManagementInterface->unmap(); + m_windowManagementInterface = nullptr; + } +} + +Options::MouseCommand AbstractClient::getMouseCommand(Qt::MouseButton button, bool *handled) const +{ + *handled = false; + if (button == Qt::NoButton) { + return Options::MouseNothing; + } + if (isActive()) { + if (options->isClickRaise() && !isMostRecentlyRaised()) { + *handled = true; + return Options::MouseActivateRaiseAndPassClick; + } + } else { + *handled = true; + switch (button) { + case Qt::LeftButton: + return options->commandWindow1(); + case Qt::MiddleButton: + return options->commandWindow2(); + case Qt::RightButton: + return options->commandWindow3(); + default: + // all other buttons pass Activate & Pass Client + return Options::MouseActivateAndPassClick; + } + } + return Options::MouseNothing; +} + +Options::MouseCommand AbstractClient::getWheelCommand(Qt::Orientation orientation, bool *handled) const +{ + *handled = false; + if (orientation != Qt::Vertical) { + return Options::MouseNothing; + } + if (!isActive()) { + *handled = true; + return options->commandWindowWheel(); + } + return Options::MouseNothing; +} + +bool AbstractClient::performMouseCommand(Options::MouseCommand cmd, const QPoint &globalPos) +{ + bool replay = false; + switch(cmd) { + case Options::MouseRaise: + workspace()->raiseClient(this); + break; + case Options::MouseLower: { + workspace()->lowerClient(this); + // used to be activateNextClient(this), then topClientOnDesktop + // since this is a mouseOp it's however safe to use the client under the mouse instead + if (isActive() && options->focusPolicyIsReasonable()) { + AbstractClient *next = workspace()->clientUnderMouse(screen()); + if (next && next != this) + workspace()->requestFocus(next, false); + } + break; + } + case Options::MouseOperationsMenu: + if (isActive() && options->isClickRaise()) + autoRaise(); + workspace()->showWindowMenu(QRect(globalPos, globalPos), this); + break; + case Options::MouseToggleRaiseAndLower: + workspace()->raiseOrLowerClient(this); + break; + case Options::MouseActivateAndRaise: { + replay = isActive(); // for clickraise mode + bool mustReplay = !rules()->checkAcceptFocus(acceptsFocus()); + if (mustReplay) { + auto it = workspace()->stackingOrder().constEnd(), + begin = workspace()->stackingOrder().constBegin(); + while (mustReplay && --it != begin && *it != this) { + AbstractClient *c = qobject_cast(*it); + if (!c || (c->keepAbove() && !keepAbove()) || (keepBelow() && !c->keepBelow())) + continue; // can never raise above "it" + mustReplay = !(c->isOnCurrentDesktop() && c->isOnCurrentActivity() && c->frameGeometry().intersects(frameGeometry())); + } + } + workspace()->takeActivity(this, Workspace::ActivityFocus | Workspace::ActivityRaise); + screens()->setCurrent(globalPos); + replay = replay || mustReplay; + break; + } + case Options::MouseActivateAndLower: + workspace()->requestFocus(this); + workspace()->lowerClient(this); + screens()->setCurrent(globalPos); + replay = replay || !rules()->checkAcceptFocus(acceptsFocus()); + break; + case Options::MouseActivate: + replay = isActive(); // for clickraise mode + workspace()->takeActivity(this, Workspace::ActivityFocus); + screens()->setCurrent(globalPos); + replay = replay || !rules()->checkAcceptFocus(acceptsFocus()); + break; + case Options::MouseActivateRaiseAndPassClick: + workspace()->takeActivity(this, Workspace::ActivityFocus | Workspace::ActivityRaise); + screens()->setCurrent(globalPos); + replay = true; + break; + case Options::MouseActivateAndPassClick: + workspace()->takeActivity(this, Workspace::ActivityFocus); + screens()->setCurrent(globalPos); + replay = true; + break; + case Options::MouseMaximize: + maximize(MaximizeFull); + break; + case Options::MouseRestore: + maximize(MaximizeRestore); + break; + case Options::MouseMinimize: + minimize(); + break; + case Options::MouseAbove: { + StackingUpdatesBlocker blocker(workspace()); + if (keepBelow()) + setKeepBelow(false); + else + setKeepAbove(true); + break; + } + case Options::MouseBelow: { + StackingUpdatesBlocker blocker(workspace()); + if (keepAbove()) + setKeepAbove(false); + else + setKeepBelow(true); + break; + } + case Options::MousePreviousDesktop: + workspace()->windowToPreviousDesktop(this); + break; + case Options::MouseNextDesktop: + workspace()->windowToNextDesktop(this); + break; + case Options::MouseOpacityMore: + if (!isDesktop()) // No point in changing the opacity of the desktop + setOpacity(qMin(opacity() + 0.1, 1.0)); + break; + case Options::MouseOpacityLess: + if (!isDesktop()) // No point in changing the opacity of the desktop + setOpacity(qMax(opacity() - 0.1, 0.1)); + break; + case Options::MouseClose: + closeWindow(); + break; + case Options::MouseActivateRaiseAndMove: + case Options::MouseActivateRaiseAndUnrestrictedMove: + workspace()->raiseClient(this); + workspace()->requestFocus(this); + screens()->setCurrent(globalPos); + // fallthrough + case Options::MouseMove: + case Options::MouseUnrestrictedMove: { + if (!isMovableAcrossScreens()) + break; + if (isMoveResize()) + finishMoveResize(false); + setMoveResizePointerMode(PositionCenter); + setMoveResizePointerButtonDown(true); + setMoveOffset(QPoint(globalPos.x() - x(), globalPos.y() - y())); // map from global + setInvertedMoveOffset(rect().bottomRight() - moveOffset()); + setUnrestrictedMoveResize((cmd == Options::MouseActivateRaiseAndUnrestrictedMove + || cmd == Options::MouseUnrestrictedMove)); + if (!startMoveResize()) + setMoveResizePointerButtonDown(false); + updateCursor(); + break; + } + case Options::MouseResize: + case Options::MouseUnrestrictedResize: { + if (!isResizable() || isShade()) + break; + if (isMoveResize()) + finishMoveResize(false); + setMoveResizePointerButtonDown(true); + const QPoint moveOffset = QPoint(globalPos.x() - x(), globalPos.y() - y()); // map from global + setMoveOffset(moveOffset); + int x = moveOffset.x(), y = moveOffset.y(); + bool left = x < width() / 3; + bool right = x >= 2 * width() / 3; + bool top = y < height() / 3; + bool bot = y >= 2 * height() / 3; + Position mode; + if (top) + mode = left ? PositionTopLeft : (right ? PositionTopRight : PositionTop); + else if (bot) + mode = left ? PositionBottomLeft : (right ? PositionBottomRight : PositionBottom); + else + mode = (x < width() / 2) ? PositionLeft : PositionRight; + setMoveResizePointerMode(mode); + setInvertedMoveOffset(rect().bottomRight() - moveOffset); + setUnrestrictedMoveResize((cmd == Options::MouseUnrestrictedResize)); + if (!startMoveResize()) + setMoveResizePointerButtonDown(false); + updateCursor(); + break; + } + case Options::MouseShade: + toggleShade(); + cancelShadeHoverTimer(); + break; + case Options::MouseSetShade: + setShade(ShadeNormal); + cancelShadeHoverTimer(); + break; + case Options::MouseUnsetShade: + setShade(ShadeNone); + cancelShadeHoverTimer(); + break; + case Options::MouseNothing: + default: + replay = true; + break; + } + return replay; +} + +void AbstractClient::setTransientFor(AbstractClient *transientFor) +{ + if (transientFor == this) { + // cannot be transient for one self + return; + } + if (m_transientFor == transientFor) { + return; + } + m_transientFor = transientFor; + emit transientChanged(); +} + +const AbstractClient *AbstractClient::transientFor() const +{ + return m_transientFor; +} + +AbstractClient *AbstractClient::transientFor() +{ + return m_transientFor; +} + +bool AbstractClient::hasTransientPlacementHint() const +{ + return false; +} + +QRect AbstractClient::transientPlacement(const QRect &bounds) const +{ + Q_UNUSED(bounds); + Q_UNREACHABLE(); + return QRect(); +} + +bool AbstractClient::hasTransient(const AbstractClient *c, bool indirect) const +{ + Q_UNUSED(indirect); + return c->transientFor() == this; +} + +QList< AbstractClient* > AbstractClient::mainClients() const +{ + if (const AbstractClient *t = transientFor()) { + return QList{const_cast< AbstractClient* >(t)}; + } + return QList(); +} + +QList AbstractClient::allMainClients() const +{ + auto result = mainClients(); + foreach (const auto *cl, result) { + result += cl->allMainClients(); + } + return result; +} + +void AbstractClient::setModal(bool m) +{ + // Qt-3.2 can have even modal normal windows :( + if (m_modal == m) + return; + m_modal = m; + emit modalChanged(); + // Changing modality for a mapped window is weird (?) + // _NET_WM_STATE_MODAL should possibly rather be _NET_WM_WINDOW_TYPE_MODAL_DIALOG +} + +bool AbstractClient::isModal() const +{ + return m_modal; +} + +void AbstractClient::addTransient(AbstractClient *cl) +{ + Q_ASSERT(!m_transients.contains(cl)); + Q_ASSERT(cl != this); + m_transients.append(cl); +} + +void AbstractClient::removeTransient(AbstractClient *cl) +{ + m_transients.removeAll(cl); + if (cl->transientFor() == this) { + cl->setTransientFor(nullptr); + } +} + +void AbstractClient::removeTransientFromList(AbstractClient *cl) +{ + m_transients.removeAll(cl); +} + +bool AbstractClient::isActiveFullScreen() const +{ + if (!isFullScreen()) + return false; + + const auto ac = workspace()->mostRecentlyActivatedClient(); // instead of activeClient() - avoids flicker + // according to NETWM spec implementation notes suggests + // "focused windows having state _NET_WM_STATE_FULLSCREEN" to be on the highest layer. + // we'll also take the screen into account + return ac && (ac == this || ac->screen() != screen()|| ac->allMainClients().contains(const_cast(this))); +} + +#define BORDER(which) \ + int AbstractClient::border##which() const \ + { \ + return isDecorated() ? decoration()->border##which() : 0; \ + } + +BORDER(Bottom) +BORDER(Left) +BORDER(Right) +BORDER(Top) +#undef BORDER + +void AbstractClient::addRepaintDuringGeometryUpdates() +{ + const QRect deco_rect = visibleRect(); + addLayerRepaint(m_visibleRectBeforeGeometryUpdate); + addLayerRepaint(deco_rect); // trigger repaint of window's new location + m_visibleRectBeforeGeometryUpdate = deco_rect; +} + +QRect AbstractClient::bufferGeometryBeforeUpdateBlocking() const +{ + return m_bufferGeometryBeforeUpdateBlocking; +} + +QRect AbstractClient::frameGeometryBeforeUpdateBlocking() const +{ + return m_frameGeometryBeforeUpdateBlocking; +} + +QRect AbstractClient::clientGeometryBeforeUpdateBlocking() const +{ + return m_clientGeometryBeforeUpdateBlocking; +} + +void AbstractClient::updateGeometryBeforeUpdateBlocking() +{ + m_bufferGeometryBeforeUpdateBlocking = bufferGeometry(); + m_frameGeometryBeforeUpdateBlocking = frameGeometry(); + m_clientGeometryBeforeUpdateBlocking = clientGeometry(); +} + +void AbstractClient::doMove(int, int) +{ +} + +void AbstractClient::updateInitialMoveResizeGeometry() +{ + m_moveResize.initialGeometry = frameGeometry(); + m_moveResize.geometry = m_moveResize.initialGeometry; + m_moveResize.startScreen = screen(); +} + +void AbstractClient::updateCursor() +{ + Position m = moveResizePointerMode(); + if (!isResizable() || isShade()) + m = PositionCenter; + CursorShape c = Qt::ArrowCursor; + switch(m) { + case PositionTopLeft: + c = KWin::ExtendedCursor::SizeNorthWest; + break; + case PositionBottomRight: + c = KWin::ExtendedCursor::SizeSouthEast; + break; + case PositionBottomLeft: + c = KWin::ExtendedCursor::SizeSouthWest; + break; + case PositionTopRight: + c = KWin::ExtendedCursor::SizeNorthEast; + break; + case PositionTop: + c = KWin::ExtendedCursor::SizeNorth; + break; + case PositionBottom: + c = KWin::ExtendedCursor::SizeSouth; + break; + case PositionLeft: + c = KWin::ExtendedCursor::SizeWest; + break; + case PositionRight: + c = KWin::ExtendedCursor::SizeEast; + break; + default: + if (isMoveResize()) + c = Qt::SizeAllCursor; + else + c = Qt::ArrowCursor; + break; + } + if (c == m_moveResize.cursor) + return; + m_moveResize.cursor = c; + emit moveResizeCursorChanged(c); +} + +void AbstractClient::leaveMoveResize() +{ + workspace()->setMoveResizeClient(nullptr); + setMoveResize(false); + if (ScreenEdges::self()->isDesktopSwitchingMovingClients()) + ScreenEdges::self()->reserveDesktopSwitching(false, Qt::Vertical|Qt::Horizontal); + if (isElectricBorderMaximizing()) { + outline()->hide(); + elevate(false); + } +} + +bool AbstractClient::s_haveResizeEffect = false; + +void AbstractClient::updateHaveResizeEffect() +{ + s_haveResizeEffect = effects && static_cast(effects)->provides(Effect::Resize); +} + +bool AbstractClient::doStartMoveResize() +{ + return true; +} + +void AbstractClient::doFinishMoveResize() +{ +} + +void AbstractClient::positionGeometryTip() +{ +} + +void AbstractClient::doPerformMoveResize() +{ +} + +bool AbstractClient::isWaitingForMoveResizeSync() const +{ + return false; +} + +void AbstractClient::doResizeSync() +{ +} + +void AbstractClient::checkQuickTilingMaximizationZones(int xroot, int yroot) +{ + QuickTileMode mode = QuickTileFlag::None; + bool innerBorder = false; + for (int i=0; i < screens()->count(); ++i) { + + if (!screens()->geometry(i).contains(QPoint(xroot, yroot))) + continue; + + auto isInScreen = [i](const QPoint &pt) { + for (int j = 0; j < screens()->count(); ++j) { + if (j == i) + continue; + if (screens()->geometry(j).contains(pt)) { + return true; + } + } + return false; + }; + + QRect area = workspace()->clientArea(MaximizeArea, QPoint(xroot, yroot), desktop()); + if (options->electricBorderTiling()) { + if (xroot <= area.x() + 20) { + mode |= QuickTileFlag::Left; + innerBorder = isInScreen(QPoint(area.x() - 1, yroot)); + } else if (xroot >= area.x() + area.width() - 20) { + mode |= QuickTileFlag::Right; + innerBorder = isInScreen(QPoint(area.right() + 1, yroot)); + } + } + + if (mode != QuickTileMode(QuickTileFlag::None)) { + if (yroot <= area.y() + area.height() * options->electricBorderCornerRatio()) + mode |= QuickTileFlag::Top; + else if (yroot >= area.y() + area.height() - area.height() * options->electricBorderCornerRatio()) + mode |= QuickTileFlag::Bottom; + } else if (options->electricBorderMaximize() && yroot <= area.y() + 5 && isMaximizable()) { + mode = QuickTileFlag::Maximize; + innerBorder = isInScreen(QPoint(xroot, area.y() - 1)); + } + break; // no point in checking other screens to contain this... "point"... + } + if (mode != electricBorderMode()) { + setElectricBorderMode(mode); + if (innerBorder) { + if (!m_electricMaximizingDelay) { + m_electricMaximizingDelay = new QTimer(this); + m_electricMaximizingDelay->setInterval(250); + m_electricMaximizingDelay->setSingleShot(true); + connect(m_electricMaximizingDelay, &QTimer::timeout, [this]() { + if (isMove()) + setElectricBorderMaximizing(electricBorderMode() != QuickTileMode(QuickTileFlag::None)); + }); + } + m_electricMaximizingDelay->start(); + } else { + setElectricBorderMaximizing(mode != QuickTileMode(QuickTileFlag::None)); + } + } +} + +void AbstractClient::keyPressEvent(uint key_code) +{ + if (!isMove() && !isResize()) + return; + bool is_control = key_code & Qt::CTRL; + bool is_alt = key_code & Qt::ALT; + key_code = key_code & ~Qt::KeyboardModifierMask; + int delta = is_control ? 1 : is_alt ? 32 : 8; + QPoint pos = Cursors::self()->mouse()->pos(); + switch(key_code) { + case Qt::Key_Left: + pos.rx() -= delta; + break; + case Qt::Key_Right: + pos.rx() += delta; + break; + case Qt::Key_Up: + pos.ry() -= delta; + break; + case Qt::Key_Down: + pos.ry() += delta; + break; + case Qt::Key_Space: + case Qt::Key_Return: + case Qt::Key_Enter: + setMoveResizePointerButtonDown(false); + finishMoveResize(false); + updateCursor(); + break; + case Qt::Key_Escape: + setMoveResizePointerButtonDown(false); + finishMoveResize(true); + updateCursor(); + break; + default: + return; + } + Cursors::self()->mouse()->setPos(pos); +} + +QSize AbstractClient::resizeIncrements() const +{ + return QSize(1, 1); +} + +void AbstractClient::dontMoveResize() +{ + setMoveResizePointerButtonDown(false); + stopDelayedMoveResize(); + if (isMoveResize()) + finishMoveResize(false); +} + +AbstractClient::Position AbstractClient::mousePosition() const +{ + if (isDecorated()) { + switch (decoration()->sectionUnderMouse()) { + case Qt::BottomLeftSection: + return PositionBottomLeft; + case Qt::BottomRightSection: + return PositionBottomRight; + case Qt::BottomSection: + return PositionBottom; + case Qt::LeftSection: + return PositionLeft; + case Qt::RightSection: + return PositionRight; + case Qt::TopSection: + return PositionTop; + case Qt::TopLeftSection: + return PositionTopLeft; + case Qt::TopRightSection: + return PositionTopRight; + default: + return PositionCenter; + } + } + return PositionCenter; +} + +void AbstractClient::endMoveResize() +{ + setMoveResizePointerButtonDown(false); + stopDelayedMoveResize(); + if (isMoveResize()) { + finishMoveResize(false); + setMoveResizePointerMode(mousePosition()); + } + updateCursor(); +} + +void AbstractClient::createDecoration(const QRect &oldGeometry) +{ + KDecoration2::Decoration *decoration = Decoration::DecorationBridge::self()->createDecoration(this); + if (decoration) { + QMetaObject::invokeMethod(decoration, "update", Qt::QueuedConnection); + connect(decoration, &KDecoration2::Decoration::shadowChanged, this, &Toplevel::updateShadow); + connect(decoration, &KDecoration2::Decoration::bordersChanged, this, [this]() { + GeometryUpdatesBlocker blocker(this); + const QRect oldGeometry = frameGeometry(); + if (!isShade()) { + checkWorkspacePosition(oldGeometry); + } + emit geometryShapeChanged(this, oldGeometry); + }); + } + setDecoration(decoration); + setFrameGeometry(QRect(oldGeometry.topLeft(), clientSizeToFrameSize(clientSize()))); + + emit geometryShapeChanged(this, oldGeometry); +} + +void AbstractClient::destroyDecoration() +{ + delete m_decoration.decoration; + m_decoration.decoration = nullptr; +} + +bool AbstractClient::decorationHasAlpha() const +{ + if (!isDecorated() || decoration()->isOpaque()) { + // either no decoration or decoration has alpha disabled + return false; + } + return true; +} + +void AbstractClient::triggerDecorationRepaint() +{ + if (isDecorated()) { + decoration()->update(); + } +} + +void AbstractClient::layoutDecorationRects(QRect &left, QRect &top, QRect &right, QRect &bottom) const +{ + if (!isDecorated()) { + return; + } + QRect r = decoration()->rect(); + + top = QRect(r.x(), r.y(), r.width(), borderTop()); + bottom = QRect(r.x(), r.y() + r.height() - borderBottom(), + r.width(), borderBottom()); + left = QRect(r.x(), r.y() + top.height(), + borderLeft(), r.height() - top.height() - bottom.height()); + right = QRect(r.x() + r.width() - borderRight(), r.y() + top.height(), + borderRight(), r.height() - top.height() - bottom.height()); +} + +void AbstractClient::processDecorationMove(const QPoint &localPos, const QPoint &globalPos) +{ + if (isMoveResizePointerButtonDown()) { + handleMoveResize(localPos.x(), localPos.y(), globalPos.x(), globalPos.y()); + return; + } + // TODO: handle modifiers + Position newmode = mousePosition(); + if (newmode != moveResizePointerMode()) { + setMoveResizePointerMode(newmode); + updateCursor(); + } +} + +bool AbstractClient::processDecorationButtonPress(QMouseEvent *event, bool ignoreMenu) +{ + Options::MouseCommand com = Options::MouseNothing; + bool active = isActive(); + if (!wantsInput()) // we cannot be active, use it anyway + active = true; + + // check whether it is a double click + if (event->button() == Qt::LeftButton && titlebarPositionUnderMouse()) { + if (m_decoration.doubleClickTimer.isValid()) { + const qint64 interval = m_decoration.doubleClickTimer.elapsed(); + m_decoration.doubleClickTimer.invalidate(); + if (interval > QGuiApplication::styleHints()->mouseDoubleClickInterval()) { + m_decoration.doubleClickTimer.start(); // expired -> new first click and pot. init + } else { + Workspace::self()->performWindowOperation(this, options->operationTitlebarDblClick()); + dontMoveResize(); + return false; + } + } + else { + m_decoration.doubleClickTimer.start(); // new first click and pot. init, could be invalidated by release - see below + } + } + + if (event->button() == Qt::LeftButton) + com = active ? options->commandActiveTitlebar1() : options->commandInactiveTitlebar1(); + else if (event->button() == Qt::MiddleButton) + com = active ? options->commandActiveTitlebar2() : options->commandInactiveTitlebar2(); + else if (event->button() == Qt::RightButton) + com = active ? options->commandActiveTitlebar3() : options->commandInactiveTitlebar3(); + if (event->button() == Qt::LeftButton + && com != Options::MouseOperationsMenu // actions where it's not possible to get the matching + && com != Options::MouseMinimize) // mouse release event + { + setMoveResizePointerMode(mousePosition()); + setMoveResizePointerButtonDown(true); + setMoveOffset(event->pos()); + setInvertedMoveOffset(rect().bottomRight() - moveOffset()); + setUnrestrictedMoveResize(false); + startDelayedMoveResize(); + updateCursor(); + } + // In the new API the decoration may process the menu action to display an inactive tab's menu. + // If the event is unhandled then the core will create one for the active window in the group. + if (!ignoreMenu || com != Options::MouseOperationsMenu) + performMouseCommand(com, event->globalPos()); + return !( // Return events that should be passed to the decoration in the new API + com == Options::MouseRaise || + com == Options::MouseOperationsMenu || + com == Options::MouseActivateAndRaise || + com == Options::MouseActivate || + com == Options::MouseActivateRaiseAndPassClick || + com == Options::MouseActivateAndPassClick || + com == Options::MouseNothing); +} + +void AbstractClient::processDecorationButtonRelease(QMouseEvent *event) +{ + if (isDecorated()) { + if (event->isAccepted() || !titlebarPositionUnderMouse()) { + invalidateDecorationDoubleClickTimer(); // click was for the deco and shall not init a doubleclick + } + } + + if (event->buttons() == Qt::NoButton) { + setMoveResizePointerButtonDown(false); + stopDelayedMoveResize(); + if (isMoveResize()) { + finishMoveResize(false); + setMoveResizePointerMode(mousePosition()); + } + updateCursor(); + } +} + + +void AbstractClient::startDecorationDoubleClickTimer() +{ + m_decoration.doubleClickTimer.start(); +} + +void AbstractClient::invalidateDecorationDoubleClickTimer() +{ + m_decoration.doubleClickTimer.invalidate(); +} + +bool AbstractClient::providesContextHelp() const +{ + return false; +} + +void AbstractClient::showContextHelp() +{ +} + +QPointer AbstractClient::decoratedClient() const +{ + return m_decoration.client; +} + +void AbstractClient::setDecoratedClient(QPointer< Decoration::DecoratedClientImpl > client) +{ + m_decoration.client = client; +} + +void AbstractClient::enterEvent(const QPoint &globalPos) +{ + if (options->isShadeHover()) { + cancelShadeHoverTimer(); + startShadeHoverTimer(); + } + + if (options->focusPolicy() == Options::ClickToFocus || workspace()->userActionsMenu()->isShown()) + return; + + if (options->isAutoRaise() && !isDesktop() && + !isDock() && workspace()->focusChangeEnabled() && + globalPos != workspace()->focusMousePosition() && + workspace()->topClientOnDesktop(VirtualDesktopManager::self()->current(), + options->isSeparateScreenFocus() ? screen() : -1) != this) { + startAutoRaise(); + } + + if (isDesktop() || isDock()) + return; + // for FocusFollowsMouse, change focus only if the mouse has actually been moved, not if the focus + // change came because of window changes (e.g. closing a window) - #92290 + if (options->focusPolicy() != Options::FocusFollowsMouse + || globalPos != workspace()->focusMousePosition()) { + workspace()->requestDelayFocus(this); + } +} + +void AbstractClient::leaveEvent() +{ + cancelAutoRaise(); + workspace()->cancelDelayFocus(); + cancelShadeHoverTimer(); + startShadeUnhoverTimer(); + // TODO: send hover leave to deco + // TODO: handle Options::FocusStrictlyUnderMouse +} + +QRect AbstractClient::iconGeometry() const +{ + if (!windowManagementInterface() || !waylandServer()) { + // window management interface is only available if the surface is mapped + return QRect(); + } + + int minDistance = INT_MAX; + AbstractClient *candidatePanel = nullptr; + QRect candidateGeom; + + for (auto i = windowManagementInterface()->minimizedGeometries().constBegin(), end = windowManagementInterface()->minimizedGeometries().constEnd(); i != end; ++i) { + AbstractClient *client = waylandServer()->findClient(i.key()); + if (!client) { + continue; + } + const int distance = QPoint(client->pos() - pos()).manhattanLength(); + if (distance < minDistance) { + minDistance = distance; + candidatePanel = client; + candidateGeom = i.value(); + } + } + if (!candidatePanel) { + return QRect(); + } + return candidateGeom.translated(candidatePanel->pos()); +} + +QRect AbstractClient::inputGeometry() const +{ + if (isDecorated()) { + return Toplevel::inputGeometry() + decoration()->resizeOnlyBorders(); + } + return Toplevel::inputGeometry(); +} + +QRect AbstractClient::virtualKeyboardGeometry() const +{ + return m_virtualKeyboardGeometry; +} + +void AbstractClient::setVirtualKeyboardGeometry(const QRect &geo) +{ + // No keyboard anymore + if (geo.isEmpty() && !m_keyboardGeometryRestore.isEmpty()) { + setFrameGeometry(m_keyboardGeometryRestore); + m_keyboardGeometryRestore = QRect(); + } else if (geo.isEmpty()) { + return; + // The keyboard has just been opened (rather than resized) save client geometry for a restore + } else if (m_keyboardGeometryRestore.isEmpty()) { + m_keyboardGeometryRestore = frameGeometry(); + } + + m_virtualKeyboardGeometry = geo; + + // Don't resize Desktop and fullscreen windows + if (isFullScreen() || isDesktop()) { + return; + } + + if (!geo.intersects(m_keyboardGeometryRestore)) { + return; + } + + const QRect availableArea = workspace()->clientArea(MaximizeArea, this); + QRect newWindowGeometry = m_keyboardGeometryRestore; + newWindowGeometry.moveBottom(geo.top()); + newWindowGeometry.setTop(qMax(newWindowGeometry.top(), availableArea.top())); + + setFrameGeometry(newWindowGeometry); +} + +QRect AbstractClient::keyboardGeometryRestore() const +{ + return m_keyboardGeometryRestore; +} + +void AbstractClient::setKeyboardGeometryRestore(const QRect &geom) +{ + m_keyboardGeometryRestore = geom; +} + +bool AbstractClient::dockWantsInput() const +{ + return false; +} + +void AbstractClient::setDesktopFileName(QByteArray name) +{ + name = rules()->checkDesktopFile(name).toUtf8(); + if (name == m_desktopFileName) { + return; + } + m_desktopFileName = name; + updateWindowRules(Rules::DesktopFile); + emit desktopFileNameChanged(); +} + +QString AbstractClient::iconFromDesktopFile() const +{ + if (m_desktopFileName.isEmpty()) { + return {}; + } + + const QString desktopFileName = QString::fromUtf8(m_desktopFileName); + QString desktopFilePath; + + if (QDir::isAbsolutePath(desktopFileName)) { + desktopFilePath = desktopFileName; + } + + if (desktopFilePath.isEmpty()) { + desktopFilePath = QStandardPaths::locate(QStandardPaths::ApplicationsLocation, + desktopFileName); + } + if (desktopFilePath.isEmpty()) { + desktopFilePath = QStandardPaths::locate(QStandardPaths::ApplicationsLocation, + desktopFileName + QLatin1String(".desktop")); + } + + KDesktopFile df(desktopFilePath); + return df.readIcon(); +} + +bool AbstractClient::hasApplicationMenu() const +{ + return ApplicationMenu::self()->applicationMenuEnabled() && !m_applicationMenuServiceName.isEmpty() && !m_applicationMenuObjectPath.isEmpty(); +} + +void AbstractClient::updateApplicationMenuServiceName(const QString &serviceName) +{ + const bool old_hasApplicationMenu = hasApplicationMenu(); + + m_applicationMenuServiceName = serviceName; + + const bool new_hasApplicationMenu = hasApplicationMenu(); + + emit applicationMenuChanged(); + if (old_hasApplicationMenu != new_hasApplicationMenu) { + emit hasApplicationMenuChanged(new_hasApplicationMenu); + } +} + +void AbstractClient::updateApplicationMenuObjectPath(const QString &objectPath) +{ + const bool old_hasApplicationMenu = hasApplicationMenu(); + + m_applicationMenuObjectPath = objectPath; + + const bool new_hasApplicationMenu = hasApplicationMenu(); + + emit applicationMenuChanged(); + if (old_hasApplicationMenu != new_hasApplicationMenu) { + emit hasApplicationMenuChanged(new_hasApplicationMenu); + } +} + +void AbstractClient::setApplicationMenuActive(bool applicationMenuActive) +{ + if (m_applicationMenuActive != applicationMenuActive) { + m_applicationMenuActive = applicationMenuActive; + emit applicationMenuActiveChanged(applicationMenuActive); + } +} + +void AbstractClient::showApplicationMenu(int actionId) +{ + if (isDecorated()) { + decoration()->showApplicationMenu(actionId); + } else { + // we don't know where the application menu button will be, show it in the top left corner instead + Workspace::self()->showApplicationMenu(QRect(), this, actionId); + } +} + +bool AbstractClient::unresponsive() const +{ + return m_unresponsive; +} + +void AbstractClient::setUnresponsive(bool unresponsive) +{ + if (m_unresponsive != unresponsive) { + m_unresponsive = unresponsive; + emit unresponsiveChanged(m_unresponsive); + emit captionChanged(); + } +} + +QString AbstractClient::shortcutCaptionSuffix() const +{ + if (shortcut().isEmpty()) { + return QString(); + } + return QLatin1String(" {") + shortcut().toString() + QLatin1Char('}'); +} + +AbstractClient *AbstractClient::findClientWithSameCaption() const +{ + auto fetchNameInternalPredicate = [this](const AbstractClient *cl) { + return (!cl->isSpecialWindow() || cl->isToolbar()) && cl != this && cl->captionNormal() == captionNormal() && cl->captionSuffix() == captionSuffix(); + }; + return workspace()->findAbstractClient(fetchNameInternalPredicate); +} + +QString AbstractClient::caption() const +{ + QString cap = captionNormal() + captionSuffix(); + if (unresponsive()) { + cap += QLatin1String(" "); + cap += i18nc("Application is not responding, appended to window title", "(Not Responding)"); + } + return cap; +} + +void AbstractClient::removeRule(Rules* rule) +{ + m_rules.remove(rule); +} + +void AbstractClient::discardTemporaryRules() +{ + m_rules.discardTemporary(); +} + +void AbstractClient::evaluateWindowRules() +{ + setupWindowRules(true); + applyWindowRules(); +} + +void AbstractClient::setOnActivities(QStringList newActivitiesList) +{ + Q_UNUSED(newActivitiesList) +} + +void AbstractClient::checkNoBorder() +{ + setNoBorder(false); +} + +bool AbstractClient::groupTransient() const +{ + return false; +} + +const Group *AbstractClient::group() const +{ + return nullptr; +} + +Group *AbstractClient::group() +{ + return nullptr; +} + +bool AbstractClient::isInternal() const +{ + return false; +} + +bool AbstractClient::supportsWindowRules() const +{ + return false; +} + +QMargins AbstractClient::frameMargins() const +{ + return QMargins(borderLeft(), borderTop(), borderRight(), borderBottom()); +} + +QPoint AbstractClient::framePosToClientPos(const QPoint &point) const +{ + return point + QPoint(borderLeft(), borderTop()); +} + +QPoint AbstractClient::clientPosToFramePos(const QPoint &point) const +{ + return point - QPoint(borderLeft(), borderTop()); +} + +QSize AbstractClient::frameSizeToClientSize(const QSize &size) const +{ + const int width = size.width() - borderLeft() - borderRight(); + const int height = size.height() - borderTop() - borderBottom(); + return QSize(width, height); +} + +QSize AbstractClient::clientSizeToFrameSize(const QSize &size) const +{ + const int width = size.width() + borderLeft() + borderRight(); + const int height = size.height() + borderTop() + borderBottom(); + return QSize(width, height); +} + +QRect AbstractClient::frameRectToClientRect(const QRect &rect) const +{ + const QPoint position = framePosToClientPos(rect.topLeft()); + const QSize size = frameSizeToClientSize(rect.size()); + return QRect(position, size); +} + +QRect AbstractClient::clientRectToFrameRect(const QRect &rect) const +{ + const QPoint position = clientPosToFramePos(rect.topLeft()); + const QSize size = clientSizeToFrameSize(rect.size()); + return QRect(position, size); +} + +void AbstractClient::setElectricBorderMode(QuickTileMode mode) +{ + if (mode != QuickTileMode(QuickTileFlag::Maximize)) { + // sanitize the mode, ie. simplify "invalid" combinations + if ((mode & QuickTileFlag::Horizontal) == QuickTileMode(QuickTileFlag::Horizontal)) + mode &= ~QuickTileMode(QuickTileFlag::Horizontal); + if ((mode & QuickTileFlag::Vertical) == QuickTileMode(QuickTileFlag::Vertical)) + mode &= ~QuickTileMode(QuickTileFlag::Vertical); + } + m_electricMode = mode; +} + +void AbstractClient::setElectricBorderMaximizing(bool maximizing) +{ + m_electricMaximizing = maximizing; + if (maximizing) + outline()->show(electricBorderMaximizeGeometry(Cursors::self()->mouse()->pos(), desktop()), moveResizeGeometry()); + else + outline()->hide(); + elevate(maximizing); +} + +QRect AbstractClient::electricBorderMaximizeGeometry(QPoint pos, int desktop) +{ + if (electricBorderMode() == QuickTileMode(QuickTileFlag::Maximize)) { + if (maximizeMode() == MaximizeFull) + return geometryRestore(); + else + return workspace()->clientArea(MaximizeArea, pos, desktop); + } + + QRect ret = workspace()->clientArea(MaximizeArea, pos, desktop); + if (electricBorderMode() & QuickTileFlag::Left) + ret.setRight(ret.left()+ret.width()/2 - 1); + else if (electricBorderMode() & QuickTileFlag::Right) + ret.setLeft(ret.right()-(ret.width()-ret.width()/2) + 1); + if (electricBorderMode() & QuickTileFlag::Top) + ret.setBottom(ret.top()+ret.height()/2 - 1); + else if (electricBorderMode() & QuickTileFlag::Bottom) + ret.setTop(ret.bottom()-(ret.height()-ret.height()/2) + 1); + + return ret; +} + +void AbstractClient::setQuickTileMode(QuickTileMode mode, bool keyboard) +{ + // Only allow quick tile on a regular window. + if (!isResizable()) { + return; + } + + workspace()->updateFocusMousePosition(Cursors::self()->mouse()->pos()); // may cause leave event + + GeometryUpdatesBlocker blocker(this); + + if (mode == QuickTileMode(QuickTileFlag::Maximize)) { + m_quickTileMode = int(QuickTileFlag::None); + if (maximizeMode() == MaximizeFull) { + setMaximize(false, false); + } else { + QRect prev_geom_restore = geometryRestore(); // setMaximize() would set moveResizeGeom as geom_restore + m_quickTileMode = int(QuickTileFlag::Maximize); + setMaximize(true, true); + QRect clientArea = workspace()->clientArea(MaximizeArea, this); + if (frameGeometry().top() != clientArea.top()) { + QRect r(frameGeometry()); + r.moveTop(clientArea.top()); + setFrameGeometry(r); + } + setGeometryRestore(prev_geom_restore); + } + doSetQuickTileMode(); + emit quickTileModeChanged(); + return; + } + + // sanitize the mode, ie. simplify "invalid" combinations + if ((mode & QuickTileFlag::Horizontal) == QuickTileMode(QuickTileFlag::Horizontal)) + mode &= ~QuickTileMode(QuickTileFlag::Horizontal); + if ((mode & QuickTileFlag::Vertical) == QuickTileMode(QuickTileFlag::Vertical)) + mode &= ~QuickTileMode(QuickTileFlag::Vertical); + + setElectricBorderMode(mode); // used by ::electricBorderMaximizeGeometry(.) + + // restore from maximized so that it is possible to tile maximized windows with one hit or by dragging + if (maximizeMode() != MaximizeRestore) { + + if (mode != QuickTileMode(QuickTileFlag::None)) { + // decorations may turn off some borders when tiled + const ForceGeometry_t geom_mode = isDecorated() ? ForceGeometrySet : NormalGeometrySet; + m_quickTileMode = int(QuickTileFlag::None); // Temporary, so the maximize code doesn't get all confused + + setMaximize(false, false); + + setFrameGeometry(electricBorderMaximizeGeometry(keyboard ? frameGeometry().center() : Cursors::self()->mouse()->pos(), desktop()), geom_mode); + // Store the mode change + m_quickTileMode = mode; + } else { + m_quickTileMode = mode; + setMaximize(false, false); + } + + doSetQuickTileMode(); + emit quickTileModeChanged(); + + return; + } + + if (mode != QuickTileMode(QuickTileFlag::None)) { + QPoint whichScreen = keyboard ? frameGeometry().center() : Cursors::self()->mouse()->pos(); + + // If trying to tile to the side that the window is already tiled to move the window to the next + // screen if it exists, otherwise toggle the mode (set QuickTileFlag::None) + if (quickTileMode() == mode) { + const int numScreens = screens()->count(); + const int curScreen = screen(); + int nextScreen = curScreen; + QVarLengthArray screens(numScreens); + for (int i = 0; i < numScreens; ++i) // Cache + screens[i] = Screens::self()->geometry(i); + for (int i = 0; i < numScreens; ++i) { + + if (i == curScreen) + continue; + + if (screens[i].bottom() <= screens[curScreen].top() || screens[i].top() >= screens[curScreen].bottom()) + continue; // not in horizontal line + + const int x = screens[i].center().x(); + if ((mode & QuickTileFlag::Horizontal) == QuickTileMode(QuickTileFlag::Left)) { + if (x >= screens[curScreen].center().x() || (curScreen != nextScreen && x <= screens[nextScreen].center().x())) + continue; // not left of current or more left then found next + } else if ((mode & QuickTileFlag::Horizontal) == QuickTileMode(QuickTileFlag::Right)) { + if (x <= screens[curScreen].center().x() || (curScreen != nextScreen && x >= screens[nextScreen].center().x())) + continue; // not right of current or more right then found next + } + + nextScreen = i; + } + + if (nextScreen == curScreen) { + mode = QuickTileFlag::None; // No other screens, toggle tiling + } else { + // Move to other screen + setFrameGeometry(geometryRestore().translated(screens[nextScreen].topLeft() - screens[curScreen].topLeft())); + whichScreen = screens[nextScreen].center(); + + // Swap sides + if (mode & QuickTileFlag::Horizontal) { + mode = (~mode & QuickTileFlag::Horizontal) | (mode & QuickTileFlag::Vertical); + } + } + setElectricBorderMode(mode); // used by ::electricBorderMaximizeGeometry(.) + } else if (quickTileMode() == QuickTileMode(QuickTileFlag::None)) { + // Not coming out of an existing tile, not shifting monitors, we're setting a brand new tile. + // Store geometry first, so we can go out of this tile later. + setGeometryRestore(frameGeometry()); + } + + if (mode != QuickTileMode(QuickTileFlag::None)) { + m_quickTileMode = mode; + // decorations may turn off some borders when tiled + const ForceGeometry_t geom_mode = isDecorated() ? ForceGeometrySet : NormalGeometrySet; + // Temporary, so the maximize code doesn't get all confused + m_quickTileMode = int(QuickTileFlag::None); + setFrameGeometry(electricBorderMaximizeGeometry(whichScreen, desktop()), geom_mode); + } + + // Store the mode change + m_quickTileMode = mode; + } + + if (mode == QuickTileMode(QuickTileFlag::None)) { + m_quickTileMode = int(QuickTileFlag::None); + // Untiling, so just restore geometry, and we're done. + if (!geometryRestore().isValid()) // invalid if we started maximized and wait for placement + setGeometryRestore(frameGeometry()); + // decorations may turn off some borders when tiled + const ForceGeometry_t geom_mode = isDecorated() ? ForceGeometrySet : NormalGeometrySet; + setFrameGeometry(geometryRestore(), geom_mode); + checkWorkspacePosition(); // Just in case it's a different screen + } + doSetQuickTileMode(); + emit quickTileModeChanged(); +} + +void AbstractClient::doSetQuickTileMode() +{ +} + +void AbstractClient::sendToScreen(int newScreen) +{ + newScreen = rules()->checkScreen(newScreen); + if (isActive()) { + screens()->setCurrent(newScreen); + // might impact the layer of a fullscreen window + foreach (AbstractClient *cc, workspace()->allClientList()) { + if (cc->isFullScreen() && cc->screen() == newScreen) { + cc->updateLayer(); + } + } + } + if (screen() == newScreen) // Don't use isOnScreen(), that's true even when only partially + return; + + GeometryUpdatesBlocker blocker(this); + + // operating on the maximized / quicktiled window would leave the old geom_restore behind, + // so we clear the state first + MaximizeMode maxMode = maximizeMode(); + QuickTileMode qtMode = quickTileMode(); + if (maxMode != MaximizeRestore) + maximize(MaximizeRestore); + if (qtMode != QuickTileMode(QuickTileFlag::None)) + setQuickTileMode(QuickTileFlag::None, true); + + QRect oldScreenArea = workspace()->clientArea(MaximizeArea, this); + QRect screenArea = workspace()->clientArea(MaximizeArea, newScreen, desktop()); + + // the window can have its center so that the position correction moves the new center onto + // the old screen, what will tile it where it is. Ie. the screen is not changed + // this happens esp. with electric border quicktiling + if (qtMode != QuickTileMode(QuickTileFlag::None)) + keepInArea(oldScreenArea); + + QRect oldGeom = frameGeometry(); + QRect newGeom = oldGeom; + // move the window to have the same relative position to the center of the screen + // (i.e. one near the middle of the right edge will also end up near the middle of the right edge) + QPoint center = newGeom.center() - oldScreenArea.center(); + center.setX(center.x() * screenArea.width() / oldScreenArea.width()); + center.setY(center.y() * screenArea.height() / oldScreenArea.height()); + center += screenArea.center(); + newGeom.moveCenter(center); + setFrameGeometry(newGeom); + + // If the window was inside the old screen area, explicitly make sure its inside also the new screen area. + // Calling checkWorkspacePosition() should ensure that, but when moving to a small screen the window could + // be big enough to overlap outside of the new screen area, making struts from other screens come into effect, + // which could alter the resulting geometry. + if (oldScreenArea.contains(oldGeom)) { + keepInArea(screenArea); + } + + // align geom_restore - checkWorkspacePosition operates on it + setGeometryRestore(frameGeometry()); + + checkWorkspacePosition(oldGeom); + + // re-align geom_restore to constrained geometry + setGeometryRestore(frameGeometry()); + + // finally reset special states + // NOTICE that MaximizeRestore/QuickTileFlag::None checks are required. + // eg. setting QuickTileFlag::None would break maximization + if (maxMode != MaximizeRestore) + maximize(maxMode); + if (qtMode != QuickTileMode(QuickTileFlag::None) && qtMode != quickTileMode()) + setQuickTileMode(qtMode, true); + + auto tso = workspace()->ensureStackingOrder(transients()); + for (auto it = tso.constBegin(), end = tso.constEnd(); it != end; ++it) + (*it)->sendToScreen(newScreen); +} + +void AbstractClient::checkWorkspacePosition(QRect oldGeometry, int oldDesktop, QRect oldClientGeometry) +{ + if (isDock() || isDesktop() || !isPlaceable()) { + return; + } + enum { Left = 0, Top, Right, Bottom }; + const int border[4] = { borderLeft(), borderTop(), borderRight(), borderBottom() }; + if( !oldGeometry.isValid()) + oldGeometry = frameGeometry(); + if( oldDesktop == -2 ) + oldDesktop = desktop(); + if (!oldClientGeometry.isValid()) + oldClientGeometry = oldGeometry.adjusted(border[Left], border[Top], -border[Right], -border[Bottom]); + if (isFullScreen()) { + QRect area = workspace()->clientArea(FullScreenArea, this); + if (frameGeometry() != area) + setFrameGeometry(area); + return; + } + + if (maximizeMode() != MaximizeRestore) { + GeometryUpdatesBlocker block(this); + changeMaximize(false, false, true); // adjust size + const QRect screenArea = workspace()->clientArea(ScreenArea, this); + QRect geom = frameGeometry(); + checkOffscreenPosition(&geom, screenArea); + setFrameGeometry(geom); + return; + } + + if (quickTileMode() != QuickTileMode(QuickTileFlag::None)) { + setFrameGeometry(electricBorderMaximizeGeometry(frameGeometry().center(), desktop())); + return; + } + + // this can be true only if this window was mapped before KWin + // was started - in such case, don't adjust position to workarea, + // because the window already had its position, and if a window + // with a strut altering the workarea would be managed in initialization + // after this one, this window would be moved + if (!workspace() || workspace()->initializing()) + return; + + // If the window was touching an edge before but not now move it so it is again. + // Old and new maximums have different starting values so windows on the screen + // edge will move when a new strut is placed on the edge. + QRect oldScreenArea; + if( workspace()->inUpdateClientArea()) { + // we need to find the screen area as it was before the change + oldScreenArea = QRect( 0, 0, workspace()->oldDisplayWidth(), workspace()->oldDisplayHeight()); + int distance = INT_MAX; + foreach(const QRect &r, workspace()->previousScreenSizes()) { + int d = r.contains( oldGeometry.center()) ? 0 : ( r.center() - oldGeometry.center()).manhattanLength(); + if( d < distance ) { + distance = d; + oldScreenArea = r; + } + } + } else { + oldScreenArea = workspace()->clientArea(ScreenArea, oldGeometry.center(), oldDesktop); + } + const QRect oldGeomTall = QRect(oldGeometry.x(), oldScreenArea.y(), oldGeometry.width(), oldScreenArea.height()); // Full screen height + const QRect oldGeomWide = QRect(oldScreenArea.x(), oldGeometry.y(), oldScreenArea.width(), oldGeometry.height()); // Full screen width + int oldTopMax = oldScreenArea.y(); + int oldRightMax = oldScreenArea.x() + oldScreenArea.width(); + int oldBottomMax = oldScreenArea.y() + oldScreenArea.height(); + int oldLeftMax = oldScreenArea.x(); + const QRect screenArea = workspace()->clientArea(ScreenArea, geometryRestore().center(), desktop()); + int topMax = screenArea.y(); + int rightMax = screenArea.x() + screenArea.width(); + int bottomMax = screenArea.y() + screenArea.height(); + int leftMax = screenArea.x(); + QRect newGeom = geometryRestore(); // geometry(); + QRect newClientGeom = newGeom.adjusted(border[Left], border[Top], -border[Right], -border[Bottom]); + const QRect newGeomTall = QRect(newGeom.x(), screenArea.y(), newGeom.width(), screenArea.height()); // Full screen height + const QRect newGeomWide = QRect(screenArea.x(), newGeom.y(), screenArea.width(), newGeom.height()); // Full screen width + // Get the max strut point for each side where the window is (E.g. Highest point for + // the bottom struts bounded by the window's left and right sides). + + // These 4 compute old bounds ... + auto moveAreaFunc = workspace()->inUpdateClientArea() ? + &Workspace::previousRestrictedMoveArea : //... the restricted areas changed + &Workspace::restrictedMoveArea; //... when e.g. active desktop or screen changes + + for (const QRect &r : (workspace()->*moveAreaFunc)(oldDesktop, StrutAreaTop)) { + QRect rect = r & oldGeomTall; + if (!rect.isEmpty()) + oldTopMax = qMax(oldTopMax, rect.y() + rect.height()); + } + for (const QRect &r : (workspace()->*moveAreaFunc)(oldDesktop, StrutAreaRight)) { + QRect rect = r & oldGeomWide; + if (!rect.isEmpty()) + oldRightMax = qMin(oldRightMax, rect.x()); + } + for (const QRect &r : (workspace()->*moveAreaFunc)(oldDesktop, StrutAreaBottom)) { + QRect rect = r & oldGeomTall; + if (!rect.isEmpty()) + oldBottomMax = qMin(oldBottomMax, rect.y()); + } + for (const QRect &r : (workspace()->*moveAreaFunc)(oldDesktop, StrutAreaLeft)) { + QRect rect = r & oldGeomWide; + if (!rect.isEmpty()) + oldLeftMax = qMax(oldLeftMax, rect.x() + rect.width()); + } + + // These 4 compute new bounds + for (const QRect &r : workspace()->restrictedMoveArea(desktop(), StrutAreaTop)) { + QRect rect = r & newGeomTall; + if (!rect.isEmpty()) + topMax = qMax(topMax, rect.y() + rect.height()); + } + for (const QRect &r : workspace()->restrictedMoveArea(desktop(), StrutAreaRight)) { + QRect rect = r & newGeomWide; + if (!rect.isEmpty()) + rightMax = qMin(rightMax, rect.x()); + } + for (const QRect &r : workspace()->restrictedMoveArea(desktop(), StrutAreaBottom)) { + QRect rect = r & newGeomTall; + if (!rect.isEmpty()) + bottomMax = qMin(bottomMax, rect.y()); + } + for (const QRect &r : workspace()->restrictedMoveArea(desktop(), StrutAreaLeft)) { + QRect rect = r & newGeomWide; + if (!rect.isEmpty()) + leftMax = qMax(leftMax, rect.x() + rect.width()); + } + + + // Check if the sides were inside or touching but are no longer + bool keep[4] = {false, false, false, false}; + bool save[4] = {false, false, false, false}; + int padding[4] = {0, 0, 0, 0}; + if (oldGeometry.x() >= oldLeftMax) + save[Left] = newGeom.x() < leftMax; + if (oldGeometry.x() == oldLeftMax) + keep[Left] = newGeom.x() != leftMax; + else if (oldClientGeometry.x() == oldLeftMax && newClientGeom.x() != leftMax) { + padding[0] = border[Left]; + keep[Left] = true; + } + if (oldGeometry.y() >= oldTopMax) + save[Top] = newGeom.y() < topMax; + if (oldGeometry.y() == oldTopMax) + keep[Top] = newGeom.y() != topMax; + else if (oldClientGeometry.y() == oldTopMax && newClientGeom.y() != topMax) { + padding[1] = border[Left]; + keep[Top] = true; + } + if (oldGeometry.right() <= oldRightMax - 1) + save[Right] = newGeom.right() > rightMax - 1; + if (oldGeometry.right() == oldRightMax - 1) + keep[Right] = newGeom.right() != rightMax - 1; + else if (oldClientGeometry.right() == oldRightMax - 1 && newClientGeom.right() != rightMax - 1) { + padding[2] = border[Right]; + keep[Right] = true; + } + if (oldGeometry.bottom() <= oldBottomMax - 1) + save[Bottom] = newGeom.bottom() > bottomMax - 1; + if (oldGeometry.bottom() == oldBottomMax - 1) + keep[Bottom] = newGeom.bottom() != bottomMax - 1; + else if (oldClientGeometry.bottom() == oldBottomMax - 1 && newClientGeom.bottom() != bottomMax - 1) { + padding[3] = border[Bottom]; + keep[Bottom] = true; + } + + // if randomly touches opposing edges, do not favor either + if (keep[Left] && keep[Right]) { + keep[Left] = keep[Right] = false; + padding[0] = padding[2] = 0; + } + if (keep[Top] && keep[Bottom]) { + keep[Top] = keep[Bottom] = false; + padding[1] = padding[3] = 0; + } + + if (save[Left] || keep[Left]) + newGeom.moveLeft(qMax(leftMax, screenArea.x()) - padding[0]); + if (padding[0] && screens()->intersecting(newGeom) > 1) + newGeom.moveLeft(newGeom.left() + padding[0]); + if (save[Top] || keep[Top]) + newGeom.moveTop(qMax(topMax, screenArea.y()) - padding[1]); + if (padding[1] && screens()->intersecting(newGeom) > 1) + newGeom.moveTop(newGeom.top() + padding[1]); + if (save[Right] || keep[Right]) + newGeom.moveRight(qMin(rightMax - 1, screenArea.right()) + padding[2]); + if (padding[2] && screens()->intersecting(newGeom) > 1) + newGeom.moveRight(newGeom.right() - padding[2]); + if (oldGeometry.x() >= oldLeftMax && newGeom.x() < leftMax) + newGeom.setLeft(qMax(leftMax, screenArea.x())); + else if (oldClientGeometry.x() >= oldLeftMax && newGeom.x() + border[Left] < leftMax) { + newGeom.setLeft(qMax(leftMax, screenArea.x()) - border[Left]); + if (screens()->intersecting(newGeom) > 1) + newGeom.setLeft(newGeom.left() + border[Left]); + } + if (save[Bottom] || keep[Bottom]) + newGeom.moveBottom(qMin(bottomMax - 1, screenArea.bottom()) + padding[3]); + if (padding[3] && screens()->intersecting(newGeom) > 1) + newGeom.moveBottom(newGeom.bottom() - padding[3]); + if (oldGeometry.y() >= oldTopMax && newGeom.y() < topMax) + newGeom.setTop(qMax(topMax, screenArea.y())); + else if (oldClientGeometry.y() >= oldTopMax && newGeom.y() + border[Top] < topMax) { + newGeom.setTop(qMax(topMax, screenArea.y()) - border[Top]); + if (screens()->intersecting(newGeom) > 1) + newGeom.setTop(newGeom.top() + border[Top]); + } + + checkOffscreenPosition(&newGeom, screenArea); + // Obey size hints. TODO: We really should make sure it stays in the right place + if (!isShade()) + newGeom.setSize(constrainFrameSize(newGeom.size())); + + if (newGeom != frameGeometry()) + setFrameGeometry(newGeom); +} + +void AbstractClient::checkOffscreenPosition(QRect* geom, const QRect& screenArea) +{ + if (geom->left() > screenArea.right()) { + geom->moveLeft(screenArea.right() - screenArea.width()/4); + } else if (geom->right() < screenArea.left()) { + geom->moveRight(screenArea.left() + screenArea.width()/4); + } + if (geom->top() > screenArea.bottom()) { + geom->moveTop(screenArea.bottom() - screenArea.height()/4); + } else if (geom->bottom() < screenArea.top()) { + geom->moveBottom(screenArea.top() + screenArea.width()/4); + } +} + +/** + * Returns the appropriate frame size for the current client size. + * + * This is equivalent to clientSizeToFrameSize(constrainClientSize(clientSize())). + */ +QSize AbstractClient::adjustedSize() const +{ + return clientSizeToFrameSize(constrainClientSize(clientSize())); +} + +/** + * Constrains the client size @p size according to a set of the window's size hints. + * + * Default implementation applies only minimum and maximum size constraints. + */ +QSize AbstractClient::constrainClientSize(const QSize &size, SizeMode mode) const +{ + Q_UNUSED(mode) + + int width = size.width(); + int height = size.height(); + + // When user is resizing the window, the move resize geometry may have negative width or + // height. In which case, we need to set negative dimensions to reasonable values. + if (width < 1) { + width = 1; + } + if (height < 1) { + height = 1; + } + + const QSize minimumSize = minSize(); + const QSize maximumSize = maxSize(); + + width = qBound(minimumSize.width(), width, maximumSize.width()); + height = qBound(minimumSize.height(), height, maximumSize.height()); + + return QSize(width, height); +} + +/** + * Constrains the frame size @p size according to a set of the window's size hints. + */ +QSize AbstractClient::constrainFrameSize(const QSize &size, SizeMode mode) const +{ + const QSize unconstrainedClientSize = frameSizeToClientSize(size); + const QSize constrainedClientSize = constrainClientSize(unconstrainedClientSize, mode); + return clientSizeToFrameSize(constrainedClientSize); +} + +/** + * Returns @c true if the AbstractClient can be shown in full screen mode; otherwise @c false. + * + * Default implementation returns @c false. + */ +bool AbstractClient::isFullScreenable() const +{ + return false; +} + +/** + * Returns @c true if the AbstractClient is currently being shown in full screen mode; otherwise @c false. + * + * A client in full screen mode occupies the entire screen with no window frame around it. + * + * Default implementation returns @c false. + */ +bool AbstractClient::isFullScreen() const +{ + return false; +} + +/** + * Returns whether requests initiated by the user to enter or leave full screen mode are honored. + * + * Default implementation returns @c false. + */ +bool AbstractClient::userCanSetFullScreen() const +{ + return false; +} + +/** + * Asks the AbstractClient to enter or leave full screen mode. + * + * Default implementation does nothing. + * + * @param set @c true if the AbstractClient has to be shown in full screen mode, otherwise @c false + * @param user @c true if the request is initiated by the user, otherwise @c false + */ +void AbstractClient::setFullScreen(bool set, bool user) +{ + Q_UNUSED(set) + Q_UNUSED(user) + qCWarning(KWIN_CORE, "%s doesn't support setting fullscreen state", metaObject()->className()); +} + +/** + * Returns @c true if the AbstractClient can be minimized; otherwise @c false. + * + * Default implementation returns @c false. + */ +bool AbstractClient::isMinimizable() const +{ + return false; +} + +/** + * Returns @c true if the AbstractClient can be maximized; otherwise @c false. + * + * Default implementation returns @c false. + */ +bool AbstractClient::isMaximizable() const +{ + return false; +} + +/** + * Returns the currently applied maximize mode. + * + * Default implementation returns MaximizeRestore. + */ +MaximizeMode AbstractClient::maximizeMode() const +{ + return MaximizeRestore; +} + +/** + * Returns the last requested maximize mode. + * + * On X11, this method always matches maximizeMode(). On Wayland, it is asynchronous. + * + * Default implementation matches maximizeMode(). + */ +MaximizeMode AbstractClient::requestedMaximizeMode() const +{ + return maximizeMode(); +} + +/** + * Returns the geometry of the AbstractClient before it was maximized or quick tiled. + */ +QRect AbstractClient::geometryRestore() const +{ + return m_maximizeGeometryRestore; +} + +/** + * Sets the geometry of the AbstractClient before it was maximized or quick tiled to @p rect. + */ +void AbstractClient::setGeometryRestore(const QRect &rect) +{ + m_maximizeGeometryRestore = rect; +} + +/** + * Toggles the maximized state along specified dimensions @p horizontal and @p vertical. + * + * If @p adjust is @c true, only frame geometry will be updated to match requestedMaximizeMode(). + * + * Default implementation does nothing. + */ +void AbstractClient::changeMaximize(bool horizontal, bool vertical, bool adjust) +{ + Q_UNUSED(horizontal) + Q_UNUSED(vertical) + Q_UNUSED(adjust) + qCWarning(KWIN_CORE, "%s doesn't support setting maximized state", metaObject()->className()); +} + +void AbstractClient::updateDecoration(bool check_workspace_pos, bool force) +{ + Q_UNUSED(check_workspace_pos) + Q_UNUSED(force) + qCWarning(KWIN_CORE, "%s doesn't support server side decorations", metaObject()->className()); +} + +bool AbstractClient::noBorder() const +{ + return true; +} + +bool AbstractClient::userCanSetNoBorder() const +{ + return false; +} + +void AbstractClient::setNoBorder(bool set) +{ + Q_UNUSED(set) + qCWarning(KWIN_CORE, "%s doesn't support setting decorations", metaObject()->className()); +} + +void AbstractClient::showOnScreenEdge() +{ + qCWarning(KWIN_CORE, "%s doesn't support screen edge activation", metaObject()->className()); +} + +bool AbstractClient::isPlaceable() const +{ + return true; +} + +} diff --git a/abstract_client.h b/abstract_client.h new file mode 100644 index 0000000..0e830a7 --- /dev/null +++ b/abstract_client.h @@ -0,0 +1,1371 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_ABSTRACT_CLIENT_H +#define KWIN_ABSTRACT_CLIENT_H + +#include "toplevel.h" +#include "options.h" +#include "rules.h" +#include "cursor.h" + +#include + +#include +#include + +namespace KWaylandServer +{ +class PlasmaWindowInterface; +} + +namespace KDecoration2 +{ +class Decoration; +} + +namespace KWin +{ +class Group; + +namespace TabBox +{ +class TabBoxClientImpl; +} + +namespace Decoration +{ +class DecoratedClientImpl; +class DecorationPalette; +} + +class KWIN_EXPORT AbstractClient : public Toplevel +{ + Q_OBJECT + + /** + * Whether this Client is fullScreen. A Client might either be fullScreen due to the _NET_WM property + * or through a legacy support hack. The fullScreen state can only be changed if the Client does not + * use the legacy hack. To be sure whether the state changed, connect to the notify signal. + */ + Q_PROPERTY(bool fullScreen READ isFullScreen WRITE setFullScreen NOTIFY fullScreenChanged) + + /** + * Whether the Client can be set to fullScreen. The property is evaluated each time it is invoked. + * Because of that there is no notify signal. + */ + Q_PROPERTY(bool fullScreenable READ isFullScreenable) + + /** + * Whether this Client is active or not. Use Workspace::activateClient() to activate a Client. + * @see Workspace::activateClient + */ + Q_PROPERTY(bool active READ isActive NOTIFY activeChanged) + + /** + * The desktop this Client is on. If the Client is on all desktops the property has value -1. + * This is a legacy property, use x11DesktopIds instead + */ + Q_PROPERTY(int desktop READ desktop WRITE setDesktop NOTIFY desktopChanged) + + /** + * Whether the Client is on all desktops. That is desktop is -1. + */ + Q_PROPERTY(bool onAllDesktops READ isOnAllDesktops WRITE setOnAllDesktops NOTIFY desktopChanged) + + /** + * The x11 ids for all desktops this client is in. On X11 this list will always have a length of 1 + */ + Q_PROPERTY(QVector x11DesktopIds READ x11DesktopIds NOTIFY x11DesktopIdsChanged) + + /** + * Indicates that the window should not be included on a taskbar. + */ + Q_PROPERTY(bool skipTaskbar READ skipTaskbar WRITE setSkipTaskbar NOTIFY skipTaskbarChanged) + + /** + * Indicates that the window should not be included on a Pager. + */ + Q_PROPERTY(bool skipPager READ skipPager WRITE setSkipPager NOTIFY skipPagerChanged) + + /** + * Whether the Client should be excluded from window switching effects. + */ + Q_PROPERTY(bool skipSwitcher READ skipSwitcher WRITE setSkipSwitcher NOTIFY skipSwitcherChanged) + + /** + * Whether the window can be closed by the user. The value is evaluated each time the getter is called. + * Because of that no changed signal is provided. + */ + Q_PROPERTY(bool closeable READ isCloseable) + + Q_PROPERTY(QIcon icon READ icon NOTIFY iconChanged) + + /** + * Whether the Client is set to be kept above other windows. + */ + Q_PROPERTY(bool keepAbove READ keepAbove WRITE setKeepAbove NOTIFY keepAboveChanged) + + /** + * Whether the Client is set to be kept below other windows. + */ + Q_PROPERTY(bool keepBelow READ keepBelow WRITE setKeepBelow NOTIFY keepBelowChanged) + + /** + * Whether the Client can be shaded. The property is evaluated each time it is invoked. + * Because of that there is no notify signal. + */ + Q_PROPERTY(bool shadeable READ isShadeable) + + /** + * Whether the Client is shaded. + */ + Q_PROPERTY(bool shade READ isShade WRITE setShade NOTIFY shadeChanged) + + /** + * Whether the Client can be minimized. The property is evaluated each time it is invoked. + * Because of that there is no notify signal. + */ + Q_PROPERTY(bool minimizable READ isMinimizable) + + /** + * Whether the Client is minimized. + */ + Q_PROPERTY(bool minimized READ isMinimized WRITE setMinimized NOTIFY minimizedChanged) + + /** + * The optional geometry representing the minimized Client in e.g a taskbar. + * See _NET_WM_ICON_GEOMETRY at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + * The value is evaluated each time the getter is called. + * Because of that no changed signal is provided. + */ + Q_PROPERTY(QRect iconGeometry READ iconGeometry) + + /** + * Returns whether the window is any of special windows types (desktop, dock, splash, ...), + * i.e. window types that usually don't have a window frame and the user does not use window + * management (moving, raising,...) on them. + * The value is evaluated each time the getter is called. + * Because of that no changed signal is provided. + */ + Q_PROPERTY(bool specialWindow READ isSpecialWindow) + + /** + * Whether window state _NET_WM_STATE_DEMANDS_ATTENTION is set. This state indicates that some + * action in or with the window happened. For example, it may be set by the Window Manager if + * the window requested activation but the Window Manager refused it, or the application may set + * it if it finished some work. This state may be set by both the Client and the Window Manager. + * It should be unset by the Window Manager when it decides the window got the required attention + * (usually, that it got activated). + */ + Q_PROPERTY(bool demandsAttention READ isDemandingAttention WRITE demandAttention NOTIFY demandsAttentionChanged) + + /** + * The Caption of the Client. Read from WM_NAME property together with a suffix for hostname and shortcut. + * To read only the caption as provided by WM_NAME, use the getter with an additional @c false value. + */ + Q_PROPERTY(QString caption READ caption NOTIFY captionChanged) + + /** + * Minimum size as specified in WM_NORMAL_HINTS + */ + Q_PROPERTY(QSize minSize READ minSize) + + /** + * Maximum size as specified in WM_NORMAL_HINTS + */ + Q_PROPERTY(QSize maxSize READ maxSize) + + /** + * Whether the Client can accept keyboard focus. + * The value is evaluated each time the getter is called. + * Because of that no changed signal is provided. + */ + Q_PROPERTY(bool wantsInput READ wantsInput) + + /** + * Whether the Client is a transient Window to another Window. + * @see transientFor + */ + Q_PROPERTY(bool transient READ isTransient NOTIFY transientChanged) + + /** + * The Client to which this Client is a transient if any. + */ + Q_PROPERTY(KWin::AbstractClient *transientFor READ transientFor NOTIFY transientChanged) + + /** + * Whether the Client represents a modal window. + */ + Q_PROPERTY(bool modal READ isModal NOTIFY modalChanged) + + /** + * The geometry of this Client. Be aware that depending on resize mode the frameGeometryChanged + * signal might be emitted at each resize step or only at the end of the resize operation. + * + * @deprecated Use frameGeometry + */ + Q_PROPERTY(QRect geometry READ frameGeometry WRITE setFrameGeometry) + + /** + * The geometry of this Client. Be aware that depending on resize mode the frameGeometryChanged + * signal might be emitted at each resize step or only at the end of the resize operation. + */ + Q_PROPERTY(QRect frameGeometry READ frameGeometry WRITE setFrameGeometry) + + /** + * Whether the Client is currently being moved by the user. + * Notify signal is emitted when the Client starts or ends move/resize mode. + */ + Q_PROPERTY(bool move READ isMove NOTIFY moveResizedChanged) + + /** + * Whether the Client is currently being resized by the user. + * Notify signal is emitted when the Client starts or ends move/resize mode. + */ + Q_PROPERTY(bool resize READ isResize NOTIFY moveResizedChanged) + + /** + * Whether the decoration is currently using an alpha channel. + */ + Q_PROPERTY(bool decorationHasAlpha READ decorationHasAlpha) + + /** + * Whether the window has a decoration or not. + * This property is not allowed to be set by applications themselves. + * The decision whether a window has a border or not belongs to the window manager. + * If this property gets abused by application developers, it will be removed again. + */ + Q_PROPERTY(bool noBorder READ noBorder WRITE setNoBorder) + + /** + * Whether the Client provides context help. Mostly needed by decorations to decide whether to + * show the help button or not. + */ + Q_PROPERTY(bool providesContextHelp READ providesContextHelp CONSTANT) + + /** + * Whether the Client can be maximized both horizontally and vertically. + * The property is evaluated each time it is invoked. + * Because of that there is no notify signal. + */ + Q_PROPERTY(bool maximizable READ isMaximizable) + + /** + * Whether the Client is moveable. Even if it is not moveable, it might be possible to move + * it to another screen. The property is evaluated each time it is invoked. + * Because of that there is no notify signal. + * @see moveableAcrossScreens + */ + Q_PROPERTY(bool moveable READ isMovable) + + /** + * Whether the Client can be moved to another screen. The property is evaluated each time it is invoked. + * Because of that there is no notify signal. + * @see moveable + */ + Q_PROPERTY(bool moveableAcrossScreens READ isMovableAcrossScreens) + + /** + * Whether the Client can be resized. The property is evaluated each time it is invoked. + * Because of that there is no notify signal. + */ + Q_PROPERTY(bool resizeable READ isResizable) + + /** + * The desktop file name of the application this AbstractClient belongs to. + * + * This is either the base name without full path and without file extension of the + * desktop file for the window's application (e.g. "org.kde.foo"). + * + * The application's desktop file name can also be the full path to the desktop file + * (e.g. "/opt/kde/share/org.kde.foo.desktop") in case it's not in a standard location. + */ + Q_PROPERTY(QByteArray desktopFileName READ desktopFileName NOTIFY desktopFileNameChanged) + + /** + * Whether an application menu is available for this Client + */ + Q_PROPERTY(bool hasApplicationMenu READ hasApplicationMenu NOTIFY hasApplicationMenuChanged) + + /** + * Whether the application menu for this Client is currently opened + */ + Q_PROPERTY(bool applicationMenuActive READ applicationMenuActive NOTIFY applicationMenuActiveChanged) + + /** + * Whether this client is unresponsive. + * + * When an application failed to react on a ping request in time, it is + * considered unresponsive. This usually indicates that the application froze or crashed. + */ + Q_PROPERTY(bool unresponsive READ unresponsive NOTIFY unresponsiveChanged) + + /** + * The color scheme set on this client + * Absolute file path, or name of palette in the user's config directory following KColorSchemes format. + * An empty string indicates the default palette from kdeglobals is used. + * @note this indicates the colour scheme requested, which might differ from the theme applied if the colorScheme cannot be found + */ + Q_PROPERTY(QString colorScheme READ colorScheme NOTIFY colorSchemeChanged) + +public: + ~AbstractClient() override; + + QWeakPointer tabBoxClient() const { + return m_tabBoxClient.toWeakRef(); + } + bool isFirstInTabBox() const { + return m_firstInTabBox; + } + bool skipSwitcher() const { + return m_skipSwitcher; + } + void setSkipSwitcher(bool set); + + bool skipTaskbar() const { + return m_skipTaskbar; + } + void setSkipTaskbar(bool set); + void setOriginalSkipTaskbar(bool set); + bool originalSkipTaskbar() const { + return m_originalSkipTaskbar; + } + + bool skipPager() const { + return m_skipPager; + } + void setSkipPager(bool set); + + const QIcon &icon() const { + return m_icon; + } + + bool isZombie() const; + bool isActive() const { + return m_active; + } + /** + * Sets the client's active state to \a act. + * + * This function does only change the visual appearance of the client, + * it does not change the focus setting. Use + * Workspace::activateClient() or Workspace::requestFocus() instead. + * + * If a client receives or looses the focus, it calls setActive() on + * its own. + */ + void setActive(bool); + + bool keepAbove() const { + return m_keepAbove; + } + void setKeepAbove(bool); + bool keepBelow() const { + return m_keepBelow; + } + void setKeepBelow(bool); + + void demandAttention(bool set = true); + bool isDemandingAttention() const { + return m_demandsAttention; + } + + void cancelAutoRaise(); + + bool wantsTabFocus() const; + + QMargins frameMargins() const override; + QPoint clientPos() const override { + return QPoint(borderLeft(), borderTop()); + } + + virtual void updateMouseGrab(); + /** + * @returns The caption consisting of captionNormal and captionSuffix + * @see captionNormal + * @see captionSuffix + */ + QString caption() const; + /** + * @returns The caption as set by the AbstractClient without any suffix. + * @see caption + * @see captionSuffix + */ + virtual QString captionNormal() const = 0; + /** + * @returns The suffix added to the caption (e.g. shortcut, machine name, etc.) + * @see caption + * @see captionNormal + */ + virtual QString captionSuffix() const = 0; + virtual bool isPlaceable() const; + virtual bool isCloseable() const = 0; + // TODO: remove boolean trap + virtual bool isShown(bool shaded_is_shown) const = 0; + virtual bool isHiddenInternal() const = 0; + // TODO: remove boolean trap + virtual void hideClient(bool hide) = 0; + virtual bool isFullScreenable() const; + virtual bool isFullScreen() const; + // TODO: remove boolean trap + virtual AbstractClient *findModal(bool allow_itself = false) = 0; + virtual bool isTransient() const; + /** + * @returns Whether there is a hint available to place the AbstractClient on it's parent, default @c false. + * @see transientPlacementHint + */ + virtual bool hasTransientPlacementHint() const; + /** + * Only valid id hasTransientPlacementHint is true + * @returns The position the transient wishes to position itself + */ + virtual QRect transientPlacement(const QRect &bounds) const; + const AbstractClient* transientFor() const; + AbstractClient* transientFor(); + /** + * @returns @c true if c is the transient_for window for this client, + * or recursively the transient_for window + * @todo: remove boolean trap + */ + virtual bool hasTransient(const AbstractClient* c, bool indirect) const; + const QList& transients() const; // Is not indirect + virtual void addTransient(AbstractClient *client); + virtual void removeTransient(AbstractClient* cl); + virtual QList mainClients() const; // Call once before loop , is not indirect + QList allMainClients() const; // Call once before loop , is indirect + /** + * Returns true for "special" windows and false for windows which are "normal" + * (normal=window which has a border, can be moved by the user, can be closed, etc.) + * true for Desktop, Dock, Splash, Override and TopMenu (and Toolbar??? - for now) + * false for Normal, Dialog, Utility and Menu (and Toolbar??? - not yet) TODO + */ + bool isSpecialWindow() const; + void sendToScreen(int screen); + const QKeySequence &shortcut() const { + return _shortcut; + } + void setShortcut(const QString &cut); + bool performMouseCommand(Options::MouseCommand, const QPoint &globalPos); + void setOnAllDesktops(bool set); + void setDesktop(int); + void enterDesktop(VirtualDesktop *desktop); + void leaveDesktop(VirtualDesktop *desktop); + + /** + * Set the window as being on the attached list of desktops + * On X11 it will be set to the last entry + */ + void setDesktops(QVector desktops); + + int desktop() const override { + return m_desktops.isEmpty() ? (int)NET::OnAllDesktops : m_desktops.last()->x11DesktopNumber(); + } + QVector desktops() const override { + return m_desktops; + } + QVector x11DesktopIds() const; + + void setMinimized(bool set); + /** + * Minimizes this client plus its transients + */ + void minimize(bool avoid_animation = false); + void unminimize(bool avoid_animation = false); + bool isMinimized() const { + return m_minimized; + } + virtual void setFullScreen(bool set, bool user = true); + + virtual void setClientShown(bool shown); + + QRect geometryRestore() const; + virtual MaximizeMode maximizeMode() const; + virtual MaximizeMode requestedMaximizeMode() const; + void maximize(MaximizeMode); + /** + * Sets the maximization according to @p vertically and @p horizontally. + */ + Q_INVOKABLE void setMaximize(bool vertically, bool horizontally); + virtual bool noBorder() const; + virtual void setNoBorder(bool set); + virtual void blockActivityUpdates(bool b = true) = 0; + QPalette palette() const; + const Decoration::DecorationPalette *decorationPalette() const; + /** + * Returns whether the window is resizable or has a fixed size. + */ + virtual bool isResizable() const = 0; + /** + * Returns whether the window is moveable or has a fixed position. + */ + virtual bool isMovable() const = 0; + /** + * Returns whether the window can be moved to another screen. + */ + virtual bool isMovableAcrossScreens() const = 0; + /** + * @c true only for @c ShadeNormal + */ + bool isShade() const { + return shadeMode() == ShadeNormal; + } + ShadeMode shadeMode() const; // Prefer isShade() + void setShade(bool set); + void setShade(ShadeMode mode); + void toggleShade(); + void cancelShadeHoverTimer(); + /** + * Whether the Client can be shaded. Default implementation returns @c false. + */ + virtual bool isShadeable() const; + virtual bool isMaximizable() const; + virtual bool isMinimizable() const; + virtual QRect iconGeometry() const; + virtual bool userCanSetFullScreen() const; + virtual bool userCanSetNoBorder() const; + virtual void checkNoBorder(); + virtual void setOnActivities(QStringList newActivitiesList); + virtual void setOnAllActivities(bool set) = 0; + const WindowRules* rules() const { + return &m_rules; + } + void removeRule(Rules* r); + void setupWindowRules(bool ignore_temporary); + void evaluateWindowRules(); + virtual void applyWindowRules(); + virtual bool takeFocus() = 0; + virtual bool wantsInput() const = 0; + /** + * Whether a dock window wants input. + * + * By default KWin doesn't pass focus to a dock window unless a force activate + * request is provided. + * + * This method allows to have dock windows take focus also through flags set on + * the window. + * + * The default implementation returns @c false. + */ + virtual bool dockWantsInput() const; + void checkWorkspacePosition(QRect oldGeometry = QRect(), int oldDesktop = -2, QRect oldClientGeometry = QRect()); + virtual xcb_timestamp_t userTime() const; + virtual void updateWindowRules(Rules::Types selection); + + void growHorizontal(); + void shrinkHorizontal(); + void growVertical(); + void shrinkVertical(); + void updateMoveResize(const QPointF ¤tGlobalCursor); + /** + * Ends move resize when all pointer buttons are up again. + */ + void endMoveResize(); + void keyPressEvent(uint key_code); + + void enterEvent(const QPoint &globalPos); + void leaveEvent(); + + /** + * These values represent positions inside an area + */ + enum Position { + // without prefix, they'd conflict with Qt::TopLeftCorner etc. :( + PositionCenter = 0x00, + PositionLeft = 0x01, + PositionRight = 0x02, + PositionTop = 0x04, + PositionBottom = 0x08, + PositionTopLeft = PositionLeft | PositionTop, + PositionTopRight = PositionRight | PositionTop, + PositionBottomLeft = PositionLeft | PositionBottom, + PositionBottomRight = PositionRight | PositionBottom + }; + Position titlebarPosition() const; + bool titlebarPositionUnderMouse() const; + + // a helper for the workspace window packing. tests for screen validity and updates since in maximization case as with normal moving + void packTo(int left, int top); + + /** + * Sets the quick tile mode ("snap") of this window. + * This will also handle preserving and restoring of window geometry as necessary. + * @param mode The tile mode (left/right) to give this window. + * @param keyboard Defines whether to take keyboard cursor into account. + */ + void setQuickTileMode(QuickTileMode mode, bool keyboard = false); + QuickTileMode quickTileMode() const { + return QuickTileMode(m_quickTileMode); + } + Layer layer() const override; + void updateLayer(); + + void placeIn(const QRect &area); + + enum ForceGeometry_t { NormalGeometrySet, ForceGeometrySet }; + virtual void move(int x, int y, ForceGeometry_t force = NormalGeometrySet); + void move(const QPoint &p, ForceGeometry_t force = NormalGeometrySet); + virtual void resizeWithChecks(const QSize& s, ForceGeometry_t force = NormalGeometrySet) = 0; + void keepInArea(QRect area, bool partial = false); + virtual QSize minSize() const; + virtual QSize maxSize() const; + virtual void setFrameGeometry(const QRect &rect, ForceGeometry_t force = NormalGeometrySet) = 0; + + /** + * How to resize the window in order to obey constraints (mainly aspect ratios). + */ + enum SizeMode { + SizeModeAny, + SizeModeFixedW, ///< Try not to affect width + SizeModeFixedH, ///< Try not to affect height + SizeModeMax ///< Try not to make it larger in either direction + }; + + virtual QSize constrainClientSize(const QSize &size, SizeMode mode = SizeModeAny) const; + QSize constrainFrameSize(const QSize &size, SizeMode mode = SizeModeAny) const; + QSize adjustedSize() const; + + /** + * Calculates the matching client position for the given frame position @p point. + */ + virtual QPoint framePosToClientPos(const QPoint &point) const; + /** + * Calculates the matching frame position for the given client position @p point. + */ + virtual QPoint clientPosToFramePos(const QPoint &point) const; + /** + * Calculates the matching client size for the given frame size @p size. + * + * Notice that size constraints won't be applied. + * + * Default implementation returns the frame size with frame margins being excluded. + */ + virtual QSize frameSizeToClientSize(const QSize &size) const; + /** + * Calculates the matching frame size for the given client size @p size. + * + * Notice that size constraints won't be applied. + * + * Default implementation returns the client size with frame margins being included. + */ + virtual QSize clientSizeToFrameSize(const QSize &size) const; + /** + * Calculates the matching client rect for the given frame rect @p rect. + * + * Notice that size constraints won't be applied. + */ + QRect frameRectToClientRect(const QRect &rect) const; + /** + * Calculates the matching frame rect for the given client rect @p rect. + * + * Notice that size constraints won't be applied. + */ + QRect clientRectToFrameRect(const QRect &rect) const; + + /** + * Returns @c true if the Client is being interactively moved; otherwise @c false. + */ + bool isMove() const { + return isMoveResize() && moveResizePointerMode() == PositionCenter; + } + /** + * Returns @c true if the Client is being interactively resized; otherwise @c false. + */ + bool isResize() const { + return isMoveResize() && moveResizePointerMode() != PositionCenter; + } + /** + * Cursor shape for move/resize mode. + */ + CursorShape cursor() const { + return m_moveResize.cursor; + } + + virtual StrutRect strutRect(StrutArea area) const; + StrutRects strutRects() const; + virtual bool hasStrut() const; + + void setModal(bool modal); + bool isModal() const; + + /** + * Determines the mouse command for the given @p button in the current state. + * + * The @p handled argument specifies whether the button was handled or not. + * This value should be used to determine whether the mouse button should be + * passed to the AbstractClient or being filtered out. + */ + Options::MouseCommand getMouseCommand(Qt::MouseButton button, bool *handled) const; + Options::MouseCommand getWheelCommand(Qt::Orientation orientation, bool *handled) const; + + // decoration related + KDecoration2::Decoration *decoration() { + return m_decoration.decoration; + } + const KDecoration2::Decoration *decoration() const { + return m_decoration.decoration; + } + bool isDecorated() const { + return m_decoration.decoration != nullptr; + } + QPointer decoratedClient() const; + void setDecoratedClient(QPointer client); + bool decorationHasAlpha() const; + void triggerDecorationRepaint(); + virtual void layoutDecorationRects(QRect &left, QRect &top, QRect &right, QRect &bottom) const; + void processDecorationMove(const QPoint &localPos, const QPoint &globalPos); + bool processDecorationButtonPress(QMouseEvent *event, bool ignoreMenu = false); + void processDecorationButtonRelease(QMouseEvent *event); + + /** + * TODO: fix boolean traps + */ + virtual void updateDecoration(bool check_workspace_pos, bool force = false); + + /** + * Returns whether the window provides context help or not. If it does, + * you should show a help menu item or a help button like '?' and call + * contextHelp() if this is invoked. + * + * Default implementation returns @c false. + * @see showContextHelp; + */ + virtual bool providesContextHelp() const; + + /** + * Invokes context help on the window. Only works if the window + * actually provides context help. + * + * Default implementation does nothing. + * + * @see providesContextHelp() + */ + virtual void showContextHelp(); + + QRect inputGeometry() const override; + + /** + * @returns the geometry of the virtual keyboard + * This geometry is in global coordinates + */ + QRect virtualKeyboardGeometry() const; + + /** + * Sets the geometry of the virtual keyboard, The window may resize itself in order to make space for the keybaord + * This geometry is in global coordinates + */ + virtual void setVirtualKeyboardGeometry(const QRect &geo); + + /** + * Restores the AbstractClient after it had been hidden due to show on screen edge functionality. + * The AbstractClient also gets raised (e.g. Panel mode windows can cover) and the AbstractClient + * gets informed in a window specific way that it is shown and raised again. + */ + virtual void showOnScreenEdge(); + + QByteArray desktopFileName() const { + return m_desktopFileName; + } + + /** + * Tries to terminate the process of this AbstractClient. + * + * Implementing subclasses can perform a windowing system solution for terminating. + */ + virtual void killWindow() = 0; + virtual void destroyClient() = 0; + + enum class SameApplicationCheck { + RelaxedForActive = 1 << 0, + AllowCrossProcesses = 1 << 1 + }; + Q_DECLARE_FLAGS(SameApplicationChecks, SameApplicationCheck) + static bool belongToSameApplication(const AbstractClient* c1, const AbstractClient* c2, SameApplicationChecks checks = SameApplicationChecks()); + + bool hasApplicationMenu() const; + bool applicationMenuActive() const { + return m_applicationMenuActive; + } + void setApplicationMenuActive(bool applicationMenuActive); + + QString applicationMenuServiceName() const { + return m_applicationMenuServiceName; + } + QString applicationMenuObjectPath() const { + return m_applicationMenuObjectPath; + } + + virtual QString preferredColorScheme() const; + QString colorScheme() const; + void setColorScheme(const QString &colorScheme); + + /** + * Request showing the application menu bar + * @param actionId The DBus menu ID of the action that should be highlighted, 0 for the root menu + */ + void showApplicationMenu(int actionId); + + bool unresponsive() const; + + virtual bool isInitialPositionSet() const { + return false; + } + + /** + * Default implementation returns @c null. + * Mostly intended for X11 clients, from EWMH: + * @verbatim + * If the WM_TRANSIENT_FOR property is set to None or Root window, the window should be + * treated as a transient for all other windows in the same group. It has been noted that this + * is a slight ICCCM violation, but as this behavior is pretty standard for many toolkits and + * window managers, and is extremely unlikely to break anything, it seems reasonable to document + * it as standard. + * @endverbatim + */ + virtual bool groupTransient() const; + /** + * Default implementation returns @c null. + * + * Mostly for X11 clients, holds the client group + */ + virtual const Group *group() const; + /** + * Default implementation returns @c null. + * + * Mostly for X11 clients, holds the client group + */ + virtual Group *group(); + + /** + * Returns whether this is an internal client. + * + * Internal clients are created by KWin and used for special purpose windows, + * like the task switcher, etc. + * + * Default implementation returns @c false. + */ + virtual bool isInternal() const; + + /** + * Returns whether window rules can be applied to this client. + * + * Default implementation returns @c false. + */ + virtual bool supportsWindowRules() const; + + /** + * Return window management interface + */ + KWaylandServer::PlasmaWindowInterface *windowManagementInterface() const { + return m_windowManagementInterface; + } + +public Q_SLOTS: + virtual void closeWindow() = 0; + +Q_SIGNALS: + void fullScreenChanged(); + void skipTaskbarChanged(); + void skipPagerChanged(); + void skipSwitcherChanged(); + void iconChanged(); + void activeChanged(); + void keepAboveChanged(bool); + void keepBelowChanged(bool); + /** + * Emitted whenever the demands attention state changes. + */ + void demandsAttentionChanged(); + void desktopPresenceChanged(KWin::AbstractClient*, int); // to be forwarded by Workspace + void desktopChanged(); + void x11DesktopIdsChanged(); + void shadeChanged(); + void minimizedChanged(); + void clientMinimized(KWin::AbstractClient* client, bool animate); + void clientUnminimized(KWin::AbstractClient* client, bool animate); + void paletteChanged(const QPalette &p); + void colorSchemeChanged(); + void captionChanged(); + void clientMaximizedStateChanged(KWin::AbstractClient*, MaximizeMode); + void clientMaximizedStateChanged(KWin::AbstractClient* c, bool h, bool v); + void transientChanged(); + void modalChanged(); + void quickTileModeChanged(); + void moveResizedChanged(); + void moveResizeCursorChanged(CursorShape); + void clientStartUserMovedResized(KWin::AbstractClient*); + void clientStepUserMovedResized(KWin::AbstractClient *, const QRect&); + void clientFinishUserMovedResized(KWin::AbstractClient*); + void closeableChanged(bool); + void minimizeableChanged(bool); + void shadeableChanged(bool); + void maximizeableChanged(bool); + void desktopFileNameChanged(); + void applicationMenuChanged(); + void hasApplicationMenuChanged(bool); + void applicationMenuActiveChanged(bool); + void unresponsiveChanged(bool); + +protected: + AbstractClient(); + void setFirstInTabBox(bool enable) { + m_firstInTabBox = enable; + } + void setIcon(const QIcon &icon); + void startAutoRaise(); + void autoRaise(); + bool isMostRecentlyRaised() const; + void markAsZombie(); + /** + * Whether the window accepts focus. + * The difference to wantsInput is that the implementation should not check rules and return + * what the window effectively supports. + */ + virtual bool acceptsFocus() const = 0; + /** + * Called from setActive once the active value got updated, but before the changed signal + * is emitted. + * + * Default implementation does nothing. + */ + virtual void doSetActive(); + /** + * Called from setKeepAbove once the keepBelow value got updated, but before the changed signal + * is emitted. + * + * Default implementation does nothing. + */ + virtual void doSetKeepAbove(); + /** + * Called from setKeepBelow once the keepBelow value got updated, but before the changed signal + * is emitted. + * + * Default implementation does nothing. + */ + virtual void doSetKeepBelow(); + /** + * Called from setShade() once the shadeMode value got updated, but before the changed signal + * is emitted. + * + * Default implementation does nothing. + */ + virtual void doSetShade(ShadeMode previousShadeMode); + /** + * Called from setDeskop once the desktop value got updated, but before the changed signal + * is emitted. + * + * Default implementation does nothing. + */ + virtual void doSetDesktop(); + /** + * Called from @ref minimize and @ref unminimize once the minimized value got updated, but before the + * changed signal is emitted. + * + * Default implementation does nothig. + */ + virtual void doMinimize(); + virtual bool belongsToSameApplication(const AbstractClient *other, SameApplicationChecks checks) const = 0; + + virtual void doSetSkipTaskbar(); + virtual void doSetSkipPager(); + virtual void doSetSkipSwitcher(); + virtual void doSetDemandsAttention(); + virtual void doSetQuickTileMode(); + + void setupWindowManagementInterface(); + void destroyWindowManagementInterface(); + void updateColorScheme(); + void setTransientFor(AbstractClient *transientFor); + /** + * Just removes the @p cl from the transients without any further checks. + */ + void removeTransientFromList(AbstractClient* cl); + + virtual Layer belongsToLayer() const; + virtual bool belongsToDesktop() const; + void invalidateLayer(); + bool isActiveFullScreen() const; + virtual Layer layerForDock() const; + + // electric border / quick tiling + void setElectricBorderMode(QuickTileMode mode); + QuickTileMode electricBorderMode() const { + return m_electricMode; + } + void setElectricBorderMaximizing(bool maximizing); + bool isElectricBorderMaximizing() const { + return m_electricMaximizing; + } + QRect electricBorderMaximizeGeometry(QPoint pos, int desktop); + void updateQuickTileMode(QuickTileMode newMode) { + m_quickTileMode = newMode; + } + + // geometry handling + void checkOffscreenPosition(QRect *geom, const QRect &screenArea); + int borderLeft() const; + int borderRight() const; + int borderTop() const; + int borderBottom() const; + virtual void changeMaximize(bool horizontal, bool vertical, bool adjust); + void setGeometryRestore(const QRect &rect); + + /** + * Called from move after updating the geometry. Can be reimplemented to perform specific tasks. + * The base implementation does nothing. + */ + virtual void doMove(int x, int y); + void blockGeometryUpdates(bool block); + void blockGeometryUpdates(); + void unblockGeometryUpdates(); + bool areGeometryUpdatesBlocked() const; + enum PendingGeometry_t { + PendingGeometryNone, + PendingGeometryNormal, + PendingGeometryForced + }; + PendingGeometry_t pendingGeometryUpdate() const; + void setPendingGeometryUpdate(PendingGeometry_t update); + QRect bufferGeometryBeforeUpdateBlocking() const; + QRect frameGeometryBeforeUpdateBlocking() const; + QRect clientGeometryBeforeUpdateBlocking() const; + void updateGeometryBeforeUpdateBlocking(); + /** + * Schedules a repaint for the visibleRect before and after a + * geometry update. The current visibleRect is stored for the + * next time this method is called as the before geometry. + */ + void addRepaintDuringGeometryUpdates(); + + /** + * @returns whether the Client is currently in move resize mode + */ + bool isMoveResize() const { + return m_moveResize.enabled; + } + /** + * Sets whether the Client is in move resize mode to @p enabled. + */ + void setMoveResize(bool enabled) { + m_moveResize.enabled = enabled; + } + /** + * @returns whether the move resize mode is unrestricted. + */ + bool isUnrestrictedMoveResize() const { + return m_moveResize.unrestricted; + } + /** + * Sets whether move resize mode is unrestricted to @p set. + */ + void setUnrestrictedMoveResize(bool set) { + m_moveResize.unrestricted = set; + } + QPoint moveOffset() const { + return m_moveResize.offset; + } + void setMoveOffset(const QPoint &offset) { + m_moveResize.offset = offset; + } + QPoint invertedMoveOffset() const { + return m_moveResize.invertedOffset; + } + void setInvertedMoveOffset(const QPoint &offset) { + m_moveResize.invertedOffset = offset; + } + QRect initialMoveResizeGeometry() const { + return m_moveResize.initialGeometry; + } + /** + * Sets the initial move resize geometry to the current geometry. + */ + void updateInitialMoveResizeGeometry(); + QRect moveResizeGeometry() const { + return m_moveResize.geometry; + } + void setMoveResizeGeometry(const QRect &geo) { + m_moveResize.geometry = geo; + } + Position moveResizePointerMode() const { + return m_moveResize.pointer; + } + void setMoveResizePointerMode(Position mode) { + m_moveResize.pointer = mode; + } + bool isMoveResizePointerButtonDown() const { + return m_moveResize.buttonDown; + } + void setMoveResizePointerButtonDown(bool down) { + m_moveResize.buttonDown = down; + } + int moveResizeStartScreen() const { + return m_moveResize.startScreen; + } + void checkUnrestrictedMoveResize(); + /** + * Sets an appropriate cursor shape for the logical mouse position. + */ + void updateCursor(); + void startDelayedMoveResize(); + void stopDelayedMoveResize(); + bool startMoveResize(); + /** + * Called from startMoveResize. + * + * Implementing classes should return @c false if starting move resize should + * get aborted. In that case startMoveResize will also return @c false. + * + * Base implementation returns @c true. + */ + virtual bool doStartMoveResize(); + virtual void doFinishMoveResize(); + void finishMoveResize(bool cancel); + /** + * Leaves the move resize mode. + * + * Inheriting classes must invoke the base implementation which + * ensures that the internal mode is properly ended. + */ + virtual void leaveMoveResize(); + virtual void positionGeometryTip(); + void performMoveResize(); + /** + * Called from performMoveResize() after actually performing the change of geometry. + * Implementing subclasses can perform windowing system specific handling here. + * + * Default implementation does nothing. + */ + virtual void doPerformMoveResize(); + /* + * Checks if the mouse cursor is near the edge of the screen and if so + * activates quick tiling or maximization + */ + void checkQuickTilingMaximizationZones(int xroot, int yroot); + /** + * Whether a sync request is still pending. + * Default implementation returns @c false. + */ + virtual bool isWaitingForMoveResizeSync() const; + /** + * Called during handling a resize. Implementing subclasses can use this + * method to perform windowing system specific syncing. + * + * Default implementation does nothing. + */ + virtual void doResizeSync(); + void handleMoveResize(int x, int y, int x_root, int y_root); + void handleMoveResize(const QPoint &local, const QPoint &global); + void dontMoveResize(); + + virtual QSize resizeIncrements() const; + + /** + * Returns the position depending on the Decoration's section under mouse. + * If no decoration it returns PositionCenter. + */ + Position mousePosition() const; + + static bool haveResizeEffect() { + return s_haveResizeEffect; + } + static void updateHaveResizeEffect(); + static void resetHaveResizeEffect() { + s_haveResizeEffect = false; + } + + void setDecoration(KDecoration2::Decoration *decoration) { + m_decoration.decoration = decoration; + } + virtual void createDecoration(const QRect &oldGeometry); + virtual void destroyDecoration(); + void startDecorationDoubleClickTimer(); + void invalidateDecorationDoubleClickTimer(); + + void setDesktopFileName(QByteArray name); + QString iconFromDesktopFile() const; + + void updateApplicationMenuServiceName(const QString &serviceName); + void updateApplicationMenuObjectPath(const QString &objectPath); + + void setUnresponsive(bool unresponsive); + + virtual void setShortcutInternal(); + QString shortcutCaptionSuffix() const; + virtual void updateCaption() = 0; + + /** + * Looks for another AbstractClient with same captionNormal and captionSuffix. + * If no such AbstractClient exists @c nullptr is returned. + */ + AbstractClient *findClientWithSameCaption() const; + + void finishWindowRules(); + void discardTemporaryRules(); + + bool tabTo(AbstractClient *other, bool behind, bool activate); + + void startShadeHoverTimer(); + void startShadeUnhoverTimer(); + + // The geometry that the client should be restored when the virtual keyboard closes + QRect keyboardGeometryRestore() const; + void setKeyboardGeometryRestore(const QRect &geom); + + QRect m_virtualKeyboardGeometry; +private Q_SLOTS: + void shadeHover(); + void shadeUnhover(); + +private: + void handlePaletteChange(); + QSharedPointer m_tabBoxClient; + bool m_firstInTabBox = false; + bool m_skipTaskbar = false; + /** + * Unaffected by KWin + */ + bool m_originalSkipTaskbar = false; + bool m_skipPager = false; + bool m_skipSwitcher = false; + QIcon m_icon; + bool m_active = false; + bool m_zombie = false; + bool m_keepAbove = false; + bool m_keepBelow = false; + bool m_demandsAttention = false; + bool m_minimized = false; + QTimer *m_autoRaiseTimer = nullptr; + QTimer *m_shadeHoverTimer = nullptr; + ShadeMode m_shadeMode = ShadeNone; + QVector m_desktops; + + QString m_colorScheme; + std::shared_ptr m_palette; + static QHash> s_palettes; + static std::shared_ptr s_defaultPalette; + + KWaylandServer::PlasmaWindowInterface *m_windowManagementInterface = nullptr; + + AbstractClient *m_transientFor = nullptr; + QList m_transients; + bool m_modal = false; + Layer m_layer = UnknownLayer; + + // electric border/quick tiling + QuickTileMode m_electricMode = QuickTileFlag::None; + bool m_electricMaximizing = false; + // The quick tile mode of this window. + int m_quickTileMode = int(QuickTileFlag::None); + QTimer *m_electricMaximizingDelay = nullptr; + + // geometry + int m_blockGeometryUpdates = 0; // > 0 = New geometry is remembered, but not actually set + PendingGeometry_t m_pendingGeometryUpdate = PendingGeometryNone; + friend class GeometryUpdatesBlocker; + QRect m_visibleRectBeforeGeometryUpdate; + QRect m_bufferGeometryBeforeUpdateBlocking; + QRect m_frameGeometryBeforeUpdateBlocking; + QRect m_clientGeometryBeforeUpdateBlocking; + QRect m_keyboardGeometryRestore; + QRect m_maximizeGeometryRestore; + + struct { + bool enabled = false; + bool unrestricted = false; + QPoint offset; + QPoint invertedOffset; + QRect initialGeometry; + QRect geometry; + Position pointer = PositionCenter; + bool buttonDown = false; + CursorShape cursor = Qt::ArrowCursor; + int startScreen = 0; + QTimer *delayedTimer = nullptr; + } m_moveResize; + + struct { + KDecoration2::Decoration *decoration = nullptr; + QPointer client; + QElapsedTimer doubleClickTimer; + } m_decoration; + QByteArray m_desktopFileName; + + bool m_applicationMenuActive = false; + QString m_applicationMenuServiceName; + QString m_applicationMenuObjectPath; + + bool m_unresponsive = false; + + QKeySequence _shortcut; + + WindowRules m_rules; + + static bool s_haveResizeEffect; +}; + +/** + * Helper for AbstractClient::blockGeometryUpdates() being called in pairs (true/false) + */ +class GeometryUpdatesBlocker +{ +public: + explicit GeometryUpdatesBlocker(AbstractClient* c) + : cl(c) { + cl->blockGeometryUpdates(true); + } + ~GeometryUpdatesBlocker() { + cl->blockGeometryUpdates(false); + } + +private: + AbstractClient* cl; +}; + +inline void AbstractClient::move(const QPoint& p, ForceGeometry_t force) +{ + move(p.x(), p.y(), force); +} + +inline const QList& AbstractClient::transients() const +{ + return m_transients; +} + +inline bool AbstractClient::areGeometryUpdatesBlocked() const +{ + return m_blockGeometryUpdates != 0; +} + +inline void AbstractClient::blockGeometryUpdates() +{ + m_blockGeometryUpdates++; +} + +inline void AbstractClient::unblockGeometryUpdates() +{ + m_blockGeometryUpdates--; +} + +inline AbstractClient::PendingGeometry_t AbstractClient::pendingGeometryUpdate() const +{ + return m_pendingGeometryUpdate; +} + +inline void AbstractClient::setPendingGeometryUpdate(PendingGeometry_t update) +{ + m_pendingGeometryUpdate = update; +} + +} + +Q_DECLARE_METATYPE(KWin::AbstractClient*) +Q_DECLARE_METATYPE(QList) +Q_DECLARE_OPERATORS_FOR_FLAGS(KWin::AbstractClient::SameApplicationChecks) + +#endif diff --git a/abstract_opengl_context_attribute_builder.cpp b/abstract_opengl_context_attribute_builder.cpp new file mode 100644 index 0000000..1b24b01 --- /dev/null +++ b/abstract_opengl_context_attribute_builder.cpp @@ -0,0 +1,29 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "abstract_opengl_context_attribute_builder.h" + +namespace KWin +{ + +QDebug AbstractOpenGLContextAttributeBuilder::operator<<(QDebug dbg) const +{ + QDebugStateSaver saver(dbg); + dbg.nospace() << "\nVersion requested:\t" << isVersionRequested() << "\n"; + if (isVersionRequested()) { + dbg.nospace() << "Version:\t" << majorVersion() << "." << minorVersion() << "\n"; + } + dbg.nospace() << "Robust:\t" << isRobust() << "\n"; + dbg.nospace() << "Forward compatible:\t" << isForwardCompatible() << "\n"; + dbg.nospace() << "Core profile:\t" << isCoreProfile() << "\n"; + dbg.nospace() << "Compatibility profile:\t" << isCompatibilityProfile() << "\n"; + dbg.nospace() << "High priority:\t" << isHighPriority(); + return dbg; +} + +} diff --git a/abstract_opengl_context_attribute_builder.h b/abstract_opengl_context_attribute_builder.h new file mode 100644 index 0000000..1d6b6e9 --- /dev/null +++ b/abstract_opengl_context_attribute_builder.h @@ -0,0 +1,115 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include +#include + +namespace KWin +{ + +class KWIN_EXPORT AbstractOpenGLContextAttributeBuilder +{ +public: + virtual ~AbstractOpenGLContextAttributeBuilder() { + } + + void setVersion(int major, int minor = 0) { + m_versionRequested = true; + m_majorVersion = major; + m_minorVersion = minor; + } + + bool isVersionRequested() const { + return m_versionRequested; + } + + int majorVersion() const { + return m_majorVersion; + } + + int minorVersion() const { + return m_minorVersion; + } + + void setRobust(bool robust) { + m_robust = robust; + } + + bool isRobust() const { + return m_robust; + } + + void setForwardCompatible(bool forward) { + m_forwardCompatible = forward; + } + + bool isForwardCompatible() const { + return m_forwardCompatible; + } + + void setCoreProfile(bool core) { + m_coreProfile = core; + if (m_coreProfile) { + setCompatibilityProfile(false); + } + } + + bool isCoreProfile() const { + return m_coreProfile; + } + + void setCompatibilityProfile(bool compatibility) { + m_compatibilityProfile = compatibility; + if (m_compatibilityProfile) { + setCoreProfile(false); + } + } + + bool isCompatibilityProfile() const { + return m_compatibilityProfile; + } + + void setResetOnVideoMemoryPurge(bool reset) { + m_resetOnVideoMemoryPurge = reset; + } + + bool isResetOnVideoMemoryPurge() const { + return m_resetOnVideoMemoryPurge; + } + + void setHighPriority(bool highPriority) { + m_highPriority = highPriority; + } + + bool isHighPriority() const { + return m_highPriority; + } + + virtual std::vector build() const = 0; + + QDebug operator<<(QDebug dbg) const; + +private: + bool m_versionRequested = false; + int m_majorVersion = 0; + int m_minorVersion = 0; + bool m_robust = false; + bool m_forwardCompatible = false; + bool m_coreProfile = false; + bool m_compatibilityProfile = false; + bool m_resetOnVideoMemoryPurge = false; + bool m_highPriority = false; +}; + +inline QDebug operator<<(QDebug dbg, const AbstractOpenGLContextAttributeBuilder *attribs) +{ + return attribs->operator<<(dbg); +} + +} diff --git a/abstract_output.cpp b/abstract_output.cpp new file mode 100644 index 0000000..c72f74e --- /dev/null +++ b/abstract_output.cpp @@ -0,0 +1,106 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "abstract_output.h" + +namespace KWin +{ + +GammaRamp::GammaRamp(uint32_t size) + : m_table(3 * size) + , m_size(size) +{ +} + +uint32_t GammaRamp::size() const +{ + return m_size; +} + +uint16_t *GammaRamp::red() +{ + return m_table.data(); +} + +const uint16_t *GammaRamp::red() const +{ + return m_table.data(); +} + +uint16_t *GammaRamp::green() +{ + return m_table.data() + m_size; +} + +const uint16_t *GammaRamp::green() const +{ + return m_table.data() + m_size; +} + +uint16_t *GammaRamp::blue() +{ + return m_table.data() + 2 * m_size; +} + +const uint16_t *GammaRamp::blue() const +{ + return m_table.data() + 2 * m_size; +} + +AbstractOutput::AbstractOutput(QObject *parent) + : QObject(parent) +{ +} + +AbstractOutput::~AbstractOutput() +{ +} + +QByteArray AbstractOutput::uuid() const +{ + return QByteArray(); +} + +void AbstractOutput::setEnabled(bool enable) +{ + Q_UNUSED(enable) +} + +void AbstractOutput::applyChanges(const KWaylandServer::OutputChangeSet *changeSet) +{ + Q_UNUSED(changeSet) +} + +bool AbstractOutput::isInternal() const +{ + return false; +} + +qreal AbstractOutput::scale() const +{ + return 1; +} + +QSize AbstractOutput::physicalSize() const +{ + return QSize(); +} + +int AbstractOutput::gammaRampSize() const +{ + return 0; +} + +bool AbstractOutput::setGammaRamp(const GammaRamp &gamma) +{ + Q_UNUSED(gamma); + return false; +} + +} // namespace KWin diff --git a/abstract_output.h b/abstract_output.h new file mode 100644 index 0000000..090266d --- /dev/null +++ b/abstract_output.h @@ -0,0 +1,179 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_ABSTRACT_OUTPUT_H +#define KWIN_ABSTRACT_OUTPUT_H + +#include + +#include +#include +#include +#include + +namespace KWaylandServer +{ +class OutputChangeSet; +} + +namespace KWin +{ + +class KWIN_EXPORT GammaRamp +{ +public: + GammaRamp(uint32_t size); + + /** + * Returns the size of the gamma ramp. + */ + uint32_t size() const; + + /** + * Returns pointer to the first red component in the gamma ramp. + * + * The returned pointer can be used for altering the red component + * in the gamma ramp. + */ + uint16_t *red(); + + /** + * Returns pointer to the first red component in the gamma ramp. + */ + const uint16_t *red() const; + + /** + * Returns pointer to the first green component in the gamma ramp. + * + * The returned pointer can be used for altering the green component + * in the gamma ramp. + */ + uint16_t *green(); + + /** + * Returns pointer to the first green component in the gamma ramp. + */ + const uint16_t *green() const; + + /** + * Returns pointer to the first blue component in the gamma ramp. + * + * The returned pointer can be used for altering the blue component + * in the gamma ramp. + */ + uint16_t *blue(); + + /** + * Returns pointer to the first blue component in the gamma ramp. + */ + const uint16_t *blue() const; + +private: + QVector m_table; + uint32_t m_size; +}; + +/** + * Generic output representation. + */ +class KWIN_EXPORT AbstractOutput : public QObject +{ + Q_OBJECT + +public: + explicit AbstractOutput(QObject *parent = nullptr); + ~AbstractOutput() override; + + /** + * Returns a short identifiable name of this output. + */ + virtual QString name() const = 0; + + /** + * Returns the identifying uuid of this output. + * + * Default implementation returns an empty byte array. + */ + virtual QByteArray uuid() const; + + /** + * Enable or disable the output. + * + * Default implementation does nothing + */ + virtual void setEnabled(bool enable); + + /** + * This sets the changes and tests them against the specific output. + * + * Default implementation does nothing + */ + virtual void applyChanges(const KWaylandServer::OutputChangeSet *changeSet); + + /** + * Returns geometry of this output in device independent pixels. + */ + virtual QRect geometry() const = 0; + + /** + * Returns the approximate vertical refresh rate of this output, in mHz. + */ + virtual int refreshRate() const = 0; + + /** + * Returns whether this output is connected through an internal connector, + * e.g. LVDS, or eDP. + * + * Default implementation returns @c false. + */ + virtual bool isInternal() const; + + /** + * Returns the ratio between physical pixels and logical pixels. + * + * Default implementation returns 1. + */ + virtual qreal scale() const; + + /** + * Returns the physical size of this output, in millimeters. + * + * Default implementation returns an invalid QSize. + */ + virtual QSize physicalSize() const; + + /** + * Returns the size of the gamma lookup table. + * + * Default implementation returns 0. + */ + virtual int gammaRampSize() const; + + /** + * Sets the gamma ramp of this output. + * + * Returns @c true if the gamma ramp was successfully set. + */ + virtual bool setGammaRamp(const GammaRamp &gamma); + + /** Returns the resolution of the output. */ + virtual QSize pixelSize() const = 0; + +Q_SIGNALS: + /** + * This signal is emitted when the geometry of this output has changed. + */ + void geometryChanged(); + +private: + Q_DISABLE_COPY(AbstractOutput) +}; + +} // namespace KWin + +#endif diff --git a/abstract_wayland_output.cpp b/abstract_wayland_output.cpp new file mode 100644 index 0000000..9f75df9 --- /dev/null +++ b/abstract_wayland_output.cpp @@ -0,0 +1,357 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Roman Gilg + SPDX-FileCopyrightText: 2020 David Edmundson + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "abstract_wayland_output.h" + +#include "screens.h" +#include "wayland_server.h" + +// KWayland +#include +#include +#include +// KF5 +#include + +#include +#include + +namespace KWin +{ + +AbstractWaylandOutput::AbstractWaylandOutput(QObject *parent) + : AbstractOutput(parent) +{ + m_waylandOutput = waylandServer()->display()->createOutput(this); + m_waylandOutputDevice = waylandServer()->display()->createOutputDevice(this); + m_xdgOutputV1 = waylandServer()->xdgOutputManagerV1()->createXdgOutput(m_waylandOutput, this); + + connect(m_waylandOutput, &KWaylandServer::OutputInterface::dpmsModeRequested, this, + [this] (KWaylandServer::OutputInterface::DpmsMode mode) { + updateDpms(mode); + }); + + connect(m_waylandOutput, &KWaylandServer::OutputInterface::globalPositionChanged, this, &AbstractWaylandOutput::geometryChanged); + connect(m_waylandOutput, &KWaylandServer::OutputInterface::pixelSizeChanged, this, &AbstractWaylandOutput::geometryChanged); + connect(m_waylandOutput, &KWaylandServer::OutputInterface::scaleChanged, this, &AbstractWaylandOutput::geometryChanged); +} + +AbstractWaylandOutput::~AbstractWaylandOutput() +{ +} + +QString AbstractWaylandOutput::name() const +{ + return m_name; +} + +QByteArray AbstractWaylandOutput::uuid() const +{ + return m_waylandOutputDevice->uuid(); +} + +QRect AbstractWaylandOutput::geometry() const +{ + return QRect(globalPos(), pixelSize() / scale()); +} + +QSize AbstractWaylandOutput::physicalSize() const +{ + return orientateSize(m_waylandOutputDevice->physicalSize()); +} + +int AbstractWaylandOutput::refreshRate() const +{ + return m_waylandOutputDevice->refreshRate(); +} + +QPoint AbstractWaylandOutput::globalPos() const +{ + return m_waylandOutputDevice->globalPosition(); +} + +void AbstractWaylandOutput::setGlobalPos(const QPoint &pos) +{ + m_waylandOutputDevice->setGlobalPosition(pos); + + m_waylandOutput->setGlobalPosition(pos); + m_xdgOutputV1->setLogicalPosition(pos); + m_xdgOutputV1->done(); +} + +QSize AbstractWaylandOutput::modeSize() const +{ + return m_waylandOutputDevice->pixelSize(); +} + +QSize AbstractWaylandOutput::pixelSize() const +{ + return orientateSize(m_waylandOutputDevice->pixelSize()); +} + +qreal AbstractWaylandOutput::scale() const +{ + return m_waylandOutputDevice->scaleF(); +} + +void AbstractWaylandOutput::setScale(qreal scale) +{ + m_waylandOutputDevice->setScaleF(scale); + + // this is the scale that clients will ideally use for their buffers + // this has to be an int which is fine + + // I don't know whether we want to round or ceil + // or maybe even set this to 3 when we're scaling to 1.5 + // don't treat this like it's chosen deliberately + m_waylandOutput->setScale(std::ceil(scale)); + m_xdgOutputV1->setLogicalSize(pixelSize() / scale); + m_xdgOutputV1->done(); +} + +using DeviceInterface = KWaylandServer::OutputDeviceInterface; + +KWaylandServer::OutputInterface::Transform toOutputTransform(DeviceInterface::Transform transform) +{ + using Transform = DeviceInterface::Transform; + using OutputTransform = KWaylandServer::OutputInterface::Transform; + + switch (transform) { + case Transform::Rotated90: + return OutputTransform::Rotated90; + case Transform::Rotated180: + return OutputTransform::Rotated180; + case Transform::Rotated270: + return OutputTransform::Rotated270; + case Transform::Flipped: + return OutputTransform::Flipped; + case Transform::Flipped90: + return OutputTransform::Flipped90; + case Transform::Flipped180: + return OutputTransform::Flipped180; + case Transform::Flipped270: + return OutputTransform::Flipped270; + default: + return OutputTransform::Normal; + } +} + +void AbstractWaylandOutput::setTransform(DeviceInterface::Transform transform) +{ + m_waylandOutputDevice->setTransform(transform); + + m_waylandOutput->setTransform(toOutputTransform(transform)); + m_xdgOutputV1->setLogicalSize(pixelSize() / scale()); + m_xdgOutputV1->done(); +} + +inline +AbstractWaylandOutput::Transform toTransform(DeviceInterface::Transform deviceTransform) +{ + return static_cast(deviceTransform); +} + +inline +DeviceInterface::Transform toDeviceTransform(AbstractWaylandOutput::Transform transform) +{ + return static_cast(transform); +} + +void AbstractWaylandOutput::applyChanges(const KWaylandServer::OutputChangeSet *changeSet) +{ + qCDebug(KWIN_CORE) << "Apply changes to the Wayland output."; + bool emitModeChanged = false; + bool overallSizeCheckNeeded = false; + + // Enablement changes are handled by platform. + if (changeSet->modeChanged()) { + qCDebug(KWIN_CORE) << "Setting new mode:" << changeSet->mode(); + m_waylandOutputDevice->setCurrentMode(changeSet->mode()); + updateMode(changeSet->mode()); + emitModeChanged = true; + } + if (changeSet->transformChanged()) { + qCDebug(KWIN_CORE) << "Server setting transform: " << (int)(changeSet->transform()); + setTransform(changeSet->transform()); + updateTransform(toTransform(changeSet->transform())); + emitModeChanged = true; + } + if (changeSet->positionChanged()) { + qCDebug(KWIN_CORE) << "Server setting position: " << changeSet->position(); + setGlobalPos(changeSet->position()); + // may just work already! + overallSizeCheckNeeded = true; + } + if (changeSet->scaleChanged()) { + qCDebug(KWIN_CORE) << "Setting scale:" << changeSet->scaleF(); + setScale(changeSet->scaleF()); + emitModeChanged = true; + } + + overallSizeCheckNeeded |= emitModeChanged; + if (overallSizeCheckNeeded) { + emit screens()->changed(); + } + + if (emitModeChanged) { + emit modeChanged(); + } +} + +bool AbstractWaylandOutput::isEnabled() const +{ + return m_waylandOutputDevice->enabled() == DeviceInterface::Enablement::Enabled; +} + +void AbstractWaylandOutput::setEnabled(bool enable) +{ + if (enable == isEnabled()) { + return; + } + + if (enable) { + m_waylandOutputDevice->setEnabled(DeviceInterface::Enablement::Enabled); + m_waylandOutput->create(); + updateEnablement(true); + } else { + m_waylandOutputDevice->setEnabled(DeviceInterface::Enablement::Disabled); + m_waylandOutput->destroy(); + // xdg-output is destroyed in KWayland on wl_output going away. + updateEnablement(false); + } +} + +QString AbstractWaylandOutput::description() const +{ + return QStringLiteral("%1 %2").arg(m_waylandOutputDevice->manufacturer()).arg( + m_waylandOutputDevice->model()); +} + +void AbstractWaylandOutput::setWaylandMode(const QSize &size, int refreshRate) +{ + m_waylandOutput->setCurrentMode(size, refreshRate); + m_xdgOutputV1->setLogicalSize(pixelSize() / scale()); + m_xdgOutputV1->done(); +} + +void AbstractWaylandOutput::initInterfaces(const QString &model, const QString &manufacturer, + const QByteArray &uuid, const QSize &physicalSize, + const QVector &modes) +{ + m_waylandOutputDevice->setUuid(uuid); + + if (!manufacturer.isEmpty()) { + m_waylandOutputDevice->setManufacturer(manufacturer); + } else { + m_waylandOutputDevice->setManufacturer(i18n("unknown")); + } + + m_waylandOutputDevice->setModel(model); + m_waylandOutputDevice->setPhysicalSize(physicalSize); + + m_waylandOutput->setManufacturer(m_waylandOutputDevice->manufacturer()); + m_waylandOutput->setModel(m_waylandOutputDevice->model()); + m_waylandOutput->setPhysicalSize(m_waylandOutputDevice->physicalSize()); + + int i = 0; + for (auto mode : modes) { + qCDebug(KWIN_CORE).nospace() << "Adding mode " << ++i << ": " << mode.size << " [" << mode.refreshRate << "]"; + m_waylandOutputDevice->addMode(mode); + + KWaylandServer::OutputInterface::ModeFlags flags; + if (mode.flags & DeviceInterface::ModeFlag::Current) { + flags |= KWaylandServer::OutputInterface::ModeFlag::Current; + } + if (mode.flags & DeviceInterface::ModeFlag::Preferred) { + flags |= KWaylandServer::OutputInterface::ModeFlag::Preferred; + } + m_waylandOutput->addMode(mode.size, flags, mode.refreshRate); + } + + m_waylandOutputDevice->create(); + + // start off enabled + + m_waylandOutput->create(); + m_xdgOutputV1->setName(name()); + m_xdgOutputV1->setDescription(description()); + m_xdgOutputV1->setLogicalSize(pixelSize() / scale()); + m_xdgOutputV1->done(); +} + +QSize AbstractWaylandOutput::orientateSize(const QSize &size) const +{ + using Transform = DeviceInterface::Transform; + const Transform transform = m_waylandOutputDevice->transform(); + if (transform == Transform::Rotated90 || transform == Transform::Rotated270 || + transform == Transform::Flipped90 || transform == Transform::Flipped270) { + return size.transposed(); + } + return size; +} + +void AbstractWaylandOutput::setTransform(Transform transform) +{ + const auto deviceTransform = toDeviceTransform(transform); + if (deviceTransform == m_waylandOutputDevice->transform()) { + return; + } + setTransform(deviceTransform); + emit modeChanged(); +} + +AbstractWaylandOutput::Transform AbstractWaylandOutput::transform() const +{ + return static_cast(m_waylandOutputDevice->transform()); +} + +QMatrix4x4 AbstractWaylandOutput::logicalToNativeMatrix(const QRect &rect, qreal scale, Transform transform) +{ + QMatrix4x4 matrix; + matrix.scale(scale); + + switch (transform) { + case Transform::Normal: + case Transform::Flipped: + break; + case Transform::Rotated90: + case Transform::Flipped90: + matrix.translate(0, rect.width()); + matrix.rotate(-90, 0, 0, 1); + break; + case Transform::Rotated180: + case Transform::Flipped180: + matrix.translate(rect.width(), rect.height()); + matrix.rotate(-180, 0, 0, 1); + break; + case Transform::Rotated270: + case Transform::Flipped270: + matrix.translate(rect.height(), 0); + matrix.rotate(-270, 0, 0, 1); + break; + } + + switch (transform) { + case Transform::Flipped: + case Transform::Flipped90: + case Transform::Flipped180: + case Transform::Flipped270: + matrix.translate(rect.width(), 0); + matrix.scale(-1, 1); + break; + default: + break; + } + + matrix.translate(-rect.x(), -rect.y()); + + return matrix; +} + +} diff --git a/abstract_wayland_output.h b/abstract_wayland_output.h new file mode 100644 index 0000000..f889eb3 --- /dev/null +++ b/abstract_wayland_output.h @@ -0,0 +1,176 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_ABSTRACT_WAYLAND_OUTPUT_H +#define KWIN_ABSTRACT_WAYLAND_OUTPUT_H + +#include "abstract_output.h" +#include "utils.h" +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace KWaylandServer +{ +class OutputInterface; +class OutputDeviceInterface; +class OutputChangeSet; +class OutputManagementInterface; +class XdgOutputV1Interface; +} + +namespace KWin +{ + +/** + * Generic output representation in a Wayland session + */ +class KWIN_EXPORT AbstractWaylandOutput : public AbstractOutput +{ + Q_OBJECT +public: + enum class Transform { + Normal, + Rotated90, + Rotated180, + Rotated270, + Flipped, + Flipped90, + Flipped180, + Flipped270 + }; + + explicit AbstractWaylandOutput(QObject *parent = nullptr); + ~AbstractWaylandOutput() override; + + QString name() const override; + QByteArray uuid() const override; + + QSize modeSize() const; + + // TODO: The name is ambiguous. Rename this function. + QSize pixelSize() const override; + + qreal scale() const override; + + /** + * The geometry of this output in global compositor co-ordinates (i.e scaled) + */ + QRect geometry() const override; + QSize physicalSize() const override; + + /** + * Returns the orientation of this output. + * + * - Flipped along the vertical axis is landscape + inv. portrait. + * - Rotated 90° and flipped along the horizontal axis is portrait + inv. landscape + * - Rotated 180° and flipped along the vertical axis is inv. landscape + inv. portrait + * - Rotated 270° and flipped along the horizontal axis is inv. portrait + inv. landscape + + * portrait + */ + Transform transform() const; + + /** + * Current refresh rate in 1/ms. + */ + int refreshRate() const override; + + bool isInternal() const override { + return m_internal; + } + + void setGlobalPos(const QPoint &pos); + void setScale(qreal scale); + + void applyChanges(const KWaylandServer::OutputChangeSet *changeSet) override; + + QPointer waylandOutput() const { + return m_waylandOutput; + } + + bool isEnabled() const; + /** + * Enable or disable the output. + * + * This differs from updateDpms as it also removes the wl_output. + * The default is on. + */ + void setEnabled(bool enable) override; + + QString description() const; + + /** + * Returns a matrix that can translate into the display's coordinates system + */ + static QMatrix4x4 logicalToNativeMatrix(const QRect &rect, qreal scale, Transform transform); + +Q_SIGNALS: + void modeChanged(); + void outputChange(const QRegion &damagedRegion); + +protected: + void initInterfaces(const QString &model, const QString &manufacturer, + const QByteArray &uuid, const QSize &physicalSize, + const QVector &modes); + + QPoint globalPos() const; + + bool internal() const { + return m_internal; + } + void setName(const QString &name) { + m_name = name; + } + void setInternal(bool set) { + m_internal = set; + } + void setDpmsSupported(bool set) { + m_waylandOutput->setDpmsSupported(set); + } + + virtual void updateEnablement(bool enable) { + Q_UNUSED(enable); + } + virtual void updateDpms(KWaylandServer::OutputInterface::DpmsMode mode) { + Q_UNUSED(mode); + } + virtual void updateMode(int modeIndex) { + Q_UNUSED(modeIndex); + } + virtual void updateTransform(Transform transform) { + Q_UNUSED(transform); + } + + void setWaylandMode(const QSize &size, int refreshRate); + void setTransform(Transform transform); + + QSize orientateSize(const QSize &size) const; + +private: + void setTransform(KWaylandServer::OutputDeviceInterface::Transform transform); + + KWaylandServer::OutputInterface *m_waylandOutput; + KWaylandServer::XdgOutputV1Interface *m_xdgOutputV1; + KWaylandServer::OutputDeviceInterface *m_waylandOutputDevice; + KWaylandServer::OutputInterface::DpmsMode m_dpms = KWaylandServer::OutputInterface::DpmsMode::On; + + QString m_name; + bool m_internal = false; +}; + +} + +#endif // KWIN_OUTPUT_H diff --git a/activation.cpp b/activation.cpp new file mode 100644 index 0000000..8335a52 --- /dev/null +++ b/activation.cpp @@ -0,0 +1,881 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +/* + + This file contains things relevant to window activation and focus + stealing prevention. + +*/ + +#include "x11client.h" +#include "cursor.h" +#include "focuschain.h" +#include "netinfo.h" +#include "workspace.h" +#ifdef KWIN_BUILD_ACTIVITIES +#include "activities.h" +#endif + +#include +#include +#include + +#include "atoms.h" +#include "group.h" +#include "rules.h" +#include "screens.h" +#include "useractions.h" +#include + +namespace KWin +{ + +/* + Prevention of focus stealing: + + KWin tries to prevent unwanted changes of focus, that would result + from mapping a new window. Also, some nasty applications may try + to force focus change even in cases when ICCCM 4.2.7 doesn't allow it + (e.g. they may try to activate their main window because the user + definitely "needs" to see something happened - misusing + of QWidget::setActiveWindow() may be such case). + + There are 4 ways how a window may become active: + - the user changes the active window (e.g. focus follows mouse, clicking + on some window's titlebar) - the change of focus will + be done by KWin, so there's nothing to solve in this case + - the change of active window will be requested using the _NET_ACTIVE_WINDOW + message (handled in RootInfo::changeActiveWindow()) - such requests + will be obeyed, because this request is meant mainly for e.g. taskbar + asking the WM to change the active window as a result of some user action. + Normal applications should use this request only rarely in special cases. + See also below the discussion of _NET_ACTIVE_WINDOW_TRANSFER. + - the change of active window will be done by performing XSetInputFocus() + on a window that's not currently active. ICCCM 4.2.7 describes when + the application may perform change of input focus. In order to handle + misbehaving applications, KWin will try to detect focus changes to + windows that don't belong to currently active application, and restore + focus back to the currently active window, instead of activating the window + that got focus (unfortunately there's no way to FocusChangeRedirect similar + to e.g. SubstructureRedirect, so there will be short time when the focus + will be changed). The check itself that's done is + Workspace::allowClientActivation() (see below). + - a new window will be mapped - this is the most complicated case. If + the new window belongs to the currently active application, it may be safely + mapped on top and activated. The same if there's no active window, + or the active window is the desktop. These checks are done by + Workspace::allowClientActivation(). + Following checks need to compare times. One time is the timestamp + of last user action in the currently active window, the other time is + the timestamp of the action that originally caused mapping of the new window + (e.g. when the application was started). If the first time is newer than + the second one, the window will not be activated, as that indicates + futher user actions took place after the action leading to this new + mapped window. This check is done by Workspace::allowClientActivation(). + There are several ways how to get the timestamp of action that caused + the new mapped window (done in X11Client::readUserTimeMapTimestamp()) : + - the window may have the _NET_WM_USER_TIME property. This way + the application may either explicitly request that the window is not + activated (by using 0 timestamp), or the property contains the time + of last user action in the application. + - KWin itself tries to detect time of last user action in every window, + by watching KeyPress and ButtonPress events on windows. This way some + events may be missed (if they don't propagate to the toplevel window), + but it's good as a fallback for applications that don't provide + _NET_WM_USER_TIME, and missing some events may at most lead + to unwanted focus stealing. + - the timestamp may come from application startup notification. + Application startup notification, if it exists for the new mapped window, + should include time of the user action that caused it. + - if there's no timestamp available, it's checked whether the new window + belongs to some already running application - if yes, the timestamp + will be 0 (i.e. refuse activation) + - if the window is from session restored window, the timestamp will + be 0 too, unless this application was the active one at the time + when the session was saved, in which case the window will be + activated if there wasn't any user interaction since the time + KWin was started. + - as the last resort, the _KDE_NET_USER_CREATION_TIME timestamp + is used. For every toplevel window that is created (see CreateNotify + handling), this property is set to the at that time current time. + Since at this time it's known that the new window doesn't belong + to any existing application (better said, the application doesn't + have any other window mapped), it is either the very first window + of the application, or it is the only window of the application + that was hidden before. The latter case is handled by removing + the property from windows before withdrawing them, making + the timestamp empty for next mapping of the window. In the sooner + case, the timestamp will be used. This helps in case when + an application is launched without application startup notification, + it creates its mainwindow, and starts its initialization (that + may possibly take long time). The timestamp used will be older + than any user action done after launching this application. + - if no timestamp is found at all, the window is activated. + The check whether two windows belong to the same application (same + process) is done in X11Client::belongToSameApplication(). Not 100% reliable, + but hopefully 99,99% reliable. + + As a somewhat special case, window activation is always enabled when + session saving is in progress. When session saving, the session + manager allows only one application to interact with the user. + Not allowing window activation in such case would result in e.g. dialogs + not becoming active, so focus stealing prevention would cause here + more harm than good. + + Windows that attempted to become active but KWin prevented this will + be marked as demanding user attention. They'll get + the _NET_WM_STATE_DEMANDS_ATTENTION state, and the taskbar should mark + them specially (blink, etc.). The state will be reset when the window + eventually really becomes active. + + There are two more ways how a window can become obtrusive, window stealing + focus: By showing above the active window, by either raising itself, + or by moving itself on the active desktop. + - KWin will refuse raising non-active window above the active one, + unless they belong to the same application. Applications shouldn't + raise their windows anyway (unless the app wants to raise one + of its windows above another of its windows). + - KWin activates windows moved to the current desktop (as that seems + logical from the user's point of view, after sending the window + there directly from KWin, or e.g. using pager). This means + applications shouldn't send their windows to another desktop + (SELI TODO - but what if they do?) + + Special cases I can think of: + - konqueror reusing, i.e. kfmclient tells running Konqueror instance + to open new window + - without focus stealing prevention - no problem + - with ASN (application startup notification) - ASN is forwarded, + and because it's newer than the instance's user timestamp, + it takes precedence + - without ASN - user timestamp needs to be reset, otherwise it would + be used, and it's old; moreover this new window mustn't be detected + as window belonging to already running application, or it wouldn't + be activated - see X11Client::sameAppWindowRoleMatch() for the (rather ugly) + hack + - konqueror preloading, i.e. window is created in advance, and kfmclient + tells this Konqueror instance to show it later + - without focus stealing prevention - no problem + - with ASN - ASN is forwarded, and because it's newer than the instance's + user timestamp, it takes precedence + - without ASN - user timestamp needs to be reset, otherwise it would + be used, and it's old; also, creation timestamp is changed to + the time the instance starts (re-)initializing the window, + this ensures creation timestamp will still work somewhat even in this case + - KUniqueApplication - when the window is already visible, and the new instance + wants it to activate + - without focus stealing prevention - _NET_ACTIVE_WINDOW - no problem + - with ASN - ASN is forwarded, and set on the already visible window, KWin + treats the window as new with that ASN + - without ASN - _NET_ACTIVE_WINDOW as application request is used, + and there's no really usable timestamp, only timestamp + from the time the (new) application instance was started, + so KWin will activate the window *sigh* + - the bad thing here is that there's absolutely no chance to recognize + the case of starting this KUniqueApp from Konsole (and thus wanting + the already visible window to become active) from the case + when something started this KUniqueApp without ASN (in which case + the already visible window shouldn't become active) + - the only solution is using ASN for starting applications, at least silent + (i.e. without feedback) + - when one application wants to activate another application's window (e.g. KMail + activating already running KAddressBook window ?) + - without focus stealing prevention - _NET_ACTIVE_WINDOW - no problem + - with ASN - can't be here, it's the KUniqueApp case then + - without ASN - _NET_ACTIVE_WINDOW as application request should be used, + KWin will activate the new window depending on the timestamp and + whether it belongs to the currently active application + + _NET_ACTIVE_WINDOW usage: + data.l[0]= 1 ->app request + = 2 ->pager request + = 0 - backwards compatibility + data.l[1]= timestamp +*/ + + +//**************************************** +// Workspace +//**************************************** + + +/** + * Informs the workspace about the active client, i.e. the client that + * has the focus (or None if no client has the focus). This functions + * is called by the client itself that gets focus. It has no other + * effect than fixing the focus chain and the return value of + * activeClient(). And of course, to propagate the active client to the + * world. + */ +void Workspace::setActiveClient(AbstractClient* c) +{ + if (active_client == c) + return; + + if (active_popup && active_popup_client != c && set_active_client_recursion == 0) + closeActivePopup(); + if (m_userActionsMenu->hasClient() && !m_userActionsMenu->isMenuClient(c) && set_active_client_recursion == 0) { + m_userActionsMenu->close(); + } + StackingUpdatesBlocker blocker(this); + ++set_active_client_recursion; + updateFocusMousePosition(Cursors::self()->mouse()->pos()); + if (active_client != nullptr) { + // note that this may call setActiveClient( NULL ), therefore the recursion counter + active_client->setActive(false); + } + active_client = c; + Q_ASSERT(c == nullptr || c->isActive()); + + if (active_client) { + last_active_client = active_client; + FocusChain::self()->update(active_client, FocusChain::MakeFirst); + active_client->demandAttention(false); + + // activating a client can cause a non active fullscreen window to loose the ActiveLayer status on > 1 screens + if (screens()->count() > 1) { + for (auto it = m_allClients.begin(); it != m_allClients.end(); ++it) { + if (*it != active_client && (*it)->layer() == ActiveLayer && (*it)->screen() == active_client->screen()) { + updateClientLayer(*it); + } + } + } + } + + updateToolWindows(false); + if (c) + disableGlobalShortcutsForClient(c->rules()->checkDisableGlobalShortcuts(false)); + else + disableGlobalShortcutsForClient(false); + + updateStackingOrder(); // e.g. fullscreens have different layer when active/not-active + + if (rootInfo()) { + rootInfo()->setActiveClient(active_client); + } + + emit clientActivated(active_client); + --set_active_client_recursion; +} + +/** + * Tries to activate the client \a c. This function performs what you + * expect when clicking the respective entry in a taskbar: showing and + * raising the client (this may imply switching to the another virtual + * desktop) and putting the focus onto it. Once X really gave focus to + * the client window as requested, the client itself will call + * setActiveClient() and the operation is complete. This may not happen + * with certain focus policies, though. + * + * @see setActiveClient + * @see requestFocus + */ +void Workspace::activateClient(AbstractClient* c, bool force) +{ + if (c == nullptr) { + focusToNull(); + setActiveClient(nullptr); + return; + } + raiseClient(c); + if (!c->isOnCurrentDesktop()) { + ++block_focus; + VirtualDesktopManager::self()->setCurrent(c->desktop()); + --block_focus; + } +#ifdef KWIN_BUILD_ACTIVITIES + if (!c->isOnCurrentActivity()) { + ++block_focus; + //DBUS! + Activities::self()->setCurrent(c->activities().first()); //first isn't necessarily best, but it's easiest + --block_focus; + } +#endif + if (c->isMinimized()) + c->unminimize(); + + // ensure the window is really visible - could eg. be a hidden utility window, see bug #348083 + c->hideClient(false); + +// TODO force should perhaps allow this only if the window already contains the mouse + if (options->focusPolicyIsReasonable() || force) + requestFocus(c, force); + + // Don't update user time for clients that have focus stealing workaround. + // As they usually belong to the current active window but fail to provide + // this information, updating their user time would make the user time + // of the currently active window old, and reject further activation for it. + // E.g. typing URL in minicli which will show kio_uiserver dialog (with workaround), + // and then kdesktop shows dialog about SSL certificate. + // This needs also avoiding user creation time in X11Client::readUserTimeMapTimestamp(). + if (X11Client *client = dynamic_cast(c)) { + // updateUserTime is X11 specific + client->updateUserTime(); + } +} + +/** + * Tries to activate the client by asking X for the input focus. This + * function does not perform any show, raise or desktop switching. See + * Workspace::activateClient() instead. + * + * @see activateClient + */ +bool Workspace::requestFocus(AbstractClient* c, bool force) +{ + return takeActivity(c, force ? ActivityFocusForce : ActivityFocus); +} + +bool Workspace::takeActivity(AbstractClient* c, ActivityFlags flags) +{ + // the 'if ( c == active_client ) return;' optimization mustn't be done here + if (!focusChangeEnabled() && (c != active_client)) + flags &= ~ActivityFocus; + + if (!c) { + focusToNull(); + return true; + } + + if (flags & ActivityFocus) { + AbstractClient* modal = c->findModal(); + if (modal != nullptr && modal != c) { + if (!modal->isOnDesktop(c->desktop())) + modal->setDesktop(c->desktop()); + if (!modal->isShown(true) && !modal->isMinimized()) // forced desktop or utility window + activateClient(modal); // activating a minimized blocked window will unminimize its modal implicitly + // if the click was inside the window (i.e. handled is set), + // but it has a modal, there's no need to use handled mode, because + // the modal doesn't get the click anyway + // raising of the original window needs to be still done + if (flags & ActivityRaise) + raiseClient(c); + c = modal; + } + cancelDelayFocus(); + } + if (!flags.testFlag(ActivityFocusForce) && (c->isDock() || c->isSplash())) { + // toplevel menus and dock windows don't take focus if not forced + // and don't have a flag that they take focus + if (!c->dockWantsInput()) { + flags &= ~ActivityFocus; + } + } + if (c->isShade()) { + if (c->wantsInput() && (flags & ActivityFocus)) { + // client cannot accept focus, but at least the window should be active (window menu, et. al. ) + c->setActive(true); + focusToNull(); + } + flags &= ~ActivityFocus; + } + if (!c->isShown(true)) { // shouldn't happen, call activateClient() if needed + qCWarning(KWIN_CORE) << "takeActivity: not shown" ; + return false; + } + + bool ret = true; + + if (flags & ActivityFocus) + ret &= c->takeFocus(); + if (flags & ActivityRaise) + workspace()->raiseClient(c); + + if (!c->isOnActiveScreen()) + screens()->setCurrent(c->screen()); + + return ret; +} + +/** + * Informs the workspace that the client \a c has been hidden. If it + * was the active client (or to-become the active client), + * the workspace activates another one. + * + * @note @p c may already be destroyed. + */ +void Workspace::clientHidden(AbstractClient* c) +{ + Q_ASSERT(!c->isShown(true) || !c->isOnCurrentDesktop() || !c->isOnCurrentActivity()); + activateNextClient(c); +} + +AbstractClient *Workspace::clientUnderMouse(int screen) const +{ + auto it = stackingOrder().constEnd(); + while (it != stackingOrder().constBegin()) { + AbstractClient *client = qobject_cast(*(--it)); + if (!client) { + continue; + } + + // rule out clients which are not really visible. + // the screen test is rather superfluous for xrandr & twinview since the geometry would differ -> TODO: might be dropped + if (!(client->isShown(false) && client->isOnCurrentDesktop() && + client->isOnCurrentActivity() && client->isOnScreen(screen))) + continue; + + if (client->frameGeometry().contains(Cursors::self()->mouse()->pos())) { + return client; + } + } + return nullptr; +} + +// deactivates 'c' and activates next client +bool Workspace::activateNextClient(AbstractClient* c) +{ + // if 'c' is not the active or the to-become active one, do nothing + if (!(c == active_client || (should_get_focus.count() > 0 && c == should_get_focus.last()))) + return false; + + closeActivePopup(); + + if (c != nullptr) { + if (c == active_client) + setActiveClient(nullptr); + should_get_focus.removeAll(c); + } + + // if blocking focus, move focus to the desktop later if needed + // in order to avoid flickering + if (!focusChangeEnabled()) { + focusToNull(); + return true; + } + + if (!options->focusPolicyIsReasonable()) + return false; + + AbstractClient* get_focus = nullptr; + + const int desktop = VirtualDesktopManager::self()->current(); + + if (!get_focus && showingDesktop()) + get_focus = findDesktop(true, desktop); // to not break the state + + if (!get_focus && options->isNextFocusPrefersMouse()) { + get_focus = clientUnderMouse(c ? c->screen() : screens()->current()); + if (get_focus && (get_focus == c || get_focus->isDesktop())) { + // should rather not happen, but it cannot get the focus. rest of usability is tested above + get_focus = nullptr; + } + } + + if (!get_focus) { // no suitable window under the mouse -> find sth. else + // first try to pass the focus to the (former) active clients leader + if (c && c->isTransient()) { + auto leaders = c->mainClients(); + if (leaders.count() == 1 && FocusChain::self()->isUsableFocusCandidate(leaders.at(0), c)) { + get_focus = leaders.at(0); + raiseClient(get_focus); // also raise - we don't know where it came from + } + } + if (!get_focus) { + // nope, ask the focus chain for the next candidate + get_focus = FocusChain::self()->nextForDesktop(c, desktop); + } + } + + if (get_focus == nullptr) // last chance: focus the desktop + get_focus = findDesktop(true, desktop); + + if (get_focus != nullptr) + requestFocus(get_focus); + else + focusToNull(); + + return true; + +} + +void Workspace::setCurrentScreen(int new_screen) +{ + if (new_screen < 0 || new_screen >= screens()->count()) + return; + if (!options->focusPolicyIsReasonable()) + return; + closeActivePopup(); + const int desktop = VirtualDesktopManager::self()->current(); + AbstractClient *get_focus = FocusChain::self()->getForActivation(desktop, new_screen); + if (get_focus == nullptr) + get_focus = findDesktop(true, desktop); + if (get_focus != nullptr && get_focus != mostRecentlyActivatedClient()) + requestFocus(get_focus); + screens()->setCurrent(new_screen); +} + +void Workspace::gotFocusIn(const AbstractClient* c) +{ + if (should_get_focus.contains(const_cast< AbstractClient* >(c))) { + // remove also all sooner elements that should have got FocusIn, + // but didn't for some reason (and also won't anymore, because they were sooner) + while (should_get_focus.first() != c) + should_get_focus.pop_front(); + should_get_focus.pop_front(); // remove 'c' + } +} + +void Workspace::setShouldGetFocus(AbstractClient* c) +{ + should_get_focus.append(c); + updateStackingOrder(); // e.g. fullscreens have different layer when active/not-active +} + + +namespace FSP { + enum Level { None = 0, Low, Medium, High, Extreme }; +} + +// focus_in -> the window got FocusIn event +// ignore_desktop - call comes from _NET_ACTIVE_WINDOW message, don't refuse just because of window +// is on a different desktop +bool Workspace::allowClientActivation(const KWin::AbstractClient *c, xcb_timestamp_t time, bool focus_in, bool ignore_desktop) +{ + // options->focusStealingPreventionLevel : + // 0 - none - old KWin behaviour, new windows always get focus + // 1 - low - focus stealing prevention is applied normally, when unsure, activation is allowed + // 2 - normal - focus stealing prevention is applied normally, when unsure, activation is not allowed, + // this is the default + // 3 - high - new window gets focus only if it belongs to the active application, + // or when no window is currently active + // 4 - extreme - no window gets focus without user intervention + if (time == -1U) + time = c->userTime(); + int level = c->rules()->checkFSP(options->focusStealingPreventionLevel()); + if (sessionManager()->state() == SessionState::Saving && level <= FSP::Medium) { // <= normal + return true; + } + AbstractClient* ac = mostRecentlyActivatedClient(); + if (focus_in) { + if (should_get_focus.contains(const_cast< AbstractClient* >(c))) + return true; // FocusIn was result of KWin's action + // Before getting FocusIn, the active Client already + // got FocusOut, and therefore got deactivated. + ac = last_active_client; + } + if (time == 0) { // explicitly asked not to get focus + if (!c->rules()->checkAcceptFocus(false)) + return false; + } + const int protection = ac ? ac->rules()->checkFPP(2) : 0; + + // stealing is unconditionally allowed (NETWM behavior) + if (level == FSP::None || protection == FSP::None) + return true; + + // The active client "grabs" the focus or stealing is generally forbidden + if (level == FSP::Extreme || protection == FSP::Extreme) + return false; + + // Desktop switching is only allowed in the "no protection" case + if (!ignore_desktop && !c->isOnCurrentDesktop()) + return false; // allow only with level == 0 + + // No active client, it's ok to pass focus + // NOTICE that extreme protection needs to be handled before to allow protection on unmanged windows + if (ac == nullptr || ac->isDesktop()) { + qCDebug(KWIN_CORE) << "Activation: No client active, allowing"; + return true; // no active client -> always allow + } + + // TODO window urgency -> return true? + + // Unconditionally allow intra-client passing around for lower stealing protections + // unless the active client has High interest + if (AbstractClient::belongToSameApplication(c, ac, AbstractClient::SameApplicationCheck::RelaxedForActive) && protection < FSP::High) { + qCDebug(KWIN_CORE) << "Activation: Belongs to active application"; + return true; + } + + if (!c->isOnCurrentDesktop()) // we allowed explicit self-activation across virtual desktops + return false; // inside a client or if no client was active, but not otherwise + + // High FPS, not intr-client change. Only allow if the active client has only minor interest + if (level > FSP::Medium && protection > FSP::Low) + return false; + + if (time == -1U) { // no time known + qCDebug(KWIN_CORE) << "Activation: No timestamp at all"; + // Only allow for Low protection unless active client has High interest in focus + if (level < FSP::Medium && protection < FSP::High) + return true; + // no timestamp at all, don't activate - because there's also creation timestamp + // done on CreateNotify, this case should happen only in case application + // maps again already used window, i.e. this won't happen after app startup + return false; + } + + // Low or medium FSP, usertime comparism is possible + const xcb_timestamp_t user_time = ac->userTime(); + qCDebug(KWIN_CORE) << "Activation, compared:" << c << ":" << time << ":" << user_time + << ":" << (NET::timestampCompare(time, user_time) >= 0); + return NET::timestampCompare(time, user_time) >= 0; // time >= user_time +} + +// basically the same like allowClientActivation(), this time allowing +// a window to be fully raised upon its own request (XRaiseWindow), +// if refused, it will be raised only on top of windows belonging +// to the same application +bool Workspace::allowFullClientRaising(const KWin::AbstractClient *c, xcb_timestamp_t time) +{ + int level = c->rules()->checkFSP(options->focusStealingPreventionLevel()); + if (sessionManager()->state() == SessionState::Saving && level <= 2) { // <= normal + return true; + } + AbstractClient* ac = mostRecentlyActivatedClient(); + if (level == 0) // none + return true; + if (level == 4) // extreme + return false; + if (ac == nullptr || ac->isDesktop()) { + qCDebug(KWIN_CORE) << "Raising: No client active, allowing"; + return true; // no active client -> always allow + } + // TODO window urgency -> return true? + if (AbstractClient::belongToSameApplication(c, ac, AbstractClient::SameApplicationCheck::RelaxedForActive)) { + qCDebug(KWIN_CORE) << "Raising: Belongs to active application"; + return true; + } + if (level == 3) // high + return false; + xcb_timestamp_t user_time = ac->userTime(); + qCDebug(KWIN_CORE) << "Raising, compared:" << time << ":" << user_time + << ":" << (NET::timestampCompare(time, user_time) >= 0); + return NET::timestampCompare(time, user_time) >= 0; // time >= user_time +} + +/** + * Called from X11Client after FocusIn that wasn't initiated by KWin and the client wasn't + * allowed to activate. + * + * Returns @c true if the focus has been restored successfully; otherwise returns @c false. + */ +bool Workspace::restoreFocus() +{ + // this updateXTime() is necessary - as FocusIn events don't have + // a timestamp *sigh*, kwin's timestamp would be older than the timestamp + // that was used by whoever caused the focus change, and therefore + // the attempt to restore the focus would fail due to old timestamp + updateXTime(); + if (should_get_focus.count() > 0) + return requestFocus(should_get_focus.last()); + else if (last_active_client) + return requestFocus(last_active_client); + return true; +} + +void Workspace::clientAttentionChanged(AbstractClient* c, bool set) +{ + if (set) { + attention_chain.removeAll(c); + attention_chain.prepend(c); + } else + attention_chain.removeAll(c); + emit clientDemandsAttentionChanged(c, set); +} + +//******************************************** +// Client +//******************************************** + +/** + * Updates the user time (time of last action in the active window). + * This is called inside kwin for every action with the window + * that qualifies for user interaction (clicking on it, activate it + * externally, etc.). + */ +void X11Client::updateUserTime(xcb_timestamp_t time) +{ + // copied in Group::updateUserTime + if (time == XCB_TIME_CURRENT_TIME) { + updateXTime(); + time = xTime(); + } + if (time != -1U + && (m_userTime == XCB_TIME_CURRENT_TIME + || NET::timestampCompare(time, m_userTime) > 0)) { // time > user_time + m_userTime = time; + shade_below = nullptr; // do not hover re-shade a window after it got interaction + } + group()->updateUserTime(m_userTime); +} + +xcb_timestamp_t X11Client::readUserCreationTime() const +{ + Xcb::Property prop(false, window(), atoms->kde_net_wm_user_creation_time, XCB_ATOM_CARDINAL, 0, 1); + return prop.value(-1); +} + +xcb_timestamp_t X11Client::readUserTimeMapTimestamp(const KStartupInfoId *asn_id, const KStartupInfoData *asn_data, + bool session) const +{ + xcb_timestamp_t time = info->userTime(); + //qDebug() << "User timestamp, initial:" << time; + //^^ this deadlocks kwin --replace sometimes. + + // newer ASN timestamp always replaces user timestamp, unless user timestamp is 0 + // helps e.g. with konqy reusing + if (asn_data != nullptr && time != 0) { + if (asn_id->timestamp() != 0 + && (time == -1U || NET::timestampCompare(asn_id->timestamp(), time) > 0)) { + time = asn_id->timestamp(); + } + } + qCDebug(KWIN_CORE) << "User timestamp, ASN:" << time; + if (time == -1U) { + // The window doesn't have any timestamp. + // If it's the first window for its application + // (i.e. there's no other window from the same app), + // use the _KDE_NET_WM_USER_CREATION_TIME trick. + // Otherwise, refuse activation of a window + // from already running application if this application + // is not the active one (unless focus stealing prevention is turned off). + X11Client *act = dynamic_cast(workspace()->mostRecentlyActivatedClient()); + if (act != nullptr && !belongToSameApplication(act, this, SameApplicationCheck::RelaxedForActive)) { + bool first_window = true; + auto sameApplicationActiveHackPredicate = [this](const X11Client *cl) { + // ignore already existing splashes, toolbars, utilities and menus, + // as the app may show those before the main window + return !cl->isSplash() && !cl->isToolbar() && !cl->isUtility() && !cl->isMenu() + && cl != this && X11Client::belongToSameApplication(cl, this, SameApplicationCheck::RelaxedForActive); + }; + if (isTransient()) { + auto clientMainClients = [this]() { + QList ret; + const auto mcs = mainClients(); + for (auto mc: mcs) { + if (X11Client *c = dynamic_cast(mc)) { + ret << c; + } + } + return ret; + }; + if (act->hasTransient(this, true)) + ; // is transient for currently active window, even though it's not + // the same app (e.g. kcookiejar dialog) -> allow activation + else if (groupTransient() && + findInList(clientMainClients(), sameApplicationActiveHackPredicate) == nullptr) + ; // standalone transient + else + first_window = false; + } else { + if (workspace()->findClient(sameApplicationActiveHackPredicate)) + first_window = false; + } + // don't refuse if focus stealing prevention is turned off + if (!first_window && rules()->checkFSP(options->focusStealingPreventionLevel()) > 0) { + qCDebug(KWIN_CORE) << "User timestamp, already exists:" << 0; + return 0; // refuse activation + } + } + // Creation time would just mess things up during session startup, + // as possibly many apps are started up at the same time. + // If there's no active window yet, no timestamp will be needed, + // as plain Workspace::allowClientActivation() will return true + // in such case. And if there's already active window, + // it's better not to activate the new one. + // Unless it was the active window at the time + // of session saving and there was no user interaction yet, + // this check will be done in manage(). + if (session) + return -1U; + time = readUserCreationTime(); + } + qCDebug(KWIN_CORE) << "User timestamp, final:" << this << ":" << time; + return time; +} + +xcb_timestamp_t X11Client::userTime() const +{ + xcb_timestamp_t time = m_userTime; + if (time == 0) // doesn't want focus after showing + return 0; + Q_ASSERT(group() != nullptr); + if (time == -1U + || (group()->userTime() != -1U + && NET::timestampCompare(group()->userTime(), time) > 0)) + time = group()->userTime(); + return time; +} + +void X11Client::doSetActive() +{ + updateUrgency(); // demand attention again if it's still urgent + info->setState(isActive() ? NET::Focused : NET::States(), NET::Focused); +} + +void X11Client::startupIdChanged() +{ + KStartupInfoId asn_id; + KStartupInfoData asn_data; + bool asn_valid = workspace()->checkStartupNotification(window(), asn_id, asn_data); + if (!asn_valid) + return; + // If the ASN contains desktop, move it to the desktop, otherwise move it to the current + // desktop (since the new ASN should make the window act like if it's a new application + // launched). However don't affect the window's desktop if it's set to be on all desktops. + int desktop = VirtualDesktopManager::self()->current(); + if (asn_data.desktop() != 0) + desktop = asn_data.desktop(); + if (!isOnAllDesktops()) + workspace()->sendClientToDesktop(this, desktop, true); + if (asn_data.xinerama() != -1) + workspace()->sendClientToScreen(this, asn_data.xinerama()); + const xcb_timestamp_t timestamp = asn_id.timestamp(); + if (timestamp != 0) { + bool activate = workspace()->allowClientActivation(this, timestamp); + if (asn_data.desktop() != 0 && !isOnCurrentDesktop()) + activate = false; // it was started on different desktop than current one + if (activate) + workspace()->activateClient(this); + else + demandAttention(); + } +} + +void X11Client::updateUrgency() +{ + if (info->urgency()) + demandAttention(); +} + +//**************************************** +// Group +//**************************************** + +void Group::startupIdChanged() +{ + KStartupInfoId asn_id; + KStartupInfoData asn_data; + bool asn_valid = workspace()->checkStartupNotification(leader_wid, asn_id, asn_data); + if (!asn_valid) + return; + if (asn_id.timestamp() != 0 && user_time != -1U + && NET::timestampCompare(asn_id.timestamp(), user_time) > 0) { + user_time = asn_id.timestamp(); + } +} + +void Group::updateUserTime(xcb_timestamp_t time) +{ + // copy of X11Client::updateUserTime + if (time == XCB_CURRENT_TIME) { + updateXTime(); + time = xTime(); + } + if (time != -1U + && (user_time == XCB_CURRENT_TIME + || NET::timestampCompare(time, user_time) > 0)) // time > user_time + user_time = time; +} + +} // namespace diff --git a/activities.cpp b/activities.cpp new file mode 100644 index 0000000..54e8f89 --- /dev/null +++ b/activities.cpp @@ -0,0 +1,211 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "activities.h" +// KWin +#include "x11client.h" +#include "workspace.h" +// KDE +#include +#include +// Qt +#include +#include +#include +#include + +namespace KWin +{ + +KWIN_SINGLETON_FACTORY(Activities) + +Activities::Activities(QObject *parent) + : QObject(parent) + , m_controller(new KActivities::Controller(this)) +{ + connect(m_controller, &KActivities::Controller::activityRemoved, this, &Activities::slotRemoved); + connect(m_controller, &KActivities::Controller::activityRemoved, this, &Activities::removed); + connect(m_controller, &KActivities::Controller::activityAdded, this, &Activities::added); + connect(m_controller, &KActivities::Controller::currentActivityChanged, this, &Activities::slotCurrentChanged); +} + +Activities::~Activities() +{ + s_self = nullptr; +} + +KActivities::Consumer::ServiceStatus Activities::serviceStatus() const +{ + return m_controller->serviceStatus(); +} + +void Activities::setCurrent(const QString &activity) +{ + m_controller->setCurrentActivity(activity); +} + +void Activities::slotCurrentChanged(const QString &newActivity) +{ + if (m_current == newActivity) { + return; + } + m_previous = m_current; + m_current = newActivity; + emit currentChanged(newActivity); +} + +void Activities::slotRemoved(const QString &activity) +{ + foreach (X11Client *client, Workspace::self()->clientList()) { + if (client->isDesktop()) + continue; + client->setOnActivity(activity, false); + } + //toss out any session data for it + KConfigGroup cg(KSharedConfig::openConfig(), QByteArray("SubSession: ").append(activity.toUtf8()).constData()); + cg.deleteGroup(); +} + +void Activities::toggleClientOnActivity(X11Client *c, const QString &activity, bool dont_activate) +{ + //int old_desktop = c->desktop(); + bool was_on_activity = c->isOnActivity(activity); + bool was_on_all = c->isOnAllActivities(); + //note: all activities === no activities + bool enable = was_on_all || !was_on_activity; + c->setOnActivity(activity, enable); + if (c->isOnActivity(activity) == was_on_activity && c->isOnAllActivities() == was_on_all) // No change + return; + + Workspace *ws = Workspace::self(); + if (c->isOnCurrentActivity()) { + if (c->wantsTabFocus() && options->focusPolicyIsReasonable() && + !was_on_activity && // for stickyness changes + //FIXME not sure if the line above refers to the correct activity + !dont_activate) + ws->requestFocus(c); + else + ws->restackClientUnderActive(c); + } else + ws->raiseClient(c); + + //notifyWindowDesktopChanged( c, old_desktop ); + + auto transients_stacking_order = ws->ensureStackingOrder(c->transients()); + for (auto it = transients_stacking_order.constBegin(); + it != transients_stacking_order.constEnd(); + ++it) { + X11Client *c = dynamic_cast(*it); + if (!c) { + continue; + } + toggleClientOnActivity(c, activity, dont_activate); + } + ws->updateClientArea(); +} + +bool Activities::start(const QString &id) +{ + Workspace *ws = Workspace::self(); + if (ws->sessionManager()->state() == SessionState::Saving) { + return false; //ksmserver doesn't queue requests (yet) + } + + if (!all().contains(id)) { + return false; //bogus id + } + + ws->loadSubSessionInfo(id); + + QDBusInterface ksmserver("org.kde.ksmserver", "/KSMServer", "org.kde.KSMServerInterface"); + if (ksmserver.isValid()) { + ksmserver.asyncCall("restoreSubSession", id); + } else { + qCDebug(KWIN_CORE) << "couldn't get ksmserver interface"; + return false; + } + return true; +} + +bool Activities::stop(const QString &id) +{ + if (Workspace::self()->sessionManager()->state() == SessionState::Saving) { + return false; //ksmserver doesn't queue requests (yet) + //FIXME what about session *loading*? + } + + //ugly hack to avoid dbus deadlocks + QMetaObject::invokeMethod(this, "reallyStop", Qt::QueuedConnection, Q_ARG(QString, id)); + //then lie and assume it worked. + return true; +} + +void Activities::reallyStop(const QString &id) +{ + Workspace *ws = Workspace::self(); + if (ws->sessionManager()->state() == SessionState::Saving) + return; //ksmserver doesn't queue requests (yet) + + qCDebug(KWIN_CORE) << id; + + QSet saveSessionIds; + QSet dontCloseSessionIds; + const QList &clients = ws->clientList(); + for (auto it = clients.constBegin(); it != clients.constEnd(); ++it) { + const X11Client *c = (*it); + if (c->isDesktop()) + continue; + const QByteArray sessionId = c->sessionId(); + if (sessionId.isEmpty()) { + continue; //TODO support old wm_command apps too? + } + + //qDebug() << sessionId; + + //if it's on the activity that's closing, it needs saving + //but if a process is on some other open activity, I don't wanna close it yet + //this is, of course, complicated by a process having many windows. + if (c->isOnAllActivities()) { + dontCloseSessionIds << sessionId; + continue; + } + + const QStringList activities = c->activities(); + foreach (const QString & activityId, activities) { + if (activityId == id) { + saveSessionIds << sessionId; + } else if (running().contains(activityId)) { + dontCloseSessionIds << sessionId; + } + } + } + + ws->storeSubSession(id, saveSessionIds); + + QStringList saveAndClose; + QStringList saveOnly; + foreach (const QByteArray & sessionId, saveSessionIds) { + if (dontCloseSessionIds.contains(sessionId)) { + saveOnly << sessionId; + } else { + saveAndClose << sessionId; + } + } + + qCDebug(KWIN_CORE) << "saveActivity" << id << saveAndClose << saveOnly; + + //pass off to ksmserver + QDBusInterface ksmserver("org.kde.ksmserver", "/KSMServer", "org.kde.KSMServerInterface"); + if (ksmserver.isValid()) { + ksmserver.asyncCall("saveSubSession", id, saveAndClose, saveOnly); + } else { + qCDebug(KWIN_CORE) << "couldn't get ksmserver interface"; + } +} + +} // namespace diff --git a/activities.h b/activities.h new file mode 100644 index 0000000..6af94fe --- /dev/null +++ b/activities.h @@ -0,0 +1,118 @@ +/* + 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 KWIN_ACTIVITIES_H +#define KWIN_ACTIVITIES_H + +#include + +#include +#include + +#include + +namespace KActivities { +class Controller; +} + +namespace KWin +{ +class X11Client; + +class KWIN_EXPORT Activities : public QObject +{ + Q_OBJECT + +public: + ~Activities() override; + + bool stop(const QString &id); + bool start(const QString &id); + void setCurrent(const QString &activity); + /** + * Adds/removes client \a c to/from \a activity. + * + * Takes care of transients as well. + */ + void toggleClientOnActivity(X11Client *c, const QString &activity, bool dont_activate); + + QStringList running() const; + QStringList all() const; + const QString ¤t() const; + const QString &previous() const; + + static QString nullUuid(); + + KActivities::Controller::ServiceStatus serviceStatus() const; + +Q_SIGNALS: + /** + * This signal is emitted when the global + * activity is changed + * @param id id of the new current activity + */ + void currentChanged(const QString &id); + /** + * This signal is emitted when a new activity is added + * @param id id of the new activity + */ + void added(const QString &id); + /** + * This signal is emitted when the activity + * is removed + * @param id id of the removed activity + */ + void removed(const QString &id); + +private Q_SLOTS: + void slotRemoved(const QString &activity); + void slotCurrentChanged(const QString &newActivity); + void reallyStop(const QString &id); //dbus deadlocks suck + +private: + QString m_previous; + QString m_current; + KActivities::Controller *m_controller; + + KWIN_SINGLETON(Activities) +}; + +inline +QStringList Activities::all() const +{ + return m_controller->activities(); +} + +inline +const QString &Activities::current() const +{ + return m_current; +} + +inline +const QString &Activities::previous() const +{ + return m_previous; +} + +inline +QStringList Activities::running() const +{ + return m_controller->activities(KActivities::Info::Running); +} + +inline +QString Activities::nullUuid() +{ + // cloned from kactivities/src/lib/core/consumer.cpp + return QStringLiteral("00000000-0000-0000-0000-000000000000"); +} + +} + +#endif // KWIN_ACTIVITIES_H diff --git a/appmenu.cpp b/appmenu.cpp new file mode 100644 index 0000000..01a036f --- /dev/null +++ b/appmenu.cpp @@ -0,0 +1,123 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2011 Lionel Chauvin + SPDX-FileCopyrightText: 2011, 2012 Cédric Bellegarde + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "appmenu.h" +#include "x11client.h" +#include "workspace.h" +#include + +#include +#include + +#include "decorations/decorationbridge.h" +#include + +namespace KWin +{ + +KWIN_SINGLETON_FACTORY(ApplicationMenu) + +static const QString s_viewService(QStringLiteral("org.kde.kappmenuview")); + +ApplicationMenu::ApplicationMenu(QObject *parent) + : QObject(parent) + , m_appmenuInterface(new OrgKdeKappmenuInterface(QStringLiteral("org.kde.kappmenu"), QStringLiteral("/KAppMenu"), QDBusConnection::sessionBus(), this)) +{ + connect(m_appmenuInterface, &OrgKdeKappmenuInterface::showRequest, this, &ApplicationMenu::slotShowRequest); + connect(m_appmenuInterface, &OrgKdeKappmenuInterface::menuShown, this, &ApplicationMenu::slotMenuShown); + connect(m_appmenuInterface, &OrgKdeKappmenuInterface::menuHidden, this, &ApplicationMenu::slotMenuHidden); + + m_kappMenuWatcher = new QDBusServiceWatcher(QStringLiteral("org.kde.kappmenu"), QDBusConnection::sessionBus(), + QDBusServiceWatcher::WatchForRegistration|QDBusServiceWatcher::WatchForUnregistration, this); + + connect(m_kappMenuWatcher, &QDBusServiceWatcher::serviceRegistered, + this, [this] () { + m_applicationMenuEnabled = true; + emit applicationMenuEnabledChanged(true); + }); + connect(m_kappMenuWatcher, &QDBusServiceWatcher::serviceUnregistered, + this, [this] () { + m_applicationMenuEnabled = false; + emit applicationMenuEnabledChanged(false); + }); + + m_applicationMenuEnabled = QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.kappmenu")); +} + +ApplicationMenu::~ApplicationMenu() +{ + s_self = nullptr; +} + +bool ApplicationMenu::applicationMenuEnabled() const +{ + return m_applicationMenuEnabled; +} + +void ApplicationMenu::setViewEnabled(bool enabled) +{ + if (enabled) { + QDBusConnection::sessionBus().interface()->registerService(s_viewService, + QDBusConnectionInterface::QueueService, + QDBusConnectionInterface::DontAllowReplacement); + } else { + QDBusConnection::sessionBus().interface()->unregisterService(s_viewService); + } +} + +void ApplicationMenu::slotShowRequest(const QString &serviceName, const QDBusObjectPath &menuObjectPath, int actionId) +{ + // Ignore show request when user has not configured the application menu title bar button + auto decorationSettings = Decoration::DecorationBridge::self()->settings(); + if (!decorationSettings->decorationButtonsLeft().contains(KDecoration2::DecorationButtonType::ApplicationMenu) + && !decorationSettings->decorationButtonsRight().contains(KDecoration2::DecorationButtonType::ApplicationMenu)) { + return; + } + + if (AbstractClient *c = findAbstractClientWithApplicationMenu(serviceName, menuObjectPath)) { + c->showApplicationMenu(actionId); + } +} + +void ApplicationMenu::slotMenuShown(const QString &serviceName, const QDBusObjectPath &menuObjectPath) +{ + if (AbstractClient *c = findAbstractClientWithApplicationMenu(serviceName, menuObjectPath)) { + c->setApplicationMenuActive(true); + } +} + +void ApplicationMenu::slotMenuHidden(const QString &serviceName, const QDBusObjectPath &menuObjectPath) +{ + if (AbstractClient *c = findAbstractClientWithApplicationMenu(serviceName, menuObjectPath)) { + c->setApplicationMenuActive(false); + } +} + +void ApplicationMenu::showApplicationMenu(const QPoint &p, AbstractClient *c, int actionId) +{ + if (!c->hasApplicationMenu()) { + return; + } + m_appmenuInterface->showMenu(p.x(), p.y(), c->applicationMenuServiceName(), QDBusObjectPath(c->applicationMenuObjectPath()), actionId); +} + +AbstractClient *ApplicationMenu::findAbstractClientWithApplicationMenu(const QString &serviceName, const QDBusObjectPath &menuObjectPath) +{ + if (serviceName.isEmpty() || menuObjectPath.path().isEmpty()) { + return nullptr; + } + + return Workspace::self()->findAbstractClient([&](const AbstractClient *c) { + return c->applicationMenuServiceName() == serviceName + && c->applicationMenuObjectPath() == menuObjectPath.path(); + }); +} + +} // namespace KWin diff --git a/appmenu.h b/appmenu.h new file mode 100644 index 0000000..c69b638 --- /dev/null +++ b/appmenu.h @@ -0,0 +1,64 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2011 Lionel Chauvin + SPDX-FileCopyrightText: 2011, 2012 Cédric Bellegarde + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_APPMENU_H +#define KWIN_APPMENU_H +// KWin +#include +// Qt +#include +// xcb +#include + +class QPoint; +class OrgKdeKappmenuInterface; +class QDBusObjectPath; +class QDBusServiceWatcher; + +namespace KWin +{ + +class AbstractClient; + +class ApplicationMenu : public QObject +{ + Q_OBJECT + +public: + ~ApplicationMenu() override; + + void showApplicationMenu(const QPoint &pos, AbstractClient *c, int actionId); + + bool applicationMenuEnabled() const; + + void setViewEnabled(bool enabled); + +signals: + void applicationMenuEnabledChanged(bool enabled); + +private Q_SLOTS: + void slotShowRequest(const QString &serviceName, const QDBusObjectPath &menuObjectPath, int actionId); + void slotMenuShown(const QString &serviceName, const QDBusObjectPath &menuObjectPath); + void slotMenuHidden(const QString &serviceName, const QDBusObjectPath &menuObjectPath); + +private: + OrgKdeKappmenuInterface *m_appmenuInterface; + QDBusServiceWatcher *m_kappMenuWatcher; + + AbstractClient *findAbstractClientWithApplicationMenu(const QString &serviceName, const QDBusObjectPath &menuObjectPath); + + bool m_applicationMenuEnabled = false; + + KWIN_SINGLETON(ApplicationMenu) +}; + +} + +#endif // KWIN_APPMENU_H diff --git a/atoms.cpp b/atoms.cpp new file mode 100644 index 0000000..bc68143 --- /dev/null +++ b/atoms.cpp @@ -0,0 +1,91 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "atoms.h" + +namespace KWin +{ + +Atoms::Atoms() + : kwin_running(QByteArrayLiteral("KWIN_RUNNING")) + , activities(QByteArrayLiteral("_KDE_NET_WM_ACTIVITIES")) + , wm_protocols(QByteArrayLiteral("WM_PROTOCOLS")) + , wm_delete_window(QByteArrayLiteral("WM_DELETE_WINDOW")) + , wm_take_focus(QByteArrayLiteral("WM_TAKE_FOCUS")) + , wm_change_state(QByteArrayLiteral("WM_CHANGE_STATE")) + , wm_client_leader(QByteArrayLiteral("WM_CLIENT_LEADER")) + , wm_window_role(QByteArrayLiteral("WM_WINDOW_ROLE")) + , wm_state(QByteArrayLiteral("WM_STATE")) + , sm_client_id(QByteArrayLiteral("SM_CLIENT_ID")) + , motif_wm_hints(QByteArrayLiteral("_MOTIF_WM_HINTS")) + , net_wm_context_help(QByteArrayLiteral("_NET_WM_CONTEXT_HELP")) + , net_wm_ping(QByteArrayLiteral("_NET_WM_PING")) + , net_wm_user_time(QByteArrayLiteral("_NET_WM_USER_TIME")) + , kde_net_wm_user_creation_time(QByteArrayLiteral("_KDE_NET_WM_USER_CREATION_TIME")) + , net_wm_take_activity(QByteArrayLiteral("_NET_WM_TAKE_ACTIVITY")) + , net_wm_window_opacity(QByteArrayLiteral("_NET_WM_WINDOW_OPACITY")) + , xdnd_selection(QByteArrayLiteral("XdndSelection")) + , xdnd_aware(QByteArrayLiteral("XdndAware")) + , xdnd_enter(QByteArrayLiteral("XdndEnter")) + , xdnd_type_list(QByteArrayLiteral("XdndTypeList")) + , xdnd_position(QByteArrayLiteral("XdndPosition")) + , xdnd_status(QByteArrayLiteral("XdndStatus")) + , xdnd_action_copy(QByteArrayLiteral("XdndActionCopy")) + , xdnd_action_move(QByteArrayLiteral("XdndActionMove")) + , xdnd_action_ask(QByteArrayLiteral("XdndActionAsk")) + , xdnd_drop(QByteArrayLiteral("XdndDrop")) + , xdnd_leave(QByteArrayLiteral("XdndLeave")) + , xdnd_finished(QByteArrayLiteral("XdndFinished")) + , net_frame_extents(QByteArrayLiteral("_NET_FRAME_EXTENTS")) + , kde_net_wm_frame_strut(QByteArrayLiteral("_KDE_NET_WM_FRAME_STRUT")) + , net_wm_sync_request_counter(QByteArrayLiteral("_NET_WM_SYNC_REQUEST_COUNTER")) + , net_wm_sync_request(QByteArrayLiteral("_NET_WM_SYNC_REQUEST")) + , kde_net_wm_shadow(QByteArrayLiteral("_KDE_NET_WM_SHADOW")) + , kde_first_in_window_list(QByteArrayLiteral("_KDE_FIRST_IN_WINDOWLIST")) + , kde_color_sheme(QByteArrayLiteral("_KDE_NET_WM_COLOR_SCHEME")) + , kde_skip_close_animation(QByteArrayLiteral("_KDE_NET_WM_SKIP_CLOSE_ANIMATION")) + , kde_screen_edge_show(QByteArrayLiteral("_KDE_NET_WM_SCREEN_EDGE_SHOW")) + , kwin_dbus_service(QByteArrayLiteral("_ORG_KDE_KWIN_DBUS_SERVICE")) + , utf8_string(QByteArrayLiteral("UTF8_STRING")) + , text(QByteArrayLiteral("TEXT")) + , uri_list(QByteArrayLiteral("text/uri-list")) + , netscape_url(QByteArrayLiteral("_NETSCAPE_URL")) + , moz_url(QByteArrayLiteral("text/x-moz-url")) + , wl_surface_id(QByteArrayLiteral("WL_SURFACE_ID")) + , kde_net_wm_appmenu_service_name(QByteArrayLiteral("_KDE_NET_WM_APPMENU_SERVICE_NAME")) + , kde_net_wm_appmenu_object_path(QByteArrayLiteral("_KDE_NET_WM_APPMENU_OBJECT_PATH")) + , clipboard(QByteArrayLiteral("CLIPBOARD")) + , timestamp(QByteArrayLiteral("TIMESTAMP")) + , targets(QByteArrayLiteral("TARGETS")) + , delete_atom(QByteArrayLiteral("DELETE")) + , incr(QByteArrayLiteral("INCR")) + , wl_selection(QByteArrayLiteral("WL_SELECTION")) + , m_dtSmWindowInfo(QByteArrayLiteral("_DT_SM_WINDOW_INFO")) + , m_motifSupport(QByteArrayLiteral("_MOTIF_WM_INFO")) + , m_helpersRetrieved(false) +{ +} + +void Atoms::retrieveHelpers() +{ + if (m_helpersRetrieved) { + return; + } + // just retrieve the atoms once, all others are retrieved when being accessed + // Q_UNUSED is used in the hope that the compiler doesn't optimize the operations away + xcb_atom_t atom = m_dtSmWindowInfo; + Q_UNUSED(atom) + atom = m_motifSupport; + Q_UNUSED(atom) + m_helpersRetrieved = true; +} + +} // namespace diff --git a/atoms.h b/atoms.h new file mode 100644 index 0000000..52aaa4a --- /dev/null +++ b/atoms.h @@ -0,0 +1,98 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_ATOMS_H +#define KWIN_ATOMS_H + +#include "xcbutils.h" + +namespace KWin +{ + +class KWIN_EXPORT Atoms +{ +public: + Atoms(); + + Xcb::Atom kwin_running; + Xcb::Atom activities; + + Xcb::Atom wm_protocols; + Xcb::Atom wm_delete_window; + Xcb::Atom wm_take_focus; + Xcb::Atom wm_change_state; + Xcb::Atom wm_client_leader; + Xcb::Atom wm_window_role; + Xcb::Atom wm_state; + Xcb::Atom sm_client_id; + + Xcb::Atom motif_wm_hints; + Xcb::Atom net_wm_context_help; + Xcb::Atom net_wm_ping; + Xcb::Atom net_wm_user_time; + Xcb::Atom kde_net_wm_user_creation_time; + Xcb::Atom net_wm_take_activity; + Xcb::Atom net_wm_window_opacity; + Xcb::Atom xdnd_selection; + Xcb::Atom xdnd_aware; + Xcb::Atom xdnd_enter; + Xcb::Atom xdnd_type_list; + Xcb::Atom xdnd_position; + Xcb::Atom xdnd_status; + Xcb::Atom xdnd_action_copy; + Xcb::Atom xdnd_action_move; + Xcb::Atom xdnd_action_ask; + Xcb::Atom xdnd_drop; + Xcb::Atom xdnd_leave; + Xcb::Atom xdnd_finished; + Xcb::Atom net_frame_extents; + Xcb::Atom kde_net_wm_frame_strut; + Xcb::Atom net_wm_sync_request_counter; + Xcb::Atom net_wm_sync_request; + Xcb::Atom kde_net_wm_shadow; + Xcb::Atom kde_first_in_window_list; + Xcb::Atom kde_color_sheme; + Xcb::Atom kde_skip_close_animation; + Xcb::Atom kde_screen_edge_show; + Xcb::Atom kwin_dbus_service; + Xcb::Atom utf8_string; + Xcb::Atom text; + Xcb::Atom uri_list; + Xcb::Atom netscape_url; + Xcb::Atom moz_url; + Xcb::Atom wl_surface_id; + Xcb::Atom kde_net_wm_appmenu_service_name; + Xcb::Atom kde_net_wm_appmenu_object_path; + Xcb::Atom clipboard; + Xcb::Atom timestamp; + Xcb::Atom targets; + Xcb::Atom delete_atom; + Xcb::Atom incr; + Xcb::Atom wl_selection; + + /** + * @internal + */ + void retrieveHelpers(); + +private: + // helper atoms we need to resolve to "announce" support (see #172028) + Xcb::Atom m_dtSmWindowInfo; + Xcb::Atom m_motifSupport; + bool m_helpersRetrieved; +}; + + +extern KWIN_EXPORT Atoms* atoms; + +} // namespace + +#endif diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt new file mode 100644 index 0000000..d553e44 --- /dev/null +++ b/autotests/CMakeLists.txt @@ -0,0 +1,426 @@ +add_definitions(-DKWIN_UNIT_TEST) +remove_definitions(-DQT_USE_QSTRINGBUILDER) +add_subdirectory(libkwineffects) +add_subdirectory(libxrenderutils) +add_subdirectory(integration) +add_subdirectory(libinput) +if (HAVE_DRM) + add_subdirectory(drm) +endif() +add_subdirectory(tabbox) + +######################################################## +# Test ScreenPaintData +######################################################## +set(testScreenPaintData_SRCS test_screen_paint_data.cpp) +add_executable(testScreenPaintData ${testScreenPaintData_SRCS}) +target_link_libraries(testScreenPaintData kwineffects Qt5::Test Qt5::Widgets KF5::WindowSystem) +add_test(NAME kwin-testScreenPaintData COMMAND testScreenPaintData) +ecm_mark_as_test(testScreenPaintData) + +######################################################## +# Test WindowPaintData +######################################################## +set(testWindowPaintData_SRCS test_window_paint_data.cpp) +add_executable(testWindowPaintData ${testWindowPaintData_SRCS}) +target_link_libraries(testWindowPaintData kwineffects Qt5::Widgets Qt5::Test ) +add_test(NAME kwin-testWindowPaintData COMMAND testWindowPaintData) +ecm_mark_as_test(testWindowPaintData) + +######################################################## +# Test VirtualDesktopManager +######################################################## +set(testVirtualDesktops_SRCS + ../virtualdesktops.cpp + test_virtual_desktops.cpp +) +add_executable(testVirtualDesktops ${testVirtualDesktops_SRCS}) + +target_link_libraries(testVirtualDesktops + Qt5::Test + Qt5::Widgets + + KF5::ConfigCore + KF5::GlobalAccel + KF5::I18n + Plasma::KWaylandServer + KF5::WindowSystem +) +add_test(NAME kwin-testVirtualDesktops COMMAND testVirtualDesktops) +ecm_mark_as_test(testVirtualDesktops) + +######################################################## +# Test ClientMachine +######################################################## +set(testClientMachine_SRCS + ../client_machine.cpp + test_client_machine.cpp +) +add_executable(testClientMachine ${testClientMachine_SRCS}) +set_target_properties(testClientMachine PROPERTIES COMPILE_DEFINITIONS "NO_NONE_WINDOW") + +target_link_libraries(testClientMachine + Qt5::Concurrent + Qt5::Test + Qt5::Widgets + Qt5::X11Extras + + KF5::ConfigCore + KF5::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 +######################################################## +set(testXcbWrapper_SRCS + test_xcb_wrapper.cpp +) +add_executable(testXcbWrapper ${testXcbWrapper_SRCS}) + +target_link_libraries(testXcbWrapper + Qt5::Test + Qt5::Widgets + Qt5::X11Extras + + KF5::ConfigCore + KF5::WindowSystem + + XCB::XCB +) +add_test(NAME kwin-testXcbWrapper COMMAND testXcbWrapper) +ecm_mark_as_test(testXcbWrapper) + +if (XCB_ICCCM_FOUND) + add_executable(testXcbSizeHints test_xcb_size_hints.cpp) + set_target_properties(testXcbSizeHints PROPERTIES COMPILE_DEFINITIONS "NO_NONE_WINDOW") + target_link_libraries(testXcbSizeHints + Qt5::Test + Qt5::Widgets + Qt5::X11Extras + + KF5::ConfigCore + KF5::WindowSystem + + XCB::ICCCM + XCB::XCB + ) + add_test(NAME kwin-testXcbSizeHints COMMAND testXcbSizeHints) + ecm_mark_as_test(testXcbSizeHints) +endif() + +######################################################## +# Test XcbWindow +######################################################## +set(testXcbWindow_SRCS + test_xcb_window.cpp +) +add_executable(testXcbWindow ${testXcbWindow_SRCS}) + +target_link_libraries(testXcbWindow + Qt5::Test + Qt5::Widgets + Qt5::X11Extras + + KF5::ConfigCore + KF5::WindowSystem + + XCB::XCB +) +add_test(NAME kwin-testXcbWindow COMMAND testXcbWindow) +ecm_mark_as_test(testXcbWindow) + +######################################################## +# Test BuiltInEffectLoader +######################################################## +set(testBuiltInEffectLoader_SRCS + ../effectloader.cpp + mock_effectshandler.cpp + test_builtin_effectloader.cpp +) +add_executable(testBuiltInEffectLoader ${testBuiltInEffectLoader_SRCS}) +set_target_properties(testBuiltInEffectLoader PROPERTIES COMPILE_DEFINITIONS "NO_NONE_WINDOW") + +target_link_libraries(testBuiltInEffectLoader + Qt5::Concurrent + Qt5::Test + Qt5::X11Extras + + KF5::Package + + kwineffects + kwin4_effect_builtins +) + +add_test(NAME kwin-testBuiltInEffectLoader COMMAND testBuiltInEffectLoader) +ecm_mark_as_test(testBuiltInEffectLoader) + +######################################################## +# Test ScriptedEffectLoader +######################################################## +include_directories(${KWin_SOURCE_DIR}) +set(testScriptedEffectLoader_SRCS + ../effectloader.cpp + ../cursor.cpp + ../screens.cpp + ../scripting/scriptedeffect.cpp + ../scripting/scripting_logging.cpp + ../scripting/scriptingutils.cpp + mock_abstract_client.cpp + mock_effectshandler.cpp + mock_screens.cpp + mock_workspace.cpp + test_scripted_effectloader.cpp +) +kconfig_add_kcfg_files(testScriptedEffectLoader_SRCS ../settings.kcfgc) +add_executable(testScriptedEffectLoader ${testScriptedEffectLoader_SRCS}) + +target_link_libraries(testScriptedEffectLoader + Qt5::Concurrent + Qt5::Qml + Qt5::Script + Qt5::Sensors + Qt5::Test + Qt5::X11Extras + + KF5::ConfigGui + KF5::GlobalAccel + KF5::I18n + KF5::Notifications + KF5::Package + + kwineffects + kwin4_effect_builtins +) + +add_test(NAME kwin-testScriptedEffectLoader COMMAND testScriptedEffectLoader) +ecm_mark_as_test(testScriptedEffectLoader) + +######################################################## +# Test PluginEffectLoader +######################################################## +set(testPluginEffectLoader_SRCS + ../effectloader.cpp + mock_effectshandler.cpp + test_plugin_effectloader.cpp +) +add_executable(testPluginEffectLoader ${testPluginEffectLoader_SRCS}) + +target_link_libraries(testPluginEffectLoader + Qt5::Concurrent + Qt5::Test + Qt5::X11Extras + + KF5::Package + + kwineffects + kwin4_effect_builtins +) + +add_test(NAME kwin-testPluginEffectLoader COMMAND testPluginEffectLoader) +ecm_mark_as_test(testPluginEffectLoader) + +######################################################## +# FakeEffectPlugin +######################################################## +add_library(fakeeffectplugin MODULE fakeeffectplugin.cpp) +set_target_properties(fakeeffectplugin PROPERTIES PREFIX "") +target_link_libraries(fakeeffectplugin kwineffects) + +######################################################## +# FakeEffectPlugin-Version +######################################################## +add_library(effectversionplugin MODULE fakeeffectplugin_version.cpp) +set_target_properties(effectversionplugin PROPERTIES PREFIX "") +target_link_libraries(effectversionplugin kwineffects) + +######################################################## +# Test Screens +######################################################## +set(testScreens_SRCS + ../screens.cpp + ../cursor.cpp + ../x11eventfilter.cpp + mock_abstract_client.cpp + mock_screens.cpp + mock_workspace.cpp + mock_x11client.cpp + test_screens.cpp +) +kconfig_add_kcfg_files(testScreens_SRCS ../settings.kcfgc) + +add_executable(testScreens ${testScreens_SRCS}) +target_include_directories(testScreens BEFORE PRIVATE ./) +target_link_libraries(testScreens + Qt5::DBus + Qt5::Sensors + Qt5::Test + Qt5::Widgets + Qt5::X11Extras + + KF5::ConfigCore + KF5::ConfigGui + KF5::I18n + KF5::Notifications + KF5::WindowSystem + + XCB::XCB #for xcbutils.h +) + +add_test(NAME kwin_testScreens COMMAND testScreens) +ecm_mark_as_test(testScreens) + +######################################################## +# Test ScreenEdges +######################################################## +set(testScreenEdges_SRCS + ../atoms.cpp + ../gestures.cpp + ../plugins/platforms/x11/standalone/edge.cpp + ../screenedge.cpp + ../screens.cpp + ../virtualdesktops.cpp + ../cursor.cpp + ../xcbutils.cpp # init of extensions + mock_abstract_client.cpp + mock_screens.cpp + mock_workspace.cpp + mock_x11client.cpp + test_screen_edges.cpp +) +kconfig_add_kcfg_files(testScreenEdges_SRCS ../settings.kcfgc) +qt5_add_dbus_interface(testScreenEdges_SRCS ${KSCREENLOCKER_DBUS_INTERFACES_DIR}/kf5_org.freedesktop.ScreenSaver.xml screenlocker_interface ) + +add_executable(testScreenEdges ${testScreenEdges_SRCS}) +set_target_properties(testScreenEdges PROPERTIES COMPILE_DEFINITIONS "NO_NONE_WINDOW") +target_include_directories(testScreenEdges BEFORE PRIVATE ./) +target_link_libraries(testScreenEdges + Qt5::DBus + Qt5::Sensors + Qt5::Test + Qt5::X11Extras + + KF5::ConfigCore + KF5::ConfigGui + KF5::GlobalAccel + KF5::I18n + KF5::Notifications + Plasma::KWaylandServer + KF5::WindowSystem + + XCB::COMPOSITE + XCB::DAMAGE + XCB::GLX + XCB::RANDR + XCB::SHM + XCB::SYNC + XCB::XCB + XCB::XFIXES +) + +add_test(NAME kwin_testScreenEdges COMMAND testScreenEdges) +ecm_mark_as_test(testScreenEdges) + +######################################################## +# Test OnScreenNotification +######################################################## +set(testOnScreenNotification_SRCS + ../input_event_spy.cpp + ../onscreennotification.cpp + onscreennotificationtest.cpp +) +add_executable(testOnScreenNotification ${testOnScreenNotification_SRCS}) + +target_link_libraries(testOnScreenNotification + Qt5::DBus + Qt5::Quick + Qt5::Test + Qt5::Widgets # QAction include + + KF5::ConfigCore +) + +add_test(NAME kwin-testOnScreenNotification COMMAND testOnScreenNotification) +ecm_mark_as_test(testOnScreenNotification) + +######################################################## +# Test Gestures +######################################################## +set(testGestures_SRCS + ../gestures.cpp + test_gestures.cpp +) +add_executable(testGestures ${testGestures_SRCS}) + +target_link_libraries(testGestures + Qt5::Test +) + +add_test(NAME kwin-testGestures COMMAND testGestures) +ecm_mark_as_test(testGestures) + +######################################################## +# Test X11 TimestampUpdate +######################################################## +add_executable(testX11TimestampUpdate test_x11_timestamp_update.cpp) +target_link_libraries(testX11TimestampUpdate + KF5::CoreAddons + Qt5::Test + kwin +) +add_test(NAME kwin-testX11TimestampUpdate COMMAND testX11TimestampUpdate) +ecm_mark_as_test(testX11TimestampUpdate) + +set(testOpenGLContextAttributeBuilder_SRCS + ../abstract_opengl_context_attribute_builder.cpp + ../egl_context_attribute_builder.cpp + opengl_context_attribute_builder_test.cpp +) + +if (HAVE_EPOXY_GLX) + set(testOpenGLContextAttributeBuilder_SRCS ${testOpenGLContextAttributeBuilder_SRCS} ../plugins/platforms/x11/standalone/glx_context_attribute_builder.cpp) +endif() +add_executable(testOpenGLContextAttributeBuilder ${testOpenGLContextAttributeBuilder_SRCS}) +target_link_libraries(testOpenGLContextAttributeBuilder Qt5::Test) +add_test(NAME kwin-testOpenGLContextAttributeBuilder COMMAND testOpenGLContextAttributeBuilder) +ecm_mark_as_test(testOpenGLContextAttributeBuilder) + +set(testXkb_SRCS + ../xkb.cpp + test_xkb.cpp +) +add_executable(testXkb ${testXkb_SRCS}) +target_link_libraries(testXkb + Qt5::Gui + Qt5::Test + Qt5::Widgets + + KF5::ConfigCore + Plasma::KWaylandServer + KF5::WindowSystem + + XKB::XKB +) +add_test(NAME kwin-testXkb COMMAND testXkb) +ecm_mark_as_test(testXkb) + +if (HAVE_GBM) + add_executable(testGbmSurface test_gbm_surface.cpp ../plugins/platforms/drm/gbm_surface.cpp) + target_link_libraries(testGbmSurface Qt5::Test) + add_test(NAME kwin-testGbmSurface COMMAND testGbmSurface) + ecm_mark_as_test(testGbmSurface) +endif() + +add_executable(testVirtualKeyboardDBus test_virtualkeyboard_dbus.cpp ../virtualkeyboard_dbus.cpp) +target_link_libraries(testVirtualKeyboardDBus + Qt5::DBus + Qt5::Test +) +add_test(NAME kwin-testVirtualKeyboardDBus COMMAND testVirtualKeyboardDBus) +ecm_mark_as_test(testVirtualKeyboardDBus) diff --git a/autotests/abstract_client.h b/autotests/abstract_client.h new file mode 100644 index 0000000..5582917 --- /dev/null +++ b/autotests/abstract_client.h @@ -0,0 +1 @@ +#include "mock_abstract_client.h" diff --git a/autotests/drm/CMakeLists.txt b/autotests/drm/CMakeLists.txt new file mode 100644 index 0000000..2968762 --- /dev/null +++ b/autotests/drm/CMakeLists.txt @@ -0,0 +1,26 @@ +include_directories(${Libdrm_INCLUDE_DIRS}) + +set(mockDRM_SRCS + mock_drm.cpp + ../../plugins/platforms/drm/drm_buffer.cpp + ../../plugins/platforms/drm/drm_object.cpp + ../../plugins/platforms/drm/drm_object_connector.cpp + ../../plugins/platforms/drm/drm_object_plane.cpp + ../../plugins/platforms/drm/logging.cpp +) + +add_library(mockDrm STATIC ${mockDRM_SRCS}) +target_link_libraries(mockDrm Qt5::Gui) +ecm_mark_as_test(mockDrm) + +function(drmTest) + set(oneValueArgs NAME) + set(multiValueArgs SRCS ) + cmake_parse_arguments(ARGS "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + add_executable(${ARGS_NAME} ${ARGS_SRCS}) + target_link_libraries(${ARGS_NAME} mockDrm Qt5::Test) + add_test(NAME kwin-drm-${ARGS_NAME} COMMAND ${ARGS_NAME}) + ecm_mark_as_test(${ARGS_NAME}) +endfunction() + +drmTest(NAME objecttest SRCS objecttest.cpp) diff --git a/autotests/drm/mock_drm.cpp b/autotests/drm/mock_drm.cpp new file mode 100644 index 0000000..40fef6e --- /dev/null +++ b/autotests/drm/mock_drm.cpp @@ -0,0 +1,67 @@ +/* + 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 "mock_drm.h" + +#include +#include + +static QMap> s_drmProperties{}; + +namespace MockDrm +{ + +void addDrmModeProperties(int fd, const QVector<_drmModeProperty> &properties) +{ + s_drmProperties.insert(fd, properties); +} + +} + +int drmModeAtomicAddProperty(drmModeAtomicReqPtr req, uint32_t object_id, uint32_t property_id, uint64_t value) +{ + Q_UNUSED(req) + Q_UNUSED(object_id) + Q_UNUSED(property_id) + Q_UNUSED(value) + return 0; +} + +drmModePropertyPtr drmModeGetProperty(int fd, uint32_t propertyId) +{ + auto it = s_drmProperties.find(fd); + if (it == s_drmProperties.end()) { + return nullptr; + } + auto it2 = std::find_if(it->constBegin(), it->constEnd(), + [propertyId] (const auto &property) { + return property.prop_id == propertyId; + } + ); + if (it2 == it->constEnd()) { + return nullptr; + } + + auto *property = new _drmModeProperty; + property->prop_id = it2->prop_id; + property->flags = it2->flags; + strcpy(property->name, it2->name); + property->count_values = it2->count_values; + property->values = it2->values; + property->count_enums = it2->count_enums; + property->enums = it2->enums; + property->count_blobs = it2->count_blobs; + property->blob_ids = it2->blob_ids; + + return property; +} + +void drmModeFreeProperty(drmModePropertyPtr ptr) +{ + delete ptr; +} diff --git a/autotests/drm/mock_drm.h b/autotests/drm/mock_drm.h new file mode 100644 index 0000000..88ee4b9 --- /dev/null +++ b/autotests/drm/mock_drm.h @@ -0,0 +1,21 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include +#include +#include + +#include + +namespace MockDrm +{ + +void addDrmModeProperties(int fd, const QVector<_drmModeProperty> &properties); + +} diff --git a/autotests/drm/objecttest.cpp b/autotests/drm/objecttest.cpp new file mode 100644 index 0000000..63000e2 --- /dev/null +++ b/autotests/drm/objecttest.cpp @@ -0,0 +1,208 @@ +/* + 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 "mock_drm.h" +#include "../../plugins/platforms/drm/drm_object.h" +#include + +class MockDrmObject : public KWin::DrmObject +{ +public: + MockDrmObject(uint32_t id, int fd) + : DrmObject(id, fd) + { + } + ~MockDrmObject() override {} + bool atomicInit() override; + bool initProps() override; + + void setProperties(uint32_t count, uint32_t *props, uint64_t *values) { + m_count = count; + m_props = props; + m_values = values; + } + + QByteArray name(int prop) const { + auto property = DrmObject::m_props.at(prop); + if (!property) { + return QByteArray(); + } + return property->name(); + } + + uint32_t propertyId(int prop) const { + auto property = DrmObject::m_props.at(prop); + if (!property) { + return 0xFFFFFFFFu; + } + return property->propId(); + } + +private: + uint32_t m_count = 0; + uint32_t *m_props = nullptr; + uint64_t *m_values = nullptr; +}; + +bool MockDrmObject::atomicInit() +{ + return initProps(); +} + +bool MockDrmObject::initProps() +{ + setPropertyNames({"foo", "bar", "baz"}); + drmModeObjectProperties properties{m_count, m_props, m_values}; + for (int i = 0; i < 3; i++) { + initProp(i, &properties); + } + return false; +} + +class ObjectTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testId_data(); + void testId(); + void testFd_data(); + void testFd(); + void testOutput(); + void testInitProperties(); +}; + +void ObjectTest::testId_data() +{ + QTest::addColumn("id"); + + QTest::newRow("0") << 0u; + QTest::newRow("1") << 1u; + QTest::newRow("10") << 10u; + QTest::newRow("uint max") << 0xFFFFFFFFu; +} + +void ObjectTest::testId() +{ + QFETCH(quint32, id); + MockDrmObject object{id, -1}; + QCOMPARE(object.id(), id); +} + +void ObjectTest::testFd_data() +{ + QTest::addColumn("fd"); + + QTest::newRow("-1") << -1; + QTest::newRow("0") << 0; + QTest::newRow("1") << 1; + QTest::newRow("2") << 2; + QTest::newRow("100") << 100; + QTest::newRow("int max") << 0x7FFFFFFF; +} + +void ObjectTest::testFd() +{ + QFETCH(int, fd); + MockDrmObject object{0, fd}; + QCOMPARE(object.fd(), fd); +} + +namespace KWin +{ +class DrmOutput { +public: + int foo; +}; +} + +void ObjectTest::testOutput() +{ + MockDrmObject object{0, 1}; + + QVERIFY(!object.output()); + + KWin::DrmOutput output{2}; + object.setOutput(&output); + QCOMPARE(object.output(), &output); + QCOMPARE(object.output()->foo, 2); +} + +void ObjectTest::testInitProperties() +{ + MockDrmObject object{0, 20}; + uint32_t propertiesIds[] = { 0, 1, 2, 3}; + uint64_t values[] = { 0, 2, 10, 20 }; + object.setProperties(4, propertiesIds, values); + + MockDrm::addDrmModeProperties(20, QVector<_drmModeProperty>{ + _drmModeProperty{ + 0, + 0, + "foo\0", + 0, + nullptr, + 0, + nullptr, + 0, + nullptr + }, + _drmModeProperty{ + 1, + 0, + "foobar\0", + 0, + nullptr, + 0, + nullptr, + 0, + nullptr + }, + _drmModeProperty{ + 2, + 0, + "baz\0", + 0, + nullptr, + 0, + nullptr, + 0, + nullptr + }, + _drmModeProperty{ + 3, + 0, + "foobarbaz\0", + 0, + nullptr, + 0, + nullptr, + 0, + nullptr + } + }); + + object.atomicInit(); + + // verify the names + QCOMPARE(object.name(0), QByteArrayLiteral("foo")); + QCOMPARE(object.name(1), QByteArray()); + QCOMPARE(object.name(2), QByteArrayLiteral("baz")); + + // verify the property ids + QCOMPARE(object.propertyId(0), 0u); + QCOMPARE(object.propertyId(1), 0xFFFFFFFFu); + QCOMPARE(object.propertyId(2), 2u); + + // doesn't have enums + QCOMPARE(object.propHasEnum(0, 0), false); + QCOMPARE(object.propHasEnum(1, 0), false); + QCOMPARE(object.propHasEnum(2, 0), false); +} + +QTEST_GUILESS_MAIN(ObjectTest) +#include "objecttest.moc" diff --git a/autotests/fakeeffectplugin.cpp b/autotests/fakeeffectplugin.cpp new file mode 100644 index 0000000..c2f1e68 --- /dev/null +++ b/autotests/fakeeffectplugin.cpp @@ -0,0 +1,38 @@ +/* + 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 + +namespace KWin +{ + +class FakeEffect : public Effect +{ + Q_OBJECT +public: + FakeEffect() {} + ~FakeEffect() override {} + + static bool supported() { + return effects->isOpenGLCompositing(); + } + + static bool enabledByDefault() { + return effects->property("testEnabledByDefault").toBool(); + } +}; + +} // namespace + +KWIN_EFFECT_FACTORY_SUPPORTED_ENABLED( FakeEffectPluginFactory, + KWin::FakeEffect, + "fakeeffectplugin.json", + return KWin::FakeEffect::supported();, + return KWin::FakeEffect::enabledByDefault();) + +#include "fakeeffectplugin.moc" diff --git a/autotests/fakeeffectplugin.json b/autotests/fakeeffectplugin.json new file mode 100644 index 0000000..e6727e8 --- /dev/null +++ b/autotests/fakeeffectplugin.json @@ -0,0 +1,13 @@ +{ + "KPlugin": { + "Id": "fakeeffectplugin", + "ServiceTypes": ["KWin/Effect"] + }, + "Type": "Service", + "X-KDE-Library": "fakeeffectplugin", + "X-KDE-PluginInfo-EnabledByDefault": true, + "X-KDE-PluginInfo-Name": "fakeeffectplugin", + "X-KDE-ServiceTypes": [ + "KWin/Effect" + ] +} diff --git a/autotests/fakeeffectplugin_version.cpp b/autotests/fakeeffectplugin_version.cpp new file mode 100644 index 0000000..3a38c6f --- /dev/null +++ b/autotests/fakeeffectplugin_version.cpp @@ -0,0 +1,39 @@ +/* + 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 + +namespace KWin +{ + +class FakeVersionEffect : public Effect +{ + Q_OBJECT +public: + FakeVersionEffect() {} + ~FakeVersionEffect() override {} +}; + +} // namespace + +class FakeEffectPluginFactory : public KWin::EffectPluginFactory +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID KPluginFactory_iid FILE "fakeeffectplugin_version.json") + Q_INTERFACES(KPluginFactory) +public: + FakeEffectPluginFactory() {} + ~FakeEffectPluginFactory() override {} + KWin::Effect *createEffect() const override { + return new KWin::FakeVersionEffect(); + } +}; +K_EXPORT_PLUGIN_VERSION(quint32(KWIN_EFFECT_API_VERSION) - 1) + + +#include "fakeeffectplugin_version.moc" diff --git a/autotests/fakeeffectplugin_version.json b/autotests/fakeeffectplugin_version.json new file mode 100644 index 0000000..ee21839 --- /dev/null +++ b/autotests/fakeeffectplugin_version.json @@ -0,0 +1,13 @@ +{ + "KPlugin": { + "Id": "effectversion", + "ServiceTypes": ["KWin/Effect"] + }, + "Type": "Service", + "X-KDE-Library": "effectversionplugin", + "X-KDE-PluginInfo-EnabledByDefault": true, + "X-KDE-PluginInfo-Name": "effectversion", + "X-KDE-ServiceTypes": [ + "KWin/Effect" + ] +} diff --git a/autotests/integration/CMakeLists.txt b/autotests/integration/CMakeLists.txt new file mode 100644 index 0000000..146ff21 --- /dev/null +++ b/autotests/integration/CMakeLists.txt @@ -0,0 +1,114 @@ +add_subdirectory(helper) + +set(KWinIntegrationTestFramework_SOURCES + ../../cursor.cpp + + generic_scene_opengl_test.cpp + kwin_wayland_test.cpp + test_helpers.cpp + + ${kwin_XWAYLAND_SRCS} +) +ecm_add_qtwayland_client_protocol(KWinIntegrationTestFramework_SOURCES + PROTOCOL ${WaylandProtocols_DATADIR}/unstable/input-method/input-method-unstable-v1.xml + BASENAME input-method-unstable-v1 +) +ecm_add_qtwayland_client_protocol(KWinIntegrationTestFramework_SOURCES + PROTOCOL protocols/wlr-layer-shell-unstable-v1.xml + BASENAME wlr-layer-shell-unstable-v1 +) +ecm_add_qtwayland_client_protocol(KWinIntegrationTestFramework_SOURCES + PROTOCOL ${WaylandProtocols_DATADIR}/stable/xdg-shell/xdg-shell.xml + BASENAME xdg-shell +) +add_library(KWinIntegrationTestFramework STATIC ${KWinIntegrationTestFramework_SOURCES}) +target_link_libraries(KWinIntegrationTestFramework kwin Qt5::Test Wayland::Client) + +function(integrationTest) + set(optionArgs WAYLAND_ONLY) + set(oneValueArgs NAME) + set(multiValueArgs SRCS LIBS) + cmake_parse_arguments(ARGS "${optionArgs}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + add_executable(${ARGS_NAME} ${ARGS_SRCS}) + target_link_libraries(${ARGS_NAME} KWinIntegrationTestFramework kwin Qt5::Test ${ARGS_LIBS}) + add_test(NAME kwin-${ARGS_NAME} COMMAND dbus-run-session ${CMAKE_BINARY_DIR}/bin/${ARGS_NAME}) + if (${ARGS_WAYLAND_ONLY}) + add_executable(${ARGS_NAME}_waylandonly ${ARGS_SRCS} ) + set_target_properties(${ARGS_NAME}_waylandonly PROPERTIES COMPILE_DEFINITIONS "NO_XWAYLAND") + target_link_libraries(${ARGS_NAME}_waylandonly KWinIntegrationTestFramework kwin Qt5::Test ${ARGS_LIBS}) + add_test(NAME kwin-${ARGS_NAME}-waylandonly COMMAND dbus-run-session ${CMAKE_BINARY_DIR}/bin/${ARGS_NAME}_waylandonly) + endif() +endfunction() + +integrationTest(NAME testDontCrashGlxgears SRCS dont_crash_glxgears.cpp) +integrationTest(NAME testLockScreen SRCS lockscreen.cpp) +integrationTest(WAYLAND_ONLY NAME testDecorationInput SRCS decoration_input_test.cpp) +integrationTest(WAYLAND_ONLY NAME testInternalWindow SRCS internal_window.cpp) +integrationTest(WAYLAND_ONLY NAME testTouchInput SRCS touch_input_test.cpp) +integrationTest(WAYLAND_ONLY NAME testInputStackingOrder SRCS input_stacking_order.cpp) +integrationTest(NAME testPointerInput SRCS pointer_input.cpp) +integrationTest(NAME testPlatformCursor SRCS platformcursor.cpp) +integrationTest(WAYLAND_ONLY NAME testDontCrashCancelAnimation SRCS dont_crash_cancel_animation.cpp) +integrationTest(WAYLAND_ONLY NAME testTransientPlacement SRCS transient_placement.cpp) +integrationTest(NAME testDebugConsole SRCS debug_console_test.cpp) +integrationTest(NAME testDontCrashEmptyDeco SRCS dont_crash_empty_deco.cpp) +integrationTest(WAYLAND_ONLY NAME testPlasmaSurface SRCS plasma_surface_test.cpp) +integrationTest(WAYLAND_ONLY NAME testMaximized SRCS maximize_test.cpp) +integrationTest(WAYLAND_ONLY NAME testXdgShellClient SRCS xdgshellclient_test.cpp) +integrationTest(WAYLAND_ONLY NAME testDontCrashNoBorder SRCS dont_crash_no_border.cpp) +integrationTest(NAME testXwaylandSelections SRCS xwayland_selections_test.cpp) +integrationTest(WAYLAND_ONLY NAME testSceneOpenGL SRCS scene_opengl_test.cpp ) +integrationTest(WAYLAND_ONLY NAME testSceneOpenGLShadow SRCS scene_opengl_shadow_test.cpp) +integrationTest(WAYLAND_ONLY NAME testSceneOpenGLES SRCS scene_opengl_es_test.cpp ) +integrationTest(WAYLAND_ONLY NAME testNoXdgRuntimeDir SRCS no_xdg_runtime_dir_test.cpp) +integrationTest(WAYLAND_ONLY NAME testScreenChanges SRCS screen_changes_test.cpp) +integrationTest(NAME testModiferOnlyShortcut SRCS modifier_only_shortcut_test.cpp) +integrationTest(WAYLAND_ONLY NAME testTabBox SRCS tabbox_test.cpp) +integrationTest(WAYLAND_ONLY NAME testWindowSelection SRCS window_selection_test.cpp) +integrationTest(WAYLAND_ONLY NAME testPointerConstraints SRCS pointer_constraints_test.cpp) +integrationTest(WAYLAND_ONLY NAME testKeyboardLayout SRCS keyboard_layout_test.cpp) +integrationTest(WAYLAND_ONLY NAME testKeymapCreationFailure SRCS keymap_creation_failure_test.cpp) +integrationTest(WAYLAND_ONLY NAME testShowingDesktop SRCS showing_desktop_test.cpp) +integrationTest(WAYLAND_ONLY NAME testDontCrashUseractionsMenu SRCS dont_crash_useractions_menu.cpp) +integrationTest(WAYLAND_ONLY NAME testKWinBindings SRCS kwinbindings_test.cpp) +integrationTest(WAYLAND_ONLY NAME testLayerShellV1Client SRCS layershellv1client_test.cpp) +integrationTest(WAYLAND_ONLY NAME testVirtualDesktop SRCS virtual_desktop_test.cpp) +integrationTest(WAYLAND_ONLY NAME testXdgShellClientRules SRCS xdgshellclient_rules_test.cpp) +integrationTest(WAYLAND_ONLY NAME testIdleInhibition SRCS idle_inhibition_test.cpp) +integrationTest(WAYLAND_ONLY NAME testColorCorrectNightColor SRCS colorcorrect_nightcolor_test.cpp) +integrationTest(WAYLAND_ONLY NAME testDontCrashCursorPhysicalSizeEmpty SRCS dont_crash_cursor_physical_size_empty.cpp) +integrationTest(WAYLAND_ONLY NAME testDontCrashReinitializeCompositor SRCS dont_crash_reinitialize_compositor.cpp) +integrationTest(WAYLAND_ONLY NAME testNoGlobalShortcuts SRCS no_global_shortcuts_test.cpp) +integrationTest(WAYLAND_ONLY NAME testBufferSizeChange SRCS buffer_size_change_test.cpp ) +integrationTest(WAYLAND_ONLY NAME testPlacement SRCS placement_test.cpp) +integrationTest(WAYLAND_ONLY NAME testActivation SRCS activation_test.cpp) +integrationTest(WAYLAND_ONLY NAME testVirtualKeyboard SRCS virtualkeyboard_test.cpp) + +if (XCB_ICCCM_FOUND) + integrationTest(NAME testMoveResize SRCS move_resize_window_test.cpp LIBS XCB::ICCCM) + integrationTest(NAME testStruts SRCS struts_test.cpp LIBS XCB::ICCCM) + integrationTest(NAME testShade SRCS shade_test.cpp LIBS XCB::ICCCM) + integrationTest(NAME testDontCrashAuroraeDestroyDeco SRCS dont_crash_aurorae_destroy_deco.cpp LIBS XCB::ICCCM) + integrationTest(NAME testPlasmaWindow SRCS plasmawindow_test.cpp LIBS XCB::ICCCM) + integrationTest(NAME testScreenEdgeClientShow SRCS screenedge_client_show_test.cpp LIBS XCB::ICCCM) + integrationTest(NAME testX11DesktopWindow SRCS desktop_window_x11_test.cpp LIBS XCB::ICCCM) + integrationTest(NAME testXwaylandInput SRCS xwayland_input_test.cpp LIBS XCB::ICCCM) + integrationTest(NAME testWindowRules SRCS window_rules_test.cpp LIBS XCB::ICCCM) + integrationTest(NAME testX11Client SRCS x11_client_test.cpp LIBS XCB::ICCCM) + integrationTest(NAME testQuickTiling SRCS quick_tiling_test.cpp LIBS XCB::ICCCM) + integrationTest(NAME testGlobalShortcuts SRCS globalshortcuts_test.cpp LIBS XCB::ICCCM) + integrationTest(NAME testSceneQPainter SRCS scene_qpainter_test.cpp LIBS XCB::ICCCM) + integrationTest(NAME testSceneQPainterShadow SRCS scene_qpainter_shadow_test.cpp LIBS XCB::ICCCM) + integrationTest(NAME testStackingOrder SRCS stacking_order_test.cpp LIBS XCB::ICCCM) + integrationTest(NAME testDbusInterface SRCS dbus_interface_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) + + if (KWIN_BUILD_ACTIVITIES) + integrationTest(NAME testActivities SRCS activities_test.cpp LIBS XCB::ICCCM) + endif() +endif() + +add_subdirectory(scripting) +add_subdirectory(effects) +add_subdirectory(fakes) diff --git a/autotests/integration/activation_test.cpp b/autotests/integration/activation_test.cpp new file mode 100644 index 0000000..6d2da4d --- /dev/null +++ b/autotests/integration/activation_test.cpp @@ -0,0 +1,573 @@ +/* + 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 "abstract_client.h" +#include "cursor.h" +#include "platform.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" + +#include + +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(); + +private: + void stackScreensHorizontally(); + void stackScreensVertically(); +}; + +void ActivationTest::initTestCase() +{ + qRegisterMetaType(); + + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + waylandServer()->initWorkspace(); +} + +void ActivationTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); + + screens()->setCurrent(0); + Cursors::self()->mouse()->setPos(QPoint(640, 512)); +} + +void ActivationTest::cleanup() +{ + Test::destroyWaylandConnection(); + + stackScreensHorizontally(); +} + +void ActivationTest::testSwitchToWindowToLeft() +{ + // This test verifies that "Switch to Window to the Left" shortcut works. + + using namespace KWayland::Client; + + // Prepare the test environment. + stackScreensHorizontally(); + + // Create several clients on the left screen. + QScopedPointer surface1(Test::createSurface()); + QScopedPointer shellSurface1(Test::createXdgShellStableSurface(surface1.data())); + AbstractClient *client1 = Test::renderAndWaitForShown(surface1.data(), QSize(100, 50), Qt::blue); + QVERIFY(client1); + QVERIFY(client1->isActive()); + + QScopedPointer surface2(Test::createSurface()); + QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data())); + AbstractClient *client2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 50), Qt::blue); + QVERIFY(client2); + QVERIFY(client2->isActive()); + + client1->move(QPoint(300, 200)); + client2->move(QPoint(500, 200)); + + // Create several clients on the right screen. + QScopedPointer surface3(Test::createSurface()); + QScopedPointer shellSurface3(Test::createXdgShellStableSurface(surface3.data())); + AbstractClient *client3 = Test::renderAndWaitForShown(surface3.data(), QSize(100, 50), Qt::blue); + QVERIFY(client3); + QVERIFY(client3->isActive()); + + QScopedPointer surface4(Test::createSurface()); + QScopedPointer shellSurface4(Test::createXdgShellStableSurface(surface4.data())); + AbstractClient *client4 = Test::renderAndWaitForShown(surface4.data(), QSize(100, 50), Qt::blue); + QVERIFY(client4); + QVERIFY(client4->isActive()); + + client3->move(QPoint(1380, 200)); + client4->move(QPoint(1580, 200)); + + // Switch to window to the left. + workspace()->switchWindow(Workspace::DirectionWest); + QVERIFY(client3->isActive()); + + // Switch to window to the left. + workspace()->switchWindow(Workspace::DirectionWest); + QVERIFY(client2->isActive()); + + // Switch to window to the left. + workspace()->switchWindow(Workspace::DirectionWest); + QVERIFY(client1->isActive()); + + // Switch to window to the left. + workspace()->switchWindow(Workspace::DirectionWest); + QVERIFY(client4->isActive()); + + // Destroy all clients. + shellSurface1.reset(); + QVERIFY(Test::waitForWindowDestroyed(client1)); + shellSurface2.reset(); + QVERIFY(Test::waitForWindowDestroyed(client2)); + shellSurface3.reset(); + QVERIFY(Test::waitForWindowDestroyed(client3)); + shellSurface4.reset(); + QVERIFY(Test::waitForWindowDestroyed(client4)); +} + +void ActivationTest::testSwitchToWindowToRight() +{ + // This test verifies that "Switch to Window to the Right" shortcut works. + + using namespace KWayland::Client; + + // Prepare the test environment. + stackScreensHorizontally(); + + // Create several clients on the left screen. + QScopedPointer surface1(Test::createSurface()); + QScopedPointer shellSurface1(Test::createXdgShellStableSurface(surface1.data())); + AbstractClient *client1 = Test::renderAndWaitForShown(surface1.data(), QSize(100, 50), Qt::blue); + QVERIFY(client1); + QVERIFY(client1->isActive()); + + QScopedPointer surface2(Test::createSurface()); + QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data())); + AbstractClient *client2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 50), Qt::blue); + QVERIFY(client2); + QVERIFY(client2->isActive()); + + client1->move(QPoint(300, 200)); + client2->move(QPoint(500, 200)); + + // Create several clients on the right screen. + QScopedPointer surface3(Test::createSurface()); + QScopedPointer shellSurface3(Test::createXdgShellStableSurface(surface3.data())); + AbstractClient *client3 = Test::renderAndWaitForShown(surface3.data(), QSize(100, 50), Qt::blue); + QVERIFY(client3); + QVERIFY(client3->isActive()); + + QScopedPointer surface4(Test::createSurface()); + QScopedPointer shellSurface4(Test::createXdgShellStableSurface(surface4.data())); + AbstractClient *client4 = Test::renderAndWaitForShown(surface4.data(), QSize(100, 50), Qt::blue); + QVERIFY(client4); + QVERIFY(client4->isActive()); + + client3->move(QPoint(1380, 200)); + client4->move(QPoint(1580, 200)); + + // Switch to window to the right. + workspace()->switchWindow(Workspace::DirectionEast); + QVERIFY(client1->isActive()); + + // Switch to window to the right. + workspace()->switchWindow(Workspace::DirectionEast); + QVERIFY(client2->isActive()); + + // Switch to window to the right. + workspace()->switchWindow(Workspace::DirectionEast); + QVERIFY(client3->isActive()); + + // Switch to window to the right. + workspace()->switchWindow(Workspace::DirectionEast); + QVERIFY(client4->isActive()); + + // Destroy all clients. + shellSurface1.reset(); + QVERIFY(Test::waitForWindowDestroyed(client1)); + shellSurface2.reset(); + QVERIFY(Test::waitForWindowDestroyed(client2)); + shellSurface3.reset(); + QVERIFY(Test::waitForWindowDestroyed(client3)); + shellSurface4.reset(); + QVERIFY(Test::waitForWindowDestroyed(client4)); +} + +void ActivationTest::testSwitchToWindowAbove() +{ + // This test verifies that "Switch to Window Above" shortcut works. + + using namespace KWayland::Client; + + // Prepare the test environment. + stackScreensVertically(); + + // Create several clients on the top screen. + QScopedPointer surface1(Test::createSurface()); + QScopedPointer shellSurface1(Test::createXdgShellStableSurface(surface1.data())); + AbstractClient *client1 = Test::renderAndWaitForShown(surface1.data(), QSize(100, 50), Qt::blue); + QVERIFY(client1); + QVERIFY(client1->isActive()); + + QScopedPointer surface2(Test::createSurface()); + QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data())); + AbstractClient *client2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 50), Qt::blue); + QVERIFY(client2); + QVERIFY(client2->isActive()); + + client1->move(QPoint(200, 300)); + client2->move(QPoint(200, 500)); + + // Create several clients on the bottom screen. + QScopedPointer surface3(Test::createSurface()); + QScopedPointer shellSurface3(Test::createXdgShellStableSurface(surface3.data())); + AbstractClient *client3 = Test::renderAndWaitForShown(surface3.data(), QSize(100, 50), Qt::blue); + QVERIFY(client3); + QVERIFY(client3->isActive()); + + QScopedPointer surface4(Test::createSurface()); + QScopedPointer shellSurface4(Test::createXdgShellStableSurface(surface4.data())); + AbstractClient *client4 = Test::renderAndWaitForShown(surface4.data(), QSize(100, 50), Qt::blue); + QVERIFY(client4); + QVERIFY(client4->isActive()); + + client3->move(QPoint(200, 1224)); + client4->move(QPoint(200, 1424)); + + // Switch to window above. + workspace()->switchWindow(Workspace::DirectionNorth); + QVERIFY(client3->isActive()); + + // Switch to window above. + workspace()->switchWindow(Workspace::DirectionNorth); + QVERIFY(client2->isActive()); + + // Switch to window above. + workspace()->switchWindow(Workspace::DirectionNorth); + QVERIFY(client1->isActive()); + + // Switch to window above. + workspace()->switchWindow(Workspace::DirectionNorth); + QVERIFY(client4->isActive()); + + // Destroy all clients. + shellSurface1.reset(); + QVERIFY(Test::waitForWindowDestroyed(client1)); + shellSurface2.reset(); + QVERIFY(Test::waitForWindowDestroyed(client2)); + shellSurface3.reset(); + QVERIFY(Test::waitForWindowDestroyed(client3)); + shellSurface4.reset(); + QVERIFY(Test::waitForWindowDestroyed(client4)); +} + +void ActivationTest::testSwitchToWindowBelow() +{ + // This test verifies that "Switch to Window Bottom" shortcut works. + + using namespace KWayland::Client; + + // Prepare the test environment. + stackScreensVertically(); + + // Create several clients on the top screen. + QScopedPointer surface1(Test::createSurface()); + QScopedPointer shellSurface1(Test::createXdgShellStableSurface(surface1.data())); + AbstractClient *client1 = Test::renderAndWaitForShown(surface1.data(), QSize(100, 50), Qt::blue); + QVERIFY(client1); + QVERIFY(client1->isActive()); + + QScopedPointer surface2(Test::createSurface()); + QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data())); + AbstractClient *client2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 50), Qt::blue); + QVERIFY(client2); + QVERIFY(client2->isActive()); + + client1->move(QPoint(200, 300)); + client2->move(QPoint(200, 500)); + + // Create several clients on the bottom screen. + QScopedPointer surface3(Test::createSurface()); + QScopedPointer shellSurface3(Test::createXdgShellStableSurface(surface3.data())); + AbstractClient *client3 = Test::renderAndWaitForShown(surface3.data(), QSize(100, 50), Qt::blue); + QVERIFY(client3); + QVERIFY(client3->isActive()); + + QScopedPointer surface4(Test::createSurface()); + QScopedPointer shellSurface4(Test::createXdgShellStableSurface(surface4.data())); + AbstractClient *client4 = Test::renderAndWaitForShown(surface4.data(), QSize(100, 50), Qt::blue); + QVERIFY(client4); + QVERIFY(client4->isActive()); + + client3->move(QPoint(200, 1224)); + client4->move(QPoint(200, 1424)); + + // Switch to window below. + workspace()->switchWindow(Workspace::DirectionSouth); + QVERIFY(client1->isActive()); + + // Switch to window below. + workspace()->switchWindow(Workspace::DirectionSouth); + QVERIFY(client2->isActive()); + + // Switch to window below. + workspace()->switchWindow(Workspace::DirectionSouth); + QVERIFY(client3->isActive()); + + // Switch to window below. + workspace()->switchWindow(Workspace::DirectionSouth); + QVERIFY(client4->isActive()); + + // Destroy all clients. + shellSurface1.reset(); + QVERIFY(Test::waitForWindowDestroyed(client1)); + shellSurface2.reset(); + QVERIFY(Test::waitForWindowDestroyed(client2)); + shellSurface3.reset(); + QVERIFY(Test::waitForWindowDestroyed(client3)); + shellSurface4.reset(); + QVERIFY(Test::waitForWindowDestroyed(client4)); +} + +void ActivationTest::testSwitchToWindowMaximized() +{ + // This test verifies that we switch to the top-most maximized client, i.e. + // the one that user sees at the moment. See bug 411356. + + using namespace KWayland::Client; + + // Prepare the test environment. + stackScreensHorizontally(); + + // Create several maximized clients on the left screen. + QScopedPointer surface1(Test::createSurface()); + QScopedPointer shellSurface1(Test::createXdgShellStableSurface(surface1.data())); + AbstractClient *client1 = Test::renderAndWaitForShown(surface1.data(), QSize(100, 50), Qt::blue); + QVERIFY(client1); + QVERIFY(client1->isActive()); + QSignalSpy configureRequestedSpy1(shellSurface1.data(), &XdgShellSurface::configureRequested); + QVERIFY(configureRequestedSpy1.wait()); + workspace()->slotWindowMaximize(); + QVERIFY(configureRequestedSpy1.wait()); + QSignalSpy frameGeometryChangedSpy1(client1, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy1.isValid()); + shellSurface1->ackConfigure(configureRequestedSpy1.last().at(2).value()); + Test::render(surface1.data(), configureRequestedSpy1.last().at(0).toSize(), Qt::red); + QVERIFY(frameGeometryChangedSpy1.wait()); + + QScopedPointer surface2(Test::createSurface()); + QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data())); + AbstractClient *client2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 50), Qt::blue); + QVERIFY(client2); + QVERIFY(client2->isActive()); + QSignalSpy configureRequestedSpy2(shellSurface2.data(), &XdgShellSurface::configureRequested); + QVERIFY(configureRequestedSpy2.wait()); + workspace()->slotWindowMaximize(); + QVERIFY(configureRequestedSpy2.wait()); + QSignalSpy frameGeometryChangedSpy2(client2, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy2.isValid()); + shellSurface2->ackConfigure(configureRequestedSpy2.last().at(2).value()); + Test::render(surface2.data(), configureRequestedSpy2.last().at(0).toSize(), Qt::red); + QVERIFY(frameGeometryChangedSpy2.wait()); + + const QList stackingOrder = workspace()->stackingOrder(); + QVERIFY(stackingOrder.indexOf(client1) < stackingOrder.indexOf(client2)); + QCOMPARE(client1->maximizeMode(), MaximizeFull); + QCOMPARE(client2->maximizeMode(), MaximizeFull); + + // Create several clients on the right screen. + QScopedPointer surface3(Test::createSurface()); + QScopedPointer shellSurface3(Test::createXdgShellStableSurface(surface3.data())); + AbstractClient *client3 = Test::renderAndWaitForShown(surface3.data(), QSize(100, 50), Qt::blue); + QVERIFY(client3); + QVERIFY(client3->isActive()); + + QScopedPointer surface4(Test::createSurface()); + QScopedPointer shellSurface4(Test::createXdgShellStableSurface(surface4.data())); + AbstractClient *client4 = Test::renderAndWaitForShown(surface4.data(), QSize(100, 50), Qt::blue); + QVERIFY(client4); + QVERIFY(client4->isActive()); + + client3->move(QPoint(1380, 200)); + client4->move(QPoint(1580, 200)); + + // Switch to window to the left. + workspace()->switchWindow(Workspace::DirectionWest); + QVERIFY(client3->isActive()); + + // Switch to window to the left. + workspace()->switchWindow(Workspace::DirectionWest); + QVERIFY(client2->isActive()); + + // Switch to window to the left. + workspace()->switchWindow(Workspace::DirectionWest); + QVERIFY(client4->isActive()); + + // Destroy all clients. + shellSurface1.reset(); + QVERIFY(Test::waitForWindowDestroyed(client1)); + shellSurface2.reset(); + QVERIFY(Test::waitForWindowDestroyed(client2)); + shellSurface3.reset(); + QVERIFY(Test::waitForWindowDestroyed(client3)); + shellSurface4.reset(); + QVERIFY(Test::waitForWindowDestroyed(client4)); +} + +void ActivationTest::testSwitchToWindowFullScreen() +{ + // This test verifies that we switch to the top-most fullscreen client, i.e. + // the one that user sees at the moment. See bug 411356. + + using namespace KWayland::Client; + + // Prepare the test environment. + stackScreensVertically(); + + // Create several maximized clients on the top screen. + QScopedPointer surface1(Test::createSurface()); + QScopedPointer shellSurface1(Test::createXdgShellStableSurface(surface1.data())); + AbstractClient *client1 = Test::renderAndWaitForShown(surface1.data(), QSize(100, 50), Qt::blue); + QVERIFY(client1); + QVERIFY(client1->isActive()); + QSignalSpy configureRequestedSpy1(shellSurface1.data(), &XdgShellSurface::configureRequested); + QVERIFY(configureRequestedSpy1.wait()); + workspace()->slotWindowFullScreen(); + QVERIFY(configureRequestedSpy1.wait()); + QSignalSpy frameGeometryChangedSpy1(client1, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy1.isValid()); + shellSurface1->ackConfigure(configureRequestedSpy1.last().at(2).value()); + Test::render(surface1.data(), configureRequestedSpy1.last().at(0).toSize(), Qt::red); + QVERIFY(frameGeometryChangedSpy1.wait()); + + QScopedPointer surface2(Test::createSurface()); + QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data())); + AbstractClient *client2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 50), Qt::blue); + QVERIFY(client2); + QVERIFY(client2->isActive()); + QSignalSpy configureRequestedSpy2(shellSurface2.data(), &XdgShellSurface::configureRequested); + QVERIFY(configureRequestedSpy2.wait()); + workspace()->slotWindowFullScreen(); + QVERIFY(configureRequestedSpy2.wait()); + QSignalSpy frameGeometryChangedSpy2(client2, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy2.isValid()); + shellSurface2->ackConfigure(configureRequestedSpy2.last().at(2).value()); + Test::render(surface2.data(), configureRequestedSpy2.last().at(0).toSize(), Qt::red); + QVERIFY(frameGeometryChangedSpy2.wait()); + + const QList stackingOrder = workspace()->stackingOrder(); + QVERIFY(stackingOrder.indexOf(client1) < stackingOrder.indexOf(client2)); + QVERIFY(client1->isFullScreen()); + QVERIFY(client2->isFullScreen()); + + // Create several clients on the bottom screen. + QScopedPointer surface3(Test::createSurface()); + QScopedPointer shellSurface3(Test::createXdgShellStableSurface(surface3.data())); + AbstractClient *client3 = Test::renderAndWaitForShown(surface3.data(), QSize(100, 50), Qt::blue); + QVERIFY(client3); + QVERIFY(client3->isActive()); + + QScopedPointer surface4(Test::createSurface()); + QScopedPointer shellSurface4(Test::createXdgShellStableSurface(surface4.data())); + AbstractClient *client4 = Test::renderAndWaitForShown(surface4.data(), QSize(100, 50), Qt::blue); + QVERIFY(client4); + QVERIFY(client4->isActive()); + + client3->move(QPoint(200, 1224)); + client4->move(QPoint(200, 1424)); + + // Switch to window above. + workspace()->switchWindow(Workspace::DirectionNorth); + QVERIFY(client3->isActive()); + + // Switch to window above. + workspace()->switchWindow(Workspace::DirectionNorth); + QVERIFY(client2->isActive()); + + // Switch to window above. + workspace()->switchWindow(Workspace::DirectionNorth); + QVERIFY(client4->isActive()); + + // Destroy all clients. + shellSurface1.reset(); + QVERIFY(Test::waitForWindowDestroyed(client1)); + shellSurface2.reset(); + QVERIFY(Test::waitForWindowDestroyed(client2)); + shellSurface3.reset(); + QVERIFY(Test::waitForWindowDestroyed(client3)); + shellSurface4.reset(); + QVERIFY(Test::waitForWindowDestroyed(client4)); +} + +void ActivationTest::stackScreensHorizontally() +{ + // Process pending wl_output bind requests before destroying all outputs. + QTest::qWait(1); + + const QVector screenGeometries { + QRect(0, 0, 1280, 1024), + QRect(1280, 0, 1280, 1024), + }; + + const QVector screenScales { + 1, + 1, + }; + + QMetaObject::invokeMethod(kwinApp()->platform(), + "setVirtualOutputs", + Qt::DirectConnection, + Q_ARG(int, screenGeometries.count()), + Q_ARG(QVector, screenGeometries), + Q_ARG(QVector, screenScales) + ); +} + +void ActivationTest::stackScreensVertically() +{ + // Process pending wl_output bind requests before destroying all outputs. + QTest::qWait(1); + + const QVector screenGeometries { + QRect(0, 0, 1280, 1024), + QRect(0, 1024, 1280, 1024), + }; + + const QVector screenScales { + 1, + 1, + }; + + QMetaObject::invokeMethod(kwinApp()->platform(), + "setVirtualOutputs", + Qt::DirectConnection, + Q_ARG(int, screenGeometries.count()), + Q_ARG(QVector, screenGeometries), + Q_ARG(QVector, screenScales) + ); +} + +} + +WAYLANDTEST_MAIN(KWin::ActivationTest) +#include "activation_test.moc" diff --git a/autotests/integration/activities_test.cpp b/autotests/integration/activities_test.cpp new file mode 100644 index 0000000..4c0a6e6 --- /dev/null +++ b/autotests/integration/activities_test.cpp @@ -0,0 +1,151 @@ +/* + 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 "platform.h" +#include "activities.h" +#include "x11client.h" +#include "cursor.h" +#include "deleted.h" +#include "screenedge.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include "xcbutils.h" +#include + +#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(); + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + kwinApp()->setUseKActivities(true); + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + setenv("QT_QPA_PLATFORM", "wayland", true); + waylandServer()->initWorkspace(); +} + +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() +{ + screens()->setCurrent(0); + Cursors::self()->mouse()->setPos(QPoint(640, 512)); +} + +void ActivitiesTest::cleanup() +{ +} + +struct XcbConnectionDeleter +{ + static inline void cleanup(xcb_connection_t *pointer) + { + xcb_disconnect(pointer); + } +}; + +void ActivitiesTest::testSetOnActivitiesValidates() +{ + // this test verifies that windows can't be placed on activities that don't exist + // create an xcb window + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + + xcb_window_t w = xcb_generate_id(c.data()); + const QRect windowGeometry(0, 0, 100, 200); + + auto cookie = xcb_create_window_checked(c.data(), 0, w, rootWindow(), + windowGeometry.x(), + windowGeometry.y(), + windowGeometry.width(), + windowGeometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, 0, 0, nullptr); + QVERIFY(!xcb_request_check(c.data(), cookie)); + xcb_size_hints_t hints; + memset(&hints, 0, sizeof(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.data(), w, &hints); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + // we should get a client for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->window(), w); + QVERIFY(client->isDecorated()); + + //verify the test machine doesn't have the following activities used + QVERIFY(!Activities::self()->all().contains(QStringLiteral("foo"))); + QVERIFY(!Activities::self()->all().contains(QStringLiteral("bar"))); + + client->setOnActivities(QStringList{QStringLiteral("foo"), QStringLiteral("bar")}); + QVERIFY(!client->activities().contains(QLatin1String("foo"))); + QVERIFY(!client->activities().contains(QLatin1String("bar"))); + + // and destroy the window again + xcb_unmap_window(c.data(), w); + xcb_destroy_window(c.data(), w); + xcb_flush(c.data()); + c.reset(); + + QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + QVERIFY(windowClosedSpy.wait()); +} + +} + +WAYLANDTEST_MAIN(KWin::ActivitiesTest) +#include "activities_test.moc" diff --git a/autotests/integration/buffer_size_change_test.cpp b/autotests/integration/buffer_size_change_test.cpp new file mode 100644 index 0000000..66b48e1 --- /dev/null +++ b/autotests/integration/buffer_size_change_test.cpp @@ -0,0 +1,115 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "generic_scene_opengl_test.h" + +#include "abstract_client.h" +#include "composite.h" +#include "wayland_server.h" + +#include +#include +#include + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_buffer_size_change-0"); + +class BufferSizeChangeTest : public GenericSceneOpenGLTest +{ + Q_OBJECT +public: + BufferSizeChangeTest() : GenericSceneOpenGLTest(QByteArrayLiteral("O2")) {} +private Q_SLOTS: + void init(); + void testShmBufferSizeChange(); + void testShmBufferSizeChangeOnSubSurface(); +}; + +void BufferSizeChangeTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); +} + +void BufferSizeChangeTest::testShmBufferSizeChange() +{ + // This test verifies that an SHM buffer size change is handled correctly + + using namespace KWayland::Client; + + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + + // set buffer size + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + + // add a first repaint + QSignalSpy swapSpy(Compositor::self(), &Compositor::bufferSwapCompleted); + QVERIFY(swapSpy.isValid()); + Compositor::self()->addRepaintFull(); + QVERIFY(swapSpy.wait()); + + // now change buffer size + Test::render(surface.data(), QSize(30, 10), Qt::red); + + QSignalSpy damagedSpy(client, &AbstractClient::damaged); + QVERIFY(damagedSpy.isValid()); + QVERIFY(damagedSpy.wait()); + KWin::Compositor::self()->addRepaintFull(); + QVERIFY(swapSpy.wait()); +} + +void BufferSizeChangeTest::testShmBufferSizeChangeOnSubSurface() +{ + using namespace KWayland::Client; + + // setup parent surface + QScopedPointer parentSurface(Test::createSurface()); + QVERIFY(!parentSurface.isNull()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(parentSurface.data())); + QVERIFY(!shellSurface.isNull()); + + // setup sub surface + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + QScopedPointer subSurface(Test::createSubSurface(surface.data(), parentSurface.data())); + QVERIFY(!subSurface.isNull()); + + // set buffer sizes + Test::render(surface.data(), QSize(30, 10), Qt::red); + AbstractClient *parent = Test::renderAndWaitForShown(parentSurface.data(), QSize(100, 50), Qt::blue); + QVERIFY(parent); + + // add a first repaint + QSignalSpy swapSpy(Compositor::self(), &Compositor::bufferSwapCompleted); + QVERIFY(swapSpy.isValid()); + Compositor::self()->addRepaintFull(); + QVERIFY(swapSpy.wait()); + + // change buffer size of sub surface + QSignalSpy damagedParentSpy(parent, &AbstractClient::damaged); + QVERIFY(damagedParentSpy.isValid()); + Test::render(surface.data(), QSize(20, 10), Qt::red); + parentSurface->commit(Surface::CommitFlag::None); + + QVERIFY(damagedParentSpy.wait()); + + // add a second repaint + KWin::Compositor::self()->addRepaintFull(); + QVERIFY(swapSpy.wait()); +} + +} + +WAYLANDTEST_MAIN(KWin::BufferSizeChangeTest) +#include "buffer_size_change_test.moc" diff --git a/autotests/integration/colorcorrect_nightcolor_test.cpp b/autotests/integration/colorcorrect_nightcolor_test.cpp new file mode 100644 index 0000000..d5920ca --- /dev/null +++ b/autotests/integration/colorcorrect_nightcolor_test.cpp @@ -0,0 +1,328 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "platform.h" +#include "wayland_server.h" +#include "colorcorrection/manager.h" +#include "colorcorrection/constants.h" + +#include + +using namespace KWin; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_colorcorrect_nightcolor-0"); + +class ColorCorrectNightColorTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testConfigRead_data(); + void testConfigRead(); + void testChangeConfiguration_data(); + void testChangeConfiguration(); + void testAutoLocationUpdate(); +}; + +void ColorCorrectNightColorTest::initTestCase() +{ + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + waylandServer()->initWorkspace(); +} + +void ColorCorrectNightColorTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); +} + +void ColorCorrectNightColorTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void ColorCorrectNightColorTest::testConfigRead_data() +{ + QTest::addColumn("active"); + QTest::addColumn("mode"); + QTest::addColumn("nightTemperature"); + QTest::addColumn("latitudeFixed"); + QTest::addColumn("longitudeFixed"); + QTest::addColumn("morningBeginFixed"); + QTest::addColumn("eveningBeginFixed"); + QTest::addColumn("transitionTime"); + QTest::addColumn("success"); + + QTest::newRow("activeMode0") << "true" << 0 << 4500 << 45.5 << 35.1 << "0600" << "1800" << 30 << true; + QTest::newRow("activeMode1") << "true" << 1 << 2500 << -10.5 << -8. << "0020" << "2000" << 60 << true; + QTest::newRow("activeMode2") << "true" << 3 << 3500 << 45.5 << 35.1 << "0600" << "1800" << 60 << true; + QTest::newRow("notActiveMode2") << "false" << 2 << 5000 << 90. << -180. << "0600" << "1800" << 1 << true; + QTest::newRow("wrongData1") << "fa" << 4 << 7000 << 91. << -181. << "060" << "800" << 999999 << false; + QTest::newRow("wrongData2") << "fa" << 4 << 7000 << 91. << -181. << "060" << "800" << -2 << false; +} + +void ColorCorrectNightColorTest::testConfigRead() +{ + QFETCH(QString, active); + QFETCH(int, mode); + QFETCH(int, nightTemperature); + QFETCH(double, latitudeFixed); + QFETCH(double, longitudeFixed); + QFETCH(QString, morningBeginFixed); + QFETCH(QString, eveningBeginFixed); + QFETCH(int, transitionTime); + QFETCH(bool, success); + + const bool activeDefault = true; + const int modeDefault = 0; + const int nightTemperatureUpperEnd = ColorCorrect::NEUTRAL_TEMPERATURE; + const double latitudeFixedDefault = 0; + const double longitudeFixedDefault = 0; + const QTime morningBeginFixedDefault = QTime(6,0,0); + const QTime eveningBeginFixedDefault = QTime(18,0,0); + const int transitionTimeDefault = 30; + + KConfigGroup cfgGroup = kwinApp()->config()->group("NightColor"); + + cfgGroup.writeEntry("Active", activeDefault); + cfgGroup.writeEntry("Mode", modeDefault); + cfgGroup.writeEntry("NightTemperature", nightTemperatureUpperEnd); + cfgGroup.writeEntry("LatitudeFixed", latitudeFixedDefault); + cfgGroup.writeEntry("LongitudeFixed", longitudeFixedDefault); + cfgGroup.writeEntry("MorningBeginFixed", morningBeginFixedDefault.toString("hhmm")); + cfgGroup.writeEntry("EveningBeginFixed", eveningBeginFixedDefault.toString("hhmm")); + cfgGroup.writeEntry("TransitionTime", transitionTimeDefault); + + ColorCorrect::Manager *manager = kwinApp()->platform()->colorCorrectManager(); + manager->reparseConfigAndReset(); + auto info = manager->info(); + QVERIFY(!info.isEmpty()); + + QCOMPARE(info.value("Active").toBool(), activeDefault); + QCOMPARE(info.value("Mode").toInt(), modeDefault); + QCOMPARE(info.value("NightTemperature").toInt(), nightTemperatureUpperEnd); + QCOMPARE(info.value("LatitudeFixed").toDouble(), latitudeFixedDefault); + QCOMPARE(info.value("LongitudeFixed").toDouble(), longitudeFixedDefault); + QCOMPARE(QTime::fromString(info.value("MorningBeginFixed").toString(), Qt::ISODate), morningBeginFixedDefault); + QCOMPARE(QTime::fromString(info.value("EveningBeginFixed").toString(), Qt::ISODate), eveningBeginFixedDefault); + QCOMPARE(info.value("TransitionTime").toInt(), transitionTimeDefault); + + cfgGroup.writeEntry("Active", active); + cfgGroup.writeEntry("Mode", mode); + cfgGroup.writeEntry("NightTemperature", nightTemperature); + cfgGroup.writeEntry("LatitudeFixed", latitudeFixed); + cfgGroup.writeEntry("LongitudeFixed", longitudeFixed); + cfgGroup.writeEntry("MorningBeginFixed", morningBeginFixed); + cfgGroup.writeEntry("EveningBeginFixed", eveningBeginFixed); + cfgGroup.writeEntry("TransitionTime", transitionTime); + + manager->reparseConfigAndReset(); + info = manager->info(); + QVERIFY(!info.isEmpty()); + + if (success) { + QCOMPARE(info.value("Active").toBool() ? QString("true") : QString("false"), active); + QCOMPARE(info.value("Mode").toInt(), mode); + QCOMPARE(info.value("NightTemperature").toInt(), nightTemperature); + QCOMPARE(info.value("LatitudeFixed").toDouble(), latitudeFixed); + QCOMPARE(info.value("LongitudeFixed").toDouble(), longitudeFixed); + QCOMPARE(QTime::fromString(info.value("MorningBeginFixed").toString(), Qt::ISODate), QTime::fromString(morningBeginFixed, "hhmm")); + QCOMPARE(QTime::fromString(info.value("EveningBeginFixed").toString(), Qt::ISODate), QTime::fromString(eveningBeginFixed, "hhmm")); + QCOMPARE(info.value("TransitionTime").toInt(), transitionTime); + } else { + QCOMPARE(info.value("Active").toBool(), activeDefault); + QCOMPARE(info.value("Mode").toInt(), modeDefault); + QCOMPARE(info.value("NightTemperature").toInt(), nightTemperatureUpperEnd); + QCOMPARE(info.value("LatitudeFixed").toDouble(), latitudeFixedDefault); + QCOMPARE(info.value("LongitudeFixed").toDouble(), longitudeFixedDefault); + QCOMPARE(QTime::fromString(info.value("MorningBeginFixed").toString(), Qt::ISODate), morningBeginFixedDefault); + QCOMPARE(QTime::fromString(info.value("EveningBeginFixed").toString(), Qt::ISODate), eveningBeginFixedDefault); + QCOMPARE(info.value("TransitionTime").toInt(), transitionTimeDefault); + } +} + +void ColorCorrectNightColorTest::testChangeConfiguration_data() +{ + QTest::addColumn("activeReadIn"); + QTest::addColumn("modeReadIn"); + QTest::addColumn("nightTemperatureReadIn"); + QTest::addColumn("latitudeFixedReadIn"); + QTest::addColumn("longitudeFixedReadIn"); + QTest::addColumn("morBeginFixedReadIn"); + QTest::addColumn("eveBeginFixedReadIn"); + QTest::addColumn("transitionTimeReadIn"); + QTest::addColumn("successReadIn"); + + QTest::newRow("data0") << true << 0 << 4500 << 45.5 << 35.1 << QTime(6,0,0) << QTime(18,0,0) << 30 << true; + QTest::newRow("data1") << true << 1 << 2500 << -10.5 << -8. << QTime(0,2,0) << QTime(20,0,0) << 60 << true; + QTest::newRow("data2") << false << 2 << 5000 << 90. << -180. << QTime(6,0,0) << QTime(19,1,1) << 1 << true; + QTest::newRow("data3") << false << 3 << 2000 << 90. << -180. << QTime(6,0,0) << QTime(18,0,0) << 1 << true; + QTest::newRow("wrongData0") << true << 4 << 4500 << 0. << 0. << QTime(6,0,0) << QTime(18,0,0) << 30 << false; + QTest::newRow("wrongData1") << true << 0 << 500 << 0. << 0. << QTime(6,0,0) << QTime(18,0,0) << 30 << false; + QTest::newRow("wrongData2") << true << 0 << 7000 << 0. << 0. << QTime(6,0,0) << QTime(18,0,0) << 30 << false; + QTest::newRow("wrongData3") << true << 0 << 4500 << 91. << -181. << QTime(6,0,0) << QTime(18,0,0) << 30 << false; + QTest::newRow("wrongData4") << true << 0 << 4500 << 0. << 0. << QTime(18,0,0) << QTime(6,0,0) << 30 << false; + QTest::newRow("wrongData5") << true << 0 << 4500 << 0. << 0. << QTime(6,0,0) << QTime(18,0,0) << 0 << false; + QTest::newRow("wrongData6") << true << 0 << 4500 << 0. << 0. << QTime(6,0,0) << QTime(18,0,0) << -1 << false; + QTest::newRow("wrongData7") << true << 0 << 4500 << 0. << 0. << QTime(12,0,0) << QTime(12,30,0) << 30 << false; + QTest::newRow("wrongData8") << true << 0 << 4500 << 0. << 0. << QTime(1,0,0) << QTime(23,30,0) << 90 << false; +} + +void ColorCorrectNightColorTest::testChangeConfiguration() +{ + QFETCH(bool, activeReadIn); + QFETCH(int, modeReadIn); + QFETCH(int, nightTemperatureReadIn); + QFETCH(double, latitudeFixedReadIn); + QFETCH(double, longitudeFixedReadIn); + QFETCH(QTime, morBeginFixedReadIn); + QFETCH(QTime, eveBeginFixedReadIn); + QFETCH(int, transitionTimeReadIn); + QFETCH(bool, successReadIn); + + const bool activeDefault = true; + const int modeDefault = 0; + const int nightTemperatureDefault = ColorCorrect::DEFAULT_NIGHT_TEMPERATURE; + const double latitudeFixedDefault = 0; + const double longitudeFixedDefault = 0; + const QTime morningBeginFixedDefault = QTime(6,0,0); + const QTime eveningBeginFixedDefault = QTime(18,0,0); + const int transitionTimeDefault = 30; + + // init with default values + bool active = activeDefault; + int mode = modeDefault; + int nightTemperature = nightTemperatureDefault; + double latitudeFixed = latitudeFixedDefault; + double longitudeFixed = longitudeFixedDefault; + QTime morningBeginFixed = morningBeginFixedDefault; + QTime eveningBeginFixed = eveningBeginFixedDefault; + int transitionTime = transitionTimeDefault; + + bool activeExpect = activeDefault; + int modeExpect = modeDefault; + int nightTemperatureExpect = nightTemperatureDefault; + double latitudeFixedExpect = latitudeFixedDefault; + double longitudeFixedExpect = longitudeFixedDefault; + QTime morningBeginFixedExpect = morningBeginFixedDefault; + QTime eveningBeginFixedExpect = eveningBeginFixedDefault; + int transitionTimeExpect = transitionTimeDefault; + + QHash data; + + auto setData = [&active, &mode, &nightTemperature, + &latitudeFixed, &longitudeFixed, + &morningBeginFixed, &eveningBeginFixed, &transitionTime, + &data]() { + data["Active"] = active; + data["Mode"] = mode; + data["NightTemperature"] = nightTemperature; + + data["LatitudeFixed"] = latitudeFixed; + data["LongitudeFixed"] = longitudeFixed; + + data["MorningBeginFixed"] = morningBeginFixed.toString(Qt::ISODate); + data["EveningBeginFixed"] = eveningBeginFixed.toString(Qt::ISODate); + data["TransitionTime"] = transitionTime; + }; + + auto compareValues = [&activeExpect, &modeExpect, &nightTemperatureExpect, + &latitudeFixedExpect, &longitudeFixedExpect, + &morningBeginFixedExpect, &eveningBeginFixedExpect, + &transitionTimeExpect](QHash info) { + QCOMPARE(info.value("Active").toBool(), activeExpect); + QCOMPARE(info.value("Mode").toInt(), modeExpect); + QCOMPARE(info.value("NightTemperature").toInt(), nightTemperatureExpect); + QCOMPARE(info.value("LatitudeFixed").toDouble(), latitudeFixedExpect); + QCOMPARE(info.value("LongitudeFixed").toDouble(), longitudeFixedExpect); + QCOMPARE(info.value("MorningBeginFixed").toString(), morningBeginFixedExpect.toString(Qt::ISODate)); + QCOMPARE(info.value("EveningBeginFixed").toString(), eveningBeginFixedExpect.toString(Qt::ISODate)); + QCOMPARE(info.value("TransitionTime").toInt(), transitionTimeExpect); + }; + + ColorCorrect::Manager *manager = kwinApp()->platform()->colorCorrectManager(); + + // test with default values + setData(); + manager->changeConfiguration(data); + compareValues(manager->info()); + + // set to test values + active = activeReadIn; + mode = modeReadIn; + nightTemperature = nightTemperatureReadIn; + latitudeFixed = latitudeFixedReadIn; + longitudeFixed = longitudeFixedReadIn; + morningBeginFixed = morBeginFixedReadIn; + eveningBeginFixed = eveBeginFixedReadIn; + transitionTime = transitionTimeReadIn; + + if (successReadIn) { + activeExpect = activeReadIn; + modeExpect = modeReadIn; + nightTemperatureExpect = nightTemperatureReadIn; + latitudeFixedExpect = latitudeFixedReadIn; + longitudeFixedExpect = longitudeFixedReadIn; + morningBeginFixedExpect = morBeginFixedReadIn; + eveningBeginFixedExpect = eveBeginFixedReadIn; + transitionTimeExpect = transitionTimeReadIn; + } + + // test with test values + setData(); + QCOMPARE(manager->changeConfiguration(data), successReadIn); + compareValues(manager->info()); +} + +void ColorCorrectNightColorTest::testAutoLocationUpdate() +{ + ColorCorrect::Manager *manager = kwinApp()->platform()->colorCorrectManager(); + auto info = manager->info(); + QCOMPARE(info.value("LatitudeAuto").toDouble(), 0.); + QCOMPARE(info.value("LongitudeAuto").toDouble(), 0.); + + // wrong latitude value + manager->autoLocationUpdate(91, 15); + info = manager->info(); + QCOMPARE(info.value("LatitudeAuto").toDouble(), 0.); + QCOMPARE(info.value("LongitudeAuto").toDouble(), 0.); + + // wrong longitude value + manager->autoLocationUpdate(50, -181); + info = manager->info(); + QCOMPARE(info.value("LatitudeAuto").toDouble(), 0.); + QCOMPARE(info.value("LongitudeAuto").toDouble(), 0.); + + // change + manager->autoLocationUpdate(50, -180); + info = manager->info(); + QCOMPARE(info.value("LatitudeAuto").toDouble(), 50.); + QCOMPARE(info.value("LongitudeAuto").toDouble(), -180.); + + // small deviation only + manager->autoLocationUpdate(51.5, -179.5); + info = manager->info(); + QCOMPARE(info.value("LongitudeAuto").toDouble(), -180.); + QCOMPARE(info.value("LatitudeAuto").toDouble(), 50.); +} + +WAYLANDTEST_MAIN(ColorCorrectNightColorTest) +#include "colorcorrect_nightcolor_test.moc" diff --git a/autotests/integration/data/anim-data-delete-effect/effect.js b/autotests/integration/data/anim-data-delete-effect/effect.js new file mode 100644 index 0000000..0a4170b --- /dev/null +++ b/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/autotests/integration/data/example.desktop b/autotests/integration/data/example.desktop new file mode 100644 index 0000000..739f5d3 --- /dev/null +++ b/autotests/integration/data/example.desktop @@ -0,0 +1,3 @@ +[Desktop Entry] +Name=An example application +Icon=kwin diff --git a/autotests/integration/data/rules/maximize-vert-apply-initial b/autotests/integration/data/rules/maximize-vert-apply-initial new file mode 100644 index 0000000..1f8d42e --- /dev/null +++ b/autotests/integration/data/rules/maximize-vert-apply-initial @@ -0,0 +1,13 @@ +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 diff --git a/autotests/integration/dbus_interface_test.cpp b/autotests/integration/dbus_interface_test.cpp new file mode 100644 index 0000000..316c279 --- /dev/null +++ b/autotests/integration/dbus_interface_test.cpp @@ -0,0 +1,378 @@ +/* + 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 "kwin_wayland_test.h" +#include "abstract_client.h" +#include "atoms.h" +#include "x11client.h" +#include "deleted.h" +#include "platform.h" +#include "rules.h" +#include "screens.h" +#include "virtualdesktops.h" +#include "wayland_server.h" +#include "workspace.h" + +#include + +#include +#include +#include +#include +#include + +#include +#include + +using namespace KWin; +using namespace KWayland::Client; + +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(); + qRegisterMetaType(); + + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + waylandServer()->initWorkspace(); + 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 clientAddedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(clientAddedSpy.isValid()); + + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + shellSurface->setAppId(QByteArrayLiteral("org.kde.foo")); + shellSurface->setTitle(QStringLiteral("Test window")); + + // now let's render + Test::render(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(clientAddedSpy.isEmpty()); + QVERIFY(clientAddedSpy.wait()); + auto client = clientAddedSpy.first().first().value(); + QVERIFY(client); + + // let's get the window info + QDBusPendingReply reply{getWindowInfo(client->internalId())}; + reply.waitForFinished(); + QVERIFY(reply.isValid()); + QVERIFY(!reply.isError()); + auto windowData = reply.value(); + QVERIFY(!windowData.isEmpty()); + QCOMPARE(windowData.size(), 24); + QCOMPARE(windowData.value(QStringLiteral("type")).toInt(), NET::Normal); + QCOMPARE(windowData.value(QStringLiteral("x")).toInt(), client->x()); + QCOMPARE(windowData.value(QStringLiteral("y")).toInt(), client->y()); + QCOMPARE(windowData.value(QStringLiteral("width")).toInt(), client->width()); + QCOMPARE(windowData.value(QStringLiteral("height")).toInt(), client->height()); + QCOMPARE(windowData.value(QStringLiteral("x11DesktopNumber")).toInt(), 1); + QCOMPARE(windowData.value(QStringLiteral("minimized")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("shaded")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("fullscreen")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("keepAbove")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("keepBelow")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("skipTaskbar")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("skipPager")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("skipSwitcher")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("maximizeHorizontal")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("maximizeVertical")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("noBorder")).toBool(), true); + QCOMPARE(windowData.value(QStringLiteral("clientMachine")).toString(), QString()); + QCOMPARE(windowData.value(QStringLiteral("localhost")).toBool(), true); + QCOMPARE(windowData.value(QStringLiteral("role")).toString(), QString()); + QCOMPARE(windowData.value(QStringLiteral("resourceName")).toString(), QStringLiteral("testDbusInterface")); + QCOMPARE(windowData.value(QStringLiteral("resourceClass")).toString(), QStringLiteral("org.kde.foo")); + QCOMPARE(windowData.value(QStringLiteral("desktopFile")).toString(), QStringLiteral("org.kde.foo")); + QCOMPARE(windowData.value(QStringLiteral("caption")).toString(), QStringLiteral("Test window")); + + auto verifyProperty = [client] (const QString &name) { + QDBusPendingReply reply{getWindowInfo(client->internalId())}; + reply.waitForFinished(); + return reply.value().value(name).toBool(); + }; + + QVERIFY(!client->isMinimized()); + client->setMinimized(true); + QVERIFY(client->isMinimized()); + QCOMPARE(verifyProperty(QStringLiteral("minimized")), true); + + QVERIFY(!client->keepAbove()); + client->setKeepAbove(true); + QVERIFY(client->keepAbove()); + QCOMPARE(verifyProperty(QStringLiteral("keepAbove")), true); + + QVERIFY(!client->keepBelow()); + client->setKeepBelow(true); + QVERIFY(client->keepBelow()); + QCOMPARE(verifyProperty(QStringLiteral("keepBelow")), true); + + QVERIFY(!client->skipTaskbar()); + client->setSkipTaskbar(true); + QVERIFY(client->skipTaskbar()); + QCOMPARE(verifyProperty(QStringLiteral("skipTaskbar")), true); + + QVERIFY(!client->skipPager()); + client->setSkipPager(true); + QVERIFY(client->skipPager()); + QCOMPARE(verifyProperty(QStringLiteral("skipPager")), true); + + QVERIFY(!client->skipSwitcher()); + client->setSkipSwitcher(true); + QVERIFY(client->skipSwitcher()); + QCOMPARE(verifyProperty(QStringLiteral("skipSwitcher")), true); + + // not testing shaded as that's X11 + // not testing fullscreen, maximizeHorizontal, maximizeVertical and noBorder as those require window geometry changes + + QCOMPARE(client->desktop(), 1); + workspace()->sendClientToDesktop(client, 2, false); + QCOMPARE(client->desktop(), 2); + reply = getWindowInfo(client->internalId()); + reply.waitForFinished(); + QCOMPARE(reply.value().value(QStringLiteral("x11DesktopNumber")).toInt(), 2); + + client->move(10, 20); + reply = getWindowInfo(client->internalId()); + reply.waitForFinished(); + QCOMPARE(reply.value().value(QStringLiteral("x")).toInt(), client->x()); + QCOMPARE(reply.value().value(QStringLiteral("y")).toInt(), client->y()); + // not testing width, height as that would require window geometry change + + // finally close window + const auto id = client->internalId(); + QSignalSpy windowClosedSpy(client, &AbstractClient::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + shellSurface.reset(); + surface.reset(); + QVERIFY(windowClosedSpy.wait()); + QCOMPARE(windowClosedSpy.count(), 1); + + reply = getWindowInfo(id); + reply.waitForFinished(); + QVERIFY(reply.value().empty()); +} + + +struct XcbConnectionDeleter +{ + static inline void cleanup(xcb_connection_t *pointer) + { + xcb_disconnect(pointer); + } +}; + +void TestDbusInterface::testGetWindowInfoX11Client() +{ + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + const QRect windowGeometry(0, 0, 600, 400); + xcb_window_t w = xcb_generate_id(c.data()); + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, 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; + memset(&hints, 0, sizeof(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.data(), w, &hints); + xcb_icccm_set_wm_class(c.data(), w, 7, "foo\0bar"); + NETWinInfo winInfo(c.data(), w, rootWindow(), NET::Properties(), NET::Properties2()); + winInfo.setName("Some caption"); + winInfo.setDesktopFileName("org.kde.foo"); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + // we should get a client for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->window(), w); + QCOMPARE(client->clientSize(), windowGeometry.size()); + + // let's get the window info + QDBusPendingReply reply{getWindowInfo(client->internalId())}; + reply.waitForFinished(); + QVERIFY(reply.isValid()); + QVERIFY(!reply.isError()); + auto windowData = reply.value(); + QVERIFY(!windowData.isEmpty()); + QCOMPARE(windowData.size(), 24); + QCOMPARE(windowData.value(QStringLiteral("type")).toInt(), NET::Normal); + QCOMPARE(windowData.value(QStringLiteral("x")).toInt(), client->x()); + QCOMPARE(windowData.value(QStringLiteral("y")).toInt(), client->y()); + QCOMPARE(windowData.value(QStringLiteral("width")).toInt(), client->width()); + QCOMPARE(windowData.value(QStringLiteral("height")).toInt(), client->height()); + QCOMPARE(windowData.value(QStringLiteral("x11DesktopNumber")).toInt(), 1); + QCOMPARE(windowData.value(QStringLiteral("minimized")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("shaded")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("fullscreen")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("keepAbove")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("keepBelow")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("skipTaskbar")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("skipPager")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("skipSwitcher")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("maximizeHorizontal")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("maximizeVertical")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("noBorder")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("role")).toString(), QString()); + QCOMPARE(windowData.value(QStringLiteral("resourceName")).toString(), QStringLiteral("foo")); + QCOMPARE(windowData.value(QStringLiteral("resourceClass")).toString(), QStringLiteral("bar")); + QCOMPARE(windowData.value(QStringLiteral("desktopFile")).toString(), QStringLiteral("org.kde.foo")); + QCOMPARE(windowData.value(QStringLiteral("caption")).toString(), QStringLiteral("Some caption")); + // not testing clientmachine as that is system dependent + // due to that also not testing localhost + + auto verifyProperty = [client] (const QString &name) { + QDBusPendingReply reply{getWindowInfo(client->internalId())}; + reply.waitForFinished(); + return reply.value().value(name).toBool(); + }; + + QVERIFY(!client->isMinimized()); + client->setMinimized(true); + QVERIFY(client->isMinimized()); + QCOMPARE(verifyProperty(QStringLiteral("minimized")), true); + + QVERIFY(!client->keepAbove()); + client->setKeepAbove(true); + QVERIFY(client->keepAbove()); + QCOMPARE(verifyProperty(QStringLiteral("keepAbove")), true); + + QVERIFY(!client->keepBelow()); + client->setKeepBelow(true); + QVERIFY(client->keepBelow()); + QCOMPARE(verifyProperty(QStringLiteral("keepBelow")), true); + + QVERIFY(!client->skipTaskbar()); + client->setSkipTaskbar(true); + QVERIFY(client->skipTaskbar()); + QCOMPARE(verifyProperty(QStringLiteral("skipTaskbar")), true); + + QVERIFY(!client->skipPager()); + client->setSkipPager(true); + QVERIFY(client->skipPager()); + QCOMPARE(verifyProperty(QStringLiteral("skipPager")), true); + + QVERIFY(!client->skipSwitcher()); + client->setSkipSwitcher(true); + QVERIFY(client->skipSwitcher()); + QCOMPARE(verifyProperty(QStringLiteral("skipSwitcher")), true); + + QVERIFY(!client->isShade()); + client->setShade(ShadeNormal); + QVERIFY(client->isShade()); + QCOMPARE(verifyProperty(QStringLiteral("shaded")), true); + client->setShade(ShadeNone); + QVERIFY(!client->isShade()); + + QVERIFY(!client->noBorder()); + client->setNoBorder(true); + QVERIFY(client->noBorder()); + QCOMPARE(verifyProperty(QStringLiteral("noBorder")), true); + client->setNoBorder(false); + QVERIFY(!client->noBorder()); + + QVERIFY(!client->isFullScreen()); + client->setFullScreen(true); + QVERIFY(client->isFullScreen()); + QVERIFY(client->clientSize() != windowGeometry.size()); + QCOMPARE(verifyProperty(QStringLiteral("fullscreen")), true); + reply = getWindowInfo(client->internalId()); + reply.waitForFinished(); + QCOMPARE(reply.value().value(QStringLiteral("width")).toInt(), client->width()); + QCOMPARE(reply.value().value(QStringLiteral("height")).toInt(), client->height()); + client->setFullScreen(false); + QVERIFY(!client->isFullScreen()); + + // maximize + client->setMaximize(true, false); + QCOMPARE(verifyProperty(QStringLiteral("maximizeVertical")), true); + QCOMPARE(verifyProperty(QStringLiteral("maximizeHorizontal")), false); + client->setMaximize(false, true); + QCOMPARE(verifyProperty(QStringLiteral("maximizeVertical")), false); + QCOMPARE(verifyProperty(QStringLiteral("maximizeHorizontal")), true); + + const auto id = client->internalId(); + // destroy the window + xcb_unmap_window(c.data(), w); + xcb_flush(c.data()); + + QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + QVERIFY(windowClosedSpy.wait()); + xcb_destroy_window(c.data(), w); + c.reset(); + + reply = getWindowInfo(id); + reply.waitForFinished(); + QVERIFY(reply.value().empty()); +} + +WAYLANDTEST_MAIN(TestDbusInterface) +#include "dbus_interface_test.moc" diff --git a/autotests/integration/debug_console_test.cpp b/autotests/integration/debug_console_test.cpp new file mode 100644 index 0000000..6ec625f --- /dev/null +++ b/autotests/integration/debug_console_test.cpp @@ -0,0 +1,503 @@ +/* + 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 "abstract_client.h" +#include "debug_console.h" +#include "internal_client.h" +#include "platform.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include "xcbutils.h" + +#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(); + void testX11Client(); + void testX11Unmanaged(); + void testWaylandClient(); + void testInternalWindow(); + void testClosingDebugConsole(); +}; + +void DebugConsoleTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + setenv("QT_QPA_PLATFORM", "wayland", true); + waylandServer()->initWorkspace(); +} + +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()); + } + } +} + +void DebugConsoleTest::testX11Client() +{ + 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); + QVERIFY(rowsInsertedSpy.isValid()); + + 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 clientIndex = model.index(0, 0, x11TopLevelIndex); + QVERIFY(clientIndex.isValid()); + QCOMPARE(model.parent(clientIndex), x11TopLevelIndex); + QVERIFY(model.hasChildren(clientIndex)); + QVERIFY(model.rowCount(clientIndex) != 0); + QCOMPARE(model.columnCount(clientIndex), 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 clientIndex has children and those are properties + for (int i = 0; i < model.rowCount(clientIndex); i++) { + const QModelIndex propNameIndex = model.index(i, 0, clientIndex); + QVERIFY(propNameIndex.isValid()); + QCOMPARE(model.parent(propNameIndex), clientIndex); + 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, clientIndex); + QVERIFY(propValueIndex.isValid()); + QCOMPARE(model.parent(propValueIndex), clientIndex); + 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, clientIndex).isValid()); + } + // row after count should be invalid + QVERIFY(!model.index(model.rowCount(clientIndex), 0, clientIndex).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); + QVERIFY(rowsRemovedSpy.isValid()); + + 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); + QVERIFY(rowsInsertedSpy.isValid()); + + // let's create an override redirect window + const uint32_t values[] = {true}; + Xcb::Window window(QRect(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 clientIndex = model.index(0, 0, unmanagedTopLevelIndex); + QVERIFY(clientIndex.isValid()); + QCOMPARE(model.parent(clientIndex), unmanagedTopLevelIndex); + QVERIFY(model.hasChildren(clientIndex)); + QVERIFY(model.rowCount(clientIndex) != 0); + QCOMPARE(model.columnCount(clientIndex), 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(clientIndex, Qt::DisplayRole).toString(), QString::number(window)); + + // the clientIndex has children and those are properties + for (int i = 0; i < model.rowCount(clientIndex); i++) { + const QModelIndex propNameIndex = model.index(i, 0, clientIndex); + QVERIFY(propNameIndex.isValid()); + QCOMPARE(model.parent(propNameIndex), clientIndex); + 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, clientIndex); + QVERIFY(propValueIndex.isValid()); + QCOMPARE(model.parent(propValueIndex), clientIndex); + 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, clientIndex).isValid()); + } + // row after count should be invalid + QVERIFY(!model.index(model.rowCount(clientIndex), 0, clientIndex).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); + QVERIFY(rowsRemovedSpy.isValid()); + + 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()))); +} + +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); + QVERIFY(rowsInsertedSpy.isValid()); + + // create our connection + QVERIFY(Test::setupWaylandConnection()); + + // create the Surface and ShellSurface + using namespace KWayland::Client; + QScopedPointer surface(Test::createSurface()); + QVERIFY(surface->isValid()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + Test::render(surface.data(), 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 clientIndex = model.index(0, 0, waylandTopLevelIndex); + QVERIFY(clientIndex.isValid()); + QCOMPARE(model.parent(clientIndex), waylandTopLevelIndex); + QVERIFY(model.hasChildren(clientIndex)); + QVERIFY(model.rowCount(clientIndex) != 0); + QCOMPARE(model.columnCount(clientIndex), 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 clientIndex has children and those are properties + for (int i = 0; i < model.rowCount(clientIndex); i++) { + const QModelIndex propNameIndex = model.index(i, 0, clientIndex); + QVERIFY(propNameIndex.isValid()); + QCOMPARE(model.parent(propNameIndex), clientIndex); + 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, clientIndex); + QVERIFY(propValueIndex.isValid()); + QCOMPARE(model.parent(propValueIndex), clientIndex); + 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, clientIndex).isValid()); + } + // row after count should be invalid + QVERIFY(!model.index(model.rowCount(clientIndex), 0, clientIndex).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); + QVERIFY(rowsRemovedSpy.isValid()); + + surface->attachBuffer(Buffer::Ptr()); + surface->commit(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 { + Q_UNUSED(event) + 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); + QVERIFY(rowsInsertedSpy.isValid()); + + QScopedPointer w(new HelperWindow); + w->setGeometry(0, 0, 100, 100); + w->show(); + + QTRY_COMPARE(rowsInsertedSpy.count(), 1); + QCOMPARE(rowsInsertedSpy.first().first().value(), internalTopLevelIndex); + + QModelIndex clientIndex = model.index(rowsInsertedSpy.first().last().toInt(), 0, internalTopLevelIndex); + QVERIFY(clientIndex.isValid()); + QCOMPARE(model.parent(clientIndex), internalTopLevelIndex); + QVERIFY(model.hasChildren(clientIndex)); + QVERIFY(model.rowCount(clientIndex) != 0); + QCOMPARE(model.columnCount(clientIndex), 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 clientIndex has children and those are properties + for (int i = 0; i < model.rowCount(clientIndex); i++) { + const QModelIndex propNameIndex = model.index(i, 0, clientIndex); + QVERIFY(propNameIndex.isValid()); + QCOMPARE(model.parent(propNameIndex), clientIndex); + 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, clientIndex); + QVERIFY(propValueIndex.isValid()); + QCOMPARE(model.parent(propValueIndex), clientIndex); + 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, clientIndex).isValid()); + } + // row after count should be invalid + QVERIFY(!model.index(model.rowCount(clientIndex), 0, clientIndex).isValid()); + + // now close the window again, it should be removed from the model + QSignalSpy rowsRemovedSpy(&model, &QAbstractItemModel::rowsRemoved); + QVERIFY(rowsRemovedSpy.isValid()); + + 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); + QVERIFY(destroyedSpy.isValid()); + + QSignalSpy clientAddedSpy(workspace(), &Workspace::internalClientAdded); + QVERIFY(clientAddedSpy.isValid()); + console->show(); + QCOMPARE(console->windowHandle()->isVisible(), true); + QTRY_COMPARE(clientAddedSpy.count(), 1); + InternalClient *c = clientAddedSpy.first().first().value(); + QVERIFY(c->isInternal()); + QCOMPARE(c->internalWindow(), console->windowHandle()); + QVERIFY(c->isDecorated()); + QCOMPARE(c->isMinimizable(), false); + c->closeWindow(); + QVERIFY(destroyedSpy.wait()); +} + +} + +WAYLANDTEST_MAIN(KWin::DebugConsoleTest) +#include "debug_console_test.moc" diff --git a/autotests/integration/decoration_input_test.cpp b/autotests/integration/decoration_input_test.cpp new file mode 100644 index 0000000..dd2d9f0 --- /dev/null +++ b/autotests/integration/decoration_input_test.cpp @@ -0,0 +1,795 @@ +/* + 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 "abstract_client.h" +#include "cursor.h" +#include "internal_client.h" +#include "platform.h" +#include "pointer_input.h" +#include "touch_input.h" +#include "screenedge.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include + +#include "decorations/decoratedclient.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 testDoubleClick_data(); + void testDoubleClick(); + void testDoubleTap_data(); + 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: + AbstractClient *showWindow(); +}; + +#define MOTION(target) \ + kwinApp()->platform()->pointerMotion(target, timestamp++) + +#define PRESS \ + kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++) + +#define RELEASE \ + kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++) + +AbstractClient *DecorationInputTest::showWindow() +{ + using namespace KWayland::Client; +#define VERIFY(statement) \ + if (!QTest::qVerify((statement), #statement, "", __FILE__, __LINE__))\ + return nullptr; +#define COMPARE(actual, expected) \ + if (!QTest::qCompare(actual, expected, #actual, #expected, __FILE__, __LINE__))\ + return nullptr; + + Surface *surface = Test::createSurface(Test::waylandCompositor()); + VERIFY(surface); + XdgShellSurface *shellSurface = Test::createXdgShellStableSurface(surface, surface); + VERIFY(shellSurface); + auto deco = Test::waylandServerSideDecoration()->create(surface, surface); + QSignalSpy decoSpy(deco, &ServerSideDecoration::modeChanged); + VERIFY(decoSpy.isValid()); + VERIFY(decoSpy.wait()); + deco->requestMode(ServerSideDecoration::Mode::Server); + VERIFY(decoSpy.wait()); + COMPARE(deco->mode(), ServerSideDecoration::Mode::Server); + // let's render + auto c = Test::renderAndWaitForShown(surface, QSize(500, 50), Qt::blue); + VERIFY(c); + COMPARE(workspace()->activeClient(), c); + +#undef VERIFY +#undef COMPARE + + return c; +} + +void DecorationInputTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + // 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(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + setenv("QT_QPA_PLATFORM", "wayland", true); + waylandServer()->initWorkspace(); +} + +void DecorationInputTest::init() +{ + using namespace KWayland::Client; + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::Decoration)); + QVERIFY(Test::waitForWaylandPointer()); + + screens()->setCurrent(0); + Cursors::self()->mouse()->setPos(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() +{ + AbstractClient *c = showWindow(); + QVERIFY(c); + QVERIFY(c->isDecorated()); + QVERIFY(!c->noBorder()); + QCOMPARE(c->titlebarPosition(), AbstractClient::PositionTop); + QVERIFY(!c->keepAbove()); + QVERIFY(!c->keepBelow()); + + quint32 timestamp = 1; + MOTION(QPoint(c->frameGeometry().center().x(), c->clientPos().y() / 2)); + QVERIFY(!input()->pointer()->decoration().isNull()); + QCOMPARE(input()->pointer()->decoration()->decoration()->sectionUnderMouse(), Qt::TitleBarArea); + + // TODO: mouse wheel direction looks wrong to me + // simulate wheel + kwinApp()->platform()->pointerAxisVertical(5.0, timestamp++); + QVERIFY(c->keepBelow()); + QVERIFY(!c->keepAbove()); + kwinApp()->platform()->pointerAxisVertical(-5.0, timestamp++); + QVERIFY(!c->keepBelow()); + QVERIFY(!c->keepAbove()); + kwinApp()->platform()->pointerAxisVertical(-5.0, timestamp++); + QVERIFY(!c->keepBelow()); + QVERIFY(c->keepAbove()); + + // test top most deco pixel, BUG: 362860 + c->move(0, 0); + QFETCH(QPoint, decoPoint); + MOTION(decoPoint); + QVERIFY(!input()->pointer()->decoration().isNull()); + QCOMPARE(input()->pointer()->decoration()->client(), c); + QTEST(input()->pointer()->decoration()->decoration()->sectionUnderMouse(), "expectedSection"); + kwinApp()->platform()->pointerAxisVertical(5.0, timestamp++); + QVERIFY(!c->keepBelow()); + QVERIFY(!c->keepAbove()); +} + +void DecorationInputTest::testDoubleClick_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 KWin::DecorationInputTest::testDoubleClick() +{ + AbstractClient *c = showWindow(); + QVERIFY(c); + QVERIFY(c->isDecorated()); + QVERIFY(!c->noBorder()); + QVERIFY(!c->isOnAllDesktops()); + quint32 timestamp = 1; + MOTION(QPoint(c->frameGeometry().center().x(), c->clientPos().y() / 2)); + + // double click + PRESS; + RELEASE; + PRESS; + RELEASE; + QVERIFY(c->isOnAllDesktops()); + // double click again + PRESS; + RELEASE; + QVERIFY(c->isOnAllDesktops()); + PRESS; + RELEASE; + QVERIFY(!c->isOnAllDesktops()); + + // test top most deco pixel, BUG: 362860 + c->move(0, 0); + QFETCH(QPoint, decoPoint); + MOTION(decoPoint); + QVERIFY(!input()->pointer()->decoration().isNull()); + QCOMPARE(input()->pointer()->decoration()->client(), c); + QTEST(input()->pointer()->decoration()->decoration()->sectionUnderMouse(), "expectedSection"); + // double click + PRESS; + RELEASE; + QVERIFY(!c->isOnAllDesktops()); + PRESS; + RELEASE; + QVERIFY(c->isOnAllDesktops()); +} + +void DecorationInputTest::testDoubleTap_data() +{ + QTest::addColumn("decoPoint"); + QTest::addColumn("expectedSection"); + + QTest::newRow("topLeft") << QPoint(10, 10) << Qt::TopLeftSection; + QTest::newRow("top") << QPoint(260, 10) << Qt::TopSection; + QTest::newRow("topRight") << QPoint(509, 10) << Qt::TopRightSection; +} + +void KWin::DecorationInputTest::testDoubleTap() +{ + AbstractClient *c = showWindow(); + QVERIFY(c); + QVERIFY(c->isDecorated()); + QVERIFY(!c->noBorder()); + QVERIFY(!c->isOnAllDesktops()); + quint32 timestamp = 1; + const QPoint tapPoint(c->frameGeometry().center().x(), c->clientPos().y() / 2); + + // double tap + kwinApp()->platform()->touchDown(0, tapPoint, timestamp++); + kwinApp()->platform()->touchUp(0, timestamp++); + kwinApp()->platform()->touchDown(0, tapPoint, timestamp++); + kwinApp()->platform()->touchUp(0, timestamp++); + QVERIFY(c->isOnAllDesktops()); + // double tap again + kwinApp()->platform()->touchDown(0, tapPoint, timestamp++); + kwinApp()->platform()->touchUp(0, timestamp++); + QVERIFY(c->isOnAllDesktops()); + kwinApp()->platform()->touchDown(0, tapPoint, timestamp++); + kwinApp()->platform()->touchUp(0, timestamp++); + QVERIFY(!c->isOnAllDesktops()); + + // test top most deco pixel, BUG: 362860 + // + // Not directly at (0, 0), otherwise ScreenEdgeInputFilter catches + // event before DecorationEventFilter. + c->move(10, 10); + QFETCH(QPoint, decoPoint); + // double click + kwinApp()->platform()->touchDown(0, decoPoint, timestamp++); + QVERIFY(!input()->touch()->decoration().isNull()); + QCOMPARE(input()->touch()->decoration()->client(), c); + QTEST(input()->touch()->decoration()->decoration()->sectionUnderMouse(), "expectedSection"); + kwinApp()->platform()->touchUp(0, timestamp++); + QVERIFY(!c->isOnAllDesktops()); + kwinApp()->platform()->touchDown(0, decoPoint, timestamp++); + kwinApp()->platform()->touchUp(0, timestamp++); + QVERIFY(c->isOnAllDesktops()); +} + +void DecorationInputTest::testHover() +{ + AbstractClient *c = showWindow(); + QVERIFY(c); + QVERIFY(c->isDecorated()); + QVERIFY(!c->noBorder()); + + // our left border is moved out of the visible area, so move the window to a better place + c->move(QPoint(20, 0)); + + quint32 timestamp = 1; + MOTION(QPoint(c->frameGeometry().center().x(), c->clientPos().y() / 2)); + QCOMPARE(c->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 = Decoration::DecorationBridge::self()->settings()->borderSize() != KDecoration2::BorderSize::None; + auto deviation = [hasBorders] { + return hasBorders ? -1 : 0; + }; + + MOTION(QPoint(c->frameGeometry().x(), 0)); + QCOMPARE(c->cursor(), CursorShape(KWin::ExtendedCursor::SizeNorthWest)); + MOTION(QPoint(c->frameGeometry().x() + c->frameGeometry().width() / 2, 0)); + QCOMPARE(c->cursor(), CursorShape(KWin::ExtendedCursor::SizeNorth)); + MOTION(QPoint(c->frameGeometry().x() + c->frameGeometry().width() - 1, 0)); + QCOMPARE(c->cursor(), CursorShape(KWin::ExtendedCursor::SizeNorthEast)); + MOTION(QPoint(c->frameGeometry().x() + c->frameGeometry().width() + deviation(), c->height() / 2)); + QCOMPARE(c->cursor(), CursorShape(KWin::ExtendedCursor::SizeEast)); + MOTION(QPoint(c->frameGeometry().x() + c->frameGeometry().width() + deviation(), c->height() - 1)); + QCOMPARE(c->cursor(), CursorShape(KWin::ExtendedCursor::SizeSouthEast)); + MOTION(QPoint(c->frameGeometry().x() + c->frameGeometry().width() / 2, c->height() + deviation())); + QCOMPARE(c->cursor(), CursorShape(KWin::ExtendedCursor::SizeSouth)); + MOTION(QPoint(c->frameGeometry().x(), c->height() + deviation())); + QCOMPARE(c->cursor(), CursorShape(KWin::ExtendedCursor::SizeSouthWest)); + MOTION(QPoint(c->frameGeometry().x() - 1, c->height() / 2)); + QCOMPARE(c->cursor(), CursorShape(KWin::ExtendedCursor::SizeWest)); + + MOTION(c->frameGeometry().center()); + QEXPECT_FAIL("", "Cursor not set back on leave", Continue); + QCOMPARE(c->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() +{ + AbstractClient *c = showWindow(); + QVERIFY(c); + QVERIFY(c->isDecorated()); + QVERIFY(!c->noBorder()); + c->move(screens()->geometry(0).center() - QPoint(c->width()/2, c->height()/2)); + QSignalSpy startMoveResizedSpy(c, &AbstractClient::clientStartUserMovedResized); + QVERIFY(startMoveResizedSpy.isValid()); + QSignalSpy clientFinishUserMovedResizedSpy(c, &AbstractClient::clientFinishUserMovedResized); + QVERIFY(clientFinishUserMovedResizedSpy.isValid()); + + quint32 timestamp = 1; + MOTION(QPoint(c->frameGeometry().center().x(), c->y() + c->clientPos().y() / 2)); + QCOMPARE(c->cursor(), CursorShape(Qt::ArrowCursor)); + + PRESS; + QVERIFY(!c->isMove()); + QFETCH(QPoint, offset); + MOTION(QPoint(c->frameGeometry().center().x(), c->y() + c->clientPos().y() / 2) + offset); + const QPoint oldPos = c->pos(); + QVERIFY(c->isMove()); + QCOMPARE(startMoveResizedSpy.count(), 1); + + RELEASE; + QTRY_VERIFY(!c->isMove()); + QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); + QEXPECT_FAIL("", "Just trigger move doesn't move the window", Continue); + QCOMPARE(c->pos(), oldPos + offset); + + // again + PRESS; + QVERIFY(!c->isMove()); + QFETCH(QPoint, offset2); + MOTION(QPoint(c->frameGeometry().center().x(), c->y() + c->clientPos().y() / 2) + offset2); + QVERIFY(c->isMove()); + QCOMPARE(startMoveResizedSpy.count(), 2); + QFETCH(QPoint, offset3); + MOTION(QPoint(c->frameGeometry().center().x(), c->y() + c->clientPos().y() / 2) + offset3); + + RELEASE; + QTRY_VERIFY(!c->isMove()); + QCOMPARE(clientFinishUserMovedResizedSpy.count(), 2); + // TODO: the offset should also be included + QCOMPARE(c->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() +{ + AbstractClient *c = showWindow(); + QVERIFY(c); + QVERIFY(c->isDecorated()); + QVERIFY(!c->noBorder()); + c->move(screens()->geometry(0).center() - QPoint(c->width()/2, c->height()/2)); + QSignalSpy startMoveResizedSpy(c, &AbstractClient::clientStartUserMovedResized); + QVERIFY(startMoveResizedSpy.isValid()); + QSignalSpy clientFinishUserMovedResizedSpy(c, &AbstractClient::clientFinishUserMovedResized); + QVERIFY(clientFinishUserMovedResizedSpy.isValid()); + + quint32 timestamp = 1; + QPoint p = QPoint(c->frameGeometry().center().x(), c->y() + c->clientPos().y() / 2); + + kwinApp()->platform()->touchDown(0, p, timestamp++); + QVERIFY(!c->isMove()); + QFETCH(QPoint, offset); + QCOMPARE(input()->touch()->decorationPressId(), 0); + kwinApp()->platform()->touchMotion(0, p + offset, timestamp++); + const QPoint oldPos = c->pos(); + QVERIFY(c->isMove()); + QCOMPARE(startMoveResizedSpy.count(), 1); + + kwinApp()->platform()->touchUp(0, timestamp++); + QTRY_VERIFY(!c->isMove()); + QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); + QEXPECT_FAIL("", "Just trigger move doesn't move the window", Continue); + QCOMPARE(c->pos(), oldPos + offset); + + // again + kwinApp()->platform()->touchDown(1, p + offset, timestamp++); + QCOMPARE(input()->touch()->decorationPressId(), 1); + QVERIFY(!c->isMove()); + QFETCH(QPoint, offset2); + kwinApp()->platform()->touchMotion(1, QPoint(c->frameGeometry().center().x(), c->y() + c->clientPos().y() / 2) + offset2, timestamp++); + QVERIFY(c->isMove()); + QCOMPARE(startMoveResizedSpy.count(), 2); + QFETCH(QPoint, offset3); + kwinApp()->platform()->touchMotion(1, QPoint(c->frameGeometry().center().x(), c->y() + c->clientPos().y() / 2) + offset3, timestamp++); + + kwinApp()->platform()->touchUp(1, timestamp++); + QTRY_VERIFY(!c->isMove()); + QCOMPARE(clientFinishUserMovedResizedSpy.count(), 2); + // TODO: the offset should also be included + QCOMPARE(c->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("org.kde.kdecoration2").writeEntry("BorderSize", QStringLiteral("None")); + kwinApp()->config()->sync(); + workspace()->slotReconfigure(); + + // now create window + AbstractClient *c = showWindow(); + QVERIFY(c); + QVERIFY(c->isDecorated()); + QVERIFY(!c->noBorder()); + c->move(screens()->geometry(0).center() - QPoint(c->width()/2, c->height()/2)); + QVERIFY(c->frameGeometry() != c->inputGeometry()); + QVERIFY(c->inputGeometry().contains(c->frameGeometry())); + QSignalSpy startMoveResizedSpy(c, &AbstractClient::clientStartUserMovedResized); + QVERIFY(startMoveResizedSpy.isValid()); + + // go to border + quint32 timestamp = 1; + QFETCH(Qt::Edge, edge); + switch (edge) { + case Qt::LeftEdge: + MOTION(QPoint(c->frameGeometry().x() -1, c->frameGeometry().center().y())); + break; + case Qt::RightEdge: + MOTION(QPoint(c->frameGeometry().x() + c->frameGeometry().width() +1, c->frameGeometry().center().y())); + break; + case Qt::BottomEdge: + MOTION(QPoint(c->frameGeometry().center().x(), c->frameGeometry().y() + c->frameGeometry().height() + 1)); + break; + default: + break; + } + QVERIFY(!c->frameGeometry().contains(KWin::Cursors::self()->mouse()->pos())); + + // pressing should trigger resize + PRESS; + QVERIFY(!c->isResize()); + QVERIFY(startMoveResizedSpy.wait()); + QVERIFY(c->isResize()); + + RELEASE; + QVERIFY(!c->isResize()); +} + +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("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 + AbstractClient *c = showWindow(); + QVERIFY(c); + QVERIFY(c->isDecorated()); + QVERIFY(!c->noBorder()); + c->move(screens()->geometry(0).center() - QPoint(c->width()/2, c->height()/2)); + // move cursor on window + Cursors::self()->mouse()->setPos(QPoint(c->frameGeometry().center().x(), c->y() + c->clientPos().y() / 2)); + + // simulate modifier+click + quint32 timestamp = 1; + QFETCH(bool, capsLock); + if (capsLock) { + kwinApp()->platform()->keyboardKeyPressed(KEY_CAPSLOCK, timestamp++); + } + QFETCH(int, modifierKey); + QFETCH(int, mouseButton); + kwinApp()->platform()->keyboardKeyPressed(modifierKey, timestamp++); + QVERIFY(!c->isMove()); + kwinApp()->platform()->pointerButtonPressed(mouseButton, timestamp++); + QVERIFY(c->isMove()); + // release modifier should not change it + kwinApp()->platform()->keyboardKeyReleased(modifierKey, timestamp++); + QVERIFY(c->isMove()); + // but releasing the key should end move/resize + kwinApp()->platform()->pointerButtonReleased(mouseButton, timestamp++); + QVERIFY(!c->isMove()); + if (capsLock) { + kwinApp()->platform()->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("MouseBindings"); + group.writeEntry("CommandAllKey", modKey); + group.writeEntry("CommandAllWheel", "change opacity"); + group.sync(); + workspace()->slotReconfigure(); + + AbstractClient *c = showWindow(); + QVERIFY(c); + QVERIFY(c->isDecorated()); + QVERIFY(!c->noBorder()); + c->move(screens()->geometry(0).center() - QPoint(c->width()/2, c->height()/2)); + // move cursor on window + Cursors::self()->mouse()->setPos(QPoint(c->frameGeometry().center().x(), c->y() + c->clientPos().y() / 2)); + // set the opacity to 0.5 + c->setOpacity(0.5); + QCOMPARE(c->opacity(), 0.5); + + // simulate modifier+wheel + quint32 timestamp = 1; + QFETCH(bool, capsLock); + if (capsLock) { + kwinApp()->platform()->keyboardKeyPressed(KEY_CAPSLOCK, timestamp++); + } + QFETCH(int, modifierKey); + kwinApp()->platform()->keyboardKeyPressed(modifierKey, timestamp++); + kwinApp()->platform()->pointerAxisVertical(-5, timestamp++); + QCOMPARE(c->opacity(), 0.6); + kwinApp()->platform()->pointerAxisVertical(5, timestamp++); + QCOMPARE(c->opacity(), 0.5); + kwinApp()->platform()->keyboardKeyReleased(modifierKey, timestamp++); + if (capsLock) { + kwinApp()->platform()->keyboardKeyReleased(KEY_CAPSLOCK, timestamp++); + } +} + +class EventHelper : public QObject +{ + Q_OBJECT +public: + EventHelper() : QObject() {} + ~EventHelper() override = default; + + bool eventFilter(QObject *watched, QEvent *event) override + { + Q_UNUSED(watched) + if (event->type() == QEvent::HoverMove) { + emit hoverMove(); + } else if (event->type() == QEvent::HoverLeave) { + 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 + AbstractClient *c = showWindow(); + QVERIFY(c); + QVERIFY(c->isDecorated()); + QVERIFY(!c->noBorder()); + + EventHelper helper; + c->decoration()->installEventFilter(&helper); + QSignalSpy hoverMoveSpy(&helper, &EventHelper::hoverMove); + QVERIFY(hoverMoveSpy.isValid()); + QSignalSpy hoverLeaveSpy(&helper, &EventHelper::hoverLeave); + QVERIFY(hoverLeaveSpy.isValid()); + + quint32 timestamp = 1; + const QPoint tapPoint(c->frameGeometry().center().x(), c->clientPos().y() / 2); + + QVERIFY(!input()->touch()->decoration()); + kwinApp()->platform()->touchDown(0, tapPoint, timestamp++); + QVERIFY(input()->touch()->decoration()); + QCOMPARE(input()->touch()->decoration()->decoration(), c->decoration()); + QCOMPARE(hoverMoveSpy.count(), 1); + QCOMPARE(hoverLeaveSpy.count(), 0); + kwinApp()->platform()->touchUp(0, timestamp++); + QCOMPARE(hoverMoveSpy.count(), 1); + QCOMPARE(hoverLeaveSpy.count(), 1); + + QCOMPARE(c->isMove(), false); + + // let's check that a hover motion is sent if the pointer is on deco, when touch release + Cursors::self()->mouse()->setPos(tapPoint); + QCOMPARE(hoverMoveSpy.count(), 2); + kwinApp()->platform()->touchDown(0, tapPoint, timestamp++); + QCOMPARE(hoverMoveSpy.count(), 3); + QCOMPARE(hoverLeaveSpy.count(), 1); + kwinApp()->platform()->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); + QVERIFY(enteredSpy.isValid()); + + AbstractClient *c = showWindow(); + QVERIFY(c); + QVERIFY(c->isDecorated()); + QVERIFY(!c->noBorder()); + QTRY_COMPARE(enteredSpy.count(), 1); + + QSignalSpy keyEvent(keyboard, &KWayland::Client::Keyboard::keyChanged); + QVERIFY(keyEvent.isValid()); + + QSignalSpy clientAddedSpy(workspace(), &Workspace::internalClientAdded); + QVERIFY(clientAddedSpy.isValid()); + c->decoratedClient()->requestShowToolTip(QStringLiteral("test")); + // now we should get an internal window + QVERIFY(clientAddedSpy.wait()); + InternalClient *internal = clientAddedSpy.first().first().value(); + QVERIFY(internal->isInternal()); + QVERIFY(internal->internalWindow()->flags().testFlag(Qt::ToolTip)); + + // now send a key + quint32 timestamp = 0; + kwinApp()->platform()->keyboardKeyPressed(KEY_A, timestamp++); + QVERIFY(keyEvent.wait()); + kwinApp()->platform()->keyboardKeyReleased(KEY_A, timestamp++); + QVERIFY(keyEvent.wait()); + + c->decoratedClient()->requestHideToolTip(); + Test::waitForWindowDestroyed(internal); +} + +} + +WAYLANDTEST_MAIN(KWin::DecorationInputTest) +#include "decoration_input_test.moc" diff --git a/autotests/integration/desktop_window_x11_test.cpp b/autotests/integration/desktop_window_x11_test.cpp new file mode 100644 index 0000000..ef2b212 --- /dev/null +++ b/autotests/integration/desktop_window_x11_test.cpp @@ -0,0 +1,166 @@ +/* + 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 "abstract_client.h" +#include "platform.h" +#include "x11client.h" +#include "cursor.h" +#include "deleted.h" +#include "screenedge.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include "xcbutils.h" +#include + +#include +#include + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_x11_desktop_window-0"); + +class X11DesktopWindowTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + void testDesktopWindow(); + +private: +}; + +void X11DesktopWindowTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + setenv("QT_QPA_PLATFORM", "wayland", true); + waylandServer()->initWorkspace(); +} + +void X11DesktopWindowTest::init() +{ + screens()->setCurrent(0); + Cursors::self()->mouse()->setPos(QPoint(640, 512)); +} + +void X11DesktopWindowTest::cleanup() +{ +} + +struct XcbConnectionDeleter +{ + static inline void cleanup(xcb_connection_t *pointer) + { + xcb_disconnect(pointer); + } +}; + +void X11DesktopWindowTest::testDesktopWindow() +{ + // this test creates a desktop window with an RGBA visual and verifies that it's only considered + // as an RGB (opaque) window in KWin + + // create an xcb window + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + + xcb_window_t w = xcb_generate_id(c.data()); + const QRect windowGeometry(0, 0, 1280, 1024); + + // helper to find the visual + auto findDepth = [&c] () -> xcb_visualid_t { + // find a visual with 32 depth + const xcb_setup_t *setup = xcb_get_setup(c.data()); + + for (auto screen = xcb_setup_roots_iterator(setup); screen.rem; xcb_screen_next(&screen)) { + for (auto depth = xcb_screen_allowed_depths_iterator(screen.data); depth.rem; xcb_depth_next(&depth)) { + if (depth.data->depth != 32) { + continue; + } + const int len = xcb_depth_visuals_length(depth.data); + const xcb_visualtype_t *visuals = xcb_depth_visuals(depth.data); + + for (int i = 0; i < len; i++) { + return visuals[0].visual_id; + } + } + } + return 0; + }; + auto visualId = findDepth(); + auto colormapId = xcb_generate_id(c.data()); + auto cmCookie = xcb_create_colormap_checked(c.data(), XCB_COLORMAP_ALLOC_NONE, colormapId, rootWindow(), visualId); + QVERIFY(!xcb_request_check(c.data(), cmCookie)); + + const uint32_t values[] = {XCB_PIXMAP_NONE, kwinApp()->x11DefaultScreen()->black_pixel, colormapId}; + auto cookie = xcb_create_window_checked(c.data(), 32, w, rootWindow(), + windowGeometry.x(), + windowGeometry.y(), + windowGeometry.width(), + windowGeometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, visualId, XCB_CW_BACK_PIXMAP | XCB_CW_BORDER_PIXEL | XCB_CW_COLORMAP, values); + QVERIFY(!xcb_request_check(c.data(), cookie)); + xcb_size_hints_t hints; + memset(&hints, 0, sizeof(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.data(), w, &hints); + NETWinInfo info(c.data(), w, rootWindow(), NET::WMAllProperties, NET::WM2AllProperties); + info.setWindowType(NET::Desktop); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + // verify through a geometry request that it's depth 32 + Xcb::WindowGeometry geo(w); + QCOMPARE(geo->depth, uint8_t(32)); + + // we should get a client for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->window(), w); + QVERIFY(!client->isDecorated()); + QCOMPARE(client->windowType(), NET::Desktop); + QCOMPARE(client->frameGeometry(), windowGeometry); + QVERIFY(client->isDesktop()); + QCOMPARE(client->depth(), 24); + QVERIFY(!client->hasAlpha()); + + // and destroy the window again + xcb_unmap_window(c.data(), w); + xcb_destroy_window(c.data(), w); + xcb_flush(c.data()); + c.reset(); + + QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + QVERIFY(windowClosedSpy.wait()); +} + +} + +WAYLANDTEST_MAIN(KWin::X11DesktopWindowTest) +#include "desktop_window_x11_test.moc" diff --git a/autotests/integration/dont_crash_aurorae_destroy_deco.cpp b/autotests/integration/dont_crash_aurorae_destroy_deco.cpp new file mode 100644 index 0000000..2846972 --- /dev/null +++ b/autotests/integration/dont_crash_aurorae_destroy_deco.cpp @@ -0,0 +1,146 @@ +/* + 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 "platform.h" +#include "x11client.h" +#include "composite.h" +#include "cursor.h" +#include "screenedge.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include "scene.h" +#include + +#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() +{ + qputenv("XDG_DATA_DIRS", QCoreApplication::applicationDirPath().toUtf8()); + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("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(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + setenv("QT_QPA_PLATFORM", "wayland", true); + waylandServer()->initWorkspace(); + + auto scene = KWin::Compositor::self()->scene(); + QVERIFY(scene); + QCOMPARE(scene->compositingType(), KWin::OpenGL2Compositing); +} + +void DontCrashAuroraeDestroyDecoTest::init() +{ + screens()->setCurrent(0); + Cursors::self()->mouse()->setPos(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("Windows"); + group.writeEntry("BorderlessMaximizedWindows", true); + group.sync(); + workspace()->slotReconfigure(); + QCOMPARE(options->borderlessMaximizedWindows(), true); + + // create an xcb window + xcb_connection_t *c = xcb_connect(nullptr, nullptr); + QVERIFY(!xcb_connection_has_error(c)); + + xcb_window_t w = xcb_generate_id(c); + xcb_create_window(c, XCB_COPY_FROM_PARENT, w, rootWindow(), 0, 0, 100, 200, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); + xcb_map_window(c, w); + xcb_flush(c); + + // we should get a client for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->window(), w); + QVERIFY(client->isDecorated()); + QCOMPARE(client->maximizeMode(), MaximizeRestore); + QCOMPARE(client->noBorder(), false); + // verify that the deco is Aurorae + QCOMPARE(qstrcmp(client->decoration()->metaObject()->className(), "Aurorae::Decoration"), 0); + // find the maximize button + QQuickItem *item = client->decoration()->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(client, "setReadyForPainting"); + QVERIFY(client->readyForPainting()); + + // simulate click on maximize button + QSignalSpy maximizedStateChangedSpy(client, static_cast(&AbstractClient::clientMaximizedStateChanged)); + QVERIFY(maximizedStateChangedSpy.isValid()); + quint32 timestamp = 1; + kwinApp()->platform()->pointerMotion(client->frameGeometry().topLeft() + scenePoint.toPoint(), timestamp++); + kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); + kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); + QVERIFY(maximizedStateChangedSpy.wait()); + QCOMPARE(client->maximizeMode(), MaximizeFull); + QCOMPARE(client->noBorder(), true); + + // and destroy the window again + xcb_unmap_window(c, w); + xcb_destroy_window(c, w); + xcb_flush(c); + xcb_disconnect(c); + + QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + QVERIFY(windowClosedSpy.wait()); +} + +} + +WAYLANDTEST_MAIN(KWin::DontCrashAuroraeDestroyDecoTest) +#include "dont_crash_aurorae_destroy_deco.moc" diff --git a/autotests/integration/dont_crash_cancel_animation.cpp b/autotests/integration/dont_crash_cancel_animation.cpp new file mode 100644 index 0000000..3d033ed --- /dev/null +++ b/autotests/integration/dont_crash_cancel_animation.cpp @@ -0,0 +1,112 @@ +/* + 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 "platform.h" +#include "abstract_client.h" +#include "x11client.h" +#include "composite.h" +#include "deleted.h" +#include "effects.h" +#include "effectloader.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include "scripting/scriptedeffect.h" + +#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(); + qRegisterMetaType(); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + kwinApp()->start(); + QVERIFY(Compositor::self()); + QSignalSpy compositorToggledSpy(Compositor::self(), &Compositor::compositingToggled); + QVERIFY(compositorToggledSpy.isValid()); + QVERIFY(compositorToggledSpy.wait()); + 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); + 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(static_cast(effects)->isEffectLoaded(QStringLiteral("crashy"))); + + using namespace KWayland::Client; + // create a window + Surface *surface = Test::createSurface(Test::waylandCompositor()); + QVERIFY(surface); + XdgShellSurface *shellSurface = Test::createXdgShellStableSurface(surface, surface); + QVERIFY(shellSurface); + // let's render + auto c = Test::renderAndWaitForShown(surface, QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(workspace()->activeClient(), c); + + // make sure we animate + QTest::qWait(200); + + // wait for the window to be passed to Deleted + QSignalSpy windowDeletedSpy(c, &AbstractClient::windowClosed); + QVERIFY(windowDeletedSpy.isValid()); + + surface->deleteLater(); + + QVERIFY(windowDeletedSpy.wait()); + // make sure we animate + QTest::qWait(200); +} + +} + +WAYLANDTEST_MAIN(KWin::DontCrashCancelAnimationFromAnimationEndedTest) +#include "dont_crash_cancel_animation.moc" diff --git a/autotests/integration/dont_crash_cursor_physical_size_empty.cpp b/autotests/integration/dont_crash_cursor_physical_size_empty.cpp new file mode 100644 index 0000000..56702f1 --- /dev/null +++ b/autotests/integration/dont_crash_cursor_physical_size_empty.cpp @@ -0,0 +1,99 @@ +/* + 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 "composite.h" +#include "effectloader.h" +#include "x11client.h" +#include "cursor.h" +#include "effects.h" +#include "platform.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" + +#include + +#include +#include +#include +#include +#include + +using namespace KWin; +using namespace KWayland::Client; +static const QString s_socketName = QStringLiteral("wayland_test_kwin_crash_cursor_physical_size_empty-0"); + +class DontCrashCursorPhysicalSizeEmpty : public QObject +{ +Q_OBJECT +private Q_SLOTS: + void init(); + void initTestCase(); + void cleanup(); + void testMoveCursorOverDeco(); +}; + +void DontCrashCursorPhysicalSizeEmpty::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Decoration)); + + screens()->setCurrent(0); + KWin::Cursors::self()->mouse()->setPos(QPoint(640, 512)); +} + +void DontCrashCursorPhysicalSizeEmpty::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void DontCrashCursorPhysicalSizeEmpty::initTestCase() +{ + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + if (!QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("icons/DMZ-White/index.theme")).isEmpty()) { + qputenv("XCURSOR_THEME", QByteArrayLiteral("DMZ-White")); + } else { + // might be vanilla-dmz (e.g. Arch, FreeBSD) + qputenv("XCURSOR_THEME", QByteArrayLiteral("Vanilla-DMZ")); + } + qputenv("XCURSOR_SIZE", QByteArrayLiteral("0")); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); +} + +void DontCrashCursorPhysicalSizeEmpty::testMoveCursorOverDeco() +{ + // This test ensures that there is no endless recursion if the cursor theme cannot be created + // a reason for creation failure could be physical size not existing + // see BUG: 390314 + QScopedPointer surface(Test::createSurface()); + Test::waylandServerSideDecoration()->create(surface.data(), surface.data()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QVERIFY(c->isDecorated()); + + // destroy physical size + KWaylandServer::Display *display = waylandServer()->display(); + auto output = display->outputs().first(); + output->setPhysicalSize(QSize(0, 0)); + // and fake a cursor theme change, so that the theme gets recreated + emit KWin::Cursors::self()->mouse()->themeChanged(); + + KWin::Cursors::self()->mouse()->setPos(QPoint(c->frameGeometry().center().x(), c->clientPos().y() / 2)); +} + +WAYLANDTEST_MAIN(DontCrashCursorPhysicalSizeEmpty) +#include "dont_crash_cursor_physical_size_empty.moc" diff --git a/autotests/integration/dont_crash_empty_deco.cpp b/autotests/integration/dont_crash_empty_deco.cpp new file mode 100644 index 0000000..9f0e354 --- /dev/null +++ b/autotests/integration/dont_crash_empty_deco.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 "platform.h" +#include "x11client.h" +#include "composite.h" +#include "cursor.h" +#include "scene.h" +#include "screenedge.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include + +#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() +{ + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + // this test needs to enforce OpenGL compositing to get into the crashy condition + qputenv("KWIN_COMPOSE", QByteArrayLiteral("O2")); + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + setenv("QT_QPA_PLATFORM", "wayland", true); + waylandServer()->initWorkspace(); + + auto scene = KWin::Compositor::self()->scene(); + QVERIFY(scene); + QCOMPARE(scene->compositingType(), KWin::OpenGL2Compositing); +} + +void DontCrashEmptyDecorationTest::init() +{ + screens()->setCurrent(0); + Cursors::self()->mouse()->setPos(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 + xcb_connection_t *c = xcb_connect(nullptr, nullptr); + QVERIFY(!xcb_connection_has_error(c)); + + xcb_window_t w = xcb_generate_id(c); + xcb_create_window(c, XCB_COPY_FROM_PARENT, w, rootWindow(), 0, 0, 10, 10, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); + xcb_map_window(c, w); + xcb_flush(c); + + // we should get a client for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->window(), w); + QVERIFY(client->isDecorated()); + + // let's set a stupid geometry + client->setFrameGeometry({0, 0, 0, 0}); + QCOMPARE(client->frameGeometry(), QRect(0, 0, 0, 0)); + + // and destroy the window again + xcb_unmap_window(c, w); + xcb_destroy_window(c, w); + xcb_flush(c); + xcb_disconnect(c); + + QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + QVERIFY(windowClosedSpy.wait()); +} + +} + +WAYLANDTEST_MAIN(KWin::DontCrashEmptyDecorationTest) +#include "dont_crash_empty_deco.moc" diff --git a/autotests/integration/dont_crash_glxgears.cpp b/autotests/integration/dont_crash_glxgears.cpp new file mode 100644 index 0000000..d9554d8 --- /dev/null +++ b/autotests/integration/dont_crash_glxgears.cpp @@ -0,0 +1,95 @@ +/* + 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 "platform.h" +#include "abstract_client.h" +#include "x11client.h" +#include "deleted.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" + +#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(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); +} + +void DontCrashGlxgearsTest::testGlxgears() +{ + // closing a glxgears window through Aurorae themes used to crash KWin + // Let's make sure that doesn't happen anymore + + QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(clientAddedSpy.isValid()); + + QProcess glxgears; + glxgears.setProgram(QStringLiteral("glxgears")); + glxgears.start(); + QVERIFY(glxgears.waitForStarted()); + + QVERIFY(clientAddedSpy.wait()); + QCOMPARE(clientAddedSpy.count(), 1); + QCOMPARE(workspace()->clientList().count(), 1); + X11Client *glxgearsClient = workspace()->clientList().first(); + QVERIFY(glxgearsClient->isDecorated()); + QSignalSpy closedSpy(glxgearsClient, &X11Client::windowClosed); + QVERIFY(closedSpy.isValid()); + KDecoration2::Decoration *decoration = glxgearsClient->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/autotests/integration/dont_crash_no_border.cpp b/autotests/integration/dont_crash_no_border.cpp new file mode 100644 index 0000000..50422f5 --- /dev/null +++ b/autotests/integration/dont_crash_no_border.cpp @@ -0,0 +1,112 @@ + +/* + 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 "platform.h" +#include "x11client.h" +#include "composite.h" +#include "cursor.h" +#include "scene.h" +#include "screenedge.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include + +#include +#include + +#include + +#include + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_dont_crash_no_border-0"); + +class DontCrashNoBorder : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + void testCreateWindow(); +}; + +void DontCrashNoBorder::initTestCase() +{ + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("org.kde.kdecoration2").writeEntry("NoPlugin", true); + 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(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + setenv("QT_QPA_PLATFORM", "wayland", true); + waylandServer()->initWorkspace(); + + auto scene = KWin::Compositor::self()->scene(); + QVERIFY(scene); + QCOMPARE(scene->compositingType(), KWin::OpenGL2Compositing); +} + +void DontCrashNoBorder::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Decoration)); + + screens()->setCurrent(0); + Cursors::self()->mouse()->setPos(QPoint(640, 512)); +} + +void DontCrashNoBorder::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void DontCrashNoBorder::testCreateWindow() +{ + // create a window and ensure that this doesn't crash + using namespace KWayland::Client; + + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(shellSurface); + QScopedPointer deco(Test::waylandServerSideDecoration()->create(surface.data())); + QSignalSpy decoSpy(deco.data(), &ServerSideDecoration::modeChanged); + QVERIFY(decoSpy.isValid()); + QVERIFY(decoSpy.wait()); + deco->requestMode(ServerSideDecoration::Mode::Server); + QVERIFY(decoSpy.wait()); + QCOMPARE(deco->mode(), ServerSideDecoration::Mode::Server); + // let's render + auto c = Test::renderAndWaitForShown(surface.data(), QSize(500, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(workspace()->activeClient(), c); + QVERIFY(!c->isDecorated()); +} + +} + +WAYLANDTEST_MAIN(KWin::DontCrashNoBorder) +#include "dont_crash_no_border.moc" diff --git a/autotests/integration/dont_crash_reinitialize_compositor.cpp b/autotests/integration/dont_crash_reinitialize_compositor.cpp new file mode 100644 index 0000000..7e3c1f8 --- /dev/null +++ b/autotests/integration/dont_crash_reinitialize_compositor.cpp @@ -0,0 +1,156 @@ +/* + 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 "abstract_client.h" +#include "composite.h" +#include "deleted.h" +#include "effectloader.h" +#include "effects.h" +#include "platform.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" + +#include "effect_builtins.h" + +#include +#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() +{ + qputenv("XDG_DATA_DIRS", QCoreApplication::applicationDirPath().toUtf8()); + + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup plugins(config, QStringLiteral("Plugins")); + ScriptedEffectLoader loader; + const auto builtinNames = BuiltInEffects::availableEffectNames() << loader.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(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + waylandServer()->initWorkspace(); + + auto scene = KWin::Compositor::self()->scene(); + QVERIFY(scene); + QCOMPARE(scene->compositingType(), KWin::OpenGL2Compositing); +} + +void DontCrashReinitializeCompositorTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); +} + +void DontCrashReinitializeCompositorTest::cleanup() +{ + // Unload all effects. + auto effectsImpl = qobject_cast(effects); + QVERIFY(effectsImpl); + effectsImpl->unloadAllEffects(); + QVERIFY(effectsImpl->loadedEffects().isEmpty()); + + Test::destroyWaylandConnection(); +} + +void DontCrashReinitializeCompositorTest::testReinitializeCompositor_data() +{ + QTest::addColumn("effectName"); + + QTest::newRow("Fade") << QStringLiteral("kwin4_effect_fade"); + QTest::newRow("Glide") << QStringLiteral("glide"); + QTest::newRow("Scale") << QStringLiteral("kwin4_effect_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. + + // Make sure that we have the right effects ptr. + auto effectsImpl = qobject_cast(effects); + QVERIFY(effectsImpl); + + // Create the test client. + using namespace KWayland::Client; + + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + + // Make sure that only the test effect is loaded. + QFETCH(QString, effectName); + QVERIFY(effectsImpl->loadEffect(effectName)); + QCOMPARE(effectsImpl->loadedEffects().count(), 1); + QCOMPARE(effectsImpl->loadedEffects().first(), effectName); + Effect *effect = effectsImpl->findEffect(effectName); + QVERIFY(effect); + QVERIFY(!effect->isActive()); + + // Close the test client. + QSignalSpy windowClosedSpy(client, &AbstractClient::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + shellSurface.reset(); + surface.reset(); + QVERIFY(windowClosedSpy.wait()); + + // The test effect should start animating the test client. Is there a better + // way to verify that the test effect actually animates the test client? + 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/autotests/integration/dont_crash_useractions_menu.cpp b/autotests/integration/dont_crash_useractions_menu.cpp new file mode 100644 index 0000000..c7dbede --- /dev/null +++ b/autotests/integration/dont_crash_useractions_menu.cpp @@ -0,0 +1,104 @@ +/* + 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 "abstract_client.h" +#include "cursor.h" +#include "keyboard_input.h" +#include "platform.h" +#include "pointer_input.h" +#include "screens.h" +#include "useractions.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace KWin; +using namespace KWayland::Client; + +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(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + // force style to breeze as that's the one which triggered the crash + QVERIFY(kwinApp()->setStyle(QStringLiteral("breeze"))); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + waylandServer()->initWorkspace(); +} + +void TestDontCrashUseractionsMenu::init() +{ + QVERIFY(Test::setupWaylandConnection()); + + screens()->setCurrent(0); + KWin::Cursors::self()->mouse()->setPos(QPoint(1280, 512)); +} + +void TestDontCrashUseractionsMenu::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void TestDontCrashUseractionsMenu::testShowHideShowUseractionsMenu() +{ + // this test creates the condition of BUG 382063 + QScopedPointer surface1(Test::createSurface()); + QScopedPointer shellSurface1(Test::createXdgShellStableSurface(surface1.data())); + auto client = Test::renderAndWaitForShown(surface1.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + + workspace()->showWindowMenu(QRect(), client); + auto userActionsMenu = workspace()->userActionsMenu(); + QTRY_VERIFY(userActionsMenu->isShown()); + QVERIFY(userActionsMenu->hasClient()); + + kwinApp()->platform()->keyboardKeyPressed(KEY_ESC, 0); + kwinApp()->platform()->keyboardKeyReleased(KEY_ESC, 1); + QTRY_VERIFY(!userActionsMenu->isShown()); + QVERIFY(!userActionsMenu->hasClient()); + + // and show again, this triggers BUG 382063 + workspace()->showWindowMenu(QRect(), client); + QTRY_VERIFY(userActionsMenu->isShown()); + QVERIFY(userActionsMenu->hasClient()); +} + +WAYLANDTEST_MAIN(TestDontCrashUseractionsMenu) +#include "dont_crash_useractions_menu.moc" diff --git a/autotests/integration/effects/CMakeLists.txt b/autotests/integration/effects/CMakeLists.txt new file mode 100644 index 0000000..da9eb2a --- /dev/null +++ b/autotests/integration/effects/CMakeLists.txt @@ -0,0 +1,12 @@ +if (XCB_ICCCM_FOUND) + integrationTest(NAME testTranslucency SRCS translucency_test.cpp LIBS XCB::ICCCM) + integrationTest(NAME testSlidingPopups SRCS slidingpopups_test.cpp LIBS XCB::ICCCM) + integrationTest(NAME testShadeWobblyWindows SRCS wobbly_shade_test.cpp LIBS XCB::ICCCM) +endif() +integrationTest(WAYLAND_ONLY NAME testEffectWindowGeometry SRCS windowgeometry_test.cpp) +integrationTest(NAME testScriptedEffects SRCS scripted_effects_test.cpp) +integrationTest(WAYLAND_ONLY NAME testToplevelOpenCloseAnimation SRCS toplevel_open_close_animation_test.cpp) +integrationTest(WAYLAND_ONLY NAME testPopupOpenCloseAnimation SRCS popup_open_close_animation_test.cpp) +integrationTest(WAYLAND_ONLY NAME testDesktopSwitchingAnimation SRCS desktop_switching_animation_test.cpp) +integrationTest(WAYLAND_ONLY NAME testMinimizeAnimation SRCS minimize_animation_test.cpp) +integrationTest(WAYLAND_ONLY NAME testMaximizeAnimation SRCS maximize_animation_test.cpp) diff --git a/autotests/integration/effects/desktop_switching_animation_test.cpp b/autotests/integration/effects/desktop_switching_animation_test.cpp new file mode 100644 index 0000000..bf94bde --- /dev/null +++ b/autotests/integration/effects/desktop_switching_animation_test.cpp @@ -0,0 +1,150 @@ +/* + 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 "abstract_client.h" +#include "composite.h" +#include "effectloader.h" +#include "effects.h" +#include "platform.h" +#include "scene.h" +#include "wayland_server.h" +#include "workspace.h" + +#include "effect_builtins.h" + +#include +#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() +{ + qputenv("XDG_DATA_DIRS", QCoreApplication::applicationDirPath().toUtf8()); + + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup plugins(config, QStringLiteral("Plugins")); + ScriptedEffectLoader loader; + const auto builtinNames = BuiltInEffects::availableEffectNames() << loader.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(); + QVERIFY(applicationStartedSpy.wait()); + waylandServer()->initWorkspace(); + + auto scene = Compositor::self()->scene(); + QVERIFY(scene); + QCOMPARE(scene->compositingType(), OpenGL2Compositing); +} + +void DesktopSwitchingAnimationTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); +} + +void DesktopSwitchingAnimationTest::cleanup() +{ + auto effectsImpl = qobject_cast(effects); + QVERIFY(effectsImpl); + effectsImpl->unloadAllEffects(); + QVERIFY(effectsImpl->loadedEffects().isEmpty()); + + VirtualDesktopManager::self()->setCount(1); + + Test::destroyWaylandConnection(); +} + +void DesktopSwitchingAnimationTest::testSwitchDesktops_data() +{ + QTest::addColumn("effectName"); + + QTest::newRow("Desktop Cube Animation") << QStringLiteral("cubeslide"); + QTest::newRow("Fade Desktop") << QStringLiteral("kwin4_effect_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 clients to fade, + // so we have to create a dummy test client. + using namespace KWayland::Client; + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + QCOMPARE(client->desktops().count(), 1); + QCOMPARE(client->desktops().first(), VirtualDesktopManager::self()->desktops().first()); + + // Load effect that will be tested. + QFETCH(QString, effectName); + auto effectsImpl = qobject_cast(effects); + QVERIFY(effectsImpl); + QVERIFY(effectsImpl->loadEffect(effectName)); + QCOMPARE(effectsImpl->loadedEffects().count(), 1); + QCOMPARE(effectsImpl->loadedEffects().first(), effectName); + Effect *effect = effectsImpl->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 client. + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +WAYLANDTEST_MAIN(DesktopSwitchingAnimationTest) +#include "desktop_switching_animation_test.moc" diff --git a/autotests/integration/effects/maximize_animation_test.cpp b/autotests/integration/effects/maximize_animation_test.cpp new file mode 100644 index 0000000..0b81e35 --- /dev/null +++ b/autotests/integration/effects/maximize_animation_test.cpp @@ -0,0 +1,189 @@ +/* + 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 "abstract_client.h" +#include "composite.h" +#include "effectloader.h" +#include "effects.h" +#include "platform.h" +#include "scene.h" +#include "wayland_server.h" +#include "workspace.h" + +#include "effect_builtins.h" + +#include +#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() +{ + qputenv("XDG_DATA_DIRS", QCoreApplication::applicationDirPath().toUtf8()); + + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup plugins(config, QStringLiteral("Plugins")); + ScriptedEffectLoader loader; + const auto builtinNames = BuiltInEffects::availableEffectNames() << loader.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(); + QVERIFY(applicationStartedSpy.wait()); + waylandServer()->initWorkspace(); +} + +void MaximizeAnimationTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); +} + +void MaximizeAnimationTest::cleanup() +{ + auto effectsImpl = qobject_cast(effects); + QVERIFY(effectsImpl); + effectsImpl->unloadAllEffects(); + QVERIFY(effectsImpl->loadedEffects().isEmpty()); + + Test::destroyWaylandConnection(); +} + +void MaximizeAnimationTest::testMaximizeRestore() +{ + // This test verifies that the maximize effect animates a client + // when it's maximized or restored. + + using namespace KWayland::Client; + + // Create the test client. + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data(), nullptr, Test::CreationSetup::CreateOnly)); + + // Wait for the initial configure event. + XdgShellSurface::States states; + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + + surface->commit(Surface::CommitFlag::None); + + QVERIFY(configureRequestedSpy.isValid()); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 1); + QCOMPARE(configureRequestedSpy.last().at(0).value(), QSize(0, 0)); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); + + // Draw contents of the surface. + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); + + // We should receive a configure event when the client becomes active. + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 2); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); + + // Load effect that will be tested. + const QString effectName = QStringLiteral("kwin4_effect_maximize"); + auto effectsImpl = qobject_cast(effects); + QVERIFY(effectsImpl); + QVERIFY(effectsImpl->loadEffect(effectName)); + QCOMPARE(effectsImpl->loadedEffects().count(), 1); + QCOMPARE(effectsImpl->loadedEffects().first(), effectName); + Effect *effect = effectsImpl->findEffect(effectName); + QVERIFY(effect); + QVERIFY(!effect->isActive()); + + // Maximize the client. + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); + QSignalSpy maximizeChangedSpy(client, qOverload(&AbstractClient::clientMaximizedStateChanged)); + QVERIFY(maximizeChangedSpy.isValid()); + + workspace()->slotWindowMaximize(); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 3); + QCOMPARE(configureRequestedSpy.last().at(0).value(), QSize(1280, 1024)); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); + + // Draw contents of the maximized client. + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + Test::render(surface.data(), QSize(1280, 1024), Qt::red); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(frameGeometryChangedSpy.count(), 1); + QCOMPARE(maximizeChangedSpy.count(), 1); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); + QVERIFY(effect->isActive()); + + // Eventually, the animation will be complete. + QTRY_VERIFY(!effect->isActive()); + + // Restore the client. + workspace()->slotWindowMaximize(); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 4); + QCOMPARE(configureRequestedSpy.last().at(0).value(), QSize(100, 50)); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); + + // Draw contents of the restored client. + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + Test::render(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(frameGeometryChangedSpy.count(), 2); + QCOMPARE(maximizeChangedSpy.count(), 2); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); + QVERIFY(effect->isActive()); + + // Eventually, the animation will be complete. + QTRY_VERIFY(!effect->isActive()); + + // Destroy the test client. + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +WAYLANDTEST_MAIN(MaximizeAnimationTest) +#include "maximize_animation_test.moc" diff --git a/autotests/integration/effects/minimize_animation_test.cpp b/autotests/integration/effects/minimize_animation_test.cpp new file mode 100644 index 0000000..386f1fd --- /dev/null +++ b/autotests/integration/effects/minimize_animation_test.cpp @@ -0,0 +1,185 @@ +/* + 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 "abstract_client.h" +#include "composite.h" +#include "effectloader.h" +#include "effects.h" +#include "platform.h" +#include "scene.h" +#include "wayland_server.h" +#include "workspace.h" + +#include "effect_builtins.h" + +#include +#include +#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() +{ + qputenv("XDG_DATA_DIRS", QCoreApplication::applicationDirPath().toUtf8()); + + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup plugins(config, QStringLiteral("Plugins")); + ScriptedEffectLoader loader; + const auto builtinNames = BuiltInEffects::availableEffectNames() << loader.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(); + QVERIFY(applicationStartedSpy.wait()); + waylandServer()->initWorkspace(); + + auto scene = Compositor::self()->scene(); + QVERIFY(scene); + QCOMPARE(scene->compositingType(), OpenGL2Compositing); +} + +void MinimizeAnimationTest::init() +{ + QVERIFY(Test::setupWaylandConnection( + Test::AdditionalWaylandInterface::PlasmaShell | + Test::AdditionalWaylandInterface::WindowManagement + )); +} + +void MinimizeAnimationTest::cleanup() +{ + auto effectsImpl = qobject_cast(effects); + QVERIFY(effectsImpl); + effectsImpl->unloadAllEffects(); + QVERIFY(effectsImpl->loadedEffects().isEmpty()); + + Test::destroyWaylandConnection(); +} + +void MinimizeAnimationTest::testMinimizeUnminimize_data() +{ + QTest::addColumn("effectName"); + + QTest::newRow("Magic Lamp") << QStringLiteral("magiclamp"); + QTest::newRow("Squash") << QStringLiteral("kwin4_effect_squash"); +} + +void MinimizeAnimationTest::testMinimizeUnminimize() +{ + // This test verifies that a minimize effect tries to animate a client + // when it's minimized or unminimized. + + using namespace KWayland::Client; + + QSignalSpy plasmaWindowCreatedSpy(Test::waylandWindowManagement(), &PlasmaWindowManagement::windowCreated); + QVERIFY(plasmaWindowCreatedSpy.isValid()); + + // Create a panel at the top of the screen. + const QRect panelRect = QRect(0, 0, 1280, 36); + QScopedPointer panelSurface(Test::createSurface()); + QVERIFY(!panelSurface.isNull()); + QScopedPointer panelShellSurface(Test::createXdgShellStableSurface(panelSurface.data())); + QVERIFY(!panelShellSurface.isNull()); + QScopedPointer plasmaPanelShellSurface(Test::waylandPlasmaShell()->createSurface(panelSurface.data())); + QVERIFY(!plasmaPanelShellSurface.isNull()); + plasmaPanelShellSurface->setRole(PlasmaShellSurface::Role::Panel); + plasmaPanelShellSurface->setPosition(panelRect.topLeft()); + plasmaPanelShellSurface->setPanelBehavior(PlasmaShellSurface::PanelBehavior::AlwaysVisible); + AbstractClient *panel = Test::renderAndWaitForShown(panelSurface.data(), panelRect.size(), Qt::blue); + QVERIFY(panel); + QVERIFY(panel->isDock()); + QCOMPARE(panel->frameGeometry(), panelRect); + QVERIFY(plasmaWindowCreatedSpy.wait()); + QCOMPARE(plasmaWindowCreatedSpy.count(), 1); + + // Create the test client. + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::red); + QVERIFY(client); + QVERIFY(plasmaWindowCreatedSpy.wait()); + QCOMPARE(plasmaWindowCreatedSpy.count(), 2); + + // We have to set the minimized geometry because the squash effect needs it, + // otherwise it won't start animation. + auto window = plasmaWindowCreatedSpy.last().first().value(); + QVERIFY(window); + const QRect iconRect = QRect(0, 0, 42, 36); + window->setMinimizedGeometry(panelSurface.data(), iconRect); + Test::flushWaylandConnection(); + QTRY_COMPARE(client->iconGeometry(), iconRect.translated(panel->frameGeometry().topLeft())); + + // Load effect that will be tested. + QFETCH(QString, effectName); + auto effectsImpl = qobject_cast(effects); + QVERIFY(effectsImpl); + QVERIFY(effectsImpl->loadEffect(effectName)); + QCOMPARE(effectsImpl->loadedEffects().count(), 1); + QCOMPARE(effectsImpl->loadedEffects().first(), effectName); + Effect *effect = effectsImpl->findEffect(effectName); + QVERIFY(effect); + QVERIFY(!effect->isActive()); + + // Start the minimize animation. + client->minimize(); + QVERIFY(effect->isActive()); + + // Eventually, the animation will be complete. + QTRY_VERIFY(!effect->isActive()); + + // Start the unminimize animation. + client->unminimize(); + QVERIFY(effect->isActive()); + + // Eventually, the animation will be complete. + QTRY_VERIFY(!effect->isActive()); + + // Destroy the panel. + panelSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(panel)); + + // Destroy the test client. + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +WAYLANDTEST_MAIN(MinimizeAnimationTest) +#include "minimize_animation_test.moc" diff --git a/autotests/integration/effects/popup_open_close_animation_test.cpp b/autotests/integration/effects/popup_open_close_animation_test.cpp new file mode 100644 index 0000000..b68ed8b --- /dev/null +++ b/autotests/integration/effects/popup_open_close_animation_test.cpp @@ -0,0 +1,267 @@ +/* + 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 "abstract_client.h" +#include "deleted.h" +#include "effectloader.h" +#include "effects.h" +#include "internal_client.h" +#include "platform.h" +#include "useractions.h" +#include "wayland_server.h" +#include "workspace.h" + +#include "decorations/decoratedclient.h" + +#include "effect_builtins.h" + +#include +#include +#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(); + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup plugins(config, QStringLiteral("Plugins")); + ScriptedEffectLoader loader; + const auto builtinNames = BuiltInEffects::availableEffectNames() << loader.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(); + QVERIFY(applicationStartedSpy.wait()); + waylandServer()->initWorkspace(); +} + +void PopupOpenCloseAnimationTest::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::XdgDecoration)); +} + +void PopupOpenCloseAnimationTest::cleanup() +{ + auto effectsImpl = qobject_cast(effects); + QVERIFY(effectsImpl); + effectsImpl->unloadAllEffects(); + QVERIFY(effectsImpl->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). + + // Make sure that we have the right effects ptr. + auto effectsImpl = qobject_cast(effects); + QVERIFY(effectsImpl); + + // Create the main window. + using namespace KWayland::Client; + QScopedPointer mainWindowSurface(Test::createSurface()); + QVERIFY(!mainWindowSurface.isNull()); + QScopedPointer mainWindowShellSurface(Test::createXdgShellStableSurface(mainWindowSurface.data())); + QVERIFY(!mainWindowShellSurface.isNull()); + AbstractClient *mainWindow = Test::renderAndWaitForShown(mainWindowSurface.data(), QSize(100, 50), Qt::blue); + QVERIFY(mainWindow); + + // Load effect that will be tested. + const QString effectName = QStringLiteral("kwin4_effect_fadingpopups"); + QVERIFY(effectsImpl->loadEffect(effectName)); + QCOMPARE(effectsImpl->loadedEffects().count(), 1); + QCOMPARE(effectsImpl->loadedEffects().first(), effectName); + Effect *effect = effectsImpl->findEffect(effectName); + QVERIFY(effect); + QVERIFY(!effect->isActive()); + + // Create a popup, it should be animated. + QScopedPointer popupSurface(Test::createSurface()); + QVERIFY(!popupSurface.isNull()); + XdgPositioner positioner(QSize(20, 20), QRect(0, 0, 10, 10)); + positioner.setGravity(Qt::BottomEdge | Qt::RightEdge); + positioner.setAnchorEdge(Qt::BottomEdge | Qt::LeftEdge); + QScopedPointer popupShellSurface(Test::createXdgShellStablePopup(popupSurface.data(), mainWindowShellSurface.data(), positioner)); + QVERIFY(!popupShellSurface.isNull()); + AbstractClient *popup = Test::renderAndWaitForShown(popupSurface.data(), positioner.initialSize(), 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, &AbstractClient::windowClosed); + QVERIFY(popupClosedSpy.isValid()); + 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::waitForWindowDestroyed(mainWindow)); +} + +void PopupOpenCloseAnimationTest::testAnimateUserActionsPopup() +{ + // This test verifies that popup open/close animation effects try + // to animate the user actions popup. + + // Make sure that we have the right effects ptr. + auto effectsImpl = qobject_cast(effects); + QVERIFY(effectsImpl); + + // Create the test client. + using namespace KWayland::Client; + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + + // Load effect that will be tested. + const QString effectName = QStringLiteral("kwin4_effect_fadingpopups"); + QVERIFY(effectsImpl->loadEffect(effectName)); + QCOMPARE(effectsImpl->loadedEffects().count(), 1); + QCOMPARE(effectsImpl->loadedEffects().first(), effectName); + Effect *effect = effectsImpl->findEffect(effectName); + QVERIFY(effect); + QVERIFY(!effect->isActive()); + + // Show the user actions popup. + workspace()->showWindowMenu(QRect(), client); + auto userActionsMenu = workspace()->userActionsMenu(); + QTRY_VERIFY(userActionsMenu->isShown()); + QVERIFY(userActionsMenu->hasClient()); + QVERIFY(effect->isActive()); + + // Eventually, the animation will be complete. + QTRY_VERIFY(!effect->isActive()); + + // Close the user actions popup. + kwinApp()->platform()->keyboardKeyPressed(KEY_ESC, 0); + kwinApp()->platform()->keyboardKeyReleased(KEY_ESC, 1); + QTRY_VERIFY(!userActionsMenu->isShown()); + QVERIFY(!userActionsMenu->hasClient()); + QVERIFY(effect->isActive()); + + // Eventually, the animation will be complete. + QTRY_VERIFY(!effect->isActive()); + + // Destroy the test client. + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void PopupOpenCloseAnimationTest::testAnimateDecorationTooltips() +{ + // This test verifies that popup open/close animation effects try + // to animate decoration tooltips. + + // Make sure that we have the right effects ptr. + auto effectsImpl = qobject_cast(effects); + QVERIFY(effectsImpl); + + // Create the test client. + using namespace KWayland::Client; + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + QScopedPointer deco(Test::xdgDecorationManager()->getToplevelDecoration(shellSurface.data())); + QVERIFY(!deco.isNull()); + deco->setMode(XdgDecoration::Mode::ServerSide); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + QVERIFY(client->isDecorated()); + + // Load effect that will be tested. + const QString effectName = QStringLiteral("kwin4_effect_fadingpopups"); + QVERIFY(effectsImpl->loadEffect(effectName)); + QCOMPARE(effectsImpl->loadedEffects().count(), 1); + QCOMPARE(effectsImpl->loadedEffects().first(), effectName); + Effect *effect = effectsImpl->findEffect(effectName); + QVERIFY(effect); + QVERIFY(!effect->isActive()); + + // Show a decoration tooltip. + QSignalSpy tooltipAddedSpy(workspace(), &Workspace::internalClientAdded); + QVERIFY(tooltipAddedSpy.isValid()); + client->decoratedClient()->requestShowToolTip(QStringLiteral("KWin rocks!")); + QVERIFY(tooltipAddedSpy.wait()); + InternalClient *tooltip = tooltipAddedSpy.first().first().value(); + QVERIFY(tooltip->isInternal()); + QVERIFY(tooltip->isPopupWindow()); + QVERIFY(tooltip->internalWindow()->flags().testFlag(Qt::ToolTip)); + QVERIFY(effect->isActive()); + + // Eventually, the animation will be complete. + QTRY_VERIFY(!effect->isActive()); + + // Hide the decoration tooltip. + QSignalSpy tooltipClosedSpy(tooltip, &InternalClient::windowClosed); + QVERIFY(tooltipClosedSpy.isValid()); + client->decoratedClient()->requestHideToolTip(); + QVERIFY(tooltipClosedSpy.wait()); + QVERIFY(effect->isActive()); + + // Eventually, the animation will be complete. + QTRY_VERIFY(!effect->isActive()); + + // Destroy the test client. + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +WAYLANDTEST_MAIN(PopupOpenCloseAnimationTest) +#include "popup_open_close_animation_test.moc" diff --git a/autotests/integration/effects/scripted_effects_test.cpp b/autotests/integration/effects/scripted_effects_test.cpp new file mode 100644 index 0000000..e851500 --- /dev/null +++ b/autotests/integration/effects/scripted_effects_test.cpp @@ -0,0 +1,780 @@ +/* + 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 "scripting/scriptedeffect.h" +#include "libkwineffects/anidata_p.h" + +#include "abstract_client.h" +#include "composite.h" +#include "cursor.h" +#include "deleted.h" +#include "effect_builtins.h" +#include "effectloader.h" +#include "effects.h" +#include "kwin_wayland_test.h" +#include "platform.h" +#include "virtualdesktops.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include +#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; +signals: + void testOutput(const QString &data); +}; + +QScriptValue kwinEffectScriptTestOut(QScriptContext *context, QScriptEngine *engine) +{ + auto *script = qobject_cast(context->callee().data().toQObject()); + QString result; + for (int i = 0; i < context->argumentCount(); ++i) { + if (i > 0) { + result.append(QLatin1Char(' ')); + } + result.append(context->argument(i).toString()); + } + emit script->testOutput(result); + + return engine->undefinedValue(); +} + +ScriptedEffectWithDebugSpy::ScriptedEffectWithDebugSpy() + : ScriptedEffect() +{ + QScriptValue testHookFunc = engine()->newFunction(kwinEffectScriptTestOut); + testHookFunc.setData(engine()->newQObject(this)); + engine()->globalObject().setProperty(QStringLiteral("sendTestResponse"), testHookFunc); +} + +bool ScriptedEffectWithDebugSpy::load(const QString &name) +{ + const QString path = QFINDTESTDATA("./scripts/" + name + ".js"); + if (!init(name, path)) { + return false; + } + + // inject our newly created effect to be registered with the EffectsHandlerImpl::loaded_effects + // this is private API so some horrible code is used to find the internal effectloader + // and register ourselves + auto c = effects->children(); + for (auto it = c.begin(); it != c.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 (static_cast(effects)->isEffectLoaded(name)); +} + +void ScriptedEffectsTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + ScriptedEffectLoader loader; + + // 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 = BuiltInEffects::availableEffectNames() << loader.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(); + QVERIFY(applicationStartedSpy.wait()); + QVERIFY(Compositor::self()); + + auto scene = KWin::Compositor::self()->scene(); + QVERIFY(scene); + QCOMPARE(scene->compositingType(), KWin::OpenGL2Compositing); + + KWin::VirtualDesktopManager::self()->setCount(2); +} + +void ScriptedEffectsTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); +} + +void ScriptedEffectsTest::cleanup() +{ + Test::destroyWaylandConnection(); + + auto effectsImpl = static_cast(effects); + effectsImpl->unloadAllEffects(); + QVERIFY(effectsImpl->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 + using namespace KWayland::Client; + auto *surface = Test::createSurface(Test::waylandCompositor()); + QVERIFY(surface); + auto *shellSurface = Test::createXdgShellStableSurface(surface, surface); + QVERIFY(shellSurface); + shellSurface->setTitle("WindowA"); + auto *c = Test::renderAndWaitForShown(surface, QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(workspace()->activeClient(), c); + + waitFor("windowAdded - WindowA"); + waitFor("stackingOrder - 1 WindowA"); + + // windowMinimsed + c->minimize(); + waitFor("windowMinimized - WindowA"); + + c->unminimize(); + waitFor("windowUnminimized - WindowA"); + + surface->deleteLater(); + 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() +{ + // this tests method registerShortcut + auto *effect = new ScriptedEffectWithDebugSpy; // cleaned up in ::clean + QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput); + QVERIFY(effect->load("shortcutsTest")); + QCOMPARE(effect->shortcutCallbacks().count(), 1); + QAction *action = effect->shortcutCallbacks().keys()[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 + using namespace KWayland::Client; + auto *surface = Test::createSurface(Test::waylandCompositor()); + QVERIFY(surface); + auto *shellSurface = Test::createXdgShellStableSurface(surface, surface); + QVERIFY(shellSurface); + shellSurface->setTitle("Window 1"); + auto *c = Test::renderAndWaitForShown(surface, QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(workspace()->activeClient(), c); + + { + const auto state = effect->state(); + QCOMPARE(state.count(), 1); + QCOMPARE(state.firstKey(), c->effectWindow()); + const auto &animationsForWindow = state.first().first; + QCOMPARE(animationsForWindow.count(), 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.count(), 1); + const auto &animationsForWindow = state.first().first; + QCOMPARE(animationsForWindow.count(), 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.count(), 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")); + auto actions = effect->findChildren(QString(), Qt::FindDirectChildrenOnly); + 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); + + using namespace KWayland::Client; + auto *surface = Test::createSurface(Test::waylandCompositor()); + QVERIFY(surface); + auto *shellSurface = Test::createXdgShellStableSurface(surface, surface); + QVERIFY(shellSurface); + shellSurface->setTitle("Window 1"); + auto *c = Test::renderAndWaitForShown(surface, QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(workspace()->activeClient(), 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(effectOutputSpy.isValid()); + QVERIFY(effect->load(file)); + + // create a window + using namespace KWayland::Client; + auto *surface = Test::createSurface(Test::waylandCompositor()); + QVERIFY(surface); + auto *shellSurface = Test::createXdgShellStableSurface(surface, surface); + QVERIFY(shellSurface); + auto *c = Test::renderAndWaitForShown(surface, QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(workspace()->activeClient(), c); + + // no active animations at the beginning + QCOMPARE(effect->state().count(), 0); + + // trigger windowClosed signal + surface->deleteLater(); + QVERIFY(effectOutputSpy.count() == 1 || effectOutputSpy.wait()); + + if (keepAlive) { + QCOMPARE(effect->state().count(), 1); + + QTest::qWait(500); + QCOMPARE(effect->state().count(), 1); + + QTest::qWait(500 + 100); // 100ms is extra safety margin + QCOMPARE(effect->state().count(), 0); + } else { + // the test effect doesn't keep the window alive, so it should be + // removed immediately + QSignalSpy deletedRemovedSpy(workspace(), &Workspace::deletedRemoved); + QVERIFY(deletedRemovedSpy.isValid()); + QVERIFY(deletedRemovedSpy.count() == 1 || deletedRemovedSpy.wait(100)); // 100ms is less than duration of the animation + QCOMPARE(effect->state().count(), 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(effectOutputSpy.isValid()); + QVERIFY(effect->load(QStringLiteral("grabTest"))); + + // create test client + using namespace KWayland::Client; + Surface *surface = Test::createSurface(Test::waylandCompositor()); + QVERIFY(surface); + XdgShellSurface *shellSurface = Test::createXdgShellStableSurface(surface, surface); + QVERIFY(shellSurface); + AbstractClient *c = Test::renderAndWaitForShown(surface, QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(workspace()->activeClient(), c); + + // the test effect should grab the test client successfully + QCOMPARE(effectOutputSpy.count(), 1); + QCOMPARE(effectOutputSpy.first().first(), QStringLiteral("ok")); + QCOMPARE(c->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(ownerOutputSpy.isValid()); + 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(grabberOutputSpy.isValid()); + QVERIFY(grabber->load(QStringLiteral("grabAlreadyGrabbedWindowTest_grabber"))); + + // create test client + using namespace KWayland::Client; + Surface *surface = Test::createSurface(Test::waylandCompositor()); + QVERIFY(surface); + XdgShellSurface *shellSurface = Test::createXdgShellStableSurface(surface, surface); + QVERIFY(shellSurface); + AbstractClient *c = Test::renderAndWaitForShown(surface, QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(workspace()->activeClient(), c); + + // effect that initially held the grab should still hold the grab + QCOMPARE(ownerOutputSpy.count(), 1); + QCOMPARE(ownerOutputSpy.first().first(), QStringLiteral("ok")); + QCOMPARE(c->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(ownerOutputSpy.isValid()); + 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(thiefOutputSpy.isValid()); + QVERIFY(thief->load(QStringLiteral("grabAlreadyGrabbedWindowForcedTest_thief"))); + + // create test client + using namespace KWayland::Client; + Surface *surface = Test::createSurface(Test::waylandCompositor()); + QVERIFY(surface); + XdgShellSurface *shellSurface = Test::createXdgShellStableSurface(surface, surface); + QVERIFY(shellSurface); + AbstractClient *c = Test::renderAndWaitForShown(surface, QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(workspace()->activeClient(), c); + + // verify that the owner in fact held the grab + QCOMPARE(ownerOutputSpy.count(), 1); + QCOMPARE(ownerOutputSpy.first().first(), QStringLiteral("ok")); + + // effect that grabbed the test client forcefully should now hold the grab + QCOMPARE(thiefOutputSpy.count(), 1); + QCOMPARE(thiefOutputSpy.first().first(), QStringLiteral("ok")); + QCOMPARE(c->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(effectOutputSpy.isValid()); + QVERIFY(effect->load(QStringLiteral("ungrabTest"))); + + // create test client + using namespace KWayland::Client; + Surface *surface = Test::createSurface(Test::waylandCompositor()); + QVERIFY(surface); + XdgShellSurface *shellSurface = Test::createXdgShellStableSurface(surface, surface); + QVERIFY(shellSurface); + AbstractClient *c = Test::renderAndWaitForShown(surface, QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(workspace()->activeClient(), c); + + // the test effect should grab the test client successfully + QCOMPARE(effectOutputSpy.count(), 1); + QCOMPARE(effectOutputSpy.first().first(), QStringLiteral("ok")); + QCOMPARE(c->effectWindow()->data(WindowAddedGrabRole).value(), effect); + + // when the test effect sees that a window was minimized, it will try to ungrab it + effectOutputSpy.clear(); + c->setMinimized(true); + + QCOMPARE(effectOutputSpy.count(), 1); + QCOMPARE(effectOutputSpy.first().first(), QStringLiteral("ok")); + QCOMPARE(c->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 client + using namespace KWayland::Client; + Surface *surface = Test::createSurface(Test::waylandCompositor()); + QVERIFY(surface); + XdgShellSurface *shellSurface = Test::createXdgShellStableSurface(surface, surface); + QVERIFY(shellSurface); + AbstractClient *c = Test::renderAndWaitForShown(surface, QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(workspace()->activeClient(), c); + + auto around = [] (std::chrono::milliseconds elapsed, + std::chrono::milliseconds pivot, + std::chrono::milliseconds margin) { + return qAbs(elapsed.count() - pivot.count()) < margin.count(); + }; + + // initially, the test animation is at the source position + + { + const auto state = effect->state(); + QCOMPARE(state.count(), 1); + QCOMPARE(state.firstKey(), c->effectWindow()); + const QList animations = state.first().first; + QCOMPARE(animations.count(), 1); + QCOMPARE(animations[0].timeLine.direction(), TimeLine::Forward); + QVERIFY(around(animations[0].timeLine.elapsed(), 0ms, 50ms)); + } + + // minimize the test client 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); + QVERIFY(effectOutputSpy.isValid()); + + c->setMinimized(true); + + QCOMPARE(effectOutputSpy.count(), 1); + QCOMPARE(effectOutputSpy.first().first(), QStringLiteral("ok")); + + { + const auto state = effect->state(); + QCOMPARE(state.count(), 1); + QCOMPARE(state.firstKey(), c->effectWindow()); + const QList animations = state.first().first; + QCOMPARE(animations.count(), 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.count(), 0); + } else { + const auto state = effect->state(); + QCOMPARE(state.count(), 1); + QCOMPARE(state.firstKey(), c->effectWindow()); + const QList animations = state.first().first; + QCOMPARE(animations.count(), 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 client + using namespace KWayland::Client; + Surface *surface = Test::createSurface(Test::waylandCompositor()); + QVERIFY(surface); + XdgShellSurface *shellSurface = Test::createXdgShellStableSurface(surface, surface); + QVERIFY(shellSurface); + AbstractClient *c = Test::renderAndWaitForShown(surface, QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(workspace()->activeClient(), c); + + auto around = [] (std::chrono::milliseconds elapsed, + std::chrono::milliseconds pivot, + std::chrono::milliseconds margin) { + return qAbs(elapsed.count() - pivot.count()) < margin.count(); + }; + + // initially, the test animation should be at the start position + { + const auto state = effect->state(); + QCOMPARE(state.count(), 1); + QCOMPARE(state.firstKey(), c->effectWindow()); + const QList animations = state.first().first; + QCOMPARE(animations.count(), 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.count(), 1); + QCOMPARE(state.firstKey(), c->effectWindow()); + const QList animations = state.first().first; + QCOMPARE(animations.count(), 1); + QVERIFY(around(animations[0].timeLine.elapsed(), 250ms, 50ms)); + QVERIFY(!animations[0].timeLine.done()); + } + + // minimize the test client, when the test effect sees that a window was + // minimized, it will try to complete animation for it + QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput); + QVERIFY(effectOutputSpy.isValid()); + + c->setMinimized(true); + + QCOMPARE(effectOutputSpy.count(), 1); + QCOMPARE(effectOutputSpy.first().first(), QStringLiteral("ok")); + + { + const auto state = effect->state(); + QCOMPARE(state.count(), 1); + QCOMPARE(state.firstKey(), c->effectWindow()); + const QList animations = state.first().first; + QCOMPARE(animations.count(), 1); + QCOMPARE(animations[0].timeLine.elapsed(), 1000ms); + QVERIFY(animations[0].timeLine.done()); + } +} + +WAYLANDTEST_MAIN(ScriptedEffectsTest) +#include "scripted_effects_test.moc" diff --git a/autotests/integration/effects/scripts/animationTest.js b/autotests/integration/effects/scripts/animationTest.js new file mode 100644 index 0000000..29b9663 --- /dev/null +++ b/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"); +}); + +effects.windowUnminimized.connect(function(w) { + cancel(w.anim1); +}); + +effects.windowMinimized.connect(function(w) { + retarget(w.anim1, 1.5, 200); +}); diff --git a/autotests/integration/effects/scripts/animationTestMulti.js b/autotests/integration/effects/scripts/animationTestMulti.js new file mode 100644 index 0000000..ab77286 --- /dev/null +++ b/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)); +}); + +effects.windowUnminimized.connect(function(w) { + cancel(w.anim1); +}); + +effects.windowMinimized.connect(function(w) { + retarget(w.anim1, 1.5, 200); +}); diff --git a/autotests/integration/effects/scripts/completeTest.js b/autotests/integration/effects/scripts/completeTest.js new file mode 100644 index 0000000..af9a70c --- /dev/null +++ b/autotests/integration/effects/scripts/completeTest.js @@ -0,0 +1,19 @@ +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 + }); +}); + +effects.windowMinimized.connect(function (window) { + if (complete(window.animation)) { + sendTestResponse('ok'); + } else { + sendTestResponse('fail'); + } +}); diff --git a/autotests/integration/effects/scripts/effectContext.js b/autotests/integration/effects/scripts/effectContext.js new file mode 100644 index 0000000..193afab --- /dev/null +++ b/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/autotests/integration/effects/scripts/effectsHandler.js b/autotests/integration/effects/scripts/effectsHandler.js new file mode 100644 index 0000000..661b181 --- /dev/null +++ b/autotests/integration/effects/scripts/effectsHandler.js @@ -0,0 +1,16 @@ +effects.windowAdded.connect(function(window) { + sendTestResponse("windowAdded - " + window.caption); + sendTestResponse("stackingOrder - " + effects.stackingOrder.length + " " + effects.stackingOrder[0].caption); +}); +effects.windowClosed.connect(function(window) { + sendTestResponse("windowClosed - " + window.caption); +}); +effects.windowMinimized.connect(function(window) { + sendTestResponse("windowMinimized - " + window.caption); +}); +effects.windowUnminimized.connect(function(window) { + sendTestResponse("windowUnminimized - " + window.caption); +}); +effects['desktopChanged(int,int)'].connect(function(old, current) { + sendTestResponse("desktopChanged - " + old + " " + current); +}); diff --git a/autotests/integration/effects/scripts/fullScreenEffectTest.js b/autotests/integration/effects/scripts/fullScreenEffectTest.js new file mode 100644 index 0000000..014ef47 --- /dev/null +++ b/autotests/integration/effects/scripts/fullScreenEffectTest.js @@ -0,0 +1,8 @@ +effects['desktopChanged(int,int)'].connect(function(old, current) { + var stackingOrder = effects.stackingOrder; + for (var i=0; i + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" +#include "x11client.h" +#include "composite.h" +#include "deleted.h" +#include "effects.h" +#include "effectloader.h" +#include "cursor.h" +#include "platform.h" +#include "scene.h" +#include "wayland_server.h" +#include "workspace.h" +#include "effect_builtins.h" + +#include + +#include +#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 testWithOtherEffect_data(); + void testWithOtherEffect(); + void testWithOtherEffectWayland_data(); + void testWithOtherEffectWayland(); +}; + +void SlidingPopupsTest::initTestCase() +{ + qputenv("XDG_DATA_DIRS", QCoreApplication::applicationDirPath().toUtf8()); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + // 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")); + ScriptedEffectLoader loader; + const auto builtinNames = BuiltInEffects::availableEffectNames() << loader.listOfKnownEffects(); + for (QString name : builtinNames) { + plugins.writeEntry(name + QStringLiteral("Enabled"), false); + } + KConfigGroup wobblyGroup = config->group("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(); + QVERIFY(applicationStartedSpy.wait()); + QVERIFY(Compositor::self()); + + auto scene = KWin::Compositor::self()->scene(); + QVERIFY(scene); + QCOMPARE(scene->compositingType(), KWin::OpenGL2Compositing); +} + +void SlidingPopupsTest::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Decoration)); +} + +void SlidingPopupsTest::cleanup() +{ + Test::destroyWaylandConnection(); + EffectsHandlerImpl *e = static_cast(effects); + while (!e->loadedEffects().isEmpty()) { + const QString effect = e->loadedEffects().first(); + e->unloadEffect(effect); + QVERIFY(!e->isEffectLoaded(effect)); + } +} + +struct XcbConnectionDeleter +{ + static inline void cleanup(xcb_connection_t *pointer) + { + xcb_disconnect(pointer); + } +}; + + +void SlidingPopupsTest::testWithOtherEffect_data() +{ + QTest::addColumn("effectsToLoad"); + + QTest::newRow("fade, slide") << QStringList{QStringLiteral("kwin4_effect_fade"), QStringLiteral("slidingpopups")}; + QTest::newRow("slide, fade") << QStringList{QStringLiteral("slidingpopups"), QStringLiteral("kwin4_effect_fade")}; + QTest::newRow("scale, slide") << QStringList{QStringLiteral("kwin4_effect_scale"), QStringLiteral("slidingpopups")}; + QTest::newRow("slide, scale") << QStringList{QStringLiteral("slidingpopups"), QStringLiteral("kwin4_effect_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::testWithOtherEffect() +{ + // this test verifies that slidingpopups effect grabs the window added role + // independently of the sequence how the effects are loaded. + // see BUG 336866 + EffectsHandlerImpl *e = static_cast(effects); + // find the effectsloader + auto effectloader = e->findChild(); + QVERIFY(effectloader); + QSignalSpy effectLoadedSpy(effectloader, &AbstractEffectLoader::effectLoaded); + QVERIFY(effectLoadedSpy.isValid()); + + Effect *slidingPoupus = nullptr; + Effect *otherEffect = nullptr; + QFETCH(QStringList, effectsToLoad); + for (const QString &effectName : effectsToLoad) { + QVERIFY(!e->isEffectLoaded(effectName)); + QVERIFY(e->loadEffect(effectName)); + QVERIFY(e->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()); + + // give the compositor some time to render + QTest::qWait(50); + + QSignalSpy windowAddedSpy(effects, &EffectsHandler::windowAdded); + QVERIFY(windowAddedSpy.isValid()); + + // create an xcb window + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + const QRect windowGeometry(0, 0, 100, 200); + xcb_window_t w = xcb_generate_id(c.data()); + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, 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; + memset(&hints, 0, sizeof(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.data(), w, &hints); + NETWinInfo winInfo(c.data(), w, rootWindow(), NET::Properties(), NET::Properties2()); + winInfo.setWindowType(NET::Normal); + + // and get the slide atom + const QByteArray effectAtomName = QByteArrayLiteral("_KDE_SLIDE"); + xcb_intern_atom_cookie_t atomCookie = xcb_intern_atom_unchecked(c.data(), false, effectAtomName.length(), effectAtomName.constData()); + const int size = 2; + int32_t data[size]; + data[0] = 0; + data[1] = 0; + QScopedPointer atom(xcb_intern_atom_reply(c.data(), atomCookie, nullptr)); + QVERIFY(!atom.isNull()); + xcb_change_property(c.data(), XCB_PROP_MODE_REPLACE, w, atom->atom, atom->atom, 32, size, data); + + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + // we should get a client for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->window(), w); + QVERIFY(client->isNormalWindow()); + + // sliding popups should be active + QVERIFY(windowAddedSpy.wait()); + QTRY_VERIFY(slidingPoupus->isActive()); + QVERIFY(!otherEffect->isActive()); + + // wait till effect ends + QTRY_VERIFY(!slidingPoupus->isActive()); + QTest::qWait(300); + QVERIFY(!otherEffect->isActive()); + + // and destroy the window again + xcb_unmap_window(c.data(), w); + xcb_flush(c.data()); + + QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + + QSignalSpy windowDeletedSpy(effects, &EffectsHandler::windowDeleted); + QVERIFY(windowDeletedSpy.isValid()); + QVERIFY(windowClosedSpy.wait()); + + // again we should have the sliding popups active + QVERIFY(slidingPoupus->isActive()); + QVERIFY(!otherEffect->isActive()); + + QVERIFY(windowDeletedSpy.wait()); + + QCOMPARE(windowDeletedSpy.count(), 1); + QTRY_VERIFY(!slidingPoupus->isActive()); + QTest::qWait(300); + QVERIFY(!otherEffect->isActive()); + xcb_destroy_window(c.data(), w); + c.reset(); +} + +void SlidingPopupsTest::testWithOtherEffectWayland_data() +{ + QTest::addColumn("effectsToLoad"); + + QTest::newRow("fade, slide") << QStringList{QStringLiteral("kwin4_effect_fade"), QStringLiteral("slidingpopups")}; + QTest::newRow("slide, fade") << QStringList{QStringLiteral("slidingpopups"), QStringLiteral("kwin4_effect_fade")}; + QTest::newRow("scale, slide") << QStringList{QStringLiteral("kwin4_effect_scale"), QStringLiteral("slidingpopups")}; + QTest::newRow("slide, scale") << QStringList{QStringLiteral("slidingpopups"), QStringLiteral("kwin4_effect_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 + EffectsHandlerImpl *e = static_cast(effects); + // find the effectsloader + auto effectloader = e->findChild(); + QVERIFY(effectloader); + QSignalSpy effectLoadedSpy(effectloader, &AbstractEffectLoader::effectLoaded); + QVERIFY(effectLoadedSpy.isValid()); + + Effect *slidingPoupus = nullptr; + Effect *otherEffect = nullptr; + QFETCH(QStringList, effectsToLoad); + for (const QString &effectName : effectsToLoad) { + QVERIFY(!e->isEffectLoaded(effectName)); + QVERIFY(e->loadEffect(effectName)); + QVERIFY(e->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); + QVERIFY(windowAddedSpy.isValid()); + + using namespace KWayland::Client; + // the test created the slide protocol, let's create a Registry and listen for it + QScopedPointer registry(new Registry); + registry->create(Test::waylandConnection()); + + QSignalSpy interfacesAnnouncedSpy(registry.data(), &Registry::interfacesAnnounced); + QVERIFY(interfacesAnnouncedSpy.isValid()); + registry->setup(); + QVERIFY(interfacesAnnouncedSpy.wait()); + auto slideInterface = registry->interface(Registry::Interface::Slide); + QVERIFY(slideInterface.name != 0); + QScopedPointer slideManager(registry->createSlideManager(slideInterface.name, slideInterface.version)); + QVERIFY(slideManager); + + // create Wayland window + QScopedPointer surface(Test::createSurface()); + QVERIFY(surface); + QScopedPointer slide(slideManager->createSlide(surface.data())); + slide->setLocation(Slide::Location::Left); + slide->commit(); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(shellSurface); + QCOMPARE(windowAddedSpy.count(), 0); + auto client = Test::renderAndWaitForShown(surface.data(), QSize(10, 20), Qt::blue); + QVERIFY(client); + QVERIFY(client->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()); + QTest::qWait(300); + QVERIFY(!otherEffect->isActive()); + + // and destroy the window again + shellSurface.reset(); + surface.reset(); + + QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + + QSignalSpy windowDeletedSpy(effects, &EffectsHandler::windowDeleted); + QVERIFY(windowDeletedSpy.isValid()); + QVERIFY(windowClosedSpy.wait()); + + // again we should have the sliding popups active + QVERIFY(slidingPoupus->isActive()); + QVERIFY(!otherEffect->isActive()); + + QVERIFY(windowDeletedSpy.wait()); + + QCOMPARE(windowDeletedSpy.count(), 1); + QTRY_VERIFY(!slidingPoupus->isActive()); + QTest::qWait(300); + QVERIFY(!otherEffect->isActive()); +} + +WAYLANDTEST_MAIN(SlidingPopupsTest) +#include "slidingpopups_test.moc" diff --git a/autotests/integration/effects/toplevel_open_close_animation_test.cpp b/autotests/integration/effects/toplevel_open_close_animation_test.cpp new file mode 100644 index 0000000..54af9f9 --- /dev/null +++ b/autotests/integration/effects/toplevel_open_close_animation_test.cpp @@ -0,0 +1,213 @@ +/* + 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 "abstract_client.h" +#include "composite.h" +#include "deleted.h" +#include "effectloader.h" +#include "effects.h" +#include "platform.h" +#include "scene.h" +#include "wayland_server.h" +#include "workspace.h" + +#include "effect_builtins.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() +{ + qputenv("XDG_DATA_DIRS", QCoreApplication::applicationDirPath().toUtf8()); + + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup plugins(config, QStringLiteral("Plugins")); + ScriptedEffectLoader loader; + const auto builtinNames = BuiltInEffects::availableEffectNames() << loader.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(); + QVERIFY(applicationStartedSpy.wait()); + waylandServer()->initWorkspace(); + + auto scene = KWin::Compositor::self()->scene(); + QVERIFY(scene); + QCOMPARE(scene->compositingType(), KWin::OpenGL2Compositing); +} + +void ToplevelOpenCloseAnimationTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); +} + +void ToplevelOpenCloseAnimationTest::cleanup() +{ + auto effectsImpl = qobject_cast(effects); + QVERIFY(effectsImpl); + effectsImpl->unloadAllEffects(); + QVERIFY(effectsImpl->loadedEffects().isEmpty()); + + Test::destroyWaylandConnection(); +} + +void ToplevelOpenCloseAnimationTest::testAnimateToplevels_data() +{ + QTest::addColumn("effectName"); + + QTest::newRow("Fade") << QStringLiteral("kwin4_effect_fade"); + QTest::newRow("Glide") << QStringLiteral("glide"); + QTest::newRow("Scale") << QStringLiteral("kwin4_effect_scale"); +} + +void ToplevelOpenCloseAnimationTest::testAnimateToplevels() +{ + // This test verifies that window open/close animation effects try to + // animate the appearing and the disappearing of toplevel windows. + + // Make sure that we have the right effects ptr. + auto effectsImpl = qobject_cast(effects); + QVERIFY(effectsImpl); + + // Load effect that will be tested. + QFETCH(QString, effectName); + QVERIFY(effectsImpl->loadEffect(effectName)); + QCOMPARE(effectsImpl->loadedEffects().count(), 1); + QCOMPARE(effectsImpl->loadedEffects().first(), effectName); + Effect *effect = effectsImpl->findEffect(effectName); + QVERIFY(effect); + QVERIFY(!effect->isActive()); + + // Create the test client. + using namespace KWayland::Client; + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + QVERIFY(effect->isActive()); + + // Eventually, the animation will be complete. + QTRY_VERIFY(!effect->isActive()); + + // Close the test client, the effect should start animating the disappearing + // of the client. + QSignalSpy windowClosedSpy(client, &AbstractClient::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + 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("kwin4_effect_fade"); + QTest::newRow("Glide") << QStringLiteral("glide"); + QTest::newRow("Scale") << QStringLiteral("kwin4_effect_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). + + // Make sure that we have the right effects ptr. + auto effectsImpl = qobject_cast(effects); + QVERIFY(effectsImpl); + + // Create the main window. + using namespace KWayland::Client; + QScopedPointer mainWindowSurface(Test::createSurface()); + QVERIFY(!mainWindowSurface.isNull()); + QScopedPointer mainWindowShellSurface(Test::createXdgToplevelSurface(mainWindowSurface.data())); + QVERIFY(!mainWindowShellSurface.isNull()); + AbstractClient *mainWindow = Test::renderAndWaitForShown(mainWindowSurface.data(), QSize(100, 50), Qt::blue); + QVERIFY(mainWindow); + + // Load effect that will be tested. + QFETCH(QString, effectName); + QVERIFY(effectsImpl->loadEffect(effectName)); + QCOMPARE(effectsImpl->loadedEffects().count(), 1); + QCOMPARE(effectsImpl->loadedEffects().first(), effectName); + Effect *effect = effectsImpl->findEffect(effectName); + QVERIFY(effect); + QVERIFY(!effect->isActive()); + + // Create a popup, it should not be animated. + QScopedPointer popupSurface(Test::createSurface()); + QVERIFY(!popupSurface.isNull()); + QScopedPointer 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); + QScopedPointer popupShellSurface(Test::createXdgPopupSurface(popupSurface.data(), mainWindowShellSurface->xdgSurface(), positioner.data())); + QVERIFY(!popupShellSurface.isNull()); + AbstractClient *popup = Test::renderAndWaitForShown(popupSurface.data(), 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, &AbstractClient::windowClosed); + QVERIFY(popupClosedSpy.isValid()); + popupShellSurface.reset(); + popupSurface.reset(); + QVERIFY(popupClosedSpy.wait()); + QVERIFY(!effect->isActive()); + + // Destroy the main window. + mainWindowSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(mainWindow)); +} + +WAYLANDTEST_MAIN(ToplevelOpenCloseAnimationTest) +#include "toplevel_open_close_animation_test.moc" diff --git a/autotests/integration/effects/translucency_test.cpp b/autotests/integration/effects/translucency_test.cpp new file mode 100644 index 0000000..70eb09d --- /dev/null +++ b/autotests/integration/effects/translucency_test.cpp @@ -0,0 +1,237 @@ +/* + 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 "x11client.h" +#include "composite.h" +#include "effects.h" +#include "effectloader.h" +#include "cursor.h" +#include "platform.h" +#include "wayland_server.h" +#include "workspace.h" +#include "effect_builtins.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(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + // 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")); + ScriptedEffectLoader loader; + const auto builtinNames = BuiltInEffects::availableEffectNames() << loader.listOfKnownEffects(); + for (QString name : builtinNames) { + plugins.writeEntry(name + QStringLiteral("Enabled"), false); + } + config->group("Outline").writeEntry(QStringLiteral("QmlPath"), QString("/does/not/exist.qml")); + config->group("Effect-kwin4_effect_translucency").writeEntry(QStringLiteral("Dialogs"), 90); + + config->sync(); + kwinApp()->setConfig(config); + + qputenv("KWIN_EFFECTS_FORCE_ANIMATIONS", "1"); + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QVERIFY(Compositor::self()); +} + +void TranslucencyTest::init() +{ + // load the translucency effect + EffectsHandlerImpl *e = static_cast(effects); + // find the effectsloader + auto effectloader = e->findChild(); + QVERIFY(effectloader); + QSignalSpy effectLoadedSpy(effectloader, &AbstractEffectLoader::effectLoaded); + QVERIFY(effectLoadedSpy.isValid()); + + QVERIFY(!e->isEffectLoaded(QStringLiteral("kwin4_effect_translucency"))); + QVERIFY(e->loadEffect(QStringLiteral("kwin4_effect_translucency"))); + QVERIFY(e->isEffectLoaded(QStringLiteral("kwin4_effect_translucency"))); + + QCOMPARE(effectLoadedSpy.count(), 1); + m_translucencyEffect = effectLoadedSpy.first().first().value(); + QVERIFY(m_translucencyEffect); +} + +void TranslucencyTest::cleanup() +{ + EffectsHandlerImpl *e = static_cast(effects); + if (e->isEffectLoaded(QStringLiteral("kwin4_effect_translucency"))) { + e->unloadEffect(QStringLiteral("kwin4_effect_translucency")); + } + QVERIFY(!e->isEffectLoaded(QStringLiteral("kwin4_effect_translucency"))); + m_translucencyEffect = nullptr; +} + +struct XcbConnectionDeleter +{ + static inline void cleanup(xcb_connection_t *pointer) + { + xcb_disconnect(pointer); + } +}; + +void TranslucencyTest::testMoveAfterDesktopChange() +{ + // test tries to simulate the condition of bug 366081 + QVERIFY(!m_translucencyEffect->isActive()); + + QSignalSpy windowAddedSpy(effects, &EffectsHandler::windowAdded); + QVERIFY(windowAddedSpy.isValid()); + + // create an xcb window + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + const QRect windowGeometry(0, 0, 100, 200); + xcb_window_t w = xcb_generate_id(c.data()); + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, 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; + memset(&hints, 0, sizeof(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.data(), w, &hints); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + // we should get a client for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->window(), w); + QVERIFY(client->isDecorated()); + + QVERIFY(windowAddedSpy.wait()); + QVERIFY(!m_translucencyEffect->isActive()); + // let's send the window to desktop 2 + effects->setNumberOfDesktops(2); + QCOMPARE(effects->numberOfDesktops(), 2); + workspace()->sendClientToDesktop(client, 2, false); + effects->setCurrentDesktop(2); + QVERIFY(!m_translucencyEffect->isActive()); + KWin::Cursors::self()->mouse()->setPos(client->frameGeometry().center()); + workspace()->performWindowOperation(client, Options::MoveOp); + QVERIFY(m_translucencyEffect->isActive()); + QTest::qWait(200); + QVERIFY(m_translucencyEffect->isActive()); + // now end move resize + client->endMoveResize(); + QVERIFY(m_translucencyEffect->isActive()); + QTest::qWait(500); + QTRY_VERIFY(!m_translucencyEffect->isActive()); + + // and destroy the window again + xcb_unmap_window(c.data(), w); + xcb_flush(c.data()); + + QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + QVERIFY(windowClosedSpy.wait()); + xcb_destroy_window(c.data(), w); + 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); + QVERIFY(windowAddedSpy.isValid()); + + // create an xcb window + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + const QRect windowGeometry(0, 0, 100, 200); + xcb_window_t w = xcb_generate_id(c.data()); + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, 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; + memset(&hints, 0, sizeof(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.data(), w, &hints); + NETWinInfo winInfo(c.data(), w, rootWindow(), NET::Properties(), NET::Properties2()); + winInfo.setWindowType(NET::Dialog); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + // we should get a client for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->window(), w); + QVERIFY(client->isDecorated()); + QVERIFY(client->isDialog()); + + QVERIFY(windowAddedSpy.wait()); + QTRY_VERIFY(m_translucencyEffect->isActive()); + // and destroy the window again + xcb_unmap_window(c.data(), w); + xcb_flush(c.data()); + + QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + + QSignalSpy windowDeletedSpy(effects, &EffectsHandler::windowDeleted); + QVERIFY(windowDeletedSpy.isValid()); + QVERIFY(windowClosedSpy.wait()); + if (windowDeletedSpy.isEmpty()) { + QVERIFY(windowDeletedSpy.wait()); + } + QCOMPARE(windowDeletedSpy.count(), 1); + QTRY_VERIFY(!m_translucencyEffect->isActive()); + xcb_destroy_window(c.data(), w); + c.reset(); +} + +WAYLANDTEST_MAIN(TranslucencyTest) +#include "translucency_test.moc" diff --git a/autotests/integration/effects/windowgeometry_test.cpp b/autotests/integration/effects/windowgeometry_test.cpp new file mode 100644 index 0000000..4a723d4 --- /dev/null +++ b/autotests/integration/effects/windowgeometry_test.cpp @@ -0,0 +1,86 @@ +/* + 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 "abstract_client.h" +#include "composite.h" +#include "effects.h" +#include "effectloader.h" +#include "cursor.h" +#include "platform.h" +#include "wayland_server.h" +#include "workspace.h" +#include "effect_builtins.h" + +#include + +#include +#include + +using namespace KWin; +using namespace KWayland::Client; +static const QString s_socketName = QStringLiteral("wayland_test_effects_windowgeometry-0"); + +class WindowGeometryTest : public QObject +{ +Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testStartup(); +}; + +void WindowGeometryTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + // 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")); + ScriptedEffectLoader loader; + const auto builtinNames = BuiltInEffects::availableEffectNames() << loader.listOfKnownEffects(); + for (QString name : builtinNames) { + plugins.writeEntry(name + QStringLiteral("Enabled"), false); + } + plugins.writeEntry(BuiltInEffects::nameForEffect(BuiltInEffect::WindowGeometry) + QStringLiteral("Enabled"), true); + + config->sync(); + kwinApp()->setConfig(config); + + qputenv("KWIN_EFFECTS_FORCE_ANIMATIONS", "1"); + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QVERIFY(KWin::Compositor::self()); +} + +void WindowGeometryTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); +} + +void WindowGeometryTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void WindowGeometryTest::testStartup() +{ + // just a test to load the effect to verify it doesn't crash + EffectsHandlerImpl *e = static_cast(effects); + QVERIFY(e->isEffectLoaded(BuiltInEffects::nameForEffect(BuiltInEffect::WindowGeometry))); +} + +WAYLANDTEST_MAIN(WindowGeometryTest) +#include "windowgeometry_test.moc" diff --git a/autotests/integration/effects/wobbly_shade_test.cpp b/autotests/integration/effects/wobbly_shade_test.cpp new file mode 100644 index 0000000..d8a346c --- /dev/null +++ b/autotests/integration/effects/wobbly_shade_test.cpp @@ -0,0 +1,185 @@ +/* + 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 "x11client.h" +#include "composite.h" +#include "cursor.h" +#include "effects.h" +#include "effectloader.h" +#include "platform.h" +#include "wayland_server.h" +#include "workspace.h" +#include "effect_builtins.h" + +#include + +#include +#include +#include +#include + +#include +#include + +using namespace KWin; +static const QString s_socketName = QStringLiteral("wayland_test_effects_wobbly_shade-0"); + +class WobblyWindowsShadeTest : public QObject +{ +Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testShadeMove(); +}; + +void WobblyWindowsShadeTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + // 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")); + ScriptedEffectLoader loader; + const auto builtinNames = BuiltInEffects::availableEffectNames() << loader.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(); + QVERIFY(applicationStartedSpy.wait()); + QVERIFY(Compositor::self()); + + auto scene = KWin::Compositor::self()->scene(); + QVERIFY(scene); + QCOMPARE(scene->compositingType(), KWin::OpenGL2Compositing); +} + +void WobblyWindowsShadeTest::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Decoration)); +} + +void WobblyWindowsShadeTest::cleanup() +{ + Test::destroyWaylandConnection(); + + auto effectsImpl = static_cast(effects); + effectsImpl->unloadAllEffects(); + QVERIFY(effectsImpl->loadedEffects().isEmpty()); +} + +struct XcbConnectionDeleter +{ + static inline void cleanup(xcb_connection_t *pointer) + { + xcb_disconnect(pointer); + } +}; + +void WobblyWindowsShadeTest::testShadeMove() +{ + // this test simulates the condition from BUG 390953 + EffectsHandlerImpl *e = static_cast(effects); + QVERIFY(e->loadEffect(BuiltInEffects::nameForEffect(BuiltInEffect::WobblyWindows))); + QVERIFY(e->isEffectLoaded(BuiltInEffects::nameForEffect(BuiltInEffect::WobblyWindows))); + + + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + const QRect windowGeometry(0, 0, 100, 200); + xcb_window_t w = xcb_generate_id(c.data()); + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, 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; + memset(&hints, 0, sizeof(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.data(), w, &hints); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + // we should get a client for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->window(), w); + QVERIFY(client->isDecorated()); + QVERIFY(client->isShadeable()); + QVERIFY(!client->isShade()); + QVERIFY(client->isActive()); + + QSignalSpy windowShownSpy(client, &AbstractClient::windowShown); + QVERIFY(windowShownSpy.isValid()); + QVERIFY(windowShownSpy.wait()); + + // now shade the window + workspace()->slotWindowShade(); + QVERIFY(client->isShade()); + + QSignalSpy windowStartUserMovedResizedSpy(e, &EffectsHandler::windowStartUserMovedResized); + QVERIFY(windowStartUserMovedResizedSpy.isValid()); + + // begin move + QVERIFY(workspace()->moveResizeClient() == nullptr); + QCOMPARE(client->isMove(), false); + workspace()->slotWindowMove(); + QCOMPARE(workspace()->moveResizeClient(), client); + QCOMPARE(client->isMove(), true); + QCOMPARE(windowStartUserMovedResizedSpy.count(), 1); + + // wait for frame rendered + QTest::qWait(100); + + // send some key events, not going through input redirection + client->keyPressEvent(Qt::Key_Right); + client->updateMoveResize(KWin::Cursors::self()->mouse()->pos()); + + // wait for frame rendered + QTest::qWait(100); + + client->keyPressEvent(Qt::Key_Right); + client->updateMoveResize(KWin::Cursors::self()->mouse()->pos()); + + // wait for frame rendered + QTest::qWait(100); + + client->keyPressEvent(Qt::Key_Down | Qt::ALT); + client->updateMoveResize(KWin::Cursors::self()->mouse()->pos()); + + // wait for frame rendered + QTest::qWait(100); + + // let's end + client->keyPressEvent(Qt::Key_Enter); + + // wait for frame rendered + QTest::qWait(100); +} + +WAYLANDTEST_MAIN(WobblyWindowsShadeTest) +#include "wobbly_shade_test.moc" diff --git a/autotests/integration/fakes/CMakeLists.txt b/autotests/integration/fakes/CMakeLists.txt new file mode 100644 index 0000000..e62ffea --- /dev/null +++ b/autotests/integration/fakes/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(org.kde.kdecoration2) + diff --git a/autotests/integration/fakes/org.kde.kdecoration2/CMakeLists.txt b/autotests/integration/fakes/org.kde.kdecoration2/CMakeLists.txt new file mode 100644 index 0000000..96033e5 --- /dev/null +++ b/autotests/integration/fakes/org.kde.kdecoration2/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.kdecoration2") +target_link_libraries(fakedecoshadows + PUBLIC + Qt5::Core + Qt5::Gui + PRIVATE + KDecoration2::KDecoration + KF5::CoreAddons) + diff --git a/autotests/integration/fakes/org.kde.kdecoration2/fakedecoration_with_shadows.cpp b/autotests/integration/fakes/org.kde.kdecoration2/fakedecoration_with_shadows.cpp new file mode 100644 index 0000000..1451d15 --- /dev/null +++ b/autotests/integration/fakes/org.kde.kdecoration2/fakedecoration_with_shadows.cpp @@ -0,0 +1,61 @@ +/* + 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 KDecoration2::Decoration +{ + Q_OBJECT + +public: + explicit FakeDecoWithShadows(QObject *parent = nullptr, const QVariantList &args = QVariantList()) + : Decoration(parent, args) {} + ~FakeDecoWithShadows() override {} + + void paint(QPainter *painter, const QRect &repaintRegion) override { + Q_UNUSED(painter) + Q_UNUSED(repaintRegion) + } + +public Q_SLOTS: + void 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 = QSharedPointer::create(); + decoShadow->setPadding(padding); + decoShadow->setInnerShadowRect(QRect(shadowRect.center(), QSize(1, 1))); + decoShadow->setShadow(shadowTexture); + + setShadow(decoShadow); + } +}; + +K_PLUGIN_FACTORY_WITH_JSON( + FakeDecoWithShadowsFactory, + "fakedecoration_with_shadows.json", + registerPlugin(); +) + +#include "fakedecoration_with_shadows.moc" diff --git a/autotests/integration/fakes/org.kde.kdecoration2/fakedecoration_with_shadows.json b/autotests/integration/fakes/org.kde.kdecoration2/fakedecoration_with_shadows.json new file mode 100644 index 0000000..16577df --- /dev/null +++ b/autotests/integration/fakes/org.kde.kdecoration2/fakedecoration_with_shadows.json @@ -0,0 +1,15 @@ +{ + "KPlugin": { + "Description": "Window decoration to test shadow tile overlaps", + "EnabledByDefault": false, + "Id": "org.kde.test.fakedecowithshadows", + "Name": "Fake Decoration With Shadows", + "ServiceTypes": [ + "org.kde.kdecoration2" + ] + }, + "org.kde.kdecoration2": { + "blur": false, + "kcmodule": false + } +} diff --git a/autotests/integration/generic_scene_opengl_test.cpp b/autotests/integration/generic_scene_opengl_test.cpp new file mode 100644 index 0000000..763c1ff --- /dev/null +++ b/autotests/integration/generic_scene_opengl_test.cpp @@ -0,0 +1,108 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "generic_scene_opengl_test.h" +#include "abstract_client.h" +#include "composite.h" +#include "effectloader.h" +#include "cursor.h" +#include "platform.h" +#include "scene.h" +#include "wayland_server.h" +#include "effect_builtins.h" + +#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() +{ + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + // 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")); + ScriptedEffectLoader loader; + const auto builtinNames = BuiltInEffects::availableEffectNames() << loader.listOfKnownEffects(); + for (QString name : builtinNames) { + plugins.writeEntry(name + QStringLiteral("Enabled"), false); + } + + config->sync(); + kwinApp()->setConfig(config); + + qputenv("XCURSOR_THEME", QByteArrayLiteral("DMZ-White")); + qputenv("XCURSOR_SIZE", QByteArrayLiteral("24")); + qputenv("KWIN_COMPOSE", m_envVariable); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QVERIFY(Compositor::self()); + + auto scene = KWin::Compositor::self()->scene(); + QVERIFY(scene); + QCOMPARE(scene->compositingType(), KWin::OpenGL2Compositing); + QCOMPARE(kwinApp()->platform()->selectedCompositor(), KWin::OpenGLCompositing); +} + +void GenericSceneOpenGLTest::testRestart_data() +{ + QTest::addColumn("core"); + + QTest::newRow("GLCore") << true; + QTest::newRow("Legacy") << false; +} + +void GenericSceneOpenGLTest::testRestart() +{ + // simple restart of the OpenGL compositor without any windows being shown + + // setup opengl compositing options + auto compositingGroup = kwinApp()->config()->group("Compositing"); + QFETCH(bool, core); + compositingGroup.writeEntry("GLCore", core); + compositingGroup.sync(); + + QSignalSpy sceneCreatedSpy(KWin::Compositor::self(), &Compositor::sceneCreated); + QVERIFY(sceneCreatedSpy.isValid()); + KWin::Compositor::self()->reinitialize(); + if (sceneCreatedSpy.isEmpty()) { + QVERIFY(sceneCreatedSpy.wait()); + } + QCOMPARE(sceneCreatedSpy.count(), 1); + auto scene = KWin::Compositor::self()->scene(); + QVERIFY(scene); + QCOMPARE(scene->compositingType(), KWin::OpenGL2Compositing); + QCOMPARE(kwinApp()->platform()->selectedCompositor(), KWin::OpenGLCompositing); + + // trigger a repaint + KWin::Compositor::self()->addRepaintFull(); + // and wait 100 msec to ensure it's rendered + // TODO: introduce frameRendered signal in SceneOpenGL + QTest::qWait(100); +} diff --git a/autotests/integration/generic_scene_opengl_test.h b/autotests/integration/generic_scene_opengl_test.h new file mode 100644 index 0000000..8e8876e --- /dev/null +++ b/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(); + void testRestart_data(); + void testRestart(); + +private: + QByteArray m_envVariable; +}; diff --git a/autotests/integration/globalshortcuts_test.cpp b/autotests/integration/globalshortcuts_test.cpp new file mode 100644 index 0000000..1f287bb --- /dev/null +++ b/autotests/integration/globalshortcuts_test.cpp @@ -0,0 +1,370 @@ +/* + 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 "x11client.h" +#include "cursor.h" +#include "input.h" +#include "internal_client.h" +#include "platform.h" +#include "screens.h" +#include "useractions.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include + +#include +#include + +#include +#include + +using namespace KWin; +using namespace KWayland::Client; + +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 testConsumedShift(); + void testRepeatedTrigger(); + void testUserActionsMenu(); + void testMetaShiftW(); + void testComponseKey(); + void testX11ClientShortcut(); + void testWaylandClientShortcut(); + void testSetupWindowShortcut(); +}; + +void GlobalShortcutsTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); + qputenv("KWIN_XKB_DEFAULT_KEYMAP", "1"); + qputenv("XKB_DEFAULT_RULES", "evdev"); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + waylandServer()->initWorkspace(); +} + +void GlobalShortcutsTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); + screens()->setCurrent(0); + KWin::Cursors::self()->mouse()->setPos(QPoint(640, 512)); +} + +void GlobalShortcutsTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void GlobalShortcutsTest::testConsumedShift() +{ + // this test verifies that a shortcut with a consumed shift modifier triggers + // create the action + QScopedPointer action(new QAction(nullptr)); + action->setProperty("componentName", QStringLiteral(KWIN_NAME)); + action->setObjectName(QStringLiteral("globalshortcuts-test-consumed-shift")); + QSignalSpy triggeredSpy(action.data(), &QAction::triggered); + QVERIFY(triggeredSpy.isValid()); + KGlobalAccel::self()->setShortcut(action.data(), QList{Qt::Key_Percent}, KGlobalAccel::NoAutoloading); + input()->registerShortcut(Qt::Key_Percent, action.data()); + + // press shift+5 + quint32 timestamp = 0; + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTSHIFT, timestamp++); + QCOMPARE(input()->keyboardModifiers(), Qt::ShiftModifier); + kwinApp()->platform()->keyboardKeyPressed(KEY_5, timestamp++); + QTRY_COMPARE(triggeredSpy.count(), 1); + kwinApp()->platform()->keyboardKeyReleased(KEY_5, timestamp++); + + // release shift + kwinApp()->platform()->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 + + QScopedPointer action(new QAction(nullptr)); + action->setProperty("componentName", QStringLiteral(KWIN_NAME)); + action->setObjectName(QStringLiteral("globalshortcuts-test-consumed-shift")); + QSignalSpy triggeredSpy(action.data(), &QAction::triggered); + QVERIFY(triggeredSpy.isValid()); + KGlobalAccel::self()->setShortcut(action.data(), QList{Qt::Key_Percent}, KGlobalAccel::NoAutoloading); + input()->registerShortcut(Qt::Key_Percent, action.data()); + + // we need to configure the key repeat first. It is only enabled on libinput + waylandServer()->seat()->setKeyRepeatInfo(25, 300); + + // press shift+5 + quint32 timestamp = 0; + kwinApp()->platform()->keyboardKeyPressed(KEY_WAKEUP, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTSHIFT, timestamp++); + QCOMPARE(input()->keyboardModifiers(), Qt::ShiftModifier); + kwinApp()->platform()->keyboardKeyPressed(KEY_5, timestamp++); + QTRY_COMPARE(triggeredSpy.count(), 1); + // and should repeat + QVERIFY(triggeredSpy.wait()); + QVERIFY(triggeredSpy.wait()); + // now release the key + kwinApp()->platform()->keyboardKeyReleased(KEY_5, timestamp++); + QVERIFY(!triggeredSpy.wait(500)); + + kwinApp()->platform()->keyboardKeyReleased(KEY_WAKEUP, timestamp++); + QVERIFY(!triggeredSpy.wait(500)); + + // release shift + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTSHIFT, timestamp++); +} + +void GlobalShortcutsTest::testUserActionsMenu() +{ + // this test tries to trigger the user actions 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 + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QVERIFY(c->isActive()); + + quint32 timestamp = 0; + QVERIFY(!workspace()->userActionsMenu()->isShown()); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_F3, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_F3, timestamp++); + QTRY_VERIFY(workspace()->userActionsMenu()->isShown()); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); +} + +void GlobalShortcutsTest::testMetaShiftW() +{ + // BUG 370341 + QScopedPointer action(new QAction(nullptr)); + action->setProperty("componentName", QStringLiteral(KWIN_NAME)); + action->setObjectName(QStringLiteral("globalshortcuts-test-meta-shift-w")); + QSignalSpy triggeredSpy(action.data(), &QAction::triggered); + QVERIFY(triggeredSpy.isValid()); + KGlobalAccel::self()->setShortcut(action.data(), QList{Qt::META + Qt::SHIFT + Qt::Key_W}, KGlobalAccel::NoAutoloading); + input()->registerShortcut(Qt::META + Qt::SHIFT + Qt::Key_W, action.data()); + + // press meta+shift+w + quint32 timestamp = 0; + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + QCOMPARE(input()->keyboardModifiers(), Qt::MetaModifier); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTSHIFT, timestamp++); + QCOMPARE(input()->keyboardModifiers(), Qt::ShiftModifier | Qt::MetaModifier); + kwinApp()->platform()->keyboardKeyPressed(KEY_W, timestamp++); + QTRY_COMPARE(triggeredSpy.count(), 1); + kwinApp()->platform()->keyboardKeyReleased(KEY_W, timestamp++); + + // release meta+shift + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTSHIFT, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTMETA, timestamp++); +} + +void GlobalShortcutsTest::testComponseKey() +{ + // BUG 390110 + QScopedPointer action(new QAction(nullptr)); + action->setProperty("componentName", QStringLiteral(KWIN_NAME)); + action->setObjectName(QStringLiteral("globalshortcuts-accent")); + QSignalSpy triggeredSpy(action.data(), &QAction::triggered); + QVERIFY(triggeredSpy.isValid()); + KGlobalAccel::self()->setShortcut(action.data(), QList{Qt::UNICODE_ACCEL}, KGlobalAccel::NoAutoloading); + input()->registerShortcut(Qt::UNICODE_ACCEL, action.data()); + + // press & release ` + quint32 timestamp = 0; + kwinApp()->platform()->keyboardKeyPressed(KEY_RESERVED, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_RESERVED, timestamp++); + + QTRY_COMPARE(triggeredSpy.count(), 0); +} + +struct XcbConnectionDeleter +{ + static inline void cleanup(xcb_connection_t *pointer) + { + xcb_disconnect(pointer); + } +}; + +void GlobalShortcutsTest::testX11ClientShortcut() +{ + // create an X11 window + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + xcb_window_t w = xcb_generate_id(c.data()); + const QRect windowGeometry = QRect(0, 0, 10, 20); + const uint32_t values[] = { + XCB_EVENT_MASK_ENTER_WINDOW | + XCB_EVENT_MASK_LEAVE_WINDOW + }; + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, 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; + memset(&hints, 0, sizeof(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.data(), w, &hints); + NETWinInfo info(c.data(), w, rootWindow(), NET::WMAllProperties, NET::WM2AllProperties); + info.setWindowType(NET::Normal); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.last().first().value(); + QVERIFY(client); + + QCOMPARE(workspace()->activeClient(), client); + QVERIFY(client->isActive()); + QCOMPARE(client->shortcut(), QKeySequence()); + const QKeySequence seq(Qt::META + Qt::SHIFT + Qt::Key_Y); + QVERIFY(workspace()->shortcutAvailable(seq)); + client->setShortcut(seq.toString()); + QCOMPARE(client->shortcut(), seq); + QVERIFY(!workspace()->shortcutAvailable(seq)); + QCOMPARE(client->caption(), QStringLiteral(" {Meta+Shift+Y}")); + + // it's delayed + QCoreApplication::processEvents(); + + workspace()->activateClient(nullptr); + QVERIFY(!workspace()->activeClient()); + QVERIFY(!client->isActive()); + + // now let's trigger the shortcut + quint32 timestamp = 0; + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTSHIFT, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_Y, timestamp++); + QTRY_COMPARE(workspace()->activeClient(), client); + kwinApp()->platform()->keyboardKeyReleased(KEY_Y, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTSHIFT, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTMETA, timestamp++); + + // destroy window again + QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + xcb_unmap_window(c.data(), w); + xcb_destroy_window(c.data(), w); + xcb_flush(c.data()); + QVERIFY(windowClosedSpy.wait()); +} + +void GlobalShortcutsTest::testWaylandClientShortcut() +{ + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + + QCOMPARE(workspace()->activeClient(), client); + QVERIFY(client->isActive()); + QCOMPARE(client->shortcut(), QKeySequence()); + const QKeySequence seq(Qt::META + Qt::SHIFT + Qt::Key_Y); + QVERIFY(workspace()->shortcutAvailable(seq)); + client->setShortcut(seq.toString()); + QCOMPARE(client->shortcut(), seq); + QVERIFY(!workspace()->shortcutAvailable(seq)); + QCOMPARE(client->caption(), QStringLiteral(" {Meta+Shift+Y}")); + + workspace()->activateClient(nullptr); + QVERIFY(!workspace()->activeClient()); + QVERIFY(!client->isActive()); + + // now let's trigger the shortcut + quint32 timestamp = 0; + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTSHIFT, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_Y, timestamp++); + QTRY_COMPARE(workspace()->activeClient(), client); + kwinApp()->platform()->keyboardKeyReleased(KEY_Y, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTSHIFT, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTMETA, timestamp++); + + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); + QVERIFY(workspace()->shortcutAvailable(seq)); +} + +void GlobalShortcutsTest::testSetupWindowShortcut() +{ + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + + QCOMPARE(workspace()->activeClient(), client); + QVERIFY(client->isActive()); + QCOMPARE(client->shortcut(), QKeySequence()); + + QSignalSpy shortcutDialogAddedSpy(workspace(), &Workspace::internalClientAdded); + QVERIFY(shortcutDialogAddedSpy.isValid()); + 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); + + // the QKeySequenceEdit field does not get focus, we need to pass it focus manually + QEXPECT_FAIL("", "Edit does not have focus", Continue); + QVERIFY(sequenceEdit->hasFocus()); + sequenceEdit->setFocus(); + QTRY_VERIFY(sequenceEdit->hasFocus()); + + quint32 timestamp = 0; + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTSHIFT, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_Y, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_Y, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTSHIFT, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTMETA, timestamp++); + + // the sequence gets accepted after one second, so wait a bit longer + QTest::qWait(2000); + // now send in enter + kwinApp()->platform()->keyboardKeyPressed(KEY_ENTER, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_ENTER, timestamp++); + QTRY_COMPARE(client->shortcut(), QKeySequence(Qt::META + Qt::SHIFT + Qt::Key_Y)); +} + +WAYLANDTEST_MAIN(GlobalShortcutsTest) +#include "globalshortcuts_test.moc" diff --git a/autotests/integration/helper/CMakeLists.txt b/autotests/integration/helper/CMakeLists.txt new file mode 100644 index 0000000..0aba097 --- /dev/null +++ b/autotests/integration/helper/CMakeLists.txt @@ -0,0 +1,11 @@ +add_executable(copy copy.cpp) +target_link_libraries(copy Qt5::Gui) +ecm_mark_as_test(copy) +###################### +add_executable(paste paste.cpp) +target_link_libraries(paste Qt5::Gui) +ecm_mark_as_test(paste) +###################### +add_executable(kill kill.cpp) +target_link_libraries(kill Qt5::Widgets) +ecm_mark_as_test(kill) diff --git a/autotests/integration/helper/copy.cpp b/autotests/integration/helper/copy.cpp new file mode 100644 index 0000000..92029f0 --- /dev/null +++ b/autotests/integration/helper/copy.cpp @@ -0,0 +1,60 @@ +/* + 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 +#include +#include +#include + +class Window : public QRasterWindow +{ + Q_OBJECT +public: + explicit Window(); + ~Window() override; + +protected: + void paintEvent(QPaintEvent *event) override; + void focusInEvent(QFocusEvent *event) override; +}; + +Window::Window() + : QRasterWindow() +{ +} + +Window::~Window() = default; + +void Window::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event) + QPainter p(this); + p.fillRect(0, 0, width(), height(), Qt::red); +} + +void Window::focusInEvent(QFocusEvent *event) +{ + QRasterWindow::focusInEvent(event); + // TODO: make it work without singleshot + QTimer::singleShot(100,[] { + qApp->clipboard()->setText(QStringLiteral("test")); + }); +} + +int main(int argc, char *argv[]) +{ + QGuiApplication app(argc, argv); + QScopedPointer w(new Window); + w->setGeometry(QRect(0, 0, 100, 200)); + w->show(); + + return app.exec(); +} + +#include "copy.moc" diff --git a/autotests/integration/helper/kill.cpp b/autotests/integration/helper/kill.cpp new file mode 100644 index 0000000..7930123 --- /dev/null +++ b/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/autotests/integration/helper/paste.cpp b/autotests/integration/helper/paste.cpp new file mode 100644 index 0000000..ea4c8b1 --- /dev/null +++ b/autotests/integration/helper/paste.cpp @@ -0,0 +1,57 @@ +/* + 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 +#include +#include +#include + +class Window : public QRasterWindow +{ + Q_OBJECT +public: + explicit Window(); + ~Window() override; + +protected: + void paintEvent(QPaintEvent *event) override; +}; + +Window::Window() + : QRasterWindow() +{ +} + +Window::~Window() = default; + +void Window::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event) + QPainter p(this); + p.fillRect(0, 0, width(), height(), Qt::blue); +} + +int main(int argc, char *argv[]) +{ + QGuiApplication app(argc, argv); + QObject::connect(app.clipboard(), &QClipboard::changed, &app, + [] { + if (qApp->clipboard()->text() == QLatin1String("test")) { + QTimer::singleShot(100, qApp, &QCoreApplication::quit); + } + } + ); + QScopedPointer w(new Window); + w->setGeometry(QRect(0, 0, 100, 200)); + w->show(); + + return app.exec(); +} + +#include "paste.moc" diff --git a/autotests/integration/idle_inhibition_test.cpp b/autotests/integration/idle_inhibition_test.cpp new file mode 100644 index 0000000..c341f2e --- /dev/null +++ b/autotests/integration/idle_inhibition_test.cpp @@ -0,0 +1,359 @@ +/* + 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 "abstract_client.h" +#include "platform.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include +#include + +#include +#include + +using namespace KWin; +using namespace KWayland::Client; +using KWaylandServer::IdleInterface; + +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 TestIdleInhibition::initTestCase() +{ + qRegisterMetaType(); + + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + waylandServer()->initWorkspace(); +} + +void TestIdleInhibition::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::IdleInhibition)); + +} + +void TestIdleInhibition::cleanup() +{ + Test::destroyWaylandConnection(); + + VirtualDesktopManager::self()->setCount(1); + QCOMPARE(VirtualDesktopManager::self()->count(), 1u); +} + +void TestIdleInhibition::testInhibit() +{ + auto idle = waylandServer()->display()->findChild(); + QVERIFY(idle); + QVERIFY(!idle->isInhibited()); + QSignalSpy inhibitedSpy(idle, &IdleInterface::inhibitedChanged); + QVERIFY(inhibitedSpy.isValid()); + + // now create window + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + + // now create inhibition on window + QScopedPointer inhibitor(Test::waylandIdleInhibitManager()->createInhibitor(surface.data())); + QVERIFY(inhibitor->isValid()); + + // render the client + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + + // this should inhibit our server object + QVERIFY(idle->isInhibited()); + + // deleting the object should uninhibit again + inhibitor.reset(); + QVERIFY(inhibitedSpy.wait()); + QVERIFY(!idle->isInhibited()); + + // inhibit again and destroy window + Test::waylandIdleInhibitManager()->createInhibitor(surface.data(), surface.data()); + QVERIFY(inhibitedSpy.wait()); + QVERIFY(idle->isInhibited()); + + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(c)); + QTRY_VERIFY(!idle->isInhibited()); + QCOMPARE(inhibitedSpy.count(), 4); +} + +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); + + // Get reference to the idle interface. + auto idle = waylandServer()->display()->findChild(); + QVERIFY(idle); + QVERIFY(!idle->isInhibited()); + QSignalSpy inhibitedSpy(idle, &IdleInterface::inhibitedChanged); + QVERIFY(inhibitedSpy.isValid()); + + // Create the test client. + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + + // Create the inhibitor object. + QScopedPointer inhibitor(Test::waylandIdleInhibitManager()->createInhibitor(surface.data())); + QVERIFY(inhibitor->isValid()); + + // Render the client. + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + + // The test client should be only on the first virtual desktop. + QCOMPARE(c->desktops().count(), 1); + QCOMPARE(c->desktops().first(), VirtualDesktopManager::self()->desktops().first()); + + // This should inhibit our server object. + QVERIFY(idle->isInhibited()); + QCOMPARE(inhibitedSpy.count(), 1); + + // 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. + QVERIFY(!idle->isInhibited()); + QCOMPARE(inhibitedSpy.count(), 2); + + // Switch back to the first virtual desktop. + VirtualDesktopManager::self()->setCurrent(1); + + // The test client became visible again, so the compositor has to honor the idle + // inhibitor object back again. + QVERIFY(idle->isInhibited()); + QCOMPARE(inhibitedSpy.count(), 3); + + // Destroy the test client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(c)); + QTRY_VERIFY(!idle->isInhibited()); + QCOMPARE(inhibitedSpy.count(), 4); +} + +void TestIdleInhibition::testDontInhibitWhenMinimized() +{ + // This test verifies that the idle inhibitor object is not honored when the + // associated surface is minimized. + + // Get reference to the idle interface. + auto idle = waylandServer()->display()->findChild(); + QVERIFY(idle); + QVERIFY(!idle->isInhibited()); + QSignalSpy inhibitedSpy(idle, &IdleInterface::inhibitedChanged); + QVERIFY(inhibitedSpy.isValid()); + + // Create the test client. + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + + // Create the inhibitor object. + QScopedPointer inhibitor(Test::waylandIdleInhibitManager()->createInhibitor(surface.data())); + QVERIFY(inhibitor->isValid()); + + // Render the client. + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + + // This should inhibit our server object. + QVERIFY(idle->isInhibited()); + QCOMPARE(inhibitedSpy.count(), 1); + + // Minimize the client, the idle inhibitor object should not be honored. + c->minimize(); + QVERIFY(!idle->isInhibited()); + QCOMPARE(inhibitedSpy.count(), 2); + + // Unminimize the client, the idle inhibitor object should be honored back again. + c->unminimize(); + QVERIFY(idle->isInhibited()); + QCOMPARE(inhibitedSpy.count(), 3); + + // Destroy the test client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(c)); + QTRY_VERIFY(!idle->isInhibited()); + QCOMPARE(inhibitedSpy.count(), 4); +} + +void TestIdleInhibition::testDontInhibitWhenUnmapped() +{ + // This test verifies that the idle inhibitor object is not honored by KWin + // when the associated client is unmapped. + + // Get reference to the idle interface. + auto idle = waylandServer()->display()->findChild(); + QVERIFY(idle); + QVERIFY(!idle->isInhibited()); + QSignalSpy inhibitedSpy(idle, &IdleInterface::inhibitedChanged); + QVERIFY(inhibitedSpy.isValid()); + + // Create the test client. + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + + // Create the inhibitor object. + QScopedPointer inhibitor(Test::waylandIdleInhibitManager()->createInhibitor(surface.data())); + QVERIFY(inhibitor->isValid()); + + // Map the client. + QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(clientAddedSpy.isValid()); + Test::render(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(clientAddedSpy.isEmpty()); + QVERIFY(clientAddedSpy.wait()); + QCOMPARE(clientAddedSpy.count(), 1); + AbstractClient *client = clientAddedSpy.last().first().value(); + QVERIFY(client); + QCOMPARE(client->readyForPainting(), true); + + // The compositor will respond with a configure event when the surface becomes active. + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 1); + + // This should inhibit our server object. + QVERIFY(idle->isInhibited()); + QCOMPARE(inhibitedSpy.count(), 1); + + // Unmap the client. + surface->attachBuffer(Buffer::Ptr()); + surface->commit(Surface::CommitFlag::None); + QVERIFY(Test::waitForWindowDestroyed(client)); + + // The surface is no longer visible, so the compositor doesn't have to honor the + // idle inhibitor object. + QVERIFY(!idle->isInhibited()); + QCOMPARE(inhibitedSpy.count(), 2); + + // Tell the compositor that we want to map the surface. + surface->commit(Surface::CommitFlag::None); + + // The compositor will respond with a configure event. + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 2); + + // Map the client. + Test::render(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(clientAddedSpy.wait()); + QCOMPARE(clientAddedSpy.count(), 2); + client = clientAddedSpy.last().first().value(); + QVERIFY(client); + QCOMPARE(client->readyForPainting(), true); + + // The test client became visible again, so the compositor has to honor the idle + // inhibitor object back again. + QVERIFY(idle->isInhibited()); + QCOMPARE(inhibitedSpy.count(), 3); + + // Destroy the test client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); + QTRY_VERIFY(!idle->isInhibited()); + QCOMPARE(inhibitedSpy.count(), 4); +} + +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); + + // Get reference to the idle interface. + auto idle = waylandServer()->display()->findChild(); + QVERIFY(idle); + QVERIFY(!idle->isInhibited()); + QSignalSpy inhibitedSpy(idle, &IdleInterface::inhibitedChanged); + QVERIFY(inhibitedSpy.isValid()); + + // Create the test client. + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + + // Create the inhibitor object. + QScopedPointer inhibitor(Test::waylandIdleInhibitManager()->createInhibitor(surface.data())); + QVERIFY(inhibitor->isValid()); + + // Render the client. + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + + // The test client should be only on the first virtual desktop. + QCOMPARE(c->desktops().count(), 1); + QCOMPARE(c->desktops().first(), VirtualDesktopManager::self()->desktops().first()); + + // This should inhibit our server object. + QVERIFY(idle->isInhibited()); + QCOMPARE(inhibitedSpy.count(), 1); + + // Let the client enter the second virtual desktop. + c->enterDesktop(VirtualDesktopManager::self()->desktops().at(1)); + QCOMPARE(inhibitedSpy.count(), 1); + + // If the client leaves the first virtual desktop, then the associated idle + // inhibitor object should not be honored. + c->leaveDesktop(VirtualDesktopManager::self()->desktops().at(0)); + QVERIFY(!idle->isInhibited()); + QCOMPARE(inhibitedSpy.count(), 2); + + // If the client enters the first desktop, then the associated idle inhibitor + // object should be honored back again. + c->enterDesktop(VirtualDesktopManager::self()->desktops().at(0)); + QVERIFY(idle->isInhibited()); + QCOMPARE(inhibitedSpy.count(), 3); + + // Destroy the test client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(c)); + QTRY_VERIFY(!idle->isInhibited()); + QCOMPARE(inhibitedSpy.count(), 4); +} + +WAYLANDTEST_MAIN(TestIdleInhibition) +#include "idle_inhibition_test.moc" diff --git a/autotests/integration/input_stacking_order.cpp b/autotests/integration/input_stacking_order.cpp new file mode 100644 index 0000000..fa4e3d4 --- /dev/null +++ b/autotests/integration/input_stacking_order.cpp @@ -0,0 +1,166 @@ +/* + 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 "platform.h" +#include "abstract_client.h" +#include "cursor.h" +#include "deleted.h" +#include "screenedge.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include + +#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(); + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + setenv("QT_QPA_PLATFORM", "wayland", true); + waylandServer()->initWorkspace(); +} + +void InputStackingOrderTest::init() +{ + using namespace KWayland::Client; + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat)); + QVERIFY(Test::waitForWaylandPointer()); + + screens()->setCurrent(0); + Cursors::self()->mouse()->setPos(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 + using namespace KWayland::Client; + // 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, &Pointer::entered); + QVERIFY(enteredSpy.isValid()); + QSignalSpy leftSpy(pointer, &Pointer::left); + QVERIFY(leftSpy.isValid()); + + // now create the two windows and make them overlap + QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(clientAddedSpy.isValid()); + Surface *surface1 = Test::createSurface(Test::waylandCompositor()); + QVERIFY(surface1); + XdgShellSurface *shellSurface1 = Test::createXdgShellStableSurface(surface1, surface1); + QVERIFY(shellSurface1); + render(surface1); + QVERIFY(clientAddedSpy.wait()); + AbstractClient *window1 = workspace()->activeClient(); + QVERIFY(window1); + + Surface *surface2 = Test::createSurface(Test::waylandCompositor()); + QVERIFY(surface2); + XdgShellSurface *shellSurface2 = Test::createXdgShellStableSurface(surface2, surface2); + QVERIFY(shellSurface2); + render(surface2); + QVERIFY(clientAddedSpy.wait()); + + AbstractClient *window2 = workspace()->activeClient(); + QVERIFY(window2); + QVERIFY(window1 != window2); + + // now make windows overlap + window2->move(window1->pos()); + QCOMPARE(window1->frameGeometry(), window2->frameGeometry()); + + // enter + kwinApp()->platform()->pointerMotion(QPointF(25, 25), 1); + QVERIFY(enteredSpy.wait()); + QCOMPARE(enteredSpy.count(), 1); + // window 2 should have focus + QCOMPARE(pointer->enteredSurface(), surface2); + // also on the server + QCOMPARE(waylandServer()->seat()->focusedPointerSurface(), window2->surface()); + + // raise window 1 above window 2 + QVERIFY(leftSpy.isEmpty()); + workspace()->raiseClient(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); + QCOMPARE(waylandServer()->seat()->focusedPointerSurface(), window1->surface()); + + // let's destroy window1, that should pass focus to window2 again + QSignalSpy windowClosedSpy(window1, &Toplevel::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + surface1->deleteLater(); + QVERIFY(windowClosedSpy.wait()); + QVERIFY(enteredSpy.wait()); + QCOMPARE(enteredSpy.count(), 3); + QCOMPARE(pointer->enteredSurface(), surface2); + QCOMPARE(waylandServer()->seat()->focusedPointerSurface(), window2->surface()); +} + +} + +WAYLANDTEST_MAIN(KWin::InputStackingOrderTest) +#include "input_stacking_order.moc" diff --git a/autotests/integration/internal_window.cpp b/autotests/integration/internal_window.cpp new file mode 100644 index 0000000..1c5a91f --- /dev/null +++ b/autotests/integration/internal_window.cpp @@ -0,0 +1,815 @@ +/* + 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 "platform.h" +#include "cursor.h" +#include "effects.h" +#include "internal_client.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include + +#include +#include +#include +#include + +#include + +#include + +using namespace KWayland::Client; + +Q_DECLARE_METATYPE(NET::WindowType); + +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 testEnterLeave(); + void testPointerPressRelease(); + void testPointerAxis(); + void testKeyboard_data(); + void testKeyboard(); + void testKeyboardShowWithoutActivating(); + void testKeyboardTriggersLeave(); + void testTouch(); + void testOpacity(); + void testMove(); + void testSkipCloseAnimation_data(); + void testSkipCloseAnimation(); + void testModifierClickUnrestrictedMove(); + void testModifierScroll(); + void testPopup(); + void testScale(); + void testWindowType_data(); + void testWindowType(); + void testChangeWindowType_data(); + void testChangeWindowType(); + void testEffectWindow(); + void testReentrantSetFrameGeometry(); +}; + +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(); + +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; + +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) +{ + Q_UNUSED(event) + QPainter p(this); + p.fillRect(0, 0, width(), height(), Qt::red); +} + +bool HelperWindow::event(QEvent *event) +{ + if (event->type() == QEvent::Enter) { + emit entered(); + } + if (event->type() == QEvent::Leave) { + emit left(); + } + return QRasterWindow::event(event); +} + +void HelperWindow::mouseMoveEvent(QMouseEvent *event) +{ + m_latestGlobalMousePos = event->globalPos(); + emit mouseMoved(event->globalPos()); +} + +void HelperWindow::mousePressEvent(QMouseEvent *event) +{ + m_latestGlobalMousePos = event->globalPos(); + m_pressedButtons = event->buttons(); + emit mousePressed(); +} + +void HelperWindow::mouseReleaseEvent(QMouseEvent *event) +{ + m_latestGlobalMousePos = event->globalPos(); + m_pressedButtons = event->buttons(); + emit mouseReleased(); +} + +void HelperWindow::wheelEvent(QWheelEvent *event) +{ + Q_UNUSED(event) + emit wheel(); +} + +void HelperWindow::keyPressEvent(QKeyEvent *event) +{ + Q_UNUSED(event) + emit keyPressed(); +} + +void HelperWindow::keyReleaseEvent(QKeyEvent *event) +{ + Q_UNUSED(event) + emit keyReleased(); +} + +void InternalWindowTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + waylandServer()->initWorkspace(); +} + +void InternalWindowTest::init() +{ + Cursors::self()->mouse()->setPos(QPoint(1280, 512)); + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat)); + QVERIFY(Test::waitForWaylandKeyboard()); +} + +void InternalWindowTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void InternalWindowTest::testEnterLeave() +{ + QSignalSpy clientAddedSpy(workspace(), &Workspace::internalClientAdded); + QVERIFY(clientAddedSpy.isValid()); + HelperWindow win; + QVERIFY(!workspace()->findInternal(nullptr)); + QVERIFY(!workspace()->findInternal(&win)); + win.setGeometry(0, 0, 100, 100); + win.show(); + + QTRY_COMPARE(clientAddedSpy.count(), 1); + QVERIFY(!workspace()->activeClient()); + InternalClient *c = clientAddedSpy.first().first().value(); + QVERIFY(c); + QVERIFY(c->isInternal()); + QVERIFY(!c->isDecorated()); + QCOMPARE(workspace()->findInternal(&win), c); + QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 100)); + QVERIFY(c->isShown(false)); + QVERIFY(workspace()->xStackingOrder().contains(c)); + + QSignalSpy enterSpy(&win, &HelperWindow::entered); + QVERIFY(enterSpy.isValid()); + QSignalSpy leaveSpy(&win, &HelperWindow::left); + QVERIFY(leaveSpy.isValid()); + QSignalSpy moveSpy(&win, &HelperWindow::mouseMoved); + QVERIFY(moveSpy.isValid()); + + quint32 timestamp = 1; + kwinApp()->platform()->pointerMotion(QPoint(50, 50), timestamp++); + QTRY_COMPARE(moveSpy.count(), 1); + + kwinApp()->platform()->pointerMotion(QPoint(60, 50), timestamp++); + QTRY_COMPARE(moveSpy.count(), 2); + QCOMPARE(moveSpy[1].first().toPoint(), QPoint(60, 50)); + + kwinApp()->platform()->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 + kwinApp()->platform()->pointerMotion(QPoint(5, 5), timestamp++); + QVERIFY(!enterSpy.wait(100)); + QCOMPARE(enterSpy.count(), 1); + // inside the mask we should still get an enter + kwinApp()->platform()->pointerMotion(QPoint(25, 27), timestamp++); + QTRY_COMPARE(enterSpy.count(), 2); +} + +void InternalWindowTest::testPointerPressRelease() +{ + QSignalSpy clientAddedSpy(workspace(), &Workspace::internalClientAdded); + QVERIFY(clientAddedSpy.isValid()); + HelperWindow win; + win.setGeometry(0, 0, 100, 100); + win.show(); + QSignalSpy pressSpy(&win, &HelperWindow::mousePressed); + QVERIFY(pressSpy.isValid()); + QSignalSpy releaseSpy(&win, &HelperWindow::mouseReleased); + QVERIFY(releaseSpy.isValid()); + + QTRY_COMPARE(clientAddedSpy.count(), 1); + + quint32 timestamp = 1; + kwinApp()->platform()->pointerMotion(QPoint(50, 50), timestamp++); + + kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); + QTRY_COMPARE(pressSpy.count(), 1); + kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); + QTRY_COMPARE(releaseSpy.count(), 1); +} + +void InternalWindowTest::testPointerAxis() +{ + QSignalSpy clientAddedSpy(workspace(), &Workspace::internalClientAdded); + QVERIFY(clientAddedSpy.isValid()); + HelperWindow win; + win.setGeometry(0, 0, 100, 100); + win.show(); + QSignalSpy wheelSpy(&win, &HelperWindow::wheel); + QVERIFY(wheelSpy.isValid()); + QTRY_COMPARE(clientAddedSpy.count(), 1); + + quint32 timestamp = 1; + kwinApp()->platform()->pointerMotion(QPoint(50, 50), timestamp++); + + kwinApp()->platform()->pointerAxisVertical(5.0, timestamp++); + QTRY_COMPARE(wheelSpy.count(), 1); + kwinApp()->platform()->pointerAxisHorizontal(5.0, timestamp++); + QTRY_COMPARE(wheelSpy.count(), 2); +} + +void InternalWindowTest::testKeyboard_data() +{ + QTest::addColumn("cursorPos"); + + QTest::newRow("on Window") << QPoint(50, 50); + QTest::newRow("outside Window") << QPoint(250, 250); +} + +void InternalWindowTest::testKeyboard() +{ + QSignalSpy clientAddedSpy(workspace(), &Workspace::internalClientAdded); + QVERIFY(clientAddedSpy.isValid()); + HelperWindow win; + win.setGeometry(0, 0, 100, 100); + win.show(); + QSignalSpy pressSpy(&win, &HelperWindow::keyPressed); + QVERIFY(pressSpy.isValid()); + QSignalSpy releaseSpy(&win, &HelperWindow::keyReleased); + QVERIFY(releaseSpy.isValid()); + QTRY_COMPARE(clientAddedSpy.count(), 1); + auto internalClient = clientAddedSpy.first().first().value(); + QVERIFY(internalClient); + QVERIFY(internalClient->isInternal()); + QVERIFY(internalClient->readyForPainting()); + + quint32 timestamp = 1; + QFETCH(QPoint, cursorPos); + kwinApp()->platform()->pointerMotion(cursorPos, timestamp++); + + kwinApp()->platform()->keyboardKeyPressed(KEY_A, timestamp++); + QTRY_COMPARE(pressSpy.count(), 1); + QCOMPARE(releaseSpy.count(), 0); + kwinApp()->platform()->keyboardKeyReleased(KEY_A, timestamp++); + QTRY_COMPARE(releaseSpy.count(), 1); + QCOMPARE(pressSpy.count(), 1); +} + +void InternalWindowTest::testKeyboardShowWithoutActivating() +{ + QSignalSpy clientAddedSpy(workspace(), &Workspace::internalClientAdded); + QVERIFY(clientAddedSpy.isValid()); + HelperWindow win; + win.setProperty("_q_showWithoutActivating", true); + win.setGeometry(0, 0, 100, 100); + win.show(); + QSignalSpy pressSpy(&win, &HelperWindow::keyPressed); + QVERIFY(pressSpy.isValid()); + QSignalSpy releaseSpy(&win, &HelperWindow::keyReleased); + QVERIFY(releaseSpy.isValid()); + QTRY_COMPARE(clientAddedSpy.count(), 1); + auto internalClient = clientAddedSpy.first().first().value(); + QVERIFY(internalClient); + QVERIFY(internalClient->isInternal()); + QVERIFY(internalClient->readyForPainting()); + + quint32 timestamp = 1; + const QPoint cursorPos = QPoint(50, 50); + kwinApp()->platform()->pointerMotion(cursorPos, timestamp++); + + kwinApp()->platform()->keyboardKeyPressed(KEY_A, timestamp++); + QCOMPARE(pressSpy.count(), 0); + QVERIFY(!pressSpy.wait(100)); + QCOMPARE(releaseSpy.count(), 0); + kwinApp()->platform()->keyboardKeyReleased(KEY_A, timestamp++); + QCOMPARE(releaseSpy.count(), 0); + QVERIFY(!releaseSpy.wait(100)); + QCOMPARE(pressSpy.count(), 0); +} + +void InternalWindowTest::testKeyboardTriggersLeave() +{ + // this test verifies that a leave event is sent to a client when an internal window + // gets a key event + QScopedPointer keyboard(Test::waylandSeat()->createKeyboard()); + QVERIFY(!keyboard.isNull()); + QVERIFY(keyboard->isValid()); + QSignalSpy enteredSpy(keyboard.data(), &Keyboard::entered); + QVERIFY(enteredSpy.isValid()); + QSignalSpy leftSpy(keyboard.data(), &Keyboard::left); + QVERIFY(leftSpy.isValid()); + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + + // now let's render + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QVERIFY(c->isActive()); + QVERIFY(!c->isInternal()); + + if (enteredSpy.isEmpty()) { + QVERIFY(enteredSpy.wait()); + } + QCOMPARE(enteredSpy.count(), 1); + + // create internal window + QSignalSpy clientAddedSpy(workspace(), &Workspace::internalClientAdded); + QVERIFY(clientAddedSpy.isValid()); + HelperWindow win; + win.setGeometry(0, 0, 100, 100); + win.show(); + QSignalSpy pressSpy(&win, &HelperWindow::keyPressed); + QVERIFY(pressSpy.isValid()); + QSignalSpy releaseSpy(&win, &HelperWindow::keyReleased); + QVERIFY(releaseSpy.isValid()); + QTRY_COMPARE(clientAddedSpy.count(), 1); + auto internalClient = clientAddedSpy.first().first().value(); + QVERIFY(internalClient); + QVERIFY(internalClient->isInternal()); + QVERIFY(internalClient->readyForPainting()); + + QVERIFY(leftSpy.isEmpty()); + QVERIFY(!leftSpy.wait(100)); + + // now let's trigger a key, which should result in a leave + quint32 timestamp = 1; + kwinApp()->platform()->keyboardKeyPressed(KEY_A, timestamp++); + QVERIFY(leftSpy.wait()); + QCOMPARE(pressSpy.count(), 1); + + kwinApp()->platform()->keyboardKeyReleased(KEY_A, timestamp++); + QTRY_COMPARE(releaseSpy.count(), 1); + + // after hiding the internal window, next key press should trigger an enter + win.hide(); + kwinApp()->platform()->keyboardKeyPressed(KEY_A, timestamp++); + QVERIFY(enteredSpy.wait()); + kwinApp()->platform()->keyboardKeyReleased(KEY_A, timestamp++); + + // Destroy the test client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(c)); +} + +void InternalWindowTest::testTouch() +{ + // touch events for internal windows are emulated through mouse events + QSignalSpy clientAddedSpy(workspace(), &Workspace::internalClientAdded); + QVERIFY(clientAddedSpy.isValid()); + HelperWindow win; + win.setGeometry(0, 0, 100, 100); + win.show(); + QTRY_COMPARE(clientAddedSpy.count(), 1); + + QSignalSpy pressSpy(&win, &HelperWindow::mousePressed); + QVERIFY(pressSpy.isValid()); + QSignalSpy releaseSpy(&win, &HelperWindow::mouseReleased); + QVERIFY(releaseSpy.isValid()); + QSignalSpy moveSpy(&win, &HelperWindow::mouseMoved); + QVERIFY(moveSpy.isValid()); + + quint32 timestamp = 1; + QCOMPARE(win.pressedButtons(), Qt::MouseButtons()); + kwinApp()->platform()->touchDown(0, QPointF(50, 50), timestamp++); + QCOMPARE(pressSpy.count(), 1); + QCOMPARE(win.latestGlobalMousePos(), QPoint(50, 50)); + QCOMPARE(win.pressedButtons(), Qt::MouseButtons(Qt::LeftButton)); + + // further touch down should not trigger + kwinApp()->platform()->touchDown(1, QPointF(75, 75), timestamp++); + QCOMPARE(pressSpy.count(), 1); + kwinApp()->platform()->touchUp(1, timestamp++); + QCOMPARE(releaseSpy.count(), 0); + QCOMPARE(win.latestGlobalMousePos(), QPoint(50, 50)); + QCOMPARE(win.pressedButtons(), Qt::MouseButtons(Qt::LeftButton)); + + // another press + kwinApp()->platform()->touchDown(1, QPointF(10, 10), timestamp++); + QCOMPARE(pressSpy.count(), 1); + QCOMPARE(win.latestGlobalMousePos(), QPoint(50, 50)); + QCOMPARE(win.pressedButtons(), Qt::MouseButtons(Qt::LeftButton)); + + // simulate the move + QCOMPARE(moveSpy.count(), 0); + kwinApp()->platform()->touchMotion(0, QPointF(80, 90), timestamp++); + QCOMPARE(moveSpy.count(), 1); + QCOMPARE(win.latestGlobalMousePos(), QPoint(80, 90)); + QCOMPARE(win.pressedButtons(), Qt::MouseButtons(Qt::LeftButton)); + + // move on other ID should not do anything + kwinApp()->platform()->touchMotion(1, QPointF(20, 30), timestamp++); + QCOMPARE(moveSpy.count(), 1); + QCOMPARE(win.latestGlobalMousePos(), QPoint(80, 90)); + QCOMPARE(win.pressedButtons(), Qt::MouseButtons(Qt::LeftButton)); + + // now up our main point + kwinApp()->platform()->touchUp(0, timestamp++); + QCOMPARE(releaseSpy.count(), 1); + QCOMPARE(win.latestGlobalMousePos(), QPoint(80, 90)); + QCOMPARE(win.pressedButtons(), Qt::MouseButtons()); + + // and up the additional point + kwinApp()->platform()->touchUp(1, timestamp++); + QCOMPARE(releaseSpy.count(), 1); + QCOMPARE(moveSpy.count(), 1); + QCOMPARE(win.latestGlobalMousePos(), QPoint(80, 90)); + QCOMPARE(win.pressedButtons(), Qt::MouseButtons()); +} + +void InternalWindowTest::testOpacity() +{ + // this test verifies that opacity is properly synced from QWindow to InternalClient + QSignalSpy clientAddedSpy(workspace(), &Workspace::internalClientAdded); + QVERIFY(clientAddedSpy.isValid()); + HelperWindow win; + win.setOpacity(0.5); + win.setGeometry(0, 0, 100, 100); + win.show(); + QTRY_COMPARE(clientAddedSpy.count(), 1); + auto internalClient = clientAddedSpy.first().first().value(); + QVERIFY(internalClient); + QVERIFY(internalClient->isInternal()); + QCOMPARE(internalClient->opacity(), 0.5); + + QSignalSpy opacityChangedSpy(internalClient, &InternalClient::opacityChanged); + QVERIFY(opacityChangedSpy.isValid()); + win.setOpacity(0.75); + QCOMPARE(opacityChangedSpy.count(), 1); + QCOMPARE(internalClient->opacity(), 0.75); +} + +void InternalWindowTest::testMove() +{ + QSignalSpy clientAddedSpy(workspace(), &Workspace::internalClientAdded); + QVERIFY(clientAddedSpy.isValid()); + HelperWindow win; + win.setOpacity(0.5); + win.setGeometry(0, 0, 100, 100); + win.show(); + QTRY_COMPARE(clientAddedSpy.count(), 1); + auto internalClient = clientAddedSpy.first().first().value(); + QVERIFY(internalClient); + QCOMPARE(internalClient->frameGeometry(), QRect(0, 0, 100, 100)); + + // normal move should be synced + internalClient->move(5, 10); + QCOMPARE(internalClient->frameGeometry(), QRect(5, 10, 100, 100)); + QTRY_COMPARE(win.geometry(), QRect(5, 10, 100, 100)); + // another move should also be synced + internalClient->move(10, 20); + QCOMPARE(internalClient->frameGeometry(), QRect(10, 20, 100, 100)); + QTRY_COMPARE(win.geometry(), QRect(10, 20, 100, 100)); + + // now move with a Geometry update blocker + { + GeometryUpdatesBlocker blocker(internalClient); + internalClient->move(5, 10); + // not synced! + QCOMPARE(win.geometry(), QRect(10, 20, 100, 100)); + } + // after destroying the blocker it should be synced + QTRY_COMPARE(win.geometry(), QRect(5, 10, 100, 100)); +} + +void InternalWindowTest::testSkipCloseAnimation_data() +{ + QTest::addColumn("initial"); + + QTest::newRow("set") << true; + QTest::newRow("not set") << false; +} + +void InternalWindowTest::testSkipCloseAnimation() +{ + QSignalSpy clientAddedSpy(workspace(), &Workspace::internalClientAdded); + QVERIFY(clientAddedSpy.isValid()); + 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(clientAddedSpy.count(), 1); + auto internalClient = clientAddedSpy.first().first().value(); + QVERIFY(internalClient); + QCOMPARE(internalClient->skipsCloseAnimation(), initial); + QSignalSpy skipCloseChangedSpy(internalClient, &Toplevel::skipCloseAnimationChanged); + QVERIFY(skipCloseChangedSpy.isValid()); + win.setProperty("KWIN_SKIP_CLOSE_ANIMATION", !initial); + QCOMPARE(skipCloseChangedSpy.count(), 1); + QCOMPARE(internalClient->skipsCloseAnimation(), !initial); + win.setProperty("KWIN_SKIP_CLOSE_ANIMATION", initial); + QCOMPARE(skipCloseChangedSpy.count(), 2); + QCOMPARE(internalClient->skipsCloseAnimation(), initial); +} + +void InternalWindowTest::testModifierClickUnrestrictedMove() +{ + QSignalSpy clientAddedSpy(workspace(), &Workspace::internalClientAdded); + QVERIFY(clientAddedSpy.isValid()); + HelperWindow win; + win.setGeometry(0, 0, 100, 100); + win.setFlags(win.flags() & ~Qt::FramelessWindowHint); + win.show(); + QTRY_COMPARE(clientAddedSpy.count(), 1); + auto internalClient = clientAddedSpy.first().first().value(); + QVERIFY(internalClient); + QVERIFY(internalClient->isDecorated()); + + KConfigGroup group = kwinApp()->config()->group("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 + Cursors::self()->mouse()->setPos(internalClient->frameGeometry().center()); + + // simulate modifier+click + quint32 timestamp = 1; + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + QVERIFY(!internalClient->isMove()); + kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); + QVERIFY(internalClient->isMove()); + // release modifier should not change it + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTMETA, timestamp++); + QVERIFY(internalClient->isMove()); + // but releasing the key should end move/resize + kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); + QVERIFY(!internalClient->isMove()); +} + +void InternalWindowTest::testModifierScroll() +{ + QSignalSpy clientAddedSpy(workspace(), &Workspace::internalClientAdded); + QVERIFY(clientAddedSpy.isValid()); + HelperWindow win; + win.setGeometry(0, 0, 100, 100); + win.setFlags(win.flags() & ~Qt::FramelessWindowHint); + win.show(); + QTRY_COMPARE(clientAddedSpy.count(), 1); + auto internalClient = clientAddedSpy.first().first().value(); + QVERIFY(internalClient); + QVERIFY(internalClient->isDecorated()); + + KConfigGroup group = kwinApp()->config()->group("MouseBindings"); + group.writeEntry("CommandAllKey", "Meta"); + group.writeEntry("CommandAllWheel", "change opacity"); + group.sync(); + workspace()->slotReconfigure(); + + // move cursor on window + Cursors::self()->mouse()->setPos(internalClient->frameGeometry().center()); + + // set the opacity to 0.5 + internalClient->setOpacity(0.5); + QCOMPARE(internalClient->opacity(), 0.5); + quint32 timestamp = 1; + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + kwinApp()->platform()->pointerAxisVertical(-5, timestamp++); + QCOMPARE(internalClient->opacity(), 0.6); + kwinApp()->platform()->pointerAxisVertical(5, timestamp++); + QCOMPARE(internalClient->opacity(), 0.5); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTMETA, timestamp++); +} + +void InternalWindowTest::testPopup() +{ + QSignalSpy clientAddedSpy(workspace(), &Workspace::internalClientAdded); + QVERIFY(clientAddedSpy.isValid()); + HelperWindow win; + win.setGeometry(0, 0, 100, 100); + win.setFlags(win.flags() | Qt::Popup); + win.show(); + QTRY_COMPARE(clientAddedSpy.count(), 1); + auto internalClient = clientAddedSpy.first().first().value(); + QVERIFY(internalClient); + QCOMPARE(internalClient->isPopupWindow(), true); +} + +void InternalWindowTest::testScale() +{ + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, + Q_ARG(int, 2), + Q_ARG(QVector, QVector({QRect(0,0,1280, 1024), QRect(1280/2, 0, 1280, 1024)})), + Q_ARG(QVector, QVector({2,2}))); + + QSignalSpy clientAddedSpy(workspace(), &Workspace::internalClientAdded); + QVERIFY(clientAddedSpy.isValid()); + HelperWindow win; + win.setGeometry(0, 0, 100, 100); + win.setFlags(win.flags() | Qt::Popup); + win.show(); + QCOMPARE(win.devicePixelRatio(), 2.0); + QTRY_COMPARE(clientAddedSpy.count(), 1); + auto internalClient = clientAddedSpy.first().first().value(); + QCOMPARE(internalClient->bufferScale(), 2); +} + +void InternalWindowTest::testWindowType_data() +{ + QTest::addColumn("windowType"); + + QTest::newRow("normal") << NET::Normal; + QTest::newRow("desktop") << NET::Desktop; + QTest::newRow("Dock") << NET::Dock; + QTest::newRow("Toolbar") << NET::Toolbar; + QTest::newRow("Menu") << NET::Menu; + QTest::newRow("Dialog") << NET::Dialog; + QTest::newRow("Utility") << NET::Utility; + QTest::newRow("Splash") << NET::Splash; + QTest::newRow("DropdownMenu") << NET::DropdownMenu; + QTest::newRow("PopupMenu") << NET::PopupMenu; + QTest::newRow("Tooltip") << NET::Tooltip; + QTest::newRow("Notification") << NET::Notification; + QTest::newRow("ComboBox") << NET::ComboBox; + QTest::newRow("OnScreenDisplay") << NET::OnScreenDisplay; + QTest::newRow("CriticalNotification") << NET::CriticalNotification; +} + +void InternalWindowTest::testWindowType() +{ + QSignalSpy clientAddedSpy(workspace(), &Workspace::internalClientAdded); + QVERIFY(clientAddedSpy.isValid()); + HelperWindow win; + win.setGeometry(0, 0, 100, 100); + QFETCH(NET::WindowType, windowType); + KWindowSystem::setType(win.winId(), windowType); + win.show(); + QTRY_COMPARE(clientAddedSpy.count(), 1); + auto internalClient = clientAddedSpy.first().first().value(); + QVERIFY(internalClient); + QCOMPARE(internalClient->windowType(), windowType); +} + +void InternalWindowTest::testChangeWindowType_data() +{ + QTest::addColumn("windowType"); + + QTest::newRow("desktop") << NET::Desktop; + QTest::newRow("Dock") << NET::Dock; + QTest::newRow("Toolbar") << NET::Toolbar; + QTest::newRow("Menu") << NET::Menu; + QTest::newRow("Dialog") << NET::Dialog; + QTest::newRow("Utility") << NET::Utility; + QTest::newRow("Splash") << NET::Splash; + QTest::newRow("DropdownMenu") << NET::DropdownMenu; + QTest::newRow("PopupMenu") << NET::PopupMenu; + QTest::newRow("Tooltip") << NET::Tooltip; + QTest::newRow("Notification") << NET::Notification; + QTest::newRow("ComboBox") << NET::ComboBox; + QTest::newRow("OnScreenDisplay") << NET::OnScreenDisplay; + QTest::newRow("CriticalNotification") << NET::CriticalNotification; +} + +void InternalWindowTest::testChangeWindowType() +{ + QSignalSpy clientAddedSpy(workspace(), &Workspace::internalClientAdded); + QVERIFY(clientAddedSpy.isValid()); + HelperWindow win; + win.setGeometry(0, 0, 100, 100); + win.show(); + QTRY_COMPARE(clientAddedSpy.count(), 1); + auto internalClient = clientAddedSpy.first().first().value(); + QVERIFY(internalClient); + QCOMPARE(internalClient->windowType(), NET::Normal); + + QFETCH(NET::WindowType, windowType); + KWindowSystem::setType(win.winId(), windowType); + QTRY_COMPARE(internalClient->windowType(), windowType); + + KWindowSystem::setType(win.winId(), NET::Normal); + QTRY_COMPARE(internalClient->windowType(), NET::Normal); +} + +void InternalWindowTest::testEffectWindow() +{ + QSignalSpy clientAddedSpy(workspace(), &Workspace::internalClientAdded); + QVERIFY(clientAddedSpy.isValid()); + HelperWindow win; + win.setGeometry(0, 0, 100, 100); + win.show(); + QTRY_COMPARE(clientAddedSpy.count(), 1); + auto internalClient = clientAddedSpy.first().first().value(); + QVERIFY(internalClient); + QVERIFY(internalClient->effectWindow()); + QCOMPARE(internalClient->effectWindow()->internalWindow(), &win); + + QCOMPARE(effects->findWindow(&win), internalClient->effectWindow()); + QCOMPARE(effects->findWindow(&win)->internalWindow(), &win); +} + +void InternalWindowTest::testReentrantSetFrameGeometry() +{ + // This test verifies that calling setFrameGeometry() from a slot connected directly + // to the frameGeometryChanged() signal won't cause an infinite recursion. + + // Create an internal window. + QSignalSpy clientAddedSpy(workspace(), &Workspace::internalClientAdded); + QVERIFY(clientAddedSpy.isValid()); + HelperWindow win; + win.setGeometry(0, 0, 100, 100); + win.show(); + QTRY_COMPARE(clientAddedSpy.count(), 1); + auto client = clientAddedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->pos(), QPoint(0, 0)); + + // Let's pretend that there is a script that really wants the client to be at (100, 100). + connect(client, &AbstractClient::frameGeometryChanged, this, [client]() { + client->setFrameGeometry(QRect(QPoint(100, 100), client->size())); + }); + + // Trigger the lambda above. + client->move(QPoint(40, 50)); + + // Eventually, the client will end up at (100, 100). + QCOMPARE(client->pos(), QPoint(100, 100)); +} + +} + +WAYLANDTEST_MAIN(KWin::InternalWindowTest) +#include "internal_window.moc" diff --git a/autotests/integration/keyboard_layout_test.cpp b/autotests/integration/keyboard_layout_test.cpp new file mode 100644 index 0000000..70b6b3a --- /dev/null +++ b/autotests/integration/keyboard_layout_test.cpp @@ -0,0 +1,544 @@ +/* + 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 "abstract_client.h" +#include "keyboard_input.h" +#include "keyboard_layout.h" +#include "platform.h" +#include "virtualdesktops.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include + +using namespace KWin; +using namespace KWayland::Client; + +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(layoutsReconfiguredSpy.isValid()); + QVERIFY(layoutChangedSpy.isValid()); + + 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("currentLayoutChanged"), this, SIGNAL(layoutChanged(QString)))); + } + +Q_SIGNALS: + void layoutChanged(const QString &name); + void layoutListChanged(); + +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testReconfigure(); + void testChangeLayoutThroughDBus(); + void testPerLayoutShortcut(); + void testDBusServiceExport(); + void testVirtualDesktopPolicy(); + void testWindowPolicy(); + void testApplicationPolicy(); + void testNumLock(); + +private: + void reconfigureLayouts(); + void resetLayouts(); + auto changeLayout(const QString &layoutName); + void callSession(const QString &method); + QSignalSpy layoutsReconfiguredSpy; + QSignalSpy layoutChangedSpy; + KConfigGroup layoutGroup; +}; + +void KeyboardLayoutTest::reconfigureLayouts() +{ + // create DBus signal to reload + QDBusMessage message = QDBusMessage::createSignal(QStringLiteral("/Layouts"), QStringLiteral("org.kde.keyboard"), QStringLiteral("reloadConfig")); + QVERIFY(QDBusConnection::sessionBus().send(message)); + + 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"); + } else { + layoutGroup.deleteEntry("SwitchMode"); + } + reconfigureLayouts(); + + layoutGroup.writeEntry("SwitchMode", policy); + reconfigureLayouts(); + + callSession(QStringLiteral("loadSession")); +} + +auto KeyboardLayoutTest::changeLayout(const QString &layoutName) { + QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.keyboard"), QStringLiteral("/Layouts"), QStringLiteral("org.kde.KeyboardLayouts"), QStringLiteral("setLayout")); + msg << layoutName; + 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(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); + kwinApp()->setKxkbConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); + + layoutGroup = kwinApp()->kxkbConfig()->group("Layout"); + layoutGroup.deleteGroup(); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + waylandServer()->initWorkspace(); + + // 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)")); + auto layouts = xkb->layoutNames(); + QCOMPARE(layouts.size(), 1); + QVERIFY(layouts.contains(0)); + QCOMPARE(layouts[0], QStringLiteral("English (US)")); + + // create a new keymap + KConfigGroup layoutGroup = kwinApp()->kxkbConfig()->group("Layout"); + layoutGroup.writeEntry("LayoutList", QStringLiteral("de,us")); + layoutGroup.sync(); + + reconfigureLayouts(); + // now we should have two layouts + QCOMPARE(xkb->numberOfLayouts(), 2u); + // default layout is German + QCOMPARE(xkb->layoutName(), QStringLiteral("German")); + layouts = xkb->layoutNames(); + QCOMPARE(layouts.size(), 2); + QVERIFY(layouts.contains(0)); + QVERIFY(layouts.contains(1)); + QCOMPARE(layouts[0], QStringLiteral("German")); + QCOMPARE(layouts[1], QStringLiteral("English (US)")); +} + +void KeyboardLayoutTest::testChangeLayoutThroughDBus() +{ + // this test verifies that the layout can be changed through DBus + // first configure layouts + layoutGroup.writeEntry("LayoutList", QStringLiteral("de,us,de(neo)")); + 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"); + // make sure the garbage is wiped out on saving + resetLayouts(); + QVERIFY(!layoutGroup.hasKey("LayoutDefaultFoo")); + + // now change through DBus to English + auto reply = changeLayout(QStringLiteral("English (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)")); + + // switch to a layout which does not exist + reply = changeLayout(QStringLiteral("French")); + QVERIFY(!reply.isError()); + QCOMPARE(reply.reply().arguments().first().toBool(), false); + QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)")); + QVERIFY(!layoutChangedSpy.wait(1000)); + QVERIFY(layoutChangedSpy.isEmpty()); + + // switch to another layout should work + reply = changeLayout(QStringLiteral("German")); + QVERIFY(!reply.isError()); + QCOMPARE(reply.reply().arguments().first().toBool(), true); + QCOMPARE(xkb->layoutName(), QStringLiteral("German")); + // FIXME: need to pass +// QVERIFY(layoutChangedSpy.wait(1000)); +// QCOMPARE(layoutChangedSpy.count(), 1); +// layoutChangedSpy.clear(); + + // switching to same layout should also work + reply = changeLayout(QStringLiteral("German")); + QVERIFY(!reply.isError()); + QCOMPARE(reply.reply().arguments().first().toBool(), true); + QCOMPARE(xkb->layoutName(), QStringLiteral("German")); + QVERIFY(!layoutChangedSpy.wait(1000)); + QVERIFY(layoutChangedSpy.isEmpty()); +} + +void KeyboardLayoutTest::testPerLayoutShortcut() +{ + // this test verifies that per-layout global shortcuts are working correctly. + // first configure layouts + layoutGroup.writeEntry("LayoutList", QStringLiteral("us,de,de(neo)")); + 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; + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_2, timestamp++); + QVERIFY(layoutChangedSpy.wait()); + // now layout should be German + QCOMPARE(xkb->layoutName(), QStringLiteral("German")); + // release keys again + kwinApp()->platform()->keyboardKeyReleased(KEY_2, timestamp++); + // switch back to English + kwinApp()->platform()->keyboardKeyPressed(KEY_1, timestamp++); + QVERIFY(layoutChangedSpy.wait()); + QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)")); + // release keys again + kwinApp()->platform()->keyboardKeyReleased(KEY_1, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); +} + +void KeyboardLayoutTest::testDBusServiceExport() +{ + // verifies that the dbus service is only exported if there are at least two layouts + + // first configure layouts, with just one layout + layoutGroup.writeEntry("LayoutList", QStringLiteral("us")); + layoutGroup.sync(); + reconfigureLayouts(); + auto xkb = input()->keyboard()->xkb(); + QCOMPARE(xkb->numberOfLayouts(), 1u); + // default layout is English + QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)")); + // with one layout we should not have the dbus interface + QVERIFY(!QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.keyboard")).value()); + + // reconfigure to two layouts + layoutGroup.writeEntry("LayoutList", QStringLiteral("us,de")); + layoutGroup.sync(); + reconfigureLayouts(); + QCOMPARE(xkb->numberOfLayouts(), 2u); + QVERIFY(QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.keyboard")).value()); + + // and back to one layout + layoutGroup.writeEntry("LayoutList", QStringLiteral("us")); + layoutGroup.sync(); + reconfigureLayouts(); + QCOMPARE(xkb->numberOfLayouts(), 1u); + QVERIFY(!QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.keyboard")).value()); +} + +void KeyboardLayoutTest::testVirtualDesktopPolicy() +{ + layoutGroup.writeEntry("LayoutList", QStringLiteral("us,de,de(neo)")); + layoutGroup.writeEntry("SwitchMode", QStringLiteral("Desktop")); + 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(xkb->layoutNames()[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.isValid()); + QVERIFY(deletedDesktopSpy.wait()); + resetLayouts(); + QCOMPARE(layoutGroup.keyList().filter( QStringLiteral("LayoutDefault") ).count(), 1); +} + +void KeyboardLayoutTest::testWindowPolicy() +{ + layoutGroup.writeEntry("LayoutList", QStringLiteral("us,de,de(neo)")); + layoutGroup.writeEntry("SwitchMode", QStringLiteral("Window")); + layoutGroup.sync(); + reconfigureLayouts(); + auto xkb = input()->keyboard()->xkb(); + QCOMPARE(xkb->numberOfLayouts(), 3u); + QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)")); + + // create a window + using namespace KWayland::Client; + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + auto c1 = Test::renderAndWaitForShown(surface.data(), QSize(100, 100), Qt::blue); + QVERIFY(c1); + + // now switch layout + auto reply = changeLayout(QStringLiteral("German")); + reply.waitForFinished(); + QCOMPARE(xkb->layoutName(), QStringLiteral("German")); + + // create a second window + QScopedPointer surface2(Test::createSurface()); + QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data())); + auto c2 = Test::renderAndWaitForShown(surface2.data(), 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(QStringLiteral("German (Neo 2)")); + reply.waitForFinished(); + QCOMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)")); + + // activate other window + workspace()->activateClient(c1); + QCOMPARE(xkb->layoutName(), QStringLiteral("German")); + workspace()->activateClient(c2); + QCOMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)")); +} + +void KeyboardLayoutTest::testApplicationPolicy() +{ + layoutGroup.writeEntry("LayoutList", QStringLiteral("us,de,de(neo)")); + layoutGroup.writeEntry("SwitchMode", QStringLiteral("WinClass")); + layoutGroup.sync(); + reconfigureLayouts(); + auto xkb = input()->keyboard()->xkb(); + QCOMPARE(xkb->numberOfLayouts(), 3u); + QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)")); + + // create a window + using namespace KWayland::Client; + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + shellSurface->setAppId(QByteArrayLiteral("org.kde.foo")); + auto c1 = Test::renderAndWaitForShown(surface.data(), QSize(100, 100), Qt::blue); + QVERIFY(c1); + + // create a second window + QScopedPointer surface2(Test::createSurface()); + QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data())); + shellSurface2->setAppId(QByteArrayLiteral("org.kde.foo")); + auto c2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 100), Qt::red); + QVERIFY(c2); + // now switch layout + layoutChangedSpy.clear(); + changeLayout(QStringLiteral("German (Neo 2)")); + QVERIFY(layoutChangedSpy.wait()); + QCOMPARE(layoutChangedSpy.count(), 1); + QCOMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)")); + + resetLayouts(); + // to trigger layout apply for current client + workspace()->activateClient(c1); + workspace()->activateClient(c2); + + QCOMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)")); + + // activate other window + workspace()->activateClient(c1); + // it is the same application and should not switch the layout + QVERIFY(!layoutChangedSpy.wait(1000)); + QCOMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)")); + workspace()->activateClient(c2); + QVERIFY(!layoutChangedSpy.wait(1000)); + QCOMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)")); + + shellSurface2.reset(); + surface2.reset(); + QVERIFY(Test::waitForWindowDestroyed(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")); + 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(Xkb::LED::NumLock)); + quint32 timestamp = 0; + kwinApp()->platform()->keyboardKeyPressed(KEY_NUMLOCK, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_NUMLOCK, timestamp++); + // now it should be on + QVERIFY(xkb->leds().testFlag(Xkb::LED::NumLock)); + // and back to off + kwinApp()->platform()->keyboardKeyPressed(KEY_NUMLOCK, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_NUMLOCK, timestamp++); + QVERIFY(!xkb->leds().testFlag(Xkb::LED::NumLock)); + + // let's reconfigure to enable through config + auto group = InputConfig::self()->inputConfig()->group("Keyboard"); + group.writeEntry("NumLock", 0); + group.sync(); + xkb->reconfigure(); + // now it should be on + QVERIFY(xkb->leds().testFlag(Xkb::LED::NumLock)); + // pressing should result in it being off + kwinApp()->platform()->keyboardKeyPressed(KEY_NUMLOCK, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_NUMLOCK, timestamp++); + QVERIFY(!xkb->leds().testFlag(Xkb::LED::NumLock)); + + // pressing again should enable it + kwinApp()->platform()->keyboardKeyPressed(KEY_NUMLOCK, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_NUMLOCK, timestamp++); + QVERIFY(xkb->leds().testFlag(Xkb::LED::NumLock)); + + // now reconfigure to disable on load + group.writeEntry("NumLock", 1); + group.sync(); + xkb->reconfigure(); + QVERIFY(!xkb->leds().testFlag(Xkb::LED::NumLock)); +} + +WAYLANDTEST_MAIN(KeyboardLayoutTest) +#include "keyboard_layout_test.moc" diff --git a/autotests/integration/keymap_creation_failure_test.cpp b/autotests/integration/keymap_creation_failure_test.cpp new file mode 100644 index 0000000..9dcfa17 --- /dev/null +++ b/autotests/integration/keymap_creation_failure_test.cpp @@ -0,0 +1,90 @@ +/* + 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 "abstract_client.h" +#include "keyboard_input.h" +#include "keyboard_layout.h" +#include "platform.h" +#include "virtualdesktops.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include + +#include + +using namespace KWin; +using namespace KWayland::Client; + +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(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); + kwinApp()->setKxkbConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); + KConfigGroup layoutGroup = kwinApp()->kxkbConfig()->group("Layout"); + layoutGroup.writeEntry("LayoutList", QStringLiteral("no")); + layoutGroup.writeEntry("Model", "no"); + layoutGroup.writeEntry("Options", "no"); + layoutGroup.sync(); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + waylandServer()->initWorkspace(); +} + +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 + kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, 0); + kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, 1); +} + +WAYLANDTEST_MAIN(KeymapCreationFailureTest) +#include "keymap_creation_failure_test.moc" diff --git a/autotests/integration/kwin_wayland_test.cpp b/autotests/integration/kwin_wayland_test.cpp new file mode 100644 index 0000000..8bfa215 --- /dev/null +++ b/autotests/integration/kwin_wayland_test.cpp @@ -0,0 +1,181 @@ +/* + 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 "../../platform.h" +#include "../../composite.h" +#include "../../effects.h" +#include "../../wayland_server.h" +#include "../../workspace.h" +#include "../../xcbutils.h" +#include "../../xwl/xwayland.h" +#include "../../virtualkeyboard.h" + +#include + +#include +#include +#include +#include +#include +#include + +// system +#include +#include +#include + +namespace KWin +{ + +WaylandTestApplication::WaylandTestApplication(OperationMode mode, int &argc, char **argv) + : ApplicationWaylandAbstract(mode, argc, argv) +{ + QStandardPaths::setTestModeEnabled(true); + // TODO: add a test move to kglobalaccel instead? + QFile{QStandardPaths::locate(QStandardPaths::ConfigLocation, QStringLiteral("kglobalshortcutsrc"))}.remove(); + QIcon::setThemeName(QStringLiteral("breeze")); +#ifdef KWIN_BUILD_ACTIVITIES + setUseKActivities(false); +#endif + qputenv("KWIN_COMPOSE", QByteArrayLiteral("Q")); + qunsetenv("XKB_DEFAULT_RULES"); + qunsetenv("XKB_DEFAULT_MODEL"); + qunsetenv("XKB_DEFAULT_LAYOUT"); + qunsetenv("XKB_DEFAULT_VARIANT"); + qunsetenv("XKB_DEFAULT_OPTIONS"); + + const auto ownPath = libraryPaths().last(); + removeLibraryPath(ownPath); + addLibraryPath(ownPath); + + const auto plugins = KPluginLoader::findPluginsById(QStringLiteral("org.kde.kwin.waylandbackends"), "KWinWaylandVirtualBackend"); + if (plugins.empty()) { + quit(); + return; + } + initPlatform(plugins.first()); + WaylandServer::create(this); + setProcessStartupEnvironment(QProcessEnvironment::systemEnvironment()); +} + +WaylandTestApplication::~WaylandTestApplication() +{ + setTerminating(); + kwinApp()->platform()->setOutputsEnabled(false); + // 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) { + static_cast(effects)->unloadAllEffects(); + } + delete m_xwayland; + m_xwayland = nullptr; + destroyWorkspace(); + waylandServer()->dispatch(); + if (QStyle *s = style()) { + s->unpolish(this); + } + waylandServer()->terminateClientConnections(); + destroyCompositor(); +} + +void WaylandTestApplication::performStartup() +{ + if (!m_inputMethodServerToStart.isEmpty()) { + VirtualKeyboard::create(); + if (m_inputMethodServerToStart != QStringLiteral("internal")) { + int socket = dup(waylandServer()->createInputMethodConnection()); + if (socket >= 0) { + QProcessEnvironment environment = processStartupEnvironment(); + environment.insert(QStringLiteral("WAYLAND_SOCKET"), QByteArray::number(socket)); + environment.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("wayland")); + environment.remove("DISPLAY"); + environment.remove("WAYLAND_DISPLAY"); + QProcess *p = new Process(this); + p->setProcessChannelMode(QProcess::ForwardedErrorChannel); + connect(p, qOverload(&QProcess::finished), this, + [p] { + if (waylandServer()) { + waylandServer()->destroyInputMethodConnection(); + } + p->deleteLater(); + } + ); + p->setProcessEnvironment(environment); + p->setProgram(m_inputMethodServerToStart); + // p->setArguments(arguments); + p->start(); + connect(waylandServer(), &WaylandServer::terminatingInternalClientConnection, p, [p] { + p->kill(); + p->waitForFinished(); + }); + } + } + } + + // first load options - done internally by a different thread + createOptions(); + waylandServer()->createInternalConnection(); + + // try creating the Wayland Backend + createInput(); + createBackend(); +} + +void WaylandTestApplication::createBackend() +{ + Platform *platform = kwinApp()->platform(); + connect(platform, &Platform::screensQueried, this, &WaylandTestApplication::continueStartupWithScreens); + connect(platform, &Platform::initFailed, this, + [] () { + std::cerr << "FATAL ERROR: backend failed to initialize, exiting now" << std::endl; + ::exit(1); + } + ); + platform->init(); +} + +void WaylandTestApplication::continueStartupWithScreens() +{ + disconnect(kwinApp()->platform(), &Platform::screensQueried, this, &WaylandTestApplication::continueStartupWithScreens); + createScreens(); + WaylandCompositor::create(); + connect(Compositor::self(), &Compositor::sceneCreated, this, &WaylandTestApplication::continueStartupWithScene); +} + +void WaylandTestApplication::finalizeStartup() +{ + if (m_xwayland) { + disconnect(m_xwayland, &Xwl::Xwayland::errorOccurred, this, &WaylandTestApplication::finalizeStartup); + disconnect(m_xwayland, &Xwl::Xwayland::started, this, &WaylandTestApplication::finalizeStartup); + } + notifyStarted(); +} + +void WaylandTestApplication::continueStartupWithScene() +{ + disconnect(Compositor::self(), &Compositor::sceneCreated, this, &WaylandTestApplication::continueStartupWithScene); + + createWorkspace(); + + if (!waylandServer()->start()) { + qFatal("Failed to initialize the Wayland server, exiting now"); + } + + if (operationMode() == OperationModeWaylandOnly) { + finalizeStartup(); + return; + } + + m_xwayland = new Xwl::Xwayland(this); + connect(m_xwayland, &Xwl::Xwayland::errorOccurred, this, &WaylandTestApplication::finalizeStartup); + connect(m_xwayland, &Xwl::Xwayland::started, this, &WaylandTestApplication::finalizeStartup); + m_xwayland->start(); +} + +} diff --git a/autotests/integration/kwin_wayland_test.h b/autotests/integration/kwin_wayland_test.h new file mode 100644 index 0000000..31e212e --- /dev/null +++ b/autotests/integration/kwin_wayland_test.h @@ -0,0 +1,378 @@ +/* + 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 "../../main.h" + +// Qt +#include + +// KWayland +#include + +#include "qwayland-wlr-layer-shell-unstable-v1.h" +#include "qwayland-xdg-shell.h" + +namespace KWayland +{ +namespace Client +{ +class AppMenuManager; +class ConnectionThread; +class Compositor; +class IdleInhibitManager; +class PlasmaShell; +class PlasmaWindowManagement; +class PointerConstraints; +class Seat; +class ServerSideDecorationManager; +class ShadowManager; +class ShmPool; +class SubCompositor; +class SubSurface; +class Surface; +class XdgDecorationManager; +class OutputManagement; +class TextInputManager; +} +} + +namespace QtWayland +{ +class zwp_input_panel_surface_v1; +} + +namespace KWin +{ +namespace Xwl +{ +class Xwayland; +} + +class AbstractClient; + +class WaylandTestApplication : public ApplicationWaylandAbstract +{ + Q_OBJECT +public: + WaylandTestApplication(OperationMode mode, int &argc, char **argv); + ~WaylandTestApplication() override; + + void setInputMethodServerToStart(const QString &inputMethodServer) { + m_inputMethodServerToStart = inputMethodServer; + } +protected: + void performStartup() override; + +private: + void createBackend(); + void continueStartupWithScreens(); + void continueStartupWithScene(); + void finalizeStartup(); + + Xwl::Xwayland *m_xwayland = nullptr; + QString m_inputMethodServerToStart; +}; + +namespace Test +{ + +class MockInputMethod; + +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; +}; + +/** + * 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: + QScopedPointer 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 QRect &rect); + +protected: + void xdg_popup_configure(int32_t x, int32_t y, int32_t width, int32_t height) override; + +private: + QScopedPointer m_xdgSurface; +}; + +enum class AdditionalWaylandInterface { + Seat = 1 << 0, + Decoration = 1 << 1, + PlasmaShell = 1 << 2, + WindowManagement = 1 << 3, + PointerConstraints = 1 << 4, + IdleInhibition = 1 << 5, + AppMenu = 1 << 6, + ShadowManager = 1 << 7, + XdgDecoration = 1 << 8, + OutputManagement = 1 << 9, + TextInputManagerV2 = 1 << 10, + InputMethodV1 = 1 << 11, + LayerShellV1 = 1 << 12, +}; +Q_DECLARE_FLAGS(AdditionalWaylandInterfaces, AdditionalWaylandInterface) +/** + * 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::ServerSideDecorationManager *waylandServerSideDecoration(); +KWayland::Client::PlasmaShell *waylandPlasmaShell(); +KWayland::Client::PlasmaWindowManagement *waylandWindowManagement(); +KWayland::Client::PointerConstraints *waylandPointerConstraints(); +KWayland::Client::IdleInhibitManager *waylandIdleInhibitManager(); +KWayland::Client::AppMenuManager *waylandAppMenuManager(); +KWayland::Client::XdgDecorationManager *xdgDecorationManager(); +KWayland::Client::OutputManagement *waylandOutputManagement(); +KWayland::Client::TextInputManager *waylandTextInputManager(); +QVector waylandOutputs(); + +bool waitForWaylandPointer(); +bool waitForWaylandTouch(); +bool waitForWaylandKeyboard(); + +void flushWaylandConnection(); + +KWayland::Client::Surface *createSurface(QObject *parent = nullptr); +KWayland::Client::SubSurface *createSubSurface(KWayland::Client::Surface *surface, + KWayland::Client::Surface *parentSurface, QObject *parent = nullptr); + +LayerSurfaceV1 *createLayerSurfaceV1(KWayland::Client::Surface *surface, + const QString &scope, + KWayland::Client::Output *output = nullptr, + LayerShellV1::layer layer = LayerShellV1::layer_top); + +enum class CreationSetup { + CreateOnly, + CreateAndConfigure, /// commit and wait for the configure event, making this surface ready to commit buffers +}; + +QtWayland::zwp_input_panel_surface_v1 *createInputPanelSurfaceV1(KWayland::Client::Surface *surface, + KWayland::Client::Output *output); + +KWayland::Client::XdgShellSurface *createXdgShellStableSurface(KWayland::Client::Surface *surface, + QObject *parent = nullptr, + CreationSetup = CreationSetup::CreateAndConfigure); +KWayland::Client::XdgShellPopup *createXdgShellStablePopup(KWayland::Client::Surface *surface, + KWayland::Client::XdgShellSurface *parentSurface, + const KWayland::Client::XdgPositioner &positioner, + QObject *parent = nullptr, + CreationSetup = CreationSetup::CreateAndConfigure); + +XdgToplevel *createXdgToplevelSurface(KWayland::Client::Surface *surface, QObject *parent = nullptr, + CreationSetup configureMode = CreationSetup::CreateAndConfigure); + +XdgPositioner *createXdgPositioner(); + +XdgPopup *createXdgPopupSurface(KWayland::Client::Surface *surface, XdgSurface *parentSurface, + XdgPositioner *positioner, QObject *parent = nullptr, + CreationSetup configureMode = CreationSetup::CreateAndConfigure); + + +/** + * Commits the XdgShellSurface to the given surface, and waits for the configure event from the compositor + */ +void initXdgShellSurface(KWayland::Client::Surface *surface, KWayland::Client::XdgShellSurface *shellSurface); +void initXdgShellPopup(KWayland::Client::Surface *surface, KWayland::Client::XdgShellPopup *popup); + + + +/** + * 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); + +/** + * 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); + +/** + * Waits till a new AbstractClient is shown and returns the created AbstractClient. + * If no AbstractClient gets shown during @p timeout @c null is returned. + */ +AbstractClient *waitForWaylandWindowShown(int timeout = 5000); + +/** + * Combination of @link{render} and @link{waitForWaylandWindowShown}. + */ +AbstractClient *renderAndWaitForShown(KWayland::Client::Surface *surface, const QSize &size, const QColor &color, const QImage::Format &format = QImage::Format_ARGB32, int timeout = 5000); + +/** + * Waits for the @p client to be destroyed. + */ +bool waitForWindowDestroyed(AbstractClient *client); + +/** + * 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(); +} + +} + +Q_DECLARE_OPERATORS_FOR_FLAGS(KWin::Test::AdditionalWaylandInterfaces) +Q_DECLARE_METATYPE(KWin::Test::XdgToplevel::States) + +#define WAYLANDTEST_MAIN_HELPER(TestObject, DPI, OperationMode) \ +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"); \ + DPI; \ + KWin::WaylandTestApplication app(OperationMode, argc, argv); \ + app.setAttribute(Qt::AA_Use96Dpi, true); \ + TestObject tc; \ + return QTest::qExec(&tc, argc, argv); \ +} + +#ifdef NO_XWAYLAND +#define WAYLANDTEST_MAIN(TestObject) WAYLANDTEST_MAIN_HELPER(TestObject, QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps), KWin::Application::OperationModeWaylandOnly) +#else +#define WAYLANDTEST_MAIN(TestObject) WAYLANDTEST_MAIN_HELPER(TestObject, QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps), KWin::Application::OperationModeXwayland) +#endif + +#endif diff --git a/autotests/integration/kwinbindings_test.cpp b/autotests/integration/kwinbindings_test.cpp new file mode 100644 index 0000000..164dc20 --- /dev/null +++ b/autotests/integration/kwinbindings_test.cpp @@ -0,0 +1,254 @@ +/* + 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 "abstract_client.h" +#include "cursor.h" +#include "input.h" +#include "platform.h" +#include "screens.h" +#include "scripting/scripting.h" +#include "useractions.h" +#include "virtualdesktops.h" +#include "wayland_server.h" +#include "workspace.h" + +#include + +#include +#include +#include + +using namespace KWin; +using namespace KWayland::Client; + +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(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + waylandServer()->initWorkspace(); +} + +void KWinBindingsTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); + screens()->setCurrent(0); + KWin::Cursors::self()->mouse()->setPos(QPoint(640, 512)); +} + +void KWinBindingsTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void KWinBindingsTest::testSwitchWindow() +{ + // first create windows + QScopedPointer surface1(Test::createSurface()); + QScopedPointer shellSurface1(Test::createXdgShellStableSurface(surface1.data())); + auto c1 = Test::renderAndWaitForShown(surface1.data(), QSize(100, 50), Qt::blue); + QScopedPointer surface2(Test::createSurface()); + QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data())); + auto c2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 50), Qt::blue); + QScopedPointer surface3(Test::createSurface()); + QScopedPointer shellSurface3(Test::createXdgShellStableSurface(surface3.data())); + auto c3 = Test::renderAndWaitForShown(surface3.data(), QSize(100, 50), Qt::blue); + QScopedPointer surface4(Test::createSurface()); + QScopedPointer shellSurface4(Test::createXdgShellStableSurface(surface4.data())); + auto c4 = Test::renderAndWaitForShown(surface4.data(), QSize(100, 50), Qt::blue); + + QVERIFY(c4->isActive()); + QVERIFY(c4 != c3); + QVERIFY(c3 != c2); + QVERIFY(c2 != c1); + + // let's position all windows + c1->move(0, 0); + c2->move(200, 0); + c3->move(200, 200); + c4->move(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()->activeClient(), c1); + invokeShortcut(QStringLiteral("Switch Window Right")); + QTRY_COMPARE(workspace()->activeClient(), c2); + invokeShortcut(QStringLiteral("Switch Window Down")); + QTRY_COMPARE(workspace()->activeClient(), c3); + invokeShortcut(QStringLiteral("Switch Window Left")); + QTRY_COMPARE(workspace()->activeClient(), c4); + // test opposite direction + invokeShortcut(QStringLiteral("Switch Window Left")); + QTRY_COMPARE(workspace()->activeClient(), c3); + invokeShortcut(QStringLiteral("Switch Window Down")); + QTRY_COMPARE(workspace()->activeClient(), c2); + invokeShortcut(QStringLiteral("Switch Window Right")); + QTRY_COMPARE(workspace()->activeClient(), c1); + invokeShortcut(QStringLiteral("Switch Window Up")); + QTRY_COMPARE(workspace()->activeClient(), c4); +} + +void KWinBindingsTest::testSwitchWindowScript() +{ + QVERIFY(Scripting::self()); + + // first create windows + QScopedPointer surface1(Test::createSurface()); + QScopedPointer shellSurface1(Test::createXdgShellStableSurface(surface1.data())); + auto c1 = Test::renderAndWaitForShown(surface1.data(), QSize(100, 50), Qt::blue); + QScopedPointer surface2(Test::createSurface()); + QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data())); + auto c2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 50), Qt::blue); + QScopedPointer surface3(Test::createSurface()); + QScopedPointer shellSurface3(Test::createXdgShellStableSurface(surface3.data())); + auto c3 = Test::renderAndWaitForShown(surface3.data(), QSize(100, 50), Qt::blue); + QScopedPointer surface4(Test::createSurface()); + QScopedPointer shellSurface4(Test::createXdgShellStableSurface(surface4.data())); + auto c4 = Test::renderAndWaitForShown(surface4.data(), QSize(100, 50), Qt::blue); + + QVERIFY(c4->isActive()); + QVERIFY(c4 != c3); + QVERIFY(c3 != c2); + QVERIFY(c2 != c1); + + // let's position all windows + c1->move(0, 0); + c2->move(200, 0); + c3->move(200, 200); + c4->move(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); + QVERIFY(runningChangedSpy.isValid()); + s->run(); + QTRY_COMPARE(runningChangedSpy.count(), 1); + }; + + runScript(QStringLiteral("slotSwitchWindowUp")); + QTRY_COMPARE(workspace()->activeClient(), c1); + runScript(QStringLiteral("slotSwitchWindowRight")); + QTRY_COMPARE(workspace()->activeClient(), c2); + runScript(QStringLiteral("slotSwitchWindowDown")); + QTRY_COMPARE(workspace()->activeClient(), c3); + runScript(QStringLiteral("slotSwitchWindowLeft")); + QTRY_COMPARE(workspace()->activeClient(), 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 + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QSignalSpy desktopChangedSpy(c, &AbstractClient::desktopChanged); + QVERIFY(desktopChangedSpy.isValid()); + QCOMPARE(workspace()->activeClient(), c); + + QFETCH(int, desktop); + VirtualDesktopManager::self()->setCount(desktop); + + // 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(desktopChangedSpy.wait()); + QCOMPARE(c->desktop(), desktop); + // back to desktop 1 + invokeShortcut(1); + QVERIFY(desktopChangedSpy.wait()); + QCOMPARE(c->desktop(), 1); + // invoke with one desktop too many + invokeShortcut(desktop + 1); + // that should fail + QVERIFY(!desktopChangedSpy.wait(100)); +} + +WAYLANDTEST_MAIN(KWinBindingsTest) +#include "kwinbindings_test.moc" diff --git a/autotests/integration/layershellv1client_test.cpp b/autotests/integration/layershellv1client_test.cpp new file mode 100644 index 0000000..edc8a01 --- /dev/null +++ b/autotests/integration/layershellv1client_test.cpp @@ -0,0 +1,584 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwin_wayland_test.h" +#include "abstract_client.h" +#include "abstract_output.h" +#include "main.h" +#include "platform.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include + +Q_DECLARE_METATYPE(QMargins) +Q_DECLARE_METATYPE(KWin::Layer) + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_layershellv1client-0"); + +class LayerShellV1ClientTest : 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 testPlacementArea_data(); + void testPlacementArea(); + void testFill_data(); + void testFill(); + void testStack(); + void testFocus(); + void testActivate_data(); + void testActivate(); + void testUnmap(); +}; + +void LayerShellV1ClientTest::initTestCase() +{ + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + waylandServer()->initWorkspace(); +} + +void LayerShellV1ClientTest::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::LayerShellV1)); + + screens()->setCurrent(0); + Cursors::self()->mouse()->setPos(QPoint(640, 512)); +} + +void LayerShellV1ClientTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void LayerShellV1ClientTest::testOutput_data() +{ + QTest::addColumn("screenId"); + + QTest::addRow("first output") << 0; + QTest::addRow("second output") << 1; +} + +void LayerShellV1ClientTest::testOutput() +{ + // Fetch the wl_output object. + QFETCH(int, screenId); + KWayland::Client::Output *output = Test::waylandOutputs().value(screenId); + QVERIFY(output); + + // Create a layer shell surface. + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createLayerSurfaceV1(surface.data(), 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.data(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + QVERIFY(configureRequestedSpy.wait()); + const QSize requestedSize = configureRequestedSpy.last().at(1).toSize(); + + // Map the layer surface. + shellSurface->ack_configure(configureRequestedSpy.last().at(0).toUInt()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), requestedSize, Qt::red); + QVERIFY(client); + + // Verify that the client is on the requested screen. + QVERIFY(output->geometry().contains(client->frameGeometry())); + + // Destroy the client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void LayerShellV1ClientTest::testAnchor_data() +{ + QTest::addColumn("anchor"); + QTest::addColumn("expectedGeometry"); + + QTest::addRow("left") << int(Test::LayerSurfaceV1::anchor_left) + << QRect(0, 450, 280, 124); + + QTest::addRow("top left") << (Test::LayerSurfaceV1::anchor_top | + Test::LayerSurfaceV1::anchor_left) + << QRect(0, 0, 280, 124); + + QTest::addRow("top") << int(Test::LayerSurfaceV1::anchor_top) + << QRect(500, 0, 280, 124); + + QTest::addRow("top right") << (Test::LayerSurfaceV1::anchor_top | + Test::LayerSurfaceV1::anchor_right) + << QRect(1000, 0, 280, 124); + + QTest::addRow("right") << int(Test::LayerSurfaceV1::anchor_right) + << QRect(1000, 450, 280, 124); + + QTest::addRow("bottom right") << (Test::LayerSurfaceV1::anchor_bottom | + Test::LayerSurfaceV1::anchor_right) + << QRect(1000, 900, 280, 124); + + QTest::addRow("bottom") << int(Test::LayerSurfaceV1::anchor_bottom) + << QRect(500, 900, 280, 124); + + QTest::addRow("bottom left") << (Test::LayerSurfaceV1::anchor_bottom | + Test::LayerSurfaceV1::anchor_left) + << QRect(0, 900, 280, 124); +} + +void LayerShellV1ClientTest::testAnchor() +{ + // Create a layer shell surface. + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createLayerSurfaceV1(surface.data(), 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.data(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + 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()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(280, 124), Qt::red); + QVERIFY(client); + + // Verify that the client is placed at expected location. + QTEST(client->frameGeometry(), "expectedGeometry"); + + // Destroy the client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void LayerShellV1ClientTest::testMargins_data() +{ + QTest::addColumn("anchor"); + QTest::addColumn("margins"); + QTest::addColumn("expectedGeometry"); + + QTest::addRow("left") << int(Test::LayerSurfaceV1::anchor_left) + << QMargins(100, 0, 0, 0) + << QRect(100, 450, 280, 124); + + QTest::addRow("top left") << (Test::LayerSurfaceV1::anchor_top | + Test::LayerSurfaceV1::anchor_left) + << QMargins(100, 200, 0, 0) + << QRect(100, 200, 280, 124); + + QTest::addRow("top") << int(Test::LayerSurfaceV1::anchor_top) + << QMargins(0, 200, 0, 0) + << QRect(500, 200, 280, 124); + + QTest::addRow("top right") << (Test::LayerSurfaceV1::anchor_top | + Test::LayerSurfaceV1::anchor_right) + << QMargins(0, 200, 300, 0) + << QRect(700, 200, 280, 124); + + QTest::addRow("right") << int(Test::LayerSurfaceV1::anchor_right) + << QMargins(0, 0, 300, 0) + << QRect(700, 450, 280, 124); + + QTest::addRow("bottom right") << (Test::LayerSurfaceV1::anchor_bottom | + Test::LayerSurfaceV1::anchor_right) + << QMargins(0, 0, 300, 400) + << QRect(700, 500, 280, 124); + + QTest::addRow("bottom") << int(Test::LayerSurfaceV1::anchor_bottom) + << QMargins(0, 0, 0, 400) + << QRect(500, 500, 280, 124); + + QTest::addRow("bottom left") << (Test::LayerSurfaceV1::anchor_bottom | + Test::LayerSurfaceV1::anchor_left) + << QMargins(100, 0, 0, 400) + << QRect(100, 500, 280, 124); +} + +void LayerShellV1ClientTest::testMargins() +{ + // Create a layer shell surface. + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createLayerSurfaceV1(surface.data(), 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.data(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + QVERIFY(configureRequestedSpy.wait()); + const QSize requestedSize = configureRequestedSpy.last().at(1).toSize(); + + // Map the layer surface. + shellSurface->ack_configure(configureRequestedSpy.last().at(0).toUInt()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), requestedSize, Qt::red); + QVERIFY(client); + + // Verify that the client is placed at expected location. + QTEST(client->frameGeometry(), "expectedGeometry"); + + // Destroy the client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void LayerShellV1ClientTest::testLayer_data() +{ + QTest::addColumn("protocolLayer"); + QTest::addColumn("compositorLayer"); + + QTest::addRow("overlay") << int(Test::LayerShellV1::layer_overlay) << UnmanagedLayer; + 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 LayerShellV1ClientTest::testLayer() +{ + // Create a layer shell surface. + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createLayerSurfaceV1(surface.data(), 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.data(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + QVERIFY(configureRequestedSpy.wait()); + const QSize requestedSize = configureRequestedSpy.last().at(1).toSize(); + + // Map the layer surface. + shellSurface->ack_configure(configureRequestedSpy.last().at(0).toUInt()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), requestedSize, Qt::red); + QVERIFY(client); + + // Verify that the client is placed at expected location. + QTEST(client->layer(), "compositorLayer"); + + // Destroy the client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void LayerShellV1ClientTest::testPlacementArea_data() +{ + QTest::addColumn("anchor"); + QTest::addColumn("exclusiveZone"); + QTest::addColumn("placementArea"); + + QTest::addRow("left") << int(Test::LayerSurfaceV1::anchor_left) << 300 << QRect(300, 0, 980, 1024); + QTest::addRow("top") << int(Test::LayerSurfaceV1::anchor_top) << 300 << QRect(0, 300, 1280, 724); + QTest::addRow("right") << int(Test::LayerSurfaceV1::anchor_right) << 300 << QRect(0, 0, 980, 1024); + QTest::addRow("bottom") << int(Test::LayerSurfaceV1::anchor_bottom) << 300 << QRect(0, 0, 1280, 724); +} + +void LayerShellV1ClientTest::testPlacementArea() +{ + // Create a layer shell surface. + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createLayerSurfaceV1(surface.data(), QStringLiteral("test"))); + + // Set the initial state of the layer surface. + QFETCH(int, anchor); + QFETCH(int, exclusiveZone); + shellSurface->set_anchor(anchor); + shellSurface->set_exclusive_zone(exclusiveZone); + shellSurface->set_size(280, 124); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the compositor to position the surface. + QSignalSpy configureRequestedSpy(shellSurface.data(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + QVERIFY(configureRequestedSpy.wait()); + const QSize requestedSize = configureRequestedSpy.last().at(1).toSize(); + + // Map the layer surface. + shellSurface->ack_configure(configureRequestedSpy.last().at(0).toUInt()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), requestedSize, Qt::red); + QVERIFY(client); + + // Verify that the work area has been adjusted. + QTEST(workspace()->clientArea(PlacementArea, client), "placementArea"); + + // Destroy the client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void LayerShellV1ClientTest::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) + << QRect(0, 450, 1280, 124); + + QTest::addRow("vertical") << (Test::LayerSurfaceV1::anchor_top | + Test::LayerSurfaceV1::anchor_bottom) + << QSize(280, 0) + << QRect(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) + << QRect(0, 0, 1280, 1024); +} + +void LayerShellV1ClientTest::testFill() +{ + // Create a layer shell surface. + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createLayerSurfaceV1(surface.data(), 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.data(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + QVERIFY(configureRequestedSpy.wait()); + const QSize requestedSize = configureRequestedSpy.last().at(1).toSize(); + + // Map the layer surface. + shellSurface->ack_configure(configureRequestedSpy.last().at(0).toUInt()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), requestedSize, Qt::red); + QVERIFY(client); + + // Verify that the client is placed at expected location. + QTEST(client->frameGeometry(), "expectedGeometry"); + + // Destroy the client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void LayerShellV1ClientTest::testStack() +{ + // Create a layer shell surface. + QScopedPointer surface1(Test::createSurface()); + QScopedPointer shellSurface1(Test::createLayerSurfaceV1(surface1.data(), QStringLiteral("test"))); + + QScopedPointer surface2(Test::createSurface()); + QScopedPointer shellSurface2(Test::createLayerSurfaceV1(surface2.data(), 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.data(), &Test::LayerSurfaceV1::configureRequested); + QSignalSpy configureRequestedSpy2(shellSurface2.data(), &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()); + AbstractClient *client1 = Test::renderAndWaitForShown(surface1.data(), requestedSize1, Qt::red); + QVERIFY(client1); + + shellSurface2->ack_configure(configureRequestedSpy2.last().at(0).toUInt()); + AbstractClient *client2 = Test::renderAndWaitForShown(surface2.data(), requestedSize2, Qt::red); + QVERIFY(client2); + + // Check that the second layer surface is placed next to the first. + QCOMPARE(client1->frameGeometry(), QRect(0, 450, 80, 124)); + QCOMPARE(client2->frameGeometry(), QRect(80, 450, 200, 124)); + + // Check that the work area has been adjusted accordingly. + QCOMPARE(workspace()->clientArea(PlacementArea, client1), QRect(280, 0, 1000, 1024)); + QCOMPARE(workspace()->clientArea(PlacementArea, client2), QRect(280, 0, 1000, 1024)); + + // Destroy the client. + shellSurface1.reset(); + QVERIFY(Test::waitForWindowDestroyed(client1)); + shellSurface2.reset(); + QVERIFY(Test::waitForWindowDestroyed(client2)); +} + +void LayerShellV1ClientTest::testFocus() +{ + // Create a layer shell surface. + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createLayerSurfaceV1(surface.data(), QStringLiteral("test"))); + + // Set the initial state of the layer surface. + shellSurface->set_keyboard_interactivity(1); + shellSurface->set_size(280, 124); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the compositor to position the surface. + QSignalSpy configureRequestedSpy(shellSurface.data(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + QVERIFY(configureRequestedSpy.wait()); + const QSize requestedSize = configureRequestedSpy.last().at(1).toSize(); + + // Map the layer surface. + shellSurface->ack_configure(configureRequestedSpy.last().at(0).toUInt()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), requestedSize, Qt::red); + QVERIFY(client); + + // The layer surface must be focused when it's mapped. + QVERIFY(client->isActive()); + + // Destroy the client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void LayerShellV1ClientTest::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 LayerShellV1ClientTest::testActivate() +{ + // Create a layer shell surface. + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createLayerSurfaceV1(surface.data(), 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.data(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + QVERIFY(configureRequestedSpy.wait()); + const QSize requestedSize = configureRequestedSpy.last().at(1).toSize(); + + // Map the layer surface. + shellSurface->ack_configure(configureRequestedSpy.last().at(0).toUInt()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), requestedSize, Qt::red); + QVERIFY(client); + QVERIFY(!client->isActive()); + + // Try to activate the layer surface. + shellSurface->set_keyboard_interactivity(1); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + QSignalSpy activeChangedSpy(client, &AbstractClient::activeChanged); + QVERIFY(activeChangedSpy.isValid()); + QTEST(activeChangedSpy.wait(1000), "active"); + + // Destroy the client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void LayerShellV1ClientTest::testUnmap() +{ + // Create a layer shell surface. + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createLayerSurfaceV1(surface.data(), 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.data(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + QVERIFY(configureRequestedSpy.wait()); + + // Map the layer surface. + shellSurface->ack_configure(configureRequestedSpy.last().at(0).toUInt()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(280, 124), Qt::red); + QVERIFY(client); + + // Unmap the layer surface. + surface->attachBuffer(KWayland::Client::Buffer::Ptr()); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(Test::waitForWindowDestroyed(client)); + + // 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()); + client = Test::renderAndWaitForShown(surface.data(), QSize(280, 124), Qt::red); + QVERIFY(client); + + // Destroy the client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +} // namespace KWin + +WAYLANDTEST_MAIN(KWin::LayerShellV1ClientTest) +#include "layershellv1client_test.moc" diff --git a/autotests/integration/lockscreen.cpp b/autotests/integration/lockscreen.cpp new file mode 100644 index 0000000..89f97e4 --- /dev/null +++ b/autotests/integration/lockscreen.cpp @@ -0,0 +1,773 @@ +/* + 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 "platform.h" +#include "abstract_client.h" +#include "composite.h" +#include "cursor.h" +#include "scene.h" +#include "screenedge.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +//screenlocker +#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 testKeyboardShortcut(); + void testTouch(); + +private: + void unlock(); + AbstractClient *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 { + emit inputEvent(); + } + void grabbedKeyboardEvent(QKeyEvent *e) override { + emit keyEvent(e->text()); + } + +Q_SIGNALS: + void inputEvent(); + void keyEvent(const QString&); +}; + +#define LOCK \ + QVERIFY(!waylandServer()->isScreenLocked()); \ + QSignalSpy lockStateChangedSpy(ScreenLocker::KSldApp::self(), &ScreenLocker::KSldApp::lockStateChanged); \ + QVERIFY(lockStateChangedSpy.isValid()); \ + ScreenLocker::KSldApp::self()->lock(ScreenLocker::EstablishLock::Immediate); \ + QCOMPARE(lockStateChangedSpy.count(), 1); \ + QVERIFY(waylandServer()->isScreenLocked()); + +#define UNLOCK \ + int expectedLockCount = 1; \ + if (ScreenLocker::KSldApp::self()->lockState() == ScreenLocker::KSldApp::Locked) { \ + expectedLockCount = 2; \ + } \ + QCOMPARE(lockStateChangedSpy.count(), expectedLockCount); \ + unlock(); \ + if (lockStateChangedSpy.count() < expectedLockCount + 1) { \ + QVERIFY(lockStateChangedSpy.wait()); \ + } \ + QCOMPARE(lockStateChangedSpy.count(), expectedLockCount + 1); \ + QVERIFY(!waylandServer()->isScreenLocked()); + +#define MOTION(target) \ + kwinApp()->platform()->pointerMotion(target, timestamp++) + +#define PRESS \ + kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++) + +#define RELEASE \ + kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++) + +#define KEYPRESS( key ) \ + kwinApp()->platform()->keyboardKeyPressed(key, timestamp++) + +#define KEYRELEASE( key ) \ + kwinApp()->platform()->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; + } +} + +AbstractClient *LockScreenTest::showWindow() +{ + using namespace KWayland::Client; +#define VERIFY(statement) \ + if (!QTest::qVerify((statement), #statement, "", __FILE__, __LINE__))\ + return nullptr; +#define COMPARE(actual, expected) \ + if (!QTest::qCompare(actual, expected, #actual, #expected, __FILE__, __LINE__))\ + return nullptr; + + Surface *surface = Test::createSurface(m_compositor); + VERIFY(surface); + XdgShellSurface *shellSurface = Test::createXdgShellStableSurface(surface, surface); + VERIFY(shellSurface); + // let's render + auto c = Test::renderAndWaitForShown(surface, QSize(100, 50), Qt::blue); + + VERIFY(c); + COMPARE(workspace()->activeClient(), c); + +#undef VERIFY +#undef COMPARE + + return c; +} + +void LockScreenTest::initTestCase() +{ + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + qputenv("KWIN_COMPOSE", QByteArrayLiteral("O2")); + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + setenv("QT_QPA_PLATFORM", "wayland", true); + waylandServer()->initWorkspace(); + + auto scene = KWin::Compositor::self()->scene(); + QVERIFY(scene); + QCOMPARE(scene->compositingType(), KWin::OpenGL2Compositing); +} + +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(); + + screens()->setCurrent(0); + Cursors::self()->mouse()->setPos(QPoint(640, 512)); +} + +void LockScreenTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void LockScreenTest::testStackingOrder() +{ + // This test verifies that the lockscreen greeter is placed above other windows. + + QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(clientAddedSpy.isValid()); + + LOCK + QVERIFY(clientAddedSpy.wait()); + + AbstractClient *client = clientAddedSpy.first().first().value(); + QVERIFY(client); + QVERIFY(client->isLockScreen()); + QCOMPARE(client->layer(), UnmanagedLayer); + + UNLOCK +} + +void LockScreenTest::testPointer() +{ + using namespace KWayland::Client; + + QScopedPointer pointer(m_seat->createPointer()); + QVERIFY(!pointer.isNull()); + QSignalSpy enteredSpy(pointer.data(), &Pointer::entered); + QVERIFY(enteredSpy.isValid()); + QSignalSpy leftSpy(pointer.data(), &Pointer::left); + QVERIFY(leftSpy.isValid()); + + AbstractClient *c = showWindow(); + QVERIFY(c); + + // first move cursor into the center of the window + quint32 timestamp = 1; + MOTION(c->frameGeometry().center()); + QVERIFY(enteredSpy.wait()); + + LOCK + + QVERIFY(leftSpy.wait()); + QCOMPARE(leftSpy.count(), 1); + + // simulate moving out in and out again + MOTION(c->frameGeometry().center()); + MOTION(c->frameGeometry().bottomRight() + QPoint(100, 100)); + MOTION(c->frameGeometry().bottomRight() + QPoint(100, 100)); + QVERIFY(!leftSpy.wait()); + QCOMPARE(leftSpy.count(), 1); + QCOMPARE(enteredSpy.count(), 1); + + // go back on the window + MOTION(c->frameGeometry().center()); + // and unlock + UNLOCK + + QVERIFY(enteredSpy.wait()); + QCOMPARE(enteredSpy.count(), 2); + // move on the window + MOTION(c->frameGeometry().center() + QPoint(100, 100)); + QVERIFY(leftSpy.wait()); + MOTION(c->frameGeometry().center()); + QVERIFY(enteredSpy.wait()); + QCOMPARE(enteredSpy.count(), 3); +} + +void LockScreenTest::testPointerButton() +{ + using namespace KWayland::Client; + + QScopedPointer pointer(m_seat->createPointer()); + QVERIFY(!pointer.isNull()); + QSignalSpy enteredSpy(pointer.data(), &Pointer::entered); + QVERIFY(enteredSpy.isValid()); + QSignalSpy buttonChangedSpy(pointer.data(), &Pointer::buttonStateChanged); + QVERIFY(buttonChangedSpy.isValid()); + + AbstractClient *c = showWindow(); + QVERIFY(c); + + // first move cursor into the center of the window + quint32 timestamp = 1; + MOTION(c->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()); + RELEASE; + QVERIFY(!buttonChangedSpy.wait()); + + UNLOCK + QVERIFY(enteredSpy.wait()); + QCOMPARE(enteredSpy.count(), 2); + + // and click again + PRESS; + QVERIFY(buttonChangedSpy.wait()); + RELEASE; + QVERIFY(buttonChangedSpy.wait()); +} + +void LockScreenTest::testPointerAxis() +{ + using namespace KWayland::Client; + + QScopedPointer pointer(m_seat->createPointer()); + QVERIFY(!pointer.isNull()); + QSignalSpy axisChangedSpy(pointer.data(), &Pointer::axisChanged); + QVERIFY(axisChangedSpy.isValid()); + QSignalSpy enteredSpy(pointer.data(), &Pointer::entered); + QVERIFY(enteredSpy.isValid()); + + AbstractClient *c = showWindow(); + QVERIFY(c); + + // first move cursor into the center of the window + quint32 timestamp = 1; + MOTION(c->frameGeometry().center()); + QVERIFY(enteredSpy.wait()); + // and simulate axis + kwinApp()->platform()->pointerAxisHorizontal(5.0, timestamp++); + QVERIFY(axisChangedSpy.wait()); + + LOCK + + // and simulate axis + kwinApp()->platform()->pointerAxisHorizontal(5.0, timestamp++); + QVERIFY(!axisChangedSpy.wait(100)); + kwinApp()->platform()->pointerAxisVertical(5.0, timestamp++); + QVERIFY(!axisChangedSpy.wait(100)); + + // and unlock + UNLOCK + QVERIFY(enteredSpy.wait()); + QCOMPARE(enteredSpy.count(), 2); + + // and move axis again + kwinApp()->platform()->pointerAxisHorizontal(5.0, timestamp++); + QVERIFY(axisChangedSpy.wait()); + kwinApp()->platform()->pointerAxisVertical(5.0, timestamp++); + QVERIFY(axisChangedSpy.wait()); +} + +void LockScreenTest::testKeyboard() +{ + using namespace KWayland::Client; + + QScopedPointer keyboard(m_seat->createKeyboard()); + QVERIFY(!keyboard.isNull()); + QSignalSpy enteredSpy(keyboard.data(), &Keyboard::entered); + QVERIFY(enteredSpy.isValid()); + QSignalSpy leftSpy(keyboard.data(), &Keyboard::left); + QVERIFY(leftSpy.isValid()); + QSignalSpy keyChangedSpy(keyboard.data(), &Keyboard::keyChanged); + QVERIFY(keyChangedSpy.isValid()); + + AbstractClient *c = showWindow(); + QVERIFY(c); + 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(), 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(), 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(), Keyboard::KeyState::Pressed); + QCOMPARE(keyChangedSpy.at(3).at(1).value(), Keyboard::KeyState::Released); +} + +void LockScreenTest::testScreenEdge() +{ + QSignalSpy screenEdgeSpy(ScreenEdges::self(), &ScreenEdges::approaching); + QVERIFY(screenEdgeSpy.isValid()); + QCOMPARE(screenEdgeSpy.count(), 0); + + 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() +{ + QScopedPointer effect(new HelperEffect); + QSignalSpy inputSpy(effect.data(), &HelperEffect::inputEvent); + QVERIFY(inputSpy.isValid()); + effects->startMouseInterception(effect.data(), 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.data()); +} + +void LockScreenTest::testEffectsKeyboard() +{ + QScopedPointer effect(new HelperEffect); + QSignalSpy inputSpy(effect.data(), &HelperEffect::keyEvent); + QVERIFY(inputSpy.isValid()); + effects->grabKeyboard(effect.data()); + + 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 + QScopedPointer effect(new HelperEffect); + QSignalSpy inputSpy(effect.data(), &HelperEffect::keyEvent); + QVERIFY(inputSpy.isValid()); + effects->grabKeyboard(effect.data()); + + // we need to configure the key repeat first. It is only enabled on libinput + waylandServer()->seat()->setKeyRepeatInfo(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(200)); + KEYRELEASE(KEY_B); + QVERIFY(!inputSpy.wait(200)); + + UNLOCK + // don't test again, that's covered by testEffectsKeyboard + + effects->ungrabKeyboard(); +} + +void LockScreenTest::testMoveWindow() +{ + using namespace KWayland::Client; + AbstractClient *c = showWindow(); + QVERIFY(c); + QSignalSpy clientStepUserMovedResizedSpy(c, &AbstractClient::clientStepUserMovedResized); + QVERIFY(clientStepUserMovedResizedSpy.isValid()); + quint32 timestamp = 1; + + workspace()->slotWindowMove(); + QCOMPARE(workspace()->moveResizeClient(), c); + QVERIFY(c->isMove()); + kwinApp()->platform()->keyboardKeyPressed(KEY_RIGHT, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_RIGHT, timestamp++); + QEXPECT_FAIL("", "First event is ignored", Continue); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); + + // TODO adjust once the expected fail is fixed + kwinApp()->platform()->keyboardKeyPressed(KEY_RIGHT, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_RIGHT, timestamp++); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); + + // while locking our window should continue to be in move resize + LOCK + QCOMPARE(workspace()->moveResizeClient(), c); + QVERIFY(c->isMove()); + kwinApp()->platform()->keyboardKeyPressed(KEY_RIGHT, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_RIGHT, timestamp++); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); + + UNLOCK + QCOMPARE(workspace()->moveResizeClient(), c); + QVERIFY(c->isMove()); + kwinApp()->platform()->keyboardKeyPressed(KEY_RIGHT, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_RIGHT, timestamp++); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 2); + kwinApp()->platform()->keyboardKeyPressed(KEY_ESC, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_ESC, timestamp++); + QVERIFY(!c->isMove()); +} + +void LockScreenTest::testPointerShortcut() +{ + using namespace KWayland::Client; + QScopedPointer action(new QAction(nullptr)); + QSignalSpy actionSpy(action.data(), &QAction::triggered); + QVERIFY(actionSpy.isValid()); + input()->registerPointerShortcut(Qt::MetaModifier, Qt::LeftButton, action.data()); + + // try to trigger the shortcut + quint32 timestamp = 1; +#define PERFORM(expectedCount) \ + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTMETA, timestamp++); \ + PRESS; \ + QCoreApplication::instance()->processEvents(); \ + QCOMPARE(actionSpy.count(), expectedCount); \ + RELEASE; \ + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTMETA, timestamp++); \ + QCoreApplication::instance()->processEvents(); \ + QCOMPARE(actionSpy.count(), expectedCount); + + 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() +{ + 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() +{ + using namespace KWayland::Client; + QScopedPointer action(new QAction(nullptr)); + QSignalSpy actionSpy(action.data(), &QAction::triggered); + QVERIFY(actionSpy.isValid()); + 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.data()); + + // try to trigger the shortcut + quint32 timestamp = 1; +#define PERFORM(expectedCount) \ + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTMETA, timestamp++); \ + if (direction == Qt::Vertical) \ + kwinApp()->platform()->pointerAxisVertical(sign * 5.0, timestamp++); \ + else \ + kwinApp()->platform()->pointerAxisHorizontal(sign * 5.0, timestamp++); \ + QCoreApplication::instance()->processEvents(); \ + QCOMPARE(actionSpy.count(), expectedCount); \ + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTMETA, timestamp++); \ + QCoreApplication::instance()->processEvents(); \ + QCOMPARE(actionSpy.count(), expectedCount); + + PERFORM(1) + + // now the same thing with a locked screen + LOCK + PERFORM(1) + + // and as unlocked + UNLOCK + PERFORM(2) +#undef PERFORM +} + +void LockScreenTest::testKeyboardShortcut() +{ + using namespace KWayland::Client; + QScopedPointer action(new QAction(nullptr)); + QSignalSpy actionSpy(action.data(), &QAction::triggered); + QVERIFY(actionSpy.isValid()); + action->setProperty("componentName", QStringLiteral(KWIN_NAME)); + action->setObjectName("LockScreenTest::testKeyboardShortcut"); + KGlobalAccel::self()->setDefaultShortcut(action.data(), QList{Qt::CTRL + Qt::META + Qt::ALT + Qt::Key_Space}); + KGlobalAccel::self()->setShortcut(action.data(), 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()); + QCOMPARE(actionSpy.count(), 1); + + LOCK + KEYPRESS(KEY_SPACE); + QVERIFY(!actionSpy.wait()); + QCOMPARE(actionSpy.count(), 1); + KEYRELEASE(KEY_SPACE); + QVERIFY(!actionSpy.wait()); + QCOMPARE(actionSpy.count(), 1); + + UNLOCK + KEYPRESS(KEY_SPACE); + QVERIFY(actionSpy.wait()); + QCOMPARE(actionSpy.count(), 2); + KEYRELEASE(KEY_SPACE); + QVERIFY(!actionSpy.wait()); + QCOMPARE(actionSpy.count(), 2); + KEYRELEASE(KEY_LEFTCTRL); + KEYRELEASE(KEY_LEFTMETA); + KEYRELEASE(KEY_LEFTALT); +} + +void LockScreenTest::testTouch() +{ + using namespace KWayland::Client; + auto touch = m_seat->createTouch(m_seat); + QVERIFY(touch); + QVERIFY(touch->isValid()); + AbstractClient *c = showWindow(); + QVERIFY(c); + QSignalSpy sequenceStartedSpy(touch, &Touch::sequenceStarted); + QVERIFY(sequenceStartedSpy.isValid()); + QSignalSpy cancelSpy(touch, &Touch::sequenceCanceled); + QVERIFY(cancelSpy.isValid()); + QSignalSpy pointRemovedSpy(touch, &Touch::pointRemoved); + QVERIFY(pointRemovedSpy.isValid()); + + quint32 timestamp = 1; + kwinApp()->platform()->touchDown(1, QPointF(25, 25), timestamp++); + QVERIFY(sequenceStartedSpy.wait()); + QCOMPARE(sequenceStartedSpy.count(), 1); + + LOCK + QVERIFY(cancelSpy.wait()); + + kwinApp()->platform()->touchUp(1, timestamp++); + QVERIFY(!pointRemovedSpy.wait(100)); + kwinApp()->platform()->touchDown(1, QPointF(25, 25), timestamp++); + kwinApp()->platform()->touchMotion(1, QPointF(26, 26), timestamp++); + kwinApp()->platform()->touchUp(1, timestamp++); + + UNLOCK + kwinApp()->platform()->touchDown(1, QPointF(25, 25), timestamp++); + QVERIFY(sequenceStartedSpy.wait()); + QCOMPARE(sequenceStartedSpy.count(), 2); + kwinApp()->platform()->touchUp(1, timestamp++); + QVERIFY(pointRemovedSpy.wait()); + QCOMPARE(pointRemovedSpy.count(), 1); +} + +} + +WAYLANDTEST_MAIN(KWin::LockScreenTest) +#include "lockscreen.moc" diff --git a/autotests/integration/maximize_test.cpp b/autotests/integration/maximize_test.cpp new file mode 100644 index 0000000..ad94c6a --- /dev/null +++ b/autotests/integration/maximize_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 "abstract_client.h" +#include "cursor.h" +#include "decorations/decorationbridge.h" +#include "decorations/settings.h" +#include "platform.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace KWin; +using namespace KWayland::Client; + +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 testInitiallyMaximized(); + void testInitiallyMaximizedBorderless(); + void testBorderlessMaximizedWindow(); + void testBorderlessMaximizedWindowNoClientSideDecoration(); + void testMaximizedGainFocusAndBeActivated(); +}; + +void TestMaximized::initTestCase() +{ + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + waylandServer()->initWorkspace(); +} + +void TestMaximized::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Decoration | + Test::AdditionalWaylandInterface::XdgDecoration | + Test::AdditionalWaylandInterface::PlasmaShell)); + + screens()->setCurrent(0); + KWin::Cursors::self()->mouse()->setPos(QPoint(1280, 512)); +} + +void TestMaximized::cleanup() +{ + Test::destroyWaylandConnection(); + + // adjust config + auto group = kwinApp()->config()->group("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 client. + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QScopedPointer ssd(Test::waylandServerSideDecoration()->create(surface.data())); + + auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + QVERIFY(client->isDecorated()); + + auto decoration = client->decoration(); + QVERIFY(decoration); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); + + // Wait for configure event that signals the client is active now. + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 1); + + // When there are no borders, there is no change to them when maximizing. + // TODO: we should test both cases with fixed fake decoration for autotests. + const bool hasBorders = Decoration::DecorationBridge::self()->settings()->borderSize() != KDecoration2::BorderSize::None; + + // now maximize + QSignalSpy bordersChangedSpy(decoration, &KDecoration2::Decoration::bordersChanged); + QVERIFY(bordersChangedSpy.isValid()); + QSignalSpy maximizedChangedSpy(decoration->client().toStrongRef().data(), &KDecoration2::DecoratedClient::maximizedChanged); + QVERIFY(maximizedChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); + + workspace()->slotWindowMaximize(); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 2); + QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(1280, 1024 - decoration->borderTop())); + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + Test::render(surface.data(), configureRequestedSpy.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(), hasBorders ? 2 : 1); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(maximizedChangedSpy.count(), 1); + QCOMPARE(maximizedChangedSpy.last().first().toBool(), true); + QCOMPARE(bordersChangedSpy.count(), hasBorders ? 1 : 0); + QCOMPARE(decoration->borderLeft(), 0); + QCOMPARE(decoration->borderBottom(), 0); + QCOMPARE(decoration->borderRight(), 0); + QVERIFY(decoration->borderTop() != 0); + + // now unmaximize again + workspace()->slotWindowMaximize(); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 3); + QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(100, 50)); + + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + Test::render(surface.data(), QSize(100, 50), Qt::red); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(frameGeometryChangedSpy.count(), hasBorders ? 4 : 2); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(maximizedChangedSpy.count(), 2); + QCOMPARE(maximizedChangedSpy.last().first().toBool(), false); + QCOMPARE(bordersChangedSpy.count(), hasBorders ? 2 : 0); + QVERIFY(decoration->borderTop() != 0); + QVERIFY(decoration->borderLeft() != !hasBorders); + QVERIFY(decoration->borderRight() != !hasBorders); + QVERIFY(decoration->borderBottom() != !hasBorders); + + // Destroy the test client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestMaximized::testInitiallyMaximized() +{ + // This test verifies that a window created as maximized, will be maximized. + + // Create the test client. + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface( + Test::createXdgShellStableSurface(surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + shellSurface->setMaximized(true); + surface->commit(Surface::CommitFlag::None); + + // Wait for the initial configure event. + XdgShellSurface::States states; + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 1); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); + + // Now let's render in an incorrect size. + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + QCOMPARE(client->frameGeometry(), QRect(0, 0, 100, 50)); + QEXPECT_FAIL("", "Should go out of maximzied", Continue); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); + + // Destroy the client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +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("Windows"); + group.writeEntry("BorderlessMaximizedWindows", true); + group.sync(); + Workspace::self()->slotReconfigure(); + QCOMPARE(options->borderlessMaximizedWindows(), true); + + // Create the test client. + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface( + Test::createXdgShellStableSurface(surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + QScopedPointer decoration( + Test::xdgDecorationManager()->getToplevelDecoration(shellSurface.data())); + + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + shellSurface->setMaximized(true); + QSignalSpy decorationConfiguredSpy(decoration.data(), &XdgDecoration::modeChanged); + QVERIFY(decorationConfiguredSpy.isValid()); + decoration->setMode(XdgDecoration::Mode::ServerSide); + surface->commit(Surface::CommitFlag::None); + + // Wait for the initial configure event. + XdgShellSurface::States states; + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 1); + QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(1280, 1024)); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); + + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(1280, 1024), Qt::blue); + QVERIFY(client); + QVERIFY(!client->isDecorated()); + QVERIFY(client->isActive()); + QVERIFY(client->isMaximizable()); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->frameGeometry(), QRect(0, 0, 1280, 1024)); + QCOMPARE(decoration->mode(), XdgDecoration::Mode::ServerSide); + + // Destroy the client. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} +void TestMaximized::testBorderlessMaximizedWindow() +{ + // This test verifies that a maximized client looses it's server-side + // decoration when the borderless maximized option is on. + + // Enable the borderless maximized windows option. + auto group = kwinApp()->config()->group("Windows"); + group.writeEntry("BorderlessMaximizedWindows", true); + group.sync(); + Workspace::self()->slotReconfigure(); + QCOMPARE(options->borderlessMaximizedWindows(), true); + + // Create the test client. + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface( + Test::createXdgShellStableSurface(surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + QScopedPointer decoration( + Test::xdgDecorationManager()->getToplevelDecoration(shellSurface.data())); + QSignalSpy decorationConfiguredSpy(decoration.data(), &XdgDecoration::modeChanged); + QVERIFY(decorationConfiguredSpy.isValid()); + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + decoration->setMode(XdgDecoration::Mode::ServerSide); + surface->commit(Surface::CommitFlag::None); + + // Wait for the initial configure event. + XdgShellSurface::States states; + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 1); + QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(0, 0)); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); + + // Map the client. + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(client->isDecorated(), true); + + // We should receive a configure event when the client becomes active. + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 2); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); + + // Maximize the client. + const QRect maximizeRestoreGeometry = client->frameGeometry(); + workspace()->slotWindowMaximize(); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 3); + QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(1280, 1024)); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); + + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + Test::render(surface.data(), QSize(1280, 1024), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(client->frameGeometry(), QRect(0, 0, 1280, 1024)); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->isDecorated(), false); + + // Restore the client. + workspace()->slotWindowMaximize(); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 4); + QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(100, 50)); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); + + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + Test::render(surface.data(), QSize(100, 50), Qt::red); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(client->frameGeometry(), maximizeRestoreGeometry); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(client->isDecorated(), true); + + // Destroy the client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestMaximized::testBorderlessMaximizedWindowNoClientSideDecoration() +{ + // test case verifies that borderless maximized windows doesn't cause + // clients to render client-side decorations instead (BUG 405385) + + XdgShellSurface::States states; + + // adjust config + auto group = kwinApp()->config()->group("Windows"); + group.writeEntry("BorderlessMaximizedWindows", true); + group.sync(); + Workspace::self()->slotReconfigure(); + QCOMPARE(options->borderlessMaximizedWindows(), true); + + QScopedPointer surface(Test::createSurface()); + QScopedPointer xdgShellSurface(Test::createXdgShellStableSurface(surface.data())); + QScopedPointer deco(Test::xdgDecorationManager()->getToplevelDecoration(xdgShellSurface.data())); + + QSignalSpy decorationConfiguredSpy(deco.data(), &XdgDecoration::modeChanged); + QVERIFY(decorationConfiguredSpy.isValid()); + + auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client->isDecorated()); + QVERIFY(!client->noBorder()); + + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); + QSignalSpy configureRequestedSpy(xdgShellSurface.data(), &XdgShellSurface::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + + // Wait for the compositor to send a configure event with the Activated state. + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 1); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states & XdgShellSurface::State::Activated); + + QCOMPARE(decorationConfiguredSpy.count(), 1); + QCOMPARE(deco->mode(), XdgDecoration::Mode::ServerSide); + + // go to maximized + xdgShellSurface->setMaximized(true); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 2); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states & XdgShellSurface::State::Maximized); + + xdgShellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::red); + QVERIFY(frameGeometryChangedSpy.wait()); + + // no deco + QVERIFY(!client->isDecorated()); + QVERIFY(client->noBorder()); + // but still server-side + QCOMPARE(deco->mode(), XdgDecoration::Mode::ServerSide); + + // go back to normal + xdgShellSurface->setMaximized(false); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 3); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(!(states & XdgShellSurface::State::Maximized)); + + xdgShellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::red); + QVERIFY(frameGeometryChangedSpy.wait()); + + QVERIFY(client->isDecorated()); + QVERIFY(!client->noBorder()); + QCOMPARE(deco->mode(), XdgDecoration::Mode::ServerSide); +} + +void TestMaximized::testMaximizedGainFocusAndBeActivated() +{ + // This test verifies that a window will be raised and gain focus when it's maximized + QScopedPointer surface(Test::createSurface()); + QScopedPointer xdgShellSurface(Test::createXdgShellStableSurface(surface.data())); + auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QScopedPointer surface2(Test::createSurface()); + QScopedPointer xdgShellSurface2(Test::createXdgShellStableSurface(surface2.data())); + auto client2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 50), Qt::blue); + + QVERIFY(!client->isActive()); + QVERIFY(client2->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{client, client2})); + + workspace()->performWindowOperation(client, Options::MaximizeOp); + + QVERIFY(client->isActive()); + QVERIFY(!client2->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{client2, client})); + + xdgShellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); + xdgShellSurface2.reset(); + QVERIFY(Test::waitForWindowDestroyed(client2)); +} + +WAYLANDTEST_MAIN(TestMaximized) +#include "maximize_test.moc" diff --git a/autotests/integration/modifier_only_shortcut_test.cpp b/autotests/integration/modifier_only_shortcut_test.cpp new file mode 100644 index 0000000..0a83b45 --- /dev/null +++ b/autotests/integration/modifier_only_shortcut_test.cpp @@ -0,0 +1,375 @@ +/* + 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 "input.h" +#include "keyboard_input.h" +#include "platform.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" + +#include + +#include + +#include + +using namespace KWin; +using namespace KWayland::Client; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_modifier_only_shortcut-0"); +static const QString s_serviceName = QStringLiteral("org.kde.KWin.Test.ModifierOnlyShortcut"); +static const QString s_path = QStringLiteral("/Test"); + +class ModifierOnlyShortcutTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testTrigger_data(); + void testTrigger(); + void testCapsLock(); + void testGlobalShortcutsDisabled_data(); + void testGlobalShortcutsDisabled(); +}; + +class Target : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.KWin.Test.ModifierOnlyShortcut") + +public: + Target(); + ~Target() override; + +public Q_SLOTS: + Q_SCRIPTABLE void shortcut(); + +Q_SIGNALS: + void shortcutTriggered(); +}; + +Target::Target() + : QObject() +{ + QDBusConnection::sessionBus().registerService(s_serviceName); + QDBusConnection::sessionBus().registerObject(s_path, s_serviceName, this, QDBusConnection::ExportScriptableSlots); +} + +Target::~Target() +{ + QDBusConnection::sessionBus().unregisterObject(s_path); + QDBusConnection::sessionBus().unregisterService(s_serviceName); +} + +void Target::shortcut() +{ + emit shortcutTriggered(); +} + +void ModifierOnlyShortcutTest::initTestCase() +{ + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); + qputenv("KWIN_XKB_DEFAULT_KEYMAP", "1"); + qputenv("XKB_DEFAULT_RULES", "evdev"); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + waylandServer()->initWorkspace(); +} + +void ModifierOnlyShortcutTest::init() +{ + screens()->setCurrent(0); + KWin::Cursors::self()->mouse()->setPos(QPoint(640, 512)); +} + +void ModifierOnlyShortcutTest::cleanup() +{ +} + +void ModifierOnlyShortcutTest::testTrigger_data() +{ + QTest::addColumn("metaConfig"); + QTest::addColumn("altConfig"); + QTest::addColumn("controlConfig"); + QTest::addColumn("shiftConfig"); + QTest::addColumn("modifier"); + QTest::addColumn>("nonTriggeringMods"); + + const QStringList trigger = QStringList{s_serviceName, s_path, s_serviceName, QStringLiteral("shortcut")}; + const QStringList e = QStringList(); + + QTest::newRow("leftMeta") << trigger << e << e << e << KEY_LEFTMETA << QList{KEY_LEFTALT, KEY_RIGHTALT, KEY_LEFTCTRL, KEY_RIGHTCTRL, KEY_LEFTSHIFT, KEY_RIGHTSHIFT}; + QTest::newRow("rightMeta") << trigger << e << e << e << KEY_RIGHTMETA << QList{KEY_LEFTALT, KEY_RIGHTALT, KEY_LEFTCTRL, KEY_RIGHTCTRL, KEY_LEFTSHIFT, KEY_RIGHTSHIFT}; + QTest::newRow("leftAlt") << e << trigger << e << e << KEY_LEFTALT << QList{KEY_LEFTMETA, KEY_RIGHTMETA, KEY_LEFTCTRL, KEY_RIGHTCTRL, KEY_LEFTSHIFT, KEY_RIGHTSHIFT}; + QTest::newRow("rightAlt") << e << trigger << e << e << KEY_RIGHTALT << QList{KEY_LEFTMETA, KEY_RIGHTMETA, KEY_LEFTCTRL, KEY_RIGHTCTRL, KEY_LEFTSHIFT, KEY_RIGHTSHIFT}; + QTest::newRow("leftControl") << e << e << trigger << e << KEY_LEFTCTRL << QList{KEY_LEFTALT, KEY_RIGHTALT, KEY_LEFTMETA, KEY_RIGHTMETA, KEY_LEFTSHIFT, KEY_RIGHTSHIFT}; + QTest::newRow("rightControl") << e << e << trigger << e << KEY_RIGHTCTRL << QList{KEY_LEFTALT, KEY_RIGHTALT, KEY_LEFTMETA, KEY_RIGHTMETA, KEY_LEFTSHIFT, KEY_RIGHTSHIFT}; + QTest::newRow("leftShift") << e << e << e << trigger << KEY_LEFTSHIFT << QList{KEY_LEFTALT, KEY_RIGHTALT, KEY_LEFTCTRL, KEY_RIGHTCTRL, KEY_LEFTMETA, KEY_RIGHTMETA}; + QTest::newRow("rightShift") << e << e << e << trigger <{KEY_LEFTALT, KEY_RIGHTALT, KEY_LEFTCTRL, KEY_RIGHTCTRL, KEY_LEFTMETA, KEY_RIGHTMETA}; +} + +void ModifierOnlyShortcutTest::testTrigger() +{ + // this test verifies that modifier only shortcut triggers correctly + Target target; + QSignalSpy triggeredSpy(&target, &Target::shortcutTriggered); + QVERIFY(triggeredSpy.isValid()); + + KConfigGroup group = kwinApp()->config()->group("ModifierOnlyShortcuts"); + QFETCH(QStringList, metaConfig); + QFETCH(QStringList, altConfig); + QFETCH(QStringList, shiftConfig); + QFETCH(QStringList, controlConfig); + group.writeEntry("Meta", metaConfig); + group.writeEntry("Alt", altConfig); + group.writeEntry("Shift", shiftConfig); + group.writeEntry("Control", controlConfig); + group.sync(); + workspace()->slotReconfigure(); + + // configured shortcut should trigger + quint32 timestamp = 1; + QFETCH(int, modifier); + kwinApp()->platform()->keyboardKeyPressed(modifier, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(modifier, timestamp++); + QCOMPARE(triggeredSpy.count(), 1); + + // the other shortcuts should not trigger + QFETCH(QList, nonTriggeringMods); + for (auto it = nonTriggeringMods.constBegin(), end = nonTriggeringMods.constEnd(); it != end; it++) { + kwinApp()->platform()->keyboardKeyPressed(*it, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(*it, timestamp++); + QCOMPARE(triggeredSpy.count(), 1); + } + + // try configured again + kwinApp()->platform()->keyboardKeyPressed(modifier, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(modifier, timestamp++); + QCOMPARE(triggeredSpy.count(), 2); + + // click another key while modifier is held + kwinApp()->platform()->keyboardKeyPressed(modifier, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_A, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_A, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(modifier, timestamp++); + QCOMPARE(triggeredSpy.count(), 2); + + // release other key after modifier release + kwinApp()->platform()->keyboardKeyPressed(modifier, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_A, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(modifier, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_A, timestamp++); + QCOMPARE(triggeredSpy.count(), 2); + + // press key before pressing modifier + kwinApp()->platform()->keyboardKeyPressed(KEY_A, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(modifier, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(modifier, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_A, timestamp++); + QCOMPARE(triggeredSpy.count(), 2); + + // mouse button pressed before clicking modifier + kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); + QCOMPARE(input()->qtButtonStates(), Qt::LeftButton); + kwinApp()->platform()->keyboardKeyPressed(modifier, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(modifier, timestamp++); + kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); + QCOMPARE(input()->qtButtonStates(), Qt::NoButton); + QCOMPARE(triggeredSpy.count(), 2); + + // mouse button press before mod press, release before mod release + kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); + QCOMPARE(input()->qtButtonStates(), Qt::LeftButton); + kwinApp()->platform()->keyboardKeyPressed(modifier, timestamp++); + kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(modifier, timestamp++); + QCOMPARE(input()->qtButtonStates(), Qt::NoButton); + QCOMPARE(triggeredSpy.count(), 2); + + // mouse button click while mod is pressed + kwinApp()->platform()->keyboardKeyPressed(modifier, timestamp++); + kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); + QCOMPARE(input()->qtButtonStates(), Qt::LeftButton); + kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(modifier, timestamp++); + QCOMPARE(input()->qtButtonStates(), Qt::NoButton); + QCOMPARE(triggeredSpy.count(), 2); + + // scroll while mod is pressed + kwinApp()->platform()->keyboardKeyPressed(modifier, timestamp++); + kwinApp()->platform()->pointerAxisVertical(5.0, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(modifier, timestamp++); + QCOMPARE(triggeredSpy.count(), 2); + + // same for horizontal + kwinApp()->platform()->keyboardKeyPressed(modifier, timestamp++); + kwinApp()->platform()->pointerAxisHorizontal(5.0, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(modifier, timestamp++); + QCOMPARE(triggeredSpy.count(), 2); + + // now try to lock the screen while modifier key is pressed + kwinApp()->platform()->keyboardKeyPressed(modifier, timestamp++); + QVERIFY(Test::lockScreen()); + kwinApp()->platform()->keyboardKeyReleased(modifier, timestamp++); + QCOMPARE(triggeredSpy.count(), 2); + + // now trigger while screen is locked, should also not work + kwinApp()->platform()->keyboardKeyPressed(modifier, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(modifier, timestamp++); + QCOMPARE(triggeredSpy.count(), 2); + + QVERIFY(Test::unlockScreen()); +} + +void ModifierOnlyShortcutTest::testCapsLock() +{ + // this test verifies that Capslock does not trigger the shift shortcut + // but other shortcuts still trigger even when Capslock is on + Target target; + QSignalSpy triggeredSpy(&target, &Target::shortcutTriggered); + QVERIFY(triggeredSpy.isValid()); + + KConfigGroup group = kwinApp()->config()->group("ModifierOnlyShortcuts"); + group.writeEntry("Meta", QStringList()); + group.writeEntry("Alt", QStringList()); + group.writeEntry("Shift", QStringList{s_serviceName, s_path, s_serviceName, QStringLiteral("shortcut")}); + group.writeEntry("Control", QStringList()); + group.sync(); + workspace()->slotReconfigure(); + + // first test that the normal shortcut triggers + quint32 timestamp = 1; + const int modifier = KEY_LEFTSHIFT; + kwinApp()->platform()->keyboardKeyPressed(modifier, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(modifier, timestamp++); + QCOMPARE(triggeredSpy.count(), 1); + + // now capslock + kwinApp()->platform()->keyboardKeyPressed(KEY_CAPSLOCK, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_CAPSLOCK, timestamp++); + QCOMPARE(input()->keyboardModifiers(), Qt::ShiftModifier); + QCOMPARE(triggeredSpy.count(), 1); + + // currently caps lock is on + // shift still triggers + kwinApp()->platform()->keyboardKeyPressed(modifier, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(modifier, timestamp++); + QCOMPARE(input()->keyboardModifiers(), Qt::ShiftModifier); + QCOMPARE(triggeredSpy.count(), 2); + + // meta should also trigger + group.writeEntry("Meta", QStringList{s_serviceName, s_path, s_serviceName, QStringLiteral("shortcut")}); + group.writeEntry("Alt", QStringList()); + group.writeEntry("Shift", QStringList{}); + group.writeEntry("Control", QStringList()); + group.sync(); + workspace()->slotReconfigure(); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + QCOMPARE(input()->keyboardModifiers(), Qt::ShiftModifier | Qt::MetaModifier); + QCOMPARE(input()->keyboard()->xkb()->modifiersRelevantForGlobalShortcuts(), Qt::MetaModifier); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTMETA, timestamp++); + QCOMPARE(triggeredSpy.count(), 3); + + // set back to shift to ensure we don't trigger with capslock + group.writeEntry("Meta", QStringList()); + group.writeEntry("Alt", QStringList()); + group.writeEntry("Shift", QStringList{s_serviceName, s_path, s_serviceName, QStringLiteral("shortcut")}); + group.writeEntry("Control", QStringList()); + group.sync(); + workspace()->slotReconfigure(); + + // release caps lock + kwinApp()->platform()->keyboardKeyPressed(KEY_CAPSLOCK, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_CAPSLOCK, timestamp++); + QCOMPARE(input()->keyboardModifiers(), Qt::NoModifier); + QCOMPARE(triggeredSpy.count(), 3); +} + +void ModifierOnlyShortcutTest::testGlobalShortcutsDisabled_data() +{ + QTest::addColumn("metaConfig"); + QTest::addColumn("altConfig"); + QTest::addColumn("controlConfig"); + QTest::addColumn("shiftConfig"); + QTest::addColumn("modifier"); + + const QStringList trigger = QStringList{s_serviceName, s_path, s_serviceName, QStringLiteral("shortcut")}; + const QStringList e = QStringList(); + + QTest::newRow("leftMeta") << trigger << e << e << e << KEY_LEFTMETA; + QTest::newRow("rightMeta") << trigger << e << e << e << KEY_RIGHTMETA; + QTest::newRow("leftAlt") << e << trigger << e << e << KEY_LEFTALT; + QTest::newRow("rightAlt") << e << trigger << e << e << KEY_RIGHTALT; + QTest::newRow("leftControl") << e << e << trigger << e << KEY_LEFTCTRL; + QTest::newRow("rightControl") << e << e << trigger << e << KEY_RIGHTCTRL; + QTest::newRow("leftShift") << e << e << e << trigger << KEY_LEFTSHIFT; + QTest::newRow("rightShift") << e << e << e << trigger <config()->group("ModifierOnlyShortcuts"); + QFETCH(QStringList, metaConfig); + QFETCH(QStringList, altConfig); + QFETCH(QStringList, shiftConfig); + QFETCH(QStringList, controlConfig); + group.writeEntry("Meta", metaConfig); + group.writeEntry("Alt", altConfig); + group.writeEntry("Shift", shiftConfig); + group.writeEntry("Control", controlConfig); + group.sync(); + workspace()->slotReconfigure(); + + // trigger once to verify the shortcut works + quint32 timestamp = 1; + QFETCH(int, modifier); + QVERIFY(!workspace()->globalShortcutsDisabled()); + kwinApp()->platform()->keyboardKeyPressed(modifier, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(modifier, timestamp++); + QCOMPARE(triggeredSpy.count(), 1); + triggeredSpy.clear(); + + // now disable global shortcuts + workspace()->disableGlobalShortcutsForClient(true); + QVERIFY(workspace()->globalShortcutsDisabled()); + // Should not get triggered + kwinApp()->platform()->keyboardKeyPressed(modifier, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(modifier, timestamp++); + QCOMPARE(triggeredSpy.count(), 0); + triggeredSpy.clear(); + + // enable again + workspace()->disableGlobalShortcutsForClient(false); + QVERIFY(!workspace()->globalShortcutsDisabled()); + // should get triggered again + kwinApp()->platform()->keyboardKeyPressed(modifier, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(modifier, timestamp++); + QCOMPARE(triggeredSpy.count(), 1); +} + +WAYLANDTEST_MAIN(ModifierOnlyShortcutTest) +#include "modifier_only_shortcut_test.moc" diff --git a/autotests/integration/move_resize_window_test.cpp b/autotests/integration/move_resize_window_test.cpp new file mode 100644 index 0000000..5d1e0d8 --- /dev/null +++ b/autotests/integration/move_resize_window_test.cpp @@ -0,0 +1,1148 @@ + +/* + 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 "atoms.h" +#include "platform.h" +#include "abstract_client.h" +#include "x11client.h" +#include "cursor.h" +#include "effects.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include "deleted.h" + +#include +#include +#include +#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 testPlasmaShellSurfaceMovable_data(); + void testPlasmaShellSurfaceMovable(); + void testNetMove(); + void testAdjustClientGeometryOfAutohidingX11Panel_data(); + void testAdjustClientGeometryOfAutohidingX11Panel(); + void testAdjustClientGeometryOfAutohidingWaylandPanel_data(); + void testAdjustClientGeometryOfAutohidingWaylandPanel(); + void testResizeForVirtualKeyboard(); + void testResizeForVirtualKeyboardWithMaximize(); + void testResizeForVirtualKeyboardWithFullScreen(); + void testDestroyMoveClient(); + void testDestroyResizeClient(); + void testSetFullScreenWhenMoving(); + void testSetMaximizeWhenMoving(); + +private: + KWayland::Client::ConnectionThread *m_connection = nullptr; + KWayland::Client::Compositor *m_compositor = nullptr; +}; + +void MoveResizeWindowTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType("MaximizeMode"); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 1); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); +} + +void MoveResizeWindowTest::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::PlasmaShell | Test::AdditionalWaylandInterface::Seat)); + QVERIFY(Test::waitForWaylandPointer()); + m_connection = Test::waylandConnection(); + m_compositor = Test::waylandCompositor(); + + screens()->setCurrent(0); +} + +void MoveResizeWindowTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void MoveResizeWindowTest::testMove() +{ + using namespace KWayland::Client; + + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + QSignalSpy sizeChangeSpy(shellSurface.data(), &XdgShellSurface::sizeChanged); + QVERIFY(sizeChangeSpy.isValid()); + // let's render + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + + QVERIFY(c); + QCOMPARE(workspace()->activeClient(), c); + QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); + QSignalSpy startMoveResizedSpy(c, &AbstractClient::clientStartUserMovedResized); + QVERIFY(startMoveResizedSpy.isValid()); + QSignalSpy moveResizedChangedSpy(c, &AbstractClient::moveResizedChanged); + QVERIFY(moveResizedChangedSpy.isValid()); + QSignalSpy clientStepUserMovedResizedSpy(c, &AbstractClient::clientStepUserMovedResized); + QVERIFY(clientStepUserMovedResizedSpy.isValid()); + QSignalSpy clientFinishUserMovedResizedSpy(c, &AbstractClient::clientFinishUserMovedResized); + QVERIFY(clientFinishUserMovedResizedSpy.isValid()); + + // effects signal handlers + QSignalSpy windowStartUserMovedResizedSpy(effects, &EffectsHandler::windowStartUserMovedResized); + QVERIFY(windowStartUserMovedResizedSpy.isValid()); + QSignalSpy windowStepUserMovedResizedSpy(effects, &EffectsHandler::windowStepUserMovedResized); + QVERIFY(windowStepUserMovedResizedSpy.isValid()); + QSignalSpy windowFinishUserMovedResizedSpy(effects, &EffectsHandler::windowFinishUserMovedResized); + QVERIFY(windowFinishUserMovedResizedSpy.isValid()); + + // begin move + QVERIFY(workspace()->moveResizeClient() == nullptr); + QCOMPARE(c->isMove(), false); + workspace()->slotWindowMove(); + QCOMPARE(workspace()->moveResizeClient(), c); + QCOMPARE(startMoveResizedSpy.count(), 1); + QCOMPARE(moveResizedChangedSpy.count(), 1); + QCOMPARE(windowStartUserMovedResizedSpy.count(), 1); + QCOMPARE(c->isMove(), true); + QCOMPARE(c->geometryRestore(), QRect(0, 0, 100, 50)); + + // send some key events, not going through input redirection + const QPoint cursorPos = Cursors::self()->mouse()->pos(); + c->keyPressEvent(Qt::Key_Right); + c->updateMoveResize(Cursors::self()->mouse()->pos()); + QCOMPARE(Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0)); + QEXPECT_FAIL("", "First event is ignored", Continue); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); + clientStepUserMovedResizedSpy.clear(); + windowStepUserMovedResizedSpy.clear(); + + c->keyPressEvent(Qt::Key_Right); + c->updateMoveResize(Cursors::self()->mouse()->pos()); + QCOMPARE(Cursors::self()->mouse()->pos(), cursorPos + QPoint(16, 0)); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); + QCOMPARE(windowStepUserMovedResizedSpy.count(), 1); + + c->keyPressEvent(Qt::Key_Down | Qt::ALT); + c->updateMoveResize(Cursors::self()->mouse()->pos()); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 2); + QCOMPARE(windowStepUserMovedResizedSpy.count(), 2); + QCOMPARE(c->frameGeometry(), QRect(16, 32, 100, 50)); + QCOMPARE(Cursors::self()->mouse()->pos(), cursorPos + QPoint(16, 32)); + + // let's end + QCOMPARE(clientFinishUserMovedResizedSpy.count(), 0); + c->keyPressEvent(Qt::Key_Enter); + QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); + QCOMPARE(moveResizedChangedSpy.count(), 2); + QCOMPARE(windowFinishUserMovedResizedSpy.count(), 1); + QCOMPARE(c->frameGeometry(), QRect(16, 32, 100, 50)); + QCOMPARE(c->isMove(), false); + QVERIFY(workspace()->moveResizeClient() == nullptr); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(c)); +} + +void MoveResizeWindowTest::testResize() +{ + // a test case which manually resizes a window + using namespace KWayland::Client; + + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + + QScopedPointer shellSurface(Test::createXdgShellStableSurface( + surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + QVERIFY(!shellSurface.isNull()); + + // Wait for the initial configure event. + XdgShellSurface::States states; + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + surface->commit(Surface::CommitFlag::None); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 1); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(XdgShellSurface::State::Resizing)); + + // Let's render. + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QSignalSpy surfaceSizeChangedSpy(shellSurface.data(), &XdgShellSurface::sizeChanged); + QVERIFY(surfaceSizeChangedSpy.isValid()); + + // We have to receive a configure event when the client becomes active. + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 2); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(XdgShellSurface::State::Resizing)); + + QVERIFY(c); + QCOMPARE(workspace()->activeClient(), c); + QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); + QSignalSpy frameGeometryChangedSpy(c, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); + QSignalSpy startMoveResizedSpy(c, &AbstractClient::clientStartUserMovedResized); + QVERIFY(startMoveResizedSpy.isValid()); + QSignalSpy moveResizedChangedSpy(c, &AbstractClient::moveResizedChanged); + QVERIFY(moveResizedChangedSpy.isValid()); + QSignalSpy clientStepUserMovedResizedSpy(c, &AbstractClient::clientStepUserMovedResized); + QVERIFY(clientStepUserMovedResizedSpy.isValid()); + QSignalSpy clientFinishUserMovedResizedSpy(c, &AbstractClient::clientFinishUserMovedResized); + QVERIFY(clientFinishUserMovedResizedSpy.isValid()); + + // begin resize + QCOMPARE(workspace()->moveResizeClient(), nullptr); + QCOMPARE(c->isMove(), false); + QCOMPARE(c->isResize(), false); + workspace()->slotWindowResize(); + QCOMPARE(workspace()->moveResizeClient(), c); + QCOMPARE(startMoveResizedSpy.count(), 1); + QCOMPARE(moveResizedChangedSpy.count(), 1); + QCOMPARE(c->isResize(), true); + QCOMPARE(c->geometryRestore(), QRect(0, 0, 100, 50)); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 3); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Resizing)); + + // Trigger a change. + const QPoint cursorPos = Cursors::self()->mouse()->pos(); + c->keyPressEvent(Qt::Key_Right); + c->updateMoveResize(Cursors::self()->mouse()->pos()); + QCOMPARE(Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0)); + + // The client should receive a configure event with the new size. + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 4); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Resizing)); + QCOMPARE(surfaceSizeChangedSpy.count(), 1); + QCOMPARE(surfaceSizeChangedSpy.last().first().toSize(), QSize(108, 50)); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 0); + + // Now render new size. + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + Test::render(surface.data(), QSize(108, 50), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(c->frameGeometry(), QRect(0, 0, 108, 50)); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); + + // Go down. + c->keyPressEvent(Qt::Key_Down); + c->updateMoveResize(Cursors::self()->mouse()->pos()); + QCOMPARE(Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 8)); + + // The client should receive another configure event. + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 5); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Resizing)); + QCOMPARE(surfaceSizeChangedSpy.count(), 2); + QCOMPARE(surfaceSizeChangedSpy.last().first().toSize(), QSize(108, 58)); + + // Now render new size. + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + Test::render(surface.data(), QSize(108, 58), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(c->frameGeometry(), QRect(0, 0, 108, 58)); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 2); + + // Let's finalize the resize operation. + QCOMPARE(clientFinishUserMovedResizedSpy.count(), 0); + c->keyPressEvent(Qt::Key_Enter); + QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); + QCOMPARE(moveResizedChangedSpy.count(), 2); + QCOMPARE(c->isResize(), false); + QCOMPARE(workspace()->moveResizeClient(), nullptr); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 6); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(XdgShellSurface::State::Resizing)); + + // Destroy the client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(c)); +} + +void MoveResizeWindowTest::testPackTo_data() +{ + QTest::addColumn("methodCall"); + QTest::addColumn("expectedGeometry"); + + QTest::newRow("left") << QStringLiteral("slotWindowPackLeft") << QRect(0, 487, 100, 50); + QTest::newRow("up") << QStringLiteral("slotWindowPackUp") << QRect(590, 0, 100, 50); + QTest::newRow("right") << QStringLiteral("slotWindowPackRight") << QRect(1180, 487, 100, 50); + QTest::newRow("down") << QStringLiteral("slotWindowPackDown") << QRect(590, 974, 100, 50); +} + +void MoveResizeWindowTest::testPackTo() +{ + using namespace KWayland::Client; + + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + QSignalSpy sizeChangeSpy(shellSurface.data(), &XdgShellSurface::sizeChanged); + QVERIFY(sizeChangeSpy.isValid()); + // let's render + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + + QVERIFY(c); + QCOMPARE(workspace()->activeClient(), c); + QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); + + // let's place it centered + Placement::self()->placeCentered(c, QRect(0, 0, 1280, 1024)); + QCOMPARE(c->frameGeometry(), QRect(590, 487, 100, 50)); + + QFETCH(QString, methodCall); + QMetaObject::invokeMethod(workspace(), methodCall.toLocal8Bit().constData()); + QTEST(c->frameGeometry(), "expectedGeometry"); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(c)); +} + +void MoveResizeWindowTest::testPackAgainstClient_data() +{ + QTest::addColumn("methodCall"); + QTest::addColumn("expectedGeometry"); + + QTest::newRow("left") << QStringLiteral("slotWindowPackLeft") << QRect(10, 487, 100, 50); + QTest::newRow("up") << QStringLiteral("slotWindowPackUp") << QRect(590, 10, 100, 50); + QTest::newRow("right") << QStringLiteral("slotWindowPackRight") << QRect(1170, 487, 100, 50); + QTest::newRow("down") << QStringLiteral("slotWindowPackDown") << QRect(590, 964, 100, 50); +} + +void MoveResizeWindowTest::testPackAgainstClient() +{ + using namespace KWayland::Client; + + QScopedPointer surface1(Test::createSurface()); + QVERIFY(!surface1.isNull()); + QScopedPointer surface2(Test::createSurface()); + QVERIFY(!surface2.isNull()); + QScopedPointer surface3(Test::createSurface()); + QVERIFY(!surface3.isNull()); + QScopedPointer surface4(Test::createSurface()); + QVERIFY(!surface4.isNull()); + + QScopedPointer shellSurface1(Test::createXdgShellStableSurface(surface1.data())); + QVERIFY(!shellSurface1.isNull()); + QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data())); + QVERIFY(!shellSurface2.isNull()); + QScopedPointer shellSurface3(Test::createXdgShellStableSurface(surface3.data())); + QVERIFY(!shellSurface3.isNull()); + QScopedPointer shellSurface4(Test::createXdgShellStableSurface(surface4.data())); + QVERIFY(!shellSurface4.isNull()); + auto renderWindow = [] (Surface *surface, const QString &methodCall, const QRect &expectedGeometry) { + // let's render + auto c = Test::renderAndWaitForShown(surface, QSize(10, 10), Qt::blue); + + QVERIFY(c); + QCOMPARE(workspace()->activeClient(), c); + QCOMPARE(c->frameGeometry().size(), QSize(10, 10)); + // let's place it centered + Placement::self()->placeCentered(c, QRect(0, 0, 1280, 1024)); + QCOMPARE(c->frameGeometry(), QRect(635, 507, 10, 10)); + QMetaObject::invokeMethod(workspace(), methodCall.toLocal8Bit().constData()); + QCOMPARE(c->frameGeometry(), expectedGeometry); + }; + renderWindow(surface1.data(), QStringLiteral("slotWindowPackLeft"), QRect(0, 507, 10, 10)); + renderWindow(surface2.data(), QStringLiteral("slotWindowPackUp"), QRect(635, 0, 10, 10)); + renderWindow(surface3.data(), QStringLiteral("slotWindowPackRight"), QRect(1270, 507, 10, 10)); + renderWindow(surface4.data(), QStringLiteral("slotWindowPackDown"), QRect(635, 1014, 10, 10)); + + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + + QVERIFY(c); + QCOMPARE(workspace()->activeClient(), c); + // let's place it centered + Placement::self()->placeCentered(c, QRect(0, 0, 1280, 1024)); + QCOMPARE(c->frameGeometry(), QRect(590, 487, 100, 50)); + + QFETCH(QString, methodCall); + QMetaObject::invokeMethod(workspace(), methodCall.toLocal8Bit().constData()); + QTEST(c->frameGeometry(), "expectedGeometry"); +} + +void MoveResizeWindowTest::testGrowShrink_data() +{ + QTest::addColumn("methodCall"); + QTest::addColumn("expectedGeometry"); + + QTest::newRow("grow vertical") << QStringLiteral("slotWindowGrowVertical") << QRect(590, 487, 100, 537); + QTest::newRow("grow horizontal") << QStringLiteral("slotWindowGrowHorizontal") << QRect(590, 487, 690, 50); + QTest::newRow("shrink vertical") << QStringLiteral("slotWindowShrinkVertical") << QRect(590, 487, 100, 23); + QTest::newRow("shrink horizontal") << QStringLiteral("slotWindowShrinkHorizontal") << QRect(590, 487, 40, 50); +} + +void MoveResizeWindowTest::testGrowShrink() +{ + using namespace KWayland::Client; + + // block geometry helper + QScopedPointer surface1(Test::createSurface()); + QVERIFY(!surface1.isNull()); + QScopedPointer shellSurface1(Test::createXdgShellStableSurface(surface1.data())); + QVERIFY(!shellSurface1.isNull()); + Test::render(surface1.data(), QSize(650, 514), Qt::blue); + QVERIFY(Test::waitForWaylandWindowShown()); + workspace()->slotWindowPackRight(); + workspace()->slotWindowPackDown(); + + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + QSignalSpy sizeChangeSpy(shellSurface.data(), &XdgShellSurface::sizeChanged); + QVERIFY(sizeChangeSpy.isValid()); + // let's render + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + + QVERIFY(c); + QCOMPARE(workspace()->activeClient(), c); + + // let's place it centered + Placement::self()->placeCentered(c, QRect(0, 0, 1280, 1024)); + QCOMPARE(c->frameGeometry(), QRect(590, 487, 100, 50)); + + QFETCH(QString, methodCall); + QMetaObject::invokeMethod(workspace(), methodCall.toLocal8Bit().constData()); + QVERIFY(sizeChangeSpy.wait()); + Test::render(surface.data(), shellSurface->size(), Qt::red); + + QSignalSpy frameGeometryChangedSpy(c, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); + m_connection->flush(); + QVERIFY(frameGeometryChangedSpy.wait()); + QTEST(c->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 + using namespace KWayland::Client; + + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + QSignalSpy sizeChangeSpy(shellSurface.data(), &XdgShellSurface::sizeChanged); + QVERIFY(sizeChangeSpy.isValid()); + // let's render + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + + QVERIFY(c); + QCOMPARE(c, workspace()->activeClient()); + QVERIFY(!c->isMove()); + + // let's trigger the left button + quint32 timestamp = 1; + kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); + QVERIFY(!c->isMove()); + workspace()->slotWindowMove(); + QVERIFY(c->isMove()); + + // let's press another button + QFETCH(int, additionalButton); + kwinApp()->platform()->pointerButtonPressed(additionalButton, timestamp++); + QVERIFY(c->isMove()); + + // release the left button, should still have the window moving + kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); + QVERIFY(c->isMove()); + + // but releasing the other button should now end moving + kwinApp()->platform()->pointerButtonReleased(additionalButton, timestamp++); + QVERIFY(!c->isMove()); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(c)); +} +void MoveResizeWindowTest::testClientSideMove() +{ + using namespace KWayland::Client; + Cursors::self()->mouse()->setPos(640, 512); + QScopedPointer pointer(Test::waylandSeat()->createPointer()); + QSignalSpy pointerEnteredSpy(pointer.data(), &Pointer::entered); + QVERIFY(pointerEnteredSpy.isValid()); + QSignalSpy pointerLeftSpy(pointer.data(), &Pointer::left); + QVERIFY(pointerLeftSpy.isValid()); + QSignalSpy buttonSpy(pointer.data(), &Pointer::buttonStateChanged); + QVERIFY(buttonSpy.isValid()); + + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + + // move pointer into center of geometry + const QRect startGeometry = c->frameGeometry(); + Cursors::self()->mouse()->setPos(startGeometry.center()); + QVERIFY(pointerEnteredSpy.wait()); + QCOMPARE(pointerEnteredSpy.first().last().toPoint(), QPoint(49, 24)); + // simulate press + quint32 timestamp = 1; + kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); + QVERIFY(buttonSpy.wait()); + QSignalSpy moveStartSpy(c, &AbstractClient::clientStartUserMovedResized); + QVERIFY(moveStartSpy.isValid()); + shellSurface->requestMove(Test::waylandSeat(), buttonSpy.first().first().value()); + QVERIFY(moveStartSpy.wait()); + QCOMPARE(c->isMove(), true); + QVERIFY(pointerLeftSpy.wait()); + + // move a bit + QSignalSpy clientMoveStepSpy(c, &AbstractClient::clientStepUserMovedResized); + QVERIFY(clientMoveStepSpy.isValid()); + const QPoint startPoint = startGeometry.center(); + const int dragDistance = QApplication::startDragDistance(); + // Why? + kwinApp()->platform()->pointerMotion(startPoint + QPoint(dragDistance, dragDistance) + QPoint(6, 6), timestamp++); + QCOMPARE(clientMoveStepSpy.count(), 1); + + // and release again + kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); + QVERIFY(pointerEnteredSpy.wait()); + QCOMPARE(c->isMove(), false); + QCOMPARE(c->frameGeometry(), startGeometry.translated(QPoint(dragDistance, dragDistance) + QPoint(6, 6))); + QCOMPARE(pointerEnteredSpy.last().last().toPoint(), QPoint(49, 24)); +} + +void MoveResizeWindowTest::testPlasmaShellSurfaceMovable_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 MoveResizeWindowTest::testPlasmaShellSurfaceMovable() +{ + // this test verifies that certain window types from PlasmaShellSurface are not moveable or resizable + using namespace KWayland::Client; + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + // and a PlasmaShellSurface + QScopedPointer plasmaSurface(Test::waylandPlasmaShell()->createSurface(surface.data())); + QVERIFY(!plasmaSurface.isNull()); + QFETCH(KWayland::Client::PlasmaShellSurface::Role, role); + plasmaSurface->setRole(role); + // let's render + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + + QVERIFY(c); + QTEST(c->isMovable(), "movable"); + QTEST(c->isMovableAcrossScreens(), "movableAcrossScreens"); + QTEST(c->isResizable(), "resizable"); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(c)); +} + +struct XcbConnectionDeleter +{ + static inline void cleanup(xcb_connection_t *pointer) + { + xcb_disconnect(pointer); + } +}; + +void MoveResizeWindowTest::testNetMove() +{ + // this test verifies that a move request for an X11 window through NET API works + // create an xcb window + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + + xcb_window_t w = xcb_generate_id(c.data()); + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, rootWindow(), + 0, 0, 100, 100, + 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, 0, 0); + xcb_icccm_size_hints_set_size(&hints, 1, 100, 100); + xcb_icccm_set_wm_normal_hints(c.data(), w, &hints); + // let's set a no-border + NETWinInfo winInfo(c.data(), w, rootWindow(), NET::WMWindowType, NET::Properties2()); + winInfo.setWindowType(NET::Override); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->window(), w); + const QRect origGeo = client->frameGeometry(); + + // let's move the cursor outside the window + Cursors::self()->mouse()->setPos(screens()->geometry(0).center()); + QVERIFY(!origGeo.contains(Cursors::self()->mouse()->pos())); + + QSignalSpy moveStartSpy(client, &X11Client::clientStartUserMovedResized); + QVERIFY(moveStartSpy.isValid()); + QSignalSpy moveEndSpy(client, &X11Client::clientFinishUserMovedResized); + QVERIFY(moveEndSpy.isValid()); + QSignalSpy moveStepSpy(client, &X11Client::clientStepUserMovedResized); + QVERIFY(moveStepSpy.isValid()); + QVERIFY(!workspace()->moveResizeClient()); + + // use NETRootInfo to trigger a move request + NETRootInfo root(c.data(), NET::Properties()); + root.moveResizeRequest(w, origGeo.center().x(), origGeo.center().y(), NET::Move); + xcb_flush(c.data()); + + QVERIFY(moveStartSpy.wait()); + QCOMPARE(workspace()->moveResizeClient(), client); + QVERIFY(client->isMove()); + QCOMPARE(client->geometryRestore(), origGeo); + QCOMPARE(Cursors::self()->mouse()->pos(), origGeo.center()); + + // let's move a step + Cursors::self()->mouse()->setPos(Cursors::self()->mouse()->pos() + QPoint(10, 10)); + QCOMPARE(moveStepSpy.count(), 1); + QCOMPARE(moveStepSpy.first().last().toRect(), origGeo.translated(10, 10)); + + // let's cancel the move resize again through the net API + root.moveResizeRequest(w, client->frameGeometry().center().x(), client->frameGeometry().center().y(), NET::MoveResizeCancel); + xcb_flush(c.data()); + QVERIFY(moveEndSpy.wait()); + + // and destroy the window again + xcb_unmap_window(c.data(), w); + xcb_destroy_window(c.data(), w); + xcb_flush(c.data()); + c.reset(); + + QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + QVERIFY(windowClosedSpy.wait()); +} + +void MoveResizeWindowTest::testAdjustClientGeometryOfAutohidingX11Panel_data() +{ + QTest::addColumn("panelGeometry"); + QTest::addColumn("targetPoint"); + QTest::addColumn("expectedAdjustedPoint"); + QTest::addColumn("hideLocation"); + + QTest::newRow("top") << QRect(0, 0, 100, 20) << QPoint(50, 25) << QPoint(50, 20) << 0u; + QTest::newRow("bottom") << QRect(0, 1024-20, 100, 20) << QPoint(50, 1024 - 25 - 50) << QPoint(50, 1024 - 20 - 50) << 2u; + QTest::newRow("left") << QRect(0, 0, 20, 100) << QPoint(25, 50) << QPoint(20, 50) << 3u; + QTest::newRow("right") << QRect(1280 - 20, 0, 20, 100) << QPoint(1280 - 25 - 100, 50) << QPoint(1280 - 20 - 100, 50) << 1u; +} + +void MoveResizeWindowTest::testAdjustClientGeometryOfAutohidingX11Panel() +{ + // this test verifies that auto hiding panels are ignored when adjusting client geometry + // see BUG 365892 + + // first create our panel + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + + xcb_window_t w = xcb_generate_id(c.data()); + QFETCH(QRect, panelGeometry); + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, rootWindow(), + panelGeometry.x(), panelGeometry.y(), panelGeometry.width(), panelGeometry.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, panelGeometry.x(), panelGeometry.y()); + xcb_icccm_size_hints_set_size(&hints, 1, panelGeometry.width(), panelGeometry.height()); + xcb_icccm_set_wm_normal_hints(c.data(), w, &hints); + NETWinInfo winInfo(c.data(), w, rootWindow(), NET::WMWindowType, NET::Properties2()); + winInfo.setWindowType(NET::Dock); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *panel = windowCreatedSpy.first().first().value(); + QVERIFY(panel); + QCOMPARE(panel->window(), w); + QCOMPARE(panel->frameGeometry(), panelGeometry); + QVERIFY(panel->isDock()); + + // let's create a window + using namespace KWayland::Client; + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + auto testWindow = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + + QVERIFY(testWindow); + QVERIFY(testWindow->isMovable()); + // panel is not yet hidden, we should snap against it + QFETCH(QPoint, targetPoint); + QTEST(Workspace::self()->adjustClientPosition(testWindow, targetPoint, false), "expectedAdjustedPoint"); + + // now let's hide the panel + QSignalSpy panelHiddenSpy(panel, &AbstractClient::windowHidden); + QVERIFY(panelHiddenSpy.isValid()); + QFETCH(quint32, hideLocation); + xcb_change_property(c.data(), XCB_PROP_MODE_REPLACE, w, atoms->kde_screen_edge_show, XCB_ATOM_CARDINAL, 32, 1, &hideLocation); + xcb_flush(c.data()); + QVERIFY(panelHiddenSpy.wait()); + + // now try to snap again + QCOMPARE(Workspace::self()->adjustClientPosition(testWindow, targetPoint, false), targetPoint); + + // and destroy the panel again + xcb_unmap_window(c.data(), w); + xcb_destroy_window(c.data(), w); + xcb_flush(c.data()); + c.reset(); + + QSignalSpy panelClosedSpy(panel, &X11Client::windowClosed); + QVERIFY(panelClosedSpy.isValid()); + QVERIFY(panelClosedSpy.wait()); + + // snap once more + QCOMPARE(Workspace::self()->adjustClientPosition(testWindow, targetPoint, false), targetPoint); + + // and close + QSignalSpy windowClosedSpy(testWindow, &AbstractClient::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + shellSurface.reset(); + surface.reset(); + QVERIFY(windowClosedSpy.wait()); +} + +void MoveResizeWindowTest::testAdjustClientGeometryOfAutohidingWaylandPanel_data() +{ + QTest::addColumn("panelGeometry"); + QTest::addColumn("targetPoint"); + QTest::addColumn("expectedAdjustedPoint"); + + QTest::newRow("top") << QRect(0, 0, 100, 20) << QPoint(50, 25) << QPoint(50, 20); + QTest::newRow("bottom") << QRect(0, 1024-20, 100, 20) << QPoint(50, 1024 - 25 - 50) << QPoint(50, 1024 - 20 - 50); + QTest::newRow("left") << QRect(0, 0, 20, 100) << QPoint(25, 50) << QPoint(20, 50); + QTest::newRow("right") << QRect(1280 - 20, 0, 20, 100) << QPoint(1280 - 25 - 100, 50) << QPoint(1280 - 20 - 100, 50); +} + +void MoveResizeWindowTest::testAdjustClientGeometryOfAutohidingWaylandPanel() +{ + // this test verifies that auto hiding panels are ignored when adjusting client geometry + // see BUG 365892 + + // first create our panel + using namespace KWayland::Client; + QScopedPointer panelSurface(Test::createSurface()); + QVERIFY(!panelSurface.isNull()); + QScopedPointer panelShellSurface(Test::createXdgShellStableSurface(panelSurface.data())); + QVERIFY(!panelShellSurface.isNull()); + QScopedPointer plasmaSurface(Test::waylandPlasmaShell()->createSurface(panelSurface.data())); + QVERIFY(!plasmaSurface.isNull()); + plasmaSurface->setRole(PlasmaShellSurface::Role::Panel); + plasmaSurface->setPanelBehavior(PlasmaShellSurface::PanelBehavior::AutoHide); + QFETCH(QRect, panelGeometry); + plasmaSurface->setPosition(panelGeometry.topLeft()); + // let's render + auto panel = Test::renderAndWaitForShown(panelSurface.data(), panelGeometry.size(), Qt::blue); + QVERIFY(panel); + QCOMPARE(panel->frameGeometry(), panelGeometry); + QVERIFY(panel->isDock()); + + // let's create a window + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + auto testWindow = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + + QVERIFY(testWindow); + QVERIFY(testWindow->isMovable()); + // panel is not yet hidden, we should snap against it + QFETCH(QPoint, targetPoint); + QTEST(Workspace::self()->adjustClientPosition(testWindow, targetPoint, false), "expectedAdjustedPoint"); + + // now let's hide the panel + QSignalSpy panelHiddenSpy(panel, &AbstractClient::windowHidden); + QVERIFY(panelHiddenSpy.isValid()); + plasmaSurface->requestHideAutoHidingPanel(); + QVERIFY(panelHiddenSpy.wait()); + + // now try to snap again + QCOMPARE(Workspace::self()->adjustClientPosition(testWindow, targetPoint, false), targetPoint); + + // and destroy the panel again + QSignalSpy panelClosedSpy(panel, &AbstractClient::windowClosed); + QVERIFY(panelClosedSpy.isValid()); + plasmaSurface.reset(); + panelShellSurface.reset(); + panelSurface.reset(); + QVERIFY(panelClosedSpy.wait()); + + // snap once more + QCOMPARE(Workspace::self()->adjustClientPosition(testWindow, targetPoint, false), targetPoint); + + // and close + QSignalSpy windowClosedSpy(testWindow, &AbstractClient::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + shellSurface.reset(); + surface.reset(); + QVERIFY(windowClosedSpy.wait()); +} + +void MoveResizeWindowTest::testResizeForVirtualKeyboard() +{ + using namespace KWayland::Client; + + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + + // let's render + auto client = Test::renderAndWaitForShown(surface.data(), QSize(500, 800), Qt::blue); + QVERIFY(client); + + // The client should receive a configure event upon becoming active. + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + QVERIFY(configureRequestedSpy.wait()); + + client->move(100, 300); + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); + + QCOMPARE(client->frameGeometry(), QRect(100, 300, 500, 800)); + client->setVirtualKeyboardGeometry(QRect(0, 100, 1280, 500)); + QVERIFY(configureRequestedSpy.wait()); + + shellSurface->ackConfigure(configureRequestedSpy.last()[2].toInt()); + // render at the new size + Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + + QCOMPARE(client->frameGeometry(), QRect(100, 0, 500, 101)); + client->setVirtualKeyboardGeometry(QRect()); + QVERIFY(configureRequestedSpy.wait()); + + shellSurface->ackConfigure(configureRequestedSpy.last()[2].toInt()); + // render at the new size + Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(client->frameGeometry(), QRect(100, 300, 500, 800)); +} + +void MoveResizeWindowTest::testResizeForVirtualKeyboardWithMaximize() +{ + using namespace KWayland::Client; + + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + + // let's render + auto client = Test::renderAndWaitForShown(surface.data(), QSize(500, 800), Qt::blue); + QVERIFY(client); + + // The client should receive a configure event upon becoming active. + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + QVERIFY(configureRequestedSpy.wait()); + + client->move(100, 300); + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); + + QCOMPARE(client->frameGeometry(), QRect(100, 300, 500, 800)); + client->setVirtualKeyboardGeometry(QRect(0, 100, 1280, 500)); + QVERIFY(configureRequestedSpy.wait()); + + shellSurface->ackConfigure(configureRequestedSpy.last()[2].toInt()); + // render at the new size + Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(client->frameGeometry(), QRect(100, 0, 500, 101)); + + client->setMaximize(true, true); + QVERIFY(configureRequestedSpy.wait()); + shellSurface->ackConfigure(configureRequestedSpy.last()[2].toInt()); + Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(client->frameGeometry(), QRect(0, 0, 1280, 1024)); + + client->setVirtualKeyboardGeometry(QRect()); + QVERIFY(!configureRequestedSpy.wait(10)); + + // render at the size of the configureRequested.. it won't have changed + Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::blue); + QVERIFY(!frameGeometryChangedSpy.wait(10)); + + // Size will NOT be restored + QCOMPARE(client->frameGeometry(), QRect(0, 0, 1280, 1024)); +} + +void MoveResizeWindowTest::testResizeForVirtualKeyboardWithFullScreen() +{ + using namespace KWayland::Client; + + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + + // let's render + auto client = Test::renderAndWaitForShown(surface.data(), QSize(500, 800), Qt::blue); + QVERIFY(client); + + // The client should receive a configure event upon becoming active. + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + QVERIFY(configureRequestedSpy.wait()); + + client->move(100, 300); + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); + + QCOMPARE(client->frameGeometry(), QRect(100, 300, 500, 800)); + client->setVirtualKeyboardGeometry(QRect(0, 100, 1280, 500)); + QVERIFY(configureRequestedSpy.wait()); + + shellSurface->ackConfigure(configureRequestedSpy.last()[2].toInt()); + // render at the new size + Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(client->frameGeometry(), QRect(100, 0, 500, 101)); + + client->setFullScreen(true, true); + QVERIFY(configureRequestedSpy.wait()); + shellSurface->ackConfigure(configureRequestedSpy.last()[2].toInt()); + Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(client->frameGeometry(), QRect(0, 0, 1280, 1024)); + + client->setVirtualKeyboardGeometry(QRect()); + QVERIFY(!configureRequestedSpy.wait(10)); + + // render at the size of the configureRequested.. it won't have changed + Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::blue); + QVERIFY(!frameGeometryChangedSpy.wait(10)); + // Size will NOT be restored + QCOMPARE(client->frameGeometry(), QRect(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. + using namespace KWayland::Client; + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + + // Start moving the client. + QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); + QVERIFY(clientStartMoveResizedSpy.isValid()); + QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); + QVERIFY(clientFinishUserMovedResizedSpy.isValid()); + + QCOMPARE(workspace()->moveResizeClient(), nullptr); + QCOMPARE(client->isMove(), false); + QCOMPARE(client->isResize(), false); + workspace()->slotWindowMove(); + QCOMPARE(clientStartMoveResizedSpy.count(), 1); + QCOMPARE(workspace()->moveResizeClient(), client); + QCOMPARE(client->isMove(), true); + QCOMPARE(client->isResize(), false); + + // Let's pretend that the client crashed. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); + QCOMPARE(clientFinishUserMovedResizedSpy.count(), 0); + QCOMPARE(workspace()->moveResizeClient(), nullptr); +} + +void MoveResizeWindowTest::testDestroyResizeClient() +{ + // This test verifies that active resize operation gets finished when + // the associated client is destroyed. + + // Create the test client. + using namespace KWayland::Client; + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + + // Start resizing the client. + QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); + QVERIFY(clientStartMoveResizedSpy.isValid()); + QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); + QVERIFY(clientFinishUserMovedResizedSpy.isValid()); + + QCOMPARE(workspace()->moveResizeClient(), nullptr); + QCOMPARE(client->isMove(), false); + QCOMPARE(client->isResize(), false); + workspace()->slotWindowResize(); + QCOMPARE(clientStartMoveResizedSpy.count(), 1); + QCOMPARE(workspace()->moveResizeClient(), client); + QCOMPARE(client->isMove(), false); + QCOMPARE(client->isResize(), true); + + // Let's pretend that the client crashed. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); + QCOMPARE(clientFinishUserMovedResizedSpy.count(), 0); + QCOMPARE(workspace()->moveResizeClient(), nullptr); +} + +void MoveResizeWindowTest::testSetFullScreenWhenMoving() +{ + // Ensure we disable moving event when setFullScreen is triggered + using namespace KWayland::Client; + + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + + // let's render + auto client = Test::renderAndWaitForShown(surface.data(), QSize(500, 800), Qt::blue); + QVERIFY(client); + + workspace()->slotWindowMove(); + QCOMPARE(client->isMove(), true); + client->setFullScreen(true); + QCOMPARE(client->isMove(), false); + QCOMPARE(workspace()->moveResizeClient(), nullptr); + // Let's pretend that the client crashed. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void MoveResizeWindowTest::testSetMaximizeWhenMoving() +{ + // Ensure we disable moving event when changeMaximize is triggered + using namespace KWayland::Client; + + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + + // let's render + auto client = Test::renderAndWaitForShown(surface.data(), QSize(500, 800), Qt::blue); + QVERIFY(client); + + workspace()->slotWindowMove(); + QCOMPARE(client->isMove(), true); + client->setMaximize(true, true); + QCOMPARE(client->isMove(), false); + QCOMPARE(workspace()->moveResizeClient(), nullptr); + // Let's pretend that the client crashed. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} +} + +WAYLANDTEST_MAIN(KWin::MoveResizeWindowTest) +#include "move_resize_window_test.moc" diff --git a/autotests/integration/no_global_shortcuts_test.cpp b/autotests/integration/no_global_shortcuts_test.cpp new file mode 100644 index 0000000..4a0f0f1 --- /dev/null +++ b/autotests/integration/no_global_shortcuts_test.cpp @@ -0,0 +1,274 @@ +/* + 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 "cursor.h" +#include "input.h" +#include "keyboard_input.h" +#include "platform.h" +#include "screenedge.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include + +#include + +#include + +using namespace KWin; +using namespace KWayland::Client; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_no_global_shortcuts-0"); +static const QString s_serviceName = QStringLiteral("org.kde.KWin.Test.ModifierOnlyShortcut"); +static const QString s_path = QStringLiteral("/Test"); + +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 testTrigger_data(); + void testTrigger(); + void testKGlobalAccel(); + void testPointerShortcut(); + void testAxisShortcut_data(); + void testAxisShortcut(); + void testScreenEdge(); +}; + +class Target : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.KWin.Test.ModifierOnlyShortcut") + +public: + Target(); + ~Target() override; + +public Q_SLOTS: + Q_SCRIPTABLE void shortcut(); + +Q_SIGNALS: + void shortcutTriggered(); +}; + +Target::Target() + : QObject() +{ + QDBusConnection::sessionBus().registerService(s_serviceName); + QDBusConnection::sessionBus().registerObject(s_path, s_serviceName, this, QDBusConnection::ExportScriptableSlots); +} + +Target::~Target() +{ + QDBusConnection::sessionBus().unregisterObject(s_path); + QDBusConnection::sessionBus().unregisterService(s_serviceName); +} + +void Target::shortcut() +{ + emit shortcutTriggered(); +} + +void NoGlobalShortcutsTest::initTestCase() +{ + qRegisterMetaType("ElectricBorder"); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit(), KWin::WaylandServer::InitializationFlag::NoGlobalShortcuts)); + + kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); + qputenv("KWIN_XKB_DEFAULT_KEYMAP", "1"); + qputenv("XKB_DEFAULT_RULES", "evdev"); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + waylandServer()->initWorkspace(); +} + +void NoGlobalShortcutsTest::init() +{ + screens()->setCurrent(0); + KWin::Cursors::self()->mouse()->setPos(QPoint(640, 512)); +} + +void NoGlobalShortcutsTest::cleanup() +{ +} + +void NoGlobalShortcutsTest::testTrigger_data() +{ + QTest::addColumn("metaConfig"); + QTest::addColumn("altConfig"); + QTest::addColumn("controlConfig"); + QTest::addColumn("shiftConfig"); + QTest::addColumn("modifier"); + QTest::addColumn>("nonTriggeringMods"); + + const QStringList trigger = QStringList{s_serviceName, s_path, s_serviceName, QStringLiteral("shortcut")}; + const QStringList e = QStringList(); + + QTest::newRow("leftMeta") << trigger << e << e << e << KEY_LEFTMETA << QList{KEY_LEFTALT, KEY_RIGHTALT, KEY_LEFTCTRL, KEY_RIGHTCTRL, KEY_LEFTSHIFT, KEY_RIGHTSHIFT}; + QTest::newRow("rightMeta") << trigger << e << e << e << KEY_RIGHTMETA << QList{KEY_LEFTALT, KEY_RIGHTALT, KEY_LEFTCTRL, KEY_RIGHTCTRL, KEY_LEFTSHIFT, KEY_RIGHTSHIFT}; + QTest::newRow("leftAlt") << e << trigger << e << e << KEY_LEFTALT << QList{KEY_LEFTMETA, KEY_RIGHTMETA, KEY_LEFTCTRL, KEY_RIGHTCTRL, KEY_LEFTSHIFT, KEY_RIGHTSHIFT}; + QTest::newRow("rightAlt") << e << trigger << e << e << KEY_RIGHTALT << QList{KEY_LEFTMETA, KEY_RIGHTMETA, KEY_LEFTCTRL, KEY_RIGHTCTRL, KEY_LEFTSHIFT, KEY_RIGHTSHIFT}; + QTest::newRow("leftControl") << e << e << trigger << e << KEY_LEFTCTRL << QList{KEY_LEFTALT, KEY_RIGHTALT, KEY_LEFTMETA, KEY_RIGHTMETA, KEY_LEFTSHIFT, KEY_RIGHTSHIFT}; + QTest::newRow("rightControl") << e << e << trigger << e << KEY_RIGHTCTRL << QList{KEY_LEFTALT, KEY_RIGHTALT, KEY_LEFTMETA, KEY_RIGHTMETA, KEY_LEFTSHIFT, KEY_RIGHTSHIFT}; + QTest::newRow("leftShift") << e << e << e << trigger << KEY_LEFTSHIFT << QList{KEY_LEFTALT, KEY_RIGHTALT, KEY_LEFTCTRL, KEY_RIGHTCTRL, KEY_LEFTMETA, KEY_RIGHTMETA}; + QTest::newRow("rightShift") << e << e << e << trigger <{KEY_LEFTALT, KEY_RIGHTALT, KEY_LEFTCTRL, KEY_RIGHTCTRL, KEY_LEFTMETA, KEY_RIGHTMETA}; +} + +void NoGlobalShortcutsTest::testTrigger() +{ + // test based on ModifierOnlyShortcutTest::testTrigger + Target target; + QSignalSpy triggeredSpy(&target, &Target::shortcutTriggered); + QVERIFY(triggeredSpy.isValid()); + + KConfigGroup group = kwinApp()->config()->group("ModifierOnlyShortcuts"); + QFETCH(QStringList, metaConfig); + QFETCH(QStringList, altConfig); + QFETCH(QStringList, shiftConfig); + QFETCH(QStringList, controlConfig); + group.writeEntry("Meta", metaConfig); + group.writeEntry("Alt", altConfig); + group.writeEntry("Shift", shiftConfig); + group.writeEntry("Control", controlConfig); + group.sync(); + workspace()->slotReconfigure(); + + // configured shortcut should trigger + quint32 timestamp = 1; + QFETCH(int, modifier); + kwinApp()->platform()->keyboardKeyPressed(modifier, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(modifier, timestamp++); + QCOMPARE(triggeredSpy.count(), 0); + + // the other shortcuts should not trigger + QFETCH(QList, nonTriggeringMods); + for (auto it = nonTriggeringMods.constBegin(), end = nonTriggeringMods.constEnd(); it != end; it++) { + kwinApp()->platform()->keyboardKeyPressed(*it, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(*it, timestamp++); + QCOMPARE(triggeredSpy.count(), 0); + } +} + +void NoGlobalShortcutsTest::testKGlobalAccel() +{ + QScopedPointer action(new QAction(nullptr)); + action->setProperty("componentName", QStringLiteral(KWIN_NAME)); + action->setObjectName(QStringLiteral("globalshortcuts-test-meta-shift-w")); + QSignalSpy triggeredSpy(action.data(), &QAction::triggered); + QVERIFY(triggeredSpy.isValid()); + KGlobalAccel::self()->setShortcut(action.data(), QList{Qt::META + Qt::SHIFT + Qt::Key_W}, KGlobalAccel::NoAutoloading); + input()->registerShortcut(Qt::META + Qt::SHIFT + Qt::Key_W, action.data()); + + // press meta+shift+w + quint32 timestamp = 0; + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + QCOMPARE(input()->keyboardModifiers(), Qt::MetaModifier); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTSHIFT, timestamp++); + QCOMPARE(input()->keyboardModifiers(), Qt::ShiftModifier | Qt::MetaModifier); + kwinApp()->platform()->keyboardKeyPressed(KEY_W, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_W, timestamp++); + + // release meta+shift + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTSHIFT, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTMETA, timestamp++); + + QVERIFY(!triggeredSpy.wait()); + QCOMPARE(triggeredSpy.count(), 0); +} + +void NoGlobalShortcutsTest::testPointerShortcut() +{ + // based on LockScreenTest::testPointerShortcut + QScopedPointer action(new QAction(nullptr)); + QSignalSpy actionSpy(action.data(), &QAction::triggered); + QVERIFY(actionSpy.isValid()); + input()->registerPointerShortcut(Qt::MetaModifier, Qt::LeftButton, action.data()); + + // try to trigger the shortcut + quint32 timestamp = 1; + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); + QCoreApplication::instance()->processEvents(); + QCOMPARE(actionSpy.count(), 0); + kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); + kwinApp()->platform()->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 + QScopedPointer action(new QAction(nullptr)); + QSignalSpy actionSpy(action.data(), &QAction::triggered); + QVERIFY(actionSpy.isValid()); + 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.data()); + + // try to trigger the shortcut + quint32 timestamp = 1; + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + if (direction == Qt::Vertical) + kwinApp()->platform()->pointerAxisVertical(sign * 5.0, timestamp++); + else + kwinApp()->platform()->pointerAxisHorizontal(sign * 5.0, timestamp++); + QCoreApplication::instance()->processEvents(); + QCOMPARE(actionSpy.count(), 0); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTMETA, timestamp++); + QCoreApplication::instance()->processEvents(); + QCOMPARE(actionSpy.count(), 0); +} + +void NoGlobalShortcutsTest::testScreenEdge() +{ + // based on LockScreenTest::testScreenEdge + QSignalSpy screenEdgeSpy(ScreenEdges::self(), &ScreenEdges::approaching); + QVERIFY(screenEdgeSpy.isValid()); + QCOMPARE(screenEdgeSpy.count(), 0); + + quint32 timestamp = 1; + kwinApp()->platform()->pointerMotion({5, 5}, timestamp++); + QCOMPARE(screenEdgeSpy.count(), 0); +} + +WAYLANDTEST_MAIN(NoGlobalShortcutsTest) +#include "no_global_shortcuts_test.moc" diff --git a/autotests/integration/no_xdg_runtime_dir_test.cpp b/autotests/integration/no_xdg_runtime_dir_test.cpp new file mode 100644 index 0000000..0c9254c --- /dev/null +++ b/autotests/integration/no_xdg_runtime_dir_test.cpp @@ -0,0 +1,37 @@ +/* + 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 "wayland_server.h" + +using namespace KWin; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_no_xdg_runtime_dir-0"); + +class NoXdgRuntimeDirTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testInitFails(); +}; + +void NoXdgRuntimeDirTest::initTestCase() +{ + qunsetenv("XDG_RUNTIME_DIR"); +} + +void NoXdgRuntimeDirTest::testInitFails() +{ + // this test verifies that without an XDG_RUNTIME_DIR the WaylandServer fails to start + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QVERIFY(!waylandServer()->start()); +} + +WAYLANDTEST_MAIN(NoXdgRuntimeDirTest) +#include "no_xdg_runtime_dir_test.moc" diff --git a/autotests/integration/placement_test.cpp b/autotests/integration/placement_test.cpp new file mode 100644 index 0000000..20e4c20 --- /dev/null +++ b/autotests/integration/placement_test.cpp @@ -0,0 +1,349 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 David Edmundson + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "abstract_client.h" +#include "cursor.h" +#include "kwin_wayland_test.h" +#include "platform.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include +#include +#include +#include +#include +#include + +using namespace KWin; +using namespace KWayland::Client; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_placement-0"); + +struct PlaceWindowResult +{ + QSize initiallyConfiguredSize; + KWayland::Client::XdgShellSurface::States initiallyConfiguredStates; + QRect finalGeometry; +}; + +class TestPlacement : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void init(); + void cleanup(); + void initTestCase(); + + void testPlaceSmart(); + void testPlaceZeroCornered(); + void testPlaceMaximized(); + void testPlaceMaximizedLeavesFullscreen(); + void testPlaceCentered(); + void testPlaceUnderMouse(); + void testPlaceCascaded(); + void testPlaceRandom(); + +private: + void setPlacementPolicy(Placement::Policy policy); + /* + * Create a window with the lifespan of parent 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. + */ + PlaceWindowResult createAndPlaceWindow(const QSize &defaultSize, QObject *parent); +}; + +void TestPlacement::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::XdgDecoration | + Test::AdditionalWaylandInterface::PlasmaShell)); + + screens()->setCurrent(0); + KWin::Cursors::self()->mouse()->setPos(QPoint(512, 512)); +} + +void TestPlacement::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void TestPlacement::initTestCase() +{ + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + waylandServer()->initWorkspace(); +} + +void TestPlacement::setPlacementPolicy(Placement::Policy policy) +{ + auto group = kwinApp()->config()->group("Windows"); + group.writeEntry("Placement", Placement::policyToString(policy)); + group.sync(); + Workspace::self()->slotReconfigure(); +} + +PlaceWindowResult TestPlacement::createAndPlaceWindow(const QSize &defaultSize, QObject *parent) +{ + PlaceWindowResult rc; + + // create a new window + auto surface = Test::createSurface(parent); + auto shellSurface = Test::createXdgShellStableSurface(surface, surface, Test::CreationSetup::CreateOnly); + QSignalSpy configSpy(shellSurface, &XdgShellSurface::configureRequested); + surface->commit(Surface::CommitFlag::None); + configSpy.wait(); + + rc.initiallyConfiguredSize = configSpy[0][0].toSize(); + rc.initiallyConfiguredStates = configSpy[0][1].value(); + shellSurface->ackConfigure(configSpy[0][2].toUInt()); + + QSize size = rc.initiallyConfiguredSize; + + if (size.isEmpty()) { + size = defaultSize; + } + + auto c = Test::renderAndWaitForShown(surface, size, Qt::red); + + rc.finalGeometry = c->frameGeometry(); + return rc; +} + +void TestPlacement::testPlaceSmart() +{ + setPlacementPolicy(Placement::Smart); + + QScopedPointer testParent(new QObject); //dumb QObject just for scoping surfaces to the test + + QRegion usedArea; + + for (int i = 0; i < 4; i++) { + PlaceWindowResult windowPlacement = createAndPlaceWindow(QSize(600, 500), testParent.data()); + // smart placement shouldn't define a size on clients + QCOMPARE(windowPlacement.initiallyConfiguredSize, QSize(0, 0)); + QCOMPARE(windowPlacement.finalGeometry.size(), QSize(600, 500)); + + // exact placement isn't a defined concept that should be tested + // but the goal of smart placement is to make sure windows don't overlap until they need to + // 4 windows of 600, 500 should fit without overlap + QVERIFY(!usedArea.intersects(windowPlacement.finalGeometry)); + usedArea += windowPlacement.finalGeometry; + } +} + +void TestPlacement::testPlaceZeroCornered() +{ + setPlacementPolicy(Placement::ZeroCornered); + + QScopedPointer testParent(new QObject); + + for (int i = 0; i < 4; i++) { + PlaceWindowResult windowPlacement = createAndPlaceWindow(QSize(600, 500), testParent.data()); + // smart placement shouldn't define a size on clients + QCOMPARE(windowPlacement.initiallyConfiguredSize, QSize(0, 0)); + // size should match our buffer + QCOMPARE(windowPlacement.finalGeometry.size(), QSize(600, 500)); + //and it should be in the corner + QCOMPARE(windowPlacement.finalGeometry.topLeft(), QPoint(0, 0)); + } +} + +void TestPlacement::testPlaceMaximized() +{ + setPlacementPolicy(Placement::Maximizing); + + // add a top panel + QScopedPointer panelSurface(Test::createSurface()); + QScopedPointer panelShellSurface(Test::createXdgShellStableSurface(panelSurface.data())); + QScopedPointer plasmaSurface(Test::waylandPlasmaShell()->createSurface(panelSurface.data())); + plasmaSurface->setRole(PlasmaShellSurface::Role::Panel); + plasmaSurface->setPosition(QPoint(0, 0)); + Test::renderAndWaitForShown(panelSurface.data(), QSize(1280, 20), Qt::blue); + + QScopedPointer testParent(new QObject); + + // all windows should be initially maximized with an initial configure size sent + for (int i = 0; i < 4; i++) { + PlaceWindowResult windowPlacement = createAndPlaceWindow(QSize(600, 500), testParent.data()); + QVERIFY(windowPlacement.initiallyConfiguredStates & XdgShellSurface::State::Maximized); + QCOMPARE(windowPlacement.initiallyConfiguredSize, QSize(1280, 1024 - 20)); + QCOMPARE(windowPlacement.finalGeometry, QRect(0, 20, 1280, 1024 - 20)); // under the panel + } +} + +void TestPlacement::testPlaceMaximizedLeavesFullscreen() +{ + setPlacementPolicy(Placement::Maximizing); + + // add a top panel + QScopedPointer panelSurface(Test::createSurface()); + QScopedPointer panelShellSurface(Test::createXdgShellStableSurface(panelSurface.data())); + QScopedPointer plasmaSurface(Test::waylandPlasmaShell()->createSurface(panelSurface.data())); + plasmaSurface->setRole(PlasmaShellSurface::Role::Panel); + plasmaSurface->setPosition(QPoint(0, 0)); + Test::renderAndWaitForShown(panelSurface.data(), QSize(1280, 20), Qt::blue); + + QScopedPointer testParent(new QObject); + + // all windows should be initially fullscreen with an initial configure size sent, despite the policy + for (int i = 0; i < 4; i++) { + auto surface = Test::createSurface(testParent.data()); + auto shellSurface = Test::createXdgShellStableSurface(surface, surface, Test::CreationSetup::CreateOnly); + shellSurface->setFullscreen(true); + QSignalSpy configSpy(shellSurface, &XdgShellSurface::configureRequested); + surface->commit(Surface::CommitFlag::None); + configSpy.wait(); + + auto initiallyConfiguredSize = configSpy[0][0].toSize(); + auto initiallyConfiguredStates = configSpy[0][1].value(); + shellSurface->ackConfigure(configSpy[0][2].toUInt()); + + auto c = Test::renderAndWaitForShown(surface, initiallyConfiguredSize, Qt::red); + + QVERIFY(initiallyConfiguredStates & XdgShellSurface::State::Fullscreen); + QCOMPARE(initiallyConfiguredSize, QSize(1280, 1024 )); + QCOMPARE(c->frameGeometry(), QRect(0, 0, 1280, 1024)); + } +} + +void TestPlacement::testPlaceCentered() +{ + // This test verifies that Centered placement policy works. + + KConfigGroup group = kwinApp()->config()->group("Windows"); + group.writeEntry("Placement", Placement::policyToString(Placement::Centered)); + group.sync(); + workspace()->slotReconfigure(); + + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::red); + QVERIFY(client); + QCOMPARE(client->frameGeometry(), QRect(590, 487, 100, 50)); + + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestPlacement::testPlaceUnderMouse() +{ + // This test verifies that Under Mouse placement policy works. + + KConfigGroup group = kwinApp()->config()->group("Windows"); + group.writeEntry("Placement", Placement::policyToString(Placement::UnderMouse)); + group.sync(); + workspace()->slotReconfigure(); + + KWin::Cursors::self()->mouse()->setPos(QPoint(200, 300)); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), QPoint(200, 300)); + + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::red); + QVERIFY(client); + QCOMPARE(client->frameGeometry(), QRect(151, 276, 100, 50)); + + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestPlacement::testPlaceCascaded() +{ + // This test verifies that Cascaded placement policy works. + + KConfigGroup group = kwinApp()->config()->group("Windows"); + group.writeEntry("Placement", Placement::policyToString(Placement::Cascade)); + group.sync(); + workspace()->slotReconfigure(); + + QScopedPointer surface1(Test::createSurface()); + QScopedPointer shellSurface1(Test::createXdgShellStableSurface(surface1.data())); + AbstractClient *client1 = Test::renderAndWaitForShown(surface1.data(), QSize(100, 50), Qt::red); + QVERIFY(client1); + QCOMPARE(client1->pos(), QPoint(0, 0)); + QCOMPARE(client1->size(), QSize(100, 50)); + + QScopedPointer surface2(Test::createSurface()); + QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data())); + AbstractClient *client2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 50), Qt::blue); + QVERIFY(client2); + QCOMPARE(client2->pos(), client1->pos() + workspace()->cascadeOffset(client2)); + QCOMPARE(client2->size(), QSize(100, 50)); + + QScopedPointer surface3(Test::createSurface()); + QScopedPointer shellSurface3(Test::createXdgShellStableSurface(surface3.data())); + AbstractClient *client3 = Test::renderAndWaitForShown(surface3.data(), QSize(100, 50), Qt::green); + QVERIFY(client3); + QCOMPARE(client3->pos(), client2->pos() + workspace()->cascadeOffset(client3)); + QCOMPARE(client3->size(), QSize(100, 50)); + + shellSurface3.reset(); + QVERIFY(Test::waitForWindowDestroyed(client3)); + shellSurface2.reset(); + QVERIFY(Test::waitForWindowDestroyed(client2)); + shellSurface1.reset(); + QVERIFY(Test::waitForWindowDestroyed(client1)); +} + +void TestPlacement::testPlaceRandom() +{ + // This test verifies that Random placement policy works. + + KConfigGroup group = kwinApp()->config()->group("Windows"); + group.writeEntry("Placement", Placement::policyToString(Placement::Random)); + group.sync(); + workspace()->slotReconfigure(); + + QScopedPointer surface1(Test::createSurface()); + QScopedPointer shellSurface1(Test::createXdgShellStableSurface(surface1.data())); + AbstractClient *client1 = Test::renderAndWaitForShown(surface1.data(), QSize(100, 50), Qt::red); + QVERIFY(client1); + QCOMPARE(client1->size(), QSize(100, 50)); + + QScopedPointer surface2(Test::createSurface()); + QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data())); + AbstractClient *client2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 50), Qt::blue); + QVERIFY(client2); + QVERIFY(client2->pos() != client1->pos()); + QCOMPARE(client2->size(), QSize(100, 50)); + + QScopedPointer surface3(Test::createSurface()); + QScopedPointer shellSurface3(Test::createXdgShellStableSurface(surface3.data())); + AbstractClient *client3 = Test::renderAndWaitForShown(surface3.data(), QSize(100, 50), Qt::green); + QVERIFY(client3); + QVERIFY(client3->pos() != client1->pos()); + QVERIFY(client3->pos() != client2->pos()); + QCOMPARE(client3->size(), QSize(100, 50)); + + shellSurface3.reset(); + QVERIFY(Test::waitForWindowDestroyed(client3)); + shellSurface2.reset(); + QVERIFY(Test::waitForWindowDestroyed(client2)); + shellSurface1.reset(); + QVERIFY(Test::waitForWindowDestroyed(client1)); +} + +WAYLANDTEST_MAIN(TestPlacement) +#include "placement_test.moc" diff --git a/autotests/integration/plasma_surface_test.cpp b/autotests/integration/plasma_surface_test.cpp new file mode 100644 index 0000000..6650227 --- /dev/null +++ b/autotests/integration/plasma_surface_test.cpp @@ -0,0 +1,413 @@ +/* + 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 "abstract_client.h" +#include "platform.h" +#include "cursor.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include +#include +#include +#include +#include +#include +#include + +using namespace KWin; +using namespace KWayland::Client; + +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 testDesktopIsOpaque(); + void testPanelWindowsCanCover_data(); + void testPanelWindowsCanCover(); + void testOSDPlacement(); + void testOSDPlacementManualPosition(); + void testPanelTypeHasStrut_data(); + void testPanelTypeHasStrut(); + void testPanelActivate_data(); + void testPanelActivate(); + +private: + KWayland::Client::Compositor *m_compositor = nullptr; + PlasmaShell *m_plasmaShell = nullptr; +}; + +void PlasmaSurfaceTest::initTestCase() +{ + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); +} + +void PlasmaSurfaceTest::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::PlasmaShell)); + m_compositor = Test::waylandCompositor(); + m_plasmaShell = Test::waylandPlasmaShell(); + + KWin::Cursors::self()->mouse()->setPos(640, 512); +} + +void PlasmaSurfaceTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void PlasmaSurfaceTest::testRoleOnAllDesktops_data() +{ + QTest::addColumn("role"); + QTest::addColumn("expectedOnAllDesktops"); + + QTest::newRow("Desktop") << PlasmaShellSurface::Role::Desktop << true; + QTest::newRow("Panel") << PlasmaShellSurface::Role::Panel << true; + QTest::newRow("OSD") << PlasmaShellSurface::Role::OnScreenDisplay << true; + QTest::newRow("Normal") << PlasmaShellSurface::Role::Normal << false; + QTest::newRow("Notification") << PlasmaShellSurface::Role::Notification << true; + QTest::newRow("ToolTip") << PlasmaShellSurface::Role::ToolTip << true; + QTest::newRow("CriticalNotification") << PlasmaShellSurface::Role::CriticalNotification << true; +} + +void PlasmaSurfaceTest::testRoleOnAllDesktops() +{ + // this test verifies that a XdgShellClient is set on all desktops when the role changes + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + QScopedPointer plasmaSurface(m_plasmaShell->createSurface(surface.data())); + QVERIFY(!plasmaSurface.isNull()); + + // now render to map the window + AbstractClient *c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(workspace()->activeClient(), c); + + // currently the role is not yet set, so the window should not be on all desktops + QCOMPARE(c->isOnAllDesktops(), false); + + // now let's try to change that + QSignalSpy onAllDesktopsSpy(c, &AbstractClient::desktopChanged); + QVERIFY(onAllDesktopsSpy.isValid()); + QFETCH(PlasmaShellSurface::Role, role); + plasmaSurface->setRole(role); + QFETCH(bool, expectedOnAllDesktops); + QCOMPARE(onAllDesktopsSpy.wait(), expectedOnAllDesktops); + QCOMPARE(c->isOnAllDesktops(), expectedOnAllDesktops); + + // let's create a second window where we init a little bit different + // first creating the PlasmaSurface then the Shell Surface + QScopedPointer surface2(Test::createSurface()); + QVERIFY(!surface2.isNull()); + QScopedPointer plasmaSurface2(m_plasmaShell->createSurface(surface2.data())); + QVERIFY(!plasmaSurface2.isNull()); + plasmaSurface2->setRole(role); + QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data())); + QVERIFY(!shellSurface2.isNull()); + auto c2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 50), Qt::blue); + QVERIFY(c2); + QVERIFY(c != c2); + + QCOMPARE(c2->isOnAllDesktops(), expectedOnAllDesktops); +} + +void PlasmaSurfaceTest::testAcceptsFocus_data() +{ + QTest::addColumn("role"); + QTest::addColumn("wantsInput"); + QTest::addColumn("active"); + + QTest::newRow("Desktop") << PlasmaShellSurface::Role::Desktop << true << true; + QTest::newRow("Panel") << PlasmaShellSurface::Role::Panel << true << false; + QTest::newRow("OSD") << PlasmaShellSurface::Role::OnScreenDisplay << false << false; + QTest::newRow("Normal") << PlasmaShellSurface::Role::Normal << true << true; + QTest::newRow("Notification") << PlasmaShellSurface::Role::Notification << false << false; + QTest::newRow("ToolTip") << PlasmaShellSurface::Role::ToolTip << false << false; + QTest::newRow("CriticalNotification") << PlasmaShellSurface::Role::CriticalNotification << false << false; +} + +void PlasmaSurfaceTest::testAcceptsFocus() +{ + // this test verifies that some surface roles don't get focus + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + QScopedPointer plasmaSurface(m_plasmaShell->createSurface(surface.data())); + QVERIFY(!plasmaSurface.isNull()); + QFETCH(PlasmaShellSurface::Role, role); + plasmaSurface->setRole(role); + + // now render to map the window + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + + QVERIFY(c); + QTEST(c->wantsInput(), "wantsInput"); + QTEST(c->isActive(), "active"); +} + +void PlasmaSurfaceTest::testDesktopIsOpaque() +{ + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + QScopedPointer plasmaSurface(m_plasmaShell->createSurface(surface.data())); + QVERIFY(!plasmaSurface.isNull()); + plasmaSurface->setRole(PlasmaShellSurface::Role::Desktop); + + // now render to map the window + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + + QVERIFY(c); + QCOMPARE(c->windowType(), NET::Desktop); + QVERIFY(c->isDesktop()); + + QVERIFY(!c->hasAlpha()); + QCOMPARE(c->depth(), 24); +} + +void PlasmaSurfaceTest::testOSDPlacement() +{ + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + QScopedPointer plasmaSurface(m_plasmaShell->createSurface(surface.data())); + QVERIFY(!plasmaSurface.isNull()); + plasmaSurface->setRole(PlasmaShellSurface::Role::OnScreenDisplay); + + // now render and map the window + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + + QVERIFY(c); + QCOMPARE(c->windowType(), NET::OnScreenDisplay); + QVERIFY(c->isOnScreenDisplay()); + QCOMPARE(c->frameGeometry(), QRect(1280 / 2 - 100 / 2, 2 * 1024 / 3 - 50 / 2, 100, 50)); + + // change the screen size + QSignalSpy screensChangedSpy(screens(), &Screens::changed); + QVERIFY(screensChangedSpy.isValid()); + const QVector geometries{QRect(0, 0, 1280, 1024), QRect(1280, 0, 1280, 1024)}; + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", + Qt::DirectConnection, + Q_ARG(int, 2), + Q_ARG(QVector, geometries)); + QVERIFY(screensChangedSpy.wait()); + QCOMPARE(screensChangedSpy.count(), 1); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), geometries.at(0)); + QCOMPARE(screens()->geometry(1), geometries.at(1)); + + QCOMPARE(c->frameGeometry(), QRect(1280 / 2 - 100 / 2, 2 * 1024 / 3 - 50 / 2, 100, 50)); + + // change size of window + QSignalSpy frameGeometryChangedSpy(c, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); + Test::render(surface.data(), QSize(200, 100), Qt::red); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(c->frameGeometry(), QRect(1280 / 2 - 200 / 2, 2 * 1024 / 3 - 100 / 2, 200, 100)); +} + +void PlasmaSurfaceTest::testOSDPlacementManualPosition() +{ + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + QScopedPointer plasmaSurface(m_plasmaShell->createSurface(surface.data())); + QVERIFY(!plasmaSurface.isNull()); + plasmaSurface->setRole(PlasmaShellSurface::Role::OnScreenDisplay); + + plasmaSurface->setPosition(QPoint(50, 70)); + + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + + // now render and map the window + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + + QVERIFY(c); + QVERIFY(c->isInitialPositionSet()); + QCOMPARE(c->windowType(), NET::OnScreenDisplay); + QVERIFY(c->isOnScreenDisplay()); + QCOMPARE(c->frameGeometry(), QRect(50, 70, 100, 50)); +} + + +void PlasmaSurfaceTest::testPanelTypeHasStrut_data() +{ + QTest::addColumn("panelBehavior"); + QTest::addColumn("expectedStrut"); + QTest::addColumn("expectedMaxArea"); + QTest::addColumn("expectedLayer"); + + QTest::newRow("always visible - xdgWmBase") << PlasmaShellSurface::PanelBehavior::AlwaysVisible << true << QRect(0, 50, 1280, 974) << KWin::DockLayer; + QTest::newRow("autohide - xdgWmBase") << PlasmaShellSurface::PanelBehavior::AutoHide << false << QRect(0, 0, 1280, 1024) << KWin::AboveLayer; + QTest::newRow("windows can cover - xdgWmBase") << PlasmaShellSurface::PanelBehavior::WindowsCanCover << false << QRect(0, 0, 1280, 1024) << KWin::NormalLayer; + QTest::newRow("windows go below - xdgWmBase") << PlasmaShellSurface::PanelBehavior::WindowsGoBelow << false << QRect(0, 0, 1280, 1024) << KWin::AboveLayer; +} + +void PlasmaSurfaceTest::testPanelTypeHasStrut() +{ + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + QScopedPointer plasmaSurface(m_plasmaShell->createSurface(surface.data())); + QVERIFY(!plasmaSurface.isNull()); + plasmaSurface->setRole(PlasmaShellSurface::Role::Panel); + plasmaSurface->setPosition(QPoint(0, 0)); + QFETCH(PlasmaShellSurface::PanelBehavior, panelBehavior); + plasmaSurface->setPanelBehavior(panelBehavior); + + // now render and map the window + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + + QVERIFY(c); + QCOMPARE(c->windowType(), NET::Dock); + QVERIFY(c->isDock()); + QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); + QTEST(c->hasStrut(), "expectedStrut"); + QTEST(workspace()->clientArea(MaximizeArea, 0, 0), "expectedMaxArea"); + QTEST(c->layer(), "expectedLayer"); +} + +void PlasmaSurfaceTest::testPanelWindowsCanCover_data() +{ + QTest::addColumn("panelGeometry"); + QTest::addColumn("windowGeometry"); + QTest::addColumn("triggerPoint"); + + QTest::newRow("top-full-edge") << QRect(0, 0, 1280, 30) << QRect(0, 0, 200, 300) << QPoint(100, 0); + QTest::newRow("top-left-edge") << QRect(0, 0, 1000, 30) << QRect(0, 0, 200, 300) << QPoint(100, 0); + QTest::newRow("top-right-edge") << QRect(280, 0, 1000, 30) << QRect(1000, 0, 200, 300) << QPoint(1000, 0); + QTest::newRow("bottom-full-edge") << QRect(0, 994, 1280, 30) << QRect(0, 724, 200, 300) << QPoint(100, 1023); + QTest::newRow("bottom-left-edge") << QRect(0, 994, 1000, 30) << QRect(0, 724, 200, 300) << QPoint(100, 1023); + QTest::newRow("bottom-right-edge") << QRect(280, 994, 1000, 30) << QRect(1000, 724, 200, 300) << QPoint(1000, 1023); + QTest::newRow("left-full-edge") << QRect(0, 0, 30, 1024) << QRect(0, 0, 200, 300) << QPoint(0, 100); + QTest::newRow("left-top-edge") << QRect(0, 0, 30, 800) << QRect(0, 0, 200, 300) << QPoint(0, 100); + QTest::newRow("left-bottom-edge") << QRect(0, 200, 30, 824) << QRect(0, 0, 200, 300) << QPoint(0, 250); + QTest::newRow("right-full-edge") << QRect(1250, 0, 30, 1024) << QRect(1080, 0, 200, 300) << QPoint(1279, 100); + QTest::newRow("right-top-edge") << QRect(1250, 0, 30, 800) << QRect(1080, 0, 200, 300) << QPoint(1279, 100); + QTest::newRow("right-bottom-edge") << QRect(1250, 200, 30, 824) << QRect(1080, 0, 200, 300) << QPoint(1279, 250); +} + +void PlasmaSurfaceTest::testPanelWindowsCanCover() +{ + // this test verifies the behavior of a panel with windows can cover + // triggering the screen edge should raise the panel. + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + QScopedPointer plasmaSurface(m_plasmaShell->createSurface(surface.data())); + QVERIFY(!plasmaSurface.isNull()); + plasmaSurface->setRole(PlasmaShellSurface::Role::Panel); + QFETCH(QRect, panelGeometry); + plasmaSurface->setPosition(panelGeometry.topLeft()); + plasmaSurface->setPanelBehavior(PlasmaShellSurface::PanelBehavior::WindowsCanCover); + + // now render and map the window + auto panel = Test::renderAndWaitForShown(surface.data(), panelGeometry.size(), Qt::blue); + + QVERIFY(panel); + QCOMPARE(panel->windowType(), NET::Dock); + QVERIFY(panel->isDock()); + QCOMPARE(panel->frameGeometry(), panelGeometry); + QCOMPARE(panel->hasStrut(), false); + QCOMPARE(workspace()->clientArea(MaximizeArea, 0, 0), QRect(0, 0, 1280, 1024)); + QCOMPARE(panel->layer(), KWin::NormalLayer); + + // create a Window + QScopedPointer surface2(Test::createSurface()); + QVERIFY(!surface2.isNull()); + QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data())); + QVERIFY(!shellSurface2.isNull()); + + QFETCH(QRect, windowGeometry); + auto c = Test::renderAndWaitForShown(surface2.data(), windowGeometry.size(), Qt::red); + + QVERIFY(c); + QCOMPARE(c->windowType(), NET::Normal); + QVERIFY(c->isActive()); + QCOMPARE(c->layer(), KWin::NormalLayer); + c->move(windowGeometry.topLeft()); + QCOMPARE(c->frameGeometry(), windowGeometry); + + auto stackingOrder = workspace()->stackingOrder(); + QCOMPARE(stackingOrder.count(), 2); + QCOMPARE(stackingOrder.first(), panel); + QCOMPARE(stackingOrder.last(), c); + + QSignalSpy stackingOrderChangedSpy(workspace(), &Workspace::stackingOrderChanged); + QVERIFY(stackingOrderChangedSpy.isValid()); + // trigger screenedge + QFETCH(QPoint, triggerPoint); + KWin::Cursors::self()->mouse()->setPos(triggerPoint); + QVERIFY(stackingOrderChangedSpy.wait()); + QCOMPARE(stackingOrderChangedSpy.count(), 1); + stackingOrder = workspace()->stackingOrder(); + QCOMPARE(stackingOrder.count(), 2); + QCOMPARE(stackingOrder.first(), c); + QCOMPARE(stackingOrder.last(), panel); +} + +void PlasmaSurfaceTest::testPanelActivate_data() +{ + QTest::addColumn("wantsFocus"); + QTest::addColumn("active"); + + QTest::newRow("no focus") << false << false; + QTest::newRow("focus") << true << true; +} + +void PlasmaSurfaceTest::testPanelActivate() +{ + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + QScopedPointer plasmaSurface(m_plasmaShell->createSurface(surface.data())); + QVERIFY(!plasmaSurface.isNull()); + plasmaSurface->setRole(PlasmaShellSurface::Role::Panel); + QFETCH(bool, wantsFocus); + plasmaSurface->setPanelTakesFocus(wantsFocus); + + auto panel = Test::renderAndWaitForShown(surface.data(), QSize(100, 200), Qt::blue); + + QVERIFY(panel); + QCOMPARE(panel->windowType(), NET::Dock); + QVERIFY(panel->isDock()); + QFETCH(bool, active); + QCOMPARE(panel->dockWantsInput(), active); + QCOMPARE(panel->isActive(), active); +} + +WAYLANDTEST_MAIN(PlasmaSurfaceTest) +#include "plasma_surface_test.moc" diff --git a/autotests/integration/plasmawindow_test.cpp b/autotests/integration/plasmawindow_test.cpp new file mode 100644 index 0000000..5ae5f7e --- /dev/null +++ b/autotests/integration/plasmawindow_test.cpp @@ -0,0 +1,319 @@ +/* + 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 "platform.h" +#include "x11client.h" +#include "cursor.h" +#include "screenedge.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include + +#include +#include +#include +#include +//screenlocker +#include + +#include +#include + +#include +#include + +using namespace KWayland::Client; + +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: + PlasmaWindowManagement *m_windowManagement = nullptr; + KWayland::Client::Compositor *m_compositor = nullptr; +}; + +void PlasmaWindowTest::initTestCase() +{ + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + setenv("QT_QPA_PLATFORM", "wayland", true); + setenv("QMLSCENE_DEVICE", "softwarecontext", true); + waylandServer()->initWorkspace(); +} + +void PlasmaWindowTest::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::WindowManagement)); + m_windowManagement = Test::waylandWindowManagement(); + m_compositor = Test::waylandCompositor(); + + screens()->setCurrent(0); + Cursors::self()->mouse()->setPos(QPoint(640, 512)); +} + +void PlasmaWindowTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void PlasmaWindowTest::testCreateDestroyX11PlasmaWindow() +{ + // this test verifies that a PlasmaWindow gets unmapped on Client side when an X11 client is destroyed + QSignalSpy plasmaWindowCreatedSpy(m_windowManagement, &PlasmaWindowManagement::windowCreated); + QVERIFY(plasmaWindowCreatedSpy.isValid()); + + // create an xcb window + struct XcbConnectionDeleter + { + static inline void cleanup(xcb_connection_t *pointer) + { + xcb_disconnect(pointer); + } + }; + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + const QRect windowGeometry(0, 0, 100, 200); + xcb_window_t w = xcb_generate_id(c.data()); + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, 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; + memset(&hints, 0, sizeof(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.data(), w, &hints); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + // we should get a client for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->window(), w); + QVERIFY(client->isDecorated()); + QVERIFY(client->isActive()); + // verify that it gets the keyboard focus + if (!client->surface()) { + // we don't have a surface yet, so focused keyboard surface if set is not ours + QVERIFY(!waylandServer()->seat()->focusedKeyboardSurface()); + QSignalSpy surfaceChangedSpy(client, &Toplevel::surfaceChanged); + QVERIFY(surfaceChangedSpy.isValid()); + QVERIFY(surfaceChangedSpy.wait()); + } + QVERIFY(client->surface()); + QCOMPARE(waylandServer()->seat()->focusedKeyboardSurface(), client->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(), client->frameGeometry()); + QSignalSpy geometryChangedSpy(pw, &PlasmaWindow::geometryChanged); + QVERIFY(geometryChangedSpy.isValid()); + + QSignalSpy unmappedSpy(m_windowManagement->windows().first(), &PlasmaWindow::unmapped); + QVERIFY(unmappedSpy.isValid()); + QSignalSpy destroyedSpy(m_windowManagement->windows().first(), &QObject::destroyed); + QVERIFY(destroyedSpy.isValid()); + + // now shade the window + const QRect geoBeforeShade = client->frameGeometry(); + QVERIFY(geoBeforeShade.isValid()); + QVERIFY(!geoBeforeShade.isEmpty()); + workspace()->slotWindowShade(); + QVERIFY(client->isShade()); + QVERIFY(client->frameGeometry() != geoBeforeShade); + QVERIFY(geometryChangedSpy.wait()); + QCOMPARE(pw->geometry(), client->frameGeometry()); + // and unshade again + workspace()->slotWindowShade(); + QVERIFY(!client->isShade()); + QCOMPARE(client->frameGeometry(), geoBeforeShade); + QVERIFY(geometryChangedSpy.wait()); + QCOMPARE(pw->geometry(), geoBeforeShade); + + // and destroy the window again + xcb_unmap_window(c.data(), w); + xcb_flush(c.data()); + + QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + QVERIFY(windowClosedSpy.wait()); + xcb_destroy_window(c.data(), w); + c.reset(); + + QVERIFY(unmappedSpy.wait()); + QCOMPARE(unmappedSpy.count(), 1); + + QVERIFY(destroyedSpy.wait()); +} + +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) +{ + Q_UNUSED(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 to the client + QSignalSpy plasmaWindowCreatedSpy(m_windowManagement, &PlasmaWindowManagement::windowCreated); + QVERIFY(plasmaWindowCreatedSpy.isValid()); + HelperWindow win; + win.setGeometry(0, 0, 100, 100); + win.show(); + + QVERIFY(!plasmaWindowCreatedSpy.wait()); +} + +void PlasmaWindowTest::testPopupWindowNoPlasmaWindow() +{ + // this test verifies that for a popup window no PlasmaWindow is sent to the client + QSignalSpy plasmaWindowCreatedSpy(m_windowManagement, &PlasmaWindowManagement::windowCreated); + QVERIFY(plasmaWindowCreatedSpy.isValid()); + + // first create the parent window + QScopedPointer parentSurface(Test::createSurface()); + QScopedPointer parentShellSurface(Test::createXdgShellStableSurface(parentSurface.data())); + AbstractClient *parentClient = Test::renderAndWaitForShown(parentSurface.data(), QSize(100, 50), Qt::blue); + QVERIFY(parentClient); + QVERIFY(plasmaWindowCreatedSpy.wait()); + QCOMPARE(plasmaWindowCreatedSpy.count(), 1); + + // now let's create a popup window for it + XdgPositioner positioner(QSize(10, 10), QRect(0, 0, 10, 10)); + positioner.setAnchorEdge(Qt::BottomEdge | Qt::RightEdge); + positioner.setGravity(Qt::BottomEdge | Qt::RightEdge); + QScopedPointer popupSurface(Test::createSurface()); + QScopedPointer popupShellSurface(Test::createXdgShellStablePopup(popupSurface.data(), parentShellSurface.data(), positioner)); + AbstractClient *popupClient = Test::renderAndWaitForShown(popupSurface.data(), positioner.initialSize(), Qt::blue); + QVERIFY(popupClient); + QVERIFY(!plasmaWindowCreatedSpy.wait(100)); + QCOMPARE(plasmaWindowCreatedSpy.count(), 1); + + // let's destroy the windows + popupShellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(popupClient)); + parentShellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(parentClient)); +} + +void PlasmaWindowTest::testLockScreenNoPlasmaWindow() +{ + // this test verifies that lock screen windows are not exposed to PlasmaWindow + QSignalSpy plasmaWindowCreatedSpy(m_windowManagement, &PlasmaWindowManagement::windowCreated); + QVERIFY(plasmaWindowCreatedSpy.isValid()); + + // this time we use a QSignalSpy on XdgShellClient as it'a a little bit more complex setup + QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(clientAddedSpy.isValid()); + // lock + ScreenLocker::KSldApp::self()->lock(ScreenLocker::EstablishLock::Immediate); + QVERIFY(clientAddedSpy.wait()); + QVERIFY(clientAddedSpy.first().first().value()->isLockScreen()); + // should not be sent to the client + QVERIFY(plasmaWindowCreatedSpy.isEmpty()); + QVERIFY(!plasmaWindowCreatedSpy.wait()); + + // fake unlock + QSignalSpy lockStateChangedSpy(ScreenLocker::KSldApp::self(), &ScreenLocker::KSldApp::lockStateChanged); + QVERIFY(lockStateChangedSpy.isValid()); + 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()); +} + +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, &PlasmaWindowManagement::windowCreated); + QVERIFY(plasmaWindowCreatedSpy.isValid()); + + // first create the parent window + QScopedPointer parentSurface(Test::createSurface()); + QScopedPointer parentShellSurface(Test::createXdgShellStableSurface(parentSurface.data())); + // map that window + Test::render(parentSurface.data(), 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); + QVERIFY(destroyedSpy.isValid()); + + // now destroy without an unmap + parentShellSurface.reset(); + parentSurface.reset(); + QVERIFY(destroyedSpy.wait()); +} + +} + +WAYLANDTEST_MAIN(KWin::PlasmaWindowTest) +#include "plasmawindow_test.moc" diff --git a/autotests/integration/platformcursor.cpp b/autotests/integration/platformcursor.cpp new file mode 100644 index 0000000..fe6f30a --- /dev/null +++ b/autotests/integration/platformcursor.cpp @@ -0,0 +1,60 @@ +/* + 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 "platform.h" +#include "wayland_server.h" + +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() +{ + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + kwinApp()->start(); + QVERIFY(applicationStartedSpy.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(639, 511)); + QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(639, 511)); + + // 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/autotests/integration/pointer_constraints_test.cpp b/autotests/integration/pointer_constraints_test.cpp new file mode 100644 index 0000000..bf74998 --- /dev/null +++ b/autotests/integration/pointer_constraints_test.cpp @@ -0,0 +1,372 @@ +/* + 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 "abstract_client.h" +#include "cursor.h" +#include "keyboard_input.h" +#include "platform.h" +#include "pointer_input.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +using namespace KWin; +using namespace KWayland::Client; + +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(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + // set custom config which disables the OnScreenNotification + KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup group = config->group("OnScreenNotification"); + group.writeEntry(QStringLiteral("QmlPath"), QString("/does/not/exist.qml")); + group.sync(); + + kwinApp()->setConfig(config); + + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + waylandServer()->initWorkspace(); +} + +void TestPointerConstraints::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::PointerConstraints)); + QVERIFY(Test::waitForWaylandPointer()); + + screens()->setCurrent(0); + KWin::Cursors::self()->mouse()->setPos(QPoint(1280, 512)); +} + +void TestPointerConstraints::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void TestPointerConstraints::testConfinedPointer_data() +{ + QTest::addColumn("positionFunction"); + QTest::addColumn("xOffset"); + QTest::addColumn("yOffset"); + PointerFunc bottomLeft = &QRect::bottomLeft; + PointerFunc bottomRight = &QRect::bottomRight; + PointerFunc topRight = &QRect::topRight; + PointerFunc topLeft = &QRect::topLeft; + + 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 + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QScopedPointer pointer(Test::waylandSeat()->createPointer()); + QScopedPointer confinedPointer(Test::waylandPointerConstraints()->confinePointer(surface.data(), pointer.data(), nullptr, PointerConstraints::LifeTime::OneShot)); + QSignalSpy confinedSpy(confinedPointer.data(), &ConfinedPointer::confined); + QVERIFY(confinedSpy.isValid()); + QSignalSpy unconfinedSpy(confinedPointer.data(), &ConfinedPointer::unconfined); + QVERIFY(unconfinedSpy.isValid()); + + // now map the window + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 100), Qt::blue); + QVERIFY(c); + if (c->pos() == QPoint(0, 0)) { + c->move(QPoint(1, 1)); + } + QVERIFY(!c->frameGeometry().contains(KWin::Cursors::self()->mouse()->pos())); + + // now let's confine + QCOMPARE(input()->pointer()->isConstrained(), false); + KWin::Cursors::self()->mouse()->setPos(c->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); + QVERIFY(pointerPositionChangedSpy.isValid()); + KWin::Cursors::self()->mouse()->setPos(QPoint(1280, 512)); + QVERIFY(pointerPositionChangedSpy.isEmpty()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), c->frameGeometry().center()); + + // TODO: test relative motion + QFETCH(PointerFunc, positionFunction); + const QPoint position = positionFunction(c->frameGeometry()); + KWin::Cursors::self()->mouse()->setPos(position); + QCOMPARE(pointerPositionChangedSpy.count(), 1); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), position); + // moving one to right should not be possible + QFETCH(int, xOffset); + KWin::Cursors::self()->mouse()->setPos(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::Cursors::self()->mouse()->setPos(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("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; + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); + kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); + QVERIFY(!c->isMove()); + kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); + + // set the opacity to 0.5 + c->setOpacity(0.5); + QCOMPARE(c->opacity(), 0.5); + + // pointer is confined so shortcut should not work + kwinApp()->platform()->pointerAxisVertical(-5, timestamp++); + QCOMPARE(c->opacity(), 0.5); + kwinApp()->platform()->pointerAxisVertical(5, timestamp++); + QCOMPARE(c->opacity(), 0.5); + + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); + + // deactivate the client, this should unconfine + workspace()->activateClient(nullptr); + QVERIFY(unconfinedSpy.wait()); + QCOMPARE(input()->pointer()->isConstrained(), false); + + // reconfine pointer (this time with persistent life time) + confinedPointer.reset(Test::waylandPointerConstraints()->confinePointer(surface.data(), pointer.data(), nullptr, PointerConstraints::LifeTime::Persistent)); + QSignalSpy confinedSpy2(confinedPointer.data(), &ConfinedPointer::confined); + QVERIFY(confinedSpy2.isValid()); + QSignalSpy unconfinedSpy2(confinedPointer.data(), &ConfinedPointer::unconfined); + QVERIFY(unconfinedSpy2.isValid()); + + // activate it again, this confines again + workspace()->activateClient(static_cast(input()->pointer()->focus())); + QVERIFY(confinedSpy2.wait()); + QCOMPARE(input()->pointer()->isConstrained(), true); + + // deactivate the client one more time with the persistent life time constraint, this should unconfine + workspace()->activateClient(nullptr); + QVERIFY(unconfinedSpy2.wait()); + QCOMPARE(input()->pointer()->isConstrained(), false); + // activate it again, this confines again + workspace()->activateClient(static_cast(input()->pointer()->focus())); + QVERIFY(confinedSpy2.wait()); + QCOMPARE(input()->pointer()->isConstrained(), true); + + // create a second window and move it above our constrained window + QScopedPointer surface2(Test::createSurface()); + QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data())); + auto c2 = Test::renderAndWaitForShown(surface2.data(), 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(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(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(), &KWaylandServer::SurfaceInterface::pointerConstraintsChanged); + QVERIFY(constraintsChangedSpy.isValid()); + QVERIFY(constraintsChangedSpy.wait()); + + // should be unconfined + QCOMPARE(input()->pointer()->isConstrained(), false); + + // confine again + confinedPointer.reset(Test::waylandPointerConstraints()->confinePointer(surface.data(), pointer.data(), nullptr, PointerConstraints::LifeTime::Persistent)); + QSignalSpy confinedSpy3(confinedPointer.data(), &ConfinedPointer::confined); + QVERIFY(confinedSpy3.isValid()); + QVERIFY(confinedSpy3.wait()); + QCOMPARE(input()->pointer()->isConstrained(), true); + + // and now unmap + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(c)); + 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 + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QScopedPointer pointer(Test::waylandSeat()->createPointer()); + QScopedPointer lockedPointer(Test::waylandPointerConstraints()->lockPointer(surface.data(), pointer.data(), nullptr, PointerConstraints::LifeTime::OneShot)); + QSignalSpy lockedSpy(lockedPointer.data(), &LockedPointer::locked); + QVERIFY(lockedSpy.isValid()); + QSignalSpy unlockedSpy(lockedPointer.data(), &LockedPointer::unlocked); + QVERIFY(unlockedSpy.isValid()); + + // now map the window + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 100), Qt::blue); + QVERIFY(c); + QVERIFY(!c->frameGeometry().contains(KWin::Cursors::self()->mouse()->pos())); + + // now let's lock + QCOMPARE(input()->pointer()->isConstrained(), false); + KWin::Cursors::self()->mouse()->setPos(c->frameGeometry().center()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), c->frameGeometry().center()); + QCOMPARE(input()->pointer()->isConstrained(), true); + QVERIFY(lockedSpy.wait()); + + // try to move the pointer + // TODO: add relative pointer + KWin::Cursors::self()->mouse()->setPos(c->frameGeometry().center() + QPoint(1, 1)); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), c->frameGeometry().center()); + + // deactivate the client, this should unlock + workspace()->activateClient(nullptr); + QCOMPARE(input()->pointer()->isConstrained(), false); + QVERIFY(unlockedSpy.wait()); + + // moving cursor should be allowed again + KWin::Cursors::self()->mouse()->setPos(c->frameGeometry().center() + QPoint(1, 1)); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), c->frameGeometry().center() + QPoint(1, 1)); + + lockedPointer.reset(Test::waylandPointerConstraints()->lockPointer(surface.data(), pointer.data(), nullptr, PointerConstraints::LifeTime::Persistent)); + QSignalSpy lockedSpy2(lockedPointer.data(), &LockedPointer::locked); + QVERIFY(lockedSpy2.isValid()); + + // activate the client again, this should lock again + workspace()->activateClient(static_cast(input()->pointer()->focus())); + QVERIFY(lockedSpy2.wait()); + QCOMPARE(input()->pointer()->isConstrained(), true); + + // try to move the pointer + QCOMPARE(input()->pointer()->isConstrained(), true); + KWin::Cursors::self()->mouse()->setPos(c->frameGeometry().center()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), c->frameGeometry().center() + QPoint(1, 1)); + + // delete pointer lock + lockedPointer.reset(nullptr); + Test::flushWaylandConnection(); + + QSignalSpy constraintsChangedSpy(input()->pointer()->focus()->surface(), &KWaylandServer::SurfaceInterface::pointerConstraintsChanged); + QVERIFY(constraintsChangedSpy.isValid()); + QVERIFY(constraintsChangedSpy.wait()); + + // moving cursor should be allowed again + QCOMPARE(input()->pointer()->isConstrained(), false); + KWin::Cursors::self()->mouse()->setPos(c->frameGeometry().center()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), c->frameGeometry().center()); +} + +void TestPointerConstraints::testCloseWindowWithLockedPointer() +{ + // test case which verifies that the pointer gets unlocked when the window for it gets closed + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QScopedPointer pointer(Test::waylandSeat()->createPointer()); + QScopedPointer lockedPointer(Test::waylandPointerConstraints()->lockPointer(surface.data(), pointer.data(), nullptr, PointerConstraints::LifeTime::OneShot)); + QSignalSpy lockedSpy(lockedPointer.data(), &LockedPointer::locked); + QVERIFY(lockedSpy.isValid()); + QSignalSpy unlockedSpy(lockedPointer.data(), &LockedPointer::unlocked); + QVERIFY(unlockedSpy.isValid()); + + // now map the window + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 100), Qt::blue); + QVERIFY(c); + QVERIFY(!c->frameGeometry().contains(KWin::Cursors::self()->mouse()->pos())); + + // now let's lock + QCOMPARE(input()->pointer()->isConstrained(), false); + KWin::Cursors::self()->mouse()->setPos(c->frameGeometry().center()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), c->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/autotests/integration/pointer_input.cpp b/autotests/integration/pointer_input.cpp new file mode 100644 index 0000000..63f27ad --- /dev/null +++ b/autotests/integration/pointer_input.cpp @@ -0,0 +1,1625 @@ +/* + 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 "platform.h" +#include "abstract_client.h" +#include "cursor.h" +#include "deleted.h" +#include "effects.h" +#include "pointer_input.h" +#include "options.h" +#include "screenedge.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include "xcursortheme.h" +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +namespace KWin +{ + +static PlatformCursorImage loadReferenceThemeCursor_helper(const KXcursorTheme &theme, + const QByteArray &name) +{ + const QVector sprites = theme.shape(name); + if (sprites.isEmpty()) { + return PlatformCursorImage(); + } + + QImage cursorImage = sprites.first().data(); + cursorImage.setDevicePixelRatio(theme.devicePixelRatio()); + + QPoint cursorHotspot = sprites.first().hotspot(); + + return PlatformCursorImage(cursorImage, cursorHotspot); +} + +static PlatformCursorImage loadReferenceThemeCursor(const QByteArray &name) +{ + const Cursor *pointerCursor = Cursors::self()->mouse(); + + const KXcursorTheme theme = KXcursorTheme::fromTheme(pointerCursor->themeName(), + pointerCursor->themeSize(), + screens()->maxScale()); + if (theme.isEmpty()) { + return PlatformCursorImage(); + } + + PlatformCursorImage platformCursorImage = loadReferenceThemeCursor_helper(theme, name); + if (!platformCursorImage.isNull()) { + return platformCursorImage; + } + + const QVector alternativeNames = Cursor::cursorAlternativeNames(name); + for (const QByteArray &alternativeName : alternativeNames) { + platformCursorImage = loadReferenceThemeCursor_helper(theme, alternativeName); + if (!platformCursorImage.isNull()) { + break; + } + } + + return platformCursorImage; +} + +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 testWarpingDuringFilter(); + void testUpdateFocusAfterScreenChange(); + void testModifierClickUnrestrictedMove_data(); + void testModifierClickUnrestrictedMove(); + 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 testEffectOverrideCursorImage(); + void testPopup(); + void testDecoCancelsPopup(); + void testWindowUnderCursorWhileButtonPressed(); + void testConfineToScreenGeometry_data(); + void testConfineToScreenGeometry(); + void testResizeCursor_data(); + void testResizeCursor(); + void testMoveCursor(); + void testHideShowCursor(); + +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(); + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); + + if (!QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("icons/DMZ-White/index.theme")).isEmpty()) { + qputenv("XCURSOR_THEME", QByteArrayLiteral("DMZ-White")); + } else { + // might be vanilla-dmz (e.g. Arch, FreeBSD) + qputenv("XCURSOR_THEME", QByteArrayLiteral("Vanilla-DMZ")); + } + qputenv("XCURSOR_SIZE", QByteArrayLiteral("24")); + qputenv("XKB_DEFAULT_RULES", "evdev"); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + setenv("QT_QPA_PLATFORM", "wayland", true); + waylandServer()->initWorkspace(); +} + +void PointerInputTest::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::Decoration)); + QVERIFY(Test::waitForWaylandPointer()); + m_compositor = Test::waylandCompositor(); + m_seat = Test::waylandSeat(); + + screens()->setCurrent(0); + Cursors::self()->mouse()->setPos(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 + using namespace KWayland::Client; + // 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, &Pointer::entered); + QVERIFY(enteredSpy.isValid()); + QSignalSpy leftSpy(pointer, &Pointer::left); + QVERIFY(leftSpy.isValid()); + + // create a window + QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(clientAddedSpy.isValid()); + Surface *surface = Test::createSurface(m_compositor); + QVERIFY(surface); + XdgShellSurface *shellSurface = Test::createXdgShellStableSurface(surface, surface); + QVERIFY(shellSurface); + render(surface); + QVERIFY(clientAddedSpy.wait()); + AbstractClient *window = workspace()->activeClient(); + QVERIFY(window); + + // currently there should not be a focused pointer surface + QVERIFY(!waylandServer()->seat()->focusedPointerSurface()); + QVERIFY(!pointer->enteredSurface()); + + // enter + Cursors::self()->mouse()->setPos(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); + // also on the server + QCOMPARE(waylandServer()->seat()->focusedPointerSurface(), window->surface()); + + // and out again + Cursors::self()->mouse()->setPos(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 + using namespace KWayland::Client; + // create pointer and signal spy for enter and motion + auto pointer = m_seat->createPointer(m_seat); + QVERIFY(pointer); + QVERIFY(pointer->isValid()); + QSignalSpy enteredSpy(pointer, &Pointer::entered); + QVERIFY(enteredSpy.isValid()); + QSignalSpy movedSpy(pointer, &Pointer::motion); + QVERIFY(movedSpy.isValid()); + + // create a window + QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(clientAddedSpy.isValid()); + Surface *surface = Test::createSurface(m_compositor); + QVERIFY(surface); + XdgShellSurface *shellSurface = Test::createXdgShellStableSurface(surface, surface); + QVERIFY(shellSurface); + render(surface); + QVERIFY(clientAddedSpy.wait()); + AbstractClient *window = workspace()->activeClient(); + QVERIFY(window); + + // enter + kwinApp()->platform()->pointerMotion(QPointF(25, 25), 1); + QVERIFY(enteredSpy.wait()); + QCOMPARE(enteredSpy.first().at(1).toPointF(), QPointF(25, 25)); + + // now warp + Cursors::self()->mouse()->setPos(QPoint(26, 26)); + QVERIFY(movedSpy.wait()); + QCOMPARE(movedSpy.count(), 1); + QCOMPARE(movedSpy.last().first().toPointF(), QPointF(26, 26)); +} + +void PointerInputTest::testWarpingDuringFilter() +{ + // this test verifies that pointer motion is handled correctly if + // the pointer gets warped during processing of input events + using namespace KWayland::Client; + + // create pointer + auto pointer = m_seat->createPointer(m_seat); + QVERIFY(pointer); + QVERIFY(pointer->isValid()); + QSignalSpy movedSpy(pointer, &Pointer::motion); + QVERIFY(movedSpy.isValid()); + + // warp cursor into expected geometry + Cursors::self()->mouse()->setPos(10, 10); + + // create a window + QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(clientAddedSpy.isValid()); + Surface *surface = Test::createSurface(m_compositor); + QVERIFY(surface); + XdgShellSurface *shellSurface = Test::createXdgShellStableSurface(surface, surface); + QVERIFY(shellSurface); + render(surface); + QVERIFY(clientAddedSpy.wait()); + AbstractClient *window = workspace()->activeClient(); + QVERIFY(window); + + QCOMPARE(window->pos(), QPoint(0, 0)); + QVERIFY(window->frameGeometry().contains(Cursors::self()->mouse()->pos())); + + // is PresentWindows effect for top left screen edge loaded + QVERIFY(static_cast(effects)->isEffectLoaded("presentwindows")); + QVERIFY(movedSpy.isEmpty()); + quint32 timestamp = 0; + kwinApp()->platform()->pointerMotion(QPoint(0, 0), timestamp++); + // screen edges push back + QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(1, 1)); + QVERIFY(movedSpy.wait()); + QCOMPARE(movedSpy.count(), 2); + QCOMPARE(movedSpy.at(0).first().toPoint(), QPoint(0, 0)); + QCOMPARE(movedSpy.at(1).first().toPoint(), QPoint(1, 1)); +} + +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 + using namespace KWayland::Client; + // ensure cursor is on second screen + Cursors::self()->mouse()->setPos(1500, 300); + + // create pointer and signal spy for enter and motion + auto pointer = m_seat->createPointer(m_seat); + QVERIFY(pointer); + QVERIFY(pointer->isValid()); + QSignalSpy enteredSpy(pointer, &Pointer::entered); + QVERIFY(enteredSpy.isValid()); + + // create a window + QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(clientAddedSpy.isValid()); + Surface *surface = Test::createSurface(m_compositor); + QVERIFY(surface); + XdgShellSurface *shellSurface = Test::createXdgShellStableSurface(surface, surface); + QVERIFY(shellSurface); + render(surface, QSize(1280, 1024)); + QVERIFY(clientAddedSpy.wait()); + AbstractClient *window = workspace()->activeClient(); + QVERIFY(window); + QVERIFY(!window->frameGeometry().contains(Cursors::self()->mouse()->pos())); + + QSignalSpy screensChangedSpy(screens(), &Screens::changed); + QVERIFY(screensChangedSpy.isValid()); + // now let's remove the screen containing the cursor + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", + Qt::DirectConnection, + Q_ARG(int, 1), + Q_ARG(QVector, QVector{QRect(0, 0, 1280, 1024)})); + QVERIFY(screensChangedSpy.wait()); + QCOMPARE(screens()->count(), 1); + + // this should have warped the cursor + QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(639, 511)); + QVERIFY(window->frameGeometry().contains(Cursors::self()->mouse()->pos())); + + // and we should get an enter event + QTRY_COMPARE(enteredSpy.count(), 1); +} + +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 + using namespace KWayland::Client; + // create pointer and signal spy for button events + auto pointer = m_seat->createPointer(m_seat); + QVERIFY(pointer); + QVERIFY(pointer->isValid()); + QSignalSpy buttonSpy(pointer, &Pointer::buttonStateChanged); + QVERIFY(buttonSpy.isValid()); + + // first modify the config for this run + QFETCH(QString, modKey); + KConfigGroup group = kwinApp()->config()->group("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 clientAddedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(clientAddedSpy.isValid()); + Surface *surface = Test::createSurface(m_compositor); + QVERIFY(surface); + XdgShellSurface *shellSurface = Test::createXdgShellStableSurface(surface, surface); + QVERIFY(shellSurface); + render(surface); + QVERIFY(clientAddedSpy.wait()); + AbstractClient *window = workspace()->activeClient(); + QVERIFY(window); + + // move cursor on window + Cursors::self()->mouse()->setPos(window->frameGeometry().center()); + + // simulate modifier+click + quint32 timestamp = 1; + QFETCH(bool, capsLock); + if (capsLock) { + kwinApp()->platform()->keyboardKeyPressed(KEY_CAPSLOCK, timestamp++); + } + QFETCH(int, modifierKey); + QFETCH(int, mouseButton); + kwinApp()->platform()->keyboardKeyPressed(modifierKey, timestamp++); + QVERIFY(!window->isMove()); + kwinApp()->platform()->pointerButtonPressed(mouseButton, timestamp++); + QVERIFY(window->isMove()); + // release modifier should not change it + kwinApp()->platform()->keyboardKeyReleased(modifierKey, timestamp++); + QVERIFY(window->isMove()); + // but releasing the key should end move/resize + kwinApp()->platform()->pointerButtonReleased(mouseButton, timestamp++); + QVERIFY(!window->isMove()); + if (capsLock) { + kwinApp()->platform()->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(!buttonSpy.wait(100)); +} + +void PointerInputTest::testModifierClickUnrestrictedMoveGlobalShortcutsDisabled() +{ + // this test ensures that Alt+mouse button press triggers unrestricted move + using namespace KWayland::Client; + // create pointer and signal spy for button events + auto pointer = m_seat->createPointer(m_seat); + QVERIFY(pointer); + QVERIFY(pointer->isValid()); + QSignalSpy buttonSpy(pointer, &Pointer::buttonStateChanged); + QVERIFY(buttonSpy.isValid()); + + // first modify the config for this run + KConfigGroup group = kwinApp()->config()->group("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 clientAddedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(clientAddedSpy.isValid()); + Surface *surface = Test::createSurface(m_compositor); + QVERIFY(surface); + XdgShellSurface *shellSurface = Test::createXdgShellStableSurface(surface, surface); + QVERIFY(shellSurface); + render(surface); + QVERIFY(clientAddedSpy.wait()); + AbstractClient *window = workspace()->activeClient(); + QVERIFY(window); + + // disable global shortcuts + QVERIFY(!workspace()->globalShortcutsDisabled()); + workspace()->disableGlobalShortcutsForClient(true); + QVERIFY(workspace()->globalShortcutsDisabled()); + + // move cursor on window + Cursors::self()->mouse()->setPos(window->frameGeometry().center()); + + // simulate modifier+click + quint32 timestamp = 1; + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + QVERIFY(!window->isMove()); + kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); + QVERIFY(!window->isMove()); + // release modifier should not change it + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTMETA, timestamp++); + QVERIFY(!window->isMove()); + kwinApp()->platform()->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 + using namespace KWayland::Client; + // create pointer and signal spy for button events + auto pointer = m_seat->createPointer(m_seat); + QVERIFY(pointer); + QVERIFY(pointer->isValid()); + QSignalSpy axisSpy(pointer, &Pointer::axisChanged); + QVERIFY(axisSpy.isValid()); + + // first modify the config for this run + QFETCH(QString, modKey); + KConfigGroup group = kwinApp()->config()->group("MouseBindings"); + group.writeEntry("CommandAllKey", modKey); + group.writeEntry("CommandAllWheel", "change opacity"); + group.sync(); + workspace()->slotReconfigure(); + + // create a window + QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(clientAddedSpy.isValid()); + Surface *surface = Test::createSurface(m_compositor); + QVERIFY(surface); + XdgShellSurface *shellSurface = Test::createXdgShellStableSurface(surface, surface); + QVERIFY(shellSurface); + render(surface); + QVERIFY(clientAddedSpy.wait()); + AbstractClient *window = workspace()->activeClient(); + QVERIFY(window); + // set the opacity to 0.5 + window->setOpacity(0.5); + QCOMPARE(window->opacity(), 0.5); + + // move cursor on window + Cursors::self()->mouse()->setPos(window->frameGeometry().center()); + + // simulate modifier+wheel + quint32 timestamp = 1; + QFETCH(bool, capsLock); + if (capsLock) { + kwinApp()->platform()->keyboardKeyPressed(KEY_CAPSLOCK, timestamp++); + } + QFETCH(int, modifierKey); + kwinApp()->platform()->keyboardKeyPressed(modifierKey, timestamp++); + kwinApp()->platform()->pointerAxisVertical(-5, timestamp++); + QCOMPARE(window->opacity(), 0.6); + kwinApp()->platform()->pointerAxisVertical(5, timestamp++); + QCOMPARE(window->opacity(), 0.5); + kwinApp()->platform()->keyboardKeyReleased(modifierKey, timestamp++); + if (capsLock) { + kwinApp()->platform()->keyboardKeyReleased(KEY_CAPSLOCK, timestamp++); + } + + // axis should have been filtered out + QCOMPARE(axisSpy.count(), 0); + QVERIFY(!axisSpy.wait(100)); +} + +void PointerInputTest::testModifierScrollOpacityGlobalShortcutsDisabled() +{ + // this test verifies that mod+wheel performs a window operation and does not + // pass the wheel to the window + using namespace KWayland::Client; + // create pointer and signal spy for button events + auto pointer = m_seat->createPointer(m_seat); + QVERIFY(pointer); + QVERIFY(pointer->isValid()); + QSignalSpy axisSpy(pointer, &Pointer::axisChanged); + QVERIFY(axisSpy.isValid()); + + // first modify the config for this run + KConfigGroup group = kwinApp()->config()->group("MouseBindings"); + group.writeEntry("CommandAllKey", "Meta"); + group.writeEntry("CommandAllWheel", "change opacity"); + group.sync(); + workspace()->slotReconfigure(); + + // create a window + QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(clientAddedSpy.isValid()); + Surface *surface = Test::createSurface(m_compositor); + QVERIFY(surface); + XdgShellSurface *shellSurface = Test::createXdgShellStableSurface(surface, surface); + QVERIFY(shellSurface); + render(surface); + QVERIFY(clientAddedSpy.wait()); + AbstractClient *window = workspace()->activeClient(); + QVERIFY(window); + // set the opacity to 0.5 + window->setOpacity(0.5); + QCOMPARE(window->opacity(), 0.5); + + // move cursor on window + Cursors::self()->mouse()->setPos(window->frameGeometry().center()); + + // disable global shortcuts + QVERIFY(!workspace()->globalShortcutsDisabled()); + workspace()->disableGlobalShortcutsForClient(true); + QVERIFY(workspace()->globalShortcutsDisabled()); + + // simulate modifier+wheel + quint32 timestamp = 1; + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + kwinApp()->platform()->pointerAxisVertical(-5, timestamp++); + QCOMPARE(window->opacity(), 0.5); + kwinApp()->platform()->pointerAxisVertical(5, timestamp++); + QCOMPARE(window->opacity(), 0.5); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTMETA, timestamp++); + + workspace()->disableGlobalShortcutsForClient(false); +} + +void PointerInputTest::testScrollAction() +{ + // this test verifies that scroll on inactive window performs a mouse action + using namespace KWayland::Client; + auto pointer = m_seat->createPointer(m_seat); + QVERIFY(pointer); + QVERIFY(pointer->isValid()); + QSignalSpy axisSpy(pointer, &Pointer::axisChanged); + QVERIFY(axisSpy.isValid()); + + // first modify the config for this run + KConfigGroup group = kwinApp()->config()->group("MouseBindings"); + group.writeEntry("CommandWindowWheel", "activate and scroll"); + group.sync(); + workspace()->slotReconfigure(); + // create two windows + QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(clientAddedSpy.isValid()); + Surface *surface1 = Test::createSurface(m_compositor); + QVERIFY(surface1); + XdgShellSurface *shellSurface1 = Test::createXdgShellStableSurface(surface1, surface1); + QVERIFY(shellSurface1); + render(surface1); + QVERIFY(clientAddedSpy.wait()); + AbstractClient *window1 = workspace()->activeClient(); + QVERIFY(window1); + Surface *surface2 = Test::createSurface(m_compositor); + QVERIFY(surface2); + XdgShellSurface *shellSurface2 = Test::createXdgShellStableSurface(surface2, surface2); + QVERIFY(shellSurface2); + render(surface2); + QVERIFY(clientAddedSpy.wait()); + AbstractClient *window2 = workspace()->activeClient(); + QVERIFY(window2); + QVERIFY(window1 != window2); + + // move cursor to the inactive window + Cursors::self()->mouse()->setPos(window1->frameGeometry().center()); + + quint32 timestamp = 1; + QVERIFY(!window1->isActive()); + kwinApp()->platform()->pointerAxisVertical(5, timestamp++); + QVERIFY(window1->isActive()); + + // but also the wheel event should be passed to the window + QVERIFY(axisSpy.wait()); + + // we need to wait a little bit, otherwise the test crashes in effectshandler, needs fixing + QTest::qWait(100); +} + +void PointerInputTest::testFocusFollowsMouse() +{ + using namespace KWayland::Client; + // 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 + Cursors::self()->mouse()->setPos(900, 900); + + // first modify the config for this run + KConfigGroup group = kwinApp()->config()->group("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 clientAddedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(clientAddedSpy.isValid()); + Surface *surface1 = Test::createSurface(m_compositor); + QVERIFY(surface1); + XdgShellSurface *shellSurface1 = Test::createXdgShellStableSurface(surface1, surface1); + QVERIFY(shellSurface1); + render(surface1, QSize(800, 800)); + QVERIFY(clientAddedSpy.wait()); + AbstractClient *window1 = workspace()->activeClient(); + QVERIFY(window1); + Surface *surface2 = Test::createSurface(m_compositor); + QVERIFY(surface2); + XdgShellSurface *shellSurface2 = Test::createXdgShellStableSurface(surface2, surface2); + QVERIFY(shellSurface2); + render(surface2, QSize(800, 800)); + QVERIFY(clientAddedSpy.wait()); + AbstractClient *window2 = workspace()->activeClient(); + QVERIFY(window2); + QVERIFY(window1 != window2); + QCOMPARE(workspace()->topClientOnDesktop(1, -1), 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::clientActivated); + QVERIFY(activeWindowChangedSpy.isValid()); + QSignalSpy stackingOrderChangedSpy(workspace(), &Workspace::stackingOrderChanged); + QVERIFY(stackingOrderChangedSpy.isValid()); + + QVERIFY(!window1->isActive()); + QVERIFY(window2->isActive()); + + // move on top of first window + QVERIFY(window1->frameGeometry().contains(10, 10)); + QVERIFY(!window2->frameGeometry().contains(10, 10)); + Cursors::self()->mouse()->setPos(10, 10); + QVERIFY(stackingOrderChangedSpy.wait()); + QCOMPARE(stackingOrderChangedSpy.count(), 1); + QCOMPARE(workspace()->topClientOnDesktop(1, -1), window1); + QTRY_VERIFY(window1->isActive()); + + // move on second window, but move away before active window change delay hits + Cursors::self()->mouse()->setPos(810, 810); + QVERIFY(stackingOrderChangedSpy.wait()); + QCOMPARE(stackingOrderChangedSpy.count(), 2); + QCOMPARE(workspace()->topClientOnDesktop(1, -1), window2); + Cursors::self()->mouse()->setPos(10, 10); + QVERIFY(!activeWindowChangedSpy.wait(250)); + QVERIFY(window1->isActive()); + QCOMPARE(workspace()->topClientOnDesktop(1, -1), 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 + Cursors::self()->mouse()->setPos(810, 810); + Cursors::self()->mouse()->setPos(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 + using namespace KWayland::Client; + + // first modify the config for this run - disable FocusFollowsMouse + KConfigGroup group = kwinApp()->config()->group("Windows"); + group.writeEntry("FocusPolicy", "ClickToFocus"); + group.sync(); + group = kwinApp()->config()->group("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 clientAddedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(clientAddedSpy.isValid()); + Surface *surface1 = Test::createSurface(m_compositor); + QVERIFY(surface1); + XdgShellSurface *shellSurface1 = Test::createXdgShellStableSurface(surface1, surface1); + QVERIFY(shellSurface1); + render(surface1, QSize(800, 800)); + QVERIFY(clientAddedSpy.wait()); + AbstractClient *window1 = workspace()->activeClient(); + QVERIFY(window1); + Surface *surface2 = Test::createSurface(m_compositor); + QVERIFY(surface2); + XdgShellSurface *shellSurface2 = Test::createXdgShellStableSurface(surface2, surface2); + QVERIFY(shellSurface2); + render(surface2, QSize(800, 800)); + QVERIFY(clientAddedSpy.wait()); + AbstractClient *window2 = workspace()->activeClient(); + QVERIFY(window2); + QVERIFY(window1 != window2); + QCOMPARE(workspace()->topClientOnDesktop(1, -1), 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::clientActivated); + QVERIFY(activeWindowChangedSpy.isValid()); + QSignalSpy stackingOrderChangedSpy(workspace(), &Workspace::stackingOrderChanged); + QVERIFY(stackingOrderChangedSpy.isValid()); + + QVERIFY(!window1->isActive()); + QVERIFY(window2->isActive()); + + // move on top of first window + QVERIFY(window1->frameGeometry().contains(10, 10)); + QVERIFY(!window2->frameGeometry().contains(10, 10)); + Cursors::self()->mouse()->setPos(10, 10); + // no focus follows mouse + QVERIFY(!stackingOrderChangedSpy.wait(200)); + QVERIFY(stackingOrderChangedSpy.isEmpty()); + QVERIFY(activeWindowChangedSpy.isEmpty()); + QVERIFY(window2->isActive()); + // and click + quint32 timestamp = 1; + QFETCH(quint32, button); + kwinApp()->platform()->pointerButtonPressed(button, timestamp++); + // should raise window1 and activate it + QCOMPARE(stackingOrderChangedSpy.count(), 1); + QVERIFY(!activeWindowChangedSpy.isEmpty()); + QCOMPARE(workspace()->topClientOnDesktop(1, -1), window1); + QVERIFY(window1->isActive()); + QVERIFY(!window2->isActive()); + + // release again + kwinApp()->platform()->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 + using namespace KWayland::Client; + // 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, &Pointer::buttonStateChanged); + QVERIFY(buttonSpy.isValid()); + + // adjust config for this run + QFETCH(bool, clickRaise); + KConfigGroup group = kwinApp()->config()->group("Windows"); + group.writeEntry("ClickRaise", clickRaise); + group.sync(); + workspace()->slotReconfigure(); + QCOMPARE(options->isClickRaise(), clickRaise); + + // create two windows + QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(clientAddedSpy.isValid()); + Surface *surface1 = Test::createSurface(m_compositor); + QVERIFY(surface1); + XdgShellSurface *shellSurface1 = Test::createXdgShellStableSurface(surface1, surface1); + QVERIFY(shellSurface1); + render(surface1, QSize(800, 800)); + QVERIFY(clientAddedSpy.wait()); + AbstractClient *window1 = workspace()->activeClient(); + QVERIFY(window1); + QSignalSpy window1DestroyedSpy(window1, &QObject::destroyed); + QVERIFY(window1DestroyedSpy.isValid()); + Surface *surface2 = Test::createSurface(m_compositor); + QVERIFY(surface2); + XdgShellSurface *shellSurface2 = Test::createXdgShellStableSurface(surface2, surface2); + QVERIFY(shellSurface2); + render(surface2, QSize(800, 800)); + QVERIFY(clientAddedSpy.wait()); + AbstractClient *window2 = workspace()->activeClient(); + QVERIFY(window2); + QVERIFY(window1 != window2); + QSignalSpy window2DestroyedSpy(window2, &QObject::destroyed); + QVERIFY(window2DestroyedSpy.isValid()); + QCOMPARE(workspace()->topClientOnDesktop(1, -1), window2); + // geometry of the two windows should be overlapping + QVERIFY(window1->frameGeometry().intersects(window2->frameGeometry())); + // lower the currently active window + workspace()->lowerClient(window2); + QCOMPARE(workspace()->topClientOnDesktop(1, -1), window1); + + // signal spy for stacking order spy + QSignalSpy stackingOrderChangedSpy(workspace(), &Workspace::stackingOrderChanged); + QVERIFY(stackingOrderChangedSpy.isValid()); + + // move on top of second window + QVERIFY(!window1->frameGeometry().contains(900, 900)); + QVERIFY(window2->frameGeometry().contains(900, 900)); + Cursors::self()->mouse()->setPos(900, 900); + + // and click + quint32 timestamp = 1; + QFETCH(quint32, button); + kwinApp()->platform()->pointerButtonPressed(button, timestamp++); + QVERIFY(buttonSpy.wait()); + if (clickRaise) { + QCOMPARE(stackingOrderChangedSpy.count(), 1); + QTRY_COMPARE_WITH_TIMEOUT(workspace()->topClientOnDesktop(1, -1), window2, 200); + } else { + QCOMPARE(stackingOrderChangedSpy.count(), 0); + QVERIFY(!stackingOrderChangedSpy.wait(100)); + QCOMPARE(workspace()->topClientOnDesktop(1, -1), window1); + } + + // release again + kwinApp()->platform()->pointerButtonReleased(button, timestamp++); + + delete surface1; + QVERIFY(window1DestroyedSpy.wait()); + delete surface2; + QVERIFY(window2DestroyedSpy.wait()); +} + +void PointerInputTest::testCursorImage() +{ + // this test verifies that the pointer image gets updated correctly from the client provided data + using namespace KWayland::Client; + // we need a pointer to get the enter event + auto pointer = m_seat->createPointer(m_seat); + QVERIFY(pointer); + QVERIFY(pointer->isValid()); + QSignalSpy enteredSpy(pointer, &Pointer::entered); + QVERIFY(enteredSpy.isValid()); + + // move cursor somewhere the new window won't open + auto cursor = Cursors::self()->mouse(); + cursor->setPos(800, 800); + auto p = input()->pointer(); + // at the moment it should be the fallback cursor + const QImage fallbackCursor = cursor->image(); + QVERIFY(!fallbackCursor.isNull()); + + // create a window + QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(clientAddedSpy.isValid()); + Surface *surface = Test::createSurface(m_compositor); + QVERIFY(surface); + XdgShellSurface *shellSurface = Test::createXdgShellStableSurface(surface, surface); + QVERIFY(shellSurface); + render(surface); + QVERIFY(clientAddedSpy.wait()); + AbstractClient *window = workspace()->activeClient(); + QVERIFY(window); + + // move cursor to center of window, this should first set a null pointer, so we still show old cursor + cursor->setPos(window->frameGeometry().center()); + QCOMPARE(p->focus(), window); + QCOMPARE(cursor->image(), fallbackCursor); + QVERIFY(enteredSpy.wait()); + + // create a cursor on the pointer + Surface *cursorSurface = Test::createSurface(m_compositor); + QVERIFY(cursorSurface); + QSignalSpy cursorRenderedSpy(cursorSurface, &Surface::frameRendered); + QVERIFY(cursorRenderedSpy.isValid()); + 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, QPoint(5, 5)); + QVERIFY(cursorRenderedSpy.wait()); + QCOMPARE(cursor->image(), red); + QCOMPARE(cursor->hotspot(), QPoint(5, 5)); + // change hotspot + pointer->setCursor(cursorSurface, QPoint(6, 6)); + Test::flushWaylandConnection(); + QTRY_COMPARE(cursor->hotspot(), QPoint(6, 6)); + QCOMPARE(cursor->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(cursor->image(), blue); + QCOMPARE(cursor->hotspot(), QPoint(6, 6)); + + // scaled cursor + QImage blueScaled = QImage(QSize(20, 20), QImage::Format_ARGB32_Premultiplied); + blueScaled.setDevicePixelRatio(2); + blueScaled.fill(Qt::blue); + auto bs = Test::waylandShmPool()->createBuffer(blueScaled); + cursorSurface->attachBuffer(bs); + cursorSurface->setScale(2); + cursorSurface->damage(QRect(0, 0, 20, 20)); + cursorSurface->commit(); + QVERIFY(cursorRenderedSpy.wait()); + QTRY_COMPARE(cursor->image(), blueScaled); + QCOMPARE(cursor->hotspot(), QPoint(6, 6)); //surface-local (so not changed) + + // hide the cursor + pointer->setCursor(nullptr); + Test::flushWaylandConnection(); + QTRY_VERIFY(cursor->image().isNull()); + + // move cursor somewhere else, should reset to fallback cursor + Cursors::self()->mouse()->setPos(window->frameGeometry().bottomLeft() + QPoint(20, 20)); + QVERIFY(!p->focus()); + QVERIFY(!cursor->image().isNull()); + QCOMPARE(cursor->image(), fallbackCursor); +} + +class HelperEffect : public Effect +{ + Q_OBJECT +public: + HelperEffect() {} + ~HelperEffect() override {} +}; + +void PointerInputTest::testEffectOverrideCursorImage() +{ + // this test verifies the effect cursor override handling + using namespace KWayland::Client; + // we need a pointer to get the enter event and set a cursor + auto pointer = m_seat->createPointer(m_seat); + auto cursor = Cursors::self()->mouse(); + QVERIFY(pointer); + QVERIFY(pointer->isValid()); + QSignalSpy enteredSpy(pointer, &Pointer::entered); + QVERIFY(enteredSpy.isValid()); + QSignalSpy leftSpy(pointer, &Pointer::left); + QVERIFY(leftSpy.isValid()); + // move cursor somewhere the new window won't open + cursor->setPos(800, 800); + // here we should have the fallback cursor + const QImage fallback = cursor->image(); + QVERIFY(!fallback.isNull()); + + // now let's create a window + QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(clientAddedSpy.isValid()); + Surface *surface = Test::createSurface(m_compositor); + QVERIFY(surface); + XdgShellSurface *shellSurface = Test::createXdgShellStableSurface(surface, surface); + QVERIFY(shellSurface); + render(surface); + QVERIFY(clientAddedSpy.wait()); + AbstractClient *window = workspace()->activeClient(); + QVERIFY(window); + + // and move cursor to the window + QVERIFY(!window->frameGeometry().contains(QPoint(800, 800))); + cursor->setPos(window->frameGeometry().center()); + QVERIFY(enteredSpy.wait()); + // cursor image should still be fallback + QCOMPARE(cursor->image(), fallback); + + // now create an effect and set an override cursor + QScopedPointer effect(new HelperEffect); + effects->startMouseInterception(effect.data(), Qt::SizeAllCursor); + const QImage sizeAll = cursor->image(); + QVERIFY(!sizeAll.isNull()); + QVERIFY(sizeAll != fallback); + QVERIFY(leftSpy.wait()); + + // let's change to arrow cursor, this should be our fallback + effects->defineCursor(Qt::ArrowCursor); + QCOMPARE(cursor->image(), fallback); + + // back to size all + effects->defineCursor(Qt::SizeAllCursor); + QCOMPARE(cursor->image(), sizeAll); + + // move cursor outside the window area + Cursors::self()->mouse()->setPos(800, 800); + // and end the override, which should switch to fallback + effects->stopMouseInterception(effect.data()); + QCOMPARE(cursor->image(), fallback); + + // start mouse interception again + effects->startMouseInterception(effect.data(), Qt::SizeAllCursor); + QCOMPARE(cursor->image(), sizeAll); + + // move cursor to area of window + Cursors::self()->mouse()->setPos(window->frameGeometry().center()); + // this should not result in an enter event + QVERIFY(!enteredSpy.wait(100)); + + // after ending the interception we should get an enter event + effects->stopMouseInterception(effect.data()); + QVERIFY(enteredSpy.wait()); + QVERIFY(cursor->image().isNull()); +} + +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 + using namespace KWayland::Client; + auto pointer = m_seat->createPointer(m_seat); + QVERIFY(pointer); + QVERIFY(pointer->isValid()); + QSignalSpy enteredSpy(pointer, &Pointer::entered); + QVERIFY(enteredSpy.isValid()); + QSignalSpy leftSpy(pointer, &Pointer::left); + QVERIFY(leftSpy.isValid()); + QSignalSpy buttonStateChangedSpy(pointer, &Pointer::buttonStateChanged); + QVERIFY(buttonStateChangedSpy.isValid()); + QSignalSpy motionSpy(pointer, &Pointer::motion); + QVERIFY(motionSpy.isValid()); + + Cursors::self()->mouse()->setPos(800, 800); + + QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(clientAddedSpy.isValid()); + Surface *surface = Test::createSurface(m_compositor); + QVERIFY(surface); + XdgShellSurface *shellSurface = Test::createXdgShellStableSurface(surface, surface); + QVERIFY(shellSurface); + render(surface); + QVERIFY(clientAddedSpy.wait()); + AbstractClient *window = workspace()->activeClient(); + QVERIFY(window); + QCOMPARE(window->hasPopupGrab(), false); + // move pointer into window + QVERIFY(!window->frameGeometry().contains(QPoint(800, 800))); + Cursors::self()->mouse()->setPos(window->frameGeometry().center()); + QVERIFY(enteredSpy.wait()); + // click inside window to create serial + quint32 timestamp = 0; + kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); + kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); + QVERIFY(buttonStateChangedSpy.wait()); + + // now create the popup surface + XdgPositioner positioner(QSize(100, 50), QRect(0, 0, 80, 20)); + positioner.setAnchorEdge(Qt::BottomEdge | Qt::RightEdge); + positioner.setGravity(Qt::BottomEdge | Qt::RightEdge); + Surface *popupSurface = Test::createSurface(m_compositor); + QVERIFY(popupSurface); + XdgShellPopup *popupShellSurface = Test::createXdgShellStablePopup(popupSurface, shellSurface, positioner); + QVERIFY(popupShellSurface); + QSignalSpy popupDoneSpy(popupShellSurface, &XdgShellPopup::popupDone); + QVERIFY(popupDoneSpy.isValid()); + popupShellSurface->requestGrab(Test::waylandSeat(), 0); // FIXME: Serial. + render(popupSurface, positioner.initialSize()); + QVERIFY(clientAddedSpy.wait()); + auto popupClient = clientAddedSpy.last().first().value(); + QVERIFY(popupClient); + QVERIFY(popupClient != window); + QCOMPARE(window, workspace()->activeClient()); + QCOMPARE(popupClient->transientFor(), window); + QCOMPARE(popupClient->pos(), window->pos() + QPoint(80, 20)); + QCOMPARE(popupClient->hasPopupGrab(), true); + + // let's move the pointer into the center of the window + Cursors::self()->mouse()->setPos(popupClient->frameGeometry().center()); + QVERIFY(enteredSpy.wait()); + QCOMPARE(enteredSpy.count(), 2); + QCOMPARE(leftSpy.count(), 1); + QCOMPARE(pointer->enteredSurface(), popupSurface); + + // let's move the pointer outside of the popup window + // this should not really change anything, it gets a leave event + Cursors::self()->mouse()->setPos(popupClient->frameGeometry().bottomRight() + QPoint(2, 2)); + QVERIFY(leftSpy.wait()); + QCOMPARE(leftSpy.count(), 2); + QVERIFY(popupDoneSpy.isEmpty()); + // now click, should trigger popupDone + kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); + QVERIFY(popupDoneSpy.wait()); + kwinApp()->platform()->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 + using namespace KWayland::Client; + auto pointer = m_seat->createPointer(m_seat); + QVERIFY(pointer); + QVERIFY(pointer->isValid()); + QSignalSpy enteredSpy(pointer, &Pointer::entered); + QVERIFY(enteredSpy.isValid()); + QSignalSpy leftSpy(pointer, &Pointer::left); + QVERIFY(leftSpy.isValid()); + QSignalSpy buttonStateChangedSpy(pointer, &Pointer::buttonStateChanged); + QVERIFY(buttonStateChangedSpy.isValid()); + QSignalSpy motionSpy(pointer, &Pointer::motion); + QVERIFY(motionSpy.isValid()); + + Cursors::self()->mouse()->setPos(800, 800); + QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(clientAddedSpy.isValid()); + Surface *surface = Test::createSurface(m_compositor); + QVERIFY(surface); + XdgShellSurface *shellSurface = Test::createXdgShellStableSurface(surface, surface); + QVERIFY(shellSurface); + + auto deco = Test::waylandServerSideDecoration()->create(surface, surface); + QSignalSpy decoSpy(deco, &ServerSideDecoration::modeChanged); + QVERIFY(decoSpy.isValid()); + QVERIFY(decoSpy.wait()); + deco->requestMode(ServerSideDecoration::Mode::Server); + QVERIFY(decoSpy.wait()); + QCOMPARE(deco->mode(), ServerSideDecoration::Mode::Server); + render(surface); + QVERIFY(clientAddedSpy.wait()); + AbstractClient *window = workspace()->activeClient(); + QVERIFY(window); + QCOMPARE(window->hasPopupGrab(), false); + QVERIFY(window->isDecorated()); + + // move pointer into window + QVERIFY(!window->frameGeometry().contains(QPoint(800, 800))); + Cursors::self()->mouse()->setPos(window->frameGeometry().center()); + QVERIFY(enteredSpy.wait()); + // click inside window to create serial + quint32 timestamp = 0; + kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); + kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); + QVERIFY(buttonStateChangedSpy.wait()); + + // now create the popup surface + XdgPositioner positioner(QSize(100, 50), QRect(0, 0, 80, 20)); + positioner.setAnchorEdge(Qt::BottomEdge | Qt::RightEdge); + positioner.setGravity(Qt::BottomEdge | Qt::RightEdge); + Surface *popupSurface = Test::createSurface(m_compositor); + QVERIFY(popupSurface); + XdgShellPopup *popupShellSurface = Test::createXdgShellStablePopup(popupSurface, shellSurface, positioner); + QVERIFY(popupShellSurface); + QSignalSpy popupDoneSpy(popupShellSurface, &XdgShellPopup::popupDone); + QVERIFY(popupDoneSpy.isValid()); + popupShellSurface->requestGrab(Test::waylandSeat(), 0); // FIXME: Serial. + render(popupSurface, positioner.initialSize()); + QVERIFY(clientAddedSpy.wait()); + auto popupClient = clientAddedSpy.last().first().value(); + QVERIFY(popupClient); + QVERIFY(popupClient != window); + QCOMPARE(window, workspace()->activeClient()); + QCOMPARE(popupClient->transientFor(), window); + QCOMPARE(popupClient->pos(), window->pos() + window->clientPos() + QPoint(80, 20)); + QCOMPARE(popupClient->hasPopupGrab(), true); + + // let's move the pointer into the center of the deco + Cursors::self()->mouse()->setPos(window->frameGeometry().center().x(), window->y() + (window->height() - window->clientSize().height()) / 2); + + kwinApp()->platform()->pointerButtonPressed(BTN_RIGHT, timestamp++); + QVERIFY(popupDoneSpy.wait()); + kwinApp()->platform()->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 + using namespace KWayland::Client; + auto pointer = m_seat->createPointer(m_seat); + QVERIFY(pointer); + QVERIFY(pointer->isValid()); + QSignalSpy enteredSpy(pointer, &Pointer::entered); + QVERIFY(enteredSpy.isValid()); + QSignalSpy leftSpy(pointer, &Pointer::left); + QVERIFY(leftSpy.isValid()); + + Cursors::self()->mouse()->setPos(800, 800); + QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(clientAddedSpy.isValid()); + Surface *surface = Test::createSurface(m_compositor); + QVERIFY(surface); + XdgShellSurface *shellSurface = Test::createXdgShellStableSurface(surface, surface); + QVERIFY(shellSurface); + render(surface); + QVERIFY(clientAddedSpy.wait()); + AbstractClient *window = workspace()->activeClient(); + QVERIFY(window); + + // move cursor over window + QVERIFY(!window->frameGeometry().contains(QPoint(800, 800))); + Cursors::self()->mouse()->setPos(window->frameGeometry().center()); + QVERIFY(enteredSpy.wait()); + // click inside window + quint32 timestamp = 0; + kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); + + // now create a second window as transient + XdgPositioner positioner(QSize(99, 49), QRect(0, 0, 1, 1)); + positioner.setAnchorEdge(Qt::BottomEdge | Qt::RightEdge); + positioner.setGravity(Qt::BottomEdge | Qt::RightEdge); + Surface *popupSurface = Test::createSurface(m_compositor); + QVERIFY(popupSurface); + XdgShellPopup *popupShellSurface = Test::createXdgShellStablePopup(popupSurface, shellSurface, positioner); + QVERIFY(popupShellSurface); + render(popupSurface, positioner.initialSize()); + QVERIFY(clientAddedSpy.wait()); + auto popupClient = clientAddedSpy.last().first().value(); + QVERIFY(popupClient); + QVERIFY(popupClient != window); + QVERIFY(window->frameGeometry().contains(Cursors::self()->mouse()->pos())); + QVERIFY(popupClient->frameGeometry().contains(Cursors::self()->mouse()->pos())); + QVERIFY(!leftSpy.wait()); + + kwinApp()->platform()->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(2559, 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, 1023); + 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 + + // unload the Present Windows effect because it pushes back + // pointer if it's at (0, 0) + static_cast(effects)->unloadEffect(QStringLiteral("presentwindows")); + + // setup screen layout + const QVector geometries { + QRect(0, 0, 1280, 1024), + QRect(1280, 0, 1280, 1024), + QRect(2560, 0, 1280, 1024), + QRect(1280, 1024, 1280, 1024) + }; + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", + Qt::DirectConnection, + Q_ARG(int, geometries.count()), + Q_ARG(QVector, geometries)); + QCOMPARE(screens()->count(), geometries.count()); + QCOMPARE(screens()->geometry(0), geometries.at(0)); + QCOMPARE(screens()->geometry(1), geometries.at(1)); + QCOMPARE(screens()->geometry(2), geometries.at(2)); + QCOMPARE(screens()->geometry(3), geometries.at(3)); + + // move pointer to initial position + QFETCH(QPoint, startPos); + Cursors::self()->mouse()->setPos(startPos); + QCOMPARE(Cursors::self()->mouse()->pos(), startPos); + + // perform movement + QFETCH(QPoint, targetPos); + kwinApp()->platform()->pointerMotion(targetPos, 1); + + QFETCH(QPoint, expectedPos); + QCOMPARE(Cursors::self()->mouse()->pos(), expectedPos); +} + +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("MouseBindings"); + group.writeEntry("CommandAllKey", "Meta"); + group.writeEntry("CommandAll3", "Resize"); + group.sync(); + workspace()->slotReconfigure(); + QCOMPARE(options->commandAllModifier(), Qt::MetaModifier); + QCOMPARE(options->commandAll3(), Options::MouseUnrestrictedResize); + + // create a test client + using namespace KWayland::Client; + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + AbstractClient *c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + + // move the cursor to the test position + QPoint cursorPos; + QFETCH(Qt::Edges, edges); + + if (edges & Qt::LeftEdge) { + cursorPos.setX(c->frameGeometry().left()); + } else if (edges & Qt::RightEdge) { + cursorPos.setX(c->frameGeometry().right()); + } else { + cursorPos.setX(c->frameGeometry().center().x()); + } + + if (edges & Qt::TopEdge) { + cursorPos.setY(c->frameGeometry().top()); + } else if (edges & Qt::BottomEdge) { + cursorPos.setY(c->frameGeometry().bottom()); + } else { + cursorPos.setY(c->frameGeometry().center().y()); + } + + Cursors::self()->mouse()->setPos(cursorPos); + + const PlatformCursorImage arrowCursor = loadReferenceThemeCursor(Qt::ArrowCursor); + QVERIFY(!arrowCursor.isNull()); + QCOMPARE(kwinApp()->platform()->cursorImage().image(), arrowCursor.image()); + QCOMPARE(kwinApp()->platform()->cursorImage().hotSpot(), arrowCursor.hotSpot()); + + // start resizing the client + int timestamp = 1; + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + kwinApp()->platform()->pointerButtonPressed(BTN_RIGHT, timestamp++); + QVERIFY(c->isResize()); + + QFETCH(KWin::CursorShape, cursorShape); + const PlatformCursorImage resizeCursor = loadReferenceThemeCursor(cursorShape); + QVERIFY(!resizeCursor.isNull()); + QCOMPARE(kwinApp()->platform()->cursorImage().image(), resizeCursor.image()); + QCOMPARE(kwinApp()->platform()->cursorImage().hotSpot(), resizeCursor.hotSpot()); + + // finish resizing the client + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTMETA, timestamp++); + kwinApp()->platform()->pointerButtonReleased(BTN_RIGHT, timestamp++); + QVERIFY(!c->isResize()); + + QCOMPARE(kwinApp()->platform()->cursorImage().image(), arrowCursor.image()); + QCOMPARE(kwinApp()->platform()->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("MouseBindings"); + group.writeEntry("CommandAllKey", "Meta"); + group.writeEntry("CommandAll1", "Move"); + group.sync(); + workspace()->slotReconfigure(); + QCOMPARE(options->commandAllModifier(), Qt::MetaModifier); + QCOMPARE(options->commandAll1(), Options::MouseUnrestrictedMove); + + // create a test client + using namespace KWayland::Client; + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + AbstractClient *c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + + // move cursor to the test position + Cursors::self()->mouse()->setPos(c->frameGeometry().center()); + + const PlatformCursorImage arrowCursor = loadReferenceThemeCursor(Qt::ArrowCursor); + QVERIFY(!arrowCursor.isNull()); + QCOMPARE(kwinApp()->platform()->cursorImage().image(), arrowCursor.image()); + QCOMPARE(kwinApp()->platform()->cursorImage().hotSpot(), arrowCursor.hotSpot()); + + // start moving the client + int timestamp = 1; + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); + QVERIFY(c->isMove()); + + const PlatformCursorImage sizeAllCursor = loadReferenceThemeCursor(Qt::SizeAllCursor); + QVERIFY(!sizeAllCursor.isNull()); + QCOMPARE(kwinApp()->platform()->cursorImage().image(), sizeAllCursor.image()); + QCOMPARE(kwinApp()->platform()->cursorImage().hotSpot(), sizeAllCursor.hotSpot()); + + // finish moving the client + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTMETA, timestamp++); + kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); + QVERIFY(!c->isMove()); + + QCOMPARE(kwinApp()->platform()->cursorImage().image(), arrowCursor.image()); + QCOMPARE(kwinApp()->platform()->cursorImage().hotSpot(), arrowCursor.hotSpot()); +} + +void PointerInputTest::testHideShowCursor() +{ + QCOMPARE(kwinApp()->platform()->isCursorHidden(), false); + kwinApp()->platform()->hideCursor(); + QCOMPARE(kwinApp()->platform()->isCursorHidden(), true); + kwinApp()->platform()->showCursor(); + QCOMPARE(kwinApp()->platform()->isCursorHidden(), false); + + kwinApp()->platform()->hideCursor(); + QCOMPARE(kwinApp()->platform()->isCursorHidden(), true); + kwinApp()->platform()->hideCursor(); + kwinApp()->platform()->hideCursor(); + kwinApp()->platform()->hideCursor(); + QCOMPARE(kwinApp()->platform()->isCursorHidden(), true); + + kwinApp()->platform()->showCursor(); + QCOMPARE(kwinApp()->platform()->isCursorHidden(), true); + kwinApp()->platform()->showCursor(); + QCOMPARE(kwinApp()->platform()->isCursorHidden(), true); + kwinApp()->platform()->showCursor(); + QCOMPARE(kwinApp()->platform()->isCursorHidden(), true); + kwinApp()->platform()->showCursor(); + QCOMPARE(kwinApp()->platform()->isCursorHidden(), false); +} + +} + +WAYLANDTEST_MAIN(KWin::PointerInputTest) +#include "pointer_input.moc" diff --git a/autotests/integration/protocols/wlr-layer-shell-unstable-v1.xml b/autotests/integration/protocols/wlr-layer-shell-unstable-v1.xml new file mode 100644 index 0000000..0736a45 --- /dev/null +++ b/autotests/integration/protocols/wlr-layer-shell-unstable-v1.xml @@ -0,0 +1,325 @@ + + + + Copyright © 2017 Drew DeVault + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + + Clients can use this interface to assign the surface_layer role to + wl_surfaces. Such surfaces are assigned to a "layer" of the output and + rendered with a defined z-depth respective to each other. They may also be + anchored to the edges and corners of a screen and specify input handling + semantics. This interface should be suitable for the implementation of + many desktop shell components, and a broad number of other applications + that interact with the desktop. + + + + + Create a layer surface for an existing surface. This assigns the role of + layer_surface, or raises a protocol error if another role is already + assigned. + + Creating a layer surface from a wl_surface which has a buffer attached + or committed is a client error, and any attempts by a client to attach + or manipulate a buffer prior to the first layer_surface.configure call + must also be treated as errors. + + After creating a layer_surface object and setting it up, the client + must perform an initial commit without any buffer attached. + The compositor will reply with a layer_surface.configure event. + The client must acknowledge it and is then allowed to attach a buffer + to map the surface. + + You may pass NULL for output to allow the compositor to decide which + output to use. Generally this will be the one that the user most + recently interacted with. + + Clients can specify a namespace that defines the purpose of the layer + surface. + + + + + + + + + + + + + + + + + These values indicate which layers a surface can be rendered in. They + are ordered by z depth, bottom-most first. Traditional shell surfaces + will typically be rendered between the bottom and top layers. + Fullscreen shell surfaces are typically rendered at the top layer. + Multiple surfaces can share a single layer, and ordering within a + single layer is undefined. + + + + + + + + + + + + + This request indicates that the client will not use the layer_shell + object any more. Objects that have been created through this instance + are not affected. + + + + + + + An interface that may be implemented by a wl_surface, for surfaces that + are designed to be rendered as a layer of a stacked desktop-like + environment. + + Layer surface state (layer, size, anchor, exclusive zone, + margin, interactivity) is double-buffered, and will be applied at the + time wl_surface.commit of the corresponding wl_surface is called. + + Attaching a null buffer to a layer surface unmaps it. + + Unmapping a layer_surface means that the surface cannot be shown by the + compositor until it is explicitly mapped again. The layer_surface + returns to the state it had right after layer_shell.get_layer_surface. + The client can re-map the surface by performing a commit without any + buffer attached, waiting for a configure event and handling it as usual. + + + + + Sets the size of the surface in surface-local coordinates. The + compositor will display the surface centered with respect to its + anchors. + + If you pass 0 for either value, the compositor will assign it and + inform you of the assignment in the configure event. You must set your + anchor to opposite edges in the dimensions you omit; not doing so is a + protocol error. Both values are 0 by default. + + Size is double-buffered, see wl_surface.commit. + + + + + + + + Requests that the compositor anchor the surface to the specified edges + and corners. If two orthogonal edges are specified (e.g. 'top' and + 'left'), then the anchor point will be the intersection of the edges + (e.g. the top left corner of the output); otherwise the anchor point + will be centered on that edge, or in the center if none is specified. + + Anchor is double-buffered, see wl_surface.commit. + + + + + + + Requests that the compositor avoids occluding an area with other + surfaces. The compositor's use of this information is + implementation-dependent - do not assume that this region will not + actually be occluded. + + A positive value is only meaningful if the surface is anchored to one + edge or an edge and both perpendicular edges. If the surface is not + anchored, anchored to only two perpendicular edges (a corner), anchored + to only two parallel edges or anchored to all edges, a positive value + will be treated the same as zero. + + A positive zone is the distance from the edge in surface-local + coordinates to consider exclusive. + + Surfaces that do not wish to have an exclusive zone may instead specify + how they should interact with surfaces that do. If set to zero, the + surface indicates that it would like to be moved to avoid occluding + surfaces with a positive exclusive zone. If set to -1, the surface + indicates that it would not like to be moved to accommodate for other + surfaces, and the compositor should extend it all the way to the edges + it is anchored to. + + For example, a panel might set its exclusive zone to 10, so that + maximized shell surfaces are not shown on top of it. A notification + might set its exclusive zone to 0, so that it is moved to avoid + occluding the panel, but shell surfaces are shown underneath it. A + wallpaper or lock screen might set their exclusive zone to -1, so that + they stretch below or over the panel. + + The default value is 0. + + Exclusive zone is double-buffered, see wl_surface.commit. + + + + + + + Requests that the surface be placed some distance away from the anchor + point on the output, in surface-local coordinates. Setting this value + for edges you are not anchored to has no effect. + + The exclusive zone includes the margin. + + Margin is double-buffered, see wl_surface.commit. + + + + + + + + + + Set to 1 to request that the seat send keyboard events to this layer + surface. For layers below the shell surface layer, the seat will use + normal focus semantics. For layers above the shell surface layers, the + seat will always give exclusive keyboard focus to the top-most layer + which has keyboard interactivity set to true. + + Layer surfaces receive pointer, touch, and tablet events normally. If + you do not want to receive them, set the input region on your surface + to an empty region. + + Events is double-buffered, see wl_surface.commit. + + + + + + + This assigns an xdg_popup's parent to this layer_surface. This popup + should have been created via xdg_surface::get_popup with the parent set + to NULL, and this request must be invoked before committing the popup's + initial state. + + See the documentation of xdg_popup for more details about what an + xdg_popup is and how it is used. + + + + + + + When a configure event is received, if a client commits the + surface in response to the configure event, then the client + must make an ack_configure request sometime before the commit + request, passing along the serial of the configure event. + + If the client receives multiple configure events before it + can respond to one, it only has to ack the last configure event. + + A client is not required to commit immediately after sending + an ack_configure request - it may even ack_configure several times + before its next surface commit. + + A client may send multiple ack_configure requests before committing, but + only the last request sent before a commit indicates which configure + event the client really is responding to. + + + + + + + This request destroys the layer surface. + + + + + + The configure event asks the client to resize its surface. + + Clients should arrange their surface for the new states, and then send + an ack_configure request with the serial sent in this configure event at + some point before committing the new surface. + + The client is free to dismiss all but the last configure event it + received. + + The width and height arguments specify the size of the window in + surface-local coordinates. + + The size is a hint, in the sense that the client is free to ignore it if + it doesn't resize, pick a smaller size (to satisfy aspect ratio or + resize in steps of NxM pixels). If the client picks a smaller size and + is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the + surface will be centered on this axis. + + If the width or height arguments are zero, it means the client should + decide its own window dimension. + + + + + + + + + The closed event is sent by the compositor when the surface will no + longer be shown. The output may have been destroyed or the user may + have asked for it to be removed. Further changes to the surface will be + ignored. The client should destroy the resource after receiving this + event, and create a new surface if they so choose. + + + + + + + + + + + + + + + + + + + + + Change the layer that the surface is rendered on. + + Layer is double-buffered, see wl_surface.commit. + + + + + diff --git a/autotests/integration/quick_tiling_test.cpp b/autotests/integration/quick_tiling_test.cpp new file mode 100644 index 0000000..9becf6a --- /dev/null +++ b/autotests/integration/quick_tiling_test.cpp @@ -0,0 +1,885 @@ +/* + 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 "platform.h" +#include "abstract_client.h" +#include "x11client.h" +#include "cursor.h" +#include "decorations/decorationbridge.h" +#include "decorations/settings.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include "scripting/scripting.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#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 QuickTilingTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + void testQuickTiling_data(); + void testQuickTiling(); + void testQuickMaximizing_data(); + void testQuickMaximizing(); + 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 testScript_data(); + void testScript(); + +private: + KWayland::Client::ConnectionThread *m_connection = nullptr; + KWayland::Client::Compositor *m_compositor = nullptr; +}; + +void QuickTilingTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType("MaximizeMode"); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + // set custom config which disables the Outline + KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup group = config->group("Outline"); + group.writeEntry(QStringLiteral("QmlPath"), QString("/does/not/exist.qml")); + group.sync(); + + kwinApp()->setConfig(config); + + qputenv("XKB_DEFAULT_RULES", "evdev"); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); +} + +void QuickTilingTest::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Decoration)); + m_connection = Test::waylandConnection(); + m_compositor = Test::waylandCompositor(); + + screens()->setCurrent(0); +} + +void QuickTilingTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +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) << QRect(0, 0, 640, 1024) << QRect(1280, 0, 640, 1024) << FLAG(Right); + QTest::newRow("top") << FLAG(Top) << QRect(0, 0, 1280, 512) << QRect(1280, 0, 1280, 512) << FLAG(Top); + QTest::newRow("right") << FLAG(Right) << QRect(640, 0, 640, 1024) << QRect(1920, 0, 640, 1024) << QuickTileMode(); + QTest::newRow("bottom") << FLAG(Bottom) << QRect(0, 512, 1280, 512) << QRect(1280, 512, 1280, 512) << FLAG(Bottom); + + QTest::newRow("top left") << (FLAG(Left) | FLAG(Top)) << QRect(0, 0, 640, 512) << QRect(1280, 0, 640, 512) << (FLAG(Right) | FLAG(Top)); + QTest::newRow("top right") << (FLAG(Right) | FLAG(Top)) << QRect(640, 0, 640, 512) << QRect(1920, 0, 640, 512) << QuickTileMode(); + QTest::newRow("bottom left") << (FLAG(Left) | FLAG(Bottom)) << QRect(0, 512, 640, 512) << QRect(1280, 512, 640, 512) << (FLAG(Right) | FLAG(Bottom)); + QTest::newRow("bottom right") << (FLAG(Right) | FLAG(Bottom)) << QRect(640, 512, 640, 512) << QRect(1920, 512, 640, 512) << QuickTileMode(); + + QTest::newRow("maximize") << FLAG(Maximize) << QRect(0, 0, 1280, 1024) << QRect(1280, 0, 1280, 1024) << QuickTileMode(); + +#undef FLAG +} + +void QuickTilingTest::testQuickTiling() +{ + using namespace KWayland::Client; + + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + + // Map the client. + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(workspace()->activeClient(), c); + QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); + QCOMPARE(c->quickTileMode(), QuickTileMode(QuickTileFlag::None)); + + // We have to receive a configure event when the client becomes active. + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 1); + + QSignalSpy quickTileChangedSpy(c, &AbstractClient::quickTileModeChanged); + QVERIFY(quickTileChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(c, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); + + QFETCH(QuickTileMode, mode); + QFETCH(QRect, expectedGeometry); + c->setQuickTileMode(mode, true); + QCOMPARE(quickTileChangedSpy.count(), 1); + // at this point the geometry did not yet change + QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); + // but quick tile mode already changed + QCOMPARE(c->quickTileMode(), mode); + + // but we got requested a new geometry + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 2); + QCOMPARE(configureRequestedSpy.last().at(0).toSize(), expectedGeometry.size()); + + // attach a new image + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + Test::render(surface.data(), expectedGeometry.size(), Qt::red); + + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(frameGeometryChangedSpy.count(), 1); + QCOMPARE(c->frameGeometry(), expectedGeometry); + + // send window to other screen + QCOMPARE(c->screen(), 0); + c->sendToScreen(1); + QCOMPARE(c->screen(), 1); + // quick tile should not be changed + QCOMPARE(c->quickTileMode(), mode); + QTEST(c->frameGeometry(), "secondScreen"); + + // now try to toggle again + c->setQuickTileMode(mode, true); + QTEST(c->quickTileMode(), "expectedModeAfterToggle"); +} + +void QuickTilingTest::testQuickMaximizing_data() +{ + QTest::addColumn("mode"); + +#define FLAG(name) QuickTileMode(QuickTileFlag::name) + + QTest::newRow("maximize") << FLAG(Maximize); + QTest::newRow("none") << FLAG(None); + +#undef FLAG +} + +void QuickTilingTest::testQuickMaximizing() +{ + using namespace KWayland::Client; + + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + + // Map the client. + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(workspace()->activeClient(), c); + QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); + QCOMPARE(c->quickTileMode(), QuickTileMode(QuickTileFlag::None)); + QCOMPARE(c->maximizeMode(), MaximizeRestore); + + // We have to receive a configure event upon becoming active. + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 1); + + QSignalSpy quickTileChangedSpy(c, &AbstractClient::quickTileModeChanged); + QVERIFY(quickTileChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(c, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); + QSignalSpy maximizeChangedSpy1(c, qOverload(&AbstractClient::clientMaximizedStateChanged)); + QVERIFY(maximizeChangedSpy1.isValid()); + QSignalSpy maximizeChangedSpy2(c, qOverload(&AbstractClient::clientMaximizedStateChanged)); + QVERIFY(maximizeChangedSpy2.isValid()); + + c->setQuickTileMode(QuickTileFlag::Maximize, true); + QCOMPARE(quickTileChangedSpy.count(), 1); + + // at this point the geometry did not yet change + QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); + // but quick tile mode already changed + QCOMPARE(c->quickTileMode(), QuickTileFlag::Maximize); + QCOMPARE(c->geometryRestore(), QRect(0, 0, 100, 50)); + + // but we got requested a new geometry + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 2); + QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(1280, 1024)); + + // attach a new image + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + Test::render(surface.data(), QSize(1280, 1024), Qt::red); + + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(frameGeometryChangedSpy.count(), 1); + QCOMPARE(c->frameGeometry(), QRect(0, 0, 1280, 1024)); + QCOMPARE(c->geometryRestore(), QRect(0, 0, 100, 50)); + + // client is now set to maximised + QCOMPARE(maximizeChangedSpy1.count(), 1); + QCOMPARE(maximizeChangedSpy1.first().first().value(), c); + QCOMPARE(maximizeChangedSpy1.first().last().value(), MaximizeFull); + QCOMPARE(maximizeChangedSpy2.count(), 1); + QCOMPARE(maximizeChangedSpy2.first().first().value(), c); + QCOMPARE(maximizeChangedSpy2.first().at(1).toBool(), true); + QCOMPARE(maximizeChangedSpy2.first().at(2).toBool(), true); + QCOMPARE(c->maximizeMode(), MaximizeFull); + + // go back to quick tile none + QFETCH(QuickTileMode, mode); + c->setQuickTileMode(mode, true); + QCOMPARE(c->quickTileMode(), QuickTileMode(QuickTileFlag::None)); + QCOMPARE(quickTileChangedSpy.count(), 2); + // geometry not yet changed + QCOMPARE(c->frameGeometry(), QRect(0, 0, 1280, 1024)); + QCOMPARE(c->geometryRestore(), QRect(0, 0, 100, 50)); + // we got requested a new geometry + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 3); + QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(100, 50)); + + // render again + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + Test::render(surface.data(), QSize(100, 50), Qt::yellow); + + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(frameGeometryChangedSpy.count(), 2); + QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); + QCOMPARE(c->geometryRestore(), QRect(0, 0, 100, 50)); + QCOMPARE(maximizeChangedSpy1.count(), 2); + QCOMPARE(maximizeChangedSpy1.last().first().value(), c); + QCOMPARE(maximizeChangedSpy1.last().last().value(), MaximizeRestore); + QCOMPARE(maximizeChangedSpy2.count(), 2); + QCOMPARE(maximizeChangedSpy2.last().first().value(), c); + QCOMPARE(maximizeChangedSpy2.last().at(1).toBool(), false); + QCOMPARE(maximizeChangedSpy2.last().at(2).toBool(), false); +} + +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() +{ + using namespace KWayland::Client; + + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + QSignalSpy sizeChangeSpy(shellSurface.data(), &XdgShellSurface::sizeChanged); + QVERIFY(sizeChangeSpy.isValid()); + // let's render + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + + QVERIFY(c); + QCOMPARE(workspace()->activeClient(), c); + QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); + QCOMPARE(c->quickTileMode(), QuickTileMode(QuickTileFlag::None)); + QCOMPARE(c->maximizeMode(), MaximizeRestore); + + QSignalSpy quickTileChangedSpy(c, &AbstractClient::quickTileModeChanged); + QVERIFY(quickTileChangedSpy.isValid()); + + workspace()->performWindowOperation(c, Options::UnrestrictedMoveOp); + QCOMPARE(c, workspace()->moveResizeClient()); + QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(49, 24)); + + QFETCH(QPoint, targetPos); + quint32 timestamp = 1; + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); + while (Cursors::self()->mouse()->pos().x() > targetPos.x()) { + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFT, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFT, timestamp++); + } + while (Cursors::self()->mouse()->pos().x() < targetPos.x()) { + kwinApp()->platform()->keyboardKeyPressed(KEY_RIGHT, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_RIGHT, timestamp++); + } + while (Cursors::self()->mouse()->pos().y() < targetPos.y()) { + kwinApp()->platform()->keyboardKeyPressed(KEY_DOWN, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_DOWN, timestamp++); + } + while (Cursors::self()->mouse()->pos().y() > targetPos.y()) { + kwinApp()->platform()->keyboardKeyPressed(KEY_UP, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_UP, timestamp++); + } + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_ENTER, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_ENTER, timestamp++); + QCOMPARE(Cursors::self()->mouse()->pos(), targetPos); + QVERIFY(!workspace()->moveResizeClient()); + + QCOMPARE(quickTileChangedSpy.count(), 1); + QTEST(c->quickTileMode(), "expectedMode"); +} + +void QuickTilingTest::testQuickTilingPointerMove_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::testQuickTilingPointerMove() +{ + using namespace KWayland::Client; + + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + + QScopedPointer shellSurface(Test::createXdgShellStableSurface( + surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + QVERIFY(!shellSurface.isNull()); + + // wait for the initial configure event + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + surface->commit(Surface::CommitFlag::None); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 1); + + // let's render + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + + QVERIFY(c); + QCOMPARE(workspace()->activeClient(), c); + QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); + QCOMPARE(c->quickTileMode(), QuickTileMode(QuickTileFlag::None)); + QCOMPARE(c->maximizeMode(), MaximizeRestore); + + // we have to receive a configure event when the client becomes active + QVERIFY(configureRequestedSpy.wait()); + QTRY_COMPARE(configureRequestedSpy.count(), 2); + + QSignalSpy quickTileChangedSpy(c, &AbstractClient::quickTileModeChanged); + QVERIFY(quickTileChangedSpy.isValid()); + + workspace()->performWindowOperation(c, Options::UnrestrictedMoveOp); + QCOMPARE(c, workspace()->moveResizeClient()); + QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(49, 24)); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 3); + + QFETCH(QPoint, targetPos); + quint32 timestamp = 1; + kwinApp()->platform()->pointerMotion(targetPos, timestamp++); + kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); + kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); + QCOMPARE(Cursors::self()->mouse()->pos(), targetPos); + QVERIFY(!workspace()->moveResizeClient()); + + QCOMPARE(quickTileChangedSpy.count(), 1); + QTEST(c->quickTileMode(), "expectedMode"); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 4); + QCOMPARE(false, configureRequestedSpy.last().first().toSize().isEmpty()); +} + +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 + using namespace KWayland::Client; + + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + QScopedPointer deco(Test::waylandServerSideDecoration()->create(surface.data())); + + QScopedPointer shellSurface(Test::createXdgShellStableSurface( + surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + QVERIFY(!shellSurface.isNull()); + + // wait for the initial configure event + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + surface->commit(Surface::CommitFlag::None); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 1); + + // let's render + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + auto c = Test::renderAndWaitForShown(surface.data(), QSize(1000, 50), Qt::blue); + + QVERIFY(c); + QVERIFY(c->isDecorated()); + const auto decoration = c->decoration(); + QCOMPARE(workspace()->activeClient(), c); + QCOMPARE(c->frameGeometry(), QRect(-decoration->borderLeft(), 0, + 1000 + decoration->borderLeft() + decoration->borderRight(), + 50 + decoration->borderTop() + decoration->borderBottom())); + QCOMPARE(c->quickTileMode(), QuickTileMode(QuickTileFlag::None)); + QCOMPARE(c->maximizeMode(), MaximizeRestore); + + // we have to receive a configure event when the client becomes active + QVERIFY(configureRequestedSpy.wait()); + QTRY_COMPARE(configureRequestedSpy.count(), 2); + + QSignalSpy quickTileChangedSpy(c, &AbstractClient::quickTileModeChanged); + QVERIFY(quickTileChangedSpy.isValid()); + + quint32 timestamp = 1; + kwinApp()->platform()->touchDown(0, QPointF(c->frameGeometry().center().x(), c->frameGeometry().y() + decoration->borderTop() / 2), timestamp++); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(c, workspace()->moveResizeClient()); + QCOMPARE(configureRequestedSpy.count(), 3); + + QFETCH(QPoint, targetPos); + kwinApp()->platform()->touchMotion(0, targetPos, timestamp++); + kwinApp()->platform()->touchUp(0, timestamp++); + QVERIFY(!workspace()->moveResizeClient()); + + + // 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 = Decoration::DecorationBridge::self()->settings()->borderSize() != KDecoration2::BorderSize::None; + + QCOMPARE(quickTileChangedSpy.count(), 1); + QTEST(c->quickTileMode(), "expectedMode"); + QVERIFY(configureRequestedSpy.wait()); + QTRY_COMPARE(configureRequestedSpy.count(), hasBorders ? 5 : 4); + QCOMPARE(false, configureRequestedSpy.last().first().toSize().isEmpty()); +} + +struct XcbConnectionDeleter +{ + static inline void cleanup(xcb_connection_t *pointer) + { + xcb_disconnect(pointer); + } +}; + +void QuickTilingTest::testX11QuickTiling_data() +{ + QTest::addColumn("mode"); + QTest::addColumn("expectedGeometry"); + QTest::addColumn("screen"); + QTest::addColumn("modeAfterToggle"); + +#define FLAG(name) QuickTileMode(QuickTileFlag::name) + + QTest::newRow("left") << FLAG(Left) << QRect(0, 0, 640, 1024) << 0 << QuickTileMode(); + QTest::newRow("top") << FLAG(Top) << QRect(0, 0, 1280, 512) << 1 << FLAG(Top); + QTest::newRow("right") << FLAG(Right) << QRect(640, 0, 640, 1024) << 1 << FLAG(Left); + QTest::newRow("bottom") << FLAG(Bottom) << QRect(0, 512, 1280, 512) << 1 << FLAG(Bottom); + + QTest::newRow("top left") << (FLAG(Left) | FLAG(Top)) << QRect(0, 0, 640, 512) << 0 << QuickTileMode(); + QTest::newRow("top right") << (FLAG(Right) | FLAG(Top)) << QRect(640, 0, 640, 512) << 1 << (FLAG(Left) | FLAG(Top)); + QTest::newRow("bottom left") << (FLAG(Left) | FLAG(Bottom)) << QRect(0, 512, 640, 512) << 0 << QuickTileMode(); + QTest::newRow("bottom right") << (FLAG(Right) | FLAG(Bottom)) << QRect(640, 512, 640, 512) << 1 << (FLAG(Left) | FLAG(Bottom)); + + QTest::newRow("maximize") << FLAG(Maximize) << QRect(0, 0, 1280, 1024) << 0 << QuickTileMode(); + +#undef FLAG +} +void QuickTilingTest::testX11QuickTiling() +{ + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + const QRect windowGeometry(0, 0, 100, 200); + xcb_window_t w = xcb_generate_id(c.data()); + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, 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; + memset(&hints, 0, sizeof(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.data(), w, &hints); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + // we should get a client for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->window(), w); + + // now quick tile + QSignalSpy quickTileChangedSpy(client, &AbstractClient::quickTileModeChanged); + QVERIFY(quickTileChangedSpy.isValid()); + const QRect origGeo = client->frameGeometry(); + QFETCH(QuickTileMode, mode); + client->setQuickTileMode(mode, true); + QCOMPARE(client->quickTileMode(), mode); + QTEST(client->frameGeometry(), "expectedGeometry"); + QCOMPARE(client->geometryRestore(), origGeo); + QEXPECT_FAIL("maximize", "For maximize we get two changed signals", Continue); + QCOMPARE(quickTileChangedSpy.count(), 1); + + // quick tile to same edge again should also act like send to screen + QCOMPARE(client->screen(), 0); + client->setQuickTileMode(mode, true); + QTEST(client->screen(), "screen"); + QTEST(client->quickTileMode(), "modeAfterToggle"); + QCOMPARE(client->geometryRestore(), origGeo); + + // and destroy the window again + xcb_unmap_window(c.data(), w); + xcb_destroy_window(c.data(), w); + xcb_flush(c.data()); + c.reset(); + + QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + QVERIFY(windowClosedSpy.wait()); +} + +void QuickTilingTest::testX11QuickTilingAfterVertMaximize_data() +{ + QTest::addColumn("mode"); + QTest::addColumn("expectedGeometry"); + +#define FLAG(name) QuickTileMode(QuickTileFlag::name) + + QTest::newRow("left") << FLAG(Left) << QRect(0, 0, 640, 1024); + QTest::newRow("top") << FLAG(Top) << QRect(0, 0, 1280, 512); + QTest::newRow("right") << FLAG(Right) << QRect(640, 0, 640, 1024); + QTest::newRow("bottom") << FLAG(Bottom) << QRect(0, 512, 1280, 512); + + QTest::newRow("top left") << (FLAG(Left) | FLAG(Top)) << QRect(0, 0, 640, 512); + QTest::newRow("top right") << (FLAG(Right) | FLAG(Top)) << QRect(640, 0, 640, 512); + QTest::newRow("bottom left") << (FLAG(Left) | FLAG(Bottom)) << QRect(0, 512, 640, 512); + QTest::newRow("bottom right") << (FLAG(Right) | FLAG(Bottom)) << QRect(640, 512, 640, 512); + + QTest::newRow("maximize") << FLAG(Maximize) << QRect(0, 0, 1280, 1024); + +#undef FLAG +} + +void QuickTilingTest::testX11QuickTilingAfterVertMaximize() +{ + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + const QRect windowGeometry(0, 0, 100, 200); + xcb_window_t w = xcb_generate_id(c.data()); + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, 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; + memset(&hints, 0, sizeof(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.data(), w, &hints); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + // we should get a client for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->window(), w); + + const QRect origGeo = client->frameGeometry(); + QCOMPARE(client->maximizeMode(), MaximizeRestore); + // vertically maximize the window + client->maximize(client->maximizeMode() ^ MaximizeVertical); + QCOMPARE(client->frameGeometry().width(), origGeo.width()); + QCOMPARE(client->height(), screens()->size(client->screen()).height()); + QCOMPARE(client->geometryRestore(), origGeo); + + // now quick tile + QSignalSpy quickTileChangedSpy(client, &AbstractClient::quickTileModeChanged); + QVERIFY(quickTileChangedSpy.isValid()); + QFETCH(QuickTileMode, mode); + client->setQuickTileMode(mode, true); + QCOMPARE(client->quickTileMode(), mode); + QTEST(client->frameGeometry(), "expectedGeometry"); + QEXPECT_FAIL("", "We get two changed events", Continue); + QCOMPARE(quickTileChangedSpy.count(), 1); + + // and destroy the window again + xcb_unmap_window(c.data(), w); + xcb_destroy_window(c.data(), w); + xcb_flush(c.data()); + c.reset(); + + QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + QVERIFY(windowClosedSpy.wait()); +} + +void QuickTilingTest::testShortcut_data() +{ + QTest::addColumn("shortcutList"); + QTest::addColumn("expectedMode"); + QTest::addColumn("expectedGeometry"); + +#define FLAG(name) QuickTileMode(QuickTileFlag::name) + QTest::newRow("top") << QStringList{QStringLiteral("Window Quick Tile Top")} << FLAG(Top) << QRect(0, 0, 1280, 512); + QTest::newRow("bottom") << QStringList{QStringLiteral("Window Quick Tile Bottom")} << FLAG(Bottom) << QRect(0, 512, 1280, 512); + QTest::newRow("top right") << QStringList{QStringLiteral("Window Quick Tile Top Right")} << (FLAG(Top) | FLAG(Right)) << QRect(640, 0, 640, 512); + QTest::newRow("top left") << QStringList{QStringLiteral("Window Quick Tile Top Left")} << (FLAG(Top) | FLAG(Left)) << QRect(0, 0, 640, 512); + QTest::newRow("bottom right") << QStringList{QStringLiteral("Window Quick Tile Bottom Right")} << (FLAG(Bottom) | FLAG(Right)) << QRect(640, 512, 640, 512); + QTest::newRow("bottom left") << QStringList{QStringLiteral("Window Quick Tile Bottom Left")} << (FLAG(Bottom) | FLAG(Left)) << QRect(0, 512, 640, 512); + QTest::newRow("left") << QStringList{QStringLiteral("Window Quick Tile Left")} << FLAG(Left) << QRect(0, 0, 640, 1024); + QTest::newRow("right") << QStringList{QStringLiteral("Window Quick Tile Right")} << FLAG(Right) << QRect(640, 0, 640, 1024); + + // Test combined actions for corner tiling + QTest::newRow("top left combined") << QStringList{QStringLiteral("Window Quick Tile Left"), QStringLiteral("Window Quick Tile Top")} << (FLAG(Top) | FLAG(Left)) << QRect(0, 0, 640, 512); + QTest::newRow("top right combined") << QStringList{QStringLiteral("Window Quick Tile Right"), QStringLiteral("Window Quick Tile Top")} << (FLAG(Top) | FLAG(Right)) << QRect(640, 0, 640, 512); + QTest::newRow("bottom left combined") << QStringList{QStringLiteral("Window Quick Tile Left"), QStringLiteral("Window Quick Tile Bottom")} << (FLAG(Bottom) | FLAG(Left)) << QRect(0, 512, 640, 512); + QTest::newRow("bottom right combined") << QStringList{QStringLiteral("Window Quick Tile Right"), QStringLiteral("Window Quick Tile Bottom")} << (FLAG(Bottom) | FLAG(Right)) << QRect(640, 512, 640, 512); +#undef FLAG +} + +void QuickTilingTest::testShortcut() +{ + using namespace KWayland::Client; + + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + + // Map the client. + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(workspace()->activeClient(), c); + QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); + QCOMPARE(c->quickTileMode(), QuickTileMode(QuickTileFlag::None)); + + // We have to receive a configure event when the client becomes active. + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 1); + + QFETCH(QStringList, shortcutList); + QFETCH(QRect, expectedGeometry); + + const int numberOfQuickTileActions = shortcutList.count(); + + if (numberOfQuickTileActions > 1) { + QTest::qWait(1001); + } + + for (QString shortcut : shortcutList) { + // 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); + } + + QSignalSpy quickTileChangedSpy(c, &AbstractClient::quickTileModeChanged); + QVERIFY(quickTileChangedSpy.isValid()); + QTRY_COMPARE(quickTileChangedSpy.count(), numberOfQuickTileActions); + // at this point the geometry did not yet change + QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); + // but quick tile mode already changed + QTEST(c->quickTileMode(), "expectedMode"); + + // but we got requested a new geometry + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 2); + QCOMPARE(configureRequestedSpy.last().at(0).toSize(), expectedGeometry.size()); + + // attach a new image + QSignalSpy frameGeometryChangedSpy(c, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + Test::render(surface.data(), expectedGeometry.size(), Qt::red); + + QVERIFY(frameGeometryChangedSpy.wait()); + QEXPECT_FAIL("maximize", "Geometry changed called twice for maximize", Continue); + QCOMPARE(frameGeometryChangedSpy.count(), 1); + QCOMPARE(c->frameGeometry(), expectedGeometry); +} + +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) << QRect(0, 0, 1280, 512); + QTest::newRow("bottom") << QStringLiteral("Bottom") << FLAG(Bottom) << QRect(0, 512, 1280, 512); + QTest::newRow("top right") << QStringLiteral("TopRight") << (FLAG(Top) | FLAG(Right)) << QRect(640, 0, 640, 512); + QTest::newRow("top left") << QStringLiteral("TopLeft") << (FLAG(Top) | FLAG(Left)) << QRect(0, 0, 640, 512); + QTest::newRow("bottom right") << QStringLiteral("BottomRight") << (FLAG(Bottom) | FLAG(Right)) << QRect(640, 512, 640, 512); + QTest::newRow("bottom left") << QStringLiteral("BottomLeft") << (FLAG(Bottom) | FLAG(Left)) << QRect(0, 512, 640, 512); + QTest::newRow("left") << QStringLiteral("Left") << FLAG(Left) << QRect(0, 0, 640, 1024); + QTest::newRow("right") << QStringLiteral("Right") << FLAG(Right) << QRect(640, 0, 640, 1024); +#undef FLAG +} + +void QuickTilingTest::testScript() +{ + using namespace KWayland::Client; + + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + + // Map the client. + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(workspace()->activeClient(), c); + QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); + QCOMPARE(c->quickTileMode(), QuickTileMode(QuickTileFlag::None)); + + // We have to receive a configure event upon the client becoming active. + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 1); + + QSignalSpy quickTileChangedSpy(c, &AbstractClient::quickTileModeChanged); + QVERIFY(quickTileChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(c, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); + + QVERIFY(Scripting::self()); + QTemporaryFile tmpFile; + QVERIFY(tmpFile.open()); + QTextStream out(&tmpFile); + + QFETCH(QString, action); + out << "workspace.slotWindowQuickTile" << action << "()"; + out.flush(); + + QFETCH(QuickTileMode, expectedMode); + QFETCH(QRect, 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); + QVERIFY(runningChangedSpy.isValid()); + s->run(); + + QVERIFY(quickTileChangedSpy.wait()); + QCOMPARE(quickTileChangedSpy.count(), 1); + + QCOMPARE(runningChangedSpy.count(), 1); + QCOMPARE(runningChangedSpy.first().first().toBool(), true); + + // at this point the geometry did not yet change + QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); + // but quick tile mode already changed + QCOMPARE(c->quickTileMode(), expectedMode); + + // but we got requested a new geometry + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 2); + QCOMPARE(configureRequestedSpy.last().at(0).toSize(), expectedGeometry.size()); + + // attach a new image + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + Test::render(surface.data(), expectedGeometry.size(), Qt::red); + + QVERIFY(frameGeometryChangedSpy.wait()); + QEXPECT_FAIL("maximize", "Geometry changed called twice for maximize", Continue); + QCOMPARE(frameGeometryChangedSpy.count(), 1); + QCOMPARE(c->frameGeometry(), expectedGeometry); +} + +} + +WAYLANDTEST_MAIN(KWin::QuickTilingTest) +#include "quick_tiling_test.moc" diff --git a/autotests/integration/scene_opengl_es_test.cpp b/autotests/integration/scene_opengl_es_test.cpp new file mode 100644 index 0000000..d9aa9ca --- /dev/null +++ b/autotests/integration/scene_opengl_es_test.cpp @@ -0,0 +1,19 @@ +/* + 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/autotests/integration/scene_opengl_shadow_test.cpp b/autotests/integration/scene_opengl_shadow_test.cpp new file mode 100644 index 0000000..a35a02f --- /dev/null +++ b/autotests/integration/scene_opengl_shadow_test.cpp @@ -0,0 +1,852 @@ +/* + 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 +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "kwin_wayland_test.h" + +#include "abstract_client.h" +#include "composite.h" +#include "effect_builtins.h" +#include "effectloader.h" +#include "effects.h" +#include "platform.h" +#include "shadow.h" +#include "wayland_server.h" +#include "workspace.h" + +Q_DECLARE_METATYPE(KWin::WindowQuadList); + +using namespace KWin; +using namespace KWayland::Client; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_scene_opengl_shadow-0"); + +class SceneOpenGLShadowTest : public QObject +{ + Q_OBJECT + +public: + SceneOpenGLShadowTest() {} + +private Q_SLOTS: + void initTestCase(); + void cleanup(); + + void testShadowTileOverlaps_data(); + void testShadowTileOverlaps(); + void testNoCornerShadowTiles(); + void testDistributeHugeCornerTiles(); + +}; + +inline bool isClose(double a, double b, double eps = 1e-5) +{ + if (a == b) { + return true; + } + const double diff = std::fabs(a - b); + if (a == 0 || b == 0) { + return diff < eps; + } + return diff / std::max(a, b) < eps; +} + +inline bool compareQuads(const WindowQuad &a, const WindowQuad &b) +{ + for (int i = 0; i < 4; i++) { + if (!isClose(a[i].x(), b[i].x()) + || !isClose(a[i].y(), b[i].y()) + || !isClose(a[i].u(), b[i].u()) + || !isClose(a[i].v(), b[i].v())) { + return false; + } + } + return true; +} + +inline WindowQuad makeShadowQuad(const QRectF &geo, qreal tx1, qreal ty1, qreal tx2, qreal ty2) +{ + WindowQuad quad(WindowQuadShadow); + quad[0] = WindowVertex(geo.left(), geo.top(), tx1, ty1); + quad[1] = WindowVertex(geo.right(), geo.top(), tx2, ty1); + quad[2] = WindowVertex(geo.right(), geo.bottom(), tx2, ty2); + quad[3] = WindowVertex(geo.left(), geo.bottom(), tx1, ty2); + return quad; +} + +void SceneOpenGLShadowTest::initTestCase() +{ + // Copied from generic_scene_opengl_test.cpp + + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + // 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")); + ScriptedEffectLoader loader; + const auto builtinNames = BuiltInEffects::availableEffectNames() << loader.listOfKnownEffects(); + for (QString name : builtinNames) { + plugins.writeEntry(name + QStringLiteral("Enabled"), false); + } + + config->sync(); + kwinApp()->setConfig(config); + + qputenv("XCURSOR_THEME", QByteArrayLiteral("DMZ-White")); + qputenv("XCURSOR_SIZE", QByteArrayLiteral("24")); + qputenv("KWIN_COMPOSE", QByteArrayLiteral("O2")); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QVERIFY(KWin::Compositor::self()); + + // Add directory with fake decorations to the plugin search path. + QCoreApplication::addLibraryPath( + QDir(QCoreApplication::applicationDirPath()).absoluteFilePath("fakes") + ); + + // Change decoration theme. + KConfigGroup group = kwinApp()->config()->group("org.kde.kdecoration2"); + group.writeEntry("library", "org.kde.test.fakedecowithshadows"); + group.sync(); + Workspace::self()->slotReconfigure(); + + auto scene = KWin::Compositor::self()->scene(); + QVERIFY(scene); + QCOMPARE(scene->compositingType(), KWin::OpenGL2Compositing); + +} + +void SceneOpenGLShadowTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +namespace { + const int SHADOW_SIZE = 128; + + const int SHADOW_OFFSET_TOP = 64; + const int SHADOW_OFFSET_LEFT = 48; + + // NOTE: We assume deco shadows are generated with blur so that's + // why there is 4, 1 is the size of the inner shadow rect. + const int SHADOW_TEXTURE_WIDTH = 4 * SHADOW_SIZE + 1; + const int SHADOW_TEXTURE_HEIGHT = 4 * SHADOW_SIZE + 1; + + const int SHADOW_PADDING_TOP = SHADOW_SIZE - SHADOW_OFFSET_TOP; + const int SHADOW_PADDING_RIGHT = SHADOW_SIZE + SHADOW_OFFSET_LEFT; + const int SHADOW_PADDING_BOTTOM = SHADOW_SIZE + SHADOW_OFFSET_TOP; + const int SHADOW_PADDING_LEFT = SHADOW_SIZE - SHADOW_OFFSET_LEFT; + + const QRectF SHADOW_INNER_RECT(2 * SHADOW_SIZE, 2 * SHADOW_SIZE, 1, 1); +} + +void SceneOpenGLShadowTest::testShadowTileOverlaps_data() +{ + QTest::addColumn("windowSize"); + QTest::addColumn("expectedQuads"); + + // Precompute shadow tile geometries(in texture's space). + const QRectF topLeftTile( + 0, + 0, + SHADOW_INNER_RECT.x(), + SHADOW_INNER_RECT.y()); + const QRectF topRightTile( + SHADOW_INNER_RECT.right(), + 0, + SHADOW_TEXTURE_WIDTH - SHADOW_INNER_RECT.right(), + SHADOW_INNER_RECT.y()); + const QRectF topTile(topLeftTile.topRight(), topRightTile.bottomLeft()); + + const QRectF bottomLeftTile( + 0, + SHADOW_INNER_RECT.bottom(), + SHADOW_INNER_RECT.x(), + SHADOW_TEXTURE_HEIGHT - SHADOW_INNER_RECT.bottom()); + const QRectF bottomRightTile( + SHADOW_INNER_RECT.right(), + SHADOW_INNER_RECT.bottom(), + SHADOW_TEXTURE_WIDTH - SHADOW_INNER_RECT.right(), + SHADOW_TEXTURE_HEIGHT - SHADOW_INNER_RECT.bottom()); + const QRectF bottomTile(bottomLeftTile.topRight(), bottomRightTile.bottomLeft()); + + const QRectF leftTile(topLeftTile.bottomLeft(), bottomLeftTile.topRight()); + const QRectF rightTile(topRightTile.bottomLeft(), bottomRightTile.topRight()); + + qreal tx1 = 0; + qreal ty1 = 0; + qreal tx2 = 0; + qreal ty2 = 0; + + // Explanation behind numbers: (256+1 x 256+1) is the minimum window size + // which doesn't cause overlapping of shadow tiles. For example, if a window + // has (256 x 256+1) size, top-left and top-right or bottom-left and + // bottom-right shadow tiles overlap. + + // No overlaps: In this case corner tiles are rendered as they are, + // and top/right/bottom/left tiles are stretched. + { + const QSize windowSize(256 + 1, 256 + 1); + WindowQuadList shadowQuads; + + const QRectF outerRect( + -SHADOW_PADDING_LEFT, + -SHADOW_PADDING_TOP, + windowSize.width() + SHADOW_PADDING_LEFT + SHADOW_PADDING_RIGHT, + windowSize.height() + SHADOW_PADDING_TOP + SHADOW_PADDING_BOTTOM); + + const QRectF topLeft( + outerRect.left(), + outerRect.top(), + topLeftTile.width(), + topLeftTile.height()); + tx1 = topLeftTile.left() / SHADOW_TEXTURE_WIDTH; + ty1 = topLeftTile.top() / SHADOW_TEXTURE_HEIGHT; + tx2 = topLeftTile.right() / SHADOW_TEXTURE_WIDTH; + ty2 = topLeftTile.bottom() / SHADOW_TEXTURE_HEIGHT; + shadowQuads << makeShadowQuad(topLeft, tx1, ty1, tx2, ty2); + + const QRectF topRight( + outerRect.right() - topRightTile.width(), + outerRect.top(), + topRightTile.width(), + topRightTile.height()); + tx1 = topRightTile.left() / SHADOW_TEXTURE_WIDTH; + ty1 = topRightTile.top() / SHADOW_TEXTURE_HEIGHT; + tx2 = topRightTile.right() / SHADOW_TEXTURE_WIDTH; + ty2 = topRightTile.bottom() / SHADOW_TEXTURE_HEIGHT; + shadowQuads << makeShadowQuad(topRight, tx1, ty1, tx2, ty2); + + const QRectF top(topLeft.topRight(), topRight.bottomLeft()); + tx1 = topTile.left() / SHADOW_TEXTURE_WIDTH; + ty1 = topTile.top() / SHADOW_TEXTURE_HEIGHT; + tx2 = topTile.right() / SHADOW_TEXTURE_WIDTH; + ty2 = topTile.bottom() / SHADOW_TEXTURE_HEIGHT; + shadowQuads << makeShadowQuad(top, tx1, ty1, tx2, ty2); + + const QRectF bottomLeft( + outerRect.left(), + outerRect.bottom() - bottomLeftTile.height(), + bottomLeftTile.width(), + bottomLeftTile.height()); + tx1 = bottomLeftTile.left() / SHADOW_TEXTURE_WIDTH; + ty1 = bottomLeftTile.top() / SHADOW_TEXTURE_HEIGHT; + tx2 = bottomLeftTile.right() / SHADOW_TEXTURE_WIDTH; + ty2 = bottomLeftTile.bottom() / SHADOW_TEXTURE_HEIGHT; + shadowQuads << makeShadowQuad(bottomLeft, tx1, ty1, tx2, ty2); + + const QRectF bottomRight( + outerRect.right() - bottomRightTile.width(), + outerRect.bottom() - bottomRightTile.height(), + bottomRightTile.width(), + bottomRightTile.height()); + tx1 = bottomRightTile.left() / SHADOW_TEXTURE_WIDTH; + ty1 = bottomRightTile.top() / SHADOW_TEXTURE_HEIGHT; + tx2 = bottomRightTile.right() / SHADOW_TEXTURE_WIDTH; + ty2 = bottomRightTile.bottom() / SHADOW_TEXTURE_HEIGHT; + shadowQuads << makeShadowQuad(bottomRight, tx1, ty1, tx2, ty2); + + const QRectF bottom(bottomLeft.topRight(), bottomRight.bottomLeft()); + tx1 = bottomTile.left() / SHADOW_TEXTURE_WIDTH; + ty1 = bottomTile.top() / SHADOW_TEXTURE_HEIGHT; + tx2 = bottomTile.right() / SHADOW_TEXTURE_WIDTH; + ty2 = bottomTile.bottom() / SHADOW_TEXTURE_HEIGHT; + shadowQuads << makeShadowQuad(bottom, tx1, ty1, tx2, ty2); + + const QRectF left(topLeft.bottomLeft(), bottomLeft.topRight()); + tx1 = leftTile.left() / SHADOW_TEXTURE_WIDTH; + ty1 = leftTile.top() / SHADOW_TEXTURE_HEIGHT; + tx2 = leftTile.right() / SHADOW_TEXTURE_WIDTH; + ty2 = leftTile.bottom() / SHADOW_TEXTURE_HEIGHT; + shadowQuads << makeShadowQuad(left, tx1, ty1, tx2, ty2); + + const QRectF right(topRight.bottomLeft(), bottomRight.topRight()); + tx1 = rightTile.left() / SHADOW_TEXTURE_WIDTH; + ty1 = rightTile.top() / SHADOW_TEXTURE_HEIGHT; + tx2 = rightTile.right() / SHADOW_TEXTURE_WIDTH; + ty2 = rightTile.bottom() / SHADOW_TEXTURE_HEIGHT; + shadowQuads << makeShadowQuad(right, tx1, ty1, tx2, ty2); + + QTest::newRow("no overlaps") << windowSize << shadowQuads; + } + + // Top-Left & Bottom-Left/Top-Right & Bottom-Right overlap: + // In this case overlapping parts are clipped and left/right + // tiles aren't rendered. + const QVector> verticalOverlapTestTable { + QPair { + QByteArray("top-left & bottom-left/top-right & bottom-right overlap"), + QSize(256 + 1, 256) + }, + QPair { + QByteArray("top-left & bottom-left/top-right & bottom-right overlap :: pre"), + QSize(256 + 1, 256 - 1) + } + // No need to test the case when window size is QSize(256 + 1, 256 + 1). + // It has been tested already (no overlaps test case). + }; + + for (auto const &tt : verticalOverlapTestTable) { + const char *testName = tt.first.constData(); + const QSize windowSize = tt.second; + + WindowQuadList shadowQuads; + qreal halfOverlap = 0.0; + + const QRectF outerRect( + -SHADOW_PADDING_LEFT, + -SHADOW_PADDING_TOP, + windowSize.width() + SHADOW_PADDING_LEFT + SHADOW_PADDING_RIGHT, + windowSize.height() + SHADOW_PADDING_TOP + SHADOW_PADDING_BOTTOM); + + QRectF topLeft( + outerRect.left(), + outerRect.top(), + topLeftTile.width(), + topLeftTile.height()); + + QRectF bottomLeft( + outerRect.left(), + outerRect.bottom() - bottomLeftTile.height(), + bottomLeftTile.width(), + bottomLeftTile.height()); + + halfOverlap = qAbs(topLeft.bottom() - bottomLeft.top()) / 2; + topLeft.setBottom(topLeft.bottom() - halfOverlap); + bottomLeft.setTop(bottomLeft.top() + halfOverlap); + + tx1 = topLeftTile.left() / SHADOW_TEXTURE_WIDTH; + ty1 = topLeftTile.top() / SHADOW_TEXTURE_HEIGHT; + tx2 = topLeftTile.right() / SHADOW_TEXTURE_WIDTH; + ty2 = topLeft.height() / SHADOW_TEXTURE_HEIGHT; + shadowQuads << makeShadowQuad(topLeft, tx1, ty1, tx2, ty2); + + tx1 = bottomLeftTile.left() / SHADOW_TEXTURE_WIDTH; + ty1 = 1.0 - (bottomLeft.height() / SHADOW_TEXTURE_HEIGHT); + tx2 = bottomLeftTile.right() / SHADOW_TEXTURE_WIDTH; + ty2 = bottomLeftTile.bottom() / SHADOW_TEXTURE_HEIGHT; + shadowQuads << makeShadowQuad(bottomLeft, tx1, ty1, tx2, ty2); + + QRectF topRight( + outerRect.right() - topRightTile.width(), + outerRect.top(), + topRightTile.width(), + topRightTile.height()); + + QRectF bottomRight( + outerRect.right() - bottomRightTile.width(), + outerRect.bottom() - bottomRightTile.height(), + bottomRightTile.width(), + bottomRightTile.height()); + + halfOverlap = qAbs(topRight.bottom() - bottomRight.top()) / 2; + topRight.setBottom(topRight.bottom() - halfOverlap); + bottomRight.setTop(bottomRight.top() + halfOverlap); + + tx1 = topRightTile.left() / SHADOW_TEXTURE_WIDTH; + ty1 = topRightTile.top() / SHADOW_TEXTURE_HEIGHT; + tx2 = topRightTile.right() / SHADOW_TEXTURE_WIDTH; + ty2 = topRight.height() / SHADOW_TEXTURE_HEIGHT; + shadowQuads << makeShadowQuad(topRight, tx1, ty1, tx2, ty2); + + tx1 = bottomRightTile.left() / SHADOW_TEXTURE_WIDTH; + ty1 = 1.0 - (bottomRight.height() / SHADOW_TEXTURE_HEIGHT); + tx2 = bottomRightTile.right() / SHADOW_TEXTURE_WIDTH; + ty2 = bottomRightTile.bottom() / SHADOW_TEXTURE_HEIGHT; + shadowQuads << makeShadowQuad(bottomRight, tx1, ty1, tx2, ty2); + + const QRectF top(topLeft.topRight(), topRight.bottomLeft()); + tx1 = topTile.left() / SHADOW_TEXTURE_WIDTH; + ty1 = topTile.top() / SHADOW_TEXTURE_HEIGHT; + tx2 = topTile.right() / SHADOW_TEXTURE_WIDTH; + ty2 = top.height() / SHADOW_TEXTURE_HEIGHT; + shadowQuads << makeShadowQuad(top, tx1, ty1, tx2, ty2); + + const QRectF bottom(bottomLeft.topRight(), bottomRight.bottomLeft()); + tx1 = bottomTile.left() / SHADOW_TEXTURE_WIDTH; + ty1 = 1.0 - (bottom.height() / SHADOW_TEXTURE_HEIGHT); + tx2 = bottomTile.right() / SHADOW_TEXTURE_WIDTH; + ty2 = bottomTile.bottom() / SHADOW_TEXTURE_HEIGHT; + shadowQuads << makeShadowQuad(bottom, tx1, ty1, tx2, ty2); + + QTest::newRow(testName) << windowSize << shadowQuads; + } + + // Top-Left & Top-Right/Bottom-Left & Bottom-Right overlap: + // In this case overlapping parts are clipped and top/bottom + // tiles aren't rendered. + const QVector> horizontalOverlapTestTable { + QPair { + QByteArray("top-left & top-right/bottom-left & bottom-right overlap"), + QSize(256, 256 + 1) + }, + QPair { + QByteArray("top-left & top-right/bottom-left & bottom-right overlap :: pre"), + QSize(256 - 1, 256 + 1) + } + // No need to test the case when window size is QSize(256 + 1, 256 + 1). + // It has been tested already (no overlaps test case). + }; + + for (auto const &tt : horizontalOverlapTestTable) { + const char *testName = tt.first.constData(); + const QSize windowSize = tt.second; + + WindowQuadList shadowQuads; + qreal halfOverlap = 0.0; + + const QRectF outerRect( + -SHADOW_PADDING_LEFT, + -SHADOW_PADDING_TOP, + windowSize.width() + SHADOW_PADDING_LEFT + SHADOW_PADDING_RIGHT, + windowSize.height() + SHADOW_PADDING_TOP + SHADOW_PADDING_BOTTOM); + + QRectF topLeft( + outerRect.left(), + outerRect.top(), + topLeftTile.width(), + topLeftTile.height()); + + QRectF topRight( + outerRect.right() - topRightTile.width(), + outerRect.top(), + topRightTile.width(), + topRightTile.height()); + + halfOverlap = qAbs(topLeft.right() - topRight.left()) / 2; + topLeft.setRight(topLeft.right() - halfOverlap); + topRight.setLeft(topRight.left() + halfOverlap); + + tx1 = topLeftTile.left() / SHADOW_TEXTURE_WIDTH; + ty1 = topLeftTile.top() / SHADOW_TEXTURE_HEIGHT; + tx2 = topLeft.width() / SHADOW_TEXTURE_WIDTH; + ty2 = topLeftTile.bottom() / SHADOW_TEXTURE_HEIGHT; + shadowQuads << makeShadowQuad(topLeft, tx1, ty1, tx2, ty2); + + tx1 = 1.0 - (topRight.width() / SHADOW_TEXTURE_WIDTH); + ty1 = topRightTile.top() / SHADOW_TEXTURE_HEIGHT; + tx2 = topRightTile.right() / SHADOW_TEXTURE_WIDTH; + ty2 = topRightTile.bottom() / SHADOW_TEXTURE_HEIGHT; + shadowQuads << makeShadowQuad(topRight, tx1, ty1, tx2, ty2); + + QRectF bottomLeft( + outerRect.left(), + outerRect.bottom() - bottomLeftTile.height(), + bottomLeftTile.width(), + bottomLeftTile.height()); + + QRectF bottomRight( + outerRect.right() - bottomRightTile.width(), + outerRect.bottom() - bottomRightTile.height(), + bottomRightTile.width(), + bottomRightTile.height()); + + halfOverlap = qAbs(bottomLeft.right() - bottomRight.left()) / 2; + bottomLeft.setRight(bottomLeft.right() - halfOverlap); + bottomRight.setLeft(bottomRight.left() + halfOverlap); + + tx1 = bottomLeftTile.left() / SHADOW_TEXTURE_WIDTH; + ty1 = bottomLeftTile.top() / SHADOW_TEXTURE_HEIGHT; + tx2 = bottomLeft.width() / SHADOW_TEXTURE_WIDTH; + ty2 = bottomLeftTile.bottom() / SHADOW_TEXTURE_HEIGHT; + shadowQuads << makeShadowQuad(bottomLeft, tx1, ty1, tx2, ty2); + + tx1 = 1.0 - (bottomRight.width() / SHADOW_TEXTURE_WIDTH); + ty1 = bottomRightTile.top() / SHADOW_TEXTURE_HEIGHT; + tx2 = bottomRightTile.right() / SHADOW_TEXTURE_WIDTH; + ty2 = bottomRightTile.bottom() / SHADOW_TEXTURE_HEIGHT; + shadowQuads << makeShadowQuad(bottomRight, tx1, ty1, tx2, ty2); + + const QRectF left(topLeft.bottomLeft(), bottomLeft.topRight()); + tx1 = leftTile.left() / SHADOW_TEXTURE_WIDTH; + ty1 = leftTile.top() / SHADOW_TEXTURE_HEIGHT; + tx2 = left.width() / SHADOW_TEXTURE_WIDTH; + ty2 = leftTile.bottom() / SHADOW_TEXTURE_HEIGHT; + shadowQuads << makeShadowQuad(left, tx1, ty1, tx2, ty2); + + const QRectF right(topRight.bottomLeft(), bottomRight.topRight()); + tx1 = 1.0 - (right.width() / SHADOW_TEXTURE_WIDTH); + ty1 = rightTile.top() / SHADOW_TEXTURE_HEIGHT; + tx2 = rightTile.right() / SHADOW_TEXTURE_WIDTH; + ty2 = rightTile.bottom() / SHADOW_TEXTURE_HEIGHT; + shadowQuads << makeShadowQuad(right, tx1, ty1, tx2, ty2); + + QTest::newRow(testName) << windowSize << shadowQuads; + } + + // All shadow tiles overlap: In this case all overlapping parts + // are clippend and top/right/bottom/left tiles aren't rendered. + const QVector> allOverlapTestTable { + QPair { + QByteArray("all corner tiles overlap"), + QSize(256, 256) + }, + QPair { + QByteArray("all corner tiles overlap :: pre"), + QSize(256 - 1, 256 - 1) + } + // No need to test the case when window size is QSize(256 + 1, 256 + 1). + // It has been tested already (no overlaps test case). + }; + + for (auto const &tt : allOverlapTestTable) { + const char *testName = tt.first.constData(); + const QSize windowSize = tt.second; + + WindowQuadList shadowQuads; + qreal halfOverlap = 0.0; + + const QRectF outerRect( + -SHADOW_PADDING_LEFT, + -SHADOW_PADDING_TOP, + windowSize.width() + SHADOW_PADDING_LEFT + SHADOW_PADDING_RIGHT, + windowSize.height() + SHADOW_PADDING_TOP + SHADOW_PADDING_BOTTOM); + + QRectF topLeft( + outerRect.left(), + outerRect.top(), + topLeftTile.width(), + topLeftTile.height()); + + QRectF topRight( + outerRect.right() - topRightTile.width(), + outerRect.top(), + topRightTile.width(), + topRightTile.height()); + + QRectF bottomLeft( + outerRect.left(), + outerRect.bottom() - bottomLeftTile.height(), + bottomLeftTile.width(), + bottomLeftTile.height()); + + QRectF bottomRight( + outerRect.right() - bottomRightTile.width(), + outerRect.bottom() - bottomRightTile.height(), + bottomRightTile.width(), + bottomRightTile.height()); + + halfOverlap = qAbs(topLeft.right() - topRight.left()) / 2; + topLeft.setRight(topLeft.right() - halfOverlap); + topRight.setLeft(topRight.left() + halfOverlap); + + halfOverlap = qAbs(bottomLeft.right() - bottomRight.left()) / 2; + bottomLeft.setRight(bottomLeft.right() - halfOverlap); + bottomRight.setLeft(bottomRight.left() + halfOverlap); + + halfOverlap = qAbs(topLeft.bottom() - bottomLeft.top()) / 2; + topLeft.setBottom(topLeft.bottom() - halfOverlap); + bottomLeft.setTop(bottomLeft.top() + halfOverlap); + + halfOverlap = qAbs(topRight.bottom() - bottomRight.top()) / 2; + topRight.setBottom(topRight.bottom() - halfOverlap); + bottomRight.setTop(bottomRight.top() + halfOverlap); + + tx1 = topLeftTile.left() / SHADOW_TEXTURE_WIDTH; + ty1 = topLeftTile.top() / SHADOW_TEXTURE_HEIGHT; + tx2 = topLeft.width() / SHADOW_TEXTURE_WIDTH; + ty2 = topLeft.height() / SHADOW_TEXTURE_HEIGHT; + shadowQuads << makeShadowQuad(topLeft, tx1, ty1, tx2, ty2); + + tx1 = 1.0 - (topRight.width() / SHADOW_TEXTURE_WIDTH); + ty1 = topRightTile.top() / SHADOW_TEXTURE_HEIGHT; + tx2 = topRightTile.right() / SHADOW_TEXTURE_WIDTH; + ty2 = topRight.height() / SHADOW_TEXTURE_HEIGHT; + shadowQuads << makeShadowQuad(topRight, tx1, ty1, tx2, ty2); + + tx1 = bottomLeftTile.left() / SHADOW_TEXTURE_WIDTH; + ty1 = 1.0 - (bottomLeft.height() / SHADOW_TEXTURE_HEIGHT); + tx2 = bottomLeft.width() / SHADOW_TEXTURE_WIDTH; + ty2 = bottomLeftTile.bottom() / SHADOW_TEXTURE_HEIGHT; + shadowQuads << makeShadowQuad(bottomLeft, tx1, ty1, tx2, ty2); + + tx1 = 1.0 - (bottomRight.width() / SHADOW_TEXTURE_WIDTH); + ty1 = 1.0 - (bottomRight.height() / SHADOW_TEXTURE_HEIGHT); + tx2 = bottomRightTile.right() / SHADOW_TEXTURE_WIDTH; + ty2 = bottomRightTile.bottom() / SHADOW_TEXTURE_HEIGHT; + shadowQuads << makeShadowQuad(bottomRight, tx1, ty1, tx2, ty2); + + QTest::newRow(testName) << windowSize << shadowQuads; + } + + // Window is too small: do not render any shadow tiles. + { + const QSize windowSize(1, 1); + const WindowQuadList shadowQuads; + + QTest::newRow("window is too small") << windowSize << shadowQuads; + } +} + +void SceneOpenGLShadowTest::testShadowTileOverlaps() +{ + QFETCH(QSize, windowSize); + QFETCH(WindowQuadList, expectedQuads); + + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Decoration)); + + // Create a decorated client. + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QScopedPointer ssd(Test::waylandServerSideDecoration()->create(surface.data())); + + auto *client = Test::renderAndWaitForShown(surface.data(), windowSize, Qt::blue); + + QSignalSpy sizeChangedSpy(shellSurface.data(), &XdgShellSurface::sizeChanged); + QVERIFY(sizeChangedSpy.isValid()); + + // Check the client is decorated. + QVERIFY(client); + QVERIFY(client->isDecorated()); + auto *decoration = client->decoration(); + QVERIFY(decoration); + + // If speciefied decoration theme is not found, KWin loads a default one + // so we have to check whether a client has right decoration. + auto decoShadow = decoration->shadow(); + QCOMPARE(decoShadow->shadow().size(), QSize(SHADOW_TEXTURE_WIDTH, SHADOW_TEXTURE_HEIGHT)); + QCOMPARE(decoShadow->paddingTop(), SHADOW_PADDING_TOP); + QCOMPARE(decoShadow->paddingRight(), SHADOW_PADDING_RIGHT); + QCOMPARE(decoShadow->paddingBottom(), SHADOW_PADDING_BOTTOM); + QCOMPARE(decoShadow->paddingLeft(), SHADOW_PADDING_LEFT); + + // Get shadow. + QVERIFY(client->effectWindow()); + QVERIFY(client->effectWindow()->sceneWindow()); + QVERIFY(client->effectWindow()->sceneWindow()->shadow()); + auto *shadow = client->effectWindow()->sceneWindow()->shadow(); + + // Validate shadow quads. + const WindowQuadList &quads = shadow->shadowQuads(); + QCOMPARE(quads.size(), expectedQuads.size()); + + QVector mask(expectedQuads.size(), false); + for (const auto &q : quads) { + for (int i = 0; i < expectedQuads.size(); i++) { + if (!compareQuads(q, expectedQuads[i])) { + continue; + } + if (!mask[i]) { + mask[i] = true; + break; + } else { + QFAIL("got a duplicate shadow quad"); + } + } + } + + for (const auto &v : qAsConst(mask)) { + if (!v) { + QFAIL("missed a shadow quad"); + } + } +} + +void SceneOpenGLShadowTest::testNoCornerShadowTiles() +{ + // this test verifies that top/right/bottom/left shadow tiles are + // still drawn even when corner tiles are missing + + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::ShadowManager)); + + // Create a surface. + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + auto *client = Test::renderAndWaitForShown(surface.data(), QSize(512, 512), Qt::blue); + QVERIFY(client); + QVERIFY(!client->isDecorated()); + + // Render reference shadow texture with the following params: + // - shadow size: 128 + // - inner rect size: 1 + // - padding: 128 + QImage referenceShadowTexture(256 + 1, 256 + 1, QImage::Format_ARGB32_Premultiplied); + referenceShadowTexture.fill(Qt::transparent); + + // We don't care about content of the shadow. + + // Submit the shadow to KWin. + QScopedPointer clientShadow(Test::waylandShadowManager()->createShadow(surface.data())); + QVERIFY(clientShadow->isValid()); + + auto *shmPool = Test::waylandShmPool(); + + Buffer::Ptr bufferTop = shmPool->createBuffer( + referenceShadowTexture.copy(QRect(128, 0, 1, 128))); + clientShadow->attachTop(bufferTop); + + Buffer::Ptr bufferRight = shmPool->createBuffer( + referenceShadowTexture.copy(QRect(128 + 1, 128, 128, 1))); + clientShadow->attachRight(bufferRight); + + Buffer::Ptr bufferBottom = shmPool->createBuffer( + referenceShadowTexture.copy(QRect(128, 128 + 1, 1, 128))); + clientShadow->attachBottom(bufferBottom); + + Buffer::Ptr bufferLeft = shmPool->createBuffer( + referenceShadowTexture.copy(QRect(0, 128, 128, 1))); + clientShadow->attachLeft(bufferLeft); + + clientShadow->setOffsets(QMarginsF(128, 128, 128, 128)); + + QSignalSpy shadowChangedSpy(client->surface(), &KWaylandServer::SurfaceInterface::shadowChanged); + QVERIFY(shadowChangedSpy.isValid()); + clientShadow->commit(); + surface->commit(Surface::CommitFlag::None); + QVERIFY(shadowChangedSpy.wait()); + + // Check that we got right shadow from the client. + QPointer shadowIface = client->surface()->shadow(); + QVERIFY(!shadowIface.isNull()); + QCOMPARE(shadowIface->offset().left(), 128.0); + QCOMPARE(shadowIface->offset().top(), 128.0); + QCOMPARE(shadowIface->offset().right(), 128.0); + QCOMPARE(shadowIface->offset().bottom(), 128.0); + + QVERIFY(client->effectWindow()); + QVERIFY(client->effectWindow()->sceneWindow()); + KWin::Shadow *shadow = client->effectWindow()->sceneWindow()->shadow(); + QVERIFY(shadow != nullptr); + + const WindowQuadList &quads = shadow->shadowQuads(); + QCOMPARE(quads.count(), 4); + + // Shadow size: 128 + // Padding: QMargins(128, 128, 128, 128) + // Inner rect: QRect(128, 128, 1, 1) + // Texture size: QSize(257, 257) + // Window size: QSize(512, 512) + WindowQuadList expectedQuads; + expectedQuads << makeShadowQuad(QRectF( 0, -128, 512, 128), 128.0 / 257.0, 0.0, 129.0 / 257.0, 128.0 / 257.0); // top + expectedQuads << makeShadowQuad(QRectF( 512, 0, 128, 512), 129.0 / 257.0, 128.0 / 257.0, 1.0, 129.0 / 257.0); // right + expectedQuads << makeShadowQuad(QRectF( 0, 512, 512, 128), 128.0 / 257.0, 129.0 / 257.0, 129.0 / 257.0, 1.0); // bottom + expectedQuads << makeShadowQuad(QRectF(-128, 0, 128, 512), 0.0, 128.0 / 257.0, 128.0 / 257.0, 129.0 / 257.0); // left + + for (const WindowQuad &expectedQuad : expectedQuads) { + auto it = std::find_if(quads.constBegin(), quads.constEnd(), + [&expectedQuad](const WindowQuad &quad) { + return compareQuads(quad, expectedQuad); + }); + if (it == quads.constEnd()) { + const QString message = QStringLiteral("Missing shadow quad (left: %1, top: %2, right: %3, bottom: %4)") + .arg(expectedQuad.left()) + .arg(expectedQuad.top()) + .arg(expectedQuad.right()) + .arg(expectedQuad.bottom()); + const QByteArray rawMessage = message.toLocal8Bit().data(); + QFAIL(rawMessage.data()); + } + } +} + +void SceneOpenGLShadowTest::testDistributeHugeCornerTiles() +{ + // this test verifies that huge corner tiles are distributed correctly + + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::ShadowManager)); + + // Create a surface. + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + auto *client = Test::renderAndWaitForShown(surface.data(), QSize(64, 64), Qt::blue); + QVERIFY(client); + QVERIFY(!client->isDecorated()); + + // Submit the shadow to KWin. + QScopedPointer clientShadow(Test::waylandShadowManager()->createShadow(surface.data())); + QVERIFY(clientShadow->isValid()); + + QImage referenceTileTexture(512, 512, QImage::Format_ARGB32_Premultiplied); + referenceTileTexture.fill(Qt::transparent); + + auto *shmPool = Test::waylandShmPool(); + + Buffer::Ptr bufferTopLeft = shmPool->createBuffer(referenceTileTexture); + clientShadow->attachTopLeft(bufferTopLeft); + + Buffer::Ptr bufferTopRight = shmPool->createBuffer(referenceTileTexture); + clientShadow->attachTopRight(bufferTopRight); + + clientShadow->setOffsets(QMarginsF(256, 256, 256, 0)); + + QSignalSpy shadowChangedSpy(client->surface(), &KWaylandServer::SurfaceInterface::shadowChanged); + QVERIFY(shadowChangedSpy.isValid()); + clientShadow->commit(); + surface->commit(Surface::CommitFlag::None); + QVERIFY(shadowChangedSpy.wait()); + + // Check that we got right shadow from the client. + QPointer shadowIface = client->surface()->shadow(); + QVERIFY(!shadowIface.isNull()); + QCOMPARE(shadowIface->offset().left(), 256.0); + QCOMPARE(shadowIface->offset().top(), 256.0); + QCOMPARE(shadowIface->offset().right(), 256.0); + QCOMPARE(shadowIface->offset().bottom(), 0.0); + + QVERIFY(client->effectWindow()); + QVERIFY(client->effectWindow()->sceneWindow()); + KWin::Shadow *shadow = client->effectWindow()->sceneWindow()->shadow(); + QVERIFY(shadow != nullptr); + + WindowQuadList expectedQuads; + + // Top-left quad + expectedQuads << makeShadowQuad( + QRectF(-256, -256, 256 + 32, 256 + 64), + 0.0, 0.0, (256.0 + 32.0) / 1024.0, (256.0 + 64.0) / 512.0); + + // Top-right quad + expectedQuads << makeShadowQuad( + QRectF(32, -256, 256 + 32, 256 + 64), + 1.0 - (256.0 + 32.0) / 1024.0, 0.0, 1.0, (256.0 + 64.0) / 512.0); + + const WindowQuadList &quads = shadow->shadowQuads(); + QCOMPARE(quads.count(), expectedQuads.count()); + + for (const WindowQuad &expectedQuad : expectedQuads) { + auto it = std::find_if(quads.constBegin(), quads.constEnd(), + [&expectedQuad](const WindowQuad &quad) { + return compareQuads(quad, expectedQuad); + }); + if (it == quads.constEnd()) { + const QString message = QStringLiteral("Missing shadow quad (left: %1, top: %2, right: %3, bottom: %4)") + .arg(expectedQuad.left()) + .arg(expectedQuad.top()) + .arg(expectedQuad.right()) + .arg(expectedQuad.bottom()); + const QByteArray rawMessage = message.toLocal8Bit().data(); + QFAIL(rawMessage.data()); + } + } +} + +WAYLANDTEST_MAIN(SceneOpenGLShadowTest) +#include "scene_opengl_shadow_test.moc" diff --git a/autotests/integration/scene_opengl_test.cpp b/autotests/integration/scene_opengl_test.cpp new file mode 100644 index 0000000..c9d4575 --- /dev/null +++ b/autotests/integration/scene_opengl_test.cpp @@ -0,0 +1,19 @@ +/* + 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/autotests/integration/scene_qpainter_shadow_test.cpp b/autotests/integration/scene_qpainter_shadow_test.cpp new file mode 100644 index 0000000..11ba2fa --- /dev/null +++ b/autotests/integration/scene_qpainter_shadow_test.cpp @@ -0,0 +1,769 @@ +/* + 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 +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "kwin_wayland_test.h" + +#include "abstract_client.h" +#include "composite.h" +#include "effect_builtins.h" +#include "effectloader.h" +#include "effects.h" +#include "platform.h" +#include "plugins/scenes/qpainter/scene_qpainter.h" +#include "shadow.h" +#include "wayland_server.h" +#include "workspace.h" + +Q_DECLARE_METATYPE(KWin::WindowQuadList) + +using namespace KWin; +using namespace KWayland::Client; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_scene_qpainter_shadow-0"); + +class SceneQPainterShadowTest : public QObject +{ + Q_OBJECT + +public: + SceneQPainterShadowTest() {} + +private Q_SLOTS: + void initTestCase(); + void cleanup(); + + void testShadowTileOverlaps_data(); + void testShadowTileOverlaps(); + void testShadowTextureReconstruction(); + +}; + +inline bool isClose(double a, double b, double eps = 1e-5) +{ + if (a == b) { + return true; + } + const double diff = std::fabs(a - b); + if (a == 0 || b == 0) { + return diff < eps; + } + return diff / std::max(a, b) < eps; +} + +inline bool compareQuads(const WindowQuad &a, const WindowQuad &b) +{ + for (int i = 0; i < 4; i++) { + if (!isClose(a[i].x(), b[i].x()) + || !isClose(a[i].y(), b[i].y()) + || !isClose(a[i].textureX(), b[i].textureX()) + || !isClose(a[i].textureY(), b[i].textureY())) { + return false; + } + } + return true; +} + +inline WindowQuad makeShadowQuad(const QRectF &geo, qreal tx1, qreal ty1, qreal tx2, qreal ty2) +{ + WindowQuad quad(WindowQuadShadow); + quad[0] = WindowVertex(geo.left(), geo.top(), tx1, ty1); + quad[1] = WindowVertex(geo.right(), geo.top(), tx2, ty1); + quad[2] = WindowVertex(geo.right(), geo.bottom(), tx2, ty2); + quad[3] = WindowVertex(geo.left(), geo.bottom(), tx1, ty2); + return quad; +} + +void SceneQPainterShadowTest::initTestCase() +{ + // Copied from scene_qpainter_test.cpp + + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + // 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")); + ScriptedEffectLoader loader; + const auto builtinNames = BuiltInEffects::availableEffectNames() << loader.listOfKnownEffects(); + for (QString name : builtinNames) { + plugins.writeEntry(name + QStringLiteral("Enabled"), false); + } + + config->sync(); + kwinApp()->setConfig(config); + + if (!QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("icons/DMZ-White/index.theme")).isEmpty()) { + qputenv("XCURSOR_THEME", QByteArrayLiteral("DMZ-White")); + } else { + // might be vanilla-dmz (e.g. Arch, FreeBSD) + qputenv("XCURSOR_THEME", QByteArrayLiteral("Vanilla-DMZ")); + } + qputenv("XCURSOR_SIZE", QByteArrayLiteral("24")); + qputenv("KWIN_COMPOSE", QByteArrayLiteral("Q")); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QVERIFY(KWin::Compositor::self()); + + // Add directory with fake decorations to the plugin search path. + QCoreApplication::addLibraryPath( + QDir(QCoreApplication::applicationDirPath()).absoluteFilePath("fakes") + ); + + // Change decoration theme. + KConfigGroup group = kwinApp()->config()->group("org.kde.kdecoration2"); + group.writeEntry("library", "org.kde.test.fakedecowithshadows"); + group.sync(); + Workspace::self()->slotReconfigure(); +} + +void SceneQPainterShadowTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +namespace { + const int SHADOW_SIZE = 128; + + const int SHADOW_OFFSET_TOP = 64; + const int SHADOW_OFFSET_LEFT = 48; + + // NOTE: We assume deco shadows are generated with blur so that's + // why there is 4, 1 is the size of the inner shadow rect. + const int SHADOW_TEXTURE_WIDTH = 4 * SHADOW_SIZE + 1; + const int SHADOW_TEXTURE_HEIGHT = 4 * SHADOW_SIZE + 1; + + const int SHADOW_PADDING_TOP = SHADOW_SIZE - SHADOW_OFFSET_TOP; + const int SHADOW_PADDING_RIGHT = SHADOW_SIZE + SHADOW_OFFSET_LEFT; + const int SHADOW_PADDING_BOTTOM = SHADOW_SIZE + SHADOW_OFFSET_TOP; + const int SHADOW_PADDING_LEFT = SHADOW_SIZE - SHADOW_OFFSET_LEFT; + + const QRectF SHADOW_INNER_RECT(2 * SHADOW_SIZE, 2 * SHADOW_SIZE, 1, 1); +} + +void SceneQPainterShadowTest::testShadowTileOverlaps_data() +{ + QTest::addColumn("windowSize"); + QTest::addColumn("expectedQuads"); + + // Precompute shadow tile geometries(in texture's space). + const QRectF topLeftTile( + 0, + 0, + SHADOW_INNER_RECT.x(), + SHADOW_INNER_RECT.y()); + const QRectF topRightTile( + SHADOW_INNER_RECT.right(), + 0, + SHADOW_TEXTURE_WIDTH - SHADOW_INNER_RECT.right(), + SHADOW_INNER_RECT.y()); + const QRectF topTile(topLeftTile.topRight(), topRightTile.bottomLeft()); + + const QRectF bottomLeftTile( + 0, + SHADOW_INNER_RECT.bottom(), + SHADOW_INNER_RECT.x(), + SHADOW_TEXTURE_HEIGHT - SHADOW_INNER_RECT.bottom()); + const QRectF bottomRightTile( + SHADOW_INNER_RECT.right(), + SHADOW_INNER_RECT.bottom(), + SHADOW_TEXTURE_WIDTH - SHADOW_INNER_RECT.right(), + SHADOW_TEXTURE_HEIGHT - SHADOW_INNER_RECT.bottom()); + const QRectF bottomTile(bottomLeftTile.topRight(), bottomRightTile.bottomLeft()); + + const QRectF leftTile(topLeftTile.bottomLeft(), bottomLeftTile.topRight()); + const QRectF rightTile(topRightTile.bottomLeft(), bottomRightTile.topRight()); + + qreal tx1 = 0; + qreal ty1 = 0; + qreal tx2 = 0; + qreal ty2 = 0; + + // Explanation behind numbers: (256+1 x 256+1) is the minimum window size + // which doesn't cause overlapping of shadow tiles. For example, if a window + // has (256 x 256+1) size, top-left and top-right or bottom-left and + // bottom-right shadow tiles overlap. + + // No overlaps: In this case corner tiles are rendered as they are, + // and top/right/bottom/left tiles are stretched. + { + const QSize windowSize(256 + 1, 256 + 1); + WindowQuadList shadowQuads; + + const QRectF outerRect( + -SHADOW_PADDING_LEFT, + -SHADOW_PADDING_TOP, + windowSize.width() + SHADOW_PADDING_LEFT + SHADOW_PADDING_RIGHT, + windowSize.height() + SHADOW_PADDING_TOP + SHADOW_PADDING_BOTTOM); + + const QRectF topLeft( + outerRect.left(), + outerRect.top(), + topLeftTile.width(), + topLeftTile.height()); + tx1 = topLeftTile.left(); + ty1 = topLeftTile.top(); + tx2 = topLeftTile.right(); + ty2 = topLeftTile.bottom(); + shadowQuads << makeShadowQuad(topLeft, tx1, ty1, tx2, ty2); + + const QRectF topRight( + outerRect.right() - topRightTile.width(), + outerRect.top(), + topRightTile.width(), + topRightTile.height()); + tx1 = topRightTile.left(); + ty1 = topRightTile.top(); + tx2 = topRightTile.right(); + ty2 = topRightTile.bottom(); + shadowQuads << makeShadowQuad(topRight, tx1, ty1, tx2, ty2); + + const QRectF top(topLeft.topRight(), topRight.bottomLeft()); + tx1 = topTile.left(); + ty1 = topTile.top(); + tx2 = topTile.right(); + ty2 = topTile.bottom(); + shadowQuads << makeShadowQuad(top, tx1, ty1, tx2, ty2); + + const QRectF bottomLeft( + outerRect.left(), + outerRect.bottom() - bottomLeftTile.height(), + bottomLeftTile.width(), + bottomLeftTile.height()); + tx1 = bottomLeftTile.left(); + ty1 = bottomLeftTile.top(); + tx2 = bottomLeftTile.right(); + ty2 = bottomLeftTile.bottom(); + shadowQuads << makeShadowQuad(bottomLeft, tx1, ty1, tx2, ty2); + + const QRectF bottomRight( + outerRect.right() - bottomRightTile.width(), + outerRect.bottom() - bottomRightTile.height(), + bottomRightTile.width(), + bottomRightTile.height()); + tx1 = bottomRightTile.left(); + ty1 = bottomRightTile.top(); + tx2 = bottomRightTile.right(); + ty2 = bottomRightTile.bottom(); + shadowQuads << makeShadowQuad(bottomRight, tx1, ty1, tx2, ty2); + + const QRectF bottom(bottomLeft.topRight(), bottomRight.bottomLeft()); + tx1 = bottomTile.left(); + ty1 = bottomTile.top(); + tx2 = bottomTile.right(); + ty2 = bottomTile.bottom(); + shadowQuads << makeShadowQuad(bottom, tx1, ty1, tx2, ty2); + + const QRectF left(topLeft.bottomLeft(), bottomLeft.topRight()); + tx1 = leftTile.left(); + ty1 = leftTile.top(); + tx2 = leftTile.right(); + ty2 = leftTile.bottom(); + shadowQuads << makeShadowQuad(left, tx1, ty1, tx2, ty2); + + const QRectF right(topRight.bottomLeft(), bottomRight.topRight()); + tx1 = rightTile.left(); + ty1 = rightTile.top(); + tx2 = rightTile.right(); + ty2 = rightTile.bottom(); + shadowQuads << makeShadowQuad(right, tx1, ty1, tx2, ty2); + + QTest::newRow("no overlaps") << windowSize << shadowQuads; + } + + // Top-Left & Bottom-Left/Top-Right & Bottom-Right overlap: + // In this case overlapping parts are clipped and left/right + // tiles aren't rendered. + const QVector> verticalOverlapTestTable { + QPair { + QByteArray("top-left & bottom-left/top-right & bottom-right overlap"), + QSize(256 + 1, 256) + }, + QPair { + QByteArray("top-left & bottom-left/top-right & bottom-right overlap :: pre"), + QSize(256 + 1, 256 - 1) + } + // No need to test the case when window size is QSize(256 + 1, 256 + 1). + // It has been tested already (no overlaps test case). + }; + + for (auto const &tt : verticalOverlapTestTable) { + const char *testName = tt.first.constData(); + const QSize windowSize = tt.second; + + WindowQuadList shadowQuads; + qreal halfOverlap = 0.0; + + const QRectF outerRect( + -SHADOW_PADDING_LEFT, + -SHADOW_PADDING_TOP, + windowSize.width() + SHADOW_PADDING_LEFT + SHADOW_PADDING_RIGHT, + windowSize.height() + SHADOW_PADDING_TOP + SHADOW_PADDING_BOTTOM); + + QRectF topLeft( + outerRect.left(), + outerRect.top(), + topLeftTile.width(), + topLeftTile.height()); + + QRectF bottomLeft( + outerRect.left(), + outerRect.bottom() - bottomLeftTile.height(), + bottomLeftTile.width(), + bottomLeftTile.height()); + + halfOverlap = qAbs(topLeft.bottom() - bottomLeft.top()) / 2; + topLeft.setBottom(topLeft.bottom() - std::floor(halfOverlap)); + bottomLeft.setTop(bottomLeft.top() + std::ceil(halfOverlap)); + + tx1 = topLeftTile.left(); + ty1 = topLeftTile.top(); + tx2 = topLeftTile.right(); + ty2 = topLeft.height(); + shadowQuads << makeShadowQuad(topLeft, tx1, ty1, tx2, ty2); + + tx1 = bottomLeftTile.left(); + ty1 = SHADOW_TEXTURE_HEIGHT - bottomLeft.height(); + tx2 = bottomLeftTile.right(); + ty2 = bottomLeftTile.bottom(); + shadowQuads << makeShadowQuad(bottomLeft, tx1, ty1, tx2, ty2); + + QRectF topRight( + outerRect.right() - topRightTile.width(), + outerRect.top(), + topRightTile.width(), + topRightTile.height()); + + QRectF bottomRight( + outerRect.right() - bottomRightTile.width(), + outerRect.bottom() - bottomRightTile.height(), + bottomRightTile.width(), + bottomRightTile.height()); + + halfOverlap = qAbs(topRight.bottom() - bottomRight.top()) / 2; + topRight.setBottom(topRight.bottom() - std::floor(halfOverlap)); + bottomRight.setTop(bottomRight.top() + std::ceil(halfOverlap)); + + tx1 = topRightTile.left(); + ty1 = topRightTile.top(); + tx2 = topRightTile.right(); + ty2 = topRight.height(); + shadowQuads << makeShadowQuad(topRight, tx1, ty1, tx2, ty2); + + tx1 = bottomRightTile.left(); + ty1 = SHADOW_TEXTURE_HEIGHT - bottomRight.height(); + tx2 = bottomRightTile.right(); + ty2 = bottomRightTile.bottom(); + shadowQuads << makeShadowQuad(bottomRight, tx1, ty1, tx2, ty2); + + const QRectF top(topLeft.topRight(), topRight.bottomLeft()); + tx1 = topTile.left(); + ty1 = topTile.top(); + tx2 = topTile.right(); + ty2 = top.height(); + shadowQuads << makeShadowQuad(top, tx1, ty1, tx2, ty2); + + const QRectF bottom(bottomLeft.topRight(), bottomRight.bottomLeft()); + tx1 = bottomTile.left(); + ty1 = SHADOW_TEXTURE_HEIGHT - bottom.height(); + tx2 = bottomTile.right(); + ty2 = bottomTile.bottom(); + shadowQuads << makeShadowQuad(bottom, tx1, ty1, tx2, ty2); + + QTest::newRow(testName) << windowSize << shadowQuads; + } + + // Top-Left & Top-Right/Bottom-Left & Bottom-Right overlap: + // In this case overlapping parts are clipped and top/bottom + // tiles aren't rendered. + const QVector> horizontalOverlapTestTable { + QPair { + QByteArray("top-left & top-right/bottom-left & bottom-right overlap"), + QSize(256, 256 + 1) + }, + QPair { + QByteArray("top-left & top-right/bottom-left & bottom-right overlap :: pre"), + QSize(256 - 1, 256 + 1) + } + // No need to test the case when window size is QSize(256 + 1, 256 + 1). + // It has been tested already (no overlaps test case). + }; + + for (auto const &tt : horizontalOverlapTestTable) { + const char *testName = tt.first.constData(); + const QSize windowSize = tt.second; + + WindowQuadList shadowQuads; + qreal halfOverlap = 0.0; + + const QRectF outerRect( + -SHADOW_PADDING_LEFT, + -SHADOW_PADDING_TOP, + windowSize.width() + SHADOW_PADDING_LEFT + SHADOW_PADDING_RIGHT, + windowSize.height() + SHADOW_PADDING_TOP + SHADOW_PADDING_BOTTOM); + + QRectF topLeft( + outerRect.left(), + outerRect.top(), + topLeftTile.width(), + topLeftTile.height()); + + QRectF topRight( + outerRect.right() - topRightTile.width(), + outerRect.top(), + topRightTile.width(), + topRightTile.height()); + + halfOverlap = qAbs(topLeft.right() - topRight.left()) / 2; + topLeft.setRight(topLeft.right() - std::floor(halfOverlap)); + topRight.setLeft(topRight.left() + std::ceil(halfOverlap)); + + tx1 = topLeftTile.left(); + ty1 = topLeftTile.top(); + tx2 = topLeft.width(); + ty2 = topLeftTile.bottom(); + shadowQuads << makeShadowQuad(topLeft, tx1, ty1, tx2, ty2); + + tx1 = SHADOW_TEXTURE_WIDTH - topRight.width(); + ty1 = topRightTile.top(); + tx2 = topRightTile.right(); + ty2 = topRightTile.bottom(); + shadowQuads << makeShadowQuad(topRight, tx1, ty1, tx2, ty2); + + QRectF bottomLeft( + outerRect.left(), + outerRect.bottom() - bottomLeftTile.height(), + bottomLeftTile.width(), + bottomLeftTile.height()); + + QRectF bottomRight( + outerRect.right() - bottomRightTile.width(), + outerRect.bottom() - bottomRightTile.height(), + bottomRightTile.width(), + bottomRightTile.height()); + + halfOverlap = qAbs(bottomLeft.right() - bottomRight.left()) / 2; + bottomLeft.setRight(bottomLeft.right() - std::floor(halfOverlap)); + bottomRight.setLeft(bottomRight.left() + std::ceil(halfOverlap)); + + tx1 = bottomLeftTile.left(); + ty1 = bottomLeftTile.top(); + tx2 = bottomLeft.width(); + ty2 = bottomLeftTile.bottom(); + shadowQuads << makeShadowQuad(bottomLeft, tx1, ty1, tx2, ty2); + + tx1 = SHADOW_TEXTURE_WIDTH - bottomRight.width(); + ty1 = bottomRightTile.top(); + tx2 = bottomRightTile.right(); + ty2 = bottomRightTile.bottom(); + shadowQuads << makeShadowQuad(bottomRight, tx1, ty1, tx2, ty2); + + const QRectF left(topLeft.bottomLeft(), bottomLeft.topRight()); + tx1 = leftTile.left(); + ty1 = leftTile.top(); + tx2 = left.width(); + ty2 = leftTile.bottom(); + shadowQuads << makeShadowQuad(left, tx1, ty1, tx2, ty2); + + const QRectF right(topRight.bottomLeft(), bottomRight.topRight()); + tx1 = SHADOW_TEXTURE_WIDTH - right.width(); + ty1 = rightTile.top(); + tx2 = rightTile.right(); + ty2 = rightTile.bottom(); + shadowQuads << makeShadowQuad(right, tx1, ty1, tx2, ty2); + + QTest::newRow(testName) << windowSize << shadowQuads; + } + + // All shadow tiles overlap: In this case all overlapping parts + // are clippend and top/right/bottom/left tiles aren't rendered. + const QVector> allOverlapTestTable { + QPair { + QByteArray("all corner tiles overlap"), + QSize(256, 256) + }, + QPair { + QByteArray("all corner tiles overlap :: pre"), + QSize(256 - 1, 256 - 1) + } + // No need to test the case when window size is QSize(256 + 1, 256 + 1). + // It has been tested already (no overlaps test case). + }; + + for (auto const &tt : allOverlapTestTable) { + const char *testName = tt.first.constData(); + const QSize windowSize = tt.second; + + WindowQuadList shadowQuads; + qreal halfOverlap = 0.0; + + const QRectF outerRect( + -SHADOW_PADDING_LEFT, + -SHADOW_PADDING_TOP, + windowSize.width() + SHADOW_PADDING_LEFT + SHADOW_PADDING_RIGHT, + windowSize.height() + SHADOW_PADDING_TOP + SHADOW_PADDING_BOTTOM); + + QRectF topLeft( + outerRect.left(), + outerRect.top(), + topLeftTile.width(), + topLeftTile.height()); + + QRectF topRight( + outerRect.right() - topRightTile.width(), + outerRect.top(), + topRightTile.width(), + topRightTile.height()); + + QRectF bottomLeft( + outerRect.left(), + outerRect.bottom() - bottomLeftTile.height(), + bottomLeftTile.width(), + bottomLeftTile.height()); + + QRectF bottomRight( + outerRect.right() - bottomRightTile.width(), + outerRect.bottom() - bottomRightTile.height(), + bottomRightTile.width(), + bottomRightTile.height()); + + halfOverlap = qAbs(topLeft.right() - topRight.left()) / 2; + topLeft.setRight(topLeft.right() - std::floor(halfOverlap)); + topRight.setLeft(topRight.left() + std::ceil(halfOverlap)); + + halfOverlap = qAbs(bottomLeft.right() - bottomRight.left()) / 2; + bottomLeft.setRight(bottomLeft.right() - std::floor(halfOverlap)); + bottomRight.setLeft(bottomRight.left() + std::ceil(halfOverlap)); + + halfOverlap = qAbs(topLeft.bottom() - bottomLeft.top()) / 2; + topLeft.setBottom(topLeft.bottom() - std::floor(halfOverlap)); + bottomLeft.setTop(bottomLeft.top() + std::ceil(halfOverlap)); + + halfOverlap = qAbs(topRight.bottom() - bottomRight.top()) / 2; + topRight.setBottom(topRight.bottom() - std::floor(halfOverlap)); + bottomRight.setTop(bottomRight.top() + std::ceil(halfOverlap)); + + tx1 = topLeftTile.left(); + ty1 = topLeftTile.top(); + tx2 = topLeft.width(); + ty2 = topLeft.height(); + shadowQuads << makeShadowQuad(topLeft, tx1, ty1, tx2, ty2); + + tx1 = SHADOW_TEXTURE_WIDTH - topRight.width(); + ty1 = topRightTile.top(); + tx2 = topRightTile.right(); + ty2 = topRight.height(); + shadowQuads << makeShadowQuad(topRight, tx1, ty1, tx2, ty2); + + tx1 = bottomLeftTile.left(); + ty1 = SHADOW_TEXTURE_HEIGHT - bottomLeft.height(); + tx2 = bottomLeft.width(); + ty2 = bottomLeftTile.bottom(); + shadowQuads << makeShadowQuad(bottomLeft, tx1, ty1, tx2, ty2); + + tx1 = SHADOW_TEXTURE_WIDTH - bottomRight.width(); + ty1 = SHADOW_TEXTURE_HEIGHT - bottomRight.height(); + tx2 = bottomRightTile.right(); + ty2 = bottomRightTile.bottom(); + shadowQuads << makeShadowQuad(bottomRight, tx1, ty1, tx2, ty2); + + QTest::newRow(testName) << windowSize << shadowQuads; + } + + // Window is too small: do not render any shadow tiles. + { + const QSize windowSize(1, 1); + const WindowQuadList shadowQuads; + + QTest::newRow("window is too small") << windowSize << shadowQuads; + } +} + +void SceneQPainterShadowTest::testShadowTileOverlaps() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Decoration)); + + QFETCH(QSize, windowSize); + QFETCH(WindowQuadList, expectedQuads); + + // Create a decorated client. + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QScopedPointer ssd(Test::waylandServerSideDecoration()->create(surface.data())); + + auto *client = Test::renderAndWaitForShown(surface.data(), windowSize, Qt::blue); + + QSignalSpy sizeChangedSpy(shellSurface.data(), &XdgShellSurface::sizeChanged); + QVERIFY(sizeChangedSpy.isValid()); + + // Check the client is decorated. + QVERIFY(client); + QVERIFY(client->isDecorated()); + auto *decoration = client->decoration(); + QVERIFY(decoration); + + // If speciefied decoration theme is not found, KWin loads a default one + // so we have to check whether a client has right decoration. + auto decoShadow = decoration->shadow(); + QCOMPARE(decoShadow->shadow().size(), QSize(SHADOW_TEXTURE_WIDTH, SHADOW_TEXTURE_HEIGHT)); + QCOMPARE(decoShadow->paddingTop(), SHADOW_PADDING_TOP); + QCOMPARE(decoShadow->paddingRight(), SHADOW_PADDING_RIGHT); + QCOMPARE(decoShadow->paddingBottom(), SHADOW_PADDING_BOTTOM); + QCOMPARE(decoShadow->paddingLeft(), SHADOW_PADDING_LEFT); + + // Get shadow. + QVERIFY(client->effectWindow()); + QVERIFY(client->effectWindow()->sceneWindow()); + QVERIFY(client->effectWindow()->sceneWindow()->shadow()); + auto *shadow = client->effectWindow()->sceneWindow()->shadow(); + + // Validate shadow quads. + const WindowQuadList &quads = shadow->shadowQuads(); + QCOMPARE(quads.size(), expectedQuads.size()); + + QVector mask(expectedQuads.size(), false); + for (const auto &q : quads) { + for (int i = 0; i < expectedQuads.size(); i++) { + if (!compareQuads(q, expectedQuads[i])) { + continue; + } + if (!mask[i]) { + mask[i] = true; + break; + } else { + QFAIL("got a duplicate shadow quad"); + } + } + } + + for (const auto &v : qAsConst(mask)) { + if (!v) { + QFAIL("missed a shadow quad"); + } + } +} + +void SceneQPainterShadowTest::testShadowTextureReconstruction() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::ShadowManager)); + + // Create a surface. + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + auto *client = Test::renderAndWaitForShown(surface.data(), QSize(512, 512), Qt::blue); + QVERIFY(client); + QVERIFY(!client->isDecorated()); + + // Render reference shadow texture with the following params: + // - shadow size: 128 + // - inner rect size: 1 + // - padding: 128 + QImage referenceShadowTexture(QSize(256 + 1, 256 + 1), QImage::Format_ARGB32_Premultiplied); + referenceShadowTexture.fill(Qt::transparent); + + QPainter painter(&referenceShadowTexture); + painter.fillRect(QRect(10, 10, 192, 200), QColor(255, 0, 0, 128)); + painter.fillRect(QRect(128, 30, 10, 180), QColor(0, 0, 0, 30)); + painter.fillRect(QRect(20, 140, 160, 10), QColor(0, 255, 0, 128)); + + painter.setCompositionMode(QPainter::CompositionMode_DestinationOut); + painter.fillRect(QRect(128, 128, 1, 1), Qt::black); + painter.end(); + + // Create shadow. + QScopedPointer clientShadow(Test::waylandShadowManager()->createShadow(surface.data())); + QVERIFY(clientShadow->isValid()); + + auto *shmPool = Test::waylandShmPool(); + + Buffer::Ptr bufferTopLeft = shmPool->createBuffer( + referenceShadowTexture.copy(QRect(0, 0, 128, 128))); + clientShadow->attachTopLeft(bufferTopLeft); + + Buffer::Ptr bufferTop = shmPool->createBuffer( + referenceShadowTexture.copy(QRect(128, 0, 1, 128))); + clientShadow->attachTop(bufferTop); + + Buffer::Ptr bufferTopRight = shmPool->createBuffer( + referenceShadowTexture.copy(QRect(128 + 1, 0, 128, 128))); + clientShadow->attachTopRight(bufferTopRight); + + Buffer::Ptr bufferRight = shmPool->createBuffer( + referenceShadowTexture.copy(QRect(128 + 1, 128, 128, 1))); + clientShadow->attachRight(bufferRight); + + Buffer::Ptr bufferBottomRight = shmPool->createBuffer( + referenceShadowTexture.copy(QRect(128 + 1, 128 + 1, 128, 128))); + clientShadow->attachBottomRight(bufferBottomRight); + + Buffer::Ptr bufferBottom = shmPool->createBuffer( + referenceShadowTexture.copy(QRect(128, 128 + 1, 1, 128))); + clientShadow->attachBottom(bufferBottom); + + Buffer::Ptr bufferBottomLeft = shmPool->createBuffer( + referenceShadowTexture.copy(QRect(0, 128 + 1, 128, 128))); + clientShadow->attachBottomLeft(bufferBottomLeft); + + Buffer::Ptr bufferLeft = shmPool->createBuffer( + referenceShadowTexture.copy(QRect(0, 128, 128, 1))); + clientShadow->attachLeft(bufferLeft); + + clientShadow->setOffsets(QMarginsF(128, 128, 128, 128)); + + // Commit shadow. + QSignalSpy shadowChangedSpy(client->surface(), &KWaylandServer::SurfaceInterface::shadowChanged); + QVERIFY(shadowChangedSpy.isValid()); + clientShadow->commit(); + surface->commit(Surface::CommitFlag::None); + QVERIFY(shadowChangedSpy.wait()); + + // Check whether we've got right shadow. + auto shadowIface = client->surface()->shadow(); + QVERIFY(!shadowIface.isNull()); + QCOMPARE(shadowIface->offset().left(), 128.0); + QCOMPARE(shadowIface->offset().top(), 128.0); + QCOMPARE(shadowIface->offset().right(), 128.0); + QCOMPARE(shadowIface->offset().bottom(), 128.0); + + // Get SceneQPainterShadow's texture. + QVERIFY(client->effectWindow()); + QVERIFY(client->effectWindow()->sceneWindow()); + QVERIFY(client->effectWindow()->sceneWindow()->shadow()); + auto &shadowTexture = static_cast(client->effectWindow()->sceneWindow()->shadow())->shadowTexture(); + + QCOMPARE(shadowTexture, referenceShadowTexture); +} + +WAYLANDTEST_MAIN(SceneQPainterShadowTest) +#include "scene_qpainter_shadow_test.moc" diff --git a/autotests/integration/scene_qpainter_test.cpp b/autotests/integration/scene_qpainter_test.cpp new file mode 100644 index 0000000..f5434a2 --- /dev/null +++ b/autotests/integration/scene_qpainter_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-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" +#include "composite.h" +#include "effectloader.h" +#include "x11client.h" +#include "cursor.h" +#include "effects.h" +#include "platform.h" +#include "wayland_server.h" +#include "effect_builtins.h" +#include "workspace.h" + +#include + +#include +#include +#include +#include +#include + +#include + +#include +#include + +using namespace KWin; +static const QString s_socketName = QStringLiteral("wayland_test_kwin_scene_qpainter-0"); + +class SceneQPainterTest : public QObject +{ +Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void cleanup(); + void testStartFrame(); + void testCursorMoving(); + void testWindow(); + void testWindowScaled(); + void testCompositorRestart(); + void testX11Window(); +}; + +void SceneQPainterTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void SceneQPainterTest::initTestCase() +{ + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + // 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")); + ScriptedEffectLoader loader; + const auto builtinNames = BuiltInEffects::availableEffectNames() << loader.listOfKnownEffects(); + for (QString name : builtinNames) { + plugins.writeEntry(name + QStringLiteral("Enabled"), false); + } + + config->sync(); + kwinApp()->setConfig(config); + + if (!QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("icons/DMZ-White/index.theme")).isEmpty()) { + qputenv("XCURSOR_THEME", QByteArrayLiteral("DMZ-White")); + } else { + // might be vanilla-dmz (e.g. Arch, FreeBSD) + qputenv("XCURSOR_THEME", QByteArrayLiteral("Vanilla-DMZ")); + } + qputenv("XCURSOR_SIZE", QByteArrayLiteral("24")); + qputenv("KWIN_COMPOSE", QByteArrayLiteral("Q")); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QVERIFY(Compositor::self()); +} + +void SceneQPainterTest::testStartFrame() +{ + // this test verifies that the initial rendering is correct + Compositor::self()->addRepaintFull(); + auto scene = Compositor::self()->scene(); + QVERIFY(scene); + QCOMPARE(kwinApp()->platform()->selectedCompositor(), QPainterCompositing); + QSignalSpy frameRenderedSpy(scene, &Scene::frameRendered); + QVERIFY(frameRenderedSpy.isValid()); + QVERIFY(frameRenderedSpy.wait()); + // now let's render a reference image for comparison + QImage referenceImage(QSize(1280, 1024), QImage::Format_RGB32); + referenceImage.fill(Qt::black); + QPainter p(&referenceImage); + + auto cursor = KWin::Cursors::self()->mouse(); + const QImage cursorImage = cursor->image(); + QVERIFY(!cursorImage.isNull()); + p.drawImage(cursor->pos() - cursor->hotspot(), cursorImage); + QCOMPARE(referenceImage, *scene->qpainterRenderBuffer()); +} + +void SceneQPainterTest::testCursorMoving() +{ + // this test verifies that rendering is correct also after moving the cursor a few times + auto scene = Compositor::self()->scene(); + QVERIFY(scene); + QSignalSpy frameRenderedSpy(scene, &Scene::frameRendered); + QVERIFY(frameRenderedSpy.isValid()); + KWin::Cursors::self()->mouse()->setPos(0, 0); + QVERIFY(frameRenderedSpy.wait()); + KWin::Cursors::self()->mouse()->setPos(10, 0); + QVERIFY(frameRenderedSpy.wait()); + KWin::Cursors::self()->mouse()->setPos(10, 12); + QVERIFY(frameRenderedSpy.wait()); + KWin::Cursors::self()->mouse()->setPos(12, 14); + QVERIFY(frameRenderedSpy.wait()); + KWin::Cursors::self()->mouse()->setPos(50, 60); + QVERIFY(frameRenderedSpy.wait()); + KWin::Cursors::self()->mouse()->setPos(45, 45); + QVERIFY(frameRenderedSpy.wait()); + // now let's render a reference image for comparison + QImage referenceImage(QSize(1280, 1024), QImage::Format_RGB32); + referenceImage.fill(Qt::black); + QPainter p(&referenceImage); + + auto cursor = Cursors::self()->currentCursor(); + const QImage cursorImage = cursor->image(); + QVERIFY(!cursorImage.isNull()); + p.drawImage(QPoint(45, 45) - cursor->hotspot(), cursorImage); + QCOMPARE(referenceImage, *scene->qpainterRenderBuffer()); +} + +void SceneQPainterTest::testWindow() +{ + KWin::Cursors::self()->mouse()->setPos(45, 45); + // this test verifies that a window is rendered correctly + using namespace KWayland::Client; + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat)); + QVERIFY(Test::waitForWaylandPointer()); + QScopedPointer s(Test::createSurface()); + QScopedPointer ss(Test::createXdgShellStableSurface(s.data())); + QScopedPointer p(Test::waylandSeat()->createPointer()); + + auto scene = KWin::Compositor::self()->scene(); + QVERIFY(scene); + QSignalSpy frameRenderedSpy(scene, &Scene::frameRendered); + QVERIFY(frameRenderedSpy.isValid()); + + // now let's map the window + QVERIFY(Test::renderAndWaitForShown(s.data(), QSize(200, 300), Qt::blue)); + // which should trigger a frame + if (frameRenderedSpy.isEmpty()) { + QVERIFY(frameRenderedSpy.wait()); + } + // we didn't set a cursor image on the surface yet, so it should be just black + window and previous cursor + QImage referenceImage(QSize(1280, 1024), QImage::Format_RGB32); + referenceImage.fill(Qt::black); + QPainter painter(&referenceImage); + painter.fillRect(0, 0, 200, 300, Qt::blue); + + // now let's set a cursor image + QScopedPointer cs(Test::createSurface()); + QVERIFY(!cs.isNull()); + Test::render(cs.data(), QSize(10, 10), Qt::red); + p->setCursor(cs.data(), QPoint(5, 5)); + QVERIFY(frameRenderedSpy.wait()); + painter.fillRect(KWin::Cursors::self()->mouse()->pos().x() - 5, KWin::Cursors::self()->mouse()->pos().y() - 5, 10, 10, Qt::red); + QCOMPARE(referenceImage, *scene->qpainterRenderBuffer()); + // let's move the cursor again + KWin::Cursors::self()->mouse()->setPos(10, 10); + QVERIFY(frameRenderedSpy.wait()); + painter.fillRect(0, 0, 200, 300, Qt::blue); + painter.fillRect(5, 5, 10, 10, Qt::red); + QCOMPARE(referenceImage, *scene->qpainterRenderBuffer()); +} + +void SceneQPainterTest::testWindowScaled() +{ + KWin::Cursors::self()->mouse()->setPos(10, 10); + // this test verifies that a window is rendered correctly + using namespace KWayland::Client; + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat)); + QVERIFY(Test::waitForWaylandPointer()); + QScopedPointer s(Test::createSurface()); + QScopedPointer ss(Test::createXdgShellStableSurface(s.data())); + QScopedPointer p(Test::waylandSeat()->createPointer()); + QSignalSpy pointerEnteredSpy(p.data(), &Pointer::entered); + QVERIFY(pointerEnteredSpy.isValid()); + + auto scene = KWin::Compositor::self()->scene(); + QVERIFY(scene); + QSignalSpy frameRenderedSpy(scene, &Scene::frameRendered); + QVERIFY(frameRenderedSpy.isValid()); + + // now let's set a cursor image + QScopedPointer cs(Test::createSurface()); + QVERIFY(!cs.isNull()); + Test::render(cs.data(), QSize(10, 10), Qt::red); + + // now let's map the window + s->setScale(2); + + //draw a blue square@400x600 with red rectangle@200x200 in the middle + const QSize size(400,600); + QImage img(size, QImage::Format_ARGB32_Premultiplied); + img.fill(Qt::blue); + QPainter surfacePainter(&img); + surfacePainter.fillRect(200,300,200,200, Qt::red); + + //add buffer + Test::render(s.data(), img); + QVERIFY(pointerEnteredSpy.wait()); + p->setCursor(cs.data(), QPoint(5, 5)); + + // which should trigger a frame + QVERIFY(frameRenderedSpy.wait()); + QImage referenceImage(QSize(1280, 1024), QImage::Format_RGB32); + referenceImage.fill(Qt::black); + QPainter painter(&referenceImage); + painter.fillRect(0, 0, 200, 300, Qt::blue); + painter.fillRect(100, 150, 100, 100, Qt::red); + painter.fillRect(5, 5, 10, 10, Qt::red); //cursor + + QCOMPARE(referenceImage, *scene->qpainterRenderBuffer()); +} + +void SceneQPainterTest::testCompositorRestart() +{ + // this test verifies that the compositor/SceneQPainter survive a restart of the compositor and still render correctly + KWin::Cursors::self()->mouse()->setPos(400, 400); + + // first create a window + using namespace KWayland::Client; + QVERIFY(Test::setupWaylandConnection()); + QScopedPointer s(Test::createSurface()); + QScopedPointer ss(Test::createXdgShellStableSurface(s.data())); + QVERIFY(Test::renderAndWaitForShown(s.data(), QSize(200, 300), Qt::blue)); + + // now let's try to reinitialize the compositing scene + auto oldScene = KWin::Compositor::self()->scene(); + QVERIFY(oldScene); + QSignalSpy sceneCreatedSpy(KWin::Compositor::self(), &KWin::Compositor::sceneCreated); + QVERIFY(sceneCreatedSpy.isValid()); + KWin::Compositor::self()->reinitialize(); + if (sceneCreatedSpy.isEmpty()) { + QVERIFY(sceneCreatedSpy.wait()); + } + QCOMPARE(sceneCreatedSpy.count(), 1); + auto scene = KWin::Compositor::self()->scene(); + QVERIFY(scene); + + // this should directly trigger a frame + KWin::Compositor::self()->addRepaintFull(); + QSignalSpy frameRenderedSpy(scene, &Scene::frameRendered); + QVERIFY(frameRenderedSpy.isValid()); + QVERIFY(frameRenderedSpy.wait()); + + // render reference image + QImage referenceImage(QSize(1280, 1024), QImage::Format_RGB32); + referenceImage.fill(Qt::black); + QPainter painter(&referenceImage); + painter.fillRect(0, 0, 200, 300, Qt::blue); + + auto cursor = Cursors::self()->mouse(); + const QImage cursorImage = cursor->image(); + QVERIFY(!cursorImage.isNull()); + painter.drawImage(QPoint(400, 400) - cursor->hotspot(), cursorImage); + QCOMPARE(referenceImage, *scene->qpainterRenderBuffer()); +} + +struct XcbConnectionDeleter +{ + static inline void cleanup(xcb_connection_t *pointer) + { + xcb_disconnect(pointer); + } +}; + +void SceneQPainterTest::testX11Window() +{ + // this test verifies the condition of BUG: 382748 + + // create X11 window + QSignalSpy windowAddedSpy(effects, &EffectsHandler::windowAdded); + QVERIFY(windowAddedSpy.isValid()); + + // create an xcb window + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + const QRect windowGeometry(0, 0, 100, 200); + xcb_window_t w = xcb_generate_id(c.data()); + uint32_t value = kwinApp()->x11DefaultScreen()->white_pixel; + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, rootWindow(), + windowGeometry.x(), + windowGeometry.y(), + windowGeometry.width(), + windowGeometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, XCB_CW_BACK_PIXEL, &value); + xcb_size_hints_t hints; + memset(&hints, 0, sizeof(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.data(), w, &hints); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + + // we should get a client for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->window(), w); + QCOMPARE(client->clientSize(), QSize(100, 200)); + if (!client->surface()) { + // wait for surface + QSignalSpy surfaceChangedSpy(client, &Toplevel::surfaceChanged); + QVERIFY(surfaceChangedSpy.isValid()); + QVERIFY(surfaceChangedSpy.wait()); + } + QVERIFY(client->surface()); + QTRY_VERIFY(client->surface()->buffer()); + QTRY_COMPARE(client->surface()->buffer()->data().size(), client->size()); + QImage compareImage(client->clientSize(), QImage::Format_RGB32); + compareImage.fill(Qt::white); + QCOMPARE(client->surface()->buffer()->data().copy(QRect(client->clientPos(), client->clientSize())), compareImage); + + // enough time for rendering the window + QTest::qWait(100); + + auto scene = KWin::Compositor::self()->scene(); + QVERIFY(scene); + + // this should directly trigger a frame + KWin::Compositor::self()->addRepaintFull(); + QSignalSpy frameRenderedSpy(scene, &Scene::frameRendered); + QVERIFY(frameRenderedSpy.isValid()); + QVERIFY(frameRenderedSpy.wait()); + + const QPoint startPos = client->pos() + client->clientPos(); + auto image = scene->qpainterRenderBuffer(); + QCOMPARE(image->copy(QRect(startPos, client->clientSize())), compareImage); + + // and destroy the window again + xcb_unmap_window(c.data(), w); + xcb_flush(c.data()); + + QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + QVERIFY(windowClosedSpy.wait()); + xcb_destroy_window(c.data(), w); + c.reset(); +} + +WAYLANDTEST_MAIN(SceneQPainterTest) +#include "scene_qpainter_test.moc" diff --git a/autotests/integration/screen_changes_test.cpp b/autotests/integration/screen_changes_test.cpp new file mode 100644 index 0000000..e08d004 --- /dev/null +++ b/autotests/integration/screen_changes_test.cpp @@ -0,0 +1,188 @@ +/* + 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 "platform.h" +#include "screens.h" +#include "wayland_server.h" + +#include +#include +#include + +using namespace KWin; +using namespace KWayland::Client; + +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() +{ + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + setenv("QT_QPA_PLATFORM", "wayland", true); + waylandServer()->initWorkspace(); +} + +void ScreenChangesTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); + + screens()->setCurrent(0); + KWin::Cursors::self()->mouse()->setPos(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 + Registry registry; + QSignalSpy allAnnounced(®istry, &Registry::interfacesAnnounced); + QVERIFY(allAnnounced.isValid()); + QSignalSpy outputAnnouncedSpy(®istry, &Registry::outputAnnounced); + QVERIFY(outputAnnouncedSpy.isValid()); + QSignalSpy outputRemovedSpy(®istry, &Registry::outputRemoved); + QVERIFY(outputRemovedSpy.isValid()); + registry.create(Test::waylandConnection()); + QVERIFY(registry.isValid()); + registry.setup(); + QVERIFY(allAnnounced.wait()); + const auto xdgOMData = registry.interface(Registry::Interface::XdgOutputUnstableV1); + auto xdgOutputManager = registry.createXdgOutputManager(xdgOMData.name, xdgOMData.version); + + // should be one output + QCOMPARE(screens()->count(), 1); + QCOMPARE(outputAnnouncedSpy.count(), 1); + const quint32 firstOutputId = outputAnnouncedSpy.first().first().value(); + QVERIFY(firstOutputId != 0u); + outputAnnouncedSpy.clear(); + + // let's announce a new output + QSignalSpy screensChangedSpy(screens(), &Screens::changed); + QVERIFY(screensChangedSpy.isValid()); + const QVector geometries{QRect(0, 0, 1280, 1024), QRect(1280, 0, 1280, 1024)}; + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", + Qt::DirectConnection, + Q_ARG(int, 2), + Q_ARG(QVector, geometries)); + QVERIFY(screensChangedSpy.wait()); + QCOMPARE(screensChangedSpy.count(), 1); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), geometries.at(0)); + QCOMPARE(screens()->geometry(1), geometries.at(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 + QScopedPointer o1(registry.createOutput(outputAnnouncedSpy.first().first().value(), outputAnnouncedSpy.first().last().value())); + QVERIFY(o1->isValid()); + QSignalSpy o1ChangedSpy(o1.data(), &Output::changed); + QVERIFY(o1ChangedSpy.isValid()); + QVERIFY(o1ChangedSpy.wait()); + QCOMPARE(o1->geometry(), geometries.at(0)); + QScopedPointer o2(registry.createOutput(outputAnnouncedSpy.last().first().value(), outputAnnouncedSpy.last().last().value())); + QVERIFY(o2->isValid()); + QSignalSpy o2ChangedSpy(o2.data(), &Output::changed); + QVERIFY(o2ChangedSpy.isValid()); + QVERIFY(o2ChangedSpy.wait()); + QCOMPARE(o2->geometry(), geometries.at(1)); + + //and check XDGOutput is synced + QScopedPointer xdgO1(xdgOutputManager->getXdgOutput(o1.data())); + QSignalSpy xdgO1ChangedSpy(xdgO1.data(), &XdgOutput::changed); + QVERIFY(xdgO1ChangedSpy.isValid()); + QVERIFY(xdgO1ChangedSpy.wait()); + QCOMPARE(xdgO1->logicalPosition(), geometries.at(0).topLeft()); + QCOMPARE(xdgO1->logicalSize(), geometries.at(0).size()); + QScopedPointer xdgO2(xdgOutputManager->getXdgOutput(o2.data())); + QSignalSpy xdgO2ChangedSpy(xdgO2.data(), &XdgOutput::changed); + QVERIFY(xdgO2ChangedSpy.isValid()); + QVERIFY(xdgO2ChangedSpy.wait()); + QCOMPARE(xdgO2->logicalPosition(), geometries.at(1).topLeft()); + QCOMPARE(xdgO2->logicalSize(), geometries.at(1).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(); + screensChangedSpy.clear(); + + QSignalSpy o1RemovedSpy(o1.data(), &Output::removed); + QVERIFY(o1RemovedSpy.isValid()); + QSignalSpy o2RemovedSpy(o2.data(), &Output::removed); + QVERIFY(o2RemovedSpy.isValid()); + + const QVector geometries2{QRect(0, 0, 1280, 1024)}; + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", + Qt::DirectConnection, + Q_ARG(int, 1), + Q_ARG(QVector, geometries2)); + QVERIFY(screensChangedSpy.wait()); + QCOMPARE(screensChangedSpy.count(), 1); + QCOMPARE(screens()->count(), 1); + QCOMPARE(screens()->geometry(0), 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/autotests/integration/screenedge_client_show_test.cpp b/autotests/integration/screenedge_client_show_test.cpp new file mode 100644 index 0000000..637b63b --- /dev/null +++ b/autotests/integration/screenedge_client_show_test.cpp @@ -0,0 +1,283 @@ +/* + 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 "platform.h" +#include "x11client.h" +#include "cursor.h" +#include "deleted.h" +#include "screenedge.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include + +#include +#include + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_screenedge_client_show-0"); + +class ScreenEdgeClientShowTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void testScreenEdgeShowHideX11_data(); + void testScreenEdgeShowHideX11(); + void testScreenEdgeShowX11Touch_data(); + void testScreenEdgeShowX11Touch(); +}; + +void ScreenEdgeClientShowTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + // set custom config which disable touch edge + KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup group = config->group("TabBox"); + group.writeEntry(QStringLiteral("TouchBorderActivate"), "9"); + group.sync(); + + kwinApp()->setConfig(config); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + setenv("QT_QPA_PLATFORM", "wayland", true); + waylandServer()->initWorkspace(); +} + +void ScreenEdgeClientShowTest::init() +{ + screens()->setCurrent(0); + Cursors::self()->mouse()->setPos(QPoint(640, 512)); + QVERIFY(waylandServer()->clients().isEmpty()); +} + + +struct XcbConnectionDeleter +{ + static inline void cleanup(xcb_connection_t *pointer) + { + xcb_disconnect(pointer); + } +}; + +void ScreenEdgeClientShowTest::testScreenEdgeShowHideX11_data() +{ + QTest::addColumn("windowGeometry"); + QTest::addColumn("resizedWindowGeometry"); + QTest::addColumn("location"); + QTest::addColumn("triggerPos"); + + QTest::newRow("bottom/left") << QRect(50, 1004, 1180, 20) << QRect(150, 1004, 1000, 20) << 2u << QPoint(100, 1023); + QTest::newRow("bottom/right") << QRect(1330, 1004, 1180, 20) << QRect(1410, 1004, 1000, 20) << 2u << QPoint(1400, 1023); + QTest::newRow("top/left") << QRect(50, 0, 1180, 20) << QRect(150, 0, 1000, 20) << 0u << QPoint(100, 0); + QTest::newRow("top/right") << QRect(1330, 0, 1180, 20) << QRect(1410, 0, 1000, 20) << 0u << QPoint(1400, 0); + QTest::newRow("left") << QRect(0, 10, 20, 1000) << QRect(0, 70, 20, 800) << 3u << QPoint(0, 50); + QTest::newRow("right") << QRect(2540, 10, 20, 1000) << QRect(2540, 70, 20, 800) << 1u << QPoint(2559, 60); +} + +void ScreenEdgeClientShowTest::testScreenEdgeShowHideX11() +{ + // this test creates a window which borders the screen and sets the screenedge show hint + // that should trigger a show of the window whenever the cursor is pushed against the screen edge + + // create the test window + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + // atom for the screenedge show hide functionality + Xcb::Atom atom(QByteArrayLiteral("_KDE_NET_WM_SCREEN_EDGE_SHOW"), false, c.data()); + + xcb_window_t w = xcb_generate_id(c.data()); + QFETCH(QRect, windowGeometry); + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, 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; + memset(&hints, 0, sizeof(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.data(), w, &hints); + NETWinInfo info(c.data(), w, rootWindow(), NET::WMAllProperties, NET::WM2AllProperties); + info.setWindowType(NET::Dock); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.last().first().value(); + QVERIFY(client); + QVERIFY(!client->isDecorated()); + QCOMPARE(client->frameGeometry(), windowGeometry); + QVERIFY(!client->hasStrut()); + QVERIFY(!client->isHiddenInternal()); + + QSignalSpy effectsWindowAdded(effects, &EffectsHandler::windowAdded); + QVERIFY(effectsWindowAdded.isValid()); + QVERIFY(effectsWindowAdded.wait()); + + // now try to hide + QFETCH(quint32, location); + xcb_change_property(c.data(), XCB_PROP_MODE_REPLACE, w, atom, XCB_ATOM_CARDINAL, 32, 1, &location); + xcb_flush(c.data()); + + QSignalSpy effectsWindowHiddenSpy(effects, &EffectsHandler::windowHidden); + QVERIFY(effectsWindowHiddenSpy.isValid()); + QSignalSpy clientHiddenSpy(client, &X11Client::windowHidden); + QVERIFY(clientHiddenSpy.isValid()); + QVERIFY(clientHiddenSpy.wait()); + QVERIFY(client->isHiddenInternal()); + QCOMPARE(effectsWindowHiddenSpy.count(), 1); + + // now trigger the edge + QSignalSpy effectsWindowShownSpy(effects, &EffectsHandler::windowShown); + QVERIFY(effectsWindowShownSpy.isValid()); + QFETCH(QPoint, triggerPos); + Cursors::self()->mouse()->setPos(triggerPos); + QVERIFY(!client->isHiddenInternal()); + QCOMPARE(effectsWindowShownSpy.count(), 1); + + // go into event loop to trigger xcb_flush + QTest::qWait(1); + + //hide window again + Cursors::self()->mouse()->setPos(QPoint(640, 512)); + xcb_change_property(c.data(), XCB_PROP_MODE_REPLACE, w, atom, XCB_ATOM_CARDINAL, 32, 1, &location); + xcb_flush(c.data()); + QVERIFY(clientHiddenSpy.wait()); + QVERIFY(client->isHiddenInternal()); + QFETCH(QRect, resizedWindowGeometry); + //resizewhile hidden + client->setFrameGeometry(resizedWindowGeometry); + //triggerPos shouldn't be valid anymore + Cursors::self()->mouse()->setPos(triggerPos); + QVERIFY(client->isHiddenInternal()); + + // destroy window again + QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + xcb_unmap_window(c.data(), w); + xcb_destroy_window(c.data(), w); + xcb_flush(c.data()); + QVERIFY(windowClosedSpy.wait()); +} + +void ScreenEdgeClientShowTest::testScreenEdgeShowX11Touch_data() +{ + QTest::addColumn("windowGeometry"); + QTest::addColumn("location"); + QTest::addColumn("touchDownPos"); + QTest::addColumn("targetPos"); + + QTest::newRow("bottom/left") << QRect(50, 1004, 1180, 20) << 2u << QPoint(100, 1023) << QPoint(100, 540); + QTest::newRow("bottom/right") << QRect(1330, 1004, 1180, 20) << 2u << QPoint(1400, 1023) << QPoint(1400, 520); + QTest::newRow("top/left") << QRect(50, 0, 1180, 20) << 0u << QPoint(100, 0) << QPoint(100, 350); + QTest::newRow("top/right") << QRect(1330, 0, 1180, 20) << 0u << QPoint(1400, 0) << QPoint(1400, 400); + QTest::newRow("left") << QRect(0, 10, 20, 1000) << 3u << QPoint(0, 50) << QPoint(400, 50); + QTest::newRow("right") << QRect(2540, 10, 20, 1000) << 1u << QPoint(2559, 60) << QPoint(2200, 60); +} + +void ScreenEdgeClientShowTest::testScreenEdgeShowX11Touch() +{ + // this test creates a window which borders the screen and sets the screenedge show hint + // that should trigger a show of the window whenever the touch screen swipe gesture is triggered + + // create the test window + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + // atom for the screenedge show hide functionality + Xcb::Atom atom(QByteArrayLiteral("_KDE_NET_WM_SCREEN_EDGE_SHOW"), false, c.data()); + + xcb_window_t w = xcb_generate_id(c.data()); + QFETCH(QRect, windowGeometry); + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, 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; + memset(&hints, 0, sizeof(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.data(), w, &hints); + NETWinInfo info(c.data(), w, rootWindow(), NET::WMAllProperties, NET::WM2AllProperties); + info.setWindowType(NET::Dock); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.last().first().value(); + QVERIFY(client); + QVERIFY(!client->isDecorated()); + QCOMPARE(client->frameGeometry(), windowGeometry); + QVERIFY(!client->hasStrut()); + QVERIFY(!client->isHiddenInternal()); + + QSignalSpy effectsWindowAdded(effects, &EffectsHandler::windowAdded); + QVERIFY(effectsWindowAdded.isValid()); + QVERIFY(effectsWindowAdded.wait()); + + // now try to hide + QFETCH(quint32, location); + xcb_change_property(c.data(), XCB_PROP_MODE_REPLACE, w, atom, XCB_ATOM_CARDINAL, 32, 1, &location); + xcb_flush(c.data()); + + QSignalSpy effectsWindowHiddenSpy(effects, &EffectsHandler::windowHidden); + QVERIFY(effectsWindowHiddenSpy.isValid()); + QSignalSpy clientHiddenSpy(client, &X11Client::windowHidden); + QVERIFY(clientHiddenSpy.isValid()); + QVERIFY(clientHiddenSpy.wait()); + QVERIFY(client->isHiddenInternal()); + QCOMPARE(effectsWindowHiddenSpy.count(), 1); + + // now trigger the edge + QSignalSpy effectsWindowShownSpy(effects, &EffectsHandler::windowShown); + QVERIFY(effectsWindowShownSpy.isValid()); + quint32 timestamp = 0; + QFETCH(QPoint, touchDownPos); + QFETCH(QPoint, targetPos); + kwinApp()->platform()->touchDown(0, touchDownPos, timestamp++); + kwinApp()->platform()->touchMotion(0, targetPos, timestamp++); + kwinApp()->platform()->touchUp(0, timestamp++); + QVERIFY(effectsWindowShownSpy.wait()); + QVERIFY(!client->isHiddenInternal()); + QCOMPARE(effectsWindowShownSpy.count(), 1); + + // destroy window again + QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + xcb_unmap_window(c.data(), w); + xcb_destroy_window(c.data(), w); + xcb_flush(c.data()); + QVERIFY(windowClosedSpy.wait()); +} + +} + +WAYLANDTEST_MAIN(KWin::ScreenEdgeClientShowTest) +#include "screenedge_client_show_test.moc" diff --git a/autotests/integration/scripting/CMakeLists.txt b/autotests/integration/scripting/CMakeLists.txt new file mode 100644 index 0000000..29ea184 --- /dev/null +++ b/autotests/integration/scripting/CMakeLists.txt @@ -0,0 +1,2 @@ +integrationTest(NAME testScriptingScreenEdge SRCS screenedge_test.cpp) +integrationTest(WAYLAND_ONLY NAME testMinimizeAllScript SRCS minimizeall_test.cpp) diff --git a/autotests/integration/scripting/minimizeall_test.cpp b/autotests/integration/scripting/minimizeall_test.cpp new file mode 100644 index 0000000..110baad --- /dev/null +++ b/autotests/integration/scripting/minimizeall_test.cpp @@ -0,0 +1,158 @@ +/* + 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 "abstract_client.h" +#include "platform.h" +#include "screens.h" +#include "scripting/scripting.h" +#include "wayland_server.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(); + + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + waylandServer()->initWorkspace(); +} + +static QString locateMainScript(const QString &pluginName) +{ + const QList offers = KPackage::PackageLoader::self()->findPackages( + QStringLiteral("KWin/Script"), + QStringLiteral("kwin/scripts"), + [&](const KPluginMetaData &metaData) { + return metaData.pluginId() == pluginName; + } + ); + if (offers.isEmpty()) { + return QString(); + } + const KPluginMetaData &metaData = offers.first(); + const QString mainScriptFileName = metaData.value(QStringLiteral("X-Plasma-MainScript")); + const QFileInfo metaDataFileInfo(metaData.fileName()); + return metaDataFileInfo.path() + QLatin1String("/contents/") + mainScriptFileName; +} + +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); + QVERIFY(runningChangedSpy.isValid()); + 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() +{ + // This test verifies that all windows are minimized when Meta+Shift+D + // is pressed, and unminimized when the shortcut is pressed once again. + + using namespace KWayland::Client; + + // Create a couple of test clients. + QScopedPointer surface1(Test::createSurface()); + QScopedPointer shellSurface1(Test::createXdgShellStableSurface(surface1.data())); + AbstractClient *client1 = Test::renderAndWaitForShown(surface1.data(), QSize(100, 50), Qt::blue); + QVERIFY(client1); + QVERIFY(client1->isActive()); + QVERIFY(client1->isMinimizable()); + + QScopedPointer surface2(Test::createSurface()); + QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data())); + AbstractClient *client2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 50), Qt::red); + QVERIFY(client2); + QVERIFY(client2->isActive()); + QVERIFY(client2->isMinimizable()); + + // Minimize the windows. + quint32 timestamp = 1; + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTSHIFT, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_D, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_D, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTSHIFT, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTMETA, timestamp++); + + QTRY_VERIFY(client1->isMinimized()); + QTRY_VERIFY(client2->isMinimized()); + + // Unminimize the windows. + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTSHIFT, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_D, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_D, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTSHIFT, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTMETA, timestamp++); + + QTRY_VERIFY(!client1->isMinimized()); + QTRY_VERIFY(!client2->isMinimized()); + + // Destroy test clients. + shellSurface2.reset(); + QVERIFY(Test::waitForWindowDestroyed(client2)); + shellSurface1.reset(); + QVERIFY(Test::waitForWindowDestroyed(client1)); +} + +} + +WAYLANDTEST_MAIN(KWin::MinimizeAllScriptTest) +#include "minimizeall_test.moc" diff --git a/autotests/integration/scripting/screenedge_test.cpp b/autotests/integration/scripting/screenedge_test.cpp new file mode 100644 index 0000000..e954438 --- /dev/null +++ b/autotests/integration/scripting/screenedge_test.cpp @@ -0,0 +1,287 @@ +/* + 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 "effectloader.h" +#include "platform.h" +#include "wayland_server.h" +#include "workspace.h" +#include "scripting/scripting.h" +#include "effect_builtins.h" + +#define private public +#include "screenedge.h" +#undef private + +#include + +Q_DECLARE_METATYPE(KWin::ElectricBorder) + +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() +{ + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + // 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")); + ScriptedEffectLoader loader; + const auto builtinNames = BuiltInEffects::availableEffectNames() << loader.listOfKnownEffects(); + for (QString name : builtinNames) { + plugins.writeEntry(name + QStringLiteral("Enabled"), false); + } + + // disable electric border pushback + config->group("Windows").writeEntry("ElectricBorderPushbackPixels", 0); + config->group("TabBox").writeEntry("TouchBorderActivate", int(ElectricNone)); + + config->sync(); + kwinApp()->setConfig(config); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QVERIFY(Scripting::self()); + + ScreenEdges::self()->setTimeThreshold(0); + ScreenEdges::self()->setReActivationThreshold(0); +} + +void ScreenEdgeTest::init() +{ + KWin::Cursors::self()->mouse()->setPos(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); + QVERIFY(runningChangedSpy.isValid()); + 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); + QVERIFY(showDesktopSpy.isValid()); + + // trigger the edge + QFETCH(QPoint, triggerPos); + KWin::Cursors::self()->mouse()->setPos(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); + QVERIFY(runningChangedSpy.isValid()); + 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); + QVERIFY(showDesktopSpy.isValid()); + + // trigger the edge + QFETCH(QPoint, triggerPos); + quint32 timestamp = 0; + kwinApp()->platform()->touchDown(0, triggerPos, timestamp++); + QFETCH(QPoint, motionPos); + kwinApp()->platform()->touchMotion(0, motionPos, timestamp++); + kwinApp()->platform()->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); + QVERIFY(showDesktopSpy.isValid()); + + //trigger the edge + KWin::Cursors::self()->mouse()->setPos(triggerPos); + QCOMPARE(showDesktopSpy.count(), 1); + + //reset + KWin::Cursors::self()->mouse()->setPos(500,500); + workspace()->slotToggleShowDesktop(); + showDesktopSpy.clear(); + + //trigger again, to show that retriggering works + KWin::Cursors::self()->mouse()->setPos(triggerPos); + QCOMPARE(showDesktopSpy.count(), 1); + + //reset + KWin::Cursors::self()->mouse()->setPos(500,500); + workspace()->slotToggleShowDesktop(); + showDesktopSpy.clear(); + + //make the script unregister the edge + configGroup.writeEntry("mode", "unregister"); + triggerConfigReload(); + KWin::Cursors::self()->mouse()->setPos(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); + QVERIFY(showDesktopSpy.isValid()); + + // Trigger the edge through touch + quint32 timestamp = 0; + kwinApp()->platform()->touchDown(0, QPointF(0, 50), timestamp++); + kwinApp()->platform()->touchMotion(0, QPointF(500, 50), timestamp++); + kwinApp()->platform()->touchUp(0, timestamp++); + + QVERIFY(showDesktopSpy.wait()); +} + +WAYLANDTEST_MAIN(ScreenEdgeTest) +#include "screenedge_test.moc" diff --git a/autotests/integration/scripting/scripts/screenedge.js b/autotests/integration/scripting/scripts/screenedge.js new file mode 100644 index 0000000..4d02e83 --- /dev/null +++ b/autotests/integration/scripting/scripts/screenedge.js @@ -0,0 +1 @@ +registerScreenEdge(readConfig("Edge", 1), function() { workspace.slotToggleShowDesktop(); }); diff --git a/autotests/integration/scripting/scripts/screenedgetouch.qml b/autotests/integration/scripting/scripts/screenedgetouch.qml new file mode 100644 index 0000000..04c136a --- /dev/null +++ b/autotests/integration/scripting/scripts/screenedgetouch.qml @@ -0,0 +1,10 @@ +import QtQuick 2.0; +import org.kde.kwin 2.0; + +ScreenEdgeItem { + edge: ScreenEdgeItem.LeftEdge + mode: ScreenEdgeItem.Touch + onActivated: { + workspace.slotToggleShowDesktop(); + } +} diff --git a/autotests/integration/scripting/scripts/screenedgeunregister.js b/autotests/integration/scripting/scripts/screenedgeunregister.js new file mode 100644 index 0000000..a73b68e --- /dev/null +++ b/autotests/integration/scripting/scripts/screenedgeunregister.js @@ -0,0 +1,12 @@ +function init() { + var edge = readConfig("Edge", 1); + if (readConfig("mode", "") == "unregister") { + unregisterScreenEdge(edge); + } else { + registerScreenEdge(edge, function() { workspace.slotToggleShowDesktop(); }); + } +} +options.configChanged.connect(init); + +init(); + diff --git a/autotests/integration/scripting/scripts/touchScreenedge.js b/autotests/integration/scripting/scripts/touchScreenedge.js new file mode 100644 index 0000000..b796a36 --- /dev/null +++ b/autotests/integration/scripting/scripts/touchScreenedge.js @@ -0,0 +1 @@ +registerTouchScreenEdge(readConfig("Edge", 1), function() { workspace.slotToggleShowDesktop(); }); diff --git a/autotests/integration/shade_test.cpp b/autotests/integration/shade_test.cpp new file mode 100644 index 0000000..9ef9ce2 --- /dev/null +++ b/autotests/integration/shade_test.cpp @@ -0,0 +1,130 @@ +/* + 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 "platform.h" +#include "x11client.h" +#include "cursor.h" +#include "screenedge.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include + +#include + +#include +#include + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_shade-0"); + +class ShadeTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void testShadeGeometry(); +}; + +void ShadeTest::initTestCase() +{ + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + setenv("QT_QPA_PLATFORM", "wayland", true); + waylandServer()->initWorkspace(); +} + +void ShadeTest::init() +{ + screens()->setCurrent(0); + Cursors::self()->mouse()->setPos(QPoint(640, 512)); +} + +void ShadeTest::testShadeGeometry() +{ + // this test verifies that the geometry is properly restored after shading + // see BUG: 362501 + // create an xcb window + struct XcbConnectionDeleter + { + static inline void cleanup(xcb_connection_t *pointer) + { + xcb_disconnect(pointer); + } + }; + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + const QRect windowGeometry(0, 0, 100, 200); + xcb_window_t w = xcb_generate_id(c.data()); + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, 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; + memset(&hints, 0, sizeof(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.data(), w, &hints); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + // we should get a client for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->window(), w); + QVERIFY(client->isDecorated()); + QVERIFY(client->isShadeable()); + QVERIFY(!client->isShade()); + QVERIFY(client->isActive()); + + // now shade the window + const QRect geoBeforeShade = client->frameGeometry(); + QVERIFY(geoBeforeShade.isValid()); + QVERIFY(!geoBeforeShade.isEmpty()); + workspace()->slotWindowShade(); + QVERIFY(client->isShade()); + QVERIFY(client->frameGeometry() != geoBeforeShade); + // and unshade again + workspace()->slotWindowShade(); + QVERIFY(!client->isShade()); + QCOMPARE(client->frameGeometry(), geoBeforeShade); + + // and destroy the window again + xcb_unmap_window(c.data(), w); + xcb_destroy_window(c.data(), w); + xcb_flush(c.data()); + c.reset(); + + QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + QVERIFY(windowClosedSpy.wait()); +} + +} + +WAYLANDTEST_MAIN(KWin::ShadeTest) +#include "shade_test.moc" diff --git a/autotests/integration/showing_desktop_test.cpp b/autotests/integration/showing_desktop_test.cpp new file mode 100644 index 0000000..62b07ff --- /dev/null +++ b/autotests/integration/showing_desktop_test.cpp @@ -0,0 +1,115 @@ +/* + 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 "abstract_client.h" +#include "platform.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include + +using namespace KWin; +using namespace KWayland::Client; + +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 ShowingDesktopTest::initTestCase() +{ + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + waylandServer()->initWorkspace(); +} + +void ShowingDesktopTest::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::PlasmaShell)); +} + +void ShowingDesktopTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void ShowingDesktopTest::testRestoreFocus() +{ + QScopedPointer surface1(Test::createSurface()); + QScopedPointer shellSurface1(Test::createXdgShellStableSurface(surface1.data())); + auto client1 = Test::renderAndWaitForShown(surface1.data(), QSize(100, 50), Qt::blue); + QScopedPointer surface2(Test::createSurface()); + QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data())); + auto client2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 50), Qt::blue); + QVERIFY(client1 != client2); + + QCOMPARE(workspace()->activeClient(), client2); + workspace()->slotToggleShowDesktop(); + QVERIFY(workspace()->showingDesktop()); + workspace()->slotToggleShowDesktop(); + QVERIFY(!workspace()->showingDesktop()); + + QVERIFY(workspace()->activeClient()); + QCOMPARE(workspace()->activeClient(), client2); +} + +void ShowingDesktopTest::testRestoreFocusWithDesktopWindow() +{ + // first create a desktop window + + QScopedPointer desktopSurface(Test::createSurface()); + QVERIFY(!desktopSurface.isNull()); + QScopedPointer desktopShellSurface(Test::createXdgShellStableSurface(desktopSurface.data())); + QVERIFY(!desktopSurface.isNull()); + QScopedPointer plasmaSurface(Test::waylandPlasmaShell()->createSurface(desktopSurface.data())); + QVERIFY(!plasmaSurface.isNull()); + plasmaSurface->setRole(PlasmaShellSurface::Role::Desktop); + + auto desktop = Test::renderAndWaitForShown(desktopSurface.data(), QSize(100, 50), Qt::blue); + QVERIFY(desktop); + QVERIFY(desktop->isDesktop()); + + // now create some windows + QScopedPointer surface1(Test::createSurface()); + QScopedPointer shellSurface1(Test::createXdgShellStableSurface(surface1.data())); + auto client1 = Test::renderAndWaitForShown(surface1.data(), QSize(100, 50), Qt::blue); + QScopedPointer surface2(Test::createSurface()); + QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data())); + auto client2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 50), Qt::blue); + QVERIFY(client1 != client2); + + QCOMPARE(workspace()->activeClient(), client2); + workspace()->slotToggleShowDesktop(); + QVERIFY(workspace()->showingDesktop()); + QCOMPARE(workspace()->activeClient(), desktop); + workspace()->slotToggleShowDesktop(); + QVERIFY(!workspace()->showingDesktop()); + + QVERIFY(workspace()->activeClient()); + QCOMPARE(workspace()->activeClient(), client2); +} + +WAYLANDTEST_MAIN(ShowingDesktopTest) +#include "showing_desktop_test.moc" diff --git a/autotests/integration/stacking_order_test.cpp b/autotests/integration/stacking_order_test.cpp new file mode 100644 index 0000000..10f2a3a --- /dev/null +++ b/autotests/integration/stacking_order_test.cpp @@ -0,0 +1,897 @@ +/* + 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 "abstract_client.h" +#include "atoms.h" +#include "x11client.h" +#include "deleted.h" +#include "main.h" +#include "platform.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include + +#include +#include + +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 testDeletedTransient(); + + void testGroupTransientIsAboveWindowGroup(); + void testRaiseGroupTransient(); + void testDeletedGroupTransient(); + void testDontKeepAboveNonModalDialogGroupTransients(); + + void testKeepAbove(); + void testKeepBelow(); + +}; + +void StackingOrderTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + waylandServer()->initWorkspace(); +} + +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. + KWayland::Client::Surface *parentSurface = + Test::createSurface(Test::waylandCompositor()); + QVERIFY(parentSurface); + KWayland::Client::XdgShellSurface *parentShellSurface = + Test::createXdgShellStableSurface(parentSurface, parentSurface); + QVERIFY(parentShellSurface); + AbstractClient *parent = Test::renderAndWaitForShown(parentSurface, 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. + KWayland::Client::Surface *transientSurface = + Test::createSurface(Test::waylandCompositor()); + QVERIFY(transientSurface); + KWayland::Client::XdgShellSurface *transientShellSurface = + Test::createXdgShellStableSurface(transientSurface, transientSurface); + QVERIFY(transientShellSurface); + transientShellSurface->setTransientFor(parentShellSurface); + AbstractClient *transient = Test::renderAndWaitForShown( + transientSurface, 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()->activateClient(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. + KWayland::Client::Surface *parentSurface = + Test::createSurface(Test::waylandCompositor()); + QVERIFY(parentSurface); + KWayland::Client::XdgShellSurface *parentShellSurface = + Test::createXdgShellStableSurface(parentSurface, parentSurface); + QVERIFY(parentShellSurface); + AbstractClient *parent = Test::renderAndWaitForShown(parentSurface, 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. + KWayland::Client::Surface *transientSurface = + Test::createSurface(Test::waylandCompositor()); + QVERIFY(transientSurface); + KWayland::Client::XdgShellSurface *transientShellSurface = + Test::createXdgShellStableSurface(transientSurface, transientSurface); + QVERIFY(transientShellSurface); + transientShellSurface->setTransientFor(parentShellSurface); + AbstractClient *transient = Test::renderAndWaitForShown( + transientSurface, 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. + KWayland::Client::Surface *anotherSurface = + Test::createSurface(Test::waylandCompositor()); + QVERIFY(anotherSurface); + KWayland::Client::XdgShellSurface *anotherShellSurface = + Test::createXdgShellStableSurface(anotherSurface, anotherSurface); + QVERIFY(anotherShellSurface); + AbstractClient *anotherClient = Test::renderAndWaitForShown(anotherSurface, QSize(128, 128), Qt::green); + QVERIFY(anotherClient); + QVERIFY(anotherClient->isActive()); + QVERIFY(!anotherClient->isTransient()); + + // The newly created surface has to be above both the parent and the transient. + QCOMPARE(workspace()->stackingOrder(), (QList{parent, transient, anotherClient})); + + // If we activate the parent, the transient should be raised too. + workspace()->activateClient(parent); + QTRY_VERIFY(parent->isActive()); + QTRY_VERIFY(!transient->isActive()); + QTRY_VERIFY(!anotherClient->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{anotherClient, parent, transient})); + + // Go back to the initial setup. + workspace()->activateClient(anotherClient); + QTRY_VERIFY(!parent->isActive()); + QTRY_VERIFY(!transient->isActive()); + QTRY_VERIFY(anotherClient->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{parent, transient, anotherClient})); + + // If we activate the transient, the parent should be raised too. + workspace()->activateClient(transient); + QTRY_VERIFY(!parent->isActive()); + QTRY_VERIFY(transient->isActive()); + QTRY_VERIFY(!anotherClient->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{anotherClient, parent, transient})); +} + +struct WindowUnrefDeleter +{ + static inline void cleanup(Deleted *d) { + if (d != nullptr) { + d->unrefWindow(); + } + } +}; + +void StackingOrderTest::testDeletedTransient() +{ + // This test verifies that deleted transients are kept above their + // old parents. + + // Create the parent. + KWayland::Client::Surface *parentSurface = + Test::createSurface(Test::waylandCompositor()); + QVERIFY(parentSurface); + KWayland::Client::XdgShellSurface *parentShellSurface = + Test::createXdgShellStableSurface(parentSurface, parentSurface); + QVERIFY(parentShellSurface); + AbstractClient *parent = Test::renderAndWaitForShown(parentSurface, QSize(256, 256), Qt::blue); + QVERIFY(parent); + QVERIFY(parent->isActive()); + QVERIFY(!parent->isTransient()); + + QCOMPARE(workspace()->stackingOrder(), (QList{parent})); + + // Create the first transient. + KWayland::Client::Surface *transient1Surface = + Test::createSurface(Test::waylandCompositor()); + QVERIFY(transient1Surface); + KWayland::Client::XdgShellSurface *transient1ShellSurface = + Test::createXdgShellStableSurface(transient1Surface, transient1Surface); + QVERIFY(transient1ShellSurface); + transient1ShellSurface->setTransientFor(parentShellSurface); + AbstractClient *transient1 = Test::renderAndWaitForShown( + transient1Surface, 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. + KWayland::Client::Surface *transient2Surface = + Test::createSurface(Test::waylandCompositor()); + QVERIFY(transient2Surface); + KWayland::Client::XdgShellSurface *transient2ShellSurface = + Test::createXdgShellStableSurface(transient2Surface, transient2Surface); + QVERIFY(transient2ShellSurface); + transient2ShellSurface->setTransientFor(transient1ShellSurface); + AbstractClient *transient2 = Test::renderAndWaitForShown( + transient2Surface, 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()->activateClient(parent); + QTRY_VERIFY(parent->isActive()); + QTRY_VERIFY(!transient1->isActive()); + QTRY_VERIFY(!transient2->isActive()); + + // Close the top-most transient. + connect(transient2, &AbstractClient::windowClosed, this, + [](Toplevel *toplevel, Deleted *deleted) { + Q_UNUSED(toplevel) + deleted->refWindow(); + } + ); + + QSignalSpy windowClosedSpy(transient2, &AbstractClient::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + delete transient2ShellSurface; + delete transient2Surface; + QVERIFY(windowClosedSpy.wait()); + + QScopedPointer deletedTransient( + windowClosedSpy.first().at(1).value()); + QVERIFY(deletedTransient.data()); + + // 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, deletedTransient.data()})); +} + +static xcb_window_t createGroupWindow(xcb_connection_t *conn, + const QRect &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; +} + +struct XcbConnectionDeleter +{ + static inline void cleanup(xcb_connection_t *c) { + xcb_disconnect(c); + } +}; + +void StackingOrderTest::testGroupTransientIsAboveWindowGroup() +{ + // This test verifies that group transients are always above other + // window group members. + + const QRect geometry = QRect(0, 0, 128, 128); + + QScopedPointer conn( + xcb_connect(nullptr, nullptr)); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + + // Create the group leader. + xcb_window_t leaderWid = createGroupWindow(conn.data(), geometry); + xcb_map_window(conn.data(), leaderWid); + xcb_flush(conn.data()); + + QVERIFY(windowCreatedSpy.wait()); + X11Client *leader = windowCreatedSpy.first().first().value(); + QVERIFY(leader); + QVERIFY(leader->isActive()); + QCOMPARE(leader->windowId(), leaderWid); + QVERIFY(!leader->isTransient()); + + QCOMPARE(workspace()->stackingOrder(), (QList{leader})); + + // Create another group member. + windowCreatedSpy.clear(); + xcb_window_t member1Wid = createGroupWindow(conn.data(), geometry, leaderWid); + xcb_map_window(conn.data(), member1Wid); + xcb_flush(conn.data()); + + QVERIFY(windowCreatedSpy.wait()); + X11Client *member1 = windowCreatedSpy.first().first().value(); + QVERIFY(member1); + QVERIFY(member1->isActive()); + QCOMPARE(member1->windowId(), 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.data(), geometry, leaderWid); + xcb_map_window(conn.data(), member2Wid); + xcb_flush(conn.data()); + + QVERIFY(windowCreatedSpy.wait()); + X11Client *member2 = windowCreatedSpy.first().first().value(); + QVERIFY(member2); + QVERIFY(member2->isActive()); + QCOMPARE(member2->windowId(), 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.data(), geometry, leaderWid); + xcb_icccm_set_wm_transient_for(conn.data(), 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.data()); + xcb_atom_t net_wm_window_type_normal = Xcb::Atom( + QByteArrayLiteral("_NET_WM_WINDOW_TYPE_NORMAL"), false, conn.data()); + xcb_change_property( + conn.data(), // 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.data(), transientWid); + xcb_flush(conn.data()); + + QVERIFY(windowCreatedSpy.wait()); + X11Client *transient = windowCreatedSpy.first().first().value(); + QVERIFY(transient); + QVERIFY(transient->isActive()); + QCOMPARE(transient->windowId(), 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()->activateClient(leader); + QTRY_VERIFY(leader->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{member1, member2, leader, transient})); + + workspace()->activateClient(member1); + QTRY_VERIFY(member1->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{member2, leader, member1, transient})); + + workspace()->activateClient(member2); + QTRY_VERIFY(member2->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{leader, member1, member2, transient})); + + workspace()->activateClient(transient); + QTRY_VERIFY(transient->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{leader, member1, member2, transient})); +} + +void StackingOrderTest::testRaiseGroupTransient() +{ + const QRect geometry = QRect(0, 0, 128, 128); + + QScopedPointer conn( + xcb_connect(nullptr, nullptr)); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + + // Create the group leader. + xcb_window_t leaderWid = createGroupWindow(conn.data(), geometry); + xcb_map_window(conn.data(), leaderWid); + xcb_flush(conn.data()); + + QVERIFY(windowCreatedSpy.wait()); + X11Client *leader = windowCreatedSpy.first().first().value(); + QVERIFY(leader); + QVERIFY(leader->isActive()); + QCOMPARE(leader->windowId(), leaderWid); + QVERIFY(!leader->isTransient()); + + QCOMPARE(workspace()->stackingOrder(), (QList{leader})); + + // Create another group member. + windowCreatedSpy.clear(); + xcb_window_t member1Wid = createGroupWindow(conn.data(), geometry, leaderWid); + xcb_map_window(conn.data(), member1Wid); + xcb_flush(conn.data()); + + QVERIFY(windowCreatedSpy.wait()); + X11Client *member1 = windowCreatedSpy.first().first().value(); + QVERIFY(member1); + QVERIFY(member1->isActive()); + QCOMPARE(member1->windowId(), 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.data(), geometry, leaderWid); + xcb_map_window(conn.data(), member2Wid); + xcb_flush(conn.data()); + + QVERIFY(windowCreatedSpy.wait()); + X11Client *member2 = windowCreatedSpy.first().first().value(); + QVERIFY(member2); + QVERIFY(member2->isActive()); + QCOMPARE(member2->windowId(), 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.data(), geometry, leaderWid); + xcb_icccm_set_wm_transient_for(conn.data(), 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.data()); + xcb_atom_t net_wm_window_type_normal = Xcb::Atom( + QByteArrayLiteral("_NET_WM_WINDOW_TYPE_NORMAL"), false, conn.data()); + xcb_change_property( + conn.data(), // 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.data(), transientWid); + xcb_flush(conn.data()); + + QVERIFY(windowCreatedSpy.wait()); + X11Client *transient = windowCreatedSpy.first().first().value(); + QVERIFY(transient); + QVERIFY(transient->isActive()); + QCOMPARE(transient->windowId(), 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 client that is not a member of the window group. + KWayland::Client::Surface *anotherSurface = + Test::createSurface(Test::waylandCompositor()); + QVERIFY(anotherSurface); + KWayland::Client::XdgShellSurface *anotherShellSurface = + Test::createXdgShellStableSurface(anotherSurface, anotherSurface); + QVERIFY(anotherShellSurface); + AbstractClient *anotherClient = Test::renderAndWaitForShown(anotherSurface, QSize(128, 128), Qt::green); + QVERIFY(anotherClient); + QVERIFY(anotherClient->isActive()); + QVERIFY(!anotherClient->isTransient()); + + QCOMPARE(workspace()->stackingOrder(), (QList{leader, member1, member2, transient, anotherClient})); + + // If we activate the leader, then only it and the transient have to be raised. + workspace()->activateClient(leader); + QTRY_VERIFY(leader->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{member1, member2, anotherClient, leader, transient})); + + // If another member of the window group is activated, then the transient will + // be above that member and the leader. + workspace()->activateClient(member2); + QTRY_VERIFY(member2->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{member1, anotherClient, leader, member2, transient})); + + // FIXME: If we activate the transient, only it will be raised. + workspace()->activateClient(anotherClient); + QTRY_VERIFY(anotherClient->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{member1, leader, member2, transient, anotherClient})); + + workspace()->activateClient(transient); + QTRY_VERIFY(transient->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{member1, leader, member2, anotherClient, transient})); +} + +void StackingOrderTest::testDeletedGroupTransient() +{ + // This test verifies that deleted group transients are kept above their + // old window groups. + + const QRect geometry = QRect(0, 0, 128, 128); + + QScopedPointer conn( + xcb_connect(nullptr, nullptr)); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + + // Create the group leader. + xcb_window_t leaderWid = createGroupWindow(conn.data(), geometry); + xcb_map_window(conn.data(), leaderWid); + xcb_flush(conn.data()); + + QVERIFY(windowCreatedSpy.wait()); + X11Client *leader = windowCreatedSpy.first().first().value(); + QVERIFY(leader); + QVERIFY(leader->isActive()); + QCOMPARE(leader->windowId(), leaderWid); + QVERIFY(!leader->isTransient()); + + QCOMPARE(workspace()->stackingOrder(), (QList{leader})); + + // Create another group member. + windowCreatedSpy.clear(); + xcb_window_t member1Wid = createGroupWindow(conn.data(), geometry, leaderWid); + xcb_map_window(conn.data(), member1Wid); + xcb_flush(conn.data()); + + QVERIFY(windowCreatedSpy.wait()); + X11Client *member1 = windowCreatedSpy.first().first().value(); + QVERIFY(member1); + QVERIFY(member1->isActive()); + QCOMPARE(member1->windowId(), 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.data(), geometry, leaderWid); + xcb_map_window(conn.data(), member2Wid); + xcb_flush(conn.data()); + + QVERIFY(windowCreatedSpy.wait()); + X11Client *member2 = windowCreatedSpy.first().first().value(); + QVERIFY(member2); + QVERIFY(member2->isActive()); + QCOMPARE(member2->windowId(), 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.data(), geometry, leaderWid); + xcb_icccm_set_wm_transient_for(conn.data(), 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.data()); + xcb_atom_t net_wm_window_type_normal = Xcb::Atom( + QByteArrayLiteral("_NET_WM_WINDOW_TYPE_NORMAL"), false, conn.data()); + xcb_change_property( + conn.data(), // 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.data(), transientWid); + xcb_flush(conn.data()); + + QVERIFY(windowCreatedSpy.wait()); + X11Client *transient = windowCreatedSpy.first().first().value(); + QVERIFY(transient); + QVERIFY(transient->isActive()); + QCOMPARE(transient->windowId(), 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, &X11Client::windowClosed, this, + [](Toplevel *toplevel, Deleted *deleted) { + Q_UNUSED(toplevel) + deleted->refWindow(); + } + ); + + QSignalSpy windowClosedSpy(transient, &X11Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + xcb_unmap_window(conn.data(), transientWid); + xcb_flush(conn.data()); + QVERIFY(windowClosedSpy.wait()); + + QScopedPointer deletedTransient( + windowClosedSpy.first().at(1).value()); + QVERIFY(deletedTransient.data()); + + // The transient has to be above each member of the window group. + QCOMPARE(workspace()->stackingOrder(), (QList{leader, member1, member2, deletedTransient.data()})); +} + +void StackingOrderTest::testDontKeepAboveNonModalDialogGroupTransients() +{ + // Bug 76026 + + const QRect geometry = QRect(0, 0, 128, 128); + + QScopedPointer conn( + xcb_connect(nullptr, nullptr)); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + + // Create the group leader. + xcb_window_t leaderWid = createGroupWindow(conn.data(), geometry); + xcb_map_window(conn.data(), leaderWid); + xcb_flush(conn.data()); + + QVERIFY(windowCreatedSpy.wait()); + X11Client *leader = windowCreatedSpy.first().first().value(); + QVERIFY(leader); + QVERIFY(leader->isActive()); + QCOMPARE(leader->windowId(), leaderWid); + QVERIFY(!leader->isTransient()); + + QCOMPARE(workspace()->stackingOrder(), (QList{leader})); + + // Create another group member. + windowCreatedSpy.clear(); + xcb_window_t member1Wid = createGroupWindow(conn.data(), geometry, leaderWid); + xcb_map_window(conn.data(), member1Wid); + xcb_flush(conn.data()); + + QVERIFY(windowCreatedSpy.wait()); + X11Client *member1 = windowCreatedSpy.first().first().value(); + QVERIFY(member1); + QVERIFY(member1->isActive()); + QCOMPARE(member1->windowId(), 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.data(), geometry, leaderWid); + xcb_map_window(conn.data(), member2Wid); + xcb_flush(conn.data()); + + QVERIFY(windowCreatedSpy.wait()); + X11Client *member2 = windowCreatedSpy.first().first().value(); + QVERIFY(member2); + QVERIFY(member2->isActive()); + QCOMPARE(member2->windowId(), 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.data(), geometry, leaderWid); + xcb_icccm_set_wm_transient_for(conn.data(), transientWid, rootWindow()); + xcb_map_window(conn.data(), transientWid); + xcb_flush(conn.data()); + + QVERIFY(windowCreatedSpy.wait()); + X11Client *transient = windowCreatedSpy.first().first().value(); + QVERIFY(transient); + QVERIFY(transient->isActive()); + QCOMPARE(transient->windowId(), 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()->activateClient(leader); + QTRY_VERIFY(leader->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{member1, member2, transient, leader})); + + workspace()->activateClient(member1); + QTRY_VERIFY(member1->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{member2, transient, leader, member1})); + + workspace()->activateClient(member2); + QTRY_VERIFY(member2->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{transient, leader, member1, member2})); + + workspace()->activateClient(transient); + QTRY_VERIFY(transient->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{leader, member1, member2, transient})); +} + +void StackingOrderTest::testKeepAbove() +{ + // This test verifies that "keep-above" windows are kept above other windows. + + // Create the first client. + KWayland::Client::Surface *clientASurface = + Test::createSurface(Test::waylandCompositor()); + QVERIFY(clientASurface); + KWayland::Client::XdgShellSurface *clientAShellSurface = + Test::createXdgShellStableSurface(clientASurface, clientASurface); + QVERIFY(clientAShellSurface); + AbstractClient *clientA = Test::renderAndWaitForShown(clientASurface, QSize(128, 128), Qt::green); + QVERIFY(clientA); + QVERIFY(clientA->isActive()); + QVERIFY(!clientA->keepAbove()); + + QCOMPARE(workspace()->stackingOrder(), (QList{clientA})); + + // Create the second client. + KWayland::Client::Surface *clientBSurface = + Test::createSurface(Test::waylandCompositor()); + QVERIFY(clientBSurface); + KWayland::Client::XdgShellSurface *clientBShellSurface = + Test::createXdgShellStableSurface(clientBSurface, clientBSurface); + QVERIFY(clientBShellSurface); + AbstractClient *clientB = Test::renderAndWaitForShown(clientBSurface, QSize(128, 128), Qt::green); + QVERIFY(clientB); + QVERIFY(clientB->isActive()); + QVERIFY(!clientB->keepAbove()); + + QCOMPARE(workspace()->stackingOrder(), (QList{clientA, clientB})); + + // Go to the initial test position. + workspace()->activateClient(clientA); + QTRY_VERIFY(clientA->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{clientB, clientA})); + + // Set the "keep-above" flag on the client B, it should go above other clients. + { + StackingUpdatesBlocker blocker(workspace()); + clientB->setKeepAbove(true); + } + + QVERIFY(clientB->keepAbove()); + QVERIFY(!clientB->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{clientA, clientB})); +} + +void StackingOrderTest::testKeepBelow() +{ + // This test verifies that "keep-below" windows are kept below other windows. + + // Create the first client. + KWayland::Client::Surface *clientASurface = + Test::createSurface(Test::waylandCompositor()); + QVERIFY(clientASurface); + KWayland::Client::XdgShellSurface *clientAShellSurface = + Test::createXdgShellStableSurface(clientASurface, clientASurface); + QVERIFY(clientAShellSurface); + AbstractClient *clientA = Test::renderAndWaitForShown(clientASurface, QSize(128, 128), Qt::green); + QVERIFY(clientA); + QVERIFY(clientA->isActive()); + QVERIFY(!clientA->keepBelow()); + + QCOMPARE(workspace()->stackingOrder(), (QList{clientA})); + + // Create the second client. + KWayland::Client::Surface *clientBSurface = + Test::createSurface(Test::waylandCompositor()); + QVERIFY(clientBSurface); + KWayland::Client::XdgShellSurface *clientBShellSurface = + Test::createXdgShellStableSurface(clientBSurface, clientBSurface); + QVERIFY(clientBShellSurface); + AbstractClient *clientB = Test::renderAndWaitForShown(clientBSurface, QSize(128, 128), Qt::green); + QVERIFY(clientB); + QVERIFY(clientB->isActive()); + QVERIFY(!clientB->keepBelow()); + + QCOMPARE(workspace()->stackingOrder(), (QList{clientA, clientB})); + + // Set the "keep-below" flag on the client B, it should go below other clients. + { + StackingUpdatesBlocker blocker(workspace()); + clientB->setKeepBelow(true); + } + + QVERIFY(clientB->isActive()); + QVERIFY(clientB->keepBelow()); + QCOMPARE(workspace()->stackingOrder(), (QList{clientB, clientA})); +} + +WAYLANDTEST_MAIN(StackingOrderTest) +#include "stacking_order_test.moc" diff --git a/autotests/integration/struts_test.cpp b/autotests/integration/struts_test.cpp new file mode 100644 index 0000000..43864b7 --- /dev/null +++ b/autotests/integration/struts_test.cpp @@ -0,0 +1,954 @@ +/* + 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 "platform.h" +#include "x11client.h" +#include "cursor.h" +#include "deleted.h" +#include "screenedge.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include + +#include +#include +#include + +#include + +#include +#include + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_struts-0"); + +class StrutsTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + void testWaylandStruts_data(); + void testWaylandStruts(); + void testMoveWaylandPanel(); + void testWaylandMobilePanel(); + void testX11Struts_data(); + void testX11Struts(); + void test363804(); + void testLeftScreenSmallerBottomAligned(); + void testWindowMoveWithPanelBetweenScreens(); + +private: + KWayland::Client::Compositor *m_compositor = nullptr; + KWayland::Client::PlasmaShell *m_plasmaShell = nullptr; +}; + +void StrutsTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + // set custom config which disables the Outline + KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup group = config->group("Outline"); + group.writeEntry(QStringLiteral("QmlPath"), QString("/does/not/exist.qml")); + group.sync(); + + kwinApp()->setConfig(config); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + setenv("QT_QPA_PLATFORM", "wayland", true); + waylandServer()->initWorkspace(); +} + +void StrutsTest::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::PlasmaShell)); + m_compositor = Test::waylandCompositor(); + m_plasmaShell = Test::waylandPlasmaShell(); + + screens()->setCurrent(0); + Cursors::self()->mouse()->setPos(QPoint(640, 512)); + QVERIFY(waylandServer()->clients().isEmpty()); +} + +void StrutsTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void StrutsTest::testWaylandStruts_data() +{ + QTest::addColumn>("windowGeometries"); + QTest::addColumn("screen0Maximized"); + QTest::addColumn("screen1Maximized"); + QTest::addColumn("workArea"); + QTest::addColumn("restrictedMoveArea"); + + QTest::newRow("bottom/0") << QVector{QRect(0, 992, 1280, 32)} << QRect(0, 0, 1280, 992) << QRect(1280, 0, 1280, 1024) << QRect(0, 0, 2560, 992) << QRegion(0, 992, 1280, 32); + QTest::newRow("bottom/1") << QVector{QRect(1280, 992, 1280, 32)} << QRect(0, 0, 1280, 1024) << QRect(1280, 0, 1280, 992) << QRect(0, 0, 2560, 992) << QRegion(1280, 992, 1280, 32); + QTest::newRow("top/0") << QVector{QRect(0, 0, 1280, 32)} << QRect(0, 32, 1280, 992) << QRect(1280, 0, 1280, 1024) << QRect(0, 32, 2560, 992) << QRegion(0, 0, 1280, 32); + QTest::newRow("top/1") << QVector{QRect(1280, 0, 1280, 32)} << QRect(0, 0, 1280, 1024) << QRect(1280, 32, 1280, 992) << QRect(0, 32, 2560, 992) << QRegion(1280, 0, 1280, 32); + QTest::newRow("left/0") << QVector{QRect(0, 0, 32, 1024)} << QRect(32, 0, 1248, 1024) << QRect(1280, 0, 1280, 1024) << QRect(32, 0, 2528, 1024) << QRegion(0, 0, 32, 1024); + QTest::newRow("left/1") << QVector{QRect(1280, 0, 32, 1024)} << QRect(0, 0, 1280, 1024) << QRect(1312, 0, 1248, 1024) << QRect(0, 0, 2560, 1024) << QRegion(1280, 0, 32, 1024); + QTest::newRow("right/0") << QVector{QRect(1248, 0, 32, 1024)} << QRect(0, 0, 1248, 1024) << QRect(1280, 0, 1280, 1024) << QRect(0, 0, 2560, 1024) << QRegion(1248, 0, 32, 1024); + QTest::newRow("right/1") << QVector{QRect(2528, 0, 32, 1024)} << QRect(0, 0, 1280, 1024) << QRect(1280, 0, 1248, 1024) << QRect(0, 0, 2528, 1024) << QRegion(2528, 0, 32, 1024); + + // same with partial panels not covering the whole area + QTest::newRow("part bottom/0") << QVector{QRect(100, 992, 1080, 32)} << QRect(0, 0, 1280, 992) << QRect(1280, 0, 1280, 1024) << QRect(0, 0, 2560, 992) << QRegion(100, 992, 1080, 32); + QTest::newRow("part bottom/1") << QVector{QRect(1380, 992, 1080, 32)} << QRect(0, 0, 1280, 1024) << QRect(1280, 0, 1280, 992) << QRect(0, 0, 2560, 992) << QRegion(1380, 992, 1080, 32); + QTest::newRow("part top/0") << QVector{QRect(100, 0, 1080, 32)} << QRect(0, 32, 1280, 992) << QRect(1280, 0, 1280, 1024) << QRect(0, 32, 2560, 992) << QRegion(100, 0, 1080, 32); + QTest::newRow("part top/1") << QVector{QRect(1380, 0, 1080, 32)} << QRect(0, 0, 1280, 1024) << QRect(1280, 32, 1280, 992) << QRect(0, 32, 2560, 992) << QRegion(1380, 0, 1080, 32); + QTest::newRow("part left/0") << QVector{QRect(0, 100, 32, 824)} << QRect(32, 0, 1248, 1024) << QRect(1280, 0, 1280, 1024) << QRect(32, 0, 2528, 1024) << QRegion(0, 100, 32, 824); + QTest::newRow("part left/1") << QVector{QRect(1280, 100, 32, 824)} << QRect(0, 0, 1280, 1024) << QRect(1312, 0, 1248, 1024) << QRect(0, 0, 2560, 1024) << QRegion(1280, 100, 32, 824); + QTest::newRow("part right/0") << QVector{QRect(1248, 100, 32, 824)} << QRect(0, 0, 1248, 1024) << QRect(1280, 0, 1280, 1024) << QRect(0, 0, 2560, 1024) << QRegion(1248, 100, 32, 824); + QTest::newRow("part right/1") << QVector{QRect(2528, 100, 32, 824)} << QRect(0, 0, 1280, 1024) << QRect(1280, 0, 1248, 1024) << QRect(0, 0, 2528, 1024) << QRegion(2528, 100, 32, 824); + + // multiple panels + QTest::newRow("two bottom panels") << QVector{QRect(100, 992, 1080, 32), QRect(1380, 984, 1080, 40)} << QRect(0, 0, 1280, 992) << QRect(1280, 0, 1280, 984) << QRect(0, 0, 2560, 984) << QRegion(100, 992, 1080, 32).united(QRegion(1380, 984, 1080, 40)); + QTest::newRow("two left panels") << QVector{QRect(0, 10, 32, 390), QRect(0, 450, 40, 100)} << QRect(40, 0, 1240, 1024) << QRect(1280, 0, 1280, 1024) << QRect(40, 0, 2520, 1024) << QRegion(0, 10, 32, 390).united(QRegion(0, 450, 40, 100)); +} + +void StrutsTest::testWaylandStruts() +{ + // this test verifies that struts on Wayland panels are handled correctly + using namespace KWayland::Client; + // no, struts yet + QVERIFY(waylandServer()->clients().isEmpty()); + // first screen + QCOMPARE(workspace()->clientArea(PlacementArea, 0, 1), QRect(0, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(MovementArea, 0, 1), QRect(0, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(MaximizeArea, 0, 1), QRect(0, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(MaximizeFullArea, 0, 1), QRect(0, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(FullScreenArea, 0, 1), QRect(0, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(ScreenArea, 0, 1), QRect(0, 0, 1280, 1024)); + // second screen + QCOMPARE(workspace()->clientArea(PlacementArea, 1, 1), QRect(1280, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(MovementArea, 1, 1), QRect(1280, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(MaximizeArea, 1, 1), QRect(1280, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(MaximizeFullArea, 1, 1), QRect(1280, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(FullScreenArea, 1, 1), QRect(1280, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(ScreenArea, 1, 1), QRect(1280, 0, 1280, 1024)); + // combined + QCOMPARE(workspace()->clientArea(WorkArea, 0, 1), QRect(0, 0, 2560, 1024)); + QCOMPARE(workspace()->clientArea(FullArea, 0, 1), QRect(0, 0, 2560, 1024)); + QCOMPARE(workspace()->restrictedMoveArea(-1), QRegion()); + + QFETCH(QVector, windowGeometries); + // create the panels + QHash clients; + for (auto it = windowGeometries.constBegin(), end = windowGeometries.constEnd(); it != end; it++) { + const QRect windowGeometry = *it; + Surface *surface = Test::createSurface(m_compositor); + XdgShellSurface *shellSurface = Test::createXdgShellStableSurface(surface, surface, Test::CreationSetup::CreateOnly); + PlasmaShellSurface *plasmaSurface = m_plasmaShell->createSurface(surface, surface); + plasmaSurface->setPosition(windowGeometry.topLeft()); + plasmaSurface->setRole(PlasmaShellSurface::Role::Panel); + Test::initXdgShellSurface(surface, shellSurface); + + // map the window + auto c = Test::renderAndWaitForShown(surface, windowGeometry.size(), Qt::red, QImage::Format_RGB32); + + QVERIFY(c); + QVERIFY(!c->isActive()); + QCOMPARE(c->frameGeometry(), windowGeometry); + QVERIFY(c->isDock()); + QVERIFY(c->hasStrut()); + clients.insert(surface, c); + } + + // some props are independent of struts - those first + // screen 0 + QCOMPARE(workspace()->clientArea(MovementArea, 0, 1), QRect(0, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(MaximizeFullArea, 0, 1), QRect(0, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(FullScreenArea, 0, 1), QRect(0, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(ScreenArea, 0, 1), QRect(0, 0, 1280, 1024)); + // screen 1 + QCOMPARE(workspace()->clientArea(MovementArea, 1, 1), QRect(1280, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(MaximizeFullArea, 1, 1), QRect(1280, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(FullScreenArea, 1, 1), QRect(1280, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(ScreenArea, 1, 1), QRect(1280, 0, 1280, 1024)); + // combined + QCOMPARE(workspace()->clientArea(FullArea, 0, 1), QRect(0, 0, 2560, 1024)); + + // now verify the actual updated client areas + QTEST(workspace()->clientArea(PlacementArea, 0, 1), "screen0Maximized"); + QTEST(workspace()->clientArea(MaximizeArea, 0, 1), "screen0Maximized"); + QTEST(workspace()->clientArea(PlacementArea, 1, 1), "screen1Maximized"); + QTEST(workspace()->clientArea(MaximizeArea, 1, 1), "screen1Maximized"); + QTEST(workspace()->clientArea(WorkArea, 0, 1), "workArea"); + QTEST(workspace()->restrictedMoveArea(-1), "restrictedMoveArea"); + + // delete all surfaces + for (auto it = clients.begin(); it != clients.end(); it++) { + QSignalSpy destroyedSpy(it.value(), &QObject::destroyed); + QVERIFY(destroyedSpy.isValid()); + delete it.key(); + QVERIFY(destroyedSpy.wait()); + } + QCOMPARE(workspace()->restrictedMoveArea(-1), QRegion()); +} + +void StrutsTest::testMoveWaylandPanel() +{ + // this test verifies that repositioning a Wayland panel updates the client area + using namespace KWayland::Client; + const QRect windowGeometry(0, 1000, 1280, 24); + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + QScopedPointer plasmaSurface(m_plasmaShell->createSurface(surface.data())); + plasmaSurface->setPosition(windowGeometry.topLeft()); + plasmaSurface->setRole(PlasmaShellSurface::Role::Panel); + Test::initXdgShellSurface(surface.data(), shellSurface.data()); + + // map the window + auto c = Test::renderAndWaitForShown(surface.data(), windowGeometry.size(), Qt::red, QImage::Format_RGB32); + QVERIFY(c); + QVERIFY(!c->isActive()); + QCOMPARE(c->frameGeometry(), windowGeometry); + QVERIFY(c->isDock()); + QVERIFY(c->hasStrut()); + QCOMPARE(workspace()->clientArea(PlacementArea, 0, 1), QRect(0, 0, 1280, 1000)); + QCOMPARE(workspace()->clientArea(MaximizeArea, 0, 1), QRect(0, 0, 1280, 1000)); + QCOMPARE(workspace()->clientArea(PlacementArea, 1, 1), QRect(1280, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(MaximizeArea, 1, 1), QRect(1280, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(WorkArea, 0, 1), QRect(0, 0, 2560, 1000)); + + QSignalSpy frameGeometryChangedSpy(c, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); + plasmaSurface->setPosition(QPoint(1280, 1000)); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(c->frameGeometry(), QRect(1280, 1000, 1280, 24)); + QCOMPARE(workspace()->clientArea(PlacementArea, 0, 1), QRect(0, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(MaximizeArea, 0, 1), QRect(0, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(PlacementArea, 1, 1), QRect(1280, 0, 1280, 1000)); + QCOMPARE(workspace()->clientArea(MaximizeArea, 1, 1), QRect(1280, 0, 1280, 1000)); + QCOMPARE(workspace()->clientArea(WorkArea, 0, 1), QRect(0, 0, 2560, 1000)); +} + +void StrutsTest::testWaylandMobilePanel() +{ + using namespace KWayland::Client; + + //First enable maxmizing policy + KConfigGroup group = kwinApp()->config()->group("Windows"); + group.writeEntry("Placement", "Maximizing"); + group.sync(); + workspace()->slotReconfigure(); + + // create first top panel + const QRect windowGeometry(0, 0, 1280, 60); + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + QScopedPointer plasmaSurface(m_plasmaShell->createSurface(surface.data())); + plasmaSurface->setPosition(windowGeometry.topLeft()); + plasmaSurface->setRole(PlasmaShellSurface::Role::Panel); + Test::initXdgShellSurface(surface.data(), shellSurface.data()); + + // map the first panel + auto c = Test::renderAndWaitForShown(surface.data(), windowGeometry.size(), Qt::red, QImage::Format_RGB32); + QVERIFY(c); + QVERIFY(!c->isActive()); + QCOMPARE(c->frameGeometry(), windowGeometry); + QVERIFY(c->isDock()); + QVERIFY(c->hasStrut()); + + QCOMPARE(workspace()->clientArea(PlacementArea, 0, 1), QRect(0, 60, 1280, 964)); + QCOMPARE(workspace()->clientArea(MaximizeArea, 0, 1), QRect(0, 60, 1280, 964)); + QCOMPARE(workspace()->clientArea(PlacementArea, 1, 1), QRect(1280, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(MaximizeArea, 1, 1), QRect(1280, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(WorkArea, 0, 1), QRect(0, 60, 2560, 964)); + + // create another bottom panel + const QRect windowGeometry2(0, 874, 1280, 150); + QScopedPointer surface2(Test::createSurface()); + QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data(), surface2.data(), Test::CreationSetup::CreateOnly)); + QScopedPointer plasmaSurface2(m_plasmaShell->createSurface(surface2.data())); + plasmaSurface2->setPosition(windowGeometry2.topLeft()); + plasmaSurface2->setRole(PlasmaShellSurface::Role::Panel); + Test::initXdgShellSurface(surface2.data(), shellSurface2.data()); + + auto c1 = Test::renderAndWaitForShown(surface2.data(), windowGeometry2.size(), Qt::blue, QImage::Format_RGB32); + + QVERIFY(c1); + QVERIFY(!c1->isActive()); + QCOMPARE(c1->frameGeometry(), windowGeometry2); + QVERIFY(c1->isDock()); + QVERIFY(c1->hasStrut()); + + QCOMPARE(workspace()->clientArea(PlacementArea, 0, 1), QRect(0, 60, 1280, 814)); + QCOMPARE(workspace()->clientArea(MaximizeArea, 0, 1), QRect(0, 60, 1280, 814)); + QCOMPARE(workspace()->clientArea(PlacementArea, 1, 1), QRect(1280, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(MaximizeArea, 1, 1), QRect(1280, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(WorkArea, 0, 1), QRect(0, 60, 2560, 814)); + + // Destroy test clients. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(c)); + shellSurface2.reset(); + QVERIFY(Test::waitForWindowDestroyed(c1)); +} + +void StrutsTest::testX11Struts_data() +{ + QTest::addColumn("windowGeometry"); + QTest::addColumn("leftStrut"); + QTest::addColumn("rightStrut"); + QTest::addColumn("topStrut"); + QTest::addColumn("bottomStrut"); + QTest::addColumn("leftStrutStart"); + QTest::addColumn("leftStrutEnd"); + QTest::addColumn("rightStrutStart"); + QTest::addColumn("rightStrutEnd"); + QTest::addColumn("topStrutStart"); + QTest::addColumn("topStrutEnd"); + QTest::addColumn("bottomStrutStart"); + QTest::addColumn("bottomStrutEnd"); + QTest::addColumn("screen0Maximized"); + QTest::addColumn("screen1Maximized"); + QTest::addColumn("workArea"); + QTest::addColumn("restrictedMoveArea"); + + QTest::newRow("bottom panel/no strut") << QRect(0, 980, 1280, 44) + << 0 << 0 << 0 << 0 + << 0 << 0 + << 0 << 0 + << 0 << 0 + << 0 << 0 + << QRect(0, 0, 1280, 1024) + << QRect(1280, 0, 1280, 1024) + << QRect(0, 0, 2560, 1024) + << QRegion(); + QTest::newRow("bottom panel/strut") << QRect(0, 980, 1280, 44) + << 0 << 0 << 0 << 44 + << 0 << 0 + << 0 << 0 + << 0 << 0 + << 0 << 1279 + << QRect(0, 0, 1280, 980) + << QRect(1280, 0, 1280, 1024) + << QRect(0, 0, 2560, 980) + << QRegion(0, 980, 1279, 44); + QTest::newRow("top panel/no strut") << QRect(0, 0, 1280, 44) + << 0 << 0 << 0 << 0 + << 0 << 0 + << 0 << 0 + << 0 << 0 + << 0 << 0 + << QRect(0, 0, 1280, 1024) + << QRect(1280, 0, 1280, 1024) + << QRect(0, 0, 2560, 1024) + << QRegion(); + QTest::newRow("top panel/strut") << QRect(0, 0, 1280, 44) + << 0 << 0 << 44 << 0 + << 0 << 0 + << 0 << 0 + << 0 << 1279 + << 0 << 0 + << QRect(0, 44, 1280, 980) + << QRect(1280, 0, 1280, 1024) + << QRect(0, 44, 2560, 980) + << QRegion(0, 0, 1279, 44); + QTest::newRow("left panel/no strut") << QRect(0, 0, 60, 1024) + << 0 << 0 << 0 << 0 + << 0 << 0 + << 0 << 0 + << 0 << 0 + << 0 << 0 + << QRect(0, 0, 1280, 1024) + << QRect(1280, 0, 1280, 1024) + << QRect(0, 0, 2560, 1024) + << QRegion(); + QTest::newRow("left panel/strut") << QRect(0, 0, 60, 1024) + << 60 << 0 << 0 << 0 + << 0 << 1023 + << 0 << 0 + << 0 << 0 + << 0 << 0 + << QRect(60, 0, 1220, 1024) + << QRect(1280, 0, 1280, 1024) + << QRect(60, 0, 2500, 1024) + << QRegion(0, 0, 60, 1023); + QTest::newRow("right panel/no strut") << QRect(1220, 0, 60, 1024) + << 0 << 0 << 0 << 0 + << 0 << 0 + << 0 << 0 + << 0 << 0 + << 0 << 0 + << QRect(0, 0, 1280, 1024) + << QRect(1280, 0, 1280, 1024) + << QRect(0, 0, 2560, 1024) + << QRegion(); + QTest::newRow("right panel/strut") << QRect(1220, 0, 60, 1024) + << 0 << 1340 << 0 << 0 + << 0 << 0 + << 0 << 1023 + << 0 << 0 + << 0 << 0 + << QRect(0, 0, 1220, 1024) + << QRect(1280, 0, 1280, 1024) + << QRect(0, 0, 2560, 1024) + << QRegion(1220, 0, 60, 1023); + // second screen + QTest::newRow("bottom panel 1/no strut") << QRect(1280, 980, 1280, 44) + << 0 << 0 << 0 << 0 + << 0 << 0 + << 0 << 0 + << 0 << 0 + << 0 << 0 + << QRect(0, 0, 1280, 1024) + << QRect(1280, 0, 1280, 1024) + << QRect(0, 0, 2560, 1024) + << QRegion(); + QTest::newRow("bottom panel 1/strut") << QRect(1280, 980, 1280, 44) + << 0 << 0 << 0 << 44 + << 0 << 0 + << 0 << 0 + << 0 << 0 + << 1280 << 2559 + << QRect(0, 0, 1280, 1024) + << QRect(1280, 0, 1280, 980) + << QRect(0, 0, 2560, 980) + << QRegion(1280, 980, 1279, 44); + QTest::newRow("top panel 1/no strut") << QRect(1280, 0, 1280, 44) + << 0 << 0 << 0 << 0 + << 0 << 0 + << 0 << 0 + << 0 << 0 + << 0 << 0 + << QRect(0, 0, 1280, 1024) + << QRect(1280, 0, 1280, 1024) + << QRect(0, 0, 2560, 1024) + << QRegion(); + QTest::newRow("top panel 1 /strut") << QRect(1280, 0, 1280, 44) + << 0 << 0 << 44 << 0 + << 0 << 0 + << 0 << 0 + << 1280 << 2559 + << 0 << 0 + << QRect(0, 0, 1280, 1024) + << QRect(1280, 44, 1280, 980) + << QRect(0, 44, 2560, 980) + << QRegion(1280, 0, 1279, 44); + QTest::newRow("left panel 1/no strut") << QRect(1280, 0, 60, 1024) + << 0 << 0 << 0 << 0 + << 0 << 0 + << 0 << 0 + << 0 << 0 + << 0 << 0 + << QRect(0, 0, 1280, 1024) + << QRect(1280, 0, 1280, 1024) + << QRect(0, 0, 2560, 1024) + << QRegion(); + QTest::newRow("left panel 1/strut") << QRect(1280, 0, 60, 1024) + << 1340 << 0 << 0 << 0 + << 0 << 1023 + << 0 << 0 + << 0 << 0 + << 0 << 0 + << QRect(0, 0, 1280, 1024) + << QRect(1340, 0, 1220, 1024) + << QRect(0, 0, 2560, 1024) + << QRegion(1280, 0, 60, 1023); + // invalid struts + QTest::newRow("bottom panel/ invalid strut") << QRect(0, 980, 1280, 44) + << 1280 << 0 << 0 << 44 + << 980 << 1024 + << 0 << 0 + << 0 << 0 + << 0 << 1279 + << QRect(0, 0, 1280, 1024) + << QRect(1280, 0, 1280, 1024) + << QRect(0, 0, 2560, 1024) + << QRegion(0, 980, 1280, 44); + QTest::newRow("top panel/ invalid strut") << QRect(0, 0, 1280, 44) + << 1280 << 0 << 44 << 0 + << 0 << 44 + << 0 << 0 + << 0 << 1279 + << 0 << 0 + << QRect(0, 0, 1280, 1024) + << QRect(1280, 0, 1280, 1024) + << QRect(0, 0, 2560, 1024) + << QRegion(0, 0, 1280, 44); + QTest::newRow("top panel/invalid strut 2") << QRect(0, 0, 1280, 44) + << 0 << 0 << 1024 << 0 + << 0 << 0 + << 0 << 0 + << 0 << 1279 + << 0 << 0 + << QRect(0, 0, 1280, 1024) + << QRect(1280, 0, 1280, 1024) + << QRect(0, 0, 2560, 1024) + << QRegion(0, 0, 1279, 1024); +} + +struct XcbConnectionDeleter +{ + static inline void cleanup(xcb_connection_t *pointer) + { + xcb_disconnect(pointer); + } +}; + +void StrutsTest::testX11Struts() +{ + // this test verifies that struts are applied correctly for X11 windows + + // no, struts yet + // first screen + QCOMPARE(workspace()->clientArea(PlacementArea, 0, 1), QRect(0, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(MovementArea, 0, 1), QRect(0, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(MaximizeArea, 0, 1), QRect(0, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(MaximizeFullArea, 0, 1), QRect(0, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(FullScreenArea, 0, 1), QRect(0, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(ScreenArea, 0, 1), QRect(0, 0, 1280, 1024)); + // second screen + QCOMPARE(workspace()->clientArea(PlacementArea, 1, 1), QRect(1280, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(MovementArea, 1, 1), QRect(1280, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(MaximizeArea, 1, 1), QRect(1280, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(MaximizeFullArea, 1, 1), QRect(1280, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(FullScreenArea, 1, 1), QRect(1280, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(ScreenArea, 1, 1), QRect(1280, 0, 1280, 1024)); + // combined + QCOMPARE(workspace()->clientArea(WorkArea, 0, 1), QRect(0, 0, 2560, 1024)); + QCOMPARE(workspace()->clientArea(FullArea, 0, 1), QRect(0, 0, 2560, 1024)); + QCOMPARE(workspace()->restrictedMoveArea(-1), QRegion()); + + // create an xcb window + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + + xcb_window_t w = xcb_generate_id(c.data()); + QFETCH(QRect, windowGeometry); + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, 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; + memset(&hints, 0, sizeof(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.data(), w, &hints); + NETWinInfo info(c.data(), w, rootWindow(), NET::WMAllProperties, NET::WM2AllProperties); + info.setWindowType(NET::Dock); + // set the extended strut + QFETCH(int, leftStrut); + QFETCH(int, rightStrut); + QFETCH(int, topStrut); + QFETCH(int, bottomStrut); + QFETCH(int, leftStrutStart); + QFETCH(int, leftStrutEnd); + QFETCH(int, rightStrutStart); + QFETCH(int, rightStrutEnd); + QFETCH(int, topStrutStart); + QFETCH(int, topStrutEnd); + QFETCH(int, bottomStrutStart); + QFETCH(int, bottomStrutEnd); + NETExtendedStrut strut; + strut.left_start = leftStrutStart; + strut.left_end = leftStrutEnd; + strut.left_width = leftStrut; + strut.right_start = rightStrutStart; + strut.right_end = rightStrutEnd; + strut.right_width = rightStrut; + strut.top_start = topStrutStart; + strut.top_end = topStrutEnd; + strut.top_width = topStrut; + strut.bottom_start = bottomStrutStart; + strut.bottom_end = bottomStrutEnd; + strut.bottom_width = bottomStrut; + info.setExtendedStrut(strut); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + // we should get a client for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->window(), w); + QVERIFY(!client->isDecorated()); + QCOMPARE(client->windowType(), NET::Dock); + QCOMPARE(client->frameGeometry(), windowGeometry); + + // this should have affected the client area + // some props are independent of struts - those first + // screen 0 + QCOMPARE(workspace()->clientArea(MovementArea, 0, 1), QRect(0, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(MaximizeFullArea, 0, 1), QRect(0, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(FullScreenArea, 0, 1), QRect(0, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(ScreenArea, 0, 1), QRect(0, 0, 1280, 1024)); + // screen 1 + QCOMPARE(workspace()->clientArea(MovementArea, 1, 1), QRect(1280, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(MaximizeFullArea, 1, 1), QRect(1280, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(FullScreenArea, 1, 1), QRect(1280, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(ScreenArea, 1, 1), QRect(1280, 0, 1280, 1024)); + // combined + QCOMPARE(workspace()->clientArea(FullArea, 0, 1), QRect(0, 0, 2560, 1024)); + + // now verify the actual updated client areas + QTEST(workspace()->clientArea(PlacementArea, 0, 1), "screen0Maximized"); + QTEST(workspace()->clientArea(MaximizeArea, 0, 1), "screen0Maximized"); + QTEST(workspace()->clientArea(PlacementArea, 1, 1), "screen1Maximized"); + QTEST(workspace()->clientArea(MaximizeArea, 1, 1), "screen1Maximized"); + QTEST(workspace()->clientArea(WorkArea, 0, 1), "workArea"); + QTEST(workspace()->restrictedMoveArea(-1), "restrictedMoveArea"); + + // and destroy the window again + xcb_unmap_window(c.data(), w); + xcb_destroy_window(c.data(), w); + xcb_flush(c.data()); + c.reset(); + + QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + QVERIFY(windowClosedSpy.wait()); + + // now struts should be removed again + QCOMPARE(workspace()->clientArea(PlacementArea, 0, 1), QRect(0, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(MovementArea, 0, 1), QRect(0, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(MaximizeArea, 0, 1), QRect(0, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(MaximizeFullArea, 0, 1), QRect(0, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(FullScreenArea, 0, 1), QRect(0, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(ScreenArea, 0, 1), QRect(0, 0, 1280, 1024)); + // second screen + QCOMPARE(workspace()->clientArea(PlacementArea, 1, 1), QRect(1280, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(MovementArea, 1, 1), QRect(1280, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(MaximizeArea, 1, 1), QRect(1280, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(MaximizeFullArea, 1, 1), QRect(1280, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(FullScreenArea, 1, 1), QRect(1280, 0, 1280, 1024)); + QCOMPARE(workspace()->clientArea(ScreenArea, 1, 1), QRect(1280, 0, 1280, 1024)); + // combined + QCOMPARE(workspace()->clientArea(WorkArea, 0, 1), QRect(0, 0, 2560, 1024)); + QCOMPARE(workspace()->clientArea(FullArea, 0, 1), QRect(0, 0, 2560, 1024)); + QCOMPARE(workspace()->restrictedMoveArea(-1), QRegion()); +} + +void StrutsTest::test363804() +{ + // this test verifies the condition described in BUG 363804 + // two screens in a vertical setup, aligned to right border with panel on the bottom screen + const QVector geometries{QRect(0, 0, 1920, 1080), QRect(554, 1080, 1366, 768)}; + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", + Qt::DirectConnection, + Q_ARG(int, 2), + Q_ARG(QVector, geometries)); + QCOMPARE(screens()->geometry(0), geometries.at(0)); + QCOMPARE(screens()->geometry(1), geometries.at(1)); + QCOMPARE(screens()->geometry(), QRect(0, 0, 1920, 1848)); + + // create an xcb window + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + + xcb_window_t w = xcb_generate_id(c.data()); + const QRect windowGeometry(554, 1812, 1366, 36); + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, 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; + memset(&hints, 0, sizeof(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.data(), w, &hints); + NETWinInfo info(c.data(), w, rootWindow(), NET::WMAllProperties, NET::WM2AllProperties); + info.setWindowType(NET::Dock); + NETExtendedStrut strut; + strut.left_start = 0; + strut.left_end = 0; + strut.left_width = 0; + strut.right_start = 0; + strut.right_end = 0; + strut.right_width = 0; + strut.top_start = 0; + strut.top_end = 0; + strut.top_width = 0; + strut.bottom_start = 554; + strut.bottom_end = 1919; + strut.bottom_width = 36; + info.setExtendedStrut(strut); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + // we should get a client for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->window(), w); + QVERIFY(!client->isDecorated()); + QCOMPARE(client->windowType(), NET::Dock); + QCOMPARE(client->frameGeometry(), windowGeometry); + + // now verify the actual updated client areas + QCOMPARE(workspace()->clientArea(PlacementArea, 0, 1), geometries.at(0)); + QCOMPARE(workspace()->clientArea(MaximizeArea, 0, 1), geometries.at(0)); + QCOMPARE(workspace()->clientArea(PlacementArea, 1, 1), QRect(554, 1080, 1366, 732)); + QCOMPARE(workspace()->clientArea(MaximizeArea, 1, 1), QRect(554, 1080, 1366, 732)); + QCOMPARE(workspace()->clientArea(WorkArea, 0, 1), QRect(0, 0, 1920, 1812)); + + // and destroy the window again + xcb_unmap_window(c.data(), w); + xcb_destroy_window(c.data(), w); + xcb_flush(c.data()); + c.reset(); + + QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + QVERIFY(windowClosedSpy.wait()); +} + +void StrutsTest::testLeftScreenSmallerBottomAligned() +{ + // this test verifies a two screen setup with the left screen smaller than the right and bottom aligned + // the panel is on the top of the left screen, thus not at 0/0 + // what this test in addition tests is whether a window larger than the left screen is not placed into + // the dead area + const QVector geometries{QRect(0, 282, 1366, 768), QRect(1366, 0, 1680, 1050)}; + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", + Qt::DirectConnection, + Q_ARG(int, 2), + Q_ARG(QVector, geometries)); + QCOMPARE(screens()->geometry(0), geometries.at(0)); + QCOMPARE(screens()->geometry(1), geometries.at(1)); + QCOMPARE(screens()->geometry(), QRect(0, 0, 3046, 1050)); + + // create the panel + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + + xcb_window_t w = xcb_generate_id(c.data()); + const QRect windowGeometry(0, 282, 1366, 24); + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, 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; + memset(&hints, 0, sizeof(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.data(), w, &hints); + NETWinInfo info(c.data(), w, rootWindow(), NET::WMAllProperties, NET::WM2AllProperties); + info.setWindowType(NET::Dock); + NETExtendedStrut strut; + strut.left_start = 0; + strut.left_end = 0; + strut.left_width = 0; + strut.right_start = 0; + strut.right_end = 0; + strut.right_width = 0; + strut.top_start = 0; + strut.top_end = 1365; + strut.top_width = 306; + strut.bottom_start = 0; + strut.bottom_end = 0; + strut.bottom_width = 0; + info.setExtendedStrut(strut); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + // we should get a client for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->window(), w); + QVERIFY(!client->isDecorated()); + QCOMPARE(client->windowType(), NET::Dock); + QCOMPARE(client->frameGeometry(), windowGeometry); + + // now verify the actual updated client areas + QCOMPARE(workspace()->clientArea(PlacementArea, 0, 1), QRect(0, 306, 1366, 744)); + QCOMPARE(workspace()->clientArea(MaximizeArea, 0, 1), QRect(0, 306, 1366, 744)); + QCOMPARE(workspace()->clientArea(PlacementArea, 1, 1), geometries.at(1)); + QCOMPARE(workspace()->clientArea(MaximizeArea, 1, 1), geometries.at(1)); + QCOMPARE(workspace()->clientArea(WorkArea, 0, 1), QRect(0, 0, 3046, 1050)); + + // now create a window which is larger than screen 0 + + xcb_window_t w2 = xcb_generate_id(c.data()); + const QRect windowGeometry2(0, 26, 1280, 774); + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w2, rootWindow(), + windowGeometry2.x(), + windowGeometry2.y(), + windowGeometry2.width(), + windowGeometry2.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); + xcb_size_hints_t hints2; + memset(&hints2, 0, sizeof(hints2)); + xcb_icccm_size_hints_set_min_size(&hints2, 868, 431); + xcb_icccm_set_wm_normal_hints(c.data(), w2, &hints2); + xcb_map_window(c.data(), w2); + xcb_flush(c.data()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client2 = windowCreatedSpy.last().first().value(); + QVERIFY(client2); + QVERIFY(client2 != client); + QVERIFY(client2->isDecorated()); + QCOMPARE(client2->frameGeometry(), QRect(0, 306, 1366, 744)); + QCOMPARE(client2->maximizeMode(), KWin::MaximizeFull); + // destroy window again + QSignalSpy normalWindowClosedSpy(client2, &X11Client::windowClosed); + QVERIFY(normalWindowClosedSpy.isValid()); + xcb_unmap_window(c.data(), w2); + xcb_destroy_window(c.data(), w2); + xcb_flush(c.data()); + QVERIFY(normalWindowClosedSpy.wait()); + + // and destroy the window again + xcb_unmap_window(c.data(), w); + xcb_destroy_window(c.data(), w); + xcb_flush(c.data()); + c.reset(); + + QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + QVERIFY(windowClosedSpy.wait()); +} + +void StrutsTest::testWindowMoveWithPanelBetweenScreens() +{ + // this test verifies the condition of BUG + // when moving a window with decorations in a restricted way it should pass from one screen + // to the other even if there is a panel in between. + + // left screen must be smaller than right screen + const QVector geometries{QRect(0, 282, 1366, 768), QRect(1366, 0, 1680, 1050)}; + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", + Qt::DirectConnection, + Q_ARG(int, 2), + Q_ARG(QVector, geometries)); + QCOMPARE(screens()->geometry(0), geometries.at(0)); + QCOMPARE(screens()->geometry(1), geometries.at(1)); + QCOMPARE(screens()->geometry(), QRect(0, 0, 3046, 1050)); + + // create the panel on the right screen, left edge + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + + xcb_window_t w = xcb_generate_id(c.data()); + const QRect windowGeometry(1366, 0, 24, 1050); + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, 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; + memset(&hints, 0, sizeof(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.data(), w, &hints); + NETWinInfo info(c.data(), w, rootWindow(), NET::WMAllProperties, NET::WM2AllProperties); + info.setWindowType(NET::Dock); + NETExtendedStrut strut; + strut.left_start = 0; + strut.left_end = 1050; + strut.left_width = 1366+24; + strut.right_start = 0; + strut.right_end = 0; + strut.right_width = 0; + strut.top_start = 0; + strut.top_end = 0; + strut.top_width = 0; + strut.bottom_start = 0; + strut.bottom_end = 0; + strut.bottom_width = 0; + info.setExtendedStrut(strut); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + // we should get a client for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->window(), w); + QVERIFY(!client->isDecorated()); + QCOMPARE(client->windowType(), NET::Dock); + QCOMPARE(client->frameGeometry(), windowGeometry); + + // now verify the actual updated client areas + QCOMPARE(workspace()->clientArea(PlacementArea, 0, 1), QRect(0, 282, 1366, 768)); + QCOMPARE(workspace()->clientArea(MaximizeArea, 0, 1), QRect(0, 282, 1366, 768)); + QCOMPARE(workspace()->clientArea(PlacementArea, 1, 1), QRect(1390, 0, 1656, 1050)); + QCOMPARE(workspace()->clientArea(MaximizeArea, 1, 1), QRect(1390, 0, 1656, 1050)); + QCOMPARE(workspace()->clientArea(WorkArea, 0, 1), QRect(0, 0, 3046, 1050)); + QCOMPARE(workspace()->restrictedMoveArea(-1), QRegion(1366, 0, 24, 1050)); + + // create another window and try to move it + + xcb_window_t w2 = xcb_generate_id(c.data()); + const QRect windowGeometry2(1500, 400, 200, 300); + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w2, rootWindow(), + windowGeometry2.x(), + windowGeometry2.y(), + windowGeometry2.width(), + windowGeometry2.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); + xcb_size_hints_t hints2; + memset(&hints2, 0, sizeof(hints2)); + xcb_icccm_size_hints_set_position(&hints2, 1, windowGeometry2.x(), windowGeometry2.y()); + xcb_icccm_size_hints_set_min_size(&hints2, 200, 300); + xcb_icccm_set_wm_normal_hints(c.data(), w2, &hints2); + xcb_map_window(c.data(), w2); + xcb_flush(c.data()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client2 = windowCreatedSpy.last().first().value(); + QVERIFY(client2); + QVERIFY(client2 != client); + QVERIFY(client2->isDecorated()); + QCOMPARE(client2->clientSize(), QSize(200, 300)); + QCOMPARE(client2->pos(), QPoint(1500, 400)); + + const QRect origGeo = client2->frameGeometry(); + Cursors::self()->mouse()->setPos(origGeo.center()); + workspace()->performWindowOperation(client2, Options::MoveOp); + QTRY_COMPARE(workspace()->moveResizeClient(), client2); + QVERIFY(client2->isMove()); + // move to next screen - step is 8 pixel, so 800 pixel + for (int i = 0; i < 100; i++) { + client2->keyPressEvent(Qt::Key_Left); + QTest::qWait(50); + } + client2->keyPressEvent(Qt::Key_Enter); + QCOMPARE(client2->isMove(), false); + QVERIFY(workspace()->moveResizeClient() == nullptr); + QCOMPARE(client2->frameGeometry(), QRect(origGeo.translated(-800, 0))); +} + +} + +WAYLANDTEST_MAIN(KWin::StrutsTest) +#include "struts_test.moc" diff --git a/autotests/integration/tabbox_test.cpp b/autotests/integration/tabbox_test.cpp new file mode 100644 index 0000000..034954f --- /dev/null +++ b/autotests/integration/tabbox_test.cpp @@ -0,0 +1,244 @@ +/* + 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 "abstract_client.h" +#include "cursor.h" +#include "input.h" +#include "platform.h" +#include "screens.h" +#include "tabbox/tabbox.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include + +#include + +using namespace KWin; +using namespace KWayland::Client; + +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 TabBoxTest::initTestCase() +{ + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + KSharedConfigPtr c = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + c->group("TabBox").writeEntry("ShowTabBox", false); + c->sync(); + kwinApp()->setConfig(c); + qputenv("KWIN_XKB_DEFAULT_KEYMAP", "1"); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + waylandServer()->initWorkspace(); +} + +void TabBoxTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); + screens()->setCurrent(0); + KWin::Cursors::self()->mouse()->setPos(QPoint(640, 512)); +} + +void TabBoxTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void TabBoxTest::testCapsLock() +{ + // this test verifies that Alt+tab works correctly also when Capslock is on + // bug 368590 + + // first create three windows + QScopedPointer surface1(Test::createSurface()); + QScopedPointer shellSurface1(Test::createXdgShellStableSurface(surface1.data())); + auto c1 = Test::renderAndWaitForShown(surface1.data(), QSize(100, 50), Qt::blue); + QVERIFY(c1); + QVERIFY(c1->isActive()); + QScopedPointer surface2(Test::createSurface()); + QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data())); + auto c2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 50), Qt::red); + QVERIFY(c2); + QVERIFY(c2->isActive()); + QScopedPointer surface3(Test::createSurface()); + QScopedPointer shellSurface3(Test::createXdgShellStableSurface(surface3.data())); + auto c3 = Test::renderAndWaitForShown(surface3.data(), QSize(100, 50), Qt::red); + QVERIFY(c3); + QVERIFY(c3->isActive()); + + // Setup tabbox signal spies + QSignalSpy tabboxAddedSpy(TabBox::TabBox::self(), &TabBox::TabBox::tabBoxAdded); + QVERIFY(tabboxAddedSpy.isValid()); + QSignalSpy tabboxClosedSpy(TabBox::TabBox::self(), &TabBox::TabBox::tabBoxClosed); + QVERIFY(tabboxClosedSpy.isValid()); + + // enable capslock + quint32 timestamp = 0; + kwinApp()->platform()->keyboardKeyPressed(KEY_CAPSLOCK, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_CAPSLOCK, timestamp++); + QCOMPARE(input()->keyboardModifiers(), Qt::ShiftModifier); + + // press alt+tab + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); + QCOMPARE(input()->keyboardModifiers(), Qt::ShiftModifier | Qt::AltModifier); + kwinApp()->platform()->keyboardKeyPressed(KEY_TAB, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_TAB, timestamp++); + + QVERIFY(tabboxAddedSpy.wait()); + QVERIFY(TabBox::TabBox::self()->isGrabbed()); + + // release alt + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); + QCOMPARE(tabboxClosedSpy.count(), 1); + QCOMPARE(TabBox::TabBox::self()->isGrabbed(), false); + + // release caps lock + kwinApp()->platform()->keyboardKeyPressed(KEY_CAPSLOCK, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_CAPSLOCK, timestamp++); + QCOMPARE(input()->keyboardModifiers(), Qt::NoModifier); + QCOMPARE(tabboxClosedSpy.count(), 1); + QCOMPARE(TabBox::TabBox::self()->isGrabbed(), false); + QCOMPARE(workspace()->activeClient(), c2); + + surface3.reset(); + QVERIFY(Test::waitForWindowDestroyed(c3)); + surface2.reset(); + QVERIFY(Test::waitForWindowDestroyed(c2)); + surface1.reset(); + QVERIFY(Test::waitForWindowDestroyed(c1)); +} + +void TabBoxTest::testMoveForward() +{ + // this test verifies that Alt+tab works correctly moving forward + + // first create three windows + QScopedPointer surface1(Test::createSurface()); + QScopedPointer shellSurface1(Test::createXdgShellStableSurface(surface1.data())); + auto c1 = Test::renderAndWaitForShown(surface1.data(), QSize(100, 50), Qt::blue); + QVERIFY(c1); + QVERIFY(c1->isActive()); + QScopedPointer surface2(Test::createSurface()); + QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data())); + auto c2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 50), Qt::red); + QVERIFY(c2); + QVERIFY(c2->isActive()); + QScopedPointer surface3(Test::createSurface()); + QScopedPointer shellSurface3(Test::createXdgShellStableSurface(surface3.data())); + auto c3 = Test::renderAndWaitForShown(surface3.data(), QSize(100, 50), Qt::red); + QVERIFY(c3); + QVERIFY(c3->isActive()); + + // Setup tabbox signal spies + QSignalSpy tabboxAddedSpy(TabBox::TabBox::self(), &TabBox::TabBox::tabBoxAdded); + QVERIFY(tabboxAddedSpy.isValid()); + QSignalSpy tabboxClosedSpy(TabBox::TabBox::self(), &TabBox::TabBox::tabBoxClosed); + QVERIFY(tabboxClosedSpy.isValid()); + + // press alt+tab + quint32 timestamp = 0; + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); + QCOMPARE(input()->keyboardModifiers(), Qt::AltModifier); + kwinApp()->platform()->keyboardKeyPressed(KEY_TAB, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_TAB, timestamp++); + + QVERIFY(tabboxAddedSpy.wait()); + QVERIFY(TabBox::TabBox::self()->isGrabbed()); + + // release alt + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); + QCOMPARE(tabboxClosedSpy.count(), 1); + QCOMPARE(TabBox::TabBox::self()->isGrabbed(), false); + QCOMPARE(workspace()->activeClient(), c2); + + surface3.reset(); + QVERIFY(Test::waitForWindowDestroyed(c3)); + surface2.reset(); + QVERIFY(Test::waitForWindowDestroyed(c2)); + surface1.reset(); + QVERIFY(Test::waitForWindowDestroyed(c1)); +} + +void TabBoxTest::testMoveBackward() +{ + // this test verifies that Alt+Shift+tab works correctly moving backward + + // first create three windows + QScopedPointer surface1(Test::createSurface()); + QScopedPointer shellSurface1(Test::createXdgShellStableSurface(surface1.data())); + auto c1 = Test::renderAndWaitForShown(surface1.data(), QSize(100, 50), Qt::blue); + QVERIFY(c1); + QVERIFY(c1->isActive()); + QScopedPointer surface2(Test::createSurface()); + QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data())); + auto c2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 50), Qt::red); + QVERIFY(c2); + QVERIFY(c2->isActive()); + QScopedPointer surface3(Test::createSurface()); + QScopedPointer shellSurface3(Test::createXdgShellStableSurface(surface3.data())); + auto c3 = Test::renderAndWaitForShown(surface3.data(), QSize(100, 50), Qt::red); + QVERIFY(c3); + QVERIFY(c3->isActive()); + + // Setup tabbox signal spies + QSignalSpy tabboxAddedSpy(TabBox::TabBox::self(), &TabBox::TabBox::tabBoxAdded); + QVERIFY(tabboxAddedSpy.isValid()); + QSignalSpy tabboxClosedSpy(TabBox::TabBox::self(), &TabBox::TabBox::tabBoxClosed); + QVERIFY(tabboxClosedSpy.isValid()); + + // press alt+shift+tab + quint32 timestamp = 0; + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); + QCOMPARE(input()->keyboardModifiers(), Qt::AltModifier); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTSHIFT, timestamp++); + QCOMPARE(input()->keyboardModifiers(), Qt::AltModifier | Qt::ShiftModifier); + kwinApp()->platform()->keyboardKeyPressed(KEY_TAB, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_TAB, timestamp++); + + QVERIFY(tabboxAddedSpy.wait()); + QVERIFY(TabBox::TabBox::self()->isGrabbed()); + + // release alt + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTSHIFT, timestamp++); + QCOMPARE(tabboxClosedSpy.count(), 0); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); + QCOMPARE(tabboxClosedSpy.count(), 1); + QCOMPARE(TabBox::TabBox::self()->isGrabbed(), false); + QCOMPARE(workspace()->activeClient(), c1); + + surface3.reset(); + QVERIFY(Test::waitForWindowDestroyed(c3)); + surface2.reset(); + QVERIFY(Test::waitForWindowDestroyed(c2)); + surface1.reset(); + QVERIFY(Test::waitForWindowDestroyed(c1)); +} + +WAYLANDTEST_MAIN(TabBoxTest) +#include "tabbox_test.moc" diff --git a/autotests/integration/test_helpers.cpp b/autotests/integration/test_helpers.cpp new file mode 100644 index 0000000..e8acdbb --- /dev/null +++ b/autotests/integration/test_helpers.cpp @@ -0,0 +1,911 @@ +/* + 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 "abstract_client.h" +#include "screenlockerwatcher.h" +#include "wayland_server.h" +#include "workspace.h" +#include "qwayland-input-method-unstable-v1.h" +#include "virtualkeyboard.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +//screenlocker +#include + +#include + +// system +#include +#include +#include + +using namespace KWayland::Client; + +namespace KWin +{ +namespace Test +{ + +LayerShellV1::~LayerShellV1() +{ + destroy(); +} + +LayerSurfaceV1::~LayerSurfaceV1() +{ + destroy(); +} + +void LayerSurfaceV1::zwlr_layer_surface_v1_configure(uint32_t serial, uint32_t width, uint32_t height) +{ + emit configureRequested(serial, QSize(width, height)); +} + +void LayerSurfaceV1::zwlr_layer_surface_v1_closed() +{ + emit closeRequested(); +} + +XdgShell::~XdgShell() +{ + destroy(); +} + +XdgSurface::XdgSurface(XdgShell *shell, Surface *surface, QObject *parent) + : QObject(parent) + , QtWayland::xdg_surface(shell->get_xdg_surface(*surface)) + , m_surface(surface) +{ +} + +XdgSurface::~XdgSurface() +{ + destroy(); +} + +Surface *XdgSurface::surface() const +{ + return m_surface; +} + +void XdgSurface::xdg_surface_configure(uint32_t serial) +{ + 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.data(); +} + +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; + } + } + + emit configureRequested(QSize(width, height), requestedStates); +} + +void XdgToplevel::xdg_toplevel_close() +{ + 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.data(); +} + +void XdgPopup::xdg_popup_configure(int32_t x, int32_t y, int32_t width, int32_t height) +{ + emit configureRequested(QRect(x, y, width, height)); +} + +static struct { + ConnectionThread *connection = nullptr; + EventQueue *queue = nullptr; + KWayland::Client::Compositor *compositor = nullptr; + SubCompositor *subCompositor = nullptr; + ServerSideDecorationManager *decoration = nullptr; + ShadowManager *shadowManager = nullptr; + KWayland::Client::XdgShell *xdgShellStable = nullptr; + XdgShell *xdgShell = nullptr; + ShmPool *shm = nullptr; + Seat *seat = nullptr; + PlasmaShell *plasmaShell = nullptr; + PlasmaWindowManagement *windowManagement = nullptr; + PointerConstraints *pointerConstraints = nullptr; + Registry *registry = nullptr; + OutputManagement* outputManagement = nullptr; + QThread *thread = nullptr; + QVector outputs; + IdleInhibitManager *idleInhibit = nullptr; + AppMenuManager *appMenu = nullptr; + XdgDecorationManager *xdgDecoration = nullptr; + TextInputManager *textInputManager = nullptr; + QtWayland::zwp_input_panel_v1 *inputPanelV1 = nullptr; + MockInputMethod *inputMethodV1 = nullptr; + QtWayland::zwp_input_method_context_v1 *inputMethodContextV1 = nullptr; + LayerShellV1 *layerShellV1 = nullptr; +} s_waylandConnection; + +class MockInputMethod : public QtWayland::zwp_input_method_v1 +{ +public: + MockInputMethod(struct wl_registry *registry, int id, int version); + ~MockInputMethod(); + +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: + Surface *m_inputSurface = nullptr; + QtWayland::zwp_input_panel_surface_v1 *m_inputMethodSurface = nullptr; + AbstractClient *m_client = nullptr; +}; + +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) +{ + Q_UNUSED(context) + if (!m_inputSurface) { + m_inputSurface = Test::createSurface(); + m_inputMethodSurface = Test::createInputPanelSurfaceV1(m_inputSurface, s_waylandConnection.outputs.first()); + } + m_client = Test::renderAndWaitForShown(m_inputSurface, QSize(1280, 400), Qt::blue); +} + +void MockInputMethod::zwp_input_method_v1_deactivate(struct ::zwp_input_method_context_v1 *context) +{ + zwp_input_method_context_v1_destroy(context); + + if (m_inputSurface) { + m_inputSurface->release(); + m_inputSurface->destroy(); + delete m_inputSurface; + m_inputSurface = nullptr; + delete m_inputMethodSurface; + m_inputMethodSurface = nullptr; + } +} + +bool setupWaylandConnection(AdditionalWaylandInterfaces flags) +{ + if (s_waylandConnection.connection) { + return false; + } + + int sx[2]; + if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sx) < 0) { + return false; + } + KWin::waylandServer()->display()->createClient(sx[0]); + // setup connection + s_waylandConnection.connection = new ConnectionThread; + QSignalSpy connectedSpy(s_waylandConnection.connection, &ConnectionThread::connected); + if (!connectedSpy.isValid()) { + return false; + } + s_waylandConnection.connection->setSocketFd(sx[1]); + + s_waylandConnection.thread = new QThread(kwinApp()); + s_waylandConnection.connection->moveToThread(s_waylandConnection.thread); + s_waylandConnection.thread->start(); + + s_waylandConnection.connection->initConnection(); + if (!connectedSpy.wait()) { + return false; + } + + s_waylandConnection.queue = new EventQueue; + s_waylandConnection.queue->setup(s_waylandConnection.connection); + if (!s_waylandConnection.queue->isValid()) { + return false; + } + + Registry *registry = new Registry; + s_waylandConnection.registry = registry; + registry->setEventQueue(s_waylandConnection.queue); + + QObject::connect(registry, &Registry::outputAnnounced, [=](quint32 name, quint32 version) { + auto output = registry->createOutput(name, version, s_waylandConnection.registry); + s_waylandConnection.outputs << output; + QObject::connect(output, &Output::removed, [=]() { + output->deleteLater(); + s_waylandConnection.outputs.removeOne(output); + }); + QObject::connect(output, &Output::destroyed, [=]() { + s_waylandConnection.outputs.removeOne(output); + }); + }); + + QObject::connect(registry, &Registry::interfaceAnnounced, [=](const QByteArray &interface, quint32 name, quint32 version) { + if (flags & AdditionalWaylandInterface::InputMethodV1) { + if (interface == QByteArrayLiteral("zwp_input_method_v1")) { + s_waylandConnection.inputMethodV1 = new MockInputMethod(*registry, name, version); + } else if (interface == QByteArrayLiteral("zwp_input_panel_v1")) { + s_waylandConnection.inputPanelV1 = new QtWayland::zwp_input_panel_v1(*registry, name, version); + } + } + if (flags & AdditionalWaylandInterface::LayerShellV1) { + if (interface == QByteArrayLiteral("zwlr_layer_shell_v1")) { + s_waylandConnection.layerShellV1 = new LayerShellV1(); + s_waylandConnection.layerShellV1->init(*registry, name, version); + } + } + if (interface == QByteArrayLiteral("xdg_wm_base")) { + s_waylandConnection.xdgShell = new XdgShell(); + s_waylandConnection.xdgShell->init(*registry, name, version); + } + }); + + QSignalSpy allAnnounced(registry, &Registry::interfacesAnnounced); + if (!allAnnounced.isValid()) { + return false; + } + registry->create(s_waylandConnection.connection); + if (!registry->isValid()) { + return false; + } + registry->setup(); + if (!allAnnounced.wait()) { + return false; + } + + s_waylandConnection.compositor = registry->createCompositor(registry->interface(Registry::Interface::Compositor).name, registry->interface(Registry::Interface::Compositor).version); + if (!s_waylandConnection.compositor->isValid()) { + return false; + } + s_waylandConnection.subCompositor = registry->createSubCompositor(registry->interface(Registry::Interface::SubCompositor).name, registry->interface(Registry::Interface::SubCompositor).version); + if (!s_waylandConnection.subCompositor->isValid()) { + return false; + } + s_waylandConnection.shm = registry->createShmPool(registry->interface(Registry::Interface::Shm).name, registry->interface(Registry::Interface::Shm).version); + if (!s_waylandConnection.shm->isValid()) { + return false; + } + s_waylandConnection.xdgShellStable = registry->createXdgShell(registry->interface(Registry::Interface::XdgShellStable).name, registry->interface(Registry::Interface::XdgShellStable).version); + if (!s_waylandConnection.xdgShellStable->isValid()) { + return false; + } + if (flags.testFlag(AdditionalWaylandInterface::Seat)) { + s_waylandConnection.seat = registry->createSeat(registry->interface(Registry::Interface::Seat).name, registry->interface(Registry::Interface::Seat).version); + if (!s_waylandConnection.seat->isValid()) { + return false; + } + } + if (flags.testFlag(AdditionalWaylandInterface::ShadowManager)) { + s_waylandConnection.shadowManager = registry->createShadowManager(registry->interface(Registry::Interface::Shadow).name, + registry->interface(Registry::Interface::Shadow).version); + if (!s_waylandConnection.shadowManager->isValid()) { + return false; + } + } + if (flags.testFlag(AdditionalWaylandInterface::Decoration)) { + s_waylandConnection.decoration = registry->createServerSideDecorationManager(registry->interface(Registry::Interface::ServerSideDecorationManager).name, + registry->interface(Registry::Interface::ServerSideDecorationManager).version); + if (!s_waylandConnection.decoration->isValid()) { + return false; + } + } + if (flags.testFlag(AdditionalWaylandInterface::OutputManagement)) { + s_waylandConnection.outputManagement = registry->createOutputManagement(registry->interface(Registry::Interface::OutputManagement).name, + registry->interface(Registry::Interface::OutputManagement).version); + if (!s_waylandConnection.outputManagement->isValid()) { + return false; + } + } + if (flags.testFlag(AdditionalWaylandInterface::PlasmaShell)) { + s_waylandConnection.plasmaShell = registry->createPlasmaShell(registry->interface(Registry::Interface::PlasmaShell).name, + registry->interface(Registry::Interface::PlasmaShell).version); + if (!s_waylandConnection.plasmaShell->isValid()) { + return false; + } + } + if (flags.testFlag(AdditionalWaylandInterface::WindowManagement)) { + s_waylandConnection.windowManagement = registry->createPlasmaWindowManagement(registry->interface(Registry::Interface::PlasmaWindowManagement).name, + registry->interface(Registry::Interface::PlasmaWindowManagement).version); + if (!s_waylandConnection.windowManagement->isValid()) { + return false; + } + } + if (flags.testFlag(AdditionalWaylandInterface::PointerConstraints)) { + s_waylandConnection.pointerConstraints = registry->createPointerConstraints(registry->interface(Registry::Interface::PointerConstraintsUnstableV1).name, + registry->interface(Registry::Interface::PointerConstraintsUnstableV1).version); + if (!s_waylandConnection.pointerConstraints->isValid()) { + return false; + } + } + if (flags.testFlag(AdditionalWaylandInterface::IdleInhibition)) { + s_waylandConnection.idleInhibit = registry->createIdleInhibitManager(registry->interface(Registry::Interface::IdleInhibitManagerUnstableV1).name, + registry->interface(Registry::Interface::IdleInhibitManagerUnstableV1).version); + if (!s_waylandConnection.idleInhibit->isValid()) { + return false; + } + } + if (flags.testFlag(AdditionalWaylandInterface::AppMenu)) { + s_waylandConnection.appMenu = registry->createAppMenuManager(registry->interface(Registry::Interface::AppMenu).name, registry->interface(Registry::Interface::AppMenu).version); + if (!s_waylandConnection.appMenu->isValid()) { + return false; + } + } + if (flags.testFlag(AdditionalWaylandInterface::XdgDecoration)) { + s_waylandConnection.xdgDecoration = registry->createXdgDecorationManager(registry->interface(Registry::Interface::XdgDecorationUnstableV1).name, registry->interface(Registry::Interface::XdgDecorationUnstableV1).version); + if (!s_waylandConnection.xdgDecoration->isValid()) { + return false; + } + } + if (flags.testFlag(AdditionalWaylandInterface::TextInputManagerV2)) { + s_waylandConnection.textInputManager = registry->createTextInputManager(registry->interface(Registry::Interface::TextInputManagerUnstableV2).name, registry->interface(Registry::Interface::TextInputManagerUnstableV2).version); + if (!s_waylandConnection.textInputManager->isValid()) { + return false; + } + } + + return true; +} + +void destroyWaylandConnection() +{ + delete s_waylandConnection.compositor; + s_waylandConnection.compositor = nullptr; + delete s_waylandConnection.subCompositor; + s_waylandConnection.subCompositor = nullptr; + delete s_waylandConnection.windowManagement; + s_waylandConnection.windowManagement = nullptr; + delete s_waylandConnection.plasmaShell; + s_waylandConnection.plasmaShell = nullptr; + delete s_waylandConnection.decoration; + s_waylandConnection.decoration = nullptr; + delete s_waylandConnection.decoration; + s_waylandConnection.decoration = nullptr; + delete s_waylandConnection.seat; + s_waylandConnection.seat = nullptr; + delete s_waylandConnection.pointerConstraints; + s_waylandConnection.pointerConstraints = nullptr; + delete s_waylandConnection.xdgShellStable; + s_waylandConnection.xdgShellStable = nullptr; + delete s_waylandConnection.xdgShell; + s_waylandConnection.xdgShell = nullptr; + delete s_waylandConnection.shadowManager; + s_waylandConnection.shadowManager = nullptr; + delete s_waylandConnection.idleInhibit; + s_waylandConnection.idleInhibit = nullptr; + delete s_waylandConnection.shm; + s_waylandConnection.shm = nullptr; + delete s_waylandConnection.queue; + s_waylandConnection.queue = nullptr; + delete s_waylandConnection.registry; + s_waylandConnection.registry = nullptr; + delete s_waylandConnection.appMenu; + s_waylandConnection.appMenu = nullptr; + delete s_waylandConnection.xdgDecoration; + s_waylandConnection.xdgDecoration = nullptr; + delete s_waylandConnection.textInputManager; + s_waylandConnection.textInputManager = nullptr; + delete s_waylandConnection.inputPanelV1; + s_waylandConnection.inputPanelV1 = nullptr; + delete s_waylandConnection.layerShellV1; + s_waylandConnection.layerShellV1 = nullptr; + if (s_waylandConnection.thread) { + QSignalSpy spy(s_waylandConnection.connection, &QObject::destroyed); + s_waylandConnection.connection->deleteLater(); + if (spy.isEmpty()) { + QVERIFY(spy.wait()); + } + s_waylandConnection.thread->quit(); + s_waylandConnection.thread->wait(); + delete s_waylandConnection.thread; + s_waylandConnection.thread = nullptr; + s_waylandConnection.connection = nullptr; + } +} + +ConnectionThread *waylandConnection() +{ + return s_waylandConnection.connection; +} + +KWayland::Client::Compositor *waylandCompositor() +{ + return s_waylandConnection.compositor; +} + +SubCompositor *waylandSubCompositor() +{ + return s_waylandConnection.subCompositor; +} + +ShadowManager *waylandShadowManager() +{ + return s_waylandConnection.shadowManager; +} + +ShmPool *waylandShmPool() +{ + return s_waylandConnection.shm; +} + +Seat *waylandSeat() +{ + return s_waylandConnection.seat; +} + +ServerSideDecorationManager *waylandServerSideDecoration() +{ + return s_waylandConnection.decoration; +} + +PlasmaShell *waylandPlasmaShell() +{ + return s_waylandConnection.plasmaShell; +} + +PlasmaWindowManagement *waylandWindowManagement() +{ + return s_waylandConnection.windowManagement; +} + +PointerConstraints *waylandPointerConstraints() +{ + return s_waylandConnection.pointerConstraints; +} + +IdleInhibitManager *waylandIdleInhibitManager() +{ + return s_waylandConnection.idleInhibit; +} + +AppMenuManager* waylandAppMenuManager() +{ + return s_waylandConnection.appMenu; +} + +XdgDecorationManager *xdgDecorationManager() +{ + return s_waylandConnection.xdgDecoration; +} + +OutputManagement *waylandOutputManagement() +{ + return s_waylandConnection.outputManagement; +} + +TextInputManager *waylandTextInputManager() +{ + return s_waylandConnection.textInputManager; +} + +QVector waylandOutputs() +{ + return s_waylandConnection.outputs; +} + +bool waitForWaylandPointer() +{ + if (!s_waylandConnection.seat) { + return false; + } + QSignalSpy hasPointerSpy(s_waylandConnection.seat, &Seat::hasPointerChanged); + if (!hasPointerSpy.isValid()) { + return false; + } + return hasPointerSpy.wait(); +} + +bool waitForWaylandTouch() +{ + if (!s_waylandConnection.seat) { + return false; + } + QSignalSpy hasTouchSpy(s_waylandConnection.seat, &Seat::hasTouchChanged); + if (!hasTouchSpy.isValid()) { + return false; + } + return hasTouchSpy.wait(); +} + +bool waitForWaylandKeyboard() +{ + if (!s_waylandConnection.seat) { + return false; + } + QSignalSpy hasKeyboardSpy(s_waylandConnection.seat, &Seat::hasKeyboardChanged); + if (!hasKeyboardSpy.isValid()) { + return false; + } + return hasKeyboardSpy.wait(); +} + +void render(Surface *surface, const QSize &size, const QColor &color, const QImage::Format &format) +{ + QImage img(size, format); + img.fill(color); + render(surface, img); +} + +void render(Surface *surface, const QImage &img) +{ + surface->attachBuffer(s_waylandConnection.shm->createBuffer(img)); + surface->damage(QRect(QPoint(0, 0), img.size())); + surface->commit(Surface::CommitFlag::None); +} + +AbstractClient *waitForWaylandWindowShown(int timeout) +{ + QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded); + if (!clientAddedSpy.isValid()) { + return nullptr; + } + if (!clientAddedSpy.wait(timeout)) { + return nullptr; + } + return clientAddedSpy.first().first().value(); +} + +AbstractClient *renderAndWaitForShown(Surface *surface, const QSize &size, const QColor &color, const QImage::Format &format, int timeout) +{ + QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded); + if (!clientAddedSpy.isValid()) { + return nullptr; + } + render(surface, size, color, format); + flushWaylandConnection(); + if (!clientAddedSpy.wait(timeout)) { + return nullptr; + } + return clientAddedSpy.first().first().value(); +} + +void flushWaylandConnection() +{ + if (s_waylandConnection.connection) { + s_waylandConnection.connection->flush(); + } +} + +Surface *createSurface(QObject *parent) +{ + if (!s_waylandConnection.compositor) { + return nullptr; + } + auto s = s_waylandConnection.compositor->createSurface(parent); + if (!s->isValid()) { + delete s; + return nullptr; + } + return s; +} + +SubSurface *createSubSurface(Surface *surface, Surface *parentSurface, QObject *parent) +{ + if (!s_waylandConnection.subCompositor) { + return nullptr; + } + auto s = s_waylandConnection.subCompositor->createSubSurface(surface, parentSurface, parent); + if (!s->isValid()) { + delete s; + return nullptr; + } + return s; +} + +LayerSurfaceV1 *createLayerSurfaceV1(Surface *surface, const QString &scope, 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; + } + + LayerSurfaceV1 *shellSurface = new LayerSurfaceV1(); + shellSurface->init(shell->get_layer_surface(*surface, nativeOutput, layer, scope)); + + return shellSurface; +} + +XdgShellSurface *createXdgShellStableSurface(Surface *surface, QObject *parent, CreationSetup creationSetup) +{ + if (!s_waylandConnection.xdgShellStable) { + return nullptr; + } + auto s = s_waylandConnection.xdgShellStable->createSurface(surface, parent); + if (!s->isValid()) { + delete s; + return nullptr; + } + if (creationSetup == CreationSetup::CreateAndConfigure) { + initXdgShellSurface(surface, s); + } + return s; +} + +QtWayland::zwp_input_panel_surface_v1 *createInputPanelSurfaceV1(Surface *surface, Output *output) +{ + if (!s_waylandConnection.inputPanelV1) { + qWarning() << "Unable to create the input panel surface. The interface input_panel global is not bound"; + return nullptr; + } + QtWayland::zwp_input_panel_surface_v1 *s = new QtWayland::zwp_input_panel_surface_v1(s_waylandConnection.inputPanelV1->get_input_panel_surface(*surface)); + + if (!s->isInitialized()) { + delete s; + return nullptr; + } + + s->set_toplevel(output->output(), QtWayland::zwp_input_panel_surface_v1::position_center_bottom); + + return s; +} + +XdgShellPopup *createXdgShellStablePopup(Surface *surface, XdgShellSurface *parentSurface, const KWayland::Client::XdgPositioner &positioner, QObject *parent, CreationSetup creationSetup) +{ + if (!s_waylandConnection.xdgShellStable) { + return nullptr; + } + auto s = s_waylandConnection.xdgShellStable->createPopup(surface, parentSurface, positioner, parent); + if (!s->isValid()) { + delete s; + return nullptr; + } + if (creationSetup == CreationSetup::CreateAndConfigure) { + initXdgShellPopup(surface, s); + } + return s; +} + +void initXdgShellSurface(KWayland::Client::Surface *surface, KWayland::Client::XdgShellSurface *shellSurface) +{ + //wait for configure + QSignalSpy configureRequestedSpy(shellSurface, &KWayland::Client::XdgShellSurface::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + surface->commit(Surface::CommitFlag::None); + QVERIFY(configureRequestedSpy.wait()); + shellSurface->ackConfigure(configureRequestedSpy.last()[2].toInt()); +} + +void initXdgShellPopup(KWayland::Client::Surface *surface, KWayland::Client::XdgShellPopup *shellPopup) +{ + //wait for configure + QSignalSpy configureRequestedSpy(shellPopup, &KWayland::Client::XdgShellPopup::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + surface->commit(Surface::CommitFlag::None); + QVERIFY(configureRequestedSpy.wait()); + shellPopup->ackConfigure(configureRequestedSpy.last()[1].toInt()); +} + +static void waitForConfigured(XdgSurface *shellSurface) +{ + QSignalSpy surfaceConfigureRequestedSpy(shellSurface, &XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.isValid()); + + shellSurface->surface()->commit(Surface::CommitFlag::None); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + shellSurface->ack_configure(surfaceConfigureRequestedSpy.last().first().toUInt()); +} + +XdgToplevel *createXdgToplevelSurface(Surface *surface, QObject *parent, 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; + } + + XdgSurface *xdgSurface = new XdgSurface(shell, surface, parent); + XdgToplevel *xdgToplevel = new XdgToplevel(xdgSurface, parent); + + if (configureMode == CreationSetup::CreateAndConfigure) { + waitForConfigured(xdgSurface); + } + + return xdgToplevel; +} + +XdgPositioner *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 new XdgPositioner(shell); +} + +XdgPopup *createXdgPopupSurface(Surface *surface, XdgSurface *parentSurface, XdgPositioner *positioner, + QObject *parent, 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, parent); + XdgPopup *xdgPopup = new XdgPopup(xdgSurface, parentSurface, positioner, parent); + + if (configureMode == CreationSetup::CreateAndConfigure) { + waitForConfigured(xdgSurface); + } + + return xdgPopup; +} + +bool waitForWindowDestroyed(AbstractClient *client) +{ + QSignalSpy destroyedSpy(client, &QObject::destroyed); + if (!destroyedSpy.isValid()) { + return false; + } + return destroyedSpy.wait(); +} + +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 (!ScreenLockerWatcher::self()->isLocked()) { + QSignalSpy lockedSpy(ScreenLockerWatcher::self(), &ScreenLockerWatcher::locked); + if (!lockedSpy.isValid()) { + return false; + } + if (!lockedSpy.wait()) { + return false; + } + if (!ScreenLockerWatcher::self()->isLocked()) { + 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 (ScreenLockerWatcher::self()->isLocked()) { + QSignalSpy lockedSpy(ScreenLockerWatcher::self(), &ScreenLockerWatcher::locked); + if (!lockedSpy.isValid()) { + return false; + } + if (!lockedSpy.wait()) { + return false; + } + if (ScreenLockerWatcher::self()->isLocked()) { + return false; + } + } + return true; +} + +} +} diff --git a/autotests/integration/touch_input_test.cpp b/autotests/integration/touch_input_test.cpp new file mode 100644 index 0000000..c77838c --- /dev/null +++ b/autotests/integration/touch_input_test.cpp @@ -0,0 +1,272 @@ +/* + 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 "abstract_client.h" +#include "platform.h" +#include "cursor.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" + +#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(); + +private: + AbstractClient *showWindow(bool decorated = false); + KWayland::Client::Touch *m_touch = nullptr; +}; + +void TouchInputTest::initTestCase() +{ + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + waylandServer()->initWorkspace(); +} + +void TouchInputTest::init() +{ + using namespace KWayland::Client; + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::Decoration)); + QVERIFY(Test::waitForWaylandTouch()); + m_touch = Test::waylandSeat()->createTouch(Test::waylandSeat()); + QVERIFY(m_touch); + QVERIFY(m_touch->isValid()); + + screens()->setCurrent(0); + Cursors::self()->mouse()->setPos(QPoint(1280, 512)); +} + +void TouchInputTest::cleanup() +{ + delete m_touch; + m_touch = nullptr; + Test::destroyWaylandConnection(); +} + +AbstractClient *TouchInputTest::showWindow(bool decorated) +{ + using namespace KWayland::Client; +#define VERIFY(statement) \ + if (!QTest::qVerify((statement), #statement, "", __FILE__, __LINE__))\ + return nullptr; +#define COMPARE(actual, expected) \ + if (!QTest::qCompare(actual, expected, #actual, #expected, __FILE__, __LINE__))\ + return nullptr; + + Surface *surface = Test::createSurface(Test::waylandCompositor()); + VERIFY(surface); + XdgShellSurface *shellSurface = Test::createXdgShellStableSurface(surface, surface); + VERIFY(shellSurface); + if (decorated) { + auto deco = Test::waylandServerSideDecoration()->create(surface, surface); + QSignalSpy decoSpy(deco, &ServerSideDecoration::modeChanged); + VERIFY(decoSpy.isValid()); + VERIFY(decoSpy.wait()); + deco->requestMode(ServerSideDecoration::Mode::Server); + VERIFY(decoSpy.wait()); + COMPARE(deco->mode(), ServerSideDecoration::Mode::Server); + } + // let's render + auto c = Test::renderAndWaitForShown(surface, QSize(100, 50), Qt::blue); + + VERIFY(c); + COMPARE(workspace()->activeClient(), c); + +#undef VERIFY +#undef COMPARE + + return c; +} + +void TouchInputTest::testTouchHidesCursor() +{ + QCOMPARE(kwinApp()->platform()->isCursorHidden(), false); + quint32 timestamp = 1; + kwinApp()->platform()->touchDown(1, QPointF(125, 125), timestamp++); + QCOMPARE(kwinApp()->platform()->isCursorHidden(), true); + kwinApp()->platform()->touchDown(2, QPointF(130, 125), timestamp++); + kwinApp()->platform()->touchUp(2, timestamp++); + kwinApp()->platform()->touchUp(1, timestamp++); + + // now a mouse event should show the cursor again + kwinApp()->platform()->pointerMotion(QPointF(0, 0), timestamp++); + QCOMPARE(kwinApp()->platform()->isCursorHidden(), false); + + // touch should hide again + kwinApp()->platform()->touchDown(1, QPointF(125, 125), timestamp++); + kwinApp()->platform()->touchUp(1, timestamp++); + QCOMPARE(kwinApp()->platform()->isCursorHidden(), true); + + // wheel should also show + kwinApp()->platform()->pointerAxisVertical(1.0, timestamp++); + QCOMPARE(kwinApp()->platform()->isCursorHidden(), false); +} + +void TouchInputTest::testMultipleTouchPoints_data() +{ + QTest::addColumn("decorated"); + + QTest::newRow("undecorated") << false; + QTest::newRow("decorated") << true; +} + +void TouchInputTest::testMultipleTouchPoints() +{ + using namespace KWayland::Client; + QFETCH(bool, decorated); + AbstractClient *c = showWindow(decorated); + QCOMPARE(c->isDecorated(), decorated); + c->move(100, 100); + QVERIFY(c); + QSignalSpy sequenceStartedSpy(m_touch, &Touch::sequenceStarted); + QVERIFY(sequenceStartedSpy.isValid()); + QSignalSpy pointAddedSpy(m_touch, &Touch::pointAdded); + QVERIFY(pointAddedSpy.isValid()); + QSignalSpy pointMovedSpy(m_touch, &Touch::pointMoved); + QVERIFY(pointMovedSpy.isValid()); + QSignalSpy pointRemovedSpy(m_touch, &Touch::pointRemoved); + QVERIFY(pointRemovedSpy.isValid()); + QSignalSpy endedSpy(m_touch, &Touch::sequenceEnded); + QVERIFY(endedSpy.isValid()); + + quint32 timestamp = 1; + kwinApp()->platform()->touchDown(1, QPointF(125, 125) + c->clientPos(), 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); + + // a point outside the window + kwinApp()->platform()->touchDown(2, QPointF(0, 0) + c->clientPos(), 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)->position(), QPointF(-100, -100)); + QCOMPARE(pointMovedSpy.count(), 0); + + // let's move that one + kwinApp()->platform()->touchMotion(2, QPointF(100, 100) + c->clientPos(), 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)->position(), QPointF(0, 0)); + + kwinApp()->platform()->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); + + kwinApp()->platform()->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() +{ + using namespace KWayland::Client; + AbstractClient *c = showWindow(); + c->move(100, 100); + QVERIFY(c); + QSignalSpy sequenceStartedSpy(m_touch, &Touch::sequenceStarted); + QVERIFY(sequenceStartedSpy.isValid()); + QSignalSpy cancelSpy(m_touch, &Touch::sequenceCanceled); + QVERIFY(cancelSpy.isValid()); + QSignalSpy pointRemovedSpy(m_touch, &Touch::pointRemoved); + QVERIFY(pointRemovedSpy.isValid()); + + quint32 timestamp = 1; + kwinApp()->platform()->touchDown(1, QPointF(125, 125), timestamp++); + QVERIFY(sequenceStartedSpy.wait()); + QCOMPARE(sequenceStartedSpy.count(), 1); + + // cancel + kwinApp()->platform()->touchCancel(); + QVERIFY(cancelSpy.wait()); + QCOMPARE(cancelSpy.count(), 1); + + kwinApp()->platform()->touchUp(1, timestamp++); + QVERIFY(!pointRemovedSpy.wait(100)); + QCOMPARE(pointRemovedSpy.count(), 0); +} + +void TouchInputTest::testTouchMouseAction() +{ + // this test verifies that a touch down on an inactive client will activate it + using namespace KWayland::Client; + // create two windows + AbstractClient *c1 = showWindow(); + QVERIFY(c1); + AbstractClient *c2 = 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, &Touch::sequenceStarted); + QVERIFY(sequenceStartedSpy.isValid()); + + quint32 timestamp = 1; + kwinApp()->platform()->touchDown(1, c1->frameGeometry().center(), timestamp++); + QVERIFY(c1->isActive()); + + QVERIFY(sequenceStartedSpy.wait()); + QCOMPARE(sequenceStartedSpy.count(), 1); + + // cleanup + kwinApp()->platform()->touchCancel(); +} + +} + +WAYLANDTEST_MAIN(KWin::TouchInputTest) +#include "touch_input_test.moc" diff --git a/autotests/integration/transient_placement.cpp b/autotests/integration/transient_placement.cpp new file mode 100644 index 0000000..6d407de --- /dev/null +++ b/autotests/integration/transient_placement.cpp @@ -0,0 +1,355 @@ +/* + 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 "platform.h" +#include "abstract_client.h" +#include "cursor.h" +#include "screenedge.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + setenv("QT_QPA_PLATFORM", "wayland", true); + waylandServer()->initWorkspace(); +} + +void TransientPlacementTest::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Decoration | Test::AdditionalWaylandInterface::PlasmaShell)); + + screens()->setCurrent(0); + Cursors::self()->mouse()->setPos(QPoint(640, 512)); +} + +void TransientPlacementTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void TransientPlacementTest::testXdgPopup_data() +{ + using namespace KWayland::Client; + + QTest::addColumn("parentSize"); + QTest::addColumn("parentPosition"); + QTest::addColumn("positioner"); + QTest::addColumn("expectedGeometry"); + + // window in the middle, plenty of room either side: Changing anchor + + // parent window is 500,500, starting at 300,300, anchorRect is therefore between 350->750 in both dirs + XdgPositioner positioner(QSize(200,200), QRect(50,50, 400,400)); + positioner.setGravity(Qt::BottomEdge | Qt::RightEdge); + + positioner.setAnchorEdge(Qt::Edges()); + QTest::newRow("anchorCentre") << QSize(500, 500) << QPoint(300,300) << positioner << QRect(550, 550, 200, 200); + positioner.setAnchorEdge(Qt::TopEdge | Qt::LeftEdge); + QTest::newRow("anchorTopLeft") << QSize(500, 500) << QPoint(300,300) << positioner << QRect(350,350, 200, 200); + positioner.setAnchorEdge(Qt::TopEdge); + QTest::newRow("anchorTop") << QSize(500, 500) << QPoint(300,300) << positioner << QRect(550, 350, 200, 200); + positioner.setAnchorEdge(Qt::TopEdge | Qt::RightEdge); + QTest::newRow("anchorTopRight") << QSize(500, 500) << QPoint(300,300) << positioner << QRect(750, 350, 200, 200); + positioner.setAnchorEdge(Qt::RightEdge); + QTest::newRow("anchorRight") << QSize(500, 500) << QPoint(300,300) << positioner << QRect(750, 550, 200, 200); + positioner.setAnchorEdge(Qt::BottomEdge | Qt::RightEdge); + QTest::newRow("anchorBottomRight") << QSize(500,500) << QPoint(300,300) << positioner << QRect(750, 750, 200, 200); + positioner.setAnchorEdge(Qt::BottomEdge); + QTest::newRow("anchorBottom") << QSize(500, 500) << QPoint(300,300) << positioner << QRect(550, 750, 200, 200); + positioner.setAnchorEdge(Qt::BottomEdge | Qt::LeftEdge); + QTest::newRow("anchorBottomLeft") << QSize(500, 500) << QPoint(300,300) << positioner << QRect(350, 750, 200, 200); + positioner.setAnchorEdge(Qt::LeftEdge); + QTest::newRow("anchorLeft") << QSize(500, 500) << QPoint(300,300) << positioner << QRect(350, 550, 200, 200); + + // ---------------------------------------------------------------- + // window in the middle, plenty of room either side: Changing gravity around the bottom right anchor + positioner.setAnchorEdge(Qt::BottomEdge | Qt::RightEdge); + positioner.setGravity(Qt::Edges()); + QTest::newRow("gravityCentre") << QSize(500, 500) << QPoint(300, 300) << positioner << QRect(650, 650, 200, 200); + positioner.setGravity(Qt::TopEdge | Qt::LeftEdge); + QTest::newRow("gravityTopLeft") << QSize(500, 500) << QPoint(300, 300) << positioner << QRect(550, 550, 200, 200); + positioner.setGravity(Qt::TopEdge); + QTest::newRow("gravityTop") << QSize(500, 500) << QPoint(300, 300) << positioner << QRect(650, 550, 200, 200); + positioner.setGravity(Qt::TopEdge | Qt::RightEdge); + QTest::newRow("gravityTopRight") << QSize(500, 500) << QPoint(300, 300) << positioner << QRect(750, 550, 200, 200); + positioner.setGravity(Qt::RightEdge); + QTest::newRow("gravityRight") << QSize(500, 500) << QPoint(300, 300) << positioner << QRect(750, 650, 200, 200); + positioner.setGravity(Qt::BottomEdge | Qt::RightEdge); + QTest::newRow("gravityBottomRight") << QSize(500, 500) << QPoint(300, 300) << positioner << QRect(750, 750, 200, 200); + positioner.setGravity(Qt::BottomEdge); + QTest::newRow("gravityBottom") << QSize(500, 500) << QPoint(300, 300) << positioner << QRect(650, 750, 200, 200); + positioner.setGravity(Qt::BottomEdge | Qt::LeftEdge); + QTest::newRow("gravityBottomLeft") << QSize(500, 500) << QPoint(300, 300) << positioner << QRect(550, 750, 200, 200); + positioner.setGravity(Qt::LeftEdge); + QTest::newRow("gravityLeft") << QSize(500, 500) << QPoint(300, 300) << positioner << QRect(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 + positioner.setConstraints(XdgPositioner::Constraint::SlideX | XdgPositioner::Constraint::SlideY); + + positioner.setAnchorEdge(Qt::TopEdge); + positioner.setGravity(Qt::TopEdge); + QTest::newRow("constraintSlideTop") << QSize(500, 500) << QPoint(80, 80) << positioner << QRect(80 + 250 - 100, 0, 200, 200); + + positioner.setAnchorEdge(Qt::LeftEdge); + positioner.setGravity(Qt::LeftEdge); + QTest::newRow("constraintSlideLeft") << QSize(500, 500) << QPoint(80, 80) << positioner << QRect(0, 80 + 250 - 100, 200, 200); + + positioner.setAnchorEdge(Qt::RightEdge); + positioner.setGravity(Qt::RightEdge); + QTest::newRow("constraintSlideRight") << QSize(500, 500) << QPoint(700, 80) << positioner << QRect(1280 - 200, 80 + 250 - 100, 200, 200); + + positioner.setAnchorEdge(Qt::BottomEdge); + positioner.setGravity(Qt::BottomEdge); + QTest::newRow("constraintSlideBottom") << QSize(500, 500) << QPoint(80, 500) << positioner << QRect(80 + 250 - 100, 1024 - 200, 200, 200); + + positioner.setAnchorEdge(Qt::BottomEdge | Qt::RightEdge); + positioner.setGravity(Qt::BottomEdge| Qt::RightEdge); + QTest::newRow("constraintSlideBottomRight") << QSize(500, 500) << QPoint(700, 1000) << positioner << QRect(1280 - 200, 1024 - 200, 200, 200); + + + // ---------------------------------------------------------------- + // constrain and flip + positioner.setConstraints(XdgPositioner::Constraint::FlipX | XdgPositioner::Constraint::FlipY); + + positioner.setAnchorEdge(Qt::TopEdge); + positioner.setGravity(Qt::TopEdge); + QTest::newRow("constraintFlipTop") << QSize(500, 500) << QPoint(80, 80) << positioner << QRect(230, 80 + 500 - 50, 200, 200); + + positioner.setAnchorEdge(Qt::LeftEdge); + positioner.setGravity(Qt::LeftEdge); + QTest::newRow("constraintFlipLeft") << QSize(500, 500) << QPoint(80, 80) << positioner << QRect(80 + 500 - 50, 230, 200, 200); + + positioner.setAnchorEdge(Qt::RightEdge); + positioner.setGravity(Qt::RightEdge); + QTest::newRow("constraintFlipRight") << QSize(500, 500) << QPoint(700, 80) << positioner << QRect(700 + 50 - 200, 230, 200, 200); + + positioner.setAnchorEdge(Qt::BottomEdge); + positioner.setGravity(Qt::BottomEdge); + QTest::newRow("constraintFlipBottom") << QSize(500, 500) << QPoint(80, 500) << positioner << QRect(230, 500 + 50 - 200, 200, 200); + + positioner.setAnchorEdge(Qt::BottomEdge | Qt::RightEdge); + positioner.setGravity(Qt::BottomEdge| Qt::RightEdge); + QTest::newRow("constraintFlipBottomRight") << QSize(500, 500) << QPoint(700, 500) << positioner << QRect(700 + 50 - 200, 500 + 50 - 200, 200, 200); + + positioner.setAnchorEdge(Qt::TopEdge); + positioner.setGravity(Qt::RightEdge); + //as popup is positioned in the middle of the parent we need a massive popup to be able to overflow + positioner.setInitialSize(QSize(400, 400)); + QTest::newRow("constraintFlipRightNoAnchor") << QSize(500, 500) << QPoint(700, 80) << positioner << QRect(700 + 250 - 400, 330, 400, 400); + + positioner.setAnchorEdge(Qt::RightEdge); + positioner.setGravity(Qt::TopEdge); + positioner.setInitialSize(QSize(300, 200)); + QTest::newRow("constraintFlipRightNoGravity") << QSize(500, 500) << QPoint(700, 80) << positioner << QRect(700 + 50 - 150, 130, 300, 200); + + // ---------------------------------------------------------------- + // resize + positioner.setConstraints(XdgPositioner::Constraint::ResizeX | XdgPositioner::Constraint::ResizeY); + positioner.setInitialSize(QSize(200, 200)); + + positioner.setAnchorEdge(Qt::TopEdge); + positioner.setGravity(Qt::TopEdge); + QTest::newRow("resizeTop") << QSize(500, 500) << QPoint(80, 80) << positioner << QRect(80 + 250 - 100, 0, 200, 130); + + positioner.setAnchorEdge(Qt::LeftEdge); + positioner.setGravity(Qt::LeftEdge); + QTest::newRow("resizeLeft") << QSize(500, 500) << QPoint(80, 80) << positioner << QRect(0, 80 + 250 - 100, 130, 200); + + positioner.setAnchorEdge(Qt::RightEdge); + positioner.setGravity(Qt::RightEdge); + QTest::newRow("resizeRight") << QSize(500, 500) << QPoint(700, 80) << positioner << QRect(700 + 50 + 400, 80 + 250 - 100, 130, 200); + + positioner.setAnchorEdge(Qt::BottomEdge); + positioner.setGravity(Qt::BottomEdge); + QTest::newRow("resizeBottom") << QSize(500, 500) << QPoint(80, 500) << positioner << QRect(80 + 250 - 100, 500 + 50 + 400, 200, 74); +} + +void TransientPlacementTest::testXdgPopup() +{ + using namespace KWayland::Client; + + // 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(QRect, expectedGeometry); + const QRect expectedRelativeGeometry = expectedGeometry.translated(-parentPosition); + + Surface *surface = Test::createSurface(Test::waylandCompositor()); + QVERIFY(surface); + auto parentShellSurface = Test::createXdgShellStableSurface(surface, Test::waylandCompositor()); + QVERIFY(parentShellSurface); + auto parent = Test::renderAndWaitForShown(surface, parentSize, Qt::blue); + QVERIFY(parent); + + QVERIFY(!parent->isDecorated()); + parent->move(parentPosition); + QCOMPARE(parent->frameGeometry(), QRect(parentPosition, parentSize)); + + //create popup + QFETCH(XdgPositioner, positioner); + + Surface *transientSurface = Test::createSurface(Test::waylandCompositor()); + QVERIFY(transientSurface); + + auto popup = Test::createXdgShellStablePopup(transientSurface, parentShellSurface, positioner, Test::waylandCompositor(), Test::CreationSetup::CreateOnly); + QSignalSpy configureRequestedSpy(popup, &XdgShellPopup::configureRequested); + transientSurface->commit(Surface::CommitFlag::None); + + configureRequestedSpy.wait(); + QCOMPARE(configureRequestedSpy.count(), 1); + QCOMPARE(configureRequestedSpy.first()[0].value(), expectedRelativeGeometry); + popup->ackConfigure(configureRequestedSpy.first()[1].toUInt()); + + auto transient = Test::renderAndWaitForShown(transientSurface, expectedRelativeGeometry.size(), Qt::red); + QVERIFY(transient); + + QVERIFY(!transient->isDecorated()); + QVERIFY(transient->hasTransientPlacementHint()); + QCOMPARE(transient->frameGeometry(), expectedGeometry); + + QCOMPARE(configureRequestedSpy.count(), 1); // check that we did not get reconfigured +} + +void TransientPlacementTest::testXdgPopupWithPanel() +{ + using namespace KWayland::Client; + + QScopedPointer surface{Test::createSurface()}; + QVERIFY(!surface.isNull()); + QScopedPointer dockShellSurface{Test::createXdgShellStableSurface(surface.data(), surface.data())}; + QVERIFY(!dockShellSurface.isNull()); + QScopedPointer plasmaSurface(Test::waylandPlasmaShell()->createSurface(surface.data())); + QVERIFY(!plasmaSurface.isNull()); + plasmaSurface->setRole(PlasmaShellSurface::Role::Panel); + plasmaSurface->setPosition(QPoint(0, screens()->geometry(0).height() - 50)); + plasmaSurface->setPanelBehavior(PlasmaShellSurface::PanelBehavior::AlwaysVisible); + + // now render and map the window + QVERIFY(workspace()->clientArea(PlacementArea, 0, 1) == workspace()->clientArea(FullScreenArea, 0, 1)); + auto dock = Test::renderAndWaitForShown(surface.data(), QSize(1280, 50), Qt::blue); + QVERIFY(dock); + QCOMPARE(dock->windowType(), NET::Dock); + QVERIFY(dock->isDock()); + QCOMPARE(dock->frameGeometry(), QRect(0, screens()->geometry(0).height() - 50, 1280, 50)); + QCOMPARE(dock->hasStrut(), true); + QVERIFY(workspace()->clientArea(PlacementArea, 0, 1) != workspace()->clientArea(FullScreenArea, 0, 1)); + + //create parent + Surface *parentSurface = Test::createSurface(Test::waylandCompositor()); + QVERIFY(parentSurface); + auto parentShellSurface = Test::createXdgShellStableSurface(parentSurface, Test::waylandCompositor()); + QVERIFY(parentShellSurface); + auto parent = Test::renderAndWaitForShown(parentSurface, {800, 600}, Qt::blue); + QVERIFY(parent); + + QVERIFY(!parent->isDecorated()); + parent->move({0, screens()->geometry(0).height() - 600}); + parent->keepInArea(workspace()->clientArea(PlacementArea, parent)); + QCOMPARE(parent->frameGeometry(), QRect(0, screens()->geometry(0).height() - 600 - 50, 800, 600)); + + Surface *transientSurface = Test::createSurface(Test::waylandCompositor()); + QVERIFY(transientSurface); + XdgPositioner positioner(QSize(200,200), QRect(50,500, 200,200)); + + auto transientShellSurface = Test::createXdgShellStablePopup(transientSurface, parentShellSurface, positioner, Test::waylandCompositor()); + auto transient = Test::renderAndWaitForShown(transientSurface, positioner.initialSize(), Qt::red); + QVERIFY(transient); + + QVERIFY(!transient->isDecorated()); + QVERIFY(transient->hasTransientPlacementHint()); + + QCOMPARE(transient->frameGeometry(), QRect(50, screens()->geometry(0).height() - 200 - 50, 200, 200)); + + transientShellSurface->deleteLater(); + transientSurface->deleteLater(); + QVERIFY(Test::waitForWindowDestroyed(transient)); + + // now parent to fullscreen - on fullscreen the panel is ignored + QSignalSpy fullscreenSpy{parentShellSurface, &XdgShellSurface::configureRequested}; + QVERIFY(fullscreenSpy.isValid()); + parent->setFullScreen(true); + QVERIFY(fullscreenSpy.wait()); + parentShellSurface->ackConfigure(fullscreenSpy.first().at(2).value()); + QSignalSpy frameGeometryChangedSpy{parent, &AbstractClient::frameGeometryChanged}; + QVERIFY(frameGeometryChangedSpy.isValid()); + Test::render(parentSurface, fullscreenSpy.first().at(0).toSize(), Qt::red); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(parent->frameGeometry(), screens()->geometry(0)); + QVERIFY(parent->isFullScreen()); + + // another transient, with same hints as before from bottom of window + transientSurface = Test::createSurface(Test::waylandCompositor()); + QVERIFY(transientSurface); + + XdgPositioner positioner2(QSize(200,200), QRect(50,screens()->geometry(0).height()-100, 200,200)); + transientShellSurface = Test::createXdgShellStablePopup(transientSurface, parentShellSurface, positioner2, Test::waylandCompositor()); + transient = Test::renderAndWaitForShown(transientSurface, positioner2.initialSize(), Qt::red); + QVERIFY(transient); + + QVERIFY(!transient->isDecorated()); + QVERIFY(transient->hasTransientPlacementHint()); + + QCOMPARE(transient->frameGeometry(), QRect(50, screens()->geometry(0).height() - 200, 200, 200)); +} + +} + +WAYLANDTEST_MAIN(KWin::TransientPlacementTest) +#include "transient_placement.moc" diff --git a/autotests/integration/virtual_desktop_test.cpp b/autotests/integration/virtual_desktop_test.cpp new file mode 100644 index 0000000..5bcff6b --- /dev/null +++ b/autotests/integration/virtual_desktop_test.cpp @@ -0,0 +1,287 @@ +/* + 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 "abstract_client.h" +#include "main.h" +#include "platform.h" +#include "screens.h" +#include "wayland_server.h" +#include "virtualdesktops.h" + +#include + +using namespace KWin; +using namespace KWayland::Client; + +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(); + + void testNetCurrentDesktop(); + void testLastDesktopRemoved(); + void testWindowOnMultipleDesktops(); + void testRemoveDesktopWithWindow(); +}; + +void VirtualDesktopTest::initTestCase() +{ + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); + qputenv("KWIN_XKB_DEFAULT_KEYMAP", "1"); + qputenv("XKB_DEFAULT_RULES", "evdev"); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + waylandServer()->initWorkspace(); + + 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); + bool ok = true; + QCOMPARE(currentDesktop.value(0, &ok), 0); + QVERIFY(ok); + } +} + +void VirtualDesktopTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); + screens()->setCurrent(0); + VirtualDesktopManager::self()->setCount(1); +} + +void VirtualDesktopTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +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); + bool ok = true; + QCOMPARE(currentDesktop.value(0, &ok), 0); + QVERIFY(ok); + + // go to desktop 2 + VirtualDesktopManager::self()->setCurrent(2); + currentDesktop = Xcb::Property(0, kwinApp()->x11RootWindow(), currentDesktopAtom, XCB_ATOM_CARDINAL, 0, 1); + QCOMPARE(currentDesktop.value(0, &ok), 1); + QVERIFY(ok); + + // go to desktop 3 + VirtualDesktopManager::self()->setCurrent(3); + currentDesktop = Xcb::Property(0, kwinApp()->x11RootWindow(), currentDesktopAtom, XCB_ATOM_CARDINAL, 0, 1); + QCOMPARE(currentDesktop.value(0, &ok), 2); + QVERIFY(ok); + + // go to desktop 4 + VirtualDesktopManager::self()->setCurrent(4); + currentDesktop = Xcb::Property(0, kwinApp()->x11RootWindow(), currentDesktopAtom, XCB_ATOM_CARDINAL, 0, 1); + QCOMPARE(currentDesktop.value(0, &ok), 3); + QVERIFY(ok); + + // and back to first + VirtualDesktopManager::self()->setCurrent(1); + currentDesktop = Xcb::Property(0, kwinApp()->x11RootWindow(), currentDesktopAtom, XCB_ATOM_CARDINAL, 0, 1); + QCOMPARE(currentDesktop.value(0, &ok), 0); + QVERIFY(ok); +} + +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 + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + + QVERIFY(client); + QCOMPARE(client->desktop(), 2); + QSignalSpy desktopPresenceChangedSpy(client, &AbstractClient::desktopPresenceChanged); + QVERIFY(desktopPresenceChangedSpy.isValid()); + + QCOMPARE(client->desktops().count(), 1u); + QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), client->desktops().first()); + + // and remove last desktop + VirtualDesktopManager::self()->setCount(1); + QCOMPARE(VirtualDesktopManager::self()->count(), 1u); + // now the client should be moved as well + QTRY_COMPARE(desktopPresenceChangedSpy.count(), 1); + QCOMPARE(client->desktop(), 1); + + QCOMPARE(client->desktops().count(), 1u); + QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), client->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 + VirtualDesktopManager::self()->setCurrent(VirtualDesktopManager::self()->desktops().last()); + QCOMPARE(VirtualDesktopManager::self()->current(), 3u); + + // now create a window on this desktop + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + + QVERIFY(client); + QCOMPARE(client->desktop(), 3u); + QSignalSpy desktopPresenceChangedSpy(client, &AbstractClient::desktopPresenceChanged); + QVERIFY(desktopPresenceChangedSpy.isValid()); + + QCOMPARE(client->desktops().count(), 1u); + QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), client->desktops().first()); + + //Set the window on desktop 2 as well + client->enterDesktop(VirtualDesktopManager::self()->desktopForX11Id(2)); + QCOMPARE(client->desktops().count(), 2u); + QCOMPARE(VirtualDesktopManager::self()->desktops()[2], client->desktops()[0]); + QCOMPARE(VirtualDesktopManager::self()->desktops()[1], client->desktops()[1]); + QVERIFY(client->isOnDesktop(2)); + QVERIFY(client->isOnDesktop(3)); + + //leave desktop 3 + client->leaveDesktop(VirtualDesktopManager::self()->desktopForX11Id(3)); + QCOMPARE(client->desktops().count(), 1u); + //leave desktop 2 + client->leaveDesktop(VirtualDesktopManager::self()->desktopForX11Id(2)); + QCOMPARE(client->desktops().count(), 0u); + //we should be on all desktops now + QVERIFY(client->isOnAllDesktops()); + //put on desktop 1 + client->enterDesktop(VirtualDesktopManager::self()->desktopForX11Id(1)); + QVERIFY(client->isOnDesktop(1)); + QVERIFY(!client->isOnDesktop(2)); + QVERIFY(!client->isOnDesktop(3)); + QCOMPARE(client->desktops().count(), 1u); + //put on desktop 2 + client->enterDesktop(VirtualDesktopManager::self()->desktopForX11Id(2)); + QVERIFY(client->isOnDesktop(1)); + QVERIFY(client->isOnDesktop(2)); + QVERIFY(!client->isOnDesktop(3)); + QCOMPARE(client->desktops().count(), 2u); + //put on desktop 3 + client->enterDesktop(VirtualDesktopManager::self()->desktopForX11Id(3)); + QVERIFY(client->isOnDesktop(1)); + QVERIFY(client->isOnDesktop(2)); + QVERIFY(client->isOnDesktop(3)); + QCOMPARE(client->desktops().count(), 3u); + + //entering twice dooes nothing + client->enterDesktop(VirtualDesktopManager::self()->desktopForX11Id(3)); + QCOMPARE(client->desktops().count(), 3u); + + //adding to "all desktops" results in just that one desktop + client->setOnAllDesktops(true); + QCOMPARE(client->desktops().count(), 0u); + client->enterDesktop(VirtualDesktopManager::self()->desktopForX11Id(3)); + QVERIFY(client->isOnDesktop(3)); + QCOMPARE(client->desktops().count(), 1u); + + //leaving a desktop on "all desktops" puts on everything else + client->setOnAllDesktops(true); + QCOMPARE(client->desktops().count(), 0u); + client->leaveDesktop(VirtualDesktopManager::self()->desktopForX11Id(3)); + QVERIFY(client->isOnDesktop(1)); + QVERIFY(client->isOnDesktop(2)); + QCOMPARE(client->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 + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + + QVERIFY(client); + QCOMPARE(client->desktop(), 3u); + QSignalSpy desktopPresenceChangedSpy(client, &AbstractClient::desktopPresenceChanged); + QVERIFY(desktopPresenceChangedSpy.isValid()); + + QCOMPARE(client->desktops().count(), 1u); + QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), client->desktops().first()); + + //Set the window on desktop 2 as well + client->enterDesktop(VirtualDesktopManager::self()->desktops()[1]); + QCOMPARE(client->desktops().count(), 2u); + QCOMPARE(VirtualDesktopManager::self()->desktops()[2], client->desktops()[0]); + QCOMPARE(VirtualDesktopManager::self()->desktops()[1], client->desktops()[1]); + QVERIFY(client->isOnDesktop(2)); + QVERIFY(client->isOnDesktop(3)); + + //remove desktop 3 + VirtualDesktopManager::self()->setCount(2); + QCOMPARE(client->desktops().count(), 1u); + //window is only on desktop 2 + QCOMPARE(VirtualDesktopManager::self()->desktops()[1], client->desktops()[0]); + + //Again 3 desktops + VirtualDesktopManager::self()->setCount(3); + //move window to be only on desktop 3 + client->enterDesktop(VirtualDesktopManager::self()->desktops()[2]); + client->leaveDesktop(VirtualDesktopManager::self()->desktops()[1]); + QCOMPARE(client->desktops().count(), 1u); + //window is only on desktop 3 + QCOMPARE(VirtualDesktopManager::self()->desktops()[2], client->desktops()[0]); + + //remove desktop 3 + VirtualDesktopManager::self()->setCount(2); + QCOMPARE(client->desktops().count(), 1u); + //window is only on desktop 2 + QCOMPARE(VirtualDesktopManager::self()->desktops()[1], client->desktops()[0]); +} + +WAYLANDTEST_MAIN(VirtualDesktopTest) +#include "virtual_desktop_test.moc" diff --git a/autotests/integration/virtualkeyboard_test.cpp b/autotests/integration/virtualkeyboard_test.cpp new file mode 100644 index 0000000..713b806 --- /dev/null +++ b/autotests/integration/virtualkeyboard_test.cpp @@ -0,0 +1,154 @@ +/* + 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 "abstract_client.h" +#include "cursor.h" +#include "effects.h" +#include "deleted.h" +#include "platform.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include "virtualkeyboard.h" +#include "virtualkeyboard_dbus.h" +#include "qwayland-input-method-unstable-v1.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +using namespace KWin; +using namespace KWayland::Client; +using KWin::VirtualKeyboardDBus; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_virtualkeyboard-0"); + +class VirtualKeyboardTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testOpenClose(); +}; + + +void VirtualKeyboardTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + static_cast(kwinApp())->setInputMethodServerToStart("internal"); + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + waylandServer()->initWorkspace(); + +} + +void VirtualKeyboardTest::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat | + Test::AdditionalWaylandInterface::TextInputManagerV2 | Test::AdditionalWaylandInterface::InputMethodV1)); + + + screens()->setCurrent(0); + KWin::Cursors::self()->mouse()->setPos(QPoint(1280, 512)); + + const QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kwin.testvirtualkeyboard"), + QStringLiteral("/VirtualKeyboard"), + QStringLiteral("org.kde.kwin.VirtualKeyboard"), + "enable"); + QDBusConnection::sessionBus().call(message); +} + +void VirtualKeyboardTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void VirtualKeyboardTest::testOpenClose() +{ + QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded); + QSignalSpy clientRemovedSpy(workspace(), &Workspace::clientRemoved); + QVERIFY(clientAddedSpy.isValid()); + + // Create an xdg_toplevel surface and wait for the compositor to catch up. + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(1280, 1024), Qt::red); + QVERIFY(client); + QVERIFY(client->isActive()); + QCOMPARE(client->frameGeometry().size(), QSize(1280, 1024)); + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + + QScopedPointer textInput(Test::waylandTextInputManager()->createTextInput(Test::waylandSeat())); + + QVERIFY(!textInput.isNull()); + textInput->enable(surface.data()); + QVERIFY(configureRequestedSpy.wait()); + + // Show the keyboard + textInput->showInputPanel(); + QVERIFY(clientAddedSpy.wait()); + + AbstractClient *keyboardClient = clientAddedSpy.last().first().value(); + QVERIFY(keyboardClient); + QVERIFY(keyboardClient->isInputMethod()); + + // Do the actual resize + QVERIFY(configureRequestedSpy.wait()); + + Test::render(surface.data(), configureRequestedSpy.last().first().value(), Qt::red); + QVERIFY(frameGeometryChangedSpy.wait()); + + QCOMPARE(client->frameGeometry().height(), 1024 - keyboardClient->inputGeometry().height() + 1); + + // Hide the keyboard + textInput->hideInputPanel(); + + QVERIFY(configureRequestedSpy.wait()); + Test::render(surface.data(), configureRequestedSpy.last().first().value(), Qt::red); + QVERIFY(frameGeometryChangedSpy.wait()); + + QCOMPARE(client->frameGeometry().height(), 1024); + + // Destroy the test client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +WAYLANDTEST_MAIN(VirtualKeyboardTest) + +#include "virtualkeyboard_test.moc" diff --git a/autotests/integration/window_rules_test.cpp b/autotests/integration/window_rules_test.cpp new file mode 100644 index 0000000..3e50d8d --- /dev/null +++ b/autotests/integration/window_rules_test.cpp @@ -0,0 +1,238 @@ +/* + 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 "platform.h" +#include "atoms.h" +#include "x11client.h" +#include "cursor.h" +#include "deleted.h" +#include "screenedge.h" +#include "screens.h" +#include "rules.h" +#include "wayland_server.h" +#include "workspace.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_data(); + void testApplyInitialMaximizeVert(); + void testWindowClassChange(); +}; + +void WindowRuleTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + setenv("QT_QPA_PLATFORM", "wayland", true); + waylandServer()->initWorkspace(); +} + +void WindowRuleTest::init() +{ + screens()->setCurrent(0); + Cursors::self()->mouse()->setPos(QPoint(640, 512)); + QVERIFY(waylandServer()->clients().isEmpty()); +} + +void WindowRuleTest::cleanup() +{ + // discards old rules + RuleBook::self()->load(); +} + +struct XcbConnectionDeleter +{ + static inline void cleanup(xcb_connection_t *pointer) + { + xcb_disconnect(pointer); + } +}; + +void WindowRuleTest::testApplyInitialMaximizeVert_data() +{ + QTest::addColumn("role"); + + QTest::newRow("lowercase") << QByteArrayLiteral("mainwindow"); + QTest::newRow("CamelCase") << QByteArrayLiteral("MainWindow"); +} + +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 + QFile ruleFile(QFINDTESTDATA("./data/rules/maximize-vert-apply-initial")); + QVERIFY(ruleFile.open(QIODevice::ReadOnly | QIODevice::Text)); + QMetaObject::invokeMethod(RuleBook::self(), "temporaryRulesMessage", Q_ARG(QString, QString::fromUtf8(ruleFile.readAll()))); + + // create the test window + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + + xcb_window_t w = xcb_generate_id(c.data()); + const QRect windowGeometry = QRect(0, 0, 10, 20); + const uint32_t values[] = { + XCB_EVENT_MASK_ENTER_WINDOW | + XCB_EVENT_MASK_LEAVE_WINDOW + }; + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, 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; + memset(&hints, 0, sizeof(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.data(), w, &hints); + xcb_icccm_set_wm_class(c.data(), w, 9, "kpat\0kpat"); + + QFETCH(QByteArray, role); + xcb_change_property(c.data(), XCB_PROP_MODE_REPLACE, w, atoms->wm_window_role, XCB_ATOM_STRING, 8, role.length(), role.constData()); + + NETWinInfo info(c.data(), w, rootWindow(), NET::WMAllProperties, NET::WM2AllProperties); + info.setWindowType(NET::Normal); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.last().first().value(); + QVERIFY(client); + QVERIFY(client->isDecorated()); + QVERIFY(!client->hasStrut()); + QVERIFY(!client->isHiddenInternal()); + QVERIFY(!client->readyForPainting()); + QMetaObject::invokeMethod(client, "setReadyForPainting"); + QVERIFY(client->readyForPainting()); + QVERIFY(!client->surface()); + QSignalSpy surfaceChangedSpy(client, &Toplevel::surfaceChanged); + QVERIFY(surfaceChangedSpy.isValid()); + QVERIFY(surfaceChangedSpy.wait()); + QVERIFY(client->surface()); + QCOMPARE(client->maximizeMode(), MaximizeVertical); + + // destroy window again + QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + xcb_unmap_window(c.data(), w); + xcb_destroy_window(c.data(), w); + xcb_flush(c.data()); + QVERIFY(windowClosedSpy.wait()); +} + +void WindowRuleTest::testWindowClassChange() +{ + KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + + auto group = config->group("1"); + group.writeEntry("above", true); + group.writeEntry("aboverule", 2); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", 1); + group.sync(); + + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // create the test window + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + + xcb_window_t w = xcb_generate_id(c.data()); + const QRect windowGeometry = QRect(0, 0, 10, 20); + const uint32_t values[] = { + XCB_EVENT_MASK_ENTER_WINDOW | + XCB_EVENT_MASK_LEAVE_WINDOW + }; + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, 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; + memset(&hints, 0, sizeof(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.data(), w, &hints); + xcb_icccm_set_wm_class(c.data(), w, 23, "org.kde.bar\0org.kde.bar"); + + NETWinInfo info(c.data(), w, rootWindow(), NET::WMAllProperties, NET::WM2AllProperties); + info.setWindowType(NET::Normal); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.last().first().value(); + QVERIFY(client); + QVERIFY(client->isDecorated()); + QVERIFY(!client->hasStrut()); + QVERIFY(!client->isHiddenInternal()); + QVERIFY(!client->readyForPainting()); + QMetaObject::invokeMethod(client, "setReadyForPainting"); + QVERIFY(client->readyForPainting()); + QVERIFY(!client->surface()); + QSignalSpy surfaceChangedSpy(client, &Toplevel::surfaceChanged); + QVERIFY(surfaceChangedSpy.isValid()); + QVERIFY(surfaceChangedSpy.wait()); + QVERIFY(client->surface()); + QCOMPARE(client->keepAbove(), false); + + // now change class + QSignalSpy windowClassChangedSpy{client, &X11Client::windowClassChanged}; + QVERIFY(windowClassChangedSpy.isValid()); + xcb_icccm_set_wm_class(c.data(), w, 23, "org.kde.foo\0org.kde.foo"); + xcb_flush(c.data()); + QVERIFY(windowClassChangedSpy.wait()); + QCOMPARE(client->keepAbove(), true); + + // destroy window + QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + xcb_unmap_window(c.data(), w); + xcb_destroy_window(c.data(), w); + xcb_flush(c.data()); + QVERIFY(windowClosedSpy.wait()); +} + +} + +WAYLANDTEST_MAIN(KWin::WindowRuleTest) +#include "window_rules_test.moc" diff --git a/autotests/integration/window_selection_test.cpp b/autotests/integration/window_selection_test.cpp new file mode 100644 index 0000000..91c3ac2 --- /dev/null +++ b/autotests/integration/window_selection_test.cpp @@ -0,0 +1,546 @@ +/* + 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 "abstract_client.h" +#include "cursor.h" +#include "keyboard_input.h" +#include "platform.h" +#include "pointer_input.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace KWin; +using namespace KWayland::Client; + +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(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + qputenv("XKB_DEFAULT_RULES", "evdev"); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + waylandServer()->initWorkspace(); +} + +void TestWindowSelection::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat)); + QVERIFY(Test::waitForWaylandPointer()); + + screens()->setCurrent(0); + KWin::Cursors::self()->mouse()->setPos(QPoint(1280, 512)); +} + +void TestWindowSelection::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void TestWindowSelection::testSelectOnWindowPointer() +{ + // this test verifies window selection through pointer works + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QScopedPointer pointer(Test::waylandSeat()->createPointer()); + QScopedPointer keyboard(Test::waylandSeat()->createKeyboard()); + QSignalSpy pointerEnteredSpy(pointer.data(), &Pointer::entered); + QVERIFY(pointerEnteredSpy.isValid()); + QSignalSpy pointerLeftSpy(pointer.data(), &Pointer::left); + QVERIFY(pointerLeftSpy.isValid()); + QSignalSpy keyboardEnteredSpy(keyboard.data(), &Keyboard::entered); + QVERIFY(keyboardEnteredSpy.isValid()); + QSignalSpy keyboardLeftSpy(keyboard.data(), &Keyboard::left); + QVERIFY(keyboardLeftSpy.isValid()); + + auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + QVERIFY(keyboardEnteredSpy.wait()); + KWin::Cursors::self()->mouse()->setPos(client->frameGeometry().center()); + QCOMPARE(input()->pointer()->focus(), client); + QVERIFY(pointerEnteredSpy.wait()); + + Toplevel *selectedWindow = nullptr; + auto callback = [&selectedWindow] (Toplevel *t) { + selectedWindow = t; + }; + + // start the interaction + QCOMPARE(input()->isSelectingWindow(), false); + kwinApp()->platform()->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; + kwinApp()->platform()->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 + kwinApp()->platform()->pointerButtonPressed(BTN_RIGHT, timestamp++); + kwinApp()->platform()->pointerButtonReleased(BTN_RIGHT, timestamp++); + // should not have ended the mode + QCOMPARE(input()->isSelectingWindow(), true); + QVERIFY(!selectedWindow); + // now release + kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); + QCOMPARE(input()->isSelectingWindow(), false); + QCOMPARE(selectedWindow, client); + QCOMPARE(input()->pointer()->focus(), client); + // 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 + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QScopedPointer pointer(Test::waylandSeat()->createPointer()); + QScopedPointer keyboard(Test::waylandSeat()->createKeyboard()); + QSignalSpy pointerEnteredSpy(pointer.data(), &Pointer::entered); + QVERIFY(pointerEnteredSpy.isValid()); + QSignalSpy pointerLeftSpy(pointer.data(), &Pointer::left); + QVERIFY(pointerLeftSpy.isValid()); + QSignalSpy keyboardEnteredSpy(keyboard.data(), &Keyboard::entered); + QVERIFY(keyboardEnteredSpy.isValid()); + QSignalSpy keyboardLeftSpy(keyboard.data(), &Keyboard::left); + QVERIFY(keyboardLeftSpy.isValid()); + + auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + QVERIFY(keyboardEnteredSpy.wait()); + QVERIFY(!client->frameGeometry().contains(KWin::Cursors::self()->mouse()->pos())); + + Toplevel *selectedWindow = nullptr; + auto callback = [&selectedWindow] (Toplevel *t) { + selectedWindow = t; + }; + + // start the interaction + QCOMPARE(input()->isSelectingWindow(), false); + kwinApp()->platform()->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) { + kwinApp()->platform()->keyboardKeyPressed(key, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(key, timestamp++); + }; + while (KWin::Cursors::self()->mouse()->pos().x() >= client->frameGeometry().x() + client->frameGeometry().width()) { + keyPress(KEY_LEFT); + } + while (KWin::Cursors::self()->mouse()->pos().x() <= client->frameGeometry().x()) { + keyPress(KEY_RIGHT); + } + while (KWin::Cursors::self()->mouse()->pos().y() <= client->frameGeometry().y()) { + keyPress(KEY_DOWN); + } + while (KWin::Cursors::self()->mouse()->pos().y() >= client->frameGeometry().y() + client->frameGeometry().height()) { + keyPress(KEY_UP); + } + QFETCH(qint32, key); + kwinApp()->platform()->keyboardKeyPressed(key, timestamp++); + QCOMPARE(input()->isSelectingWindow(), false); + QCOMPARE(selectedWindow, client); + QCOMPARE(input()->pointer()->focus(), client); + // 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); + kwinApp()->platform()->keyboardKeyReleased(key, timestamp++); +} + +void TestWindowSelection::testSelectOnWindowTouch() +{ + // this test verifies window selection through touch + QScopedPointer touch(Test::waylandSeat()->createTouch()); + QSignalSpy touchStartedSpy(touch.data(), &Touch::sequenceStarted); + QVERIFY(touchStartedSpy.isValid()); + QSignalSpy touchCanceledSpy(touch.data(), &Touch::sequenceCanceled); + QVERIFY(touchCanceledSpy.isValid()); + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + + Toplevel *selectedWindow = nullptr; + auto callback = [&selectedWindow] (Toplevel *t) { + selectedWindow = t; + }; + + // start the interaction + QCOMPARE(input()->isSelectingWindow(), false); + kwinApp()->platform()->startInteractiveWindowSelection(callback); + QCOMPARE(input()->isSelectingWindow(), true); + QVERIFY(!selectedWindow); + + // simulate touch down + quint32 timestamp = 0; + kwinApp()->platform()->touchDown(0, client->frameGeometry().center(), timestamp++); + QVERIFY(!selectedWindow); + kwinApp()->platform()->touchUp(0, timestamp++); + QCOMPARE(input()->isSelectingWindow(), false); + QCOMPARE(selectedWindow, client); + + // with movement + selectedWindow = nullptr; + kwinApp()->platform()->startInteractiveWindowSelection(callback); + kwinApp()->platform()->touchDown(0, client->frameGeometry().bottomRight() + QPoint(20, 20), timestamp++); + QVERIFY(!selectedWindow); + kwinApp()->platform()->touchMotion(0, client->frameGeometry().bottomRight() - QPoint(1, 1), timestamp++); + QVERIFY(!selectedWindow); + kwinApp()->platform()->touchUp(0, timestamp++); + QCOMPARE(selectedWindow, client); + QCOMPARE(input()->isSelectingWindow(), false); + + // it cancels active touch sequence on the window + kwinApp()->platform()->touchDown(0, client->frameGeometry().center(), timestamp++); + QVERIFY(touchStartedSpy.wait()); + selectedWindow = nullptr; + kwinApp()->platform()->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 + kwinApp()->platform()->touchUp(0, timestamp++); + QVERIFY(!selectedWindow); + kwinApp()->platform()->touchDown(0, client->frameGeometry().center(), timestamp++); + kwinApp()->platform()->touchUp(0, timestamp++); + QCOMPARE(selectedWindow, client); + 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 + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QScopedPointer pointer(Test::waylandSeat()->createPointer()); + QScopedPointer keyboard(Test::waylandSeat()->createKeyboard()); + QSignalSpy pointerEnteredSpy(pointer.data(), &Pointer::entered); + QVERIFY(pointerEnteredSpy.isValid()); + QSignalSpy pointerLeftSpy(pointer.data(), &Pointer::left); + QVERIFY(pointerLeftSpy.isValid()); + QSignalSpy keyboardEnteredSpy(keyboard.data(), &Keyboard::entered); + QVERIFY(keyboardEnteredSpy.isValid()); + QSignalSpy keyboardLeftSpy(keyboard.data(), &Keyboard::left); + QVERIFY(keyboardLeftSpy.isValid()); + + auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + QVERIFY(keyboardEnteredSpy.wait()); + KWin::Cursors::self()->mouse()->setPos(client->frameGeometry().center()); + QCOMPARE(input()->pointer()->focus(), client); + QVERIFY(pointerEnteredSpy.wait()); + + Toplevel *selectedWindow = nullptr; + auto callback = [&selectedWindow] (Toplevel *t) { + selectedWindow = t; + }; + + // start the interaction + QCOMPARE(input()->isSelectingWindow(), false); + kwinApp()->platform()->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; + kwinApp()->platform()->pointerButtonPressed(BTN_RIGHT, timestamp++); + kwinApp()->platform()->pointerButtonReleased(BTN_RIGHT, timestamp++); + QCOMPARE(input()->isSelectingWindow(), false); + QVERIFY(!selectedWindow); + QCOMPARE(input()->pointer()->focus(), client); + // 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 + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QScopedPointer pointer(Test::waylandSeat()->createPointer()); + QScopedPointer keyboard(Test::waylandSeat()->createKeyboard()); + QSignalSpy pointerEnteredSpy(pointer.data(), &Pointer::entered); + QVERIFY(pointerEnteredSpy.isValid()); + QSignalSpy pointerLeftSpy(pointer.data(), &Pointer::left); + QVERIFY(pointerLeftSpy.isValid()); + QSignalSpy keyboardEnteredSpy(keyboard.data(), &Keyboard::entered); + QVERIFY(keyboardEnteredSpy.isValid()); + QSignalSpy keyboardLeftSpy(keyboard.data(), &Keyboard::left); + QVERIFY(keyboardLeftSpy.isValid()); + + auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + QVERIFY(keyboardEnteredSpy.wait()); + KWin::Cursors::self()->mouse()->setPos(client->frameGeometry().center()); + QCOMPARE(input()->pointer()->focus(), client); + QVERIFY(pointerEnteredSpy.wait()); + + Toplevel *selectedWindow = nullptr; + auto callback = [&selectedWindow] (Toplevel *t) { + selectedWindow = t; + }; + + // start the interaction + QCOMPARE(input()->isSelectingWindow(), false); + kwinApp()->platform()->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; + kwinApp()->platform()->keyboardKeyPressed(KEY_ESC, timestamp++); + QCOMPARE(input()->isSelectingWindow(), false); + QVERIFY(!selectedWindow); + QCOMPARE(input()->pointer()->focus(), client); + // 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); + kwinApp()->platform()->keyboardKeyReleased(KEY_ESC, timestamp++); +} + +void TestWindowSelection::testSelectPointPointer() +{ + // this test verifies point selection through pointer works + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QScopedPointer pointer(Test::waylandSeat()->createPointer()); + QScopedPointer keyboard(Test::waylandSeat()->createKeyboard()); + QSignalSpy pointerEnteredSpy(pointer.data(), &Pointer::entered); + QVERIFY(pointerEnteredSpy.isValid()); + QSignalSpy pointerLeftSpy(pointer.data(), &Pointer::left); + QVERIFY(pointerLeftSpy.isValid()); + QSignalSpy keyboardEnteredSpy(keyboard.data(), &Keyboard::entered); + QVERIFY(keyboardEnteredSpy.isValid()); + QSignalSpy keyboardLeftSpy(keyboard.data(), &Keyboard::left); + QVERIFY(keyboardLeftSpy.isValid()); + + auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + QVERIFY(keyboardEnteredSpy.wait()); + KWin::Cursors::self()->mouse()->setPos(client->frameGeometry().center()); + QCOMPARE(input()->pointer()->focus(), client); + QVERIFY(pointerEnteredSpy.wait()); + + QPoint point; + auto callback = [&point] (const QPoint &p) { + point = p; + }; + + // start the interaction + QCOMPARE(input()->isSelectingWindow(), false); + kwinApp()->platform()->startInteractivePositionSelection(callback); + 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 + QPoint point2; + kwinApp()->platform()->startInteractivePositionSelection([&point2] (const QPoint &p) { + point2 = p; + }); + QCOMPARE(point2, QPoint(-1, -1)); + + // simulate left button press + quint32 timestamp = 0; + kwinApp()->platform()->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 + kwinApp()->platform()->pointerButtonPressed(BTN_RIGHT, timestamp++); + kwinApp()->platform()->pointerButtonReleased(BTN_RIGHT, timestamp++); + // should not have ended the mode + QCOMPARE(input()->isSelectingWindow(), true); + QCOMPARE(point, QPoint()); + // now release + kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); + QCOMPARE(input()->isSelectingWindow(), false); + QCOMPARE(point, input()->globalPointer().toPoint()); + QCOMPARE(input()->pointer()->focus(), client); + // 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 + QPoint point; + auto callback = [&point] (const QPoint &p) { + point = p; + }; + + // start the interaction + QCOMPARE(input()->isSelectingWindow(), false); + kwinApp()->platform()->startInteractivePositionSelection(callback); + QCOMPARE(input()->isSelectingWindow(), true); + QCOMPARE(point, QPoint()); + + // let's create multiple touch points + quint32 timestamp = 0; + kwinApp()->platform()->touchDown(0, QPointF(0, 1), timestamp++); + QCOMPARE(input()->isSelectingWindow(), true); + kwinApp()->platform()->touchDown(1, QPointF(10, 20), timestamp++); + QCOMPARE(input()->isSelectingWindow(), true); + kwinApp()->platform()->touchDown(2, QPointF(30, 40), timestamp++); + QCOMPARE(input()->isSelectingWindow(), true); + + // let's move our points + kwinApp()->platform()->touchMotion(0, QPointF(5, 10), timestamp++); + kwinApp()->platform()->touchMotion(2, QPointF(20, 25), timestamp++); + kwinApp()->platform()->touchMotion(1, QPointF(25, 35), timestamp++); + QCOMPARE(input()->isSelectingWindow(), true); + kwinApp()->platform()->touchUp(0, timestamp++); + QCOMPARE(input()->isSelectingWindow(), true); + kwinApp()->platform()->touchUp(2, timestamp++); + QCOMPARE(input()->isSelectingWindow(), true); + kwinApp()->platform()->touchUp(1, timestamp++); + QCOMPARE(input()->isSelectingWindow(), false); + QCOMPARE(point, QPoint(25, 35)); +} + +WAYLANDTEST_MAIN(TestWindowSelection) +#include "window_selection_test.moc" diff --git a/autotests/integration/x11_client_test.cpp b/autotests/integration/x11_client_test.cpp new file mode 100644 index 0000000..ee79221 --- /dev/null +++ b/autotests/integration/x11_client_test.cpp @@ -0,0 +1,1111 @@ +/* + 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 "x11client.h" +#include "composite.h" +#include "effects.h" +#include "effectloader.h" +#include "cursor.h" +#include "deleted.h" +#include "platform.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" + +#include + +#include +#include + +using namespace KWin; +using namespace KWayland::Client; +static const QString s_socketName = QStringLiteral("wayland_test_x11_client-0"); + +class X11ClientTest : public QObject +{ +Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testMinimumSize(); + void testMaximumSize(); + void testResizeIncrements(); + void testResizeIncrementsNoBaseSize(); + void testTrimCaption_data(); + void testTrimCaption(); + void testFullscreenLayerWithActiveWaylandWindow(); + void testFocusInWithWaylandLastActiveWindow(); + void testX11WindowId(); + void testCaptionChanges(); + void testCaptionWmName(); + void testCaptionMultipleWindows(); + void testFullscreenWindowGroups(); + void testActivateFocusedWindow(); + void testReentrantSetFrameGeometry(); +}; + +void X11ClientTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QVERIFY(KWin::Compositor::self()); + waylandServer()->initWorkspace(); +} + +void X11ClientTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); +} + +void X11ClientTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +struct XcbConnectionDeleter +{ + static inline void cleanup(xcb_connection_t *pointer) + { + xcb_disconnect(pointer); + } +}; + +void X11ClientTest::testMinimumSize() +{ + // This test verifies that the minimum size constraint is correctly applied. + + // Create an xcb window. + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + const QRect windowGeometry(0, 0, 100, 200); + xcb_window_t w = xcb_generate_id(c.data()); + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, 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; + memset(&hints, 0, sizeof(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.data(), w, &hints); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.last().first().value(); + QVERIFY(client); + QVERIFY(client->isDecorated()); + + QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); + QVERIFY(clientStartMoveResizedSpy.isValid()); + QSignalSpy clientStepUserMovedResizedSpy(client, &AbstractClient::clientStepUserMovedResized); + QVERIFY(clientStepUserMovedResizedSpy.isValid()); + QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); + QVERIFY(clientFinishUserMovedResizedSpy.isValid()); + + // Begin resize. + QCOMPARE(workspace()->moveResizeClient(), nullptr); + QVERIFY(!client->isResize()); + workspace()->slotWindowResize(); + QCOMPARE(workspace()->moveResizeClient(), client); + QCOMPARE(clientStartMoveResizedSpy.count(), 1); + QVERIFY(client->isResize()); + + const QPoint cursorPos = KWin::Cursors::self()->mouse()->pos(); + + client->keyPressEvent(Qt::Key_Left); + client->updateMoveResize(KWin::Cursors::self()->mouse()->pos()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(-8, 0)); + QVERIFY(!clientStepUserMovedResizedSpy.wait(1000)); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 0); + QCOMPARE(client->clientSize().width(), 100); + + client->keyPressEvent(Qt::Key_Right); + client->updateMoveResize(KWin::Cursors::self()->mouse()->pos()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos); + QVERIFY(!clientStepUserMovedResizedSpy.wait(1000)); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 0); + QCOMPARE(client->clientSize().width(), 100); + + client->keyPressEvent(Qt::Key_Right); + client->updateMoveResize(KWin::Cursors::self()->mouse()->pos()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0)); + QVERIFY(clientStepUserMovedResizedSpy.wait()); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); + QCOMPARE(client->clientSize().width(), 108); + + client->keyPressEvent(Qt::Key_Up); + client->updateMoveResize(KWin::Cursors::self()->mouse()->pos()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, -8)); + QVERIFY(!clientStepUserMovedResizedSpy.wait(1000)); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); + QCOMPARE(client->clientSize().height(), 200); + + client->keyPressEvent(Qt::Key_Down); + client->updateMoveResize(KWin::Cursors::self()->mouse()->pos()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0)); + QVERIFY(!clientStepUserMovedResizedSpy.wait(1000)); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); + QCOMPARE(client->clientSize().height(), 200); + + client->keyPressEvent(Qt::Key_Down); + client->updateMoveResize(KWin::Cursors::self()->mouse()->pos()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 8)); + QVERIFY(clientStepUserMovedResizedSpy.wait()); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 2); + QCOMPARE(client->clientSize().height(), 208); + + // Finish the resize operation. + QCOMPARE(clientFinishUserMovedResizedSpy.count(), 0); + client->keyPressEvent(Qt::Key_Enter); + QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); + QCOMPARE(workspace()->moveResizeClient(), nullptr); + QVERIFY(!client->isResize()); + + // Destroy the window. + QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + xcb_unmap_window(c.data(), w); + xcb_destroy_window(c.data(), w); + xcb_flush(c.data()); + QVERIFY(windowClosedSpy.wait()); + c.reset(); +} + +void X11ClientTest::testMaximumSize() +{ + // This test verifies that the maximum size constraint is correctly applied. + + // Create an xcb window. + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + const QRect windowGeometry(0, 0, 100, 200); + xcb_window_t w = xcb_generate_id(c.data()); + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, 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; + memset(&hints, 0, sizeof(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.data(), w, &hints); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.last().first().value(); + QVERIFY(client); + QVERIFY(client->isDecorated()); + + QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); + QVERIFY(clientStartMoveResizedSpy.isValid()); + QSignalSpy clientStepUserMovedResizedSpy(client, &AbstractClient::clientStepUserMovedResized); + QVERIFY(clientStepUserMovedResizedSpy.isValid()); + QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); + QVERIFY(clientFinishUserMovedResizedSpy.isValid()); + + // Begin resize. + QCOMPARE(workspace()->moveResizeClient(), nullptr); + QVERIFY(!client->isResize()); + workspace()->slotWindowResize(); + QCOMPARE(workspace()->moveResizeClient(), client); + QCOMPARE(clientStartMoveResizedSpy.count(), 1); + QVERIFY(client->isResize()); + + const QPoint cursorPos = KWin::Cursors::self()->mouse()->pos(); + + client->keyPressEvent(Qt::Key_Right); + client->updateMoveResize(KWin::Cursors::self()->mouse()->pos()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0)); + QVERIFY(!clientStepUserMovedResizedSpy.wait(1000)); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 0); + QCOMPARE(client->clientSize().width(), 100); + + client->keyPressEvent(Qt::Key_Left); + client->updateMoveResize(KWin::Cursors::self()->mouse()->pos()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos); + QVERIFY(!clientStepUserMovedResizedSpy.wait(1000)); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 0); + QCOMPARE(client->clientSize().width(), 100); + + client->keyPressEvent(Qt::Key_Left); + client->updateMoveResize(KWin::Cursors::self()->mouse()->pos()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(-8, 0)); + QVERIFY(clientStepUserMovedResizedSpy.wait()); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); + QCOMPARE(client->clientSize().width(), 92); + + client->keyPressEvent(Qt::Key_Down); + client->updateMoveResize(KWin::Cursors::self()->mouse()->pos()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(-8, 8)); + QVERIFY(!clientStepUserMovedResizedSpy.wait(1000)); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); + QCOMPARE(client->clientSize().height(), 200); + + client->keyPressEvent(Qt::Key_Up); + client->updateMoveResize(KWin::Cursors::self()->mouse()->pos()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(-8, 0)); + QVERIFY(!clientStepUserMovedResizedSpy.wait(1000)); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); + QCOMPARE(client->clientSize().height(), 200); + + client->keyPressEvent(Qt::Key_Up); + client->updateMoveResize(KWin::Cursors::self()->mouse()->pos()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(-8, -8)); + QVERIFY(clientStepUserMovedResizedSpy.wait()); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 2); + QCOMPARE(client->clientSize().height(), 192); + + // Finish the resize operation. + QCOMPARE(clientFinishUserMovedResizedSpy.count(), 0); + client->keyPressEvent(Qt::Key_Enter); + QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); + QCOMPARE(workspace()->moveResizeClient(), nullptr); + QVERIFY(!client->isResize()); + + // Destroy the window. + QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + xcb_unmap_window(c.data(), w); + xcb_destroy_window(c.data(), w); + xcb_flush(c.data()); + QVERIFY(windowClosedSpy.wait()); + c.reset(); +} + +void X11ClientTest::testResizeIncrements() +{ + // This test verifies that the resize increments constraint is correctly applied. + + // Create an xcb window. + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + const QRect windowGeometry(0, 0, 100, 200); + xcb_window_t w = xcb_generate_id(c.data()); + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, 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; + memset(&hints, 0, sizeof(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_base_size(&hints, windowGeometry.width(), windowGeometry.height()); + xcb_icccm_size_hints_set_resize_inc(&hints, 3, 5); + xcb_icccm_set_wm_normal_hints(c.data(), w, &hints); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.last().first().value(); + QVERIFY(client); + QVERIFY(client->isDecorated()); + + QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); + QVERIFY(clientStartMoveResizedSpy.isValid()); + QSignalSpy clientStepUserMovedResizedSpy(client, &AbstractClient::clientStepUserMovedResized); + QVERIFY(clientStepUserMovedResizedSpy.isValid()); + QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); + QVERIFY(clientFinishUserMovedResizedSpy.isValid()); + + // Begin resize. + QCOMPARE(workspace()->moveResizeClient(), nullptr); + QVERIFY(!client->isResize()); + workspace()->slotWindowResize(); + QCOMPARE(workspace()->moveResizeClient(), client); + QCOMPARE(clientStartMoveResizedSpy.count(), 1); + QVERIFY(client->isResize()); + + const QPoint cursorPos = KWin::Cursors::self()->mouse()->pos(); + + client->keyPressEvent(Qt::Key_Right); + client->updateMoveResize(KWin::Cursors::self()->mouse()->pos()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0)); + QVERIFY(clientStepUserMovedResizedSpy.wait()); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); + QCOMPARE(client->clientSize(), QSize(106, 200)); + + client->keyPressEvent(Qt::Key_Down); + client->updateMoveResize(KWin::Cursors::self()->mouse()->pos()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 8)); + QVERIFY(clientStepUserMovedResizedSpy.wait()); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 2); + QCOMPARE(client->clientSize(), QSize(106, 205)); + + // Finish the resize operation. + QCOMPARE(clientFinishUserMovedResizedSpy.count(), 0); + client->keyPressEvent(Qt::Key_Enter); + QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); + QCOMPARE(workspace()->moveResizeClient(), nullptr); + QVERIFY(!client->isResize()); + + // Destroy the window. + QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + xcb_unmap_window(c.data(), w); + xcb_destroy_window(c.data(), w); + xcb_flush(c.data()); + QVERIFY(windowClosedSpy.wait()); + c.reset(); +} + +void X11ClientTest::testResizeIncrementsNoBaseSize() +{ + // Create an xcb window. + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + const QRect windowGeometry(0, 0, 100, 200); + xcb_window_t w = xcb_generate_id(c.data()); + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, 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; + memset(&hints, 0, sizeof(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_size_hints_set_resize_inc(&hints, 3, 5); + xcb_icccm_set_wm_normal_hints(c.data(), w, &hints); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.last().first().value(); + QVERIFY(client); + QVERIFY(client->isDecorated()); + + QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); + QVERIFY(clientStartMoveResizedSpy.isValid()); + QSignalSpy clientStepUserMovedResizedSpy(client, &AbstractClient::clientStepUserMovedResized); + QVERIFY(clientStepUserMovedResizedSpy.isValid()); + QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); + QVERIFY(clientFinishUserMovedResizedSpy.isValid()); + + // Begin resize. + QCOMPARE(workspace()->moveResizeClient(), nullptr); + QVERIFY(!client->isResize()); + workspace()->slotWindowResize(); + QCOMPARE(workspace()->moveResizeClient(), client); + QCOMPARE(clientStartMoveResizedSpy.count(), 1); + QVERIFY(client->isResize()); + + const QPoint cursorPos = KWin::Cursors::self()->mouse()->pos(); + + client->keyPressEvent(Qt::Key_Right); + client->updateMoveResize(KWin::Cursors::self()->mouse()->pos()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0)); + QVERIFY(clientStepUserMovedResizedSpy.wait()); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); + QCOMPARE(client->clientSize(), QSize(106, 200)); + + client->keyPressEvent(Qt::Key_Down); + client->updateMoveResize(KWin::Cursors::self()->mouse()->pos()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 8)); + QVERIFY(clientStepUserMovedResizedSpy.wait()); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 2); + QCOMPARE(client->clientSize(), QSize(106, 205)); + + // Finish the resize operation. + QCOMPARE(clientFinishUserMovedResizedSpy.count(), 0); + client->keyPressEvent(Qt::Key_Enter); + QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); + QCOMPARE(workspace()->moveResizeClient(), nullptr); + QVERIFY(!client->isResize()); + + // Destroy the window. + QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + xcb_unmap_window(c.data(), w); + xcb_destroy_window(c.data(), w); + xcb_flush(c.data()); + QVERIFY(windowClosedSpy.wait()); + c.reset(); +} + +void X11ClientTest::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 X11ClientTest::testTrimCaption() +{ + // this test verifies that caption is properly trimmed + + // create an xcb window + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + const QRect windowGeometry(0, 0, 100, 200); + xcb_window_t w = xcb_generate_id(c.data()); + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, 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; + memset(&hints, 0, sizeof(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.data(), w, &hints); + NETWinInfo winInfo(c.data(), w, rootWindow(), NET::Properties(), NET::Properties2()); + QFETCH(QByteArray, originalTitle); + winInfo.setName(originalTitle); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + // we should get a client for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->window(), w); + QFETCH(QByteArray, expectedTitle); + QCOMPARE(client->caption(), QString::fromUtf8(expectedTitle)); + + // and destroy the window again + xcb_unmap_window(c.data(), w); + xcb_flush(c.data()); + + QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + QVERIFY(windowClosedSpy.wait()); + xcb_destroy_window(c.data(), w); + c.reset(); +} + +void X11ClientTest::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 + QCOMPARE(screens()->count(), 1); + + // first create an X11 window + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + const QRect windowGeometry(0, 0, 100, 200); + xcb_window_t w = xcb_generate_id(c.data()); + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, 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; + memset(&hints, 0, sizeof(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.data(), w, &hints); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + // we should get a client for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->window(), w); + QVERIFY(!client->isFullScreen()); + QVERIFY(client->isActive()); + QCOMPARE(client->layer(), NormalLayer); + + workspace()->slotWindowFullScreen(); + QVERIFY(client->isFullScreen()); + QCOMPARE(client->layer(), ActiveLayer); + QCOMPARE(workspace()->stackingOrder().last(), client); + + // now let's open a Wayland window + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + auto waylandClient = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(waylandClient); + QVERIFY(waylandClient->isActive()); + QCOMPARE(waylandClient->layer(), NormalLayer); + QCOMPARE(workspace()->stackingOrder().last(), waylandClient); + QCOMPARE(workspace()->xStackingOrder().last(), waylandClient); + QCOMPARE(client->layer(), NormalLayer); + + // now activate fullscreen again + workspace()->activateClient(client); + QTRY_VERIFY(client->isActive()); + QCOMPARE(client->layer(), ActiveLayer); + QCOMPARE(workspace()->stackingOrder().last(), client); + QCOMPARE(workspace()->xStackingOrder().last(), client); + + // activate wayland window again + workspace()->activateClient(waylandClient); + QTRY_VERIFY(waylandClient->isActive()); + QCOMPARE(workspace()->stackingOrder().last(), waylandClient); + QCOMPARE(workspace()->xStackingOrder().last(), waylandClient); + + // back to x window + workspace()->activateClient(client); + QTRY_VERIFY(client->isActive()); + // remove fullscreen + QVERIFY(client->isFullScreen()); + workspace()->slotWindowFullScreen(); + QVERIFY(!client->isFullScreen()); + // and fullscreen again + workspace()->slotWindowFullScreen(); + QVERIFY(client->isFullScreen()); + QCOMPARE(workspace()->stackingOrder().last(), client); + QCOMPARE(workspace()->xStackingOrder().last(), client); + + // activate wayland window again + workspace()->activateClient(waylandClient); + QTRY_VERIFY(waylandClient->isActive()); + QCOMPARE(workspace()->stackingOrder().last(), waylandClient); + QCOMPARE(workspace()->xStackingOrder().last(), waylandClient); + + // back to X11 window + workspace()->activateClient(client); + QTRY_VERIFY(client->isActive()); + // remove fullscreen + QVERIFY(client->isFullScreen()); + workspace()->slotWindowFullScreen(); + QVERIFY(!client->isFullScreen()); + // and fullscreen through X API + NETWinInfo info(c.data(), w, kwinApp()->x11RootWindow(), NET::Properties(), NET::Properties2()); + info.setState(NET::FullScreen, NET::FullScreen); + NETRootInfo rootInfo(c.data(), NET::Properties()); + rootInfo.setActiveWindow(w, NET::FromApplication, XCB_CURRENT_TIME, XCB_WINDOW_NONE); + xcb_flush(c.data()); + QTRY_VERIFY(client->isFullScreen()); + QCOMPARE(workspace()->stackingOrder().last(), client); + QCOMPARE(workspace()->xStackingOrder().last(), client); + + // activate wayland window again + workspace()->activateClient(waylandClient); + QTRY_VERIFY(waylandClient->isActive()); + QCOMPARE(workspace()->stackingOrder().last(), waylandClient); + QCOMPARE(workspace()->xStackingOrder().last(), waylandClient); + QCOMPARE(client->layer(), NormalLayer); + + // close the window + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(waylandClient)); + QTRY_VERIFY(client->isActive()); + QCOMPARE(client->layer(), ActiveLayer); + + // and destroy the window again + xcb_unmap_window(c.data(), w); + xcb_flush(c.data()); +} + +void X11ClientTest::testFocusInWithWaylandLastActiveWindow() +{ + // this test verifies that Workspace::allowClientActivation does not crash if last client was a Wayland client + + // create an X11 window + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + const QRect windowGeometry(0, 0, 100, 200); + xcb_window_t w = xcb_generate_id(c.data()); + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, 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; + memset(&hints, 0, sizeof(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.data(), w, &hints); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + // we should get a client for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->window(), w); + QVERIFY(client->isActive()); + + // create Wayland window + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + auto waylandClient = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(waylandClient); + QVERIFY(waylandClient->isActive()); + // activate no window + workspace()->setActiveClient(nullptr); + QVERIFY(!waylandClient->isActive()); + QVERIFY(!workspace()->activeClient()); + // and close Wayland window again + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(waylandClient)); + + // and try to activate the x11 client through X11 api + const auto cookie = xcb_set_input_focus_checked(c.data(), XCB_INPUT_FOCUS_NONE, w, XCB_CURRENT_TIME); + auto error = xcb_request_check(c.data(), cookie); + QVERIFY(!error); + // this accesses last_active_client on trying to activate + QTRY_VERIFY(client->isActive()); + + // and destroy the window again + xcb_unmap_window(c.data(), w); + xcb_flush(c.data()); +} + +void X11ClientTest::testX11WindowId() +{ + // create an X11 window + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + const QRect windowGeometry(0, 0, 100, 200); + xcb_window_t w = xcb_generate_id(c.data()); + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, 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; + memset(&hints, 0, sizeof(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.data(), w, &hints); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + // we should get a client for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->windowId(), w); + QVERIFY(client->isActive()); + QCOMPARE(client->window(), w); + QCOMPARE(client->internalId().isNull(), false); + const auto uuid = client->internalId(); + QUuid deletedUuid; + QCOMPARE(deletedUuid.isNull(), true); + + connect(client, &X11Client::windowClosed, this, [&deletedUuid] (Toplevel *, Deleted *d) { deletedUuid = d->internalId(); }); + + + NETRootInfo rootInfo(c.data(), NET::WMAllProperties); + QCOMPARE(rootInfo.activeWindow(), client->window()); + + // activate a wayland window + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + auto waylandClient = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(waylandClient); + QVERIFY(waylandClient->isActive()); + xcb_flush(kwinApp()->x11Connection()); + + NETRootInfo rootInfo2(c.data(), NET::WMAllProperties); + QCOMPARE(rootInfo2.activeWindow(), 0u); + + // back to X11 client + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(waylandClient)); + + QTRY_VERIFY(client->isActive()); + NETRootInfo rootInfo3(c.data(), NET::WMAllProperties); + QCOMPARE(rootInfo3.activeWindow(), client->window()); + + // and destroy the window again + xcb_unmap_window(c.data(), w); + xcb_flush(c.data()); + QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + QVERIFY(windowClosedSpy.wait()); + + QCOMPARE(deletedUuid.isNull(), false); + QCOMPARE(deletedUuid, uuid); +} + +void X11ClientTest::testCaptionChanges() +{ + // verifies that caption is updated correctly when the X11 window updates it + // BUG: 383444 + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + const QRect windowGeometry(0, 0, 100, 200); + xcb_window_t w = xcb_generate_id(c.data()); + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, 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; + memset(&hints, 0, sizeof(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.data(), w, &hints); + NETWinInfo info(c.data(), w, kwinApp()->x11RootWindow(), NET::Properties(), NET::Properties2()); + info.setName("foo"); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + // we should get a client for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->windowId(), w); + QCOMPARE(client->caption(), QStringLiteral("foo")); + + QSignalSpy captionChangedSpy(client, &X11Client::captionChanged); + QVERIFY(captionChangedSpy.isValid()); + info.setName("bar"); + xcb_flush(c.data()); + QVERIFY(captionChangedSpy.wait()); + QCOMPARE(client->caption(), QStringLiteral("bar")); + + // and destroy the window again + QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + xcb_unmap_window(c.data(), w); + xcb_flush(c.data()); + QVERIFY(windowClosedSpy.wait()); + xcb_destroy_window(c.data(), w); + c.reset(); +} + +void X11ClientTest::testCaptionWmName() +{ + // this test verifies that a caption set through WM_NAME is read correctly + + // open glxgears as that one only uses WM_NAME + QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(clientAddedSpy.isValid()); + + QProcess glxgears; + glxgears.setProgram(QStringLiteral("glxgears")); + glxgears.start(); + QVERIFY(glxgears.waitForStarted()); + + QVERIFY(clientAddedSpy.wait()); + QCOMPARE(clientAddedSpy.count(), 1); + QCOMPARE(workspace()->clientList().count(), 1); + X11Client *glxgearsClient = workspace()->clientList().first(); + QCOMPARE(glxgearsClient->caption(), QStringLiteral("glxgears")); + + glxgears.terminate(); + QVERIFY(glxgears.waitForFinished()); +} + +void X11ClientTest::testCaptionMultipleWindows() +{ + // BUG 384760 + // create first window + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + const QRect windowGeometry(0, 0, 100, 200); + xcb_window_t w = xcb_generate_id(c.data()); + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, 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; + memset(&hints, 0, sizeof(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.data(), w, &hints); + NETWinInfo info(c.data(), w, kwinApp()->x11RootWindow(), NET::Properties(), NET::Properties2()); + info.setName("foo"); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->windowId(), w); + QCOMPARE(client->caption(), QStringLiteral("foo")); + + // create second window with same caption + xcb_window_t w2 = xcb_generate_id(c.data()); + xcb_create_window(c.data(), 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_icccm_set_wm_normal_hints(c.data(), w2, &hints); + NETWinInfo info2(c.data(), w2, kwinApp()->x11RootWindow(), NET::Properties(), NET::Properties2()); + info2.setName("foo"); + info2.setIconName("foo"); + xcb_map_window(c.data(), w2); + xcb_flush(c.data()); + + windowCreatedSpy.clear(); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client2 = windowCreatedSpy.first().first().value(); + QVERIFY(client2); + QCOMPARE(client2->windowId(), w2); + QCOMPARE(client2->caption(), QStringLiteral("foo <2>\u200E")); + NETWinInfo info3(kwinApp()->x11Connection(), w2, kwinApp()->x11RootWindow(), NET::WMVisibleName | NET::WMVisibleIconName, NET::Properties2()); + QCOMPARE(QByteArray(info3.visibleName()), QByteArrayLiteral("foo <2>\u200E")); + QCOMPARE(QByteArray(info3.visibleIconName()), QByteArrayLiteral("foo <2>\u200E")); + + QSignalSpy captionChangedSpy(client2, &X11Client::captionChanged); + QVERIFY(captionChangedSpy.isValid()); + + NETWinInfo info4(c.data(), w2, kwinApp()->x11RootWindow(), NET::Properties(), NET::Properties2()); + info4.setName("foobar"); + info4.setIconName("foobar"); + xcb_map_window(c.data(), w2); + xcb_flush(c.data()); + + QVERIFY(captionChangedSpy.wait()); + QCOMPARE(client2->caption(), QStringLiteral("foobar")); + NETWinInfo info5(kwinApp()->x11Connection(), w2, kwinApp()->x11RootWindow(), NET::WMVisibleName | NET::WMVisibleIconName, NET::Properties2()); + QCOMPARE(QByteArray(info5.visibleName()), QByteArray()); + QTRY_COMPARE(QByteArray(info5.visibleIconName()), QByteArray()); +} + + +void X11ClientTest::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 + + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + const QRect windowGeometry(0, 0, 100, 200); + xcb_window_t w = xcb_generate_id(c.data()); + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, 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; + memset(&hints, 0, sizeof(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.data(), w, &hints); + xcb_change_property(c.data(), XCB_PROP_MODE_REPLACE, w, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &w); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->windowId(), w); + QCOMPARE(client->isActive(), true); + + QCOMPARE(client->isFullScreen(), false); + QCOMPARE(client->layer(), NormalLayer); + workspace()->slotWindowFullScreen(); + QCOMPARE(client->isFullScreen(), true); + QCOMPARE(client->layer(), ActiveLayer); + + // now let's create a second window + windowCreatedSpy.clear(); + xcb_window_t w2 = xcb_generate_id(c.data()); + xcb_create_window(c.data(), 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; + memset(&hints2, 0, sizeof(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.data(), w2, &hints2); + xcb_change_property(c.data(), XCB_PROP_MODE_REPLACE, w2, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &w); + xcb_map_window(c.data(), w2); + xcb_flush(c.data()); + + QVERIFY(windowCreatedSpy.wait()); + X11Client *client2 = windowCreatedSpy.first().first().value(); + QVERIFY(client2); + QVERIFY(client != client2); + QCOMPARE(client2->windowId(), w2); + QCOMPARE(client2->isActive(), true); + QCOMPARE(client2->group(), client->group()); + // first client should be moved back to normal layer + QCOMPARE(client->isActive(), false); + QCOMPARE(client->isFullScreen(), true); + QCOMPARE(client->layer(), NormalLayer); + + // activating the fullscreen window again, should move it to active layer + workspace()->activateClient(client); + QTRY_COMPARE(client->layer(), ActiveLayer); +} + +void X11ClientTest::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. + + QScopedPointer connection(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(connection.data())); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + + const QRect windowGeometry(0, 0, 100, 200); + xcb_size_hints_t hints; + memset(&hints, 0, sizeof(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 window1 = xcb_generate_id(connection.data()); + xcb_create_window(connection.data(), XCB_COPY_FROM_PARENT, window1, 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.data(), window1, &hints); + xcb_change_property(connection.data(), XCB_PROP_MODE_REPLACE, window1, + atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &window1); + xcb_map_window(connection.data(), window1); + xcb_flush(connection.data()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client1 = windowCreatedSpy.first().first().value(); + QVERIFY(client1); + QCOMPARE(client1->windowId(), window1); + QCOMPARE(client1->isActive(), true); + + // Create the second test window. + const xcb_window_t window2 = xcb_generate_id(connection.data()); + xcb_create_window(connection.data(), XCB_COPY_FROM_PARENT, window2, 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.data(), window2, &hints); + xcb_change_property(connection.data(), XCB_PROP_MODE_REPLACE, window2, + atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &window2); + xcb_map_window(connection.data(), window2); + xcb_flush(connection.data()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client2 = windowCreatedSpy.last().first().value(); + QVERIFY(client2); + QCOMPARE(client2->windowId(), window2); + QCOMPARE(client2->isActive(), true); + + // When the second test window is destroyed, the window manager will attempt to activate the + // next client in the focus chain, which is the first window. + xcb_set_input_focus(connection.data(), XCB_INPUT_FOCUS_POINTER_ROOT, window1, XCB_CURRENT_TIME); + xcb_destroy_window(connection.data(), window2); + xcb_flush(connection.data()); + QVERIFY(Test::waitForWindowDestroyed(client2)); + QVERIFY(client1->isActive()); + + // Destroy the first test window. + xcb_destroy_window(connection.data(), window1); + xcb_flush(connection.data()); + QVERIFY(Test::waitForWindowDestroyed(client1)); +} + +void X11ClientTest::testReentrantSetFrameGeometry() +{ + // This test verifies that calling setFrameGeometry() from a slot connected directly + // to the frameGeometryChanged() signal won't cause an infinite recursion. + + // Create a test window. + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + const QRect windowGeometry(0, 0, 100, 200); + xcb_window_t w = xcb_generate_id(c.data()); + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, 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; + memset(&hints, 0, sizeof(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.data(), w, &hints); + xcb_change_property(c.data(), XCB_PROP_MODE_REPLACE, w, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &w); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->pos(), QPoint(0, 0)); + + // Let's pretend that there is a script that really wants the client to be at (100, 100). + connect(client, &AbstractClient::frameGeometryChanged, this, [client]() { + client->setFrameGeometry(QRect(QPoint(100, 100), client->size())); + }); + + // Trigger the lambda above. + client->move(QPoint(40, 50)); + + // Eventually, the client will end up at (100, 100). + QCOMPARE(client->pos(), QPoint(100, 100)); + + // Destroy the test window. + xcb_destroy_window(c.data(), w); + xcb_flush(c.data()); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +WAYLANDTEST_MAIN(X11ClientTest) +#include "x11_client_test.moc" diff --git a/autotests/integration/xdgshellclient_rules_test.cpp b/autotests/integration/xdgshellclient_rules_test.cpp new file mode 100644 index 0000000..be331f5 --- /dev/null +++ b/autotests/integration/xdgshellclient_rules_test.cpp @@ -0,0 +1,4216 @@ +/* + 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-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwin_wayland_test.h" + +#include "abstract_client.h" +#include "cursor.h" +#include "platform.h" +#include "rules.h" +#include "screens.h" +#include "virtualdesktops.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include + +#include + +using namespace KWin; +using namespace KWayland::Client; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_xdgshellclient_rules-0"); + +class TestXdgShellClientRules : public QObject +{ + Q_OBJECT + +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 testDesktopDontAffect(); + void testDesktopApply(); + void testDesktopRemember(); + void testDesktopForce(); + void testDesktopApplyNow(); + void testDesktopForceTemporarily(); + + 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 testMatchAfterNameChange(); +}; + +void TestXdgShellClientRules::initTestCase() +{ + qRegisterMetaType(); + + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + waylandServer()->initWorkspace(); +} + +void TestXdgShellClientRules::init() +{ + VirtualDesktopManager::self()->setCurrent(VirtualDesktopManager::self()->desktops().first()); + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Decoration)); + + screens()->setCurrent(0); +} + +void TestXdgShellClientRules::cleanup() +{ + Test::destroyWaylandConnection(); + + // Unreference the previous config. + RuleBook::self()->setConfig({}); + workspace()->slotReconfigure(); + + // Restore virtual desktops to the initial state. + VirtualDesktopManager::self()->setCount(1); + QCOMPARE(VirtualDesktopManager::self()->count(), 1u); +} + +std::tuple createWindow(const QByteArray &appId) +{ + // Create an xdg surface. + Surface *surface = Test::createSurface(); + XdgShellSurface *shellSurface = Test::createXdgShellStableSurface(surface, surface, Test::CreationSetup::CreateOnly); + + // Assign the desired app id. + shellSurface->setAppId(appId); + + // Wait for the initial configure event. + QSignalSpy configureRequestedSpy(shellSurface, &XdgShellSurface::configureRequested); + surface->commit(Surface::CommitFlag::None); + configureRequestedSpy.wait(); + + // Draw content of the surface. + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + AbstractClient *client = Test::renderAndWaitForShown(surface, QSize(100, 50), Qt::blue); + + return {client, surface, shellSurface}; +} + +void TestXdgShellClientRules::testPositionDontAffect() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("position", QPoint(42, 42)); + group.writeEntry("positionrule", int(Rules::DontAffect)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(client->isActive()); + + // The position of the client should not be affected by the rule. The default + // placement policy will put the client in the top-left corner of the screen. + QVERIFY(client->isMovable()); + QVERIFY(client->isMovableAcrossScreens()); + QCOMPARE(client->pos(), QPoint(0, 0)); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testPositionApply() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("position", QPoint(42, 42)); + group.writeEntry("positionrule", int(Rules::Apply)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(client->isActive()); + + // The client should be moved to the position specified by the rule. + QVERIFY(client->isMovable()); + QVERIFY(client->isMovableAcrossScreens()); + QCOMPARE(client->pos(), QPoint(42, 42)); + + // One should still be able to move the client around. + QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); + QVERIFY(clientStartMoveResizedSpy.isValid()); + QSignalSpy clientStepUserMovedResizedSpy(client, &AbstractClient::clientStepUserMovedResized); + QVERIFY(clientStepUserMovedResizedSpy.isValid()); + QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); + QVERIFY(clientFinishUserMovedResizedSpy.isValid()); + + QCOMPARE(workspace()->moveResizeClient(), nullptr); + QVERIFY(!client->isMove()); + QVERIFY(!client->isResize()); + workspace()->slotWindowMove(); + QCOMPARE(workspace()->moveResizeClient(), client); + QCOMPARE(clientStartMoveResizedSpy.count(), 1); + QVERIFY(client->isMove()); + QVERIFY(!client->isResize()); + + const QPoint cursorPos = KWin::Cursors::self()->mouse()->pos(); + client->keyPressEvent(Qt::Key_Right); + client->updateMoveResize(KWin::Cursors::self()->mouse()->pos()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0)); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); + QCOMPARE(client->pos(), QPoint(50, 42)); + + client->keyPressEvent(Qt::Key_Enter); + QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); + QCOMPARE(workspace()->moveResizeClient(), nullptr); + QVERIFY(!client->isMove()); + QVERIFY(!client->isResize()); + QCOMPARE(client->pos(), QPoint(50, 42)); + + // The rule should be applied again if the client appears after it's been closed. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(client->isMovable()); + QVERIFY(client->isMovableAcrossScreens()); + QCOMPARE(client->pos(), QPoint(42, 42)); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testPositionRemember() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("position", QPoint(42, 42)); + group.writeEntry("positionrule", int(Rules::Remember)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(client->isActive()); + + // The client should be moved to the position specified by the rule. + QVERIFY(client->isMovable()); + QVERIFY(client->isMovableAcrossScreens()); + QCOMPARE(client->pos(), QPoint(42, 42)); + + // One should still be able to move the client around. + QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); + QVERIFY(clientStartMoveResizedSpy.isValid()); + QSignalSpy clientStepUserMovedResizedSpy(client, &AbstractClient::clientStepUserMovedResized); + QVERIFY(clientStepUserMovedResizedSpy.isValid()); + QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); + QVERIFY(clientFinishUserMovedResizedSpy.isValid()); + + QCOMPARE(workspace()->moveResizeClient(), nullptr); + QVERIFY(!client->isMove()); + QVERIFY(!client->isResize()); + workspace()->slotWindowMove(); + QCOMPARE(workspace()->moveResizeClient(), client); + QCOMPARE(clientStartMoveResizedSpy.count(), 1); + QVERIFY(client->isMove()); + QVERIFY(!client->isResize()); + + const QPoint cursorPos = KWin::Cursors::self()->mouse()->pos(); + client->keyPressEvent(Qt::Key_Right); + client->updateMoveResize(KWin::Cursors::self()->mouse()->pos()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0)); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); + QCOMPARE(client->pos(), QPoint(50, 42)); + + client->keyPressEvent(Qt::Key_Enter); + QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); + QCOMPARE(workspace()->moveResizeClient(), nullptr); + QVERIFY(!client->isMove()); + QVERIFY(!client->isResize()); + QCOMPARE(client->pos(), QPoint(50, 42)); + + // The client should be placed at the last know position if we reopen it. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(client->isMovable()); + QVERIFY(client->isMovableAcrossScreens()); + QCOMPARE(client->pos(), QPoint(50, 42)); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testPositionForce() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("position", QPoint(42, 42)); + group.writeEntry("positionrule", int(Rules::Force)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(client->isActive()); + + // The client should be moved to the position specified by the rule. + QVERIFY(!client->isMovable()); + QVERIFY(!client->isMovableAcrossScreens()); + QCOMPARE(client->pos(), QPoint(42, 42)); + + // User should not be able to move the client. + QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); + QVERIFY(clientStartMoveResizedSpy.isValid()); + QCOMPARE(workspace()->moveResizeClient(), nullptr); + QVERIFY(!client->isMove()); + QVERIFY(!client->isResize()); + workspace()->slotWindowMove(); + QCOMPARE(workspace()->moveResizeClient(), nullptr); + QCOMPARE(clientStartMoveResizedSpy.count(), 0); + QVERIFY(!client->isMove()); + QVERIFY(!client->isResize()); + + // The position should still be forced if we reopen the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(!client->isMovable()); + QVERIFY(!client->isMovableAcrossScreens()); + QCOMPARE(client->pos(), QPoint(42, 42)); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testPositionApplyNow() +{ + // Create the test client. + AbstractClient *client; + Surface *surface; + QObject *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(client->isActive()); + + // The position of the client isn't set by any rule, thus the default placement + // policy will try to put the client in the top-left corner of the screen. + QVERIFY(client->isMovable()); + QVERIFY(client->isMovableAcrossScreens()); + QCOMPARE(client->pos(), QPoint(0, 0)); + + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("position", QPoint(42, 42)); + group.writeEntry("positionrule", int(Rules::ApplyNow)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + + // The client should be moved to the position specified by the rule. + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); + workspace()->slotReconfigure(); + QCOMPARE(frameGeometryChangedSpy.count(), 1); + QCOMPARE(client->pos(), QPoint(42, 42)); + + // We still have to be able to move the client around. + QVERIFY(client->isMovable()); + QVERIFY(client->isMovableAcrossScreens()); + QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); + QVERIFY(clientStartMoveResizedSpy.isValid()); + QSignalSpy clientStepUserMovedResizedSpy(client, &AbstractClient::clientStepUserMovedResized); + QVERIFY(clientStepUserMovedResizedSpy.isValid()); + QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); + QVERIFY(clientFinishUserMovedResizedSpy.isValid()); + + QCOMPARE(workspace()->moveResizeClient(), nullptr); + QVERIFY(!client->isMove()); + QVERIFY(!client->isResize()); + workspace()->slotWindowMove(); + QCOMPARE(workspace()->moveResizeClient(), client); + QCOMPARE(clientStartMoveResizedSpy.count(), 1); + QVERIFY(client->isMove()); + QVERIFY(!client->isResize()); + + const QPoint cursorPos = KWin::Cursors::self()->mouse()->pos(); + client->keyPressEvent(Qt::Key_Right); + client->updateMoveResize(KWin::Cursors::self()->mouse()->pos()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0)); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); + QCOMPARE(client->pos(), QPoint(50, 42)); + + client->keyPressEvent(Qt::Key_Enter); + QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); + QCOMPARE(workspace()->moveResizeClient(), nullptr); + QVERIFY(!client->isMove()); + QVERIFY(!client->isResize()); + QCOMPARE(client->pos(), QPoint(50, 42)); + + // The rule should not be applied again. + client->evaluateWindowRules(); + QCOMPARE(client->pos(), QPoint(50, 42)); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testPositionForceTemporarily() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("position", QPoint(42, 42)); + group.writeEntry("positionrule", int(Rules::ForceTemporarily)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(client->isActive()); + + // The client should be moved to the position specified by the rule. + QVERIFY(!client->isMovable()); + QVERIFY(!client->isMovableAcrossScreens()); + QCOMPARE(client->pos(), QPoint(42, 42)); + + // User should not be able to move the client. + QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); + QVERIFY(clientStartMoveResizedSpy.isValid()); + QCOMPARE(workspace()->moveResizeClient(), nullptr); + QVERIFY(!client->isMove()); + QVERIFY(!client->isResize()); + workspace()->slotWindowMove(); + QCOMPARE(workspace()->moveResizeClient(), nullptr); + QCOMPARE(clientStartMoveResizedSpy.count(), 0); + QVERIFY(!client->isMove()); + QVERIFY(!client->isResize()); + + // The rule should be discarded if we close the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(client->isMovable()); + QVERIFY(client->isMovableAcrossScreens()); + QCOMPARE(client->pos(), QPoint(0, 0)); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testSizeDontAffect() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("size", QSize(480, 640)); + group.writeEntry("sizerule", int(Rules::DontAffect)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + QScopedPointer surface; + surface.reset(Test::createSurface()); + QScopedPointer shellSurface; + shellSurface.reset(createXdgShellStableSurface(surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + QScopedPointer configureRequestedSpy; + configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); + shellSurface->setAppId("org.kde.foo"); + surface->commit(Surface::CommitFlag::None); + + // The window size shouldn't be enforced by the rule. + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 1); + QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(0, 0)); + + // Map the client. + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(client->isResizable()); + QCOMPARE(client->size(), QSize(100, 50)); + + // We should receive a configure event when the client becomes active. + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 2); + + // Destroy the client. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testSizeApply() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("size", QSize(480, 640)); + group.writeEntry("sizerule", int(Rules::Apply)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + QScopedPointer surface; + surface.reset(Test::createSurface()); + QScopedPointer shellSurface; + shellSurface.reset(createXdgShellStableSurface(surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + QScopedPointer configureRequestedSpy; + configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); + shellSurface->setAppId("org.kde.foo"); + surface->commit(Surface::CommitFlag::None); + + // The initial configure event should contain size hint set by the rule. + XdgShellSurface::States states; + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 1); + QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(480, 640)); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(XdgShellSurface::State::Resizing)); + + // Map the client. + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(480, 640), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(client->isResizable()); + QCOMPARE(client->size(), QSize(480, 640)); + + // We should receive a configure event when the client becomes active. + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 2); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(XdgShellSurface::State::Resizing)); + + // One still should be able to resize the client. + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); + QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); + QVERIFY(clientStartMoveResizedSpy.isValid()); + QSignalSpy clientStepUserMovedResizedSpy(client, &AbstractClient::clientStepUserMovedResized); + QVERIFY(clientStepUserMovedResizedSpy.isValid()); + QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); + QVERIFY(clientFinishUserMovedResizedSpy.isValid()); + QSignalSpy surfaceSizeChangedSpy(shellSurface.data(), &XdgShellSurface::sizeChanged); + QVERIFY(surfaceSizeChangedSpy.isValid()); + + QCOMPARE(workspace()->moveResizeClient(), nullptr); + QVERIFY(!client->isMove()); + QVERIFY(!client->isResize()); + workspace()->slotWindowResize(); + QCOMPARE(workspace()->moveResizeClient(), client); + QCOMPARE(clientStartMoveResizedSpy.count(), 1); + QVERIFY(!client->isMove()); + QVERIFY(client->isResize()); + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 3); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Resizing)); + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + + const QPoint cursorPos = KWin::Cursors::self()->mouse()->pos(); + client->keyPressEvent(Qt::Key_Right); + client->updateMoveResize(KWin::Cursors::self()->mouse()->pos()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0)); + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 4); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Resizing)); + QCOMPARE(surfaceSizeChangedSpy.count(), 1); + QCOMPARE(surfaceSizeChangedSpy.last().first().toSize(), QSize(488, 640)); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 0); + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + Test::render(surface.data(), QSize(488, 640), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(client->size(), QSize(488, 640)); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); + + client->keyPressEvent(Qt::Key_Enter); + QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); + QCOMPARE(workspace()->moveResizeClient(), nullptr); + QVERIFY(!client->isMove()); + QVERIFY(!client->isResize()); + + QVERIFY(configureRequestedSpy->wait(10)); + QCOMPARE(configureRequestedSpy->count(), 5); + + // The rule should be applied again if the client appears after it's been closed. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); + surface.reset(Test::createSurface()); + shellSurface.reset(createXdgShellStableSurface(surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); + shellSurface->setAppId("org.kde.foo"); + surface->commit(Surface::CommitFlag::None); + + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 1); + QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(480, 640)); + + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + client = Test::renderAndWaitForShown(surface.data(), QSize(480, 640), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(client->isResizable()); + QCOMPARE(client->size(), QSize(480, 640)); + + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 2); + + // Destroy the client. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testSizeRemember() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("size", QSize(480, 640)); + group.writeEntry("sizerule", int(Rules::Remember)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + QScopedPointer surface; + surface.reset(Test::createSurface()); + QScopedPointer shellSurface; + shellSurface.reset(createXdgShellStableSurface(surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + QScopedPointer configureRequestedSpy; + configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); + shellSurface->setAppId("org.kde.foo"); + surface->commit(Surface::CommitFlag::None); + + // The initial configure event should contain size hint set by the rule. + XdgShellSurface::States states; + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 1); + QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(480, 640)); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(XdgShellSurface::State::Resizing)); + + // Map the client. + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(480, 640), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(client->isResizable()); + QCOMPARE(client->size(), QSize(480, 640)); + + // We should receive a configure event when the client becomes active. + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 2); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(XdgShellSurface::State::Resizing)); + + // One should still be able to resize the client. + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); + QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); + QVERIFY(clientStartMoveResizedSpy.isValid()); + QSignalSpy clientStepUserMovedResizedSpy(client, &AbstractClient::clientStepUserMovedResized); + QVERIFY(clientStepUserMovedResizedSpy.isValid()); + QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); + QVERIFY(clientFinishUserMovedResizedSpy.isValid()); + QSignalSpy surfaceSizeChangedSpy(shellSurface.data(), &XdgShellSurface::sizeChanged); + QVERIFY(surfaceSizeChangedSpy.isValid()); + + QCOMPARE(workspace()->moveResizeClient(), nullptr); + QVERIFY(!client->isMove()); + QVERIFY(!client->isResize()); + workspace()->slotWindowResize(); + QCOMPARE(workspace()->moveResizeClient(), client); + QCOMPARE(clientStartMoveResizedSpy.count(), 1); + QVERIFY(!client->isMove()); + QVERIFY(client->isResize()); + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 3); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Resizing)); + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + + const QPoint cursorPos = KWin::Cursors::self()->mouse()->pos(); + client->keyPressEvent(Qt::Key_Right); + client->updateMoveResize(KWin::Cursors::self()->mouse()->pos()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0)); + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 4); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Resizing)); + QCOMPARE(surfaceSizeChangedSpy.count(), 1); + QCOMPARE(surfaceSizeChangedSpy.last().first().toSize(), QSize(488, 640)); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 0); + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + Test::render(surface.data(), QSize(488, 640), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(client->size(), QSize(488, 640)); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); + + client->keyPressEvent(Qt::Key_Enter); + QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); + QCOMPARE(workspace()->moveResizeClient(), nullptr); + QVERIFY(!client->isMove()); + QVERIFY(!client->isResize()); + + QVERIFY(configureRequestedSpy->wait(10)); + QCOMPARE(configureRequestedSpy->count(), 5); + + // If the client appears again, it should have the last known size. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); + surface.reset(Test::createSurface()); + shellSurface.reset(createXdgShellStableSurface(surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); + shellSurface->setAppId("org.kde.foo"); + surface->commit(Surface::CommitFlag::None); + + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 1); + QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(488, 640)); + + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + client = Test::renderAndWaitForShown(surface.data(), QSize(488, 640), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(client->isResizable()); + QCOMPARE(client->size(), QSize(488, 640)); + + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 2); + + // Destroy the client. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testSizeForce() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("size", QSize(480, 640)); + group.writeEntry("sizerule", int(Rules::Force)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + QScopedPointer surface; + surface.reset(Test::createSurface()); + QScopedPointer shellSurface; + shellSurface.reset(createXdgShellStableSurface(surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + QScopedPointer configureRequestedSpy; + configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); + shellSurface->setAppId("org.kde.foo"); + surface->commit(Surface::CommitFlag::None); + + // The initial configure event should contain size hint set by the rule. + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 1); + QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(480, 640)); + + // Map the client. + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(480, 640), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(!client->isResizable()); + QCOMPARE(client->size(), QSize(480, 640)); + + // We should receive a configure event when the client becomes active. + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 2); + + // Any attempt to resize the client should not succeed. + QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); + QVERIFY(clientStartMoveResizedSpy.isValid()); + QCOMPARE(workspace()->moveResizeClient(), nullptr); + QVERIFY(!client->isMove()); + QVERIFY(!client->isResize()); + workspace()->slotWindowResize(); + QCOMPARE(workspace()->moveResizeClient(), nullptr); + QCOMPARE(clientStartMoveResizedSpy.count(), 0); + QVERIFY(!client->isMove()); + QVERIFY(!client->isResize()); + QVERIFY(!configureRequestedSpy->wait(100)); + + // If the client appears again, the size should still be forced. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); + surface.reset(Test::createSurface()); + shellSurface.reset(createXdgShellStableSurface(surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); + shellSurface->setAppId("org.kde.foo"); + surface->commit(Surface::CommitFlag::None); + + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 1); + QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(480, 640)); + + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + client = Test::renderAndWaitForShown(surface.data(), QSize(480, 640), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(!client->isResizable()); + QCOMPARE(client->size(), QSize(480, 640)); + + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 2); + + // Destroy the client. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testSizeApplyNow() +{ + // Create the test client. + QScopedPointer surface; + surface.reset(Test::createSurface()); + QScopedPointer shellSurface; + shellSurface.reset(createXdgShellStableSurface(surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + QScopedPointer configureRequestedSpy; + configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); + shellSurface->setAppId("org.kde.foo"); + surface->commit(Surface::CommitFlag::None); + + // The expected surface dimensions should be set by the rule. + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 1); + QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(0, 0)); + + // Map the client. + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(client->isResizable()); + QCOMPARE(client->size(), QSize(100, 50)); + + // We should receive a configure event when the client becomes active. + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 2); + + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("size", QSize(480, 640)); + group.writeEntry("sizerule", int(Rules::ApplyNow)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // The compositor should send a configure event with a new size. + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 3); + QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(480, 640)); + + // Draw the surface with the new size. + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + Test::render(surface.data(), QSize(480, 640), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(client->size(), QSize(480, 640)); + QVERIFY(!configureRequestedSpy->wait(100)); + + // The rule should not be applied again. + client->evaluateWindowRules(); + QVERIFY(!configureRequestedSpy->wait(100)); + + // Destroy the client. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testSizeForceTemporarily() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("size", QSize(480, 640)); + group.writeEntry("sizerule", int(Rules::ForceTemporarily)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + QScopedPointer surface; + surface.reset(Test::createSurface()); + QScopedPointer shellSurface; + shellSurface.reset(createXdgShellStableSurface(surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + QScopedPointer configureRequestedSpy; + configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); + shellSurface->setAppId("org.kde.foo"); + surface->commit(Surface::CommitFlag::None); + + // The initial configure event should contain size hint set by the rule. + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 1); + QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(480, 640)); + + // Map the client. + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(480, 640), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(!client->isResizable()); + QCOMPARE(client->size(), QSize(480, 640)); + + // We should receive a configure event when the client becomes active. + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 2); + + // Any attempt to resize the client should not succeed. + QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); + QVERIFY(clientStartMoveResizedSpy.isValid()); + QCOMPARE(workspace()->moveResizeClient(), nullptr); + QVERIFY(!client->isMove()); + QVERIFY(!client->isResize()); + workspace()->slotWindowResize(); + QCOMPARE(workspace()->moveResizeClient(), nullptr); + QCOMPARE(clientStartMoveResizedSpy.count(), 0); + QVERIFY(!client->isMove()); + QVERIFY(!client->isResize()); + QVERIFY(!configureRequestedSpy->wait(100)); + + // The rule should be discarded when the client is closed. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); + surface.reset(Test::createSurface()); + shellSurface.reset(createXdgShellStableSurface(surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); + shellSurface->setAppId("org.kde.foo"); + surface->commit(Surface::CommitFlag::None); + + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 1); + QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(0, 0)); + + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(client->isResizable()); + QCOMPARE(client->size(), QSize(100, 50)); + + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 2); + + // Destroy the client. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testMaximizeDontAffect() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("maximizehoriz", true); + group.writeEntry("maximizehorizrule", int(Rules::DontAffect)); + group.writeEntry("maximizevert", true); + group.writeEntry("maximizevertrule", int(Rules::DontAffect)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + QScopedPointer surface; + surface.reset(Test::createSurface()); + QScopedPointer shellSurface; + shellSurface.reset(createXdgShellStableSurface(surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + QScopedPointer configureRequestedSpy; + configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); + shellSurface->setAppId("org.kde.foo"); + surface->commit(Surface::CommitFlag::None); + + // Wait for the initial configure event. + XdgShellSurface::States states; + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 1); + QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(0, 0)); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); + + // Map the client. + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(client->isMaximizable()); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(client->size(), QSize(100, 50)); + + // We should receive a configure event when the client becomes active. + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 2); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); + + // Destroy the client. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testMaximizeApply() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("maximizehoriz", true); + group.writeEntry("maximizehorizrule", int(Rules::Apply)); + group.writeEntry("maximizevert", true); + group.writeEntry("maximizevertrule", int(Rules::Apply)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + QScopedPointer surface; + surface.reset(Test::createSurface()); + QScopedPointer shellSurface; + shellSurface.reset(createXdgShellStableSurface(surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + QScopedPointer configureRequestedSpy; + configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); + shellSurface->setAppId("org.kde.foo"); + surface->commit(Surface::CommitFlag::None); + + // Wait for the initial configure event. + XdgShellSurface::States states; + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 1); + QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024)); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); + + // Map the client. + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(1280, 1024), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(client->isMaximizable()); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->size(), QSize(1280, 1024)); + + // We should receive a configure event when the client becomes active. + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 2); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); + + // One should still be able to change the maximized state of the client. + workspace()->slotWindowMaximize(); + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 3); + QEXPECT_FAIL("", "Geometry restore is set to the first valid geometry", Continue); + QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(0, 0)); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); + + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + Test::render(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(client->size(), QSize(100, 50)); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); + + // If we create the client again, it should be initially maximized. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); + surface.reset(Test::createSurface()); + shellSurface.reset(createXdgShellStableSurface(surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); + shellSurface->setAppId("org.kde.foo"); + surface->commit(Surface::CommitFlag::None); + + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 1); + QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024)); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); + + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + client = Test::renderAndWaitForShown(surface.data(), QSize(1280, 1024), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(client->isMaximizable()); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->size(), QSize(1280, 1024)); + + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 2); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); + + // Destroy the client. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testMaximizeRemember() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("maximizehoriz", true); + group.writeEntry("maximizehorizrule", int(Rules::Remember)); + group.writeEntry("maximizevert", true); + group.writeEntry("maximizevertrule", int(Rules::Remember)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + QScopedPointer surface; + surface.reset(Test::createSurface()); + QScopedPointer shellSurface; + shellSurface.reset(createXdgShellStableSurface(surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + QScopedPointer configureRequestedSpy; + configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); + shellSurface->setAppId("org.kde.foo"); + surface->commit(Surface::CommitFlag::None); + + // Wait for the initial configure event. + XdgShellSurface::States states; + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 1); + QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024)); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); + + // Map the client. + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(1280, 1024), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(client->isMaximizable()); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->size(), QSize(1280, 1024)); + + // We should receive a configure event when the client becomes active. + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 2); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); + + // One should still be able to change the maximized state of the client. + workspace()->slotWindowMaximize(); + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 3); + QEXPECT_FAIL("", "Geometry restore is set to the first valid geometry", Continue); + QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(0, 0)); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); + + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + Test::render(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(client->size(), QSize(100, 50)); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); + + // If we create the client again, it should not be maximized (because last time it wasn't). + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); + surface.reset(Test::createSurface()); + shellSurface.reset(createXdgShellStableSurface(surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); + shellSurface->setAppId("org.kde.foo"); + surface->commit(Surface::CommitFlag::None); + + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 1); + QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(0, 0)); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); + + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(client->isMaximizable()); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(client->size(), QSize(100, 50)); + + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 2); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); + + // Destroy the client. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testMaximizeForce() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("maximizehoriz", true); + group.writeEntry("maximizehorizrule", int(Rules::Force)); + group.writeEntry("maximizevert", true); + group.writeEntry("maximizevertrule", int(Rules::Force)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + QScopedPointer surface; + surface.reset(Test::createSurface()); + QScopedPointer shellSurface; + shellSurface.reset(createXdgShellStableSurface(surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + QScopedPointer configureRequestedSpy; + configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); + shellSurface->setAppId("org.kde.foo"); + surface->commit(Surface::CommitFlag::None); + + // Wait for the initial configure event. + XdgShellSurface::States states; + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 1); + QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024)); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); + + // Map the client. + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(1280, 1024), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(!client->isMaximizable()); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->size(), QSize(1280, 1024)); + + // We should receive a configure event when the client becomes active. + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 2); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); + + // Any attempt to change the maximized state should not succeed. + const QRect oldGeometry = client->frameGeometry(); + workspace()->slotWindowMaximize(); + QVERIFY(!configureRequestedSpy->wait(100)); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->frameGeometry(), oldGeometry); + + // If we create the client again, the maximized state should still be forced. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); + surface.reset(Test::createSurface()); + shellSurface.reset(createXdgShellStableSurface(surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); + shellSurface->setAppId("org.kde.foo"); + surface->commit(Surface::CommitFlag::None); + + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 1); + QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024)); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); + + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + client = Test::renderAndWaitForShown(surface.data(), QSize(1280, 1024), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(!client->isMaximizable()); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->size(), QSize(1280, 1024)); + + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 2); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); + + // Destroy the client. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testMaximizeApplyNow() +{ + // Create the test client. + QScopedPointer surface; + surface.reset(Test::createSurface()); + QScopedPointer shellSurface; + shellSurface.reset(createXdgShellStableSurface(surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + QScopedPointer configureRequestedSpy; + configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); + shellSurface->setAppId("org.kde.foo"); + surface->commit(Surface::CommitFlag::None); + + // Wait for the initial configure event. + XdgShellSurface::States states; + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 1); + QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(0, 0)); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); + + // Map the client. + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(client->isMaximizable()); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(client->size(), QSize(100, 50)); + + // We should receive a configure event when the client becomes active. + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 2); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); + + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("maximizehoriz", true); + group.writeEntry("maximizehorizrule", int(Rules::ApplyNow)); + group.writeEntry("maximizevert", true); + group.writeEntry("maximizevertrule", int(Rules::ApplyNow)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // We should receive a configure event with a new surface size. + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 3); + QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024)); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); + + // Draw contents of the maximized client. + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + Test::render(surface.data(), QSize(1280, 1024), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(client->size(), QSize(1280, 1024)); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); + + // The client still has to be maximizeable. + QVERIFY(client->isMaximizable()); + + // Restore the client. + workspace()->slotWindowMaximize(); + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 4); + QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(100, 50)); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); + + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + Test::render(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(client->size(), QSize(100, 50)); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); + + // The rule should be discarded after it's been applied. + const QRect oldGeometry = client->frameGeometry(); + client->evaluateWindowRules(); + QVERIFY(!configureRequestedSpy->wait(100)); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(client->frameGeometry(), oldGeometry); + + // Destroy the client. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testMaximizeForceTemporarily() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("maximizehoriz", true); + group.writeEntry("maximizehorizrule", int(Rules::ForceTemporarily)); + group.writeEntry("maximizevert", true); + group.writeEntry("maximizevertrule", int(Rules::ForceTemporarily)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + QScopedPointer surface; + surface.reset(Test::createSurface()); + QScopedPointer shellSurface; + shellSurface.reset(createXdgShellStableSurface(surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + QScopedPointer configureRequestedSpy; + configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); + shellSurface->setAppId("org.kde.foo"); + surface->commit(Surface::CommitFlag::None); + + // Wait for the initial configure event. + XdgShellSurface::States states; + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 1); + QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024)); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); + + // Map the client. + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(1280, 1024), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(!client->isMaximizable()); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->size(), QSize(1280, 1024)); + + // We should receive a configure event when the client becomes active. + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 2); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); + + // Any attempt to change the maximized state should not succeed. + const QRect oldGeometry = client->frameGeometry(); + workspace()->slotWindowMaximize(); + QVERIFY(!configureRequestedSpy->wait(100)); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->frameGeometry(), oldGeometry); + + // The rule should be discarded if we close the client. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); + surface.reset(Test::createSurface()); + shellSurface.reset(createXdgShellStableSurface(surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); + shellSurface->setAppId("org.kde.foo"); + surface->commit(Surface::CommitFlag::None); + + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 1); + QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(0, 0)); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); + + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(client->isMaximizable()); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(client->size(), QSize(100, 50)); + + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 2); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); + + // Destroy the client. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testDesktopDontAffect() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("desktop", 2); + group.writeEntry("desktoprule", int(Rules::DontAffect)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // We need at least two virtual desktop for this test. + VirtualDesktopManager::self()->setCount(2); + QCOMPARE(VirtualDesktopManager::self()->count(), 2u); + VirtualDesktopManager::self()->setCurrent(1); + QCOMPARE(VirtualDesktopManager::self()->current(), 1); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // The client should appear on the current virtual desktop. + QCOMPARE(client->desktop(), 1); + QCOMPARE(VirtualDesktopManager::self()->current(), 1); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testDesktopApply() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("desktop", 2); + group.writeEntry("desktoprule", int(Rules::Apply)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // We need at least two virtual desktop for this test. + VirtualDesktopManager::self()->setCount(2); + QCOMPARE(VirtualDesktopManager::self()->count(), 2u); + VirtualDesktopManager::self()->setCurrent(1); + QCOMPARE(VirtualDesktopManager::self()->current(), 1); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // The client should appear on the second virtual desktop. + QCOMPARE(client->desktop(), 2); + QCOMPARE(VirtualDesktopManager::self()->current(), 2); + + // We still should be able to move the client between desktops. + workspace()->sendClientToDesktop(client, 1, true); + QCOMPARE(client->desktop(), 1); + QCOMPARE(VirtualDesktopManager::self()->current(), 2); + + // If we re-open the client, it should appear on the second virtual desktop again. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + VirtualDesktopManager::self()->setCurrent(1); + QCOMPARE(VirtualDesktopManager::self()->current(), 1); + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QCOMPARE(client->desktop(), 2); + QCOMPARE(VirtualDesktopManager::self()->current(), 2); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testDesktopRemember() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("desktop", 2); + group.writeEntry("desktoprule", int(Rules::Remember)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // We need at least two virtual desktop for this test. + VirtualDesktopManager::self()->setCount(2); + QCOMPARE(VirtualDesktopManager::self()->count(), 2u); + VirtualDesktopManager::self()->setCurrent(1); + QCOMPARE(VirtualDesktopManager::self()->current(), 1); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QCOMPARE(client->desktop(), 2); + QCOMPARE(VirtualDesktopManager::self()->current(), 2); + + // Move the client to the first virtual desktop. + workspace()->sendClientToDesktop(client, 1, true); + QCOMPARE(client->desktop(), 1); + QCOMPARE(VirtualDesktopManager::self()->current(), 2); + + // If we create the client again, it should appear on the first virtual desktop. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QCOMPARE(client->desktop(), 1); + QCOMPARE(VirtualDesktopManager::self()->current(), 1); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testDesktopForce() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("desktop", 2); + group.writeEntry("desktoprule", int(Rules::Force)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // We need at least two virtual desktop for this test. + VirtualDesktopManager::self()->setCount(2); + QCOMPARE(VirtualDesktopManager::self()->count(), 2u); + VirtualDesktopManager::self()->setCurrent(1); + QCOMPARE(VirtualDesktopManager::self()->current(), 1); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // The client should appear on the second virtual desktop. + QCOMPARE(client->desktop(), 2); + QCOMPARE(VirtualDesktopManager::self()->current(), 2); + + // Any attempt to move the client to another virtual desktop should fail. + workspace()->sendClientToDesktop(client, 1, true); + QCOMPARE(client->desktop(), 2); + QCOMPARE(VirtualDesktopManager::self()->current(), 2); + + // If we re-open the client, it should appear on the second virtual desktop again. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + VirtualDesktopManager::self()->setCurrent(1); + QCOMPARE(VirtualDesktopManager::self()->current(), 1); + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QCOMPARE(client->desktop(), 2); + QCOMPARE(VirtualDesktopManager::self()->current(), 2); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testDesktopApplyNow() +{ + // We need at least two virtual desktop for this test. + VirtualDesktopManager::self()->setCount(2); + QCOMPARE(VirtualDesktopManager::self()->count(), 2u); + VirtualDesktopManager::self()->setCurrent(1); + QCOMPARE(VirtualDesktopManager::self()->current(), 1); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QCOMPARE(client->desktop(), 1); + QCOMPARE(VirtualDesktopManager::self()->current(), 1); + + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("desktop", 2); + group.writeEntry("desktoprule", int(Rules::ApplyNow)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // The client should have been moved to the second virtual desktop. + QCOMPARE(client->desktop(), 2); + QCOMPARE(VirtualDesktopManager::self()->current(), 1); + + // One should still be able to move the client between desktops. + workspace()->sendClientToDesktop(client, 1, true); + QCOMPARE(client->desktop(), 1); + QCOMPARE(VirtualDesktopManager::self()->current(), 1); + + // The rule should not be applied again. + client->evaluateWindowRules(); + QCOMPARE(client->desktop(), 1); + QCOMPARE(VirtualDesktopManager::self()->current(), 1); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testDesktopForceTemporarily() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("desktop", 2); + group.writeEntry("desktoprule", int(Rules::ForceTemporarily)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // We need at least two virtual desktop for this test. + VirtualDesktopManager::self()->setCount(2); + QCOMPARE(VirtualDesktopManager::self()->count(), 2u); + VirtualDesktopManager::self()->setCurrent(1); + QCOMPARE(VirtualDesktopManager::self()->current(), 1); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // The client should appear on the second virtual desktop. + QCOMPARE(client->desktop(), 2); + QCOMPARE(VirtualDesktopManager::self()->current(), 2); + + // Any attempt to move the client to another virtual desktop should fail. + workspace()->sendClientToDesktop(client, 1, true); + QCOMPARE(client->desktop(), 2); + QCOMPARE(VirtualDesktopManager::self()->current(), 2); + + // The rule should be discarded when the client is withdrawn. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + VirtualDesktopManager::self()->setCurrent(1); + QCOMPARE(VirtualDesktopManager::self()->current(), 1); + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QCOMPARE(client->desktop(), 1); + QCOMPARE(VirtualDesktopManager::self()->current(), 1); + + // One should be able to move the client between desktops. + workspace()->sendClientToDesktop(client, 2, true); + QCOMPARE(client->desktop(), 2); + QCOMPARE(VirtualDesktopManager::self()->current(), 1); + workspace()->sendClientToDesktop(client, 1, true); + QCOMPARE(client->desktop(), 1); + QCOMPARE(VirtualDesktopManager::self()->current(), 1); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testMinimizeDontAffect() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("minimize", true); + group.writeEntry("minimizerule", int(Rules::DontAffect)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(client->isMinimizable()); + + // The client should not be minimized. + QVERIFY(!client->isMinimized()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testMinimizeApply() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("minimize", true); + group.writeEntry("minimizerule", int(Rules::Apply)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(client->isMinimizable()); + + // The client should be minimized. + QVERIFY(client->isMinimized()); + + // We should still be able to unminimize the client. + client->unminimize(); + QVERIFY(!client->isMinimized()); + + // If we re-open the client, it should be minimized back again. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(client->isMinimizable()); + QVERIFY(client->isMinimized()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testMinimizeRemember() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("minimize", false); + group.writeEntry("minimizerule", int(Rules::Remember)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(client->isMinimizable()); + QVERIFY(!client->isMinimized()); + + // Minimize the client. + client->minimize(); + QVERIFY(client->isMinimized()); + + // If we open the client again, it should be minimized. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(client->isMinimizable()); + QVERIFY(client->isMinimized()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testMinimizeForce() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("minimize", false); + group.writeEntry("minimizerule", int(Rules::Force)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(!client->isMinimizable()); + QVERIFY(!client->isMinimized()); + + // Any attempt to minimize the client should fail. + client->minimize(); + QVERIFY(!client->isMinimized()); + + // If we re-open the client, the minimized state should still be forced. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(!client->isMinimizable()); + QVERIFY(!client->isMinimized()); + client->minimize(); + QVERIFY(!client->isMinimized()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testMinimizeApplyNow() +{ + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(client->isMinimizable()); + QVERIFY(!client->isMinimized()); + + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("minimize", true); + group.writeEntry("minimizerule", int(Rules::ApplyNow)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // The client should be minimized now. + QVERIFY(client->isMinimizable()); + QVERIFY(client->isMinimized()); + + // One is still able to unminimize the client. + client->unminimize(); + QVERIFY(!client->isMinimized()); + + // The rule should not be applied again. + client->evaluateWindowRules(); + QVERIFY(client->isMinimizable()); + QVERIFY(!client->isMinimized()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testMinimizeForceTemporarily() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("minimize", false); + group.writeEntry("minimizerule", int(Rules::ForceTemporarily)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(!client->isMinimizable()); + QVERIFY(!client->isMinimized()); + + // Any attempt to minimize the client should fail until the client is closed. + client->minimize(); + QVERIFY(!client->isMinimized()); + + // The rule should be discarded when the client is closed. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(client->isMinimizable()); + QVERIFY(!client->isMinimized()); + client->minimize(); + QVERIFY(client->isMinimized()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testSkipTaskbarDontAffect() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("skiptaskbar", true); + group.writeEntry("skiptaskbarrule", int(Rules::DontAffect)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // The client should not be affected by the rule. + QVERIFY(!client->skipTaskbar()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testSkipTaskbarApply() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("skiptaskbar", true); + group.writeEntry("skiptaskbarrule", int(Rules::Apply)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // The client should not be included on a taskbar. + QVERIFY(client->skipTaskbar()); + + // Though one can change that. + client->setOriginalSkipTaskbar(false); + QVERIFY(!client->skipTaskbar()); + + // Reopen the client, the rule should be applied again. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(client->skipTaskbar()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testSkipTaskbarRemember() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("skiptaskbar", true); + group.writeEntry("skiptaskbarrule", int(Rules::Remember)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // The client should not be included on a taskbar. + QVERIFY(client->skipTaskbar()); + + // Change the skip-taskbar state. + client->setOriginalSkipTaskbar(false); + QVERIFY(!client->skipTaskbar()); + + // Reopen the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // The client should be included on a taskbar. + QVERIFY(!client->skipTaskbar()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testSkipTaskbarForce() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("skiptaskbar", true); + group.writeEntry("skiptaskbarrule", int(Rules::Force)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // The client should not be included on a taskbar. + QVERIFY(client->skipTaskbar()); + + // Any attempt to change the skip-taskbar state should not succeed. + client->setOriginalSkipTaskbar(false); + QVERIFY(client->skipTaskbar()); + + // Reopen the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // The skip-taskbar state should be still forced. + QVERIFY(client->skipTaskbar()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testSkipTaskbarApplyNow() +{ + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(!client->skipTaskbar()); + + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("skiptaskbar", true); + group.writeEntry("skiptaskbarrule", int(Rules::ApplyNow)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // The client should not be on a taskbar now. + QVERIFY(client->skipTaskbar()); + + // Also, one change the skip-taskbar state. + client->setOriginalSkipTaskbar(false); + QVERIFY(!client->skipTaskbar()); + + // The rule should not be applied again. + client->evaluateWindowRules(); + QVERIFY(!client->skipTaskbar()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testSkipTaskbarForceTemporarily() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("skiptaskbar", true); + group.writeEntry("skiptaskbarrule", int(Rules::ForceTemporarily)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // The client should not be included on a taskbar. + QVERIFY(client->skipTaskbar()); + + // Any attempt to change the skip-taskbar state should not succeed. + client->setOriginalSkipTaskbar(false); + QVERIFY(client->skipTaskbar()); + + // The rule should be discarded when the client is closed. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(!client->skipTaskbar()); + + // The skip-taskbar state is no longer forced. + client->setOriginalSkipTaskbar(true); + QVERIFY(client->skipTaskbar()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testSkipPagerDontAffect() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("skippager", true); + group.writeEntry("skippagerrule", int(Rules::DontAffect)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // The client should not be affected by the rule. + QVERIFY(!client->skipPager()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testSkipPagerApply() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("skippager", true); + group.writeEntry("skippagerrule", int(Rules::Apply)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // The client should not be included on a pager. + QVERIFY(client->skipPager()); + + // Though one can change that. + client->setSkipPager(false); + QVERIFY(!client->skipPager()); + + // Reopen the client, the rule should be applied again. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(client->skipPager()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testSkipPagerRemember() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("skippager", true); + group.writeEntry("skippagerrule", int(Rules::Remember)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // The client should not be included on a pager. + QVERIFY(client->skipPager()); + + // Change the skip-pager state. + client->setSkipPager(false); + QVERIFY(!client->skipPager()); + + // Reopen the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // The client should be included on a pager. + QVERIFY(!client->skipPager()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testSkipPagerForce() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("skippager", true); + group.writeEntry("skippagerrule", int(Rules::Force)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // The client should not be included on a pager. + QVERIFY(client->skipPager()); + + // Any attempt to change the skip-pager state should not succeed. + client->setSkipPager(false); + QVERIFY(client->skipPager()); + + // Reopen the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // The skip-pager state should be still forced. + QVERIFY(client->skipPager()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testSkipPagerApplyNow() +{ + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(!client->skipPager()); + + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("skippager", true); + group.writeEntry("skippagerrule", int(Rules::ApplyNow)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // The client should not be on a pager now. + QVERIFY(client->skipPager()); + + // Also, one change the skip-pager state. + client->setSkipPager(false); + QVERIFY(!client->skipPager()); + + // The rule should not be applied again. + client->evaluateWindowRules(); + QVERIFY(!client->skipPager()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testSkipPagerForceTemporarily() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("skippager", true); + group.writeEntry("skippagerrule", int(Rules::ForceTemporarily)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // The client should not be included on a pager. + QVERIFY(client->skipPager()); + + // Any attempt to change the skip-pager state should not succeed. + client->setSkipPager(false); + QVERIFY(client->skipPager()); + + // The rule should be discarded when the client is closed. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(!client->skipPager()); + + // The skip-pager state is no longer forced. + client->setSkipPager(true); + QVERIFY(client->skipPager()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testSkipSwitcherDontAffect() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("skipswitcher", true); + group.writeEntry("skipswitcherrule", int(Rules::DontAffect)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // The client should not be affected by the rule. + QVERIFY(!client->skipSwitcher()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testSkipSwitcherApply() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("skipswitcher", true); + group.writeEntry("skipswitcherrule", int(Rules::Apply)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // The client should be excluded from window switching effects. + QVERIFY(client->skipSwitcher()); + + // Though one can change that. + client->setSkipSwitcher(false); + QVERIFY(!client->skipSwitcher()); + + // Reopen the client, the rule should be applied again. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(client->skipSwitcher()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testSkipSwitcherRemember() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("skipswitcher", true); + group.writeEntry("skipswitcherrule", int(Rules::Remember)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // The client should be excluded from window switching effects. + QVERIFY(client->skipSwitcher()); + + // Change the skip-switcher state. + client->setSkipSwitcher(false); + QVERIFY(!client->skipSwitcher()); + + // Reopen the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // The client should be included in window switching effects. + QVERIFY(!client->skipSwitcher()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testSkipSwitcherForce() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("skipswitcher", true); + group.writeEntry("skipswitcherrule", int(Rules::Force)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // The client should be excluded from window switching effects. + QVERIFY(client->skipSwitcher()); + + // Any attempt to change the skip-switcher state should not succeed. + client->setSkipSwitcher(false); + QVERIFY(client->skipSwitcher()); + + // Reopen the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // The skip-switcher state should be still forced. + QVERIFY(client->skipSwitcher()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testSkipSwitcherApplyNow() +{ + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(!client->skipSwitcher()); + + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("skipswitcher", true); + group.writeEntry("skipswitcherrule", int(Rules::ApplyNow)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // The client should be excluded from window switching effects now. + QVERIFY(client->skipSwitcher()); + + // Also, one change the skip-switcher state. + client->setSkipSwitcher(false); + QVERIFY(!client->skipSwitcher()); + + // The rule should not be applied again. + client->evaluateWindowRules(); + QVERIFY(!client->skipSwitcher()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testSkipSwitcherForceTemporarily() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("skipswitcher", true); + group.writeEntry("skipswitcherrule", int(Rules::ForceTemporarily)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // The client should be excluded from window switching effects. + QVERIFY(client->skipSwitcher()); + + // Any attempt to change the skip-switcher state should not succeed. + client->setSkipSwitcher(false); + QVERIFY(client->skipSwitcher()); + + // The rule should be discarded when the client is closed. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(!client->skipSwitcher()); + + // The skip-switcher state is no longer forced. + client->setSkipSwitcher(true); + QVERIFY(client->skipSwitcher()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testKeepAboveDontAffect() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("above", true); + group.writeEntry("aboverule", int(Rules::DontAffect)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // The keep-above state of the client should not be affected by the rule. + QVERIFY(!client->keepAbove()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testKeepAboveApply() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("above", true); + group.writeEntry("aboverule", int(Rules::Apply)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // Initially, the client should be kept above. + QVERIFY(client->keepAbove()); + + // One should also be able to alter the keep-above state. + client->setKeepAbove(false); + QVERIFY(!client->keepAbove()); + + // If one re-opens the client, it should be kept above back again. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(client->keepAbove()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testKeepAboveRemember() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("above", true); + group.writeEntry("aboverule", int(Rules::Remember)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // Initially, the client should be kept above. + QVERIFY(client->keepAbove()); + + // Unset the keep-above state. + client->setKeepAbove(false); + QVERIFY(!client->keepAbove()); + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + + // Re-open the client, it should not be kept above. + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(!client->keepAbove()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testKeepAboveForce() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("above", true); + group.writeEntry("aboverule", int(Rules::Force)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // Initially, the client should be kept above. + QVERIFY(client->keepAbove()); + + // Any attemt to unset the keep-above should not succeed. + client->setKeepAbove(false); + QVERIFY(client->keepAbove()); + + // If we re-open the client, it should still be kept above. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(client->keepAbove()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testKeepAboveApplyNow() +{ + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(!client->keepAbove()); + + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("above", true); + group.writeEntry("aboverule", int(Rules::ApplyNow)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // The client should now be kept above other clients. + QVERIFY(client->keepAbove()); + + // One is still able to change the keep-above state of the client. + client->setKeepAbove(false); + QVERIFY(!client->keepAbove()); + + // The rule should not be applied again. + client->evaluateWindowRules(); + QVERIFY(!client->keepAbove()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testKeepAboveForceTemporarily() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("above", true); + group.writeEntry("aboverule", int(Rules::ForceTemporarily)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // Initially, the client should be kept above. + QVERIFY(client->keepAbove()); + + // Any attempt to alter the keep-above state should not succeed. + client->setKeepAbove(false); + QVERIFY(client->keepAbove()); + + // The rule should be discarded when the client is closed. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(!client->keepAbove()); + + // The keep-above state is no longer forced. + client->setKeepAbove(true); + QVERIFY(client->keepAbove()); + client->setKeepAbove(false); + QVERIFY(!client->keepAbove()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testKeepBelowDontAffect() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("below", true); + group.writeEntry("belowrule", int(Rules::DontAffect)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // The keep-below state of the client should not be affected by the rule. + QVERIFY(!client->keepBelow()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testKeepBelowApply() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("below", true); + group.writeEntry("belowrule", int(Rules::Apply)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // Initially, the client should be kept below. + QVERIFY(client->keepBelow()); + + // One should also be able to alter the keep-below state. + client->setKeepBelow(false); + QVERIFY(!client->keepBelow()); + + // If one re-opens the client, it should be kept above back again. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(client->keepBelow()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testKeepBelowRemember() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("below", true); + group.writeEntry("belowrule", int(Rules::Remember)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // Initially, the client should be kept below. + QVERIFY(client->keepBelow()); + + // Unset the keep-below state. + client->setKeepBelow(false); + QVERIFY(!client->keepBelow()); + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + + // Re-open the client, it should not be kept below. + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(!client->keepBelow()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testKeepBelowForce() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("below", true); + group.writeEntry("belowrule", int(Rules::Force)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // Initially, the client should be kept below. + QVERIFY(client->keepBelow()); + + // Any attemt to unset the keep-below should not succeed. + client->setKeepBelow(false); + QVERIFY(client->keepBelow()); + + // If we re-open the client, it should still be kept below. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(client->keepBelow()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testKeepBelowApplyNow() +{ + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(!client->keepBelow()); + + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("below", true); + group.writeEntry("belowrule", int(Rules::ApplyNow)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // The client should now be kept below other clients. + QVERIFY(client->keepBelow()); + + // One is still able to change the keep-below state of the client. + client->setKeepBelow(false); + QVERIFY(!client->keepBelow()); + + // The rule should not be applied again. + client->evaluateWindowRules(); + QVERIFY(!client->keepBelow()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testKeepBelowForceTemporarily() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("below", true); + group.writeEntry("belowrule", int(Rules::ForceTemporarily)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // Initially, the client should be kept below. + QVERIFY(client->keepBelow()); + + // Any attempt to alter the keep-below state should not succeed. + client->setKeepBelow(false); + QVERIFY(client->keepBelow()); + + // The rule should be discarded when the client is closed. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(!client->keepBelow()); + + // The keep-below state is no longer forced. + client->setKeepBelow(true); + QVERIFY(client->keepBelow()); + client->setKeepBelow(false); + QVERIFY(!client->keepBelow()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testShortcutDontAffect() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("shortcut", "Ctrl+Alt+1"); + group.writeEntry("shortcutrule", int(Rules::DontAffect)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QCOMPARE(client->shortcut(), QKeySequence()); + client->minimize(); + QVERIFY(client->isMinimized()); + + // If we press the window shortcut, nothing should happen. + QSignalSpy clientUnminimizedSpy(client, &AbstractClient::clientUnminimized); + QVERIFY(clientUnminimizedSpy.isValid()); + quint32 timestamp = 1; + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_1, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_1, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); + QVERIFY(!clientUnminimizedSpy.wait(100)); + QVERIFY(client->isMinimized()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testShortcutApply() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("shortcut", "Ctrl+Alt+1"); + group.writeEntry("shortcutrule", int(Rules::Apply)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // If we press the window shortcut, the window should be brought back to user. + QSignalSpy clientUnminimizedSpy(client, &AbstractClient::clientUnminimized); + QVERIFY(clientUnminimizedSpy.isValid()); + quint32 timestamp = 1; + QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); + client->minimize(); + QVERIFY(client->isMinimized()); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_1, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_1, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); + QVERIFY(clientUnminimizedSpy.wait()); + QVERIFY(!client->isMinimized()); + + // One can also change the shortcut. + client->setShortcut(QStringLiteral("Ctrl+Alt+2")); + QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_2})); + client->minimize(); + QVERIFY(client->isMinimized()); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_2, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_2, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); + QVERIFY(clientUnminimizedSpy.wait()); + QVERIFY(!client->isMinimized()); + + // The old shortcut should do nothing. + client->minimize(); + QVERIFY(client->isMinimized()); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_1, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_1, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); + QVERIFY(!clientUnminimizedSpy.wait(100)); + QVERIFY(client->isMinimized()); + + // Reopen the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // The window shortcut should be set back to Ctrl+Alt+1. + QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testShortcutRemember() +{ + QSKIP("KWin core doesn't try to save the last used window shortcut"); + + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("shortcut", "Ctrl+Alt+1"); + group.writeEntry("shortcutrule", int(Rules::Remember)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // If we press the window shortcut, the window should be brought back to user. + QSignalSpy clientUnminimizedSpy(client, &AbstractClient::clientUnminimized); + QVERIFY(clientUnminimizedSpy.isValid()); + quint32 timestamp = 1; + QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); + client->minimize(); + QVERIFY(client->isMinimized()); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_1, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_1, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); + QVERIFY(clientUnminimizedSpy.wait()); + QVERIFY(!client->isMinimized()); + + // Change the window shortcut to Ctrl+Alt+2. + client->setShortcut(QStringLiteral("Ctrl+Alt+2")); + QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_2})); + client->minimize(); + QVERIFY(client->isMinimized()); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_2, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_2, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); + QVERIFY(clientUnminimizedSpy.wait()); + QVERIFY(!client->isMinimized()); + + // Reopen the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // The window shortcut should be set to the last known value. + QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_2})); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testShortcutForce() +{ + QSKIP("KWin core can't release forced window shortcuts"); + + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("shortcut", "Ctrl+Alt+1"); + group.writeEntry("shortcutrule", int(Rules::Force)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // If we press the window shortcut, the window should be brought back to user. + QSignalSpy clientUnminimizedSpy(client, &AbstractClient::clientUnminimized); + QVERIFY(clientUnminimizedSpy.isValid()); + quint32 timestamp = 1; + QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); + client->minimize(); + QVERIFY(client->isMinimized()); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_1, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_1, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); + QVERIFY(clientUnminimizedSpy.wait()); + QVERIFY(!client->isMinimized()); + + // Any attempt to change the window shortcut should not succeed. + client->setShortcut(QStringLiteral("Ctrl+Alt+2")); + QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); + client->minimize(); + QVERIFY(client->isMinimized()); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_2, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_2, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); + QVERIFY(!clientUnminimizedSpy.wait(100)); + QVERIFY(client->isMinimized()); + + // Reopen the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // The window shortcut should still be forced. + QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testShortcutApplyNow() +{ + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(client->shortcut().isEmpty()); + + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("shortcut", "Ctrl+Alt+1"); + group.writeEntry("shortcutrule", int(Rules::ApplyNow)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // The client should now have a window shortcut assigned. + QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); + QSignalSpy clientUnminimizedSpy(client, &AbstractClient::clientUnminimized); + QVERIFY(clientUnminimizedSpy.isValid()); + quint32 timestamp = 1; + client->minimize(); + QVERIFY(client->isMinimized()); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_1, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_1, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); + QVERIFY(clientUnminimizedSpy.wait()); + QVERIFY(!client->isMinimized()); + + // Assign a different shortcut. + client->setShortcut(QStringLiteral("Ctrl+Alt+2")); + QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_2})); + client->minimize(); + QVERIFY(client->isMinimized()); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_2, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_2, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); + QVERIFY(clientUnminimizedSpy.wait()); + QVERIFY(!client->isMinimized()); + + // The rule should not be applied again. + client->evaluateWindowRules(); + QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_2})); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testShortcutForceTemporarily() +{ + QSKIP("KWin core can't release forced window shortcuts"); + + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("shortcut", "Ctrl+Alt+1"); + group.writeEntry("shortcutrule", int(Rules::ForceTemporarily)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + + // If we press the window shortcut, the window should be brought back to user. + QSignalSpy clientUnminimizedSpy(client, &AbstractClient::clientUnminimized); + QVERIFY(clientUnminimizedSpy.isValid()); + quint32 timestamp = 1; + QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); + client->minimize(); + QVERIFY(client->isMinimized()); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_1, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_1, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); + QVERIFY(clientUnminimizedSpy.wait()); + QVERIFY(!client->isMinimized()); + + // Any attempt to change the window shortcut should not succeed. + client->setShortcut(QStringLiteral("Ctrl+Alt+2")); + QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); + client->minimize(); + QVERIFY(client->isMinimized()); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_2, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_2, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); + QVERIFY(!clientUnminimizedSpy.wait(100)); + QVERIFY(client->isMinimized()); + + // The rule should be discarded when the client is closed. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(client->shortcut().isEmpty()); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::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 clients. + QSKIP("Needs changes in KWin core to pass"); +} + +void TestXdgShellClientRules::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 clients. + QSKIP("Needs changes in KWin core to pass"); +} + +void TestXdgShellClientRules::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 clients. + QSKIP("Needs changes in KWin core to pass"); +} + +void TestXdgShellClientRules::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 clients. + QSKIP("Needs changes in KWin core to pass"); +} + +void TestXdgShellClientRules::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 clients. + QSKIP("Needs changes in KWin core to pass"); +} + +void TestXdgShellClientRules::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 clients. + QSKIP("Needs changes in KWin core to pass"); +} + +void TestXdgShellClientRules::testActiveOpacityDontAffect() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("opacityactive", 90); + group.writeEntry("opacityactiverule", int(Rules::DontAffect)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(client->isActive()); + + // The opacity should not be affected by the rule. + QCOMPARE(client->opacity(), 1.0); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testActiveOpacityForce() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("opacityactive", 90); + group.writeEntry("opacityactiverule", int(Rules::Force)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(client->isActive()); + QCOMPARE(client->opacity(), 0.9); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testActiveOpacityForceTemporarily() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("opacityactive", 90); + group.writeEntry("opacityactiverule", int(Rules::ForceTemporarily)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(client->isActive()); + QCOMPARE(client->opacity(), 0.9); + + // The rule should be discarded when the client is closed. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(client->isActive()); + QCOMPARE(client->opacity(), 1.0); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testInactiveOpacityDontAffect() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("opacityinactive", 80); + group.writeEntry("opacityinactiverule", int(Rules::DontAffect)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(client->isActive()); + + // Make the client inactive. + workspace()->setActiveClient(nullptr); + QVERIFY(!client->isActive()); + + // The opacity of the client should not be affected by the rule. + QCOMPARE(client->opacity(), 1.0); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testInactiveOpacityForce() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("opacityinactive", 80); + group.writeEntry("opacityinactiverule", int(Rules::Force)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(client->isActive()); + QCOMPARE(client->opacity(), 1.0); + + // Make the client inactive. + workspace()->setActiveClient(nullptr); + QVERIFY(!client->isActive()); + + // The opacity should be forced by the rule. + QCOMPARE(client->opacity(), 0.8); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testInactiveOpacityForceTemporarily() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("opacityinactive", 80); + group.writeEntry("opacityinactiverule", int(Rules::ForceTemporarily)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + AbstractClient *client; + Surface *surface; + XdgShellSurface *shellSurface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(client->isActive()); + QCOMPARE(client->opacity(), 1.0); + + // Make the client inactive. + workspace()->setActiveClient(nullptr); + QVERIFY(!client->isActive()); + + // The opacity should be forced by the rule. + QCOMPARE(client->opacity(), 0.8); + + // The rule should be discarded when the client is closed. + delete shellSurface; + delete surface; + std::tie(client, surface, shellSurface) = createWindow("org.kde.foo"); + QVERIFY(client); + QVERIFY(client->isActive()); + QCOMPARE(client->opacity(), 1.0); + workspace()->setActiveClient(nullptr); + QVERIFY(!client->isActive()); + QCOMPARE(client->opacity(), 1.0); + + // Destroy the client. + delete shellSurface; + delete surface; + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClientRules::testMatchAfterNameChange() +{ + KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + + KConfigGroup group = config->group("1"); + group.writeEntry("above", true); + group.writeEntry("aboverule", int(Rules::Force)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QVERIFY(c->isActive()); + QCOMPARE(c->keepAbove(), false); + + QSignalSpy desktopFileNameSpy(c, &AbstractClient::desktopFileNameChanged); + QVERIFY(desktopFileNameSpy.isValid()); + + shellSurface->setAppId(QByteArrayLiteral("org.kde.foo")); + QVERIFY(desktopFileNameSpy.wait()); + QCOMPARE(c->keepAbove(), true); +} + +WAYLANDTEST_MAIN(TestXdgShellClientRules) +#include "xdgshellclient_rules_test.moc" diff --git a/autotests/integration/xdgshellclient_test.cpp b/autotests/integration/xdgshellclient_test.cpp new file mode 100644 index 0000000..11a0c8d --- /dev/null +++ b/autotests/integration/xdgshellclient_test.cpp @@ -0,0 +1,1596 @@ +/* + 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 "abstract_client.h" +#include "cursor.h" +#include "decorations/decorationbridge.h" +#include "decorations/settings.h" +#include "effects.h" +#include "deleted.h" +#include "platform.h" +#include "screens.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 + +// system +#include +#include +#include + +#include + +using namespace KWin; +using namespace KWayland::Client; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_xdgshellclient-0"); + +class TestXdgShellClient : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testMapUnmap(); + void testDesktopPresenceChanged(); + void testWindowOutputs(); + void testMinimizeActiveWindow(); + void testFullscreen_data(); + void testFullscreen(); + + void testFullscreenRestore(); + void testUserCanSetFullscreen(); + void testUserSetFullscreen(); + + void testMaximizedToFullscreen_data(); + void testMaximizedToFullscreen(); + void testWindowOpensLargerThanScreen(); + void testHidden(); + void testDesktopFileName(); + void testCaptionSimplified(); + void testCaptionMultipleWindows(); + void testUnresponsiveWindow_data(); + void testUnresponsiveWindow(); + void testX11WindowId(); + void testAppMenu(); + void testNoDecorationModeRequested(); + 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 testPointerInputTransform(); + void testReentrantSetFrameGeometry(); + void testDoubleMaximize(); +}; + +void TestXdgShellClient::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + waylandServer()->initWorkspace(); +} + +void TestXdgShellClient::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Decoration | + Test::AdditionalWaylandInterface::Seat | + Test::AdditionalWaylandInterface::XdgDecoration | + Test::AdditionalWaylandInterface::AppMenu)); + QVERIFY(Test::waitForWaylandPointer()); + + screens()->setCurrent(0); + KWin::Cursors::self()->mouse()->setPos(QPoint(1280, 512)); +} + +void TestXdgShellClient::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void TestXdgShellClient::testMapUnmap() +{ + // This test verifies that the compositor destroys XdgToplevelClient when the + // associated xdg_toplevel surface is unmapped. + + // Create a wl_surface and an xdg_toplevel, but don't commit them yet! + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface( + Test::createXdgShellStableSurface(surface.data(), nullptr, Test::CreationSetup::CreateOnly)); + + QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(clientAddedSpy.isValid()); + + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + + // Tell the compositor that we want to map the surface. + surface->commit(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.data(), QSize(100, 50), Qt::blue); + QVERIFY(clientAddedSpy.wait()); + QCOMPARE(clientAddedSpy.count(), 1); + AbstractClient *client = clientAddedSpy.last().first().value(); + QVERIFY(client); + QCOMPARE(client->readyForPainting(), true); + + // When the client 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(Buffer::Ptr()); + surface->commit(Surface::CommitFlag::None); + QVERIFY(Test::waitForWindowDestroyed(client)); + + // Tell the compositor that we want to re-map the xdg_toplevel surface. + surface->commit(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.data(), QSize(100, 50), Qt::blue); + QVERIFY(clientAddedSpy.wait()); + QCOMPARE(clientAddedSpy.count(), 2); + client = clientAddedSpy.last().first().value(); + QVERIFY(client); + QCOMPARE(client->readyForPainting(), true); + + // The compositor will respond with a configure event. + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 4); + + // Destroy the test client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClient::testDesktopPresenceChanged() +{ + // this test verifies that the desktop presence changed signals are properly emitted + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(c->desktop(), 1); + effects->setNumberOfDesktops(4); + QSignalSpy desktopPresenceChangedClientSpy(c, &AbstractClient::desktopPresenceChanged); + QVERIFY(desktopPresenceChangedClientSpy.isValid()); + QSignalSpy desktopPresenceChangedWorkspaceSpy(workspace(), &Workspace::desktopPresenceChanged); + QVERIFY(desktopPresenceChangedWorkspaceSpy.isValid()); + QSignalSpy desktopPresenceChangedEffectsSpy(effects, &EffectsHandler::desktopPresenceChanged); + QVERIFY(desktopPresenceChangedEffectsSpy.isValid()); + + // let's change the desktop + workspace()->sendClientToDesktop(c, 2, false); + QCOMPARE(c->desktop(), 2); + QCOMPARE(desktopPresenceChangedClientSpy.count(), 1); + QCOMPARE(desktopPresenceChangedWorkspaceSpy.count(), 1); + QCOMPARE(desktopPresenceChangedEffectsSpy.count(), 1); + + // verify the arguments + QCOMPARE(desktopPresenceChangedClientSpy.first().at(0).value(), c); + QCOMPARE(desktopPresenceChangedClientSpy.first().at(1).toInt(), 1); + QCOMPARE(desktopPresenceChangedWorkspaceSpy.first().at(0).value(), c); + QCOMPARE(desktopPresenceChangedWorkspaceSpy.first().at(1).toInt(), 1); + QCOMPARE(desktopPresenceChangedEffectsSpy.first().at(0).value(), c->effectWindow()); + QCOMPARE(desktopPresenceChangedEffectsSpy.first().at(1).toInt(), 1); + QCOMPARE(desktopPresenceChangedEffectsSpy.first().at(2).toInt(), 2); +} + +void TestXdgShellClient::testWindowOutputs() +{ + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + auto size = QSize(200,200); + + QSignalSpy outputEnteredSpy(surface.data(), &Surface::outputEntered); + QSignalSpy outputLeftSpy(surface.data(), &Surface::outputLeft); + + auto c = Test::renderAndWaitForShown(surface.data(), size, Qt::blue); + //move to be in the first screen + c->setFrameGeometry(QRect(QPoint(100,100), size)); + //we don't don't know where the compositor first placed this window, + //this might fire, it might not + outputEnteredSpy.wait(5); + outputEnteredSpy.clear(); + + QCOMPARE(surface->outputs().count(), 1); + QCOMPARE(surface->outputs().first()->globalPosition(), QPoint(0,0)); + + //move to overlapping both first and second screen + c->setFrameGeometry(QRect(QPoint(1250,100), size)); + QVERIFY(outputEnteredSpy.wait()); + QCOMPARE(outputEnteredSpy.count(), 1); + QCOMPARE(outputLeftSpy.count(), 0); + QCOMPARE(surface->outputs().count(), 2); + QVERIFY(surface->outputs()[0] != surface->outputs()[1]); + + //move entirely into second screen + c->setFrameGeometry(QRect(QPoint(1400,100), size)); + QVERIFY(outputLeftSpy.wait()); + QCOMPARE(outputEnteredSpy.count(), 1); + QCOMPARE(outputLeftSpy.count(), 1); + QCOMPARE(surface->outputs().count(), 1); + QCOMPARE(surface->outputs().first()->globalPosition(), QPoint(1280,0)); +} + +void TestXdgShellClient::testMinimizeActiveWindow() +{ + // this test verifies that when minimizing the active window it gets deactivated + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QVERIFY(c->isActive()); + QCOMPARE(workspace()->activeClient(), c); + QVERIFY(c->wantsInput()); + QVERIFY(c->wantsTabFocus()); + QVERIFY(c->isShown(true)); + + workspace()->slotWindowMinimize(); + QVERIFY(!c->isShown(true)); + QVERIFY(c->wantsInput()); + QVERIFY(c->wantsTabFocus()); + QVERIFY(!c->isActive()); + QVERIFY(!workspace()->activeClient()); + QVERIFY(c->isMinimized()); + + // unminimize again + c->unminimize(); + QVERIFY(!c->isMinimized()); + QVERIFY(c->isActive()); + QVERIFY(c->wantsInput()); + QVERIFY(c->wantsTabFocus()); + QVERIFY(c->isShown(true)); + QCOMPARE(workspace()->activeClient(), c); +} + +void TestXdgShellClient::testFullscreen_data() +{ + QTest::addColumn("decoMode"); + + QTest::newRow("client-side deco") << ServerSideDecoration::Mode::Client; + QTest::newRow("server-side deco") << ServerSideDecoration::Mode::Server; +} + +void TestXdgShellClient::testFullscreen() +{ + // this test verifies that a window can be properly fullscreened + + XdgShellSurface::States states; + + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(shellSurface); + + // create deco + QScopedPointer deco(Test::waylandServerSideDecoration()->create(surface.data())); + QSignalSpy decoSpy(deco.data(), &ServerSideDecoration::modeChanged); + QVERIFY(decoSpy.isValid()); + QVERIFY(decoSpy.wait()); + QFETCH(ServerSideDecoration::Mode, decoMode); + deco->requestMode(decoMode); + QVERIFY(decoSpy.wait()); + QCOMPARE(deco->mode(), decoMode); + + auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QCOMPARE(client->layer(), NormalLayer); + QVERIFY(!client->isFullScreen()); + QCOMPARE(client->clientSize(), QSize(100, 50)); + QCOMPARE(client->isDecorated(), decoMode == ServerSideDecoration::Mode::Server); + QCOMPARE(client->clientSizeToFrameSize(client->clientSize()), client->size()); + + QSignalSpy fullScreenChangedSpy(client, &AbstractClient::fullScreenChanged); + QVERIFY(fullScreenChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + + // Wait for the compositor to send a configure event with the Activated state. + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 1); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states & XdgShellSurface::State::Activated); + + // Ask the compositor to show the window in full screen mode. + shellSurface->setFullscreen(true); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 2); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states & XdgShellSurface::State::Fullscreen); + QCOMPARE(configureRequestedSpy.last().at(0).value(), screens()->size(0)); + + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + Test::render(surface.data(), configureRequestedSpy.last().at(0).value(), Qt::red); + +#if 0 // TODO: Uncomment when full screen state updates are truly asynchronous. + QVERIFY(fullScreenChangedSpy.wait()); + QCOMPARE(fullScreenChangedSpy.count(), 1); +#else + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(fullScreenChangedSpy.count(), 1); +#endif + QVERIFY(client->isFullScreen()); + QVERIFY(!client->isDecorated()); + QCOMPARE(client->layer(), ActiveLayer); + QCOMPARE(client->frameGeometry(), QRect(QPoint(0, 0), screens()->size(0))); + + // Ask the compositor to show the window in normal mode. + shellSurface->setFullscreen(false); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 3); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(!(states & XdgShellSurface::State::Fullscreen)); + QCOMPARE(configureRequestedSpy.last().at(0).value(), QSize(100, 50)); + + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + Test::render(surface.data(), configureRequestedSpy.last().at(0).value(), Qt::blue); + +#if 0 // TODO: Uncomment when full screen state updates are truly asynchronous. + QVERIFY(fullScreenChangedSpy.wait()); + QCOMPARE(fullScreenChangedSpy.count(), 2); +#else + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(fullScreenChangedSpy.count(), 2); +#endif + QCOMPARE(client->clientSize(), QSize(100, 50)); + QVERIFY(!client->isFullScreen()); + QCOMPARE(client->isDecorated(), decoMode == ServerSideDecoration::Mode::Server); + QCOMPARE(client->layer(), NormalLayer); + + // Destroy the client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClient::testFullscreenRestore() +{ + // this test verifies that windows created fullscreen can be later properly restored + QScopedPointer surface(Test::createSurface()); + XdgShellSurface *xdgShellSurface = Test::createXdgShellStableSurface(surface.data(), surface.data(), Test::CreationSetup::CreateOnly); + QSignalSpy configureRequestedSpy(xdgShellSurface, &XdgShellSurface::configureRequested); + + // fullscreen the window + xdgShellSurface->setFullscreen(true); + surface->commit(Surface::CommitFlag::None); + + configureRequestedSpy.wait(); + QCOMPARE(configureRequestedSpy.count(), 1); + + const auto size = configureRequestedSpy.first()[0].value(); + const auto state = configureRequestedSpy.first()[1].value(); + + QCOMPARE(size, screens()->size(0)); + QVERIFY(state & KWayland::Client::XdgShellSurface::State::Fullscreen); + xdgShellSurface->ackConfigure(configureRequestedSpy.first()[2].toUInt()); + + auto c = Test::renderAndWaitForShown(surface.data(), size, Qt::blue); + QVERIFY(c); + QVERIFY(c->isFullScreen()); + + configureRequestedSpy.wait(100); + + QSignalSpy fullscreenChangedSpy(c, &AbstractClient::fullScreenChanged); + QVERIFY(fullscreenChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(c, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); + + // swap back to normal + configureRequestedSpy.clear(); + xdgShellSurface->setFullscreen(false); + + QVERIFY(fullscreenChangedSpy.wait()); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.last().first().toSize(), QSize(0, 0)); + QVERIFY(!c->isFullScreen()); + + for (const auto &it: configureRequestedSpy) { + xdgShellSurface->ackConfigure(it[2].toUInt()); + } + + Test::render(surface.data(), QSize(100, 50), Qt::red); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(frameGeometryChangedSpy.count(), 1); + QVERIFY(!c->isFullScreen()); + QCOMPARE(c->frameGeometry().size(), QSize(100, 50)); +} + +void TestXdgShellClient::testUserCanSetFullscreen() +{ + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QVERIFY(c->isActive()); + QVERIFY(!c->isFullScreen()); + QVERIFY(c->userCanSetFullScreen()); +} + +void TestXdgShellClient::testUserSetFullscreen() +{ + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface( + surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + QVERIFY(!shellSurface.isNull()); + + // wait for the initial configure event + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + surface->commit(Surface::CommitFlag::None); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 1); + + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QVERIFY(c->isActive()); + QVERIFY(!c->isFullScreen()); + + // The client gets activated, which gets another configure event. Though that's not relevant to the test + configureRequestedSpy.wait(10); + + QSignalSpy fullscreenChangedSpy(c, &AbstractClient::fullScreenChanged); + QVERIFY(fullscreenChangedSpy.isValid()); + c->setFullScreen(true); + QCOMPARE(c->isFullScreen(), true); + configureRequestedSpy.clear(); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 1); + QCOMPARE(configureRequestedSpy.first().at(0).toSize(), screens()->size(0)); + const auto states = configureRequestedSpy.first().at(1).value(); + QVERIFY(states.testFlag(KWayland::Client::XdgShellSurface::State::Fullscreen)); + QVERIFY(states.testFlag(KWayland::Client::XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(KWayland::Client::XdgShellSurface::State::Maximized)); + QVERIFY(!states.testFlag(KWayland::Client::XdgShellSurface::State::Resizing)); + QCOMPARE(fullscreenChangedSpy.count(), 1); + QVERIFY(c->isFullScreen()); + + shellSurface->ackConfigure(configureRequestedSpy.first().at(2).value()); + + // unset fullscreen again + c->setFullScreen(false); + QCOMPARE(c->isFullScreen(), false); + configureRequestedSpy.clear(); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 1); + QCOMPARE(configureRequestedSpy.first().at(0).toSize(), QSize(100, 50)); + QVERIFY(!configureRequestedSpy.first().at(1).value().testFlag(KWayland::Client::XdgShellSurface::State::Fullscreen)); + QCOMPARE(fullscreenChangedSpy.count(), 2); + QVERIFY(!c->isFullScreen()); +} + +void TestXdgShellClient::testMaximizedToFullscreen_data() +{ + QTest::addColumn("decoMode"); + + QTest::newRow("client-side deco") << ServerSideDecoration::Mode::Client; + QTest::newRow("server-side deco") << ServerSideDecoration::Mode::Server; +} + +void TestXdgShellClient::testMaximizedToFullscreen() +{ + // this test verifies that a window can be properly fullscreened after maximizing + + XdgShellSurface::States states; + + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QVERIFY(shellSurface); + + // create deco + QScopedPointer deco(Test::waylandServerSideDecoration()->create(surface.data())); + QSignalSpy decoSpy(deco.data(), &ServerSideDecoration::modeChanged); + QVERIFY(decoSpy.isValid()); + QVERIFY(decoSpy.wait()); + QFETCH(ServerSideDecoration::Mode, decoMode); + deco->requestMode(decoMode); + QVERIFY(decoSpy.wait()); + QCOMPARE(deco->mode(), decoMode); + + auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(!client->isFullScreen()); + QCOMPARE(client->clientSize(), QSize(100, 50)); + QCOMPARE(client->isDecorated(), decoMode == ServerSideDecoration::Mode::Server); + + QSignalSpy fullscreenChangedSpy(client, &AbstractClient::fullScreenChanged); + QVERIFY(fullscreenChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + + // Wait for the compositor to send a configure event with the Activated state. + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 1); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states & XdgShellSurface::State::Activated); + + // Ask the compositor to maximize the window. + shellSurface->setMaximized(true); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 2); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states & XdgShellSurface::State::Maximized); + + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + Test::render(surface.data(), configureRequestedSpy.last().at(0).value(), Qt::red); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(client->maximizeMode(), MaximizeFull); + + // Ask the compositor to show the window in full screen mode. + shellSurface->setFullscreen(true); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 3); + QCOMPARE(configureRequestedSpy.last().at(0).value(), screens()->size(0)); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states & XdgShellSurface::State::Maximized); + QVERIFY(states & XdgShellSurface::State::Fullscreen); + + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + Test::render(surface.data(), configureRequestedSpy.last().at(0).value(), Qt::red); + +#if 0 // TODO: Uncomment when full screen changes are truly asynchronous. + QVERIFY(fullScreenChangedSpy.wait()); + QCOMPARE(fullScreenChangedSpy.count(), 1); +#else + QTRY_COMPARE(fullscreenChangedSpy.count(), 1); +#endif + QCOMPARE(client->maximizeMode(), MaximizeFull); + QVERIFY(client->isFullScreen()); + QVERIFY(!client->isDecorated()); + + // Switch back to normal mode. + shellSurface->setFullscreen(false); + shellSurface->setMaximized(false); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 4); + QCOMPARE(configureRequestedSpy.last().at(0).value(), QSize(100, 50)); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(!(states & XdgShellSurface::State::Maximized)); + QVERIFY(!(states & XdgShellSurface::State::Fullscreen)); + + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + Test::render(surface.data(), configureRequestedSpy.last().at(0).value(), Qt::red); + + QVERIFY(frameGeometryChangedSpy.wait()); + QVERIFY(!client->isFullScreen()); + QCOMPARE(client->isDecorated(), decoMode == ServerSideDecoration::Mode::Server); + QCOMPARE(client->maximizeMode(), MaximizeRestore); + + // Destroy the client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClient::testWindowOpensLargerThanScreen() +{ + // this test creates a window which is as large as the screen, but is decorated + // the window should get resized to fit into the screen, BUG: 366632 + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QSignalSpy sizeChangeRequestedSpy(shellSurface.data(), SIGNAL(sizeChanged(QSize))); + QVERIFY(sizeChangeRequestedSpy.isValid()); + + // create deco + QScopedPointer deco(Test::waylandServerSideDecoration()->create(surface.data())); + QSignalSpy decoSpy(deco.data(), &ServerSideDecoration::modeChanged); + QVERIFY(decoSpy.isValid()); + QVERIFY(decoSpy.wait()); + deco->requestMode(ServerSideDecoration::Mode::Server); + QVERIFY(decoSpy.wait()); + QCOMPARE(deco->mode(), ServerSideDecoration::Mode::Server); + + auto c = Test::renderAndWaitForShown(surface.data(), screens()->size(0), Qt::blue); + QVERIFY(c); + QVERIFY(c->isActive()); + QVERIFY(c->isDecorated()); + QEXPECT_FAIL("", "BUG 366632", Continue); + QCOMPARE(c->frameGeometry(), QRect(QPoint(0, 0), screens()->size(0))); +} + +void TestXdgShellClient::testHidden() +{ + // this test verifies that when hiding window it doesn't get shown + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QVERIFY(c->isActive()); + QCOMPARE(workspace()->activeClient(), c); + QVERIFY(c->wantsInput()); + QVERIFY(c->wantsTabFocus()); + QVERIFY(c->isShown(true)); + + c->hideClient(true); + QVERIFY(!c->isShown(true)); + QVERIFY(!c->isActive()); + QVERIFY(c->wantsInput()); + QVERIFY(c->wantsTabFocus()); + + // unhide again + c->hideClient(false); + QVERIFY(c->isShown(true)); + QVERIFY(c->wantsInput()); + QVERIFY(c->wantsTabFocus()); + + //QCOMPARE(workspace()->activeClient(), c); +} + +void TestXdgShellClient::testDesktopFileName() +{ + QIcon::setThemeName(QStringLiteral("breeze")); + // this test verifies that desktop file name is passed correctly to the window + QScopedPointer surface(Test::createSurface()); + // only xdg-shell as ShellSurface misses the setter + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + shellSurface->setAppId(QByteArrayLiteral("org.kde.foo")); + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(c->desktopFileName(), QByteArrayLiteral("org.kde.foo")); + QCOMPARE(c->resourceClass(), QByteArrayLiteral("org.kde.foo")); + QVERIFY(c->resourceName().startsWith("testXdgShellClient")); + // the desktop file does not exist, so icon should be generic Wayland + QCOMPARE(c->icon().name(), QStringLiteral("wayland")); + + QSignalSpy desktopFileNameChangedSpy(c, &AbstractClient::desktopFileNameChanged); + QVERIFY(desktopFileNameChangedSpy.isValid()); + QSignalSpy iconChangedSpy(c, &AbstractClient::iconChanged); + QVERIFY(iconChangedSpy.isValid()); + shellSurface->setAppId(QByteArrayLiteral("org.kde.bar")); + QVERIFY(desktopFileNameChangedSpy.wait()); + QCOMPARE(c->desktopFileName(), QByteArrayLiteral("org.kde.bar")); + QCOMPARE(c->resourceClass(), QByteArrayLiteral("org.kde.bar")); + QVERIFY(c->resourceName().startsWith("testXdgShellClient")); + // icon should still be wayland + QCOMPARE(c->icon().name(), QStringLiteral("wayland")); + QVERIFY(iconChangedSpy.isEmpty()); + + const QString dfPath = QFINDTESTDATA("data/example.desktop"); + shellSurface->setAppId(dfPath.toUtf8()); + QVERIFY(desktopFileNameChangedSpy.wait()); + QCOMPARE(iconChangedSpy.count(), 1); + QCOMPARE(QString::fromUtf8(c->desktopFileName()), dfPath); + QCOMPARE(c->icon().name(), QStringLiteral("kwin")); +} + +void TestXdgShellClient::testCaptionSimplified() +{ + // this test verifies that caption is properly trimmed + // see BUG 323798 comment #12 + QScopedPointer surface(Test::createSurface()); + // only done for xdg-shell as ShellSurface misses the setter + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + 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->setTitle(origTitle); + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QVERIFY(c->caption() != origTitle); + QCOMPARE(c->caption(), origTitle.simplified()); +} + +void TestXdgShellClient::testCaptionMultipleWindows() +{ + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + shellSurface->setTitle(QStringLiteral("foo")); + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(c->caption(), QStringLiteral("foo")); + QCOMPARE(c->captionNormal(), QStringLiteral("foo")); + QCOMPARE(c->captionSuffix(), QString()); + + QScopedPointer surface2(Test::createSurface()); + QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data())); + shellSurface2->setTitle(QStringLiteral("foo")); + auto c2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 50), Qt::blue); + QVERIFY(c2); + QCOMPARE(c2->caption(), QStringLiteral("foo <2>")); + QCOMPARE(c2->captionNormal(), QStringLiteral("foo")); + QCOMPARE(c2->captionSuffix(), QStringLiteral(" <2>")); + + QScopedPointer surface3(Test::createSurface()); + QScopedPointer shellSurface3(Test::createXdgShellStableSurface(surface3.data())); + shellSurface3->setTitle(QStringLiteral("foo")); + auto c3 = Test::renderAndWaitForShown(surface3.data(), QSize(100, 50), Qt::blue); + QVERIFY(c3); + QCOMPARE(c3->caption(), QStringLiteral("foo <3>")); + QCOMPARE(c3->captionNormal(), QStringLiteral("foo")); + QCOMPARE(c3->captionSuffix(), QStringLiteral(" <3>")); + + QScopedPointer surface4(Test::createSurface()); + QScopedPointer shellSurface4(Test::createXdgShellStableSurface(surface4.data())); + shellSurface4->setTitle(QStringLiteral("bar")); + auto c4 = Test::renderAndWaitForShown(surface4.data(), QSize(100, 50), Qt::blue); + QVERIFY(c4); + QCOMPARE(c4->caption(), QStringLiteral("bar")); + QCOMPARE(c4->captionNormal(), QStringLiteral("bar")); + QCOMPARE(c4->captionSuffix(), QString()); + QSignalSpy captionChangedSpy(c4, &AbstractClient::captionChanged); + QVERIFY(captionChangedSpy.isValid()); + shellSurface4->setTitle(QStringLiteral("foo")); + QVERIFY(captionChangedSpy.wait()); + QCOMPARE(captionChangedSpy.count(), 1); + QCOMPARE(c4->caption(), QStringLiteral("foo <4>")); + QCOMPARE(c4->captionNormal(), QStringLiteral("foo")); + QCOMPARE(c4->captionSuffix(), QStringLiteral(" <4>")); +} + +void TestXdgShellClient::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 TestXdgShellClient::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 clientAddedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(clientAddedSpy.isValid()); + + QScopedPointer 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.data(), &QProcess::started}; + QVERIFY(processStartedSpy.isValid()); + process->start(); + QVERIFY(processStartedSpy.wait()); + + AbstractClient *killClient = nullptr; + if (clientAddedSpy.isEmpty()) { + QVERIFY(clientAddedSpy.wait()); + } + ::kill(process->processId(), SIGUSR1); // send a signal to freeze the process + + killClient = clientAddedSpy.first().first().value(); + QVERIFY(killClient); + QSignalSpy unresponsiveSpy(killClient, &AbstractClient::unresponsiveChanged); + QSignalSpy killedSpy(process.data(), static_cast(&QProcess::finished)); + QSignalSpy deletedSpy(killClient, &QObject::destroyed); + + qint64 startTime = QDateTime::currentMSecsSinceEpoch(); + + //wait for the process to be frozen + QTest::qWait(10); + + //pretend the user clicked the close button + killClient->closeWindow(); + + //client should not yet be marked unresponsive nor killed + QVERIFY(!killClient->unresponsive()); + QVERIFY(killedSpy.isEmpty()); + + QVERIFY(unresponsiveSpy.wait()); + //client should be marked unresponsive but not killed + auto elapsed1 = QDateTime::currentMSecsSinceEpoch() - startTime; + QVERIFY(elapsed1 > 900 && elapsed1 < 1200); //ping timer is 1s, but coarse timers on a test across two processes means we need a fuzzy compare + QVERIFY(killClient->unresponsive()); + QVERIFY(killedSpy.isEmpty()); + + QVERIFY(deletedSpy.wait()); + if (!socketMode) { + //process was killed - because we're across process this could happen in either order + QVERIFY(killedSpy.count() || killedSpy.wait()); + } + + auto elapsed2 = QDateTime::currentMSecsSinceEpoch() - startTime; + QVERIFY(elapsed2 > 1800); //second ping comes in a second later +} + +void TestXdgShellClient::testX11WindowId() +{ + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QVERIFY(c->windowId() != 0); + QCOMPARE(c->window(), 0u); +} + +void TestXdgShellClient::testAppMenu() +{ + //register a faux appmenu client + QVERIFY (QDBusConnection::sessionBus().registerService("org.kde.kappmenu")); + + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QScopedPointer menu(Test::waylandAppMenuManager()->create(surface.data())); + QSignalSpy spy(c, &AbstractClient::hasApplicationMenuChanged); + menu->setAddress("service.name", "object/path"); + spy.wait(); + QCOMPARE(c->hasApplicationMenu(), true); + QCOMPARE(c->applicationMenuServiceName(), QString("service.name")); + QCOMPARE(c->applicationMenuObjectPath(), QString("object/path")); + + QVERIFY (QDBusConnection::sessionBus().unregisterService("org.kde.kappmenu")); +} + +void TestXdgShellClient::testNoDecorationModeRequested() +{ + // this test verifies that the decoration follows the default mode if no mode is explicitly requested + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QScopedPointer deco(Test::waylandServerSideDecoration()->create(surface.data())); + QSignalSpy decoSpy(deco.data(), &ServerSideDecoration::modeChanged); + QVERIFY(decoSpy.isValid()); + if (deco->mode() != ServerSideDecoration::Mode::Server) { + QVERIFY(decoSpy.wait()); + } + QCOMPARE(deco->mode(), ServerSideDecoration::Mode::Server); + + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(c->noBorder(), false); + QCOMPARE(c->isDecorated(), true); +} + +void TestXdgShellClient::testSendClientWithTransientToDesktop() +{ + // this test verifies that when sending a client to a desktop all transients are also send to that desktop + + VirtualDesktopManager::self()->setCount(2); + QScopedPointer surface{Test::createSurface()}; + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + + // let's create a transient window + QScopedPointer transientSurface{Test::createSurface()}; + QScopedPointer transientShellSurface(Test::createXdgShellStableSurface(transientSurface.data())); + transientShellSurface->setTransientFor(shellSurface.data()); + + auto transient = Test::renderAndWaitForShown(transientSurface.data(), QSize(100, 50), Qt::blue); + QVERIFY(transient); + QCOMPARE(workspace()->activeClient(), transient); + QCOMPARE(transient->transientFor(), c); + QVERIFY(c->transients().contains(transient)); + + QCOMPARE(c->desktop(), 1); + QVERIFY(!c->isOnAllDesktops()); + QCOMPARE(transient->desktop(), 1); + QVERIFY(!transient->isOnAllDesktops()); + workspace()->slotWindowToDesktop(2); + + QCOMPARE(c->desktop(), 1); + QCOMPARE(transient->desktop(), 2); + + // activate c + workspace()->activateClient(c); + QCOMPARE(workspace()->activeClient(), c); + QVERIFY(c->isActive()); + + // and send it to the desktop it's already on + QCOMPARE(c->desktop(), 1); + QCOMPARE(transient->desktop(), 2); + workspace()->slotWindowToDesktop(1); + + // which should move the transient back to the desktop + QCOMPARE(c->desktop(), 1); + QCOMPARE(transient->desktop(), 1); +} + +void TestXdgShellClient::testMinimizeWindowWithTransients() +{ + // this test verifies that when minimizing/unminimizing a window all its + // transients will be minimized/unminimized as well + + // create the main window + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QVERIFY(!c->isMinimized()); + + // create a transient window + QScopedPointer transientSurface(Test::createSurface()); + QScopedPointer transientShellSurface(Test::createXdgShellStableSurface(transientSurface.data())); + transientShellSurface->setTransientFor(shellSurface.data()); + auto transient = Test::renderAndWaitForShown(transientSurface.data(), QSize(100, 50), Qt::red); + QVERIFY(transient); + QVERIFY(!transient->isMinimized()); + QCOMPARE(transient->transientFor(), c); + QVERIFY(c->hasTransient(transient, false)); + + // minimize the main window, the transient should be minimized as well + c->minimize(); + QVERIFY(c->isMinimized()); + QVERIFY(transient->isMinimized()); + + // unminimize the main window, the transient should be unminimized as well + c->unminimize(); + QVERIFY(!c->isMinimized()); + QVERIFY(!transient->isMinimized()); +} + +void TestXdgShellClient::testXdgDecoration_data() +{ + QTest::addColumn("requestedMode"); + QTest::addColumn("expectedMode"); + + QTest::newRow("client side requested") << XdgDecoration::Mode::ClientSide << XdgDecoration::Mode::ClientSide; + QTest::newRow("server side requested") << XdgDecoration::Mode::ServerSide << XdgDecoration::Mode::ServerSide; +} + +void TestXdgShellClient::testXdgDecoration() +{ + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + QScopedPointer deco(Test::xdgDecorationManager()->getToplevelDecoration(shellSurface.data())); + + QSignalSpy decorationConfiguredSpy(deco.data(), &XdgDecoration::modeChanged); + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + + QFETCH(KWayland::Client::XdgDecoration::Mode, requestedMode); + QFETCH(KWayland::Client::XdgDecoration::Mode, expectedMode); + + //request a mode + deco->setMode(requestedMode); + + //kwin will send a configure + decorationConfiguredSpy.wait(); + configureRequestedSpy.wait(); + + QCOMPARE(decorationConfiguredSpy.count(), 1); + QCOMPARE(decorationConfiguredSpy.first()[0].value(), expectedMode); + QVERIFY(configureRequestedSpy.count() > 0); + + shellSurface->ackConfigure(configureRequestedSpy.last()[2].toInt()); + + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QCOMPARE(c->userCanSetNoBorder(), expectedMode == XdgDecoration::Mode::ServerSide); + QCOMPARE(c->isDecorated(), expectedMode == XdgDecoration::Mode::ServerSide); +} + +void TestXdgShellClient::testXdgNeverCommitted() +{ + //check we don't crash if we create a shell object but delete the XdgShellClient before committing it + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data(), nullptr, Test::CreationSetup::CreateOnly)); +} + +void TestXdgShellClient::testXdgInitialState() +{ + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data(), nullptr, Test::CreationSetup::CreateOnly)); + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + surface->commit(Surface::CommitFlag::None); + + configureRequestedSpy.wait(); + + QCOMPARE(configureRequestedSpy.count(), 1); + + const auto size = configureRequestedSpy.first()[0].value(); + + QCOMPARE(size, QSize(0, 0)); //client should chose it's preferred size + + shellSurface->ackConfigure(configureRequestedSpy.first()[2].toUInt()); + + auto c = Test::renderAndWaitForShown(surface.data(), QSize(200,100), Qt::blue); + QCOMPARE(c->size(), QSize(200, 100)); +} + +void TestXdgShellClient::testXdgInitiallyMaximised() +{ + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data(), nullptr, Test::CreationSetup::CreateOnly)); + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + + shellSurface->setMaximized(true); + surface->commit(Surface::CommitFlag::None); + + configureRequestedSpy.wait(); + + QCOMPARE(configureRequestedSpy.count(), 1); + + const auto size = configureRequestedSpy.first()[0].value(); + const auto state = configureRequestedSpy.first()[1].value(); + + QCOMPARE(size, QSize(1280, 1024)); + QVERIFY(state & KWayland::Client::XdgShellSurface::State::Maximized); + + shellSurface->ackConfigure(configureRequestedSpy.first()[2].toUInt()); + + auto c = Test::renderAndWaitForShown(surface.data(), size, Qt::blue); + QCOMPARE(c->maximizeMode(), MaximizeFull); + QCOMPARE(c->size(), QSize(1280, 1024)); +} + +void TestXdgShellClient::testXdgInitiallyFullscreen() +{ + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data(), nullptr, Test::CreationSetup::CreateOnly)); + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + + shellSurface->setFullscreen(true); + surface->commit(Surface::CommitFlag::None); + + configureRequestedSpy.wait(); + + QCOMPARE(configureRequestedSpy.count(), 1); + + const auto size = configureRequestedSpy.first()[0].value(); + const auto state = configureRequestedSpy.first()[1].value(); + + QCOMPARE(size, QSize(1280, 1024)); + QVERIFY(state & KWayland::Client::XdgShellSurface::State::Fullscreen); + + shellSurface->ackConfigure(configureRequestedSpy.first()[2].toUInt()); + + auto c = Test::renderAndWaitForShown(surface.data(), size, Qt::blue); + QCOMPARE(c->isFullScreen(), true); + QCOMPARE(c->size(), QSize(1280, 1024)); +} + +void TestXdgShellClient::testXdgInitiallyMinimized() +{ + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data(), nullptr, Test::CreationSetup::CreateOnly)); + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + + shellSurface->requestMinimize(); + surface->commit(Surface::CommitFlag::None); + + configureRequestedSpy.wait(); + + QCOMPARE(configureRequestedSpy.count(), 1); + + const auto size = configureRequestedSpy.first()[0].value(); + const auto state = configureRequestedSpy.first()[1].value(); + + QCOMPARE(size, QSize(0, 0)); + QCOMPARE(state, 0); + + shellSurface->ackConfigure(configureRequestedSpy.first()[2].toUInt()); + + QEXPECT_FAIL("", "Client created in a minimised state is not exposed to kwin bug 404838", Abort); + auto c = Test::renderAndWaitForShown(surface.data(), size, Qt::blue, QImage::Format_ARGB32, 10); + QVERIFY(c); + QVERIFY(c->isMinimized()); +} + +void TestXdgShellClient::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 client. + + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(200, 100), Qt::red); + QVERIFY(client); + QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); + QCOMPARE(client->frameGeometry().size(), QSize(200, 100)); + + const QPoint oldPosition = client->pos(); + + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); + Test::render(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(client->frameGeometry().topLeft(), oldPosition); + QCOMPARE(client->frameGeometry().size(), QSize(100, 50)); + QCOMPARE(client->bufferGeometry().topLeft(), oldPosition); + QCOMPARE(client->bufferGeometry().size(), QSize(100, 50)); + + QScopedPointer childSurface(Test::createSurface()); + QScopedPointer subSurface(Test::createSubSurface(childSurface.data(), surface.data())); + QVERIFY(subSurface); + subSurface->setPosition(QPoint(-20, -10)); + Test::render(childSurface.data(), QSize(100, 50), Qt::blue); + surface->commit(Surface::CommitFlag::None); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(client->frameGeometry().topLeft(), oldPosition); + QCOMPARE(client->frameGeometry().size(), QSize(120, 60)); + QCOMPARE(client->bufferGeometry().topLeft(), oldPosition + QPoint(20, 10)); + QCOMPARE(client->bufferGeometry().size(), QSize(100, 50)); +} + +void TestXdgShellClient::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. + + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(200, 100), Qt::red); + QVERIFY(client); + QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); + QCOMPARE(client->frameGeometry().size(), QSize(200, 100)); + + const QPoint oldPosition = client->pos(); + + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); + shellSurface->setWindowGeometry(QRect(10, 10, 180, 80)); + surface->commit(Surface::CommitFlag::None); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(frameGeometryChangedSpy.count(), 1); + QCOMPARE(client->frameGeometry().topLeft(), oldPosition); + QCOMPARE(client->frameGeometry().size(), QSize(180, 80)); + QCOMPARE(client->bufferGeometry().topLeft(), oldPosition - QPoint(10, 10)); + QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); + + Test::render(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(frameGeometryChangedSpy.count(), 2); + QCOMPARE(client->frameGeometry().topLeft(), oldPosition); + QCOMPARE(client->frameGeometry().size(), QSize(90, 40)); + QCOMPARE(client->bufferGeometry().topLeft(), oldPosition - QPoint(10, 10)); + QCOMPARE(client->bufferGeometry().size(), QSize(100, 50)); + + shellSurface->setWindowGeometry(QRect(0, 0, 100, 50)); + surface->commit(Surface::CommitFlag::None); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(frameGeometryChangedSpy.count(), 3); + QCOMPARE(client->frameGeometry().topLeft(), oldPosition); + QCOMPARE(client->frameGeometry().size(), QSize(100, 50)); + QCOMPARE(client->bufferGeometry().topLeft(), oldPosition); + QCOMPARE(client->bufferGeometry().size(), QSize(100, 50)); + + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClient::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. + + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(200, 100), Qt::red); + QVERIFY(client); + QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); + QCOMPARE(client->frameGeometry().size(), QSize(200, 100)); + + const QPoint oldPosition = client->pos(); + + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); + shellSurface->setWindowGeometry(QRect(10, 10, 180, 80)); + surface->commit(Surface::CommitFlag::None); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(client->frameGeometry().topLeft(), oldPosition); + QCOMPARE(client->frameGeometry().size(), QSize(180, 80)); + QCOMPARE(client->bufferGeometry().topLeft(), oldPosition - QPoint(10, 10)); + QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); + + QScopedPointer childSurface(Test::createSurface()); + QScopedPointer subSurface(Test::createSubSurface(childSurface.data(), surface.data())); + QVERIFY(subSurface); + subSurface->setPosition(QPoint(-20, -20)); + Test::render(childSurface.data(), QSize(100, 50), Qt::blue); + surface->commit(Surface::CommitFlag::None); + QCOMPARE(client->frameGeometry().topLeft(), oldPosition); + QCOMPARE(client->frameGeometry().size(), QSize(180, 80)); + QCOMPARE(client->bufferGeometry().topLeft(), oldPosition - QPoint(10, 10)); + QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); + + shellSurface->setWindowGeometry(QRect(-15, -15, 50, 40)); + surface->commit(Surface::CommitFlag::None); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(client->frameGeometry().topLeft(), oldPosition); + QCOMPARE(client->frameGeometry().size(), QSize(50, 40)); + QCOMPARE(client->bufferGeometry().topLeft(), oldPosition - QPoint(-15, -15)); + QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); +} + +void TestXdgShellClient::testXdgWindowGeometryInteractiveResize() +{ + // This test verifies that correct window geometry is provided along each + // configure event when an xdg-shell is being interactively resized. + + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(200, 100), Qt::red); + QVERIFY(client); + QVERIFY(client->isActive()); + QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); + QCOMPARE(client->frameGeometry().size(), QSize(200, 100)); + + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 1); + + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); + shellSurface->setWindowGeometry(QRect(10, 10, 180, 80)); + surface->commit(Surface::CommitFlag::None); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); + QCOMPARE(client->frameGeometry().size(), QSize(180, 80)); + + QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); + QVERIFY(clientStartMoveResizedSpy.isValid()); + QSignalSpy clientStepUserMovedResizedSpy(client, &AbstractClient::clientStepUserMovedResized); + QVERIFY(clientStepUserMovedResizedSpy.isValid()); + QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); + QVERIFY(clientFinishUserMovedResizedSpy.isValid()); + + // Start interactively resizing the client. + QCOMPARE(workspace()->moveResizeClient(), nullptr); + workspace()->slotWindowResize(); + QCOMPARE(workspace()->moveResizeClient(), client); + QCOMPARE(clientStartMoveResizedSpy.count(), 1); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 2); + XdgShellSurface::States states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Resizing)); + + // Go right. + QPoint cursorPos = KWin::Cursors::self()->mouse()->pos(); + client->keyPressEvent(Qt::Key_Right); + client->updateMoveResize(KWin::Cursors::self()->mouse()->pos()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0)); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 3); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Resizing)); + QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(188, 80)); + shellSurface->setWindowGeometry(QRect(10, 10, 188, 80)); + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + Test::render(surface.data(), QSize(208, 100), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); + QCOMPARE(client->bufferGeometry().size(), QSize(208, 100)); + QCOMPARE(client->frameGeometry().size(), QSize(188, 80)); + + // Go down. + cursorPos = KWin::Cursors::self()->mouse()->pos(); + client->keyPressEvent(Qt::Key_Down); + client->updateMoveResize(KWin::Cursors::self()->mouse()->pos()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(0, 8)); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 4); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Resizing)); + QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(188, 88)); + shellSurface->setWindowGeometry(QRect(10, 10, 188, 88)); + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + Test::render(surface.data(), QSize(208, 108), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 2); + QCOMPARE(client->bufferGeometry().size(), QSize(208, 108)); + QCOMPARE(client->frameGeometry().size(), QSize(188, 88)); + + // Finish resizing the client. + client->keyPressEvent(Qt::Key_Enter); + QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); + QCOMPARE(workspace()->moveResizeClient(), nullptr); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 5); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(!states.testFlag(XdgShellSurface::State::Resizing)); + + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClient::testXdgWindowGeometryFullScreen() +{ + // This test verifies that an xdg-shell receives correct window geometry when + // its fullscreen state gets changed. + + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(200, 100), Qt::red); + QVERIFY(client); + QVERIFY(client->isActive()); + QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); + QCOMPARE(client->frameGeometry().size(), QSize(200, 100)); + + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 1); + + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); + shellSurface->setWindowGeometry(QRect(10, 10, 180, 80)); + surface->commit(Surface::CommitFlag::None); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); + QCOMPARE(client->frameGeometry().size(), QSize(180, 80)); + + workspace()->slotWindowFullScreen(); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 2); + QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(1280, 1024)); + XdgShellSurface::States states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Fullscreen)); + shellSurface->setWindowGeometry(QRect(0, 0, 1280, 1024)); + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + Test::render(surface.data(), QSize(1280, 1024), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(client->bufferGeometry().size(), QSize(1280, 1024)); + QCOMPARE(client->frameGeometry().size(), QSize(1280, 1024)); + + workspace()->slotWindowFullScreen(); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 3); + QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(180, 80)); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(!states.testFlag(XdgShellSurface::State::Fullscreen)); + shellSurface->setWindowGeometry(QRect(10, 10, 180, 80)); + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + Test::render(surface.data(), QSize(200, 100), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); + QCOMPARE(client->frameGeometry().size(), QSize(180, 80)); + + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClient::testXdgWindowGeometryMaximize() +{ + // This test verifies that an xdg-shell receives correct window geometry when + // its maximized state gets changed. + + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(200, 100), Qt::red); + QVERIFY(client); + QVERIFY(client->isActive()); + QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); + QCOMPARE(client->frameGeometry().size(), QSize(200, 100)); + + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 1); + + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); + shellSurface->setWindowGeometry(QRect(10, 10, 180, 80)); + surface->commit(Surface::CommitFlag::None); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); + QCOMPARE(client->frameGeometry().size(), QSize(180, 80)); + + workspace()->slotWindowMaximize(); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 2); + QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(1280, 1024)); + XdgShellSurface::States states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); + shellSurface->setWindowGeometry(QRect(0, 0, 1280, 1024)); + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + Test::render(surface.data(), QSize(1280, 1024), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(client->bufferGeometry().size(), QSize(1280, 1024)); + QCOMPARE(client->frameGeometry().size(), QSize(1280, 1024)); + + workspace()->slotWindowMaximize(); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 3); + QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(180, 80)); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); + shellSurface->setWindowGeometry(QRect(10, 10, 180, 80)); + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + Test::render(surface.data(), QSize(200, 100), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); + QCOMPARE(client->frameGeometry().size(), QSize(180, 80)); + + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClient::testPointerInputTransform() +{ + // This test verifies that XdgToplevelClient 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. + QScopedPointer pointer(Test::waylandSeat()->createPointer()); + QVERIFY(pointer); + QVERIFY(pointer->isValid()); + QSignalSpy pointerEnteredSpy(pointer.data(), &KWayland::Client::Pointer::entered); + QVERIFY(pointerEnteredSpy.isValid()); + QSignalSpy pointerMotionSpy(pointer.data(), &KWayland::Client::Pointer::motion); + QVERIFY(pointerMotionSpy.isValid()); + + // Create an xdg_toplevel surface and wait for the compositor to catch up. + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(200, 100), Qt::red); + QVERIFY(client); + QVERIFY(client->isActive()); + QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); + QCOMPARE(client->frameGeometry().size(), QSize(200, 100)); + + // Enter the surface. + quint32 timestamp = 0; + kwinApp()->platform()->pointerMotion(client->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. + kwinApp()->platform()->pointerMotion(client->pos() + QPoint(10, 5), timestamp++); + QVERIFY(pointerMotionSpy.wait()); + QCOMPARE(pointerMotionSpy.last().first(), QPoint(10, 5)); + + // Let's pretend that the client has changed the extents of the client-side drop-shadow + // but the frame geometry didn't change. + QSignalSpy bufferGeometryChangedSpy(client, &AbstractClient::bufferGeometryChanged); + QVERIFY(bufferGeometryChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); + shellSurface->setWindowGeometry(QRect(10, 20, 200, 100)); + Test::render(surface.data(), QSize(220, 140), Qt::blue); + QVERIFY(bufferGeometryChangedSpy.wait()); + QCOMPARE(frameGeometryChangedSpy.count(), 0); + QCOMPARE(client->frameGeometry().size(), QSize(200, 100)); + QCOMPARE(client->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. + kwinApp()->platform()->pointerMotion(client->pos() + QPoint(20, 50), timestamp++); + QVERIFY(pointerMotionSpy.wait()); + QCOMPARE(pointerMotionSpy.last().first(), QPoint(10, 20) + QPoint(20, 50)); + + // Destroy the xdg-toplevel surface. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClient::testReentrantSetFrameGeometry() +{ + // This test verifies that calling setFrameGeometry() 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. + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(200, 100), Qt::red); + QVERIFY(client); + QCOMPARE(client->pos(), QPoint(0, 0)); + + // Let's pretend that there is a script that really wants the client to be at (100, 100). + connect(client, &AbstractClient::frameGeometryChanged, this, [client]() { + client->setFrameGeometry(QRect(QPoint(100, 100), client->size())); + }); + + // Trigger the lambda above. + client->move(QPoint(40, 50)); + + // Eventually, the client will end up at (100, 100). + QCOMPARE(client->pos(), QPoint(100, 100)); + + // Destroy the xdg-toplevel surface. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void TestXdgShellClient::testDoubleMaximize() +{ + // This test verifies that the case where a client issues two set_maximized() requests + // separated by the initial commit is handled properly. + + // Create the test surface. + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data(), nullptr, Test::CreationSetup::CreateOnly)); + shellSurface->setMaximized(true); + surface->commit(Surface::CommitFlag::None); + + // Wait for the compositor to respond with a configure event. + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 1); + QSize size = configureRequestedSpy.last().at(0).value(); + QCOMPARE(size, QSize(1280, 1024)); + XdgShellSurface::States states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); + + // Send another set_maximized() request, but do not attach any buffer yet. + shellSurface->setMaximized(true); + surface->commit(Surface::CommitFlag::None); + + // The compositor must respond with another configure event even if the state hasn't changed. + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 2); + size = configureRequestedSpy.last().at(0).value(); + QCOMPARE(size, QSize(1280, 1024)); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); +} + +WAYLANDTEST_MAIN(TestXdgShellClient) +#include "xdgshellclient_test.moc" diff --git a/autotests/integration/xwayland_input_test.cpp b/autotests/integration/xwayland_input_test.cpp new file mode 100644 index 0000000..5eef221 --- /dev/null +++ b/autotests/integration/xwayland_input_test.cpp @@ -0,0 +1,302 @@ +/* + 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 "platform.h" +#include "x11client.h" +#include "cursor.h" +#include "deleted.h" +#include "screenedge.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.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(); + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + setenv("QT_QPA_PLATFORM", "wayland", true); + waylandServer()->initWorkspace(); +} + +void XWaylandInputTest::init() +{ + screens()->setCurrent(0); + Cursors::self()->mouse()->setPos(QPoint(640, 512)); + xcb_warp_pointer(connection(), XCB_WINDOW_NONE, kwinApp()->x11RootWindow(), 0, 0, 0, 0, 640, 512); + xcb_flush(connection()); + QVERIFY(waylandServer()->clients().isEmpty()); +} + +struct XcbConnectionDeleter +{ + static inline void cleanup(xcb_connection_t *pointer) + { + xcb_disconnect(pointer); + } +}; + +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); + emit entered(QPoint(enterEvent->event_x, enterEvent->event_y)); + break; } + case XCB_LEAVE_NOTIFY: { + auto leaveEvent = reinterpret_cast(event); + 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 + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + if (xcb_get_setup(c.data())->release_number < 11800000) { + QSKIP("XWayland 1.18 required"); + } + X11EventReaderHelper eventReader(c.data()); + QSignalSpy enteredSpy(&eventReader, &X11EventReaderHelper::entered); + QVERIFY(enteredSpy.isValid()); + QSignalSpy leftSpy(&eventReader, &X11EventReaderHelper::left); + QVERIFY(leftSpy.isValid()); + // atom for the screenedge show hide functionality + Xcb::Atom atom(QByteArrayLiteral("_KDE_NET_WM_SCREEN_EDGE_SHOW"), false, c.data()); + + xcb_window_t w = xcb_generate_id(c.data()); + const QRect windowGeometry = QRect(0, 0, 100, 200); + const uint32_t values[] = { + XCB_EVENT_MASK_ENTER_WINDOW | + XCB_EVENT_MASK_LEAVE_WINDOW + }; + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, 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; + memset(&hints, 0, sizeof(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.data(), w, &hints); + NETWinInfo info(c.data(), w, rootWindow(), NET::WMAllProperties, NET::WM2AllProperties); + info.setWindowType(NET::Normal); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.last().first().value(); + QVERIFY(client); + QVERIFY(client->isDecorated()); + QVERIFY(!client->hasStrut()); + QVERIFY(!client->isHiddenInternal()); + QVERIFY(!client->readyForPainting()); + QMetaObject::invokeMethod(client, "setReadyForPainting"); + QVERIFY(client->readyForPainting()); + QVERIFY(!client->surface()); + QSignalSpy surfaceChangedSpy(client, &Toplevel::surfaceChanged); + QVERIFY(surfaceChangedSpy.isValid()); + QVERIFY(surfaceChangedSpy.wait()); + QVERIFY(client->surface()); + + // move pointer into the window, should trigger an enter + QVERIFY(!client->frameGeometry().contains(Cursors::self()->mouse()->pos())); + QVERIFY(enteredSpy.isEmpty()); + Cursors::self()->mouse()->setPos(client->frameGeometry().center()); + QCOMPARE(waylandServer()->seat()->focusedPointerSurface(), client->surface()); + QVERIFY(waylandServer()->seat()->focusedPointer()); + QVERIFY(enteredSpy.wait()); + QCOMPARE(enteredSpy.last().first(), client->frameGeometry().center() - client->clientPos()); + + // move out of window + Cursors::self()->mouse()->setPos(client->frameGeometry().bottomRight() + QPoint(10, 10)); + QVERIFY(leftSpy.wait()); + QCOMPARE(leftSpy.last().first(), client->frameGeometry().center() - client->clientPos()); + + // destroy window again + QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + xcb_unmap_window(c.data(), w); + xcb_destroy_window(c.data(), w); + xcb_flush(c.data()); + QVERIFY(windowClosedSpy.wait()); +} + +void XWaylandInputTest::testPointerEventLeaveCsd() +{ + // this test simulates a pointer enter and pointer leave on a client-side decorated X11 window + + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + + if (xcb_get_setup(c.data())->release_number < 11800000) { + QSKIP("XWayland 1.18 required"); + } + if (!Xcb::Extensions::self()->isShapeAvailable()) { + QSKIP("SHAPE extension is required"); + } + + X11EventReaderHelper eventReader(c.data()); + QSignalSpy enteredSpy(&eventReader, &X11EventReaderHelper::entered); + QVERIFY(enteredSpy.isValid()); + QSignalSpy leftSpy(&eventReader, &X11EventReaderHelper::left); + QVERIFY(leftSpy.isValid()); + + // Extents of the client-side drop-shadow. + NETStrut clientFrameExtent; + clientFrameExtent.left = 10; + clientFrameExtent.right = 10; + clientFrameExtent.top = 5; + clientFrameExtent.bottom = 20; + + // Need to set the bounding shape in order to create a window without decoration. + 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 window = xcb_generate_id(c.data()); + const uint32_t values[] = { + XCB_EVENT_MASK_ENTER_WINDOW | + XCB_EVENT_MASK_LEAVE_WINDOW + }; + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, window, 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; + memset(&hints, 0, sizeof(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.data(), window, &hints); + xcb_shape_rectangles(c.data(), XCB_SHAPE_SO_SET, XCB_SHAPE_SK_BOUNDING, + XCB_CLIP_ORDERING_UNSORTED, window, 0, 0, 1, &boundingRect); + NETWinInfo info(c.data(), window, rootWindow(), NET::WMAllProperties, NET::WM2AllProperties); + info.setWindowType(NET::Normal); + info.setGtkFrameExtents(clientFrameExtent); + xcb_map_window(c.data(), window); + xcb_flush(c.data()); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.last().first().value(); + QVERIFY(client); + QVERIFY(!client->isDecorated()); + QVERIFY(client->isClientSideDecorated()); + QCOMPARE(client->bufferGeometry(), QRect(0, 0, 120, 225)); + QCOMPARE(client->frameGeometry(), QRect(10, 5, 100, 200)); + + QMetaObject::invokeMethod(client, "setReadyForPainting"); + QVERIFY(client->readyForPainting()); + QVERIFY(!client->surface()); + QSignalSpy surfaceChangedSpy(client, &Toplevel::surfaceChanged); + QVERIFY(surfaceChangedSpy.isValid()); + QVERIFY(surfaceChangedSpy.wait()); + QVERIFY(client->surface()); + + // Move pointer into the window, should trigger an enter. + QVERIFY(!client->frameGeometry().contains(Cursors::self()->mouse()->pos())); + QVERIFY(enteredSpy.isEmpty()); + Cursors::self()->mouse()->setPos(client->frameGeometry().center()); + QCOMPARE(waylandServer()->seat()->focusedPointerSurface(), client->surface()); + QVERIFY(waylandServer()->seat()->focusedPointer()); + QVERIFY(enteredSpy.wait()); + QCOMPARE(enteredSpy.last().first(), QPoint(59, 104)); + + // Move out of the window, should trigger a leave. + QVERIFY(leftSpy.isEmpty()); + Cursors::self()->mouse()->setPos(client->frameGeometry().bottomRight() + QPoint(100, 100)); + QVERIFY(leftSpy.wait()); + QCOMPARE(leftSpy.last().first(), QPoint(59, 104)); + + // Destroy the window. + QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + xcb_unmap_window(c.data(), window); + xcb_destroy_window(c.data(), window); + xcb_flush(c.data()); + QVERIFY(windowClosedSpy.wait()); +} + +} + +WAYLANDTEST_MAIN(KWin::XWaylandInputTest) +#include "xwayland_input_test.moc" diff --git a/autotests/integration/xwayland_selections_test.cpp b/autotests/integration/xwayland_selections_test.cpp new file mode 100644 index 0000000..d4ab6b5 --- /dev/null +++ b/autotests/integration/xwayland_selections_test.cpp @@ -0,0 +1,172 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + SPDX-FileCopyrightText: 2019 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" +#include "abstract_client.h" +#include "platform.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include "../../xwl/databridge.h" + +#include + +#include +#include + +using namespace KWin; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_xwayland_selections-0"); + +class XwaylandSelectionsTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void cleanup(); + void testSync_data(); + void testSync(); + +private: + QProcess *m_copyProcess = nullptr; + QProcess *m_pasteProcess = nullptr; +}; + +void XwaylandSelectionsTest::initTestCase() +{ + QSKIP("Skipped as it fails for unknown reasons on build.kde.org"); + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); +// QSignalSpy clipboardSyncDevicedCreated{waylandServer(), &WaylandServer::xclipboardSyncDataDeviceCreated}; +// QVERIFY(clipboardSyncDevicedCreated.isValid()); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + waylandServer()->initWorkspace(); +// // wait till the xclipboard sync data device is created +// if (clipboardSyncDevicedCreated.empty()) { +// QVERIFY(clipboardSyncDevicedCreated.wait()); +// } + // wait till the DataBridge sync data device is created + while (Xwl::DataBridge::self()->dataDeviceIface() == nullptr) { + QCoreApplication::processEvents(QEventLoop::WaitForMoreEvents); + } + QVERIFY(Xwl::DataBridge::self()->dataDeviceIface() != nullptr); +} + +void XwaylandSelectionsTest::cleanup() +{ + if (m_copyProcess) { + m_copyProcess->terminate(); + QVERIFY(m_copyProcess->waitForFinished()); + m_copyProcess = nullptr; + } + if (m_pasteProcess) { + m_pasteProcess->terminate(); + QVERIFY(m_pasteProcess->waitForFinished()); + m_pasteProcess = nullptr; + } +} + +void XwaylandSelectionsTest::testSync_data() +{ + QTest::addColumn("copyPlatform"); + QTest::addColumn("pastePlatform"); + + QTest::newRow("x11->wayland") << QStringLiteral("xcb") << QStringLiteral("wayland"); + QTest::newRow("wayland->x11") << QStringLiteral("wayland") << QStringLiteral("xcb"); +} + +void XwaylandSelectionsTest::testSync() +{ + // this test verifies the syncing of X11 to Wayland clipboard + const QString copy = QFINDTESTDATA(QStringLiteral("copy")); + QVERIFY(!copy.isEmpty()); + const QString paste = QFINDTESTDATA(QStringLiteral("paste")); + QVERIFY(!paste.isEmpty()); + + QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(clientAddedSpy.isValid()); + QSignalSpy clipboardChangedSpy(Xwl::DataBridge::self()->dataDeviceIface(), &KWaylandServer::DataDeviceInterface::selectionChanged); + QVERIFY(clipboardChangedSpy.isValid()); + + QProcessEnvironment environment = QProcessEnvironment::systemEnvironment(); + + // start the copy process + QFETCH(QString, copyPlatform); + environment.insert(QStringLiteral("QT_QPA_PLATFORM"), copyPlatform); + environment.insert(QStringLiteral("WAYLAND_DISPLAY"), s_socketName); + m_copyProcess = new QProcess(); + m_copyProcess->setProcessEnvironment(environment); + m_copyProcess->setProcessChannelMode(QProcess::ForwardedChannels); + m_copyProcess->setProgram(copy); + m_copyProcess->start(); + QVERIFY(m_copyProcess->waitForStarted()); + + AbstractClient *copyClient = nullptr; + QVERIFY(clientAddedSpy.wait()); + copyClient = clientAddedSpy.first().first().value(); + QVERIFY(copyClient); + if (workspace()->activeClient() != copyClient) { + workspace()->activateClient(copyClient); + } + QCOMPARE(workspace()->activeClient(), copyClient); + if (copyPlatform == QLatin1String("xcb")) { + QVERIFY(clipboardChangedSpy.isEmpty()); + QVERIFY(clipboardChangedSpy.wait()); + } else { + // TODO: it would be better to be able to connect to a signal, instead of waiting + // the idea is to make sure that the clipboard is updated, thus we need to give it + // enough time before starting the paste process which creates another window + QTest::qWait(250); + } + + // start the paste process + m_pasteProcess = new QProcess(); + QSignalSpy finishedSpy(m_pasteProcess, static_cast(&QProcess::finished)); + QVERIFY(finishedSpy.isValid()); + QFETCH(QString, pastePlatform); + environment.insert(QStringLiteral("QT_QPA_PLATFORM"), pastePlatform); + m_pasteProcess->setProcessEnvironment(environment); + m_pasteProcess->setProcessChannelMode(QProcess::ForwardedChannels); + m_pasteProcess->setProgram(paste); + m_pasteProcess->start(); + QVERIFY(m_pasteProcess->waitForStarted()); + + AbstractClient *pasteClient = nullptr; + QVERIFY(clientAddedSpy.wait()); + pasteClient = clientAddedSpy.last().first().value(); + QCOMPARE(clientAddedSpy.count(), 1); + QVERIFY(pasteClient); + + if (workspace()->activeClient() != pasteClient) { + QSignalSpy clientActivatedSpy(workspace(), &Workspace::clientActivated); + QVERIFY(clientActivatedSpy.isValid()); + workspace()->activateClient(pasteClient); + QVERIFY(clientActivatedSpy.wait()); + } + QTRY_COMPARE(workspace()->activeClient(), pasteClient); + QVERIFY(finishedSpy.wait()); + QCOMPARE(finishedSpy.first().first().toInt(), 0); + delete m_pasteProcess; + m_pasteProcess = nullptr; + delete m_copyProcess; + m_copyProcess = nullptr; +} + +WAYLANDTEST_MAIN(XwaylandSelectionsTest) +#include "xwayland_selections_test.moc" diff --git a/autotests/integration/xwaylandserver_crash_test.cpp b/autotests/integration/xwaylandserver_crash_test.cpp new file mode 100644 index 0000000..77b4160 --- /dev/null +++ b/autotests/integration/xwaylandserver_crash_test.cpp @@ -0,0 +1,139 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwin_wayland_test.h" +#include "composite.h" +#include "main.h" +#include "platform.h" +#include "scene.h" +#include "screens.h" +#include "unmanaged.h" +#include "wayland_server.h" +#include "workspace.h" +#include "x11client.h" +#include "xwl/xwayland_interface.h" + +#include + +namespace KWin +{ + +struct XcbConnectionDeleter +{ + static inline void cleanup(xcb_connection_t *pointer) + { + xcb_disconnect(pointer); + } +}; + +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(); + qRegisterMetaType(); + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup xwaylandGroup = config->group("Xwayland"); + xwaylandGroup.writeEntry(QStringLiteral("XwaylandCrashPolicy"), QStringLiteral("Stop")); + xwaylandGroup.sync(); + kwinApp()->setConfig(config); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + waylandServer()->initWorkspace(); +} + +void XwaylandServerCrashTest::testCrash() +{ + // This test verifies that all connected X11 clients get destroyed when Xwayland crashes. + + // Create a normal window. + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + const QRect windowGeometry(0, 0, 100, 200); + xcb_window_t window1 = xcb_generate_id(c.data()); + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, window1, 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; + memset(&hints, 0, sizeof(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.data(), window1, &hints); + xcb_map_window(c.data(), window1); + xcb_flush(c.data()); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + QPointer client = windowCreatedSpy.last().first().value(); + QVERIFY(client); + QVERIFY(client->isDecorated()); + + // Create an override-redirect window. + xcb_window_t window2 = xcb_generate_id(c.data()); + const uint32_t values[] = { true }; + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, window2, 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.data(), window2); + xcb_flush(c.data()); + + QSignalSpy unmanagedAddedSpy(workspace(), &Workspace::unmanagedAdded); + QVERIFY(unmanagedAddedSpy.isValid()); + 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); + QVERIFY(x11ConnectionChangedSpy.isValid()); + xwayland()->process()->terminate(); + QVERIFY(x11ConnectionChangedSpy.wait()); + + // When Xwayland crashes, the compositor should tear down the XCB connection and destroy + // all connected X11 clients. + QTRY_VERIFY(!client); + QTRY_VERIFY(!unmanaged); + QCOMPARE(kwinApp()->x11Connection(), nullptr); + QCOMPARE(kwinApp()->x11DefaultScreen(), nullptr); + QCOMPARE(kwinApp()->x11RootWindow(), XCB_WINDOW_NONE); + QCOMPARE(kwinApp()->x11ScreenNumber(), -1); + + // Render a frame to ensure that the compositor doesn't crash. + Compositor::self()->addRepaintFull(); + QSignalSpy frameRenderedSpy(Compositor::self()->scene(), &Scene::frameRendered); + QVERIFY(frameRenderedSpy.wait()); +} + +} // namespace KWin + +WAYLANDTEST_MAIN(KWin::XwaylandServerCrashTest) +#include "xwaylandserver_crash_test.moc" diff --git a/autotests/integration/xwaylandserver_restart_test.cpp b/autotests/integration/xwaylandserver_restart_test.cpp new file mode 100644 index 0000000..d268320 --- /dev/null +++ b/autotests/integration/xwaylandserver_restart_test.cpp @@ -0,0 +1,122 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwin_wayland_test.h" +#include "composite.h" +#include "main.h" +#include "platform.h" +#include "scene.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include "x11client.h" +#include "xwl/xwayland.h" + +#include + +namespace KWin +{ + +struct XcbConnectionDeleter +{ + static inline void cleanup(xcb_connection_t *pointer) + { + xcb_disconnect(pointer); + } +}; + +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() +{ + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup xwaylandGroup = config->group("Xwayland"); + xwaylandGroup.writeEntry(QStringLiteral("XwaylandCrashPolicy"), QStringLiteral("Restart")); + xwaylandGroup.sync(); + kwinApp()->setConfig(config); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + waylandServer()->initWorkspace(); +} + +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(XwaylandInterface::self()); + + // Pretend that the Xwayland process has crashed by sending a SIGKILL to it. + QSignalSpy startedSpy(xwayland, &Xwl::Xwayland::started); + QVERIFY(startedSpy.isValid()); + kwin_safe_kill(xwayland->process()); + QVERIFY(startedSpy.wait()); + QCOMPARE(startedSpy.count(), 1); + + // Check that the compositor still accepts new X11 clients. + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + const QRect rect(0, 0, 100, 200); + xcb_window_t window = xcb_generate_id(c.data()); + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, window, 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; + memset(&hints, 0, sizeof(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.data(), window, &hints); + xcb_map_window(c.data(), window); + xcb_flush(c.data()); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client = windowCreatedSpy.last().first().value(); + QVERIFY(client); + QCOMPARE(client->windowId(), window); + QVERIFY(client->isDecorated()); + + // Render a frame to ensure that the compositor doesn't crash. + Compositor::self()->addRepaintFull(); + QSignalSpy frameRenderedSpy(Compositor::self()->scene(), &Scene::frameRendered); + QVERIFY(frameRenderedSpy.wait()); + + // Destroy the test window. + xcb_destroy_window(c.data(), window); + xcb_flush(c.data()); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +} // namespace KWin + +WAYLANDTEST_MAIN(KWin::XwaylandServerRestartTest) +#include "xwaylandserver_restart_test.moc" diff --git a/autotests/libinput/CMakeLists.txt b/autotests/libinput/CMakeLists.txt new file mode 100644 index 0000000..4ac4d25 --- /dev/null +++ b/autotests/libinput/CMakeLists.txt @@ -0,0 +1,85 @@ +include_directories(${Libinput_INCLUDE_DIRS}) +include_directories(${UDEV_INCLUDE_DIR}) + +add_library(LibInputTestObjects STATIC ../../libinput/device.cpp ../../libinput/events.cpp mock_libinput.cpp) +target_link_libraries(LibInputTestObjects Qt5::Test Qt5::Widgets Qt5::DBus Qt5::Gui KF5::ConfigCore) + +######################################################## +# Test Devices +######################################################## +add_executable(testLibinputDevice device_test.cpp) +target_link_libraries(testLibinputDevice Qt5::Test Qt5::DBus Qt5::Gui KF5::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 Qt5::Test Qt5::DBus Qt5::Widgets KF5::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 Qt5::Test Qt5::DBus Qt5::Widgets KF5::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 Qt5::Test Qt5::DBus Qt5::Widgets KF5::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 Qt5::Test Qt5::DBus Qt5::Widgets KF5::ConfigCore LibInputTestObjects) +add_test(NAME kwin-testLibinputGestureEvent COMMAND testLibinputGestureEvent) +ecm_mark_as_test(testLibinputGestureEvent) + +######################################################## +# Test Switch Event +######################################################## +add_executable(testLibinputSwitchEvent switch_event_test.cpp) +target_link_libraries(testLibinputSwitchEvent Qt5::Test Qt5::DBus Qt5::Widgets KF5::ConfigCore LibInputTestObjects) +add_test(NAME kwin-testLibinputSwitchEvent COMMAND testLibinputSwitchEvent) +ecm_mark_as_test(testLibinputSwitchEvent) + +######################################################## +# Test Context +######################################################## +set(testLibinputContext_SRCS + ../../libinput/context.cpp + ../../libinput/libinput_logging.cpp + ../../logind.cpp + context_test.cpp + mock_udev.cpp +) +add_executable(testLibinputContext ${testLibinputContext_SRCS}) +target_link_libraries(testLibinputContext + LibInputTestObjects + + Qt5::DBus + Qt5::Test + Qt5::Widgets + + KF5::ConfigCore + KF5::WindowSystem +) +add_test(NAME kwin-testLibinputContext COMMAND testLibinputContext) +ecm_mark_as_test(testLibinputContext) + +######################################################## +# Test Input Events +######################################################## +add_executable(testInputEvents input_event_test.cpp ../../input_event.cpp) +target_link_libraries(testInputEvents Qt5::Test Qt5::DBus Qt5::Gui Qt5::Widgets KF5::ConfigCore LibInputTestObjects) +add_test(NAME kwin-testInputEvents COMMAND testInputEvents) +ecm_mark_as_test(testInputEvents) diff --git a/autotests/libinput/context_test.cpp b/autotests/libinput/context_test.cpp new file mode 100644 index 0000000..0c5839b --- /dev/null +++ b/autotests/libinput/context_test.cpp @@ -0,0 +1,81 @@ +/* + 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 "mock_udev.h" +#include "../../libinput/context.h" +#include "../../udev.h" +#include +Q_LOGGING_CATEGORY(KWIN_CORE, "kwin_core", QtCriticalMsg) + +using namespace KWin; +using namespace KWin::LibInput; + +class TestContext : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void cleanup(); + void testCreateFailUdev(); + void testAssignSeat_data(); + void testAssignSeat(); +}; + +void TestContext::cleanup() +{ + delete udev::s_mockUdev; + udev::s_mockUdev = nullptr; +} + +void TestContext::testCreateFailUdev() +{ + // this test verifies that isValid is false if the setup fails + // we create an Udev without a mockUdev + Udev u; + QVERIFY(!(udev*)(u)); + Context context(u); + QVERIFY(!context.isValid()); + // should not have a valid libinput + libinput *libinput = context; + QVERIFY(!libinput); + QVERIFY(!context.assignSeat("testSeat")); + QCOMPARE(context.fileDescriptor(), -1); +} + +void TestContext::testAssignSeat_data() +{ + QTest::addColumn("assignShouldFail"); + QTest::addColumn("expectedValue"); + + QTest::newRow("succeeds") << false << true; + QTest::newRow("fails") << true << false; +} + +void TestContext::testAssignSeat() +{ + // this test verifies the behavior of assignSeat + // setup udev so that we can create a context + udev::s_mockUdev = new udev; + QVERIFY(udev::s_mockUdev); + Udev u; + QVERIFY((udev*)(u)); + Context context(u); + QVERIFY(context.isValid()); + // this should give as a libinput + libinput *libinput = context; + QVERIFY(libinput); + // and now we can assign it + QFETCH(bool, assignShouldFail); + libinput->assignSeatRetVal = assignShouldFail; + QTEST(context.assignSeat("testSeat"), "expectedValue"); + // of course it's not suspended + QVERIFY(!context.isSuspended()); +} + +QTEST_GUILESS_MAIN(TestContext) +#include "context_test.moc" diff --git a/autotests/libinput/device_test.cpp b/autotests/libinput/device_test.cpp new file mode 100644 index 0000000..f4c08f3 --- /dev/null +++ b/autotests/libinput/device_test.cpp @@ -0,0 +1,2417 @@ +/* + 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 "../../libinput/device.h" +#include + +#include + +#include +#include +#include + +#include + +using namespace KWin::LibInput; + +class TestLibinputDevice : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testStaticGetter(); + 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 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 testScreenId(); + 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::testStaticGetter() +{ + // this test verifies that the static getter for Device works as expected + QVERIFY(Device::devices().isEmpty()); + + // create some device + libinput_device device1; + libinput_device device2; + // at the moment not yet known to Device + QVERIFY(!Device::getDevice(&device1)); + QVERIFY(!Device::getDevice(&device2)); + QVERIFY(Device::devices().isEmpty()); + + // now create a Device for one + Device *d1 = new Device(&device1); + QCOMPARE(Device::devices().count(), 1); + QCOMPARE(Device::devices().first(), d1); + QCOMPARE(Device::getDevice(&device1), d1); + QVERIFY(!Device::getDevice(&device2)); + + // and a second Device + Device *d2 = new Device(&device2); + QCOMPARE(Device::devices().count(), 2); + QCOMPARE(Device::devices().first(), d1); + QCOMPARE(Device::devices().last(), d2); + QCOMPARE(Device::getDevice(&device1), d1); + QCOMPARE(Device::getDevice(&device2), d2); + + // now delete d1 + delete d1; + QCOMPARE(Device::devices().count(), 1); + QCOMPARE(Device::devices().first(), d2); + QCOMPARE(Device::getDevice(&device2), d2); + QVERIFY(!Device::getDevice(&device1)); + + // and delete d2 + delete d2; + QVERIFY(!Device::getDevice(&device1)); + QVERIFY(!Device::getDevice(&device2)); + QVERIFY(Device::devices().isEmpty()); +} + +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(), enabled); + QCOMPARE(d.property("tapToClickEnabledByDefault").toBool(), enabled); + QCOMPARE(dbusProperty(d.sysName(), "tapToClickEnabledByDefault"), enabled); +} + +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); + QVERIFY(pointerAccelChangedSpy.isValid()); + 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); + QVERIFY(leftHandedChangedSpy.isValid()); + 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"); + + QVector 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(QVector, 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::newRow("unsupported/true") << false << false << true << false << true; + QTest::newRow("unsupported/false") << false << false << false << true << true; + 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::testEnabled() +{ + libinput_device device; + QFETCH(bool, supported); + QFETCH(bool, setShouldFail); + QFETCH(bool, initValue); + device.supportsDisableEvents = supported; + device.enabled = initValue; + 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); + QVERIFY(enabledChangedSpy.isValid()); + QFETCH(bool, setValue); + d.setEnabled(setValue); + QFETCH(bool, expectedValue); + QCOMPARE(d.isEnabled(), expectedValue); + + QCOMPARE(dbusProperty(d.sysName(), "enabled"), expectedValue); +} + +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); + QVERIFY(tapToClickChangedSpy.isValid()); + 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(), enabled); + QCOMPARE(d.property("tapAndDragEnabledByDefault").toBool(), enabled); + QCOMPARE(dbusProperty(d.sysName(), "tapAndDragEnabledByDefault"), enabled); +} + +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); + QVERIFY(tapAndDragChangedSpy.isValid()); + 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); + QVERIFY(tapDragLockChangedSpy.isValid()); + 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); + QVERIFY(middleEmulationChangedSpy.isValid()); + 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); + QVERIFY(naturalScrollChangedSpy.isValid()); + 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); + QVERIFY(scrollFactorChangedSpy.isValid()); + + 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); + QVERIFY(scrollMethodChangedSpy.isValid()); + 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); + QVERIFY(scrollMethodChangedSpy.isValid()); + 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); + QVERIFY(scrollMethodChangedSpy.isValid()); + 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); + QVERIFY(scrollButtonChangedSpy.isValid()); + 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); + QVERIFY(tapButtonMapChangedSpy.isValid()); + 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); + QVERIFY(disableWhileTypingChangedSpy.isValid()); + 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::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::testScreenId() +{ + libinput_device device; + Device d(&device); + QCOMPARE(d.screenId(), 0); + d.setScreenId(1); + QCOMPARE(d.screenId(), 1); +} + +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/autotests/libinput/gesture_event_test.cpp b/autotests/libinput/gesture_event_test.cpp new file mode 100644 index 0000000..972bbae --- /dev/null +++ b/autotests/libinput/gesture_event_test.cpp @@ -0,0 +1,203 @@ +/* + 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 "../../libinput/device.h" +#include "../../libinput/events.h" + +#include + +#include + +Q_DECLARE_METATYPE(libinput_event_type) + +using namespace KWin::LibInput; + +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; + + QScopedPointer 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.data()), gestureEvent); + // verify it's a pointer event + QVERIFY(dynamic_cast(event.data())); + QCOMPARE((libinput_event_gesture*)(*dynamic_cast(event.data())), 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 = 100u; + + QScopedPointer event(Event::create(gestureEvent)); + auto ge = dynamic_cast(event.data()); + QVERIFY(ge); + QCOMPARE(ge->fingerCount(), gestureEvent->fingerCount); + QVERIFY(!ge->isCancelled()); + QCOMPARE(ge->time(), gestureEvent->time); + QCOMPARE(ge->delta(), QSizeF(0, 0)); + if (ge->type() == LIBINPUT_EVENT_GESTURE_PINCH_BEGIN) { + auto pe = dynamic_cast(event.data()); + 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 = 200u; + gestureEvent->delta = QSizeF(2, 3); + + QScopedPointer event(Event::create(gestureEvent)); + auto se = dynamic_cast(event.data()); + QVERIFY(se); + QCOMPARE(se->fingerCount(), gestureEvent->fingerCount); + QVERIFY(!se->isCancelled()); + QCOMPARE(se->time(), gestureEvent->time); + QCOMPARE(se->delta(), QSizeF(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 = 600u; + gestureEvent->delta = QSizeF(5, 4); + gestureEvent->scale = 2; + gestureEvent->angleDelta = -30; + + QScopedPointer event(Event::create(gestureEvent)); + auto pe = dynamic_cast(event.data()); + QVERIFY(pe); + QCOMPARE(pe->fingerCount(), gestureEvent->fingerCount); + QVERIFY(!pe->isCancelled()); + QCOMPARE(pe->time(), gestureEvent->time); + QCOMPARE(pe->delta(), QSizeF(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 = 300u; + gestureEvent->scale = 3; + + QScopedPointer event(Event::create(gestureEvent)); + auto ge = dynamic_cast(event.data()); + QVERIFY(ge); + QCOMPARE(ge->fingerCount(), gestureEvent->fingerCount); + QCOMPARE(ge->isCancelled(), cancelled); + QCOMPARE(ge->time(), gestureEvent->time); + QCOMPARE(ge->delta(), QSizeF(0, 0)); + if (ge->type() == LIBINPUT_EVENT_GESTURE_PINCH_END) { + auto pe = dynamic_cast(event.data()); + QCOMPARE(pe->scale(), gestureEvent->scale); + QCOMPARE(pe->angleDelta(), 0.0); + } +} + +QTEST_GUILESS_MAIN(TestLibinputGestureEvent) +#include "gesture_event_test.moc" diff --git a/autotests/libinput/input_event_test.cpp b/autotests/libinput/input_event_test.cpp new file mode 100644 index 0000000..4f918dd --- /dev/null +++ b/autotests/libinput/input_event_test.cpp @@ -0,0 +1,179 @@ +/* + 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 "../../libinput/device.h" +#include "../input_event.h" + +#include + +Q_DECLARE_METATYPE(KWin::SwitchEvent::State); + +using namespace KWin; +using namespace KWin::LibInput; + +class InputEventsTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testInitMouseEvent_data(); + void testInitMouseEvent(); + void testInitKeyEvent_data(); + void testInitKeyEvent(); + void testInitWheelEvent_data(); + void testInitWheelEvent(); + void testInitSwitchEvent_data(); + void testInitSwitchEvent(); +}; + +void InputEventsTest::testInitMouseEvent_data() +{ + QTest::addColumn("type"); + + QTest::newRow("Press") << QEvent::MouseButtonPress; + QTest::newRow("Release") << QEvent::MouseButtonRelease; + QTest::newRow("Move") << QEvent::MouseMove; +} + +void InputEventsTest::testInitMouseEvent() +{ + // this test verifies that a MouseEvent is constructed correctly + + // first create the test LibInput::Device + libinput_device device; + Device d(&device); + + QFETCH(QEvent::Type, type); + // now create our own event + MouseEvent event(type, QPointF(100, 200), Qt::LeftButton, Qt::LeftButton | Qt::RightButton, + Qt::ShiftModifier | Qt::ControlModifier, 300, QSizeF(1, 2), QSizeF(3, 4), quint64(-1), &d); + // and verify the contract of QMouseEvent + QCOMPARE(event.type(), type); + QCOMPARE(event.globalPos(), QPoint(100, 200)); + QCOMPARE(event.screenPos(), QPointF(100, 200)); + QCOMPARE(event.localPos(), QPointF(100, 200)); + QCOMPARE(event.button(), Qt::LeftButton); + QCOMPARE(event.buttons(), Qt::LeftButton | Qt::RightButton); + QCOMPARE(event.modifiers(), Qt::ShiftModifier | Qt::ControlModifier); + QCOMPARE(event.timestamp(), 300ul); + // and our custom argument + QCOMPARE(event.device(), &d); + QCOMPARE(event.delta(), QSizeF(1, 2)); + QCOMPARE(event.deltaUnaccelerated(), QSizeF(3, 4)); + QCOMPARE(event.timestampMicroseconds(), quint64(-1)); +} + +void InputEventsTest::testInitKeyEvent_data() +{ + QTest::addColumn("type"); + QTest::addColumn("autorepeat"); + + QTest::newRow("Press") << QEvent::KeyPress << false; + QTest::newRow("Repeat") << QEvent::KeyPress << true; + QTest::newRow("Release") << QEvent::KeyRelease << false; +} + +void InputEventsTest::testInitKeyEvent() +{ + // this test verifies that a KeyEvent is constructed correctly + + // first create the test LibInput::Device + libinput_device device; + Device d(&device); + + // setup event + QFETCH(QEvent::Type, type); + QFETCH(bool, autorepeat); + KeyEvent event(type, Qt::Key_Space, Qt::ShiftModifier | Qt::ControlModifier, 200, 300, + QStringLiteral(" "), autorepeat, 400, &d); + // and verify the contract of QKeyEvent + QCOMPARE(event.type(), type); + QCOMPARE(event.isAutoRepeat(), autorepeat); + QCOMPARE(event.key(), int(Qt::Key_Space)); + QCOMPARE(event.nativeScanCode(), 200u); + QCOMPARE(event.nativeVirtualKey(), 300u); + QCOMPARE(event.text(), QStringLiteral(" ")); + QCOMPARE(event.count(), 1); + QCOMPARE(event.nativeModifiers(), 0u); + QCOMPARE(event.modifiers(), Qt::ShiftModifier | Qt::ControlModifier); + QCOMPARE(event.timestamp(), 400ul); + // and our custom argument + QCOMPARE(event.device(), &d); +} + +void InputEventsTest::testInitWheelEvent_data() +{ + QTest::addColumn("orientation"); + QTest::addColumn("delta"); + QTest::addColumn("discreteDelta"); + QTest::addColumn("expectedAngleDelta"); + + QTest::newRow("horiz") << Qt::Horizontal << 3.3 << 1 << QPoint(3, 0); + QTest::newRow("vert") << Qt::Vertical << 2.4 << 2 << QPoint(0, 2); +} + +void InputEventsTest::testInitWheelEvent() +{ + // this test verifies that a WheelEvent is constructed correctly + + // first create the test LibInput::Device + libinput_device device; + Device d(&device); + + // setup event + QFETCH(Qt::Orientation, orientation); + QFETCH(qreal, delta); + QFETCH(qint32, discreteDelta); + WheelEvent event(QPointF(100, 200), delta, discreteDelta, orientation, Qt::LeftButton | Qt::RightButton, + Qt::ShiftModifier | Qt::ControlModifier, InputRedirection::PointerAxisSourceWheel, 300, &d); + // compare QWheelEvent contract + QCOMPARE(event.type(), QEvent::Wheel); + QCOMPARE(event.posF(), QPointF(100, 200)); + QCOMPARE(event.globalPosF(), QPointF(100, 200)); + QCOMPARE(event.buttons(), Qt::LeftButton | Qt::RightButton); + QCOMPARE(event.modifiers(), Qt::ShiftModifier | Qt::ControlModifier); + QCOMPARE(event.timestamp(), 300ul); + QTEST(event.angleDelta(), "expectedAngleDelta"); + QTEST(event.orientation(), "orientation"); + QTEST(event.delta(), "delta"); + QTEST(event.discreteDelta(), "discreteDelta"); + QCOMPARE(event.axisSource(), InputRedirection::PointerAxisSourceWheel); + // and our custom argument + QCOMPARE(event.device(), &d); + +} + +void InputEventsTest::testInitSwitchEvent_data() +{ + QTest::addColumn("state"); + QTest::addColumn("timestamp"); + QTest::addColumn("micro"); + + QTest::newRow("on") << SwitchEvent::State::On << 23u << quint64{23456790}; + QTest::newRow("off") << SwitchEvent::State::Off << 456892u << quint64{45689235987}; +} + +void InputEventsTest::testInitSwitchEvent() +{ + // this test verifies that a SwitchEvent is constructed correctly + libinput_device device; + Device d(&device); + + QFETCH(SwitchEvent::State, state); + QFETCH(quint32, timestamp); + QFETCH(quint64, micro); + SwitchEvent event(state, timestamp, micro, &d); + + QCOMPARE(event.state(), state); + QCOMPARE(event.timestamp(), ulong(timestamp)); + QCOMPARE(event.timestampMicroseconds(), micro); + QCOMPARE(event.device(), &d); +} + +QTEST_GUILESS_MAIN(InputEventsTest) +#include "input_event_test.moc" diff --git a/autotests/libinput/key_event_test.cpp b/autotests/libinput/key_event_test.cpp new file mode 100644 index 0000000..b234b38 --- /dev/null +++ b/autotests/libinput/key_event_test.cpp @@ -0,0 +1,105 @@ +/* + 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 "../../libinput/device.h" +#include "../../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; + + QScopedPointer 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.data()), keyEvent); + // verify it's a key event + QVERIFY(dynamic_cast(event.data())); + QCOMPARE((libinput_event_keyboard*)(*dynamic_cast(event.data())), 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::InputRedirection::KeyboardKeyPressed << quint32(KEY_A) << 100u; + QTest::newRow("released") << LIBINPUT_KEY_STATE_RELEASED << KWin::InputRedirection::KeyboardKeyReleased << 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 = time; + + QScopedPointer event(Event::create(keyEvent)); + auto ke = dynamic_cast(event.data()); + QVERIFY(ke); + QTEST(ke->state(), "expectedKeyState"); + QCOMPARE(ke->key(), key); + QCOMPARE(ke->time(), time); +} + +QTEST_GUILESS_MAIN(TestLibinputKeyEvent) +#include "key_event_test.moc" diff --git a/autotests/libinput/mock_libinput.cpp b/autotests/libinput/mock_libinput.cpp new file mode 100644 index 0000000..d854b44 --- /dev/null +++ b/autotests/libinput/mock_libinput.cpp @@ -0,0 +1,925 @@ +/* + 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 + +#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; +} + +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_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) +{ + if (device->enabled) { + return LIBINPUT_CONFIG_SEND_EVENTS_ENABLED; + } else { + // TODO: disabled on eternal mouse + return LIBINPUT_CONFIG_SEND_EVENTS_DISABLED; + } +} + +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_BACK: + return device->supportedButtons.testFlag(Qt::BackButton); + case BTN_FORWARD: + return device->supportedButtons.testFlag(Qt::ForwardButton); + case BTN_TASK: + return device->supportedButtons.testFlag(Qt::TaskButton); + 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_ENABLED); + 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) +{ + if (event->type == LIBINPUT_EVENT_POINTER_MOTION || + event->type == LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE || + event->type == LIBINPUT_EVENT_POINTER_BUTTON || + event->type == LIBINPUT_EVENT_POINTER_AXIS) { + return reinterpret_cast(event); + } + return nullptr; +} + +struct libinput_event_touch *libinput_event_get_touch_event(struct libinput_event *event) +{ + if (event->type == LIBINPUT_EVENT_TOUCH_DOWN || + event->type == LIBINPUT_EVENT_TOUCH_UP || + event->type == LIBINPUT_EVENT_TOUCH_MOTION || + event->type == LIBINPUT_EVENT_TOUCH_CANCEL || + event->type == LIBINPUT_EVENT_TOUCH_FRAME) { + return reinterpret_cast(event); + } + return nullptr; +} + +struct libinput_event_gesture *libinput_event_get_gesture_event(struct libinput_event *event) +{ + if (event->type == LIBINPUT_EVENT_GESTURE_PINCH_BEGIN || + event->type == LIBINPUT_EVENT_GESTURE_PINCH_UPDATE || + event->type == LIBINPUT_EVENT_GESTURE_PINCH_END || + event->type == LIBINPUT_EVENT_GESTURE_SWIPE_BEGIN || + event->type == LIBINPUT_EVENT_GESTURE_SWIPE_UPDATE || + event->type == LIBINPUT_EVENT_GESTURE_SWIPE_END) { + return reinterpret_cast(event); + } + 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; +} + +uint32_t libinput_event_gesture_get_time(struct libinput_event_gesture *event) +{ + return event->time; +} + +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.width(); + } + 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.height(); + } + return 0.0; +} + +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; +} + +uint32_t libinput_event_keyboard_get_time(struct libinput_event_keyboard *event) +{ + return event->time; +} + +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.width(); +} + +double libinput_event_pointer_get_dy(struct libinput_event_pointer *event) +{ + return event->delta.height(); +} + +double libinput_event_pointer_get_dx_unaccelerated(struct libinput_event_pointer *event) +{ + return event->delta.width(); +} + +double libinput_event_pointer_get_dy_unaccelerated(struct libinput_event_pointer *event) +{ + return event->delta.height(); +} + +uint32_t libinput_event_pointer_get_time(struct libinput_event_pointer *event) +{ + return event->time; +} + +uint64_t libinput_event_pointer_get_time_usec(struct libinput_event_pointer *event) +{ + return quint64(event->time * 1000); +} + +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_axis_value(struct libinput_event_pointer *event, enum libinput_pointer_axis axis) +{ + if (axis == LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL) { + return event->verticalAxisValue; + } else { + return event->horizontalAxisValue; + } +} + +double libinput_event_pointer_get_axis_value_discrete(struct libinput_event_pointer *event, enum libinput_pointer_axis axis) +{ + if (axis == LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL) { + return event->verticalDiscreteAxisValue; + } else { + return event->horizontalDiscreteAxisValue; + } +} + +enum libinput_pointer_axis_source libinput_event_pointer_get_axis_source(struct libinput_event_pointer *event) +{ + return event->axisSource; +} + +uint32_t libinput_event_touch_get_time(struct libinput_event_touch *event) +{ + return event->time; +} + +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_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; + } + Q_UNUSED(interface) + Q_UNUSED(user_data) + return new libinput; +} + +void libinput_log_set_priority(struct libinput *libinput, enum libinput_log_priority priority) +{ + Q_UNUSED(libinput) + Q_UNUSED(priority) +} + +void libinput_log_set_handler(struct libinput *libinput, libinput_log_handler log_handler) +{ + Q_UNUSED(libinput) + Q_UNUSED(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) +{ + Q_UNUSED(libinput) + return -1; +} + +int libinput_dispatch(struct libinput *libinput) +{ + Q_UNUSED(libinput) + return 0; +} + +struct libinput_event *libinput_get_event(struct libinput *libinput) +{ + Q_UNUSED(libinput) + return nullptr; +} + +void libinput_suspend(struct libinput *libinput) +{ + Q_UNUSED(libinput) +} + +int libinput_resume(struct libinput *libinput) +{ + Q_UNUSED(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(); + } +} + +uint32_t libinput_event_switch_get_time(struct libinput_event_switch *event) +{ + return event->time;; +} + +uint64_t libinput_event_switch_get_time_usec(struct libinput_event_switch *event) +{ + return event->timeMicroseconds; +} + +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) +{ + if (event->type == LIBINPUT_EVENT_TABLET_TOOL_AXIS || + event->type == LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY || + event->type == LIBINPUT_EVENT_TABLET_TOOL_TIP) { + return reinterpret_cast(event); + } + 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_buttons(struct libinput_device *device) +{ + return device->buttonCount; +} diff --git a/autotests/libinput/mock_libinput.h b/autotests/libinput/mock_libinput.h new file mode 100644 index 0000000..c0bfaf6 --- /dev/null +++ b/autotests/libinput/mock_libinput.h @@ -0,0 +1,158 @@ +/* + 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 + +struct libinput_device { + 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; + 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; + QVector keys; + bool enabled = true; + 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 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; +}; + +struct libinput_event { + libinput_device *device = nullptr; + libinput_event_type type = LIBINPUT_EVENT_NONE; + quint32 time = 0; +}; + +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 horizontalAxisValue = 0.0; + qreal verticalAxisValue = 0.0; + qreal horizontalDiscreteAxisValue = 0.0; + qreal verticalDiscreteAxisValue = 0.0; + libinput_pointer_axis_source axisSource = {}; + QSizeF 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; + QSizeF delta = QSizeF(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; + quint64 timeMicroseconds = 0; +}; + +struct libinput { + int refCount = 1; + QByteArray seat; + int assignSeatRetVal = 0; +}; + +#endif diff --git a/autotests/libinput/mock_udev.cpp b/autotests/libinput/mock_udev.cpp new file mode 100644 index 0000000..2548d66 --- /dev/null +++ b/autotests/libinput/mock_udev.cpp @@ -0,0 +1,26 @@ +/* + 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 "../../udev.h" +#include "mock_udev.h" + +udev *udev::s_mockUdev = nullptr; + +namespace KWin +{ + +Udev::Udev() + : m_udev(udev::s_mockUdev) +{ +} + +Udev::~Udev() +{ +} + +} diff --git a/autotests/libinput/mock_udev.h b/autotests/libinput/mock_udev.h new file mode 100644 index 0000000..8560521 --- /dev/null +++ b/autotests/libinput/mock_udev.h @@ -0,0 +1,17 @@ +/* + 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_UDEV_H +#define MOCK_UDEV_H + +struct udev { + static udev *s_mockUdev; +}; + +#endif + diff --git a/autotests/libinput/pointer_event_test.cpp b/autotests/libinput/pointer_event_test.cpp new file mode 100644 index 0000000..d0d16c9 --- /dev/null +++ b/autotests/libinput/pointer_event_test.cpp @@ -0,0 +1,221 @@ +/* + 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 "../../libinput/device.h" +#include "../../libinput/events.h" + +#include + +#include + +Q_DECLARE_METATYPE(libinput_event_type) +Q_DECLARE_METATYPE(libinput_button_state) +Q_DECLARE_METATYPE(libinput_pointer_axis_source) + +using namespace KWin::LibInput; + +class TestLibinputPointerEvent : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void init(); + void cleanup(); + + void testType_data(); + void testType(); + void testButton_data(); + void testButton(); + void testAxis_data(); + void testAxis(); + 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("axis") << LIBINPUT_EVENT_POINTER_AXIS; +} + +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; + + QScopedPointer 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.data()), pointerEvent); + // verify it's a pointer event + QVERIFY(dynamic_cast(event.data())); + QCOMPARE((libinput_event_pointer*)(*dynamic_cast(event.data())), 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::InputRedirection::PointerButtonReleased << quint32(BTN_RIGHT) << 100u; + QTest::newRow("released") << LIBINPUT_BUTTON_STATE_PRESSED << KWin::InputRedirection::PointerButtonPressed << 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 = time; + + QScopedPointer event(Event::create(pointerEvent)); + auto pe = dynamic_cast(event.data()); + QVERIFY(pe); + QCOMPARE(pe->type(), LIBINPUT_EVENT_POINTER_BUTTON); + QTEST(pe->buttonState(), "expectedButtonState"); + QCOMPARE(pe->button(), button); + QCOMPARE(pe->time(), time); + QCOMPARE(pe->timeMicroseconds(), quint64(time * 1000)); +} + +void TestLibinputPointerEvent::testAxis_data() +{ + QTest::addColumn("horizontal"); + QTest::addColumn("vertical"); + QTest::addColumn("value"); + QTest::addColumn("discreteValue"); + QTest::addColumn("axisSource"); + QTest::addColumn("expectedAxisSource"); + QTest::addColumn("time"); + + QTest::newRow("wheel/horizontal") << true << false << QPointF(3.0, 0.0) << QPoint(1, 0) << LIBINPUT_POINTER_AXIS_SOURCE_WHEEL << KWin::InputRedirection::PointerAxisSourceWheel << 100u; + QTest::newRow("wheel/vertical") << false << true << QPointF(0.0, 2.5) << QPoint(0, 1) << LIBINPUT_POINTER_AXIS_SOURCE_WHEEL << KWin::InputRedirection::PointerAxisSourceWheel << 200u; + QTest::newRow("wheel/both") << true << true << QPointF(1.1, 4.2) << QPoint(1, 1) << LIBINPUT_POINTER_AXIS_SOURCE_WHEEL << KWin::InputRedirection::PointerAxisSourceWheel << 300u; + + QTest::newRow("finger/horizontal") << true << false << QPointF(3.0, 0.0) << QPoint(0, 0) << LIBINPUT_POINTER_AXIS_SOURCE_FINGER << KWin::InputRedirection::PointerAxisSourceFinger << 400u; + QTest::newRow("stop finger/horizontal") << true << false << QPointF(0.0, 0.0) << QPoint(0, 0) << LIBINPUT_POINTER_AXIS_SOURCE_FINGER << KWin::InputRedirection::PointerAxisSourceFinger << 500u; + QTest::newRow("finger/vertical") << false << true << QPointF(0.0, 2.5) << QPoint(0, 0) << LIBINPUT_POINTER_AXIS_SOURCE_FINGER << KWin::InputRedirection::PointerAxisSourceFinger << 600u; + QTest::newRow("stop finger/vertical") << false << true << QPointF(0.0, 0.0) << QPoint(0, 0) << LIBINPUT_POINTER_AXIS_SOURCE_FINGER << KWin::InputRedirection::PointerAxisSourceFinger << 700u; + QTest::newRow("finger/both") << true << true << QPointF(1.1, 4.2) << QPoint(0, 0) << LIBINPUT_POINTER_AXIS_SOURCE_FINGER << KWin::InputRedirection::PointerAxisSourceFinger << 800u; + QTest::newRow("stop finger/both") << true << true << QPointF(0.0, 0.0) << QPoint(0, 0) << LIBINPUT_POINTER_AXIS_SOURCE_FINGER << KWin::InputRedirection::PointerAxisSourceFinger << 900u; + + QTest::newRow("continuous/horizontal") << true << false << QPointF(3.0, 0.0) << QPoint(0, 0) << LIBINPUT_POINTER_AXIS_SOURCE_CONTINUOUS << KWin::InputRedirection::PointerAxisSourceContinuous << 1000u; + QTest::newRow("continuous/vertical") << false << true << QPointF(0.0, 2.5) << QPoint(0, 0) << LIBINPUT_POINTER_AXIS_SOURCE_CONTINUOUS << KWin::InputRedirection::PointerAxisSourceContinuous << 1100u; + QTest::newRow("continuous/both") << true << true << QPointF(1.1, 4.2) << QPoint(0, 0) << LIBINPUT_POINTER_AXIS_SOURCE_CONTINUOUS << KWin::InputRedirection::PointerAxisSourceContinuous << 1200u; +} + +void TestLibinputPointerEvent::testAxis() +{ + // this test verifies pointer axis functionality + libinput_event_pointer *pointerEvent = new libinput_event_pointer; + pointerEvent->device = m_nativeDevice; + pointerEvent->type = LIBINPUT_EVENT_POINTER_AXIS; + QFETCH(bool, horizontal); + QFETCH(bool, vertical); + QFETCH(QPointF, value); + QFETCH(QPoint, discreteValue); + QFETCH(libinput_pointer_axis_source, axisSource); + QFETCH(quint32, time); + pointerEvent->horizontalAxis = horizontal; + pointerEvent->verticalAxis = vertical; + pointerEvent->horizontalAxisValue = value.x(); + pointerEvent->verticalAxisValue = value.y(); + pointerEvent->horizontalDiscreteAxisValue = discreteValue.x(); + pointerEvent->verticalDiscreteAxisValue = discreteValue.y(); + pointerEvent->axisSource = axisSource; + pointerEvent->time = time; + + QScopedPointer event(Event::create(pointerEvent)); + auto pe = dynamic_cast(event.data()); + QVERIFY(pe); + QCOMPARE(pe->type(), LIBINPUT_EVENT_POINTER_AXIS); + QCOMPARE(pe->axis().contains(KWin::InputRedirection::PointerAxisHorizontal), horizontal); + QCOMPARE(pe->axis().contains(KWin::InputRedirection::PointerAxisVertical), vertical); + QCOMPARE(pe->axisValue(KWin::InputRedirection::PointerAxisHorizontal), value.x()); + QCOMPARE(pe->axisValue(KWin::InputRedirection::PointerAxisVertical), value.y()); + QCOMPARE(pe->discreteAxisValue(KWin::InputRedirection::PointerAxisHorizontal), discreteValue.x()); + QCOMPARE(pe->discreteAxisValue(KWin::InputRedirection::PointerAxisVertical), discreteValue.y()); + QTEST(pe->axisSource(), "expectedAxisSource"); + QCOMPARE(pe->time(), 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 = QSizeF(2.1, 4.5); + pointerEvent->time = 500u; + + QScopedPointer event(Event::create(pointerEvent)); + auto pe = dynamic_cast(event.data()); + QVERIFY(pe); + QCOMPARE(pe->type(), LIBINPUT_EVENT_POINTER_MOTION); + QCOMPARE(pe->time(), 500u); + QCOMPARE(pe->delta(), QSizeF(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 = 500u; + + QScopedPointer event(Event::create(pointerEvent)); + auto pe = dynamic_cast(event.data()); + QVERIFY(pe); + QCOMPARE(pe->type(), LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE); + QCOMPARE(pe->time(), 500u); + 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/autotests/libinput/switch_event_test.cpp b/autotests/libinput/switch_event_test.cpp new file mode 100644 index 0000000..f057baa --- /dev/null +++ b/autotests/libinput/switch_event_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 "mock_libinput.h" +#include "../../libinput/device.h" +#include "../../libinput/events.h" + +#include + +#include + +Q_DECLARE_METATYPE(KWin::LibInput::SwitchEvent::State) + +using namespace KWin::LibInput; + +class TestLibinputSwitchEvent : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void init(); + void cleanup(); + + void testToggled_data(); + void testToggled(); + +private: + std::unique_ptr m_nativeDevice; + std::unique_ptr m_device; +}; + +void TestLibinputSwitchEvent::init() +{ + m_nativeDevice = std::make_unique(); + m_nativeDevice->switchDevice = true; + m_device = std::make_unique(m_nativeDevice.get()); +} + +void TestLibinputSwitchEvent::cleanup() +{ + m_device.reset(); + m_nativeDevice.reset(); +} + +void TestLibinputSwitchEvent::testToggled_data() +{ + QTest::addColumn("state"); + + QTest::newRow("on") << KWin::LibInput::SwitchEvent::State::On; + QTest::newRow("off") << KWin::LibInput::SwitchEvent::State::Off; +} + +void TestLibinputSwitchEvent::testToggled() +{ + libinput_event_switch *nativeEvent = new libinput_event_switch; + nativeEvent->type = LIBINPUT_EVENT_SWITCH_TOGGLE; + nativeEvent->device = m_nativeDevice.get(); + QFETCH(KWin::LibInput::SwitchEvent::State, state); + switch (state) { + case SwitchEvent::State::Off: + nativeEvent->state = libinput_event_switch::State::Off; + break; + case SwitchEvent::State::On: + nativeEvent->state = libinput_event_switch::State::On; + break; + default: + Q_UNREACHABLE(); + } + nativeEvent->time = 23; + nativeEvent->timeMicroseconds = 23456789; + + QScopedPointer event(Event::create(nativeEvent)); + auto se = dynamic_cast(event.data()); + QVERIFY(se); + QCOMPARE(se->device(), m_device.get()); + QCOMPARE(se->nativeDevice(), m_nativeDevice.get()); + QCOMPARE(se->type(), LIBINPUT_EVENT_SWITCH_TOGGLE); + QCOMPARE(se->state(), state); + QCOMPARE(se->time(), 23u); + QCOMPARE(se->timeMicroseconds(), 23456789u); +} + +QTEST_GUILESS_MAIN(TestLibinputSwitchEvent) +#include "switch_event_test.moc" diff --git a/autotests/libinput/touch_event_test.cpp b/autotests/libinput/touch_event_test.cpp new file mode 100644 index 0000000..0068a46 --- /dev/null +++ b/autotests/libinput/touch_event_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 "mock_libinput.h" +#include "../../libinput/device.h" +#include "../../libinput/events.h" + +#include + +#include + +Q_DECLARE_METATYPE(libinput_event_type) + +using namespace KWin::LibInput; + +class TestLibinputTouchEvent : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void init(); + void cleanup(); + + void testType_data(); + void testType(); + void testAbsoluteMotion_data(); + void testAbsoluteMotion(); + void testNoAssignedSlot(); + +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; + + QScopedPointer 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.data()), touchEvent); + // verify it's a pointer event + QVERIFY(dynamic_cast(event.data())); + QCOMPARE((libinput_event_touch*)(*dynamic_cast(event.data())), touchEvent); + QFETCH(bool, hasId); + if (hasId) { + QCOMPARE(dynamic_cast(event.data())->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 = 500u; + touchEvent->slot = 1; + + QScopedPointer event(Event::create(touchEvent)); + auto te = dynamic_cast(event.data()); + QVERIFY(te); + QCOMPARE(te->type(), type); + QCOMPARE(te->time(), 500u); + QCOMPARE(te->absolutePos(), QPointF(6.25, 6.9)); + QCOMPARE(te->absolutePos(QSize(1280, 1024)), QPointF(640, 512)); +} + +void TestLibinputTouchEvent::testNoAssignedSlot() +{ + // this test verifies that touch events without an assigned slot get id == 0 + libinput_event_touch *touchEvent = new libinput_event_touch; + touchEvent->type = LIBINPUT_EVENT_TOUCH_UP; + touchEvent->device = m_nativeDevice; + // touch events without an assigned slot have slot == -1 + touchEvent->slot = -1; + + QScopedPointer event(Event::create(touchEvent)); + QVERIFY(dynamic_cast(event.data())); + QCOMPARE(dynamic_cast(event.data())->id(), 0); +} + +QTEST_GUILESS_MAIN(TestLibinputTouchEvent) +#include "touch_event_test.moc" diff --git a/autotests/libkwineffects/CMakeLists.txt b/autotests/libkwineffects/CMakeLists.txt new file mode 100644 index 0000000..ebbbcc2 --- /dev/null +++ b/autotests/libkwineffects/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} Qt5::Test kwineffects) + ecm_mark_as_test(${_testname}) + endforeach() +endmacro() + +kwineffects_unit_tests( + windowquadlisttest + timelinetest +) + +add_executable(kwinglplatformtest kwinglplatformtest.cpp mock_gl.cpp ../../libkwineffects/kwinglplatform.cpp) +add_test(NAME kwineffects-kwinglplatformtest COMMAND kwinglplatformtest) +target_link_libraries(kwinglplatformtest Qt5::Test Qt5::Gui Qt5::X11Extras KF5::ConfigCore XCB::XCB) +ecm_mark_as_test(kwinglplatformtest) diff --git a/autotests/libkwineffects/data/glplatform/amd-catalyst-radeonhd-7700M-3.1.13399 b/autotests/libkwineffects/data/glplatform/amd-catalyst-radeonhd-7700M-3.1.13399 new file mode 100644 index 0000000..a768214 --- /dev/null +++ b/autotests/libkwineffects/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=9 diff --git a/autotests/libkwineffects/data/glplatform/amd-gallium-bonaire-3.0 b/autotests/libkwineffects/data/glplatform/amd-gallium-bonaire-3.0 new file mode 100644 index 0000000..7c18f2c --- /dev/null +++ b/autotests/libkwineffects/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=9 diff --git a/autotests/libkwineffects/data/glplatform/amd-gallium-cayman-gles-3.0 b/autotests/libkwineffects/data/glplatform/amd-gallium-cayman-gles-3.0 new file mode 100644 index 0000000..4656c8c --- /dev/null +++ b/autotests/libkwineffects/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=9 diff --git a/autotests/libkwineffects/data/glplatform/amd-gallium-hawaii-3.0 b/autotests/libkwineffects/data/glplatform/amd-gallium-hawaii-3.0 new file mode 100644 index 0000000..28017db --- /dev/null +++ b/autotests/libkwineffects/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=9 diff --git a/autotests/libkwineffects/data/glplatform/amd-gallium-navi-4.5 b/autotests/libkwineffects/data/glplatform/amd-gallium-navi-4.5 new file mode 100644 index 0000000..056086c --- /dev/null +++ b/autotests/libkwineffects/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=9 diff --git a/autotests/libkwineffects/data/glplatform/amd-gallium-radeon-r9-290-4.5 b/autotests/libkwineffects/data/glplatform/amd-gallium-radeon-r9-290-4.5 new file mode 100644 index 0000000..320fdb0 --- /dev/null +++ b/autotests/libkwineffects/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=9 diff --git a/autotests/libkwineffects/data/glplatform/amd-gallium-radeon-rx-480-series-4.5 b/autotests/libkwineffects/data/glplatform/amd-gallium-radeon-rx-480-series-4.5 new file mode 100644 index 0000000..c34da71 --- /dev/null +++ b/autotests/libkwineffects/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=9 diff --git a/autotests/libkwineffects/data/glplatform/amd-gallium-radeon-rx-550-series-3.1 b/autotests/libkwineffects/data/glplatform/amd-gallium-radeon-rx-550-series-3.1 new file mode 100644 index 0000000..067d5ca --- /dev/null +++ b/autotests/libkwineffects/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=9 diff --git a/autotests/libkwineffects/data/glplatform/amd-gallium-radeon-rx-580-series-4.5 b/autotests/libkwineffects/data/glplatform/amd-gallium-radeon-rx-580-series-4.5 new file mode 100644 index 0000000..447d68c --- /dev/null +++ b/autotests/libkwineffects/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=9 diff --git a/autotests/libkwineffects/data/glplatform/amd-gallium-radeon-rx-vega-56-4.5 b/autotests/libkwineffects/data/glplatform/amd-gallium-radeon-rx-vega-56-4.5 new file mode 100644 index 0000000..aa01e2f --- /dev/null +++ b/autotests/libkwineffects/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=9 diff --git a/autotests/libkwineffects/data/glplatform/amd-gallium-radeon-rx-vega-64-4.5 b/autotests/libkwineffects/data/glplatform/amd-gallium-radeon-rx-vega-64-4.5 new file mode 100644 index 0000000..684fb78 --- /dev/null +++ b/autotests/libkwineffects/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=9 diff --git a/autotests/libkwineffects/data/glplatform/amd-gallium-redwood-3.0 b/autotests/libkwineffects/data/glplatform/amd-gallium-redwood-3.0 new file mode 100644 index 0000000..9dd8fe3 --- /dev/null +++ b/autotests/libkwineffects/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=9 diff --git a/autotests/libkwineffects/data/glplatform/amd-gallium-tonga-4.1 b/autotests/libkwineffects/data/glplatform/amd-gallium-tonga-4.1 new file mode 100644 index 0000000..9b34861 --- /dev/null +++ b/autotests/libkwineffects/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=9 diff --git a/autotests/libkwineffects/data/glplatform/intel-broadwell-gt2-3.3 b/autotests/libkwineffects/data/glplatform/intel-broadwell-gt2-3.3 new file mode 100644 index 0000000..1bdb0b8 --- /dev/null +++ b/autotests/libkwineffects/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=9 diff --git a/autotests/libkwineffects/data/glplatform/intel-haswell-mobile-3.3 b/autotests/libkwineffects/data/glplatform/intel-haswell-mobile-3.3 new file mode 100644 index 0000000..7b7e52c --- /dev/null +++ b/autotests/libkwineffects/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=9 diff --git a/autotests/libkwineffects/data/glplatform/intel-ivybridge-desktop-3.0 b/autotests/libkwineffects/data/glplatform/intel-ivybridge-desktop-3.0 new file mode 100644 index 0000000..7629c0b --- /dev/null +++ b/autotests/libkwineffects/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=9 + diff --git a/autotests/libkwineffects/data/glplatform/intel-ivybridge-desktop-3.3 b/autotests/libkwineffects/data/glplatform/intel-ivybridge-desktop-3.3 new file mode 100644 index 0000000..47039a4 --- /dev/null +++ b/autotests/libkwineffects/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=9 diff --git a/autotests/libkwineffects/data/glplatform/intel-ivybridge-mobile-3.3 b/autotests/libkwineffects/data/glplatform/intel-ivybridge-mobile-3.3 new file mode 100644 index 0000000..ef2fdda --- /dev/null +++ b/autotests/libkwineffects/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=9 diff --git a/autotests/libkwineffects/data/glplatform/intel-sandybridge-mobile-3.3 b/autotests/libkwineffects/data/glplatform/intel-sandybridge-mobile-3.3 new file mode 100644 index 0000000..8921490 --- /dev/null +++ b/autotests/libkwineffects/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=9 diff --git a/autotests/libkwineffects/data/glplatform/intel-skylake-gt2-3.0 b/autotests/libkwineffects/data/glplatform/intel-skylake-gt2-3.0 new file mode 100644 index 0000000..0af1412 --- /dev/null +++ b/autotests/libkwineffects/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=9 diff --git a/autotests/libkwineffects/data/glplatform/llvmpipe-10.0 b/autotests/libkwineffects/data/glplatform/llvmpipe-10.0 new file mode 100644 index 0000000..92581af --- /dev/null +++ b/autotests/libkwineffects/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=9 + diff --git a/autotests/libkwineffects/data/glplatform/llvmpipe-3.0 b/autotests/libkwineffects/data/glplatform/llvmpipe-3.0 new file mode 100644 index 0000000..824fb3e --- /dev/null +++ b/autotests/libkwineffects/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=9 + diff --git a/autotests/libkwineffects/data/glplatform/llvmpipe-5.0 b/autotests/libkwineffects/data/glplatform/llvmpipe-5.0 new file mode 100644 index 0000000..56aa352 --- /dev/null +++ b/autotests/libkwineffects/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=9 + diff --git a/autotests/libkwineffects/data/glplatform/nvidia-geforce-gtx-560-4.5 b/autotests/libkwineffects/data/glplatform/nvidia-geforce-gtx-560-4.5 new file mode 100644 index 0000000..4c92365 --- /dev/null +++ b/autotests/libkwineffects/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=9 + diff --git a/autotests/libkwineffects/data/glplatform/nvidia-geforce-gtx-660-3.1 b/autotests/libkwineffects/data/glplatform/nvidia-geforce-gtx-660-3.1 new file mode 100644 index 0000000..12e769c --- /dev/null +++ b/autotests/libkwineffects/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=9 diff --git a/autotests/libkwineffects/data/glplatform/nvidia-geforce-gtx-950-4.5 b/autotests/libkwineffects/data/glplatform/nvidia-geforce-gtx-950-4.5 new file mode 100644 index 0000000..6e010f4 --- /dev/null +++ b/autotests/libkwineffects/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=9 diff --git a/autotests/libkwineffects/data/glplatform/nvidia-geforce-gtx-970-3.1 b/autotests/libkwineffects/data/glplatform/nvidia-geforce-gtx-970-3.1 new file mode 100644 index 0000000..eee24d6 --- /dev/null +++ b/autotests/libkwineffects/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=9 diff --git a/autotests/libkwineffects/data/glplatform/nvidia-geforce-gtx-970M-3.1 b/autotests/libkwineffects/data/glplatform/nvidia-geforce-gtx-970M-3.1 new file mode 100644 index 0000000..069dc95 --- /dev/null +++ b/autotests/libkwineffects/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=9 diff --git a/autotests/libkwineffects/data/glplatform/nvidia-geforce-gtx-980-3.1 b/autotests/libkwineffects/data/glplatform/nvidia-geforce-gtx-980-3.1 new file mode 100644 index 0000000..ad53428 --- /dev/null +++ b/autotests/libkwineffects/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=9 diff --git a/autotests/libkwineffects/data/glplatform/qualcomm-adreno-330-libhybris-gles-3.0 b/autotests/libkwineffects/data/glplatform/qualcomm-adreno-330-libhybris-gles-3.0 new file mode 100644 index 0000000..c1d157c --- /dev/null +++ b/autotests/libkwineffects/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=9 diff --git a/autotests/libkwineffects/data/glplatform/virgl-3.1 b/autotests/libkwineffects/data/glplatform/virgl-3.1 new file mode 100644 index 0000000..631a758 --- /dev/null +++ b/autotests/libkwineffects/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=9 diff --git a/autotests/libkwineffects/kwinglplatformtest.cpp b/autotests/libkwineffects/kwinglplatformtest.cpp new file mode 100644 index 0000000..25bf25c --- /dev/null +++ b/autotests/libkwineffects/kwinglplatformtest.cpp @@ -0,0 +1,272 @@ +/* + 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_gl.h" +#include +#include + +#include +#include + +Q_DECLARE_METATYPE(KWin::Driver) +Q_DECLARE_METATYPE(KWin::ChipClass) + +using namespace KWin; + +void KWin::cleanupGL() +{ + GLPlatform::cleanup(); +} + +class GLPlatformTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void cleanup(); + + void testDriverToString_data(); + void testDriverToString(); + void testChipClassToString_data(); + void testChipClassToString(); + void testPriorDetect(); + void testDetect_data(); + void testDetect(); +}; + +void GLPlatformTest::cleanup() +{ + cleanupGL(); + delete s_gl; + s_gl = nullptr; +} + +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("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("UnknwonAdreno") << UnknownAdreno << QStringLiteral("Unknown"); + QTest::newRow("UnknownChipClass") << UnknownChipClass << QStringLiteral("Unknown"); +} + +void GLPlatformTest::testChipClassToString() +{ + QFETCH(ChipClass, chipClass); + QTEST(GLPlatform::chipClassToString(chipClass), "expected"); +} + +void GLPlatformTest::testPriorDetect() +{ + auto *gl = GLPlatform::instance(); + QVERIFY(gl); + QCOMPARE(gl->supports(LooseBinding), false); + QCOMPARE(gl->supports(GLSL), false); + QCOMPARE(gl->supports(LimitedGLSL), false); + QCOMPARE(gl->supports(TextureNPOT), false); + QCOMPARE(gl->supports(LimitedNPOT), false); + + QCOMPARE(gl->glVersion(), 0); + QCOMPARE(gl->glslVersion(), 0); + QCOMPARE(gl->mesaVersion(), 0); + QCOMPARE(gl->galliumVersion(), 0); + QCOMPARE(gl->serverVersion(), 0); + QCOMPARE(gl->kernelVersion(), 0); + QCOMPARE(gl->driverVersion(), 0); + + QCOMPARE(gl->driver(), Driver_Unknown); + QCOMPARE(gl->chipClass(), UnknownChipClass); + + QCOMPARE(gl->isMesaDriver(), false); + QCOMPARE(gl->isGalliumDriver(), false); + QCOMPARE(gl->isRadeon(), false); + QCOMPARE(gl->isNvidia(), false); + QCOMPARE(gl->isIntel(), false); + QCOMPARE(gl->isVirtualBox(), false); + QCOMPARE(gl->isVMware(), false); + + QCOMPARE(gl->isSoftwareEmulation(), false); + QCOMPARE(gl->isVirtualMachine(), false); + + QCOMPARE(gl->glVersionString(), QByteArray()); + QCOMPARE(gl->glRendererString(), QByteArray()); + QCOMPARE(gl->glVendorString(), QByteArray()); + QCOMPARE(gl->glShadingLanguageVersionString(), QByteArray()); + + QCOMPARE(gl->isLooseBinding(), false); + QCOMPARE(gl->isGLES(), false); + QCOMPARE(gl->recommendedCompositor(), XRenderCompositing); + QCOMPARE(gl->preferBufferSubData(), false); + QCOMPARE(gl->platformInterface(), NoOpenGLPlatformInterface); +} + +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 qint64 readVersion(const KConfigGroup &group, const char *entry) +{ + const QStringList parts = group.readEntry(entry, QString()).split(','); + if (parts.count() < 2) { + return 0; + } + QVector 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 kVersionNumber(versionParts.at(0), versionParts.at(1), versionParts.at(2)); +} + +void GLPlatformTest::testDetect() +{ + QFETCH(QString, configFile); + KConfig config(configFile); + const KConfigGroup driverGroup = config.group("Driver"); + s_gl = new MockGL; + s_gl->getString.vendor = driverGroup.readEntry("Vendor").toUtf8(); + s_gl->getString.renderer = driverGroup.readEntry("Renderer").toUtf8(); + s_gl->getString.version = driverGroup.readEntry("Version").toUtf8(); + s_gl->getString.shadingLanguageVersion = driverGroup.readEntry("ShadingLanguageVersion").toUtf8(); + s_gl->getString.extensions = QVector{QByteArrayLiteral("GL_ARB_shader_objects"), + QByteArrayLiteral("GL_ARB_fragment_shader"), + QByteArrayLiteral("GL_ARB_vertex_shader"), + QByteArrayLiteral("GL_ARB_texture_non_power_of_two")}; + s_gl->getString.extensionsString = QByteArray(); + + auto *gl = GLPlatform::instance(); + QVERIFY(gl); + gl->detect(EglPlatformInterface); + QCOMPARE(gl->platformInterface(), EglPlatformInterface); + + const KConfigGroup settingsGroup = config.group("Settings"); + + QCOMPARE(gl->supports(LooseBinding), settingsGroup.readEntry("LooseBinding", false)); + QCOMPARE(gl->supports(GLSL), settingsGroup.readEntry("GLSL", false)); + QCOMPARE(gl->supports(LimitedGLSL), settingsGroup.readEntry("LimitedGLSL", false)); + QCOMPARE(gl->supports(TextureNPOT), settingsGroup.readEntry("TextureNPOT", false)); + QCOMPARE(gl->supports(LimitedNPOT), settingsGroup.readEntry("LimitedNPOT", false)); + + QCOMPARE(gl->glVersion(), readVersion(settingsGroup, "GLVersion")); + QCOMPARE(gl->glslVersion(), readVersion(settingsGroup, "GLSLVersion")); + QCOMPARE(gl->mesaVersion(), readVersion(settingsGroup, "MesaVersion")); + QCOMPARE(gl->galliumVersion(), readVersion(settingsGroup, "GalliumVersion")); + QCOMPARE(gl->serverVersion(), 0); + 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->isGalliumDriver(), settingsGroup.readEntry("Gallium", 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->isVirgl(), settingsGroup.readEntry("Virgl", false)); + + QCOMPARE(gl->isSoftwareEmulation(), settingsGroup.readEntry("SoftwareEmulation", false)); + QCOMPARE(gl->isVirtualMachine(), settingsGroup.readEntry("VirtualMachine", false)); + + QCOMPARE(gl->glVersionString(), s_gl->getString.version); + QCOMPARE(gl->glRendererString(), s_gl->getString.renderer); + QCOMPARE(gl->glVendorString(), s_gl->getString.vendor); + QCOMPARE(gl->glShadingLanguageVersionString(), s_gl->getString.shadingLanguageVersion); + + QCOMPARE(gl->isLooseBinding(), settingsGroup.readEntry("LooseBinding", false)); + QCOMPARE(gl->isGLES(), settingsGroup.readEntry("GLES", 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/autotests/libkwineffects/mock_gl.cpp b/autotests/libkwineffects/mock_gl.cpp new file mode 100644 index 0000000..de8c52b --- /dev/null +++ b/autotests/libkwineffects/mock_gl.cpp @@ -0,0 +1,59 @@ +/* + 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_gl.h" +#include + +MockGL *s_gl = nullptr; + +static const GLubyte *mock_glGetString(GLenum name) +{ + if (!s_gl) { + return nullptr; + } + switch (name) { + case GL_VENDOR: + return (const GLubyte*)s_gl->getString.vendor.constData(); + case GL_RENDERER: + return (const GLubyte*)s_gl->getString.renderer.constData(); + case GL_VERSION: + return (const GLubyte*)s_gl->getString.version.constData(); + case GL_EXTENSIONS: + return (const GLubyte*)s_gl->getString.extensionsString.constData(); + case GL_SHADING_LANGUAGE_VERSION: + return (const GLubyte*)s_gl->getString.shadingLanguageVersion.constData(); + default: + return nullptr; + } +} + +static const GLubyte *mock_glGetStringi(GLenum name, GLuint index) +{ + if (!s_gl) { + return nullptr; + } + if (name == GL_EXTENSIONS && index < uint(s_gl->getString.extensions.count())) { + return (const GLubyte*)s_gl->getString.extensions.at(index).constData(); + } + return nullptr; +} + +static void mock_glGetIntegerv(GLenum pname, GLint *data) +{ + Q_UNUSED(pname) + Q_UNUSED(data) + if (pname == GL_NUM_EXTENSIONS) { + if (data && s_gl) { + *data = s_gl->getString.extensions.count(); + } + } +} + +PFNGLGETSTRINGPROC epoxy_glGetString = mock_glGetString; +PFNGLGETSTRINGIPROC epoxy_glGetStringi = mock_glGetStringi; +PFNGLGETINTEGERVPROC epoxy_glGetIntegerv = mock_glGetIntegerv; diff --git a/autotests/libkwineffects/mock_gl.h b/autotests/libkwineffects/mock_gl.h new file mode 100644 index 0000000..a88ee2b --- /dev/null +++ b/autotests/libkwineffects/mock_gl.h @@ -0,0 +1,28 @@ +/* + 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_GL_H +#define MOCK_GL_H + +#include +#include + +struct MockGL { + struct { + QByteArray vendor; + QByteArray renderer; + QByteArray version; + QVector extensions; + QByteArray extensionsString; + QByteArray shadingLanguageVersion; + } getString; +}; + +extern MockGL *s_gl; + +#endif diff --git a/autotests/libkwineffects/timelinetest.cpp b/autotests/libkwineffects/timelinetest.cpp new file mode 100644 index 0000000..895bece --- /dev/null +++ b/autotests/libkwineffects/timelinetest.cpp @@ -0,0 +1,403 @@ +/* + 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 + +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 + QCOMPARE(timeLine.value(), 0.0); + QVERIFY(!timeLine.done()); + + // 100/1000 + timeLine.update(100ms); + QCOMPARE(timeLine.value(), 0.1); + QVERIFY(!timeLine.done()); + + // 400/1000 + timeLine.update(300ms); + QCOMPARE(timeLine.value(), 0.4); + QVERIFY(!timeLine.done()); + + // 900/1000 + timeLine.update(500ms); + QCOMPARE(timeLine.value(), 0.9); + QVERIFY(!timeLine.done()); + + // 1000/1000 + timeLine.update(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 + QCOMPARE(timeLine.value(), 1.0); + QVERIFY(!timeLine.done()); + + // 100/1000 + timeLine.update(100ms); + QCOMPARE(timeLine.value(), 0.9); + QVERIFY(!timeLine.done()); + + // 400/1000 + timeLine.update(300ms); + QCOMPARE(timeLine.value(), 0.6); + QVERIFY(!timeLine.done()); + + // 900/1000 + timeLine.update(500ms); + QCOMPARE(timeLine.value(), 0.1); + QVERIFY(!timeLine.done()); + + // 1000/1000 + timeLine.update(3000ms); + QCOMPARE(timeLine.value(), 0.0); + QVERIFY(timeLine.done()); +} + +void TimeLineTest::testUpdateFinished() +{ + KWin::TimeLine timeLine(1000ms, KWin::TimeLine::Forward); + timeLine.setEasingCurve(QEasingCurve::Linear); + + timeLine.update(1000ms); + QCOMPARE(timeLine.value(), 1.0); + QVERIFY(timeLine.done()); + + timeLine.update(42ms); + QCOMPARE(timeLine.value(), 1.0); + QVERIFY(timeLine.done()); +} + +void TimeLineTest::testToggleDirection() +{ + KWin::TimeLine timeLine(1000ms, KWin::TimeLine::Forward); + timeLine.setEasingCurve(QEasingCurve::Linear); + + QCOMPARE(timeLine.value(), 0.0); + QVERIFY(!timeLine.done()); + + timeLine.update(600ms); + QCOMPARE(timeLine.value(), 0.6); + QVERIFY(!timeLine.done()); + + timeLine.toggleDirection(); + QCOMPARE(timeLine.value(), 0.6); + QVERIFY(!timeLine.done()); + + timeLine.update(200ms); + QCOMPARE(timeLine.value(), 0.4); + QVERIFY(!timeLine.done()); + + timeLine.update(3000ms); + QCOMPARE(timeLine.value(), 0.0); + QVERIFY(timeLine.done()); +} + +void TimeLineTest::testReset() +{ + KWin::TimeLine timeLine(1000ms, KWin::TimeLine::Forward); + timeLine.setEasingCurve(QEasingCurve::Linear); + + timeLine.update(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); + + if (initiallyDone) { + timeLine.update(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.update(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.update(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); + + QVERIFY(!timeLine.running()); + QVERIFY(!timeLine.done()); + + timeLine.update(100ms); + QVERIFY(timeLine.running()); + QVERIFY(!timeLine.done()); + + timeLine.update(900ms); + 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); + + QTEST(timeLine.direction(), "initialDirection"); + QTEST(timeLine.value(), "initialValue"); + QCOMPARE(timeLine.targetRedirectMode(), KWin::TimeLine::RedirectMode::Strict); + QVERIFY(!timeLine.running()); + QVERIFY(!timeLine.done()); + + timeLine.update(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); + + QTEST(timeLine.direction(), "initialDirection"); + QTEST(timeLine.value(), "initialValue"); + QCOMPARE(timeLine.targetRedirectMode(), KWin::TimeLine::RedirectMode::Relaxed); + QVERIFY(!timeLine.running()); + QVERIFY(!timeLine.done()); + + timeLine.update(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()); + + timeLine.update(1000ms); + QTEST(timeLine.direction(), "finalDirection"); + QTEST(timeLine.value(), "initialValue"); + QVERIFY(!timeLine.running()); + QVERIFY(timeLine.done()); +} + +QTEST_MAIN(TimeLineTest) + +#include "timelinetest.moc" diff --git a/autotests/libkwineffects/windowquadlisttest.cpp b/autotests/libkwineffects/windowquadlisttest.cpp new file mode 100644 index 0000000..2bcc4ac --- /dev/null +++ b/autotests/libkwineffects/windowquadlisttest.cpp @@ -0,0 +1,210 @@ +/* + 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 +#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 QRectF &rect); +}; + +KWin::WindowQuad WindowQuadListTest::makeQuad(const QRectF &r) +{ + KWin::WindowQuad quad(KWin::WindowQuadContents); + 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(QRectF(0, 0, 10, 10))); + expected.append(makeQuad(QRectF(0, 0, 10, 10))); + QTest::newRow("quadSizeTooLarge") << orig << 10 << 1 << expected; + + expected.clear(); + expected.append(makeQuad(QRectF(0, 0, 5, 5))); + expected.append(makeQuad(QRectF(0, 5, 5, 5))); + expected.append(makeQuad(QRectF(5, 0, 5, 5))); + expected.append(makeQuad(QRectF(5, 5, 5, 5))); + QTest::newRow("regularGrid") << orig << 5 << 4 << expected; + + expected.clear(); + expected.append(makeQuad(QRectF(0, 0, 9, 9))); + expected.append(makeQuad(QRectF(0, 9, 9, 1))); + expected.append(makeQuad(QRectF(9, 0, 1, 9))); + expected.append(makeQuad(QRectF(9, 9, 1, 1))); + QTest::newRow("irregularGrid") << orig << 9 << 4 << expected; + + orig.append(makeQuad(QRectF(0, 10, 4, 3))); + expected.clear(); + expected.append(makeQuad(QRectF(0, 0, 4, 4))); + expected.append(makeQuad(QRectF(0, 4, 4, 4))); + expected.append(makeQuad(QRectF(0, 8, 4, 2))); + expected.append(makeQuad(QRectF(0, 10, 4, 2))); + expected.append(makeQuad(QRectF(0, 12, 4, 1))); + expected.append(makeQuad(QRectF(4, 0, 4, 4))); + expected.append(makeQuad(QRectF(4, 4, 4, 4))); + expected.append(makeQuad(QRectF(4, 8, 4, 2))); + expected.append(makeQuad(QRectF(8, 0, 2, 4))); + expected.append(makeQuad(QRectF(8, 4, 2, 4))); + expected.append(makeQuad(QRectF(8, 8, 2, 2))); + QTest::newRow("irregularGrid2") << orig << 4 << 11 << expected; +} + +void WindowQuadListTest::testMakeGrid() +{ + QFETCH(KWin::WindowQuadList, orig); + QFETCH(int, quadSize); + KWin::WindowQuadList actual = orig.makeGrid(quadSize); + QTEST(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 (actualVertex.u() != expectedVertex.u()) return false; + if (actualVertex.v() != expectedVertex.v()) return false; + if (actualVertex.originalX() != expectedVertex.originalX()) return false; + if (actualVertex.originalY() != expectedVertex.originalY()) return false; + if (actualVertex.textureX() != expectedVertex.textureX()) return false; + if (actualVertex.textureY() != expectedVertex.textureY()) 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(QRectF(0, 0, 10, 10))); + expected.append(makeQuad(QRectF(0, 0, 10, 10))); + QTest::newRow("noSplit") << orig << 1 << 1 << 1 << expected; + + expected.clear(); + expected.append(makeQuad(QRectF(0, 0, 5, 10))); + expected.append(makeQuad(QRectF(5, 0, 5, 10))); + QTest::newRow("xSplit") << orig << 2 << 1 << 2 << expected; + + expected.clear(); + expected.append(makeQuad(QRectF(0, 0, 10, 5))); + expected.append(makeQuad(QRectF(0, 5, 10, 5))); + QTest::newRow("ySplit") << orig << 1 << 2 << 2 << expected; + + expected.clear(); + expected.append(makeQuad(QRectF(0, 0, 5, 5))); + expected.append(makeQuad(QRectF(5, 0, 5, 5))); + expected.append(makeQuad(QRectF(0, 5, 5, 5))); + expected.append(makeQuad(QRectF(5, 5, 5, 5))); + QTest::newRow("xySplit") << orig << 2 << 2 << 4 << expected; + + orig.append(makeQuad(QRectF(0, 10, 4, 2))); + expected.clear(); + expected.append(makeQuad(QRectF(0, 0, 5, 3))); + expected.append(makeQuad(QRectF(5, 0, 5, 3))); + expected.append(makeQuad(QRectF(0, 3, 5, 3))); + expected.append(makeQuad(QRectF(5, 3, 5, 3))); + expected.append(makeQuad(QRectF(0, 6, 5, 3))); + expected.append(makeQuad(QRectF(5, 6, 5, 3))); + expected.append(makeQuad(QRectF(0, 9, 5, 1))); + expected.append(makeQuad(QRectF(0, 10, 4, 2))); + expected.append(makeQuad(QRectF(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); + KWin::WindowQuadList actual = orig.makeRegularGrid(xSubdivisions, ySubdivisions); + QTEST(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 (actualVertex.u() != expectedVertex.u()) return false; + if (actualVertex.v() != expectedVertex.v()) return false; + if (actualVertex.originalX() != expectedVertex.originalX()) return false; + if (actualVertex.originalY() != expectedVertex.originalY()) return false; + if (actualVertex.textureX() != expectedVertex.textureX()) return false; + if (actualVertex.textureY() != expectedVertex.textureY()) 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/autotests/libxrenderutils/CMakeLists.txt b/autotests/libxrenderutils/CMakeLists.txt new file mode 100644 index 0000000..ceefcc2 --- /dev/null +++ b/autotests/libxrenderutils/CMakeLists.txt @@ -0,0 +1,13 @@ +add_executable(blendPictureTest blendpicture_test.cpp) +set_target_properties(blendPictureTest PROPERTIES COMPILE_DEFINITIONS "NO_NONE_WINDOW") +add_test(NAME xrenderutils-blendPictureTest COMMAND blendPictureTest) +target_link_libraries(blendPictureTest + kwinxrenderutils + Qt5::Test + Qt5::Gui + Qt5::X11Extras + XCB::XCB + XCB::RENDER + XCB::XFIXES +) +ecm_mark_as_test(blendPictureTest) diff --git a/autotests/libxrenderutils/blendpicture_test.cpp b/autotests/libxrenderutils/blendpicture_test.cpp new file mode 100644 index 0000000..9b04757 --- /dev/null +++ b/autotests/libxrenderutils/blendpicture_test.cpp @@ -0,0 +1,50 @@ +/* + 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 +#include + +#include "../testutils.h" +#include "../../libkwineffects/kwinxrenderutils.h" + +class BlendPictureTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + + void testDontCrashOnTeardown(); +}; + +void BlendPictureTest::initTestCase() +{ + KWin::XRenderUtils::init(QX11Info::connection(), QX11Info::appRootWindow()); +} + +void BlendPictureTest::cleanupTestCase() +{ + KWin::XRenderUtils::cleanup(); +} + +void BlendPictureTest::testDontCrashOnTeardown() +{ + // this test uses xrenderBlendPicture - the only idea is to trigger the creation + // closing the application should not crash + // see BUG 363251 + const auto picture = KWin::xRenderBlendPicture(0.5); + // and a second one + const auto picture2 = KWin::xRenderBlendPicture(0.6); + Q_UNUSED(picture) + Q_UNUSED(picture2) +} + +Q_CONSTRUCTOR_FUNCTION(forceXcb) +QTEST_MAIN(BlendPictureTest) +#include "blendpicture_test.moc" diff --git a/autotests/mock_abstract_client.cpp b/autotests/mock_abstract_client.cpp new file mode 100644 index 0000000..969447d --- /dev/null +++ b/autotests/mock_abstract_client.cpp @@ -0,0 +1,106 @@ +/* + 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 "mock_abstract_client.h" + +namespace KWin +{ + +AbstractClient::AbstractClient(QObject *parent) + : QObject(parent) + , m_active(false) + , m_screen(0) + , m_fullscreen(false) + , m_hiddenInternal(false) + , m_keepBelow(false) + , m_frameGeometry() + , m_resize(false) +{ +} + +AbstractClient::~AbstractClient() = default; + +bool AbstractClient::isActive() const +{ + return m_active; +} + +void AbstractClient::setActive(bool active) +{ + m_active = active; +} + +void AbstractClient::setScreen(int screen) +{ + m_screen = screen; +} + +bool AbstractClient::isOnScreen(int screen) const +{ + // TODO: mock checking client geometry + return screen == m_screen; +} + +int AbstractClient::screen() const +{ + return m_screen; +} + +void AbstractClient::setFullScreen(bool set) +{ + m_fullscreen = set; +} + +bool AbstractClient::isFullScreen() const +{ + return m_fullscreen; +} + +bool AbstractClient::isHiddenInternal() const +{ + return m_hiddenInternal; +} + +void AbstractClient::setHiddenInternal(bool set) +{ + m_hiddenInternal = set; +} + +void AbstractClient::setFrameGeometry(const QRect &rect) +{ + m_frameGeometry = rect; + emit geometryChanged(); +} + +QRect AbstractClient::frameGeometry() const +{ + return m_frameGeometry; +} + +bool AbstractClient::keepBelow() const +{ + return m_keepBelow; +} + +void AbstractClient::setKeepBelow(bool keepBelow) +{ + m_keepBelow = keepBelow; + emit keepBelowChanged(); +} + +bool AbstractClient::isResize() const +{ + return m_resize; +} + +void AbstractClient::setResize(bool set) +{ + m_resize = set; +} + +} diff --git a/autotests/mock_abstract_client.h b/autotests/mock_abstract_client.h new file mode 100644 index 0000000..268e003 --- /dev/null +++ b/autotests/mock_abstract_client.h @@ -0,0 +1,59 @@ +/* + 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 +*/ +#ifndef KWIN_MOCK_ABSTRACT_CLIENT_H +#define KWIN_MOCK_ABSTRACT_CLIENT_H + +#include +#include + +namespace KWin +{ + +class AbstractClient : public QObject +{ + Q_OBJECT +public: + explicit AbstractClient(QObject *parent); + ~AbstractClient() override; + + int screen() const; + bool isOnScreen(int screen) const; + bool isActive() const; + bool isFullScreen() const; + bool isHiddenInternal() const; + QRect frameGeometry() const; + bool keepBelow() const; + + void setActive(bool active); + void setScreen(int screen); + void setFullScreen(bool set); + void setHiddenInternal(bool set); + void setFrameGeometry(const QRect &rect); + void setKeepBelow(bool); + bool isResize() const; + void setResize(bool set); + virtual void showOnScreenEdge() = 0; + +Q_SIGNALS: + void geometryChanged(); + void keepBelowChanged(); + +private: + bool m_active; + int m_screen; + bool m_fullscreen; + bool m_hiddenInternal; + bool m_keepBelow; + QRect m_frameGeometry; + bool m_resize; +}; + +} + +#endif diff --git a/autotests/mock_effectshandler.cpp b/autotests/mock_effectshandler.cpp new file mode 100644 index 0000000..b493992 --- /dev/null +++ b/autotests/mock_effectshandler.cpp @@ -0,0 +1,27 @@ +/* + 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 "mock_effectshandler.h" + +MockEffectsHandler::MockEffectsHandler(KWin::CompositingType type) + : EffectsHandler(type) +{ +} + + +KSharedConfigPtr MockEffectsHandler::config() const +{ + static const KSharedConfigPtr s_config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + return s_config; +} + +KSharedConfigPtr MockEffectsHandler::inputConfig() const +{ + static const KSharedConfigPtr s_inputConfig = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + return s_inputConfig; +} diff --git a/autotests/mock_effectshandler.h b/autotests/mock_effectshandler.h new file mode 100644 index 0000000..9a0f6a6 --- /dev/null +++ b/autotests/mock_effectshandler.h @@ -0,0 +1,280 @@ +/* + 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 +*/ +#ifndef MOCK_EFFECTS_HANDLER_H +#define MOCK_EFFECTS_HANDLER_H + +#include +#include + +class MockEffectsHandler : public KWin::EffectsHandler +{ + Q_OBJECT +public: + explicit MockEffectsHandler(KWin::CompositingType type); + void activateWindow(KWin::EffectWindow *) override {} + KWin::Effect *activeFullScreenEffect() const override { + return nullptr; + } + bool hasActiveFullScreenEffect() const override { + return false; + } + int activeScreen() const override { + return 0; + } + KWin::EffectWindow *activeWindow() const override { + return nullptr; + } + void addRepaint(const QRect &) override {} + void addRepaint(const QRegion &) override {} + void addRepaint(int, int, int, int) override {} + void addRepaintFull() override {} + double animationTimeFactor() const override { + return 0; + } + xcb_atom_t announceSupportProperty(const QByteArray &, KWin::Effect *) override { + return XCB_ATOM_NONE; + } + void buildQuads(KWin::EffectWindow *, KWin::WindowQuadList &) override {} + QRect clientArea(KWin::clientAreaOption, const QPoint &, int) const override { + return QRect(); + } + QRect clientArea(KWin::clientAreaOption, const KWin::EffectWindow *) const override { + return QRect(); + } + QRect clientArea(KWin::clientAreaOption, int, int) const override { + return QRect(); + } + void closeTabBox() override {} + QString currentActivity() const override { + return QString(); + } + int currentDesktop() const override { + return 0; + } + int currentTabBoxDesktop() const override { + return 0; + } + QList< int > currentTabBoxDesktopList() const override { + return QList(); + } + KWin::EffectWindow *currentTabBoxWindow() const override { + return nullptr; + } + KWin::EffectWindowList currentTabBoxWindowList() const override { + return KWin::EffectWindowList(); + } + QPoint cursorPos() const override { + return QPoint(); + } + bool decorationsHaveAlpha() const override { + return false; + } + bool decorationSupportsBlurBehind() const override { + return false; + } + void defineCursor(Qt::CursorShape) override {} + int desktopAbove(int, bool) const override { + return 0; + } + int desktopAtCoords(QPoint) const override { + return 0; + } + int desktopBelow(int, bool) const override { + return 0; + } + QPoint desktopCoords(int) const override { + return QPoint(); + } + QPoint desktopGridCoords(int) const override { + return QPoint(); + } + int desktopGridHeight() const override { + return 0; + } + QSize desktopGridSize() const override { + return QSize(); + } + int desktopGridWidth() const override { + return 0; + } + QString desktopName(int) const override { + return QString(); + } + int desktopToLeft(int, bool) const override { + return 0; + } + int desktopToRight(int, bool) const override { + return 0; + } + void doneOpenGLContextCurrent() override {} + void drawWindow(KWin::EffectWindow *, int, const QRegion &, KWin::WindowPaintData &) override {} + KWin::EffectFrame *effectFrame(KWin::EffectFrameStyle, bool, const QPoint &, Qt::Alignment) const override { + return nullptr; + } + KWin::EffectWindow *findWindow(WId) const override { + return nullptr; + } + KWin::EffectWindow *findWindow(KWaylandServer::SurfaceInterface *) const override { + return nullptr; + } + KWin::EffectWindow *findWindow(QWindow *w) const override { + Q_UNUSED(w) + return nullptr; + } + KWin::EffectWindow *findWindow(const QUuid &id) const override { + Q_UNUSED(id) + return nullptr; + } + void *getProxy(QString) override { + return nullptr; + } + bool grabKeyboard(KWin::Effect *) override { + return false; + } + bool hasDecorationShadows() const override { + return false; + } + bool isScreenLocked() const override { + return false; + } + QVariant kwinOption(KWin::KWinOption) override { + return QVariant(); + } + bool makeOpenGLContextCurrent() override { + return false; + } + void moveWindow(KWin::EffectWindow *, const QPoint &, bool, double) override {} + KWin::WindowQuadType newWindowQuadType() override { + return KWin::WindowQuadError; + } + int numberOfDesktops() const override { + return 0; + } + int numScreens() const override { + return 0; + } + bool optionRollOverDesktops() const override { + return false; + } + void paintEffectFrame(KWin::EffectFrame *, const QRegion &, double, double) override {} + void paintScreen(int, const QRegion &, KWin::ScreenPaintData &) override {} + void paintWindow(KWin::EffectWindow *, int, const QRegion &, KWin::WindowPaintData &) override {} + void postPaintScreen() override {} + void postPaintWindow(KWin::EffectWindow *) override {} + void prePaintScreen(KWin::ScreenPrePaintData &, int) override {} + void prePaintWindow(KWin::EffectWindow *, KWin::WindowPrePaintData &, int) override {} + QByteArray readRootProperty(long int, long int, int) const override { + return QByteArray(); + } + void reconfigure() override {} + void refTabBox() override {} + void registerAxisShortcut(Qt::KeyboardModifiers, KWin::PointerAxisDirection, QAction *) override {} + void registerGlobalShortcut(const QKeySequence &, QAction *) override {} + void registerPointerShortcut(Qt::KeyboardModifiers, Qt::MouseButton, QAction *) override {} + void registerTouchpadSwipeShortcut(KWin::SwipeDirection, QAction *) override {} + void reloadEffect(KWin::Effect *) override {} + void removeSupportProperty(const QByteArray &, KWin::Effect *) override {} + void reserveElectricBorder(KWin::ElectricBorder, KWin::Effect *) override {} + void registerTouchBorder(KWin::ElectricBorder, QAction *) override {} + void unregisterTouchBorder(KWin::ElectricBorder, QAction *) override {} + QPainter *scenePainter() override { + return nullptr; + } + int screenNumber(const QPoint &) const override { + return 0; + } + void setActiveFullScreenEffect(KWin::Effect *) override {} + void setCurrentDesktop(int) override {} + void setElevatedWindow(KWin::EffectWindow *, bool) override {} + void setNumberOfDesktops(int) override {} + void setShowingDesktop(bool) override {} + void setTabBoxDesktop(int) override {} + void setTabBoxWindow(KWin::EffectWindow*) override {} + KWin::EffectWindowList stackingOrder() const override { + return KWin::EffectWindowList(); + } + void startMouseInterception(KWin::Effect *, Qt::CursorShape) override {} + void startMousePolling() override {} + void stopMouseInterception(KWin::Effect *) override {} + void stopMousePolling() override {} + void ungrabKeyboard() override {} + void unrefTabBox() override {} + void unreserveElectricBorder(KWin::ElectricBorder, KWin::Effect *) override {} + QRect virtualScreenGeometry() const override { + return QRect(); + } + QSize virtualScreenSize() const override { + return QSize(); + } + void windowToDesktop(KWin::EffectWindow *, int) override {} + void windowToScreen(KWin::EffectWindow *, int) override {} + int workspaceHeight() const override { + return 0; + } + int workspaceWidth() const override { + return 0; + } + long unsigned int xrenderBufferPicture() override { + return 0; + } + xcb_connection_t *xcbConnection() const override { + return QX11Info::connection(); + } + xcb_window_t x11RootWindow() const override { + return QX11Info::appRootWindow(); + } + KWaylandServer::Display *waylandDisplay() const override { + return nullptr; + } + + bool animationsSupported() const override { + return m_animationsSuported; + } + void setAnimationsSupported(bool set) { + m_animationsSuported = set; + } + + KWin::PlatformCursorImage cursorImage() const override { + return KWin::PlatformCursorImage(); + } + + void hideCursor() override {} + + void showCursor() override {} + + void startInteractiveWindowSelection(std::function callback) override { + callback(nullptr); + } + void startInteractivePositionSelection(std::function callback) override { + callback(QPoint(-1, -1)); + } + void showOnScreenMessage(const QString &message, const QString &iconName = QString()) override { + Q_UNUSED(message) + Q_UNUSED(iconName) + } + void hideOnScreenMessage(OnScreenMessageHideFlags flags = OnScreenMessageHideFlags()) override { Q_UNUSED(flags)} + + void windowToDesktops(KWin::EffectWindow *w, const QVector &desktops) override { + Q_UNUSED(w) + Q_UNUSED(desktops) + } + + KSharedConfigPtr config() const override; + KSharedConfigPtr inputConfig() const override; + void renderEffectQuickView(KWin::EffectQuickView *quickView) const override { + Q_UNUSED(quickView); + } + KWin::SessionState sessionState() const override { + return KWin::SessionState::Normal; + } + +private: + bool m_animationsSuported = true; +}; +#endif diff --git a/autotests/mock_screens.cpp b/autotests/mock_screens.cpp new file mode 100644 index 0000000..5c9b2e9 --- /dev/null +++ b/autotests/mock_screens.cpp @@ -0,0 +1,87 @@ +/* + 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 "mock_screens.h" + +namespace KWin +{ + +MockScreens::MockScreens(QObject *parent) + : Screens(parent) +{ +} + +MockScreens::~MockScreens() = default; + +QRect MockScreens::geometry(int screen) const +{ + if (screen >= m_geometries.count()) { + return QRect(); + } + return m_geometries.at(screen); +} + +QString MockScreens::name(int screen) const +{ + Q_UNUSED(screen); + return QLatin1String("MoccaScreen"); // mock-a-screen =) +} + +float MockScreens::refreshRate(int screen) const +{ + Q_UNUSED(screen); + return 60.0f; +} + +QSize MockScreens::size(int screen) const +{ + return geometry(screen).size(); +} + +int MockScreens::number(const QPoint &pos) const +{ + int bestScreen = 0; + int minDistance = INT_MAX; + for (int i = 0; i < m_geometries.size(); ++i) { + const QRect &geo = m_geometries.at(i); + if (geo.contains(pos)) { + return i; + } + int distance = QPoint(geo.topLeft() - pos).manhattanLength(); + distance = qMin(distance, QPoint(geo.topRight() - pos).manhattanLength()); + distance = qMin(distance, QPoint(geo.bottomRight() - pos).manhattanLength()); + distance = qMin(distance, QPoint(geo.bottomLeft() - pos).manhattanLength()); + if (distance < minDistance) { + minDistance = distance; + bestScreen = i; + } + } + return bestScreen; +} + +void MockScreens::init() +{ + Screens::init(); + m_scheduledGeometries << QRect(0, 0, 100, 100); + updateCount(); +} + +void MockScreens::updateCount() +{ + m_geometries = m_scheduledGeometries; + setCount(m_geometries.size()); + emit changed(); +} + +void MockScreens::setGeometries(const QList< QRect > &geometries) +{ + m_scheduledGeometries = geometries; + startChangedTimer(); +} + +} diff --git a/autotests/mock_screens.h b/autotests/mock_screens.h new file mode 100644 index 0000000..ac7d157 --- /dev/null +++ b/autotests/mock_screens.h @@ -0,0 +1,42 @@ +/* + 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 +*/ +#ifndef KWIN_MOCK_SCREENS_H +#define KWIN_MOCK_SCREENS_H + +#include "../screens.h" + +namespace KWin +{ + +class MockScreens : public Screens +{ + Q_OBJECT +public: + explicit MockScreens(QObject *parent = nullptr); + ~MockScreens() override; + QRect geometry(int screen) const override; + int number(const QPoint &pos) const override; + QString name(int screen) const override; + float refreshRate(int screen) const override; + QSize size(int screen) const override; + void init() override; + + void setGeometries(const QList &geometries); + +protected Q_SLOTS: + void updateCount() override; + +private: + QList m_scheduledGeometries; + QList m_geometries; +}; + +} + +#endif diff --git a/autotests/mock_workspace.cpp b/autotests/mock_workspace.cpp new file mode 100644 index 0000000..adcb65d --- /dev/null +++ b/autotests/mock_workspace.cpp @@ -0,0 +1,79 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "mock_workspace.h" +#include "mock_abstract_client.h" + +namespace KWin +{ + +Workspace *MockWorkspace::s_self = nullptr; + +MockWorkspace::MockWorkspace(QObject *parent) + : QObject(parent) + , m_activeClient(nullptr) + , m_moveResizeClient(nullptr) + , m_showingDesktop(false) +{ + s_self = this; +} + +MockWorkspace::~MockWorkspace() +{ + s_self = nullptr; +} + +AbstractClient *MockWorkspace::activeClient() const +{ + return m_activeClient; +} + +void MockWorkspace::setActiveClient(AbstractClient *c) +{ + m_activeClient = c; +} + +AbstractClient *MockWorkspace::moveResizeClient() const +{ + return m_moveResizeClient; +} + +void MockWorkspace::setMoveResizeClient(AbstractClient *c) +{ + m_moveResizeClient = c; +} + +void MockWorkspace::setShowingDesktop(bool showing) +{ + m_showingDesktop = showing; +} + +bool MockWorkspace::showingDesktop() const +{ + return m_showingDesktop; +} + +QRect MockWorkspace::clientArea(clientAreaOption, int screen, int desktop) const +{ + Q_UNUSED(screen) + Q_UNUSED(desktop) + return QRect(); +} + +void MockWorkspace::registerEventFilter(X11EventFilter *filter) +{ + Q_UNUSED(filter) +} + +void MockWorkspace::unregisterEventFilter(X11EventFilter *filter) +{ + Q_UNUSED(filter) +} + +} + diff --git a/autotests/mock_workspace.h b/autotests/mock_workspace.h new file mode 100644 index 0000000..11c0123 --- /dev/null +++ b/autotests/mock_workspace.h @@ -0,0 +1,72 @@ +/* + 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 +*/ +#ifndef KWIN_MOCK_WORKSPACE_H +#define KWIN_MOCK_WORKSPACE_H + +#include +#include + +namespace KWin +{ + +class AbstractClient; +class X11Client; +class X11EventFilter; + +class MockWorkspace; +typedef MockWorkspace Workspace; + +class MockWorkspace : public QObject +{ + Q_OBJECT +public: + explicit MockWorkspace(QObject *parent = nullptr); + ~MockWorkspace() override; + AbstractClient *activeClient() const; + AbstractClient *moveResizeClient() const; + void setShowingDesktop(bool showing); + bool showingDesktop() const; + QRect clientArea(clientAreaOption, int screen, int desktop) const; + + void setActiveClient(AbstractClient *c); + void setMoveResizeClient(AbstractClient *c); + + void registerEventFilter(X11EventFilter *filter); + void unregisterEventFilter(X11EventFilter *filter); + + bool compositing() const { + return false; + } + + static Workspace *self(); + +Q_SIGNALS: + void clientRemoved(KWin::X11Client *); + +private: + AbstractClient *m_activeClient; + AbstractClient *m_moveResizeClient; + bool m_showingDesktop; + static Workspace *s_self; +}; + +inline +Workspace *MockWorkspace::self() +{ + return s_self; +} + +inline Workspace *workspace() +{ + return Workspace::self(); +} + +} + +#endif diff --git a/autotests/mock_x11client.cpp b/autotests/mock_x11client.cpp new file mode 100644 index 0000000..cebb740 --- /dev/null +++ b/autotests/mock_x11client.cpp @@ -0,0 +1,27 @@ +/* + 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 "mock_x11client.h" + +namespace KWin +{ + +X11Client::X11Client(QObject *parent) + : AbstractClient(parent) +{ +} + +X11Client::~X11Client() = default; + +void X11Client::showOnScreenEdge() +{ + setKeepBelow(false); + setHiddenInternal(false); +} + +} diff --git a/autotests/mock_x11client.h b/autotests/mock_x11client.h new file mode 100644 index 0000000..d2f44ff --- /dev/null +++ b/autotests/mock_x11client.h @@ -0,0 +1,32 @@ +/* + 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 +*/ +#ifndef KWIN_MOCK_CLIENT_H +#define KWIN_MOCK_CLIENT_H + +#include + +#include +#include + +namespace KWin +{ + +class X11Client : public AbstractClient +{ + Q_OBJECT +public: + explicit X11Client(QObject *parent); + ~X11Client() override; + void showOnScreenEdge() override; + +}; + +} + +#endif diff --git a/autotests/onscreennotificationtest.cpp b/autotests/onscreennotificationtest.cpp new file mode 100644 index 0000000..6ea01d1 --- /dev/null +++ b/autotests/onscreennotificationtest.cpp @@ -0,0 +1,125 @@ +/* + 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 "../onscreennotification.h" +#include "../input.h" + +#include +#include + +#include +#include +#include + +QTEST_MAIN(OnScreenNotificationTest); + +namespace KWin +{ + +void InputRedirection::installInputEventSpy(InputEventSpy *spy) +{ + Q_UNUSED(spy); +} + +void InputRedirection::uninstallInputEventSpy(InputEventSpy *spy) +{ + Q_UNUSED(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("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); + QVERIFY(iconNameChangedSpy.isValid()); + 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); + QVERIFY(messageChangedSpy.isValid()); + 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); +} diff --git a/autotests/onscreennotificationtest.h b/autotests/onscreennotificationtest.h new file mode 100644 index 0000000..2576184 --- /dev/null +++ b/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 slots: + + void show(); + void timeout(); + void iconName(); + void message(); +}; + +#endif // ONSCREENNOTIFICATIONTEST_H diff --git a/autotests/opengl_context_attribute_builder_test.cpp b/autotests/opengl_context_attribute_builder_test.cpp new file mode 100644 index 0000000..8128765 --- /dev/null +++ b/autotests/opengl_context_attribute_builder_test.cpp @@ -0,0 +1,431 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "../abstract_opengl_context_attribute_builder.h" +#include "../egl_context_attribute_builder.h" +#include +#include + +#include +#if HAVE_EPOXY_GLX +#include "../plugins/platforms/x11/standalone/glx_context_attribute_builder.h" +#include + +#ifndef GLX_GENERATE_RESET_ON_VIDEO_MEMORY_PURGE_NV +#define GLX_GENERATE_RESET_ON_VIDEO_MEMORY_PURGE_NV 0x20F7 +#endif +#endif + +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(); + void testGlx_data(); + void testGlx(); +}; + +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"); +} + +void OpenGLContextAttributeBuilderTest::testGlx_data() +{ +#if HAVE_EPOXY_GLX + QTest::addColumn("requestVersion"); + QTest::addColumn("major"); + QTest::addColumn("minor"); + QTest::addColumn("robust"); + QTest::addColumn("videoPurge"); + QTest::addColumn>("expectedAttribs"); + + QTest::newRow("fallback") << true << 2 << 1 << false << false << std::vector{ + GLX_CONTEXT_MAJOR_VERSION_ARB, 2, + GLX_CONTEXT_MINOR_VERSION_ARB, 1, + 0}; + QTest::newRow("legacy/robust") << false << 0 << 0 << true << false << std::vector{ + GLX_CONTEXT_FLAGS_ARB, GLX_CONTEXT_ROBUST_ACCESS_BIT_ARB, + GLX_CONTEXT_RESET_NOTIFICATION_STRATEGY_ARB, GLX_LOSE_CONTEXT_ON_RESET_ARB, + 0 + }; + QTest::newRow("legacy/robust/videoPurge") << false << 0 << 0 << true << true << std::vector{ + GLX_CONTEXT_FLAGS_ARB, GLX_CONTEXT_ROBUST_ACCESS_BIT_ARB, + GLX_CONTEXT_RESET_NOTIFICATION_STRATEGY_ARB, GLX_LOSE_CONTEXT_ON_RESET_ARB, + GLX_GENERATE_RESET_ON_VIDEO_MEMORY_PURGE_NV, GL_TRUE, + 0 + }; + QTest::newRow("core") << true << 3 << 1 << false << false << std::vector{ + GLX_CONTEXT_MAJOR_VERSION_ARB, 3, + GLX_CONTEXT_MINOR_VERSION_ARB, 1, + 0}; + QTest::newRow("core/robust") << true << 3 << 1 << true << false << std::vector{ + GLX_CONTEXT_MAJOR_VERSION_ARB, 3, + GLX_CONTEXT_MINOR_VERSION_ARB, 1, + GLX_CONTEXT_FLAGS_ARB, GLX_CONTEXT_ROBUST_ACCESS_BIT_ARB, + GLX_CONTEXT_RESET_NOTIFICATION_STRATEGY_ARB, GLX_LOSE_CONTEXT_ON_RESET_ARB, + 0 + }; + QTest::newRow("core/robust/videoPurge") << true << 3 << 1 << true << true << std::vector{ + GLX_CONTEXT_MAJOR_VERSION_ARB, 3, + GLX_CONTEXT_MINOR_VERSION_ARB, 1, + GLX_CONTEXT_FLAGS_ARB, GLX_CONTEXT_ROBUST_ACCESS_BIT_ARB, + GLX_CONTEXT_RESET_NOTIFICATION_STRATEGY_ARB, GLX_LOSE_CONTEXT_ON_RESET_ARB, + GLX_GENERATE_RESET_ON_VIDEO_MEMORY_PURGE_NV, GL_TRUE, + 0 + }; +#endif +} + +void OpenGLContextAttributeBuilderTest::testGlx() +{ +#if HAVE_EPOXY_GLX + QFETCH(bool, requestVersion); + QFETCH(int, major); + QFETCH(int, minor); + QFETCH(bool, robust); + QFETCH(bool, videoPurge); + + GlxContextAttributeBuilder builder; + if (requestVersion) { + builder.setVersion(major, minor); + } + builder.setRobust(robust); + builder.setResetOnVideoMemoryPurge(videoPurge); + + auto attribs = builder.build(); + QTEST(attribs, "expectedAttribs"); +#endif +} + +QTEST_GUILESS_MAIN(OpenGLContextAttributeBuilderTest) +#include "opengl_context_attribute_builder_test.moc" diff --git a/autotests/tabbox/CMakeLists.txt b/autotests/tabbox/CMakeLists.txt new file mode 100644 index 0000000..1f148bc --- /dev/null +++ b/autotests/tabbox/CMakeLists.txt @@ -0,0 +1,100 @@ +include_directories(${KWin_SOURCE_DIR}) +add_definitions(-DKWIN_UNIT_TEST) +######################################################## +# Test TabBox::ClientModel +######################################################## +set(testTabBoxClientModel_SRCS + ../../tabbox/clientmodel.cpp + ../../tabbox/desktopmodel.cpp + ../../tabbox/tabbox_logging.cpp + ../../tabbox/tabboxconfig.cpp + ../../tabbox/tabboxhandler.cpp + mock_tabboxclient.cpp + mock_tabboxhandler.cpp + test_tabbox_clientmodel.cpp +) + +add_executable(testTabBoxClientModel ${testTabBoxClientModel_SRCS}) +set_target_properties(testTabBoxClientModel PROPERTIES COMPILE_DEFINITIONS "NO_NONE_WINDOW") +target_link_libraries(testTabBoxClientModel + Qt5::Core + Qt5::DBus + Qt5::Quick + Qt5::Script + Qt5::Test + Qt5::Widgets + Qt5::X11Extras + + KF5::ConfigCore + KF5::I18n + KF5::Package + KF5::WindowSystem + + XCB::XCB +) +add_test(NAME kwin-testTabBoxClientModel COMMAND testTabBoxClientModel) +ecm_mark_as_test(testTabBoxClientModel) + +######################################################## +# Test TabBox::TabBoxHandler +######################################################## +set(testTabBoxHandler_SRCS + ../../tabbox/clientmodel.cpp + ../../tabbox/desktopmodel.cpp + ../../tabbox/tabbox_logging.cpp + ../../tabbox/tabboxconfig.cpp + ../../tabbox/tabboxhandler.cpp + mock_tabboxclient.cpp + mock_tabboxhandler.cpp + test_tabbox_handler.cpp +) + +add_executable(testTabBoxHandler ${testTabBoxHandler_SRCS}) +set_target_properties(testTabBoxHandler PROPERTIES COMPILE_DEFINITIONS "NO_NONE_WINDOW") +target_link_libraries(testTabBoxHandler + Qt5::Core + Qt5::DBus + Qt5::Quick + Qt5::Script + Qt5::Test + Qt5::Widgets + Qt5::X11Extras + + KF5::ConfigCore + KF5::I18n + KF5::Package + KF5::WindowSystem + + XCB::XCB +) +add_test(NAME kwin-testTabBoxHandler COMMAND testTabBoxHandler) +ecm_mark_as_test(testTabBoxHandler) + +######################################################## +# Test TabBox::TabBoxConfig +######################################################## +set(testTabBoxConfig_SRCS + ../../tabbox/tabbox_logging.cpp + ../../tabbox/tabboxconfig.cpp + test_tabbox_config.cpp +) + +add_executable(testTabBoxConfig ${testTabBoxConfig_SRCS}) +target_link_libraries(testTabBoxConfig Qt5::Core Qt5::Test) +add_test(NAME kwin-testTabBoxConfig COMMAND testTabBoxConfig) +ecm_mark_as_test(testTabBoxConfig) + + +######################################################## +# Test TabBox::DesktopChainManager +######################################################## +set(testDesktopChain_SRCS + ../../tabbox/desktopchain.cpp + ../../tabbox/tabbox_logging.cpp + test_desktopchain.cpp +) + +add_executable(testDesktopChain ${testDesktopChain_SRCS}) +target_link_libraries(testDesktopChain Qt5::Core Qt5::Test) +add_test(NAME kwin-testDesktopChain COMMAND testDesktopChain) +ecm_mark_as_test(testDesktopChain) diff --git a/autotests/tabbox/mock_tabboxclient.cpp b/autotests/tabbox/mock_tabboxclient.cpp new file mode 100644 index 0000000..ced75db --- /dev/null +++ b/autotests/tabbox/mock_tabboxclient.cpp @@ -0,0 +1,26 @@ +/* + 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 "mock_tabboxclient.h" +#include "mock_tabboxhandler.h" + +namespace KWin +{ + +MockTabBoxClient::MockTabBoxClient(QString caption) + : TabBoxClient() + , m_caption(caption) +{ +} + +void MockTabBoxClient::close() +{ + static_cast(TabBox::tabBox)->closeWindow(this); +} + +} // namespace KWin diff --git a/autotests/tabbox/mock_tabboxclient.h b/autotests/tabbox/mock_tabboxclient.h new file mode 100644 index 0000000..3d4c42e --- /dev/null +++ b/autotests/tabbox/mock_tabboxclient.h @@ -0,0 +1,63 @@ +/* + 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 +*/ +#ifndef KWIN_MOCK_TABBOX_CLIENT_H +#define KWIN_MOCK_TABBOX_CLIENT_H + +#include "../../tabbox/tabboxhandler.h" + +#include +#include + +namespace KWin +{ +class MockTabBoxClient : public TabBox::TabBoxClient +{ +public: + explicit MockTabBoxClient(QString caption); + bool isMinimized() const override { + return false; + } + QString caption() const override { + return m_caption; + } + void close() override; + int height() const override { + return 100; + } + virtual QPixmap icon(const QSize &size = QSize(32, 32)) const { + return QPixmap(size); + } + bool isCloseable() const override { + return true; + } + bool isFirstInTabBox() const override { + return false; + } + int width() const override { + return 100; + } + int x() const override { + return 0; + } + int y() const override { + return 0; + } + QIcon icon() const override { + return QIcon(); + } + + QUuid internalId() const override { + return QUuid{}; + } + +private: + QString m_caption; +}; +} // namespace KWin +#endif diff --git a/autotests/tabbox/mock_tabboxhandler.cpp b/autotests/tabbox/mock_tabboxhandler.cpp new file mode 100644 index 0000000..cc797e5 --- /dev/null +++ b/autotests/tabbox/mock_tabboxhandler.cpp @@ -0,0 +1,111 @@ +/* + 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 "mock_tabboxhandler.h" +#include "mock_tabboxclient.h" + +namespace KWin +{ + +MockTabBoxHandler::MockTabBoxHandler(QObject *parent) + : TabBoxHandler(parent) +{ +} + +MockTabBoxHandler::~MockTabBoxHandler() +{ +} + +void MockTabBoxHandler::grabbedKeyEvent(QKeyEvent *event) const +{ + Q_UNUSED(event) +} + +QWeakPointer< TabBox::TabBoxClient > MockTabBoxHandler::activeClient() const +{ + return m_activeClient; +} + +void MockTabBoxHandler::setActiveClient(const QWeakPointer< TabBox::TabBoxClient >& client) +{ + m_activeClient = client; +} + +QWeakPointer< TabBox::TabBoxClient > MockTabBoxHandler::clientToAddToList(TabBox::TabBoxClient *client, int desktop) const +{ + Q_UNUSED(desktop) + QList< QSharedPointer< TabBox::TabBoxClient > >::const_iterator it = m_windows.constBegin(); + for (; it != m_windows.constEnd(); ++it) { + if ((*it).data() == client) { + return QWeakPointer< TabBox::TabBoxClient >(*it); + } + } + return QWeakPointer< TabBox::TabBoxClient >(); +} + +QWeakPointer< TabBox::TabBoxClient > MockTabBoxHandler::nextClientFocusChain(TabBox::TabBoxClient *client) const +{ + QList< QSharedPointer< TabBox::TabBoxClient > >::const_iterator it = m_windows.constBegin(); + for (; it != m_windows.constEnd(); ++it) { + if ((*it).data() == client) { + ++it; + if (it == m_windows.constEnd()) { + return QWeakPointer< TabBox::TabBoxClient >(m_windows.first()); + } else { + return QWeakPointer< TabBox::TabBoxClient >(*it); + } + } + } + if (!m_windows.isEmpty()) { + return QWeakPointer< TabBox::TabBoxClient >(m_windows.last()); + } + return QWeakPointer< TabBox::TabBoxClient >(); +} + +QWeakPointer< TabBox::TabBoxClient > MockTabBoxHandler::firstClientFocusChain() const +{ + if (m_windows.isEmpty()) { + return QWeakPointer(); + } + return m_windows.first(); +} + +bool MockTabBoxHandler::isInFocusChain(TabBox::TabBoxClient *client) const +{ + if (!client) { + return false; + } + QList< QSharedPointer< TabBox::TabBoxClient > >::const_iterator it = m_windows.constBegin(); + for (; it != m_windows.constEnd(); ++it) { + if ((*it).data() == client) { + return true; + } + } + return false; +} + +QWeakPointer< TabBox::TabBoxClient > MockTabBoxHandler::createMockWindow(const QString &caption) +{ + QSharedPointer< TabBox::TabBoxClient > client(new MockTabBoxClient(caption)); + m_windows.append(client); + m_activeClient = client; + return QWeakPointer< TabBox::TabBoxClient >(client); +} + +void MockTabBoxHandler::closeWindow(TabBox::TabBoxClient *client) +{ + QList< QSharedPointer< TabBox::TabBoxClient > >::iterator it = m_windows.begin(); + for (; it != m_windows.end(); ++it) { + if ((*it).data() == client) { + m_windows.erase(it); + return; + } + } +} + +} // namespace KWin diff --git a/autotests/tabbox/mock_tabboxhandler.h b/autotests/tabbox/mock_tabboxhandler.h new file mode 100644 index 0000000..0a3ebd8 --- /dev/null +++ b/autotests/tabbox/mock_tabboxhandler.h @@ -0,0 +1,99 @@ +/* + 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 +*/ +#ifndef KWIN_MOCK_TABBOX_HANDLER_H +#define KWIN_MOCK_TABBOX_HANDLER_H + +#include "../../tabbox/tabboxhandler.h" +namespace KWin +{ +class MockTabBoxHandler : public TabBox::TabBoxHandler +{ + Q_OBJECT +public: + MockTabBoxHandler(QObject *parent = nullptr); + ~MockTabBoxHandler() override; + void activateAndClose() override { + } + QWeakPointer< TabBox::TabBoxClient > activeClient() const override; + void setActiveClient(const QWeakPointer &client); + int activeScreen() const override { + return 0; + } + QWeakPointer< TabBox::TabBoxClient > clientToAddToList(TabBox::TabBoxClient *client, int desktop) const override; + int currentDesktop() const override { + return 1; + } + QWeakPointer< TabBox::TabBoxClient > desktopClient() const override { + return QWeakPointer(); + } + QString desktopName(int desktop) const override { + Q_UNUSED(desktop) + return "desktop 1"; + } + QString desktopName(TabBox::TabBoxClient *client) const override { + Q_UNUSED(client) + return "desktop"; + } + void elevateClient(TabBox::TabBoxClient *c, QWindow *tabbox, bool elevate) const override { + Q_UNUSED(c) + Q_UNUSED(tabbox) + Q_UNUSED(elevate) + } + void shadeClient(TabBox::TabBoxClient *c, bool b) const override { + Q_UNUSED(c) + Q_UNUSED(b) + } + virtual void hideOutline() { + } + QWeakPointer< TabBox::TabBoxClient > nextClientFocusChain(TabBox::TabBoxClient *client) const override; + QWeakPointer firstClientFocusChain() const override; + bool isInFocusChain (TabBox::TabBoxClient* client) const override; + int nextDesktopFocusChain(int desktop) const override { + Q_UNUSED(desktop) + return 1; + } + int numberOfDesktops() const override { + return 1; + } + bool isKWinCompositing() const override { + return false; + } + void raiseClient(TabBox::TabBoxClient *c) const override { + Q_UNUSED(c) + } + void restack(TabBox::TabBoxClient *c, TabBox::TabBoxClient *under) override { + Q_UNUSED(c) + Q_UNUSED(under) + } + virtual void showOutline(const QRect &outline) { + Q_UNUSED(outline) + } + TabBox::TabBoxClientList stackingOrder() const override { + return TabBox::TabBoxClientList(); + } + void grabbedKeyEvent(QKeyEvent *event) const override; + + void highlightWindows(TabBox::TabBoxClient *window = nullptr, QWindow *controller = nullptr) override { + Q_UNUSED(window) + Q_UNUSED(controller) + } + + bool noModifierGrab() const override { + return false; + } + + // mock methods + QWeakPointer createMockWindow(const QString &caption); + void closeWindow(TabBox::TabBoxClient *client); +private: + QList< QSharedPointer > m_windows; + QWeakPointer m_activeClient; +}; +} // namespace KWin +#endif diff --git a/autotests/tabbox/test_desktopchain.cpp b/autotests/tabbox/test_desktopchain.cpp new file mode 100644 index 0000000..7841635 --- /dev/null +++ b/autotests/tabbox/test_desktopchain.cpp @@ -0,0 +1,252 @@ +/* + 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 +*/ + +// KWin +#include "../../tabbox/desktopchain.h" + +#include + +using namespace KWin::TabBox; + +class TestDesktopChain : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void chainInit_data(); + void chainInit(); + void chainAdd_data(); + void chainAdd(); + void resize_data(); + void resize(); + void resizeAdd(); + void useChain(); +}; + +void TestDesktopChain::chainInit_data() +{ + QTest::addColumn("size"); + QTest::addColumn("next"); + QTest::addColumn("result"); + + QTest::newRow("0/1") << (uint)0 << (uint)1 << (uint)1; + QTest::newRow("0/5") << (uint)0 << (uint)5 << (uint)1; + QTest::newRow("1/1") << (uint)1 << (uint)1 << (uint)1; + QTest::newRow("1/2") << (uint)1 << (uint)2 << (uint)1; + QTest::newRow("4/1") << (uint)4 << (uint)1 << (uint)2; + QTest::newRow("4/2") << (uint)4 << (uint)2 << (uint)3; + QTest::newRow("4/3") << (uint)4 << (uint)3 << (uint)4; + QTest::newRow("4/4") << (uint)4 << (uint)4 << (uint)1; + QTest::newRow("4/5") << (uint)4 << (uint)5 << (uint)1; + QTest::newRow("4/7") << (uint)4 << (uint)7 << (uint)1; +} + +void TestDesktopChain::chainInit() +{ + QFETCH(uint, size); + QFETCH(uint, next); + DesktopChain chain(size); + QTEST(chain.next(next), "result"); + + DesktopChainManager manager(this); + manager.resize(0, size); + QTEST(manager.next(next), "result"); +} + +void TestDesktopChain::chainAdd_data() +{ + QTest::addColumn("size"); + QTest::addColumn("add"); + QTest::addColumn("next"); + QTest::addColumn("result"); + + // invalid size, should not crash + QTest::newRow("0/1/1/1") << (uint)0 << (uint)1 << (uint)1 << (uint)1; + // moving first element to the front, shouldn't change the chain + QTest::newRow("4/1/1/2") << (uint)4 << (uint)1 << (uint)1 << (uint)2; + QTest::newRow("4/1/2/3") << (uint)4 << (uint)1 << (uint)2 << (uint)3; + QTest::newRow("4/1/3/4") << (uint)4 << (uint)1 << (uint)3 << (uint)4; + QTest::newRow("4/1/4/1") << (uint)4 << (uint)1 << (uint)4 << (uint)1; + // moving an element from middle to front, should reorder + QTest::newRow("4/3/1/1") << (uint)4 << (uint)3 << (uint)1 << (uint)2; + QTest::newRow("4/3/2/4") << (uint)4 << (uint)3 << (uint)2 << (uint)4; + QTest::newRow("4/3/3/1") << (uint)4 << (uint)3 << (uint)3 << (uint)1; + QTest::newRow("4/3/4/3") << (uint)4 << (uint)3 << (uint)4 << (uint)3; + // adding an element which does not exist - should leave the chain untouched + QTest::newRow("4/5/1/2") << (uint)4 << (uint)5 << (uint)1 << (uint)2; + QTest::newRow("4/5/2/3") << (uint)4 << (uint)5 << (uint)2 << (uint)3; + QTest::newRow("4/5/3/4") << (uint)4 << (uint)5 << (uint)3 << (uint)4; + QTest::newRow("4/5/4/1") << (uint)4 << (uint)5 << (uint)4 << (uint)1; +} + +void TestDesktopChain::chainAdd() +{ + QFETCH(uint, size); + QFETCH(uint, add); + QFETCH(uint, next); + DesktopChain chain(size); + chain.add(add); + QTEST(chain.next(next), "result"); + + DesktopChainManager manager(this); + manager.resize(0, size); + manager.addDesktop(0, add); + QTEST(manager.next(next), "result"); +} + +void TestDesktopChain::resize_data() +{ + QTest::addColumn("size"); + QTest::addColumn("add"); + QTest::addColumn("newSize"); + QTest::addColumn("next"); + QTest::addColumn("result"); + + // basic test - increment by one + QTest::newRow("1->2/1") << (uint)1 << (uint)1 << (uint)2 << (uint)1 << (uint)2; + QTest::newRow("1->2/2") << (uint)1 << (uint)1 << (uint)2 << (uint)2 << (uint)1; + // more complex test - increment by three, keep chain untouched + QTest::newRow("3->6/1") << (uint)3 << (uint)1 << (uint)6 << (uint)1 << (uint)2; + QTest::newRow("3->6/2") << (uint)3 << (uint)1 << (uint)6 << (uint)2 << (uint)3; + QTest::newRow("3->6/3") << (uint)3 << (uint)1 << (uint)6 << (uint)3 << (uint)4; + QTest::newRow("3->6/4") << (uint)3 << (uint)1 << (uint)6 << (uint)4 << (uint)5; + QTest::newRow("3->6/5") << (uint)3 << (uint)1 << (uint)6 << (uint)5 << (uint)6; + QTest::newRow("3->6/6") << (uint)3 << (uint)1 << (uint)6 << (uint)6 << (uint)1; + // increment by three, but change it before + QTest::newRow("3->6/3/1") << (uint)3 << (uint)3 << (uint)6 << (uint)1 << (uint)2; + QTest::newRow("3->6/3/2") << (uint)3 << (uint)3 << (uint)6 << (uint)2 << (uint)4; + QTest::newRow("3->6/3/3") << (uint)3 << (uint)3 << (uint)6 << (uint)3 << (uint)1; + QTest::newRow("3->6/3/4") << (uint)3 << (uint)3 << (uint)6 << (uint)4 << (uint)5; + QTest::newRow("3->6/3/5") << (uint)3 << (uint)3 << (uint)6 << (uint)5 << (uint)6; + QTest::newRow("3->6/3/6") << (uint)3 << (uint)3 << (uint)6 << (uint)6 << (uint)3; + + // basic test - decrement by one + QTest::newRow("2->1/1") << (uint)2 << (uint)1 << (uint)1 << (uint)1 << (uint)1; + QTest::newRow("2->1/2") << (uint)2 << (uint)2 << (uint)1 << (uint)1 << (uint)1; + // more complex test - decrement by three, keep chain untouched + QTest::newRow("6->3/1") << (uint)6 << (uint)1 << (uint)3 << (uint)1 << (uint)2; + QTest::newRow("6->3/2") << (uint)6 << (uint)1 << (uint)3 << (uint)2 << (uint)3; + QTest::newRow("6->3/3") << (uint)6 << (uint)1 << (uint)3 << (uint)3 << (uint)1; + // more complex test - decrement by three, move element to front + QTest::newRow("6->3/6/1") << (uint)6 << (uint)6 << (uint)3 << (uint)1 << (uint)2; + QTest::newRow("6->3/6/2") << (uint)6 << (uint)6 << (uint)3 << (uint)2 << (uint)3; + QTest::newRow("6->3/6/3") << (uint)6 << (uint)6 << (uint)3 << (uint)3 << (uint)1; +} + +void TestDesktopChain::resize() +{ + QFETCH(uint, size); + DesktopChain chain(size); + QFETCH(uint, add); + chain.add(add); + QFETCH(uint, newSize); + chain.resize(size, newSize); + QFETCH(uint, next); + QTEST(chain.next(next), "result"); + + DesktopChainManager manager(this); + manager.resize(0, size); + manager.addDesktop(0, add); + manager.resize(size, newSize); + QTEST(manager.next(next), "result"); +} + +void TestDesktopChain::resizeAdd() +{ + // test that verifies that add works after shrinking the chain + DesktopChain chain(6); + DesktopChainManager manager(this); + manager.resize(0, 6); + chain.add(4); + manager.addDesktop(0, 4); + chain.add(5); + manager.addDesktop(4, 5); + chain.add(6); + manager.addDesktop(5, 6); + QCOMPARE(chain.next(6), (uint)5); + QCOMPARE(manager.next(6), (uint)5); + QCOMPARE(chain.next(5), (uint)4); + QCOMPARE(manager.next(5), (uint)4); + QCOMPARE(chain.next(4), (uint)1); + QCOMPARE(manager.next(4), (uint)1); + chain.resize(6, 3); + manager.resize(6, 3); + QCOMPARE(chain.next(3), (uint)3); + QCOMPARE(manager.next(3), (uint)3); + QCOMPARE(chain.next(1), (uint)3); + QCOMPARE(manager.next(1), (uint)3); + QCOMPARE(chain.next(2), (uint)3); + QCOMPARE(manager.next(2), (uint)3); + // add + chain.add(1); + manager.addDesktop(3, 1); + QCOMPARE(chain.next(3), (uint)3); + QCOMPARE(manager.next(3), (uint)3); + QCOMPARE(chain.next(1), (uint)3); + QCOMPARE(manager.next(1), (uint)3); + chain.add(2); + manager.addDesktop(1, 2); + QCOMPARE(chain.next(1), (uint)3); + QCOMPARE(manager.next(1), (uint)3); + QCOMPARE(chain.next(2), (uint)1); + QCOMPARE(manager.next(2), (uint)1); + QCOMPARE(chain.next(3), (uint)2); + QCOMPARE(manager.next(3), (uint)2); +} + +void TestDesktopChain::useChain() +{ + DesktopChainManager manager(this); + manager.resize(0, 4); + manager.addDesktop(0, 3); + // creating the first chain, should keep it unchanged + manager.useChain(QStringLiteral("test")); + QCOMPARE(manager.next(3), (uint)1); + QCOMPARE(manager.next(1), (uint)2); + QCOMPARE(manager.next(2), (uint)4); + QCOMPARE(manager.next(4), (uint)3); + // but creating a second chain, should create an empty one + manager.useChain(QStringLiteral("second chain")); + QCOMPARE(manager.next(1), (uint)2); + QCOMPARE(manager.next(2), (uint)3); + QCOMPARE(manager.next(3), (uint)4); + QCOMPARE(manager.next(4), (uint)1); + // adding a desktop should only affect the currently used one + manager.addDesktop(3, 2); + QCOMPARE(manager.next(1), (uint)3); + QCOMPARE(manager.next(2), (uint)1); + QCOMPARE(manager.next(3), (uint)4); + QCOMPARE(manager.next(4), (uint)2); + // verify by switching back + manager.useChain(QStringLiteral("test")); + QCOMPARE(manager.next(3), (uint)1); + QCOMPARE(manager.next(1), (uint)2); + QCOMPARE(manager.next(2), (uint)4); + QCOMPARE(manager.next(4), (uint)3); + manager.addDesktop(3, 1); + // use second chain again and put 4th desktop to front + manager.useChain(QStringLiteral("second chain")); + manager.addDesktop(3, 4); + // just for the fun a third chain, and let's shrink it + manager.useChain(QStringLiteral("third chain")); + manager.resize(4, 3); + QCOMPARE(manager.next(1), (uint)2); + QCOMPARE(manager.next(2), (uint)3); + // it must have affected all chains + manager.useChain(QStringLiteral("test")); + QCOMPARE(manager.next(1), (uint)3); + QCOMPARE(manager.next(3), (uint)2); + QCOMPARE(manager.next(2), (uint)1); + manager.useChain(QStringLiteral("second chain")); + QCOMPARE(manager.next(3), (uint)2); + QCOMPARE(manager.next(1), (uint)3); + QCOMPARE(manager.next(2), (uint)1); +} + +QTEST_MAIN(TestDesktopChain) +#include "test_desktopchain.moc" diff --git a/autotests/tabbox/test_tabbox_clientmodel.cpp b/autotests/tabbox/test_tabbox_clientmodel.cpp new file mode 100644 index 0000000..14f637c --- /dev/null +++ b/autotests/tabbox/test_tabbox_clientmodel.cpp @@ -0,0 +1,83 @@ +/* + 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 "test_tabbox_clientmodel.h" +#include "mock_tabboxhandler.h" +#include "clientmodel.h" +#include "../testutils.h" + +#include +#include +using namespace KWin; + +void TestTabBoxClientModel::initTestCase() +{ + qApp->setProperty("x11Connection", QVariant::fromValue(QX11Info::connection())); +} + +void TestTabBoxClientModel::testLongestCaptionWithNullClient() +{ + MockTabBoxHandler tabboxhandler; + TabBox::ClientModel *clientModel = new TabBox::ClientModel(&tabboxhandler); + clientModel->createClientList(); + QCOMPARE(clientModel->longestCaption(), QString()); + // add a window to the mock + tabboxhandler.createMockWindow(QString("test")); + clientModel->createClientList(); + QCOMPARE(clientModel->longestCaption(), QString("test")); + // delete the one client in the list + QModelIndex index = clientModel->index(0, 0); + QVERIFY(index.isValid()); + TabBox::TabBoxClient *client = static_cast(clientModel->data(index, TabBox::ClientModel::ClientRole).value()); + client->close(); + // internal model of ClientModel now contains a deleted pointer + // longestCaption should behave just as if the window were not in the list + QCOMPARE(clientModel->longestCaption(), QString()); +} + +void TestTabBoxClientModel::testCreateClientListNoActiveClient() +{ + MockTabBoxHandler tabboxhandler; + tabboxhandler.setConfig(TabBox::TabBoxConfig()); + TabBox::ClientModel *clientModel = new TabBox::ClientModel(&tabboxhandler); + clientModel->createClientList(); + QCOMPARE(clientModel->rowCount(), 0); + // create two windows, rowCount() should go to two + QWeakPointer client = tabboxhandler.createMockWindow(QString("test")); + tabboxhandler.createMockWindow(QString("test2")); + clientModel->createClientList(); + QCOMPARE(clientModel->rowCount(), 2); + // let's ensure there is no active client + tabboxhandler.setActiveClient(QWeakPointer()); + // now it should still have two members in the list + clientModel->createClientList(); + QCOMPARE(clientModel->rowCount(), 2); +} + +void TestTabBoxClientModel::testCreateClientListActiveClientNotInFocusChain() +{ + MockTabBoxHandler tabboxhandler; + tabboxhandler.setConfig(TabBox::TabBoxConfig()); + TabBox::ClientModel *clientModel = new TabBox::ClientModel(&tabboxhandler); + // create two windows, rowCount() should go to two + QWeakPointer client = tabboxhandler.createMockWindow(QString("test")); + client = tabboxhandler.createMockWindow(QString("test2")); + clientModel->createClientList(); + QCOMPARE(clientModel->rowCount(), 2); + + // simulate that the active client is not in the focus chain + // for that we use the closeWindow of the MockTabBoxHandler which + // removes the Client from the Focus Chain but leaves the active window as it is + QSharedPointer clientOwner = client.toStrongRef(); + tabboxhandler.closeWindow(clientOwner.data()); + clientModel->createClientList(); + QCOMPARE(clientModel->rowCount(), 1); +} + +Q_CONSTRUCTOR_FUNCTION(forceXcb) +QTEST_MAIN(TestTabBoxClientModel) diff --git a/autotests/tabbox/test_tabbox_clientmodel.h b/autotests/tabbox/test_tabbox_clientmodel.h new file mode 100644 index 0000000..71678de --- /dev/null +++ b/autotests/tabbox/test_tabbox_clientmodel.h @@ -0,0 +1,42 @@ +/* + 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 +*/ +#ifndef TEST_TABBOX_CLIENT_MODEL_H +#define TEST_TABBOX_CLIENT_MODEL_H +#include + +class TestTabBoxClientModel : public QObject +{ + Q_OBJECT +private slots: + void initTestCase(); + /** + * Tests that calculating the longest caption does not + * crash in case the internal m_clientList contains a weak + * pointer to a deleted TabBoxClient. + * + * See bug #303840 + */ + void testLongestCaptionWithNullClient(); + /** + * Tests the creation of the Client list for the case that + * there is no active Client, but that Clients actually exist. + * + * See BUG: 305449 + */ + void testCreateClientListNoActiveClient(); + /** + * Tests the creation of the Client list for the case that + * the active Client is not in the Focus chain. + * + * See BUG: 306260 + */ + void testCreateClientListActiveClientNotInFocusChain(); +}; + +#endif diff --git a/autotests/tabbox/test_tabbox_config.cpp b/autotests/tabbox/test_tabbox_config.cpp new file mode 100644 index 0000000..f5e197f --- /dev/null +++ b/autotests/tabbox/test_tabbox_config.cpp @@ -0,0 +1,74 @@ +/* + 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 "../../tabbox/tabboxconfig.h" +#include +using namespace KWin; +using namespace KWin::TabBox; + +class TestTabBoxConfig : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testDefaultCtor(); + void testAssignmentOperator(); +}; + +void TestTabBoxConfig::testDefaultCtor() +{ + TabBoxConfig config; + QCOMPARE(config.isShowTabBox(), TabBoxConfig::defaultShowTabBox()); + QCOMPARE(config.isHighlightWindows(), TabBoxConfig::defaultHighlightWindow()); + QCOMPARE(config.tabBoxMode(), TabBoxConfig::ClientTabBox); + QCOMPARE(config.clientDesktopMode(), TabBoxConfig::defaultDesktopMode()); + QCOMPARE(config.clientActivitiesMode(), TabBoxConfig::defaultActivitiesMode()); + QCOMPARE(config.clientApplicationsMode(), TabBoxConfig::defaultApplicationsMode()); + QCOMPARE(config.clientMinimizedMode(), TabBoxConfig::defaultMinimizedMode()); + QCOMPARE(config.showDesktopMode(), TabBoxConfig::defaultShowDesktopMode()); + QCOMPARE(config.clientMultiScreenMode(), TabBoxConfig::defaultMultiScreenMode()); + QCOMPARE(config.clientSwitchingMode(), TabBoxConfig::defaultSwitchingMode()); + QCOMPARE(config.desktopSwitchingMode(), TabBoxConfig::MostRecentlyUsedDesktopSwitching); + QCOMPARE(config.layoutName(), TabBoxConfig::defaultLayoutName()); +} + +void TestTabBoxConfig::testAssignmentOperator() +{ + TabBoxConfig config; + // changing all values of the config object + config.setShowTabBox(!TabBoxConfig::defaultShowTabBox()); + config.setHighlightWindows(!TabBoxConfig::defaultHighlightWindow()); + config.setTabBoxMode(TabBoxConfig::DesktopTabBox); + config.setClientDesktopMode(TabBoxConfig::AllDesktopsClients); + config.setClientActivitiesMode(TabBoxConfig::AllActivitiesClients); + config.setClientApplicationsMode(TabBoxConfig::OneWindowPerApplication); + config.setClientMinimizedMode(TabBoxConfig::ExcludeMinimizedClients); + config.setShowDesktopMode(TabBoxConfig::ShowDesktopClient); + config.setClientMultiScreenMode(TabBoxConfig::ExcludeCurrentScreenClients); + config.setClientSwitchingMode(TabBoxConfig::StackingOrderSwitching); + config.setDesktopSwitchingMode(TabBoxConfig::StaticDesktopSwitching); + config.setLayoutName(QStringLiteral("grid")); + TabBoxConfig config2; + config2 = config; + // verify the config2 values + QCOMPARE(config2.isShowTabBox(), !TabBoxConfig::defaultShowTabBox()); + QCOMPARE(config2.isHighlightWindows(), !TabBoxConfig::defaultHighlightWindow()); + QCOMPARE(config2.tabBoxMode(), TabBoxConfig::DesktopTabBox); + QCOMPARE(config2.clientDesktopMode(), TabBoxConfig::AllDesktopsClients); + QCOMPARE(config2.clientActivitiesMode(), TabBoxConfig::AllActivitiesClients); + QCOMPARE(config2.clientApplicationsMode(), TabBoxConfig::OneWindowPerApplication); + QCOMPARE(config2.clientMinimizedMode(), TabBoxConfig::ExcludeMinimizedClients); + QCOMPARE(config2.showDesktopMode(), TabBoxConfig::ShowDesktopClient); + QCOMPARE(config2.clientMultiScreenMode(), TabBoxConfig::ExcludeCurrentScreenClients); + QCOMPARE(config2.clientSwitchingMode(), TabBoxConfig::StackingOrderSwitching); + QCOMPARE(config2.desktopSwitchingMode(), TabBoxConfig::StaticDesktopSwitching); + QCOMPARE(config2.layoutName(), QStringLiteral("grid")); +} + +QTEST_MAIN(TestTabBoxConfig) + +#include "test_tabbox_config.moc" diff --git a/autotests/tabbox/test_tabbox_handler.cpp b/autotests/tabbox/test_tabbox_handler.cpp new file mode 100644 index 0000000..10284f9 --- /dev/null +++ b/autotests/tabbox/test_tabbox_handler.cpp @@ -0,0 +1,52 @@ +/* + 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 "mock_tabboxhandler.h" +#include "clientmodel.h" +#include "../testutils.h" +#include +#include + +using namespace KWin; + +class TestTabBoxHandler : public QObject +{ + Q_OBJECT +private slots: + void initTestCase(); + /** + * Test to verify that update outline does not crash + * if the ModelIndex for which the outline should be + * shown is not valid. That is accessing the Pointer + * to the Client returns an invalid QVariant. + * BUG: 304620 + */ + void testDontCrashUpdateOutlineNullClient(); +}; + +void TestTabBoxHandler::initTestCase() +{ + qApp->setProperty("x11Connection", QVariant::fromValue(QX11Info::connection())); +} + +void TestTabBoxHandler::testDontCrashUpdateOutlineNullClient() +{ + MockTabBoxHandler tabboxhandler; + TabBox::TabBoxConfig config; + config.setTabBoxMode(TabBox::TabBoxConfig::ClientTabBox); + config.setShowTabBox(false); + config.setHighlightWindows(false); + tabboxhandler.setConfig(config); + // now show the tabbox which will attempt to show the outline + tabboxhandler.show(); +} + +Q_CONSTRUCTOR_FUNCTION(forceXcb) +QTEST_MAIN(TestTabBoxHandler) + +#include "test_tabbox_handler.moc" diff --git a/autotests/test_builtin_effectloader.cpp b/autotests/test_builtin_effectloader.cpp new file mode 100644 index 0000000..219965d --- /dev/null +++ b/autotests/test_builtin_effectloader.cpp @@ -0,0 +1,556 @@ +/* + 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 "../effectloader.h" +#include "../effects/effect_builtins.h" +#include "mock_effectshandler.h" +#include "../scripting/scriptedeffect.h" // for mocking ScriptedEffect::create +#include "testutils.h" +// KDE +#include +#include +// Qt +#include +#include +#include +Q_DECLARE_METATYPE(KWin::CompositingType) +Q_DECLARE_METATYPE(KWin::LoadEffectFlag) +Q_DECLARE_METATYPE(KWin::LoadEffectFlags) +Q_DECLARE_METATYPE(KWin::BuiltInEffect) +Q_DECLARE_METATYPE(KWin::Effect*) + +Q_LOGGING_CATEGORY(KWIN_CORE, "kwin_core") + +namespace KWin +{ + +ScriptedEffect *ScriptedEffect::create(const KPluginMetaData&) +{ + return nullptr; +} + +bool ScriptedEffect::supported() +{ + return true; +} + +} + +class TestBuiltInEffectLoader : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testHasEffect_data(); + void testHasEffect(); + void testKnownEffects(); + void testSupported_data(); + void testSupported(); + void testLoadEffect_data(); + void testLoadEffect(); + void testLoadBuiltInEffect_data(); + void testLoadBuiltInEffect(); + void testLoadAllEffects(); +}; + +void TestBuiltInEffectLoader::initTestCase() +{ + qApp->setProperty("x11Connection", QVariant::fromValue(QX11Info::connection())); +} + +void TestBuiltInEffectLoader::testHasEffect_data() +{ + QTest::addColumn("name"); + QTest::addColumn("expected"); + + QTest::newRow("blur") << QStringLiteral("blur") << true; + QTest::newRow("with kwin4_effect_ prefix") << QStringLiteral("kwin4_effect_blur") << false; + QTest::newRow("case sensitive") << QStringLiteral("BlUR") << true; + QTest::newRow("Colorpicker") << QStringLiteral("colorpicker") << true; + QTest::newRow("Contrast") << QStringLiteral("contrast") << true; + QTest::newRow("CoverSwitch") << QStringLiteral("coverswitch") << true; + QTest::newRow("Cube") << QStringLiteral("cube") << true; + QTest::newRow("CubeSlide") << QStringLiteral("cubeslide") << true; + QTest::newRow("DesktopGrid") << QStringLiteral("desktopgrid") << true; + QTest::newRow("DimInactive") << QStringLiteral("diminactive") << true; + QTest::newRow("FallApart") << QStringLiteral("fallapart") << true; + QTest::newRow("FlipSwitch") << QStringLiteral("flipswitch") << true; + QTest::newRow("Glide") << QStringLiteral("glide") << true; + QTest::newRow("HighlightWindow") << QStringLiteral("highlightwindow") << true; + QTest::newRow("Invert") << QStringLiteral("invert") << true; + QTest::newRow("Kscreen") << QStringLiteral("kscreen") << true; + QTest::newRow("LookingGlass") << QStringLiteral("lookingglass") << true; + QTest::newRow("MagicLamp") << QStringLiteral("magiclamp") << true; + QTest::newRow("Magnifier") << QStringLiteral("magnifier") << true; + QTest::newRow("MouseClick") << QStringLiteral("mouseclick") << true; + QTest::newRow("MouseMark") << QStringLiteral("mousemark") << true; + QTest::newRow("PresentWindows") << QStringLiteral("presentwindows") << true; + QTest::newRow("Resize") << QStringLiteral("resize") << true; + QTest::newRow("ScreenEdge") << QStringLiteral("screenedge") << true; + QTest::newRow("ScreenShot") << QStringLiteral("screenshot") << true; + QTest::newRow("Sheet") << QStringLiteral("sheet") << true; + QTest::newRow("ShowFps") << QStringLiteral("showfps") << true; + QTest::newRow("ShowPaint") << QStringLiteral("showpaint") << true; + QTest::newRow("Slide") << QStringLiteral("slide") << true; + QTest::newRow("SlideBack") << QStringLiteral("slideback") << true; + QTest::newRow("SlidingPopups") << QStringLiteral("slidingpopups") << true; + QTest::newRow("SnapHelper") << QStringLiteral("snaphelper") << true; + QTest::newRow("StartupFeedback") << QStringLiteral("startupfeedback") << true; + QTest::newRow("ThumbnailAside") << QStringLiteral("thumbnailaside") << true; + QTest::newRow("Touchpoints") << QStringLiteral("touchpoints") << true; + QTest::newRow("TrackMouse") << QStringLiteral("trackmouse") << true; + QTest::newRow("WindowGeometry") << QStringLiteral("windowgeometry") << true; + QTest::newRow("WobblyWindows") << QStringLiteral("wobblywindows") << true; + QTest::newRow("Zoom") << QStringLiteral("zoom") << true; + QTest::newRow("Non Existing") << QStringLiteral("InvalidName") << false; + QTest::newRow("Fade - Scripted") << QStringLiteral("fade") << false; + QTest::newRow("Fade - Scripted + kwin4_effect") << QStringLiteral("kwin4_effect_fade") << false; +} + +void TestBuiltInEffectLoader::testHasEffect() +{ + QFETCH(QString, name); + QFETCH(bool, expected); + + KWin::BuiltInEffectLoader loader; + QCOMPARE(loader.hasEffect(name), expected); +} + +void TestBuiltInEffectLoader::testKnownEffects() +{ + QStringList expectedEffects; + expectedEffects << QStringLiteral("blur") + << QStringLiteral("colorpicker") + << QStringLiteral("contrast") + << QStringLiteral("coverswitch") + << QStringLiteral("cube") + << QStringLiteral("cubeslide") + << QStringLiteral("desktopgrid") + << QStringLiteral("diminactive") + << QStringLiteral("fallapart") + << QStringLiteral("flipswitch") + << QStringLiteral("glide") + << QStringLiteral("highlightwindow") + << QStringLiteral("invert") + << QStringLiteral("kscreen") + << QStringLiteral("lookingglass") + << QStringLiteral("magiclamp") + << QStringLiteral("magnifier") + << QStringLiteral("mouseclick") + << QStringLiteral("mousemark") + << QStringLiteral("presentwindows") + << QStringLiteral("resize") + << QStringLiteral("screenedge") + << QStringLiteral("screenshot") + << QStringLiteral("sheet") + << QStringLiteral("showfps") + << QStringLiteral("showpaint") + << QStringLiteral("slide") + << QStringLiteral("slideback") + << QStringLiteral("slidingpopups") + << QStringLiteral("snaphelper") + << QStringLiteral("startupfeedback") + << QStringLiteral("thumbnailaside") + << QStringLiteral("touchpoints") + << QStringLiteral("trackmouse") + << QStringLiteral("windowgeometry") + << QStringLiteral("wobblywindows") + << QStringLiteral("zoom"); + + KWin::BuiltInEffectLoader loader; + QStringList result = loader.listOfKnownEffects(); + QCOMPARE(result.size(), expectedEffects.size()); + std::sort(result.begin(), result.end()); + for (int i = 0; i < expectedEffects.size(); ++i) { + QCOMPARE(result.at(i), expectedEffects.at(i)); + } +} + +void TestBuiltInEffectLoader::testSupported_data() +{ + QTest::addColumn("name"); + QTest::addColumn("expected"); + QTest::addColumn("type"); + QTest::addColumn("animationsSupported"); + + const KWin::CompositingType xc = KWin::XRenderCompositing; + const KWin::CompositingType oc = KWin::OpenGL2Compositing; + + QTest::newRow("blur") << QStringLiteral("blur") << false << xc << true; + // fails for GL as it does proper tests on what's supported and doesn't just check whether it's GL + QTest::newRow("blur-GL") << QStringLiteral("blur") << false << oc << true; + QTest::newRow("Colorpicker") << QStringLiteral("colorpicker") << false << xc << true; + QTest::newRow("Colorpicker-GL") << QStringLiteral("colorpicker") << true << oc << true; + QTest::newRow("Contrast") << QStringLiteral("contrast") << false << xc << true; + // fails for GL as it does proper tests on what's supported and doesn't just check whether it's GL + QTest::newRow("Contrast-GL") << QStringLiteral("contrast") << false << oc << true; + QTest::newRow("CoverSwitch") << QStringLiteral("coverswitch") << false << xc << true; + QTest::newRow("CoverSwitch-GL") << QStringLiteral("coverswitch") << true << oc << true; + QTest::newRow("CoverSwitch-GL-no-anim") << QStringLiteral("coverswitch") << false << oc << false; + QTest::newRow("Cube") << QStringLiteral("cube") << false << xc << true; + QTest::newRow("Cube-GL") << QStringLiteral("cube") << true << oc << true; + QTest::newRow("CubeSlide") << QStringLiteral("cubeslide") << false << xc << true; + QTest::newRow("CubeSlide-GL") << QStringLiteral("cubeslide") << true << oc << true; + QTest::newRow("CubeSlide-GL-no-anim") << QStringLiteral("cubeslide") << false << oc << false; + QTest::newRow("DesktopGrid") << QStringLiteral("desktopgrid") << true << xc << true; + QTest::newRow("DimInactive") << QStringLiteral("diminactive") << true << xc << true; + QTest::newRow("FallApart") << QStringLiteral("fallapart") << false << xc << true; + QTest::newRow("FallApart-GL") << QStringLiteral("fallapart") << true << oc << true; + QTest::newRow("FlipSwitch") << QStringLiteral("flipswitch") << false << xc << true; + QTest::newRow("FlipSwitch-GL") << QStringLiteral("flipswitch") << true << oc << true; + QTest::newRow("FlipSwitch-GL-no-anim") << QStringLiteral("flipswitch") << false << oc << false; + QTest::newRow("Glide") << QStringLiteral("glide") << false << xc << true; + QTest::newRow("Glide-GL") << QStringLiteral("glide") << true << oc << true; + QTest::newRow("Glide-GL-no-anim") << QStringLiteral("glide") << false << oc << false; + QTest::newRow("HighlightWindow") << QStringLiteral("highlightwindow") << true << xc << true; + QTest::newRow("Invert") << QStringLiteral("invert") << false << xc << true; + QTest::newRow("Invert-GL") << QStringLiteral("invert") << true << oc << true; + QTest::newRow("Kscreen") << QStringLiteral("kscreen") << true << xc << true; + QTest::newRow("LookingGlass") << QStringLiteral("lookingglass") << false << xc << true; + QTest::newRow("LookingGlass-GL") << QStringLiteral("lookingglass") << true << oc << true; + QTest::newRow("MagicLamp") << QStringLiteral("magiclamp") << false << xc << true; + QTest::newRow("MagicLamp-GL") << QStringLiteral("magiclamp") << true << oc << true; + QTest::newRow("MagicLamp-GL-no-anim") << QStringLiteral("magiclamp") << false << oc << false; + QTest::newRow("Magnifier") << QStringLiteral("magnifier") << true << xc << true; + QTest::newRow("MouseClick") << QStringLiteral("mouseclick") << true << xc << true; + QTest::newRow("MouseMark") << QStringLiteral("mousemark") << true << xc << true; + QTest::newRow("PresentWindows") << QStringLiteral("presentwindows") << true << xc << true; + QTest::newRow("Resize") << QStringLiteral("resize") << true << xc << true; + QTest::newRow("ScreenEdge") << QStringLiteral("screenedge") << true << xc << true; + QTest::newRow("ScreenShot") << QStringLiteral("screenshot") << true << xc << true; + QTest::newRow("Sheet") << QStringLiteral("sheet") << false << xc << true; + QTest::newRow("Sheet-GL") << QStringLiteral("sheet") << true << oc << true; + QTest::newRow("Sheet-GL-no-anim") << QStringLiteral("sheet") << false << oc << false; + QTest::newRow("ShowFps") << QStringLiteral("showfps") << true << xc << true; + QTest::newRow("ShowPaint") << QStringLiteral("showpaint") << true << xc << true; + QTest::newRow("Slide") << QStringLiteral("slide") << true << xc << true; + QTest::newRow("SlideBack") << QStringLiteral("slideback") << true << xc << true; + QTest::newRow("SlidingPopups") << QStringLiteral("slidingpopups") << true << xc << true; + QTest::newRow("SnapHelper") << QStringLiteral("snaphelper") << true << xc << true; + QTest::newRow("StartupFeedback") << QStringLiteral("startupfeedback") << false << xc << true; + QTest::newRow("StartupFeedback-GL") << QStringLiteral("startupfeedback") << true << oc << true; + QTest::newRow("ThumbnailAside") << QStringLiteral("thumbnailaside") << true << xc << true; + QTest::newRow("TouchPoints") << QStringLiteral("touchpoints") << true << xc << true; + QTest::newRow("TrackMouse") << QStringLiteral("trackmouse") << true << xc << true; + QTest::newRow("WindowGeometry") << QStringLiteral("windowgeometry") << true << xc << true; + QTest::newRow("WobblyWindows") << QStringLiteral("wobblywindows") << false << xc << true; + QTest::newRow("WobblyWindows-GL") << QStringLiteral("wobblywindows") << true << oc << true; + QTest::newRow("WobblyWindows-GL-no-anim") << QStringLiteral("wobblywindows") << false << oc << false; + QTest::newRow("Zoom") << QStringLiteral("zoom") << true << xc << true; + QTest::newRow("Non Existing") << QStringLiteral("InvalidName") << false << xc << true; + QTest::newRow("Fade - Scripted") << QStringLiteral("fade") << false << xc << true; + QTest::newRow("Fade - Scripted + kwin4_effect") << QStringLiteral("kwin4_effect_fade") << false << xc << true; +} + +void TestBuiltInEffectLoader::testSupported() +{ + QFETCH(QString, name); + QFETCH(bool, expected); + QFETCH(KWin::CompositingType, type); + QFETCH(bool, animationsSupported); + + MockEffectsHandler mockHandler(type); + mockHandler.setAnimationsSupported(animationsSupported); + QCOMPARE(mockHandler.animationsSupported(), animationsSupported); + KWin::BuiltInEffectLoader loader; + QCOMPARE(loader.isEffectSupported(name), expected); +} + +void TestBuiltInEffectLoader::testLoadEffect_data() +{ + QTest::addColumn("name"); + QTest::addColumn("expected"); + QTest::addColumn("type"); + + const KWin::CompositingType xc = KWin::XRenderCompositing; + const KWin::CompositingType oc = KWin::OpenGL2Compositing; + + QTest::newRow("blur") << QStringLiteral("blur") << false << xc; + // fails for GL as it does proper tests on what's supported and doesn't just check whether it's GL + QTest::newRow("blur-GL") << QStringLiteral("blur") << false << oc; + QTest::newRow("Colorpicker") << QStringLiteral("colorpicker") << false << xc; + QTest::newRow("Colorpicker-GL") << QStringLiteral("colorpicker") << true << oc; + QTest::newRow("Contrast") << QStringLiteral("contrast") << false << xc; + // fails for GL as it does proper tests on what's supported and doesn't just check whether it's GL + QTest::newRow("Contrast-GL") << QStringLiteral("contrast") << false << oc; + QTest::newRow("CoverSwitch") << QStringLiteral("coverswitch") << false << xc; + // TODO: needs GL mocking +// QTest::newRow("CoverSwitch-GL") << QStringLiteral("coverswitch") << true << oc; + QTest::newRow("Cube") << QStringLiteral("cube") << false << xc; + // TODO: needs GL mocking +// QTest::newRow("Cube-GL") << QStringLiteral("cube") << true << oc; + QTest::newRow("CubeSlide") << QStringLiteral("cubeslide") << false << xc; + QTest::newRow("CubeSlide-GL") << QStringLiteral("cubeslide") << true << oc; + QTest::newRow("DesktopGrid") << QStringLiteral("desktopgrid") << true << xc; + QTest::newRow("DimInactive") << QStringLiteral("diminactive") << true << xc; + QTest::newRow("FallApart") << QStringLiteral("fallapart") << false << xc; + QTest::newRow("FallApart-GL") << QStringLiteral("fallapart") << true << oc; + QTest::newRow("FlipSwitch") << QStringLiteral("flipswitch") << false << xc; + QTest::newRow("FlipSwitch-GL") << QStringLiteral("flipswitch") << true << oc; + QTest::newRow("Glide") << QStringLiteral("glide") << false << xc; + QTest::newRow("Glide-GL") << QStringLiteral("glide") << true << oc; + QTest::newRow("HighlightWindow") << QStringLiteral("highlightwindow") << true << xc; + QTest::newRow("Invert") << QStringLiteral("invert") << false << xc; + QTest::newRow("Invert-GL") << QStringLiteral("invert") << true << oc; + QTest::newRow("Kscreen") << QStringLiteral("kscreen") << true << xc; + QTest::newRow("LookingGlass") << QStringLiteral("lookingglass") << false << xc; + QTest::newRow("LookingGlass-GL") << QStringLiteral("lookingglass") << true << oc; + QTest::newRow("MagicLamp") << QStringLiteral("magiclamp") << false << xc; + QTest::newRow("MagicLamp-GL") << QStringLiteral("magiclamp") << true << oc; + QTest::newRow("Magnifier") << QStringLiteral("magnifier") << true << xc; + QTest::newRow("MouseClick") << QStringLiteral("mouseclick") << true << xc; + QTest::newRow("MouseMark") << QStringLiteral("mousemark") << true << xc; + QTest::newRow("PresentWindows") << QStringLiteral("presentwindows") << true << xc; + QTest::newRow("Resize") << QStringLiteral("resize") << true << xc; + QTest::newRow("ScreenEdge") << QStringLiteral("screenedge") << true << xc; + QTest::newRow("ScreenShot") << QStringLiteral("screenshot") << true << xc; + QTest::newRow("Sheet") << QStringLiteral("sheet") << false << xc; + QTest::newRow("Sheet-GL") << QStringLiteral("sheet") << true << oc; + // TODO: Accesses EffectFrame and crashes +// QTest::newRow("ShowFps") << QStringLiteral("showfps") << true << xc; + QTest::newRow("ShowPaint") << QStringLiteral("showpaint") << true << xc; + QTest::newRow("Slide") << QStringLiteral("slide") << true << xc; + QTest::newRow("SlideBack") << QStringLiteral("slideback") << true << xc; + QTest::newRow("SlidingPopups") << QStringLiteral("slidingpopups") << true << xc; + QTest::newRow("SnapHelper") << QStringLiteral("snaphelper") << true << xc; + QTest::newRow("StartupFeedback") << QStringLiteral("startupfeedback") << false << xc; + // Tries to load shader and makes our test abort +// QTest::newRow("StartupFeedback-GL") << QStringLiteral("startupfeedback") << true << oc; + QTest::newRow("ThumbnailAside") << QStringLiteral("thumbnailaside") << true << xc; + QTest::newRow("Touchpoints") << QStringLiteral("touchpoints") << true << xc; + QTest::newRow("TrackMouse") << QStringLiteral("trackmouse") << true << xc; + // TODO: Accesses EffectFrame and crashes +// QTest::newRow("WindowGeometry") << QStringLiteral("windowgeometry") << true << xc; + QTest::newRow("WobblyWindows") << QStringLiteral("wobblywindows") << false << xc; + QTest::newRow("WobblyWindows-GL") << QStringLiteral("wobblywindows") << true << oc; + QTest::newRow("Zoom") << QStringLiteral("zoom") << true << xc; + QTest::newRow("Non Existing") << QStringLiteral("InvalidName") << false << xc; + QTest::newRow("Fade - Scripted") << QStringLiteral("fade") << false << xc; + QTest::newRow("Fade - Scripted + kwin4_effect") << QStringLiteral("kwin4_effect_fade") << false << xc; +} + +void TestBuiltInEffectLoader::testLoadEffect() +{ + QFETCH(QString, name); + QFETCH(bool, expected); + QFETCH(KWin::CompositingType, type); + + QScopedPointer mockHandler(new MockEffectsHandler(type)); + KWin::BuiltInEffectLoader loader; + KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + loader.setConfig(config); + + qRegisterMetaType(); + QSignalSpy spy(&loader, &KWin::BuiltInEffectLoader::effectLoaded); + // connect to signal to ensure that we delete the Effect again as the Effect doesn't have a parent + connect(&loader, &KWin::BuiltInEffectLoader::effectLoaded, + [&name](KWin::Effect *effect, const QString &effectName) { + QCOMPARE(effectName, name); + effect->deleteLater(); + } + ); + // try to load the Effect + QCOMPARE(loader.loadEffect(name), expected); + // loading again should fail + QVERIFY(!loader.loadEffect(name)); + // signal spy should have got the signal if it was expected + QCOMPARE(spy.isEmpty(), !expected); + if (!spy.isEmpty()) { + QCOMPARE(spy.count(), 1); + // if we caught a signal it should have the effect name we passed in + QList arguments = spy.takeFirst(); + QCOMPARE(arguments.count(), 2); + QCOMPARE(arguments.at(1).toString(), name); + } + spy.clear(); + QVERIFY(spy.isEmpty()); + + // now if we wait for the events being processed, the effect will get deleted and it should load again + QTest::qWait(1); + QCOMPARE(loader.loadEffect(name), expected); + // signal spy should have got the signal if it was expected + QCOMPARE(spy.isEmpty(), !expected); + if (!spy.isEmpty()) { + QCOMPARE(spy.count(), 1); + // if we caught a signal it should have the effect name we passed in + QList arguments = spy.takeFirst(); + QCOMPARE(arguments.count(), 2); + QCOMPARE(arguments.at(1).toString(), name); + } +} + +void TestBuiltInEffectLoader::testLoadBuiltInEffect_data() +{ + // TODO: this test cannot yet test the checkEnabledByDefault functionality as that requires + // mocking enough of GL to get the blur effect to think it's supported and enabled by default + QTest::addColumn("effect"); + QTest::addColumn("name"); + QTest::addColumn("expected"); + QTest::addColumn("type"); + QTest::addColumn("loadFlags"); + + const KWin::CompositingType xc = KWin::XRenderCompositing; + const KWin::CompositingType oc = KWin::OpenGL2Compositing; + + const KWin::LoadEffectFlags checkDefault = KWin::LoadEffectFlag::Load | KWin::LoadEffectFlag::CheckDefaultFunction; + const KWin::LoadEffectFlags forceFlags = KWin::LoadEffectFlag::Load; + const KWin::LoadEffectFlags dontLoadFlags = KWin::LoadEffectFlags(); + + // enabled by default, but not supported + QTest::newRow("blur") << KWin::BuiltInEffect::Blur << QStringLiteral("blur") << false << oc << checkDefault; + // enabled by default + QTest::newRow("HighlightWindow") << KWin::BuiltInEffect::HighlightWindow << QStringLiteral("highlightwindow") << true << xc << checkDefault; + // supported but not enabled by default + QTest::newRow("LookingGlass-GL") << KWin::BuiltInEffect::LookingGlass << QStringLiteral("lookingglass") << true << oc << checkDefault; + // not enabled by default + QTest::newRow("MouseClick") << KWin::BuiltInEffect::MouseClick << QStringLiteral("mouseclick") << true << xc << checkDefault; + // Force an Effect which will load + QTest::newRow("MouseClick-Force") << KWin::BuiltInEffect::MouseClick << QStringLiteral("mouseclick") << true << xc << forceFlags; + // Force an Effect which is not supported + QTest::newRow("LookingGlass-Force") << KWin::BuiltInEffect::LookingGlass << QStringLiteral("lookingglass") << false << xc << forceFlags; + // Force the Effect as supported + QTest::newRow("LookingGlass-Force-GL") << KWin::BuiltInEffect::LookingGlass << QStringLiteral("lookingglass") << true << oc << forceFlags; + // Enforce no load of effect which is enabled by default + QTest::newRow("HighlightWindow-DontLoad") << KWin::BuiltInEffect::HighlightWindow << QStringLiteral("highlightwindow") << false << xc << dontLoadFlags; + // Enforce no load of effect which is not enabled by default, but enforced + QTest::newRow("MouseClick-DontLoad") << KWin::BuiltInEffect::MouseClick << QStringLiteral("mouseclick") << false << xc << dontLoadFlags; +} + +void TestBuiltInEffectLoader::testLoadBuiltInEffect() +{ + QFETCH(KWin::BuiltInEffect, effect); + QFETCH(QString, name); + QFETCH(bool, expected); + QFETCH(KWin::CompositingType, type); + QFETCH(KWin::LoadEffectFlags, loadFlags); + + QScopedPointer mockHandler(new MockEffectsHandler(type)); + KWin::BuiltInEffectLoader loader; + KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + loader.setConfig(config); + + qRegisterMetaType(); + QSignalSpy spy(&loader, &KWin::BuiltInEffectLoader::effectLoaded); + // connect to signal to ensure that we delete the Effect again as the Effect doesn't have a parent + connect(&loader, &KWin::BuiltInEffectLoader::effectLoaded, + [&name](KWin::Effect *effect, const QString &effectName) { + QCOMPARE(effectName, name); + effect->deleteLater(); + } + ); + // try to load the Effect + QCOMPARE(loader.loadEffect(effect, loadFlags), expected); + // loading again should fail + QVERIFY(!loader.loadEffect(effect, loadFlags)); + + // signal spy should have got the signal if it was expected + QCOMPARE(spy.isEmpty(), !expected); + if (!spy.isEmpty()) { + QCOMPARE(spy.count(), 1); + // if we caught a signal it should have the effect name we passed in + QList arguments = spy.takeFirst(); + QCOMPARE(arguments.count(), 2); + QCOMPARE(arguments.at(1).toString(), name); + } + spy.clear(); + QVERIFY(spy.isEmpty()); + + // now if we wait for the events being processed, the effect will get deleted and it should load again + QTest::qWait(1); + QCOMPARE(loader.loadEffect(effect, loadFlags), expected); + // signal spy should have got the signal if it was expected + QCOMPARE(spy.isEmpty(), !expected); + if (!spy.isEmpty()) { + QCOMPARE(spy.count(), 1); + // if we caught a signal it should have the effect name we passed in + QList arguments = spy.takeFirst(); + QCOMPARE(arguments.count(), 2); + QCOMPARE(arguments.at(1).toString(), name); + } +} + +void TestBuiltInEffectLoader::testLoadAllEffects() +{ + QScopedPointermockHandler(new MockEffectsHandler(KWin::XRenderCompositing)); + KWin::BuiltInEffectLoader loader; + + KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + + // prepare the configuration to hard enable/disable the effects we want to load + KConfigGroup plugins = config->group("Plugins"); + plugins.writeEntry(QStringLiteral("desktopgridEnabled"), false); + plugins.writeEntry(QStringLiteral("highlightwindowEnabled"), false); + plugins.writeEntry(QStringLiteral("kscreenEnabled"), false); + plugins.writeEntry(QStringLiteral("presentwindowsEnabled"), false); + plugins.writeEntry(QStringLiteral("screenedgeEnabled"), false); + plugins.writeEntry(QStringLiteral("screenshotEnabled"), false); + plugins.writeEntry(QStringLiteral("slideEnabled"), false); + plugins.writeEntry(QStringLiteral("slidingpopupsEnabled"), false); + plugins.writeEntry(QStringLiteral("startupfeedbackEnabled"), false); + plugins.writeEntry(QStringLiteral("zoomEnabled"), false); + // enable lookingglass as it's not supported + plugins.writeEntry(QStringLiteral("lookingglassEnabled"), true); + plugins.sync(); + + loader.setConfig(config); + + qRegisterMetaType(); + QSignalSpy spy(&loader, &KWin::BuiltInEffectLoader::effectLoaded); + // connect to signal to ensure that we delete the Effect again as the Effect doesn't have a parent + connect(&loader, &KWin::BuiltInEffectLoader::effectLoaded, + [](KWin::Effect *effect) { + effect->deleteLater(); + } + ); + + // the config is prepared so that no Effect gets loaded! + loader.queryAndLoadAll(); + + // we need to wait some time because it's queued + QVERIFY(!spy.wait(10)); + + // now let's prepare a config which has one effect explicitly enabled + plugins.writeEntry(QStringLiteral("mouseclickEnabled"), true); + plugins.sync(); + + loader.queryAndLoadAll(); + // should load one effect in first go + QVERIFY(spy.wait(10)); + // and afterwards it should not load another one + QVERIFY(!spy.wait(10)); + + QCOMPARE(spy.size(), 1); + // if we caught a signal it should have the effect name we passed in + QList arguments = spy.takeFirst(); + QCOMPARE(arguments.count(), 2); + QCOMPARE(arguments.at(1).toString(), QStringLiteral("mouseclick")); + spy.clear(); + + // let's delete one of the default entries + plugins.deleteEntry(QStringLiteral("kscreenEnabled")); + plugins.sync(); + + QVERIFY(spy.isEmpty()); + loader.queryAndLoadAll(); + + // let's use qWait as we need to wait for two signals to be emitted + QTest::qWait(100); + QCOMPARE(spy.size(), 2); + QStringList loadedEffects; + for (auto &list : spy) { + QCOMPARE(list.size(), 2); + loadedEffects << list.at(1).toString(); + } + std::sort(loadedEffects.begin(), loadedEffects.end()); + QCOMPARE(loadedEffects.at(0), QStringLiteral("kscreen")); + QCOMPARE(loadedEffects.at(1), QStringLiteral("mouseclick")); +} + +Q_CONSTRUCTOR_FUNCTION(forceXcb) +QTEST_MAIN(TestBuiltInEffectLoader) +#include "test_builtin_effectloader.moc" diff --git a/autotests/test_client_machine.cpp b/autotests/test_client_machine.cpp new file mode 100644 index 0000000..bf400d5 --- /dev/null +++ b/autotests/test_client_machine.cpp @@ -0,0 +1,146 @@ +/* + 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 "../xcbutils.h" +// Qt +#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 QByteArray &hostname); + QByteArray m_hostName; + QByteArray m_fqdn; +}; + +void TestClientMachine::setClientMachineProperty(xcb_window_t window, const QByteArray &hostname) +{ + xcb_change_property(connection(), XCB_PROP_MODE_REPLACE, window, + XCB_ATOM_WM_CLIENT_MACHINE, XCB_ATOM_STRING, 8, + hostname.length(), hostname.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; + memset(&addressHints, 0, sizeof(addressHints)); + addressHints.ai_family = PF_UNSPEC; + addressHints.ai_socktype = SOCK_STREAM; + addressHints.ai_flags |= AI_CANONNAME; + if (getaddrinfo(m_hostName.constData(), nullptr, &addressHints, &res) == 0) { + if (res->ai_canonname) { + m_fqdn = QByteArray(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") << QByteArray() << QByteArray("localhost") << true; + QTest::newRow("localhost") << QByteArray("localhost") << QByteArray("localhost") << true; + QTest::newRow("hostname") << m_hostName << m_hostName << true; + QTest::newRow("HOSTNAME") << m_hostName.toUpper() << m_hostName.toUpper() << true; + QByteArray cutted(m_hostName); + cutted.remove(0, 1); + QTest::newRow("ostname") << cutted << cutted << false; + QByteArray 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; + cutted = m_fqdn; + cutted.remove(0, 1); + QTest::newRow("qdn") << cutted << cutted << false; +} + +void TestClientMachine::hostName() +{ + const QRect 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(QByteArray, 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 QRect 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/autotests/test_gbm_surface.cpp b/autotests/test_gbm_surface.cpp new file mode 100644 index 0000000..40e8bb5 --- /dev/null +++ b/autotests/test_gbm_surface.cpp @@ -0,0 +1,108 @@ +/* + 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 "../plugins/platforms/drm/gbm_surface.h" +#include + +#include + +// mocking + +struct gbm_device { + bool surfaceShouldFail = false; +}; + +struct gbm_surface { + uint32_t width; + uint32_t height; + uint32_t format; + uint32_t flags; +}; + +struct gbm_bo { +}; + +struct gbm_surface *gbm_surface_create(struct gbm_device *gbm, uint32_t width, uint32_t height, uint32_t format, uint32_t flags) +{ + if (gbm && gbm->surfaceShouldFail) { + return nullptr; + } + auto ret = new gbm_surface{width, height, format, flags}; + return ret; +} + +void gbm_surface_destroy(struct gbm_surface *surface) +{ + delete surface; +} + +struct gbm_bo *gbm_surface_lock_front_buffer(struct gbm_surface *surface) +{ + Q_UNUSED(surface) + return new gbm_bo; +} + +void gbm_surface_release_buffer(struct gbm_surface *surface, struct gbm_bo *bo) +{ + Q_UNUSED(surface) + delete bo; +} + +using KWin::GbmSurface; + +class GbmSurfaceTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testCreate(); + void testCreateFailure(); + void testBo(); +}; + +void GbmSurfaceTest::testCreate() +{ + GbmSurface surface(nullptr, 2, 3, 4, 5); + gbm_surface *native = surface.surface(); + QVERIFY(surface); + QCOMPARE(native->width, 2u); + QCOMPARE(native->height, 3u); + QCOMPARE(native->format, 4u); + QCOMPARE(native->flags, 5u); +} + +void GbmSurfaceTest::testCreateFailure() +{ + gbm_device dev{true}; + GbmSurface surface(&dev, 2, 3, 4, 5); + QVERIFY(!surface); + gbm_surface *native = surface.surface(); + QVERIFY(!native); +} + +void GbmSurfaceTest::testBo() +{ + GbmSurface surface(nullptr, 2, 3, 4, 5); + // release buffer on nullptr should not be a problem + surface.releaseBuffer(nullptr); + // now an actual buffer + auto bo = surface.lockFrontBuffer(); + surface.releaseBuffer(bo); + + // and a surface which fails + gbm_device dev{true}; + GbmSurface surface2(&dev, 2, 3, 4, 5); + QVERIFY(!surface2.lockFrontBuffer()); + auto bo2 = surface.lockFrontBuffer(); + // this won't do anything + surface2.releaseBuffer(bo2); + // so we need to clean up properly + surface.releaseBuffer(bo2); +} + +QTEST_GUILESS_MAIN(GbmSurfaceTest) +#include "test_gbm_surface.moc" diff --git a/autotests/test_gestures.cpp b/autotests/test_gestures.cpp new file mode 100644 index 0000000..a01f494 --- /dev/null +++ b/autotests/test_gestures.cpp @@ -0,0 +1,604 @@ +/* + 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 + +using namespace KWin; + +class GestureTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testSwipeMinFinger_data(); + void testSwipeMinFinger(); + void testSwipeMaxFinger_data(); + void testSwipeMaxFinger(); + void testDirection_data(); + void testDirection(); + void testMinimumX_data(); + void testMinimumX(); + void testMinimumY_data(); + void testMinimumY(); + void testMaximumX_data(); + void testMaximumX(); + void testMaximumY_data(); + void testMaximumY(); + void testStartGeometry(); + void testSetMinimumDelta(); + void testMinimumDeltaReached_data(); + void testMinimumDeltaReached(); + void testUnregisterSwipeCancels(); + void testDeleteSwipeCancels(); + void testSwipeCancel_data(); + void testSwipeCancel(); + void testSwipeUpdateCancel(); + void testSwipeUpdateTrigger_data(); + void testSwipeUpdateTrigger(); + void testSwipeMinFingerStart_data(); + void testSwipeMinFingerStart(); + void testSwipeMaxFingerStart_data(); + void testSwipeMaxFingerStart(); + void testSwipeGeometryStart_data(); + void testSwipeGeometryStart(); + void testSwipeDiagonalCancels_data(); + void testSwipeDiagonalCancels(); +}; + +void GestureTest::testSwipeMinFinger_data() +{ + QTest::addColumn("count"); + QTest::addColumn("expectedCount"); + + QTest::newRow("0") << 0u << 0u; + QTest::newRow("1") << 1u << 1u; + QTest::newRow("10") << 10u << 10u; +} + +void GestureTest::testSwipeMinFinger() +{ + SwipeGesture gesture; + QCOMPARE(gesture.minimumFingerCountIsRelevant(), false); + QCOMPARE(gesture.minimumFingerCount(), 0u); + QFETCH(uint, count); + gesture.setMinimumFingerCount(count); + QCOMPARE(gesture.minimumFingerCountIsRelevant(), true); + QTEST(gesture.minimumFingerCount(), "expectedCount"); + gesture.setMinimumFingerCount(0); + QCOMPARE(gesture.minimumFingerCountIsRelevant(), true); + QCOMPARE(gesture.minimumFingerCount(), 0u); +} + +void GestureTest::testSwipeMaxFinger_data() +{ + QTest::addColumn("count"); + QTest::addColumn("expectedCount"); + + QTest::newRow("0") << 0u << 0u; + QTest::newRow("1") << 1u << 1u; + QTest::newRow("10") << 10u << 10u; +} + +void GestureTest::testSwipeMaxFinger() +{ + SwipeGesture gesture; + QCOMPARE(gesture.maximumFingerCountIsRelevant(), false); + QCOMPARE(gesture.maximumFingerCount(), 0u); + QFETCH(uint, count); + gesture.setMaximumFingerCount(count); + QCOMPARE(gesture.maximumFingerCountIsRelevant(), true); + QTEST(gesture.maximumFingerCount(), "expectedCount"); + gesture.setMaximumFingerCount(0); + QCOMPARE(gesture.maximumFingerCountIsRelevant(), true); + QCOMPARE(gesture.maximumFingerCount(), 0u); +} + +void GestureTest::testDirection_data() +{ + QTest::addColumn("direction"); + + QTest::newRow("Up") << KWin::SwipeGesture::Direction::Up; + QTest::newRow("Left") << KWin::SwipeGesture::Direction::Left; + QTest::newRow("Right") << KWin::SwipeGesture::Direction::Right; + QTest::newRow("Down") << KWin::SwipeGesture::Direction::Down; +} + +void GestureTest::testDirection() +{ + SwipeGesture gesture; + QCOMPARE(gesture.direction(), SwipeGesture::Direction::Down); + QFETCH(KWin::SwipeGesture::Direction, direction); + gesture.setDirection(direction); + QCOMPARE(gesture.direction(), direction); + // back to down + gesture.setDirection(SwipeGesture::Direction::Down); + QCOMPARE(gesture.direction(), SwipeGesture::Direction::Down); +} + +void GestureTest::testMinimumX_data() +{ + QTest::addColumn("min"); + + QTest::newRow("0") << 0; + QTest::newRow("-1") << -1; + QTest::newRow("1") << 1; +} + +void GestureTest::testMinimumX() +{ + SwipeGesture gesture; + QCOMPARE(gesture.minimumX(), 0); + QCOMPARE(gesture.minimumXIsRelevant(), false); + QFETCH(int, min); + gesture.setMinimumX(min); + QCOMPARE(gesture.minimumX(), min); + QCOMPARE(gesture.minimumXIsRelevant(), true); +} + +void GestureTest::testMinimumY_data() +{ + QTest::addColumn("min"); + + QTest::newRow("0") << 0; + QTest::newRow("-1") << -1; + QTest::newRow("1") << 1; +} + +void GestureTest::testMinimumY() +{ + SwipeGesture gesture; + QCOMPARE(gesture.minimumY(), 0); + QCOMPARE(gesture.minimumYIsRelevant(), false); + QFETCH(int, min); + gesture.setMinimumY(min); + QCOMPARE(gesture.minimumY(), min); + QCOMPARE(gesture.minimumYIsRelevant(), true); +} + +void GestureTest::testMaximumX_data() +{ + QTest::addColumn("max"); + + QTest::newRow("0") << 0; + QTest::newRow("-1") << -1; + QTest::newRow("1") << 1; +} + +void GestureTest::testMaximumX() +{ + SwipeGesture gesture; + QCOMPARE(gesture.maximumX(), 0); + QCOMPARE(gesture.maximumXIsRelevant(), false); + QFETCH(int, max); + gesture.setMaximumX(max); + QCOMPARE(gesture.maximumX(), max); + QCOMPARE(gesture.maximumXIsRelevant(), true); +} + +void GestureTest::testMaximumY_data() +{ + QTest::addColumn("max"); + + QTest::newRow("0") << 0; + QTest::newRow("-1") << -1; + QTest::newRow("1") << 1; +} + +void GestureTest::testMaximumY() +{ + SwipeGesture gesture; + QCOMPARE(gesture.maximumY(), 0); + QCOMPARE(gesture.maximumYIsRelevant(), false); + QFETCH(int, max); + gesture.setMaximumY(max); + QCOMPARE(gesture.maximumY(), max); + QCOMPARE(gesture.maximumYIsRelevant(), true); +} + +void GestureTest::testStartGeometry() +{ + SwipeGesture gesture; + gesture.setStartGeometry(QRect(1, 2, 20, 30)); + QCOMPARE(gesture.minimumXIsRelevant(), true); + QCOMPARE(gesture.minimumYIsRelevant(), true); + QCOMPARE(gesture.maximumXIsRelevant(), true); + QCOMPARE(gesture.maximumYIsRelevant(), true); + QCOMPARE(gesture.minimumX(), 1); + QCOMPARE(gesture.minimumY(), 2); + QCOMPARE(gesture.maximumX(), 21); + QCOMPARE(gesture.maximumY(), 32); +} + +void GestureTest::testSetMinimumDelta() +{ + SwipeGesture gesture; + QCOMPARE(gesture.isMinimumDeltaRelevant(), false); + QCOMPARE(gesture.minimumDelta(), QSizeF()); + QCOMPARE(gesture.minimumDeltaReached(QSizeF()), true); + gesture.setMinimumDelta(QSizeF(2, 3)); + QCOMPARE(gesture.isMinimumDeltaRelevant(), true); + QCOMPARE(gesture.minimumDelta(), QSizeF(2, 3)); + QCOMPARE(gesture.minimumDeltaReached(QSizeF()), false); + QCOMPARE(gesture.minimumDeltaReached(QSizeF(2, 3)), true); +} + +void GestureTest::testMinimumDeltaReached_data() +{ + QTest::addColumn("direction"); + QTest::addColumn("minimumDelta"); + QTest::addColumn("delta"); + QTest::addColumn("reached"); + QTest::addColumn("progress"); + + QTest::newRow("Up (more)") << KWin::SwipeGesture::Direction::Up << QSizeF(0, -30) << QSizeF(0, -40) << true << 1.0; + QTest::newRow("Up (exact)") << KWin::SwipeGesture::Direction::Up << QSizeF(0, -30) << QSizeF(0, -30) << true << 1.0; + QTest::newRow("Up (less)") << KWin::SwipeGesture::Direction::Up << QSizeF(0, -30) << QSizeF(0, -29) << false << 29.0/30.0; + QTest::newRow("Left (more)") << KWin::SwipeGesture::Direction::Left << QSizeF(-30, -30) << QSizeF(-40, 20) << true << 1.0; + QTest::newRow("Left (exact)") << KWin::SwipeGesture::Direction::Left << QSizeF(-30, -40) << QSizeF(-30, 0) << true << 1.0; + QTest::newRow("Left (less)") << KWin::SwipeGesture::Direction::Left << QSizeF(-30, -30) << QSizeF(-29, 0) << false << 29.0/30.0; + QTest::newRow("Right (more)") << KWin::SwipeGesture::Direction::Right << QSizeF(30, -30) << QSizeF(40, 20) << true << 1.0; + QTest::newRow("Right (exact)") << KWin::SwipeGesture::Direction::Right << QSizeF(30, -40) << QSizeF(30, 0) << true << 1.0; + QTest::newRow("Right (less)") << KWin::SwipeGesture::Direction::Right << QSizeF(30, -30) << QSizeF(29, 0) << false << 29.0/30.0; + QTest::newRow("Down (more)") << KWin::SwipeGesture::Direction::Down << QSizeF(0, 30) << QSizeF(0, 40) << true << 1.0; + QTest::newRow("Down (exact)") << KWin::SwipeGesture::Direction::Down << QSizeF(0, 30) << QSizeF(0, 30) << true << 1.0; + QTest::newRow("Down (less)") << KWin::SwipeGesture::Direction::Down << QSizeF(0, 30) << QSizeF(0, 29) << false << 29.0/30.0; +} + +void GestureTest::testMinimumDeltaReached() +{ + SwipeGesture gesture; + QFETCH(SwipeGesture::Direction, direction); + gesture.setDirection(direction); + QFETCH(QSizeF, minimumDelta); + gesture.setMinimumDelta(minimumDelta); + QFETCH(QSizeF, delta); + QFETCH(bool, reached); + QCOMPARE(gesture.minimumDeltaReached(delta), reached); + + GestureRecognizer recognizer; + recognizer.registerGesture(&gesture); + + QSignalSpy startedSpy(&gesture, &SwipeGesture::started); + QVERIFY(startedSpy.isValid()); + QSignalSpy triggeredSpy(&gesture, &SwipeGesture::triggered); + QVERIFY(triggeredSpy.isValid()); + QSignalSpy cancelledSpy(&gesture, &SwipeGesture::cancelled); + QVERIFY(cancelledSpy.isValid()); + QSignalSpy progressSpy(&gesture, &SwipeGesture::progress); + QVERIFY(progressSpy.isValid()); + + 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::testUnregisterSwipeCancels() +{ + GestureRecognizer recognizer; + QScopedPointer gesture(new SwipeGesture); + QSignalSpy startedSpy(gesture.data(), &SwipeGesture::started); + QVERIFY(startedSpy.isValid()); + QSignalSpy cancelledSpy(gesture.data(), &SwipeGesture::cancelled); + QVERIFY(cancelledSpy.isValid()); + + recognizer.registerGesture(gesture.data()); + recognizer.startSwipeGesture(1); + QCOMPARE(startedSpy.count(), 1); + QCOMPARE(cancelledSpy.count(), 0); + recognizer.unregisterGesture(gesture.data()); + QCOMPARE(cancelledSpy.count(), 1); + + // delete the gesture should not trigger cancel + gesture.reset(); + QCOMPARE(cancelledSpy.count(), 1); +} + +void GestureTest::testDeleteSwipeCancels() +{ + GestureRecognizer recognizer; + QScopedPointer gesture(new SwipeGesture); + QSignalSpy startedSpy(gesture.data(), &SwipeGesture::started); + QVERIFY(startedSpy.isValid()); + QSignalSpy cancelledSpy(gesture.data(), &SwipeGesture::cancelled); + QVERIFY(cancelledSpy.isValid()); + + recognizer.registerGesture(gesture.data()); + 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") << KWin::SwipeGesture::Direction::Up; + QTest::newRow("Left") << KWin::SwipeGesture::Direction::Left; + QTest::newRow("Right") << KWin::SwipeGesture::Direction::Right; + QTest::newRow("Down") << KWin::SwipeGesture::Direction::Down; +} + +void GestureTest::testSwipeCancel() +{ + GestureRecognizer recognizer; + QScopedPointer gesture(new SwipeGesture); + QFETCH(SwipeGesture::Direction, direction); + gesture->setDirection(direction); + QSignalSpy startedSpy(gesture.data(), &SwipeGesture::started); + QVERIFY(startedSpy.isValid()); + QSignalSpy cancelledSpy(gesture.data(), &SwipeGesture::cancelled); + QVERIFY(cancelledSpy.isValid()); + QSignalSpy triggeredSpy(gesture.data(), &SwipeGesture::triggered); + QVERIFY(triggeredSpy.isValid()); + + recognizer.registerGesture(gesture.data()); + recognizer.startSwipeGesture(1); + QCOMPARE(startedSpy.count(), 1); + QCOMPARE(cancelledSpy.count(), 0); + recognizer.cancelSwipeGesture(); + QCOMPARE(cancelledSpy.count(), 1); + QCOMPARE(triggeredSpy.count(), 0); +} + +void GestureTest::testSwipeUpdateCancel() +{ + GestureRecognizer recognizer; + SwipeGesture upGesture; + upGesture.setDirection(SwipeGesture::Direction::Up); + SwipeGesture downGesture; + downGesture.setDirection(SwipeGesture::Direction::Down); + SwipeGesture rightGesture; + rightGesture.setDirection(SwipeGesture::Direction::Right); + SwipeGesture leftGesture; + leftGesture.setDirection(SwipeGesture::Direction::Left); + + QSignalSpy upCancelledSpy(&upGesture, &SwipeGesture::cancelled); + QVERIFY(upCancelledSpy.isValid()); + QSignalSpy downCancelledSpy(&downGesture, &SwipeGesture::cancelled); + QVERIFY(downCancelledSpy.isValid()); + QSignalSpy rightCancelledSpy(&rightGesture, &SwipeGesture::cancelled); + QVERIFY(rightCancelledSpy.isValid()); + QSignalSpy leftCancelledSpy(&leftGesture, &SwipeGesture::cancelled); + QVERIFY(leftCancelledSpy.isValid()); + + QSignalSpy upTriggeredSpy(&upGesture, &SwipeGesture::triggered); + QVERIFY(upTriggeredSpy.isValid()); + QSignalSpy downTriggeredSpy(&downGesture, &SwipeGesture::triggered); + QVERIFY(downTriggeredSpy.isValid()); + QSignalSpy rightTriggeredSpy(&rightGesture, &SwipeGesture::triggered); + QVERIFY(rightTriggeredSpy.isValid()); + QSignalSpy leftTriggeredSpy(&leftGesture, &SwipeGesture::triggered); + QVERIFY(leftTriggeredSpy.isValid()); + + QSignalSpy upProgressSpy(&upGesture, &SwipeGesture::progress); + QVERIFY(upProgressSpy.isValid()); + QSignalSpy downProgressSpy(&downGesture, &SwipeGesture::progress); + QVERIFY(downProgressSpy.isValid()); + QSignalSpy leftProgressSpy(&leftGesture, &SwipeGesture::progress); + QVERIFY(leftProgressSpy.isValid()); + QSignalSpy rightProgressSpy(&rightGesture, &SwipeGesture::progress); + QVERIFY(rightProgressSpy.isValid()); + + recognizer.registerGesture(&upGesture); + recognizer.registerGesture(&downGesture); + recognizer.registerGesture(&rightGesture); + recognizer.registerGesture(&leftGesture); + + QCOMPARE(recognizer.startSwipeGesture(4), 4); + + // first a down gesture + recognizer.updateSwipeGesture(QSizeF(1, 20)); + QCOMPARE(upCancelledSpy.count(), 1); + QCOMPARE(downCancelledSpy.count(), 0); + QCOMPARE(leftCancelledSpy.count(), 1); + QCOMPARE(rightCancelledSpy.count(), 1); + // another down gesture + recognizer.updateSwipeGesture(QSizeF(-2, 10)); + QCOMPARE(downCancelledSpy.count(), 0); + // and an up gesture + recognizer.updateSwipeGesture(QSizeF(-2, -10)); + QCOMPARE(upCancelledSpy.count(), 1); + QCOMPARE(downCancelledSpy.count(), 1); + QCOMPARE(leftCancelledSpy.count(), 1); + QCOMPARE(rightCancelledSpy.count(), 1); + + recognizer.endSwipeGesture(); + QCOMPARE(upCancelledSpy.count(), 1); + QCOMPARE(downCancelledSpy.count(), 1); + QCOMPARE(leftCancelledSpy.count(), 1); + QCOMPARE(rightCancelledSpy.count(), 1); + QCOMPARE(upTriggeredSpy.count(), 0); + QCOMPARE(downTriggeredSpy.count(), 0); + QCOMPARE(leftTriggeredSpy.count(), 0); + QCOMPARE(rightTriggeredSpy.count(), 0); + + QCOMPARE(upProgressSpy.count(), 0); + QCOMPARE(downProgressSpy.count(), 0); + QCOMPARE(leftProgressSpy.count(), 0); + QCOMPARE(rightProgressSpy.count(), 0); +} + +void GestureTest::testSwipeUpdateTrigger_data() +{ + QTest::addColumn("direction"); + QTest::addColumn("delta"); + + QTest::newRow("Up") << KWin::SwipeGesture::Direction::Up << QSizeF(2, -3); + QTest::newRow("Left") << KWin::SwipeGesture::Direction::Left << QSizeF(-3, 1); + QTest::newRow("Right") << KWin::SwipeGesture::Direction::Right << QSizeF(20, -19); + QTest::newRow("Down") << KWin::SwipeGesture::Direction::Down << QSizeF(0, 50); +} + +void GestureTest::testSwipeUpdateTrigger() +{ + GestureRecognizer recognizer; + SwipeGesture gesture; + QFETCH(SwipeGesture::Direction, direction); + gesture.setDirection(direction); + + QSignalSpy triggeredSpy(&gesture, &SwipeGesture::triggered); + QVERIFY(triggeredSpy.isValid()); + QSignalSpy cancelledSpy(&gesture, &SwipeGesture::cancelled); + QVERIFY(cancelledSpy.isValid()); + + recognizer.registerGesture(&gesture); + + recognizer.startSwipeGesture(1); + QFETCH(QSizeF, delta); + recognizer.updateSwipeGesture(delta); + QCOMPARE(cancelledSpy.count(), 0); + QCOMPARE(triggeredSpy.count(), 0); + + recognizer.endSwipeGesture(); + QCOMPARE(cancelledSpy.count(), 0); + QCOMPARE(triggeredSpy.count(), 1); +} + +void GestureTest::testSwipeMinFingerStart_data() +{ + QTest::addColumn("min"); + QTest::addColumn("count"); + QTest::addColumn("started"); + + QTest::newRow("same") << 1u << 1u << true; + QTest::newRow("less") << 2u << 1u << false; + QTest::newRow("more") << 1u << 2u << true; +} + +void GestureTest::testSwipeMinFingerStart() +{ + GestureRecognizer recognizer; + SwipeGesture gesture; + QFETCH(uint, min); + gesture.setMinimumFingerCount(min); + + QSignalSpy startedSpy(&gesture, &SwipeGesture::started); + QVERIFY(startedSpy.isValid()); + + recognizer.registerGesture(&gesture); + QFETCH(uint, count); + recognizer.startSwipeGesture(count); + QTEST(!startedSpy.isEmpty(), "started"); +} + +void GestureTest::testSwipeMaxFingerStart_data() +{ + QTest::addColumn("max"); + QTest::addColumn("count"); + QTest::addColumn("started"); + + QTest::newRow("same") << 1u << 1u << true; + QTest::newRow("less") << 2u << 1u << true; + QTest::newRow("more") << 1u << 2u << false; +} + +void GestureTest::testSwipeMaxFingerStart() +{ + GestureRecognizer recognizer; + SwipeGesture gesture; + QFETCH(uint, max); + gesture.setMaximumFingerCount(max); + + QSignalSpy startedSpy(&gesture, &SwipeGesture::started); + QVERIFY(startedSpy.isValid()); + + recognizer.registerGesture(&gesture); + QFETCH(uint, count); + recognizer.startSwipeGesture(count); + QTEST(!startedSpy.isEmpty(), "started"); +} + +void GestureTest::testSwipeGeometryStart_data() +{ + QTest::addColumn("geometry"); + QTest::addColumn("startPos"); + QTest::addColumn("started"); + + QTest::newRow("top left") << QRect(0, 0, 10, 20) << QPointF(0, 0) << true; + QTest::newRow("top right") << QRect(0, 0, 10, 20) << QPointF(10, 0) << true; + QTest::newRow("bottom left") << QRect(0, 0, 10, 20) << QPointF(0, 20) << true; + QTest::newRow("bottom right") << QRect(0, 0, 10, 20) << QPointF(10, 20) << true; + QTest::newRow("x too small") << QRect(10, 20, 30, 40) << QPointF(9, 25) << false; + QTest::newRow("y too small") << QRect(10, 20, 30, 40) << QPointF(25, 19) << false; + QTest::newRow("x too large") << QRect(10, 20, 30, 40) << QPointF(41, 25) << false; + QTest::newRow("y too large") << QRect(10, 20, 30, 40) << QPointF(25, 61) << false; + QTest::newRow("inside") << QRect(10, 20, 30, 40) << QPointF(25, 25) << true; +} + +void GestureTest::testSwipeGeometryStart() +{ + GestureRecognizer recognizer; + SwipeGesture gesture; + QFETCH(QRect, geometry); + gesture.setStartGeometry(geometry); + + QSignalSpy startedSpy(&gesture, &SwipeGesture::started); + QVERIFY(startedSpy.isValid()); + + recognizer.registerGesture(&gesture); + QFETCH(QPointF, startPos); + recognizer.startSwipeGesture(startPos); + QTEST(!startedSpy.isEmpty(), "started"); +} + +void GestureTest::testSwipeDiagonalCancels_data() +{ + QTest::addColumn("direction"); + + QTest::newRow("Up") << KWin::SwipeGesture::Direction::Up; + QTest::newRow("Left") << KWin::SwipeGesture::Direction::Left; + QTest::newRow("Right") << KWin::SwipeGesture::Direction::Right; + QTest::newRow("Down") << KWin::SwipeGesture::Direction::Down; +} + +void GestureTest::testSwipeDiagonalCancels() +{ + GestureRecognizer recognizer; + SwipeGesture gesture; + QFETCH(SwipeGesture::Direction, direction); + gesture.setDirection(direction); + + QSignalSpy triggeredSpy(&gesture, &SwipeGesture::triggered); + QVERIFY(triggeredSpy.isValid()); + QSignalSpy cancelledSpy(&gesture, &SwipeGesture::cancelled); + QVERIFY(cancelledSpy.isValid()); + + recognizer.registerGesture(&gesture); + + recognizer.startSwipeGesture(1); + recognizer.updateSwipeGesture(QSizeF(1, 1)); + QCOMPARE(cancelledSpy.count(), 1); + QCOMPARE(triggeredSpy.count(), 0); + + recognizer.endSwipeGesture(); + QCOMPARE(cancelledSpy.count(), 1); + QCOMPARE(triggeredSpy.count(), 0); + +} + +QTEST_MAIN(GestureTest) +#include "test_gestures.moc" diff --git a/autotests/test_plugin_effectloader.cpp b/autotests/test_plugin_effectloader.cpp new file mode 100644 index 0000000..1bdc958 --- /dev/null +++ b/autotests/test_plugin_effectloader.cpp @@ -0,0 +1,413 @@ +/* + 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 "../effectloader.h" +#include "mock_effectshandler.h" +#include "../scripting/scriptedeffect.h" // for mocking ScriptedEffect::create +// KDE +#include +#include +#include +// Qt +#include +#include +Q_DECLARE_METATYPE(KWin::CompositingType) +Q_DECLARE_METATYPE(KWin::LoadEffectFlag) +Q_DECLARE_METATYPE(KWin::LoadEffectFlags) +Q_DECLARE_METATYPE(KWin::Effect*) + +Q_LOGGING_CATEGORY(KWIN_CORE, "kwin_core") + +namespace KWin +{ + +ScriptedEffect *ScriptedEffect::create(const KPluginMetaData&) +{ + return nullptr; +} + +bool ScriptedEffect::supported() +{ + return true; +} + +} + +class TestPluginEffectLoader : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testHasEffect_data(); + void testHasEffect(); + void testKnownEffects(); + void testSupported_data(); + void testSupported(); + void testLoadEffect_data(); + void testLoadEffect(); + void testLoadPluginEffect_data(); + void testLoadPluginEffect(); + void testLoadAllEffects(); + void testCancelLoadAllEffects(); +}; + +void TestPluginEffectLoader::testHasEffect_data() +{ + QTest::addColumn("name"); + QTest::addColumn("expected"); + + // all the built-in effects should fail + QTest::newRow("blur") << QStringLiteral("blur") << false; + QTest::newRow("ColorPicker") << QStringLiteral("colorpicker") << false; + QTest::newRow("Contrast") << QStringLiteral("contrast") << false; + QTest::newRow("CoverSwitch") << QStringLiteral("coverswitch") << false; + QTest::newRow("Cube") << QStringLiteral("cube") << false; + QTest::newRow("CubeSlide") << QStringLiteral("cubeslide") << false; + QTest::newRow("DesktopGrid") << QStringLiteral("desktopgrid") << false; + QTest::newRow("DimInactive") << QStringLiteral("diminactive") << false; + QTest::newRow("FallApart") << QStringLiteral("fallapart") << false; + QTest::newRow("FlipSwitch") << QStringLiteral("flipswitch") << false; + QTest::newRow("Glide") << QStringLiteral("glide") << false; + QTest::newRow("HighlightWindow") << QStringLiteral("highlightwindow") << false; + QTest::newRow("Invert") << QStringLiteral("invert") << false; + QTest::newRow("Kscreen") << QStringLiteral("kscreen") << false; + QTest::newRow("LookingGlass") << QStringLiteral("lookingglass") << false; + QTest::newRow("MagicLamp") << QStringLiteral("magiclamp") << false; + QTest::newRow("Magnifier") << QStringLiteral("magnifier") << false; + QTest::newRow("MouseClick") << QStringLiteral("mouseclick") << false; + QTest::newRow("MouseMark") << QStringLiteral("mousemark") << false; + QTest::newRow("PresentWindows") << QStringLiteral("presentwindows") << false; + QTest::newRow("Resize") << QStringLiteral("resize") << false; + QTest::newRow("ScreenEdge") << QStringLiteral("screenedge") << false; + QTest::newRow("ScreenShot") << QStringLiteral("screenshot") << false; + QTest::newRow("Sheet") << QStringLiteral("sheet") << false; + QTest::newRow("ShowFps") << QStringLiteral("showfps") << false; + QTest::newRow("ShowPaint") << QStringLiteral("showpaint") << false; + QTest::newRow("Slide") << QStringLiteral("slide") << false; + QTest::newRow("SlideBack") << QStringLiteral("slideback") << false; + QTest::newRow("SlidingPopups") << QStringLiteral("slidingpopups") << false; + QTest::newRow("SnapHelper") << QStringLiteral("snaphelper") << false; + QTest::newRow("StartupFeedback") << QStringLiteral("startupfeedback") << false; + QTest::newRow("ThumbnailAside") << QStringLiteral("thumbnailaside") << false; + QTest::newRow("TrackMouse") << QStringLiteral("trackmouse") << false; + QTest::newRow("WindowGeometry") << QStringLiteral("windowgeometry") << false; + QTest::newRow("WobblyWindows") << QStringLiteral("wobblywindows") << false; + QTest::newRow("Zoom") << QStringLiteral("zoom") << false; + QTest::newRow("Non Existing") << QStringLiteral("InvalidName") << false; + // all the scripted effects should fail + QTest::newRow("DialogParent") << QStringLiteral("kwin4_effect_dialogparent") << false; + QTest::newRow("DimScreen") << QStringLiteral("kwin4_effect_dimscreen") << false; + QTest::newRow("EyeOnScreen") << QStringLiteral("kwin4_effect_eyeonscreen") << false; + QTest::newRow("Fade") << QStringLiteral("kwin4_effect_fade") << false; + QTest::newRow("FadeDesktop") << QStringLiteral("kwin4_effect_fadedesktop") << false; + QTest::newRow("FadingPopups") << QStringLiteral("kwin4_effect_fadingpopups") << false; + QTest::newRow("FrozenApp") << QStringLiteral("kwin4_effect_frozenapp") << false; + QTest::newRow("Login") << QStringLiteral("kwin4_effect_login") << false; + QTest::newRow("Logout") << QStringLiteral("kwin4_effect_logout") << false; + QTest::newRow("Maximize") << QStringLiteral("kwin4_effect_maximize") << false; + QTest::newRow("MorphingPopups") << QStringLiteral("kwin4_effect_morphingpopups") << false; + QTest::newRow("Scale") << QStringLiteral("kwin4_effect_scale") << false; + QTest::newRow("Squash") << QStringLiteral("kwin4_effect_squash") << false; + QTest::newRow("Translucency") << QStringLiteral("kwin4_effect_translucency") << false; + QTest::newRow("WindowAperture") << QStringLiteral("kwin4_effect_windowaperture") << false; + // and the fake effects we use here + QTest::newRow("fakeeffectplugin") << QStringLiteral("fakeeffectplugin") << true; + QTest::newRow("fakeeffectplugin CS") << QStringLiteral("fakeEffectPlugin") << true; + QTest::newRow("effectversion") << QStringLiteral("effectversion") << true; +} + +void TestPluginEffectLoader::testHasEffect() +{ + QFETCH(QString, name); + QFETCH(bool, expected); + + KWin::PluginEffectLoader loader; + loader.setPluginSubDirectory(QString()); + QCOMPARE(loader.hasEffect(name), expected); +} + +void TestPluginEffectLoader::testKnownEffects() +{ + QStringList expectedEffects; + expectedEffects << QStringLiteral("fakeeffectplugin") << QStringLiteral("effectversion"); + + KWin::PluginEffectLoader loader; + loader.setPluginSubDirectory(QString()); + QStringList result = loader.listOfKnownEffects(); + // at least as many effects as we expect - system running the test could have more effects + QVERIFY(result.size() >= expectedEffects.size()); + for (const QString &effect : expectedEffects) { + QVERIFY(result.contains(effect)); + } +} + +void TestPluginEffectLoader::testSupported_data() +{ + QTest::addColumn("name"); + QTest::addColumn("expected"); + QTest::addColumn("type"); + + const KWin::CompositingType xc = KWin::XRenderCompositing; + const KWin::CompositingType oc = KWin::OpenGL2Compositing; + + QTest::newRow("invalid") << QStringLiteral("blur") << false << xc; + QTest::newRow("fake - xrender") << QStringLiteral("fakeeffectplugin") << false << xc; + QTest::newRow("fake - opengl") << QStringLiteral("fakeeffectplugin") << true << oc; + QTest::newRow("fake - CS") << QStringLiteral("fakeEffectPlugin") << true << oc; + QTest::newRow("version") << QStringLiteral("effectversion") << false << xc; +} + +void TestPluginEffectLoader::testSupported() +{ + QFETCH(QString, name); + QFETCH(bool, expected); + QFETCH(KWin::CompositingType, type); + + MockEffectsHandler mockHandler(type); + KWin::PluginEffectLoader loader; + loader.setPluginSubDirectory(QString()); + QCOMPARE(loader.isEffectSupported(name), expected); +} + +void TestPluginEffectLoader::testLoadEffect_data() +{ + QTest::addColumn("name"); + QTest::addColumn("expected"); + QTest::addColumn("type"); + + const KWin::CompositingType xc = KWin::XRenderCompositing; + const KWin::CompositingType oc = KWin::OpenGL2Compositing; + + QTest::newRow("invalid") << QStringLiteral("slide") << false << xc; + QTest::newRow("fake - xrender") << QStringLiteral("fakeeffectplugin") << false << xc; + QTest::newRow("fake - opengl") << QStringLiteral("fakeeffectplugin") << true << oc; + QTest::newRow("fake - CS") << QStringLiteral("fakeEffectPlugin") << true << oc; + QTest::newRow("version") << QStringLiteral("effectversion") << false << xc; +} + +void TestPluginEffectLoader::testLoadEffect() +{ + QFETCH(QString, name); + QFETCH(bool, expected); + QFETCH(KWin::CompositingType, type); + + QScopedPointer mockHandler(new MockEffectsHandler(type)); + KWin::PluginEffectLoader loader; + loader.setPluginSubDirectory(QString()); + KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + loader.setConfig(config); + + qRegisterMetaType(); + QSignalSpy spy(&loader, &KWin::PluginEffectLoader::effectLoaded); + // connect to signal to ensure that we delete the Effect again as the Effect doesn't have a parent + connect(&loader, &KWin::PluginEffectLoader::effectLoaded, + [&name](KWin::Effect *effect, const QString &effectName) { + QCOMPARE(effectName, name.toLower()); + effect->deleteLater(); + } + ); + // try to load the Effect + QCOMPARE(loader.loadEffect(name), expected); + // loading again should fail + QVERIFY(!loader.loadEffect(name)); + + // signal spy should have got the signal if it was expected + QCOMPARE(spy.isEmpty(), !expected); + if (!spy.isEmpty()) { + QCOMPARE(spy.count(), 1); + // if we caught a signal it should have the effect name we passed in + QList arguments = spy.takeFirst(); + QCOMPARE(arguments.count(), 2); + QCOMPARE(arguments.at(1).toString(), name.toLower()); + } + spy.clear(); + QVERIFY(spy.isEmpty()); + + // now if we wait for the events being processed, the effect will get deleted and it should load again + QTest::qWait(1); + QCOMPARE(loader.loadEffect(name), expected); + // signal spy should have got the signal if it was expected + QCOMPARE(spy.isEmpty(), !expected); + if (!spy.isEmpty()) { + QCOMPARE(spy.count(), 1); + // if we caught a signal it should have the effect name we passed in + QList arguments = spy.takeFirst(); + QCOMPARE(arguments.count(), 2); + QCOMPARE(arguments.at(1).toString(), name.toLower()); + } + +} + +void TestPluginEffectLoader::testLoadPluginEffect_data() +{ + QTest::addColumn("name"); + QTest::addColumn("expected"); + QTest::addColumn("type"); + QTest::addColumn("loadFlags"); + QTest::addColumn("enabledByDefault"); + + const KWin::CompositingType xc = KWin::XRenderCompositing; + const KWin::CompositingType oc = KWin::OpenGL2Compositing; + + const KWin::LoadEffectFlags checkDefault = KWin::LoadEffectFlag::Load | KWin::LoadEffectFlag::CheckDefaultFunction; + const KWin::LoadEffectFlags forceFlags = KWin::LoadEffectFlag::Load; + const KWin::LoadEffectFlags dontLoadFlags = KWin::LoadEffectFlags(); + + // enabled by default, but not supported + QTest::newRow("fakeeffectplugin") << QStringLiteral("fakeeffectplugin") << false << xc << checkDefault << false; + // enabled by default, check default false + QTest::newRow("supported, check default error") << QStringLiteral("fakeeffectplugin") << false << oc << checkDefault << false; + // enabled by default, check default true + QTest::newRow("supported, check default") << QStringLiteral("fakeeffectplugin") << true << oc << checkDefault << true; + // enabled by default, check default false + QTest::newRow("supported, check default error, forced") << QStringLiteral("fakeeffectplugin") << true << oc << forceFlags << false; + // enabled by default, check default true + QTest::newRow("supported, check default, don't load") << QStringLiteral("fakeeffectplugin") << false << oc << dontLoadFlags << true; + // incorrect version + QTest::newRow("Version") << QStringLiteral("effectversion") << false << xc << forceFlags << true; +} + +void TestPluginEffectLoader::testLoadPluginEffect() +{ + QFETCH(QString, name); + QFETCH(bool, expected); + QFETCH(KWin::CompositingType, type); + QFETCH(KWin::LoadEffectFlags, loadFlags); + QFETCH(bool, enabledByDefault); + + QScopedPointer mockHandler(new MockEffectsHandler(type)); + mockHandler->setProperty("testEnabledByDefault", enabledByDefault); + KWin::PluginEffectLoader loader; + loader.setPluginSubDirectory(QString()); + KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + loader.setConfig(config); + + const auto plugins = KPluginLoader::findPlugins(QString(), + [name] (const KPluginMetaData &data) { + return data.pluginId().compare(name, Qt::CaseInsensitive) == 0 && data.serviceTypes().contains(QStringLiteral("KWin/Effect")); + } + ); + QCOMPARE(plugins.size(), 1); + + qRegisterMetaType(); + QSignalSpy spy(&loader, &KWin::PluginEffectLoader::effectLoaded); + // connect to signal to ensure that we delete the Effect again as the Effect doesn't have a parent + connect(&loader, &KWin::PluginEffectLoader::effectLoaded, + [&name](KWin::Effect *effect, const QString &effectName) { + QCOMPARE(effectName, name); + effect->deleteLater(); + } + ); + // try to load the Effect + QCOMPARE(loader.loadEffect(plugins.first(), loadFlags), expected); + // loading again should fail + QVERIFY(!loader.loadEffect(plugins.first(), loadFlags)); + + // signal spy should have got the signal if it was expected + QCOMPARE(spy.isEmpty(), !expected); + if (!spy.isEmpty()) { + QCOMPARE(spy.count(), 1); + // if we caught a signal it should have the effect name we passed in + QList arguments = spy.takeFirst(); + QCOMPARE(arguments.count(), 2); + QCOMPARE(arguments.at(1).toString(), name); + } + spy.clear(); + QVERIFY(spy.isEmpty()); + + // now if we wait for the events being processed, the effect will get deleted and it should load again + QTest::qWait(1); + QCOMPARE(loader.loadEffect(plugins.first(), loadFlags), expected); + // signal spy should have got the signal if it was expected + QCOMPARE(spy.isEmpty(), !expected); + if (!spy.isEmpty()) { + QCOMPARE(spy.count(), 1); + // if we caught a signal it should have the effect name we passed in + QList arguments = spy.takeFirst(); + QCOMPARE(arguments.count(), 2); + QCOMPARE(arguments.at(1).toString(), name); + } +} + +void TestPluginEffectLoader::testLoadAllEffects() +{ + QScopedPointer mockHandler(new MockEffectsHandler(KWin::OpenGL2Compositing)); + mockHandler->setProperty("testEnabledByDefault", true); + KWin::PluginEffectLoader loader; + loader.setPluginSubDirectory(QString()); + + KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + + // prepare the configuration to hard enable/disable the effects we want to load + KConfigGroup plugins = config->group("Plugins"); + plugins.writeEntry(QStringLiteral("fakeeffectpluginEnabled"), false); + plugins.sync(); + + loader.setConfig(config); + + qRegisterMetaType(); + QSignalSpy spy(&loader, &KWin::PluginEffectLoader::effectLoaded); + // connect to signal to ensure that we delete the Effect again as the Effect doesn't have a parent + connect(&loader, &KWin::PluginEffectLoader::effectLoaded, + [](KWin::Effect *effect) { + effect->deleteLater(); + } + ); + + // the config is prepared so that no Effect gets loaded! + loader.queryAndLoadAll(); + + // we need to wait some time because it's queued and in a thread + QVERIFY(!spy.wait(100)); + + // now let's prepare a config which has one effect explicitly enabled + plugins.writeEntry(QStringLiteral("fakeeffectpluginEnabled"), true); + plugins.sync(); + + loader.queryAndLoadAll(); + // should load one effect in first go + QVERIFY(spy.wait(100)); + // and afterwards it should not load another one + QVERIFY(!spy.wait(10)); + + QCOMPARE(spy.size(), 1); + // if we caught a signal it should have the effect name we passed in + QList arguments = spy.takeFirst(); + QCOMPARE(arguments.count(), 2); + QCOMPARE(arguments.at(1).toString(), QStringLiteral("fakeeffectplugin")); + spy.clear(); +} + +void TestPluginEffectLoader::testCancelLoadAllEffects() +{ + // this test verifies that no test gets loaded when the loader gets cleared + MockEffectsHandler mockHandler(KWin::OpenGL2Compositing); + KWin::PluginEffectLoader loader; + loader.setPluginSubDirectory(QString()); + + // prepare the configuration to hard enable/disable the effects we want to load + KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup plugins = config->group("Plugins"); + plugins.writeEntry(QStringLiteral("fakeeffectpluginEnabled"), true); + plugins.sync(); + + loader.setConfig(config); + + qRegisterMetaType(); + QSignalSpy spy(&loader, &KWin::PluginEffectLoader::effectLoaded); + QVERIFY(spy.isValid()); + + loader.queryAndLoadAll(); + loader.clear(); + + // Should not load any effect + QVERIFY(!spy.wait(100)); + QVERIFY(spy.isEmpty()); +} + +QTEST_MAIN(TestPluginEffectLoader) +#include "test_plugin_effectloader.moc" diff --git a/autotests/test_screen_edges.cpp b/autotests/test_screen_edges.cpp new file mode 100644 index 0000000..ac088e1 --- /dev/null +++ b/autotests/test_screen_edges.cpp @@ -0,0 +1,1086 @@ +/* + 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 +*/ +// kwin +#include "../atoms.h" +#include "../cursor.h" +#include "../input.h" +#include "../gestures.h" +#include "../main.h" +#include "../screenedge.h" +#include "../screens.h" +#include "../utils.h" +#include "../virtualdesktops.h" +#include "../xcbutils.h" +#include "../platform.h" +#include "mock_screens.h" +#include "mock_workspace.h" +#include "mock_x11client.h" +#include "testutils.h" +// Frameworks +#include +// Qt +#include +#include +// xcb +#include +Q_DECLARE_METATYPE(KWin::ElectricBorder) + +Q_LOGGING_CATEGORY(KWIN_CORE, "kwin_core") + +namespace KWin +{ + +Atoms* atoms; +int screen_number = 0; + +InputRedirection *InputRedirection::s_self = nullptr; + +void InputRedirection::registerShortcut(const QKeySequence &shortcut, QAction *action) +{ + Q_UNUSED(shortcut) + Q_UNUSED(action) +} + +void InputRedirection::registerAxisShortcut(Qt::KeyboardModifiers modifiers, PointerAxisDirection axis, QAction *action) +{ + Q_UNUSED(modifiers) + Q_UNUSED(axis) + Q_UNUSED(action) +} + +void InputRedirection::registerTouchpadSwipeShortcut(SwipeDirection, QAction*) +{ +} + +void updateXTime() +{ +} + +class TestObject : public QObject +{ + Q_OBJECT +public Q_SLOTS: + bool callback(ElectricBorder border); +Q_SIGNALS: + void gotCallback(KWin::ElectricBorder); +}; + +bool TestObject::callback(KWin::ElectricBorder border) +{ + emit gotCallback(border); + return true; +} + +} + +class TestScreenEdges : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + void init(); + void cleanup(); + void testInit(); + void testCreatingInitialEdges(); + void testCallback(); + void testCallbackWithCheck(); + void testOverlappingEdges_data(); + void testOverlappingEdges(); + void testPushBack_data(); + void testPushBack(); + void testFullScreenBlocking(); + void testClientEdge(); + void testTouchEdge(); + void testTouchCallback_data(); + void testTouchCallback(); +}; + +void TestScreenEdges::initTestCase() +{ + qApp->setProperty("x11RootWindow", QVariant::fromValue(QX11Info::appRootWindow())); + qApp->setProperty("x11Connection", QVariant::fromValue(QX11Info::connection())); + KWin::atoms = new KWin::Atoms; + qRegisterMetaType(); +} + +void TestScreenEdges::cleanupTestCase() +{ + delete KWin::atoms; +} + +void TestScreenEdges::init() +{ + KWin::Cursors::self()->setMouse(new KWin::Cursor(this)); + + using namespace KWin; + new MockWorkspace(this); + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + Screens::create(); + QSignalSpy sp(screens(), &MockScreens::changed); + QVERIFY(sp.wait()); + + auto vd = VirtualDesktopManager::create(); + vd->setConfig(config); + vd->load(); + auto s = ScreenEdges::create(); + s->setConfig(config); +} + +void TestScreenEdges::cleanup() +{ + using namespace KWin; + delete ScreenEdges::self(); + delete VirtualDesktopManager::self(); + delete Screens::self(); + delete workspace(); +} + +void TestScreenEdges::testInit() +{ + using namespace KWin; + auto s = ScreenEdges::self(); + s->init(); + QCOMPARE(s->isDesktopSwitching(), false); + QCOMPARE(s->isDesktopSwitchingMovingClients(), false); + QCOMPARE(s->timeThreshold(), 150); + QCOMPARE(s->reActivationThreshold(), 350); + QCOMPARE(s->cursorPushBackDistance(), QSize(1, 1)); + QCOMPARE(s->actionTopLeft(), ElectricBorderAction::ElectricActionNone); + QCOMPARE(s->actionTop(), ElectricBorderAction::ElectricActionNone); + QCOMPARE(s->actionTopRight(), ElectricBorderAction::ElectricActionNone); + QCOMPARE(s->actionRight(), ElectricBorderAction::ElectricActionNone); + QCOMPARE(s->actionBottomRight(), ElectricBorderAction::ElectricActionNone); + QCOMPARE(s->actionBottom(), ElectricBorderAction::ElectricActionNone); + QCOMPARE(s->actionBottomLeft(), ElectricBorderAction::ElectricActionNone); + QCOMPARE(s->actionLeft(), ElectricBorderAction::ElectricActionNone); + + QList edges = s->findChildren(QString(), Qt::FindDirectChildrenOnly); + QCOMPARE(edges.size(), 8); + for (auto e : edges) { + QVERIFY(!e->isReserved()); + QVERIFY(e->inherits("KWin::WindowBasedEdge")); + QVERIFY(!e->inherits("KWin::AreaBasedEdge")); + QVERIFY(!e->client()); + QVERIFY(!e->isApproaching()); + } + Edge *te = edges.at(0); + QVERIFY(te->isCorner()); + QVERIFY(!te->isScreenEdge()); + QVERIFY(te->isLeft()); + QVERIFY(te->isTop()); + QVERIFY(!te->isRight()); + QVERIFY(!te->isBottom()); + QCOMPARE(te->border(), ElectricBorder::ElectricTopLeft); + te = edges.at(1); + QVERIFY(te->isCorner()); + QVERIFY(!te->isScreenEdge()); + QVERIFY(te->isLeft()); + QVERIFY(!te->isTop()); + QVERIFY(!te->isRight()); + QVERIFY(te->isBottom()); + QCOMPARE(te->border(), ElectricBorder::ElectricBottomLeft); + te = edges.at(2); + QVERIFY(!te->isCorner()); + QVERIFY(te->isScreenEdge()); + QVERIFY(te->isLeft()); + QVERIFY(!te->isTop()); + QVERIFY(!te->isRight()); + QVERIFY(!te->isBottom()); + QCOMPARE(te->border(), ElectricBorder::ElectricLeft); + te = edges.at(3); + QVERIFY(te->isCorner()); + QVERIFY(!te->isScreenEdge()); + QVERIFY(!te->isLeft()); + QVERIFY(te->isTop()); + QVERIFY(te->isRight()); + QVERIFY(!te->isBottom()); + QCOMPARE(te->border(), ElectricBorder::ElectricTopRight); + te = edges.at(4); + QVERIFY(te->isCorner()); + QVERIFY(!te->isScreenEdge()); + QVERIFY(!te->isLeft()); + QVERIFY(!te->isTop()); + QVERIFY(te->isRight()); + QVERIFY(te->isBottom()); + QCOMPARE(te->border(), ElectricBorder::ElectricBottomRight); + te = edges.at(5); + QVERIFY(!te->isCorner()); + QVERIFY(te->isScreenEdge()); + QVERIFY(!te->isLeft()); + QVERIFY(!te->isTop()); + QVERIFY(te->isRight()); + QVERIFY(!te->isBottom()); + QCOMPARE(te->border(), ElectricBorder::ElectricRight); + te = edges.at(6); + QVERIFY(!te->isCorner()); + QVERIFY(te->isScreenEdge()); + QVERIFY(!te->isLeft()); + QVERIFY(te->isTop()); + QVERIFY(!te->isRight()); + QVERIFY(!te->isBottom()); + QCOMPARE(te->border(), ElectricBorder::ElectricTop); + te = edges.at(7); + QVERIFY(!te->isCorner()); + QVERIFY(te->isScreenEdge()); + QVERIFY(!te->isLeft()); + QVERIFY(!te->isTop()); + QVERIFY(!te->isRight()); + QVERIFY(te->isBottom()); + QCOMPARE(te->border(), ElectricBorder::ElectricBottom); + + // we shouldn't have any x windows, though + QCOMPARE(s->windows().size(), 0); +} + +void TestScreenEdges::testCreatingInitialEdges() +{ + using namespace KWin; + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("Windows").writeEntry("ElectricBorders", 2/*ElectricAlways*/); + config->sync(); + + auto s = ScreenEdges::self(); + s->setConfig(config); + s->init(); + // we don't have multiple desktops, so it's returning false + QCOMPARE(s->isDesktopSwitching(), true); + QCOMPARE(s->isDesktopSwitchingMovingClients(), true); + QCOMPARE(s->actionTopLeft(), ElectricBorderAction::ElectricActionNone); + QCOMPARE(s->actionTop(), ElectricBorderAction::ElectricActionNone); + QCOMPARE(s->actionTopRight(), ElectricBorderAction::ElectricActionNone); + QCOMPARE(s->actionRight(), ElectricBorderAction::ElectricActionNone); + QCOMPARE(s->actionBottomRight(), ElectricBorderAction::ElectricActionNone); + QCOMPARE(s->actionBottom(), ElectricBorderAction::ElectricActionNone); + QCOMPARE(s->actionBottomLeft(), ElectricBorderAction::ElectricActionNone); + QCOMPARE(s->actionLeft(), ElectricBorderAction::ElectricActionNone); + + QEXPECT_FAIL("", "needs fixing", Continue); + QCOMPARE(s->windows().size(), 0); + + // set some reasonable virtual desktops + config->group("Desktops").writeEntry("Number", 4); + config->sync(); + auto vd = VirtualDesktopManager::self(); + vd->setConfig(config); + vd->load(); + vd->updateLayout(); + QCOMPARE(vd->count(), 4u); + QCOMPARE(vd->grid().width(), 2); + QCOMPARE(vd->grid().height(), 2); + + // approach windows for edges not created as screen too small + s->updateLayout(); + auto edgeWindows = s->windows(); + QCOMPARE(edgeWindows.size(), 12); + + auto testWindowGeometry = [&](int index) { + Xcb::WindowGeometry geo(edgeWindows[index]); + return geo.rect(); + }; + QRect sg = screens()->geometry(); + const int co = s->cornerOffset(); + QList expectedGeometries{ + QRect(0, 0, 1, 1), + QRect(0, 0, co, co), + QRect(0, sg.bottom(), 1, 1), + QRect(0, sg.height() - co, co, co), + QRect(0, co, 1, sg.height() - co*2), +// QRect(0, co * 2 + 1, co, sg.height() - co*4), + QRect(sg.right(), 0, 1, 1), + QRect(sg.right() - co + 1, 0, co, co), + QRect(sg.right(), sg.bottom(), 1, 1), + QRect(sg.right() - co + 1, sg.bottom() - co + 1, co, co), + QRect(sg.right(), co, 1, sg.height() - co*2), +// QRect(sg.right() - co + 1, co * 2, co, sg.height() - co*4), + QRect(co, 0, sg.width() - co * 2, 1), +// QRect(co * 2, 0, sg.width() - co * 4, co), + QRect(co, sg.bottom(), sg.width() - co * 2, 1), +// QRect(co * 2, sg.height() - co, sg.width() - co * 4, co) + }; + for (int i = 0; i < 12; ++i) { + QCOMPARE(testWindowGeometry(i), expectedGeometries.at(i)); + } + QList edges = s->findChildren(QString(), Qt::FindDirectChildrenOnly); + QCOMPARE(edges.size(), 8); + for (auto e : edges) { + QVERIFY(e->isReserved()); + QCOMPARE(e->activatesForPointer(), true); + QCOMPARE(e->activatesForTouchGesture(), false); + } + + static_cast(screens())->setGeometries(QList{QRect{0, 0, 1024, 768}}); + QSignalSpy changedSpy(screens(), &Screens::changed); + QVERIFY(changedSpy.isValid()); + QVERIFY(changedSpy.wait()); + + // let's update the layout and verify that we have edges + s->recreateEdges(); + edgeWindows = s->windows(); + QCOMPARE(edgeWindows.size(), 16); + sg = screens()->geometry(); + expectedGeometries = QList{ + QRect(0, 0, 1, 1), + QRect(0, 0, co, co), + QRect(0, sg.bottom(), 1, 1), + QRect(0, sg.height() - co, co, co), + QRect(0, co, 1, sg.height() - co*2), + QRect(0, co * 2 + 1, co, sg.height() - co*4), + QRect(sg.right(), 0, 1, 1), + QRect(sg.right() - co + 1, 0, co, co), + QRect(sg.right(), sg.bottom(), 1, 1), + QRect(sg.right() - co + 1, sg.bottom() - co + 1, co, co), + QRect(sg.right(), co, 1, sg.height() - co*2), + QRect(sg.right() - co + 1, co * 2, co, sg.height() - co*4), + QRect(co, 0, sg.width() - co * 2, 1), + QRect(co * 2, 0, sg.width() - co * 4, co), + QRect(co, sg.bottom(), sg.width() - co * 2, 1), + QRect(co * 2, sg.height() - co, sg.width() - co * 4, co) + }; + for (int i = 0; i < 16; ++i) { + QCOMPARE(testWindowGeometry(i), expectedGeometries.at(i)); + } + + // disable desktop switching again + config->group("Windows").writeEntry("ElectricBorders", 1/*ElectricMoveOnly*/); + s->reconfigure(); + QCOMPARE(s->isDesktopSwitching(), false); + QCOMPARE(s->isDesktopSwitchingMovingClients(), true); + QCOMPARE(s->windows().size(), 0); + edges = s->findChildren(QString(), Qt::FindDirectChildrenOnly); + QCOMPARE(edges.size(), 8); + for (int i = 0; i < 8; ++i) { + auto e = edges.at(i); + QVERIFY(!e->isReserved()); + QCOMPARE(e->activatesForPointer(), false); + QCOMPARE(e->activatesForTouchGesture(), false); + QCOMPARE(e->approachGeometry(), expectedGeometries.at(i*2+1)); + } + + // let's start a move of window. + X11Client client(workspace()); + workspace()->setMoveResizeClient(&client); + for (int i = 0; i < 8; ++i) { + auto e = edges.at(i); + QVERIFY(!e->isReserved()); + QCOMPARE(e->activatesForPointer(), true); + QCOMPARE(e->activatesForTouchGesture(), false); + QCOMPARE(e->approachGeometry(), expectedGeometries.at(i*2+1)); + } + // not for resize + client.setResize(true); + for (int i = 0; i < 8; ++i) { + auto e = edges.at(i); + QVERIFY(!e->isReserved()); + QCOMPARE(e->activatesForPointer(), false); + QCOMPARE(e->activatesForTouchGesture(), false); + QCOMPARE(e->approachGeometry(), expectedGeometries.at(i*2+1)); + } + workspace()->setMoveResizeClient(nullptr); +} + +void TestScreenEdges::testCallback() +{ + using namespace KWin; + MockWorkspace ws; + static_cast(screens())->setGeometries(QList{QRect{0, 0, 1024, 768}, QRect{200, 768, 1024, 768}}); + QSignalSpy changedSpy(screens(), &Screens::changed); + QVERIFY(changedSpy.isValid()); + // first is before it's updated + QVERIFY(changedSpy.wait()); + // second is after it's updated + QVERIFY(changedSpy.wait()); + auto s = ScreenEdges::self(); + s->init(); + TestObject callback; + QSignalSpy spy(&callback, &TestObject::gotCallback); + QVERIFY(spy.isValid()); + s->reserve(ElectricLeft, &callback, "callback"); + s->reserve(ElectricTopLeft, &callback, "callback"); + s->reserve(ElectricTop, &callback, "callback"); + s->reserve(ElectricTopRight, &callback, "callback"); + s->reserve(ElectricRight, &callback, "callback"); + s->reserve(ElectricBottomRight, &callback, "callback"); + s->reserve(ElectricBottom, &callback, "callback"); + s->reserve(ElectricBottomLeft, &callback, "callback"); + + QList edges = s->findChildren(QString(), Qt::FindDirectChildrenOnly); + QCOMPARE(edges.size(), 10); + for (auto e: edges) { + QVERIFY(e->isReserved()); + QCOMPARE(e->activatesForPointer(), true); + QCOMPARE(e->activatesForTouchGesture(), false); + } + auto it = std::find_if(edges.constBegin(), edges.constEnd(), [](Edge *e) { + return e->isScreenEdge() && e->isLeft() && e->approachGeometry().bottom() < 768; + }); + QVERIFY(it != edges.constEnd()); + + xcb_enter_notify_event_t event; + auto setPos = [&event] (const QPoint &pos) { + Cursors::self()->mouse()->setPos(pos); + event.root_x = pos.x(); + event.root_y = pos.y(); + event.event_x = pos.x(); + event.event_y = pos.y(); + }; + event.root = XCB_WINDOW_NONE; + event.child = XCB_WINDOW_NONE; + event.event = (*it)->window(); + event.same_screen_focus = 1; + event.time = QDateTime::currentMSecsSinceEpoch(); + setPos(QPoint(0, 50)); + auto isEntered = [s] (xcb_enter_notify_event_t *event) { + return s->handleEnterNotifiy(event->event, QPoint(event->root_x, event->root_y), QDateTime::fromMSecsSinceEpoch(event->time, Qt::UTC)); + }; + QVERIFY(isEntered(&event)); + // doesn't trigger as the edge was not triggered yet + QVERIFY(spy.isEmpty()); + QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(1, 50)); + + // test doesn't trigger due to too much offset + QTest::qWait(160); + setPos(QPoint(0, 100)); + event.time = QDateTime::currentMSecsSinceEpoch(); + QVERIFY(isEntered(&event)); + QVERIFY(spy.isEmpty()); + QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(1, 100)); + + // doesn't trigger as we are waiting too long already + QTest::qWait(200); + setPos(QPoint(0, 101)); + event.time = QDateTime::currentMSecsSinceEpoch(); + QVERIFY(isEntered(&event)); + QVERIFY(spy.isEmpty()); + QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(1, 101)); + + // doesn't activate as we are waiting too short + QTest::qWait(50); + setPos(QPoint(0, 100)); + event.time = QDateTime::currentMSecsSinceEpoch(); + QVERIFY(isEntered(&event)); + QVERIFY(spy.isEmpty()); + QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(1, 100)); + + // and this one triggers + QTest::qWait(110); + setPos(QPoint(0, 101)); + event.time = QDateTime::currentMSecsSinceEpoch(); + QVERIFY(isEntered(&event)); + QVERIFY(!spy.isEmpty()); + QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(1, 101)); + + // now let's try to trigger again + QTest::qWait(351); + setPos(QPoint(0, 100)); + event.time = QDateTime::currentMSecsSinceEpoch(); + QVERIFY(isEntered(&event)); + QCOMPARE(spy.count(), 1); + QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(1, 100)); + // it's still under the reactivation + QTest::qWait(50); + setPos(QPoint(0, 100)); + event.time = QDateTime::currentMSecsSinceEpoch(); + QVERIFY(isEntered(&event)); + QCOMPARE(spy.count(), 1); + QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(1, 100)); + // now it should trigger again + QTest::qWait(250); + setPos(QPoint(0, 100)); + event.time = QDateTime::currentMSecsSinceEpoch(); + QVERIFY(isEntered(&event)); + QCOMPARE(spy.count(), 2); + QCOMPARE(spy.first().first().value(), ElectricLeft); + QCOMPARE(spy.last().first().value(), ElectricLeft); + QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(1, 100)); + + // let's disable pushback + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("Windows").writeEntry("ElectricBorderPushbackPixels", 0); + config->sync(); + s->setConfig(config); + s->reconfigure(); + // it should trigger directly + QTest::qWait(350); + event.time = QDateTime::currentMSecsSinceEpoch(); + QVERIFY(isEntered(&event)); + QCOMPARE(spy.count(), 3); + QCOMPARE(spy.at(0).first().value(), ElectricLeft); + QCOMPARE(spy.at(1).first().value(), ElectricLeft); + QCOMPARE(spy.at(2).first().value(), ElectricLeft); + QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(0, 100)); + + // now let's unreserve again + s->unreserve(ElectricTopLeft, &callback); + s->unreserve(ElectricTop, &callback); + s->unreserve(ElectricTopRight, &callback); + s->unreserve(ElectricRight, &callback); + s->unreserve(ElectricBottomRight, &callback); + s->unreserve(ElectricBottom, &callback); + s->unreserve(ElectricBottomLeft, &callback); + s->unreserve(ElectricLeft, &callback); + for (auto e: s->findChildren(QString(), Qt::FindDirectChildrenOnly)) { + QVERIFY(!e->isReserved()); + QCOMPARE(e->activatesForPointer(), false); + QCOMPARE(e->activatesForTouchGesture(), false); + } +} + +void TestScreenEdges::testCallbackWithCheck() +{ + using namespace KWin; + auto s = ScreenEdges::self(); + s->init(); + TestObject callback; + QSignalSpy spy(&callback, &TestObject::gotCallback); + QVERIFY(spy.isValid()); + s->reserve(ElectricLeft, &callback, "callback"); + + // check activating a different edge doesn't do anything + s->check(QPoint(50, 0), QDateTime::currentDateTimeUtc(), true); + QVERIFY(spy.isEmpty()); + + // try a direct activate without pushback + Cursors::self()->mouse()->setPos(0, 50); + s->check(QPoint(0, 50), QDateTime::currentDateTimeUtc(), true); + QCOMPARE(spy.count(), 1); + QEXPECT_FAIL("", "Argument says force no pushback, but it gets pushed back. Needs investigation", Continue); + QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(0, 50)); + + // use a different edge, this time with pushback + s->reserve(KWin::ElectricRight, &callback, "callback"); + Cursors::self()->mouse()->setPos(99, 50); + s->check(QPoint(99, 50), QDateTime::currentDateTimeUtc()); + QCOMPARE(spy.count(), 1); + QCOMPARE(spy.last().first().value(), ElectricLeft); + QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(98, 50)); + // and trigger it again + QTest::qWait(160); + Cursors::self()->mouse()->setPos(99, 50); + s->check(QPoint(99, 50), QDateTime::currentDateTimeUtc()); + QCOMPARE(spy.count(), 2); + QCOMPARE(spy.last().first().value(), ElectricRight); + QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(98, 50)); +} + +void TestScreenEdges::testOverlappingEdges_data() +{ + QTest::addColumn("geo1"); + QTest::addColumn("geo2"); + + QTest::newRow("topleft-1x1") << QRect{0, 1, 1024, 768} << QRect{1, 0, 1024, 768}; + QTest::newRow("left-1x1-same") << QRect{0, 1, 1024, 766} << QRect{1, 0, 1024, 768}; + QTest::newRow("left-1x1-exchanged") << QRect{0, 1, 1024, 768} << QRect{1, 0, 1024, 766}; + QTest::newRow("bottomleft-1x1") << QRect{0, 0, 1024, 768} << QRect{1, 0, 1024, 769}; + QTest::newRow("bottomright-1x1") << QRect{0, 0, 1024, 768} << QRect{0, 0, 1023, 769}; + QTest::newRow("right-1x1-same") << QRect{0, 0, 1024, 768} << QRect{0, 1, 1025, 766}; + QTest::newRow("right-1x1-exchanged") << QRect{0, 0, 1024, 768} << QRect{1, 1, 1024, 768}; +} + + +void TestScreenEdges::testOverlappingEdges() +{ + using namespace KWin; + QFETCH(QRect, geo1); + QFETCH(QRect, geo2); + + MockScreens* mockScreens = static_cast(screens()); + QSignalSpy sp(mockScreens, &MockScreens::changed); + mockScreens->setGeometries({geo1, geo2}); + QVERIFY(sp.wait()); + + QCOMPARE(screens()->count(), 2); + auto screenEdges = ScreenEdges::self(); + screenEdges->init(); +} + +void TestScreenEdges::testPushBack_data() +{ + QTest::addColumn("border"); + QTest::addColumn("pushback"); + QTest::addColumn("trigger"); + QTest::addColumn("expected"); + + QTest::newRow("topleft-3") << KWin::ElectricTopLeft << 3 << QPoint(0, 0) << QPoint(3, 3); + QTest::newRow("top-5") << KWin::ElectricTop << 5 << QPoint(50, 0) << QPoint(50, 5); + QTest::newRow("toprigth-2") << KWin::ElectricTopRight << 2 << QPoint(99, 0) << QPoint(97, 2); + QTest::newRow("right-10") << KWin::ElectricRight << 10 << QPoint(99, 50) << QPoint(89, 50); + QTest::newRow("bottomright-5") << KWin::ElectricBottomRight << 5 << QPoint(99, 99) << QPoint(94, 94); + QTest::newRow("bottom-10") << KWin::ElectricBottom << 10 << QPoint(50, 99) << QPoint(50, 89); + QTest::newRow("bottomleft-3") << KWin::ElectricBottomLeft << 3 << QPoint(0, 99) << QPoint(3, 96); + QTest::newRow("left-10") << KWin::ElectricLeft << 10 << QPoint(0, 50) << QPoint(10, 50); + QTest::newRow("invalid") << KWin::ElectricLeft << 10 << QPoint(50, 0) << QPoint(50, 0); +} + +void TestScreenEdges::testPushBack() +{ + using namespace KWin; + QFETCH(int, pushback); + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("Windows").writeEntry("ElectricBorderPushbackPixels", pushback); + config->sync(); + + auto s = ScreenEdges::self(); + s->setConfig(config); + s->init(); + TestObject callback; + QSignalSpy spy(&callback, &TestObject::gotCallback); + QVERIFY(spy.isValid()); + QFETCH(ElectricBorder, border); + s->reserve(border, &callback, "callback"); + + QFETCH(QPoint, trigger); + Cursors::self()->mouse()->setPos(trigger); + xcb_enter_notify_event_t event; + event.root_x = trigger.x(); + event.root_y = trigger.y(); + event.event_x = trigger.x(); + event.event_y = trigger.y(); + event.root = XCB_WINDOW_NONE; + event.child = XCB_WINDOW_NONE; + event.event = s->windows().first(); + event.same_screen_focus = 1; + event.time = QDateTime::currentMSecsSinceEpoch(); + auto isEntered = [s] (xcb_enter_notify_event_t *event) { + return s->handleEnterNotifiy(event->event, QPoint(event->root_x, event->root_y), QDateTime::fromMSecsSinceEpoch(event->time)); + }; + QVERIFY(isEntered(&event)); + QVERIFY(spy.isEmpty()); + QTEST(Cursors::self()->mouse()->pos(), "expected"); + + // do the same without the event, but the check method + Cursors::self()->mouse()->setPos(trigger); + s->check(trigger, QDateTime::currentDateTimeUtc()); + QVERIFY(spy.isEmpty()); + QTEST(Cursors::self()->mouse()->pos(), "expected"); +} + +void TestScreenEdges::testFullScreenBlocking() +{ + using namespace KWin; + MockWorkspace ws; + X11Client client(&ws); + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("Windows").writeEntry("ElectricBorderPushbackPixels", 1); + config->sync(); + + auto s = ScreenEdges::self(); + s->setConfig(config); + s->init(); + TestObject callback; + QSignalSpy spy(&callback, &TestObject::gotCallback); + QVERIFY(spy.isValid()); + s->reserve(KWin::ElectricLeft, &callback, "callback"); + s->reserve(KWin::ElectricBottomRight, &callback, "callback"); + QAction action; + s->reserveTouch(KWin::ElectricRight, &action); + // currently there is no active client yet, so check blocking shouldn't do anything + emit s->checkBlocking(); + for (auto e: s->findChildren()) { + QCOMPARE(e->activatesForTouchGesture(), e->border() == KWin::ElectricRight); + } + + xcb_enter_notify_event_t event; + Cursors::self()->mouse()->setPos(0, 50); + event.root_x = 0; + event.root_y = 50; + event.event_x = 0; + event.event_y = 50; + event.root = XCB_WINDOW_NONE; + event.child = XCB_WINDOW_NONE; + event.event = s->windows().first(); + event.same_screen_focus = 1; + event.time = QDateTime::currentMSecsSinceEpoch(); + auto isEntered = [s] (xcb_enter_notify_event_t *event) { + return s->handleEnterNotifiy(event->event, QPoint(event->root_x, event->root_y), QDateTime::fromMSecsSinceEpoch(event->time)); + }; + QVERIFY(isEntered(&event)); + QVERIFY(spy.isEmpty()); + QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(1, 50)); + + client.setFrameGeometry(screens()->geometry()); + client.setActive(true); + client.setFullScreen(true); + ws.setActiveClient(&client); + emit s->checkBlocking(); + // the signal doesn't trigger for corners, let's go over all windows just to be sure that it doesn't call for corners + for (auto e: s->findChildren()) { + e->checkBlocking(); + QCOMPARE(e->activatesForTouchGesture(), false); + } + // calling again should not trigger + QTest::qWait(160); + Cursors::self()->mouse()->setPos(0, 50); + event.time = QDateTime::currentMSecsSinceEpoch(); + QVERIFY(isEntered(&event)); + QVERIFY(spy.isEmpty()); + // and no pushback + QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(0, 50)); + + // let's make the client not fullscreen, which should trigger + client.setFullScreen(false); + emit s->checkBlocking(); + for (auto e: s->findChildren()) { + QCOMPARE(e->activatesForTouchGesture(), e->border() == KWin::ElectricRight); + } + event.time = QDateTime::currentMSecsSinceEpoch(); + QVERIFY(isEntered(&event)); + QVERIFY(!spy.isEmpty()); + QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(1, 50)); + + // let's make the client fullscreen again, but with a geometry not intersecting the left edge + QTest::qWait(351); + client.setFullScreen(true); + client.setFrameGeometry(client.frameGeometry().translated(10, 0)); + emit s->checkBlocking(); + spy.clear(); + Cursors::self()->mouse()->setPos(0, 50); + event.time = QDateTime::currentMSecsSinceEpoch(); + QVERIFY(isEntered(&event)); + QVERIFY(spy.isEmpty()); + // and a pushback + QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(1, 50)); + + // just to be sure, let's set geometry back + client.setFrameGeometry(screens()->geometry()); + emit s->checkBlocking(); + Cursors::self()->mouse()->setPos(0, 50); + QVERIFY(isEntered(&event)); + QVERIFY(spy.isEmpty()); + // and no pushback + QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(0, 50)); + + // the corner should always trigger + s->unreserve(KWin::ElectricLeft, &callback); + event.event_x = 99; + event.event_y = 99; + event.root_x = 99; + event.root_y = 99; + event.event = s->windows().first(); + event.time = QDateTime::currentMSecsSinceEpoch(); + Cursors::self()->mouse()->setPos(99, 99); + QVERIFY(isEntered(&event)); + QVERIFY(spy.isEmpty()); + // and pushback + QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(98, 98)); + QTest::qWait(160); + event.time = QDateTime::currentMSecsSinceEpoch(); + Cursors::self()->mouse()->setPos(99, 99); + QVERIFY(isEntered(&event)); + QVERIFY(!spy.isEmpty()); +} + +void TestScreenEdges::testClientEdge() +{ + using namespace KWin; + X11Client client(workspace()); + client.setFrameGeometry(QRect(10, 50, 10, 50)); + auto s = ScreenEdges::self(); + s->init(); + + s->reserve(&client, KWin::ElectricBottom); + + QPointer edge = s->findChildren().last(); + + QCOMPARE(edge->isReserved(), true); + QCOMPARE(edge->activatesForPointer(), true); + QCOMPARE(edge->activatesForTouchGesture(), true); + + //remove old reserves and resize to be in the middle of the screen + s->reserve(&client, KWin::ElectricNone); + client.setFrameGeometry(QRect(2, 2, 20, 20)); + + // for none of the edges it should be able to be set + for (int i = 0; i < ELECTRIC_COUNT; ++i) { + client.setHiddenInternal(true); + s->reserve(&client, static_cast(i)); + QCOMPARE(client.isHiddenInternal(), false); + } + + // now let's try to set it and activate it + client.setFrameGeometry(screens()->geometry()); + client.setHiddenInternal(true); + s->reserve(&client, KWin::ElectricLeft); + QCOMPARE(client.isHiddenInternal(), true); + + xcb_enter_notify_event_t event; + Cursors::self()->mouse()->setPos(0, 50); + event.root_x = 0; + event.root_y = 50; + event.event_x = 0; + event.event_y = 50; + event.root = XCB_WINDOW_NONE; + event.child = XCB_WINDOW_NONE; + event.event = s->windows().first(); + event.same_screen_focus = 1; + event.time = QDateTime::currentMSecsSinceEpoch(); + auto isEntered = [s] (xcb_enter_notify_event_t *event) { + return s->handleEnterNotifiy(event->event, QPoint(event->root_x, event->root_y), QDateTime::fromMSecsSinceEpoch(event->time)); + }; + QVERIFY(isEntered(&event)); + // autohiding panels shall activate instantly + QCOMPARE(client.isHiddenInternal(), false); + QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(1, 50)); + + // now let's reserve the client for each of the edges, in the end for the right one + client.setHiddenInternal(true); + s->reserve(&client, KWin::ElectricTop); + s->reserve(&client, KWin::ElectricBottom); + QCOMPARE(client.isHiddenInternal(), true); + // corners shouldn't get reserved + s->reserve(&client, KWin::ElectricTopLeft); + QCOMPARE(client.isHiddenInternal(), false); + client.setHiddenInternal(true); + s->reserve(&client, KWin::ElectricTopRight); + QCOMPARE(client.isHiddenInternal(), false); + client.setHiddenInternal(true); + s->reserve(&client, KWin::ElectricBottomRight); + QCOMPARE(client.isHiddenInternal(), false); + client.setHiddenInternal(true); + s->reserve(&client, KWin::ElectricBottomLeft); + QCOMPARE(client.isHiddenInternal(), false); + // now finally reserve on right one + client.setHiddenInternal(true); + s->reserve(&client, KWin::ElectricRight); + QCOMPARE(client.isHiddenInternal(), true); + + // now let's emulate the removal of a Client through Workspace + emit workspace()->clientRemoved(&client); + for (auto e : s->findChildren()) { + QVERIFY(!e->client()); + } + QCOMPARE(client.isHiddenInternal(), true); + + // now let's try to trigger the client showing with the check method instead of enter notify + s->reserve(&client, KWin::ElectricTop); + QCOMPARE(client.isHiddenInternal(), true); + Cursors::self()->mouse()->setPos(50, 0); + s->check(QPoint(50, 0), QDateTime::currentDateTimeUtc()); + QCOMPARE(client.isHiddenInternal(), false); + QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(50, 1)); + + // unreserve by setting to none edge + s->reserve(&client, KWin::ElectricNone); + // check on previous edge again, should fail + client.setHiddenInternal(true); + Cursors::self()->mouse()->setPos(50, 0); + s->check(QPoint(50, 0), QDateTime::currentDateTimeUtc()); + QCOMPARE(client.isHiddenInternal(), true); + QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(50, 0)); + + // set to windows can cover + client.setFrameGeometry(screens()->geometry()); + client.setHiddenInternal(false); + client.setKeepBelow(true); + s->reserve(&client, KWin::ElectricLeft); + QCOMPARE(client.keepBelow(), true); + QCOMPARE(client.isHiddenInternal(), false); + + xcb_enter_notify_event_t event2; + Cursors::self()->mouse()->setPos(0, 50); + event2.root_x = 0; + event2.root_y = 50; + event2.event_x = 0; + event2.event_y = 50; + event2.root = XCB_WINDOW_NONE; + event2.child = XCB_WINDOW_NONE; + event2.event = s->windows().first(); + event2.same_screen_focus = 1; + event2.time = QDateTime::currentMSecsSinceEpoch(); + QVERIFY(isEntered(&event2)); + QCOMPARE(client.keepBelow(), false); + QCOMPARE(client.isHiddenInternal(), false); + QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(1, 50)); +} + +void TestScreenEdges::testTouchEdge() +{ + qRegisterMetaType("ElectricBorder"); + using namespace KWin; + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + auto group = config->group("TouchEdges"); + group.writeEntry("Top", "krunner"); + group.writeEntry("Left", "krunner"); + group.writeEntry("Bottom", "krunner"); + group.writeEntry("Right", "krunner"); + config->sync(); + + auto s = ScreenEdges::self(); + s->setConfig(config); + s->init(); + // we don't have multiple desktops, so it's returning false + QCOMPARE(s->isDesktopSwitching(), false); + QCOMPARE(s->isDesktopSwitchingMovingClients(), false); + QCOMPARE(s->actionTopLeft(), ElectricBorderAction::ElectricActionNone); + QCOMPARE(s->actionTop(), ElectricBorderAction::ElectricActionNone); + QCOMPARE(s->actionTopRight(), ElectricBorderAction::ElectricActionNone); + QCOMPARE(s->actionRight(), ElectricBorderAction::ElectricActionNone); + QCOMPARE(s->actionBottomRight(), ElectricBorderAction::ElectricActionNone); + QCOMPARE(s->actionBottom(), ElectricBorderAction::ElectricActionNone); + QCOMPARE(s->actionBottomLeft(), ElectricBorderAction::ElectricActionNone); + QCOMPARE(s->actionLeft(), ElectricBorderAction::ElectricActionNone); + + QList edges = s->findChildren(QString(), Qt::FindDirectChildrenOnly); + QCOMPARE(edges.size(), 8); + for (auto e : edges) { + QCOMPARE(e->isReserved(), e->isScreenEdge()); + QCOMPARE(e->activatesForPointer(), false); + QCOMPARE(e->activatesForTouchGesture(), e->isScreenEdge()); + } + + // try to activate the edge through pointer, should not be possible + auto it = std::find_if(edges.constBegin(), edges.constEnd(), [](Edge *e) { + return e->isScreenEdge() && e->isLeft(); + }); + QVERIFY(it != edges.constEnd()); + + QSignalSpy approachingSpy(s, &ScreenEdges::approaching); + QVERIFY(approachingSpy.isValid()); + + xcb_enter_notify_event_t event; + auto setPos = [&event] (const QPoint &pos) { + Cursors::self()->mouse()->setPos(pos); + event.root_x = pos.x(); + event.root_y = pos.y(); + event.event_x = pos.x(); + event.event_y = pos.y(); + }; + event.root = XCB_WINDOW_NONE; + event.child = XCB_WINDOW_NONE; + event.event = (*it)->window(); + event.same_screen_focus = 1; + event.time = QDateTime::currentMSecsSinceEpoch(); + setPos(QPoint(0, 50)); + auto isEntered = [s] (xcb_enter_notify_event_t *event) { + return s->handleEnterNotifiy(event->event, QPoint(event->root_x, event->root_y), QDateTime::fromMSecsSinceEpoch(event->time, Qt::UTC)); + }; + QCOMPARE(isEntered(&event), false); + QVERIFY(approachingSpy.isEmpty()); + // let's also verify the check + s->check(QPoint(0, 50), QDateTime::currentDateTimeUtc(), false); + QVERIFY(approachingSpy.isEmpty()); + + s->gestureRecognizer()->startSwipeGesture(QPoint(0, 50)); + QCOMPARE(approachingSpy.count(), 1); + s->gestureRecognizer()->cancelSwipeGesture(); + QCOMPARE(approachingSpy.count(), 2); + + // let's reconfigure + group.writeEntry("Top", "none"); + group.writeEntry("Left", "none"); + group.writeEntry("Bottom", "none"); + group.writeEntry("Right", "none"); + config->sync(); + s->reconfigure(); + + edges = s->findChildren(QString(), Qt::FindDirectChildrenOnly); + QCOMPARE(edges.size(), 8); + for (auto e : edges) { + QCOMPARE(e->isReserved(), false); + QCOMPARE(e->activatesForPointer(), false); + QCOMPARE(e->activatesForTouchGesture(), false); + } + +} + +void TestScreenEdges::testTouchCallback_data() +{ + QTest::addColumn("border"); + QTest::addColumn("startPos"); + QTest::addColumn("delta"); + + QTest::newRow("left") << KWin::ElectricLeft << QPoint(0, 50) << QSizeF(250, 20); + QTest::newRow("top") << KWin::ElectricTop << QPoint(50, 0) << QSizeF(20, 250); + QTest::newRow("right") << KWin::ElectricRight << QPoint(99, 50) << QSizeF(-200, 0); + QTest::newRow("bottom") << KWin::ElectricBottom << QPoint(50, 99) << QSizeF(0, -200); +} + +void TestScreenEdges::testTouchCallback() +{ + qRegisterMetaType("ElectricBorder"); + using namespace KWin; + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + auto group = config->group("TouchEdges"); + group.writeEntry("Top", "none"); + group.writeEntry("Left", "none"); + group.writeEntry("Bottom", "none"); + group.writeEntry("Right", "none"); + config->sync(); + + auto s = ScreenEdges::self(); + s->setConfig(config); + s->init(); + + // none of our actions should be reserved + const QList edges = s->findChildren(QString(), Qt::FindDirectChildrenOnly); + QCOMPARE(edges.size(), 8); + for (auto e : edges) { + QCOMPARE(e->isReserved(), false); + QCOMPARE(e->activatesForPointer(), false); + QCOMPARE(e->activatesForTouchGesture(), false); + } + + // let's reserve an action + QAction action; + QSignalSpy actionTriggeredSpy(&action, &QAction::triggered); + QVERIFY(actionTriggeredSpy.isValid()); + QSignalSpy approachingSpy(s, &ScreenEdges::approaching); + QVERIFY(approachingSpy.isValid()); + + // reserve on edge + QFETCH(KWin::ElectricBorder, border); + s->reserveTouch(border, &action); + for (auto e : edges) { + QCOMPARE(e->isReserved(), e->border() == border); + QCOMPARE(e->activatesForPointer(), false); + QCOMPARE(e->activatesForTouchGesture(), e->border() == border); + } + + QVERIFY(approachingSpy.isEmpty()); + QFETCH(QPoint, startPos); + QCOMPARE(s->gestureRecognizer()->startSwipeGesture(startPos), 1); + QVERIFY(actionTriggeredSpy.isEmpty()); + QCOMPARE(approachingSpy.count(), 1); + QFETCH(QSizeF, delta); + s->gestureRecognizer()->updateSwipeGesture(delta); + QCOMPARE(approachingSpy.count(), 2); + QVERIFY(actionTriggeredSpy.isEmpty()); + s->gestureRecognizer()->endSwipeGesture(); + QVERIFY(actionTriggeredSpy.wait()); + QCOMPARE(actionTriggeredSpy.count(), 1); + QCOMPARE(approachingSpy.count(), 3); + + // unreserve again + s->unreserveTouch(border, &action); + for (auto e : edges) { + QCOMPARE(e->isReserved(), false); + QCOMPARE(e->activatesForPointer(), false); + QCOMPARE(e->activatesForTouchGesture(), false); + } + + // reserve another action + QScopedPointer action2(new QAction); + s->reserveTouch(border, action2.data()); + for (auto e : edges) { + QCOMPARE(e->isReserved(), e->border() == border); + QCOMPARE(e->activatesForPointer(), false); + QCOMPARE(e->activatesForTouchGesture(), e->border() == border); + } + // and unreserve by destroying + action2.reset(); + for (auto e : edges) { + QCOMPARE(e->isReserved(), false); + QCOMPARE(e->activatesForPointer(), false); + QCOMPARE(e->activatesForTouchGesture(), false); + } +} + +Q_CONSTRUCTOR_FUNCTION(forceXcb) +QTEST_MAIN(TestScreenEdges) +#include "test_screen_edges.moc" diff --git a/autotests/test_screen_paint_data.cpp b/autotests/test_screen_paint_data.cpp new file mode 100644 index 0000000..0179501 --- /dev/null +++ b/autotests/test_screen_paint_data.cpp @@ -0,0 +1,276 @@ +/* + 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 + +#include +#include +#include + +#include + +using namespace KWin; + +class TestScreenPaintData : public QObject +{ + Q_OBJECT +private Q_SLOTS: + + void testCtor(); + void testCopyCtor(); + void testAssignmentOperator(); + void testSetScale(); + void testOperatorMultiplyAssign(); + void testSetTranslate(); + void testOperatorPlus(); + void testSetAngle(); + void testSetRotationOrigin(); + void testSetRotationAxis(); +}; + +void TestScreenPaintData::testCtor() +{ + ScreenPaintData 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.outputGeometry(), QRect()); +} + +void TestScreenPaintData::testCopyCtor() +{ + ScreenPaintData data(QMatrix4x4(), QRect(10, 20, 30, 40)); + ScreenPaintData 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.outputGeometry(), QRect(10, 20, 30, 40)); + + 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)); + + ScreenPaintData 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)); +} + +void TestScreenPaintData::testAssignmentOperator() +{ + ScreenPaintData data; + ScreenPaintData data2(QMatrix4x4(), QRect(10, 20, 30, 40)); + + 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)); + QCOMPARE(data2.outputGeometry(), QRect(10, 20, 30, 40)); + + data = data2; + // data and data2 should be the same + QCOMPARE(data.xScale(), 0.5); + QCOMPARE(data.yScale(), 2.0); + QCOMPARE(data.zScale(), 3.0); + QCOMPARE(data.xTranslation(), 0.5); + QCOMPARE(data.yTranslation(), 2.0); + QCOMPARE(data.zTranslation(), 3.0); + QCOMPARE(data.translation(), QVector3D(0.5, 2.0, 3.0)); + QCOMPARE(data.rotationAngle(), 45.0); + QCOMPARE(data.rotationOrigin(), QVector3D(1.0, 2.0, 3.0)); + QCOMPARE(data.rotationAxis(), QVector3D(1.0, 1.0, 0.0)); + QCOMPARE(data.outputGeometry(), QRect(10, 20, 30, 40)); + // data 2 + QCOMPARE(data2.xScale(), 0.5); + QCOMPARE(data2.yScale(), 2.0); + QCOMPARE(data2.zScale(), 3.0); + QCOMPARE(data2.xTranslation(), 0.5); + QCOMPARE(data2.yTranslation(), 2.0); + QCOMPARE(data2.zTranslation(), 3.0); + QCOMPARE(data2.translation(), QVector3D(0.5, 2.0, 3.0)); + QCOMPARE(data2.rotationAngle(), 45.0); + QCOMPARE(data2.rotationOrigin(), QVector3D(1.0, 2.0, 3.0)); + QCOMPARE(data2.rotationAxis(), QVector3D(1.0, 1.0, 0.0)); +} + +void TestScreenPaintData::testSetScale() +{ + ScreenPaintData 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); + // changing xScale should not affect y and z + data.setXScale(2.0); + QCOMPARE(data.xScale(), 2.0); + QCOMPARE(data.yScale(), 1.0); + QCOMPARE(data.zScale(), 1.0); + // changing yScale should not affect x and z + data.setYScale(3.0); + QCOMPARE(data.xScale(), 2.0); + QCOMPARE(data.yScale(), 3.0); + QCOMPARE(data.zScale(), 1.0); + // changing zScale should not affect x and y + data.setZScale(4.0); + QCOMPARE(data.xScale(), 2.0); + QCOMPARE(data.yScale(), 3.0); + QCOMPARE(data.zScale(), 4.0); + // setting a vector2d should affect x and y components + data.setScale(QVector2D(0.5, 2.0)); + QCOMPARE(data.xScale(), 0.5); + QCOMPARE(data.yScale(), 2.0); + QCOMPARE(data.zScale(), 4.0); + // setting a vector3d should affect all components + data.setScale(QVector3D(1.5, 2.5, 3.5)); + QCOMPARE(data.xScale(), 1.5); + QCOMPARE(data.yScale(), 2.5); + QCOMPARE(data.zScale(), 3.5); +} + +void TestScreenPaintData::testOperatorMultiplyAssign() +{ + ScreenPaintData 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 TestScreenPaintData::testSetTranslate() +{ + ScreenPaintData data; + QCOMPARE(data.xTranslation(), 0.0); + QCOMPARE(data.yTranslation(), 0.0); + QCOMPARE(data.zTranslation(), 0.0); + QCOMPARE(data.translation(), QVector3D()); + // set x translate, should not affect y and z + data.setXTranslation(1.0); + QCOMPARE(data.xTranslation(), 1.0); + QCOMPARE(data.yTranslation(), 0.0); + QCOMPARE(data.zTranslation(), 0.0); + QCOMPARE(data.translation(), QVector3D(1.0, 0.0, 0.0)); + // set y translate, should not affect x and z + data.setYTranslation(2.0); + QCOMPARE(data.xTranslation(), 1.0); + QCOMPARE(data.yTranslation(), 2.0); + QCOMPARE(data.zTranslation(), 0.0); + QCOMPARE(data.translation(), QVector3D(1.0, 2.0, 0.0)); + // set z translate, should not affect x and y + data.setZTranslation(3.0); + QCOMPARE(data.xTranslation(), 1.0); + QCOMPARE(data.yTranslation(), 2.0); + QCOMPARE(data.zTranslation(), 3.0); + QCOMPARE(data.translation(), QVector3D(1.0, 2.0, 3.0)); + // translate in x + data.translate(0.5); + QCOMPARE(data.translation(), QVector3D(1.5, 2.0, 3.0)); + // translate in x and y + data.translate(0.5, 0.75); + QCOMPARE(data.translation(), QVector3D(2.0, 2.75, 3.0)); + // translate in x, y and z + data.translate(1.0, 2.0, 3.0); + QCOMPARE(data.translation(), QVector3D(3.0, 4.75, 6.0)); + // translate using vector + data.translate(QVector3D(2.0, 1.0, 0.5)); + QCOMPARE(data.translation(), QVector3D(5.0, 5.75, 6.5)); +} + +void TestScreenPaintData::testOperatorPlus() +{ + ScreenPaintData 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 TestScreenPaintData::testSetAngle() +{ + ScreenPaintData data; + QCOMPARE(data.rotationAngle(), 0.0); + data.setRotationAngle(20.0); + QCOMPARE(data.rotationAngle(), 20.0); +} + +void TestScreenPaintData::testSetRotationOrigin() +{ + ScreenPaintData data; + QCOMPARE(data.rotationOrigin(), QVector3D()); + data.setRotationOrigin(QVector3D(1.0, 2.0, 3.0)); + QCOMPARE(data.rotationOrigin(), QVector3D(1.0, 2.0, 3.0)); +} + +void TestScreenPaintData::testSetRotationAxis() +{ + ScreenPaintData data; + QCOMPARE(data.rotationAxis(), QVector3D(0.0, 0.0, 1.0)); + data.setRotationAxis(Qt::XAxis); + QCOMPARE(data.rotationAxis(), QVector3D(1.0, 0.0, 0.0)); + data.setRotationAxis(Qt::YAxis); + QCOMPARE(data.rotationAxis(), QVector3D(0.0, 1.0, 0.0)); + data.setRotationAxis(Qt::ZAxis); + QCOMPARE(data.rotationAxis(), QVector3D(0.0, 0.0, 1.0)); + data.setRotationAxis(QVector3D(1.0, 1.0, 0.0)); + QCOMPARE(data.rotationAxis(), QVector3D(1.0, 1.0, 0.0)); +} + +QTEST_MAIN(TestScreenPaintData) +#include "test_screen_paint_data.moc" diff --git a/autotests/test_screens.cpp b/autotests/test_screens.cpp new file mode 100644 index 0000000..82bf43f --- /dev/null +++ b/autotests/test_screens.cpp @@ -0,0 +1,345 @@ +/* + 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 "mock_workspace.h" +#include "../cursor.h" +#include "mock_screens.h" +#include "mock_x11client.h" +// frameworks +#include +// Qt +#include + +Q_LOGGING_CATEGORY(KWIN_CORE, "kwin_core") + +// Mock + +class TestScreens : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void init(); + void testCurrentFollowsMouse(); + void testReconfigure_data(); + void testReconfigure(); + void testSize_data(); + void testSize(); + void testCount(); + void testIntersecting_data(); + void testIntersecting(); + void testCurrent_data(); + void testCurrent(); + void testCurrentClient(); + void testCurrentWithFollowsMouse_data(); + void testCurrentWithFollowsMouse(); + void testCurrentPoint_data(); + void testCurrentPoint(); +}; + +void TestScreens::init() +{ + KWin::Cursors::self()->setMouse(new KWin::Cursor(this)); +} + +void TestScreens::testCurrentFollowsMouse() +{ + KWin::MockWorkspace ws; + KWin::Screens *screens = KWin::Screens::create(&ws); + QVERIFY(!screens->isCurrentFollowsMouse()); + screens->setCurrentFollowsMouse(true); + QVERIFY(screens->isCurrentFollowsMouse()); + // setting to same should not do anything + screens->setCurrentFollowsMouse(true); + QVERIFY(screens->isCurrentFollowsMouse()); + + // setting back to other value + screens->setCurrentFollowsMouse(false); + QVERIFY(!screens->isCurrentFollowsMouse()); + // setting to same should not do anything + screens->setCurrentFollowsMouse(false); + QVERIFY(!screens->isCurrentFollowsMouse()); +} + +void TestScreens::testReconfigure_data() +{ + QTest::addColumn("focusPolicy"); + QTest::addColumn("expectedDefault"); + QTest::addColumn("setting"); + + QTest::newRow("ClickToFocus") << QStringLiteral("ClickToFocus") << false << true; + QTest::newRow("FocusFollowsMouse") << QStringLiteral("FocusFollowsMouse") << true << false; + QTest::newRow("FocusUnderMouse") << QStringLiteral("FocusUnderMouse") << true << false; + QTest::newRow("FocusStrictlyUnderMouse") << QStringLiteral("FocusStrictlyUnderMouse") << true << false; +} + +void TestScreens::testReconfigure() +{ + using namespace KWin; + MockWorkspace ws; + Screens::create(&ws); + screens()->reconfigure(); + QVERIFY(!screens()->isCurrentFollowsMouse()); + + QFETCH(QString, focusPolicy); + + KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("Windows").writeEntry("FocusPolicy", focusPolicy); + config->group("Windows").sync(); + config->sync(); + + screens()->setConfig(config); + screens()->reconfigure(); + QTEST(screens()->isCurrentFollowsMouse(), "expectedDefault"); + + QFETCH(bool, setting); + config->group("Windows").writeEntry("ActiveMouseScreen", setting); + config->sync(); + screens()->reconfigure(); + QCOMPARE(screens()->isCurrentFollowsMouse(), setting); +} + +void TestScreens::testSize_data() +{ + QTest::addColumn< QList >("geometries"); + QTest::addColumn("expectedSize"); + + QTest::newRow("empty") << QList{{QRect()}} << QSize(0, 0); + QTest::newRow("cloned") << QList{{QRect{0, 0, 200, 100}, QRect{0, 0, 200, 100}}} << QSize(200, 100); + QTest::newRow("adjacent") << QList{{QRect{0, 0, 200, 100}, QRect{200, 100, 400, 300}}} << QSize(600, 400); + QTest::newRow("overlapping") << QList{{QRect{-10, -20, 50, 100}, QRect{0, 0, 100, 200}}} << QSize(110, 220); + QTest::newRow("gap") << QList{{QRect{0, 0, 10, 20}, QRect{20, 40, 10, 20}}} << QSize(30, 60); +} + +void TestScreens::testSize() +{ + using namespace KWin; + MockWorkspace ws; + MockScreens *mockScreens = static_cast(Screens::create(&ws)); + QSignalSpy sizeChangedSpy(screens(), &KWin::Screens::sizeChanged); + QVERIFY(sizeChangedSpy.isValid()); + + QCOMPARE(screens()->size(), QSize(100, 100)); + QFETCH(QList, geometries); + QVERIFY(!screens()->isChanging()); + mockScreens->setGeometries(geometries); + QVERIFY(screens()->isChanging()); + + QVERIFY(sizeChangedSpy.wait()); + QVERIFY(!screens()->isChanging()); + QTEST(screens()->size(), "expectedSize"); +} + +void TestScreens::testCount() +{ + using namespace KWin; + MockWorkspace ws; + MockScreens *mockScreens = static_cast(Screens::create(&ws)); + QSignalSpy countChangedSpy(screens(), &KWin::Screens::countChanged); + QVERIFY(countChangedSpy.isValid()); + + QCOMPARE(screens()->count(), 1); + + // change to two screens + QList geometries{{QRect{0, 0, 100, 200}, QRect{100, 0, 100, 200}}}; + mockScreens->setGeometries(geometries); + + QVERIFY(countChangedSpy.wait()); + QCOMPARE(countChangedSpy.count(), 1); + QCOMPARE(countChangedSpy.first().first().toInt(), 1); + QCOMPARE(countChangedSpy.first().last().toInt(), 2); + QCOMPARE(screens()->count(), 2); + + // go back to one screen + geometries.takeLast(); + mockScreens->setGeometries(geometries); + QVERIFY(countChangedSpy.wait()); + QCOMPARE(countChangedSpy.count(), 2); + QCOMPARE(countChangedSpy.last().first().toInt(), 2); + QCOMPARE(countChangedSpy.last().last().toInt(), 1); + QCOMPARE(screens()->count(), 1); + + // setting the same geometries shouldn't emit the signal, but we should get a changed signal + QSignalSpy changedSpy(screens(), &KWin::Screens::changed); + QVERIFY(changedSpy.isValid()); + mockScreens->setGeometries(geometries); + QVERIFY(changedSpy.wait()); + QCOMPARE(countChangedSpy.count(), 2); +} + +void TestScreens::testIntersecting_data() +{ + QTest::addColumn>("geometries"); + QTest::addColumn("testGeometry"); + QTest::addColumn("expectedCount"); + + QTest::newRow("null-rect") << QList{{QRect{0, 0, 100, 100}}} << QRect() << 0; + QTest::newRow("non-overlapping") << QList{{QRect{0, 0, 100, 100}}} << QRect(100, 0, 100, 100) << 0; + QTest::newRow("in-between") << QList{{QRect{0, 0, 10, 20}, QRect{20, 40, 10, 20}}} << QRect(15, 0, 2, 2) << 0; + QTest::newRow("gap-overlapping") << QList{{QRect{0, 0, 10, 20}, QRect{20, 40, 10, 20}}} << QRect(9, 10, 200, 200) << 2; + QTest::newRow("larger") << QList{{QRect{0, 0, 100, 100}}} << QRect(-10, -10, 200, 200) << 1; + QTest::newRow("several") << QList{{QRect{0, 0, 100, 100}, QRect{100, 0, 100, 100}, QRect{200, 100, 100, 100}, QRect{300, 100, 100, 100}}} << QRect(0, 0, 300, 300) << 3; +} + +void TestScreens::testIntersecting() +{ + using namespace KWin; + MockWorkspace ws; + MockScreens *mockScreens = static_cast(Screens::create(&ws)); + QSignalSpy changedSpy(screens(), &KWin::Screens::changed); + QVERIFY(changedSpy.isValid()); + QFETCH(QList, geometries); + mockScreens->setGeometries(geometries); + // first is before it's updated + QVERIFY(changedSpy.wait()); + // second is after it's updated + QVERIFY(changedSpy.wait()); + + QFETCH(QRect, testGeometry); + QCOMPARE(screens()->count(), geometries.count()); + QTEST(screens()->intersecting(testGeometry), "expectedCount"); +} + +void TestScreens::testCurrent_data() +{ + QTest::addColumn("current"); + QTest::addColumn("signal"); + + QTest::newRow("unchanged") << 0 << false; + QTest::newRow("changed") << 1 << true; +} + +void TestScreens::testCurrent() +{ + using namespace KWin; + MockWorkspace ws; + Screens::create(&ws); + QSignalSpy currentChangedSpy(screens(), &KWin::Screens::currentChanged); + QVERIFY(currentChangedSpy.isValid()); + + QFETCH(int, current); + screens()->setCurrent(current); + QCOMPARE(screens()->current(), current); + QTEST(!currentChangedSpy.isEmpty(), "signal"); +} + +void TestScreens::testCurrentClient() +{ + using namespace KWin; + MockWorkspace ws; + MockScreens *mockScreens = static_cast(Screens::create(&ws)); + QSignalSpy changedSpy(screens(), &KWin::Screens::changed); + QVERIFY(changedSpy.isValid()); + mockScreens->setGeometries(QList{{QRect{0, 0, 100, 100}, QRect{100, 0, 100, 100}}}); + // first is before it's updated + QVERIFY(changedSpy.wait()); + // second is after it's updated + QVERIFY(changedSpy.wait()); + + QSignalSpy currentChangedSpy(screens(), &KWin::Screens::currentChanged); + QVERIFY(currentChangedSpy.isValid()); + + // create a mock client + X11Client *client = new X11Client(&ws); + client->setScreen(1); + + // it's not the active client, so changing won't work + screens()->setCurrent(client); + QVERIFY(currentChangedSpy.isEmpty()); + QCOMPARE(screens()->current(), 0); + + // making the client active should affect things + client->setActive(true); + ws.setActiveClient(client); + + // first of all current should be changed just by the fact that there is an active client + QCOMPARE(screens()->current(), 1); + // but also calling setCurrent should emit the changed signal + screens()->setCurrent(client); + QCOMPARE(currentChangedSpy.count(), 1); + QCOMPARE(screens()->current(), 1); + + // setting current with the same client again should not change, though + screens()->setCurrent(client); + QCOMPARE(currentChangedSpy.count(), 1); + + // and it should even still be on screen 1 if we make the client non-current again + ws.setActiveClient(nullptr); + client->setActive(false); + QCOMPARE(screens()->current(), 1); +} + +void TestScreens::testCurrentWithFollowsMouse_data() +{ + QTest::addColumn< QList >("geometries"); + QTest::addColumn("cursorPos"); + QTest::addColumn("expected"); + + QTest::newRow("empty") << QList{{QRect()}} << QPoint(100, 100) << 0; + QTest::newRow("cloned") << QList{{QRect{0, 0, 200, 100}, QRect{0, 0, 200, 100}}} << QPoint(50, 50) << 0; + QTest::newRow("adjacent-0") << QList{{QRect{0, 0, 200, 100}, QRect{200, 100, 400, 300}}} << QPoint(199, 99) << 0; + QTest::newRow("adjacent-1") << QList{{QRect{0, 0, 200, 100}, QRect{200, 100, 400, 300}}} << QPoint(200, 100) << 1; + QTest::newRow("gap") << QList{{QRect{0, 0, 10, 20}, QRect{20, 40, 10, 20}}} << QPoint(15, 30) << 1; +} + +void TestScreens::testCurrentWithFollowsMouse() +{ + using namespace KWin; + MockWorkspace ws; + MockScreens *mockScreens = static_cast(Screens::create(&ws)); + QSignalSpy changedSpy(screens(), &KWin::Screens::changed); + QVERIFY(changedSpy.isValid()); + screens()->setCurrentFollowsMouse(true); + QCOMPARE(screens()->current(), 0); + + QFETCH(QList, geometries); + mockScreens->setGeometries(geometries); + // first is before it's updated + QVERIFY(changedSpy.wait()); + // second is after it's updated + QVERIFY(changedSpy.wait()); + + QFETCH(QPoint, cursorPos); + KWin::Cursors::self()->mouse()->setPos(cursorPos); + QTEST(screens()->current(), "expected"); +} + +void TestScreens::testCurrentPoint_data() +{ + QTest::addColumn< QList >("geometries"); + QTest::addColumn("cursorPos"); + QTest::addColumn("expected"); + + QTest::newRow("empty") << QList{{QRect()}} << QPoint(100, 100) << 0; + QTest::newRow("cloned") << QList{{QRect{0, 0, 200, 100}, QRect{0, 0, 200, 100}}} << QPoint(50, 50) << 0; + QTest::newRow("adjacent-0") << QList{{QRect{0, 0, 200, 100}, QRect{200, 100, 400, 300}}} << QPoint(199, 99) << 0; + QTest::newRow("adjacent-1") << QList{{QRect{0, 0, 200, 100}, QRect{200, 100, 400, 300}}} << QPoint(200, 100) << 1; + QTest::newRow("gap") << QList{{QRect{0, 0, 10, 20}, QRect{20, 40, 10, 20}}} << QPoint(15, 30) << 1; +} + +void TestScreens::testCurrentPoint() +{ + using namespace KWin; + MockWorkspace ws; + MockScreens *mockScreens = static_cast(Screens::create(&ws)); + QSignalSpy changedSpy(screens(), &KWin::Screens::changed); + QVERIFY(changedSpy.isValid()); + + QFETCH(QList, geometries); + mockScreens->setGeometries(geometries); + // first is before it's updated + QVERIFY(changedSpy.wait()); + // second is after it's updated + QVERIFY(changedSpy.wait()); + + QFETCH(QPoint, cursorPos); + screens()->setCurrent(cursorPos); + QTEST(screens()->current(), "expected"); +} + +QTEST_MAIN(TestScreens) +#include "test_screens.moc" diff --git a/autotests/test_scripted_effectloader.cpp b/autotests/test_scripted_effectloader.cpp new file mode 100644 index 0000000..d0b72f5 --- /dev/null +++ b/autotests/test_scripted_effectloader.cpp @@ -0,0 +1,464 @@ +/* + 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 "../effectloader.h" +#include "mock_effectshandler.h" +#include "../scripting/scriptedeffect.h" +// for mocking +#include "../cursor.h" +#include "../input.h" +#include "../screenedge.h" +// KDE +#include +#include +#include +// Qt +#include +#include +Q_DECLARE_METATYPE(KWin::LoadEffectFlag) +Q_DECLARE_METATYPE(KWin::LoadEffectFlags) +Q_DECLARE_METATYPE(KWin::Effect*) +Q_DECLARE_METATYPE(KSharedConfigPtr) + +Q_LOGGING_CATEGORY(KWIN_CORE, "kwin_core") + +namespace KWin +{ +ScreenEdges *ScreenEdges::s_self = nullptr; + +void ScreenEdges::reserve(ElectricBorder, QObject *, const char *) +{ +} + +void ScreenEdges::reserveTouch(ElectricBorder, QAction *) +{ +} + +InputRedirection *InputRedirection::s_self = nullptr; + +void InputRedirection::registerShortcut(const QKeySequence &, QAction *) +{ +} + +namespace MetaScripting +{ +void registration(QScriptEngine *) +{ +} +} + +} + +class TestScriptedEffectLoader : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testHasEffect_data(); + void testHasEffect(); + void testKnownEffects(); + void testLoadEffect_data(); + void testLoadEffect(); + void testLoadScriptedEffect_data(); + void testLoadScriptedEffect(); + void testLoadAllEffects(); + void testCancelLoadAllEffects(); +}; + +void TestScriptedEffectLoader::initTestCase() +{ + qputenv("XDG_DATA_DIRS", QCoreApplication::applicationDirPath().toUtf8()); + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + QCoreApplication::instance()->setProperty("config", QVariant::fromValue(config)); + + KWin::Cursors::self()->setMouse(new KWin::Cursor(this)); +} + +void TestScriptedEffectLoader::testHasEffect_data() +{ + QTest::addColumn("name"); + QTest::addColumn("expected"); + + // all the built-in effects should fail + QTest::newRow("blur") << QStringLiteral("blur") << false; + QTest::newRow("Colorpicker") << QStringLiteral("colorpicker") << false; + QTest::newRow("Contrast") << QStringLiteral("contrast") << false; + QTest::newRow("CoverSwitch") << QStringLiteral("coverswitch") << false; + QTest::newRow("Cube") << QStringLiteral("cube") << false; + QTest::newRow("CubeSlide") << QStringLiteral("cubeslide") << false; + QTest::newRow("DesktopGrid") << QStringLiteral("desktopgrid") << false; + QTest::newRow("DimInactive") << QStringLiteral("diminactive") << false; + QTest::newRow("FallApart") << QStringLiteral("fallapart") << false; + QTest::newRow("FlipSwitch") << QStringLiteral("flipswitch") << false; + QTest::newRow("Glide") << QStringLiteral("glide") << false; + QTest::newRow("HighlightWindow") << QStringLiteral("highlightwindow") << false; + QTest::newRow("Invert") << QStringLiteral("invert") << false; + QTest::newRow("Kscreen") << QStringLiteral("kscreen") << false; + QTest::newRow("Logout") << QStringLiteral("logout") << false; + QTest::newRow("LookingGlass") << QStringLiteral("lookingglass") << false; + QTest::newRow("MagicLamp") << QStringLiteral("magiclamp") << false; + QTest::newRow("Magnifier") << QStringLiteral("magnifier") << false; + QTest::newRow("MouseClick") << QStringLiteral("mouseclick") << false; + QTest::newRow("MouseMark") << QStringLiteral("mousemark") << false; + QTest::newRow("PresentWindows") << QStringLiteral("presentwindows") << false; + QTest::newRow("Resize") << QStringLiteral("resize") << false; + QTest::newRow("ScreenEdge") << QStringLiteral("screenedge") << false; + QTest::newRow("ScreenShot") << QStringLiteral("screenshot") << false; + QTest::newRow("Sheet") << QStringLiteral("sheet") << false; + QTest::newRow("ShowFps") << QStringLiteral("showfps") << false; + QTest::newRow("ShowPaint") << QStringLiteral("showpaint") << false; + QTest::newRow("Slide") << QStringLiteral("slide") << false; + QTest::newRow("SlideBack") << QStringLiteral("slideback") << false; + QTest::newRow("SlidingPopups") << QStringLiteral("slidingpopups") << false; + QTest::newRow("SnapHelper") << QStringLiteral("snaphelper") << false; + QTest::newRow("StartupFeedback") << QStringLiteral("startupfeedback") << false; + QTest::newRow("ThumbnailAside") << QStringLiteral("thumbnailaside") << false; + QTest::newRow("TrackMouse") << QStringLiteral("trackmouse") << false; + QTest::newRow("WindowGeometry") << QStringLiteral("windowgeometry") << false; + QTest::newRow("WobblyWindows") << QStringLiteral("wobblywindows") << false; + QTest::newRow("Zoom") << QStringLiteral("zoom") << false; + QTest::newRow("Non Existing") << QStringLiteral("InvalidName") << false; + QTest::newRow("Fade - without kwin4_effect") << QStringLiteral("fade") << false; + + QTest::newRow("DialogParent") << QStringLiteral("kwin4_effect_dialogparent") << true; + QTest::newRow("DimScreen") << QStringLiteral("kwin4_effect_dimscreen") << true; + QTest::newRow("EyeOnScreen") << QStringLiteral("kwin4_effect_eyeonscreen") << true; + QTest::newRow("Fade + kwin4_effect") << QStringLiteral("kwin4_effect_fade") << true; + QTest::newRow("Fade + kwin4_effect + CS") << QStringLiteral("kwin4_eFfect_fAde") << true; + QTest::newRow("FadeDesktop") << QStringLiteral("kwin4_effect_fadedesktop") << true; + QTest::newRow("FadingPopups") << QStringLiteral("kwin4_effect_fadingpopups") << true; + QTest::newRow("FrozenApp") << QStringLiteral("kwin4_effect_frozenapp") << true; + QTest::newRow("FullScreen") << QStringLiteral("kwin4_effect_fullscreen") << true; + QTest::newRow("Login") << QStringLiteral("kwin4_effect_login") << true; + QTest::newRow("Logout") << QStringLiteral("kwin4_effect_logout") << true; + QTest::newRow("Maximize") << QStringLiteral("kwin4_effect_maximize") << true; + QTest::newRow("MorphingPopups") << QStringLiteral("kwin4_effect_morphingpopups") << true; + QTest::newRow("Scale") << QStringLiteral("kwin4_effect_scale") << true; + QTest::newRow("Squash") << QStringLiteral("kwin4_effect_squash") << true; + QTest::newRow("Translucency") << QStringLiteral("kwin4_effect_translucency") << true; + QTest::newRow("WindowAperture") << QStringLiteral("kwin4_effect_windowaperture") << true; +} + +void TestScriptedEffectLoader::testHasEffect() +{ + QFETCH(QString, name); + QFETCH(bool, expected); + + QScopedPointer mockHandler(new MockEffectsHandler(KWin::XRenderCompositing)); + KWin::ScriptedEffectLoader loader; + QCOMPARE(loader.hasEffect(name), expected); + + // each available effect should also be supported + QCOMPARE(loader.isEffectSupported(name), expected); + + if (expected) { + mockHandler->setAnimationsSupported(false); + QVERIFY(!loader.isEffectSupported(name)); + } +} + +void TestScriptedEffectLoader::testKnownEffects() +{ + QStringList expectedEffects; + expectedEffects << QStringLiteral("kwin4_effect_dialogparent") + << QStringLiteral("kwin4_effect_dimscreen") + << QStringLiteral("kwin4_effect_eyeonscreen") + << QStringLiteral("kwin4_effect_fade") + << QStringLiteral("kwin4_effect_fadedesktop") + << QStringLiteral("kwin4_effect_fadingpopups") + << QStringLiteral("kwin4_effect_frozenapp") + << QStringLiteral("kwin4_effect_login") + << QStringLiteral("kwin4_effect_logout") + << QStringLiteral("kwin4_effect_maximize") + << QStringLiteral("kwin4_effect_morphingpopups") + << QStringLiteral("kwin4_effect_scale") + << QStringLiteral("kwin4_effect_sessionquit") + << QStringLiteral("kwin4_effect_squash") + << QStringLiteral("kwin4_effect_translucency") + << QStringLiteral("kwin4_effect_windowaperture"); + + KWin::ScriptedEffectLoader loader; + QStringList result = loader.listOfKnownEffects(); + // at least as many effects as we expect - system running the test could have more effects + QVERIFY(result.size() >= expectedEffects.size()); + for (const QString &effect : expectedEffects) { + QVERIFY(result.contains(effect)); + } +} + +void TestScriptedEffectLoader::testLoadEffect_data() +{ + QTest::addColumn("name"); + QTest::addColumn("expected"); + + QTest::newRow("Non Existing") << QStringLiteral("InvalidName") << false; + QTest::newRow("Fade - without kwin4_effect") << QStringLiteral("fade") << false; + QTest::newRow("DialogParent") << QStringLiteral("kwin4_effect_dialogparent") << true; + QTest::newRow("DimScreen") << QStringLiteral("kwin4_effect_dimscreen") << true; + QTest::newRow("EyeOnScreen") << QStringLiteral("kwin4_effect_eyeonscreen") << true; + QTest::newRow("Fade + kwin4_effect") << QStringLiteral("kwin4_effect_fade") << true; + QTest::newRow("Fade + kwin4_effect + CS") << QStringLiteral("kwin4_eFfect_fAde") << true; + QTest::newRow("FadeDesktop") << QStringLiteral("kwin4_effect_fadedesktop") << true; + QTest::newRow("FadingPopups") << QStringLiteral("kwin4_effect_fadingpopups") << true; + QTest::newRow("FrozenApp") << QStringLiteral("kwin4_effect_frozenapp") << true; + QTest::newRow("FullScreen") << QStringLiteral("kwin4_effect_fullscreen") << true; + QTest::newRow("Login") << QStringLiteral("kwin4_effect_login") << true; + QTest::newRow("Logout") << QStringLiteral("kwin4_effect_logout") << true; + QTest::newRow("Maximize") << QStringLiteral("kwin4_effect_maximize") << true; + QTest::newRow("MorphingPopups") << QStringLiteral("kwin4_effect_morphingpopups") << true; + QTest::newRow("Scale") << QStringLiteral("kwin4_effect_scale") << true; + QTest::newRow("SessionQuit") << QStringLiteral("kwin4_effect_sessionquit") << true; + QTest::newRow("Squash") << QStringLiteral("kwin4_effect_squash") << true; + QTest::newRow("Translucency") << QStringLiteral("kwin4_effect_translucency") << true; + QTest::newRow("WindowAperture") << QStringLiteral("kwin4_effect_windowaperture") << true; + +} + +void TestScriptedEffectLoader::testLoadEffect() +{ + QFETCH(QString, name); + QFETCH(bool, expected); + + QScopedPointer mockHandler(new MockEffectsHandler(KWin::XRenderCompositing)); + KWin::ScriptedEffectLoader loader; + KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + loader.setConfig(config); + + qRegisterMetaType(); + QSignalSpy spy(&loader, &KWin::ScriptedEffectLoader::effectLoaded); + // connect to signal to ensure that we delete the Effect again as the Effect doesn't have a parent + connect(&loader, &KWin::ScriptedEffectLoader::effectLoaded, + [&name](KWin::Effect *effect, const QString &effectName) { + QCOMPARE(effectName, name.toLower()); + effect->deleteLater(); + } + ); + // try to load the Effect + QCOMPARE(loader.loadEffect(name), expected); + // loading again should fail + QVERIFY(!loader.loadEffect(name)); + + // signal spy should have got the signal if it was expected + QCOMPARE(spy.isEmpty(), !expected); + if (!spy.isEmpty()) { + QCOMPARE(spy.count(), 1); + // if we caught a signal it should have the effect name we passed in + QList arguments = spy.takeFirst(); + QCOMPARE(arguments.count(), 2); + QCOMPARE(arguments.at(1).toString(), name.toLower()); + } + spy.clear(); + QVERIFY(spy.isEmpty()); + + // now if we wait for the events being processed, the effect will get deleted and it should load again + QTest::qWait(1); + QCOMPARE(loader.loadEffect(name), expected); + // signal spy should have got the signal if it was expected + QCOMPARE(spy.isEmpty(), !expected); + if (!spy.isEmpty()) { + QCOMPARE(spy.count(), 1); + // if we caught a signal it should have the effect name we passed in + QList arguments = spy.takeFirst(); + QCOMPARE(arguments.count(), 2); + QCOMPARE(arguments.at(1).toString(), name.toLower()); + } +} + +void TestScriptedEffectLoader::testLoadScriptedEffect_data() +{ + QTest::addColumn("name"); + QTest::addColumn("expected"); + QTest::addColumn("loadFlags"); + + const KWin::LoadEffectFlags checkDefault = KWin::LoadEffectFlag::Load | KWin::LoadEffectFlag::CheckDefaultFunction; + const KWin::LoadEffectFlags forceFlags = KWin::LoadEffectFlag::Load; + const KWin::LoadEffectFlags dontLoadFlags = KWin::LoadEffectFlags(); + + // enabled by default + QTest::newRow("Fade") << QStringLiteral("kwin4_effect_fade") << true << checkDefault; + // not enabled by default + QTest::newRow("EyeOnScreen") << QStringLiteral("kwin4_effect_eyeonscreen") << true << checkDefault; + // Force an Effect which will load + QTest::newRow("EyeOnScreen-Force") << QStringLiteral("kwin4_effect_eyeonscreen") << true << forceFlags; + // Enforce no load of effect which is enabled by default + QTest::newRow("Fade-DontLoad") << QStringLiteral("kwin4_effect_fade") << false << dontLoadFlags; + // Enforce no load of effect which is not enabled by default, but enforced + QTest::newRow("EyeOnScreen-DontLoad") << QStringLiteral("kwin4_effect_eyeonscreen") << false << dontLoadFlags; +} + +void TestScriptedEffectLoader::testLoadScriptedEffect() +{ + QFETCH(QString, name); + QFETCH(bool, expected); + QFETCH(KWin::LoadEffectFlags, loadFlags); + + QScopedPointer mockHandler(new MockEffectsHandler(KWin::XRenderCompositing)); + KWin::ScriptedEffectLoader loader; + KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + loader.setConfig(config); + + const auto services = KPackage::PackageLoader::self()->findPackages(QStringLiteral("KWin/Effect"), QStringLiteral("kwin/effects"), + [name] (const KPluginMetaData &metadata) { + return metadata.pluginId().compare(name, Qt::CaseInsensitive) == 0; + } + ); + QCOMPARE(services.count(), 1); + + qRegisterMetaType(); + QSignalSpy spy(&loader, &KWin::ScriptedEffectLoader::effectLoaded); + // connect to signal to ensure that we delete the Effect again as the Effect doesn't have a parent + connect(&loader, &KWin::ScriptedEffectLoader::effectLoaded, + [&name](KWin::Effect *effect, const QString &effectName) { + QCOMPARE(effectName, name.toLower()); + effect->deleteLater(); + } + ); + // try to load the Effect + QCOMPARE(loader.loadEffect(services.first(), loadFlags), expected); + // loading again should fail + QVERIFY(!loader.loadEffect(services.first(), loadFlags)); + + // signal spy should have got the signal if it was expected + QCOMPARE(spy.isEmpty(), !expected); + if (!spy.isEmpty()) { + QCOMPARE(spy.count(), 1); + // if we caught a signal it should have the effect name we passed in + QList arguments = spy.takeFirst(); + QCOMPARE(arguments.count(), 2); + QCOMPARE(arguments.at(1).toString(), name.toLower()); + } + spy.clear(); + QVERIFY(spy.isEmpty()); + + // now if we wait for the events being processed, the effect will get deleted and it should load again + QTest::qWait(1); + QCOMPARE(loader.loadEffect(services.first(), loadFlags), expected); + // signal spy should have got the signal if it was expected + QCOMPARE(spy.isEmpty(), !expected); + if (!spy.isEmpty()) { + QCOMPARE(spy.count(), 1); + // if we caught a signal it should have the effect name we passed in + QList arguments = spy.takeFirst(); + QCOMPARE(arguments.count(), 2); + QCOMPARE(arguments.at(1).toString(), name.toLower()); + } +} + +void TestScriptedEffectLoader::testLoadAllEffects() +{ + QScopedPointer mockHandler(new MockEffectsHandler(KWin::XRenderCompositing)); + KWin::ScriptedEffectLoader loader; + + KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + + const QString kwin4 = QStringLiteral("kwin4_effect_"); + + // prepare the configuration to hard enable/disable the effects we want to load + KConfigGroup plugins = config->group("Plugins"); + plugins.writeEntry(kwin4 + QStringLiteral("dialogparentEnabled"), false); + plugins.writeEntry(kwin4 + QStringLiteral("dimscreenEnabled"), false); + plugins.writeEntry(kwin4 + QStringLiteral("fadeEnabled"), false); + plugins.writeEntry(kwin4 + QStringLiteral("fadedesktopEnabled"), false); + plugins.writeEntry(kwin4 + QStringLiteral("fadingpopupsEnabled"), false); + plugins.writeEntry(kwin4 + QStringLiteral("frozenappEnabled"), false); + plugins.writeEntry(kwin4 + QStringLiteral("fullscreenEnabled"), false); + plugins.writeEntry(kwin4 + QStringLiteral("loginEnabled"), false); + plugins.writeEntry(kwin4 + QStringLiteral("logoutEnabled"), false); + plugins.writeEntry(kwin4 + QStringLiteral("maximizeEnabled"), false); + plugins.writeEntry(kwin4 + QStringLiteral("scaleEnabled"), false); + plugins.writeEntry(kwin4 + QStringLiteral("squashEnabled"), false); + plugins.writeEntry(kwin4 + QStringLiteral("translucencyEnabled"), false); + plugins.writeEntry(kwin4 + QStringLiteral("eyeonscreenEnabled"), false); + plugins.writeEntry(kwin4 + QStringLiteral("windowapertureEnabled"), false); + plugins.writeEntry(kwin4 + QStringLiteral("morphingpopupsEnabled"), false); + plugins.writeEntry(kwin4 + QStringLiteral("sessionquitEnabled"), false); + + plugins.sync(); + + loader.setConfig(config); + + qRegisterMetaType(); + QSignalSpy spy(&loader, &KWin::ScriptedEffectLoader::effectLoaded); + // connect to signal to ensure that we delete the Effect again as the Effect doesn't have a parent + connect(&loader, &KWin::ScriptedEffectLoader::effectLoaded, + [](KWin::Effect *effect) { + effect->deleteLater(); + } + ); + + // the config is prepared so that no Effect gets loaded! + loader.queryAndLoadAll(); + + // we need to wait some time because it's queued and in a thread + QVERIFY(!spy.wait(100)); + + // now let's prepare a config which has one effect explicitly enabled + plugins.writeEntry(kwin4 + QStringLiteral("eyeonscreenEnabled"), true); + plugins.sync(); + + loader.queryAndLoadAll(); + // should load one effect in first go + QVERIFY(spy.wait(100)); + // and afterwards it should not load another one + QVERIFY(!spy.wait(10)); + + QCOMPARE(spy.size(), 1); + // if we caught a signal it should have the effect name we passed in + QList arguments = spy.takeFirst(); + QCOMPARE(arguments.count(), 2); + QCOMPARE(arguments.at(1).toString(), kwin4 + QStringLiteral("eyeonscreen")); + spy.clear(); + + // let's delete one of the default entries + plugins.deleteEntry(kwin4 + QStringLiteral("fadeEnabled")); + plugins.sync(); + + QVERIFY(spy.isEmpty()); + loader.queryAndLoadAll(); + + // let's use qWait as we need to wait for two signals to be emitted + QTRY_COMPARE(spy.size(), 2); + QStringList loadedEffects; + for (auto &list : spy) { + QCOMPARE(list.size(), 2); + loadedEffects << list.at(1).toString(); + } + std::sort(loadedEffects.begin(), loadedEffects.end()); + QCOMPARE(loadedEffects.at(0), kwin4 + QStringLiteral("eyeonscreen")); + QCOMPARE(loadedEffects.at(1), kwin4 + QStringLiteral("fade")); +} + +void TestScriptedEffectLoader::testCancelLoadAllEffects() +{ + // this test verifies that no test gets loaded when the loader gets cleared + MockEffectsHandler mockHandler(KWin::XRenderCompositing); + KWin::ScriptedEffectLoader loader; + + // prepare the configuration to hard enable/disable the effects we want to load + KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + const QString kwin4 = QStringLiteral("kwin4_effect_"); + KConfigGroup plugins = config->group("Plugins"); + plugins.writeEntry(kwin4 + QStringLiteral("eyeonscreenEnabled"), true); + plugins.sync(); + + loader.setConfig(config); + + qRegisterMetaType(); + QSignalSpy spy(&loader, &KWin::ScriptedEffectLoader::effectLoaded); + QVERIFY(spy.isValid()); + + loader.queryAndLoadAll(); + loader.clear(); + + // Should not load any effect + QVERIFY(!spy.wait(100)); + QVERIFY(spy.isEmpty()); +} + +QTEST_MAIN(TestScriptedEffectLoader) +#include "test_scripted_effectloader.moc" diff --git a/autotests/test_virtual_desktops.cpp b/autotests/test_virtual_desktops.cpp new file mode 100644 index 0000000..7af34e7 --- /dev/null +++ b/autotests/test_virtual_desktops.cpp @@ -0,0 +1,642 @@ +/* + 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 "../virtualdesktops.h" +#include "../input.h" +// KDE +#include + +#include +#include + +namespace KWin { + +int screen_number = 0; + +InputRedirection *InputRedirection::s_self = nullptr; + +void InputRedirection::registerShortcut(const QKeySequence &shortcut, QAction *action) +{ + Q_UNUSED(shortcut) + Q_UNUSED(action) +} + +void InputRedirection::registerAxisShortcut(Qt::KeyboardModifiers modifiers, PointerAxisDirection axis, QAction *action) +{ + Q_UNUSED(modifiers) + Q_UNUSED(axis) + Q_UNUSED(action) +} + +void InputRedirection::registerTouchpadSwipeShortcut(SwipeDirection, QAction*) +{ +} + +} + +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(); + template + void testDirection(const QString &actionName); +}; + +void TestVirtualDesktops::init() +{ + VirtualDesktopManager::create(); + screen_number = 0; +} + +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).type(), QVariant::UInt); + QCOMPARE(arguments.at(1).type(), QVariant::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); + vds->setCount(count); + QFETCH(uint, init); + QVERIFY(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); + QCOMPARE(arguments.at(0).type(), QVariant::UInt); + QCOMPARE(arguments.at(1).type(), QVariant::UInt); + QCOMPARE(arguments.at(0).toUInt(), init); + QCOMPARE(arguments.at(1).toUInt(), 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"); +} + +template +void TestVirtualDesktops::testDirection(const QString &actionName) +{ + VirtualDesktopManager *vds = VirtualDesktopManager::self(); + QFETCH(uint, initCount); + QFETCH(uint, initCurrent); + vds->setCount(initCount); + vds->setCurrent(initCurrent); + + QFETCH(bool, wrap); + QFETCH(uint, result); + T functor; + QCOMPARE(functor(nullptr, wrap)->x11DesktopNumber(), result); + + vds->setNavigationWrappingAround(wrap); + vds->initShortcuts(); + QAction *action = vds->findChild(actionName); + QVERIFY(action); + action->trigger(); + QCOMPARE(vds->current(), result); + QCOMPARE(functor(initCurrent, 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")); +} + +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")); +} + +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")); +} + +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")); +} + +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")); +} + +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")); +} + +void TestVirtualDesktops::updateGrid_data() +{ + QTest::addColumn("initCount"); + QTest::addColumn("size"); + QTest::addColumn("orientation"); + QTest::addColumn("coords"); + QTest::addColumn("desktop"); + const Qt::Orientation h = Qt::Horizontal; + const Qt::Orientation v = Qt::Vertical; + + QTest::newRow("one desktop, h") << (uint)1 << QSize(1, 1) << h << QPoint(0, 0) << (uint)1; + QTest::newRow("one desktop, v") << (uint)1 << QSize(1, 1) << v << QPoint(0, 0) << (uint)1; + QTest::newRow("one desktop, h, 0") << (uint)1 << QSize(1, 1) << h << QPoint(1, 0) << (uint)0; + QTest::newRow("one desktop, v, 0") << (uint)1 << QSize(1, 1) << v << QPoint(0, 1) << (uint)0; + + QTest::newRow("two desktops, h, 1") << (uint)2 << QSize(2, 1) << h << QPoint(0, 0) << (uint)1; + QTest::newRow("two desktops, h, 2") << (uint)2 << QSize(2, 1) << h << QPoint(1, 0) << (uint)2; + QTest::newRow("two desktops, h, 3") << (uint)2 << QSize(2, 1) << h << QPoint(0, 1) << (uint)0; + QTest::newRow("two desktops, h, 4") << (uint)2 << QSize(2, 1) << h << QPoint(2, 0) << (uint)0; + + QTest::newRow("two desktops, v, 1") << (uint)2 << QSize(2, 1) << v << QPoint(0, 0) << (uint)1; + QTest::newRow("two desktops, v, 2") << (uint)2 << QSize(2, 1) << v << QPoint(1, 0) << (uint)2; + QTest::newRow("two desktops, v, 3") << (uint)2 << QSize(2, 1) << v << QPoint(0, 1) << (uint)0; + QTest::newRow("two desktops, v, 4") << (uint)2 << QSize(2, 1) << v << QPoint(2, 0) << (uint)0; + + QTest::newRow("four desktops, h, one row, 1") << (uint)4 << QSize(4, 1) << h << QPoint(0, 0) << (uint)1; + QTest::newRow("four desktops, h, one row, 2") << (uint)4 << QSize(4, 1) << h << QPoint(1, 0) << (uint)2; + QTest::newRow("four desktops, h, one row, 3") << (uint)4 << QSize(4, 1) << h << QPoint(2, 0) << (uint)3; + QTest::newRow("four desktops, h, one row, 4") << (uint)4 << QSize(4, 1) << h << QPoint(3, 0) << (uint)4; + + QTest::newRow("four desktops, v, one column, 1") << (uint)4 << QSize(1, 4) << v << QPoint(0, 0) << (uint)1; + QTest::newRow("four desktops, v, one column, 2") << (uint)4 << QSize(1, 4) << v << QPoint(0, 1) << (uint)2; + QTest::newRow("four desktops, v, one column, 3") << (uint)4 << QSize(1, 4) << v << QPoint(0, 2) << (uint)3; + QTest::newRow("four desktops, v, one column, 4") << (uint)4 << QSize(1, 4) << v << QPoint(0, 3) << (uint)4; + + QTest::newRow("four desktops, h, grid, 1") << (uint)4 << QSize(2, 2) << h << QPoint(0, 0) << (uint)1; + QTest::newRow("four desktops, h, grid, 2") << (uint)4 << QSize(2, 2) << h << QPoint(1, 0) << (uint)2; + QTest::newRow("four desktops, h, grid, 3") << (uint)4 << QSize(2, 2) << h << QPoint(0, 1) << (uint)3; + QTest::newRow("four desktops, h, grid, 4") << (uint)4 << QSize(2, 2) << h << QPoint(1, 1) << (uint)4; + QTest::newRow("four desktops, h, grid, 0/3") << (uint)4 << QSize(2, 2) << h << QPoint(0, 3) << (uint)0; + + QTest::newRow("three desktops, h, grid, 1") << (uint)3 << QSize(2, 2) << h << QPoint(0, 0) << (uint)1; + QTest::newRow("three desktops, h, grid, 2") << (uint)3 << QSize(2, 2) << h << QPoint(1, 0) << (uint)2; + QTest::newRow("three desktops, h, grid, 3") << (uint)3 << QSize(2, 2) << h << QPoint(0, 1) << (uint)3; + QTest::newRow("three desktops, h, grid, 4") << (uint)3 << QSize(2, 2) << h << QPoint(1, 1) << (uint)0; +} + +void TestVirtualDesktops::updateGrid() +{ + VirtualDesktopManager *vds = VirtualDesktopManager::self(); + QFETCH(uint, initCount); + vds->setCount(initCount); + VirtualDesktopGrid grid; + + QFETCH(QSize, size); + QFETCH(Qt::Orientation, orientation); + QCOMPARE(vds->desktops().count(), int(initCount)); + grid.update(size, orientation, 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)4 << (uint)1 << "Desktop 1"; + QTest::newRow("desktop 2") << (uint)4 << (uint)2 << "Desktop 2"; + QTest::newRow("desktop 3") << (uint)4 << (uint)3 << "Desktop 3"; + QTest::newRow("desktop 4") << (uint)4 << (uint)4 << "Desktop 4"; + QTest::newRow("desktop 5") << (uint)4 << (uint)5 << "Desktop 5"; +} + +void TestVirtualDesktops::name() +{ + VirtualDesktopManager *vds = VirtualDesktopManager::self(); + QFETCH(uint, initCount); + vds->setCount(initCount); + QFETCH(uint, desktop); + + QTEST(vds->name(desktop), "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("Desktops").writeEntry("Number", 4); + vds->load(); + QCOMPARE(vds->count(), (uint)4); + + // setting the config value and reloading should update + config->group("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("Desktops"), false); + vds->save(); + QCOMPARE(config->hasGroup("Desktops"), true); + KConfigGroup desktops = config->group("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/autotests/test_virtualkeyboard_dbus.cpp b/autotests/test_virtualkeyboard_dbus.cpp new file mode 100644 index 0000000..54d7a74 --- /dev/null +++ b/autotests/test_virtualkeyboard_dbus.cpp @@ -0,0 +1,131 @@ +/* + 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 +#include +#include +#include +#include + +#include "../virtualkeyboard_dbus.h" + +using KWin::VirtualKeyboardDBus; + +class VirtualKeyboardDBusTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testEnabled(); + void testRequestEnabled_data(); + void testRequestEnabled(); +}; + +class DbusPropertyHelper : public QObject +{ + Q_OBJECT +public: + DbusPropertyHelper() + : QObject(nullptr) + { + QDBusConnection::sessionBus().connect( + QStringLiteral("org.kde.kwin.testvirtualkeyboard"), + QStringLiteral("/VirtualKeyboard"), + QStringLiteral("org.kde.kwin.VirtualKeyboard"), + QStringLiteral("enabledChanged"), + this, + SLOT(slotEnabledChanged())); + } + ~DbusPropertyHelper() override = default; + +Q_SIGNALS: + void enabledChanged(); + +private Q_SLOTS: + void slotEnabledChanged() { + emit enabledChanged(); + } +}; + +void VirtualKeyboardDBusTest::initTestCase() +{ + QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.kwin.testvirtualkeyboard")); +} + +void VirtualKeyboardDBusTest::testEnabled() +{ + VirtualKeyboardDBus dbus; + DbusPropertyHelper helper; + QSignalSpy helperChangedSpy(&helper, &DbusPropertyHelper::enabledChanged); + QVERIFY(helperChangedSpy.isValid()); + + QCOMPARE(dbus.isEnabled(), false); + QCOMPARE(dbus.property("enabled").toBool(), false); + QSignalSpy enabledChangedSpy(&dbus, &VirtualKeyboardDBus::enabledChanged); + QVERIFY(enabledChangedSpy.isValid()); + + auto readProperty = [] (bool enabled) { + const QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kwin.testvirtualkeyboard"), + QStringLiteral("/VirtualKeyboard"), + QStringLiteral("org.kde.kwin.VirtualKeyboard"), + QStringLiteral("isEnabled")); + const auto reply = QDBusConnection::sessionBus().call(message); + QCOMPARE(reply.type(), QDBusMessage::ReplyMessage); + QCOMPARE(reply.arguments().count(), 1); + QCOMPARE(reply.arguments().first().toBool(), enabled); + }; + readProperty(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); + readProperty(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); + readProperty(false); +} + +void VirtualKeyboardDBusTest::testRequestEnabled_data() +{ + QTest::addColumn("method"); + QTest::addColumn("expectedResult"); + + QTest::newRow("enable") << QStringLiteral("enable") << true; + QTest::newRow("disable") << QStringLiteral("disable") << false; +} + +void VirtualKeyboardDBusTest::testRequestEnabled() +{ + VirtualKeyboardDBus dbus; + QSignalSpy activateRequestedSpy(&dbus, &VirtualKeyboardDBus::activateRequested); + QVERIFY(activateRequestedSpy.isValid()); + QFETCH(QString, method); + const QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kwin.testvirtualkeyboard"), + QStringLiteral("/VirtualKeyboard"), + QStringLiteral("org.kde.kwin.VirtualKeyboard"), + method); + QDBusConnection::sessionBus().asyncCall(message); + QTRY_COMPARE(activateRequestedSpy.count(), 1); + QTEST(activateRequestedSpy.first().first().toBool(), "expectedResult"); +} + +QTEST_GUILESS_MAIN(VirtualKeyboardDBusTest) +#include "test_virtualkeyboard_dbus.moc" diff --git a/autotests/test_window_paint_data.cpp b/autotests/test_window_paint_data.cpp new file mode 100644 index 0000000..937f136 --- /dev/null +++ b/autotests/test_window_paint_data.cpp @@ -0,0 +1,557 @@ +/* + 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 +#include "../virtualdesktops.h" + +#include +#include +#include + +#include + +using namespace KWin; + +class MockEffectWindow : public EffectWindow +{ + Q_OBJECT +public: + MockEffectWindow(QObject *parent = nullptr); + WindowQuadList buildQuads(bool force = false) const override; + QVariant data(int role) const override; + QRect decorationInnerRect() const override; + void deleteProperty(long int atom) const override; + void disablePainting(int reason) override; + void enablePainting(int reason) override; + void addRepaint(const QRect &r) override; + void addRepaint(int x, int y, int w, int h) override; + void addRepaintFull() override; + void addLayerRepaint(const QRect &r) override; + void addLayerRepaint(int x, int y, int w, int h) override; + EffectWindow *findModal() override; + EffectWindow *transientFor() override; + const EffectWindowGroup *group() const override; + bool isPaintingEnabled() override; + EffectWindowList mainWindows() const override; + QByteArray readProperty(long int atom, long int type, int format) const override; + void refWindow() override; + void unrefWindow() override; + QRegion shape() const override; + void setData(int role, const QVariant &data) override; + void minimize() override; + void unminimize() override; + void closeWindow() override; + void referencePreviousWindowPixmap() override {} + void unreferencePreviousWindowPixmap() override {} + QWindow *internalWindow() const override { + return nullptr; + } + bool isDeleted() const override { + return false; + } + bool isMinimized() const override { + return false; + } + double opacity() const override { + return m_opacity; + } + void setOpacity(qreal opacity) { + m_opacity = opacity; + } + bool hasAlpha() const override { + return true; + } + QStringList activities() const override { + return QStringList(); + } + int desktop() const override { + return 0; + } + QVector desktops() const override { + return {}; + } + int x() const override { + return 0; + } + int y() const override { + return 0; + } + int width() const override { + return 100; + } + int height() const override { + return 100; + } + QSize basicUnit() const override { + return QSize(); + } + QRect geometry() const override { + return QRect(); + } + QRect expandedGeometry() const override { + return QRect(); + } + QRect frameGeometry() const override { + return QRect(); + } + QRect bufferGeometry() const override { + return QRect(); + } + int screen() const override { + return 0; + } + bool hasOwnShape() const override { + return false; + } + QPoint pos() const override { + return QPoint(); + } + QSize size() const override { + return QSize(100,100); + } + QRect rect() const override { + return QRect(0,0,100,100); + } + bool isMovable() const override { + return true; + } + bool isMovableAcrossScreens() const override { + return true; + } + bool isUserMove() const override { + return false; + } + bool isUserResize() const override { + return false; + } + QRect iconGeometry() const override { + return QRect(); + } + bool isDesktop() const override { + return false; + } + bool isDock() const override { + return false; + } + bool isToolbar() const override { + return false; + } + bool isMenu() const override { + return false; + } + bool isNormalWindow() const override { + return true; + } + bool isSpecialWindow() const override { + return false; + } + bool isDialog() const override { + return false; + } + bool isSplash() const override { + return false; + } + bool isUtility() const override { + return false; + } + bool isDropdownMenu() const override { + return false; + } + bool isPopupMenu() const override { + return false; + } + bool isTooltip() const override { + return false; + } + bool isNotification() const override { + return false; + } + bool isCriticalNotification() const override { + return false; + } + bool isOnScreenDisplay() const override { + return false; + } + bool isComboBox() const override { + return false; + } + bool isDNDIcon() const override { + return false; + } + QRect contentsRect() const override { + return QRect(); + } + bool decorationHasAlpha() const override { + return false; + } + QString caption() const override { + return QString(); + } + QIcon icon() const override { + return QIcon(); + } + QString windowClass() const override { + return QString(); + } + QString windowRole() const override { + return QString(); + } + NET::WindowType windowType() const override { + return NET::Normal; + } + bool acceptsFocus() const override { + return true; + } + bool keepAbove() const override { + return false; + } + bool keepBelow() const override { + return false; + } + bool isModal() const override { + return false; + } + bool isSkipSwitcher() const override { + return false; + } + bool isCurrentTab() const override { + return true; + } + bool skipsCloseAnimation() const override { + return false; + } + KWaylandServer::SurfaceInterface *surface() const override { + return nullptr; + } + bool isFullScreen() const override { + return false; + } + bool isUnresponsive() const override { + return false; + } + bool isPopupWindow() const override { + return false; + } + bool isManaged() const override { + return true; + } + bool isWaylandClient() const override { + return true; + } + bool isX11Client() const override { + return false; + } + bool isOutline() const override { + return false; + } + pid_t pid() const override { + return 0; + } + +private: + qreal m_opacity = 1.0; +}; + +MockEffectWindow::MockEffectWindow(QObject *parent) + : EffectWindow(parent) +{ +} + +WindowQuadList MockEffectWindow::buildQuads(bool force) const +{ + Q_UNUSED(force) + return WindowQuadList(); +} + +QVariant MockEffectWindow::data(int role) const +{ + Q_UNUSED(role) + return QVariant(); +} + +QRect MockEffectWindow::decorationInnerRect() const +{ + return QRect(); +} + +void MockEffectWindow::deleteProperty(long int atom) const +{ + Q_UNUSED(atom) +} + +void MockEffectWindow::disablePainting(int reason) +{ + Q_UNUSED(reason) +} + +void MockEffectWindow::enablePainting(int reason) +{ + Q_UNUSED(reason) +} + +void MockEffectWindow::addRepaint(const QRect &r) +{ + Q_UNUSED(r) +} + +void MockEffectWindow::addRepaint(int x, int y, int w, int h) +{ + Q_UNUSED(x) + Q_UNUSED(y) + Q_UNUSED(w) + Q_UNUSED(h) +} + +void MockEffectWindow::addRepaintFull() +{ +} + +void MockEffectWindow::addLayerRepaint(const QRect &r) +{ + Q_UNUSED(r) +} + +void MockEffectWindow::addLayerRepaint(int x, int y, int w, int h) +{ + Q_UNUSED(x) + Q_UNUSED(y) + Q_UNUSED(w) + Q_UNUSED(h) +} + +EffectWindow *MockEffectWindow::findModal() +{ + return nullptr; +} + +EffectWindow *MockEffectWindow::transientFor() +{ + return nullptr; +} + +const EffectWindowGroup *MockEffectWindow::group() const +{ + return nullptr; +} + +bool MockEffectWindow::isPaintingEnabled() +{ + return true; +} + +EffectWindowList MockEffectWindow::mainWindows() const +{ + return EffectWindowList(); +} + +QByteArray MockEffectWindow::readProperty(long int atom, long int type, int format) const +{ + Q_UNUSED(atom) + Q_UNUSED(type) + Q_UNUSED(format) + return QByteArray(); +} + +void MockEffectWindow::refWindow() +{ +} + +void MockEffectWindow::setData(int role, const QVariant &data) +{ + Q_UNUSED(role) + Q_UNUSED(data) +} + +void MockEffectWindow::minimize() +{ +} + +void MockEffectWindow::unminimize() +{ +} + +void MockEffectWindow::closeWindow() +{ +} + +QRegion MockEffectWindow::shape() const +{ + return QRegion(); +} + +void MockEffectWindow::unrefWindow() +{ +} + +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() +{ + MockEffectWindow w; + w.setOpacity(0.5); + WindowPaintData data(&w); + 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(), 0.5); + QCOMPARE(data.brightness(), 1.0); + QCOMPARE(data.saturation(), 1.0); +} + +void TestWindowPaintData::testCopyCtor() +{ + MockEffectWindow w; + WindowPaintData data(&w); + 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() +{ + MockEffectWindow w; + WindowPaintData data(&w); + // 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() +{ + MockEffectWindow w; + WindowPaintData data(&w); + 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() +{ + MockEffectWindow w; + WindowPaintData data(&w); + 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() +{ + MockEffectWindow w; + WindowPaintData data(&w); + 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() +{ + MockEffectWindow w; + WindowPaintData data(&w); + 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/autotests/test_x11_timestamp_update.cpp b/autotests/test_x11_timestamp_update.cpp new file mode 100644 index 0000000..f4d6b66 --- /dev/null +++ b/autotests/test_x11_timestamp_update.cpp @@ -0,0 +1,129 @@ +/* + 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 +#include + +#include +#include + +#include "main.h" +#include "utils.h" + +namespace KWin +{ + +class X11TestApplication : public Application +{ + Q_OBJECT +public: + X11TestApplication(int &argc, char **argv); + ~X11TestApplication() override; + +protected: + void performStartup() override; + +}; + +X11TestApplication::X11TestApplication(int &argc, char **argv) + : Application(OperationModeX11, argc, argv) +{ + setX11Connection(QX11Info::connection()); + setX11RootWindow(QX11Info::appRootWindow()); + + // move directory containing executable to front, so that KPluginLoader prefers the plugins in + // the build dir over system installed ones + const auto ownPath = libraryPaths().last(); + removeLibraryPath(ownPath); + addLibraryPath(ownPath); + + const auto plugins = KPluginLoader::findPluginsById(QStringLiteral("org.kde.kwin.platforms"), + QStringLiteral("KWinX11Platform")); + if (plugins.empty()) { + quit(); + return; + } + initPlatform(plugins.first()); +} + +X11TestApplication::~X11TestApplication() +{ +} + +void X11TestApplication::performStartup() +{ +} + +} + +class X11TimestampUpdateTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testGrabAfterServerTime(); + void testBeforeLastGrabTime(); +}; + +void X11TimestampUpdateTest::testGrabAfterServerTime() +{ + // this test tries to grab the X keyboard with a timestamp in future + // that should fail, but after updating the X11 timestamp, it should + // work again + KWin::updateXTime(); + QCOMPARE(KWin::grabXKeyboard(), true); + KWin::ungrabXKeyboard(); + + // now let's change the timestamp + KWin::kwinApp()->setX11Time(KWin::xTime() + 5 * 60 * 1000); + + // now grab keyboard should fail + QCOMPARE(KWin::grabXKeyboard(), false); + + // let's update timestamp, now it should work again + KWin::updateXTime(); + QCOMPARE(KWin::grabXKeyboard(), true); + KWin::ungrabXKeyboard(); +} + +void X11TimestampUpdateTest::testBeforeLastGrabTime() +{ + // this test tries to grab the X keyboard with a timestamp before the + // last grab time on the server. That should fail, but after updating the X11 + // timestamp it should work again + + // first set the grab timestamp + KWin::updateXTime(); + QCOMPARE(KWin::grabXKeyboard(), true); + KWin::ungrabXKeyboard(); + + // now go to past + const auto timestamp = KWin::xTime(); + KWin::kwinApp()->setX11Time(KWin::xTime() - 5 * 60 * 1000, KWin::Application::TimestampUpdate::Always); + QCOMPARE(KWin::xTime(), timestamp - 5 * 60 * 1000); + + // now grab keyboard should fail + QCOMPARE(KWin::grabXKeyboard(), false); + + // let's update timestamp, now it should work again + KWin::updateXTime(); + QVERIFY(KWin::xTime() >= timestamp); + QCOMPARE(KWin::grabXKeyboard(), true); + KWin::ungrabXKeyboard(); +} + +int main(int argc, char *argv[]) +{ + setenv("QT_QPA_PLATFORM", "xcb", true); + KWin::X11TestApplication app(argc, argv); + app.setAttribute(Qt::AA_Use96Dpi, true); + X11TimestampUpdateTest tc; + return QTest::qExec(&tc, argc, argv); +} + +#include "test_x11_timestamp_update.moc" diff --git a/autotests/test_xcb_size_hints.cpp b/autotests/test_xcb_size_hints.cpp new file mode 100644 index 0000000..4be7a8e --- /dev/null +++ b/autotests/test_xcb_size_hints.cpp @@ -0,0 +1,366 @@ +/* + 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 "../xcbutils.h" +// Qt +#include +#include +#include +#include +// xcb +#include +#include + +using namespace KWin; +using namespace KWin::Xcb; + +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: + 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(QRect(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; + memset(&hints, 0, sizeof(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()); + + 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 + GeometryHints::NormalHints::SizeHints 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; + memset(&xcbHints, 0, sizeof(xcbHints)); + xcb_icccm_set_wm_normal_hints(QX11Info::connection(), m_testWindow, &xcbHints); + xcb_flush(QX11Info::connection()); + + 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() +{ + 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() +{ + 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; + memset(&xcbHints, 0, sizeof(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()); + + 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/autotests/test_xcb_window.cpp b/autotests/test_xcb_window.cpp new file mode 100644 index 0000000..321cb65 --- /dev/null +++ b/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 "../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 QRect 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 QRect 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 QRect 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 QRect 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 QRect 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 QRect 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 QRect 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; + ScopedCPointer attribs(xcb_get_window_attributes_reply( + connection(), + xcb_get_window_attributes(connection(), wId), + &error)); + QVERIFY(attribs.isNull()); + 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; + ScopedCPointer attribs2(xcb_get_window_attributes_reply( + connection(), + xcb_get_window_attributes(connection(), wId), + &error)); + QVERIFY(attribs2.isNull()); + 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/autotests/test_xcb_wrapper.cpp b/autotests/test_xcb_wrapper.cpp new file mode 100644 index 0000000..f732417 --- /dev/null +++ b/autotests/test_xcb_wrapper.cpp @@ -0,0 +1,520 @@ +/* + 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 "../xcbutils.h" +// Qt +#include +#include +#include +#include +// xcb +#include + +using namespace KWin; +using namespace KWin::Xcb; + +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(WindowGeometry &geometry); + void testGeometry(WindowGeometry &geometry, const QRect &rect); + 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(QRect(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(WindowGeometry &geometry) +{ + QCOMPARE(geometry.window(), noneWindow()); + QVERIFY(!geometry.data()); + QCOMPARE(geometry.isNull(), true); + QCOMPARE(geometry.rect(), QRect()); + QVERIFY(!geometry); +} + +void TestXcbWrapper::testGeometry(WindowGeometry &geometry, const QRect &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() +{ + WindowGeometry geometry; + testEmpty(geometry); + QVERIFY(!geometry.isRetrieved()); +} + +void TestXcbWrapper::normalCtor() +{ + WindowGeometry geometry(m_testWindow); + QVERIFY(!geometry.isRetrieved()); + testGeometry(geometry, QRect(0, 0, 10, 10)); +} + +void TestXcbWrapper::copyCtorEmpty() +{ + WindowGeometry geometry; + WindowGeometry other(geometry); + testEmpty(geometry); + QVERIFY(geometry.isRetrieved()); + testEmpty(other); + QVERIFY(!other.isRetrieved()); +} + +void TestXcbWrapper::copyCtorBeforeRetrieve() +{ + WindowGeometry geometry(m_testWindow); + QVERIFY(!geometry.isRetrieved()); + WindowGeometry other(geometry); + testEmpty(geometry); + QVERIFY(geometry.isRetrieved()); + + QVERIFY(!other.isRetrieved()); + testGeometry(other, QRect(0, 0, 10, 10)); +} + +void TestXcbWrapper::copyCtorAfterRetrieve() +{ + WindowGeometry geometry(m_testWindow); + QVERIFY(geometry); + QVERIFY(geometry.isRetrieved()); + QCOMPARE(geometry.rect(), QRect(0, 0, 10, 10)); + WindowGeometry other(geometry); + testEmpty(geometry); + QVERIFY(geometry.isRetrieved()); + + QVERIFY(other.isRetrieved()); + testGeometry(other, QRect(0, 0, 10, 10)); +} + +void TestXcbWrapper::assignementEmpty() +{ + WindowGeometry geometry; + WindowGeometry other; + testEmpty(geometry); + testEmpty(other); + + other = geometry; + QVERIFY(geometry.isRetrieved()); + testEmpty(geometry); + testEmpty(other); + QVERIFY(!other.isRetrieved()); + // test assignment to self + geometry = geometry; + other = other; + testEmpty(geometry); + testEmpty(other); +} + +void TestXcbWrapper::assignmentBeforeRetrieve() +{ + WindowGeometry geometry(m_testWindow); + WindowGeometry other = geometry; + QVERIFY(geometry.isRetrieved()); + testEmpty(geometry); + + QVERIFY(!other.isRetrieved()); + testGeometry(other, QRect(0, 0, 10, 10)); + + other = WindowGeometry(m_testWindow); + QVERIFY(!other.isRetrieved()); + QCOMPARE(other.window(), (xcb_window_t)m_testWindow); + other = WindowGeometry(); + testEmpty(geometry); + // test assignment to self + geometry = geometry; + other = other; + testEmpty(geometry); +} + +void TestXcbWrapper::assignmentAfterRetrieve() +{ + WindowGeometry geometry(m_testWindow); + QVERIFY(geometry); + QVERIFY(geometry.isRetrieved()); + WindowGeometry other = geometry; + testEmpty(geometry); + + QVERIFY(other.isRetrieved()); + testGeometry(other, QRect(0, 0, 10, 10)); + + // test assignment to self + geometry = geometry; + other = other; + testEmpty(geometry); + testGeometry(other, QRect(0, 0, 10, 10)); + + // set to empty again + other = 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. + WindowGeometry *geometry = new WindowGeometry(); + delete geometry; + + geometry = new WindowGeometry(m_testWindow); + delete geometry; + + geometry = new WindowGeometry(m_testWindow); + QVERIFY(geometry->data()); + delete geometry; +} + +void TestXcbWrapper::testQueryTree() +{ + 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 + 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 + 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); + + CurrentInput input; + QCOMPARE(input.window(), (xcb_window_t)m_testWindow); + + // creating a copy should make the input object have no window any more + CurrentInput input2(input); + QCOMPARE(input2.window(), (xcb_window_t)m_testWindow); + QCOMPARE(input.window(), xcb_window_t(XCB_WINDOW_NONE)); +} + +void TestXcbWrapper::testTransientFor() +{ + TransientFor transient(m_testWindow); + QCOMPARE(transient.window(), (xcb_window_t)m_testWindow); + // our m_testWindow doesn't have a transient for hint + xcb_window_t compareWindow = XCB_WINDOW_NONE; + QVERIFY(!transient.getTransientFor(&compareWindow)); + QCOMPARE(compareWindow, xcb_window_t(XCB_WINDOW_NONE)); + bool ok = true; + QCOMPARE(transient.value(32, XCB_ATOM_WINDOW, XCB_WINDOW_NONE, &ok), xcb_window_t(XCB_WINDOW_NONE)); + QVERIFY(!ok); + ok = true; + QCOMPARE(transient.value(XCB_WINDOW_NONE, &ok), xcb_window_t(XCB_WINDOW_NONE)); + QVERIFY(!ok); + + // Create a Window with a transient for hint + Window transientWindow(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 + TransientFor realTransient(transientWindow); + QVERIFY(realTransient.getTransientFor(&compareWindow)); + QCOMPARE(compareWindow, (xcb_window_t)m_testWindow); + ok = false; + QCOMPARE(realTransient.value(32, XCB_ATOM_WINDOW, XCB_WINDOW_NONE, &ok), (xcb_window_t)m_testWindow); + QVERIFY(ok); + ok = false; + QCOMPARE(realTransient.value(XCB_WINDOW_NONE, &ok), (xcb_window_t)m_testWindow); + QVERIFY(ok); + ok = false; + QCOMPARE(realTransient.value(), (xcb_window_t)m_testWindow); + QCOMPARE(realTransient.value(nullptr, &ok)[0], (xcb_window_t)m_testWindow); + QVERIFY(ok); + QCOMPARE(realTransient.value()[0], (xcb_window_t)m_testWindow); + + // test for a not existing window + TransientFor doesntExist(XCB_WINDOW_NONE); + QVERIFY(!doesntExist.getTransientFor(&compareWindow)); +} + +void TestXcbWrapper::testPropertyByteArray() +{ + Window testWindow(createWindow()); + Property prop(false, testWindow, XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 0, 100000); + QCOMPARE(prop.toByteArray(), QByteArray()); + bool ok = true; + QCOMPARE(prop.toByteArray(&ok), QByteArray()); + QVERIFY(!ok); + ok = true; + QVERIFY(!prop.value()); + QCOMPARE(prop.value("bar", &ok), "bar"); + QVERIFY(!ok); + QCOMPARE(QByteArray(StringProperty(testWindow, XCB_ATOM_WM_NAME)), QByteArray()); + + testWindow.changeProperty(XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 8, 3, "foo"); + prop = Property(false, testWindow, XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 0, 100000); + QCOMPARE(prop.toByteArray(), QByteArrayLiteral("foo")); + QCOMPARE(prop.toByteArray(&ok), QByteArrayLiteral("foo")); + QVERIFY(ok); + QCOMPARE(prop.value(nullptr, &ok), "foo"); + QVERIFY(ok); + QCOMPARE(QByteArray(StringProperty(testWindow, XCB_ATOM_WM_NAME)), QByteArrayLiteral("foo")); + + // verify incorrect format and type + QCOMPARE(prop.toByteArray(32), QByteArray()); + QCOMPARE(prop.toByteArray(8, XCB_ATOM_CARDINAL), QByteArray()); + + // verify empty property + testWindow.changeProperty(XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 8, 0, nullptr); + prop = Property(false, testWindow, XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 0, 100000); + QCOMPARE(prop.toByteArray(), QByteArray()); + QCOMPARE(prop.toByteArray(&ok), QByteArray()); + //valid bytearray + QVERIFY(ok); + //The bytearray should be empty + QVERIFY(prop.toByteArray().isEmpty()); + //The bytearray should be not null + QVERIFY(!prop.toByteArray().isNull()); + QVERIFY(!prop.value()); + QCOMPARE(QByteArray(StringProperty(testWindow, XCB_ATOM_WM_NAME)), QByteArray()); + + // verify non existing property + Xcb::Atom invalid(QByteArrayLiteral("INVALID_ATOM")); + prop = Property(false, testWindow, invalid, XCB_ATOM_STRING, 0, 100000); + QCOMPARE(prop.toByteArray(), QByteArray()); + QCOMPARE(prop.toByteArray(&ok), QByteArray()); + //invalid bytearray + QVERIFY(!ok); + //The bytearray should be empty + QVERIFY(prop.toByteArray().isEmpty()); + //The bytearray should be not null + QVERIFY(prop.toByteArray().isNull()); + QVERIFY(!prop.value()); + QCOMPARE(QByteArray(StringProperty(testWindow, XCB_ATOM_WM_NAME)), QByteArray()); +} + +void TestXcbWrapper::testPropertyBool() +{ + Window testWindow(createWindow()); + Atom blockCompositing(QByteArrayLiteral("_KDE_NET_WM_BLOCK_COMPOSITING")); + QVERIFY(blockCompositing != XCB_ATOM_NONE); + NETWinInfo info(QX11Info::connection(), testWindow, QX11Info::appRootWindow(), NET::Properties(), NET::WM2BlockCompositing); + + Property prop(false, testWindow, blockCompositing, XCB_ATOM_CARDINAL, 0, 100000); + bool ok = true; + QVERIFY(!prop.toBool()); + QVERIFY(!prop.toBool(&ok)); + QVERIFY(!ok); + + info.setBlockingCompositing(true); + xcb_flush(QX11Info::connection()); + prop = Property(false, testWindow, blockCompositing, XCB_ATOM_CARDINAL, 0, 100000); + QVERIFY(prop.toBool()); + QVERIFY(prop.toBool(&ok)); + QVERIFY(ok); + + // incorrect type and format + QVERIFY(!prop.toBool(8)); + QVERIFY(!prop.toBool(32, blockCompositing)); + QVERIFY(!prop.toBool(32, blockCompositing, &ok)); + QVERIFY(!ok); + + // incorrect value: + uint32_t d[] = {1, 0}; + testWindow.changeProperty(blockCompositing, XCB_ATOM_CARDINAL, 32, 2, d); + prop = Property(false, testWindow, blockCompositing, XCB_ATOM_CARDINAL, 0, 100000); + QVERIFY(!prop.toBool()); + ok = true; + QVERIFY(!prop.toBool(&ok)); + QVERIFY(!ok); +} + +void TestXcbWrapper::testAtom() +{ + 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 Atom &atom2(atom); + QVERIFY(atom2.isValid()); + QVERIFY(atom2 == XCB_ATOM_WM_CLIENT_MACHINE); + QCOMPARE(atom2.name(), QByteArrayLiteral("WM_CLIENT_MACHINE")); + + //destroy before retrieved + Atom atom3(QByteArrayLiteral("WM_CLIENT_MACHINE")); + QCOMPARE(atom3.name(), QByteArrayLiteral("WM_CLIENT_MACHINE")); +} + +void TestXcbWrapper::testMotifEmpty() +{ + Atom atom(QByteArrayLiteral("_MOTIF_WM_HINTS")); + MotifHints hints(atom); + // pre init + QCOMPARE(hints.hasDecoration(), false); + QCOMPARE(hints.noBorder(), 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.hasDecoration(), false); + QCOMPARE(hints.noBorder(), 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.hasDecoration(), false); + QCOMPARE(hints.noBorder(), 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("expectedHasDecoration"); + QTest::addColumn("expectedNoBorder"); + 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() +{ + 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()); + MotifHints hints(atom); + hints.init(m_testWindow); + hints.read(); + QTEST(hints.hasDecoration(), "expectedHasDecoration"); + QTEST(hints.noBorder(), "expectedNoBorder"); + 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/autotests/test_xkb.cpp b/autotests/test_xkb.cpp new file mode 100644 index 0000000..ca60785 --- /dev/null +++ b/autotests/test_xkb.cpp @@ -0,0 +1,502 @@ +/* + 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 { + Qt::Key keySymQt; + xkb_keysym_t keySymX; + Qt::KeyboardModifiers modifiers; +}; + +static const TransKey g_rgQtToSymX[] = { + { Qt::Key_Escape, XKB_KEY_Escape, Qt::KeyboardModifiers() }, + { Qt::Key_Tab, XKB_KEY_Tab, Qt::KeyboardModifiers() }, + { Qt::Key_Backtab, XKB_KEY_ISO_Left_Tab, Qt::KeyboardModifiers() }, + { Qt::Key_Backspace, XKB_KEY_BackSpace, Qt::KeyboardModifiers() }, + { Qt::Key_Return, XKB_KEY_Return, Qt::KeyboardModifiers() }, + { Qt::Key_Insert, XKB_KEY_Insert, Qt::KeyboardModifiers() }, + { Qt::Key_Delete, XKB_KEY_Delete, Qt::KeyboardModifiers() }, + { Qt::Key_Pause, XKB_KEY_Pause, Qt::KeyboardModifiers() }, + { Qt::Key_Print, XKB_KEY_Print, Qt::KeyboardModifiers() }, + { Qt::Key_SysReq, XKB_KEY_Sys_Req, Qt::KeyboardModifiers() }, + { Qt::Key_Home, XKB_KEY_Home, Qt::KeyboardModifiers() }, + { Qt::Key_End, XKB_KEY_End, Qt::KeyboardModifiers() }, + { Qt::Key_Left, XKB_KEY_Left, Qt::KeyboardModifiers() }, + { Qt::Key_Up, XKB_KEY_Up, Qt::KeyboardModifiers() }, + { Qt::Key_Right, XKB_KEY_Right, Qt::KeyboardModifiers() }, + { Qt::Key_Down, XKB_KEY_Down, Qt::KeyboardModifiers() }, + { Qt::Key_PageUp, XKB_KEY_Prior, Qt::KeyboardModifiers() }, + { Qt::Key_PageDown, XKB_KEY_Next, Qt::KeyboardModifiers() }, + { Qt::Key_CapsLock, XKB_KEY_Caps_Lock, Qt::KeyboardModifiers() }, + { Qt::Key_NumLock, XKB_KEY_Num_Lock, Qt::KeyboardModifiers() }, + { Qt::Key_ScrollLock, XKB_KEY_Scroll_Lock, Qt::KeyboardModifiers() }, + { Qt::Key_F1, XKB_KEY_F1, Qt::KeyboardModifiers() }, + { Qt::Key_F2, XKB_KEY_F2, Qt::KeyboardModifiers() }, + { Qt::Key_F3, XKB_KEY_F3, Qt::KeyboardModifiers() }, + { Qt::Key_F4, XKB_KEY_F4, Qt::KeyboardModifiers() }, + { Qt::Key_F5, XKB_KEY_F5, Qt::KeyboardModifiers() }, + { Qt::Key_F6, XKB_KEY_F6, Qt::KeyboardModifiers() }, + { Qt::Key_F7, XKB_KEY_F7, Qt::KeyboardModifiers() }, + { Qt::Key_F8, XKB_KEY_F8, Qt::KeyboardModifiers() }, + { Qt::Key_F9, XKB_KEY_F9, Qt::KeyboardModifiers() }, + { Qt::Key_F10, XKB_KEY_F10, Qt::KeyboardModifiers() }, + { Qt::Key_F11, XKB_KEY_F11, Qt::KeyboardModifiers() }, + { Qt::Key_F12, XKB_KEY_F12, Qt::KeyboardModifiers() }, + { Qt::Key_F13, XKB_KEY_F13, Qt::KeyboardModifiers() }, + { Qt::Key_F14, XKB_KEY_F14, Qt::KeyboardModifiers() }, + { Qt::Key_F15, XKB_KEY_F15, Qt::KeyboardModifiers() }, + { Qt::Key_F16, XKB_KEY_F16, Qt::KeyboardModifiers() }, + { Qt::Key_F17, XKB_KEY_F17, Qt::KeyboardModifiers() }, + { Qt::Key_F18, XKB_KEY_F18, Qt::KeyboardModifiers() }, + { Qt::Key_F19, XKB_KEY_F19, Qt::KeyboardModifiers() }, + { Qt::Key_F20, XKB_KEY_F20, Qt::KeyboardModifiers() }, + { Qt::Key_F21, XKB_KEY_F21, Qt::KeyboardModifiers() }, + { Qt::Key_F22, XKB_KEY_F22, Qt::KeyboardModifiers() }, + { Qt::Key_F23, XKB_KEY_F23, Qt::KeyboardModifiers() }, + { Qt::Key_F24, XKB_KEY_F24, Qt::KeyboardModifiers() }, + { Qt::Key_F25, XKB_KEY_F25, Qt::KeyboardModifiers() }, + { Qt::Key_F26, XKB_KEY_F26, Qt::KeyboardModifiers() }, + { Qt::Key_F27, XKB_KEY_F27, Qt::KeyboardModifiers() }, + { Qt::Key_F28, XKB_KEY_F28, Qt::KeyboardModifiers() }, + { Qt::Key_F29, XKB_KEY_F29, Qt::KeyboardModifiers() }, + { Qt::Key_F30, XKB_KEY_F30, Qt::KeyboardModifiers() }, + { Qt::Key_F31, XKB_KEY_F31, Qt::KeyboardModifiers() }, + { Qt::Key_F32, XKB_KEY_F32, Qt::KeyboardModifiers() }, + { Qt::Key_F33, XKB_KEY_F33, Qt::KeyboardModifiers() }, + { Qt::Key_F34, XKB_KEY_F34, Qt::KeyboardModifiers() }, + { Qt::Key_F35, XKB_KEY_F35, Qt::KeyboardModifiers() }, + { Qt::Key_Super_L, XKB_KEY_Super_L, Qt::KeyboardModifiers() }, + { Qt::Key_Super_R, XKB_KEY_Super_R, Qt::KeyboardModifiers() }, + { Qt::Key_Menu, XKB_KEY_Menu, Qt::KeyboardModifiers() }, + { Qt::Key_Hyper_L, XKB_KEY_Hyper_L, Qt::KeyboardModifiers() }, + { Qt::Key_Hyper_R, XKB_KEY_Hyper_R, Qt::KeyboardModifiers() }, + { Qt::Key_Help, XKB_KEY_Help, Qt::KeyboardModifiers() }, + { Qt::Key_Space, XKB_KEY_KP_Space, Qt::KeypadModifier }, + { Qt::Key_Tab, XKB_KEY_KP_Tab, Qt::KeypadModifier }, + { Qt::Key_Enter, XKB_KEY_KP_Enter, Qt::KeypadModifier }, + { Qt::Key_Home, XKB_KEY_KP_Home, Qt::KeypadModifier }, + { Qt::Key_Left, XKB_KEY_KP_Left, Qt::KeypadModifier }, + { Qt::Key_Up, XKB_KEY_KP_Up, Qt::KeypadModifier }, + { Qt::Key_Right, XKB_KEY_KP_Right, Qt::KeypadModifier }, + { Qt::Key_Down, XKB_KEY_KP_Down, Qt::KeypadModifier }, + { Qt::Key_PageUp, XKB_KEY_KP_Prior, Qt::KeypadModifier }, + { Qt::Key_PageDown, XKB_KEY_KP_Next, Qt::KeypadModifier }, + { Qt::Key_End, XKB_KEY_KP_End, Qt::KeypadModifier }, + { Qt::Key_Clear, XKB_KEY_KP_Begin, Qt::KeypadModifier }, + { Qt::Key_Insert, XKB_KEY_KP_Insert, Qt::KeypadModifier }, + { Qt::Key_Delete, XKB_KEY_KP_Delete, Qt::KeypadModifier }, + { Qt::Key_Equal, XKB_KEY_KP_Equal, Qt::KeypadModifier }, + { Qt::Key_Asterisk, XKB_KEY_KP_Multiply, Qt::KeypadModifier }, + { Qt::Key_Plus, XKB_KEY_KP_Add, Qt::KeypadModifier }, + { Qt::Key_Comma, XKB_KEY_KP_Separator, Qt::KeypadModifier }, + { Qt::Key_Minus, XKB_KEY_KP_Subtract, Qt::KeypadModifier }, + { Qt::Key_Period, XKB_KEY_KP_Decimal, Qt::KeypadModifier }, + { Qt::Key_Slash, XKB_KEY_KP_Divide, Qt::KeypadModifier }, + { Qt::Key_Back, XKB_KEY_XF86Back, Qt::KeyboardModifiers() }, + { Qt::Key_Forward, XKB_KEY_XF86Forward, Qt::KeyboardModifiers() }, + { Qt::Key_Stop, XKB_KEY_XF86Stop, Qt::KeyboardModifiers() }, + { Qt::Key_Refresh, XKB_KEY_XF86Refresh, Qt::KeyboardModifiers() }, + { Qt::Key_Favorites, XKB_KEY_XF86Favorites, Qt::KeyboardModifiers() }, + { Qt::Key_LaunchMedia, XKB_KEY_XF86AudioMedia, Qt::KeyboardModifiers() }, + { Qt::Key_OpenUrl, XKB_KEY_XF86OpenURL, Qt::KeyboardModifiers() }, + { Qt::Key_HomePage, XKB_KEY_XF86HomePage, Qt::KeyboardModifiers() }, + { Qt::Key_Search, XKB_KEY_XF86Search, Qt::KeyboardModifiers() }, + { Qt::Key_VolumeDown, XKB_KEY_XF86AudioLowerVolume, Qt::KeyboardModifiers() }, + { Qt::Key_VolumeMute, XKB_KEY_XF86AudioMute, Qt::KeyboardModifiers() }, + { Qt::Key_VolumeUp, XKB_KEY_XF86AudioRaiseVolume, Qt::KeyboardModifiers() }, + { Qt::Key_MediaPlay, XKB_KEY_XF86AudioPlay, Qt::KeyboardModifiers() }, + { Qt::Key_MediaStop, XKB_KEY_XF86AudioStop, Qt::KeyboardModifiers() }, + { Qt::Key_MediaPrevious, XKB_KEY_XF86AudioPrev, Qt::KeyboardModifiers() }, + { Qt::Key_MediaNext, XKB_KEY_XF86AudioNext, Qt::KeyboardModifiers() }, + { Qt::Key_MediaRecord, XKB_KEY_XF86AudioRecord, Qt::KeyboardModifiers() }, + { Qt::Key_LaunchMail, XKB_KEY_XF86Mail, Qt::KeyboardModifiers() }, + { Qt::Key_Launch0, XKB_KEY_XF86MyComputer, Qt::KeyboardModifiers() }, + { Qt::Key_Launch1, XKB_KEY_XF86Calculator, Qt::KeyboardModifiers() }, + { Qt::Key_Memo, XKB_KEY_XF86Memo, Qt::KeyboardModifiers() }, + { Qt::Key_ToDoList, XKB_KEY_XF86ToDoList, Qt::KeyboardModifiers() }, + { Qt::Key_Calendar, XKB_KEY_XF86Calendar, Qt::KeyboardModifiers() }, + { Qt::Key_PowerDown, XKB_KEY_XF86PowerDown, Qt::KeyboardModifiers() }, + { Qt::Key_ContrastAdjust, XKB_KEY_XF86ContrastAdjust, Qt::KeyboardModifiers() }, + { Qt::Key_Standby, XKB_KEY_XF86Standby, Qt::KeyboardModifiers() }, + { Qt::Key_MonBrightnessUp, XKB_KEY_XF86MonBrightnessUp, Qt::KeyboardModifiers() }, + { Qt::Key_MonBrightnessDown, XKB_KEY_XF86MonBrightnessDown, Qt::KeyboardModifiers() }, + { Qt::Key_KeyboardLightOnOff, XKB_KEY_XF86KbdLightOnOff, Qt::KeyboardModifiers() }, + { Qt::Key_KeyboardBrightnessUp, XKB_KEY_XF86KbdBrightnessUp, Qt::KeyboardModifiers() }, + { Qt::Key_KeyboardBrightnessDown, XKB_KEY_XF86KbdBrightnessDown, Qt::KeyboardModifiers() }, + { Qt::Key_PowerOff, XKB_KEY_XF86PowerOff, Qt::KeyboardModifiers() }, + { Qt::Key_WakeUp, XKB_KEY_XF86WakeUp, Qt::KeyboardModifiers() }, + { Qt::Key_Eject, XKB_KEY_XF86Eject, Qt::KeyboardModifiers() }, + { Qt::Key_ScreenSaver, XKB_KEY_XF86ScreenSaver, Qt::KeyboardModifiers() }, + { Qt::Key_WWW, XKB_KEY_XF86WWW, Qt::KeyboardModifiers() }, + { Qt::Key_Sleep, XKB_KEY_XF86Sleep, Qt::KeyboardModifiers() }, + { Qt::Key_LightBulb, XKB_KEY_XF86LightBulb, Qt::KeyboardModifiers() }, + { Qt::Key_Shop, XKB_KEY_XF86Shop, Qt::KeyboardModifiers() }, + { Qt::Key_History, XKB_KEY_XF86History, Qt::KeyboardModifiers() }, + { Qt::Key_AddFavorite, XKB_KEY_XF86AddFavorite, Qt::KeyboardModifiers() }, + { Qt::Key_HotLinks, XKB_KEY_XF86HotLinks, Qt::KeyboardModifiers() }, + { Qt::Key_BrightnessAdjust, XKB_KEY_XF86BrightnessAdjust, Qt::KeyboardModifiers() }, + { Qt::Key_Finance, XKB_KEY_XF86Finance, Qt::KeyboardModifiers() }, + { Qt::Key_Community, XKB_KEY_XF86Community, Qt::KeyboardModifiers() }, + { Qt::Key_AudioRewind, XKB_KEY_XF86AudioRewind, Qt::KeyboardModifiers() }, + { Qt::Key_BackForward, XKB_KEY_XF86BackForward, Qt::KeyboardModifiers() }, + { Qt::Key_ApplicationLeft, XKB_KEY_XF86ApplicationLeft, Qt::KeyboardModifiers() }, + { Qt::Key_ApplicationRight, XKB_KEY_XF86ApplicationRight, Qt::KeyboardModifiers() }, + { Qt::Key_Book, XKB_KEY_XF86Book, Qt::KeyboardModifiers() }, + { Qt::Key_CD, XKB_KEY_XF86CD, Qt::KeyboardModifiers() }, + { Qt::Key_Calculator, XKB_KEY_XF86Calculater, Qt::KeyboardModifiers() }, + { Qt::Key_Clear, XKB_KEY_XF86Clear, Qt::KeyboardModifiers() }, + { Qt::Key_ClearGrab, XKB_KEY_XF86ClearGrab, Qt::KeyboardModifiers() }, + { Qt::Key_Close, XKB_KEY_XF86Close, Qt::KeyboardModifiers() }, + { Qt::Key_Copy, XKB_KEY_XF86Copy, Qt::KeyboardModifiers() }, + { Qt::Key_Cut, XKB_KEY_XF86Cut, Qt::KeyboardModifiers() }, + { Qt::Key_Display, XKB_KEY_XF86Display, Qt::KeyboardModifiers() }, + { Qt::Key_DOS, XKB_KEY_XF86DOS, Qt::KeyboardModifiers() }, + { Qt::Key_Documents, XKB_KEY_XF86Documents, Qt::KeyboardModifiers() }, + { Qt::Key_Excel, XKB_KEY_XF86Excel, Qt::KeyboardModifiers() }, + { Qt::Key_Explorer, XKB_KEY_XF86Explorer, Qt::KeyboardModifiers() }, + { Qt::Key_Game, XKB_KEY_XF86Game, Qt::KeyboardModifiers() }, + { Qt::Key_Go, XKB_KEY_XF86Go, Qt::KeyboardModifiers() }, + { Qt::Key_iTouch, XKB_KEY_XF86iTouch, Qt::KeyboardModifiers() }, + { Qt::Key_LogOff, XKB_KEY_XF86LogOff, Qt::KeyboardModifiers() }, + { Qt::Key_Market, XKB_KEY_XF86Market, Qt::KeyboardModifiers() }, + { Qt::Key_Meeting, XKB_KEY_XF86Meeting, Qt::KeyboardModifiers() }, + { Qt::Key_MenuKB, XKB_KEY_XF86MenuKB, Qt::KeyboardModifiers() }, + { Qt::Key_MenuPB, XKB_KEY_XF86MenuPB, Qt::KeyboardModifiers() }, + { Qt::Key_MySites, XKB_KEY_XF86MySites, Qt::KeyboardModifiers() }, + { Qt::Key_News, XKB_KEY_XF86News, Qt::KeyboardModifiers() }, + { Qt::Key_OfficeHome, XKB_KEY_XF86OfficeHome, Qt::KeyboardModifiers() }, + { Qt::Key_Option, XKB_KEY_XF86Option, Qt::KeyboardModifiers() }, + { Qt::Key_Paste, XKB_KEY_XF86Paste, Qt::KeyboardModifiers() }, + { Qt::Key_Phone, XKB_KEY_XF86Phone, Qt::KeyboardModifiers() }, + { Qt::Key_Reply, XKB_KEY_XF86Reply, Qt::KeyboardModifiers() }, + { Qt::Key_Reload, XKB_KEY_XF86Reload, Qt::KeyboardModifiers() }, + { Qt::Key_RotateWindows, XKB_KEY_XF86RotateWindows, Qt::KeyboardModifiers() }, + { Qt::Key_RotationPB, XKB_KEY_XF86RotationPB, Qt::KeyboardModifiers() }, + { Qt::Key_RotationKB, XKB_KEY_XF86RotationKB, Qt::KeyboardModifiers() }, + { Qt::Key_Save, XKB_KEY_XF86Save, Qt::KeyboardModifiers() }, + { Qt::Key_Send, XKB_KEY_XF86Send, Qt::KeyboardModifiers() }, + { Qt::Key_Spell, XKB_KEY_XF86Spell, Qt::KeyboardModifiers() }, + { Qt::Key_SplitScreen, XKB_KEY_XF86SplitScreen, Qt::KeyboardModifiers() }, + { Qt::Key_Support, XKB_KEY_XF86Support, Qt::KeyboardModifiers() }, + { Qt::Key_TaskPane, XKB_KEY_XF86TaskPane, Qt::KeyboardModifiers() }, + { Qt::Key_Terminal, XKB_KEY_XF86Terminal, Qt::KeyboardModifiers() }, + { Qt::Key_Tools, XKB_KEY_XF86Tools, Qt::KeyboardModifiers() }, + { Qt::Key_Travel, XKB_KEY_XF86Travel, Qt::KeyboardModifiers() }, + { Qt::Key_Video, XKB_KEY_XF86Video, Qt::KeyboardModifiers() }, + { Qt::Key_Word, XKB_KEY_XF86Word, Qt::KeyboardModifiers() }, + { Qt::Key_Xfer, XKB_KEY_XF86Xfer, Qt::KeyboardModifiers() }, + { Qt::Key_ZoomIn, XKB_KEY_XF86ZoomIn, Qt::KeyboardModifiers() }, + { Qt::Key_ZoomOut, XKB_KEY_XF86ZoomOut, Qt::KeyboardModifiers() }, + { Qt::Key_Away, XKB_KEY_XF86Away, Qt::KeyboardModifiers() }, + { Qt::Key_Messenger, XKB_KEY_XF86Messenger, Qt::KeyboardModifiers() }, + { Qt::Key_WebCam, XKB_KEY_XF86WebCam, Qt::KeyboardModifiers() }, + { Qt::Key_MailForward, XKB_KEY_XF86MailForward, Qt::KeyboardModifiers() }, + { Qt::Key_Pictures, XKB_KEY_XF86Pictures, Qt::KeyboardModifiers() }, + { Qt::Key_Music, XKB_KEY_XF86Music, Qt::KeyboardModifiers() }, + { Qt::Key_Battery, XKB_KEY_XF86Battery, Qt::KeyboardModifiers() }, + { Qt::Key_Bluetooth, XKB_KEY_XF86Bluetooth, Qt::KeyboardModifiers() }, + { Qt::Key_WLAN, XKB_KEY_XF86WLAN, Qt::KeyboardModifiers() }, + { Qt::Key_UWB, XKB_KEY_XF86UWB, Qt::KeyboardModifiers() }, + { Qt::Key_AudioForward, XKB_KEY_XF86AudioForward, Qt::KeyboardModifiers() }, + { Qt::Key_AudioRepeat, XKB_KEY_XF86AudioRepeat, Qt::KeyboardModifiers() }, + { Qt::Key_AudioRandomPlay, XKB_KEY_XF86AudioRandomPlay, Qt::KeyboardModifiers() }, + { Qt::Key_Subtitle, XKB_KEY_XF86Subtitle, Qt::KeyboardModifiers() }, + { Qt::Key_AudioCycleTrack, XKB_KEY_XF86AudioCycleTrack, Qt::KeyboardModifiers() }, + { Qt::Key_Time, XKB_KEY_XF86Time, Qt::KeyboardModifiers() }, + { Qt::Key_Select, XKB_KEY_XF86Select, Qt::KeyboardModifiers() }, + { Qt::Key_View, XKB_KEY_XF86View, Qt::KeyboardModifiers() }, + { Qt::Key_TopMenu, XKB_KEY_XF86TopMenu, Qt::KeyboardModifiers() }, + { Qt::Key_Bluetooth, XKB_KEY_XF86Bluetooth, Qt::KeyboardModifiers() }, + { Qt::Key_Suspend, XKB_KEY_XF86Suspend, Qt::KeyboardModifiers() }, + { Qt::Key_Hibernate, XKB_KEY_XF86Hibernate, Qt::KeyboardModifiers() }, + { Qt::Key_TouchpadToggle, XKB_KEY_XF86TouchpadToggle, Qt::KeyboardModifiers() }, + { Qt::Key_TouchpadOn, XKB_KEY_XF86TouchpadOn, Qt::KeyboardModifiers() }, + { Qt::Key_TouchpadOff, XKB_KEY_XF86TouchpadOff, Qt::KeyboardModifiers() }, + { Qt::Key_MicMute, XKB_KEY_XF86AudioMicMute, Qt::KeyboardModifiers() }, + { Qt::Key_Launch2, XKB_KEY_XF86Launch0, Qt::KeyboardModifiers() }, + { Qt::Key_Launch3, XKB_KEY_XF86Launch1, Qt::KeyboardModifiers() }, + { Qt::Key_Launch4, XKB_KEY_XF86Launch2, Qt::KeyboardModifiers() }, + { Qt::Key_Launch5, XKB_KEY_XF86Launch3, Qt::KeyboardModifiers() }, + { Qt::Key_Launch6, XKB_KEY_XF86Launch4, Qt::KeyboardModifiers() }, + { Qt::Key_Launch7, XKB_KEY_XF86Launch5, Qt::KeyboardModifiers() }, + { Qt::Key_Launch8, XKB_KEY_XF86Launch6, Qt::KeyboardModifiers() }, + { Qt::Key_Launch9, XKB_KEY_XF86Launch7, Qt::KeyboardModifiers() }, + { Qt::Key_LaunchA, XKB_KEY_XF86Launch8, Qt::KeyboardModifiers() }, + { Qt::Key_LaunchB, XKB_KEY_XF86Launch9, Qt::KeyboardModifiers() }, + { Qt::Key_LaunchC, XKB_KEY_XF86LaunchA, Qt::KeyboardModifiers() }, + { Qt::Key_LaunchD, XKB_KEY_XF86LaunchB, Qt::KeyboardModifiers() }, + { Qt::Key_LaunchE, XKB_KEY_XF86LaunchC, Qt::KeyboardModifiers() }, + { Qt::Key_LaunchF, XKB_KEY_XF86LaunchD, Qt::KeyboardModifiers() }, + +/* + * Latin 1 + * (ISO/IEC 8859-1 = Unicode U+0020..U+00FF) + * Byte 3 = 0 + */ + { Qt::Key_Exclam, XKB_KEY_exclam , Qt::KeyboardModifiers() }, + { Qt::Key_QuoteDbl, XKB_KEY_quotedbl , Qt::KeyboardModifiers() }, + { Qt::Key_NumberSign, XKB_KEY_numbersign , Qt::KeyboardModifiers() }, + { Qt::Key_Dollar, XKB_KEY_dollar , Qt::KeyboardModifiers() }, + { Qt::Key_Percent, XKB_KEY_percent , Qt::KeyboardModifiers() }, + { Qt::Key_Ampersand, XKB_KEY_ampersand , Qt::KeyboardModifiers() }, + { Qt::Key_Apostrophe, XKB_KEY_apostrophe , Qt::KeyboardModifiers() }, + { Qt::Key_ParenLeft, XKB_KEY_parenleft , Qt::KeyboardModifiers() }, + { Qt::Key_ParenRight, XKB_KEY_parenright , Qt::KeyboardModifiers() }, + { Qt::Key_Asterisk, XKB_KEY_asterisk , Qt::KeyboardModifiers() }, + { Qt::Key_Plus, XKB_KEY_plus , Qt::KeyboardModifiers() }, + { Qt::Key_Comma, XKB_KEY_comma , Qt::KeyboardModifiers() }, + { Qt::Key_Minus, XKB_KEY_minus , Qt::KeyboardModifiers() }, + { Qt::Key_Period, XKB_KEY_period , Qt::KeyboardModifiers() }, + { Qt::Key_Slash, XKB_KEY_slash , Qt::KeyboardModifiers() }, + { Qt::Key_0, XKB_KEY_0 , Qt::KeyboardModifiers() }, + { Qt::Key_1, XKB_KEY_1 , Qt::KeyboardModifiers() }, + { Qt::Key_2, XKB_KEY_2 , Qt::KeyboardModifiers() }, + { Qt::Key_3, XKB_KEY_3 , Qt::KeyboardModifiers() }, + { Qt::Key_4, XKB_KEY_4 , Qt::KeyboardModifiers() }, + { Qt::Key_5, XKB_KEY_5 , Qt::KeyboardModifiers() }, + { Qt::Key_6, XKB_KEY_6 , Qt::KeyboardModifiers() }, + { Qt::Key_7, XKB_KEY_7 , Qt::KeyboardModifiers() }, + { Qt::Key_8, XKB_KEY_8 , Qt::KeyboardModifiers() }, + { Qt::Key_9, XKB_KEY_9 , Qt::KeyboardModifiers() }, + { Qt::Key_Colon, XKB_KEY_colon , Qt::KeyboardModifiers() }, + { Qt::Key_Semicolon, XKB_KEY_semicolon , Qt::KeyboardModifiers() }, + { Qt::Key_Less, XKB_KEY_less , Qt::KeyboardModifiers() }, + { Qt::Key_Equal, XKB_KEY_equal , Qt::KeyboardModifiers() }, + { Qt::Key_Greater, XKB_KEY_greater , Qt::KeyboardModifiers() }, + { Qt::Key_Question, XKB_KEY_question , Qt::KeyboardModifiers() }, + { Qt::Key_At, XKB_KEY_at , Qt::KeyboardModifiers() }, + { Qt::Key_A, XKB_KEY_A , Qt::ShiftModifier }, + { Qt::Key_B, XKB_KEY_B , Qt::ShiftModifier }, + { Qt::Key_C, XKB_KEY_C , Qt::ShiftModifier }, + { Qt::Key_D, XKB_KEY_D , Qt::ShiftModifier }, + { Qt::Key_E, XKB_KEY_E , Qt::ShiftModifier }, + { Qt::Key_F, XKB_KEY_F , Qt::ShiftModifier }, + { Qt::Key_G, XKB_KEY_G , Qt::ShiftModifier }, + { Qt::Key_H, XKB_KEY_H , Qt::ShiftModifier }, + { Qt::Key_I, XKB_KEY_I , Qt::ShiftModifier }, + { Qt::Key_J, XKB_KEY_J , Qt::ShiftModifier }, + { Qt::Key_K, XKB_KEY_K , Qt::ShiftModifier }, + { Qt::Key_L, XKB_KEY_L , Qt::ShiftModifier }, + { Qt::Key_M, XKB_KEY_M , Qt::ShiftModifier }, + { Qt::Key_N, XKB_KEY_N , Qt::ShiftModifier }, + { Qt::Key_O, XKB_KEY_O , Qt::ShiftModifier }, + { Qt::Key_P, XKB_KEY_P , Qt::ShiftModifier }, + { Qt::Key_Q, XKB_KEY_Q , Qt::ShiftModifier }, + { Qt::Key_R, XKB_KEY_R , Qt::ShiftModifier }, + { Qt::Key_S, XKB_KEY_S , Qt::ShiftModifier }, + { Qt::Key_T, XKB_KEY_T , Qt::ShiftModifier }, + { Qt::Key_U, XKB_KEY_U , Qt::ShiftModifier }, + { Qt::Key_V, XKB_KEY_V , Qt::ShiftModifier }, + { Qt::Key_W, XKB_KEY_W , Qt::ShiftModifier }, + { Qt::Key_X, XKB_KEY_X , Qt::ShiftModifier }, + { Qt::Key_Y, XKB_KEY_Y , Qt::ShiftModifier }, + { Qt::Key_Z, XKB_KEY_Z , Qt::ShiftModifier }, + { Qt::Key_BracketLeft, XKB_KEY_bracketleft, Qt::KeyboardModifiers() }, + { Qt::Key_Backslash, XKB_KEY_backslash , Qt::KeyboardModifiers() }, + { Qt::Key_BracketRight, XKB_KEY_bracketright, Qt::KeyboardModifiers()}, + { Qt::Key_AsciiCircum, XKB_KEY_asciicircum, Qt::KeyboardModifiers() }, + { Qt::Key_Underscore, XKB_KEY_underscore , Qt::KeyboardModifiers() }, + { Qt::Key_QuoteLeft, XKB_KEY_quoteleft , Qt::KeyboardModifiers() }, + { Qt::Key_A, XKB_KEY_a , Qt::KeyboardModifiers() }, + { Qt::Key_B, XKB_KEY_b , Qt::KeyboardModifiers() }, + { Qt::Key_C, XKB_KEY_c , Qt::KeyboardModifiers() }, + { Qt::Key_D, XKB_KEY_d , Qt::KeyboardModifiers() }, + { Qt::Key_E, XKB_KEY_e , Qt::KeyboardModifiers() }, + { Qt::Key_F, XKB_KEY_f , Qt::KeyboardModifiers() }, + { Qt::Key_G, XKB_KEY_g , Qt::KeyboardModifiers() }, + { Qt::Key_H, XKB_KEY_h , Qt::KeyboardModifiers() }, + { Qt::Key_I, XKB_KEY_i , Qt::KeyboardModifiers() }, + { Qt::Key_J, XKB_KEY_j , Qt::KeyboardModifiers() }, + { Qt::Key_K, XKB_KEY_k , Qt::KeyboardModifiers() }, + { Qt::Key_L, XKB_KEY_l , Qt::KeyboardModifiers() }, + { Qt::Key_M, XKB_KEY_m , Qt::KeyboardModifiers() }, + { Qt::Key_N, XKB_KEY_n , Qt::KeyboardModifiers() }, + { Qt::Key_O, XKB_KEY_o , Qt::KeyboardModifiers() }, + { Qt::Key_P, XKB_KEY_p , Qt::KeyboardModifiers() }, + { Qt::Key_Q, XKB_KEY_q , Qt::KeyboardModifiers() }, + { Qt::Key_R, XKB_KEY_r , Qt::KeyboardModifiers() }, + { Qt::Key_S, XKB_KEY_s , Qt::KeyboardModifiers() }, + { Qt::Key_T, XKB_KEY_t , Qt::KeyboardModifiers() }, + { Qt::Key_U, XKB_KEY_u , Qt::KeyboardModifiers() }, + { Qt::Key_V, XKB_KEY_v , Qt::KeyboardModifiers() }, + { Qt::Key_W, XKB_KEY_w , Qt::KeyboardModifiers() }, + { Qt::Key_X, XKB_KEY_x , Qt::KeyboardModifiers() }, + { Qt::Key_Y, XKB_KEY_y , Qt::KeyboardModifiers() }, + { Qt::Key_Z, XKB_KEY_z , Qt::KeyboardModifiers() }, + { Qt::Key_BraceLeft, XKB_KEY_braceleft , Qt::KeyboardModifiers() }, + { Qt::Key_Bar, XKB_KEY_bar , Qt::KeyboardModifiers() }, + { Qt::Key_BraceRight, XKB_KEY_braceright , Qt::KeyboardModifiers() }, + { Qt::Key_AsciiTilde, XKB_KEY_asciitilde , Qt::KeyboardModifiers() }, + + { Qt::Key_nobreakspace, XKB_KEY_nobreakspace , Qt::KeyboardModifiers() }, + { Qt::Key_exclamdown, XKB_KEY_exclamdown , Qt::KeyboardModifiers() }, + { Qt::Key_cent, XKB_KEY_cent , Qt::KeyboardModifiers() }, + { Qt::Key_sterling, XKB_KEY_sterling , Qt::KeyboardModifiers() }, + { Qt::Key_currency, XKB_KEY_currency , Qt::KeyboardModifiers() }, + { Qt::Key_yen, XKB_KEY_yen , Qt::KeyboardModifiers() }, + { Qt::Key_brokenbar, XKB_KEY_brokenbar , Qt::KeyboardModifiers() }, + { Qt::Key_section, XKB_KEY_section , Qt::KeyboardModifiers() }, + { Qt::Key_diaeresis, XKB_KEY_diaeresis , Qt::KeyboardModifiers() }, + { Qt::Key_copyright, XKB_KEY_copyright , Qt::KeyboardModifiers() }, + { Qt::Key_ordfeminine, XKB_KEY_ordfeminine , Qt::KeyboardModifiers() }, + { Qt::Key_guillemotleft, XKB_KEY_guillemotleft , Qt::KeyboardModifiers() }, + { Qt::Key_notsign, XKB_KEY_notsign , Qt::KeyboardModifiers() }, + { Qt::Key_hyphen, XKB_KEY_hyphen , Qt::KeyboardModifiers() }, + { Qt::Key_registered, XKB_KEY_registered , Qt::KeyboardModifiers() }, + { Qt::Key_macron, XKB_KEY_macron , Qt::KeyboardModifiers() }, + { Qt::Key_degree, XKB_KEY_degree , Qt::KeyboardModifiers() }, + { Qt::Key_plusminus, XKB_KEY_plusminus , Qt::KeyboardModifiers() }, + { Qt::Key_twosuperior, XKB_KEY_twosuperior , Qt::KeyboardModifiers() }, + { Qt::Key_threesuperior, XKB_KEY_threesuperior , Qt::KeyboardModifiers() }, + { Qt::Key_acute, XKB_KEY_acute , Qt::KeyboardModifiers() }, + { Qt::Key_mu, XKB_KEY_mu , Qt::KeyboardModifiers() }, + { Qt::Key_paragraph, XKB_KEY_paragraph , Qt::KeyboardModifiers() }, + { Qt::Key_periodcentered, XKB_KEY_periodcentered, Qt::KeyboardModifiers() }, + { Qt::Key_cedilla, XKB_KEY_cedilla , Qt::KeyboardModifiers() }, + { Qt::Key_onesuperior, XKB_KEY_onesuperior , Qt::KeyboardModifiers() }, + { Qt::Key_masculine, XKB_KEY_masculine , Qt::KeyboardModifiers() }, + { Qt::Key_guillemotright, XKB_KEY_guillemotright, Qt::KeyboardModifiers() }, + { Qt::Key_onequarter, XKB_KEY_onequarter , Qt::KeyboardModifiers() }, + { Qt::Key_onehalf, XKB_KEY_onehalf , Qt::KeyboardModifiers() }, + { Qt::Key_threequarters, XKB_KEY_threequarters , Qt::KeyboardModifiers() }, + { Qt::Key_questiondown, XKB_KEY_questiondown , Qt::KeyboardModifiers() }, + { Qt::Key_Agrave, XKB_KEY_Agrave , Qt::ShiftModifier }, + { Qt::Key_Aacute, XKB_KEY_Aacute , Qt::ShiftModifier }, + { Qt::Key_Acircumflex, XKB_KEY_Acircumflex , Qt::ShiftModifier }, + { Qt::Key_Atilde, XKB_KEY_Atilde , Qt::ShiftModifier }, + { Qt::Key_Adiaeresis, XKB_KEY_Adiaeresis , Qt::ShiftModifier }, + { Qt::Key_Aring, XKB_KEY_Aring , Qt::ShiftModifier }, + { Qt::Key_AE, XKB_KEY_AE , Qt::ShiftModifier }, + { Qt::Key_Ccedilla, XKB_KEY_Ccedilla , Qt::ShiftModifier }, + { Qt::Key_Egrave, XKB_KEY_Egrave , Qt::ShiftModifier }, + { Qt::Key_Eacute, XKB_KEY_Eacute , Qt::ShiftModifier }, + { Qt::Key_Ecircumflex, XKB_KEY_Ecircumflex , Qt::ShiftModifier }, + { Qt::Key_Ediaeresis, XKB_KEY_Ediaeresis , Qt::ShiftModifier }, + { Qt::Key_Igrave, XKB_KEY_Igrave , Qt::ShiftModifier }, + { Qt::Key_Iacute, XKB_KEY_Iacute , Qt::ShiftModifier }, + { Qt::Key_Icircumflex, XKB_KEY_Icircumflex , Qt::ShiftModifier }, + { Qt::Key_Idiaeresis, XKB_KEY_Idiaeresis , Qt::ShiftModifier }, + { Qt::Key_ETH, XKB_KEY_ETH , Qt::ShiftModifier }, + { Qt::Key_Ntilde, XKB_KEY_Ntilde , Qt::ShiftModifier }, + { Qt::Key_Ograve, XKB_KEY_Ograve , Qt::ShiftModifier }, + { Qt::Key_Oacute, XKB_KEY_Oacute , Qt::ShiftModifier }, + { Qt::Key_Ocircumflex, XKB_KEY_Ocircumflex , Qt::ShiftModifier }, + { Qt::Key_Otilde, XKB_KEY_Otilde , Qt::ShiftModifier }, + { Qt::Key_Odiaeresis, XKB_KEY_Odiaeresis , Qt::ShiftModifier }, + { Qt::Key_multiply, XKB_KEY_multiply , Qt::ShiftModifier }, + { Qt::Key_Ooblique, XKB_KEY_Ooblique , Qt::ShiftModifier }, + { Qt::Key_Ugrave, XKB_KEY_Ugrave , Qt::ShiftModifier }, + { Qt::Key_Uacute, XKB_KEY_Uacute , Qt::ShiftModifier }, + { Qt::Key_Ucircumflex, XKB_KEY_Ucircumflex , Qt::ShiftModifier }, + { Qt::Key_Udiaeresis, XKB_KEY_Udiaeresis , Qt::ShiftModifier }, + { Qt::Key_Yacute, XKB_KEY_Yacute , Qt::ShiftModifier }, + { Qt::Key_THORN, XKB_KEY_THORN , Qt::ShiftModifier }, + { Qt::Key_ssharp, XKB_KEY_ssharp , Qt::KeyboardModifiers() }, + { Qt::Key_Agrave, XKB_KEY_agrave , Qt::KeyboardModifiers() }, + { Qt::Key_Aacute, XKB_KEY_aacute , Qt::KeyboardModifiers() }, + { Qt::Key_Acircumflex, XKB_KEY_acircumflex , Qt::KeyboardModifiers() }, + { Qt::Key_Atilde, XKB_KEY_atilde , Qt::KeyboardModifiers() }, + { Qt::Key_Adiaeresis, XKB_KEY_adiaeresis , Qt::KeyboardModifiers() }, + { Qt::Key_Aring, XKB_KEY_aring , Qt::KeyboardModifiers() }, + { Qt::Key_AE, XKB_KEY_ae , Qt::KeyboardModifiers() }, + { Qt::Key_Ccedilla, XKB_KEY_ccedilla , Qt::KeyboardModifiers() }, + { Qt::Key_Egrave, XKB_KEY_egrave , Qt::KeyboardModifiers() }, + { Qt::Key_Eacute, XKB_KEY_eacute , Qt::KeyboardModifiers() }, + { Qt::Key_Ecircumflex, XKB_KEY_ecircumflex , Qt::KeyboardModifiers() }, + { Qt::Key_Ediaeresis, XKB_KEY_ediaeresis , Qt::KeyboardModifiers() }, + { Qt::Key_Igrave, XKB_KEY_igrave , Qt::KeyboardModifiers() }, + { Qt::Key_Iacute, XKB_KEY_iacute , Qt::KeyboardModifiers() }, + { Qt::Key_Icircumflex, XKB_KEY_icircumflex , Qt::KeyboardModifiers() }, + { Qt::Key_Idiaeresis, XKB_KEY_idiaeresis , Qt::KeyboardModifiers() }, + { Qt::Key_ETH, XKB_KEY_eth , Qt::KeyboardModifiers() }, + { Qt::Key_Ntilde, XKB_KEY_ntilde , Qt::KeyboardModifiers() }, + { Qt::Key_Ograve, XKB_KEY_ograve , Qt::KeyboardModifiers() }, + { Qt::Key_Oacute, XKB_KEY_oacute , Qt::KeyboardModifiers() }, + { Qt::Key_Ocircumflex, XKB_KEY_ocircumflex , Qt::KeyboardModifiers() }, + { Qt::Key_Otilde, XKB_KEY_otilde , Qt::KeyboardModifiers() }, + { Qt::Key_Odiaeresis, XKB_KEY_odiaeresis , Qt::KeyboardModifiers() }, + { Qt::Key_division, XKB_KEY_division , Qt::KeyboardModifiers() }, + { Qt::Key_Ooblique, XKB_KEY_ooblique , Qt::KeyboardModifiers() }, + { Qt::Key_Ugrave, XKB_KEY_ugrave , Qt::KeyboardModifiers() }, + { Qt::Key_Uacute, XKB_KEY_uacute , Qt::KeyboardModifiers() }, + { Qt::Key_Ucircumflex, XKB_KEY_ucircumflex , Qt::KeyboardModifiers() }, + { Qt::Key_Udiaeresis, XKB_KEY_udiaeresis , Qt::KeyboardModifiers() }, + { Qt::Key_Yacute, XKB_KEY_yacute , Qt::KeyboardModifiers() }, + { Qt::Key_THORN, XKB_KEY_thorn , Qt::KeyboardModifiers() }, + { Qt::Key_ydiaeresis, XKB_KEY_ydiaeresis, Qt::KeyboardModifiers() }, + /* + * Numpad + */ + { Qt::Key_0, XKB_KEY_KP_0, Qt::KeypadModifier }, + { Qt::Key_1, XKB_KEY_KP_1, Qt::KeypadModifier }, + { Qt::Key_2, XKB_KEY_KP_2, Qt::KeypadModifier }, + { Qt::Key_3, XKB_KEY_KP_3, Qt::KeypadModifier }, + { Qt::Key_4, XKB_KEY_KP_4, Qt::KeypadModifier }, + { Qt::Key_5, XKB_KEY_KP_5, Qt::KeypadModifier }, + { Qt::Key_6, XKB_KEY_KP_6, Qt::KeypadModifier }, + { Qt::Key_7, XKB_KEY_KP_7, Qt::KeypadModifier }, + { Qt::Key_8, XKB_KEY_KP_8, Qt::KeypadModifier }, + { Qt::Key_9, XKB_KEY_KP_9, Qt::KeypadModifier } +}; + +void XkbTest::testToQtKey_data() +{ + QTest::addColumn("qt"); + QTest::addColumn("keySym"); + for (std::size_t i = 0; i < sizeof(g_rgQtToSymX) / sizeof(TransKey); i++) { + const QByteArray row = QByteArray::number(g_rgQtToSymX[i].keySymX, 16); + QTest::newRow(row.constData()) << g_rgQtToSymX[i].keySymQt << g_rgQtToSymX[i].keySymX; + } +} + +void XkbTest::testToQtKey() +{ + Xkb xkb; + QFETCH(xkb_keysym_t, keySym); + QTEST(xkb.toQtKey(keySym), "qt"); +} + +void XkbTest::testFromQtKey_data() +{ + QTest::addColumn("qt"); + QTest::addColumn("keySym"); + QTest::addColumn("modifiers"); + for (std::size_t i = 0; i < sizeof(g_rgQtToSymX) / sizeof(TransKey); i++) { + const QByteArray row = QByteArray::number(g_rgQtToSymX[i].keySymX, 16); + QTest::newRow(row.constData()) << g_rgQtToSymX[i].keySymQt << g_rgQtToSymX[i].keySymX << g_rgQtToSymX[i].modifiers; + } +} + +void XkbTest::testFromQtKey() +{ + Xkb xkb; + QFETCH(Qt::Key, qt); + QFETCH(Qt::KeyboardModifiers, modifiers); + QTEST(xkb.fromQtKey(qt, modifiers), "keySym"); +} + +QTEST_MAIN(XkbTest) +#include "test_xkb.moc" diff --git a/autotests/testutils.h b/autotests/testutils.h new file mode 100644 index 0000000..3924e22 --- /dev/null +++ b/autotests/testutils.h @@ -0,0 +1,52 @@ +/* + 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 +// XCB +#include + +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/autotests/workspace.h b/autotests/workspace.h new file mode 100644 index 0000000..0ab1a6f --- /dev/null +++ b/autotests/workspace.h @@ -0,0 +1 @@ +#include "mock_workspace.h" diff --git a/autotests/x11client.h b/autotests/x11client.h new file mode 100644 index 0000000..0e5bef1 --- /dev/null +++ b/autotests/x11client.h @@ -0,0 +1 @@ +#include "mock_x11client.h" diff --git a/client_machine.cpp b/client_machine.cpp new file mode 100644 index 0000000..7bff56a --- /dev/null +++ b/client_machine.cpp @@ -0,0 +1,231 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +// own +#include "client_machine.h" +#include "utils.h" +// KF5 +#include +// Qt +#include +#include +// system +#include +#include +#include +#include + +namespace KWin { + +static QByteArray getHostName() +{ +#ifdef HOST_NAME_MAX + char hostnamebuf[HOST_NAME_MAX]; +#else + char hostnamebuf[256]; +#endif + if (gethostname(hostnamebuf, sizeof hostnamebuf) >= 0) { + hostnamebuf[sizeof(hostnamebuf)-1] = 0; + return QByteArray(hostnamebuf); + } + return QByteArray(); +} + +GetAddrInfo::GetAddrInfo(const QByteArray &hostName, QObject *parent) + : QObject(parent) + , m_resolving(false) + , m_resolved(false) + , m_ownResolved(false) + , m_hostName(hostName) + , m_addressHints(new addrinfo) + , m_address(nullptr) + , m_ownAddress(nullptr) + , m_watcher(new QFutureWatcher(this)) + , m_ownAddressWatcher(new QFutureWatcher(this)) +{ + // watcher will be deleted together with the GetAddrInfo once the future + // got canceled or finished + connect(m_watcher, SIGNAL(canceled()), SLOT(deleteLater())); + connect(m_watcher, SIGNAL(finished()), SLOT(slotResolved())); + connect(m_ownAddressWatcher, SIGNAL(canceled()), SLOT(deleteLater())); + connect(m_ownAddressWatcher, SIGNAL(finished()), SLOT(slotOwnAddressResolved())); +} + +GetAddrInfo::~GetAddrInfo() +{ + if (m_watcher && m_watcher->isRunning()) { + m_watcher->cancel(); + m_watcher->waitForFinished(); + } + if (m_ownAddressWatcher && m_ownAddressWatcher->isRunning()) { + m_ownAddressWatcher->cancel(); + m_ownAddressWatcher->waitForFinished(); + } + if (m_address) { + freeaddrinfo(m_address); + } + if (m_ownAddress) { + freeaddrinfo(m_ownAddress); + } + delete m_addressHints; +} + +void GetAddrInfo::resolve() +{ + if (m_resolving) { + return; + } + m_resolving = true; + memset(m_addressHints, 0, sizeof(*m_addressHints)); + m_addressHints->ai_family = PF_UNSPEC; + m_addressHints->ai_socktype = SOCK_STREAM; + m_addressHints->ai_flags |= AI_CANONNAME; + + m_watcher->setFuture(QtConcurrent::run(getaddrinfo, m_hostName.constData(), nullptr, m_addressHints, &m_address)); + m_ownAddressWatcher->setFuture(QtConcurrent::run([this] { + // needs to be performed in a lambda as getHostName() returns a temporary value which would + // get destroyed in the main thread before the getaddrinfo thread is able to read it + return getaddrinfo(getHostName().constData(), nullptr, m_addressHints, &m_ownAddress); + })); +} + +void GetAddrInfo::slotResolved() +{ + if (resolved(m_watcher)) { + m_resolved = true; + compare(); + } +} + +void GetAddrInfo::slotOwnAddressResolved() +{ + if (resolved(m_ownAddressWatcher)) { + m_ownResolved = true; + compare(); + } +} + +bool GetAddrInfo::resolved(QFutureWatcher< int >* watcher) +{ + if (!watcher->isFinished()) { + return false; + } + if (watcher->result() != 0) { + qCDebug(KWIN_CORE) << "getaddrinfo failed with error:" << gai_strerror(watcher->result()); + // call failed; + deleteLater(); + return false; + } + return true; +} + +void GetAddrInfo::compare() +{ + if (!m_resolved || !m_ownResolved) { + return; + } + addrinfo *address = m_address; + while (address) { + if (address->ai_canonname && m_hostName == QByteArray(address->ai_canonname).toLower()) { + addrinfo *ownAddress = m_ownAddress; + bool localFound = false; + while (ownAddress) { + if (ownAddress->ai_canonname && QByteArray(ownAddress->ai_canonname).toLower() == m_hostName) { + localFound = true; + break; + } + ownAddress = ownAddress->ai_next; + } + if (localFound) { + emit local(); + break; + } + } + address = address->ai_next; + } + deleteLater(); +} + + +ClientMachine::ClientMachine(QObject *parent) + : QObject(parent) + , m_localhost(false) + , m_resolved(false) + , m_resolving(false) +{ +} + +ClientMachine::~ClientMachine() +{ +} + +void ClientMachine::resolve(xcb_window_t window, xcb_window_t clientLeader) +{ + if (m_resolved) { + return; + } + QByteArray name = NETWinInfo(connection(), window, rootWindow(), NET::Properties(), NET::WM2ClientMachine).clientMachine(); + if (name.isEmpty() && clientLeader && clientLeader != window) { + name = NETWinInfo(connection(), clientLeader, rootWindow(), NET::Properties(), NET::WM2ClientMachine).clientMachine(); + } + if (name.isEmpty()) { + name = localhost(); + } + if (name == localhost()) { + setLocal(); + } + m_hostName = name; + checkForLocalhost(); + m_resolved = true; +} + +void ClientMachine::checkForLocalhost() +{ + if (isLocal()) { + // nothing to do + return; + } + QByteArray host = getHostName(); + + if (!host.isEmpty()) { + host = host.toLower(); + const QByteArray lowerHostName(m_hostName.toLower()); + if (host == lowerHostName) { + setLocal(); + return; + } + if (char *dot = strchr(host.data(), '.')) { + *dot = '\0'; + if (host == lowerHostName) { + setLocal(); + return; + } + } else { + m_resolving = true; + // check using information from get addr info + // GetAddrInfo gets automatically destroyed once it finished or not + GetAddrInfo *info = new GetAddrInfo(lowerHostName, this); + connect(info, SIGNAL(local()), SLOT(setLocal())); + connect(info, SIGNAL(destroyed(QObject*)), SLOT(resolveFinished())); + info->resolve(); + } + } +} + +void ClientMachine::setLocal() +{ + m_localhost = true; + emit localhostChanged(); +} + +void ClientMachine::resolveFinished() +{ + m_resolving = false; +} + +} // namespace diff --git a/client_machine.h b/client_machine.h new file mode 100644 index 0000000..c2c191d --- /dev/null +++ b/client_machine.h @@ -0,0 +1,106 @@ +/* + 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 KWIN_CLIENT_MACHINE_H +#define KWIN_CLIENT_MACHINE_H + +#include +#include + +// forward declaration +struct addrinfo; +template +class QFutureWatcher; + +namespace KWin { + +class GetAddrInfo : public QObject +{ + Q_OBJECT +public: + explicit GetAddrInfo(const QByteArray &hostName, QObject *parent = nullptr); + ~GetAddrInfo() override; + + void resolve(); + +Q_SIGNALS: + void local(); + +private Q_SLOTS: + void slotResolved(); + void slotOwnAddressResolved(); + +private: + void compare(); + bool resolved(QFutureWatcher *watcher); + bool m_resolving; + bool m_resolved; + bool m_ownResolved; + QByteArray m_hostName; + addrinfo *m_addressHints; + addrinfo *m_address; + addrinfo *m_ownAddress; + QFutureWatcher *m_watcher; + QFutureWatcher *m_ownAddressWatcher; +}; + +class ClientMachine : public QObject +{ + Q_OBJECT +public: + explicit ClientMachine(QObject *parent = nullptr); + ~ClientMachine() override; + + void resolve(xcb_window_t window, xcb_window_t clientLeader); + const QByteArray &hostName() const; + bool isLocal() const; + static QByteArray localhost(); + bool isResolving() const; + +Q_SIGNALS: + void localhostChanged(); + +private Q_SLOTS: + void setLocal(); + void resolveFinished(); + +private: + void checkForLocalhost(); + QByteArray m_hostName; + bool m_localhost; + bool m_resolved; + bool m_resolving; +}; + +inline +bool ClientMachine::isLocal() const +{ + return m_localhost; +} + +inline +const QByteArray &ClientMachine::hostName() const +{ + return m_hostName; +} + +inline +QByteArray ClientMachine::localhost() +{ + return "localhost"; +} + +inline +bool ClientMachine::isResolving() const +{ + return m_resolving; +} + +} // namespace + +#endif diff --git a/cmake/modules/COPYING-CMAKE-SCRIPTS b/cmake/modules/COPYING-CMAKE-SCRIPTS new file mode 100644 index 0000000..4b41776 --- /dev/null +++ b/cmake/modules/COPYING-CMAKE-SCRIPTS @@ -0,0 +1,22 @@ +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 copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. The name of the author may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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/cmake/modules/FindFontconfig.cmake b/cmake/modules/FindFontconfig.cmake new file mode 100644 index 0000000..4388488 --- /dev/null +++ b/cmake/modules/FindFontconfig.cmake @@ -0,0 +1,89 @@ +#.rst: +# FindFontconfig +# -------------- +# +# Try to find Fontconfig. +# Once done this will define the following variables: +# +# ``Fontconfig_FOUND`` +# True if Fontconfig is available +# ``Fontconfig_INCLUDE_DIRS`` +# The include directory to use for the Fontconfig headers +# ``Fontconfig_LIBRARIES`` +# The Fontconfig libraries for linking +# ``Fontconfig_DEFINITIONS`` +# Compiler switches required for using Fontconfig +# ``Fontconfig_VERSION`` +# The version of Fontconfig that has been found +# +# If ``Fontconfig_FOUND`` is True, it will also define the following +# imported target: +# +# ``Fontconfig::Fontconfig`` + +#============================================================================= +# SPDX-FileCopyrightText: 2006, 2007 Laurent Montel +# SPDX-FileCopyrightText: 2018 Volker Krause +# +# SPDX-License-Identifier: BSD-3-Clause +#============================================================================= + +# 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(PC_FONTCONFIG QUIET fontconfig) + +set(Fontconfig_DEFINITIONS ${PC_FONTCONFIG_CFLAGS_OTHER}) + +find_path(Fontconfig_INCLUDE_DIRS fontconfig/fontconfig.h + PATHS + ${PC_FONTCONFIG_INCLUDE_DIRS} + /usr/X11/include +) + +find_library(Fontconfig_LIBRARIES NAMES fontconfig + PATHS + ${PC_FONTCONFIG_LIBRARY_DIRS} +) + +set(Fontconfig_VERSION ${PC_FONTCONFIG_VERSION}) +if (NOT Fontconfig_VERSION) + find_file(Fontconfig_VERSION_HEADER + NAMES "fontconfig/fontconfig.h" + HINTS ${Fontconfig_INCLUDE_DIRS} + ) + mark_as_advanced(Fontconfig_VERSION_HEADER) + if (Fontconfig_VERSION_HEADER) + file(READ ${Fontconfig_VERSION_HEADER} _fontconfig_version_header_content) + string(REGEX MATCH "#define FC_MAJOR[ \t]+[0-9]+" Fontconfig_MAJOR_VERSION_MATCH ${_fontconfig_version_header_content}) + string(REGEX MATCH "#define FC_MINOR[ \t]+[0-9]+" Fontconfig_MINOR_VERSION_MATCH ${_fontconfig_version_header_content}) + string(REGEX MATCH "#define FC_REVISION[ \t]+[0-9]+" Fontconfig_PATCH_VERSION_MATCH ${_fontconfig_version_header_content}) + string(REGEX REPLACE ".*FC_MAJOR[ \t]+(.*)" "\\1" Fontconfig_MAJOR_VERSION ${Fontconfig_MAJOR_VERSION_MATCH}) + string(REGEX REPLACE ".*FC_MINOR[ \t]+(.*)" "\\1" Fontconfig_MINOR_VERSION ${Fontconfig_MINOR_VERSION_MATCH}) + string(REGEX REPLACE ".*FC_REVISION[ \t]+(.*)" "\\1" Fontconfig_PATCH_VERSION ${Fontconfig_PATCH_VERSION_MATCH}) + set(Fontconfig_VERSION "${Fontconfig_MAJOR_VERSION}.${Fontconfig_MINOR_VERSION}.${Fontconfig_PATCH_VERSION}") + endif() +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Fontconfig + FOUND_VAR Fontconfig_FOUND + REQUIRED_VARS Fontconfig_LIBRARIES Fontconfig_INCLUDE_DIRS + VERSION_VAR Fontconfig_VERSION +) +mark_as_advanced(Fontconfig_LIBRARIES Fontconfig_INCLUDE_DIRS) + +if(Fontconfig_FOUND AND NOT TARGET Fontconfig::Fontconfig) + add_library(Fontconfig::Fontconfig UNKNOWN IMPORTED) + set_target_properties(Fontconfig::Fontconfig PROPERTIES + IMPORTED_LOCATION "${Fontconfig_LIBRARIES}" + INTERFACE_INCLUDE_DIRECTORIES "${Fontconfig_INCLUDE_DIRS}" + INTERFACE_COMPILER_DEFINITIONS "${Fontconfig_DEFINITIONS}" + ) +endif() + +include(FeatureSummary) +set_package_properties(Fontconfig PROPERTIES + URL "https://www.fontconfig.org/" + DESCRIPTION "Fontconfig is a library for configuring and customizing font access" +) diff --git a/cmake/modules/FindLibcap.cmake b/cmake/modules/FindLibcap.cmake new file mode 100644 index 0000000..f0efa3e --- /dev/null +++ b/cmake/modules/FindLibcap.cmake @@ -0,0 +1,38 @@ +# Try to find the setcap binary and cap libraries +# +# This will define: +# +# Libcap_FOUND - system has the cap library and setcap binary +# Libcap_LIBRARIES - cap libraries to link against +# SETCAP_EXECUTABLE - path of the setcap binary +# In addition, the following targets are defined: +# +# Libcap::SetCapabilities +# + + +# SPDX-FileCopyrightText: 2014 Hrvoje Senjan +# +# SPDX-License-Identifier: BSD-3-Clause + +find_program(SETCAP_EXECUTABLE NAMES setcap DOC "The setcap executable") + +find_library(Libcap_LIBRARIES NAMES cap DOC "The cap (capabilities) library") + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Libcap FOUND_VAR Libcap_FOUND + REQUIRED_VARS SETCAP_EXECUTABLE Libcap_LIBRARIES) + +if(Libcap_FOUND AND NOT TARGET Libcap::SetCapabilities) + add_executable(Libcap::SetCapabilities IMPORTED) + set_target_properties(Libcap::SetCapabilities PROPERTIES + IMPORTED_LOCATION "${SETCAP_EXECUTABLE}" + ) +endif() + +mark_as_advanced(SETCAP_EXECUTABLE Libcap_LIBRARIES) + +include(FeatureSummary) +set_package_properties(Libcap PROPERTIES + URL https://sites.google.com/site/fullycapable/ + DESCRIPTION "Capabilities are a measure to limit the omnipotence of the superuser.") diff --git a/cmake/modules/FindLibdrm.cmake b/cmake/modules/FindLibdrm.cmake new file mode 100644 index 0000000..9e9d7a6 --- /dev/null +++ b/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/cmake/modules/FindLibinput.cmake b/cmake/modules/FindLibinput.cmake new file mode 100644 index 0000000..237b790 --- /dev/null +++ b/cmake/modules/FindLibinput.cmake @@ -0,0 +1,104 @@ +#.rst: +# FindLibinput +# ------- +# +# Try to find libinput on a Unix system. +# +# This will define the following variables: +# +# ``Libinput_FOUND`` +# True if (the requested version of) libinput is available +# ``Libinput_VERSION`` +# The version of libinput +# ``Libinput_LIBRARIES`` +# This can be passed to target_link_libraries() instead of the ``Libinput::Libinput`` +# target +# ``Libinput_INCLUDE_DIRS`` +# This should be passed to target_include_directories() if the target is not +# used for linking +# ``Libinput_DEFINITIONS`` +# This should be passed to target_compile_options() if the target is not +# used for linking +# +# If ``Libinput_FOUND`` is TRUE, it will also define the following imported target: +# +# ``Libinput::Libinput`` +# The libinput 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 FindLibinput.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 FindLibinput.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_Libinput QUIET libinput) + + set(Libinput_DEFINITIONS ${PKG_Libinput_CFLAGS_OTHER}) + set(Libinput_VERSION ${PKG_Libinput_VERSION}) + + find_path(Libinput_INCLUDE_DIR + NAMES + libinput.h + HINTS + ${PKG_Libinput_INCLUDE_DIRS} + ) + find_library(Libinput_LIBRARY + NAMES + input + HINTS + ${PKG_Libinput_LIBRARY_DIRS} + ) + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(Libinput + FOUND_VAR + Libinput_FOUND + REQUIRED_VARS + Libinput_LIBRARY + Libinput_INCLUDE_DIR + VERSION_VAR + Libinput_VERSION + ) + + if(Libinput_FOUND AND NOT TARGET Libinput::Libinput) + add_library(Libinput::Libinput UNKNOWN IMPORTED) + set_target_properties(Libinput::Libinput PROPERTIES + IMPORTED_LOCATION "${Libinput_LIBRARY}" + INTERFACE_COMPILE_OPTIONS "${Libinput_DEFINITIONS}" + INTERFACE_INCLUDE_DIRECTORIES "${Libinput_INCLUDE_DIR}" + ) + endif() + + mark_as_advanced(Libinput_LIBRARY Libinput_INCLUDE_DIR) + + # compatibility variables + set(Libinput_LIBRARIES ${Libinput_LIBRARY}) + set(Libinput_INCLUDE_DIRS ${Libinput_INCLUDE_DIR}) + set(Libinput_VERSION_STRING ${Libinput_VERSION}) + +else() + message(STATUS "FindLibinput.cmake cannot find libinput on Windows systems.") + set(Libinput_FOUND FALSE) +endif() + +include(FeatureSummary) +set_package_properties(Libinput PROPERTIES + URL "https://www.freedesktop.org/wiki/Software/libinput/" + DESCRIPTION "Library to handle input devices in Wayland compositors and to provide a generic X.Org input driver." +) diff --git a/cmake/modules/FindUDev.cmake b/cmake/modules/FindUDev.cmake new file mode 100644 index 0000000..d74e05d --- /dev/null +++ b/cmake/modules/FindUDev.cmake @@ -0,0 +1,28 @@ +# - Try to find the UDev library +# Once done this will define +# +# UDEV_FOUND - system has UDev +# UDEV_INCLUDE_DIR - the libudev include directory +# UDEV_LIBS - The libudev libraries + +# SPDX-FileCopyrightText: 2010 Rafael Fernández López +# +# SPDX-License-Identifier: BSD-3-Clause + +find_path(UDEV_INCLUDE_DIR libudev.h) +find_library(UDEV_LIBS udev) + +if(UDEV_INCLUDE_DIR AND UDEV_LIBS) + include(CheckFunctionExists) + include(CMakePushCheckState) + cmake_push_check_state() + set(CMAKE_REQUIRED_LIBRARIES ${UDEV_LIBS} ) + + cmake_pop_check_state() + +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(UDev DEFAULT_MSG UDEV_INCLUDE_DIR UDEV_LIBS) + +mark_as_advanced(UDEV_INCLUDE_DIR UDEV_LIBS) diff --git a/cmake/modules/FindXKB.cmake b/cmake/modules/FindXKB.cmake new file mode 100644 index 0000000..cd9b9a9 --- /dev/null +++ b/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/cmake/modules/FindXwayland.cmake b/cmake/modules/FindXwayland.cmake new file mode 100644 index 0000000..9dd49ba --- /dev/null +++ b/cmake/modules/FindXwayland.cmake @@ -0,0 +1,13 @@ +#============================================================================= +# SPDX-FileCopyrightText: 2016 Martin Gräßlin +# +# SPDX-License-Identifier: BSD-3-Clause +#============================================================================= +find_program(Xwayland_EXECUTABLE NAMES Xwayland) +find_package_handle_standard_args(Xwayland + FOUND_VAR + Xwayland_FOUND + REQUIRED_VARS + Xwayland_EXECUTABLE +) +mark_as_advanced(Xwayland_EXECUTABLE) diff --git a/cmake/modules/Findepoll.cmake b/cmake/modules/Findepoll.cmake new file mode 100644 index 0000000..80933ce --- /dev/null +++ b/cmake/modules/Findepoll.cmake @@ -0,0 +1,59 @@ +#.rest: +# FindEpoll +# -------------- +# +# Try to find epoll or epoll-shim on this system. This finds: +# - some shim on Unix like systems (FreeBSD), or +# - the kernel's epoll on Linux systems. +# +# This will define the following variables: +# +# ``epoll_FOUND`` +# True if epoll is available +# ``epoll_LIBRARIES`` +# This has to be passed to target_link_libraries() +# ``epoll_INCLUDE_DIRS`` +# This has to be passed to target_include_directories() +# +# On Linux, the libraries and include directories are empty, +# even though epoll_FOUND may be set to TRUE. This is because +# no special includes or libraries are needed. On other systems +# these may be needed to use epoll. + +#============================================================================= +# SPDX-FileCopyrightText: 2019 Tobias C. Berner +# +# SPDX-License-Identifier: BSD-2-Clause +#============================================================================= + +find_path(epoll_INCLUDE_DIRS sys/epoll.h PATH_SUFFIXES libepoll-shim) + +if(epoll_INCLUDE_DIRS) + # On Linux there is no library to link against, on the BSDs there is. + # On the BSD's, epoll is implemented through a library, libepoll-shim. + if( CMAKE_SYSTEM_NAME MATCHES "Linux") + set(epoll_FOUND TRUE) + set(epoll_LIBRARIES "") + set(epoll_INCLUDE_DIRS "") + else() + find_library(epoll_LIBRARIES NAMES epoll-shim) + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(epoll + FOUND_VAR + epoll_FOUND + REQUIRED_VARS + epoll_LIBRARIES + epoll_INCLUDE_DIRS + ) + mark_as_advanced(epoll_LIBRARIES epoll_INCLUDE_DIRS) + include(FeatureSummary) + set_package_properties(epoll PROPERTIES + URL "https://github.com/FreeBSDDesktop/epoll-shim" + DESCRIPTION "small epoll implementation using kqueue" + ) + endif() +else() + set(epoll_FOUND FALSE) +endif() + +mark_as_advanced(epoll_LIBRARIES epoll_INCLUDE_DIRS) diff --git a/cmake/modules/Findepoxy.cmake b/cmake/modules/Findepoxy.cmake new file mode 100644 index 0000000..d137f33 --- /dev/null +++ b/cmake/modules/Findepoxy.cmake @@ -0,0 +1,34 @@ +# - Try to find libepoxy +# Once done this will define +# +# epoxy_FOUND - System has libepoxy +# epoxy_LIBRARY - The libepoxy library +# epoxy_INCLUDE_DIR - The libepoxy include dir +# epoxy_DEFINITIONS - Compiler switches required for using libepoxy +# epoxy_HAS_GLX - Whether GLX support is available + +# SPDX-FileCopyrightText: 2014 Fredrik Höglund +# +# SPDX-License-Identifier: BSD-3-Clause + +if (NOT WIN32) + find_package(PkgConfig) + pkg_check_modules(PKG_epoxy QUIET epoxy) + + set(epoxy_DEFINITIONS ${PKG_epoxy_CFLAGS}) + + find_path(epoxy_INCLUDE_DIR NAMES epoxy/gl.h HINTS ${PKG_epoxy_INCLUDEDIR} ${PKG_epoxy_INCLUDE_DIRS}) + find_library(epoxy_LIBRARY NAMES epoxy HINTS ${PKG_epoxy_LIBDIR} ${PKG_epoxy_LIBRARY_DIRS}) + find_file(epoxy_GLX_HEADER NAMES epoxy/glx.h HINTS ${epoxy_INCLUDE_DIR}) + + if (epoxy_GLX_HEADER STREQUAL "epoxy_GLX_HEADER-NOTFOUND") + set(epoxy_HAS_GLX FALSE CACHE BOOL "whether glx is available") + else () + set(epoxy_HAS_GLX TRUE CACHE BOOL "whether glx is available") + endif() + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(epoxy DEFAULT_MSG epoxy_LIBRARY epoxy_INCLUDE_DIR) + + mark_as_advanced(epoxy_INCLUDE_DIR epoxy_LIBRARY epoxy_HAS_GLX) +endif() diff --git a/cmake/modules/Findgbm.cmake b/cmake/modules/Findgbm.cmake new file mode 100644 index 0000000..12ef45f --- /dev/null +++ b/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/cmake/modules/Findhwdata.cmake b/cmake/modules/Findhwdata.cmake new file mode 100644 index 0000000..3525173 --- /dev/null +++ b/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/cmake/modules/Findlibhybris.cmake b/cmake/modules/Findlibhybris.cmake new file mode 100644 index 0000000..fcfcbc1 --- /dev/null +++ b/cmake/modules/Findlibhybris.cmake @@ -0,0 +1,164 @@ +#.rst: +# Findlibhybris +# ------- +# +# Try to find libhybris on a Unix system. + +#============================================================================= +# SPDX-FileCopyrightText: 2015 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 Findlibhybris.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 Findlibhybris.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_libhardware QUIET libhardware) + pkg_check_modules(PKG_androidheaders QUIET android-headers) + pkg_check_modules(PKG_hwcomposerwindow QUIET hwcomposer-egl) + pkg_check_modules(PKG_hybriseglplatform QUIET hybris-egl-platform) + + set(libhardware_DEFINITIONS ${PKG_libhardware_CFLAGS_OTHER}) + set(libhardware_VERSION ${PKG_libhardware_VERSION}) + + find_library(libhardware_LIBRARY + NAMES + libhardware.so + HINTS + ${PKG_libhardware_LIBRARY_DIRS} + ) + find_path(libhardware_INCLUDE_DIR + NAMES + android-version.h + HINTS + ${PKG_androidheaders_INCLUDE_DIRS} + ) + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(libhardware + FOUND_VAR + libhardware_FOUND + REQUIRED_VARS + libhardware_LIBRARY + libhardware_INCLUDE_DIR + VERSION_VAR + libhardware_VERSION + ) + + if(libhardware_FOUND AND NOT TARGET libhybris::libhardware) + add_library(libhybris::libhardware UNKNOWN IMPORTED) + set_target_properties(libhybris::libhardware PROPERTIES + IMPORTED_LOCATION "${libhardware_LIBRARY}" + INTERFACE_COMPILE_OPTIONS "${libhardware_DEFINITIONS}" + INTERFACE_INCLUDE_DIRECTORIES "${libhardware_INCLUDE_DIR}" + ) + endif() + + mark_as_advanced(libhardware_LIBRARY libhardware_INCLUDE_DIR) + + ############################################## + # hwcomposerWindow + ############################################## + set(libhwcomposer_DEFINITIONS ${PKG_hwcomposerwindow_CFLAGS_OTHER}) + set(libhwcomposer_VERSION ${PKG_hwcomposerwindow_VERSION}) + + find_library(libhwcomposer_LIBRARY + NAMES + libhybris-hwcomposerwindow.so + HINTS + ${PKG_hwcomposerwindow_LIBRARY_DIRS} + ) + find_path(libhwcomposer_INCLUDE_DIR + NAMES + hwcomposer_window.h + HINTS + ${PKG_hwcomposerwindow_INCLUDE_DIRS} + ) + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(libhwcomposer + FOUND_VAR + libhwcomposer_FOUND + REQUIRED_VARS + libhwcomposer_LIBRARY + libhwcomposer_INCLUDE_DIR + VERSION_VAR + libhwcomposer_VERSION + ) + + if(libhwcomposer_FOUND AND NOT TARGET libhybris::hwcomposer) + add_library(libhybris::hwcomposer UNKNOWN IMPORTED) + set_target_properties(libhybris::hwcomposer PROPERTIES + IMPORTED_LOCATION "${libhwcomposer_LIBRARY}" + INTERFACE_COMPILE_OPTIONS "${libhardware_DEFINITIONS}" + INTERFACE_INCLUDE_DIRECTORIES "${libhwcomposer_INCLUDE_DIR}" + ) + endif() + + mark_as_advanced(libhwcomposer_LIBRARY libhwcomposer_INCLUDE_DIR) + + ############################################## + # hybriseglplatform + ############################################## + set(hybriseglplatform_DEFINITIONS ${PKG_hybriseglplatform_CFLAGS_OTHER}) + set(hybriseglplatform_VERSION ${PKG_hybriseglplatform_VERSION}) + + find_library(hybriseglplatform_LIBRARY + NAMES + libhybris-eglplatformcommon.so + HINTS + ${PKG_hybriseglplatform_LIBRARY_DIRS} + ) + find_path(hybriseglplatform_INCLUDE_DIR + NAMES + eglplatformcommon.h + HINTS + ${PKG_hybriseglplatform_INCLUDE_DIRS} + ) + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(hybriseglplatform + FOUND_VAR + hybriseglplatform_FOUND + REQUIRED_VARS + hybriseglplatform_LIBRARY + hybriseglplatform_INCLUDE_DIR + VERSION_VAR + hybriseglplatform_VERSION + ) + + if(hybriseglplatform_FOUND AND NOT TARGET libhybris::hybriseglplatform) + add_library(libhybris::hybriseglplatform UNKNOWN IMPORTED) + set_target_properties(libhybris::hybriseglplatform PROPERTIES + IMPORTED_LOCATION "${hybriseglplatform_LIBRARY}" + INTERFACE_COMPILE_OPTIONS "${hybriseglplatform_DEFINITIONS}" + INTERFACE_INCLUDE_DIRECTORIES "${hybriseglplatform_INCLUDE_DIR}" + ) + endif() + + mark_as_advanced(hybriseglplatform_LIBRARY hybriseglplatform_INCLUDE_DIR) + + if(libhardware_FOUND AND libhwcomposer_FOUND AND hybriseglplatform_FOUND) + set(libhybris_FOUND TRUE) + else() + set(libhybris_FOUND FALSE) + endif() + +else() + message(STATUS "Findlibhardware.cmake cannot find libhybris on Windows systems.") + set(libhybris_FOUND FALSE) +endif() + +include(FeatureSummary) +set_package_properties(libhybris PROPERTIES + URL "https://github.com/libhybris/libhybris" + DESCRIPTION "libhybris allows to run bionic-based HW adaptations in glibc systems." +) diff --git a/colorcorrection/clockskewnotifier.cpp b/colorcorrection/clockskewnotifier.cpp new file mode 100644 index 0000000..b79ee8c --- /dev/null +++ b/colorcorrection/clockskewnotifier.cpp @@ -0,0 +1,78 @@ +/* + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "clockskewnotifier.h" +#include "clockskewnotifierengine_p.h" + +namespace KWin +{ + +class ClockSkewNotifier::Private +{ +public: + void loadNotifierEngine(); + void unloadNotifierEngine(); + + ClockSkewNotifier *notifier = nullptr; + ClockSkewNotifierEngine *engine = nullptr; + bool isActive = false; +}; + +void ClockSkewNotifier::Private::loadNotifierEngine() +{ + engine = ClockSkewNotifierEngine::create(notifier); + + if (engine) { + QObject::connect(engine, &ClockSkewNotifierEngine::clockSkewed, notifier, &ClockSkewNotifier::clockSkewed); + } +} + +void ClockSkewNotifier::Private::unloadNotifierEngine() +{ + if (!engine) { + return; + } + + QObject::disconnect(engine, &ClockSkewNotifierEngine::clockSkewed, notifier, &ClockSkewNotifier::clockSkewed); + engine->deleteLater(); + + engine = nullptr; +} + +ClockSkewNotifier::ClockSkewNotifier(QObject *parent) + : QObject(parent) + , d(new Private) +{ + d->notifier = this; +} + +ClockSkewNotifier::~ClockSkewNotifier() +{ +} + +bool ClockSkewNotifier::isActive() const +{ + return d->isActive; +} + +void ClockSkewNotifier::setActive(bool set) +{ + if (d->isActive == set) { + return; + } + + d->isActive = set; + + if (d->isActive) { + d->loadNotifierEngine(); + } else { + d->unloadNotifierEngine(); + } + + emit activeChanged(); +} + +} // namespace KWin diff --git a/colorcorrection/clockskewnotifier.h b/colorcorrection/clockskewnotifier.h new file mode 100644 index 0000000..6a894da --- /dev/null +++ b/colorcorrection/clockskewnotifier.h @@ -0,0 +1,62 @@ +/* + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +namespace KWin +{ + +/** + * The ClockSkewNotifier class provides a way for monitoring system clock changes. + * + * The ClockSkewNotifier class makes it possible to detect discontinuous changes to + * the system clock. Such changes are usually initiated by the user adjusting values + * in the Date and Time KCM or calls made to functions like settimeofday(). + */ +class ClockSkewNotifier : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool active READ isActive WRITE setActive NOTIFY activeChanged) + +public: + explicit ClockSkewNotifier(QObject *parent = nullptr); + ~ClockSkewNotifier() override; + + /** + * Returns @c true if the notifier is active; otherwise returns @c false. + */ + bool isActive() const; + + /** + * Sets the active status of the clock skew notifier to @p active. + * + * clockSkewed() signal won't be emitted while the notifier is inactive. + * + * The notifier is inactive by default. + * + * @see activeChanged + */ + void setActive(bool active); + +signals: + /** + * This signal is emitted whenever the active property is changed. + */ + void activeChanged(); + + /** + * This signal is emitted whenever the system clock is changed. + */ + void clockSkewed(); + +private: + class Private; + QScopedPointer d; +}; + +} // namespace KWin diff --git a/colorcorrection/clockskewnotifierengine.cpp b/colorcorrection/clockskewnotifierengine.cpp new file mode 100644 index 0000000..6df89ba --- /dev/null +++ b/colorcorrection/clockskewnotifierengine.cpp @@ -0,0 +1,29 @@ +/* + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "clockskewnotifierengine_p.h" +#if defined(Q_OS_LINUX) +#include "clockskewnotifierengine_linux.h" +#endif + +namespace KWin +{ + +ClockSkewNotifierEngine *ClockSkewNotifierEngine::create(QObject *parent) +{ +#if defined(Q_OS_LINUX) + return LinuxClockSkewNotifierEngine::create(parent); +#else + return nullptr; +#endif +} + +ClockSkewNotifierEngine::ClockSkewNotifierEngine(QObject *parent) + : QObject(parent) +{ +} + +} // namespace KWin diff --git a/colorcorrection/clockskewnotifierengine_linux.cpp b/colorcorrection/clockskewnotifierengine_linux.cpp new file mode 100644 index 0000000..8d11b4c --- /dev/null +++ b/colorcorrection/clockskewnotifierengine_linux.cpp @@ -0,0 +1,62 @@ +/* + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "clockskewnotifierengine_linux.h" + +#include + +#include +#include +#include + +#ifndef TFD_TIMER_CANCEL_ON_SET // only available in newer glib +#define TFD_TIMER_CANCEL_ON_SET (1 << 1) +#endif + +namespace KWin +{ + +LinuxClockSkewNotifierEngine *LinuxClockSkewNotifierEngine::create(QObject *parent) +{ + const int fd = timerfd_create(CLOCK_REALTIME, O_CLOEXEC | O_NONBLOCK); + if (fd == -1) { + qWarning("Couldn't create clock skew notifier engine: %s", strerror(errno)); + return nullptr; + } + + const itimerspec spec = {}; + const int ret = timerfd_settime(fd, TFD_TIMER_ABSTIME | TFD_TIMER_CANCEL_ON_SET, &spec, nullptr); + if (ret == -1) { + qWarning("Couldn't create clock skew notifier engine: %s", strerror(errno)); + close(fd); + return nullptr; + } + + return new LinuxClockSkewNotifierEngine(fd, parent); +} + +LinuxClockSkewNotifierEngine::LinuxClockSkewNotifierEngine(int fd, QObject *parent) + : ClockSkewNotifierEngine(parent) + , m_fd(fd) +{ + const QSocketNotifier *notifier = new QSocketNotifier(fd, QSocketNotifier::Read, this); + connect(notifier, &QSocketNotifier::activated, this, &LinuxClockSkewNotifierEngine::handleTimerCancelled); +} + +LinuxClockSkewNotifierEngine::~LinuxClockSkewNotifierEngine() +{ + close(m_fd); +} + +void LinuxClockSkewNotifierEngine::handleTimerCancelled() +{ + uint64_t expirationCount; + read(m_fd, &expirationCount, sizeof(expirationCount)); + + emit clockSkewed(); +} + +} // namespace KWin diff --git a/colorcorrection/clockskewnotifierengine_linux.h b/colorcorrection/clockskewnotifierengine_linux.h new file mode 100644 index 0000000..a535cb1 --- /dev/null +++ b/colorcorrection/clockskewnotifierengine_linux.h @@ -0,0 +1,32 @@ +/* + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "clockskewnotifierengine_p.h" + +namespace KWin +{ + +class LinuxClockSkewNotifierEngine : public ClockSkewNotifierEngine +{ + Q_OBJECT + +public: + ~LinuxClockSkewNotifierEngine() override; + + static LinuxClockSkewNotifierEngine *create(QObject *parent); + +private Q_SLOTS: + void handleTimerCancelled(); + +private: + LinuxClockSkewNotifierEngine(int fd, QObject *parent); + + int m_fd; +}; + +} // namespace KWin diff --git a/colorcorrection/clockskewnotifierengine_p.h b/colorcorrection/clockskewnotifierengine_p.h new file mode 100644 index 0000000..fd9e139 --- /dev/null +++ b/colorcorrection/clockskewnotifierengine_p.h @@ -0,0 +1,28 @@ +/* + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +namespace KWin +{ + +class ClockSkewNotifierEngine : public QObject +{ + Q_OBJECT + +public: + static ClockSkewNotifierEngine *create(QObject *parent); + +protected: + explicit ClockSkewNotifierEngine(QObject *parent); + +Q_SIGNALS: + void clockSkewed(); +}; + +} // namespace KWin diff --git a/colorcorrection/colorcorrect_settings.kcfg b/colorcorrection/colorcorrect_settings.kcfg new file mode 100644 index 0000000..375caf7 --- /dev/null +++ b/colorcorrection/colorcorrect_settings.kcfg @@ -0,0 +1,57 @@ + + + + + + true + + + false + + + true + + + + + + + + + NightColorMode::Automatic + + + true + + + 4500 + + + 0. + + + 0. + + + true + + + 0. + + + 0. + + + "0600" + + + "1800" + + + 30 + + + diff --git a/colorcorrection/colorcorrect_settings.kcfgc b/colorcorrection/colorcorrect_settings.kcfgc new file mode 100644 index 0000000..4c178d6 --- /dev/null +++ b/colorcorrection/colorcorrect_settings.kcfgc @@ -0,0 +1,8 @@ +File=colorcorrect_settings.kcfg +NameSpace=KWin::ColorCorrect +ClassName=Settings +Singleton=true +Mutators=true +# manager.h is needed for NightColorMode +IncludeFiles=colorcorrection/manager.h +UseEnumTypes=true diff --git a/colorcorrection/colorcorrectdbusinterface.cpp b/colorcorrection/colorcorrectdbusinterface.cpp new file mode 100644 index 0000000..f1d80d9 --- /dev/null +++ b/colorcorrection/colorcorrectdbusinterface.cpp @@ -0,0 +1,312 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "colorcorrectdbusinterface.h" +#include "colorcorrectadaptor.h" + +#include "manager.h" + +#include + +namespace KWin { +namespace ColorCorrect { + +ColorCorrectDBusInterface::ColorCorrectDBusInterface(Manager *parent) + : QObject(parent) + , m_manager(parent) + , m_inhibitorWatcher(new QDBusServiceWatcher(this)) +{ + m_inhibitorWatcher->setConnection(QDBusConnection::sessionBus()); + m_inhibitorWatcher->setWatchMode(QDBusServiceWatcher::WatchForUnregistration); + connect(m_inhibitorWatcher, &QDBusServiceWatcher::serviceUnregistered, + this, &ColorCorrectDBusInterface::removeInhibitorService); + + connect(m_manager, &Manager::inhibitedChanged, this, [this] { + QVariantMap changedProperties; + changedProperties.insert(QStringLiteral("inhibited"), m_manager->isInhibited()); + + QDBusMessage message = QDBusMessage::createSignal( + QStringLiteral("/ColorCorrect"), + QStringLiteral("org.freedesktop.DBus.Properties"), + QStringLiteral("PropertiesChanged") + ); + + message.setArguments({ + QStringLiteral("org.kde.kwin.ColorCorrect"), + changedProperties, + QStringList(), // invalidated_properties + }); + + QDBusConnection::sessionBus().send(message); + }); + + connect(m_manager, &Manager::enabledChanged, this, [this] { + QVariantMap changedProperties; + changedProperties.insert(QStringLiteral("enabled"), m_manager->isEnabled()); + + QDBusMessage message = QDBusMessage::createSignal( + QStringLiteral("/ColorCorrect"), + QStringLiteral("org.freedesktop.DBus.Properties"), + QStringLiteral("PropertiesChanged") + ); + + message.setArguments({ + QStringLiteral("org.kde.kwin.ColorCorrect"), + changedProperties, + QStringList(), // invalidated_properties + }); + + QDBusConnection::sessionBus().send(message); + }); + + connect(m_manager, &Manager::runningChanged, this, [this] { + QVariantMap changedProperties; + changedProperties.insert(QStringLiteral("running"), m_manager->isRunning()); + + QDBusMessage message = QDBusMessage::createSignal( + QStringLiteral("/ColorCorrect"), + QStringLiteral("org.freedesktop.DBus.Properties"), + QStringLiteral("PropertiesChanged") + ); + + message.setArguments({ + QStringLiteral("org.kde.kwin.ColorCorrect"), + changedProperties, + QStringList(), // invalidated_properties + }); + + QDBusConnection::sessionBus().send(message); + }); + + connect(m_manager, &Manager::currentTemperatureChanged, this, [this] { + QVariantMap changedProperties; + changedProperties.insert(QStringLiteral("currentTemperature"), m_manager->currentTemperature()); + + QDBusMessage message = QDBusMessage::createSignal( + QStringLiteral("/ColorCorrect"), + QStringLiteral("org.freedesktop.DBus.Properties"), + QStringLiteral("PropertiesChanged") + ); + + message.setArguments({ + QStringLiteral("org.kde.kwin.ColorCorrect"), + changedProperties, + QStringList(), // invalidated_properties + }); + + QDBusConnection::sessionBus().send(message); + }); + + connect(m_manager, &Manager::targetTemperatureChanged, this, [this] { + QVariantMap changedProperties; + changedProperties.insert(QStringLiteral("targetTemperature"), m_manager->targetTemperature()); + + QDBusMessage message = QDBusMessage::createSignal( + QStringLiteral("/ColorCorrect"), + QStringLiteral("org.freedesktop.DBus.Properties"), + QStringLiteral("PropertiesChanged") + ); + + message.setArguments({ + QStringLiteral("org.kde.kwin.ColorCorrect"), + changedProperties, + QStringList(), // invalidated_properties + }); + + QDBusConnection::sessionBus().send(message); + }); + + connect(m_manager, &Manager::modeChanged, this, [this] { + QVariantMap changedProperties; + changedProperties.insert(QStringLiteral("mode"), uint(m_manager->mode())); + + QDBusMessage message = QDBusMessage::createSignal( + QStringLiteral("/ColorCorrect"), + QStringLiteral("org.freedesktop.DBus.Properties"), + QStringLiteral("PropertiesChanged") + ); + + message.setArguments({ + QStringLiteral("org.kde.kwin.ColorCorrect"), + changedProperties, + QStringList(), // invalidated_properties + }); + + QDBusConnection::sessionBus().send(message); + }); + + connect(m_manager, &Manager::previousTransitionTimingsChanged, this, [this] { + QVariantMap changedProperties; + changedProperties.insert(QStringLiteral("previousTransitionDateTime"), previousTransitionDateTime()); + changedProperties.insert(QStringLiteral("previousTransitionDuration"), previousTransitionDuration()); + + QDBusMessage message = QDBusMessage::createSignal( + QStringLiteral("/ColorCorrect"), + QStringLiteral("org.freedesktop.DBus.Properties"), + QStringLiteral("PropertiesChanged") + ); + + message.setArguments({ + QStringLiteral("org.kde.kwin.ColorCorrect"), + changedProperties, + QStringList(), // invalidated_properties + }); + + QDBusConnection::sessionBus().send(message); + }); + + connect(m_manager, &Manager::scheduledTransitionTimingsChanged, this, [this] { + QVariantMap changedProperties; + changedProperties.insert(QStringLiteral("scheduledTransitionDateTime"), scheduledTransitionDateTime()); + changedProperties.insert(QStringLiteral("scheduledTransitionDuration"), scheduledTransitionDuration()); + + QDBusMessage message = QDBusMessage::createSignal( + QStringLiteral("/ColorCorrect"), + QStringLiteral("org.freedesktop.DBus.Properties"), + QStringLiteral("PropertiesChanged") + ); + + message.setArguments({ + QStringLiteral("org.kde.kwin.ColorCorrect"), + changedProperties, + QStringList(), // invalidated_properties + }); + + QDBusConnection::sessionBus().send(message); + }); + + connect(m_manager, &Manager::configChange, this, &ColorCorrectDBusInterface::nightColorConfigChanged); + new ColorCorrectAdaptor(this); + QDBusConnection::sessionBus().registerObject(QStringLiteral("/ColorCorrect"), this); +} + +bool ColorCorrectDBusInterface::isInhibited() const +{ + return m_manager->isInhibited(); +} + +bool ColorCorrectDBusInterface::isEnabled() const +{ + return m_manager->isEnabled(); +} + +bool ColorCorrectDBusInterface::isRunning() const +{ + return m_manager->isRunning(); +} + +bool ColorCorrectDBusInterface::isAvailable() const +{ + return m_manager->isAvailable(); +} + +int ColorCorrectDBusInterface::currentTemperature() const +{ + return m_manager->currentTemperature(); +} + +int ColorCorrectDBusInterface::targetTemperature() const +{ + return m_manager->targetTemperature(); +} + +int ColorCorrectDBusInterface::mode() const +{ + return m_manager->mode(); +} + +quint64 ColorCorrectDBusInterface::previousTransitionDateTime() const +{ + const QDateTime dateTime = m_manager->previousTransitionDateTime(); + if (dateTime.isValid()) { + return quint64(dateTime.toSecsSinceEpoch()); + } + return 0; +} + +quint32 ColorCorrectDBusInterface::previousTransitionDuration() const +{ + return quint32(m_manager->previousTransitionDuration()); +} + +quint64 ColorCorrectDBusInterface::scheduledTransitionDateTime() const +{ + const QDateTime dateTime = m_manager->scheduledTransitionDateTime(); + if (dateTime.isValid()) { + return quint64(dateTime.toSecsSinceEpoch()); + } + return 0; +} + +quint32 ColorCorrectDBusInterface::scheduledTransitionDuration() const +{ + return quint32(m_manager->scheduledTransitionDuration()); +} + +QHash ColorCorrectDBusInterface::nightColorInfo() +{ + return m_manager->info(); +} + +bool ColorCorrectDBusInterface::setNightColorConfig(QHash data) +{ + return m_manager->changeConfiguration(data); +} + +void ColorCorrectDBusInterface::nightColorAutoLocationUpdate(double latitude, double longitude) +{ + m_manager->autoLocationUpdate(latitude, longitude); +} + +uint ColorCorrectDBusInterface::inhibit() +{ + const QString serviceName = QDBusContext::message().service(); + + if (!m_inhibitors.contains(serviceName)) { + m_inhibitorWatcher->addWatchedService(serviceName); + } + + m_inhibitors.insert(serviceName, ++m_lastInhibitionCookie); + + m_manager->inhibit(); + + return m_lastInhibitionCookie; +} + +void ColorCorrectDBusInterface::uninhibit(uint cookie) +{ + const QString serviceName = QDBusContext::message().service(); + + uninhibit(serviceName, cookie); +} + +void ColorCorrectDBusInterface::uninhibit(const QString &serviceName, uint cookie) +{ + const int removedCount = m_inhibitors.remove(serviceName, cookie); + if (!removedCount) { + return; + } + + if (!m_inhibitors.contains(serviceName)) { + m_inhibitorWatcher->removeWatchedService(serviceName); + } + + m_manager->uninhibit(); +} + +void ColorCorrectDBusInterface::removeInhibitorService(const QString &serviceName) +{ + const auto cookies = m_inhibitors.values(serviceName); + for (const uint &cookie : cookies) { + uninhibit(serviceName, cookie); + } +} + +} +} diff --git a/colorcorrection/colorcorrectdbusinterface.h b/colorcorrection/colorcorrectdbusinterface.h new file mode 100644 index 0000000..b10ef79 --- /dev/null +++ b/colorcorrection/colorcorrectdbusinterface.h @@ -0,0 +1,156 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_NIGHTCOLOR_DBUS_INTERFACE_H +#define KWIN_NIGHTCOLOR_DBUS_INTERFACE_H + +#include +#include + +namespace KWin +{ + +namespace ColorCorrect +{ + +class Manager; + +class ColorCorrectDBusInterface : public QObject, public QDBusContext +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.kwin.ColorCorrect") + Q_PROPERTY(bool inhibited READ isInhibited) + Q_PROPERTY(bool enabled READ isEnabled) + Q_PROPERTY(bool running READ isRunning) + Q_PROPERTY(bool available READ isAvailable) + Q_PROPERTY(int currentTemperature READ currentTemperature) + Q_PROPERTY(int targetTemperature READ targetTemperature) + Q_PROPERTY(int mode READ mode) + Q_PROPERTY(quint64 previousTransitionDateTime READ previousTransitionDateTime) + Q_PROPERTY(quint32 previousTransitionDuration READ previousTransitionDuration) + Q_PROPERTY(quint64 scheduledTransitionDateTime READ scheduledTransitionDateTime) + Q_PROPERTY(quint32 scheduledTransitionDuration READ scheduledTransitionDuration) + +public: + explicit ColorCorrectDBusInterface(Manager *parent); + ~ColorCorrectDBusInterface() override = default; + + bool isInhibited() const; + bool isEnabled() const; + bool isRunning() const; + bool isAvailable() const; + int currentTemperature() const; + int targetTemperature() const; + int mode() const; + quint64 previousTransitionDateTime() const; + quint32 previousTransitionDuration() const; + quint64 scheduledTransitionDateTime() const; + quint32 scheduledTransitionDuration() const; + +public Q_SLOTS: + /** + * @brief Gives information about the current state of Night Color. + * + * The returned variant hash has always the fields: + * - ActiveEnabled + * - Active + * - Mode + * - NightTemperatureEnabled + * - NightTemperature + * - Running + * - CurrentColorTemperature + * - LatitudeAuto + * - LongitudeAuto + * - LocationEnabled + * - LatitudeFixed + * - LongitudeFixed + * - TimingsEnabled + * - MorningBeginFixed + * - EveningBeginFixed + * - TransitionTime + * + * @return QHash + * @see nightColorConfigChange + * @see signalNightColorConfigChange + * @since 5.12 + */ + QHash nightColorInfo(); + /** + * @brief Allows changing the Night Color configuration. + * + * The provided variant hash can have the following fields: + * - Active + * - Mode + * - NightTemperature + * - LatitudeAuto + * - LongitudeAuto + * - LatitudeFixed + * - LongitudeFixed + * - MorningBeginFixed + * - EveningBeginFixed + * - TransitionTime + * + * It returns true if the configuration change was successful, otherwise false. + * A change request for the location or timings needs to provide all relevant fields at the same time + * to be successful. Otherwise the whole change request will get ignored. A change request will be ignored + * as a whole as well, if one of the provided information has been sent in a wrong format. + * + * @return bool + * @see nightColorInfo + * @see signalNightColorConfigChange + * @since 5.12 + */ + bool setNightColorConfig(QHash data); + /** + * @brief For receiving auto location updates, primarily through the KDE Daemon + * @return void + * @since 5.12 + */ + void nightColorAutoLocationUpdate(double latitude, double longitude); + /** + * @brief Temporarily blocks Night Color. + * @since 5.18 + */ + uint inhibit(); + /** + * @brief Cancels the previous call to inhibit(). + * @since 5.18 + */ + void uninhibit(uint cookie); + +Q_SIGNALS: + /** + * @brief Emits that the Night Color configuration has been changed. + * + * The provided variant hash provides the same fields as nightColorInfo + * + * @return void + * @see nightColorInfo + * @see nightColorConfigChange + * @since 5.12 + */ + void nightColorConfigChanged(QHash data); + +private Q_SLOTS: + void removeInhibitorService(const QString &serviceName); + +private: + void uninhibit(const QString &serviceName, uint cookie); + + Manager *m_manager; + QDBusServiceWatcher *m_inhibitorWatcher; + QMultiHash m_inhibitors; + uint m_lastInhibitionCookie = 0; +}; + +} + +} + +#endif // KWIN_NIGHTCOLOR_DBUS_INTERFACE_H diff --git a/colorcorrection/constants.h b/colorcorrection/constants.h new file mode 100644 index 0000000..eb76f9c --- /dev/null +++ b/colorcorrection/constants.h @@ -0,0 +1,278 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_NIGHTCOLOR_CONSTANTS_H +#define KWIN_NIGHTCOLOR_CONSTANTS_H + +namespace KWin +{ +namespace ColorCorrect +{ + +static const int MSC_DAY = 86400000; +static const int MIN_TEMPERATURE = 1000; +static const int NEUTRAL_TEMPERATURE = 6500; +static const int DEFAULT_NIGHT_TEMPERATURE = 4500; +static const int FALLBACK_SLOW_UPDATE_TIME = 1800000; /* 30 minutes */ + +/** + * Whitepoint values for temperatures at 100K intervals. + * These will be interpolated for the actual temperature. + * This table was provided by Ingo Thies, 2013. + * See the following file for more information: + * https://github.com/jonls/redshift/blob/master/README-colorramp + */ +static const float blackbodyColor[] = { + 1.00000000, 0.18172716, 0.00000000, /* 1000K */ + 1.00000000, 0.25503671, 0.00000000, /* 1100K */ + 1.00000000, 0.30942099, 0.00000000, /* 1200K */ + 1.00000000, 0.35357379, 0.00000000, /* ... */ + 1.00000000, 0.39091524, 0.00000000, + 1.00000000, 0.42322816, 0.00000000, + 1.00000000, 0.45159884, 0.00000000, + 1.00000000, 0.47675916, 0.00000000, + 1.00000000, 0.49923747, 0.00000000, + 1.00000000, 0.51943421, 0.00000000, + 1.00000000, 0.54360078, 0.08679949, /* 2000K */ + 1.00000000, 0.56618736, 0.14065513, + 1.00000000, 0.58734976, 0.18362641, + 1.00000000, 0.60724493, 0.22137978, + 1.00000000, 0.62600248, 0.25591950, + 1.00000000, 0.64373109, 0.28819679, + 1.00000000, 0.66052319, 0.31873863, + 1.00000000, 0.67645822, 0.34786758, + 1.00000000, 0.69160518, 0.37579588, + 1.00000000, 0.70602449, 0.40267128, + 1.00000000, 0.71976951, 0.42860152, /* 3000K */ + 1.00000000, 0.73288760, 0.45366838, + 1.00000000, 0.74542112, 0.47793608, + 1.00000000, 0.75740814, 0.50145662, + 1.00000000, 0.76888303, 0.52427322, + 1.00000000, 0.77987699, 0.54642268, + 1.00000000, 0.79041843, 0.56793692, + 1.00000000, 0.80053332, 0.58884417, + 1.00000000, 0.81024551, 0.60916971, + 1.00000000, 0.81957693, 0.62893653, + 1.00000000, 0.82854786, 0.64816570, /* 4000K */ + 1.00000000, 0.83717703, 0.66687674, + 1.00000000, 0.84548188, 0.68508786, + 1.00000000, 0.85347859, 0.70281616, + 1.00000000, 0.86118227, 0.72007777, + 1.00000000, 0.86860704, 0.73688797, /* 4500K */ + 1.00000000, 0.87576611, 0.75326132, + 1.00000000, 0.88267187, 0.76921169, + 1.00000000, 0.88933596, 0.78475236, + 1.00000000, 0.89576933, 0.79989606, + 1.00000000, 0.90198230, 0.81465502, /* 5000K */ + 1.00000000, 0.90963069, 0.82838210, + 1.00000000, 0.91710889, 0.84190889, + 1.00000000, 0.92441842, 0.85523742, + 1.00000000, 0.93156127, 0.86836903, + 1.00000000, 0.93853986, 0.88130458, + 1.00000000, 0.94535695, 0.89404470, + 1.00000000, 0.95201559, 0.90658983, + 1.00000000, 0.95851906, 0.91894041, + 1.00000000, 0.96487079, 0.93109690, + 1.00000000, 0.97107439, 0.94305985, /* 6000K */ + 1.00000000, 0.97713351, 0.95482993, + 1.00000000, 0.98305189, 0.96640795, + 1.00000000, 0.98883326, 0.97779486, + 1.00000000, 0.99448139, 0.98899179, + 1.00000000, 1.00000000, 1.00000000, /* 6500K */ + 0.98947904, 0.99348723, 1.00000000, + 0.97940448, 0.98722715, 1.00000000, + 0.96975025, 0.98120637, 1.00000000, + 0.96049223, 0.97541240, 1.00000000, + 0.95160805, 0.96983355, 1.00000000, /* 7000K */ + 0.94303638, 0.96443333, 1.00000000, + 0.93480451, 0.95923080, 1.00000000, + 0.92689056, 0.95421394, 1.00000000, + 0.91927697, 0.94937330, 1.00000000, + 0.91194747, 0.94470005, 1.00000000, + 0.90488690, 0.94018594, 1.00000000, + 0.89808115, 0.93582323, 1.00000000, + 0.89151710, 0.93160469, 1.00000000, + 0.88518247, 0.92752354, 1.00000000, + 0.87906581, 0.92357340, 1.00000000, /* 8000K */ + 0.87315640, 0.91974827, 1.00000000, + 0.86744421, 0.91604254, 1.00000000, + 0.86191983, 0.91245088, 1.00000000, + 0.85657444, 0.90896831, 1.00000000, + 0.85139976, 0.90559011, 1.00000000, + 0.84638799, 0.90231183, 1.00000000, + 0.84153180, 0.89912926, 1.00000000, + 0.83682430, 0.89603843, 1.00000000, + 0.83225897, 0.89303558, 1.00000000, + 0.82782969, 0.89011714, 1.00000000, /* 9000K */ + 0.82353066, 0.88727974, 1.00000000, + 0.81935641, 0.88452017, 1.00000000, + 0.81530175, 0.88183541, 1.00000000, + 0.81136180, 0.87922257, 1.00000000, + 0.80753191, 0.87667891, 1.00000000, + 0.80380769, 0.87420182, 1.00000000, + 0.80018497, 0.87178882, 1.00000000, + 0.79665980, 0.86943756, 1.00000000, + 0.79322843, 0.86714579, 1.00000000, + 0.78988728, 0.86491137, 1.00000000, /* 10000K */ + 0.78663296, 0.86273225, 1.00000000, + 0.78346225, 0.86060650, 1.00000000, + 0.78037207, 0.85853224, 1.00000000, + 0.77735950, 0.85650771, 1.00000000, + 0.77442176, 0.85453121, 1.00000000, + 0.77155617, 0.85260112, 1.00000000, + 0.76876022, 0.85071588, 1.00000000, + 0.76603147, 0.84887402, 1.00000000, + 0.76336762, 0.84707411, 1.00000000, + 0.76076645, 0.84531479, 1.00000000, /* 11000K */ + 0.75822586, 0.84359476, 1.00000000, + 0.75574383, 0.84191277, 1.00000000, + 0.75331843, 0.84026762, 1.00000000, + 0.75094780, 0.83865816, 1.00000000, + 0.74863017, 0.83708329, 1.00000000, + 0.74636386, 0.83554194, 1.00000000, + 0.74414722, 0.83403311, 1.00000000, + 0.74197871, 0.83255582, 1.00000000, + 0.73985682, 0.83110912, 1.00000000, + 0.73778012, 0.82969211, 1.00000000, /* 12000K */ + 0.73574723, 0.82830393, 1.00000000, + 0.73375683, 0.82694373, 1.00000000, + 0.73180765, 0.82561071, 1.00000000, + 0.72989845, 0.82430410, 1.00000000, + 0.72802807, 0.82302316, 1.00000000, + 0.72619537, 0.82176715, 1.00000000, + 0.72439927, 0.82053539, 1.00000000, + 0.72263872, 0.81932722, 1.00000000, + 0.72091270, 0.81814197, 1.00000000, + 0.71922025, 0.81697905, 1.00000000, /* 13000K */ + 0.71756043, 0.81583783, 1.00000000, + 0.71593234, 0.81471775, 1.00000000, + 0.71433510, 0.81361825, 1.00000000, + 0.71276788, 0.81253878, 1.00000000, + 0.71122987, 0.81147883, 1.00000000, + 0.70972029, 0.81043789, 1.00000000, + 0.70823838, 0.80941546, 1.00000000, + 0.70678342, 0.80841109, 1.00000000, + 0.70535469, 0.80742432, 1.00000000, + 0.70395153, 0.80645469, 1.00000000, /* 14000K */ + 0.70257327, 0.80550180, 1.00000000, + 0.70121928, 0.80456522, 1.00000000, + 0.69988894, 0.80364455, 1.00000000, + 0.69858167, 0.80273941, 1.00000000, + 0.69729688, 0.80184943, 1.00000000, + 0.69603402, 0.80097423, 1.00000000, + 0.69479255, 0.80011347, 1.00000000, + 0.69357196, 0.79926681, 1.00000000, + 0.69237173, 0.79843391, 1.00000000, + 0.69119138, 0.79761446, 1.00000000, /* 15000K */ + 0.69003044, 0.79680814, 1.00000000, + 0.68888844, 0.79601466, 1.00000000, + 0.68776494, 0.79523371, 1.00000000, + 0.68665951, 0.79446502, 1.00000000, + 0.68557173, 0.79370830, 1.00000000, + 0.68450119, 0.79296330, 1.00000000, + 0.68344751, 0.79222975, 1.00000000, + 0.68241029, 0.79150740, 1.00000000, + 0.68138918, 0.79079600, 1.00000000, + 0.68038380, 0.79009531, 1.00000000, /* 16000K */ + 0.67939381, 0.78940511, 1.00000000, + 0.67841888, 0.78872517, 1.00000000, + 0.67745866, 0.78805526, 1.00000000, + 0.67651284, 0.78739518, 1.00000000, + 0.67558112, 0.78674472, 1.00000000, + 0.67466317, 0.78610368, 1.00000000, + 0.67375872, 0.78547186, 1.00000000, + 0.67286748, 0.78484907, 1.00000000, + 0.67198916, 0.78423512, 1.00000000, + 0.67112350, 0.78362984, 1.00000000, /* 17000K */ + 0.67027024, 0.78303305, 1.00000000, + 0.66942911, 0.78244457, 1.00000000, + 0.66859988, 0.78186425, 1.00000000, + 0.66778228, 0.78129191, 1.00000000, + 0.66697610, 0.78072740, 1.00000000, + 0.66618110, 0.78017057, 1.00000000, + 0.66539706, 0.77962127, 1.00000000, + 0.66462376, 0.77907934, 1.00000000, + 0.66386098, 0.77854465, 1.00000000, + 0.66310852, 0.77801705, 1.00000000, /* 18000K */ + 0.66236618, 0.77749642, 1.00000000, + 0.66163375, 0.77698261, 1.00000000, + 0.66091106, 0.77647551, 1.00000000, + 0.66019791, 0.77597498, 1.00000000, + 0.65949412, 0.77548090, 1.00000000, + 0.65879952, 0.77499315, 1.00000000, + 0.65811392, 0.77451161, 1.00000000, + 0.65743716, 0.77403618, 1.00000000, + 0.65676908, 0.77356673, 1.00000000, + 0.65610952, 0.77310316, 1.00000000, /* 19000K */ + 0.65545831, 0.77264537, 1.00000000, + 0.65481530, 0.77219324, 1.00000000, + 0.65418036, 0.77174669, 1.00000000, + 0.65355332, 0.77130560, 1.00000000, + 0.65293404, 0.77086988, 1.00000000, + 0.65232240, 0.77043944, 1.00000000, + 0.65171824, 0.77001419, 1.00000000, + 0.65112144, 0.76959404, 1.00000000, + 0.65053187, 0.76917889, 1.00000000, + 0.64994941, 0.76876866, 1.00000000, /* 20000K */ + 0.64937392, 0.76836326, 1.00000000, + 0.64880528, 0.76796263, 1.00000000, + 0.64824339, 0.76756666, 1.00000000, + 0.64768812, 0.76717529, 1.00000000, + 0.64713935, 0.76678844, 1.00000000, + 0.64659699, 0.76640603, 1.00000000, + 0.64606092, 0.76602798, 1.00000000, + 0.64553103, 0.76565424, 1.00000000, + 0.64500722, 0.76528472, 1.00000000, + 0.64448939, 0.76491935, 1.00000000, /* 21000K */ + 0.64397745, 0.76455808, 1.00000000, + 0.64347129, 0.76420082, 1.00000000, + 0.64297081, 0.76384753, 1.00000000, + 0.64247594, 0.76349813, 1.00000000, + 0.64198657, 0.76315256, 1.00000000, + 0.64150261, 0.76281076, 1.00000000, + 0.64102399, 0.76247267, 1.00000000, + 0.64055061, 0.76213824, 1.00000000, + 0.64008239, 0.76180740, 1.00000000, + 0.63961926, 0.76148010, 1.00000000, /* 22000K */ + 0.63916112, 0.76115628, 1.00000000, + 0.63870790, 0.76083590, 1.00000000, + 0.63825953, 0.76051890, 1.00000000, + 0.63781592, 0.76020522, 1.00000000, + 0.63737701, 0.75989482, 1.00000000, + 0.63694273, 0.75958764, 1.00000000, + 0.63651299, 0.75928365, 1.00000000, + 0.63608774, 0.75898278, 1.00000000, + 0.63566691, 0.75868499, 1.00000000, + 0.63525042, 0.75839025, 1.00000000, /* 23000K */ + 0.63483822, 0.75809849, 1.00000000, + 0.63443023, 0.75780969, 1.00000000, + 0.63402641, 0.75752379, 1.00000000, + 0.63362667, 0.75724075, 1.00000000, + 0.63323097, 0.75696053, 1.00000000, + 0.63283925, 0.75668310, 1.00000000, + 0.63245144, 0.75640840, 1.00000000, + 0.63206749, 0.75613641, 1.00000000, + 0.63168735, 0.75586707, 1.00000000, + 0.63131096, 0.75560036, 1.00000000, /* 24000K */ + 0.63093826, 0.75533624, 1.00000000, + 0.63056920, 0.75507467, 1.00000000, + 0.63020374, 0.75481562, 1.00000000, + 0.62984181, 0.75455904, 1.00000000, + 0.62948337, 0.75430491, 1.00000000, + 0.62912838, 0.75405319, 1.00000000, + 0.62877678, 0.75380385, 1.00000000, + 0.62842852, 0.75355685, 1.00000000, + 0.62808356, 0.75331217, 1.00000000, + 0.62774186, 0.75306977, 1.00000000, /* 25000K */ + 0.62740336, 0.75282962, 1.00000000 +}; + +} +} + +#endif // KWIN_NIGHTCOLOR_CONSTANTS_H diff --git a/colorcorrection/manager.cpp b/colorcorrection/manager.cpp new file mode 100644 index 0000000..d2c968f --- /dev/null +++ b/colorcorrection/manager.cpp @@ -0,0 +1,947 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "manager.h" +#include "clockskewnotifier.h" +#include "colorcorrectdbusinterface.h" +#include "suncalc.h" +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include +#include + +namespace KWin { +namespace ColorCorrect { + +static const int QUICK_ADJUST_DURATION = 2000; +static const int TEMPERATURE_STEP = 50; + +static bool checkLocation(double lat, double lng) +{ + return -90 <= lat && lat <= 90 && -180 <= lng && lng <= 180; +} + +Manager::Manager(QObject *parent) + : QObject(parent) +{ + m_iface = new ColorCorrectDBusInterface(this); + m_skewNotifier = new ClockSkewNotifier(this); + + connect(kwinApp(), &Application::workspaceCreated, this, &Manager::init); + + // Display a message when Night Color is (un)inhibited. + connect(this, &Manager::inhibitedChanged, this, [this] { + // TODO: Maybe use different icons? + const QString iconName = isInhibited() + ? QStringLiteral("preferences-desktop-display-nightcolor-off") + : QStringLiteral("preferences-desktop-display-nightcolor-on"); + + const QString text = isInhibited() + ? i18nc("Night Color was disabled", "Night Color Off") + : i18nc("Night Color was enabled", "Night Color On"); + + QDBusMessage message = QDBusMessage::createMethodCall( + QStringLiteral("org.kde.plasmashell"), + QStringLiteral("/org/kde/osdService"), + QStringLiteral("org.kde.osdService"), + QStringLiteral("showText")); + message.setArguments({ iconName, text }); + + QDBusConnection::sessionBus().asyncCall(message); + }); +} + +void Manager::init() +{ + Settings::instance(kwinApp()->config()); + // we may always read in the current config + readConfig(); + + if (!isAvailable()) { + return; + } + + connect(Screens::self(), &Screens::countChanged, this, &Manager::hardReset); + + connect(LogindIntegration::self(), &LogindIntegration::sessionActiveChanged, this, + [this](bool active) { + if (active) { + hardReset(); + } else { + cancelAllTimers(); + } + } + ); + + connect(m_skewNotifier, &ClockSkewNotifier::clockSkewed, this, [this]() { + // check if we're resuming from suspend - in this case do a hard reset + // Note: We're using the time clock to detect a suspend phase instead of connecting to the + // provided logind dbus signal, because this signal would be received way too late. + QDBusMessage message = QDBusMessage::createMethodCall("org.freedesktop.login1", + "/org/freedesktop/login1", + "org.freedesktop.DBus.Properties", + QStringLiteral("Get")); + message.setArguments(QVariantList({"org.freedesktop.login1.Manager", QStringLiteral("PreparingForSleep")})); + QDBusReply reply = QDBusConnection::systemBus().call(message); + bool comingFromSuspend; + if (reply.isValid()) { + comingFromSuspend = reply.value().toBool(); + } else { + qCDebug(KWIN_COLORCORRECTION) << "Failed to get PreparingForSleep Property of logind session:" << reply.error().message(); + // Always do a hard reset in case we have no further information. + comingFromSuspend = true; + } + + if (comingFromSuspend) { + hardReset(); + } else { + resetAllTimers(); + } + }); + + hardReset(); +} + +void Manager::hardReset() +{ + cancelAllTimers(); + + updateTransitionTimings(true); + updateTargetTemperature(); + + if (isAvailable() && isEnabled() && !isInhibited()) { + setRunning(true); + commitGammaRamps(currentTargetTemp()); + } + resetAllTimers(); +} + +void Manager::reparseConfigAndReset() +{ + cancelAllTimers(); + readConfig(); + hardReset(); +} + +void Manager::toggle() +{ + m_isGloballyInhibited = !m_isGloballyInhibited; + m_isGloballyInhibited ? inhibit() : uninhibit(); +} + +bool Manager::isInhibited() const +{ + return m_inhibitReferenceCount; +} + +void Manager::inhibit() +{ + m_inhibitReferenceCount++; + + if (m_inhibitReferenceCount == 1) { + resetAllTimers(); + emit inhibitedChanged(); + } +} + +void Manager::uninhibit() +{ + m_inhibitReferenceCount--; + + if (!m_inhibitReferenceCount) { + resetAllTimers(); + emit inhibitedChanged(); + } +} + +bool Manager::isEnabled() const +{ + return m_active; +} + +bool Manager::isRunning() const +{ + return m_running; +} + +bool Manager::isAvailable() const +{ + return kwinApp()->platform()->supportsGammaControl(); +} + +int Manager::currentTemperature() const +{ + return m_currentTemp; +} + +int Manager::targetTemperature() const +{ + return m_targetTemperature; +} + +NightColorMode Manager::mode() const +{ + return m_mode; +} + +QDateTime Manager::previousTransitionDateTime() const +{ + return m_prev.first; +} + +qint64 Manager::previousTransitionDuration() const +{ + return m_prev.first.msecsTo(m_prev.second); +} + +QDateTime Manager::scheduledTransitionDateTime() const +{ + return m_next.first; +} + +qint64 Manager::scheduledTransitionDuration() const +{ + return m_next.first.msecsTo(m_next.second); +} + +void Manager::initShortcuts() +{ + // legacy shortcut with localized key (to avoid breaking existing config) + if (i18n("Toggle Night Color") != QStringLiteral("Toggle Night Color")) { + QAction toggleActionLegacy; + toggleActionLegacy.setProperty("componentName", QStringLiteral(KWIN_NAME)); + toggleActionLegacy.setObjectName(i18n("Toggle Night Color")); + KGlobalAccel::self()->removeAllShortcuts(&toggleActionLegacy); + } + + QAction *toggleAction = new QAction(this); + toggleAction->setProperty("componentName", QStringLiteral(KWIN_NAME)); + toggleAction->setObjectName(QStringLiteral("Toggle Night Color")); + toggleAction->setText(i18n("Toggle Night Color")); + KGlobalAccel::setGlobalShortcut(toggleAction, QList()); + input()->registerShortcut(QKeySequence(), toggleAction, this, &Manager::toggle); +} + +void Manager::readConfig() +{ + Settings *s = Settings::self(); + s->load(); + + setEnabled(s->active()); + + const NightColorMode mode = s->mode(); + switch (s->mode()) { + case NightColorMode::Automatic: + case NightColorMode::Location: + case NightColorMode::Timings: + case NightColorMode::Constant: + setMode(mode); + break; + default: + // Fallback for invalid setting values. + setMode(NightColorMode::Automatic); + break; + } + + m_nightTargetTemp = qBound(MIN_TEMPERATURE, s->nightTemperature(), NEUTRAL_TEMPERATURE); + + double lat, lng; + auto correctReadin = [&lat, &lng]() { + if (!checkLocation(lat, lng)) { + // out of domain + lat = 0; + lng = 0; + } + }; + // automatic + lat = s->latitudeAuto(); + lng = s->longitudeAuto(); + correctReadin(); + m_latAuto = lat; + m_lngAuto = lng; + // fixed location + lat = s->latitudeFixed(); + lng = s->longitudeFixed(); + correctReadin(); + m_latFixed = lat; + m_lngFixed = lng; + + // fixed timings + QTime mrB = QTime::fromString(s->morningBeginFixed(), "hhmm"); + QTime evB = QTime::fromString(s->eveningBeginFixed(), "hhmm"); + + int diffME = mrB.msecsTo(evB); + if (diffME <= 0) { + // morning not strictly before evening - use defaults + mrB = QTime(6,0); + evB = QTime(18,0); + diffME = mrB.msecsTo(evB); + } + int diffMin = qMin(diffME, MSC_DAY - diffME); + + int trTime = s->transitionTime() * 1000 * 60; + if (trTime < 0 || diffMin <= trTime) { + // transition time too long - use defaults + mrB = QTime(6,0); + evB = QTime(18,0); + trTime = FALLBACK_SLOW_UPDATE_TIME; + } + m_morning = mrB; + m_evening = evB; + m_trTime = qMax(trTime / 1000 / 60, 1); +} + +void Manager::resetAllTimers() +{ + cancelAllTimers(); + if (isAvailable()) { + setRunning(isEnabled() && !isInhibited()); + // we do this also for active being false in order to reset the temperature back to the day value + resetQuickAdjustTimer(); + } else { + setRunning(false); + } +} + +void Manager::cancelAllTimers() +{ + delete m_slowUpdateStartTimer; + delete m_slowUpdateTimer; + delete m_quickAdjustTimer; + + m_slowUpdateStartTimer = nullptr; + m_slowUpdateTimer = nullptr; + m_quickAdjustTimer = nullptr; +} + +void Manager::resetQuickAdjustTimer() +{ + updateTransitionTimings(false); + updateTargetTemperature(); + + int tempDiff = qAbs(currentTargetTemp() - m_currentTemp); + // allow tolerance of one TEMPERATURE_STEP to compensate if a slow update is coincidental + if (tempDiff > TEMPERATURE_STEP) { + cancelAllTimers(); + m_quickAdjustTimer = new QTimer(this); + m_quickAdjustTimer->setSingleShot(false); + connect(m_quickAdjustTimer, &QTimer::timeout, this, &Manager::quickAdjust); + + int interval = QUICK_ADJUST_DURATION / (tempDiff / TEMPERATURE_STEP); + if (interval == 0) { + interval = 1; + } + m_quickAdjustTimer->start(interval); + } else { + resetSlowUpdateStartTimer(); + } +} + +void Manager::quickAdjust() +{ + if (!m_quickAdjustTimer) { + return; + } + + int nextTemp; + const int targetTemp = currentTargetTemp(); + + if (m_currentTemp < targetTemp) { + nextTemp = qMin(m_currentTemp + TEMPERATURE_STEP, targetTemp); + } else { + nextTemp = qMax(m_currentTemp - TEMPERATURE_STEP, targetTemp); + } + commitGammaRamps(nextTemp); + + if (nextTemp == targetTemp) { + // stop timer, we reached the target temp + delete m_quickAdjustTimer; + m_quickAdjustTimer = nullptr; + resetSlowUpdateStartTimer(); + } +} + +void Manager::resetSlowUpdateStartTimer() +{ + delete m_slowUpdateStartTimer; + m_slowUpdateStartTimer = nullptr; + + if (!m_running || m_quickAdjustTimer) { + // only reenable the slow update start timer when quick adjust is not active anymore + return; + } + + // There is no need for starting the slow update timer. Screen color temperature + // will be constant all the time now. + if (m_mode == NightColorMode::Constant) { + return; + } + + // set up the next slow update + m_slowUpdateStartTimer = new QTimer(this); + m_slowUpdateStartTimer->setSingleShot(true); + connect(m_slowUpdateStartTimer, &QTimer::timeout, this, &Manager::resetSlowUpdateStartTimer); + + updateTransitionTimings(false); + updateTargetTemperature(); + + const int diff = QDateTime::currentDateTime().msecsTo(m_next.first); + if (diff <= 0) { + qCCritical(KWIN_COLORCORRECTION) << "Error in time calculation. Deactivating Night Color."; + return; + } + m_slowUpdateStartTimer->start(diff); + + // start the current slow update + resetSlowUpdateTimer(); +} + +void Manager::resetSlowUpdateTimer() +{ + delete m_slowUpdateTimer; + m_slowUpdateTimer = nullptr; + + const QDateTime now = QDateTime::currentDateTime(); + const bool isDay = daylight(); + const int targetTemp = isDay ? m_dayTargetTemp : m_nightTargetTemp; + + // We've reached the target color temperature or the transition time is zero. + if (m_prev.first == m_prev.second || m_currentTemp == targetTemp) { + commitGammaRamps(targetTemp); + return; + } + + if (m_prev.first <= now && now <= m_prev.second) { + int availTime = now.msecsTo(m_prev.second); + m_slowUpdateTimer = new QTimer(this); + m_slowUpdateTimer->setSingleShot(false); + if (isDay) { + connect(m_slowUpdateTimer, &QTimer::timeout, this, [this]() {slowUpdate(m_dayTargetTemp);}); + } else { + connect(m_slowUpdateTimer, &QTimer::timeout, this, [this]() {slowUpdate(m_nightTargetTemp);}); + } + + // calculate interval such as temperature is changed by TEMPERATURE_STEP K per timer timeout + int interval = availTime * TEMPERATURE_STEP / qAbs(targetTemp - m_currentTemp); + if (interval == 0) { + interval = 1; + } + m_slowUpdateTimer->start(interval); + } +} + +void Manager::slowUpdate(int targetTemp) +{ + if (!m_slowUpdateTimer) { + return; + } + int nextTemp; + if (m_currentTemp < targetTemp) { + nextTemp = qMin(m_currentTemp + TEMPERATURE_STEP, targetTemp); + } else { + nextTemp = qMax(m_currentTemp - TEMPERATURE_STEP, targetTemp); + } + commitGammaRamps(nextTemp); + if (nextTemp == targetTemp) { + // stop timer, we reached the target temp + delete m_slowUpdateTimer; + m_slowUpdateTimer = nullptr; + } +} + +void Manager::updateTargetTemperature() +{ + const int targetTemperature = mode() != NightColorMode::Constant && daylight() ? m_dayTargetTemp : m_nightTargetTemp; + + if (m_targetTemperature == targetTemperature) { + return; + } + + m_targetTemperature = targetTemperature; + + emit targetTemperatureChanged(); +} + +void Manager::updateTransitionTimings(bool force) +{ + if (m_mode == NightColorMode::Constant) { + m_next = DateTimes(); + m_prev = DateTimes(); + emit previousTransitionTimingsChanged(); + emit scheduledTransitionTimingsChanged(); + return; + } + + const QDateTime todayNow = QDateTime::currentDateTime(); + + if (m_mode == NightColorMode::Timings) { + const QDateTime morB = QDateTime(todayNow.date(), m_morning); + const QDateTime morE = morB.addSecs(m_trTime * 60); + const QDateTime eveB = QDateTime(todayNow.date(), m_evening); + const QDateTime eveE = eveB.addSecs(m_trTime * 60); + + if (morB <= todayNow && todayNow < eveB) { + m_next = DateTimes(eveB, eveE); + m_prev = DateTimes(morB, morE); + } else if (todayNow < morB) { + m_next = DateTimes(morB, morE); + m_prev = DateTimes(eveB.addDays(-1), eveE.addDays(-1)); + } else { + m_next = DateTimes(morB.addDays(1), morE.addDays(1)); + m_prev = DateTimes(eveB, eveE); + } + emit previousTransitionTimingsChanged(); + emit scheduledTransitionTimingsChanged(); + return; + } + + double lat, lng; + if (m_mode == NightColorMode::Automatic) { + lat = m_latAuto; + lng = m_lngAuto; + } else { + lat = m_latFixed; + lng = m_lngFixed; + } + + if (!force) { + // first try by only switching the timings + if (daylight()) { + // next is morning + m_prev = m_next; + m_next = getSunTimings(todayNow.addDays(1), lat, lng, true); + } else { + // next is evening + m_prev = m_next; + m_next = getSunTimings(todayNow, lat, lng, false); + } + } + + if (force || !checkAutomaticSunTimings()) { + // in case this fails, reset them + DateTimes morning = getSunTimings(todayNow, lat, lng, true); + if (todayNow < morning.first) { + m_prev = getSunTimings(todayNow.addDays(-1), lat, lng, false); + m_next = morning; + } else { + DateTimes evening = getSunTimings(todayNow, lat, lng, false); + if (todayNow < evening.first) { + m_prev = morning; + m_next = evening; + } else { + m_prev = evening; + m_next = getSunTimings(todayNow.addDays(1), lat, lng, true); + } + } + } + + emit previousTransitionTimingsChanged(); + emit scheduledTransitionTimingsChanged(); +} + +DateTimes Manager::getSunTimings(const QDateTime &dateTime, double latitude, double longitude, bool morning) const +{ + DateTimes dateTimes = calculateSunTimings(dateTime, latitude, longitude, morning); + // At locations near the poles it is possible, that we can't + // calculate some or all sun timings (midnight sun). + // In this case try to fallback to sensible default values. + const bool beginDefined = !dateTimes.first.isNull(); + const bool endDefined = !dateTimes.second.isNull(); + if (!beginDefined || !endDefined) { + if (beginDefined) { + dateTimes.second = dateTimes.first.addMSecs( FALLBACK_SLOW_UPDATE_TIME ); + } else if (endDefined) { + dateTimes.first = dateTimes.second.addMSecs( - FALLBACK_SLOW_UPDATE_TIME ); + } else { + // Just use default values for morning and evening, but the user + // will probably deactivate Night Color anyway if he is living + // in a region without clear sun rise and set. + const QTime referenceTime = morning ? QTime(6, 0) : QTime(18, 0); + dateTimes.first = QDateTime(dateTime.date(), referenceTime); + dateTimes.second = dateTimes.first.addMSecs( FALLBACK_SLOW_UPDATE_TIME ); + } + } + return dateTimes; +} + +bool Manager::checkAutomaticSunTimings() const +{ + if (m_prev.first.isValid() && m_prev.second.isValid() && + m_next.first.isValid() && m_next.second.isValid()) { + const QDateTime todayNow = QDateTime::currentDateTime(); + return m_prev.first <= todayNow && todayNow < m_next.first && + m_prev.first.msecsTo(m_next.first) < MSC_DAY * 23./24; + } + return false; +} + +bool Manager::daylight() const +{ + return m_prev.first.date() == m_next.first.date(); +} + +int Manager::currentTargetTemp() const +{ + if (!m_running) { + return NEUTRAL_TEMPERATURE; + } + + if (m_mode == NightColorMode::Constant) { + return m_nightTargetTemp; + } + + const QDateTime todayNow = QDateTime::currentDateTime(); + + auto f = [this, todayNow](int target1, int target2) { + if (todayNow <= m_prev.second) { + double residueQuota = todayNow.msecsTo(m_prev.second) / (double)m_prev.first.msecsTo(m_prev.second); + + double ret = (int)((1. - residueQuota) * (double)target2 + residueQuota * (double)target1); + // remove single digits + ret = ((int)(0.1 * ret)) * 10; + return (int)ret; + } else { + return target2; + } + }; + + if (daylight()) { + return f(m_nightTargetTemp, m_dayTargetTemp); + } else { + return f(m_dayTargetTemp, m_nightTargetTemp); + } +} + +void Manager::commitGammaRamps(int temperature) +{ + const auto outs = kwinApp()->platform()->outputs(); + + for (auto *o : outs) { + int rampsize = o->gammaRampSize(); + GammaRamp ramp(rampsize); + + /* + * The gamma calculation below is based on the Redshift app: + * https://github.com/jonls/redshift + */ + uint16_t *red = ramp.red(); + uint16_t *green = ramp.green(); + uint16_t *blue = ramp.blue(); + + // linear default state + for (int i = 0; i < rampsize; i++) { + uint16_t value = (double)i / rampsize * (UINT16_MAX + 1); + red[i] = value; + green[i] = value; + blue[i] = value; + } + + // approximate white point + float whitePoint[3]; + float alpha = (temperature % 100) / 100.; + int bbCIndex = ((temperature - 1000) / 100) * 3; + whitePoint[0] = (1. - alpha) * blackbodyColor[bbCIndex] + alpha * blackbodyColor[bbCIndex + 3]; + whitePoint[1] = (1. - alpha) * blackbodyColor[bbCIndex + 1] + alpha * blackbodyColor[bbCIndex + 4]; + whitePoint[2] = (1. - alpha) * blackbodyColor[bbCIndex + 2] + alpha * blackbodyColor[bbCIndex + 5]; + + for (int i = 0; i < rampsize; i++) { + red[i] = qreal(red[i]) / (UINT16_MAX+1) * whitePoint[0] * (UINT16_MAX+1); + green[i] = qreal(green[i]) / (UINT16_MAX+1) * whitePoint[1] * (UINT16_MAX+1); + blue[i] = qreal(blue[i]) / (UINT16_MAX+1) * whitePoint[2] * (UINT16_MAX+1); + } + + if (o->setGammaRamp(ramp)) { + setCurrentTemperature(temperature); + m_failedCommitAttempts = 0; + } else { + m_failedCommitAttempts++; + if (m_failedCommitAttempts < 10) { + qCWarning(KWIN_COLORCORRECTION).nospace() << "Committing Gamma Ramp failed for output " << o->name() << + ". Trying " << (10 - m_failedCommitAttempts) << " times more."; + } else { + // TODO: On multi monitor setups we could try to rollback earlier changes for already committed outputs + qCWarning(KWIN_COLORCORRECTION) << "Gamma Ramp commit failed too often. Deactivating color correction for now."; + m_failedCommitAttempts = 0; // reset so we can try again later (i.e. after suspend phase or config change) + setRunning(false); + cancelAllTimers(); + } + } + } +} + +QHash Manager::info() const +{ + return QHash { + { QStringLiteral("Available"), isAvailable() }, + + { QStringLiteral("ActiveEnabled"), true}, + { QStringLiteral("Active"), m_active}, + + { QStringLiteral("ModeEnabled"), true}, + { QStringLiteral("Mode"), (int)m_mode}, + + { QStringLiteral("NightTemperatureEnabled"), true}, + { QStringLiteral("NightTemperature"), m_nightTargetTemp}, + + { QStringLiteral("Running"), m_running}, + { QStringLiteral("CurrentColorTemperature"), m_currentTemp}, + + { QStringLiteral("LatitudeAuto"), m_latAuto}, + { QStringLiteral("LongitudeAuto"), m_lngAuto}, + + { QStringLiteral("LocationEnabled"), true}, + { QStringLiteral("LatitudeFixed"), m_latFixed}, + { QStringLiteral("LongitudeFixed"), m_lngFixed}, + + { QStringLiteral("TimingsEnabled"), true}, + { QStringLiteral("MorningBeginFixed"), m_morning.toString(Qt::ISODate)}, + { QStringLiteral("EveningBeginFixed"), m_evening.toString(Qt::ISODate)}, + { QStringLiteral("TransitionTime"), m_trTime}, + }; +} + +bool Manager::changeConfiguration(QHash data) +{ + bool activeUpdate, modeUpdate, tempUpdate, locUpdate, timeUpdate; + activeUpdate = modeUpdate = tempUpdate = locUpdate = timeUpdate = false; + + bool active = m_active; + NightColorMode mode = m_mode; + int nightT = m_nightTargetTemp; + + double lat = m_latFixed; + double lng = m_lngFixed; + + QTime mor = m_morning; + QTime eve = m_evening; + int trT = m_trTime; + + QHash::const_iterator iter1, iter2, iter3; + + iter1 = data.constFind("Active"); + if (iter1 != data.constEnd()) { + if (!iter1.value().canConvert()) { + return false; + } + bool act = iter1.value().toBool(); + activeUpdate = m_active != act; + active = act; + } + + iter1 = data.constFind("Mode"); + if (iter1 != data.constEnd()) { + if (!iter1.value().canConvert()) { + return false; + } + int mo = iter1.value().toInt(); + if (mo < 0 || 3 < mo) { + return false; + } + NightColorMode moM; + switch (mo) { + case 0: + moM = NightColorMode::Automatic; + break; + case 1: + moM = NightColorMode::Location; + break; + case 2: + moM = NightColorMode::Timings; + break; + case 3: + moM = NightColorMode::Constant; + break; + } + modeUpdate = m_mode != moM; + mode = moM; + } + + iter1 = data.constFind("NightTemperature"); + if (iter1 != data.constEnd()) { + if (!iter1.value().canConvert()) { + return false; + } + int nT = iter1.value().toInt(); + if (nT < MIN_TEMPERATURE || NEUTRAL_TEMPERATURE < nT) { + return false; + } + tempUpdate = m_nightTargetTemp != nT; + nightT = nT; + } + + iter1 = data.constFind("LatitudeFixed"); + iter2 = data.constFind("LongitudeFixed"); + if (iter1 != data.constEnd() && iter2 != data.constEnd()) { + if (!iter1.value().canConvert() || !iter2.value().canConvert()) { + return false; + } + double la = iter1.value().toDouble(); + double ln = iter2.value().toDouble(); + if (!checkLocation(la, ln)) { + return false; + } + locUpdate = m_latFixed != la || m_lngFixed != ln; + lat = la; + lng = ln; + } + + iter1 = data.constFind("MorningBeginFixed"); + iter2 = data.constFind("EveningBeginFixed"); + iter3 = data.constFind("TransitionTime"); + if (iter1 != data.constEnd() && iter2 != data.constEnd() && iter3 != data.constEnd()) { + if (!iter1.value().canConvert() || !iter2.value().canConvert() || !iter3.value().canConvert()) { + return false; + } + QTime mo = QTime::fromString(iter1.value().toString(), Qt::ISODate); + QTime ev = QTime::fromString(iter2.value().toString(), Qt::ISODate); + if (!mo.isValid() || !ev.isValid()) { + return false; + } + int tT = iter3.value().toInt(); + + int diffME = mo.msecsTo(ev); + if (diffME <= 0 || qMin(diffME, MSC_DAY - diffME) <= tT * 60 * 1000 || tT < 1) { + // morning not strictly before evening, transition time too long or transition time out of bounds + return false; + } + + timeUpdate = m_morning != mo || m_evening != ev || m_trTime != tT; + mor = mo; + eve = ev; + trT = tT; + } + + if (!(activeUpdate || modeUpdate || tempUpdate || locUpdate || timeUpdate)) { + return true; + } + + bool resetNeeded = activeUpdate || modeUpdate || tempUpdate || + (locUpdate && mode == NightColorMode::Location) || + (timeUpdate && mode == NightColorMode::Timings); + + if (resetNeeded) { + cancelAllTimers(); + } + + Settings *s = Settings::self(); + if (activeUpdate) { + setEnabled(active); + s->setActive(active); + } + + if (modeUpdate) { + setMode(mode); + s->setMode(mode); + } + + if (tempUpdate) { + m_nightTargetTemp = nightT; + s->setNightTemperature(nightT); + } + + if (locUpdate) { + m_latFixed = lat; + m_lngFixed = lng; + s->setLatitudeFixed(lat); + s->setLongitudeFixed(lng); + } + + if (timeUpdate) { + m_morning = mor; + m_evening = eve; + m_trTime = trT; + s->setMorningBeginFixed(mor.toString("hhmm")); + s->setEveningBeginFixed(eve.toString("hhmm")); + s->setTransitionTime(trT); + } + s->save(); + + if (resetNeeded) { + resetAllTimers(); + } + emit configChange(info()); + return true; +} + +void Manager::autoLocationUpdate(double latitude, double longitude) +{ + qCDebug(KWIN_COLORCORRECTION, "Received new location (lat: %f, lng: %f)", latitude, longitude); + + if (!checkLocation(latitude, longitude)) { + return; + } + + // we tolerate small deviations with minimal impact on sun timings + if (qAbs(m_latAuto - latitude) < 2 && qAbs(m_lngAuto - longitude) < 1) { + return; + } + cancelAllTimers(); + m_latAuto = latitude; + m_lngAuto = longitude; + + Settings *s = Settings::self(); + s->setLatitudeAuto(latitude); + s->setLongitudeAuto(longitude); + s->save(); + + resetAllTimers(); + emit configChange(info()); +} + +void Manager::setEnabled(bool enabled) +{ + if (m_active == enabled) { + return; + } + m_active = enabled; + m_skewNotifier->setActive(enabled); + emit enabledChanged(); +} + +void Manager::setRunning(bool running) +{ + if (m_running == running) { + return; + } + m_running = running; + emit runningChanged(); +} + +void Manager::setCurrentTemperature(int temperature) +{ + if (m_currentTemp == temperature) { + return; + } + m_currentTemp = temperature; + emit currentTemperatureChanged(); +} + +void Manager::setMode(NightColorMode mode) +{ + if (m_mode == mode) { + return; + } + m_mode = mode; + emit modeChanged(); +} + +} +} diff --git a/colorcorrection/manager.h b/colorcorrection/manager.h new file mode 100644 index 0000000..e892a2e --- /dev/null +++ b/colorcorrection/manager.h @@ -0,0 +1,320 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_COLORCORRECT_MANAGER_H +#define KWIN_COLORCORRECT_MANAGER_H + +#include "constants.h" +#include + +#include +#include +#include + +class QTimer; + +namespace KWin +{ + +class ClockSkewNotifier; +class Workspace; + +namespace ColorCorrect +{ + +typedef QPair DateTimes; +typedef QPair Times; + +class ColorCorrectDBusInterface; + +/** + * This enum type is used to specify operation mode of the night color manager. + */ +enum NightColorMode { + /** + * Color temperature is computed based on the current position of the Sun. + * + * Location of the user is provided by Plasma. + */ + Automatic, + /** + * Color temperature is computed based on the current position of the Sun. + * + * Location of the user is provided by themselves. + */ + Location, + /** + * Color temperature is computed based on the current time. + * + * Sunrise and sunset times have to be specified by the user. + */ + Timings, + /** + * Color temperature is constant thoughout the day. + */ + Constant, +}; + +/** + * The night color manager is a blue light filter similar to Redshift. + * + * There are four modes this manager can operate in: Automatic, Location, Timings, + * and Constant. Both Automatic and Location modes derive screen color temperature + * from the current position of the Sun, the only difference between two is how + * coordinates of the user are specified. If the user is located near the North or + * South pole, we can't compute correct position of the Sun, that's why we need + * Timings and Constant mode. + * + * With the Timings mode, screen color temperature is computed based on the clock + * time. The user needs to specify timings of the sunset and sunrise as well the + * transition time. + * + * With the Constant mode, screen color temperature is always constant. + */ +class KWIN_EXPORT Manager : public QObject +{ + Q_OBJECT + +public: + Manager(QObject *parent); + void init(); + + /** + * Get current configuration + * @see changeConfiguration + * @since 5.12 + */ + QHash info() const; + /** + * Change configuration + * @see info + * @since 5.12 + */ + bool changeConfiguration(QHash data); + void autoLocationUpdate(double latitude, double longitude); + + /** + * Toggles the active state of the filter. + * + * A quick transition will be started if the difference between current screen + * color temperature and target screen color temperature is too large. Target + * temperature is defined in context of the new active state. + * + * If the filter becomes inactive after calling this method, the target color + * temperature is 6500 K. + * + * If the filter becomes active after calling this method, the target screen + * color temperature is defined by the current operation mode. + * + * Note that this method is a no-op if the underlying platform doesn't support + * adjusting gamma ramps. + */ + void toggle(); + + /** + * Returns @c true if the night color manager is blocked; otherwise @c false. + */ + bool isInhibited() const; + + /** + * Temporarily blocks the night color manager. + * + * After calling this method, the screen color temperature will be reverted + * back to 6500C. When you're done, call uninhibit() method. + */ + void inhibit(); + + /** + * Attempts to unblock the night color manager. + */ + void uninhibit(); + + /** + * Returns @c true if Night Color is enabled; otherwise @c false. + */ + bool isEnabled() const; + + /** + * Returns @c true if Night Color is currently running; otherwise @c false. + */ + bool isRunning() const; + + /** + * Returns @c true if Night Color is supported by platform; otherwise @c false. + */ + bool isAvailable() const; + + /** + * Returns the current screen color temperature. + */ + int currentTemperature() const; + + /** + * Returns the target screen color temperature. + */ + int targetTemperature() const; + + /** + * Returns the mode in which Night Color is operating. + */ + NightColorMode mode() const; + + /** + * Returns the datetime that specifies when the previous screen color temperature transition + * had started. Notice that when Night Color operates in the Constant mode, the returned date + * time object is not valid. + */ + QDateTime previousTransitionDateTime() const; + + /** + * Returns the duration of the previous screen color temperature transition, in milliseconds. + */ + qint64 previousTransitionDuration() const; + + /** + * Returns the datetime that specifies when the next screen color temperature transition will + * start. Notice that when Night Color operates in the Constant mode, the returned date time + * object is not valid. + */ + QDateTime scheduledTransitionDateTime() const; + + /** + * Returns the duration of the next screen color temperature transition, in milliseconds. + */ + qint64 scheduledTransitionDuration() const; + + // for auto tests + void reparseConfigAndReset(); + +public Q_SLOTS: + void resetSlowUpdateStartTimer(); + void quickAdjust(); + +Q_SIGNALS: + void configChange(QHash data); + + /** + * Emitted whenever the night color manager is blocked or unblocked. + */ + void inhibitedChanged(); + + /** + * Emitted whenever the night color manager is enabled or disabled. + */ + void enabledChanged(); + + /** + * Emitted whenever the night color manager starts or stops running. + */ + void runningChanged(); + + /** + * Emitted whenever the current screen color temperature has changed. + */ + void currentTemperatureChanged(); + + /** + * Emitted whenever the target screen color temperature has changed. + */ + void targetTemperatureChanged(); + + /** + * Emitted whenver the operation mode has changed. + */ + void modeChanged(); + + /** + * Emitted whenever the timings of the previous color temperature transition have changed. + */ + void previousTransitionTimingsChanged(); + + /** + * Emitted whenever the timings of the next color temperature transition have changed. + */ + void scheduledTransitionTimingsChanged(); + +private: + void initShortcuts(); + void readConfig(); + void hardReset(); + void slowUpdate(int targetTemp); + void resetAllTimers(); + int currentTargetTemp() const; + void cancelAllTimers(); + /** + * Quick shift on manual change to current target Temperature + */ + void resetQuickAdjustTimer(); + /** + * Slow shift to daytime target Temperature + */ + void resetSlowUpdateTimer(); + + void updateTargetTemperature(); + void updateTransitionTimings(bool force); + DateTimes getSunTimings(const QDateTime &dateTime, double latitude, double longitude, bool morning) const; + bool checkAutomaticSunTimings() const; + bool daylight() const; + + void commitGammaRamps(int temperature); + + void setEnabled(bool enabled); + void setRunning(bool running); + void setCurrentTemperature(int temperature); + void setMode(NightColorMode mode); + + ColorCorrectDBusInterface *m_iface; + ClockSkewNotifier *m_skewNotifier; + + // Specifies whether Night Color is enabled. + bool m_active = false; + + // Specifies whether Night Color is currently running. + bool m_running = false; + + // Specifies whether Night Color is inhibited globally. + bool m_isGloballyInhibited = false; + + NightColorMode m_mode = NightColorMode::Automatic; + + // the previous and next sunrise/sunset intervals - in UTC time + DateTimes m_prev = DateTimes(); + DateTimes m_next = DateTimes(); + + // manual times from config + QTime m_morning = QTime(6,0); + QTime m_evening = QTime(18,0); + int m_trTime = 30; // saved in minutes > 1 + + // auto location provided by work space + double m_latAuto; + double m_lngAuto; + // manual location from config + double m_latFixed; + double m_lngFixed; + + QTimer *m_slowUpdateStartTimer = nullptr; + QTimer *m_slowUpdateTimer = nullptr; + QTimer *m_quickAdjustTimer = nullptr; + + int m_currentTemp = NEUTRAL_TEMPERATURE; + int m_targetTemperature = NEUTRAL_TEMPERATURE; + int m_dayTargetTemp = NEUTRAL_TEMPERATURE; + int m_nightTargetTemp = DEFAULT_NIGHT_TEMPERATURE; + + int m_failedCommitAttempts = 0; + int m_inhibitReferenceCount = 0; + + // The Workspace class needs to call initShortcuts during initialization. + friend class KWin::Workspace; +}; + +} +} + +#endif // KWIN_COLORCORRECT_MANAGER_H diff --git a/colorcorrection/suncalc.cpp b/colorcorrection/suncalc.cpp new file mode 100644 index 0000000..20b0ae9 --- /dev/null +++ b/colorcorrection/suncalc.cpp @@ -0,0 +1,163 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "suncalc.h" +#include "constants.h" + +#include +#include +#include + +namespace KWin { +namespace ColorCorrect { + +#define TWILIGHT_NAUT -12.0 +#define TWILIGHT_CIVIL -6.0 +#define SUN_RISE_SET -0.833 +#define SUN_HIGH 2.0 + +static QTime convertToLocalTime(const QDateTime &when, const QTime &utcTime) +{ + const QTimeZone timeZone = QTimeZone::systemTimeZone(); + const int utcOffset = timeZone.offsetFromUtc(when); + return utcTime.addSecs(utcOffset); +} + +QPair calculateSunTimings(const QDateTime &dateTime, double latitude, double longitude, bool morning) +{ + // calculations based on https://aa.quae.nl/en/reken/zonpositie.html + // accuracy: +/- 5min + + // positioning + const double rad = M_PI / 180.; + const double earthObliquity = 23.4397; // epsilon + + const double lat = latitude; // phi + const double lng = -longitude; // lw + + // times + const QDateTime utcDateTime = dateTime.toUTC(); + const double juPrompt = utcDateTime.date().toJulianDay(); // J + const double ju2000 = 2451545.; // J2000 + + // geometry + auto mod360 = [](double number) -> double { + return std::fmod(number, 360.); + }; + + auto sin = [&rad](double angle) -> double { + return std::sin(angle * rad); + }; + auto cos = [&rad](double angle) -> double { + return std::cos(angle * rad); + }; + auto asin = [&rad](double val) -> double { + return std::asin(val) / rad; + }; + auto acos = [&rad](double val) -> double { + return std::acos(val) / rad; + }; + + auto anomaly = [&](const double date) -> double { // M + return mod360(357.5291 + 0.98560028 * (date - ju2000)); + }; + + auto center = [&sin](double anomaly) -> double { // C + return 1.9148 * sin(anomaly) + 0.02 * sin(2 * anomaly) + 0.0003 * sin(3 * anomaly); + }; + + auto ecliptLngMean = [](double anom) -> double { // Mean ecliptical longitude L_sun = Mean Anomaly + Perihelion + 180° + return anom + 282.9372; // anom + 102.9372 + 180° + }; + + auto ecliptLng = [&](double anom) -> double { // lambda = L_sun + C + return ecliptLngMean(anom) + center(anom); + }; + + auto declination = [&](const double date) -> double { // delta + const double anom = anomaly(date); + const double eclLng = ecliptLng(anom); + + return mod360(asin(sin(earthObliquity) * sin(eclLng))); + }; + + // sun hour angle at specific angle + auto hourAngle = [&](const double date, double angle) -> double { // H_t + const double decl = declination(date); + const double ret0 = (sin(angle) - sin(lat) * sin(decl)) / (cos(lat) * cos(decl)); + + double ret = mod360(acos( ret0 )); + if (180. < ret) { + ret = ret - 360.; + } + return ret; + }; + + /* + * Sun positions + */ + + // transit is at noon + auto getTransit = [&](const double date) -> double { // Jtransit + const double juMeanSolTime = juPrompt - ju2000 - 0.0009 - lng / 360.; // n_x = J - J_2000 - J_0 - l_w / 360° + const double juTrEstimate = date + qRound64(juMeanSolTime) - juMeanSolTime; // J_x = J + n - n_x + const double anom = anomaly(juTrEstimate); // M + const double eclLngM = ecliptLngMean(anom); // L_sun + + return juTrEstimate + 0.0053 * sin(anom) - 0.0068 * sin(2 * eclLngM); + }; + + auto getSunMorning = [&hourAngle](const double angle, const double transit) -> double { + return transit - hourAngle(transit, angle) / 360.; + }; + + auto getSunEvening = [&hourAngle](const double angle, const double transit) -> double { + return transit + hourAngle(transit, angle) / 360.; + }; + + /* + * Begin calculations + */ + + // noon - sun at the highest point + const double juNoon = getTransit(juPrompt); + + double begin, end; + if (morning) { + begin = getSunMorning(TWILIGHT_CIVIL, juNoon); + end = getSunMorning(SUN_HIGH, juNoon); + } else { + begin = getSunEvening(SUN_HIGH, juNoon); + end = getSunEvening(TWILIGHT_CIVIL, juNoon); + } + // transform to QDateTime + begin += 0.5; + end += 0.5; + + QDateTime dateTimeBegin; + QDateTime dateTimeEnd; + + if (!std::isnan(begin)) { + const double dayFraction = begin - int(begin); + const QTime utcTime = QTime::fromMSecsSinceStartOfDay(dayFraction * MSC_DAY); + const QTime localTime = convertToLocalTime(dateTime, utcTime); + dateTimeBegin = QDateTime(dateTime.date(), localTime); + } + + if (!std::isnan(end)) { + const double dayFraction = end - int(end); + const QTime utcTime = QTime::fromMSecsSinceStartOfDay(dayFraction * MSC_DAY); + const QTime localTime = convertToLocalTime(dateTime, utcTime); + dateTimeEnd = QDateTime(dateTime.date(), localTime); + } + + return { dateTimeBegin, dateTimeEnd }; +} + +} +} diff --git a/colorcorrection/suncalc.h b/colorcorrection/suncalc.h new file mode 100644 index 0000000..73fa94c --- /dev/null +++ b/colorcorrection/suncalc.h @@ -0,0 +1,36 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_SUNCALCULATOR_H +#define KWIN_SUNCALCULATOR_H + +#include +#include +#include + +namespace KWin +{ + +namespace ColorCorrect +{ + +/** + * Calculates for a given location and date two of the + * following sun timings in their temporal order: + * - Nautical dawn and sunrise for the morning + * - Sunset and nautical dusk for the evening + * @since 5.12 + */ + +QPair calculateSunTimings(const QDateTime &dateTime, double latitude, double longitude, bool morning); + + +} +} + +#endif // KWIN_SUNCALCULATOR_H diff --git a/composite.cpp b/composite.cpp new file mode 100644 index 0000000..9a261b4 --- /dev/null +++ b/composite.cpp @@ -0,0 +1,1046 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "composite.h" + +#include "dbusinterface.h" +#include "x11client.h" +#include "decorations/decoratedclient.h" +#include "deleted.h" +#include "effects.h" +#include "internal_client.h" +#include "overlaywindow.h" +#include "platform.h" +#include "scene.h" +#include "screens.h" +#include "shadow.h" +#include "unmanaged.h" +#include "useractions.h" +#include "utils.h" +#include "wayland_server.h" +#include "workspace.h" +#include "xcbutils.h" + +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +Q_DECLARE_METATYPE(KWin::X11Compositor::SuspendReason) + +namespace KWin +{ + +// See main.cpp: +extern int screen_number; + +extern bool is_multihead; +extern int currentRefreshRate(); + +Compositor *Compositor::s_compositor = nullptr; +Compositor *Compositor::self() +{ + return s_compositor; +} + +WaylandCompositor *WaylandCompositor::create(QObject *parent) +{ + Q_ASSERT(!s_compositor); + auto *compositor = new WaylandCompositor(parent); + s_compositor = compositor; + return compositor; +} +X11Compositor *X11Compositor::create(QObject *parent) +{ + Q_ASSERT(!s_compositor); + auto *compositor = new X11Compositor(parent); + s_compositor = compositor; + return compositor; +} + +class CompositorSelectionOwner : public KSelectionOwner +{ + Q_OBJECT +public: + CompositorSelectionOwner(const char *selection) + : KSelectionOwner(selection, connection(), rootWindow()) + , m_owning(false) + { + connect (this, &CompositorSelectionOwner::lostOwnership, + this, [this]() { m_owning = false; }); + } + bool owning() const { + return m_owning; + } + void setOwning(bool own) { + m_owning = own; + } +private: + bool m_owning; +}; + +static inline qint64 milliToNano(int milli) { return qint64(milli) * 1000 * 1000; } +static inline qint64 nanoToMilli(int nano) { return nano / (1000*1000); } + +Compositor::Compositor(QObject* workspace) + : QObject(workspace) + , m_state(State::Off) + , m_selectionOwner(nullptr) + , vBlankInterval(0) + , fpsInterval(0) + , m_timeSinceLastVBlank(0) + , m_scene(nullptr) + , m_bufferSwapPending(false) + , m_composeAtSwapCompletion(false) +{ + connect(options, &Options::configChanged, this, &Compositor::configChanged); + connect(options, &Options::animationSpeedChanged, this, &Compositor::configChanged); + + m_monotonicClock.start(); + + // 2 sec which should be enough to restart the compositor. + static const int compositorLostMessageDelay = 2000; + + m_releaseSelectionTimer.setSingleShot(true); + m_releaseSelectionTimer.setInterval(compositorLostMessageDelay); + connect(&m_releaseSelectionTimer, &QTimer::timeout, + this, &Compositor::releaseCompositorSelection); + + m_unusedSupportPropertyTimer.setInterval(compositorLostMessageDelay); + m_unusedSupportPropertyTimer.setSingleShot(true); + connect(&m_unusedSupportPropertyTimer, &QTimer::timeout, + this, &Compositor::deleteUnusedSupportProperties); + + // Delay the call to start by one event cycle. + // The ctor of this class is invoked from the Workspace ctor, that means before + // Workspace is completely constructed, so calling Workspace::self() would result + // in undefined behavior. This is fixed by using a delayed invocation. + if (kwinApp()->platform()->isReady()) { + QTimer::singleShot(0, this, &Compositor::start); + } + connect(kwinApp()->platform(), &Platform::readyChanged, this, + [this] (bool ready) { + if (ready) { + start(); + } else { + stop(); + } + }, Qt::QueuedConnection + ); + + if (qEnvironmentVariableIsSet("KWIN_MAX_FRAMES_TESTED")) + m_framesToTestForSafety = qEnvironmentVariableIntValue("KWIN_MAX_FRAMES_TESTED"); + + // register DBus + new CompositorDBusInterface(this); +} + +Compositor::~Compositor() +{ + emit aboutToDestroy(); + stop(); + deleteUnusedSupportProperties(); + destroyCompositorSelection(); + s_compositor = nullptr; +} + +bool Compositor::setupStart() +{ + if (kwinApp()->isTerminating()) { + // Don't start while KWin is terminating. An event to restart might be lingering + // in the event queue due to graphics reset. + return false; + } + if (m_state != State::Off) { + return false; + } + m_state = State::Starting; + + options->reloadCompositingSettings(true); + + initializeX11(); + + // There might still be a deleted around, needs to be cleared before + // creating the scene (BUG 333275). + if (Workspace::self()) { + while (!Workspace::self()->deletedList().isEmpty()) { + Workspace::self()->deletedList().first()->discard(); + } + } + + emit aboutToToggleCompositing(); + + auto supportedCompositors = kwinApp()->platform()->supportedCompositors(); + const auto userConfigIt = std::find(supportedCompositors.begin(), supportedCompositors.end(), + options->compositingMode()); + + if (userConfigIt != supportedCompositors.end()) { + supportedCompositors.erase(userConfigIt); + supportedCompositors.prepend(options->compositingMode()); + } else { + qCWarning(KWIN_CORE) + << "Configured compositor not supported by Platform. Falling back to defaults"; + } + + const auto availablePlugins = KPluginLoader::findPlugins(QStringLiteral("org.kde.kwin.scenes")); + + for (const KPluginMetaData &pluginMetaData : availablePlugins) { + qCDebug(KWIN_CORE) << "Available scene plugin:" << pluginMetaData.fileName(); + } + + for (auto type : qAsConst(supportedCompositors)) { + switch (type) { + case XRenderCompositing: + qCDebug(KWIN_CORE) << "Attempting to load the XRender scene"; + break; + case OpenGLCompositing: + case OpenGL2Compositing: + qCDebug(KWIN_CORE) << "Attempting to load the OpenGL scene"; + break; + case QPainterCompositing: + qCDebug(KWIN_CORE) << "Attempting to load the QPainter scene"; + break; + case NoCompositing: + Q_UNREACHABLE(); + } + const auto pluginIt = std::find_if(availablePlugins.begin(), availablePlugins.end(), + [type] (const auto &plugin) { + const auto &metaData = plugin.rawData(); + auto it = metaData.find(QStringLiteral("CompositingType")); + if (it != metaData.end()) { + if ((*it).toInt() == int{type}) { + return true; + } + } + return false; + }); + if (pluginIt != availablePlugins.end()) { + std::unique_ptr + factory{ qobject_cast(pluginIt->instantiate()) }; + if (factory) { + m_scene = factory->create(this); + if (m_scene) { + if (!m_scene->initFailed()) { + qCDebug(KWIN_CORE) << "Instantiated compositing plugin:" + << pluginIt->name(); + break; + } else { + delete m_scene; + m_scene = nullptr; + } + } + } + } + } + + if (m_scene == nullptr || m_scene->initFailed()) { + qCCritical(KWIN_CORE) << "Failed to initialize compositing, compositing disabled"; + m_state = State::Off; + + delete m_scene; + m_scene = nullptr; + + if (m_selectionOwner) { + m_selectionOwner->setOwning(false); + m_selectionOwner->release(); + } + if (!supportedCompositors.contains(NoCompositing)) { + qCCritical(KWIN_CORE) << "The used windowing system requires compositing"; + qCCritical(KWIN_CORE) << "We are going to quit KWin now as it is broken"; + qApp->quit(); + } + return false; + } + + CompositingType compositingType = m_scene->compositingType(); + if (compositingType & OpenGLCompositing) { + // Override for OpenGl sub-type OpenGL2Compositing. + compositingType = OpenGLCompositing; + } + kwinApp()->platform()->setSelectedCompositor(compositingType); + + if (!Workspace::self() && m_scene && m_scene->compositingType() == QPainterCompositing) { + // Force Software QtQuick on first startup with QPainter. + QQuickWindow::setSceneGraphBackend(QSGRendererInterface::Software); + } + + connect(m_scene, &Scene::resetCompositing, this, &Compositor::reinitialize); + emit sceneCreated(); + + return true; +} + +void Compositor::initializeX11() +{ + xcb_connection_t *connection = kwinApp()->x11Connection(); + if (!connection) { + return; + } + + if (!m_selectionOwner) { + char selection_name[ 100 ]; + sprintf(selection_name, "_NET_WM_CM_S%d", Application::x11ScreenNumber()); + m_selectionOwner = new CompositorSelectionOwner(selection_name); + connect(m_selectionOwner, &CompositorSelectionOwner::lostOwnership, + this, &Compositor::stop); + } + if (!m_selectionOwner->owning()) { + // Force claim ownership. + m_selectionOwner->claim(true); + m_selectionOwner->setOwning(true); + } + + xcb_composite_redirect_subwindows(connection, kwinApp()->x11RootWindow(), + XCB_COMPOSITE_REDIRECT_MANUAL); +} + +void Compositor::cleanupX11() +{ + delete m_selectionOwner; + m_selectionOwner = nullptr; +} + +void Compositor::startupWithWorkspace() +{ + connect(kwinApp(), &Application::x11ConnectionChanged, + this, &Compositor::initializeX11, Qt::UniqueConnection); + connect(kwinApp(), &Application::x11ConnectionAboutToBeDestroyed, + this, &Compositor::cleanupX11, Qt::UniqueConnection); + initializeX11(); + + Workspace::self()->markXStackingOrderAsDirty(); + Q_ASSERT(m_scene); + + connect(workspace(), &Workspace::destroyed, this, [this] { compositeTimer.stop(); }); + fpsInterval = options->maxFpsInterval(); + + if (m_scene->syncsToVBlank()) { + // If we do vsync, set the fps to the next multiple of the vblank rate. + vBlankInterval = milliToNano(1000) / currentRefreshRate(); + fpsInterval = qMax((fpsInterval / vBlankInterval) * vBlankInterval, vBlankInterval); + } else { + // No vsync - DO NOT set "0", would cause div-by-zero segfaults. + vBlankInterval = milliToNano(1); + } + + // Sets also the 'effects' pointer. + kwinApp()->platform()->createEffectsHandler(this, m_scene); + connect(Workspace::self(), &Workspace::deletedRemoved, m_scene, &Scene::removeToplevel); + connect(effects, &EffectsHandler::screenGeometryChanged, this, &Compositor::addRepaintFull); + + for (X11Client *c : Workspace::self()->clientList()) { + c->setupCompositing(); + c->updateShadow(); + } + for (Unmanaged *c : Workspace::self()->unmanagedList()) { + c->setupCompositing(); + c->updateShadow(); + } + for (InternalClient *client : workspace()->internalClients()) { + client->setupCompositing(); + client->updateShadow(); + } + + if (auto *server = waylandServer()) { + const auto clients = server->clients(); + for (AbstractClient *c : clients) { + c->setupCompositing(); + c->updateShadow(); + } + } + + m_state = State::On; + emit compositingToggled(true); + + if (m_releaseSelectionTimer.isActive()) { + m_releaseSelectionTimer.stop(); + } + + // Render at least once. + addRepaintFull(); + performCompositing(); +} + +void Compositor::scheduleRepaint() +{ + if (!compositeTimer.isActive()) + setCompositeTimer(); +} + +void Compositor::stop() +{ + if (m_state == State::Off || m_state == State::Stopping) { + return; + } + m_state = State::Stopping; + emit aboutToToggleCompositing(); + + m_releaseSelectionTimer.start(); + + // Some effects might need access to effect windows when they are about to + // be destroyed, for example to unreference deleted windows, so we have to + // make sure that effect windows outlive effects. + delete effects; + effects = nullptr; + + if (Workspace::self()) { + for (X11Client *c : Workspace::self()->clientList()) { + m_scene->removeToplevel(c); + } + for (Unmanaged *c : Workspace::self()->unmanagedList()) { + m_scene->removeToplevel(c); + } + for (InternalClient *client : workspace()->internalClients()) { + m_scene->removeToplevel(client); + } + for (X11Client *c : Workspace::self()->clientList()) { + c->finishCompositing(); + } + for (Unmanaged *c : Workspace::self()->unmanagedList()) { + c->finishCompositing(); + } + for (InternalClient *client : workspace()->internalClients()) { + client->finishCompositing(); + } + if (auto *con = kwinApp()->x11Connection()) { + xcb_composite_unredirect_subwindows(con, kwinApp()->x11RootWindow(), + XCB_COMPOSITE_REDIRECT_MANUAL); + } + while (!workspace()->deletedList().isEmpty()) { + workspace()->deletedList().first()->discard(); + } + } + + if (waylandServer()) { + for (AbstractClient *c : waylandServer()->clients()) { + m_scene->removeToplevel(c); + } + for (AbstractClient *c : waylandServer()->clients()) { + c->finishCompositing(); + } + } + + delete m_scene; + m_scene = nullptr; + compositeTimer.stop(); + repaints_region = QRegion(); + + m_state = State::Off; + emit compositingToggled(false); +} + +void Compositor::destroyCompositorSelection() +{ + delete m_selectionOwner; + m_selectionOwner = nullptr; +} + +void Compositor::releaseCompositorSelection() +{ + switch (m_state) { + case State::On: + // We are compositing at the moment. Don't release. + break; + case State::Off: + if (m_selectionOwner) { + qCDebug(KWIN_CORE) << "Releasing compositor selection"; + m_selectionOwner->setOwning(false); + m_selectionOwner->release(); + } + break; + case State::Starting: + case State::Stopping: + // Still starting or shutting down the compositor. Starting might fail + // or after stopping a restart might follow. So test again later on. + m_releaseSelectionTimer.start(); + break; + } +} + +void Compositor::keepSupportProperty(xcb_atom_t atom) +{ + m_unusedSupportProperties.removeAll(atom); +} + +void Compositor::removeSupportProperty(xcb_atom_t atom) +{ + m_unusedSupportProperties << atom; + m_unusedSupportPropertyTimer.start(); +} + +void Compositor::deleteUnusedSupportProperties() +{ + if (m_state == State::Starting || m_state == State::Stopping) { + // Currently still maybe restarting the compositor. + m_unusedSupportPropertyTimer.start(); + return; + } + if (auto *con = kwinApp()->x11Connection()) { + for (const xcb_atom_t &atom : qAsConst(m_unusedSupportProperties)) { + // remove property from root window + xcb_delete_property(con, kwinApp()->x11RootWindow(), atom); + } + m_unusedSupportProperties.clear(); + } +} + +void Compositor::configChanged() +{ + reinitialize(); + addRepaintFull(); +} + +void Compositor::reinitialize() +{ + // Reparse config. Config options will be reloaded by start() + kwinApp()->config()->reparseConfiguration(); + + // Restart compositing + stop(); + start(); + + if (effects) { // start() may fail + effects->reconfigure(); + } +} + +void Compositor::addRepaint(int x, int y, int w, int h) +{ + if (m_state != State::On) { + return; + } + repaints_region += QRegion(x, y, w, h); + scheduleRepaint(); +} + +void Compositor::addRepaint(const QRect& r) +{ + if (m_state != State::On) { + return; + } + repaints_region += r; + scheduleRepaint(); +} + +void Compositor::addRepaint(const QRegion& r) +{ + if (m_state != State::On) { + return; + } + repaints_region += r; + scheduleRepaint(); +} + +void Compositor::addRepaintFull() +{ + if (m_state != State::On) { + return; + } + const QSize &s = screens()->size(); + repaints_region = QRegion(0, 0, s.width(), s.height()); + scheduleRepaint(); +} + +void Compositor::timerEvent(QTimerEvent *te) +{ + if (te->timerId() == compositeTimer.timerId()) { + performCompositing(); + } else + QObject::timerEvent(te); +} + +void Compositor::aboutToSwapBuffers() +{ + Q_ASSERT(!m_bufferSwapPending); + + m_bufferSwapPending = true; +} + +void Compositor::bufferSwapComplete() +{ + Q_ASSERT(m_bufferSwapPending); + m_bufferSwapPending = false; + + emit bufferSwapCompleted(); + + if (m_composeAtSwapCompletion) { + m_composeAtSwapCompletion = false; + performCompositing(); + } +} + +void Compositor::performCompositing() +{ + // If a buffer swap is still pending, we return to the event loop and + // continue processing events until the swap has completed. + if (m_bufferSwapPending) { + m_composeAtSwapCompletion = true; + compositeTimer.stop(); + return; + } + + // If outputs are disabled, we return to the event loop and + // continue processing events until the outputs are enabled again + if (!kwinApp()->platform()->areOutputsEnabled()) { + compositeTimer.stop(); + return; + } + + // Create a list of all windows in the stacking order + QList windows = Workspace::self()->xStackingOrder(); + QList damaged; + + // Reset the damage state of each window and fetch the damage region + // without waiting for a reply + for (Toplevel *win : windows) { + if (win->resetAndFetchDamage()) { + damaged << win; + } + } + + if (damaged.count() > 0) { + m_scene->triggerFence(); + if (auto c = kwinApp()->x11Connection()) { + xcb_flush(c); + } + } + + // Move elevated windows to the top of the stacking order + for (EffectWindow *c : static_cast(effects)->elevatedWindows()) { + Toplevel *t = static_cast(c)->window(); + windows.removeAll(t); + windows.append(t); + } + + // Get the replies + for (Toplevel *win : damaged) { + // Discard the cached lanczos texture + if (win->effectWindow()) { + const QVariant texture = win->effectWindow()->data(LanczosCacheRole); + if (texture.isValid()) { + delete static_cast(texture.value()); + win->effectWindow()->setData(LanczosCacheRole, QVariant()); + } + } + + win->getDamageRegionReply(); + } + + if (repaints_region.isEmpty() && !windowRepaintsPending()) { + m_scene->idle(); + m_timeSinceLastVBlank = fpsInterval - (options->vBlankTime() + 1); // means "start now" + // Note: It would seem here we should undo suspended unredirect, but when scenes need + // it for some reason, e.g. transformations or translucency, the next pass that does not + // need this anymore and paints normally will also reset the suspended unredirect. + // Otherwise the window would not be painted normally anyway. + compositeTimer.stop(); + return; + } + + // Skip windows that are not yet ready for being painted and if screen is locked skip windows + // that are neither lockscreen nor inputmethod windows. + // + // TODO? This cannot be used so carelessly - needs protections against broken clients, the + // window should not get focus before it's displayed, handle unredirected windows properly and + // so on. + for (Toplevel *win : windows) { + if (!win->readyForPainting()) { + windows.removeAll(win); + } + if (waylandServer() && waylandServer()->isScreenLocked()) { + if(!win->isLockScreen() && !win->isInputMethod()) { + windows.removeAll(win); + } + } + } + + QRegion repaints = repaints_region; + // clear all repaints, so that post-pass can add repaints for the next repaint + repaints_region = QRegion(); + + if (m_framesToTestForSafety > 0 && (m_scene->compositingType() & OpenGLCompositing)) { + kwinApp()->platform()->createOpenGLSafePoint(Platform::OpenGLSafePoint::PreFrame); + } + m_timeSinceLastVBlank = m_scene->paint(repaints, windows); + if (m_framesToTestForSafety > 0) { + if (m_scene->compositingType() & OpenGLCompositing) { + kwinApp()->platform()->createOpenGLSafePoint(Platform::OpenGLSafePoint::PostFrame); + } + m_framesToTestForSafety--; + if (m_framesToTestForSafety == 0 && (m_scene->compositingType() & OpenGLCompositing)) { + kwinApp()->platform()->createOpenGLSafePoint( + Platform::OpenGLSafePoint::PostLastGuardedFrame); + } + } + + if (waylandServer()) { + const auto currentTime = static_cast(m_monotonicClock.elapsed()); + for (Toplevel *win : qAsConst(windows)) { + if (auto surface = win->surface()) { + surface->frameRendered(currentTime); + } + } + if (!kwinApp()->platform()->isCursorHidden()) { + Cursors::self()->currentCursor()->markAsRendered(); + } + } + + // Stop here to ensure *we* cause the next repaint schedule - not some effect + // through m_scene->paint(). + compositeTimer.stop(); + + // Trigger at least one more pass even if there would be nothing to paint, so that scene->idle() + // is called the next time. If there would be nothing pending, it will not restart the timer and + // scheduleRepaint() would restart it again somewhen later, called from functions that + // would again add something pending. + if (m_bufferSwapPending && m_scene->syncsToVBlank()) { + m_composeAtSwapCompletion = true; + } else { + scheduleRepaint(); + } +} + +template +static bool repaintsPending(const QList &windows) +{ + return std::any_of(windows.begin(), windows.end(), + [](T *t) { return !t->repaints().isEmpty(); }); +} + +bool Compositor::windowRepaintsPending() const +{ + if (repaintsPending(Workspace::self()->clientList())) { + return true; + } + if (repaintsPending(Workspace::self()->unmanagedList())) { + return true; + } + if (repaintsPending(Workspace::self()->deletedList())) { + return true; + } + if (auto *server = waylandServer()) { + const auto &clients = server->clients(); + auto test = [](AbstractClient *c) { + return c->readyForPainting() && !c->repaints().isEmpty(); + }; + if (std::any_of(clients.begin(), clients.end(), test)) { + return true; + } + } + const auto &internalClients = workspace()->internalClients(); + auto internalTest = [] (InternalClient *client) { + return client->isShown(true) && !client->repaints().isEmpty(); + }; + if (std::any_of(internalClients.begin(), internalClients.end(), internalTest)) { + return true; + } + return false; +} + +void Compositor::setCompositeTimer() +{ + if (m_state != State::On) { + return; + } + + // Don't start the timer if we're waiting for a swap event + if (m_bufferSwapPending && m_composeAtSwapCompletion) + return; + + // Don't start the timer if all outputs are disabled + if (!kwinApp()->platform()->areOutputsEnabled()) { + return; + } + + uint waitTime = 1; + + if (m_scene->blocksForRetrace()) { + + // TODO: make vBlankTime dynamic?! + // It's required because glXWaitVideoSync will *likely* block a full frame if one enters + // a retrace pass which can last a variable amount of time, depending on the actual screen + // Now, my ooold 19" CRT can do such retrace so that 2ms are entirely sufficient, + // while another ooold 15" TFT requires about 6ms + + qint64 padding = m_timeSinceLastVBlank; + if (padding > fpsInterval) { + // We're at low repaints or spent more time in painting than the user wanted to wait + // for that frame. Align to next vblank: + padding = vBlankInterval - (padding % vBlankInterval); + } else { + // Align to the next maxFps tick: + // "remaining time of the first vsync" + "time for the other vsyncs of the frame" + padding = ((vBlankInterval - padding % vBlankInterval) + + (fpsInterval / vBlankInterval - 1) * vBlankInterval); + } + + if (padding < options->vBlankTime()) { + // We'll likely miss this frame so we add one: + waitTime = nanoToMilli(padding + vBlankInterval - options->vBlankTime()); + } else { + waitTime = nanoToMilli(padding - options->vBlankTime()); + } + } + else { // w/o blocking vsync we just jump to the next demanded tick + if (fpsInterval > m_timeSinceLastVBlank) { + waitTime = nanoToMilli(fpsInterval - m_timeSinceLastVBlank); + if (!waitTime) { + // Will ensure we don't block out the eventloop - the system's just not faster ... + waitTime = 1; + } + } + /* else if (m_scene->syncsToVBlank() && m_timeSinceLastVBlank - fpsInterval < (vBlankInterval<<1)) { + // NOTICE - "for later" ------------------------------------------------------------------ + // It can happen that we push two frames within one refresh cycle. + // Swapping will then block even with triple buffering when the GPU does not discard but + // queues frames + // now here's the mean part: if we take that as "OMG, we're late - next frame ASAP", + // there'll immediately be 2 frames in the pipe, swapping will block, we think we're + // late ... ewww + // so instead we pad to the clock again and add 2ms safety to ensure the pipe is really + // free + // NOTICE: obviously m_timeSinceLastVBlank can be too big because we're too slow as well + // So if this code was enabled, we'd needlessly half the framerate once more (15 instead of 30) + waitTime = nanoToMilli(vBlankInterval - (m_timeSinceLastVBlank - fpsInterval)%vBlankInterval) + 2; + }*/ + else { + // "0" would be sufficient here, but the compositor isn't the WMs only task. + waitTime = 1; + } + } + // Force 4fps minimum: + compositeTimer.start(qMin(waitTime, 250u), this); +} + +bool Compositor::isActive() +{ + return m_state == State::On; +} + +WaylandCompositor::WaylandCompositor(QObject *parent) + : Compositor(parent) +{ + connect(kwinApp(), &Application::x11ConnectionAboutToBeDestroyed, + this, &WaylandCompositor::destroyCompositorSelection); +} + +void WaylandCompositor::toggleCompositing() +{ + // For the shortcut. Not possible on Wayland because we always composite. +} + +void WaylandCompositor::start() +{ + if (!Compositor::setupStart()) { + // Internal setup failed, abort. + return; + } + + if (Workspace::self()) { + startupWithWorkspace(); + } else { + connect(kwinApp(), &Application::workspaceCreated, + this, &WaylandCompositor::startupWithWorkspace); + } +} + +int WaylandCompositor::refreshRate() const +{ + // TODO: This makes no sense on Wayland. First step would be to atleast + // set the refresh rate to the highest available one. Second step + // would be to not use a uniform value at all but per screen. + return KWin::currentRefreshRate(); +} + +X11Compositor::X11Compositor(QObject *parent) + : Compositor(parent) + , m_suspended(options->isUseCompositing() ? NoReasonSuspend : UserSuspend) + , m_xrrRefreshRate(0) +{ +} + +void X11Compositor::toggleCompositing() +{ + if (m_suspended) { + // Direct user call; clear all bits. + resume(AllReasonSuspend); + } else { + // But only set the user one (sufficient to suspend). + suspend(UserSuspend); + } +} + +void X11Compositor::reinitialize() +{ + // Resume compositing if suspended. + m_suspended = NoReasonSuspend; + Compositor::reinitialize(); +} + +void X11Compositor::configChanged() +{ + if (m_suspended) { + stop(); + return; + } + Compositor::configChanged(); +} + +void X11Compositor::suspend(X11Compositor::SuspendReason reason) +{ + Q_ASSERT(reason != NoReasonSuspend); + m_suspended |= reason; + + if (reason & ScriptSuspend) { + // When disabled show a shortcut how the user can get back compositing. + const auto shortcuts = KGlobalAccel::self()->shortcut( + workspace()->findChild(QStringLiteral("Suspend Compositing"))); + if (!shortcuts.isEmpty()) { + // Display notification only if there is the shortcut. + const QString message = + i18n("Desktop effects have been suspended by another application.
" + "You can resume using the '%1' shortcut.", + shortcuts.first().toString(QKeySequence::NativeText)); + KNotification::event(QStringLiteral("compositingsuspendeddbus"), message); + } + } + stop(); +} + +void X11Compositor::resume(X11Compositor::SuspendReason reason) +{ + Q_ASSERT(reason != NoReasonSuspend); + m_suspended &= ~reason; + start(); +} + +void X11Compositor::start() +{ + if (m_suspended) { + QStringList reasons; + if (m_suspended & UserSuspend) { + reasons << QStringLiteral("Disabled by User"); + } + if (m_suspended & BlockRuleSuspend) { + reasons << QStringLiteral("Disabled by Window"); + } + if (m_suspended & ScriptSuspend) { + reasons << QStringLiteral("Disabled by Script"); + } + qCDebug(KWIN_CORE) << "Compositing is suspended, reason:" << reasons; + return; + } else if (!kwinApp()->platform()->compositingPossible()) { + qCCritical(KWIN_CORE) << "Compositing is not possible"; + return; + } + if (!Compositor::setupStart()) { + // Internal setup failed, abort. + return; + } + m_xrrRefreshRate = KWin::currentRefreshRate(); + startupWithWorkspace(); +} +void X11Compositor::performCompositing() +{ + if (scene()->usesOverlayWindow() && !isOverlayWindowVisible()) { + // Return since nothing is visible. + return; + } + Compositor::performCompositing(); +} + +bool X11Compositor::checkForOverlayWindow(WId w) const +{ + if (!scene()) { + // No scene, so it cannot be the overlay window. + return false; + } + if (!scene()->overlayWindow()) { + // No overlay window, it cannot be the overlay. + return false; + } + // Compare the window ID's. + return w == scene()->overlayWindow()->window(); +} + +bool X11Compositor::isOverlayWindowVisible() const +{ + if (!scene()) { + return false; + } + if (!scene()->overlayWindow()) { + return false; + } + return scene()->overlayWindow()->isVisible(); +} + +int X11Compositor::refreshRate() const +{ + return m_xrrRefreshRate; +} + +void X11Compositor::updateClientCompositeBlocking(X11Client *c) +{ + if (c) { + if (c->isBlockingCompositing()) { + // Do NOT attempt to call suspend(true) from within the eventchain! + if (!(m_suspended & BlockRuleSuspend)) + QMetaObject::invokeMethod(this, [this]() { + suspend(BlockRuleSuspend); + }, Qt::QueuedConnection); + } + } + else if (m_suspended & BlockRuleSuspend) { + // If !c we just check if we can resume in case a blocking client was lost. + bool shouldResume = true; + + for (auto it = Workspace::self()->clientList().constBegin(); + it != Workspace::self()->clientList().constEnd(); ++it) { + if ((*it)->isBlockingCompositing()) { + shouldResume = false; + break; + } + } + if (shouldResume) { + // Do NOT attempt to call suspend(false) from within the eventchain! + QMetaObject::invokeMethod(this, [this]() { + resume(BlockRuleSuspend); + }, Qt::QueuedConnection); + } + } +} + +X11Compositor *X11Compositor::self() +{ + return qobject_cast(Compositor::self()); +} + +} + +// included for CompositorSelectionOwner +#include "composite.moc" diff --git a/composite.h b/composite.h new file mode 100644 index 0000000..61aadb9 --- /dev/null +++ b/composite.h @@ -0,0 +1,263 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2011 Arthur Arlt + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include + +#include +#include +#include +#include +#include + +namespace KWin +{ +class CompositorSelectionOwner; +class Scene; +class X11Client; + +class KWIN_EXPORT Compositor : public QObject +{ + Q_OBJECT +public: + enum class State { + On = 0, + Off, + Starting, + Stopping + }; + + ~Compositor() override; + static Compositor *self(); + + // when adding repaints caused by a window, you probably want to use + // either Toplevel::addRepaint() or Toplevel::addWorkspaceRepaint() + void addRepaint(const QRect& r); + void addRepaint(const QRegion& r); + void addRepaint(int x, int y, int w, int h); + void addRepaintFull(); + + /** + * Schedules a new repaint if no repaint is currently scheduled. + */ + void scheduleRepaint(); + + /** + * Notifies the compositor that SwapBuffers() is about to be called. + * Rendering of the next frame will be deferred until bufferSwapComplete() + * is called. + */ + void aboutToSwapBuffers(); + + /** + * Notifies the compositor that a pending buffer swap has completed. + */ + void bufferSwapComplete(); + + /** + * Toggles compositing, that is if the Compositor is suspended it will be resumed + * and if the Compositor is active it will be suspended. + * Invoked by keybinding (shortcut default: Shift + Alt + F12). + */ + virtual void toggleCompositing() = 0; + + /** + * Re-initializes the Compositor completely. + * Connected to the D-Bus signal org.kde.KWin /KWin reinitCompositing + */ + virtual void reinitialize(); + + /** + * Whether the Compositor is active. That is a Scene is present and the Compositor is + * not shutting down itself. + */ + bool isActive(); + virtual int refreshRate() const = 0; + + Scene *scene() const { + return m_scene; + } + + /** + * @brief Static check to test whether the Compositor is available and active. + * + * @return bool @c true if there is a Compositor and it is active, @c false otherwise + */ + static bool compositing() { + return s_compositor != nullptr && s_compositor->isActive(); + } + + // for delayed supportproperty management of effects + void keepSupportProperty(xcb_atom_t atom); + void removeSupportProperty(xcb_atom_t atom); + +Q_SIGNALS: + void compositingToggled(bool active); + void aboutToDestroy(); + void aboutToToggleCompositing(); + void sceneCreated(); + void bufferSwapCompleted(); + +protected: + explicit Compositor(QObject *parent = nullptr); + void timerEvent(QTimerEvent *te) override; + + virtual void start() = 0; + void stop(); + + /** + * @brief Prepares start. + * @return bool @c true if start should be continued and @c if not. + */ + bool setupStart(); + /** + * Continues the startup after Scene And Workspace are created + */ + void startupWithWorkspace(); + virtual void performCompositing(); + + virtual void configChanged(); + + void destroyCompositorSelection(); + + static Compositor *s_compositor; + +private: + void initializeX11(); + void cleanupX11(); + + void setCompositeTimer(); + bool windowRepaintsPending() const; + + void releaseCompositorSelection(); + void deleteUnusedSupportProperties(); + + State m_state; + + QBasicTimer compositeTimer; + CompositorSelectionOwner *m_selectionOwner; + QTimer m_releaseSelectionTimer; + QList m_unusedSupportProperties; + QTimer m_unusedSupportPropertyTimer; + qint64 vBlankInterval, fpsInterval; + QRegion repaints_region; + + qint64 m_timeSinceLastVBlank; + + Scene *m_scene; + + bool m_bufferSwapPending; + bool m_composeAtSwapCompletion; + + int m_framesToTestForSafety = 3; + QElapsedTimer m_monotonicClock; +}; + +class KWIN_EXPORT WaylandCompositor : public Compositor +{ + Q_OBJECT +public: + static WaylandCompositor *create(QObject *parent = nullptr); + + int refreshRate() const override; + + void toggleCompositing() override; + +protected: + void start() override; + +private: + explicit WaylandCompositor(QObject *parent); +}; + +class KWIN_EXPORT X11Compositor : public Compositor +{ + Q_OBJECT +public: + enum SuspendReason { + NoReasonSuspend = 0, + UserSuspend = 1 << 0, + BlockRuleSuspend = 1 << 1, + ScriptSuspend = 1 << 2, + AllReasonSuspend = 0xff + }; + Q_DECLARE_FLAGS(SuspendReasons, SuspendReason) + Q_ENUM(SuspendReason) + Q_FLAG(SuspendReasons) + + static X11Compositor *create(QObject *parent = nullptr); + + /** + * @brief Suspends the Compositor if it is currently active. + * + * Note: it is possible that the Compositor is not able to suspend. Use isActive to check + * whether the Compositor has been suspended. + * + * @return void + * @see resume + * @see isActive + */ + void suspend(SuspendReason reason); + + /** + * @brief Resumes the Compositor if it is currently suspended. + * + * Note: it is possible that the Compositor cannot be resumed, that is there might be Clients + * blocking the usage of Compositing or the Scene might be broken. Use isActive to check + * whether the Compositor has been resumed. Also check isCompositingPossible and + * isOpenGLBroken. + * + * Note: The starting of the Compositor can require some time and is partially done threaded. + * After this method returns the setup may not have been completed. + * + * @return void + * @see suspend + * @see isActive + * @see isCompositingPossible + * @see isOpenGLBroken + */ + void resume(SuspendReason reason); + + void toggleCompositing() override; + void reinitialize() override; + + void configChanged() override; + + /** + * Checks whether @p w is the Scene's overlay window. + */ + bool checkForOverlayWindow(WId w) const; + + /** + * @returns Whether the Scene's Overlay X Window is visible. + */ + bool isOverlayWindowVisible() const; + + int refreshRate() const override; + + void updateClientCompositeBlocking(X11Client *client = nullptr); + + static X11Compositor *self(); + +protected: + void start() override; + void performCompositing() override; + +private: + explicit X11Compositor(QObject *parent); + /** + * Whether the Compositor is currently suspended, 8 bits encoding the reason + */ + SuspendReasons m_suspended; + + int m_xrrRefreshRate; +}; + +} diff --git a/config-kwin.h.cmake b/config-kwin.h.cmake new file mode 100644 index 0000000..25003e0 --- /dev/null +++ b/config-kwin.h.cmake @@ -0,0 +1,46 @@ +#cmakedefine KWIN_BUILD_DECORATIONS 1 +#cmakedefine KWIN_BUILD_TABBOX 1 +#cmakedefine KWIN_BUILD_ACTIVITIES 1 +#define KWIN_NAME "${KWIN_NAME}" +#define KWIN_INTERNAL_NAME_X11 "${KWIN_INTERNAL_NAME_X11}" +#define KWIN_CONFIG "${KWIN_NAME}rc" +#define KWIN_VERSION_STRING "${PROJECT_VERSION}" +#define XCB_VERSION_STRING "${XCB_VERSION}" +#define KWIN_KILLER_BIN "${CMAKE_INSTALL_FULL_LIBEXECDIR}/kwin_killer_helper" +#define KWIN_RULES_DIALOG_BIN "${CMAKE_INSTALL_FULL_LIBEXECDIR}/kwin_rules_dialog" +#define KWIN_XCLIPBOARD_SYNC_BIN "${CMAKE_INSTALL_FULL_LIBEXECDIR}/org_kde_kwin_xclipboard_syncer" +#cmakedefine01 HAVE_X11_XCB +#cmakedefine01 HAVE_X11_XINPUT +#cmakedefine01 HAVE_DRM +#cmakedefine01 HAVE_GBM +#cmakedefine01 HAVE_EGL_STREAMS +#cmakedefine01 HAVE_LIBHYBRIS +#cmakedefine01 HAVE_WAYLAND_EGL +#cmakedefine01 HAVE_SYS_PRCTL_H +#cmakedefine01 HAVE_PR_SET_DUMPABLE +#cmakedefine01 HAVE_PR_SET_PDEATHSIG +#cmakedefine01 HAVE_SYS_PROCCTL_H +#cmakedefine01 HAVE_PROC_TRACE_CTL +#cmakedefine01 HAVE_SYS_SYSMACROS_H +#cmakedefine01 HAVE_BREEZE_DECO +#cmakedefine01 HAVE_LIBCAP +#cmakedefine01 HAVE_SCHED_RESET_ON_FORK +#cmakedefine01 HAVE_ACCESSIBILITY +#if HAVE_BREEZE_DECO +#define BREEZE_KDECORATION_PLUGIN_ID "${BREEZE_KDECORATION_PLUGIN_ID}" +#endif + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_UNISTD_H 1 + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_MALLOC_H 1 + +#cmakedefine XCB_ICCCM_FOUND 1 +#ifndef XCB_ICCCM_FOUND +#define XCB_ICCCM_WM_STATE_WITHDRAWN 0 +#define XCB_ICCCM_WM_STATE_NORMAL 1 +#define XCB_ICCCM_WM_STATE_ICONIC 3 +#endif + +#cmakedefine PipeWire_FOUND 1 diff --git a/cursor.cpp b/cursor.cpp new file mode 100644 index 0000000..9b6f69b --- /dev/null +++ b/cursor.cpp @@ -0,0 +1,494 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "cursor.h" +// kwin +#include +#include "input.h" +#include "keyboard_input.h" +#include "main.h" +#include "platform.h" +#include "utils.h" +#include "xcbutils.h" +// KDE +#include +#include +// Qt +#include +#include +#include +#include + +namespace KWin +{ +Cursors *Cursors::s_self = nullptr; +Cursors *Cursors::self() { + if (!s_self) + s_self = new Cursors; + return s_self; +} + +void Cursors::addCursor(Cursor* cursor) +{ + Q_ASSERT(!m_cursors.contains(cursor)); + m_cursors += cursor; + + connect(cursor, &Cursor::posChanged, this, [this, cursor] (const QPoint &pos) { + setCurrentCursor(cursor); + Q_EMIT positionChanged(cursor, pos); + }); +} + +void Cursors::removeCursor(Cursor* cursor) +{ + m_cursors.removeOne(cursor); + if (m_currentCursor == cursor) { + if (m_cursors.isEmpty()) + m_currentCursor = nullptr; + else + setCurrentCursor(m_cursors.constFirst()); + } + if (m_mouse == cursor) { + m_mouse = nullptr; + } +} + +void Cursors::setCurrentCursor(Cursor* cursor) +{ + if (m_currentCursor == cursor) + return; + + Q_ASSERT(m_cursors.contains(cursor) || !cursor); + + if (m_currentCursor) { + disconnect(m_currentCursor, &Cursor::rendered, this, &Cursors::currentCursorRendered); + disconnect(m_currentCursor, &Cursor::cursorChanged, this, &Cursors::emitCurrentCursorChanged); + } + m_currentCursor = cursor; + connect(m_currentCursor, &Cursor::rendered, this, &Cursors::currentCursorRendered); + connect(m_currentCursor, &Cursor::cursorChanged, this, &Cursors::emitCurrentCursorChanged); + + Q_EMIT currentCursorChanged(m_currentCursor); +} + +void Cursors::emitCurrentCursorChanged() +{ + Q_EMIT currentCursorChanged(m_currentCursor); +} + +Cursor::Cursor(QObject *parent) + : QObject(parent) + , m_mousePollingCounter(0) + , m_cursorTrackingCounter(0) + , m_themeName(defaultThemeName()) + , m_themeSize(defaultThemeSize()) +{ + loadThemeSettings(); + QDBusConnection::sessionBus().connect(QString(), QStringLiteral("/KGlobalSettings"), QStringLiteral("org.kde.KGlobalSettings"), + QStringLiteral("notifyChange"), this, SLOT(slotKGlobalSettingsNotifyChange(int,int))); +} + +Cursor::~Cursor() +{ + Cursors::self()->removeCursor(this); +} + +void Cursor::loadThemeSettings() +{ + QString themeName = QString::fromUtf8(qgetenv("XCURSOR_THEME")); + bool ok = false; + // XCURSOR_SIZE might not be set (e.g. by startkde) + const uint themeSize = qEnvironmentVariableIntValue("XCURSOR_SIZE", &ok); + if (!themeName.isEmpty() && ok) { + updateTheme(themeName, themeSize); + return; + } + // didn't get from environment variables, read from config file + loadThemeFromKConfig(); +} + +void Cursor::loadThemeFromKConfig() +{ + KConfigGroup mousecfg(InputConfig::self()->inputConfig(), "Mouse"); + const QString themeName = mousecfg.readEntry("cursorTheme", defaultThemeName()); + const uint themeSize = mousecfg.readEntry("cursorSize", defaultThemeSize()); + updateTheme(themeName, themeSize); +} + +void Cursor::updateTheme(const QString &name, int size) +{ + if (m_themeName != name || m_themeSize != size) { + m_themeName = name; + m_themeSize = size; + emit themeChanged(); + } +} + +void Cursor::slotKGlobalSettingsNotifyChange(int type, int arg) +{ +// #endif + Q_UNUSED(arg) + if (type == 5 /*CursorChanged*/) { + InputConfig::self()->inputConfig()->reparseConfiguration(); + loadThemeFromKConfig(); + // sync to environment + qputenv("XCURSOR_THEME", m_themeName.toUtf8()); + qputenv("XCURSOR_SIZE", QByteArray::number(m_themeSize)); + } +} + +QRect Cursor::geometry() const +{ + return rect().translated(m_pos - hotspot()); +} + +QRect Cursor::rect() const +{ + return QRect(QPoint(0, 0), image().size() / image().devicePixelRatio()); +} + +QPoint Cursor::pos() +{ + doGetPos(); + return m_pos; +} + +void Cursor::setPos(const QPoint &pos) +{ + // first query the current pos to not warp to the already existing pos + if (pos == m_pos) { + return; + } + m_pos = pos; + doSetPos(); +} + +void Cursor::setPos(int x, int y) +{ + setPos(QPoint(x, y)); +} + +void Cursor::updateCursor(const QImage &image, const QPoint &hotspot) +{ + m_image = image; + m_hotspot = hotspot; + Q_EMIT cursorChanged(); +} + +xcb_cursor_t Cursor::getX11Cursor(CursorShape shape) +{ + Q_UNUSED(shape) + return XCB_CURSOR_NONE; +} + +xcb_cursor_t Cursor::getX11Cursor(const QByteArray &name) +{ + Q_UNUSED(name) + return XCB_CURSOR_NONE; +} + +xcb_cursor_t Cursor::x11Cursor(CursorShape shape) +{ + return getX11Cursor(shape); +} + +xcb_cursor_t Cursor::x11Cursor(const QByteArray &name) +{ + return getX11Cursor(name); +} + +void Cursor::doSetPos() +{ + emit posChanged(m_pos); +} + +void Cursor::doGetPos() +{ +} + +void Cursor::updatePos(const QPoint &pos) +{ + if (m_pos == pos) { + return; + } + m_pos = pos; + emit posChanged(m_pos); +} + +void Cursor::startMousePolling() +{ + ++m_mousePollingCounter; + if (m_mousePollingCounter == 1) { + doStartMousePolling(); + } +} + +void Cursor::stopMousePolling() +{ + Q_ASSERT(m_mousePollingCounter > 0); + --m_mousePollingCounter; + if (m_mousePollingCounter == 0) { + doStopMousePolling(); + } +} + +void Cursor::doStartMousePolling() +{ +} + +void Cursor::doStopMousePolling() +{ +} + +void Cursor::startCursorTracking() +{ + ++m_cursorTrackingCounter; + if (m_cursorTrackingCounter == 1) { + doStartCursorTracking(); + } +} + +void Cursor::stopCursorTracking() +{ + Q_ASSERT(m_cursorTrackingCounter > 0); + --m_cursorTrackingCounter; + if (m_cursorTrackingCounter == 0) { + doStopCursorTracking(); + } +} + +void Cursor::doStartCursorTracking() +{ +} + +void Cursor::doStopCursorTracking() +{ +} + +QVector Cursor::cursorAlternativeNames(const QByteArray &name) +{ + static const QHash> alternatives = { + {QByteArrayLiteral("left_ptr"), {QByteArrayLiteral("arrow"), + QByteArrayLiteral("dnd-none"), + QByteArrayLiteral("op_left_arrow")}}, + {QByteArrayLiteral("cross"), {QByteArrayLiteral("crosshair"), + QByteArrayLiteral("diamond-cross"), + QByteArrayLiteral("cross-reverse")}}, + {QByteArrayLiteral("up_arrow"), {QByteArrayLiteral("center_ptr"), + QByteArrayLiteral("sb_up_arrow"), + QByteArrayLiteral("centre_ptr")}}, + {QByteArrayLiteral("wait"), {QByteArrayLiteral("watch"), + QByteArrayLiteral("progress")}}, + {QByteArrayLiteral("ibeam"), {QByteArrayLiteral("xterm"), + QByteArrayLiteral("text")}}, + {QByteArrayLiteral("size_all"), {QByteArrayLiteral("fleur")}}, + {QByteArrayLiteral("pointing_hand"), {QByteArrayLiteral("hand2"), + QByteArrayLiteral("hand"), + QByteArrayLiteral("hand1"), + QByteArrayLiteral("pointer"), + QByteArrayLiteral("e29285e634086352946a0e7090d73106"), + QByteArrayLiteral("9d800788f1b08800ae810202380a0822")}}, + {QByteArrayLiteral("size_ver"), {QByteArrayLiteral("00008160000006810000408080010102"), + QByteArrayLiteral("sb_v_double_arrow"), + QByteArrayLiteral("v_double_arrow"), + QByteArrayLiteral("n-resize"), + QByteArrayLiteral("s-resize"), + QByteArrayLiteral("col-resize"), + QByteArrayLiteral("top_side"), + QByteArrayLiteral("bottom_side"), + QByteArrayLiteral("base_arrow_up"), + QByteArrayLiteral("base_arrow_down"), + QByteArrayLiteral("based_arrow_down"), + QByteArrayLiteral("based_arrow_up")}}, + {QByteArrayLiteral("size_hor"), {QByteArrayLiteral("028006030e0e7ebffc7f7070c0600140"), + QByteArrayLiteral("sb_h_double_arrow"), + QByteArrayLiteral("h_double_arrow"), + QByteArrayLiteral("e-resize"), + QByteArrayLiteral("w-resize"), + QByteArrayLiteral("row-resize"), + QByteArrayLiteral("right_side"), + QByteArrayLiteral("left_side")}}, + {QByteArrayLiteral("size_bdiag"), {QByteArrayLiteral("fcf1c3c7cd4491d801f1e1c78f100000"), + QByteArrayLiteral("fd_double_arrow"), + QByteArrayLiteral("bottom_left_corner"), + QByteArrayLiteral("top_right_corner")}}, + {QByteArrayLiteral("size_fdiag"), {QByteArrayLiteral("c7088f0f3e6c8088236ef8e1e3e70000"), + QByteArrayLiteral("bd_double_arrow"), + QByteArrayLiteral("bottom_right_corner"), + QByteArrayLiteral("top_left_corner")}}, + {QByteArrayLiteral("whats_this"), {QByteArrayLiteral("d9ce0ab605698f320427677b458ad60b"), + QByteArrayLiteral("left_ptr_help"), + QByteArrayLiteral("help"), + QByteArrayLiteral("question_arrow"), + QByteArrayLiteral("dnd-ask"), + QByteArrayLiteral("5c6cd98b3f3ebcb1f9c7f1c204630408")}}, + {QByteArrayLiteral("split_h"), {QByteArrayLiteral("14fef782d02440884392942c11205230"), + QByteArrayLiteral("size_hor")}}, + {QByteArrayLiteral("split_v"), {QByteArrayLiteral("2870a09082c103050810ffdffffe0204"), + QByteArrayLiteral("size_ver")}}, + {QByteArrayLiteral("forbidden"), {QByteArrayLiteral("03b6e0fcb3499374a867c041f52298f0"), + QByteArrayLiteral("circle"), + QByteArrayLiteral("dnd-no-drop"), + QByteArrayLiteral("not-allowed")}}, + {QByteArrayLiteral("left_ptr_watch"), {QByteArrayLiteral("3ecb610c1bf2410f44200f48c40d3599"), + QByteArrayLiteral("00000000000000020006000e7e9ffc3f"), + QByteArrayLiteral("08e8e1c95fe2fc01f976f1e063a24ccd")}}, + {QByteArrayLiteral("openhand"), {QByteArrayLiteral("9141b49c8149039304290b508d208c40"), + QByteArrayLiteral("all_scroll"), + QByteArrayLiteral("all-scroll")}}, + {QByteArrayLiteral("closedhand"), {QByteArrayLiteral("05e88622050804100c20044008402080"), + QByteArrayLiteral("4498f0e0c1937ffe01fd06f973665830"), + QByteArrayLiteral("9081237383d90e509aa00f00170e968f"), + QByteArrayLiteral("fcf21c00b30f7e3f83fe0dfd12e71cff")}}, + {QByteArrayLiteral("dnd-link"), {QByteArrayLiteral("link"), + QByteArrayLiteral("alias"), + QByteArrayLiteral("3085a0e285430894940527032f8b26df"), + QByteArrayLiteral("640fb0e74195791501fd1ed57b41487f"), + QByteArrayLiteral("a2a266d0498c3104214a47bd64ab0fc8")}}, + {QByteArrayLiteral("dnd-copy"), {QByteArrayLiteral("copy"), + QByteArrayLiteral("1081e37283d90000800003c07f3ef6bf"), + QByteArrayLiteral("6407b0e94181790501fd1e167b474872"), + QByteArrayLiteral("b66166c04f8c3109214a4fbd64a50fc8")}}, + {QByteArrayLiteral("dnd-move"), {QByteArrayLiteral("move")}}, + {QByteArrayLiteral("sw-resize"), {QByteArrayLiteral("size_bdiag"), + QByteArrayLiteral("fcf1c3c7cd4491d801f1e1c78f100000"), + QByteArrayLiteral("fd_double_arrow"), + QByteArrayLiteral("bottom_left_corner")}}, + {QByteArrayLiteral("se-resize"), {QByteArrayLiteral("size_fdiag"), + QByteArrayLiteral("c7088f0f3e6c8088236ef8e1e3e70000"), + QByteArrayLiteral("bd_double_arrow"), + QByteArrayLiteral("bottom_right_corner")}}, + {QByteArrayLiteral("ne-resize"), {QByteArrayLiteral("size_bdiag"), + QByteArrayLiteral("fcf1c3c7cd4491d801f1e1c78f100000"), + QByteArrayLiteral("fd_double_arrow"), + QByteArrayLiteral("top_right_corner")}}, + {QByteArrayLiteral("nw-resize"), {QByteArrayLiteral("size_fdiag"), + QByteArrayLiteral("c7088f0f3e6c8088236ef8e1e3e70000"), + QByteArrayLiteral("bd_double_arrow"), + QByteArrayLiteral("top_left_corner")}}, + {QByteArrayLiteral("n-resize"), {QByteArrayLiteral("size_ver"), + QByteArrayLiteral("00008160000006810000408080010102"), + QByteArrayLiteral("sb_v_double_arrow"), + QByteArrayLiteral("v_double_arrow"), + QByteArrayLiteral("col-resize"), + QByteArrayLiteral("top_side")}}, + {QByteArrayLiteral("e-resize"), {QByteArrayLiteral("size_hor"), + QByteArrayLiteral("028006030e0e7ebffc7f7070c0600140"), + QByteArrayLiteral("sb_h_double_arrow"), + QByteArrayLiteral("h_double_arrow"), + QByteArrayLiteral("row-resize"), + QByteArrayLiteral("left_side")}}, + {QByteArrayLiteral("s-resize"), {QByteArrayLiteral("size_ver"), + QByteArrayLiteral("00008160000006810000408080010102"), + QByteArrayLiteral("sb_v_double_arrow"), + QByteArrayLiteral("v_double_arrow"), + QByteArrayLiteral("col-resize"), + QByteArrayLiteral("bottom_side")}}, + {QByteArrayLiteral("w-resize"), {QByteArrayLiteral("size_hor"), + QByteArrayLiteral("028006030e0e7ebffc7f7070c0600140"), + QByteArrayLiteral("sb_h_double_arrow"), + QByteArrayLiteral("h_double_arrow"), + QByteArrayLiteral("right_side")}} + }; + auto it = alternatives.find(name); + if (it != alternatives.end()) { + return it.value(); + } + return QVector(); +} + +QString Cursor::defaultThemeName() +{ + return QStringLiteral("default"); +} + +int Cursor::defaultThemeSize() +{ + return 24; +} + +QByteArray CursorShape::name() const +{ + switch (m_shape) { + case Qt::ArrowCursor: + return QByteArrayLiteral("left_ptr"); + case Qt::UpArrowCursor: + return QByteArrayLiteral("up_arrow"); + case Qt::CrossCursor: + return QByteArrayLiteral("cross"); + case Qt::WaitCursor: + return QByteArrayLiteral("wait"); + case Qt::IBeamCursor: + return QByteArrayLiteral("ibeam"); + case Qt::SizeVerCursor: + return QByteArrayLiteral("size_ver"); + case Qt::SizeHorCursor: + return QByteArrayLiteral("size_hor"); + case Qt::SizeBDiagCursor: + return QByteArrayLiteral("size_bdiag"); + case Qt::SizeFDiagCursor: + return QByteArrayLiteral("size_fdiag"); + case Qt::SizeAllCursor: + return QByteArrayLiteral("size_all"); + case Qt::SplitVCursor: + return QByteArrayLiteral("split_v"); + case Qt::SplitHCursor: + return QByteArrayLiteral("split_h"); + case Qt::PointingHandCursor: + return QByteArrayLiteral("pointing_hand"); + case Qt::ForbiddenCursor: + return QByteArrayLiteral("forbidden"); + case Qt::OpenHandCursor: + return QByteArrayLiteral("openhand"); + case Qt::ClosedHandCursor: + return QByteArrayLiteral("closedhand"); + case Qt::WhatsThisCursor: + return QByteArrayLiteral("whats_this"); + case Qt::BusyCursor: + return QByteArrayLiteral("left_ptr_watch"); + case Qt::DragMoveCursor: + return QByteArrayLiteral("dnd-move"); + case Qt::DragCopyCursor: + return QByteArrayLiteral("dnd-copy"); + case Qt::DragLinkCursor: + return QByteArrayLiteral("dnd-link"); + case KWin::ExtendedCursor::SizeNorthEast: + return QByteArrayLiteral("ne-resize"); + case KWin::ExtendedCursor::SizeNorth: + return QByteArrayLiteral("n-resize"); + case KWin::ExtendedCursor::SizeNorthWest: + return QByteArrayLiteral("nw-resize"); + case KWin::ExtendedCursor::SizeEast: + return QByteArrayLiteral("e-resize"); + case KWin::ExtendedCursor::SizeWest: + return QByteArrayLiteral("w-resize"); + case KWin::ExtendedCursor::SizeSouthEast: + return QByteArrayLiteral("se-resize"); + case KWin::ExtendedCursor::SizeSouth: + return QByteArrayLiteral("s-resize"); + case KWin::ExtendedCursor::SizeSouthWest: + return QByteArrayLiteral("sw-resize"); + default: + return QByteArray(); + } +} + +InputConfig *InputConfig::s_self = nullptr; +InputConfig *InputConfig::self() { + if (!s_self) + s_self = new InputConfig; + return s_self; +} + +InputConfig::InputConfig() + : m_inputConfig(KSharedConfig::openConfig(QStringLiteral("kcminputrc"), KConfig::NoGlobals)) +{ +} + +} // namespace diff --git a/cursor.h b/cursor.h new file mode 100644 index 0000000..da70763 --- /dev/null +++ b/cursor.h @@ -0,0 +1,353 @@ + /* + 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 KWIN_CURSOR_H +#define KWIN_CURSOR_H +// kwin +#include +// Qt +#include +#include +#include +// KF +#include +// xcb +#include + +class QTimer; + +namespace KWin +{ + +namespace ExtendedCursor { +/** + * Extension of Qt::CursorShape with values not currently present there + */ +enum Shape { + SizeNorthWest = 0x100 + 0, + SizeNorth = 0x100 + 1, + SizeNorthEast = 0x100 + 2, + SizeEast = 0x100 + 3, + SizeWest = 0x100 + 4, + SizeSouthEast = 0x100 + 5, + SizeSouth = 0x100 + 6, + SizeSouthWest = 0x100 + 7 +}; +} + +/** + * @brief Wrapper round Qt::CursorShape with extensions enums into a single entity + */ +class KWIN_EXPORT CursorShape { +public: + CursorShape() = default; + CursorShape(Qt::CursorShape qtShape) { + m_shape = qtShape; + } + CursorShape(KWin::ExtendedCursor::Shape kwinShape) { + m_shape = kwinShape; + } + bool operator==(const CursorShape &o) const { + return m_shape == o.m_shape; + } + operator int() const { + return m_shape; + } + /** + * @brief The name of a cursor shape in the theme. + */ + QByteArray name() const; +private: + int m_shape = Qt::ArrowCursor; +}; + +/** + * @short Replacement for QCursor. + * + * This class provides a similar API to QCursor and should be preferred inside KWin. It allows to + * get the position and warp the mouse cursor with static methods just like QCursor. It also provides + * the possibility to get an X11 cursor for a Qt::CursorShape - a functionality lost in Qt 5's QCursor + * implementation. + * + * In addition the class provides a mouse polling facility as required by e.g. Effects and ScreenEdges + * and emits signals when the mouse position changes. In opposite to QCursor this class is a QObject + * and cannot be constructed. Instead it provides a singleton getter, though the most important + * methods are wrapped in a static method, just like QCursor. + * + * The actual implementation is split into two parts: a system independent interface and a windowing + * system specific subclass. So far only an X11 backend is implemented which uses query pointer to + * fetch the position and warp pointer to set the position. It uses a timer based mouse polling and + * can provide X11 cursors through the XCursor library. + */ +class KWIN_EXPORT Cursor : public QObject +{ + Q_OBJECT +public: + Cursor(QObject* parent); + ~Cursor() override; + void startMousePolling(); + void stopMousePolling(); + /** + * @brief Enables tracking changes of cursor images. + * + * After enabling cursor change tracking the signal cursorChanged will be emitted + * whenever a change to the cursor image is recognized. + * + * Use stopCursorTracking to no longer emit this signal. Note: the signal will be + * emitted until each call of this method has been matched with a call to stopCursorTracking. + * + * This tracking is not about pointer position tracking. + * @see stopCursorTracking + * @see cursorChanged + */ + void startCursorTracking(); + /** + * @brief Disables tracking changes of cursor images. + * + * Only call after using startCursorTracking. + * + * @see startCursorTracking + */ + void stopCursorTracking(); + + /** + * @brief The name of the currently used Cursor theme. + * + * @return const QString& + */ + const QString &themeName() const; + /** + * @brief The size of the currently used Cursor theme. + * + * @return int + */ + int themeSize() const; + /** + * @return list of alternative names for the cursor with @p name + */ + static QVector cursorAlternativeNames(const QByteArray &name); + /** + * Returns the default Xcursor theme name. + */ + static QString defaultThemeName(); + /** + * Returns the default Xcursor theme size. + */ + static int defaultThemeSize(); + + /** + * Returns the current cursor position. This method does an update of the mouse position if + * needed. It's save to call it multiple times. + * + * Implementing subclasses should prefer to use currentPos which is not performing a check + * for update. + */ + QPoint pos(); + /** + * Warps the mouse cursor to new @p pos. + */ + void setPos(const QPoint &pos); + void setPos(int x, int y); + xcb_cursor_t x11Cursor(CursorShape shape); + /** + * Notice: if available always use the CursorShape variant to avoid cache duplicates for + * ambiguous cursor names in the non existing cursor name specification + */ + xcb_cursor_t x11Cursor(const QByteArray &name); + + QImage image() const { return m_image; } + QPoint hotspot() const { return m_hotspot; } + QRect geometry() const; + QRect rect() const; + + void updateCursor(const QImage &image, const QPoint &hotspot); + void markAsRendered() { + Q_EMIT rendered(geometry()); + } + +Q_SIGNALS: + void posChanged(const QPoint& pos); + void mouseChanged(const QPoint& pos, const QPoint& oldpos, + Qt::MouseButtons buttons, Qt::MouseButtons oldbuttons, + Qt::KeyboardModifiers modifiers, Qt::KeyboardModifiers oldmodifiers); + /** + * @brief Signal emitted when the cursor image changes. + * + * To enable these signals use startCursorTracking. + * + * @see startCursorTracking + * @see stopCursorTracking + */ + void cursorChanged(); + void themeChanged(); + + void rendered(const QRect &geometry); + +protected: + /** + * Called from x11Cursor to actually retrieve the X11 cursor. Base implementation returns + * a null cursor, an implementing subclass should implement this method if it can provide X11 + * mouse cursors. + */ + virtual xcb_cursor_t getX11Cursor(CursorShape shape); + /** + * Called from x11Cursor to actually retrieve the X11 cursor. Base implementation returns + * a null cursor, an implementing subclass should implement this method if it can provide X11 + * mouse cursors. + */ + virtual xcb_cursor_t getX11Cursor(const QByteArray &name); + /** + * Performs the actual warping of the cursor. + */ + virtual void doSetPos(); + /** + * Called from @ref pos() to allow syncing the internal position with the underlying + * system's cursor position. + */ + virtual void doGetPos(); + /** + * Called from startMousePolling when the mouse polling gets activated. Base implementation + * does nothing, inheriting classes can overwrite to e.g. start a timer. + */ + virtual void doStartMousePolling(); + /** + * Called from stopMousePolling when the mouse polling gets deactivated. Base implementation + * does nothing, inheriting classes can overwrite to e.g. stop a timer. + */ + virtual void doStopMousePolling(); + /** + * Called from startCursorTracking when cursor image tracking gets activated. Inheriting class needs + * to overwrite to enable platform specific code for the tracking. + */ + virtual void doStartCursorTracking(); + /** + * Called from stopCursorTracking when cursor image tracking gets deactivated. Inheriting class needs + * to overwrite to disable platform specific code for the tracking. + */ + virtual void doStopCursorTracking(); + bool isCursorTracking() const; + /** + * Provides the actual internal cursor position to inheriting classes. If an inheriting class needs + * access to the cursor position this method should be used instead of the static @ref pos, as + * the static method syncs with the underlying system's cursor. + */ + const QPoint ¤tPos() const; + /** + * Updates the internal position to @p pos without warping the pointer as + * setPos does. + */ + void updatePos(const QPoint &pos); + void updatePos(int x, int y); + +private Q_SLOTS: + void loadThemeSettings(); + void slotKGlobalSettingsNotifyChange(int type, int arg); + +private: + void updateTheme(const QString &name, int size); + void loadThemeFromKConfig(); + QPoint m_pos; + QPoint m_hotspot; + QImage m_image; + int m_mousePollingCounter; + int m_cursorTrackingCounter; + QString m_themeName; + int m_themeSize; +}; + + +class KWIN_EXPORT Cursors : public QObject +{ + Q_OBJECT +public: + Cursor* mouse() const { + return m_mouse; + } + + void setMouse(Cursor* mouse) { + if (m_mouse != mouse) { + m_mouse = mouse; + + addCursor(m_mouse); + setCurrentCursor(m_mouse); + } + } + + void addCursor(Cursor* cursor); + void removeCursor(Cursor* cursor); + + ///@returns the last cursor that moved + Cursor* currentCursor() const { + return m_currentCursor; + } + + static Cursors* self(); + +Q_SIGNALS: + void currentCursorChanged(Cursor* cursor); + void currentCursorRendered(const QRect &geometry); + void positionChanged(Cursor* cursor, const QPoint &position); + +private: + void emitCurrentCursorChanged(); + void setCurrentCursor(Cursor* cursor); + + static Cursors* s_self; + Cursor* m_currentCursor = nullptr; + Cursor* m_mouse = nullptr; + QVector m_cursors; +}; + +class InputConfig +{ +public: + KSharedConfigPtr inputConfig() const { + return m_inputConfig; + } + void setInputConfig(KSharedConfigPtr config) { + m_inputConfig = std::move(config); + } + + static InputConfig *self(); +private: + InputConfig(); + + KSharedConfigPtr m_inputConfig; + static InputConfig *s_self; +}; + +inline const QPoint &Cursor::currentPos() const +{ + return m_pos; +} + +inline void Cursor::updatePos(int x, int y) +{ + updatePos(QPoint(x, y)); +} + +inline const QString& Cursor::themeName() const +{ + return m_themeName; +} + +inline int Cursor::themeSize() const +{ + return m_themeSize; +} + +inline bool Cursor::isCursorTracking() const +{ + return m_cursorTrackingCounter > 0; +} + +} + +Q_DECLARE_METATYPE(KWin::CursorShape) + +#endif // KWIN_CURSOR_H diff --git a/data/CMakeLists.txt b/data/CMakeLists.txt new file mode 100644 index 0000000..77148b9 --- /dev/null +++ b/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 + KF5::ConfigCore + Qt5::Core + Qt5::DBus +) +install(TARGETS kwin5_update_default_rules DESTINATION ${LIB_INSTALL_DIR}/kconf_update_bin/) + +########### install files ############### + +install(FILES org_kde_kwin.categories DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR}) diff --git a/data/icons/16-apps-kwin.png b/data/icons/16-apps-kwin.png new file mode 100644 index 0000000000000000000000000000000000000000..2ee63b13030fd0404186e336296107f61d3f15d8 GIT binary patch literal 380 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!60wlNoGJgf6n2Mb|LpV4>-?)JUISV`@iy0W0 zH-a$Z*XFmLKtah8*NBqf{Irtt#G+J&^73-M%)IR4& zz}19<>0!VJwuf!OMMXc_?s2QXKRG@9_lIb?-Uq8J->=Y(vvjz<#L{ijnxHo4UuKQE z60_3xXm9l>=3l5QF;g;YUzbhQ0-gg??$2eb`DnY1C6Uw4@_Cr%dCqMU9`xDhdx`c* z%o4Ks=n$QDBD?tP3FcS^t1lKyPm407+OH_|nIcgyx52e*6^r}}4xuM2sv9<_)*aKE z5xC%>vA36XLhuRJ0_PU*l1FR4H*;p|y)Xkdg00002b3#c}2nbc| zMg#x=010qNS#tmY2VwvK2Vwy@dYRh*000?uMObuGZ)S9NVRB^vcXxL#X>MzCV_|S* zE^l&Yo9;Xs0005vNklKS%rPu4 z0<;T>E0Lxt0_=8PI&0$&sTT~<6#A9kx$!I}ViE?^L4B z2zuf`xtro!@EiQL06;E4vs!#q=1OkBXDJuGl2Yl53hTYe4ftlO;Fy#$dlZ`_H-M{3 zy_FIKH&_%}&}X}Lq-=9Sp1x&)rnC}4P!$1A3xXF&sWk6^RffcYJWuD0 z81^y(3_546$2e&pzT0JkI1sGXtW&qaOaLyc!zWXiH)dQM_*YdF_~oFW!E0YVveP$fCqlyqTm*0JSR5*c&N%V(ZzwyR6AlUQT>8ZI)bH54k!>3 xR^tyxGrtNmv!)U?4R?enTckl-qFU`0gunM^Jq00&uW0}P002ovPDHLkV1iTW^>hFL literal 0 HcmV?d00001 diff --git a/data/icons/48-apps-kwin.png b/data/icons/48-apps-kwin.png new file mode 100644 index 0000000000000000000000000000000000000000..ea3606ec80419f8fce8e7e527f600ddfb9913986 GIT binary patch literal 877 zcmV-z1CsoSP)MzCV_|S* zE^l&Yo9;Xs0008)NklZ8?fs!!S~3E$p8lD7Z?_qkQ;7VYA272Xt{m1bAnYPU@`2p3_;O5h}e!k{!; zbic666T&F8=LPl)d!^ZR)$UU7cHvAia8RhUOIwO|YV(K?K1l`!MTxBvMhgRdCQ^67 z?&83pQnu&+28Q*S%H3q^)R+L2ZP)Y^1b*_C)2SPSCEba@>oTIGIxI40ZC+qbjf@xd zrs%Mvx~&r?VP9TgT-YYvS~UmUSL#clWs?Z2K~Y@IGGoCTc~%{^%OnlkZoFp zy~)6rq71#142%km)~HI|ZNgw-V2@HZSUT`G0A|V$d?e!p_}6|Fz9|>*g+D=W?ukWR zLWRll0Iyo*9v}Et4sb-sc%SkBr$n~i955hcyhpi!dG`son8*Tal3t%4pi_;na1+Q@ z>GPt%aZU++iUYr>_B1)*QI88%&J_U;Fd?kdqxWXR3BVy+)d(ZYzM@$V3$-Q-0(IsV zbckH-2|%;RHnt4licLa;9}57X+Y)0=XZtJx=(Saaa2E6nPlDex2`vSI(Bm4#&)^5u z4uf$m&HyfX!1G0b(5O>iw&7%e1FC#y`DT3#MS$hCCj(d9E8J-Uz^GOkC&~jX*(b79 zX>;6BZ7M~!fpP$Uc}Te1I|ke#gtcDqsi%Yo&6N)DyMBLwcZJXe81_ABfaLuwgx3Hl zH$ayTcC00000NkvXXu0mjf DC=P*` literal 0 HcmV?d00001 diff --git a/data/icons/CMakeLists.txt b/data/icons/CMakeLists.txt new file mode 100644 index 0000000..6a1b21d --- /dev/null +++ b/data/icons/CMakeLists.txt @@ -0,0 +1,11 @@ +ecm_install_icons( + ICONS + 16-apps-kwin.png + 32-apps-kwin.png + 48-apps-kwin.png + sc-apps-kwin.svgz + DESTINATION + ${ICON_INSTALL_DIR} + THEME + hicolor +) diff --git a/data/icons/sc-apps-kwin.svgz b/data/icons/sc-apps-kwin.svgz new file mode 100644 index 0000000000000000000000000000000000000000..5248264d2a60620e92918c0e81ac0175856abb08 GIT binary patch literal 3106 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 literal 0 HcmV?d00001 diff --git a/data/org_kde_kwin.categories b/data/org_kde_kwin.categories new file mode 100644 index 0000000..faedeb5 --- /dev/null +++ b/data/org_kde_kwin.categories @@ -0,0 +1,25 @@ +kwin_core KWin Core DEFAULT_SEVERITY [CRITICAL] IDENTIFIER [KWIN_CORE] +kwin_utils KWin utils DEFAULT_SEVERITY [CRITICAL] IDENTIFIER [KWIN_UTILS] +kwin_virtualkeyboard KWin Virtual Keyboard Integration DEFAULT_SEVERITY [CRITICAL] IDENTIFIER [KWIN_VIRTUALKEYBOARD] +kwineffects KWin Effects DEFAULT_SEVERITY [CRITICAL] IDENTIFIER [KWINEFFECTS] +libkwineffects KWin Effects Library DEFAULT_SEVERITY [CRITICAL] IDENTIFIER [LIBKWINEFFECTS] +libkwinglutils KWin OpenGL utility Library DEFAULT_SEVERITY [CRITICAL] IDENTIFIER [LIBKWINGLUTILS] +libkwinxrenderutils KWin XRender utility Library DEFAULT_SEVERITY [CRITICAL] IDENTIFIER [LIBKWINXRENDERUTILS] +kwin_wayland_drm KWin Wayland (DRM backend) DEFAULT_SEVERITY [CRITICAL] IDENTIFIER [KWIN_DRM] +kwin_wayland_framebuffer KWin Wayland (Framebuffer backend) DEFAULT_SEVERITY [CRITICAL] IDENTIFIER [KWIN_FB] +kwin_wayland_hwcomposer KWin Wayland (hwcomposer backend) DEFAULT_SEVERITY [CRITICAL] IDENTIFIER [KWIN_HWCOMPOSER] +kwin_wayland_backend KWin Wayland (Wayland backend) DEFAULT_SEVERITY [CRITICAL] IDENTIFIER [KWIN_WAYLAND_BACKEND] +kwin_wayland_x11windowed KWin Wayland (X11 backend) DEFAULT_SEVERITY [CRITICAL] IDENTIFIER [KWIN_X11WINDOWED] +kwin_platform_x11_standalone KWin X11 Standalone Platform DEFAULT_SEVERITY [CRITICAL] IDENTIFIER [KWIN_X11STANDALONE] +kwin_libinput KWin Libinput Integration DEFAULT_SEVERITY [CRITICAL] IDENTIFIER [KWIN_LIBINPUT] +kwin_tabbox KWin Window Switcher DEFAULT_SEVERITY [CRITICAL] IDENTIFIER [KWIN_TABBOX] +kwin_decorations KWin Decorations DEFAULT_SEVERITY [CRITICAL] IDENTIFIER [KWIN_DECORATIONS] +kwin_scripting KWin Scripting DEFAULT_SEVERITY [CRITICAL] IDENTIFIER [KWIN_SCRIPTING] +aurorae KWin Aurorae Window Decoration Engine DEFAULT_SEVERITY [CRITICAL] IDENTIFIER [AURORAE] +kwin_xkbcommon KWin xkbcommon integration DEFAULT_SEVERITY [CRITICAL] IDENTIFIER [KWIN_XKB] +kwin_qpa_plugin KWin QtPlatformAbstraction plugin DEFAULT_SEVERITY [CRITICAL] IDENTIFIER [KWIN_QPA] +kwin_scene_xrender KWin XRender based compositor scene plugin DEFAULT_SEVERITY [CRITICAL] IDENTIFIER [KWIN_XRENDER] +kwin_scene_qpainter KWin QPainter based compositor scene plugin DEFAULT_SEVERITY [CRITICAL] IDENTIFIER [KWIN_QPAINTER] +kwin_scene_opengl KWin OpenGL based compositor scene plugins DEFAULT_SEVERITY [CRITICAL] 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/data/update_default_rules.cpp b/data/update_default_rules.cpp new file mode 100644 index 0000000..9a80c39 --- /dev/null +++ b/data/update_default_rules.cpp @@ -0,0 +1,58 @@ +/* + 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 +#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/default_rules/%1" ).arg(argv[ 1 ] )); + if( file.isEmpty()) + { + qWarning() << "File " << argv[ 1 ] << " not found!" ; + return 1; + } + KConfig src_cfg( file ); + KConfig dest_cfg("kwinrulesrc", KConfig::NoGlobals); + KConfigGroup scg(&src_cfg, "General"); + KConfigGroup dcg(&dest_cfg, "General"); + int count = scg.readEntry( "count", 0 ); + int pos = dcg.readEntry( "count", 0 ); + for( int group = 1; + group <= count; + ++group ) + { + QMap< QString, QString > entries = src_cfg.entryMap( QString::number( group )); + ++pos; + dest_cfg.deleteGroup( QString::number( pos )); + KConfigGroup dcg2 (&dest_cfg, QString::number( pos )); + for( QMap< QString, QString >::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/dbusinterface.cpp b/dbusinterface.cpp new file mode 100644 index 0000000..6832fbb --- /dev/null +++ b/dbusinterface.cpp @@ -0,0 +1,511 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +// own +#include "dbusinterface.h" +#include "compositingadaptor.h" +#include "virtualdesktopmanageradaptor.h" + +// kwin +#include "abstract_client.h" +#include "atoms.h" +#include "composite.h" +#include "debug_console.h" +#include "main.h" +#include "placement.h" +#include "platform.h" +#include "kwinadaptor.h" +#include "scene.h" +#include "workspace.h" +#include "virtualdesktops.h" +#ifdef KWIN_BUILD_ACTIVITIES +#include "activities.h" +#endif + +// Qt +#include +#include + +namespace KWin +{ + +DBusInterface::DBusInterface(QObject *parent) + : QObject(parent) + , m_serviceName(QStringLiteral("org.kde.KWin")) +{ + (void) new KWinAdaptor(this); + + QDBusConnection dbus = QDBusConnection::sessionBus(); + dbus.registerObject(QStringLiteral("/KWin"), this); + const QByteArray dBusSuffix = qgetenv("KWIN_DBUS_SERVICE_SUFFIX"); + if (!dBusSuffix.isNull()) { + m_serviceName = m_serviceName + QLatin1Char('.') + dBusSuffix; + } + if (!dbus.registerService(m_serviceName)) { + QDBusServiceWatcher *dog = new QDBusServiceWatcher(m_serviceName, dbus, QDBusServiceWatcher::WatchForUnregistration, this); + connect (dog, SIGNAL(serviceUnregistered(QString)), SLOT(becomeKWinService(QString))); + } else { + announceService(); + } + dbus.connect(QString(), QStringLiteral("/KWin"), QStringLiteral("org.kde.KWin"), QStringLiteral("reloadConfig"), + Workspace::self(), SLOT(slotReloadConfig())); + connect(kwinApp(), &Application::x11ConnectionChanged, this, &DBusInterface::announceService); +} + +void DBusInterface::becomeKWinService(const QString &service) +{ + // TODO: this watchdog exists to make really safe that we at some point get the service + // but it's probably no longer needed since we explicitly unregister the service with the deconstructor + if (service == m_serviceName && QDBusConnection::sessionBus().registerService(m_serviceName) && sender()) { + sender()->deleteLater(); // bye doggy :'( + announceService(); + } +} + +DBusInterface::~DBusInterface() +{ + QDBusConnection::sessionBus().unregisterService(m_serviceName); + // KApplication automatically also grabs org.kde.kwin, so it's often been used externally - ensure to free it as well + QDBusConnection::sessionBus().unregisterService(QStringLiteral("org.kde.kwin")); + if (kwinApp()->x11Connection()) { + xcb_delete_property(kwinApp()->x11Connection(), kwinApp()->x11RootWindow(), atoms->kwin_dbus_service); + } +} + +void DBusInterface::announceService() +{ + if (!kwinApp()->x11Connection()) { + return; + } + const QByteArray service = m_serviceName.toUtf8(); + xcb_change_property(kwinApp()->x11Connection(), XCB_PROP_MODE_REPLACE, kwinApp()->x11RootWindow(), atoms->kwin_dbus_service, + atoms->utf8_string, 8, service.size(), service.constData()); +} + +// wrap void methods with no arguments to Workspace +#define WRAP(name) \ +void DBusInterface::name() \ +{\ + Workspace::self()->name();\ +} + +WRAP(reconfigure) + +#undef WRAP + +void DBusInterface::killWindow() +{ + Workspace::self()->slotKillWindow(); +} + +#define WRAP(name) \ +void DBusInterface::name() \ +{\ + Placement::self()->name();\ +} + +WRAP(cascadeDesktop) +WRAP(unclutterDesktop) + +#undef WRAP + +// wrap returning methods with no arguments to Workspace +#define WRAP( rettype, name ) \ +rettype DBusInterface::name( ) \ +{\ + return Workspace::self()->name(); \ +} + +WRAP(QString, supportInformation) + +#undef WRAP + +bool DBusInterface::startActivity(const QString &in0) +{ +#ifdef KWIN_BUILD_ACTIVITIES + if (!Activities::self()) { + return false; + } + return Activities::self()->start(in0); +#else + Q_UNUSED(in0) + return false; +#endif +} + +bool DBusInterface::stopActivity(const QString &in0) +{ +#ifdef KWIN_BUILD_ACTIVITIES + if (!Activities::self()) { + return false; + } + return Activities::self()->stop(in0); +#else + Q_UNUSED(in0) + return false; +#endif +} + +int DBusInterface::currentDesktop() +{ + return VirtualDesktopManager::self()->current(); +} + +bool DBusInterface::setCurrentDesktop(int desktop) +{ + return VirtualDesktopManager::self()->setCurrent(desktop); +} + +void DBusInterface::nextDesktop() +{ + VirtualDesktopManager::self()->moveTo(); +} + +void DBusInterface::previousDesktop() +{ + VirtualDesktopManager::self()->moveTo(); +} + +void DBusInterface::showDebugConsole() +{ + DebugConsole *console = new DebugConsole; + console->show(); +} + +namespace { +QVariantMap clientToVariantMap(const AbstractClient *c) +{ + return { + {QStringLiteral("resourceClass"), c->resourceClass()}, + {QStringLiteral("resourceName"), c->resourceName()}, + {QStringLiteral("desktopFile"), c->desktopFileName()}, + {QStringLiteral("role"), c->windowRole()}, + {QStringLiteral("caption"), c->captionNormal()}, + {QStringLiteral("clientMachine"), c->wmClientMachine(true)}, + {QStringLiteral("localhost"), c->isLocalhost()}, + {QStringLiteral("type"), c->windowType()}, + {QStringLiteral("x"), c->x()}, + {QStringLiteral("y"), c->y()}, + {QStringLiteral("width"), c->width()}, + {QStringLiteral("height"), c->height()}, + {QStringLiteral("x11DesktopNumber"), c->desktop()}, + {QStringLiteral("minimized"), c->isMinimized()}, + {QStringLiteral("shaded"), c->isShade()}, + {QStringLiteral("fullscreen"), c->isFullScreen()}, + {QStringLiteral("keepAbove"), c->keepAbove()}, + {QStringLiteral("keepBelow"), c->keepBelow()}, + {QStringLiteral("noBorder"), c->noBorder()}, + {QStringLiteral("skipTaskbar"), c->skipTaskbar()}, + {QStringLiteral("skipPager"), c->skipPager()}, + {QStringLiteral("skipSwitcher"), c->skipSwitcher()}, + {QStringLiteral("maximizeHorizontal"), c->maximizeMode() & MaximizeHorizontal}, + {QStringLiteral("maximizeVertical"), c->maximizeMode() & MaximizeVertical} + }; +} +} + +QVariantMap DBusInterface::queryWindowInfo() +{ + m_replyQueryWindowInfo = message(); + setDelayedReply(true); + kwinApp()->platform()->startInteractiveWindowSelection( + [this] (Toplevel *t) { + if (auto c = qobject_cast(t)) { + QDBusConnection::sessionBus().send(m_replyQueryWindowInfo.createReply(clientToVariantMap(c))); + } else { + QDBusConnection::sessionBus().send(m_replyQueryWindowInfo.createErrorReply(QString(), QString())); + } + } + ); + return QVariantMap{}; +} + +QVariantMap DBusInterface::getWindowInfo(const QString &uuid) +{ + const auto id = QUuid::fromString(uuid); + const auto client = workspace()->findAbstractClient([&id] (const AbstractClient *c) { return c->internalId() == id; }); + if (client) { + return clientToVariantMap(client); + } else { + return {}; + } +} + +CompositorDBusInterface::CompositorDBusInterface(Compositor *parent) + : QObject(parent) + , m_compositor(parent) +{ + connect(m_compositor, &Compositor::compositingToggled, this, &CompositorDBusInterface::compositingToggled); + new CompositingAdaptor(this); + QDBusConnection dbus = QDBusConnection::sessionBus(); + dbus.registerObject(QStringLiteral("/Compositor"), this); + dbus.connect(QString(), QStringLiteral("/Compositor"), QStringLiteral("org.kde.kwin.Compositing"), + QStringLiteral("reinit"), this, SLOT(reinitialize())); +} + +QString CompositorDBusInterface::compositingNotPossibleReason() const +{ + return kwinApp()->platform()->compositingNotPossibleReason(); +} + +QString CompositorDBusInterface::compositingType() const +{ + if (!m_compositor->scene()) { + return QStringLiteral("none"); + } + switch (m_compositor->scene()->compositingType()) { + case XRenderCompositing: + return QStringLiteral("xrender"); + case OpenGL2Compositing: + if (QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGLES) { + return QStringLiteral("gles"); + } else { + return QStringLiteral("gl2"); + } + case QPainterCompositing: + return QStringLiteral("qpainter"); + case NoCompositing: + default: + return QStringLiteral("none"); + } +} + +bool CompositorDBusInterface::isActive() const +{ + return m_compositor->isActive(); +} + +bool CompositorDBusInterface::isCompositingPossible() const +{ + return kwinApp()->platform()->compositingPossible(); +} + +bool CompositorDBusInterface::isOpenGLBroken() const +{ + return kwinApp()->platform()->openGLCompositingIsBroken(); +} + +bool CompositorDBusInterface::platformRequiresCompositing() const +{ + return kwinApp()->platform()->requiresCompositing(); +} + +void CompositorDBusInterface::resume() +{ + if (kwinApp()->operationMode() == Application::OperationModeX11) { + static_cast(m_compositor)->resume(X11Compositor::ScriptSuspend); + } +} + +void CompositorDBusInterface::suspend() +{ + if (kwinApp()->operationMode() == Application::OperationModeX11) { + static_cast(m_compositor)->suspend(X11Compositor::ScriptSuspend); + } +} + +void CompositorDBusInterface::reinitialize() +{ + m_compositor->reinitialize(); +} + +QStringList CompositorDBusInterface::supportedOpenGLPlatformInterfaces() const +{ + QStringList interfaces; + bool supportsGlx = false; +#if HAVE_EPOXY_GLX + supportsGlx = (kwinApp()->operationMode() == Application::OperationModeX11); +#endif + if (QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGLES) { + supportsGlx = false; + } + if (supportsGlx) { + interfaces << QStringLiteral("glx"); + } + interfaces << QStringLiteral("egl"); + return interfaces; +} + + + + +VirtualDesktopManagerDBusInterface::VirtualDesktopManagerDBusInterface(VirtualDesktopManager *parent) + : QObject(parent) + , m_manager(parent) +{ + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + + new VirtualDesktopManagerAdaptor(this); + QDBusConnection::sessionBus().registerObject(QStringLiteral("/VirtualDesktopManager"), + QStringLiteral("org.kde.KWin.VirtualDesktopManager"), + this + ); + + connect(m_manager, &VirtualDesktopManager::currentChanged, this, + [this](uint previousDesktop, uint newDesktop) { + Q_UNUSED(previousDesktop); + Q_UNUSED(newDesktop); + emit currentChanged(m_manager->currentDesktop()->id()); + } + ); + + connect(m_manager, &VirtualDesktopManager::countChanged, this, + [this](uint previousCount, uint newCount) { + Q_UNUSED(previousCount); + emit countChanged(newCount); + emit desktopsChanged(desktops()); + } + ); + + connect(m_manager, &VirtualDesktopManager::navigationWrappingAroundChanged, this, + [this]() { + emit navigationWrappingAroundChanged(isNavigationWrappingAround()); + } + ); + + connect(m_manager, &VirtualDesktopManager::rowsChanged, this, &VirtualDesktopManagerDBusInterface::rowsChanged); + + for (auto *vd : m_manager->desktops()) { + connect(vd, &VirtualDesktop::x11DesktopNumberChanged, this, + [this, vd]() { + DBusDesktopDataStruct data{.position = vd->x11DesktopNumber() - 1, .id = vd->id(), .name = vd->name()}; + emit desktopDataChanged(vd->id(), data); + emit desktopsChanged(desktops()); + } + ); + connect(vd, &VirtualDesktop::nameChanged, this, + [this, vd]() { + DBusDesktopDataStruct data{.position = vd->x11DesktopNumber() - 1, .id = vd->id(), .name = vd->name()}; + emit desktopDataChanged(vd->id(), data); + emit desktopsChanged(desktops()); + } + ); + } + connect(m_manager, &VirtualDesktopManager::desktopCreated, this, + [this](VirtualDesktop *vd) { + connect(vd, &VirtualDesktop::x11DesktopNumberChanged, this, + [this, vd]() { + DBusDesktopDataStruct data{.position = vd->x11DesktopNumber() - 1, .id = vd->id(), .name = vd->name()}; + emit desktopDataChanged(vd->id(), data); + emit desktopsChanged(desktops()); + } + ); + connect(vd, &VirtualDesktop::nameChanged, this, + [this, vd]() { + DBusDesktopDataStruct data{.position = vd->x11DesktopNumber() - 1, .id = vd->id(), .name = vd->name()}; + emit desktopDataChanged(vd->id(), data); + emit desktopsChanged(desktops()); + } + ); + DBusDesktopDataStruct data{.position = vd->x11DesktopNumber() - 1, .id = vd->id(), .name = vd->name()}; + emit desktopCreated(vd->id(), data); + emit desktopsChanged(desktops()); + } + ); + connect(m_manager, &VirtualDesktopManager::desktopRemoved, this, + [this](VirtualDesktop *vd) { + emit desktopRemoved(vd->id()); + emit desktopsChanged(desktops()); + } + ); +} + +uint VirtualDesktopManagerDBusInterface::count() const +{ + return m_manager->count(); +} + +void VirtualDesktopManagerDBusInterface::setRows(uint rows) +{ + if (static_cast(m_manager->grid().height()) == rows) { + return; + } + + m_manager->setRows(rows); + m_manager->save(); +} + +uint VirtualDesktopManagerDBusInterface::rows() const +{ + return m_manager->rows(); +} + +void VirtualDesktopManagerDBusInterface::setCurrent(const QString &id) +{ + if (m_manager->currentDesktop()->id() == id) { + return; + } + + auto *vd = m_manager->desktopForId(id.toUtf8()); + if (vd) { + m_manager->setCurrent(vd); + } +} + +QString VirtualDesktopManagerDBusInterface::current() const +{ + return m_manager->currentDesktop()->id(); +} + +void VirtualDesktopManagerDBusInterface::setNavigationWrappingAround(bool wraps) +{ + if (m_manager->isNavigationWrappingAround() == wraps) { + return; + } + + m_manager->setNavigationWrappingAround(wraps); +} + +bool VirtualDesktopManagerDBusInterface::isNavigationWrappingAround() const +{ + return m_manager->isNavigationWrappingAround(); +} + +DBusDesktopDataVector VirtualDesktopManagerDBusInterface::desktops() const +{ + const auto desks = m_manager->desktops(); + DBusDesktopDataVector desktopVect; + desktopVect.reserve(m_manager->count()); + + std::transform(desks.constBegin(), desks.constEnd(), + std::back_inserter(desktopVect), + [] (const VirtualDesktop *vd) { + return DBusDesktopDataStruct{.position = vd->x11DesktopNumber() - 1, .id = vd->id(), .name = vd->name()}; + } + ); + + return desktopVect; +} + +void VirtualDesktopManagerDBusInterface::createDesktop(uint position, const QString &name) +{ + m_manager->createVirtualDesktop(position, name); +} + +void VirtualDesktopManagerDBusInterface::setDesktopName(const QString &id, const QString &name) +{ + VirtualDesktop *vd = m_manager->desktopForId(id.toUtf8()); + if (!vd) { + return; + } + if (vd->name() == name) { + return; + } + + vd->setName(name); + m_manager->save(); +} + +void VirtualDesktopManagerDBusInterface::removeDesktop(const QString &id) +{ + m_manager->removeVirtualDesktop(id.toUtf8()); +} + +} // namespace diff --git a/dbusinterface.h b/dbusinterface.h new file mode 100644 index 0000000..6b8e2b4 --- /dev/null +++ b/dbusinterface.h @@ -0,0 +1,243 @@ +/* + 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 +*/ + +#ifndef KWIN_DBUS_INTERFACE_H +#define KWIN_DBUS_INTERFACE_H + +#include +#include + +#include "virtualdesktopsdbustypes.h" + +namespace KWin +{ + +class Compositor; +class VirtualDesktopManager; + +/** + * @brief This class is a wrapper for the org.kde.KWin D-Bus interface. + * + * The main purpose of this class is to be exported on the D-Bus as object /KWin. + * It is a pure wrapper to provide the deprecated D-Bus methods which have been + * removed from Workspace which used to implement the complete D-Bus interface. + * + * Nowadays the D-Bus interfaces are distributed, parts of it are exported on + * /Compositor, parts on /Effects and parts on /KWin. The implementation in this + * class just delegates the method calls to the actual implementation in one of the + * three singletons. + * + * @author Martin Gräßlin + */ +class DBusInterface: public QObject, protected QDBusContext +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.KWin") +public: + explicit DBusInterface(QObject *parent); + ~DBusInterface() override; + +public: // PROPERTIES +public Q_SLOTS: // METHODS + Q_NOREPLY void cascadeDesktop(); + int currentDesktop(); + Q_NOREPLY void killWindow(); + void nextDesktop(); + void previousDesktop(); + Q_NOREPLY void reconfigure(); + bool setCurrentDesktop(int desktop); + bool startActivity(const QString &in0); + bool stopActivity(const QString &in0); + QString supportInformation(); + Q_NOREPLY void unclutterDesktop(); + Q_NOREPLY void showDebugConsole(); + + QVariantMap queryWindowInfo(); + QVariantMap getWindowInfo(const QString &uuid); + +private Q_SLOTS: + void becomeKWinService(const QString &service); + +private: + void announceService(); + QString m_serviceName; + QDBusMessage m_replyQueryWindowInfo; +}; + +class CompositorDBusInterface : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.kwin.Compositing") + /** + * @brief Whether the Compositor is active. That is a Scene is present and the Compositor is + * not shutting down itself. + */ + Q_PROPERTY(bool active READ isActive) + /** + * @brief Whether compositing is possible. Mostly means whether the required X extensions + * are available. + */ + Q_PROPERTY(bool compositingPossible READ isCompositingPossible) + /** + * @brief The reason why compositing is not possible. Empty String if compositing is possible. + */ + Q_PROPERTY(QString compositingNotPossibleReason READ compositingNotPossibleReason) + /** + * @brief Whether OpenGL has failed badly in the past (crash) and is considered as broken. + */ + Q_PROPERTY(bool openGLIsBroken READ isOpenGLBroken) + /** + * The type of the currently used Scene: + * @li @c none No Compositing + * @li @c xrender XRender + * @li @c gl1 OpenGL 1 + * @li @c gl2 OpenGL 2 + * @li @c gles OpenGL ES 2 + */ + Q_PROPERTY(QString compositingType READ compositingType) + /** + * @brief All currently supported OpenGLPlatformInterfaces. + * + * Possible values: + * @li glx + * @li egl + * + * Values depend on operation mode and compile time options. + */ + Q_PROPERTY(QStringList supportedOpenGLPlatformInterfaces READ supportedOpenGLPlatformInterfaces) + Q_PROPERTY(bool platformRequiresCompositing READ platformRequiresCompositing) +public: + explicit CompositorDBusInterface(Compositor *parent); + ~CompositorDBusInterface() override = default; + + bool isActive() const; + bool isCompositingPossible() const; + QString compositingNotPossibleReason() const; + bool isOpenGLBroken() const; + QString compositingType() const; + QStringList supportedOpenGLPlatformInterfaces() const; + bool platformRequiresCompositing() const; + +public Q_SLOTS: + /** + * @brief Suspends the Compositor if it is currently active. + * + * Note: it is possible that the Compositor is not able to suspend. Use isActive to check + * whether the Compositor has been suspended. + * + * @return void + * @see resume + * @see isActive + */ + void suspend(); + /** + * @brief Resumes the Compositor if it is currently suspended. + * + * Note: it is possible that the Compositor cannot be resumed, that is there might be Clients + * blocking the usage of Compositing or the Scene might be broken. Use isActive to check + * whether the Compositor has been resumed. Also check isCompositingPossible and + * isOpenGLBroken. + * + * Note: The starting of the Compositor can require some time and is partially done threaded. + * After this method returns the setup may not have been completed. + * + * @return void + * @see suspend + * @see isActive + * @see isCompositingPossible + * @see isOpenGLBroken + */ + void resume(); + /** + * @brief Used by Compositing KCM after settings change. + * + * On signal Compositor reloads settings and restarts. + */ + void reinitialize(); + +Q_SIGNALS: + void compositingToggled(bool active); + +private: + Compositor *m_compositor; +}; + +//TODO: disable all of this in case of kiosk? + +class VirtualDesktopManagerDBusInterface : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.KWin.VirtualDesktopManager") + + /** + * The number of virtual desktops currently available. + * The ids of the virtual desktops are in the range [1, VirtualDesktopManager::maximum()]. + */ + Q_PROPERTY(uint count READ count NOTIFY countChanged) + /** + * The number of rows the virtual desktops will be laid out in + */ + Q_PROPERTY(uint rows READ rows WRITE setRows NOTIFY rowsChanged) + /** + * The id of the virtual desktop which is currently in use. + */ + Q_PROPERTY(QString current READ current WRITE setCurrent NOTIFY currentChanged) + /** + * Whether navigation in the desktop layout wraps around at the borders. + */ + Q_PROPERTY(bool navigationWrappingAround READ isNavigationWrappingAround WRITE setNavigationWrappingAround NOTIFY navigationWrappingAroundChanged) + + /** + * list of key/value pairs which every one of them is representing a desktop + */ + Q_PROPERTY(KWin::DBusDesktopDataVector desktops READ desktops NOTIFY desktopsChanged); + +public: + VirtualDesktopManagerDBusInterface(VirtualDesktopManager *parent); + ~VirtualDesktopManagerDBusInterface() override = default; + + uint count() const; + + void setRows(uint rows); + uint rows() const; + + void setCurrent(const QString &id); + QString current() const; + + void setNavigationWrappingAround(bool wraps); + bool isNavigationWrappingAround() const; + + KWin::DBusDesktopDataVector desktops() const; + +Q_SIGNALS: + void countChanged(uint count); + void rowsChanged(uint rows); + void currentChanged(const QString &id); + void navigationWrappingAroundChanged(bool wraps); + void desktopsChanged(KWin::DBusDesktopDataVector); + void desktopDataChanged(const QString &id, KWin::DBusDesktopDataStruct); + void desktopCreated(const QString &id, KWin::DBusDesktopDataStruct); + void desktopRemoved(const QString &id); + +public Q_SLOTS: + /** + * Create a desktop with a new name at a given position + * note: the position starts from 1 + */ + void createDesktop(uint position, const QString &name); + void setDesktopName(const QString &id, const QString &name); + void removeDesktop(const QString &id); + +private: + VirtualDesktopManager *m_manager; +}; + +} // namespace + +#endif // KWIN_DBUS_INTERFACE_H diff --git a/debug_console.cpp b/debug_console.cpp new file mode 100644 index 0000000..49d3064 --- /dev/null +++ b/debug_console.cpp @@ -0,0 +1,1562 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "debug_console.h" +#include "composite.h" +#include "x11client.h" +#include "input_event.h" +#include "internal_client.h" +#include "main.h" +#include "scene.h" +#include "unmanaged.h" +#include "waylandclient.h" +#include "wayland_server.h" +#include "workspace.h" +#include "keyboard_input.h" +#include "input_event.h" +#include "libinput/connection.h" +#include "libinput/device.h" +#include +#include + +#include "ui_debug_console.h" + +// KWayland +#include +#include +#include +#include +// frameworks +#include +#include +// Qt +#include +#include +#include + +// xkb +#include + +#include + +namespace KWin +{ + + +static QString tableHeaderRow(const QString &title) +{ + return QStringLiteral("%1").arg(title); +} + +template +static +QString tableRow(const QString &title, const T &argument) +{ + return QStringLiteral("%1%2").arg(title).arg(argument); +} + +static QString timestampRow(quint32 timestamp) +{ + return tableRow(i18n("Timestamp"), timestamp); +} + +static QString timestampRowUsec(quint64 timestamp) +{ + return tableRow(i18n("Timestamp (µsec)"), timestamp); +} + +static QString buttonToString(Qt::MouseButton button) +{ + switch (button) { + case Qt::LeftButton: + return i18nc("A mouse button", "Left"); + case Qt::RightButton: + return i18nc("A mouse button", "Right"); + case Qt::MiddleButton: + return i18nc("A mouse button", "Middle"); + case Qt::BackButton: + return i18nc("A mouse button", "Back"); + case Qt::ForwardButton: + return i18nc("A mouse button", "Forward"); + case Qt::TaskButton: + return i18nc("A mouse button", "Task"); + case Qt::ExtraButton4: + return i18nc("A mouse button", "Extra Button 4"); + case Qt::ExtraButton5: + return i18nc("A mouse button", "Extra Button 5"); + case Qt::ExtraButton6: + return i18nc("A mouse button", "Extra Button 6"); + case Qt::ExtraButton7: + return i18nc("A mouse button", "Extra Button 7"); + case Qt::ExtraButton8: + return i18nc("A mouse button", "Extra Button 8"); + case Qt::ExtraButton9: + return i18nc("A mouse button", "Extra Button 9"); + case Qt::ExtraButton10: + return i18nc("A mouse button", "Extra Button 10"); + case Qt::ExtraButton11: + return i18nc("A mouse button", "Extra Button 11"); + case Qt::ExtraButton12: + return i18nc("A mouse button", "Extra Button 12"); + case Qt::ExtraButton13: + return i18nc("A mouse button", "Extra Button 13"); + case Qt::ExtraButton14: + return i18nc("A mouse button", "Extra Button 14"); + case Qt::ExtraButton15: + return i18nc("A mouse button", "Extra Button 15"); + case Qt::ExtraButton16: + return i18nc("A mouse button", "Extra Button 16"); + case Qt::ExtraButton17: + return i18nc("A mouse button", "Extra Button 17"); + case Qt::ExtraButton18: + return i18nc("A mouse button", "Extra Button 18"); + case Qt::ExtraButton19: + return i18nc("A mouse button", "Extra Button 19"); + case Qt::ExtraButton20: + return i18nc("A mouse button", "Extra Button 20"); + case Qt::ExtraButton21: + return i18nc("A mouse button", "Extra Button 21"); + case Qt::ExtraButton22: + return i18nc("A mouse button", "Extra Button 22"); + case Qt::ExtraButton23: + return i18nc("A mouse button", "Extra Button 23"); + case Qt::ExtraButton24: + return i18nc("A mouse button", "Extra Button 24"); + default: + return QString(); + } +} + +static QString deviceRow(LibInput::Device *device) +{ + if (!device) { + return tableRow(i18n("Input Device"), i18nc("The input device of the event is not known", "Unknown")); + } + return tableRow(i18n("Input Device"), QStringLiteral("%1 (%2)").arg(device->name()).arg(device->sysName())); +} + +static QString buttonsToString(Qt::MouseButtons buttons) +{ + QString ret; + for (uint i = 1; i < Qt::ExtraButton24; i = i << 1) { + if (buttons & i) { + ret.append(buttonToString(Qt::MouseButton(uint(buttons) & i))); + ret.append(QStringLiteral(" ")); + } + }; + return ret.trimmed(); +} + +static const QString s_hr = QStringLiteral("
"); +static const QString s_tableStart = QStringLiteral(""); +static const QString s_tableEnd = QStringLiteral("
"); + +DebugConsoleFilter::DebugConsoleFilter(QTextEdit *textEdit) + : InputEventSpy() + , m_textEdit(textEdit) +{ +} + +DebugConsoleFilter::~DebugConsoleFilter() = default; + +void DebugConsoleFilter::pointerEvent(MouseEvent *event) +{ + QString text = s_hr; + const QString timestamp = timestampRow(event->timestamp()); + + text.append(s_tableStart); + switch (event->type()) { + case QEvent::MouseMove: { + text.append(tableHeaderRow(i18nc("A mouse pointer motion event", "Pointer Motion"))); + text.append(deviceRow(event->device())); + text.append(timestamp); + if (event->timestampMicroseconds() != 0) { + text.append(timestampRowUsec(event->timestampMicroseconds())); + } + if (event->delta() != QSizeF()) { + text.append(tableRow(i18nc("The relative mouse movement", "Delta"), + QStringLiteral("%1/%2").arg(event->delta().width()).arg(event->delta().height()))); + } + if (event->deltaUnaccelerated() != QSizeF()) { + text.append(tableRow(i18nc("The relative mouse movement", "Delta (not accelerated)"), + QStringLiteral("%1/%2").arg(event->deltaUnaccelerated().width()).arg(event->deltaUnaccelerated().height()))); + } + text.append(tableRow(i18nc("The global mouse pointer position", "Global Position"), QStringLiteral("%1/%2").arg(event->pos().x()).arg(event->pos().y()))); + break; + } + case QEvent::MouseButtonPress: + text.append(tableHeaderRow(i18nc("A mouse pointer button press event", "Pointer Button Press"))); + text.append(deviceRow(event->device())); + text.append(timestamp); + text.append(tableRow(i18nc("A button in a mouse press/release event", "Button"), buttonToString(event->button()))); + text.append(tableRow(i18nc("A button in a mouse press/release event", "Native Button code"), event->nativeButton())); + text.append(tableRow(i18nc("All currently pressed buttons in a mouse press/release event", "Pressed Buttons"), buttonsToString(event->buttons()))); + break; + case QEvent::MouseButtonRelease: + text.append(tableHeaderRow(i18nc("A mouse pointer button release event", "Pointer Button Release"))); + text.append(deviceRow(event->device())); + text.append(timestamp); + text.append(tableRow(i18nc("A button in a mouse press/release event", "Button"), buttonToString(event->button()))); + text.append(tableRow(i18nc("A button in a mouse press/release event", "Native Button code"), event->nativeButton())); + text.append(tableRow(i18nc("All currently pressed buttons in a mouse press/release event", "Pressed Buttons"), buttonsToString(event->buttons()))); + break; + default: + break; + } + text.append(s_tableEnd); + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::wheelEvent(WheelEvent *event) +{ + QString text = s_hr; + text.append(s_tableStart); + text.append(tableHeaderRow(i18nc("A mouse pointer axis (wheel) event", "Pointer Axis"))); + text.append(deviceRow(event->device())); + text.append(timestampRow(event->timestamp())); + const Qt::Orientation orientation = event->angleDelta().x() == 0 ? Qt::Vertical : Qt::Horizontal; + text.append(tableRow(i18nc("The orientation of a pointer axis event", "Orientation"), + orientation == Qt::Horizontal ? i18nc("An orientation of a pointer axis event", "Horizontal") + : i18nc("An orientation of a pointer axis event", "Vertical"))); + text.append(tableRow(i18nc("The angle delta of a pointer axis event", "Delta"), + orientation == Qt::Horizontal ? event->angleDelta().x() : event->angleDelta().y())); + text.append(s_tableEnd); + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::keyEvent(KeyEvent *event) +{ + QString text = s_hr; + text.append(s_tableStart); + + switch (event->type()) { + case QEvent::KeyPress: + text.append(tableHeaderRow(i18nc("A key press event", "Key Press"))); + break; + case QEvent::KeyRelease: + text.append(tableHeaderRow(i18nc("A key release event", "Key Release"))); + break; + default: + break; + } + text.append(deviceRow(event->device())); + auto modifiersToString = [event] { + QString ret; + if (event->modifiers().testFlag(Qt::ShiftModifier)) { + ret.append(i18nc("A keyboard modifier", "Shift")); + ret.append(QStringLiteral(" ")); + } + if (event->modifiers().testFlag(Qt::ControlModifier)) { + ret.append(i18nc("A keyboard modifier", "Control")); + ret.append(QStringLiteral(" ")); + } + if (event->modifiers().testFlag(Qt::AltModifier)) { + ret.append(i18nc("A keyboard modifier", "Alt")); + ret.append(QStringLiteral(" ")); + } + if (event->modifiers().testFlag(Qt::MetaModifier)) { + ret.append(i18nc("A keyboard modifier", "Meta")); + ret.append(QStringLiteral(" ")); + } + if (event->modifiers().testFlag(Qt::KeypadModifier)) { + ret.append(i18nc("A keyboard modifier", "Keypad")); + ret.append(QStringLiteral(" ")); + } + if (event->modifiers().testFlag(Qt::GroupSwitchModifier)) { + ret.append(i18nc("A keyboard modifier", "Group-switch")); + ret.append(QStringLiteral(" ")); + } + return ret; + }; + text.append(timestampRow(event->timestamp())); + text.append(tableRow(i18nc("Whether the event is an automatic key repeat", "Repeat"), event->isAutoRepeat())); + + const auto keyMetaObject = Qt::qt_getEnumMetaObject(Qt::Key()); + const auto enumerator = keyMetaObject->enumerator(keyMetaObject->indexOfEnumerator("Key")); + text.append(tableRow(i18nc("The code as read from the input device", "Scan code"), event->nativeScanCode())); + text.append(tableRow(i18nc("Key according to Qt", "Qt::Key code"), + enumerator.valueToKey(event->key()))); + text.append(tableRow(i18nc("The translated code to an Xkb symbol", "Xkb symbol"), event->nativeVirtualKey())); + text.append(tableRow(i18nc("The translated code interpreted as text", "Utf8"), event->text())); + text.append(tableRow(i18nc("The currently active modifiers", "Modifiers"), modifiersToString())); + + text.append(s_tableEnd); + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::touchDown(qint32 id, const QPointF &pos, quint32 time) +{ + QString text = s_hr; + text.append(s_tableStart); + text.append(tableHeaderRow(i18nc("A touch down event", "Touch down"))); + text.append(timestampRow(time)); + text.append(tableRow(i18nc("The id of the touch point in the touch event", "Point identifier"), id)); + text.append(tableRow(i18nc("The global position of the touch point", "Global position"), + QStringLiteral("%1/%2").arg(pos.x()).arg(pos.y()))); + text.append(s_tableEnd); + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::touchMotion(qint32 id, const QPointF &pos, quint32 time) +{ + QString text = s_hr; + text.append(s_tableStart); + text.append(tableHeaderRow(i18nc("A touch motion event", "Touch Motion"))); + text.append(timestampRow(time)); + text.append(tableRow(i18nc("The id of the touch point in the touch event", "Point identifier"), id)); + text.append(tableRow(i18nc("The global position of the touch point", "Global position"), + QStringLiteral("%1/%2").arg(pos.x()).arg(pos.y()))); + text.append(s_tableEnd); + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::touchUp(qint32 id, quint32 time) +{ + QString text = s_hr; + text.append(s_tableStart); + text.append(tableHeaderRow(i18nc("A touch up event", "Touch Up"))); + text.append(timestampRow(time)); + text.append(tableRow(i18nc("The id of the touch point in the touch event", "Point identifier"), id)); + text.append(s_tableEnd); + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::pinchGestureBegin(int fingerCount, quint32 time) +{ + QString text = s_hr; + text.append(s_tableStart); + text.append(tableHeaderRow(i18nc("A pinch gesture is started", "Pinch start"))); + text.append(timestampRow(time)); + text.append(tableRow(i18nc("Number of fingers in this pinch gesture", "Finger count"), fingerCount)); + text.append(s_tableEnd); + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::pinchGestureUpdate(qreal scale, qreal angleDelta, const QSizeF &delta, quint32 time) +{ + QString text = s_hr; + text.append(s_tableStart); + text.append(tableHeaderRow(i18nc("A pinch gesture is updated", "Pinch update"))); + text.append(timestampRow(time)); + text.append(tableRow(i18nc("Current scale in pinch gesture", "Scale"), scale)); + text.append(tableRow(i18nc("Current angle in pinch gesture", "Angle delta"), angleDelta)); + text.append(tableRow(i18nc("Current delta in pinch gesture", "Delta x"), delta.width())); + text.append(tableRow(i18nc("Current delta in pinch gesture", "Delta y"), delta.height())); + text.append(s_tableEnd); + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::pinchGestureEnd(quint32 time) +{ + QString text = s_hr; + text.append(s_tableStart); + text.append(tableHeaderRow(i18nc("A pinch gesture ended", "Pinch end"))); + text.append(timestampRow(time)); + text.append(s_tableEnd); + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::pinchGestureCancelled(quint32 time) +{ + QString text = s_hr; + text.append(s_tableStart); + text.append(tableHeaderRow(i18nc("A pinch gesture got cancelled", "Pinch cancelled"))); + text.append(timestampRow(time)); + text.append(s_tableEnd); + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::swipeGestureBegin(int fingerCount, quint32 time) +{ + QString text = s_hr; + text.append(s_tableStart); + text.append(tableHeaderRow(i18nc("A swipe gesture is started", "Swipe start"))); + text.append(timestampRow(time)); + text.append(tableRow(i18nc("Number of fingers in this swipe gesture", "Finger count"), fingerCount)); + text.append(s_tableEnd); + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::swipeGestureUpdate(const QSizeF &delta, quint32 time) +{ + QString text = s_hr; + text.append(s_tableStart); + text.append(tableHeaderRow(i18nc("A swipe gesture is updated", "Swipe update"))); + text.append(timestampRow(time)); + text.append(tableRow(i18nc("Current delta in swipe gesture", "Delta x"), delta.width())); + text.append(tableRow(i18nc("Current delta in swipe gesture", "Delta y"), delta.height())); + text.append(s_tableEnd); + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::swipeGestureEnd(quint32 time) +{ + QString text = s_hr; + text.append(s_tableStart); + text.append(tableHeaderRow(i18nc("A swipe gesture ended", "Swipe end"))); + text.append(timestampRow(time)); + text.append(s_tableEnd); + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::swipeGestureCancelled(quint32 time) +{ + QString text = s_hr; + text.append(s_tableStart); + text.append(tableHeaderRow(i18nc("A swipe gesture got cancelled", "Swipe cancelled"))); + text.append(timestampRow(time)); + text.append(s_tableEnd); + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::switchEvent(SwitchEvent *event) +{ + QString text = s_hr; + text.append(s_tableStart); + text.append(tableHeaderRow(i18nc("A hardware switch (e.g. notebook lid) got toggled", "Switch toggled"))); + text.append(timestampRow(event->timestamp())); + if (event->timestampMicroseconds() != 0) { + text.append(timestampRowUsec(event->timestampMicroseconds())); + } + text.append(deviceRow(event->device())); + QString switchName; + if (event->device()->isLidSwitch()) { + switchName = i18nc("Name of a hardware switch", "Notebook lid"); + } else if (event->device()->isTabletModeSwitch()) { + switchName = i18nc("Name of a hardware switch", "Tablet mode"); + } + text.append(tableRow(i18nc("A hardware switch", "Switch"), switchName)); + QString switchState; + switch (event->state()) { + case SwitchEvent::State::Off: + switchState = i18nc("The hardware switch got turned off", "Off"); + break; + case SwitchEvent::State::On: + switchState = i18nc("The hardware switch got turned on", "On"); + break; + default: + Q_UNREACHABLE(); + } + text.append(tableRow(i18nc("State of a hardware switch (on/off)", "State"), switchState)); + text.append(s_tableEnd); + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::tabletToolEvent(TabletEvent *event) +{ + QString typeString; + { + QDebug d(&typeString); + d << event->type(); + } + + QString text = s_hr + s_tableStart + tableHeaderRow(i18n("Tablet Tool")) + + tableRow(i18n("EventType"), typeString) + + tableRow(i18n("Position"), + QStringLiteral("%1,%2").arg(event->pos().x()).arg(event->pos().y())) + + tableRow(i18n("Tilt"), + QStringLiteral("%1,%2").arg(event->xTilt()).arg(event->yTilt())) + + tableRow(i18n("Rotation"), QString::number(event->rotation())) + + tableRow(i18n("Pressure"), QString::number(event->pressure())) + + tableRow(i18n("Buttons"), QString::number(event->buttons())) + + tableRow(i18n("Modifiers"), QString::number(event->modifiers())) + + s_tableEnd; + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::tabletToolButtonEvent(const QSet &pressedButtons) +{ + QString buttons; + for (uint b : pressedButtons) { + buttons += QString::number(b) + ' '; + } + QString text = s_hr + s_tableStart + tableHeaderRow(i18n("Tablet Tool Button")) + + tableRow(i18n("Pressed Buttons"), buttons) + + s_tableEnd; + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::tabletPadButtonEvent(const QSet &pressedButtons) +{ + QString buttons; + for (uint b : pressedButtons) { + buttons += QString::number(b) + ' '; + } + QString text = s_hr + s_tableStart + + tableHeaderRow(i18n("Tablet Pad Button")) + + tableRow(i18n("Pressed Buttons"), buttons) + + s_tableEnd; + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::tabletPadStripEvent(int number, int position, bool isFinger) +{ + QString text = s_hr + s_tableStart + tableHeaderRow(i18n("Tablet Pad Strip")) + + tableRow(i18n("Number"), number) + + tableRow(i18n("Position"), position) + + tableRow(i18n("isFinger"), isFinger) + + s_tableEnd; + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::tabletPadRingEvent(int number, int position, bool isFinger) +{ + QString text = s_hr + s_tableStart + tableHeaderRow(i18n("Tablet Pad Ring")) + + tableRow(i18n("Number"), number) + + tableRow(i18n("Position"), position) + + tableRow(i18n("isFinger"), isFinger) + + s_tableEnd; + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +DebugConsole::DebugConsole() + : QWidget() + , m_ui(new Ui::DebugConsole) +{ + setAttribute(Qt::WA_ShowWithoutActivating); + m_ui->setupUi(this); + m_ui->windowsView->setItemDelegate(new DebugConsoleDelegate(this)); + m_ui->windowsView->setModel(new DebugConsoleModel(this)); + m_ui->surfacesView->setModel(new SurfaceTreeModel(this)); + if (kwinApp()->usesLibinput()) { + m_ui->inputDevicesView->setModel(new InputDeviceModel(this)); + m_ui->inputDevicesView->setItemDelegate(new DebugConsoleDelegate(this)); + } + m_ui->quitButton->setIcon(QIcon::fromTheme(QStringLiteral("application-exit"))); + m_ui->tabWidget->setTabIcon(0, QIcon::fromTheme(QStringLiteral("view-list-tree"))); + m_ui->tabWidget->setTabIcon(1, QIcon::fromTheme(QStringLiteral("view-list-tree"))); + + if (kwinApp()->operationMode() == Application::OperationMode::OperationModeX11) { + m_ui->tabWidget->setTabEnabled(1, false); + m_ui->tabWidget->setTabEnabled(2, false); + } + if (!kwinApp()->usesLibinput()) { + m_ui->tabWidget->setTabEnabled(3, false); + } + + connect(m_ui->quitButton, &QAbstractButton::clicked, this, &DebugConsole::deleteLater); + connect(m_ui->tabWidget, &QTabWidget::currentChanged, this, + [this] (int index) { + // delay creation of input event filter until the tab is selected + if (index == 2 && m_inputFilter.isNull()) { + m_inputFilter.reset(new DebugConsoleFilter(m_ui->inputTextEdit)); + input()->installInputEventSpy(m_inputFilter.data()); + } + if (index == 5) { + updateKeyboardTab(); + connect(input(), &InputRedirection::keyStateChanged, this, &DebugConsole::updateKeyboardTab); + } + } + ); + + // for X11 + setWindowFlags(Qt::X11BypassWindowManagerHint); + + initGLTab(); +} + +DebugConsole::~DebugConsole() = default; + +void DebugConsole::initGLTab() +{ + if (!effects || !effects->isOpenGLCompositing()) { + m_ui->noOpenGLLabel->setVisible(true); + m_ui->glInfoScrollArea->setVisible(false); + return; + } + GLPlatform *gl = GLPlatform::instance(); + m_ui->noOpenGLLabel->setVisible(false); + m_ui->glInfoScrollArea->setVisible(true); + m_ui->glVendorStringLabel->setText(QString::fromLocal8Bit(gl->glVendorString())); + m_ui->glRendererStringLabel->setText(QString::fromLocal8Bit(gl->glRendererString())); + m_ui->glVersionStringLabel->setText(QString::fromLocal8Bit(gl->glVersionString())); + m_ui->glslVersionStringLabel->setText(QString::fromLocal8Bit(gl->glShadingLanguageVersionString())); + m_ui->glDriverLabel->setText(GLPlatform::driverToString(gl->driver())); + m_ui->glGPULabel->setText(GLPlatform::chipClassToString(gl->chipClass())); + m_ui->glVersionLabel->setText(GLPlatform::versionToString(gl->glVersion())); + m_ui->glslLabel->setText(GLPlatform::versionToString(gl->glslVersion())); + + auto extensionsString = [] (const auto &extensions) { + QString text = QStringLiteral("
    "); + for (auto extension : extensions) { + text.append(QStringLiteral("
  • %1
  • ").arg(QString::fromLocal8Bit(extension))); + } + text.append(QStringLiteral("
")); + return text; + }; + + m_ui->platformExtensionsLabel->setText(extensionsString(Compositor::self()->scene()->openGLPlatformInterfaceExtensions())); + m_ui->openGLExtensionsLabel->setText(extensionsString(openGLExtensions())); +} + +template +QString keymapComponentToString(xkb_keymap *map, const T &count, std::function f) +{ + QString text = QStringLiteral("
    "); + for (T i = 0; i < count; i++) { + text.append(QStringLiteral("
  • %1
  • ").arg(QString::fromLocal8Bit(f(map, i)))); + } + text.append(QStringLiteral("
")); + return text; +} + +template +QString stateActiveComponents(xkb_state *state, const T &count, std::function f, std::function name) +{ + QString text = QStringLiteral("
    "); + xkb_keymap *map = xkb_state_get_keymap(state); + for (T i = 0; i < count; i++) { + if (f(state, i) == 1) { + text.append(QStringLiteral("
  • %1
  • ").arg(QString::fromLocal8Bit(name(map, i)))); + } + } + text.append(QStringLiteral("
")); + return text; +} + +void DebugConsole::updateKeyboardTab() +{ + auto xkb = input()->keyboard()->xkb(); + xkb_keymap *map = xkb->keymap(); + xkb_state *state = xkb->state(); + m_ui->layoutsLabel->setText(keymapComponentToString(map, xkb_keymap_num_layouts(map), &xkb_keymap_layout_get_name)); + m_ui->currentLayoutLabel->setText(xkb_keymap_layout_get_name(map, xkb->currentLayout())); + m_ui->modifiersLabel->setText(keymapComponentToString(map, xkb_keymap_num_mods(map), &xkb_keymap_mod_get_name)); + m_ui->ledsLabel->setText(keymapComponentToString(map, xkb_keymap_num_leds(map), &xkb_keymap_led_get_name)); + m_ui->activeLedsLabel->setText(stateActiveComponents(state, xkb_keymap_num_leds(map), &xkb_state_led_index_is_active, &xkb_keymap_led_get_name)); + + using namespace std::placeholders; + auto modActive = std::bind(xkb_state_mod_index_is_active, _1, _2, XKB_STATE_MODS_EFFECTIVE); + m_ui->activeModifiersLabel->setText(stateActiveComponents(state, xkb_keymap_num_mods(map), modActive, &xkb_keymap_mod_get_name)); +} + +void DebugConsole::showEvent(QShowEvent *event) +{ + QWidget::showEvent(event); + + // delay the connection to the show event as in ctor the windowHandle returns null + connect(windowHandle(), &QWindow::visibleChanged, this, + [this] (bool visible) { + if (visible) { + // ignore + return; + } + deleteLater(); + } + ); +} + +DebugConsoleDelegate::DebugConsoleDelegate(QObject *parent) + : QStyledItemDelegate(parent) +{ +} + +DebugConsoleDelegate::~DebugConsoleDelegate() = default; + +QString DebugConsoleDelegate::displayText(const QVariant &value, const QLocale &locale) const +{ + switch (value.userType()) { + case QMetaType::QPoint: { + const QPoint p = value.toPoint(); + return QStringLiteral("%1,%2").arg(p.x()).arg(p.y()); + } + case QMetaType::QPointF: { + const QPointF p = value.toPointF(); + return QStringLiteral("%1,%2").arg(p.x()).arg(p.y()); + } + case QMetaType::QSize: { + const QSize s = value.toSize(); + return QStringLiteral("%1x%2").arg(s.width()).arg(s.height()); + } + case QMetaType::QSizeF: { + const QSizeF s = value.toSizeF(); + return QStringLiteral("%1x%2").arg(s.width()).arg(s.height()); + } + case QMetaType::QRect: { + const QRect r = value.toRect(); + return QStringLiteral("%1,%2 %3x%4").arg(r.x()).arg(r.y()).arg(r.width()).arg(r.height()); + } + default: + if (value.userType() == qMetaTypeId()) { + if (auto s = value.value()) { + return QStringLiteral("KWaylandServer::SurfaceInterface(0x%1)").arg(qulonglong(s), 0, 16); + } else { + return QStringLiteral("nullptr"); + } + } + if (value.userType() == qMetaTypeId()) { + const auto buttons = value.value(); + if (buttons == Qt::NoButton) { + return i18n("No Mouse Buttons"); + } + QStringList list; + if (buttons.testFlag(Qt::LeftButton)) { + list << i18nc("Mouse Button", "left"); + } + if (buttons.testFlag(Qt::RightButton)) { + list << i18nc("Mouse Button", "right"); + } + if (buttons.testFlag(Qt::MiddleButton)) { + list << i18nc("Mouse Button", "middle"); + } + if (buttons.testFlag(Qt::BackButton)) { + list << i18nc("Mouse Button", "back"); + } + if (buttons.testFlag(Qt::ForwardButton)) { + list << i18nc("Mouse Button", "forward"); + } + if (buttons.testFlag(Qt::ExtraButton1)) { + list << i18nc("Mouse Button", "extra 1"); + } + if (buttons.testFlag(Qt::ExtraButton2)) { + list << i18nc("Mouse Button", "extra 2"); + } + if (buttons.testFlag(Qt::ExtraButton3)) { + list << i18nc("Mouse Button", "extra 3"); + } + if (buttons.testFlag(Qt::ExtraButton4)) { + list << i18nc("Mouse Button", "extra 4"); + } + if (buttons.testFlag(Qt::ExtraButton5)) { + list << i18nc("Mouse Button", "extra 5"); + } + if (buttons.testFlag(Qt::ExtraButton6)) { + list << i18nc("Mouse Button", "extra 6"); + } + if (buttons.testFlag(Qt::ExtraButton7)) { + list << i18nc("Mouse Button", "extra 7"); + } + if (buttons.testFlag(Qt::ExtraButton8)) { + list << i18nc("Mouse Button", "extra 8"); + } + if (buttons.testFlag(Qt::ExtraButton9)) { + list << i18nc("Mouse Button", "extra 9"); + } + if (buttons.testFlag(Qt::ExtraButton10)) { + list << i18nc("Mouse Button", "extra 10"); + } + if (buttons.testFlag(Qt::ExtraButton11)) { + list << i18nc("Mouse Button", "extra 11"); + } + if (buttons.testFlag(Qt::ExtraButton12)) { + list << i18nc("Mouse Button", "extra 12"); + } + if (buttons.testFlag(Qt::ExtraButton13)) { + list << i18nc("Mouse Button", "extra 13"); + } + if (buttons.testFlag(Qt::ExtraButton14)) { + list << i18nc("Mouse Button", "extra 14"); + } + if (buttons.testFlag(Qt::ExtraButton15)) { + list << i18nc("Mouse Button", "extra 15"); + } + if (buttons.testFlag(Qt::ExtraButton16)) { + list << i18nc("Mouse Button", "extra 16"); + } + if (buttons.testFlag(Qt::ExtraButton17)) { + list << i18nc("Mouse Button", "extra 17"); + } + if (buttons.testFlag(Qt::ExtraButton18)) { + list << i18nc("Mouse Button", "extra 18"); + } + if (buttons.testFlag(Qt::ExtraButton19)) { + list << i18nc("Mouse Button", "extra 19"); + } + if (buttons.testFlag(Qt::ExtraButton20)) { + list << i18nc("Mouse Button", "extra 20"); + } + if (buttons.testFlag(Qt::ExtraButton21)) { + list << i18nc("Mouse Button", "extra 21"); + } + if (buttons.testFlag(Qt::ExtraButton22)) { + list << i18nc("Mouse Button", "extra 22"); + } + if (buttons.testFlag(Qt::ExtraButton23)) { + list << i18nc("Mouse Button", "extra 23"); + } + if (buttons.testFlag(Qt::ExtraButton24)) { + list << i18nc("Mouse Button", "extra 24"); + } + if (buttons.testFlag(Qt::TaskButton)) { + list << i18nc("Mouse Button", "task"); + } + return list.join(QStringLiteral(", ")); + } + break; + } + return QStyledItemDelegate::displayText(value, locale); +} + +static const int s_x11ClientId = 1; +static const int s_x11UnmanagedId = 2; +static const int s_waylandClientId = 3; +static const int s_workspaceInternalId = 4; +static const quint32 s_propertyBitMask = 0xFFFF0000; +static const quint32 s_clientBitMask = 0x0000FFFF; +static const quint32 s_idDistance = 10000; + +template +void DebugConsoleModel::add(int parentRow, QVector &clients, T *client) +{ + beginInsertRows(index(parentRow, 0, QModelIndex()), clients.count(), clients.count()); + clients.append(client); + endInsertRows(); +} + +template +void DebugConsoleModel::remove(int parentRow, QVector &clients, T *client) +{ + const int remove = clients.indexOf(client); + if (remove == -1) { + return; + } + beginRemoveRows(index(parentRow, 0, QModelIndex()), remove, remove); + clients.removeAt(remove); + endRemoveRows(); +} + +DebugConsoleModel::DebugConsoleModel(QObject *parent) + : QAbstractItemModel(parent) +{ + if (waylandServer()) { + const auto clients = waylandServer()->clients(); + for (auto c : clients) { + handleClientAdded(c); + } + } + const auto x11Clients = workspace()->clientList(); + for (auto c : x11Clients) { + handleClientAdded(c); + } + connect(workspace(), &Workspace::clientAdded, this, &DebugConsoleModel::handleClientAdded); + connect(workspace(), &Workspace::clientRemoved, this, &DebugConsoleModel::handleClientRemoved); + + const auto unmangeds = workspace()->unmanagedList(); + for (auto u : unmangeds) { + m_unmanageds.append(u); + } + connect(workspace(), &Workspace::unmanagedAdded, this, + [this] (Unmanaged *u) { + add(s_x11UnmanagedId -1, m_unmanageds, u); + } + ); + connect(workspace(), &Workspace::unmanagedRemoved, this, + [this] (Unmanaged *u) { + remove(s_x11UnmanagedId -1, m_unmanageds, u); + } + ); + for (InternalClient *client : workspace()->internalClients()) { + m_internalClients.append(client); + } + connect(workspace(), &Workspace::internalClientAdded, this, + [this](InternalClient *client) { + add(s_workspaceInternalId -1, m_internalClients, client); + } + ); + connect(workspace(), &Workspace::internalClientRemoved, this, + [this](InternalClient *client) { + remove(s_workspaceInternalId -1, m_internalClients, client); + } + ); +} + +void DebugConsoleModel::handleClientAdded(AbstractClient *client) +{ + X11Client *x11Client = qobject_cast(client); + if (x11Client) { + add(s_x11ClientId - 1, m_x11Clients, x11Client); + return; + } + + WaylandClient *waylandClient = qobject_cast(client); + if (waylandClient) { + add(s_waylandClientId - 1, m_waylandClients, waylandClient); + return; + } +} + +void DebugConsoleModel::handleClientRemoved(AbstractClient *client) +{ + X11Client *x11Client = qobject_cast(client); + if (x11Client) { + remove(s_x11ClientId - 1, m_x11Clients, x11Client); + return; + } + + WaylandClient *waylandClient = qobject_cast(client); + if (waylandClient) { + remove(s_waylandClientId - 1, m_waylandClients, waylandClient); + return; + } +} + +DebugConsoleModel::~DebugConsoleModel() = default; + +int DebugConsoleModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return 2; +} + +int DebugConsoleModel::topLevelRowCount() const +{ + return kwinApp()->shouldUseWaylandForCompositing() ? 4 : 2; +} + +template +int DebugConsoleModel::propertyCount(const QModelIndex &parent, T *(DebugConsoleModel::*filter)(const QModelIndex&) const) const +{ + if (T *t = (this->*filter)(parent)) { + return t->metaObject()->propertyCount(); + } + return 0; +} + +int DebugConsoleModel::rowCount(const QModelIndex &parent) const +{ + if (!parent.isValid()) { + return topLevelRowCount(); + } + + switch (parent.internalId()) { + case s_x11ClientId: + return m_x11Clients.count(); + case s_x11UnmanagedId: + return m_unmanageds.count(); + case s_waylandClientId: + return m_waylandClients.count(); + case s_workspaceInternalId: + return m_internalClients.count(); + default: + break; + } + + if (parent.internalId() & s_propertyBitMask) { + // properties do not have children + return 0; + } + + if (parent.internalId() < s_idDistance * (s_x11ClientId + 1)) { + return propertyCount(parent, &DebugConsoleModel::x11Client); + } else if (parent.internalId() < s_idDistance * (s_x11UnmanagedId + 1)) { + return propertyCount(parent, &DebugConsoleModel::unmanaged); + } else if (parent.internalId() < s_idDistance * (s_waylandClientId + 1)) { + return propertyCount(parent, &DebugConsoleModel::waylandClient); + } else if (parent.internalId() < s_idDistance * (s_workspaceInternalId + 1)) { + return propertyCount(parent, &DebugConsoleModel::internalClient); + } + + return 0; +} + +template +QModelIndex DebugConsoleModel::indexForClient(int row, int column, const QVector &clients, int id) const +{ + if (column != 0) { + return QModelIndex(); + } + if (row >= clients.count()) { + return QModelIndex(); + } + return createIndex(row, column, s_idDistance * id + row); +} + +template +QModelIndex DebugConsoleModel::indexForProperty(int row, int column, const QModelIndex &parent, T *(DebugConsoleModel::*filter)(const QModelIndex&) const) const +{ + if (T *t = (this->*filter)(parent)) { + if (row >= t->metaObject()->propertyCount()) { + return QModelIndex(); + } + return createIndex(row, column, quint32(row + 1) << 16 | parent.internalId()); + } + return QModelIndex(); +} + +QModelIndex DebugConsoleModel::index(int row, int column, const QModelIndex &parent) const +{ + if (!parent.isValid()) { + // index for a top level item + if (column != 0 || row >= topLevelRowCount()) { + return QModelIndex(); + } + return createIndex(row, column, row + 1); + } + if (column >= 2) { + // max of 2 columns + return QModelIndex(); + } + // index for a client (second level) + switch (parent.internalId()) { + case s_x11ClientId: + return indexForClient(row, column, m_x11Clients, s_x11ClientId); + case s_x11UnmanagedId: + return indexForClient(row, column, m_unmanageds, s_x11UnmanagedId); + case s_waylandClientId: + return indexForClient(row, column, m_waylandClients, s_waylandClientId); + case s_workspaceInternalId: + return indexForClient(row, column, m_internalClients, s_workspaceInternalId); + default: + break; + } + + // index for a property (third level) + if (parent.internalId() < s_idDistance * (s_x11ClientId + 1)) { + return indexForProperty(row, column, parent, &DebugConsoleModel::x11Client); + } else if (parent.internalId() < s_idDistance * (s_x11UnmanagedId + 1)) { + return indexForProperty(row, column, parent, &DebugConsoleModel::unmanaged); + } else if (parent.internalId() < s_idDistance * (s_waylandClientId + 1)) { + return indexForProperty(row, column, parent, &DebugConsoleModel::waylandClient); + } else if (parent.internalId() < s_idDistance * (s_workspaceInternalId + 1)) { + return indexForProperty(row, column, parent, &DebugConsoleModel::internalClient); + } + + return QModelIndex(); +} + +QModelIndex DebugConsoleModel::parent(const QModelIndex &child) const +{ + if (child.internalId() <= s_workspaceInternalId) { + return QModelIndex(); + } + if (child.internalId() & s_propertyBitMask) { + // a property + const quint32 parentId = child.internalId() & s_clientBitMask; + if (parentId < s_idDistance * (s_x11ClientId + 1)) { + return createIndex(parentId - (s_idDistance * s_x11ClientId), 0, parentId); + } else if (parentId < s_idDistance * (s_x11UnmanagedId + 1)) { + return createIndex(parentId - (s_idDistance * s_x11UnmanagedId), 0, parentId); + } else if (parentId < s_idDistance * (s_waylandClientId + 1)) { + return createIndex(parentId - (s_idDistance * s_waylandClientId), 0, parentId); + } else if (parentId < s_idDistance * (s_workspaceInternalId + 1)) { + return createIndex(parentId - (s_idDistance * s_workspaceInternalId), 0, parentId); + } + return QModelIndex(); + } + if (child.internalId() < s_idDistance * (s_x11ClientId + 1)) { + return createIndex(s_x11ClientId -1, 0, s_x11ClientId); + } else if (child.internalId() < s_idDistance * (s_x11UnmanagedId + 1)) { + return createIndex(s_x11UnmanagedId -1, 0, s_x11UnmanagedId); + } else if (child.internalId() < s_idDistance * (s_waylandClientId + 1)) { + return createIndex(s_waylandClientId -1, 0, s_waylandClientId); + } else if (child.internalId() < s_idDistance * (s_workspaceInternalId + 1)) { + return createIndex(s_workspaceInternalId -1, 0, s_workspaceInternalId); + } + return QModelIndex(); +} + +QVariant DebugConsoleModel::propertyData(QObject *object, const QModelIndex &index, int role) const +{ + Q_UNUSED(role) + const auto property = object->metaObject()->property(index.row()); + if (index.column() == 0) { + return property.name(); + } else { + const QVariant value = property.read(object); + if (qstrcmp(property.name(), "windowType") == 0) { + switch (value.toInt()) { + case NET::Normal: + return QStringLiteral("NET::Normal"); + case NET::Desktop: + return QStringLiteral("NET::Desktop"); + case NET::Dock: + return QStringLiteral("NET::Dock"); + case NET::Toolbar: + return QStringLiteral("NET::Toolbar"); + case NET::Menu: + return QStringLiteral("NET::Menu"); + case NET::Dialog: + return QStringLiteral("NET::Dialog"); + case NET::Override: + return QStringLiteral("NET::Override"); + case NET::TopMenu: + return QStringLiteral("NET::TopMenu"); + case NET::Utility: + return QStringLiteral("NET::Utility"); + case NET::Splash: + return QStringLiteral("NET::Splash"); + case NET::DropdownMenu: + return QStringLiteral("NET::DropdownMenu"); + case NET::PopupMenu: + return QStringLiteral("NET::PopupMenu"); + case NET::Tooltip: + return QStringLiteral("NET::Tooltip"); + case NET::Notification: + return QStringLiteral("NET::Notification"); + case NET::ComboBox: + return QStringLiteral("NET::ComboBox"); + case NET::DNDIcon: + return QStringLiteral("NET::DNDIcon"); + case NET::OnScreenDisplay: + return QStringLiteral("NET::OnScreenDisplay"); + case NET::CriticalNotification: + return QStringLiteral("NET::CriticalNotification"); + case NET::Unknown: + default: + return QStringLiteral("NET::Unknown"); + } + } + return value; + } + return QVariant(); +} + +template +QVariant DebugConsoleModel::clientData(const QModelIndex &index, int role, const QVector clients) const +{ + if (index.row() >= clients.count()) { + return QVariant(); + } + auto c = clients.at(index.row()); + if (role == Qt::DisplayRole) { + return QStringLiteral("%1: %2").arg(c->window()).arg(c->caption()); + } else if (role == Qt::DecorationRole) { + return c->icon(); + } + return QVariant(); +} + +QVariant DebugConsoleModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + if (!index.parent().isValid()) { + // one of the top levels + if (index.column() != 0 || role != Qt::DisplayRole) { + return QVariant(); + } + switch (index.internalId()) { + case s_x11ClientId: + return i18n("X11 Client Windows"); + case s_x11UnmanagedId: + return i18n("X11 Unmanaged Windows"); + case s_waylandClientId: + return i18n("Wayland Windows"); + case s_workspaceInternalId: + return i18n("Internal Windows"); + default: + return QVariant(); + } + } + if (index.internalId() & s_propertyBitMask) { + if (index.column() >= 2 || role != Qt::DisplayRole) { + return QVariant(); + } + if (AbstractClient *c = waylandClient(index)) { + return propertyData(c, index, role); + } else if (InternalClient *c = internalClient(index)) { + return propertyData(c, index, role); + } else if (X11Client *c = x11Client(index)) { + return propertyData(c, index, role); + } else if (Unmanaged *u = unmanaged(index)) { + return propertyData(u, index, role); + } + } else { + if (index.column() != 0) { + return QVariant(); + } + switch (index.parent().internalId()) { + case s_x11ClientId: + return clientData(index, role, m_x11Clients); + case s_x11UnmanagedId: { + if (index.row() >= m_unmanageds.count()) { + return QVariant(); + } + auto u = m_unmanageds.at(index.row()); + if (role == Qt::DisplayRole) { + return u->window(); + } + break; + } + case s_waylandClientId: + return clientData(index, role, m_waylandClients); + case s_workspaceInternalId: + return clientData(index, role, m_internalClients); + default: + break; + } + } + + return QVariant(); +} + +template +static T *clientForIndex(const QModelIndex &index, const QVector &clients, int id) +{ + const qint32 row = (index.internalId() & s_clientBitMask) - (s_idDistance * id); + if (row < 0 || row >= clients.count()) { + return nullptr; + } + return clients.at(row); +} + +WaylandClient *DebugConsoleModel::waylandClient(const QModelIndex &index) const +{ + return clientForIndex(index, m_waylandClients, s_waylandClientId); +} + +InternalClient *DebugConsoleModel::internalClient(const QModelIndex &index) const +{ + return clientForIndex(index, m_internalClients, s_workspaceInternalId); +} + +X11Client *DebugConsoleModel::x11Client(const QModelIndex &index) const +{ + return clientForIndex(index, m_x11Clients, s_x11ClientId); +} + +Unmanaged *DebugConsoleModel::unmanaged(const QModelIndex &index) const +{ + return clientForIndex(index, m_unmanageds, s_x11UnmanagedId); +} + +/////////////////////////////////////// SurfaceTreeModel +SurfaceTreeModel::SurfaceTreeModel(QObject *parent) + : QAbstractItemModel(parent) +{ + // TODO: it would be nice to not have to reset the model on each change + auto reset = [this] { + beginResetModel(); + endResetModel(); + }; + using namespace KWaylandServer; + + const auto unmangeds = workspace()->unmanagedList(); + for (auto u : unmangeds) { + if (!u->surface()) { + continue; + } + connect(u->surface(), &SurfaceInterface::subSurfaceTreeChanged, this, reset); + } + for (auto c : workspace()->allClientList()) { + if (!c->surface()) { + continue; + } + connect(c->surface(), &SurfaceInterface::subSurfaceTreeChanged, this, reset); + } + connect(workspace(), &Workspace::clientAdded, this, + [this, reset] (AbstractClient *c) { + if (c->surface()) { + connect(c->surface(), &SurfaceInterface::subSurfaceTreeChanged, this, reset); + } + reset(); + } + ); + connect(workspace(), &Workspace::clientRemoved, this, reset); + connect(workspace(), &Workspace::unmanagedAdded, this, + [this, reset] (Unmanaged *u) { + if (u->surface()) { + connect(u->surface(), &SurfaceInterface::subSurfaceTreeChanged, this, reset); + } + reset(); + } + ); + connect(workspace(), &Workspace::unmanagedRemoved, this, reset); +} + +SurfaceTreeModel::~SurfaceTreeModel() = default; + +int SurfaceTreeModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return 1; +} + +int SurfaceTreeModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + using namespace KWaylandServer; + if (SurfaceInterface *surface = static_cast(parent.internalPointer())) { + const auto &children = surface->childSubSurfaces(); + return children.count(); + } + return 0; + } + // toplevel are all windows + return workspace()->allClientList().count() + + workspace()->unmanagedList().count(); +} + +QModelIndex SurfaceTreeModel::index(int row, int column, const QModelIndex &parent) const +{ + if (column != 0) { + // invalid column + return QModelIndex(); + } + + if (parent.isValid()) { + using namespace KWaylandServer; + if (SurfaceInterface *surface = static_cast(parent.internalPointer())) { + const auto &children = surface->childSubSurfaces(); + if (row < children.count()) { + return createIndex(row, column, children.at(row)->surface().data()); + } + } + return QModelIndex(); + } + // a window + const auto &allClients = workspace()->allClientList(); + if (row < allClients.count()) { + // references a client + return createIndex(row, column, allClients.at(row)->surface()); + } + int reference = allClients.count(); + const auto &unmanaged = workspace()->unmanagedList(); + if (row < reference + unmanaged.count()) { + return createIndex(row, column, unmanaged.at(row-reference)->surface()); + } + reference += unmanaged.count(); + // not found + return QModelIndex(); +} + +QModelIndex SurfaceTreeModel::parent(const QModelIndex &child) const +{ + using namespace KWaylandServer; + if (SurfaceInterface *surface = static_cast(child.internalPointer())) { + const auto &subsurface = surface->subSurface(); + if (subsurface.isNull()) { + // doesn't reference a subsurface, this is a top-level window + return QModelIndex(); + } + SurfaceInterface *parent = subsurface->parentSurface().data(); + if (!parent) { + // something is wrong + return QModelIndex(); + } + // is the parent a subsurface itself? + if (parent->subSurface()) { + auto grandParent = parent->subSurface()->parentSurface(); + if (grandParent.isNull()) { + // something is wrong + return QModelIndex(); + } + const auto &children = grandParent->childSubSurfaces(); + for (int row = 0; row < children.count(); row++) { + if (children.at(row).data() == parent->subSurface().data()) { + return createIndex(row, 0, parent); + } + } + return QModelIndex(); + } + // not a subsurface, thus it's a true window + int row = 0; + const auto &allClients = workspace()->allClientList(); + for (; row < allClients.count(); row++) { + if (allClients.at(row)->surface() == parent) { + return createIndex(row, 0, parent); + } + } + row = allClients.count(); + const auto &unmanaged = workspace()->unmanagedList(); + for (int i = 0; i < unmanaged.count(); i++) { + if (unmanaged.at(i)->surface() == parent) { + return createIndex(row + i, 0, parent); + } + } + row += unmanaged.count(); + } + return QModelIndex(); +} + +QVariant SurfaceTreeModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + using namespace KWaylandServer; + if (SurfaceInterface *surface = static_cast(index.internalPointer())) { + if (role == Qt::DisplayRole || role == Qt::ToolTipRole) { + return QStringLiteral("%1 (%2) - %3").arg(surface->client()->executablePath()) + .arg(surface->client()->processId()) + .arg(surface->id()); + } else if (role == Qt::DecorationRole) { + if (auto buffer = surface->buffer()) { + if (buffer->shmBuffer()) { + return buffer->data().scaled(QSize(64, 64), Qt::KeepAspectRatio); + } + } + } + } + return QVariant(); +} + +InputDeviceModel::InputDeviceModel(QObject *parent) + : QAbstractItemModel(parent) + , m_devices(LibInput::Connection::self()->devices()) +{ + for (auto it = m_devices.constBegin(); it != m_devices.constEnd(); ++it) { + setupDeviceConnections(*it); + } + auto c = LibInput::Connection::self(); + connect(c, &LibInput::Connection::deviceAdded, this, + [this] (LibInput::Device *d) { + beginInsertRows(QModelIndex(), m_devices.count(), m_devices.count()); + m_devices << d; + setupDeviceConnections(d); + endInsertRows(); + } + ); + connect(c, &LibInput::Connection::deviceRemoved, this, + [this] (LibInput::Device *d) { + const int index = m_devices.indexOf(d); + if (index == -1) { + return; + } + beginRemoveRows(QModelIndex(), index, index); + m_devices.removeAt(index); + endRemoveRows(); + } + ); +} + +InputDeviceModel::~InputDeviceModel() = default; + + +int InputDeviceModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return 2; +} + +QVariant InputDeviceModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + if (!index.parent().isValid() && index.column() == 0) { + const auto devices = LibInput::Connection::self()->devices(); + if (index.row() >= devices.count()) { + return QVariant(); + } + if (role == Qt::DisplayRole) { + return devices.at(index.row())->name(); + } + } + if (index.parent().isValid()) { + if (role == Qt::DisplayRole) { + const auto device = LibInput::Connection::self()->devices().at(index.parent().row()); + const auto property = device->metaObject()->property(index.row()); + if (index.column() == 0) { + return property.name(); + } else if (index.column() == 1) { + return device->property(property.name()); + } + } + } + return QVariant(); +} + +QModelIndex InputDeviceModel::index(int row, int column, const QModelIndex &parent) const +{ + if (column >= 2) { + return QModelIndex(); + } + if (parent.isValid()) { + if (parent.internalId() & s_propertyBitMask) { + return QModelIndex(); + } + if (row >= LibInput::Connection::self()->devices().at(parent.row())->metaObject()->propertyCount()) { + return QModelIndex(); + } + return createIndex(row, column, quint32(row + 1) << 16 | parent.internalId()); + } + if (row >= LibInput::Connection::self()->devices().count()) { + return QModelIndex(); + } + return createIndex(row, column, row + 1); +} + +int InputDeviceModel::rowCount(const QModelIndex &parent) const +{ + if (!parent.isValid()) { + return LibInput::Connection::self()->devices().count(); + } + if (parent.internalId() & s_propertyBitMask) { + return 0; + } + + return LibInput::Connection::self()->devices().at(parent.row())->metaObject()->propertyCount(); +} + +QModelIndex InputDeviceModel::parent(const QModelIndex &child) const +{ + if (child.internalId() & s_propertyBitMask) { + const quintptr parentId = child.internalId() & s_clientBitMask; + return createIndex(parentId - 1, 0, parentId); + } + return QModelIndex(); +} + +void InputDeviceModel::setupDeviceConnections(LibInput::Device *device) +{ + connect(device, &LibInput::Device::enabledChanged, this, + [this, device] { + const QModelIndex parent = index(m_devices.indexOf(device), 0, QModelIndex()); + const QModelIndex child = index(device->metaObject()->indexOfProperty("enabled"), 1, parent); + emit dataChanged(child, child, QVector{Qt::DisplayRole}); + } + ); + connect(device, &LibInput::Device::leftHandedChanged, this, + [this, device] { + const QModelIndex parent = index(m_devices.indexOf(device), 0, QModelIndex()); + const QModelIndex child = index(device->metaObject()->indexOfProperty("leftHanded"), 1, parent); + emit dataChanged(child, child, QVector{Qt::DisplayRole}); + } + ); + connect(device, &LibInput::Device::pointerAccelerationChanged, this, + [this, device] { + const QModelIndex parent = index(m_devices.indexOf(device), 0, QModelIndex()); + const QModelIndex child = index(device->metaObject()->indexOfProperty("pointerAcceleration"), 1, parent); + emit dataChanged(child, child, QVector{Qt::DisplayRole}); + } + ); +} + +} diff --git a/debug_console.h b/debug_console.h new file mode 100644 index 0000000..a2d9b1d --- /dev/null +++ b/debug_console.h @@ -0,0 +1,185 @@ +/* + 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 KWIN_DEBUG_CONSOLE_H +#define KWIN_DEBUG_CONSOLE_H + +#include +#include +#include "input.h" +#include "input_event_spy.h" + +#include +#include +#include + +class QTextEdit; + +namespace Ui +{ +class DebugConsole; +} + +namespace KWin +{ + +class AbstractClient; +class X11Client; +class InternalClient; +class Unmanaged; +class DebugConsoleFilter; +class WaylandClient; + +class KWIN_EXPORT DebugConsoleModel : public QAbstractItemModel +{ + Q_OBJECT +public: + explicit DebugConsoleModel(QObject *parent = nullptr); + ~DebugConsoleModel() override; + + + int columnCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + QModelIndex index(int row, int column, const QModelIndex & parent) const override; + int rowCount(const QModelIndex &parent) const override; + QModelIndex parent(const QModelIndex &child) const override; + +private Q_SLOTS: + void handleClientAdded(AbstractClient *client); + void handleClientRemoved(AbstractClient *client); + +private: + template + QModelIndex indexForClient(int row, int column, const QVector &clients, int id) const; + template + QModelIndex indexForProperty(int row, int column, const QModelIndex &parent, T *(DebugConsoleModel::*filter)(const QModelIndex&) const) const; + template + int propertyCount(const QModelIndex &parent, T *(DebugConsoleModel::*filter)(const QModelIndex&) const) const; + QVariant propertyData(QObject *object, const QModelIndex &index, int role) const; + template + QVariant clientData(const QModelIndex &index, int role, const QVector clients) const; + template + void add(int parentRow, QVector &clients, T *client); + template + void remove(int parentRow, QVector &clients, T *client); + WaylandClient *waylandClient(const QModelIndex &index) const; + InternalClient *internalClient(const QModelIndex &index) const; + X11Client *x11Client(const QModelIndex &index) const; + Unmanaged *unmanaged(const QModelIndex &index) const; + int topLevelRowCount() const; + + QVector m_waylandClients; + QVector m_internalClients; + QVector m_x11Clients; + QVector m_unmanageds; + +}; + +class DebugConsoleDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + explicit DebugConsoleDelegate(QObject *parent = nullptr); + ~DebugConsoleDelegate() override; + + QString displayText(const QVariant &value, const QLocale &locale) const override; +}; + +class KWIN_EXPORT DebugConsole : public QWidget +{ + Q_OBJECT +public: + DebugConsole(); + ~DebugConsole() override; + +protected: + void showEvent(QShowEvent *event) override; + +private: + void initGLTab(); + void updateKeyboardTab(); + + QScopedPointer m_ui; + QScopedPointer m_inputFilter; +}; + +class SurfaceTreeModel : public QAbstractItemModel +{ + Q_OBJECT +public: + explicit SurfaceTreeModel(QObject *parent = nullptr); + ~SurfaceTreeModel() override; + + int columnCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + QModelIndex index(int row, int column, const QModelIndex & parent) const override; + int rowCount(const QModelIndex &parent) const override; + QModelIndex parent(const QModelIndex &child) const override; +}; + +class DebugConsoleFilter : public InputEventSpy +{ +public: + explicit DebugConsoleFilter(QTextEdit *textEdit); + ~DebugConsoleFilter() override; + + void pointerEvent(MouseEvent *event) override; + void wheelEvent(WheelEvent *event) override; + void keyEvent(KeyEvent *event) override; + void touchDown(qint32 id, const QPointF &pos, quint32 time) override; + void touchMotion(qint32 id, const QPointF &pos, quint32 time) override; + void touchUp(qint32 id, quint32 time) override; + + void pinchGestureBegin(int fingerCount, quint32 time) override; + void pinchGestureUpdate(qreal scale, qreal angleDelta, const QSizeF &delta, quint32 time) override; + void pinchGestureEnd(quint32 time) override; + void pinchGestureCancelled(quint32 time) override; + + void swipeGestureBegin(int fingerCount, quint32 time) override; + void swipeGestureUpdate(const QSizeF &delta, quint32 time) override; + void swipeGestureEnd(quint32 time) override; + void swipeGestureCancelled(quint32 time) override; + + void switchEvent(SwitchEvent *event) override; + + void tabletToolEvent(TabletEvent *event) override; + void tabletToolButtonEvent(const QSet &pressedButtons) override; + void tabletPadButtonEvent(const QSet &pressedButtons) override; + void tabletPadStripEvent(int number, int position, bool isFinger) override; + void tabletPadRingEvent(int number, int position, bool isFinger) override; + +private: + QTextEdit *m_textEdit; +}; + +namespace LibInput +{ +class Device; +} + +class InputDeviceModel : public QAbstractItemModel +{ + Q_OBJECT +public: + explicit InputDeviceModel(QObject *parent = nullptr); + ~InputDeviceModel() override; + + int columnCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + QModelIndex index(int row, int column, const QModelIndex & parent) const override; + int rowCount(const QModelIndex &parent) const override; + QModelIndex parent(const QModelIndex &child) const override; + +private: + void setupDeviceConnections(LibInput::Device *device); + QVector m_devices; +}; + +} + +#endif diff --git a/debug_console.ui b/debug_console.ui new file mode 100644 index 0000000..cb6cc0a --- /dev/null +++ b/debug_console.ui @@ -0,0 +1,432 @@ + + + DebugConsole + + + + 0 + 0 + 600 + 600 + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Quit Debug Console + + + + + + + + + 0 + + + + Windows + + + + + + 250 + + + + + + + + Surfaces + + + + + + + + + + Input Events + + + + + + false + + + true + + + + + + + + Input Devices + + + + + + + + + + OpenGL + + + + + + No OpenGL compositor running + + + + + + + QFrame::Plain + + + 0 + + + true + + + + + 0 + 0 + 564 + 471 + + + + + + + OpenGL (ES) driver information + + + + + + Vendor: + + + + + + + Renderer: + + + + + + + Version: + + + + + + + Shading Language Version: + + + + + + + Driver: + + + + + + + GPU class: + + + + + + + OpenGL Version: + + + + + + + GLSL Version: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Platform Extensions + + + + + + + + + + + + + + + + OpenGL (ES) Extensions + + + + + + + + + + + + + + + + + + + + + Keyboard + + + + + + QFrame::Plain + + + 0 + + + true + + + + + 0 + 0 + 564 + 495 + + + + + + + Keymap Layouts + + + + + + + + + + + + + Qt::Horizontal + + + + + + + + + Current Layout: + + + + + + + + + + + + + + + + + + + Modifiers + + + + + + + + + + + + + + + + Active Modifiers + + + + + + + + + + + + + + + + LEDs + + + + + + + + + + + + + + + + Active LEDs + + + + + + + + + + + + + + + + + + + + + + + + quitButton + + + + diff --git a/decorations/decoratedclient.cpp b/decorations/decoratedclient.cpp new file mode 100644 index 0000000..6ef1f00 --- /dev/null +++ b/decorations/decoratedclient.cpp @@ -0,0 +1,335 @@ +/* + 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 "decoratedclient.h" +#include "decorationbridge.h" +#include "decorationpalette.h" +#include "decorationrenderer.h" +#include "abstract_client.h" +#include "composite.h" +#include "cursor.h" +#include "options.h" +#include "platform.h" +#include "workspace.h" + +#include +#include + +#include +#include +#include + +namespace KWin +{ +namespace Decoration +{ + +DecoratedClientImpl::DecoratedClientImpl(AbstractClient *client, KDecoration2::DecoratedClient *decoratedClient, KDecoration2::Decoration *decoration) + : QObject() + , ApplicationMenuEnabledDecoratedClientPrivate(decoratedClient, decoration) + , m_client(client) + , m_clientSize(client->clientSize()) + , m_renderer(nullptr) +{ + createRenderer(); + client->setDecoratedClient(QPointer(this)); + connect(client, &AbstractClient::activeChanged, this, + [decoratedClient, client]() { + emit decoratedClient->activeChanged(client->isActive()); + } + ); + connect(client, &AbstractClient::clientGeometryChanged, this, + [decoratedClient, this]() { + if (m_client->clientSize() == m_clientSize) { + return; + } + const auto oldSize = m_clientSize; + m_clientSize = m_client->clientSize(); + if (oldSize.width() != m_clientSize.width()) { + emit decoratedClient->widthChanged(m_clientSize.width()); + } + if (oldSize.height() != m_clientSize.height()) { + emit decoratedClient->heightChanged(m_clientSize.height()); + } + emit decoratedClient->sizeChanged(m_clientSize); + } + ); + connect(client, &AbstractClient::desktopChanged, this, + [decoratedClient, client]() { + emit decoratedClient->onAllDesktopsChanged(client->isOnAllDesktops()); + } + ); + connect(client, &AbstractClient::captionChanged, this, + [decoratedClient, client]() { + emit decoratedClient->captionChanged(client->caption()); + } + ); + connect(client, &AbstractClient::iconChanged, this, + [decoratedClient, client]() { + emit decoratedClient->iconChanged(client->icon()); + } + ); + connect(client, &AbstractClient::shadeChanged, this, + &Decoration::DecoratedClientImpl::signalShadeChange); + connect(client, &AbstractClient::keepAboveChanged, decoratedClient, &KDecoration2::DecoratedClient::keepAboveChanged); + connect(client, &AbstractClient::keepBelowChanged, decoratedClient, &KDecoration2::DecoratedClient::keepBelowChanged); + connect(Compositor::self(), &Compositor::aboutToToggleCompositing, this, &DecoratedClientImpl::destroyRenderer); + m_compositorToggledConnection = connect(Compositor::self(), &Compositor::compositingToggled, this, + [this, decoration]() { + createRenderer(); + decoration->update(); + } + ); + connect(Compositor::self(), &Compositor::aboutToDestroy, this, + [this] { + disconnect(m_compositorToggledConnection); + m_compositorToggledConnection = QMetaObject::Connection(); + } + ); + connect(client, &AbstractClient::quickTileModeChanged, decoratedClient, + [this, decoratedClient]() { + emit decoratedClient->adjacentScreenEdgesChanged(adjacentScreenEdges()); + } + ); + connect(client, &AbstractClient::closeableChanged, decoratedClient, &KDecoration2::DecoratedClient::closeableChanged); + connect(client, &AbstractClient::shadeableChanged, decoratedClient, &KDecoration2::DecoratedClient::shadeableChanged); + connect(client, &AbstractClient::minimizeableChanged, decoratedClient, &KDecoration2::DecoratedClient::minimizeableChanged); + connect(client, &AbstractClient::maximizeableChanged, decoratedClient, &KDecoration2::DecoratedClient::maximizeableChanged); + + connect(client, &AbstractClient::paletteChanged, decoratedClient, &KDecoration2::DecoratedClient::paletteChanged); + + connect(client, &AbstractClient::hasApplicationMenuChanged, decoratedClient, &KDecoration2::DecoratedClient::hasApplicationMenuChanged); + connect(client, &AbstractClient::applicationMenuActiveChanged, decoratedClient, &KDecoration2::DecoratedClient::applicationMenuActiveChanged); + + m_toolTipWakeUp.setSingleShot(true); + connect(&m_toolTipWakeUp, &QTimer::timeout, this, + [this]() { + int fallAsleepDelay = QApplication::style()->styleHint(QStyle::SH_ToolTip_FallAsleepDelay); + this->m_toolTipFallAsleep.setRemainingTime(fallAsleepDelay); + + QToolTip::showText(Cursors::self()->mouse()->pos(), this->m_toolTipText); + m_toolTipShowing = true; + } + ); +} + +DecoratedClientImpl::~DecoratedClientImpl() +{ + if (m_toolTipShowing) { + requestHideToolTip(); + } +} + +void DecoratedClientImpl::signalShadeChange() { + emit decoratedClient()->shadedChanged(m_client->isShade()); +} + +#define DELEGATE(type, name, clientName) \ + type DecoratedClientImpl::name() const \ + { \ + return m_client->clientName(); \ + } + +#define DELEGATE2(type, name) DELEGATE(type, name, name) + +DELEGATE2(QString, caption) +DELEGATE2(bool, isActive) +DELEGATE2(bool, isCloseable) +DELEGATE(bool, isMaximizeable, isMaximizable) +DELEGATE(bool, isMinimizeable, isMinimizable) +DELEGATE2(bool, isModal) +DELEGATE(bool, isMoveable, isMovable) +DELEGATE(bool, isResizeable, isResizable) +DELEGATE2(bool, isShadeable) +DELEGATE2(bool, providesContextHelp) +DELEGATE2(int, desktop) +DELEGATE2(bool, isOnAllDesktops) +DELEGATE2(QPalette, palette) +DELEGATE2(QIcon, icon) + +#undef DELEGATE2 +#undef DELEGATE + +#define DELEGATE(type, name, clientName) \ + type DecoratedClientImpl::name() const \ + { \ + return m_client->clientName(); \ + } + +DELEGATE(bool, isKeepAbove, keepAbove) +DELEGATE(bool, isKeepBelow, keepBelow) +DELEGATE(bool, isShaded, isShade) +DELEGATE(WId, windowId, windowId) +DELEGATE(WId, decorationId, frameId) + +#undef DELEGATE + +#define DELEGATE(name, op) \ + void DecoratedClientImpl::name() \ + { \ + Workspace::self()->performWindowOperation(m_client, Options::op); \ + } + +DELEGATE(requestToggleShade, ShadeOp) +DELEGATE(requestToggleOnAllDesktops, OnAllDesktopsOp) +DELEGATE(requestToggleKeepAbove, KeepAboveOp) +DELEGATE(requestToggleKeepBelow, KeepBelowOp) + +#undef DELEGATE + +#define DELEGATE(name, clientName) \ + void DecoratedClientImpl::name() \ + { \ + m_client->clientName(); \ + } + +DELEGATE(requestContextHelp, showContextHelp) +DELEGATE(requestMinimize, minimize) + +#undef DELEGATE + +void DecoratedClientImpl::requestClose() +{ + QMetaObject::invokeMethod(m_client, "closeWindow", Qt::QueuedConnection); +} + +QColor DecoratedClientImpl::color(KDecoration2::ColorGroup group, KDecoration2::ColorRole role) const +{ + auto dp = m_client->decorationPalette(); + if (dp) { + return dp->color(group, role); + } + + return QColor(); +} + +void DecoratedClientImpl::requestShowToolTip(const QString &text) +{ + if (!DecorationBridge::self()->showToolTips()) { + return; + } + + m_toolTipText = text; + + int wakeUpDelay = QApplication::style()->styleHint(QStyle::SH_ToolTip_WakeUpDelay); + m_toolTipWakeUp.start(m_toolTipFallAsleep.hasExpired() ? wakeUpDelay : 20); +} + +void DecoratedClientImpl::requestHideToolTip() +{ + m_toolTipWakeUp.stop(); + QToolTip::hideText(); + m_toolTipShowing = false; +} + +void DecoratedClientImpl::requestShowWindowMenu() +{ + // TODO: add rect to requestShowWindowMenu + Workspace::self()->showWindowMenu(QRect(Cursors::self()->mouse()->pos(), Cursors::self()->mouse()->pos()), m_client); +} + +void DecoratedClientImpl::requestShowApplicationMenu(const QRect &rect, int actionId) +{ + Workspace::self()->showApplicationMenu(rect, m_client, actionId); +} + +void DecoratedClientImpl::showApplicationMenu(int actionId) +{ + decoration()->showApplicationMenu(actionId); +} + +void DecoratedClientImpl::requestToggleMaximization(Qt::MouseButtons buttons) +{ + QMetaObject::invokeMethod(this, "delayedRequestToggleMaximization", Qt::QueuedConnection, Q_ARG(Options::WindowOperation, options->operationMaxButtonClick(buttons))); +} + +void DecoratedClientImpl::delayedRequestToggleMaximization(Options::WindowOperation operation) +{ + Workspace::self()->performWindowOperation(m_client, operation); +} + +int DecoratedClientImpl::width() const +{ + return m_clientSize.width(); +} + +int DecoratedClientImpl::height() const +{ + return m_clientSize.height(); +} + +QSize DecoratedClientImpl::size() const +{ + return m_clientSize; +} + +bool DecoratedClientImpl::isMaximizedVertically() const +{ + return m_client->requestedMaximizeMode() & MaximizeVertical; +} + +bool DecoratedClientImpl::isMaximized() const +{ + return isMaximizedHorizontally() && isMaximizedVertically(); +} + +bool DecoratedClientImpl::isMaximizedHorizontally() const +{ + return m_client->requestedMaximizeMode() & MaximizeHorizontal; +} + +Qt::Edges DecoratedClientImpl::adjacentScreenEdges() const +{ + Qt::Edges edges; + const QuickTileMode mode = m_client->quickTileMode(); + if (mode.testFlag(QuickTileFlag::Left)) { + edges |= Qt::LeftEdge; + if (!mode.testFlag(QuickTileFlag::Top) && !mode.testFlag(QuickTileFlag::Bottom)) { + // using complete side + edges |= Qt::TopEdge | Qt::BottomEdge; + } + } + if (mode.testFlag(QuickTileFlag::Top)) { + edges |= Qt::TopEdge; + } + if (mode.testFlag(QuickTileFlag::Right)) { + edges |= Qt::RightEdge; + if (!mode.testFlag(QuickTileFlag::Top) && !mode.testFlag(QuickTileFlag::Bottom)) { + // using complete side + edges |= Qt::TopEdge | Qt::BottomEdge; + } + } + if (mode.testFlag(QuickTileFlag::Bottom)) { + edges |= Qt::BottomEdge; + } + return edges; +} + +bool DecoratedClientImpl::hasApplicationMenu() const +{ + return m_client->hasApplicationMenu(); +} + +bool DecoratedClientImpl::isApplicationMenuActive() const +{ + return m_client->applicationMenuActive(); +} + +void DecoratedClientImpl::createRenderer() +{ + m_renderer = kwinApp()->platform()->createDecorationRenderer(this); +} + +void DecoratedClientImpl::destroyRenderer() +{ + delete m_renderer; + m_renderer = nullptr; +} + +} +} diff --git a/decorations/decoratedclient.h b/decorations/decoratedclient.h new file mode 100644 index 0000000..be818e7 --- /dev/null +++ b/decorations/decoratedclient.h @@ -0,0 +1,114 @@ +/* + 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 +*/ +#ifndef KWIN_DECORATED_CLIENT_H +#define KWIN_DECORATED_CLIENT_H +#include "options.h" + +#include + +#include +#include +#include + +namespace KWin +{ + +class AbstractClient; + +namespace Decoration +{ + +class Renderer; + +class DecoratedClientImpl : public QObject, public KDecoration2::ApplicationMenuEnabledDecoratedClientPrivate +{ + Q_OBJECT +public: + explicit DecoratedClientImpl(AbstractClient *client, KDecoration2::DecoratedClient *decoratedClient, KDecoration2::Decoration *decoration); + ~DecoratedClientImpl() override; + QString caption() const override; + WId decorationId() const override; + int desktop() const override; + int height() const override; + QIcon icon() const override; + bool isActive() const override; + bool isCloseable() const override; + bool isKeepAbove() const override; + bool isKeepBelow() const override; + bool isMaximizeable() const override; + bool isMaximized() const override; + bool isMaximizedHorizontally() const override; + bool isMaximizedVertically() const override; + bool isMinimizeable() const override; + bool isModal() const override; + bool isMoveable() const override; + bool isOnAllDesktops() const override; + bool isResizeable() const override; + bool isShadeable() const override; + bool isShaded() const override; + QPalette palette() const override; + QColor color(KDecoration2::ColorGroup group, KDecoration2::ColorRole role) const override; + bool providesContextHelp() const override; + QSize size() const override; + int width() const override; + WId windowId() const override; + + Qt::Edges adjacentScreenEdges() const override; + + bool hasApplicationMenu() const override; + bool isApplicationMenuActive() const override; + + void requestShowToolTip(const QString &text) override; + void requestHideToolTip() override; + void requestClose() override; + void requestContextHelp() override; + void requestToggleMaximization(Qt::MouseButtons buttons) override; + void requestMinimize() override; + void requestShowWindowMenu() override; + void requestShowApplicationMenu(const QRect &rect, int actionId) override; + void requestToggleKeepAbove() override; + void requestToggleKeepBelow() override; + void requestToggleOnAllDesktops() override; + void requestToggleShade() override; + + void showApplicationMenu(int actionId) override; + + AbstractClient *client() { + return m_client; + } + Renderer *renderer() { + return m_renderer; + } + KDecoration2::DecoratedClient *decoratedClient() { + return KDecoration2::DecoratedClientPrivate::client(); + } + + void signalShadeChange(); + +private Q_SLOTS: + void delayedRequestToggleMaximization(Options::WindowOperation operation); + +private: + void createRenderer(); + void destroyRenderer(); + AbstractClient *m_client; + QSize m_clientSize; + Renderer *m_renderer; + QMetaObject::Connection m_compositorToggledConnection; + + QString m_toolTipText; + QTimer m_toolTipWakeUp; + QDeadlineTimer m_toolTipFallAsleep; + bool m_toolTipShowing = false; +}; + +} +} + +#endif diff --git a/decorations/decorationbridge.cpp b/decorations/decorationbridge.cpp new file mode 100644 index 0000000..567d5cc --- /dev/null +++ b/decorations/decorationbridge.cpp @@ -0,0 +1,323 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "decorationbridge.h" +#include "decoratedclient.h" +#include "decorationrenderer.h" +#include "decorations_logging.h" +#include "settings.h" +// KWin core +#include "abstract_client.h" +#include "composite.h" +#include "scene.h" +#include "wayland_server.h" +#include "workspace.h" +#include + +// KDecoration +#include +#include +#include + +// KWayland +#include + +// Frameworks +#include +#include + +// Qt +#include +#include + +namespace KWin +{ +namespace Decoration +{ + +static const QString s_aurorae = QStringLiteral("org.kde.kwin.aurorae"); +static const QString s_pluginName = QStringLiteral("org.kde.kdecoration2"); +#if HAVE_BREEZE_DECO +static const QString s_defaultPlugin = QStringLiteral(BREEZE_KDECORATION_PLUGIN_ID); +#else +static const QString s_defaultPlugin = s_aurorae; +#endif + +KWIN_SINGLETON_FACTORY(DecorationBridge) + +DecorationBridge::DecorationBridge(QObject *parent) + : KDecoration2::DecorationBridge(parent) + , m_factory(nullptr) + , m_blur(false) + , m_showToolTips(false) + , m_settings() + , m_noPlugin(false) +{ + KConfigGroup cg(KSharedConfig::openConfig(), "KDE"); + + // try to extract the proper defaults file from a lookandfeel package + const QString looknfeel = cg.readEntry(QStringLiteral("LookAndFeelPackage"), "org.kde.breeze.desktop"); + m_lnfConfig = KSharedConfig::openConfig(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("plasma/look-and-feel/") + looknfeel + QStringLiteral("/contents/defaults"))); + + readDecorationOptions(); +} + +DecorationBridge::~DecorationBridge() +{ + s_self = nullptr; +} + +QString DecorationBridge::readPlugin() +{ + //Try to get a default from look and feel + KConfigGroup cg(m_lnfConfig, "kwinrc"); + cg = KConfigGroup(&cg, "org.kde.kdecoration2"); + return kwinApp()->config()->group(s_pluginName).readEntry("library", cg.readEntry("library", s_defaultPlugin)); +} + +static bool readNoPlugin() +{ + return kwinApp()->config()->group(s_pluginName).readEntry("NoPlugin", false); +} + +QString DecorationBridge::readTheme() const +{ + //Try to get a default from look and feel + KConfigGroup cg(m_lnfConfig, "kwinrc"); + cg = KConfigGroup(&cg, "org.kde.kdecoration2"); + return kwinApp()->config()->group(s_pluginName).readEntry("theme", cg.readEntry("theme", m_defaultTheme)); +} + +void DecorationBridge::readDecorationOptions() +{ + m_showToolTips = kwinApp()->config()->group(s_pluginName).readEntry("ShowToolTips", true); +} + +void DecorationBridge::init() +{ + using namespace KWaylandServer; + m_noPlugin = readNoPlugin(); + if (m_noPlugin) { + if (waylandServer()) { + waylandServer()->decorationManager()->setDefaultMode(ServerSideDecorationManagerInterface::Mode::None); + } + return; + } + m_plugin = readPlugin(); + m_settings = QSharedPointer::create(this); + initPlugin(); + if (!m_factory) { + if (m_plugin != s_defaultPlugin) { + // try loading default plugin + m_plugin = s_defaultPlugin; + initPlugin(); + } + // default plugin failed to load, try fallback + if (!m_factory) { + m_plugin = s_aurorae; + initPlugin(); + } + } + if (waylandServer()) { + waylandServer()->decorationManager()->setDefaultMode(m_factory ? ServerSideDecorationManagerInterface::Mode::Server : ServerSideDecorationManagerInterface::Mode::None); + } +} + +void DecorationBridge::initPlugin() +{ + const auto offers = KPluginLoader::findPluginsById(s_pluginName, m_plugin); + if (offers.isEmpty()) { + qCWarning(KWIN_DECORATIONS) << "Could not locate decoration plugin"; + return; + } + qCDebug(KWIN_DECORATIONS) << "Trying to load decoration plugin: " << offers.first().fileName(); + KPluginLoader loader(offers.first().fileName()); + KPluginFactory *factory = loader.factory(); + if (!factory) { + qCWarning(KWIN_DECORATIONS) << "Error loading plugin:" << loader.errorString(); + } else { + m_factory = factory; + loadMetaData(loader.metaData().value(QStringLiteral("MetaData")).toObject()); + } +} + +static void recreateDecorations() +{ + Workspace::self()->forEachAbstractClient([](AbstractClient *c) { c->updateDecoration(true, true); }); +} + +void DecorationBridge::reconfigure() +{ + readDecorationOptions(); + + if (m_noPlugin != readNoPlugin()) { + m_noPlugin = !m_noPlugin; + // no plugin setting changed + if (m_noPlugin) { + // decorations disabled now + m_plugin = QString(); + delete m_factory; + m_factory = nullptr; + m_settings.clear(); + } else { + // decorations enabled now + init(); + } + recreateDecorations(); + return; + } + + const QString newPlugin = readPlugin(); + if (newPlugin != m_plugin) { + // plugin changed, recreate everything + auto oldFactory = m_factory; + const auto oldPluginName = m_plugin; + m_plugin = newPlugin; + initPlugin(); + if (m_factory == oldFactory) { + // loading new plugin failed + m_factory = oldFactory; + m_plugin = oldPluginName; + } else { + recreateDecorations(); + // TODO: unload and destroy old plugin + } + } else { + // same plugin, but theme might have changed + const QString oldTheme = m_theme; + m_theme = readTheme(); + if (m_theme != oldTheme) { + recreateDecorations(); + } + } +} + +void DecorationBridge::loadMetaData(const QJsonObject &object) +{ + // reset all settings + m_blur = false; + m_recommendedBorderSize = QString(); + m_theme = QString(); + m_defaultTheme = QString(); + + // load the settings + const QJsonValue decoSettings = object.value(s_pluginName); + if (decoSettings.isUndefined()) { + // no settings + return; + } + const QVariantMap decoSettingsMap = decoSettings.toObject().toVariantMap(); + auto blurIt = decoSettingsMap.find(QStringLiteral("blur")); + if (blurIt != decoSettingsMap.end()) { + m_blur = blurIt.value().toBool(); + } + auto recBorderSizeIt = decoSettingsMap.find(QStringLiteral("recommendedBorderSize")); + if (recBorderSizeIt != decoSettingsMap.end()) { + m_recommendedBorderSize = recBorderSizeIt.value().toString(); + } + findTheme(decoSettingsMap); + + Q_EMIT metaDataLoaded(); +} + +void DecorationBridge::findTheme(const QVariantMap &map) +{ + auto it = map.find(QStringLiteral("themes")); + if (it == map.end()) { + return; + } + if (!it.value().toBool()) { + return; + } + it = map.find(QStringLiteral("defaultTheme")); + m_defaultTheme = it != map.end() ? it.value().toString() : QString(); + m_theme = readTheme(); +} + +std::unique_ptr DecorationBridge::createClient(KDecoration2::DecoratedClient *client, KDecoration2::Decoration *decoration) +{ + return std::unique_ptr(new DecoratedClientImpl(static_cast(decoration->parent()), client, decoration)); +} + +std::unique_ptr DecorationBridge::settings(KDecoration2::DecorationSettings *parent) +{ + return std::unique_ptr(new SettingsImpl(parent)); +} + +void DecorationBridge::update(KDecoration2::Decoration *decoration, const QRect &geometry) +{ + // TODO: remove check once all compositors implement it + if (AbstractClient *c = Workspace::self()->findAbstractClient([decoration] (const AbstractClient *client) { return client->decoration() == decoration; })) { + if (Renderer *renderer = c->decoratedClient()->renderer()) { + renderer->schedule(geometry); + } + } +} + +KDecoration2::Decoration *DecorationBridge::createDecoration(AbstractClient *client) +{ + if (m_noPlugin) { + return nullptr; + } + if (!m_factory) { + return nullptr; + } + QVariantMap args({ {QStringLiteral("bridge"), QVariant::fromValue(this)} }); + + if (!m_theme.isEmpty()) { + args.insert(QStringLiteral("theme"), m_theme); + } + auto deco = m_factory->create(client, QVariantList({args})); + deco->setSettings(m_settings); + deco->init(); + return deco; +} + +static +QString settingsProperty(const QVariant &variant) +{ + if (QLatin1String(variant.typeName()) == QLatin1String("KDecoration2::BorderSize")) { + return QString::number(variant.toInt()); + } else if (QLatin1String(variant.typeName()) == QLatin1String("QVector")) { + const auto &b = variant.value>(); + QString buffer; + for (auto it = b.begin(); it != b.end(); ++it) { + if (it != b.begin()) { + buffer.append(QStringLiteral(", ")); + } + buffer.append(QString::number(int(*it))); + } + return buffer; + } + return variant.toString(); +} + +QString DecorationBridge::supportInformation() const +{ + QString b; + if (m_noPlugin) { + b.append(QStringLiteral("Decorations are disabled")); + } else { + b.append(QStringLiteral("Plugin: %1\n").arg(m_plugin)); + b.append(QStringLiteral("Theme: %1\n").arg(m_theme)); + b.append(QStringLiteral("Plugin recommends border size: %1\n").arg(m_recommendedBorderSize.isNull() ? "No" : m_recommendedBorderSize)); + b.append(QStringLiteral("Blur: %1\n").arg(m_blur)); + const QMetaObject *metaOptions = m_settings->metaObject(); + for (int i=0; ipropertyCount(); ++i) { + const QMetaProperty property = metaOptions->property(i); + if (QLatin1String(property.name()) == QLatin1String("objectName")) { + continue; + } + b.append(QStringLiteral("%1: %2\n").arg(property.name()).arg(settingsProperty(m_settings->property(property.name())))); + } + } + return b; +} + +} // Decoration +} // KWin diff --git a/decorations/decorationbridge.h b/decorations/decorationbridge.h new file mode 100644 index 0000000..8cf7ffd --- /dev/null +++ b/decorations/decorationbridge.h @@ -0,0 +1,93 @@ +/* + 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 +*/ +#ifndef KWIN_DECORATION_BRIDGE_H +#define KWIN_DECORATION_BRIDGE_H + +#include + +#include + +#include + +#include +#include + +class KPluginFactory; + +namespace KDecoration2 +{ +class DecorationSettings; +} + +namespace KWin +{ + +class AbstractClient; + +namespace Decoration +{ + +class KWIN_EXPORT DecorationBridge : public KDecoration2::DecorationBridge +{ + Q_OBJECT +public: + ~DecorationBridge() override; + + void init(); + KDecoration2::Decoration *createDecoration(AbstractClient *client); + + std::unique_ptr createClient(KDecoration2::DecoratedClient *client, KDecoration2::Decoration *decoration) override; + std::unique_ptr settings(KDecoration2::DecorationSettings *parent) override; + void update(KDecoration2::Decoration *decoration, const QRect &geometry) override; + + bool needsBlur() const { + return m_blur; + } + QString recommendedBorderSize() const { + return m_recommendedBorderSize; + } + + bool showToolTips() const { + return m_showToolTips; + } + + void reconfigure(); + + const QSharedPointer &settings() const { + return m_settings; + } + + QString supportInformation() const; + +Q_SIGNALS: + void metaDataLoaded(); + +private: + QString readPlugin(); + void loadMetaData(const QJsonObject &object); + void findTheme(const QVariantMap &map); + void initPlugin(); + QString readTheme() const; + void readDecorationOptions(); + KPluginFactory *m_factory; + KSharedConfig::Ptr m_lnfConfig; + bool m_blur; + bool m_showToolTips; + QString m_recommendedBorderSize; + QString m_plugin; + QString m_defaultTheme; + QString m_theme; + QSharedPointer m_settings; + bool m_noPlugin; + KWIN_SINGLETON(DecorationBridge) +}; +} // Decoration +} // KWin + +#endif diff --git a/decorations/decorationpalette.cpp b/decorations/decorationpalette.cpp new file mode 100644 index 0000000..c7c240b --- /dev/null +++ b/decorations/decorationpalette.cpp @@ -0,0 +1,127 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2014 Martin Gräßlin + SPDX-FileCopyrightText: 2014 Hugo Pereira Da Costa + SPDX-FileCopyrightText: 2015 Mika Allan Rauhala + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "decorationpalette.h" +#include "decorations_logging.h" + +#include +#include +#include + +#include +#include +#include + +namespace KWin +{ +namespace Decoration +{ + +DecorationPalette::DecorationPalette(const QString &colorScheme) + : m_colorScheme(QFileInfo(colorScheme).isAbsolute() + ? colorScheme + : QStandardPaths::locate(QStandardPaths::GenericConfigLocation, colorScheme)) +{ + if (!m_colorScheme.startsWith(QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation)) && colorScheme == QStringLiteral("kdeglobals")) { + // kdeglobals doesn't exist so create it. This is needed to monitor it using QFileSystemWatcher. + auto config = KSharedConfig::openConfig(colorScheme, KConfig::SimpleConfig); + KConfigGroup wmConfig(config, QStringLiteral("WM")); + wmConfig.writeEntry("FakeEntryToKeepThisGroup", true); + config->sync(); + + m_colorScheme = QStandardPaths::locate(QStandardPaths::GenericConfigLocation, colorScheme); + } + m_watcher.addPath(m_colorScheme); + connect(&m_watcher, &QFileSystemWatcher::fileChanged, [this]() { + m_watcher.addPath(m_colorScheme); + update(); + emit changed(); + }); + + update(); +} + +bool DecorationPalette::isValid() const +{ + return m_activeTitleBarColor.isValid(); +} + +QColor DecorationPalette::color(KDecoration2::ColorGroup group, KDecoration2::ColorRole role) const +{ + using KDecoration2::ColorRole; + using KDecoration2::ColorGroup; + + switch (role) { + case ColorRole::Frame: + switch (group) { + case ColorGroup::Active: + return m_activeFrameColor; + case ColorGroup::Inactive: + return m_inactiveFrameColor; + default: + return QColor(); + } + case ColorRole::TitleBar: + switch (group) { + case ColorGroup::Active: + return m_activeTitleBarColor; + case ColorGroup::Inactive: + return m_inactiveTitleBarColor; + default: + return QColor(); + } + case ColorRole::Foreground: + switch (group) { + case ColorGroup::Active: + return m_activeForegroundColor; + case ColorGroup::Inactive: + return m_inactiveForegroundColor; + case ColorGroup::Warning: + return m_warningForegroundColor; + default: + return QColor(); + } + default: + return QColor(); + } +} + +QPalette DecorationPalette::palette() const +{ + return m_palette; +} + +void DecorationPalette::update() +{ + auto config = KSharedConfig::openConfig(m_colorScheme, KConfig::SimpleConfig); + KConfigGroup wmConfig(config, QStringLiteral("WM")); + + if (!wmConfig.exists() && !m_colorScheme.endsWith(QStringLiteral("/kdeglobals"))) { + qCWarning(KWIN_DECORATIONS) << "Invalid color scheme" << m_colorScheme << "lacks WM group"; + return; + } + + m_palette = KColorScheme::createApplicationPalette(config); + + m_activeFrameColor = wmConfig.readEntry("frame", m_palette.color(QPalette::Active, QPalette::Window)); + m_inactiveFrameColor = wmConfig.readEntry("inactiveFrame", m_activeFrameColor); + m_activeTitleBarColor = wmConfig.readEntry("activeBackground", m_palette.color(QPalette::Active, QPalette::Highlight)); + m_inactiveTitleBarColor = wmConfig.readEntry("inactiveBackground", m_inactiveFrameColor); + m_activeForegroundColor = wmConfig.readEntry("activeForeground", m_palette.color(QPalette::Active, QPalette::HighlightedText)); + m_inactiveForegroundColor = wmConfig.readEntry("inactiveForeground", m_activeForegroundColor.darker()); + + KConfigGroup windowColorsConfig(config, QStringLiteral("Colors:Window")); + m_warningForegroundColor = windowColorsConfig.readEntry("ForegroundNegative", QColor(237, 21, 2)); + +} + +} +} diff --git a/decorations/decorationpalette.h b/decorations/decorationpalette.h new file mode 100644 index 0000000..0764afe --- /dev/null +++ b/decorations/decorationpalette.h @@ -0,0 +1,59 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2014 Martin Gräßlin + SPDX-FileCopyrightText: 2014 Hugo Pereira Da Costa + SPDX-FileCopyrightText: 2015 Mika Allan Rauhala + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_DECORATION_PALETTE_H +#define KWIN_DECORATION_PALETTE_H + +#include +#include +#include + +namespace KWin +{ +namespace Decoration +{ + +class DecorationPalette : public QObject +{ + Q_OBJECT +public: + DecorationPalette(const QString &colorScheme); + + bool isValid() const; + + QColor color(KDecoration2::ColorGroup group, KDecoration2::ColorRole role) const; + QPalette palette() const; + +Q_SIGNALS: + void changed(); +private: + void update(); + + QString m_colorScheme; + QFileSystemWatcher m_watcher; + + QPalette m_palette; + + QColor m_activeTitleBarColor; + QColor m_inactiveTitleBarColor; + + QColor m_activeFrameColor; + QColor m_inactiveFrameColor; + + QColor m_activeForegroundColor; + QColor m_inactiveForegroundColor; + QColor m_warningForegroundColor; +}; + +} +} + +#endif diff --git a/decorations/decorationrenderer.cpp b/decorations/decorationrenderer.cpp new file mode 100644 index 0000000..9a4506f --- /dev/null +++ b/decorations/decorationrenderer.cpp @@ -0,0 +1,101 @@ +/* + 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 "decorationrenderer.h" +#include "decoratedclient.h" +#include "decorations/decorations_logging.h" +#include "deleted.h" +#include "abstract_client.h" +#include "screens.h" + +#include +#include + +#include +#include + +namespace KWin +{ +namespace Decoration +{ + +Renderer::Renderer(DecoratedClientImpl *client) + : QObject(client) + , m_client(client) + , m_imageSizesDirty(true) +{ + auto markImageSizesDirty = [this]{ + schedule(m_client->client()->rect()); + m_imageSizesDirty = true; + }; + connect(client->client(), &AbstractClient::screenScaleChanged, this, markImageSizesDirty); + connect(client->decoration(), &KDecoration2::Decoration::bordersChanged, this, markImageSizesDirty); + connect(client->decoratedClient(), &KDecoration2::DecoratedClient::sizeChanged, this, markImageSizesDirty); +} + +Renderer::~Renderer() = default; + +void Renderer::schedule(const QRect &rect) +{ + m_scheduled = m_scheduled.united(rect); + emit renderScheduled(rect); +} + +QRegion Renderer::getScheduled() +{ + QRegion region = m_scheduled; + m_scheduled = QRegion(); + return region; +} + +QImage Renderer::renderToImage(const QRect &geo) +{ + Q_ASSERT(m_client); + auto dpr = client()->client()->screenScale(); + + // Guess the pixel format of the X pixmap into which the QImage will be copied. + QImage::Format format; + const int depth = client()->client()->depth(); + switch (depth) { + case 30: + format = QImage::Format_A2RGB30_Premultiplied; + break; + case 24: + case 32: + format = QImage::Format_ARGB32_Premultiplied; + break; + default: + qCCritical(KWIN_DECORATIONS) << "Unsupported client depth" << depth; + format = QImage::Format_ARGB32_Premultiplied; + break; + }; + + QImage image(geo.width() * dpr, geo.height() * dpr, format); + image.setDevicePixelRatio(dpr); + image.fill(Qt::transparent); + QPainter p(&image); + p.setRenderHint(QPainter::Antialiasing); + p.setWindow(QRect(geo.topLeft(), geo.size() * dpr)); + p.setClipRect(geo); + renderToPainter(&p, geo); + return image; +} + +void Renderer::renderToPainter(QPainter *painter, const QRect &rect) +{ + client()->decoration()->paint(painter, rect); +} + +void Renderer::reparent(Deleted *deleted) +{ + setParent(deleted); + m_client = nullptr; +} + +} +} diff --git a/decorations/decorationrenderer.h b/decorations/decorationrenderer.h new file mode 100644 index 0000000..bc2b5a3 --- /dev/null +++ b/decorations/decorationrenderer.h @@ -0,0 +1,76 @@ +/* + 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 +*/ +#ifndef KWIN_DECORATION_RENDERER_H +#define KWIN_DECORATION_RENDERER_H + +#include +#include + +#include + +namespace KWin +{ + +class Deleted; + +namespace Decoration +{ + +class DecoratedClientImpl; + +class KWIN_EXPORT Renderer : public QObject +{ + Q_OBJECT +public: + ~Renderer() override; + + void schedule(const QRect &rect); + + /** + * Reparents this Renderer to the @p deleted. + * After this call the Renderer is no longer able to render + * anything, client() returns a nullptr. + */ + virtual void reparent(Deleted *deleted); + +Q_SIGNALS: + void renderScheduled(const QRect &geo); + +protected: + explicit Renderer(DecoratedClientImpl *client); + /** + * @returns the scheduled paint region and resets + */ + QRegion getScheduled(); + + virtual void render() = 0; + + DecoratedClientImpl *client() { + return m_client; + } + + bool areImageSizesDirty() const { + return m_imageSizesDirty; + } + void resetImageSizesDirty() { + m_imageSizesDirty = false; + } + QImage renderToImage(const QRect &geo); + void renderToPainter(QPainter *painter, const QRect &rect); + +private: + DecoratedClientImpl *m_client; + QRegion m_scheduled; + bool m_imageSizesDirty; +}; + +} +} + +#endif diff --git a/decorations/decorations_logging.cpp b/decorations/decorations_logging.cpp new file mode 100644 index 0000000..180ca49 --- /dev/null +++ b/decorations/decorations_logging.cpp @@ -0,0 +1,10 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "decorations_logging.h" +Q_LOGGING_CATEGORY(KWIN_DECORATIONS, "kwin_decorations", QtCriticalMsg) diff --git a/decorations/decorations_logging.h b/decorations/decorations_logging.h new file mode 100644 index 0000000..0bd66b5 --- /dev/null +++ b/decorations/decorations_logging.h @@ -0,0 +1,15 @@ +/* + 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_DECORATIONS_LOGGING_H +#define KWIN_DECORATIONS_LOGGING_H +#include +#include +Q_DECLARE_LOGGING_CATEGORY(KWIN_DECORATIONS) + +#endif diff --git a/decorations/settings.cpp b/decorations/settings.cpp new file mode 100644 index 0000000..eacd370 --- /dev/null +++ b/decorations/settings.cpp @@ -0,0 +1,193 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "settings.h" +// KWin +#include "decorationbridge.h" +#include "composite.h" +#include "virtualdesktops.h" +#include "workspace.h" +#include "appmenu.h" + +#include + +#include + +#include + +#include + +namespace KWin +{ +namespace Decoration +{ +SettingsImpl::SettingsImpl(KDecoration2::DecorationSettings *parent) + : QObject() + , DecorationSettingsPrivate(parent) + , m_borderSize(KDecoration2::BorderSize::Normal) +{ + readSettings(); + + auto c = connect(Compositor::self(), &Compositor::compositingToggled, + parent, &KDecoration2::DecorationSettings::alphaChannelSupportedChanged); + connect(VirtualDesktopManager::self(), &VirtualDesktopManager::countChanged, this, + [parent](uint previous, uint current) { + if (previous != 1 && current != 1) { + return; + } + emit parent->onAllDesktopsAvailableChanged(current > 1); + } + ); + // prevent changes in Decoration due to Compositor being destroyed + connect(Compositor::self(), &Compositor::aboutToDestroy, this, + [c] { disconnect(c); } + ); + connect(Workspace::self(), &Workspace::configChanged, this, &SettingsImpl::readSettings); + connect(DecorationBridge::self(), &DecorationBridge::metaDataLoaded, this, &SettingsImpl::readSettings); +} + +SettingsImpl::~SettingsImpl() = default; + +bool SettingsImpl::isAlphaChannelSupported() const +{ + return Compositor::self()->compositing(); +} + +bool SettingsImpl::isOnAllDesktopsAvailable() const +{ + return VirtualDesktopManager::self()->count() > 1; +} + +bool SettingsImpl::isCloseOnDoubleClickOnMenu() const +{ + return m_closeDoubleClickMenu; +} + +static QHash s_buttonNames; +static void initButtons() +{ + if (!s_buttonNames.isEmpty()) { + return; + } + s_buttonNames[KDecoration2::DecorationButtonType::Menu] = QChar('M'); + s_buttonNames[KDecoration2::DecorationButtonType::ApplicationMenu] = QChar('N'); + s_buttonNames[KDecoration2::DecorationButtonType::OnAllDesktops] = QChar('S'); + s_buttonNames[KDecoration2::DecorationButtonType::ContextHelp] = QChar('H'); + s_buttonNames[KDecoration2::DecorationButtonType::Minimize] = QChar('I'); + s_buttonNames[KDecoration2::DecorationButtonType::Maximize] = QChar('A'); + s_buttonNames[KDecoration2::DecorationButtonType::Close] = QChar('X'); + s_buttonNames[KDecoration2::DecorationButtonType::KeepAbove] = QChar('F'); + s_buttonNames[KDecoration2::DecorationButtonType::KeepBelow] = QChar('B'); + s_buttonNames[KDecoration2::DecorationButtonType::Shade] = QChar('L'); +} + +static QString buttonsToString(const QVector &buttons) +{ + auto buttonToString = [](KDecoration2::DecorationButtonType button) -> QChar { + const auto it = s_buttonNames.constFind(button); + if (it != s_buttonNames.constEnd()) { + return it.value(); + } + return QChar(); + }; + QString ret; + for (auto button : buttons) { + ret.append(buttonToString(button)); + } + return ret; +} + +QVector< KDecoration2::DecorationButtonType > SettingsImpl::readDecorationButtons(const KConfigGroup &config, + const char *key, + const QVector< KDecoration2::DecorationButtonType > &defaultValue) const +{ + initButtons(); + auto buttonsFromString = [](const QString &buttons) -> QVector { + QVector ret; + for (auto it = buttons.begin(); it != buttons.end(); ++it) { + for (auto it2 = s_buttonNames.constBegin(); it2 != s_buttonNames.constEnd(); ++it2) { + if (it2.value() == (*it)) { + ret << it2.key(); + } + } + } + return ret; + }; + return buttonsFromString(config.readEntry(key, buttonsToString(defaultValue))); +} + +static KDecoration2::BorderSize stringToSize(const QString &name) +{ + static const QMap s_sizes = QMap({ + {QStringLiteral("None"), KDecoration2::BorderSize::None}, + {QStringLiteral("NoSides"), KDecoration2::BorderSize::NoSides}, + {QStringLiteral("Tiny"), KDecoration2::BorderSize::Tiny}, + {QStringLiteral("Normal"), KDecoration2::BorderSize::Normal}, + {QStringLiteral("Large"), KDecoration2::BorderSize::Large}, + {QStringLiteral("VeryLarge"), KDecoration2::BorderSize::VeryLarge}, + {QStringLiteral("Huge"), KDecoration2::BorderSize::Huge}, + {QStringLiteral("VeryHuge"), KDecoration2::BorderSize::VeryHuge}, + {QStringLiteral("Oversized"), KDecoration2::BorderSize::Oversized} + }); + auto it = s_sizes.constFind(name); + if (it == s_sizes.constEnd()) { + // non sense values are interpreted just like normal + return KDecoration2::BorderSize::Normal; + } + return it.value(); +} + +void SettingsImpl::readSettings() +{ + KConfigGroup config = kwinApp()->config()->group(QStringLiteral("org.kde.kdecoration2")); + const auto &left = readDecorationButtons(config, "ButtonsOnLeft", QVector({ + KDecoration2::DecorationButtonType::Menu, + KDecoration2::DecorationButtonType::OnAllDesktops + })); + if (left != m_leftButtons) { + m_leftButtons = left; + emit decorationSettings()->decorationButtonsLeftChanged(m_leftButtons); + } + const auto &right = readDecorationButtons(config, "ButtonsOnRight", QVector({ + KDecoration2::DecorationButtonType::ContextHelp, + KDecoration2::DecorationButtonType::Minimize, + KDecoration2::DecorationButtonType::Maximize, + KDecoration2::DecorationButtonType::Close + })); + if (right != m_rightButtons) { + m_rightButtons = right; + emit decorationSettings()->decorationButtonsRightChanged(m_rightButtons); + } + ApplicationMenu::self()->setViewEnabled(left.contains(KDecoration2::DecorationButtonType::ApplicationMenu) || right.contains(KDecoration2::DecorationButtonType::ApplicationMenu)); + const bool close = config.readEntry("CloseOnDoubleClickOnMenu", false); + if (close != m_closeDoubleClickMenu) { + m_closeDoubleClickMenu = close; + emit decorationSettings()->closeOnDoubleClickOnMenuChanged(m_closeDoubleClickMenu); + } + m_autoBorderSize = config.readEntry("BorderSizeAuto", true); + + auto size = stringToSize(config.readEntry("BorderSize", QStringLiteral("Normal"))); + if (m_autoBorderSize) { + /* Falls back to Normal border size, if the plugin does not provide a valid recommendation. */ + size = stringToSize(DecorationBridge::self()->recommendedBorderSize()); + } + if (size != m_borderSize) { + m_borderSize = size; + emit decorationSettings()->borderSizeChanged(m_borderSize); + } + const QFont font = QFontDatabase::systemFont(QFontDatabase::TitleFont); + if (font != m_font) { + m_font = font; + emit decorationSettings()->fontChanged(m_font); + } + + emit decorationSettings()->reconfigured(); +} + +} +} diff --git a/decorations/settings.h b/decorations/settings.h new file mode 100644 index 0000000..250447f --- /dev/null +++ b/decorations/settings.h @@ -0,0 +1,60 @@ +/* + 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 +*/ +#ifndef KWIN_DECORATION_SETTINGS_H +#define KWIN_DECORATION_SETTINGS_H + +#include + +#include + +class KConfigGroup; + +namespace KWin +{ +namespace Decoration +{ + +class SettingsImpl : public QObject, public KDecoration2::DecorationSettingsPrivate +{ + Q_OBJECT +public: + explicit SettingsImpl(KDecoration2::DecorationSettings *parent); + ~SettingsImpl() override; + bool isAlphaChannelSupported() const override; + bool isOnAllDesktopsAvailable() const override; + bool isCloseOnDoubleClickOnMenu() const override; + KDecoration2::BorderSize borderSize() const override { + return m_borderSize; + } + QVector< KDecoration2::DecorationButtonType > decorationButtonsLeft() const override { + return m_leftButtons; + } + QVector< KDecoration2::DecorationButtonType > decorationButtonsRight() const override { + return m_rightButtons; + } + QFont font() const override { + return m_font; + } + +private: + void readSettings(); + QVector< KDecoration2::DecorationButtonType > readDecorationButtons(const KConfigGroup &config, + const char *key, + const QVector< KDecoration2::DecorationButtonType > &defaultValue) const; + QVector< KDecoration2::DecorationButtonType > m_leftButtons; + QVector< KDecoration2::DecorationButtonType > m_rightButtons; + KDecoration2::BorderSize m_borderSize; + bool m_autoBorderSize = true; + bool m_closeDoubleClickMenu = false; + QFont m_font; +}; +} // Decoration +} // KWin + +#endif diff --git a/deleted.cpp b/deleted.cpp new file mode 100644 index 0000000..20982de --- /dev/null +++ b/deleted.cpp @@ -0,0 +1,301 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "deleted.h" + +#include "workspace.h" +#include "x11client.h" +#include "group.h" +#include "netinfo.h" +#include "shadow.h" +#include "waylandclient.h" +#include "decorations/decoratedclient.h" +#include "decorations/decorationrenderer.h" + +#include + +namespace KWin +{ + +Deleted::Deleted() + : Toplevel() + , delete_refcount(1) + , m_frame(XCB_WINDOW_NONE) + , no_border(true) + , m_layer(UnknownLayer) + , m_minimized(false) + , m_modal(false) + , m_wasClient(false) + , m_decorationRenderer(nullptr) + , m_fullscreen(false) + , m_keepAbove(false) + , m_keepBelow(false) + , m_wasActive(false) + , m_wasX11Client(false) + , m_wasWaylandClient(false) + , m_wasGroupTransient(false) + , m_wasPopupWindow(false) + , m_wasOutline(false) +{ +} + +Deleted::~Deleted() +{ + const QRegion dirty = repaints(); + if (!dirty.isEmpty()) { + addWorkspaceRepaint(dirty); + } + + if (delete_refcount != 0) + qCCritical(KWIN_CORE) << "Deleted client has non-zero reference count (" << delete_refcount << ")"; + Q_ASSERT(delete_refcount == 0); + if (workspace()) { + workspace()->removeDeleted(this); + } + for (Toplevel *toplevel : qAsConst(m_transientFor)) { + if (auto *deleted = qobject_cast(toplevel)) { + deleted->removeTransient(this); + } + } + for (Deleted *transient : qAsConst(m_transients)) { + transient->removeTransientFor(this); + } + deleteEffectWindow(); +} + +Deleted* Deleted::create(Toplevel* c) +{ + Deleted* d = new Deleted(); + d->copyToDeleted(c); + workspace()->addDeleted(d, c); + return d; +} + +// to be used only from Workspace::finishCompositing() +void Deleted::discard() +{ + delete_refcount = 0; + delete this; +} + +void Deleted::copyToDeleted(Toplevel* c) +{ + Q_ASSERT(dynamic_cast< Deleted* >(c) == nullptr); + Toplevel::copyToDeleted(c); + m_bufferGeometry = c->bufferGeometry(); + m_bufferMargins = c->bufferMargins(); + m_frameMargins = c->frameMargins(); + m_bufferScale = c->bufferScale(); + desk = c->desktop(); + m_desktops = c->desktops(); + activityList = c->activities(); + contentsRect = QRect(c->clientPos(), c->clientSize()); + m_contentPos = c->clientContentPos(); + transparent_rect = c->transparentRect(); + m_layer = c->layer(); + m_frame = c->frameId(); + m_opacity = c->opacity(); + m_type = c->windowType(); + m_windowRole = c->windowRole(); + if (WinInfo* cinfo = dynamic_cast< WinInfo* >(info)) + cinfo->disable(); + if (AbstractClient *client = dynamic_cast(c)) { + no_border = client->noBorder(); + if (!no_border) { + client->layoutDecorationRects(decoration_left, + decoration_top, + decoration_right, + decoration_bottom); + if (client->isDecorated()) { + if (Decoration::Renderer *renderer = client->decoratedClient()->renderer()) { + m_decorationRenderer = renderer; + m_decorationRenderer->reparent(this); + } + } + } + m_wasClient = true; + m_minimized = client->isMinimized(); + m_modal = client->isModal(); + m_mainClients = client->mainClients(); + foreach (AbstractClient *c, m_mainClients) { + addTransientFor(c); + connect(c, &AbstractClient::windowClosed, this, &Deleted::mainClientClosed); + } + m_fullscreen = client->isFullScreen(); + m_keepAbove = client->keepAbove(); + m_keepBelow = client->keepBelow(); + m_caption = client->caption(); + + m_wasActive = client->isActive(); + + m_wasGroupTransient = client->groupTransient(); + } + + for (auto vd : m_desktops) { + connect(vd, &QObject::destroyed, this, [=] { + m_desktops.removeOne(vd); + }); + } + + m_wasWaylandClient = qobject_cast(c) != nullptr; + m_wasX11Client = qobject_cast(c) != nullptr; + m_wasPopupWindow = c->isPopupWindow(); + m_wasOutline = c->isOutline(); +} + +void Deleted::unrefWindow() +{ + if (--delete_refcount > 0) + return; + // needs to be delayed + // a) when calling from effects, otherwise it'd be rather complicated to handle the case of the + // window going away during a painting pass + // b) to prevent dangeling pointers in the stacking order, see bug #317765 + deleteLater(); +} + +QRect Deleted::bufferGeometry() const +{ + return m_bufferGeometry; +} + +QMargins Deleted::bufferMargins() const +{ + return m_bufferMargins; +} + +QMargins Deleted::frameMargins() const +{ + return m_frameMargins; +} + +qreal Deleted::bufferScale() const +{ + return m_bufferScale; +} + +int Deleted::desktop() const +{ + return desk; +} + +QStringList Deleted::activities() const +{ + return activityList; +} + +QVector Deleted::desktops() const +{ + return m_desktops; +} + +QPoint Deleted::clientPos() const +{ + return contentsRect.topLeft(); +} + +void Deleted::layoutDecorationRects(QRect& left, QRect& top, QRect& right, QRect& bottom) const +{ + left = decoration_left; + top = decoration_top; + right = decoration_right; + bottom = decoration_bottom; +} + +QRect Deleted::transparentRect() const +{ + return transparent_rect; +} + +bool Deleted::isDeleted() const +{ + return true; +} + +NET::WindowType Deleted::windowType(bool direct, int supportedTypes) const +{ + Q_UNUSED(direct) + Q_UNUSED(supportedTypes) + return m_type; +} + +void Deleted::mainClientClosed(Toplevel *client) +{ + if (AbstractClient *c = dynamic_cast(client)) + m_mainClients.removeAll(c); +} + +void Deleted::transientForClosed(Toplevel *toplevel, Deleted *deleted) +{ + if (deleted == nullptr) { + m_transientFor.removeAll(toplevel); + return; + } + + const int index = m_transientFor.indexOf(toplevel); + if (index == -1) { + return; + } + + m_transientFor[index] = deleted; + deleted->addTransient(this); +} + +xcb_window_t Deleted::frameId() const +{ + return m_frame; +} + +double Deleted::opacity() const +{ + return m_opacity; +} + +QByteArray Deleted::windowRole() const +{ + return m_windowRole; +} + +QVector Deleted::x11DesktopIds() const +{ + const auto desks = desktops(); + QVector x11Ids; + x11Ids.reserve(desks.count()); + std::transform(desks.constBegin(), desks.constEnd(), + std::back_inserter(x11Ids), + [] (const VirtualDesktop *vd) { + return vd->x11DesktopNumber(); + } + ); + return x11Ids; +} + +void Deleted::addTransient(Deleted *transient) +{ + m_transients.append(transient); +} + +void Deleted::removeTransient(Deleted *transient) +{ + m_transients.removeAll(transient); +} + +void Deleted::addTransientFor(AbstractClient *parent) +{ + m_transientFor.append(parent); + connect(parent, &AbstractClient::windowClosed, this, &Deleted::transientForClosed); +} + +void Deleted::removeTransientFor(Deleted *parent) +{ + m_transientFor.removeAll(parent); +} + +} // namespace + diff --git a/deleted.h b/deleted.h new file mode 100644 index 0000000..93232a8 --- /dev/null +++ b/deleted.h @@ -0,0 +1,239 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_DELETED_H +#define KWIN_DELETED_H + +#include "toplevel.h" + +namespace KWin +{ + +class AbstractClient; + +namespace Decoration +{ +class Renderer; +} + +class KWIN_EXPORT Deleted : public Toplevel +{ + Q_OBJECT + +public: + static Deleted* create(Toplevel* c); + // used by effects to keep the window around for e.g. fadeout effects when it's destroyed + void refWindow(); + void unrefWindow(); + void discard(); + QRect bufferGeometry() const override; + QMargins bufferMargins() const override; + QMargins frameMargins() const override; + qreal bufferScale() const override; + int desktop() const override; + QStringList activities() const override; + QVector desktops() const override; + QPoint clientPos() const override; + QPoint clientContentPos() const override { + return m_contentPos; + } + QRect transparentRect() const override; + bool isDeleted() const override; + xcb_window_t frameId() const override; + bool noBorder() const { + return no_border; + } + void layoutDecorationRects(QRect &left, QRect &top, QRect &right, QRect &bottom) const; + Layer layer() const override { + return m_layer; + } + bool isMinimized() const { + return m_minimized; + } + bool isModal() const { + return m_modal; + } + QList mainClients() const { + return m_mainClients; + } + NET::WindowType windowType(bool direct = false, int supported_types = 0) const override; + bool wasClient() const { + return m_wasClient; + } + double opacity() const override; + QByteArray windowRole() const override; + + const Decoration::Renderer *decorationRenderer() const { + return m_decorationRenderer; + } + + bool isFullScreen() const { + return m_fullscreen; + } + + bool keepAbove() const { + return m_keepAbove; + } + bool keepBelow() const { + return m_keepBelow; + } + QString caption() const { + return m_caption; + } + + /** + * Returns whether the client was active. + * + * @returns @c true if the client was active at the time when it was closed, + * @c false otherwise + */ + bool wasActive() const { + return m_wasActive; + } + + /** + * Returns whether this was an X11 client. + * + * @returns @c true if it was an X11 client, @c false otherwise. + */ + bool wasX11Client() const { + return m_wasX11Client; + } + + /** + * Returns whether this was a Wayland client. + * + * @returns @c true if it was a Wayland client, @c false otherwise. + */ + bool wasWaylandClient() const { + return m_wasWaylandClient; + } + + /** + * Returns whether the client was a transient. + * + * @returns @c true if it was a transient, @c false otherwise. + */ + bool wasTransient() const { + return !m_transientFor.isEmpty(); + } + + /** + * Returns whether the client was a group transient. + * + * @returns @c true if it was a group transient, @c false otherwise. + * @note This is relevant only for X11 clients. + */ + bool wasGroupTransient() const { + return m_wasGroupTransient; + } + + /** + * Checks whether this client was a transient for given toplevel. + * + * @param toplevel Toplevel against which we are testing. + * @returns @c true if it was a transient for given toplevel, @c false otherwise. + */ + bool wasTransientFor(const Toplevel *toplevel) const { + return m_transientFor.contains(const_cast(toplevel)); + } + + /** + * Returns the list of transients. + * + * Because the window is Deleted, it can have only Deleted child transients. + */ + QList transients() const { + return m_transients; + } + + /** + * Returns whether the client was a popup. + * + * @returns @c true if the client was a popup, @c false otherwise. + */ + bool isPopupWindow() const override { + return m_wasPopupWindow; + } + + QVector x11DesktopIds() const; + + /** + * Whether this Deleted represents the outline. + */ + bool isOutline() const override { + return m_wasOutline; + } + +private Q_SLOTS: + void mainClientClosed(KWin::Toplevel *client); + void transientForClosed(Toplevel *toplevel, Deleted *deleted); + +private: + Deleted(); // use create() + void copyToDeleted(Toplevel* c); + ~Deleted() override; // deleted only using unrefWindow() + + void addTransient(Deleted *transient); + void removeTransient(Deleted *transient); + void addTransientFor(AbstractClient *parent); + void removeTransientFor(Deleted *parent); + + QRect m_bufferGeometry; + QMargins m_bufferMargins; + QMargins m_frameMargins; + + int delete_refcount; + int desk; + QStringList activityList; + QRect contentsRect; // for clientPos()/clientSize() + QPoint m_contentPos; + QRect transparent_rect; + xcb_window_t m_frame; + QVector m_desktops; + + bool no_border; + QRect decoration_left; + QRect decoration_right; + QRect decoration_top; + QRect decoration_bottom; + Layer m_layer; + bool m_minimized; + bool m_modal; + QList m_mainClients; + bool m_wasClient; + Decoration::Renderer *m_decorationRenderer; + double m_opacity; + NET::WindowType m_type = NET::Unknown; + QByteArray m_windowRole; + bool m_fullscreen; + bool m_keepAbove; + bool m_keepBelow; + QString m_caption; + bool m_wasActive; + bool m_wasX11Client; + bool m_wasWaylandClient; + bool m_wasGroupTransient; + QList m_transientFor; + QList m_transients; + bool m_wasPopupWindow; + bool m_wasOutline; + qreal m_bufferScale = 1; +}; + +inline void Deleted::refWindow() +{ + ++delete_refcount; +} + +} // namespace + +Q_DECLARE_METATYPE(KWin::Deleted*) + +#endif diff --git a/dmabuftexture.cpp b/dmabuftexture.cpp new file mode 100644 index 0000000..f1b4d8a --- /dev/null +++ b/dmabuftexture.cpp @@ -0,0 +1,28 @@ +/* + SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "dmabuftexture.h" + +#include "kwineglimagetexture.h" +#include "kwinglutils.h" + +namespace KWin +{ + +DmaBufTexture::DmaBufTexture(KWin::GLTexture *texture) + : m_texture(texture) + , m_framebuffer(new KWin::GLRenderTarget(*m_texture)) +{ +} + +DmaBufTexture::~DmaBufTexture() = default; + +KWin::GLRenderTarget *DmaBufTexture::framebuffer() const +{ + return m_framebuffer.data(); +} + +} // namespace KWin diff --git a/dmabuftexture.h b/dmabuftexture.h new file mode 100644 index 0000000..ac5b149 --- /dev/null +++ b/dmabuftexture.h @@ -0,0 +1,31 @@ +/* + SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once +#include "kwin_export.h" +#include + +namespace KWin +{ +class GLRenderTarget; +class GLTexture; + +class KWIN_EXPORT DmaBufTexture +{ +public: + explicit DmaBufTexture(KWin::GLTexture* texture); + virtual ~DmaBufTexture(); + + virtual quint32 stride() const = 0; + virtual int fd() const = 0; + KWin::GLRenderTarget* framebuffer() const; + +protected: + QScopedPointer m_texture; + QScopedPointer m_framebuffer; +}; + +} diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt new file mode 100644 index 0000000..4b9858e --- /dev/null +++ b/doc/CMakeLists.txt @@ -0,0 +1,7 @@ +ecm_optional_add_subdirectory(desktop) +ecm_optional_add_subdirectory(kwindecoration) +ecm_optional_add_subdirectory(kwinscreenedges) +ecm_optional_add_subdirectory(kwintabbox) +ecm_optional_add_subdirectory(windowbehaviour) +ecm_optional_add_subdirectory(windowspecific) +ecm_optional_add_subdirectory(kwineffects) diff --git a/doc/coding-conventions.md b/doc/coding-conventions.md new file mode 100644 index 0000000..42bb4bf --- /dev/null +++ b/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/doc/desktop/CMakeLists.txt b/doc/desktop/CMakeLists.txt new file mode 100644 index 0000000..b3bc4ec --- /dev/null +++ b/doc/desktop/CMakeLists.txt @@ -0,0 +1,2 @@ +########### install files ############### +kdoctools_create_handbook(index.docbook INSTALL_DESTINATION ${HTML_INSTALL_DIR}/en SUBDIR kcontrol/desktop) diff --git a/doc/desktop/index.docbook b/doc/desktop/index.docbook new file mode 100644 index 0000000..3e3ef04 --- /dev/null +++ b/doc/desktop/index.docbook @@ -0,0 +1,98 @@ + + + +]> + +
+Virtual Desktops + + + +&Mike.McBride; &Mike.McBride.mail; +&Jost.Schenck; &Jost.Schenck.mail; + + + +2015-04-09 +Plasma 5.3 + + +KDE +Systemsettings +desktop + + + + + +Virtual Desktops + + +<guilabel>Desktops</guilabel> + +&kde; offers you the possibility to have several virtual +desktops. In this tab you can configure the number of desktops, the number of rows in the Pager icon +as well as their names. Just use the input box to adjust the number of +desktops. You can assign names to the desktops by entering text into the +text fields below. + + + + +<guilabel>Switching</guilabel> + + + +Desktop navigation wraps around +Enable this option if you want keyboard or active desktop border navigation +beyond the edge of a desktop to take you to the opposite edge of the new desktop. + + + + +Desktop Effect Animation +Select No Animation, Slide, +Desktop Cube Animation or Fade Desktop +from the drop down box. 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. + + + + +Desktop Switch On-Screen Display +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. + + + + +Shortcuts +This section displays the configured shortcuts for switching the desktops +and allows you to edit them. + + + + + +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/doc/kwindecoration/CMakeLists.txt b/doc/kwindecoration/CMakeLists.txt new file mode 100644 index 0000000..ab41cae --- /dev/null +++ b/doc/kwindecoration/CMakeLists.txt @@ -0,0 +1,2 @@ +########### install files ############### +kdoctools_create_handbook(index.docbook INSTALL_DESTINATION ${HTML_INSTALL_DIR}/en SUBDIR kcontrol/kwindecoration) diff --git a/doc/kwindecoration/button.png b/doc/kwindecoration/button.png new file mode 100644 index 0000000000000000000000000000000000000000..e744ea478b72f4ef1dee39393d1b79247003e258 GIT binary patch literal 27081 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 KY^jV{=>G%udz>8r literal 0 HcmV?d00001 diff --git a/doc/kwindecoration/configure.png b/doc/kwindecoration/configure.png new file mode 100644 index 0000000000000000000000000000000000000000..d68764b4adafa50b28963e3b9d21c7b4d664937b GIT binary patch literal 384 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 c&cy>SbY&JUH6ts + + +]> + +
+ + +&Rik.Hemsley; &Rik.Hemsley.mail; +&Anne-Marie.Mahfouf; &Anne-Marie.Mahfouf.mail; + + + +2018-03-18 +Plasma 5.12 + + +KDE +Systemsettings +kwin +window +border +theme +style + + + +Window Decoration + +This module allows you to select a style for the buttons and borders around +windows. + + +Window Decorations + + + +Window Decoration Configuration Module + + + + + + Window Decoration Configuration Module + + + + + +Choose a window decoration style from the preview list, using the +search field at the top of the screen 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 +border size here. These large borders are easier to see for low +vision users, and easier to grab for people with limited mobility or +difficulty using a mouse. + + + + + +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 + + + + + + + +Buttons + +This tab 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 menu button +to have an additional option to the Close button or if you have removed the Close button from the titlebar. + + + + + + +
diff --git a/doc/kwindecoration/main.png b/doc/kwindecoration/main.png new file mode 100644 index 0000000000000000000000000000000000000000..55f6fb1d7e7e4ad9730b228c6a3cc3f219cad7d0 GIT binary patch literal 36500 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


LlMYX;;sdyL>@@0?eOVUsO%ec`<$yF)Kd= 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%0D|t9K=3o5TX7! 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 literal 0 HcmV?d00001 diff --git a/doc/kwineffects/CMakeLists.txt b/doc/kwineffects/CMakeLists.txt new file mode 100644 index 0000000..8f01b06 --- /dev/null +++ b/doc/kwineffects/CMakeLists.txt @@ -0,0 +1,2 @@ +########### install files ############### +kdoctools_create_handbook(index.docbook INSTALL_DESTINATION ${HTML_INSTALL_DIR}/en SUBDIR kcontrol/kwineffects) diff --git a/doc/kwineffects/configure-effects.png b/doc/kwineffects/configure-effects.png new file mode 100644 index 0000000000000000000000000000000000000000..ebfa55d49177f18d8ef6cf11ee397cdc6b13a7c9 GIT binary patch literal 512 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10A|${k#P$FG|GReY@eK&l z(Kqr74BowGuZp^svYO_ez54=!LIZ21sKjz@&Fy{Yqv$7K?l<(={7-Dhy?nG;$1_d6Miwul^8ajUL z{%_0b?Vi-T-eclfmD!QU>O3zqiMQyfD0zj-U6^6muP0V>t*I;7bhncr0V4trO$q6BL!5%;OXk;vd$@? F2>^HLtfl|} literal 0 HcmV?d00001 diff --git a/doc/kwineffects/dialog-information.png b/doc/kwineffects/dialog-information.png new file mode 100644 index 0000000000000000000000000000000000000000..f2d21888cd00ba0294a9411b026310479381549b GIT binary patch literal 745 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmSN`?>!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/doc/kwineffects/index.docbook b/doc/kwineffects/index.docbook new file mode 100644 index 0000000..b36c080 --- /dev/null +++ b/doc/kwineffects/index.docbook @@ -0,0 +1,85 @@ + + + +]> + +
+ + +Desktop Effects + +&Mike.McBride; &Mike.McBride.mail; + + + +2016-04-14 +Plasma 5.6 + + +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, Tools, +and Window Management. +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 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 +Minimize Animations 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 XRender). + + +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/doc/kwineffects/video.png b/doc/kwineffects/video.png new file mode 100644 index 0000000000000000000000000000000000000000..1e67c0a0f04bf1c8b626f5bf943bb742c08a1be8 GIT binary patch literal 375 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@K3?z5nYFPoKSkfJR9T^xl_H+M9WCijSl0AZa z85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=Y7GC&cyt|NqKrnlE3z zw6L;0*PF8#D9)JV?e4vL>4nJ@Vx|KMm}RF8K5A0iKnkC`(qAX4hy|# zmkl5xJx>?M5RU7~2?y9DoPnSrgn^m4LntAk;Na28QCqcG7cFF2$UmPgM9ziTjDg{K zC(mid=+mo!I#o+tBT7;dOH!?pi&B9UgOP!urLKX6uAy;=frXW+nU#r|wt<0_fx#wT z@5v|{a`RI%(<*UmI2`a + + +]> + +
+ + +Screen Edges + +&Mike.McBride; &Mike.McBride.mail; + + + +2015-07-14 +Plasma 5.3 + + +KDE +Systemsettings +desktop +effects + + + +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 on the 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. + +In the Window Management section you can enable to +Maximize windows by dragging them to the top of the screen +or to Tile windows by dragging them to the side of the window +and set a percentage of the screen to trigger the tiling. + + +In the Other Settings section 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/doc/kwintabbox/CMakeLists.txt b/doc/kwintabbox/CMakeLists.txt new file mode 100644 index 0000000..33d20e1 --- /dev/null +++ b/doc/kwintabbox/CMakeLists.txt @@ -0,0 +1,2 @@ +########### install files ############### +kdoctools_create_handbook(index.docbook INSTALL_DESTINATION ${HTML_INSTALL_DIR}/en SUBDIR kcontrol/kwintabbox) diff --git a/doc/kwintabbox/index.docbook b/doc/kwintabbox/index.docbook new file mode 100644 index 0000000..020fcd7 --- /dev/null +++ b/doc/kwintabbox/index.docbook @@ -0,0 +1,102 @@ + + + +]> +
+ +Task Switcher + +&Martin.Graesslin;&Martin.Graesslin.mail; + + + +2015-07-24 +&plasma; 5.4 + + +KDE +System Settings +desktop +window +navigation + + + + +Navigating through Windows + +This module offers the possibility to configure the behaviour for Navigating through windows often referred to as &Alt; . There are two independent sets of effects which can have different settings. For each of this sets there is an own tab (Main and Alternative) in this module. + + + +The first set of effects on the Main tab has predefined shortcuts. If you want to use the second set of effects on the Alternative tab, you have to set a shortcut for these effects in the Shortcut Editor manually. + + +For navigating through windows without &Alt; , you can define screen edge actions in the &systemsettings; module Screen Edges. + + +The following documentation of options applies to the general settings and the alternative settings as well. + + +Visualization + +There are several effects which can be used instead of the normal window list when compositing is enabled. By default the Breeze effect is used. This effect displays a small thumbnail of each window in a list at the left of the screen and the currently selected window is highlighted. +There are several additional predefined layouts which provide an informative or compact view, small or large icons, a grid and window title only. +Selecting one of these layouts you will have a button to show a preview. +If you enable Show selected window the currently selected window will be highlighted by fading out all other +windows. This option requires desktop effects to be active. + + +Cover Switch and Flip Switch effect are more fancy effects which require OpenGL. Cover Switch displays the windows in a gallery with a large thumbnail of the currently selected window in the center of the screen while all other windows are rotated on the left and right. Flip Switch displays all windows on a 3D stack. The selected window is on top of the stack and navigating through the windows will move the stack so that the new selected window is on the top. + + +If the effect provides additional settings the configure button will be activated. By clicking this button a configuration dialog will be shown. + + +When compositing is not active or gets suspended the normal window list will be shown. There is no loss in functionality if an effect is selected and compositing is not active. + + + +Shortcuts + +The shortcut editor provides the configuration interface for the keyboard shortcuts for navigating through windows. Each of the two possible sets of effects has two shortcuts: one for the forward direction and one for the reverse direction. Please note that the window list can only be shown if you select a shortcut with a keyboard modifier such as the &Alt; or &Ctrl; key. This modifier has to be hold while switching. Selecting a shortcut without a modifier key can break the behaviour of navigating through windows. + + + + +Content +It is possible to influence the sort order, you can either use a sort order based on the last usage of the windows or the stacking order of the windows. + +If you check Include "Show Desktop" icon an entry for the desktop will be added to list with application windows. Then you can select this entry in the list to minimize all windows. +If Only one window per application is selected then only the last used instance of an application is contained in the list. Switching to other instances is possible only via the panel then. + + + +Filter windows + +You can configure which windows should be shown in the list initiated by pressing &Alt; (predefined for the settings on the Main tab) or the user defined shortcut for the alternative settings. + +So it is possible to only show windows on the current virtual desktop or windows from all other desktops. Additionally you can apply similar rules for windows in activities or on screens for a multimonitor setup. + +The last option allows you to filter on the minimization status of windows. + + + + +
+ + + diff --git a/doc/windowbehaviour/CMakeLists.txt b/doc/windowbehaviour/CMakeLists.txt new file mode 100644 index 0000000..e93dcf9 --- /dev/null +++ b/doc/windowbehaviour/CMakeLists.txt @@ -0,0 +1,2 @@ +########### install files ############### +kdoctools_create_handbook(index.docbook INSTALL_DESTINATION ${HTML_INSTALL_DIR}/en SUBDIR kcontrol/windowbehaviour) diff --git a/doc/windowbehaviour/index.docbook b/doc/windowbehaviour/index.docbook new file mode 100644 index 0000000..75f0305 --- /dev/null +++ b/doc/windowbehaviour/index.docbook @@ -0,0 +1,672 @@ + + + +]> + + + +
+ +Window Behavior + +&Mike.McBride; &Mike.McBride.mail; +&Jost.Schenck; &Jost.Schenck.mail; + + + +2015-07-14 +Plasma 5.3 + + +KDE +KControl +system settings +actions +window placement +window size + + + +Window Behavior + + In the upper part of this control module you can see several +tabs: Focus, Titlebar Actions, +Window Actions, Moving 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. Moving 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 &kde;'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 desktop 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. + + +Focus Policy + + +There are six methods &kde; 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 on the desktop) 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. + + + + + +Focus stealing prevention level + +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 window + +Once you have determined the focus policy, there are the window +raising options. + + +With a click to focus policy by default Click raises active window +is enabled and raise on hover is not available. + + +With a hover to focus policy you can alternatively use auto raise. +By placing a mark in front of Raise on hover, delayed by, &kde; can +bring a window to the front if the mouse is over that window for a +specified period of time. You can determine the delay for this option by using the spin box control. + + + + +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. + + + + +If you do not use auto raise, make sure the +Click raises active window option has a mark in front of it. You +will not be happy with both auto raise and +Click raise active window disabled, the net effect is that +windows are not raised at all. + + + + + + + +Titlebar Actions + + +In this panel you can configure what happens to windows when a mousebutton is +clicked on their titlebars. + + + +Titlebar double-click + + +In this drop down box you can select either +Shade, several variations of +Maximize or Lower, +Close and On All Desktops. + + + +Selecting Maximize causes &kde; to maximize the +window whenever you doubleclick on the titlebar. You can further +choose to maximize windows only horizontally or only +vertically. + +Shade, on the other hand, causes the window to be +reduced to simply the titlebar. Double clicking on the titlebar again, +restores the window to its normal size. + + +Similar options are available for Wheel event. + + + + + +You can have windows automatically unshade when you simply place the +mouse over their shaded titlebar. Just check the Enable +hover check box in the Advanced tab of +this module. This is a great way to reclaim desktop space when you are +cutting and pasting between a lot of windows, for example. + + + + + + +<guilabel>Titlebar & Frame</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 display. All other windows +which overlap with this one, will be hidden below it. + + + + + +Lower + + +Will move this window to the bottom of the display. This will get the +window out of the way. + + + + + +Toggle Raise & Lower + + +This will raise windows which are not on top, and lower windows which +are already on top. + + + + + + + +Nothing + + +Just like it says. Nothing happens. + + + + + +Operations Menu + + +Will bring up a small submenu, where you can choose window related +commands (&ie; Maximize, Minimize, Close, &etc;). + + + + + + + + +<guilabel>Maximize Button</guilabel> + +This section allows you to determine the behavior of the three mouse buttons +onto the maximize button. You have the choice between vertical only, horizontal +only or both directions. + + + + + + +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 & 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 & 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 & 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. + + + + + + + + + + +<guilabel>Inner Window, Titlebar & Frame</guilabel> + + +This bottom section, allows you to configure additional actions, when +a modifier key (by default &Meta;) is pressed, and a mouse click is +made on a window. + + +Once again, you can select different actions for +Left, Middle and +Right button clicks and the Mouse +wheel. + + +Your choices are: + + + +Move + + +Allows you to drag the selected window around the desktop. + + + + + +Lower + + +Will move this window to the bottom of the display. This will get the +window out of the way. + + + + + +Nothing + + +Just like it says. Nothing happens. + + + + + +Raise + + +Will bring the window to the top of the display. All other windows +which overlap with this one, will be hidden below it. + + + + + +Resize + + +Allows you to change the size of the selected window. + + + + + +Toggle Raise & Lower + + +This will raise windows which are not on top, and lower windows which +are already on top. + + + + +Activate + + +Make this window active. + + + + + + + + + + + + + +Moving + + +<guilabel>Windows</guilabel> + +The options here determine how windows appear on screen when you +are moving them. + + + +Display window geometry when moving or resizing + +Enable this option if you want a window's geometry to be displayed +while it is being moved or resized. The window position relative to the top-left +corner of the screen is displayed together with its size. + + + + + + + +<guilabel>Snap Zones</guilabel> + +The rest of this page allows you to configure the Snap +Zones. These are like a magnetic field along the side of +the desktop and each window, which will make windows snap alongside +when moved near. + + + +Border 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 desktop. + + + + +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. + + + + + + + + +Advanced + + +In the Advanced panel you can do more advanced fine +tuning to the window behavior. + + + +Shading + + +Enable hover + + +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. + + + + + + + + +Placement +The placement policy determines where a new window will appear +on the desktop. Minimal Overlapping will try to achieve a minimum +overlap of windows, Cascaded will cascade the +windows, and Random will use a random +position. Centered will open all new windows in +the center of the screen, and In Top-Left Corner will +open all windows with their top left corner in the top left corner of +the screen. + + + + + +Special Window + +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. + + + + + + + + +
diff --git a/doc/windowspecific/CMakeLists.txt b/doc/windowspecific/CMakeLists.txt new file mode 100644 index 0000000..ad10309 --- /dev/null +++ b/doc/windowspecific/CMakeLists.txt @@ -0,0 +1,2 @@ +########### install files ############### +kdoctools_create_handbook(index.docbook INSTALL_DESTINATION ${HTML_INSTALL_DIR}/en SUBDIR kcontrol/windowspecific) diff --git a/doc/windowspecific/Face-smile.png b/doc/windowspecific/Face-smile.png new file mode 100644 index 0000000000000000000000000000000000000000..501cc796a4c4d3350bc54e5b9db44e5a5ef5bd4e GIT binary patch literal 1233 zcmV;?1TOoDP)J2(Y|dwPHGf4hw-aoe;1?|#4MdG7iD-}n6=1Moy> zcG}6S5?$h}4|^5V9hYg_FUT!*S$^8uA7usCzw?T(Ip8WM-c)$3(VV~>ig(Ua4HPf6 z;`d?CgNsym(ib>PYh1RCg;XcG>Tz`?(6$cf+y?aQ1Nx5v z#%u!B)T5c;G-k;3RbRV=OleG}y6+cESSyzUt!ue33h4btG8-xZ9@G-3rXJ0RBe=aq zeiM~@=T9=X-F{{49Q|nd3gB)M(6yVVN&pPU0aGD2tO3Su6R4)1H0w{}S0&K24)tlC z`nF_yhe^hH;Y?*d`y9}%2JR*EBs+MbgHkf{FOu0v6M<^#(M;kPG656sRlkNyvCb-+ z@s%A+u1lF|eRS&(PpOs`t1AZROMx;j0Lu7Pz_5l41(bWw)n<6M>sNiEa}oe+l#^LAlFcq{iUSfJr5w+#MwK`i~NaxRU0d zbd0H2qGFe0fn*%Xw8o#DFjn#s(C{X3=R=?~66lT>iKMNTY1?j8Q;%lEAuef1OCIVt zN3AffiD4E)<~)oPF9vGf09xJy^x>3*c80V-ecy7kBLu3cM>FCOmo%gm9u4~(Ew;pc zKiI>f@&u~n7;g+lOR)mz*h(i&O4XA{w@sj$dScd6z8d4VdD=hTCav%&i=M5uJ>#A< zJDAV7;ZdnCP#cJbvq8{guHfcd=n#+zEfRrh(bJp^K!Yk6xV4&V0!S-7E=N66VA~a4 zsSL)f`FN1O2-YjF0xcil+UZq@4RuD&9v{@_DRBShdVYNiISDevu5rP&b3EnU&A^Qy z(vVhoke_WI-`&rwJ;&z2l)F$oC#z-|&>Dt;wg{A+2u5nei`Ww6gw#kMlx4qzf!k5Q zU!MSVfy5OW^EnUbQf67nZ_>%@PM;0K2^VoYBSqerQ2X&T3T_Y%+^2<%13IDs-RD4K z2vF@$9KjvR_l6<+S&+|T?+W#HrtdrD4&x~|k=%08OKkY@idQHVEe?Td>d}li+;lMp z3uPcZ`8@IM*1q3v!dUn+Oqb;t$Z-|vEqTw2=l8Ji1p?KgH+0?u!&enBsb5F?ff*B| z|KIm11seU_EIiPfsYSx0p=8@=!;>&yycza0u?XTts2R?d@gICDHTjq#)( vn91S9;m+aB;mhH}v4~?K$6Su5r_%oi_kJ@#1rdiF00000NkvXXu0mjfmYY*6 literal 0 HcmV?d00001 diff --git a/doc/windowspecific/akgregator-info.png b/doc/windowspecific/akgregator-info.png new file mode 100644 index 0000000000000000000000000000000000000000..1b10b82ab98fb3e65f94d8f13832936adcb0ceee GIT binary patch literal 46542 zcmce8bx@UE+wVrY1O@3v=@jV(0R^PH8|jixgKp_=0cntu?(PNw>F(GxXYJ>EznF8* zJ9GXz<2a6+d*89{wXW+|i(mygDNHmXGzbKO`TpHoB?#mRE(8J>jDiF{8Mpko0scWS z7yTd#fmB4I-x(r;|5F&hQ~Cgbc+fx~e%~OF8}O0eHU#3#27&AvKp=dH5D30~>Tg8> z@B<_x8L79BN7$dtru-Q2399`&O(zKCX$R~tT#sFW3HT8C)B6t+$bV7sF_;jTQFecT zGeX|K6;*MY-(NKMQt2XUU3rW@U&48}9X0A{r}S0?cPI0lhEzgy!zd?&f{qm7?b|{l z(VZsfw`94%b3HE8DoWO~Mo zU9Lpc4<26LXzUD;;OAuWs`1A`D3^!ABFjQeq24;z?!O*BOfpzo+Z1T!p)I{lR- z-@dsZcJqku?CeDFi=CV_nz9kKU1|=m%7L$`skydw+By{Mk7bEFy6NnEdr2zr_U+r3 z@tb&WJ}O|5A4zvHsB38OEVv#R_O|-?+cvm9idq+!e4NdE;3&(;ci1}AjUW{kouCQF zJc`RMhOcut!%Kf9LvV}T>C`_y{ya|FUo17@WPjkL$;tD|bMAtHey%(& zBFXg)TMXsoo6`8}_i}QQm{I9T7W=0tuoDW?339{?4i4^9VkNoJgTsRI^QJ7TJn86O zW-1h_jJy;lW@H@acs-VD5$3%whCZ?biL88<-nyAg;^dW0St{;b+=zM7y-pnvh=IS^8I>64KpT+<_f5x= z_xg6=IA=u7gHV-0Kf-}J(7?h_G1n$vmSp5hUJ(jfliIpKWo6|dQ~}rVvv1J||H%1Y zpU2HAql3rky@RTWYtJnX<^=G#k6aua%38G_B_|{=9Go1GD5w~cjcG^9A4e6zV~6Q> z`o=Y+$V7h)MrLAWBH(sJnP^UzcY8bL!SIPsckD^XOyi(~tAhs(vMVjqm*tQ#@e^?V zox2+=0lHd*7h-o+Kkn_Q_PA_bvGGjKO1PWRgp+p>E~At1comP@q;T4b4EzXBVu$h@ z<7?x#xBp4S>Ir{I$^JQm+h#*g0|D|%;LpcfX^!3LpKX5-WyW&z2pb9>(fq~QGF?3g zzXbbb8oYj8Jz1c|J95CP(;Sjih$lFQ9(xy;jZ=m99W!aU`C_;kS|vYRYlkO`ba5DE&{tP+c1cYf z>TxmMUW^?4X?8l&=Z!~JV@VCAVNQbzUt^STG4}kXyuTf7bP#XWJ#8B7dCvQ$Ks)99 z{1YC_BV|uDjSwZ=W2=EsI$Hpevz62KEUGbn^}F54f~T$5p$X}hk3WUt3RJQhSDZa0 zY0mIi4EKYwzzs9@)R=qUYt&i4A`c_B(NHTCbN_jxvRdhD5Piz1ge9T3C!c;{y7Myw z?N6ESk!|Djk5`t}zemqcvz6%k`uoXlcgG9GBMeq8#&M`v?OfdA&^G#CI~}^dd`Yu% zxcNn^#Ao-czhfv)?m&OzxFY7!ZEk32$j$ehZQAZSZ7qMq$kt4*>(n$jG?Mk#oqJrB zZAo%0vvSB8KADf7Ad${p%5$8U0^J;<{E=(Ic+JEVi?{i&A#&jTI?Z@vTWd zh5Or($LQ2Z8~W2{QHxh^rdZ9tf;ynmey*QHwCk&Z5g4C(57ynO!mhj`IEntUceUZ0 zs@HrRdt$U!W=UG#laoX8@-{j$#T4CqqP&ZCU90jdJPwegJ>v#B60f`$p8DKc_28&i zVn;eUJJ3+jB?jGACENXJ#Psv`M?`N;YU+1NbbpBoq)(&TYPn%MFRZ75>2bGD4E zuIAJ#i6?Dq)}$~y=K7-T%)I1uyaXm%9`NFn-p>q|O%(X(PBOeOzh|j595^yD zGaGMwFK($?CN2Fl;M7#c*utW(`K#1uZr)g>rt#p=U>!bL+o`IRlk?R4!n~2CW&eq? z`8ydI+7B_^NlwiH7uOqmyZf<4sh*eQKYFy&6be;UE($x7&U~zAPx+Sz@4ie4mu!Xj zQgk3)zM^E09@k5sU05*8R;%)c3Oyy`edhCivVpDvbvo_epDw@LX1p?$NywIuWO0i>rr>?$ z?;RLX>A5W0VXofeV)Jp~C*tO&St_(4wn-I{#Wd!55@u;B_BvC0_g zg8^+@lrNH{XT7;Ri~!}a!olIL@niRPAPnxR%}KeeesyCn>Dz}Ckr*dJ`=w=Tl^je2_u+qY4ti&F7&$A z8Q7gFjiWArI3f~6o|Y;!_x3nG){Kcg2@!~QcZ>TmM63%IOKv`=M^k!O7)Gr0E9t!F z{$h$BeIWBp%th0s=I7<2iHYlEN!K?2%#vFi)lzJCUWXf|qo;=>CH$_u?wiF~p@OP0 z{e2u;7E8aLadTNCW$G;^f0KIaczdWTl=kNHh}q!ga1)!gb#Vn72kmRVnyHNQ&G%r4HE=^5MSptqP+MxTFQY^T4OTQ~dYv(t&%Mqo z|MkmYYF^(lH>c&1eA{BKm>rxUc{@=k!%+}T&P<>27B`qhE@{f7@5$0sr1{iH=4%(Gc7Jvj{UpZKK+AAq$#0A_4J zbK_;l8i7md5eQRdlg#OOmqrh;or6XLlq~cPG8_dpUYH6b%BYBqhBtlGl(A zT6|7;OrVOc6pq)sO=aXU`kJhd!ZGPy8<&ROf_kviPhP*S|I6;&-Th4(vO8|9ohS+E z{;AFbMZuMA9~uOlHr<@S;n+|gM(DB8A8;xt2@Wq zK6iGSP-SOV!Cy-?j|yE`E#*F2aM`Eief0u7?d?<2JUy^hFIJ)zuaE;WXF4v3Yr)so zM=Nl)KY3JEenRlq>scJ)RG)L^6Xa;oYz^10y|eQEGONJM%g6i7@4@ zfg8Tod&$6u*F<*s2a}qf7nNHb|J>uFM%hzvK^k7(xbraCn!6L|Jpx_%@VWbX;aIN2 zW+V9!F}GM*;JBV%Vsi3x0+M7w*1dL zijK*z_vXe(lN$kTwK7n)cZ=ZcC%(AeRB(9s)_T&}_QhY%mDj<+eJYowAJ5M;9vmNe zXBW=;kk@8?w+ZwIlLxAaYeVCfcw0i5u7z$_^%}Z6pmqmDoCLg|ATrI5(M~@28GQG5 z<9)2l?GNd3*_#)W;$)Ev&y%HX%$14ilCsszh$|Sr+Y63Rk)OWz6Dw3X zUC3CN`~AU#rnBk=Hm)9VvC^Y^6}7bkhac)%geWN~Z|^;QAHDXNSUm)hBT39B%Rk@M*zlJmbFYj> zuho}cMO>V?I2Ece!AZ(Vi zka(|_fHNO?u9qhTma1N8vRh+bE!ve|WYp1)5QD=*dpZ`GXHJowXuu+SiYu`Gj2K^k z-KpWDIGMydCQ9)U=||lD`JX4{{XQzSN`-Ipln5X6vlKFC_Co)7E7huD!Y-A)Q3N62 z@Iv9XU1m9b5ECd2Dxl-Cx&OM8;S(lSD3U)>0q|?`o1c+QP0T(CEHSaE7$Ray0Egc9 z|5&a)afvJqBY5q%f$>y8K;C$5RTGp@ona@(iTm`1p5Z;Ih6VQA#K-!wDN~25 ztM|LprNN*E>!6}Au`M_DU;n*NQWpv+Idj7jo~31Hf9C0_ZM)c)?gXIcXX(N>#Uo|I zj$3hQW(Mz22nh)%OLV{9I9r{JnNN@rdi6c9vQVab$Z2V~w$O*GAp5eYv^E!_S*NV;}vSX>;6V-Siful)74L`kv2i1q0*e#y! z^XV9w#G!s8<|9pHr5P)~_i&_KSyM=TJ)yM!7jFJ$GYx%(w2ce*v%ky8k!0rY9&77~ zu-}Rqjo)8*h^L!-v;N)Repl6vsYr)g$HzZWp-cRfbilTVW6R58DVGwk5V!*Ue7skJ ze=vJ+@yO)pS>>4qhsO3URG9Q5UgtHE72XCOI4vDffg<-I(Jhv(_*e&n`JufFwf`%J6F6ARyw?4=I}uZ`8@@H?C%w z|2^J*JA|H4wmG{sndBtVkB%WuhKP)g#l`XtdKzD+xq0`D-G6oE?I4N8i|?t@c}BCX zt{RPZ$ivsVY)ryio^U`=0I*&x$UM(6F9gFYAAed+E%%3I9T9ftm%L@-;fXgNU|3wK zL6Vb2uNlolfdTh?|CA>EmB6T(MQ~V%*;HhUA6cnxOJDGWc@qD4ga^PHNO2P6?`d>zTo#AKT(RaGxtc`wQ>_A+31dS)i~N~XwkYY85J z;U|bQn(2r6?(4Jk9xm=2x0EXGlTwaKXjNOR+4>n|7~2yuow+Z86w%(+(IbjwKBAxb zt%0AsMHmloTNo=J!^y|k(Qn)zDXiK`y1q7f{yZiqB6y{9rwS|bEV>aC;-rhACpQ;| zZTx}_*$Q;vX;?tjrPY0O6#RVBl1VpHy2`WVnROL>_mxE-^CcS(!coWz2pgv&hv7b* zrCQN12F<-1b(gr`%zWyql%p z7fff~BK#5aPEVZoveH~G4xKXH4PuA@ls<@xcC&=L{y3cr zD?>$(q#M6Xk5H3Hi%s%ODye=!lTH?(Ek=%EBOqi>HS#M2o72pq`cdHm?ZgDOvKEu4 zu8)27dpjG%Vp%pMBqfnno<)NW`Mc$s&-KVZIx*2?KRY<#J6V$+RH?xku0{b3Ntga?1>$qtQ35ZRanHL z>7`Ee+b`J4I1G5cs##uLv9h<55uH?My3@~jy&O(~L9&0~)?m(^N?_XH(f<_q zE(_)6=bJ($>0;?((PI1e$x!l?KFj5g_}Iru$9dms2(@*>TlwA~0(D00&J!06ajNKQ zv|Ln>LH(y+P7XYzqQc<{+24NmT$4o?)9oiKBu~eh?2bpHjlWEn7S0c*FX_j7C-kba z=+$f8b)IH2`8)@5(Kolb_ad-!1{uZp!TyikMFZy#j;0LxfEhLK`?+G^pPxSdBvr12 z4-wW*A#R|99d{L({7XDQ;2+@%PYAw$?>qUtfJ^| z@K8EO+B;LK*HLUudg(H ztPogRTeFxs`A~E8=c0`KD2rMO(2*h6d-8>c?8nTk#!C-#aMgjqvQ2_C%3X^ku!%)>GS3TOM9Mz~3$R#UX!>$8t!79T<8lyD^CkY-}zf zH|mXN$QW*~$`6i6-AmOO9+PS-^~%go#y4`;aTHRa7i+hwR*6?_o_8mElFeyQ&JwNa zw^<5z#?B`Njq|IT1q#h1pycy2A)6d#%c9 zObs5J5s(n8Oy;0}>fCX(iHZ3@eOUb62n1XMF#0<6jxSi{<$d&8qT`v9IaZn~&Ud(@ zTyM@-k4PKdNlUjc4M(^JVw&A|GXn)VDmuC^N)#Rg|C@6DNcm=dMsQfz+d`GG5*c$3 z=>G6$@j%V`8>wGf%v)aG7ITe+)B#v|@-3get{&WWrwhZ?icE**o%e<9Q9dLi0d1U+ z+XMHNv>UF+pgU;NcTL8$F~Z5c=SfwlPf_!X{7-pn>K&&t5JTg!iHW6>UYlP1y|0Kp zJ$*vK#TC&dli;*6{t6fE)dwPOlg@HV4Gj%!0^Gq{a-Ysi;aZ~c`Cr%ScPx5F^Y|I3 zD^fAR5fO%ib*OZz(s+Xzp`le(h6|pChOtbFUjR9PQ>c84DjiQ}s6n0))#{3l=d{!R zVo-Kydn$+6uKC0xy@!zGdHWX$nR+KEODPLW7~w@J0%{ZSyHtu?w53QxM;hRg+uPk=#ypY_g7Z*8}as99-AbZxfaF0}B-ovHB@WT5BGZ&m;f6+DS{QOO<^ozBgh-|GX2H+w1 zbGSaZA_RW>yZsmTn)?(3?&gOkCu3`WL(MJ+EmR8k;YG@7Ze; z@|4&&&I-NQEM5=bv1aPby-wvn8rag88yQwE>|9(-ixl$8c%3JM<8eTA6hS1lr>-^r z(-i#){MUDKaw{FEYi9!K&Mnq|+q-0#GqnHJSOqv)@__{x#I)0J=&9M`t5vL($OIm!R~vg z4vJXCLD_*)yrn3xSeg zY-7^}#t$r3%j)E_W}i2ZEZ^-vGlq`o}T=Nf0`1uYI@|; zv)`g(Y1Mil`ip%I3oPXA9H~&PMSu}4a?Kh6Zj7p1NxrT;H*I060QEJTCl$+ z4$3&BK(UFakO&jBXb23jB#r06(dn0x49m#@jSY9l&E;TDX1UhW*v>8zi2LSH+uVF} zdoXG9`8I8Q?Y6>a_@9ifRJ)Mye4?9?I3AkeH!?QWfT5 z8Jl%4h3tn96~D?2w*QLm&oyL(g@qZ_riE5<48${0aIr+VB4HTLhm~RUJR^Jo8w4b* z?MUgo_KuFU%?~N|=d&k&4m?(I!9xFKIrpTOOsulDwg=iSK}DTYXMgfMM-ro>d!9%} zMn-&5u(VtNi&Ut6!P~zxOx8 zBRgS43wT)n`0-;_LmP*fI1uQ*YHINs0?#$->wkl$92^`xRCVW(ZY`D<*uWnn)8b={A+;27%}{S|rtQIe{diDV$m;X~KWIjy<+3^Nso5!`0k$YE zW~!)U@QRQ&bnou&TG?DR(%4#GPdxI%GB>A{jyr8XYL}2p6B@8On13>w>maUHtOK<6 za5i?%KF+&yIw|Bp5}VYxI7AUyYbMZg0YMiUJc>T!V_^8*>Qba%#v-F${I+PyasLW+ ziI^M7$;nABRlpz2jJ72*`H}DPlM!OL2?$`M(@maa#Khn{i>7sC3JnVD?CQEM93L2n zD|+za@P!M-AI$$$xbKd_YrDnx(%YPt7R(&jT#ph&rlNX{HY3GiBEh^Cq1#}wRE%mY z8NtTH@3M-p@Gu`uo#{4O^&~1iUDQbFyCIbo0P2N>g#n@Jz`g~RGk8oT&cga9d3AMH z`9Ec>tQZ5ZNC#_LT(Lb4_+AMJC@{noYt}#EsQN-X`J0DdIm?R|&*%0-sk5Qs%3M$8 z*!s3Ze-X1zY8v|B1ejmji3I&|1I^Sx8!2q%R|3d_lRByGMrC@5%s zHaG@IBe#Xa+)`4=-<6esh0*cn4~gHm&bJ*N=Ct`w7KVTcO-n2FluSs>OYF(iPqnu8 z_AjhIhFa(6wLECQDfUJMUdda`*FAd{9lbwag`XzZ)8uJrJ%7h|u+Vffcp4NM+0*`q zB;mFRqv3M@m5@+d`O?@@`X0A!R9sx#R#$=et;^l{8IqBa@t9?`^=uuXvNDdPO4;Sx z4PZ;%HF?vPxqghy$@#icrn^w{r^z+4HPfJ@#@G5bw64w#EVwK8dnD! zK_wbcE2a_@{5m}`ufm}zoSA_fMR6+K-}$H&jae{XJX!us;_=azI6 zIV?JjZ|&cq5QQU!OLhz|dXDaWiltX30xQgNs<`dDDI-R0T7cABY(m1gJq==R+uo*f zHkbW*0DL6MW^hQJOKWKnk&zv$T%MR+hp2vhLo`0Y0lamybQ?vD^q~~ajRee;chYi_ zMN_8(0pW->qW8a@XRD4NrUS8E-Q7qK92^`Nkcc2{KuZ-?jCxK+7R#s|&uKN2HD&o; zR(2gKTthgGKW!UDx(@~^=|?y*Ko>B4IWNl`7)g@n)0EGcfZat2=YKU*tkV=g-4js- zz$-u^rz#e+^YgDgH{U$mcp}@m9Q5V(DQ{0!z?1SfUJmxp6^MFUP8S1gf;@4X2KT1{ zUlJA$*weRyGBCu2un2lo8H|nV+(Nw@xT~#ZOs;y_KOHUN18E~s&^UQTOhLtbI9)1# zVk5crqnVjBX!w<&UqR?zNPApt4||f!u9h4&)dQfO;mbF7Jal~~PYcvQ*+`)yHiO=V z*|~+2rP+N-I7UWBnlN%0&_6yba2UMWwVJOUa<6R*S`kk?8>&6uUQ7U>j(6H0WGpt$ zH9zf)1(JE+R9Hdv%!U#}nshI*2lHb<`hfcD!`HyELMLt-`D?14(GhsA>FL31OPbqD z@?7naTK6g|c|2xKIWsdFNXCcjZ*_G%9#GPE(50ht*X7S%FqjvJLFAW`!g!DL6aZXN zZ~#8f8-*%KXCP91{6n{e(DUO1(X))G<4HIGwEv#bX8>#}n?UAG0Db7x*|HZsKvHP2 z)dixBXw{DoWa$mzMD}}^q?R*3uT?glXY=4H=gUn+l5#g+(f^Z}$a`b48aOTvqmeWhai? z%99WfK*3|ulRj#OcRRQkhP@>4-K?3taNS>2s3<7yJv|ErrlUKbK&O68`jHeB)jRqu z2erwir1gL*Cp#!SG)E~fM6%>-!S=`DH0ZthsfS*~_4o!OSm#HD&~Pb9V+5gsR;@^0|g1Nb;{YKtOS~`|}Z2MIPXhH5+UNQG-zv6B9>^Jh^{6 zdxxeqTpojpa94M8EA};kS=-lmZLN9W9Ix2a?#lxtsw4lVt3G>-@=Q5zuE^k+uAs{9)3ic?lP9d>8w6-~%!>FI;PXGwTNDJLC~!Y3|AWaqnMY31ei&AN4ojOzCe zE{?0d8M4`c!_FOj#!Q*Lxy^utkqUkX-L=B{1#l&pZMz@2b6#XG)h1_I~KoX;!{$Ys>0> zAOQ2e4}D79;dIhM&uZpJZ__i?qO0SBKV@^j-34{|TH0Jg@!;)G#!WBDc{+FF2Cmj- z!et*%IrJ#~|Z8p`f+|2dYzle)n-3{zukiHHQ_E~(Z zqeFuHl&rgFAtkyaFk7wY+Oqo%0E+&Ab!2$+lHyb+hYOc&G}vvHyDYNmTwp;cUcSKv zxT2<|hQs_4L+Ez7hc`kUG}5*C3_?CP3W$Y;h1c;ItxAbf5;*@mSPL?lL+fCZ2eVA| z=M{{~+coR9!X}w|jcoJv;FaZ50KKd=pwwTH&&}83_2G`OP)u$H=fcR?Q(?DgX~}T= zlq9$R9vxa-w)LD8XJACQ)M$50Ig!s8Z)qtGRtb(<9{?n-I_yzpy0Ik#FjzM6oZUvN zshzyV*67&JM>b6;^!yw{TraYACJ+_2{IhEQL*@{q=a3lbb z!PRrris&`lzb^Yc#sC#+|6u?4*uEu0O|?6mP}RT(WNgT%OO5xppSihv7#SLhURRo) zob)wAu8|QGfmZQjC^bB^D3lV&%~?@$r2L~daq+N|D$rf6^i-ekOnpUr(eJc0Nm%#S9*25xmZg*kQ!m>*uso=>d0*jO=Oq zrw&yF#zl|8a1e9by|x{s8yu89IkxXiyok4we$pjjL)HKo`s2>i^vVL48Oho{$XCh0)J z>Kq=%Q&v`fxH@A6xz7(<+EZHdU3Cv*i}(Tf?`&Hvkt42u=ZV$6yA33JABF9Q@XItF|Ft zBIP0g2zGNMZHSeai^FSD@3LH4bC$s4&qwF(JzSqNA( zZzUxa>npK9dk5WP2P$VLu2pNd?C!>niIEK!5nbF|#U>#CyOTfTjI9Of>HF-TonT?) zPBAi{l0HH&8bt1;BA`Hx{AE3Mw{o)lJ3K9o89ddWM&}fe^qs!1gtKB~?C|Go@dBJv z20(lGz-BOc(Mz;y{)Wquj_rK9y3sWl_45g^0UZ>?4vnEe-6FZF%2+gj#BM?16<;q86GLXPRzvR?xnC3rO%anUjbJHqcUsqL-4YwouTX z8n#BQ#L}yVj8O+pD(51qaDK2Y!V#Z9E~TGoNk?O^s=;-Lr4JpKsq@<+%r3S2{+hrAac&8=P z@86R{s>7>SoON^rKy%DA3*Utf;3ck6#os0x+k3wJ8xj%BzK(eejYxA-_egZ@d;UtJ z*^Lr4_&r#Bo6&r|Y4T!5RDkGo2RI8?8a(LyZoHrYdJS+Z0hn@V&_~|cA>3c88ex;C zYWi|=LR zrGVnGb833-AqafGSXS+LkZ^^t=r#}5qT}Oh2uVpv0Y^DV{#|dnMy&xmpnGT=12Iui z?=;+ahElk)I;_eZkMt5(yCaSf1j~im|==k^MBE`hi z)Zy}vJ8*oyM7jd$9ua6Tuow~`#y=%HQtg^w@zT%P!-WS2w}EP2qK57#pC_Fvd?W=z z=0xEE*UQYgHhXHoK8;CDZC`AFI>sF+g0>!DR|AoYh(K7*z^VX2b+3CCf=etEj9iz4 z9*WrY#ZwTOF@mWo4hrSwPpf*W@t8<^VP*YO2l|SZO zI03*5mOvJ$5donNva(F^@$mtC$E7O4FvK~r15$Z^Z&igyTb38DYe3g7I=cI(Ykz+w zvtCyq%(Dqp+3YFHzTV!y{i=^TThl=6fJw7Ie$W8AUNh zK!lPBWMKq?5ji~(Tk|!ZGb|tiJf0^-9?;Am^1IrKFaYKGbDV3VU5usOr4+p*3F9_mv|tD{{Cxwo>{F+(ZCTI|Mi~w<|#pYs{8VIk+~}j zDr%L`u+gEYL!jB@kxFdV2hL6MQ%jbv{!$!{-7GiUhW z73g9oe`=mZhgy|qk)-iCzog^&R`dXOy*bgWs}18-er+A#R6dR+rwJ9jg6 zM`VcPNMb@T&UpBo_x;pT-Y1YX&H4VtwriT}V-tOzX?*J%?!`OzA2G53)xxkzXDNy+-w$~)PqXeAr=u*L(&?(d5 zL%8`{@*Yom$isD?!gP~6sSn9lE1}K{1u(NcOk8u60|9&tEPgSM|HF@`6LEWs08HaM zbvt*MY5x$aceNm?@7crwnh&t^z!Cs<1|M|*X4}xv)T{&!I7W|vRRXJPTw=MxbxWT~ z_lk7;VlbT%I)o0cJ~A%zOaTJEsH3a9J=&bMBK3>nmbR-$KJo3AZPPZlJ)L&VL;TT^ zLnrOaWAJ33j_#j=IW(BD@W55PP;Z5GkBrGY&YJemwv=RipWZdau`%|$3+U6`$zdN1 zz;>x%-qRI3wZ(+Zjh;O^5E{vXj$OleG~mVAvX+U!j7HL%DJX zTL0pC1U?l?Ac^3X)QAh$H47DjRQej#YbOk2kP4vqti4p=Xddoe`^1XJvVFG?N zs2_+FFr+}6PqOg)_iyYMFVYypk%7cPVxtis9WA;z|Mp6_zbQtZm@ zPn00ZT()oDn!E)1vPAxb!;v0A3fE)u7OOJ2xz!|FDng|I#uO4-s*h*YsSE?|Ioh}lgGHpZZRei*I47YT+7z2tbas2Hq^53@WC6L51&vh8I1e#%gl9R%zMm?Z$RA4~4%Pc;%#qSK?Fig7)V=i5s!fVfuv%oxaRoU!gB&G%k6T`EXV-4n2clT+6cr|o9nMhe|;gBTCkO4!;| zZqJ}v3~3!osZ9iO)6)L@SJ4hhAnw^+3K4U2SY7If6NxXAb|p-h5$Q#GAHnvq`4BAO2Oxpm1IyPldu9*t5SX~fo+GL7 zI~HF$?>lgPz#Q{B9YY2IN;lU@4ZtKjkdMN}ZL)tr`QZtKK4Ak>_wJ#G%jedc_-u2q zeXcJUHAbinpfObA1@sFj%T|o z>*(sBpkQ?V8N?X*{tCksU^TDnIb6^dwx{<<0d^K!m#|!d9un@}0 zDN``e&i*9>0azaR!&J;w9OweFtU{Hzt{hAfrYXTMCR*< z(G;M(oA3GWWzhwRWx>D|SoG+<5Aoi>Q|9~Sd*!rouu$Cv+YU;$_}d;7D%+*s5+*9R zfLam=WC}CHp#hwk%x%*?X50e@tI$9VVEBdc00(#F=4otfES`gOG1p|ie82G-#7D$o zlg@g!vlH60w!i-kWE#VXCVPb0Q2c;yk~Q{`;QEk_;nof8d9u*pvg~97qz5E_UotX= zrY|-qfrf#HhX(=r05GYq4G;LbBgj4{F+kWZ*?^TiKR*g_KA=QUF*Aps7)!1EHNX-9VMjn6!0Pt7aThd} zifrvzBn8?>e>}bMEVNd*MzLD@h2GID83-VpnSB8J5gnk1x`Ts5qh@AME}Pk3{-t(J z?*Z?G05rH!!p(08h|m6n9O&U_tg0e#qYu^U_4X%JpvRK%`#b?}m|nj$)v&3$I(%gr z!{V0OJ~n8UqUHuo#H7 z!g4u_YK~~pl#zw01zjXibpbsGlmMhwq``DDA5PsU8MY(XAi)s}N*I&jT!#aChE$AX z_^j_=3Vc?*AAk^UYLLGmwYhQyV;mg(9&~F%L)!?YLX)BX7a*%53>d$(o;H^t0csl4E}) zkOO(H?yjymHqnd!&>2Pwo^>1^5x;9tUs%_40nRfed(|*}35P9-9_-?)gDbnZxnHh1oW6AsokZ=nEf$`BFABsr! z90{JEc%-Z{-NlD(8DIrkJXy7 z3>%0hSZ$x&N=U7I_2f-6_Vq#m_jY1sN*%SD!HD7D;Hds(ae)?fZ_{#WT>A+L0f{Jp ziQ@K&Uj1o@P3a9X!8AV~A5fCTfh!LZ0Dy%4Cw)&$dq(Z>$nz|2e%1Q&qVmIwAGfRx4e!l_F0BOec!J4#9ld7;D)!mc9SHxeK)Q_xP60szX-CJ(l_z^EE5n{^l4m+A-Hw?9IX3cs;=eAiX^yw16yDrM!@mEsaWeKZXkJN07MML15Z}Io$v-=!2k`=^A$lq zHO5y2aPMj)@y$s`LRFyAKv|2Sjg16oLQW-&`@H{ToO=)Ql4ITPI^Q=xY-<0}(Y-$R z&A`1Gghi0WaED}GD7>-w6SQ1w-RpXPlQB)u5ClM7r|-B7?>g*=Aaj|22Siz&ML>>9 zB67+5y}bO0llOm6C(sJ^^cg2=T58XhkYmDhIbo;6anCwBovpGHt}+`La#|!Vb2R~8 z0I)<$SMcfKzkXp?EONSIn-PA+$r;xSA2<#u#S*!2cewxs1xiK_S;@T}cy};>rM^(Z zWyuSpb*_!0a1smCCI487T#q-zpx-5O-pfEh;2vn%>RMdf)-CP>YD8Sck7WqB!ez@r zIbQ`=o;?G*y^#>dpa(*dCA9$eeXxI5@J~QHWZ$3c6d)4-CP5}p+MlY&%3mzn_?___ zw1~&+oB(*CnIP0k!sqx#-Qxrm)r;4LX3?{yga01+Y^$aPbI;{)Ui?v1JoG$wC@AP1 zaDKZo;qFn5e1>g{=oPc@DEl6cEf2%TubjfC_^~Knrw=SJWIaFg>H( zS6@Z?6}iuwByRquVE{0B9nT3knv*+vVuT=nLD=PF;bom=>uJ~0)WuY?(WmW?FyAbog{`hZ(!%GcZa8_l>DDme!}Xi!u`KT105)C)dhMMTdwds0d7T4U;#04*v1E-_Jd!=v80dxHjW`K ziOY!#g@jjm@p3z}~s*%{@&Za)5k6p2P$V%=V}YZfX()rU}f4`j@@4XZJ6A=Px~~f?qgm1fAM@a z4$t3qD}hhRDM7>)7@)72Ztn&NLFz*RsBR!iOvqX_7>YG`!m$TI zMGXd-)(}<3_|1M=J{lx-b@h%t8(*9J(|dlY?3^56baEkJ0KB>0uj(rqPJjf`V8acv zN=nKVH5FWq5uu@5^B2UE#foXOvpm_^IfdHLzAp@*Lb8$(p1OVe`W2p%GE3wzgWc8H zIZJJbh)J{N3t)8=o)HrNv&GB$cKswLc$3M>GCUm6^8A$m3OW|pZ5LgBY!4v1Sm2D_ z#!?d-c(VwVo^%YtE{qnywyg7H4FePhVhbSLr%xC#(Hn$Jf#aspVu<5|&q{RZ`sowc z6C3~pZm@IX+NI41RInHQ3CI*QG)OY&=wL!Yz;E7gcxWVl-`5B7XPdsYQ8gvnD=UW) z0O!4uiEjnLR@lREqtcO$)ylI*r1AWti8efnlwM@WJp_U6^*2~#lKI1niJ)Cm<$&!l zo`x$|H+po(ELc{IR$6cD;*FC)G-_q#8Autg?5=(RGXUW0#<@aj(AfcD`wIS9T}$K0 zJU(aY+kVz@eI8oaIF^U3?|qMH0VN;h{3s#}`{g$vFjQV%-rBbG87x-6{d%$wDQ~im z{6N?Q%G~7PlJ9lxSTHr0rfQ?r(hi9I*1^M9kRtkJGZAugvCC%PYkz}D6aoynm3l%L zxW42%#5`M>&U%cBS#tosU(<{;wkaa(6Q zH$+&^-DsCOg9i4?{lHl=j0_qX-v&0Gk5MmofkchQ6b0;3*kSYki@3KA%c^brM8U#F zMMVU`1{FkFT18Y)Kthogk?!tL1XKi6q)ViwL`tMoO1h-GTe@q0>v`w<_MVw{_8iCT z*~k9t@p!nox$m{sbzSHAt5Zl4#ec?RbBONp!ij7K+s&cWO%ae*#EGbJg!mlH+ad(q zi0*?xW#VPm#c>W?M``b`SzJf2?X7ujOP4@H0ZHh6NnLlfG=#yRE?R{&>_PPvF$E$)k*ebv~E;0|INYt#;b=nUml&EdU)e(>FS~ zXRSdBAG=rh=^xtNxymJw`OYkQD!Ho1h==_wC#q^cnl>JuW@1a`Oi(D0(0zzz;m0#) zH&TM|cX9Wd?)+A(x2nGfN`H~kL$oBQ>JJ`1{0rNEs(Jwr77NM)f5U+Nblk;jSt=~={5RD&+qkfkN`oMve264a(qkobJEpb`dYgC8_j?(V`@i%u>t1F3#pi@4(&h3_to|kP6f2Oih0&>`SSBUPCuUbFlaKvbN_x<$9S2G1t(5RO4gF> zrRHw^A^;I3I40`s-TwfjY&K|{Q|x(R)FAKeeWKj~D)-(!lZ0xd8#s%EO>Hctq-n43 zCaKr3Q-3r$JbXQ(o-{J%Wu&N_0o1`ZC!6l2oo$lDY;Ow$p`~?Gvb+o$Kf4ldtE;RoB zJ$`MlZlK&Q+bAU^o`dV18wGd*No8p7i+V%xye6b(8 z3;`1G3ROs}wD0F%F3(db>WL`1Iy6>3%ArgA9#at4nJWzbkZ01fvmcgbV=MNcErpG5 zYr9J3a~wGYscAT_1qIA|O{%^=3Idb8A(Y%RJpAL!DQB&^ZwKR>5(ZFe5K6s*j7ze~ zJ!sM^{2`uX*QgF~3&}kJT#0bP0m*mWd;A)F4tvMqWs}RW&0*(epv&8&4QRcG1EA>n zvgxz&o)`ZsS6_+M=p0q!+8i5%77RWYznhQf4NqI~<)=j4`idR|zPY1n% zR%gmxal8zj*>7XM?F|!KE&q0^!K(x0z?m`8m&aUZcDis^Y=aI|eR5y4cEOMg!EdZe z=4Efe&^w^>yDBz4FUhU}H2+av?meR7;F_CjpR{83Z%mCO*Ww#k;dWbERyxanCPYeE z*^X~|E6QA)Dv#GzDMmUI_t-8@&jxx%Z zg=AYAn;WZE#9G21C7)y(#ju<=uR*_`hUXrrRv4b_*p0WKtRf-sC*t10761B3nRLcI zZMA%1m*3vo4)liEj7U_{6ZTI5g{^&5ov7$`q`W70-w6K`5f*T-;pPY8Vn9H(8+oFPr*t2SHs z+g3eOvYKa(0~8_wVUf%Me!U6@Co4hRr&ln)~>ZA#d*z= zqz;*cD~FtfZhQ8Q$g#>^k$lu4&^`QEBAK>>05Sm*@1^7L-AYD2tJBuVsI$2C`OBrB=)WX^uc$apdzm~NLpN~R&U z30KzP^Y*MR#%i}}VLKEL=BfNp9X(SY=mQJHAcM5zjMyE~Ctz|Rj?}1F2~^?Kdomt- z5EpuOgtC5VxL_9)VT4gj06pjYSflXEA&h?NxqrMlaFv4pQB9Q%vZ(p zv^n(aUb04|=3i(`mh}@oa>;0||3Z7aVw`mLQIMG1yP<%GL#`%P)tRqeyDs*ixVSh8 zp>D-Xjq%cgmE#mkh7 zx}TqPYLZNOa!THUn~9M@C65s#Q$cI%=tkB$dG~-O_P~s;b1fGZ4Y?q#&9JdT@8~7~H*2j(ojVH-UE>Eatx&Dry-~ zUD2W7K(g1efv%yZUtc=xUpN3ra;9x=`z9+2Plk37TK?n=Z5R2FMTN@ZVxAJ++hn#v zJR~pyxHS6Yt`~fT_ft=wE;>#|!%-9A9i&%{hOeO5KE=dz0#pS+TAq;0U2AZ7!X7$+ zlI~BeatK5$cGDyM^hs@G|E6Z+BRR&av(0gBnX+D^vbe17J=%b%Tk|@EaZ#E=np1z- zXS=&!-DHRWK4=MFy`|BY>S_5Xsqh93I$J$Gnz2U1lk9UmyM-6NH1xH#@z*7ZM(tLh z4x%w%{z!W!;u9Xl{jedkCp2jxmGMr+gXmBFV^ep$+(o18A}dn9eg8h#+}P66a=>Wk zG~YX?8+?9*svrK>Hp}tTIR5|zRiszCiYZ*RSUa1lDF)+YAff`;jDBak&U;A&o0*uL zWxaT@i!bcyCv`nVMbD<%+FA&CPB;~N9G~j=RD|xU8ixPng`nhFG5U)ac}m96Z*rek z5-2YX1En%sxwf5A`AS$_R1OKaw}~5L-5`v=YIc!+QhTEP=ZixqKe^sMf71B!!l$6(d%X8@V zmqmPB37nr-%;CE`Q`y8O8Q-9{s%x8cD)LifCY+_{va!>29saIwJY#NTbe3nu%u{{W zu3f?xBMxkGQq3HwP6Ur#tl~J?-8j+4uJcH+X=Q0?+bxX4{DBWU-L`zI>Y(z`Z-D< zJ9fMHCtio%vsj2Nrc4WT{ey`Lt|9@|@(40KIym^PeR8BFU6Xlw+C?R}HBH+Eg)>Y~ z6{g>|GDvkqN5w>l+$~6by6dE^R3EpBV8IGmfe>tm=;W|IPJ`ToO0lpj|D_FrXIqN{ z)6&xD1vZwJwqakw1;GwU?HUz&tFL%p7Uj`m_#5}}tKsun`^H~g=G3|GHO7@>+CR(! z@g0|8XD>)>2_=J58zL|^g(^TWZ)9v7jU533DaEmD%ZXoUa;b!5D}%FfFE#5+oF3GT zixc(k0V>t%i=FmwjIwxww_&N0hnJUO?Ak{KB%!HdP7#T}IsugW=r9s(g1zjKG7 zs;bIvAz#AC+`4(73nGmKrNupy9w(F@^I*qNuapq%qdV}@Irjw2XWUISRjGAwyY2s% z+fB_tqZn-_A%HTd;=bKR+1hOlOmm zYZ1CkMo!l=8^?D4;t@#LXQv~{zkL0Adajit+HT_<6;%KZ;F=8cOYpm>Xl2oHJy|+1 z$o!}f@bCrH7fqK#S{9lX;PIr^)g83s@IF`m@QdEEsF@8v_$u`kzpy6;1{)PK<~ZY_ zz>(Wnzlf^vc9iImk1H5 zfH>^HNA{yz^xVH>UX)5HqXE`JCcZm5|?kt`jq**5wOb6HwZ$bq7s=1J=Wlto(hgV%Fo$>rZgb0T4$+9a{EN==Ku!BZ>6MeCv(H5v~LIdnh+6KOgK> z(VTPY4ePb+IUP}wE8)iVy(UY6nJ7cxtS!FmYgHu{#vLRUds{I zF^Vl;tNEC3labx>vZG9m3HC};Ych!VQ@gbxUN*|I`tL2K0>KN|uA%aw{N4eP`7r^L zz4D2T&B8~SQz%(K-f7TUdRjJ=X1I7U-(bb1;gavvAayd_KU&Bb8ftDa9fB4k+BWm@ zEvAy5h}-zXGM?%l)SNH?j|L1yJ&d=GQlE`!1!c;+_SL~#a&(xK{`PIc?)Yc*(&60^ zzWG;zGWE{Q+4PVKuiKa#8(V0MO*$UW{@r^{qqxrq!%R6DeVKkjwV8C)n-6*k zc`-65dW6U%GsX3RFLf47Q^XMBg{$lzAnZqoTqOuZYJoH&n#RykPssaoxVCpOw?GB( zQ_~?wx3Df`Jl8>=S%m0_A4m;ATK9Q}Il)e9u0ZQ9Cd8kqX=>gD>O9gp-&15*VM|x@ zNlqFW5`n1#Px>C^m~rPhP$K>^22`cmMFC$DMCGl-9(JJ1S$v5)d*0W=Vjs&?4+XR?#?%7!7+>4Z#m3yb&MIhhB zmOyuKfgg8NQzJ5@5FF;D7o9WQ5CEF(*fx2S6sPG?NHQ}^L_021p?EX1OT9EZR8^cU zC46!S#5l?Lvr&=#h)WN6u;;{C`Ft z|FJ}@cH~&QOXT~D;yR+_DJ*IUK#0iEM~eWVddhs_;aHl1 zTXA6j&b|Hh)iEsd?hpA{(%|1%-zT8_gRn0IC> z$K_Dc0h>eDii_%bT}9G9z)gN59I^70JYpGO(Pyh1Cty-sKjrZF!8-bGyZF`g&v z_&+h$snLi=dhZeBFc8G80{HoY#bieb?w3|=g0_WFY(oxe?@2FeA|^vx)2`^d3g;ED zU{2#Hf78br*=z^R;DaUfWl-O9ZaWOc0wh;#>+bD+rBGlO7j%TsIWEy6i&;vl(_1e7 z;dHoC8z(OH)GA%ul0oeghYe#6N@gXE&1XUh%2|6sc>Hb8atBfZ0ZZ~=h1&9a7bIN= zV%K1g3*@W!eC>Ju)P+|^?otAq2mrk6(DLOKz>dTIi(BEASL<2J0tXyzC*VVVUQDB< zcRP>-R{&Sbuqmc#bsmP^6nI!~8+?>! z(r|ob(LM-oj>YsVs$BL_+EeZ>`&o`CQS0gT&vgFy>x7XnF@OCTWHNosx&&>y-R-fV z;U*Yi^gKKF?mf-T?LtofrrI+5-v;*}V05TIu0Emb%PYe{JE8%COU5gCs4AxJ8;kJ8 zf}S%a{(_5_P@gw1G+E>+eR=6q?$_TOZ8jqHhK3~=lFw9w%8LavzeYEPrzxNV3Zs)v z{xSDX)t6qo>-k}eQ2xK&J!kB=CUxO2tHc?xH0Q3JptMc^3)UI=596a`y$lFq411`n z*YR?|?$+u0x{wX|&y^l_C8xUCCRdMSe_*a>Z#%%K(_POT&9>b4ilD>KopTWhyQkXj zFNx0OIYo>(+dyjETAIc=LSkJ#xuxTKUQxWh{MM;H31lXEHY7dpMtB8cY1}kR4@sDH z?wV%Ic`HPVcF)>WLo(%tb+O}>caV4(A5hThKUl z*e=xOPquwv=y`^?6>zIgh(MW<&z6mQ6wZftYE9RvgN_5aG`hbtVVgV7p-18bA>0Ey zyKmNGoFw2Lqu^j*P>5#7tDBVd?}7vGn)5i$2`GCnib3q*1nr+t4iG%oIAR*eH4+Tu zGqi4jy#pmhGTxFIc5C10(03=~_Zj!}o&(|N3I!qzC+L(1x17uAnkWkW*36mTU7^P2a=CWjmDi zziZX#ck^kUK79tVju<>8+`dhy?0`}{OHY5WkU6eotJRdTFY2xzq$@SKx|D?M@y3nQ zc%+Tzbx2Aif^Q}_QQMr^*aZ_dXPO$2SUjSA*fXzLFqb%+ip9y)<#ohoJs*9Z)0vX1 zDlQCiY44Dcl&~m<_=q%{$q`kYxJw~VN*I~0z{v>VhTSOkwZ(7b+nUpOh5&|>^ zHaY7|OiTnO0(~PcF7VVRR$-x6t&dxBWvfbP4t5wsg;SnFF?fZ$exa%75Grzlj5Ew z8inR*j}v@dIk~3FkKte0yXooSq1Lgo?}e-K?eO1!Uz;2K4hU^JL%AJ7M%=b?aCXzv z3mW*7Zz|M|ongI*uXi^gVK=Q~^?wglG>wr?8Chy>w6_rZI4u9a;`Q66aJ&OnyyEy* z5Bpj`Peid4-1V^GA`A9zs5PHF6rpujo+W(Dp@Wal_FJ;ltHi|go_S}(#MXrIIj{f$ z`Hxxl_%X6Ysx;4vBG`h3i>o@l!`W%3c4aS*17W|%d;mE)Bm>{Z26cslMZMlvMGoO- z>zyM7koHvY7L<5SitsxaU>g;(FD@<~*sU?6>z#w>z5Y9kKe-hTyENDX594d=6=}_^ z`CdB2SMHu0qU6~GEDKtV>b*_2Chxp#imTmjNn^1U-I?EZ2@)-dzi?o z-`ma3--BEX@jX>g*qi8WTCEu7bUsVz6Z~kOodF3sgqyjF%RJmXgoN_*E$=?+o<1V| zEv1h>fM54GI!J#I);4GDeYG1gc?BosXz zlr>!<@Q{i#`70L~C+P=z$17ee79StXJnRB_G2#)jE<1VowGtBTdtoHVf-*cH6c(gRZB`n~(R>k537?v?HbmO7jsn>ljG&2MV3GEg_ z2TwyBRW6%kGVogP!A05Ph6Wd+>LuLzri{PNoZjoTX#l@i<9bdgGNF*}vvdEh?H>zt zCz{hW>7HHx33#Mxb;Y>1|3YggT^r*)NPzJZWWx{U-f=KS`nys>a{rPG`hkqBrKGj&HDbkRQj5+Y6$~Gp#M5&> zVNLW!hHLx)s78DYoV5%p8ROFY)gM3Rgxzvmm{@`q`q!DSZes7>duLg!-+H$C4FOpX zPY>MZ*QU~Pk0`tmkBaF-XXy&%}th#HlJE4Ev=m<)AgSETWF<-qY`%Z<<{n57t+ww+5Kl<<80yK2O@o8rze?)r-alkg z=(ZI6Vb!iXp*(;h4gp9(oQd)*4&$-(JR2Iwwp*=I5(u8#1u7i{o>w5pXFh1dE z&tv}CVf@>4k;KS_idBnKcs^GDtS*c+obX!h^J9+B&zyW(@|T~nET&xMa7QTuVRFp2 z3nnQ`4tH$Z&1LS_ZnHZ{s2m+6*G0TzpcubDu3MqPSnGPXf;0wqPX-OMya$H=J-1*l1i zU~>mU-g{2-kJGRAiZ@UWV(`QAD~I87gf&o82T>*|tcdhm(z_gh1dnyw`4>w~RQ6KQ z5TR66?awaxOGXLQs?gD`cI#`$UqP$&Z8tk;Xft9W!)!MHL08_-+#8Gj24xPYMetHw7LvQoiCJf1eCp#*_};yJd!I0m zXCK>9tzf$;+a+E7l~!6x>MDFowhMpzKId=d+t3sxI-Uys@Xr zTJjZkSY!A=NOCSc_=S3>S=iA4!3D*H$FY~6hm>t2KNR@waVD-seCnt)-d=k^3!vE;rbbxBQSt7RcSGmG>NV0*xD zPVw`1eBnbP0VFgeFqS@jKZGcVyc;0tCF3`9wK;dI(ao_PhPmKop(FE0NHtHf&$Y6N z-0hS`Y~yQGy(FRof{VsMvc%ux!tJ;hz^b_HXZxtov3k$)?7-zJamD#Xcyo}y^Xj!{ zF%m}7Ma5vEjLOM@tfjE9SStFSHpv1V-%XFv1|x(X;amcrhwSk?@;1jYEMuU|C+dSA zB?-zz#Vi}2m%kC~y&W*C@ceH#7l`Vvo_&$M`0ICB86HSqod=&EV}ueqOJo~=w97If zL6X=VvwQ6N3Yj|)O8E?-!!=3z)CgqvdxRjaxO&alhv^snk`!Szn-X;&1bk}RAV^f% zhl!(IKtLdN);}Q-We9SYFw;UPN=7P4QBKtFqK|Gi4pr2J3_U%nk9^<0{Q-L-X6K+3 zl!=$x4v8|#$({T6U-yo_{_HucdfELvMLDM%W7Edy^bozN-V`q-a)Fuo;zI_*M~=v7 zMdMTYyP^$@K z21Fvdh7aw@^dk8+Z(rAtM5ad0Mr+yrpLFAHvB&X-(^o3|rz;80{%hZ%z+lC}MFc`f|)>rm@HRC~GWZc8_ z-6R0`Fz&<^)!dek4LU8*@rxbjz^!Y%FAMaDNIOLWCS+!{(CsgrxTr-2ab`JC3 zFt4o$qy%Ee;JLdW-|*u0Q?kP_6`^pY`RjN^;O_$HG~L#15Jsb>;uYE3*AUQNI^~H93yz6XT^g$SisizXk}o}`1m~;nZpP<5kgYq zkrN&PaTlQN2TY298)w80VBi|5nEY`h_Q0@04xKd#2!bEQ#et2l?%e>OB|8q5a&$gM-NZPz`xLb4Q}OyWLtv=V7uZ zRD-)-y@KqsxTat}79G{uvuEE1#1YJ~>wvI+{PVJ7$89{Cn;$`p{QO2-|9FcjMEo$2 z^-`o;-WldVAc)qVj{-QfAtHt;G`3ed;L^qhu$$AeHbhP-Jv|yWwENIBlTuJ{)UvRk z90mqY#GQQq&gVCIAy+xfFn7>AG%_-}Zq>4+JUKJG$Zj`+|h)P$Ln?cq8;<0NMo z`Dk=6T@t!7{V|CMD18@E5{u*~9B9GvjhRTnPLA2`M>xwz2tm<%BEth%mGfW@KUl^7MXcYJ8?6%hV4k zB~SGA2^>f-fVHr=2x714n?o*AaTvTvEcs8hk%)NH@js?`pZ1;PJ6Je-dfwTqeo0L) z>gY)Py(}Q2a73oK&2mx-E~$I>?%j`YQik1yv}j zi)II0#>1V)$@9?Tpjl%U(966iC@3gGq`n)T*SJ@^jHjpJV(A|2eIDU`Mm9;2NTtU$8(4a{dR2GKjqW7T zFu4qn!nBHR3Nj~d8}2&xmSis)UV}%`_3Az2A65$lrGs82mKKyM5?M9mfh8S#_5cOT zeyTolpJarJijRjY@1u=fdtWXHdJR{((LnZn=(V`FoJ^M&v$44qntNiqHUuxH!kezz zj2}ZYCzB|@-FF9Qm5~ z#{fomHA77|K!Q-51GqUrNhyJZhp=y#NoQ>656ehMv>6cJ3)|x!(>?6k1~aOAiiU4UvxbnUUrcR=0)3OaOPYdB;V_Zd=I^x9UGNPaPZxo`_Yxm^2P}WN)C@pYEbx4j1+05ScnOkmschI6{&uPK z+t?}E+_Ld(BEG~Pcb6>g1q%x++0QasJfcyoB}|U335;K9M+CSyfYqzhp|FGRhu~vz zORhh&E-WeW_Vt~8l|&V=-TvDk-``)YqsH5PQV>+&nlia|xUmxp60%^E;0R&u%MgJB z5+JM|pu#}t;c*|bTSwNx%tQzSd3P7o12T- z+!0Y5Ni}mvL<}#pbxqO4XM%-a2;!Bu{A(1Bn_XT=nmf(OSvB?3zOPuU;5%8zj7=^r zHbM^%k5kPI4CIuulqduCA^kes9_P}v> zA;^O8p|WO}6LRl8crVrS_8EjYAoU8 z`h&CwLnn=tR5v^pWvX9F_3ZL_U3WNVtlZ``kdBAX`y+kIR0pkdP1n3FkdMJzdo^ zIDvY9zLDQ^q!5(pWtCj01l&?JU;AAO>hpLA#sh}u+ny(clIuY}-^N%Z5Tt}h;q2avis!4s6%KeFRu27+?tS2heN6x1P?3`9h(^n4OgDk z-8U4kQp`7|2oZO~@qlz4l!!1?{PwvdD;d`S>ECuPSy>p3xQ(U`0Deb);kWD~_R2r| zP+_b4^F>R=F-#1o`=??^#WjQwX4x(8M#?OXJp5Wg#}2QHNTGyn$$0qp!Fyu&jLpp0 z=RWd%yL+1Z!mgrqmiglipN|H*;e^9{gC$eOlq_l-}Mj)`#K(8&L0$ zpF+6Lii(O;(+%ZPnj+ zYkHg*wSkz79=WgR9#0pVmM;F( zKY!HJH1!F8hQi^5Tj`%s#+7-qy8I^A6q4Yq1xf~l37~eKnk!B|>oxJ~rngSaU~VxV zpj6kkl-Fg*dMD$}_S;Cw+dQkF&}KKOP1&pNgWrMDL-L{zC_nz{UFponL0mu*T-RK5 zV3YLLJ_ghV82un-VlqR)Pv7}3K*DiOG6R7kSMkyUimNUMxc?K5sG4GsC-8-}#ow}a z#6SKIj!TLVu;GPW1%auDsHo;|u5$Bo*QRQ6;edN1_s`#I2Y~XV-nR=WOna5OY-V3M zyk5)c+RkavegmnPpbg(x?#1Cl84gVmJ>WCa{)Ufh0S4+=%l*h571pr_Ik=X zW$UsWG&_TcHn??nGpuS;&e+W8G3*R1t7Jn@*Qu@fE&FK4Ueg1OI~QnUSp7L z-@AU6Qgb7708_aH4{mF{L|1~3ck4c|eGCTfj0MY+UIzoc8)&nwG6Ho?!bSP}h#@_b@AsYGLPAi%!{ZJhz znRpQ~ecK*`i6J~6zWW3b&x$~pr@bZ8OZMeE3SQL1mLagF#fq4&H&k4tQKBOC#U4lw zdP32}=_T&`9=j3O7U6s+4bz7gksF9Svcwx))ShMRkl#!MPwvU|76@9n= zwCEn;+Az^cCZ}sRNKVZi!bFHj{jZaB&wS3E4f;|@GP%r09@ufgb~1~XP4jPizyLz5 z?}hNagZ#2SdLV{Iz$W-b;kMUuNBYW{cSyH39C7-#WObBLv0~Ysl7)pc+`g@meKS(q z^O38I(kZ?*+Ihj)cxyxOjtL5McW&I+g5AE?J>1#I#B5u)p3zRAAM~eBzZdnnc&K}_ zWT0FXsT!i1+1m{|%aBuZNAwo$vpfV?zw zDZJnN^BR&OVFZI4fQ3><+wqc8Q&We`XDwBqJ9=6EhTiy@vJV#`8eGK!GoN$@2goGJ zuw*ADDGs7h+-7olj_+<>Sc$L4*wmbn=AY(iaKd&uxjO|YcR!`y9a9}#^xdCBRW^yFCHK5? zD6PC@G9vAx#X@)f_~g=+VmxuYHN&7NE?;7HSkfW@hbxi1Z5**9a6o*+K9HL&W~Q%9 zeQCAfX>X)9Y)2%~;$bd{AxuGyn-#$U^^a05_%)q{ObUA@*))miAFi$~SwqWvDCGzl zj`*>1<{+;78E>coFm5?{`Ld^R*S$}=L*i@GZ4WWny!87#^=ya1gZP|+HSW|U^YUBr z8A~s=yC0?eHi0=`w@{EQna|!DtPCjaQ7bVGX{xDtYctu@6Vq^s`2XYMaB1!kOn4*;meW7qNHT9UL3ce% zjPPBS`RK0Y*ql??+Ju;VIn;3^sEOau@Oy$L`l5gLWJk8Sdix6zKtKKR3tj>V)t?9M z`marFdzhqLP~`6SD8uTMuEV42CMK+oYw5W^^!HgCZc04(EOYS8nKQqBea-ApD|t># zwYg3&PTE4YUNcKMH9cLGzLr(Ap}xq1oFs|G<1hGHN?s^sAu)bCZU7?Mu35||)VI0g z#fS?!5HHKrx2VF&GSIf`1GCD0l1q2*0{QsSl6q=+C3H8bq4o{Z?um)gmhJ7ifK)V`(QY1qq(a(vB%3!j(_t zBRLBV*G{m(F@^Vul^)~w%t7!TZSuC(w=iTzU|x+_5;@p+N|M0K1b2b_Md;9h3aXneVts+LWXcb9*rfU*U)A2+SpmUQ{V5y{_P7=85^*Xr3b(h%N^rnH`TA_KNEAlc84z~U-pL*{GK z@7{(!m4%h{Q={!hdoK0sBOn->_QB-I6{pZLPm4apCap9KH^Y0bU!vmsKa)OnZI_1K-Rt^}IHQ)AP*obte`_b9CACp_GPXHoxcFrA42N(xWAM^$}xqK;?AHKQkS8(;H z*>e&{m3Cbj&7M!{W+Q)|L8Yx)IHkJ$xsP5f=!;zgxtUgzYwqg)scAPlt#fyLeIEwq z@7uR8B`xhF=fc6#(w4p}Hj8wz0ioy#GGjD7t6^U*o(V{yK(?kLH*MiNwG#G8IgB4# z9X<#-26D8tnI?y|!jju;ft2iO&+%c?bS4_`D5-(5duwtKqXQb$>fr8wY#27SmXhN2 zD3j&wSd-zW`_~i|?K~|UTSJc?9Skg|CMgP`Asicpg`)|JWaeEZ&A(ju)@;DJW2*A!m@iylU_a5w?lWVa~by zXq7%N`*qmEH1z7_o4Pk_Q_~B*io@2=#e<&~e-I%=vl!R_qtb=H{gK>kbq_-EWMF%* zC~~}v{#Fu-g(wdGi#+nPme@}L@4-BxrlIp1t4;S|;*Y|@tJPNn75u&DDmqS}_RW#b zI>awH>8sX>#}*>*TVuIPFAD9_WxEFx5lQzvpq|K&D}|kPq%+kp&)2>T{8?oGQ>-5r z4i5X7%5buN;B5> zZsxTQhSm}UGe@#=C(-0X-?QYf(QfCHa&o@hP=V5{L!eWcQ8DcU%7^>)CDh2j>zrNe zD|{v0p6wVW5jenXY!@o9dQ5ZZ5Uz(Ye69p82&M^nA@_p$jsP#y5is2q4KDEBG2TGf zrt?BrJ_8 z46zVNr<^ikdnF=*!h>_0L0w=cw4IGf37^E)7TyEc(-13L2q)$Rxw?R_>S@*Tz^Lc~ zSrg>fDb&lqNwzj@$6PeN_J7n%mHM&dXHF|G?KJa~ic0(bgc=iHRN5?wZ5X^3OkhSR z7D+bp1Td-9`=fVD)7yfAN#m(*)yl74v_&lX#d?X({n7}i0sjV;?79sS8aEd5w`ATG z(bU9#SsobH6fPBa=gyt(%&yJf<{Qw(pgR18a^$vGFT0;t=+L<0^WzQiDSAcZkx@~z zOVer(6XeC#$w!|&eHxkC2~BQI@JJT+(YmttL}<-w0^fxTi*3e^n|%_r{IAbImo-nl z)%enc=ty0QGpmx4B75Hp7(pF3vXpe?6ciK^C+}V#{%q>ds8Ul+zH_Q?!w)Fz#`A|s1Uf@; z9f<^>N?Q;K$UK^h;xcmz4YQ0ub@1I{Qn(~3tZ#VuBc=t6{2`T)fG3{rX3&16oxAv} zE8@I4c%K+DlOs4&EJS`%+jrO0BIYc$Em;jICpp;Lu-g8$Yqc2u1%VO?9QvpHF1vv& zai*DdoKv+geiL+O%pwHJ;G+~JUJb$49(1!DkmA`jMsQWir)gAtdv){Hz&!?{=;fyT zJn}aY5u7S}PZZ|3xG(qDbEb{`@e$l?C3ONXk2g>+`rN>7*x=jJ#Z)tTLiEnPe!V+8 z!3x;!L(w1tt0+^OMWk#OsgWl}8At5OHu3S*=Vk48KZrE9E7evT(@p#3 z)5i*zUU_TzlvueN7JHpyx$r9E1aEz!rb>c-0u`s#l1w%w zxpaA#>*MI^?~lt@Sqj*(duwZ^e!&;!+cVlh`(I#MJ6B(cR;$s}4+qFuUMky@U9l@9 zzFS&VLbjP+b;?JIG`+TGxvS)d8GOHw28Yd4&dnw=G6nFf9Ve!&nP-dR+j*kD2lyn} z{dKIqar;1DUu8&kPvG?3b9Z^v^z>dp`c?G$h;5wtXf{rOiAo5!V%IAKio3e}cr6R2 z=ZwgZdwE_wuq;NOrzX#o3!w6FdkX*CjtFb+@z;YHHm7EUShVZT&Uc>@a_=w558C7} zaNFlFqRyLP=$x8r`tHNJ`9U-(o$F*N97Z9Hrl)xt&?n5M3tL#{`qwt`F#6)%)1UR%&8gAGs2Be!6;?Vt?Rt#6 zH~!N^xY3tP`m1Lnf0QeIvpRU2K4x=~R}}?JS%a=td+g6WbUY7g4F0z7rQ>{oNh!aZ zqWNsPTg%_1b#J;0^e$g-HRSr%6-sl- zLh9%{Wa=5ZO@U%TnI_%c@3t@vA=Ls#s@?79HaO+7Z1!P=hQJSA8#6TIvLbin(g)3s zK)M-@NlztPe|}2=&`^g{zkX78AN*s#S!s3SqkwXBCxC$4*c*OQnUyxnmyEAzgSK4m*h@Qi-&;9qpKa!jvA@OM< zBqp3o`%iFx_PNQS(+r5RtK9rMJi5BPfCw2! zFK%jvdJR;Mao&d*e4{zT1(uW=)cGifUVr!J0qnrCb!Aaix0|OkStyjtb8c*K^5|6x>eZL~L$as4e-?a7iuTQ94WF8xIeWD~ zco5SNS!I)y`~G$qJyE06?i8f9+mExAWXqZzN??Z3PeY=K4+)g;eV7Qrcwf_&ZBmTM zPr$&yhY2h1er{boVxGRqR$Y9k8*lMh#+s4@aeCR4GR09crJpWqgreL5Q4>CzJ^AGE zW2bP{PNB%qe#D^&)d{!x3OmwGezdm-t6jftSC_U6LgBY>O(7oKMS=o% zD4yB1cVeKlqNNJ~q2++L@IfHRa$cB_uTKzBJ84^zi_|QXhbWC-B4X{yOvMR$(5)PT z$!hQ?LG^GyOG?;vFU_^m_f-h-+Q;$vL6n-C1 zw1I7I!Dsl-I3wDlHd%sMqiRE(qI?$xy=mVdQ=UrNha>S1Q=g2ShBf+T%U1(5SqGhP zsC-fF)1c+Ea|#JL!4@M{8(`F*aSx?25rZFDcou?`W6k-@=!7}C%5m+#T6SHptCM26 za-Y>~iUbysPw9UV%+0!*y-YGk$Nr%%N-2QDemA|udSzhx7)B%%{rSWZpI}8 zP7)}#toKkFW5n;DQAVuZAi#WhOY}??0hvzFHrCbEGfa$@#U51YMI8uuVDI{qZDE{- z#c*7T+`Suv3Mr44g$4u8NUC`nf44|Z-Q$}6t;^e zPM+g9qCidUx{vCmn=J-6e2}#z3|Z)=&^x%GN|#(*=fTPLC{@oOWvn@!SM9dP)8#U{ zZy2bq+w{-)-APJjWVNhqKhy9I#%#2vxDU8fO67bbc$#R5&7Xf9m`Tw3f}ttz$nGI< zG_+xi@?V4hJ4DkBTpmth4@tF>RGR@-R6tj)M-6!_v~Gi1CSc9tV%IO1WUXRM)T?%c z6_nQtjr~0W=U`eXg?8&F4)>!DZV}GT59Q3o$k6<2;O@b%;Y2=~Cs9)>{U+$jt90Fm zk_nGk60Ie|cAfvxMd@^FpI~&?kMO{Lh#OK3yGRjBM)Kp&NGg)YRO?s5hzTFvH9Z6x zQeMB+>49KEPwM3mA_4x^Ems;oJO94x^w#;}7M5!m{IeXpLB?Gbjo|50H@ZK4^+&_r z`ubMo+Agx!)HBjouUfa- z;>4Gxmx#2?ziu|t{tQp)&`b5-S@-i)a^93ult>SBSbjqsPE;t3zlnDfE1*;%XDeS^h#(uR2-@0=fNC?eEbZQeys^^a;U2P-VkHArfN?DCG9b88yX?9`5{o|)=O8OORCYMxq`}(ZfGf?y_G9j ze`dszIrD0Mtm1M@sEI-s&=Pva@!cXKrFI;4!+X>+Fs-2~c%R1KiV*-bGeFbDwj2TO zPUk2S2KVR8uq4g;j(tB@W9cWG;#|VRPtA8G7f=fAP}S3`h>T+cHvIOW%KojyeAOi! zeE7f^k~BIer?<1cqMLO6rMtUYJD2ydG`0X zm>dA^2Fg+HQJ=5{J{!KBfDG55KrF82KYlW~xf0chiUZ4w889~J9TChv(Uwu1dy9}1 zj<7wxLbjiFu+Qsj+Z1AF?gjC^OTN9A9wE%iM6i9cF~sIRLmNYKS^6r=+I??L!x`ni z1B8gVj{)O1s$5tRG~ml5@8o{>!pX_Lw?0wlaCUZ&w;X`$=)>eh%9UInKR%M2Tz+n9 z8e$x6%!Yu_H7B}_jl37Zdj^a}cLOw?GE07^n`tz$xP?KveU}rffdS-)6b_RDgwHY@ zPWeZ2a>OOmRN~QQ%GRw`{%$tv1hc%Wt81EWJ)%g=s$4=bO4xmt@-nil&||$$HQlvk z&7MO^4R0Si(l=k$XUsQz7UYKA{YCqe*<%RlaqTt@=N`<3f5#x|uI{KZNB}oan(f_q zbXaAmmKx_yAptD)=c&2j1u6&^095>mo2P}@xhVnD*mUe#dnur=!vh1}fmwDx)J3$nZuZ++*R0z&sh1@2o9T2N1w;b<(Nd%w2KDSI zYx)Y0X5>>5%~8kuEHEY#7hdvfBpa?b zlzdvz+}zxCkQD=Qwy?;6k5oz5DIq_M8M`EaZ983?+3xTK^Mytx1(FuO89sjgZ?ln# z*D*_|wZR3WWf3|Vi*s{6^pAvW2s~pVqT)|VJeBywMSg;R!j)id2;H$`2kh;8u?vH# zpE;3=_v-#H&0T3Ym3_a}(<3j5iVP9;s7z(bJU_CDqDf><<~cHC+?8e|sZ0?SY1kAQ z3z2zDM45KxP{KBkXWjL_?{&^~&iQn{oGYyH;hu~>@_fj0$t z78kV-H*;b;-x7LzSHi?>t*Qs`nUor(pAPK@+E#{ygK{!3();mcNhP8cRNw4{XWaO# zZChetoZqA(%Nj_pubBFd^($#z{y`xmgC59BcJF_1Fk0BoHs#Cq+IxH74>#L0vM5|F zcCR;!&dttl@~0SxU>Jd}Ox0i0qmtY>j?9nOEJB$3l$ZbPYymS1)C&;1m3Gv!jh-&g z^HK=2{#y`yLOIqNxI(RH{cRW-=htH9pQ-7CZ+baeUa#-%<}?df<}*$(Dh%rB2Zs&p zQwqV2-mbBGSsGJV&}~mlX!Acb5V?-ji6r0E3vqtU&CL@tQ!Q_UeYEGi%+)C7+HgM` zYP5WpVjx@-EgJoZ837}Gv#ZP1{loiSNyyosS}kfxTlt9uWnaUb9NDr;!CR{Vd% z1moMvQjKEg7<>-#J+#_AQL(W)D5&l$@EpQ=g}4VKflNdKg)-=#a?Chs(;x%OHoLYl z6>#y;LtP7v5QL$Ox^3HZ5INF*6md@V1gx=AJ>88TT%elTZbikd+5rKSS!ly#sD<_e zpNjlkqr};UmRt@myf!uP@=7l}Fw zF4z@wu(5?kMfqI(@y#J4UqvlLQ7Kxe1e*pyaH(Z;JW9#EgAD#F7 zf|;_!Vl`BuJEf=o2)F#;y5W?A!+}x52Or^cH}k!#vCX<7%e7XkehT@%x_9gNvc|#B zV|*4Jv0p2#B;+bE)jUCIT>!4|eU8yi|3MgT?l5~y31}>$ zH}Q27TRD3@pT?1Su1C33*PgY=rK{*v;rgV-Ma=x(p0CrF#$Z40i1n~SW4OY~^CDmN ze)LeWIQ%~}HJxVdG&Qw)zs1lp6EXFh{^PlG{2qsVrc9NA%u;2+jxX(-V-Ie2x>cD8 z_lJ^CDePzA9|kZ}ZKX4@Z>FbnW(y{}!C*KPaz3~f^HVWe+~_8wwKm(fZSQlBr@q3r zTtIFmy$PReN-UVMEjZ0^3!0^90c~3OhmydP#e8&8Pa3L>c-3cD7$#o@4GTRfEZhcl zPy4HD*UIuWCju+~Qka+I@K^TlIT7G2?guviSaP7>N@vmWeSCCZRmfx(KIO0zlFxbV z8|P18kOHXkZb@NApgUL8f7t&m?bdQWdvq=47f30`Hsv|4uc*+9V{XF^6>^ou+Q#Nm zys0B--|^?e`vE?;Oicv$*vG}iowlMS$KSJB$TxyKL%HAPRS1;bDWQh~!-!w_{$`>S!M*w0iznC$x`Uob)fz30_a{X+ zP&#tgcdqaUkFNn#f!S6GGy$_e)wT-@b7z~>7cS1i$Fpiy>c2tL(Q{fwpC9GP*B#RH z2=ufo!mi!h%M>j8XmoLGWTX@)B16r0EnZEQfjz>yr8%0Sp3#}p$2P-$-M`2FOMAOh z&{b#pIkrvQ)!<%1boH(*Gq`k5t`FbZe@K(&WNF^w-q_iP`BM7)$<{piHlRZcYW?=8M=wqXc4} zmxnLWTu#aK{=qxjr(+*aKE3#QsJ4P-yUgD3J1qwF>cePv(T(k-K1g<6U0aYVZa&}w zg*I#%1-{puz{Nqq@ef6h+YyOj&D9ZTH61EU(UG)Y`U8$EQLs>lOyrqghFN|l#tV9* zBC=9Cr}cEmZJ3oRCQwF*wnDvHQ8+i8#ntW#vT`RE?5NxNf=Og zJ1s9VpQj_M0j~~^@Esc%X?r4=1-}p1t74tx@g();{}FI49{)N76|SinE{)?00FA#9qn5#9eqIuN3&$>wz1d4A7ZaZ zJ&|(ikB&=s*}s4PGef^rzs+H?jZy5L#C^?|Ma%=xlb4{+CneZ z910yuSy$n@1l7^MuQqh$fd$X*UN1E6?-fMt$Z;fLlj`!HvitB-Tqk(xWR%w*UM^G) zvL$~JuR>hvUX$iGKh?#|^5#k#M?St%sl2FC$YwU8@AC&H{whs z-5V#k>0KM6uXid@F}y2KZZ&{ndXcHXrES995R+XOqnMu-`p3JfO_p0+SubPr!>|;+#aL;1 zsJx=W@bqW5NF|urEle%IYlh})XmG@o{N+kY3aKdl>Co=n^oED*U>-RRCzk9eix@sZQlhp-pZYICUE(n;i_oTP)1EHy4|=`!)M?gQ^i1n5l~s+%Gq zQGT15ZWVbv^l)4gBvTn)2>=r$oW}xhC@EkN+Mz2Xqgz6}qhoj3=xLP2b%{z3roxzS z7#lZblFp74Sw2Y%-IZwlZ@625eRvmZRng@=!opA0i~qKAbGv!>F5td_y@XhmEbUol zL?U;jK_A`Tj$kn7Wh27D{MuhTp8P_PKg91O@e*B3s%r%mrj* zC~2XST2hjdCHa@wT3cngxcY-v;u8}aQfjSG^>KJdQV*cm&%5i>#BFcBGZ`OR!*jV^ z1}c`X{rv_lBtHR?rj{p!FR0dKWpnZye1*R=rI_jV_J2LuLJAfjkw_kkZt}KXWyYNO z{gC`#MnKXQL2bb!0;?9imLA%IACEPA&5S=*){nH&V1)?&aq735Sz>UD#B(*5>&WG` zN7p)Kr*l1{Nk&w~uSnjxgiFm7$varK%6a<;Tn3Ym9@I*fWBeMgu^CaKLVKL*$z`=Q z+H*Tg7Xr5Y`|00T){ZQNWo`8Hr|*gS^%B>;j(ig*_e5^L z-Jun+^#ZD+quNRDQ2c$E&*J;uy`pwU*LH>Dwj_RA6AL0E45leh>?M3kb1_SeCh_ju z+GmDY({MvujwsIK31+pn7C@Mn#!t90n+%xO2U>=40izXH^mbrS2bG9$*+<4vT5+=$ zLClu*W{s`h?Ni23F2_1X6_?jGoA0ZK_=Xy%x|Cnzn@NLWIJ8JbOM3fWV#8}4wpgj4 zbtNS`Oq#ba(L=wsW1na!w1_SFMV2|s4KMO(b90XtmhwEgN69975pL!l9>OP*JPT{c z^x@Udx2xcC9d&47FYgK?h*^~x9tcZgq^)>$Q>+pPFoJPJ@79})Y2}<7Ynwt|Q9u?b7 zPQ`}tFT?s_b;f{;i;M91L}mEUu}>>MT@+b0C!V)S`1eP*JT*T^D{|N4ucyZ6ES26& zc%ySj_aPKYYZsdSLqJRz#oaWpbA zq*%O%a3@C1*U;*+vEc99#bb%%*H1v$iqZb;Zb(I}`(q|fni#7_9r|`JCgz&CSP1J6 zhzDzqMaA4n^K%E2|K|fGZ4)P_WGZ8123L`0(kJ89m1UP>n>Ksu(~2%=%QQxo<`5oWdp9<_sNRX74 zE`wm^e8QXAaWv|^h6RKtSSv+!ptPKfkXfNJ^&veXXZqpBEr!Q>zJ7goOI+*a_#5Lp zKB9Wa9Tw1zsKl3zn#t!upxeiJSv-6eSOjRejPMaZ1gsbF~taS z)TlDrexi!TMX#zk_UM?cLxky(kZ?+P>m)EP3{#~_Ep}|h#NP0KTojns`xKa zGoeJ|VGE6oHJ*94URqUy&joQ(8Lvu_`->6khw3F5ABvH*-F)y~29eq`VO`XqnkDvb zsGu!PA63150Ys|^eA7^8$S3LNp;IzGS4WMDbO zl`(q7OS<)O-CSHt9qSQM)R5OCfc4Ko&v{hW&aO_tYgUoHXx_xyftBgBWAByB=h z1QwQad2uSxZiPJB-(!)`_{Q6+^s(^HUAs8+Rn}WEllrta91&qYNo!VwlJ#|}U*4O2 zJ}n0-^yfHW)4f%$Mk)D>*O?>|&5kHYpt~R>e-Zp@gCn1ucOr9HNRcvmp*_tD7ru?o&&mO(1_awuvkr^}hRgqXqXh#=hQAqd&= z>8hSD&)Z&&1s4V!>#V;1uHH08iM&I1?OF^@aj6YDn*_DrAKo|g0rl;RJ(O?resX|H zCx22B4nHuQ?6g-dG;2yZ%K#9TdA?T^)(iw(c-Bt;5l)O-(SZ(Dmp#BMxOljGe*Qc- zpnKP|Q#OvuHFEG@f{$s-C}7R9hS8wsev&0!8i$tRGJ9_eQhz9wxgwXN+ogq?}H}=XH zwy3ep$W~E9C_5|s1L*%*g@l`YWGvMAllr^5u)5vXL)NG`!mdInCLX?IFNp{#^tNFx zC;kb?7Ux41HX|7?gLw}rYvT_BH7!$9cWY{C!jbpL@5+mG+Ro`2ZO!piJN%#2^KH9F zFgLKKnAl;IP`^(VxN;5do%ud}CPz63!fhZ&gj*h7$gjcZRiFYApQ?Wm|5fgoDm`vy zraK(=uR~wu4HV1&=f`z)!V&zO-b5VaKPJ}s-@ng%{sMd&;Z7?wGD*Be?*^}h zI!7Rp5(Xl7?<)jt#bh;jamjsh;v<=i#1mv@uB z3l%9d6_8YjJ$>&U$vo^YWDH+{T>u)rCH{woS)bkab|0prqub?0I_YI&)x!HcjE6(va|E#NhVB1Ne1;T;adOzfC`e8R09BDA<*v=5&|>^>l@uA0Onl>NK)*h zJIv8?pXZ@8KEme}1@`ojpi~niCu1ZTJ~MOk-J7>VBMPM)jX5@{UT+Nd=KfTh?tKca z8E$kZLw{}P`}1^p>UZ~;<#)bx>i5~p3sF~h>CVV$`tlh)?e64}?CjWWjSLra(!$U? zj5gB?Ge+q7i9Jqu>h{alhs@=eiYeqPO$C!|hG0n-T#2s)J)SsJ^@sSA(&cHK)BM2?r$K)et4!ZDe_S!)f)U=K z$j(rrylV=~UUgeltXi$gYigXu_n82!w^T_0w;d(Td7=> zP?rm5L*C#&)XehjU3LIyZ`7u~_9MKnb$=2vy5b3e}Z+SJwri+lJv+h#6&rV)=oi8_`v2_Wc zj5@xG>O;BEjX|qbX$6`g)7cd~yN*YSiuP+;g3Bh1zals9>4(RNykcE2Y|%|gFOnYK zwE&T2Rs#h&8sA&qebc>_IygXN^Vrwtp!u7fB(QoWJoYNU^<0Bzv(V5N)ivZNNO#{! zf&HtJa@T!Ud+Cww4nnS9;q&541c!+ETh9M>P9tbZ%^43RLY`I8wThcE3qpIDBXU3D z%+0A*q_(J@v-CUdF!;{YpU8MInp9JIr=6e(6rb?v6c>8lKpghX;6 zW3jxA&U~q3R6l!e4yJazc*`bd7=Y<8!}Gz@{CT9!`0M^3k6}mhNR(BJKY#XVo7JS! zmRjHArb+7i1Xlh|Vf;!&Pu2K`84?iWW!_NN&Nxtgt9j4G%b~;$A;?_}8K9n?-yUHk ztqm%@Rj@SnjOO=xg#6}7IZ2rZ#Hxny*EN4Q2Y8TVq-+Y>yIX(rM3f;vlp611TRijm z(;br1v5F@T)>!oMba>oJHk57k`-~y-`h@cQe7iQj4@o$PARok1#YI8pJ;WvEakcHp zm9248f!A-f>)%d`VH%|6RjV{mlV*uh7V6A4#Yy<{Al$71FbLVpMT?0}9TlLN-QL8$ zT8!UrN>I?zUi$0+@9^d8;bwd!Ci!>ykH&0jxa%^ez6D>VVO8_VOxu$hD z;p4EZibayIBUZnc<3f#6T2FhmAS{fEz|-b!y7VEo;??%TaY|IkncCUS$*!nqMs>p5 z1%?3)DJ7}%DC|7<^b(>J6mmw1wqoq;-`kt}xOHN{Air!6F@HTh@o69yB{+Zw5^_t% zZM(fvL9+DPtuW+9N!y!TZqYLyat$Qp5ueWo$Y8S(V8JxNnF7DX29;l`=EF6;1l#Ac+86Ii!D zHF-PTB2x{pgZ33Vo_l{h^%$QKZ>07wEKe#@Od?nI>D5=&g=5!R3_Hk^Y!lFy?%lt_ zEr^du)}zL}0cHC3vB#~ipCG1xS}UartENx2s6F0Lw(a;;*Z@~7x#aZlEkYr^RZKV{ z>PLSPHiYekkZ!BU_6z3p94GL8h$K*@sQzkmmXF)*Ai}d=U*c-TZB0lf!Hj{+I`b_C zL8>@NSS&6f!!S3Q{O8jL0W9F>jcV768Qt5iK{l#EpMp}n z*G7_rt5Z%B=y+ET}-)m6ZTTIA_c zH^|~beAMHm)$IgY-uLI%UK$H~eT6zmYBOwBE|!|5r>&UEi?uh_bj>7dW?YY9EQW#K zRt{gi%XLkBaD5ar(?r`z1ko*hXA>;9{T_04mwQ$O%J+2f{D{_&z6|~6D3F587`*S# z(F#47Zxkb=73pnoJ<3;9ybPH;hBR6=jM#39))=h4seNyo#a>wqV3-;ZWd`Q=w#w{m z=c4M<_v(MPW+jd?Gu1NIX}Bn!McpRn4UhKEqFt6ZR40}krllGd)jHb`;0W%bd{`lpu5$cUP8d|vd=>-qF9?#@)r zD`Y=JR^UC#EVirNJf90rSsuoG2?dGaIRLjXF`Mca+hhVm{%l+z7}s% z-ey9i+QERLBNtopHGDccqQPZ$_4^Bc`fM1>R5$+IX~!!}y3343o`?Cb8m^TRyP*NQ z%u{1w_Gkk(?ljmPa{vM4XGYBd`8}&o=!bkBInj&hQ*TXyy@tKCsX*3t{l}|mWh+CU zYmMW=EZ+;A?duA`CCUN0Q$PD{RlY@HKy#EJL&;!wkDTTYbE5^H^@}P|hr*OkUVkPB z0=7Lp5h9by`{DXstM53%gRdR{B#4|Zbxu6|Hprs<)0lq55qup4A?Q{sYU4q6#~!#8 z-c1+^!&fa%<1icZeJ_Z;M||$hRV`*ooB)-@?FAGVFLG_qCcDV-u5&`oLs`TZ7#oxf zHvgUUCDL1~ zMsw?_K6=&ZX8<}qES+Wq;3S@A^R7ubbUI@tp6~lgc>ob1eIAp|8seM6CT5Hb2;$ggXW- zL3M$`6(W~{7nK9?0S^_Q{5-?qCJwgXvp>3>!$~VnRGK+jF1!}6)Z$eRcE|5AjdW`* zyq6>y++9T^&8?J~(Qm1KD;XnP#&evct2r)1>V>u5%M&ScTKJMrwHfU5B!SlDKE(54-Jj0U%ltm@^f%XD$A4Eps68`A6U*Wwa$(gG=3JO z-cNOYUe31IEzGqZh1(t4$L%_3 zbkS)X{$sxCeC6Zm{W1G#Zxs&nrD*)G+P#PbY>V6yOeGk^^flex8ob+87J1Dj^}Q`6 zBbv%@$A!C!VC_I!+pe@)^LrI~HT8hhDsseS4TaVs#V+}F=7xt0L#{wi-#--zzidwf z#7z#yEBKDB$(ux#A zm9`T7O2vC_2Ma{ZH|{S1PmV-G0`5CpWi#}w**O%?bxzzmP3K)7O1TYEqYIdOy~*w^ zYHdM`)WTh58GytKX~iE4u<)p_j295bHC|f z#HO!`c_##zwoy3(VAsIjXNX;JgiH~B`vM;Y@d$A>1mCW6f(*}yEGU?6BKYC7Q9Yb? z<_XF9es`vz-RalWW24(o<%c-qty$su`gy~^`$vZ|KQh@+S6Y?e$UFwJt z{F#0g@klIE-A1F{(PnzDZ^;x{m>;v!Gs8N%Xg7(DB^KVF%-6?cZbeAC+0;d9r4eq` zxis%QTUf(D?w;IDpOv3=i67a`4Yo4BS(GQ*sfK=Pi{P;0)0*VcT&o*9PxNH~U>dY1 z#vj!A+y+PswZ72M61ty6O9}ma1daQfuBm;3I?8umdZblq|SJBO`w*qm4{wcQU__f6EB9Kqhev1nZgujcXp`s{F}sW@!dmtLY4r zh1|SO;6j~vY-|U|CvR@_AFb?q@lWsoK*)fqpfF~al=<`?Gd21+ka7tS$>SM~&U45` znh1GA9h=_GO}6ORw6dh$x>S&gk_3aYO*?{Bp@@1tQt|d}_L|iMUCsIpcy&(=u529} zhN~i9;o2gAv7qo`W@RAu-q~R>xDd{?@?96u|LDS)=nvSlUa|P-C+6+9E1V`a6bd~2 zY1C`1bZ$GZNeDeM4)tl@Fb^{y ziqH~TDGLy-L6@$Khj9>$aGdujo;y*``81J>_HqiO7P_nQuAp>sim!f5?n$FL;hXxc z`y4-(u0BCUE39SU)T4T>@Jb`px^vSC3$(bWanv!ZV2YNoWt+O-_??t6cm6ZHi7LN7 zTLyU+_S5t3o$b#4Re^?ZaoPQ_W@92Pis$VLbEaKs+1k_Th8OsC#!E%rlM6wDF}CgD z*Z9{SyN9Bin`9B1SmKV6(_NUFFHn2H>_{x@?u$8x(mBOYQlwu4`SHO^j9r)sGR<6I zL{AjANgSrDqRty&Dp)mK^1F_y!xMn461QnSVmLIOvJ;{!8s1o?8+2Apr_AMU@)_My z*6k`o!&(Uq{Pm@;iic zF_9Lq|6LFwv!X_L3(w)QJ-i7+2Tx9B@BdQ{cdzM6$#roISh5~z7Nbw2)t%EV| z_$lh#h`JLOz~B>Y`v>nr-T~$3nDA3KZ4G38vAMPGh5i{lf%23HGt2LK%zO~#y0g^{ zW>0ZZ@1!OXDlqNIJtgLRJIzcUF5i=ir}xo2GXk~K-H|C#zVwAPPTlblWW+%&;4SZ{ zm|ZeN#vHO!f*;qh|8b?>n5bE2terpc{EtzR3AEti#!Tb&siZUM%a$7mZ1o1lXbltD;xp(%R0*xCxc?e|fB=jpvW{f8Gz2t;i zvhe--h$*t27a^p0rU45U3aC%7v)8h|UT(`T6dN>gQ61vrk%~a*dZXjCgzeNN)+YYMx1P_IQPKyY5;A9*UfY> zZzjnWAwAjS%vwFh#dA-V+axZJy8BvdzL|EyorvvfhdOT}m9lV@x30Q()7$i|6nxgz z^+Y#`mWOr3=pPuL_a%}H?_qK-;7#*N1%{OxQed=t29%wG(7Otryg%(paQJ_k=hP$Z zKHd~MO>VH*?Ka!M7GQn+j8YuG_lQcSn~d(y^LZQIH|9w+F>V`M=Ki*yq+6?h@@{ZO zi7a(pyL}LwZo+oGhz_q82Ub?ehzOOOfb1H7fa4@%TVR3Qr1wp4brnC2pZYzc;ltIv zJjWc{3ERPQ;}=99(5Vv_Ktav$0_KgLwywHBKoN6BtNdZs6zl$cz+C7##l|7GrMCNI z$#qw;YyN{|M(D;hXM|UiPbAg}NDaMa!r;01^h&1UGph`F>d%0E7LX5jw()6v?~vZ%Cs zA&HWC!XC+`|7|l3SG+bvZ|4!N4K0@R$xvuq2B_V0&PUFRvZgAjo-=#Mz zMiMG+z9KhsLdx2`m@9mo}-&;W?NNsHfIW3?!qRKWwSF5<6Ze3aM9%p zoVcV98z*dTTm0%z2J;U_R2F&vws>fxM!n$U89Lv&qPJQ@xU1+qN!KTQT@Z}D+tV{0 zE`w3y`G0t!1qZhqtS%72uyxJ$i>Et#c9DpGBGQ-9*6~Xa0#wouAxT2moz@Hl!=j)f50dt9Hi9LL!ns%^f_5+XE)me%NN@9={yDt;uB^A=$K7 zsa`?saKOd{?M#kX!LP>sw`9Vr2oR8_oR$nZU+`}YveeGOzz7YwpBBrG%Y??(jyTiN zc@}yF%l1Z`qjDp#qZVs|YKtN!TU3%;vIaw~Jc~0HgZP#>8qUk#4IZ<3UdJWF$(xXJ zh2s4TNXv(ZpvdsaN>tsGp618q8f})3ffony?WFp6`zNo5`{b|VZD)?h3n{#r5s=q= z{FE%8b2CifuzY6ucdn6Bh&+zQN7akrBLiU^2Lhvgfz*QpJ>yUMEqAt3%M46|KA}-C6Q27Z#X~Wtvs+uJZHPda0x$wmHJk;;TPL8$16DaGNzm#m~tNnPl6{CNd zWLr)5K#upiO+Ee!TT+x8jwNgRni2khPDYe8+$7c&XQ|LTy!k1e%Xy**nO0qB3us*H zH(U|ToLN429YEFX5?t1#>wL00&akw#^YPU>{%#p_QV~q zz4n^c?zJAr6a&kJaDcF*9OVWxs|D0Cep)!!Iv; zT%xB#6DaSeiU|0=UUfh#_E(P)68XC7eR#~#oH07QXs&=lyoM%+7ST?Eh!7io4Ro|MpB+lXWyM-$k8^K{-#F+Tnk z!?yEzOkx091^~rC<+8%C06;X5jB6SC8wu)o-H=(@8iNYh+n&+tI*>~}Y`lBa%x*H9 z&d{A;$!R<>5yx&nSe3$N>F5!A3hFyc;Wp81^&!pH%6bWXL0>>vKw5bFt?Xc40VwjM z@vvht5sO)EyVZZ^H=c=Qyb^-S{$>7q^h-K9m_k=9R)Zu?uU#e<@-xx6Zuvrpq0iz? zTqM=dx5$zRQ$ydlNZF7ON&P58bHhebN`fH_h}VL^K*5qLFxoHw&?|=YuRj0%_*gI9ApU`faK^Vh zvPX~m#+4j>$ro73Fs#x1tppmUq~c@@t4X;teYmW8H_BS^y{03;=;1lb{8aY#A%i-;eEtkTv^!#D13STw4 z-pYNwd!0n3? zor;*8ICgp!Z`e-7(LtIwIEwrXmEveDq;*YgEv40P5>y~n#k5(a^aoOkd+cF4MO4M| zyc{be0demo_Wl9d!pTZMk~rC<2Bo%&k_OVpviIQpeO*~K*^^_kToE+LME6uU-8APW zhD4uPVlERiXYxBm>dy~_0o!HEu>RY$0o!)RPAIxETEc6gF{IO5h02z;Msc>wZPyFg zD&zC%HZ$#;4ctIVPZ`+|r8vL)2QJi@=xLFG_g^f{uH#4dp9f;{a&zR}PF5dxMH;JW zoG49Dh~7qhkL!lOJi9uYN0W7gq$qB%F1w(P(g}2a8E{C}oR!s7je4iRRMb*&?iISN zO#bsj@gcs7fl;x$?w^DGRw|(GlE9YTzSIK(8kHJ(JPke2E@?Y~OOiZ2^pLu@ z8X;e+R|L1KlI?qvAgI3rD-tHb7?VZt@p^~VX)YzCDb_@698docIE5F!u zHd$Wp+L3;gHINEaJXQh|_oSIU}R>_pTF=n)GDChtgk5>1` zK9Bd*eBfq(BV?SlZjuud5sL0@!o%*`d00cS!4`Mf373?XOt!s-46eqr(Lp5Gz@ETn zDWEjLlFVrIV_S7UDjE=42!&2f&|8SWnlD1A3SqI<=WJ0J;5Is zw%*;Es^-t_{6PE2CVCVP{puAph|MHE@vFSZgw>4eu$^*GtsXA>(tfX@TO6S$Y% z(sbO{v(YT@fU6~@iO)Y}3}@i%&yd6B{V{_8nX#yyFr@8p5`&j}n<19Y-fugrf7v$0 zan#pQDX)nlghzbnY#Lcqdb`4=k8D5CXx3CbQ2yJ%g{SX9)VE8}v?~RWt&9xupLy-x z8gh+giaSWtNqK4*=~$ht-5dAtKOrODb=iuaW@0Oy+M-qF_9Bhn0)|q_XepyPvKqE8 zasjuTzOTNBy)5+g4Qsh;`>GdP-8&=6ur;HbSCSmh`-7fT=z$80o{N34`n+pP)C_#dO{jsYGI&fJk z8sX4Rr5_6-k`e&VXQM5G#e_#ZUz9#JTkvo^yXyDs?|SO3DvP;&p3MEmUu)~FzFW9( zHDbC8a9~BWcSjFz(u`@@2Zv*|9%VmkYi>tZwzWieyxQ{SnjrrU3&mxK%>QrJ<`6;H@Ss_9Sg7BG3A3%LBWsVKgqMl`WPrJ^HcBq z@uu|z4&{95f?a-S-+8-;y`1*B4jn5`wtxNrnTwFwEqVr(G-8SL~>@HdH5KokVoc_lgCuoE9d z*6C5nn$MF2Hr;mYR=3HX(4Fbr9c-6rg)X1uCkNm{fDViPexGHN+)6pSbOo46$Jn#e`4tM(s)DH$rEE5lQ6r|6c9d3*XWpi17u2*+D>XpH%RX*o=k?&bQnoo zq-XAVF@Eu?`z?I{$@8yNeD>@zROi@D`Ck7xe{!sLn)XpIyoFtEK@Fb2A+E$lM=AK6l@P z(;$|3&n6Qzt}DLzr+Xv7s3gQ{G@O3ae40FfLq8=-Ibtm-hZ(aVI?9B@J~WX1vlG(= z;!x4Mg4g8f}pcIk`vIM!FHhLjIxGAkY|%%GEC!GG*QZ5{t3az1M1| zWJ4bkmsXu!zt4wcelL6+jlpxm+3t`<|HL{D$)FDyCc8TeoGO`J1y$qhp zezfhNnjYA?hZE6f* zaj{55LiHJm)n1RJga?4Md(~(gyPOkgJ(|bgjtIbJor=x467(-{Y*_g2R)pP=;q$g< zvOv*SANHHK@jT~iPiVv6VFUwT^yb4L8o!mz35bwBXy7mLUw?tKw4mlY2%4aLPamCl zSe*u)lFNmpU=&EU{Z_e~LW+=b8eeGlJ3!I!8(HDvN#xXeL#H0eP|Jql1$g*5i;I~J z{G!o6d>rF#uQ@VQ!a7gMzewgA1%7Y<)k{#}lNLAIeo=J!GkmEkFC|KfSmHTJq-g%+ zW?Xd`IGH%NT-8p_XB_ie9TyzT{UuH12epG2%y%1zQ*kWlPp~I0nN_x<^%q%$5=({p zw&VJ>Uzn^ynH{g^GWQ=tJQspw-cu0R6M0YXDW>rWXn;}OmoUMoyB0-M2Nu8_oCl?! z%x^vv95^n_E*;N_ruTo>Q3NHLx@3tf@QIko;^GncPfrb8#u7hsSB@O^9Pvb-vnH)3 zvOj9P!+0;hW+|Z3Vd#Ee-=A1Jb>6^HaN6OV#-BpkxVTH;KZfP!CQI5DX^r}!@V2^# zbG)6+`it5-rmohkan>ay-Z$yuNKfWki*?nb9O_ z<0dj4u14QD^X*Z>!cR&1zm(LN1bZ?q$Y{x)sAqY~BGed{Oyw(+i~ve{Saq9QP=FE7 z7w^)J1zF#-$ENoN9W$@adQ_WnFX8u6t&V2f25K`+!ecCc+@!r~SQZ_f^);haX!N zG?FojuzihdK6cihpEe4!rYydn*TbU&g(OBQs=}i%zY<*b zDaF2%6rvhh_l#!?KW|*~V-++#YUzBvNBp2&GO7gVjSD;m9D6EoxD09W2@dp&Qc!8W zcLe|=5| zqIF~%PPWT4Zx_iAHUXQ^~ZPDpH3dIgd1ljJMgX4!``D>mx3iQY#Mf~j4^;?8Ux ze3G7J5FnyS4<>B$#jtQWSTi5eRu9NMjDFkFFjj=UcNie^c?#P@vQ!C0;LD0nyAn=& zswnUHBKE8?F7r_ibw@`w2o%faZ$zPu^j!mO9Ioa)6VX*V+#wBTw7PyT>Mr*QXxe-v zmwYG`B7Sp-o3JIsK!oe})ID7J1LFuuw!wL&PC!3B?}hU}Lsp)tQf1SiVb~!G=RZ}c zi-e5DYFTqg^1hcPmWkjh`0kA4AyxSE^k)I!-lGjz!P=&7Gc94@YXCC6z*R+y+yLT^ zFs6kzNs#1nRb{}L#X+ATqw?qgiWxlLdr2EJ*7$FHPqt{kGkIGF8lta_g%+b2kz@+70o}eNA8D!4 zO$||y880u?DON8kt~mRfidLX~7vDFfrt$6bd`fO_?T^+&hGT1~X^iKot3?ha7#%qp z98o~s5FNdT;|wIUvmZT5x&mp)xEv0ILa7IJIO8vDXm^QKOkA4F5nrO&q6Aob4`*J& zy)ZLFFJoA-FgGkg?Fi2B}NK}-#>_jjvHo%#THkZd@ZSojC;RX^(}JZO33{K+slrXhEZ>C<*k5gUKcSFA zsSk26BY;5WFCjvV0S)}+LjLj~|BE8|A4wixOo8<-&a)J&eh+#Zak5V_CHL$I05}N= zOS{+X14=$$cXlw8p-M_hQh!~Kl@*5ib9tox7w2S%{y&(gL<>7Vk$N}RLNZjvLQ!2W z3mqK;rF-#>p#haPKl?Ad(QdH_p`qwxoZ4bQ3-s*rT7l7#@j~w$_qQWsqlL3$6?JVj zjyooFupT~6oX;=ML{kqB(uBmM5Xa77*)h_t+bqRoX{_EY6LZ&_mcUDh>^Ge+WN}Pj zn7HK>$NTPJa#FHsPEH)`fBL}xKim+M4f}^Ug8S%iCql@&;`ghOj0*>v1zK=n2xqFc z;P`5-wOrR@rYf=F=F;V=Q5?wg{TJi+?GT-CQ^^|WPzhQsD~&%UPtD%klnbBkY-@Tx z`B3d#)Z+~O+pEx;!b-a_2i;zlf7f&Q$sEvtZ_yp?DWLDgg}lxSlvZ+gu)KSf7pYuV z3uVZE>*Ds0>&a+oT_^m&>(V@LxLP|~)nSQ%FRq=l(e93GVIISts*riKu-AJHOa+%& zrHMASy-4KjK*k_AueJed2HN{7=@>F$&bh#G-MWw5c1sJJkI00)AE8G{hxU3~%fZ7~ z9L5SO&U604;8^GBd}=;0*cw+HQ~*>5(?hms6yrOWnuphpg?fiO(;`Vlv`<95gHn1v z&dtbvl1$((=U8BOIPZ73K%t16td0twDsdr7#L(2#5YPw9M5g-RX7Xn?1p%iQROH3t zzV7X611TA1*_1#Hh;sv#vD#QY+-z4f0KSQeqn)qoagoFVs{HN`VTN;ng({CU z@5h@_dm{7XzULaN&t%&5u7RZxTIy64u&zkLwuDSfsjI3(_0e*z@^agAep~KlGRbSI z6Q1e$g~k^F9B~j!eNvcRGAReIGcW|ssR3x^cwnD82$|k9-I&6WW=~Gs4$9N*l58^r z@^K4`wcH-ix}A!|vKn>_-kz@Q48)KzH2Cb1Hv8;ldOuu&!o$P!?an?gSvk+z3z#Qo zwAzQgBj!&BD}Y>S5=`Yds0l=)U!TSmS3Bf?)jf?90y15wZqFMj>~8kpC5=uF{3y_ z%yG_UnbL$-DKT4&Vrqbfc|(<5Fmvv&QuXDO=WG7z=j zGoUn>9gF#uj4rFmWiOK@+DES$mnQR|eW!4@e5FboPv-GD&5gsn`@P0$(CutlGs-8-dUSK>@)2g9Z5A9$DVF6=+E~rlHgD@vhv=aMNN`|KCQBz=1EG9&n@t>(&i06 zI(%+B>fCPQX6oKeJ0O`$nI&HR!O6S%Y2$VBjhY)~(Z7Kn)Q>m430C9sd79DQClZ9W z5kL;XXVt5LwdrveGm0BROHS1Ko}aB^COU4|GL;Q1%*!9{BxwgS3gBIyzZT6$eWz1s zR8r-p7$nb41xH5O3Gmo?FQ^&L!+G8&U#B&Y6InMh)eH{;s8r9KMn-DxaUx(L7ubdZ14?LG@Fb859m%Lil3u;-Cw}P29CRBGhODMGvNe-t+HVWRh7&pugAvUu-5Z2JpXQmoZ z@b6t@_WCpEphreGP!++|!1Q8Fcq^)Ky4acp)rY@kC@g-M*?ApvZ6?*p`4TJMPA=Jh zRR>X!8VRO75a>S0HuHxM2)Uc^I6tVj6&bvv*vbdhV3|KZd|48sgA95TR(oIb-;a+H zaX~~~nhuuRlZR?3h3z)fv^y=+Y%?r5_r$#w2Y75&au)G_NtvZib8b&19%2dd)^%)s zOV7Q`uqE6fSiEXY_@H;%!pnc3(=H9VaI8b!x+N6^r#j)z2U(>LDQ(Gj&yuui939=u zyJrzp;NfC22l#x`@H(&2s4;)0snaEieaQ3)gjtMYUb6=IJ{-Nttgz%71T0BziZ(8v zt6QkxhFoy{JpRzYbkcqY8`OXz3!M=vV0{cZ4xmd{tkHv#`yT7pR8#%k+o|7&Q@?uf zcsvD=rOWjt!0+~>!;~j%D%Be^TNF5Iw4>!Xa)3YEx_b(1!TphCNBo{wXj8MQU;(dH z$~rFc#-dOjk~tv3NQ#i5nF*~zDvL~XhV?g~gytydVi5t@oHxT@Yw+TDe!;cEo*HNB z?a<|gQ9n`S6+&$6YqkCBCoSL@H+fsFR3kBWW@hH=<&1j8I`q>FE$|4V`DE(?H~fD3|;doq=1rS|wD#6O**i63LB z@w6HkS|p`;%yX)Cwh@4iDS+I6b1vp@Im7)*Jn&QAd1!dJvFPaPMM6epI!q=t!E_4P zeHgdRxyN!4q++s{`OM*a;jHMG_)$D>n2{k-ufgf|mx~>DMf`T}wl0IjpOa~VIoAl; zi;h4UQ5m^;a|YW2_(^b?~ok%UaX1C#j3c z25XRIF+4h{CJ#ej^va{Xk^|XC!yB$TtnlSF@B616`Jcl{iHSdPwIy8A_1diS6B3hK z{higo$kYpOvVm4Vb;+HMPF9f3a6aMYmKvAN+4K(LGE7glzLGnC} zqNCf7h%~^0zO!glnEV}cFxE5t*>@4v&aPk}-=bw_liU4)3jw>iVE#Yq(TRmcXqAX! z_k(3fi=)I$G94#XPkk6$9JFf0U<+&Na1MuAbozX3igi7rZayNt`!r`A4~{JSg5MW3 z1p?6|Y#Sn1YyOs#RY0!kR)qbQ4o;2o+~ju1L_&^;3`yXJ4Uv&JF6?nYjiiml#CSk| zn^-t*XuN9I_n@e&VL8)V%*o!svD^I4h?xlz^jBSbk+W^_2#l836~FB)C6Hrd;i`55 zq0dR0zY~-mYKp~?sQF5S#VFzDWYVCaf2{^kG$$mBREf>faD04)K1 zV2G!WN1b^>J$hs|%70^^wvHw!wkb&R`YIEO?}>452cIc03d0;1C9; z5`P;Ju>(CW>AY=^`?rK(gjCo>)S2wx0;0GJYAO@!TtS!tq#`~A!ly_c4@b?zQCLzS zgNC*!5L5)ZUjgtGx=0jL#i0@&5#o z{>uB;H}HSvP%5Y?k+WTCre$|P=0|GwIE>!bWr~W9kG#WvN%>BP`F3uAk8O6@wnH)x%9C`-`mgz{Ea&QbL9cmq8o2m^~G zO{Efu`Rx#{vu$oLv{ng>l=IjW4U8cbDk%TkuV7+UuKH6{3ivrh-`pSIGV*^GN2t)y zhWxu$&O)0;2l;mZ{t*NN0|(pi^f@B72>bv(na%pKU$sXULhPR#TT(g<`WfPOuvbVf~tQ;(GB$v6*7oNUk6MD?Hp)h3V>^Fa%2*yc^0({Q;v-Q4PYGAZA1x=gj! z>8Dlwh)xO_tj5j{EX=xdL5)RCYlJk~t$nmU>88He^Rsn@;=q+JBJ&Nl&FjBHzIaZ1u4n4yiw@Yl?DsA zol+-P8fe*+)+>14peB7U!dGB4>iAy>UbC;m_n9F}#IItjCp2cN-NLZh?KNjb*!Ss< z;??y^ZD}Iu^3XR2>AsD1?abO&~>_XHyI3K1?{YB;bRW-7b`$vWN&Dia`UeoiN(}Gnk z9VTvJP}@`M*Gp$nC5Po#5udwxWPF61l}@4~Zny zE$I7kBJxGiNcbv1j_p92wFJ`kz={7TJyXK=B05>DYR?ugl9L3uY*^Us^tZCl{O1Sg z%D-ub{SCVE&)jg^tWfkwM>!37Py%x?L8ScNb$aL#vALUA=U2y+`>ERekn2M%a)Q)_ zuMamXk5j8F12mEZ1zQ*+;X@4jlZ>**GD|nhZ{)3O>R@ct!I8Hr4xr*6!oCM(SE!`l z{@E?yv1kW07`0_r$(n@@4>9(tsT8fY?y>4ys({s}rZjLl83nXP2VMjJf26*NX9|wV z;-`8>c$c`X+h{g?qhHdt`5cNKvhZ2<3~GXVKCQt>02ydz?a82k{b~9a_JXmCABnJM zo1phouWd72Xv{1lWL`$o6-`c}ir6d}I(Zw|9cSp|YCJy0>lvt02)Lf&tg*n{eGYlg zLkEdSF7MsaU-?jDwdmZD4_L4)j4Tq94N6+zT3@(KmUFCEH~{aInk|h9@q-N7yc2CR zCgMzX{)DL)Y{i<^-IDT_am;>^aE&|mdAe%01a;Woe_gpcY^2AakpV8C4L;oHdl4m= z);S#Akxldqbrm|HAGg&@t8!Bf0_J3DshjiiK2qbsdfsYPT6v3uCmjbbHXkMq5?z2m z8md++{Ap1?j0E{?=Pf9TD{=}EOapI24_-?nc~1H+@tCE|7sbSY{~F?-x}TZMj-?~V zyrsdBgi5Bl&N~F|2@zuyBFQFgzL&G~d*LeJxcd*rP#AELmB#ZG48zZz)A3$g6DOWTQF~*#^yABdoH(=M=Ch>XF~v>NAET5>N14 z*gtsTO^AOQhY2^wuq)V_o=MWqCPC{@l3|_m8)4^3n!Mj~97BwP-u#S~BZEV;Di_)1 zaF7fkyEt2+!Q<-qLd5|t5W=62!PZAX^wT?o)$xk4rh_GXAcG};U~+;^-{`O*i$O<$ z01YQNk`*nZ zu(7}bAi-xoQ&v^22um&Uwp4I>T3TVIXq)G=G`7lhA6%tCf!on);h1Id@9@W*5L==m zqgB5@-&YzK85n+heldAuHE7qQ(jkZQJ{aK>eAC4d1?_E-p`oEk&BxW+Q*wS=W8L=# zP_n@TNWB0|*oHzs*|S)q&E4ae2QFveFDlX2AKMnJ2AdLuF$L`C*|SPKxd2nZk{G(@ z486dXASM1>Io-k>Rzrig2vPU6A|Wy&cJ z00-~a?)|hu3PRojx*aZPMaf5d=rq4sTt9C8*!DW{FBGSj_(L_0 zy_CY^=I18;<7LsBIQoG)Mhw!0rytuX^X`eikFh3nTM%yIG^}_&H*w7{Y`JhqGP=0G zY*HVjpjrh{EHP?Usvi$1XO?&|bR%Ib(N|M${Eg1PK#6`AqjMHAm{*MS8N~x;U74<*T0q z`rJ@pXpD|M*y!o+1V=0V=%<(z}=tvQhgF6#?f#?3?nhVmTMkYvv=5<)1n&OWp5W)}7+E1n8X`)c-RryK)DVuMpjy4W}a-FX9C>mz#vnRz2>r{%7-4zX7r zX?&o!1hKIoqfiZPM(C5T>r)Qeg7N!!e(r)Zc(%Lp68Fw-HEbImN}BTti30sF0v{<( zB!F|AP+~Vt%8>*vj`)=g(F;WOKZ)i2cHdS%V9#M=G1Xs+89{b^xbf8Ji|L1Lls+5s z6Ux;n+pnGvU8pYCS3%a?TG3RW#{P@5HxGoe|M&mzq9~QENOs1SeaXJXgt3N7vQ_qM z*^M<@$bON1i9{u2H`YWL1~;;fB^hHMG4{dyuIaw-^ZA_fJ?H!To!@`MEZ6(mU(d($ z`FdY=>L$L4_*y$I1VWjc>2a`oC5u|cTV zw`yMn$Miz!ET6`o-*>z&6{>K!a=9*;{Q85-6pU|j0NK!OA5zGZC~5(+=CfePQ$|WE z8WVB`e9sI$^-=Oag+l}VHO~G~wRJv$RDTTJ{5{R$TVv=pw zH8i?A!0P{N{Mqu9OG&E7onk3{9eF>Mi;GL>U9WU`8uDx{ySbZj z+Ap=F>@>RhA}J#d4Wl9WiH1CAg`=aB48N%2E_{Q_$$4J}}^ ziFkU*iv*hw<*O^IrdZ_sr8Z%8tJR~}olMDd>n4=2sw4>YyJ75?0)P7WZ+ag5i+NE7 zdGfYK9o)QWS{XM=%J}j!td~q#vy&hcTLhxATOO>6x`r8{-9jF4F@75AuOjZSzWLM1-+v4O;|5PE#6n9LGIeWPN?Txioe$Kla* z(71@Lt)Jb6Z8KU0S(lpy$07pf>8jVe%ntrgV&;_lA;QeW>b(|pLgtze4W&p?hZZ63 z?Nc!krWJQeRI-^}A33I3wtc;KZ&Rj1fzWtZ*MtIWWwJs7YhHO*V>Ytg+SJz-m+SZL zvbrQdps3t^We1U_XEBJwy(Zcx6^W^m+i3saQbw3|e@AXyKr(y!w1HZElhDDAy zoNUSG93I-cv64rKGNzkX_Dug=?@MS;@D0b`J~-B?$WpOv{^C4`K=0qEsi`|mm~W;M zm1HmTNF2=Z;u;_xVdarYTL|JMy9NJgto226-~h& z>^#u)!>G<$DdJh|R%wnqAvf zl1-ITj)XO<;Jfw@DdLt`qMot3` zk$iQ}|FE??cNrHN?%h0#Vzik%zma0onL@9y z=+;IYGOu=y9N+6gZ)Wo4`7>mC5zMM_f@u`m5{HPo{EF7YF&E;K6OC0I?4YoZ5s(+N zjz~G-ejoUS!>5On_SV$F7tP1re7x_Q)>`nVD&`mNU`%G=n{mLgRY*h6HH(BucM&^7b7`5x4tZH6X;FuCh+ z?Y?{%`DPPWPW zQY%6w`cf@ZOk`{CUP@xbqS(;uu^zIw>ne1v!?1=9k<&s!UtyB(og5@q=AW#F(!@

w=QP$g^_34 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 literal 0 HcmV?d00001 diff --git a/doc/windowspecific/config-win-behavior.png b/doc/windowspecific/config-win-behavior.png new file mode 100644 index 0000000000000000000000000000000000000000..395827918a5b613cff8bade1148fe19a0e17181f GIT binary patch literal 36233 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�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

hbzXu{H)9jHL=^fy@w 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 k{(E~v!?*hy{yx?44^j~<& + + +]> + + +Window Rules + +&Lauri.Watts; &Lauri.Watts.mail; + + + Parts of this documentation was converted from the KDE UserBase KWin Rules page and updated by the &kde; Documentation team to Plasma 5.8. + + + + + +&FDLNotice; +2016-06-23 + Plasma 5.8 + +Here you can customize window settings specifically only for +some windows. + + +KDE +KControl +window settings +window placement +window size + + + +Window Specific Settings: Quick Start + +Here you can customize window settings specifically only for +some windows. + + +Please note that this configuration will not take effect if you +do not use &kwin; as your window manager. If you do use a different +window manager, please refer to its documentation for how to customize +window behavior. + + +Many of the settings you can configure here are those you can +configure on a global basis in the Window Behavior +&systemsettings; module, however some of them are even more detailed. + +They encompass geometry, placement, whether a window should be +kept above or below others, focus stealing prevention, and translucency +settings. + +You can access this module in two ways: from the titlebar of the +application you wish to configure, or from the &systemsettings;. If you +start it from within &systemsettings; you can use the +New... to create a window profile, and the +Detect Window Properties button on the resulting dialog to +partially fill in the required information for the application +you wish to configure. + +You can also at any time Modify... or +Delete any stored settings profile, and +reorder the list. Reordering the list using the Move Up +and Move Down buttons effects on how they are applied. + + + + + +Overview +&kwin; allows the end-user to define rules to alter an application's window attributes. + +For example, when an application is started, it can be forced to always run on Virtual Desktop 2. Or a defect in an application can be worked-around to force the window above others. + +Step-by-step examples are provided along with detailed information on using the &kwin; Rule Editor to specify Window Matching and Window Attributes. + + +Examples and Application Workaround +To see what's possible, detailed examples are provided which can also be used to model your own rules. + +A special page is to dedicated to address Application Workaround. + + +KWin Rule Editor +Invoking the KWin Rule Editor + + + + + + + + + + + + + +There are several ways to invoke the &kwin; Rule Editor. Below are two: + + +Right-click on the title-bar of any window, choose More ActionsWindow Manager Settings... and in the Configure window, select Window Rules or + + +System SettingsWindow BehaviorWindow Rules + +The main window is used to: + + +Affect rules with New..., Modify... and Delete +Share rules with others via Import and Export +Ensure desired rule evaluation using Move Up and Move Down + +Rule Evaluation +When an application starts (or the rules are modified), &kwin; evaluates the rules from the top of the list to the bottom. For all rules which match a window, the collective set of attributes are applied to the window, then the window is displayed. + +Should two or more matching rules enable the same attribute, the setting in the first rule in the list is used. + +You can tailor children windows for the application by placing the more restrictive rules first - see the Kopete and Kopete Chat Window example. + + + +Rule Editor + + + + + + + + + + + + + +The editor is composed of four tabs: + + +Window matching +Size & Position +Arrangement & Access +Appearance & Fixes + +As the name implies, Window matching is used to specify criteria to match one or more windows. The other three tabs are used to alter the attributes of the matching windows. + +Panels can also be affected. + +Window Matching +Each window rule has user specified Window Matching criteria. &kwin; uses the criteria to determine whether the rule is applicable for an application. + + +Window Attributes +Along with Window Matching criteria, each window rule has a set of Window Attributes. The attributes override the corresponding application's settings and are applied before the window is displayed by &kwin;. + + + + + +Window Matching + + + + + + + + + + + + + +The Window Matching tab is used to specify the criteria &kwin; uses to evaluate whether the rule is applicable for a given window. + +Zero (match any window) or more of the following may be specified: + + +Window class (application) - match the class. +Match whole window class - include matching the secondary class. + + +Window role - restrict the match to the function of the window (⪚ a main window, a chat window, &etc;) +Window types - restrict the match to the type of window: Normal Window, Dialog Window, &etc; +Window title - restrict the match to the title of the window. +Machine (hostname) - restrict the match to the host name associated with the window. + +While it's possible to manually enter the above information, the preferred method is to use the Detect Window Properties button. + +For each field, the following operators can be applied against the field value: + + +Unimportant - ignore the field. +Exact Match +Substring Match + +Both Exact Match and Substring Match implement case insensitive matching. For example, AB matches the string AB, ab, Ab and aB. + + +Regular Expression - Qt's regular expressions are implemented - see pattern matching using regular expressions. + +Detect Window Properties + + + + + + + + + + + + + +The Detect Window Properties function simplifies the process of entering the matching-criteria. + + +For the application you'd like to create a rule, start the application. +Next, in the Window matching tab, set the number of seconds of delay before the Detect Window Properties function starts. The default is zero seconds. +Click on Detect Window Properties and +When the mouse-cursor turns to cross-hairs, place it inside the application window (not the title bar) and left-click. +A new window is presented with information about the selected window. Select the desired fields: +Secondary class name - some applications have a secondary class name. This value can be used to restrict windows by this value. +Window role +Window type +Window title + + + +Click the OK button to back-fill the Window Matching criteria. + +By using a combination of the information, a rule can apply to an entire application (by Class) or a to a specific window Type within the Class - say a Toolbar. + + + +Window Attributes + + + + + + + + + + + + + +The attributes which can be set are grouped by function in three tabs: + + +Size & Position +Arrangement & Access +Appearance & Fixes + +Each attribute has a set of parameters which determines its disposition. + +Parameters +Each attribute, minimally, accepts one of the following parameters. Additional, attribute-specific arguments are listed within each attribute definition. + + + Do Not Affect + + Ensure a subsequent rule, which matches the window, does not affect the attribute. + + + Apply Initially + + Start the window with the attribute and allow it to be changed at run-time. + + + Remember + + Use the attribute setting as defined in the rule and if changed at run-time, save and use the new value instead. + + + Force + + The setting cannot be changed at run-time. + + + Apply Now, Force Temporarily + + Apply/Force the setting once and unset the attribute.The difference between the two is at run-time, Apply Now allows the attribute to be changed and Force Temporarily prohibits it to be altered until all affected windows exit. + + + +For Apply Now, if the rule has no other attributes set, the rule is deleted after evaluation whereas Force Temporarily, the rule is deleted after the last affected window terminates. + + +Attributes +The Detect Window Properties button back-fills attribute-specific values - for more information see Window Matching. For example the height and width values of the Size attribute is set to the height and width of the detected window. + +Yes/No arguments are used to toggle on or off attributes. Leniency with grammar helps one understand how a setting will be processed. For example, the attribute Skip taskbar, when set to No means do not skip the taskbar. In other words, show the window in the taskbar. + +Size & Position + + Position + + Position the window's upper left corner at the specified x,y coordinate. + + + +&kwin;'s origin, (0,0), is the upper left of the desktop. + + + Size + + The width and height of the window. + + + Maximized horizontally, Maximized vertically + + These attributes are used to toggle the maximum horizontal/minimum horizontal window attribute. + + + Desktop, Activity, Screen + + Place the window on the specified (Virtual) Desktop, Activity or Screen. Use All Desktops to place the window on all Virtual Desktops. + + + Fullscreen, Minimized, Shaded + + Toggle the Fullscreen, Minimize and Shading window attribute. For example, a window can be started Minimized or if it is started Minimized, it can be forced to not. + + + +Maximized attribute is emulated by using both Maximized horizontally and Maximized vertically or Initial placement with the Maximizing argument. + + + Initial placement + + Override the global window placement strategy with one of the following: + +Default - use the global window placement strategy. +No Placement - top-left corner. +Minimal Overlapping - place where no other window exists. +Maximized - start the window maximized. +Cascaded - staircase-by-title. +Centered - center of the desktop. +Random +In Top-Left Corner +Under Mouse +On Main Window - restrict placement of a child window to the boundaries of the parent window. + + + + + Ignore requested geometry + + Toggle whether to accept or ignore the window's requested geometry position. To avoid conflicts between the default placement strategy and the window's request, the placement strategy is ignored when the window's request is accepted. + + + Minimum size, Maximum size + + The minimum and maximum size allowed for the window. + + + Obey geometry restrictions + + Toggle whether to adhere to the window's requested aspect ratio or base increment.In order to understand this attribute, some background is required. Briefly, windows must request from the Window Manager, a base increment: the minimum number of height X width pixels per re-size request. Typically, it's 1x1. Other windows though, for example terminal emulators or editors, use fixed-fonts and request their base-increment according to the size of one character. + + + + +Arrangement & Access + + Keep above, Keep below + + Toggle whether to keep the window above/below all others. + + + Autogroup with identical + + Toggle the grouping (commonly known as tabbing) of windows. + + + Autogroup in foreground + + Toggle whether to make the window active when it is added to the current Autogroup. + + + Autogroup by ID + + Create a group via a user-defined ID. More than one rule can share the same ID to allow for seemingly unrelated windows to be grouped. + + + + Skip taskbar + + Toggle whether to display the window in the taskbar. + + + Skip pager + + Toggle whether to display the window in pager. + + + + + + + + + + + + + + + + + + Skip switcher + + Toggle whether to display the window in the ALT+TAB list. + + + Shortcut + + Assign a shortcut to the window. When Edit... is clicked, additional instructions are presented. + + + + +Appearance & Fixes + + No titlebar and frame + + Toggle whether to display the titlebar and frame around the window. + + + Titlebar color scheme + + Select a color scheme for the titlebar of the window. + + + Active/Inactive opacity + + When the window is active/inactive, set its opacity to the percentage specified. + + + +Active/Inactive opacity can only be affected when Desktop Effects are enabled. + + + Focus stealing prevention + + When a window wants focus, control on a scale (from None to Extreme) whether to honor the request and place above all other windows, or ignore its request (potentially leaving the window behind other windows): + +None - Always grant focus to the window. +Low +Normal +High +Extreme - The window's focus request is denied. Focus is only granted by explicitly requesting via the mousing. + + + + + +See Accept focus to make a window read-only - not accept any keyboard input. + + + Accept focus + + Toggle whether the window accepts keyboard input. Make the window read-only. + + + Ignore global shortcuts + + Toggle whether to ignore global shortcuts (as defined by System SettingsShortcuts and GesturesGlobal Shortcuts or by running kcmshell5 keys in konsole) while the window is active. + + + Closeable + + Toggle whether to display the Close button on the title bar. + + + +A terminal window may still be closed by the end user by ending the shell session however using Accept focus to disable keyboard input will make it more difficult to close the window. + + + Window type + + Change the window to another type and inherit the characteristics of that window: + +Normal Window +Dialog Window +Utility Window +Dock (panel) +Toolbar +Torn-Off Menu +Splash Screen +Desktop +Standalone Menubar + + + + + +Use with care because unwanted results may be introduced. For example, a Splash Screen is a automatically closed by &kwin; when clicked. + + + Block compositing + + Toggle whether to disable compositing while the window exists. If compositing is enabled and the rule specifies to disable compositing, while any matching window exists, compositing will be disabled. Compositing is re-enabled when the last matching window terminates. + + + + + + +Examples +The first example details all the necessary steps to create the rules. In order to keep this page a manageable size, subsequent examples only list steps specific to the example. + +The Pager attribute refers to the Virtual Desktop Manager: + + + + + + + + + + + + +Pin a Window to a Desktop and set other Attributes +Pin &akregator; to Virtual Desktop 2. Additionally, start the application with a preferred size and position. For each attribute, use the Apply Initially parameter so it can be overridden at run-time. + +The &kwin; rule is created as follows: + + +Start &akregator; on desktop two, size and position it to suit: + + + + + + + + + + + +Right-click on the titlebar and select More ActionsWindow Manager Settings...: + + + + + + + + + + + +Select the Window Rules in the left column and click on New...: + + + + + + + + + + + +The Edit Window-Specific Settings window is displayed. Window matching is the default tab: + + + + + + + + + + + +Click Detect Window Properties with 0s delay the cursor immediately turns into cross-hairs. Click (anywhere) inside the &akregator; window (but not the title bar). The window criteria are presented. Match only by primary class name so leave the check boxes unchecked - for additional information see window matching: + + + + + + + + + + + +Clicking OK the previous window back-fills the results in the Window Matching tab. Enter a meaningful text in the Description field (which is displayed in the KWin Rule window): + + + + + + + + + + + +Enable the window attributes: Position, Size and Desktop. The initial values are set by Detect Window Properties and can be overridden: + + + + + + + + + + + +Clicking OK in the previous window returns to the main KWin Rules. The new rule with its description is listed: + + + + + + + + + + + +Click OK to close the window. +Done. + + +Application on all Desktops and Handle One Child Window Uniquely +Except for conversation windows, display &kopete; and its children windows on all desktops and skip the systray and pager. For children conversation windows, treat them as the parent window except show them in systray. + +For each attribute, use the Force parameter so it can not be overridden. + +In order to implement the above, two rules need to be created: + + +A rule for Kopete Chat and +A rule for &kopete; + +The Kopete Chat rule's matching-criteria is more restrictive than the Kopete rule as it needs to match a specific Window Role: the chat window. Due to rule evaluation processing, the Kopete Chat rule must precede the &kopete; rule in the KWin Rule list for Kopete. + +Kopete Chat Rule +Assuming a Kopete Chat window is open: + + +Use Detect Window Properties and select the Kopete Chat window. Check the Window role box to restrict the criteria to chat windows - for additional information see window matching: + + + + + + + + + + + +Clicking OK in the previous window back-fills the results in the Window Matching tab. Enter a meaningful text in the Description box: + + + + + + + + + + + +Enable the following attributes: + + + + + + + + + + + +Click through to complete entry of the rule. + +The Skip taskbar attribute is set to No to display the window in the taskbar which loosely translates to: no do not skip taskbar . + + +Kopete Rule +Assuming &kopete; is open: + + +Use Detect Window Properties and select the &kopete; window. Match only by primary class name so leave the check boxes unchecked - for additional information see window matching: + + + + + + + + + + + +Clicking OK in the previous window back-fills the results in the Window Matching tab. Enter a meaningful text in the Description box: + + + + + + + + + + + +Enable the following attributes: + + + + + + + + + + + +Click through to complete entry of the rule. + + +Kopete KWin Rule List +As mentioned, due to rule evaluation processing, the Kopete Chat rule must precede the &kopete; rule: + + + + + + + + + + + + + + +Suppress a Window from showing on Pager +KNotes currently does not allow for its notes to skip the pager however a rule easily solves this shortcoming. + +Assuming a sticky note' window is available: + + +Use Detect Window Properties and select any sticky note window. Match only by primary class name so leave the check boxes unchecked - for additional information see window matching: + + + + + + + + + + + +Clicking OK in the previous window back-fills the results in the Window Matching tab. Enter a meaningful text in the Description box: + + + + + + + + + + + +Enable the Skip Pager attribute with the Force parameter: + + + + + + + + + + + +Click through to complete entry of the rule. + + +Force a Window to the Top +To pop an active window to the top, set its Focus stealing prevention attribute to None, typically, in conjunction with the Force parameter: + + + + + + + + + + + + + +Multiple Rules per Application +Thunderbird has several different child windows. This example: + + +Pin Thunderbird's main window on Virtual Desktop 1 with a specific size and location on the desktop. +Allow the Thunderbird composer window to reside on any desktop and when activated, force focus and pop it to the top of all windows. +Pop the Thunderbird reminder to the top and do not give it focus so it isn't inadvertently dismissed. + +Each rule's matching criteria is sufficiently restrictive so their order within the main &kwin; window is not important to affect rule evaluation. + +Thunderbird - Main +Assuming the Thunderbird Main window is open, sized and position to suit: + + +Use Detect Window Properties and select the Thunderbird Main window. Check the Window role box to restrict the criteria to the main window - for additional information see window matching: + + + + + + + + + + + +Clicking OK in the previous window back-fills the results in the Window Matching tab. Enter a meaningful text in the Description box: + + + + + + + + + + + +Enable the following attributes: + + + + + + + + + + + +Click through to complete entry of the rule. + + +Thunderbird - Composer +Assuming a Thunderbird Composer window is open: + + +Use Detect Window Properties and select the Thunderbird Compose window. Check the Window role and Window type boxes to restrict the criteria to composition windows - for additional information see window matching: + + + + + + + + + + + +Clicking OK in the previous window back-fills the results in the Window Matching tab. Enter a meaningful text in the Description box: + + + + + + + + + + + +Enable the following attributes: + + + + + + + + + + + +Click through to complete entry of the rule. + + +Thunderbird - Reminder +Assuming a Thunderbird Reminder window is open: + + +Use Detect Window Properties and select the Thunderbird Reminder window. Check the Secondary class name and Window Type boxes to restrict the criteria to reminder windows - for additional information see window matching: + + + + + + + + + + + +Clicking OK in the previous window back-fills the results in the Window Matching tab. Enter a meaningful text in the Description box: + + + + + + + + + + + +Enable the following attributes: + + + + + + + + + + + +Click through to complete entry of the rule. + + + + + +Application Workarounds +Below are Workarounds for misbehaving applications. + +If you are unfamiliar with creating &kwin; Rules, see this detailed example to base your new rule. + +Full-screen Re-size Error +&Emacs; and gVim, when maximized (full-screen mode) and under certain conditions may encounter window re-sizing issues - see Emacs window resizes ... A &kwin; Rule will work-around the issue. + +Assuming an &Emacs; window is open: + + +Use Detect Window Properties and select the &Emacs; window. Match only by primary class name so leave the check boxes unchecked - for additional information see window matching + + + + + + + + + + + +Clicking OK in the previous window back-fills the results in the Window Matching tab. Enter a meaningful text in the Description text box: + + + + + + + + + + + +Ignore &Emacs;'s full-screen request by enabling the Obey geometry restrictions attribute, toggling it to off (No) to ignore and selecting the Force parameter: + + + + + + + + + + + +Click through to complete entry of the rule. + + + + + + +Credits and License + +Documentation Copyright see the UserBase + KWin Rules page history + +&underFDL; + +&documentation.index; + diff --git a/doc/windowspecific/knotes-attribute.png b/doc/windowspecific/knotes-attribute.png new file mode 100644 index 0000000000000000000000000000000000000000..eeaf3ae931da1663b5e7e1c1545118ab8a003f94 GIT binary patch literal 35453 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 literal 0 HcmV?d00001 diff --git a/doc/windowspecific/knotes-info.png b/doc/windowspecific/knotes-info.png new file mode 100644 index 0000000000000000000000000000000000000000..4769a1d9b243eefea0427e107a5dcdcc1bba8021 GIT binary patch literal 21435 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 Rfu@`gT}?xcI`zAc{{ya-C1d~q literal 0 HcmV?d00001 diff --git a/doc/windowspecific/kopete-attribute-2.png b/doc/windowspecific/kopete-attribute-2.png new file mode 100644 index 0000000000000000000000000000000000000000..50f8ff76e52a5303220662ec8c113049c1f64434 GIT binary patch literal 49387 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 us*kC{Jwa$5!rkB>nbeg_-PNlnU@9L zntJPU$2}Ifc4I7_8>_LGBh)wX4UJ9g9ri$)QYcQMM~0D48S=aj2M6b{!xM)D8N9 ziQ=;48OP;0F!0|EwSRYP^ZxyDG@7IGs8ZMAIVLVClX>T@b#GuoV$9t&u^1*MCdITw ze6WN@2ZEfyRW*DuXo#T9-9&w3^l?TNrlfZK*;-H%3@mr^y*$izb9X<<5Wc4%@Fj%b zlNimue)a0goylo;U!8;|HfP;b+UYA8errp}7OIlu?aT@EA`}-VXJskZKNwdh&fU>i zYHw#`mP#gPSx1lUyQ*FywKR=K2)6vp|8vQYcoZ~m)>~L%JsvUTknlF;Q8%6JtJ*aT zi%`!w=@KPz^ulw>es}iH&qAQw$F=JKH@@=U(75E=zZm3td{$C*D`$}Ly15JMVza*Z z^8%AXSFddGOHZNI*G>s7_$k!X8Wn3G1zO|~7aZgCYt+_Vq!Qmur~|I0BjPVVpCdg6 zex7F*e0M4RqEniBDoFF)e9=mMiK2MEzUJ07vt$mgoovzujn}*Sc@!|$VC%=8_bM>I zic~jnc;)c$X~*I>IW9NRdZVx=+Xx-~-aWpCZ6b#eb>}}|fs02d7!{-Ox7x2ZIEom)%5BEL(qM>*hGs{#$QcVkLwkP^NzXJO9DjR- zittppfXWk|8?i(EPz8Oddi58msA4<311ha>`W4D&i-s0h5ONbyGAp#R*07E$vED0v zPejFyxOc^0{%1`#m){0}GEd+WugpS?hL$#CEBBv~tQ_(Q5UPh(=V@7rKs2;hziuaA z!_d(JKk-XKx-ZoKS^47n@96&WI_AGKZ#AD7RIX|NGgsi}^UsP*gFozFyHo0+=cpnh z>UtSKo6kMlg6}bhaf`ozaf3%b-G|F1%##d?!_ES()#FO-)zPuF)xn0dW9jbmpPLHx zZW|c~vE|)Dk+GykvI%3%L)+6{iL`nT6wN`*Fj2q15U_2J63M_!?i2v0TaL0X5FjCuDC05#h`4|!EDwFMnd z-!h8s?FO#*&0;~;*V|70l=Re}rRtat$g^>9KLy*Q_qR6PF&dQZ39>k30?h&;vKRmY zF=1d$wY>V{S2RR07D5a`i3K}*qWpesg_%ZqV6gk1{nPBi(wYUic2)f?==q+JIH8~F z@Zcg%f#|ua zr2f3v%d*^bqd;*I9sNT|-K4)&aCvW?1Dh+wT2H9s@#)#rnBiCOa-;RXzrRd16tAnm3NK8#x?K(^=K;mVsfX7FaT3Rw zlCPW-4%Z+_a{Bw`79&m!=f!t&o-?K=p#*SklSqPulxT=={8UBFZ*+ccJKDJdP2}ic z?~~61AME*XCxo#9sbBP%0L+`uWKK73>%&U(e|>j-raLWMSz8%9oZ~;p%blIB7H*~c z<44)nqT%C;Q};mE&_MisOG{IH_@u`lA8s?y>Kb2ONyrm^;iJ#eku1{8I zsQN7>{4f$S5?u-Lbl=T%Ob92$rt8nkjr7re&~%35N{P4O&Ev3n{ron7mY$TEN(j)K z5$v2@2tOGrr5hbvy!E3mFN=E1W&7h#VVTfvE^$ilr+KOubkbJj?lN!te-;0<7BiR= z&#w!JMCY|2pxLH2`dU^QHjs1=P^QGZW)Uw>h}Hh~HLqCNM7P$;*!(4pnW>5OgXY9I zJKdGYHIsx`4zC70f>(Py#H)N=9HaC*pSvcVu|$HAhTLt=Q&8b7O4sx|n!f>!^jn^OAGz72@NFF| zJxJ$97k~KWpRaS6$bWHU-daY9?`{SX27KErVa* zRlxnd@blzJOG{hpjpt8)tlwzQq=;F13?J?vrG-MkZT6pO28cd)BafN~E`9HZ>hM%X z{q2SiAJDhP=>JZK43m!sJ!x$Tk?+aO%CT(q$t(N4{^V_c-1S*eZa|Oxb5#t)Ag=(I zFLx#st4)0Cv~*PRre7(May&1~v)vMDSn_=-SoTsUjwfVN z%k%O6*G3xd?zUzs<8`|{@ulA{A>EA4$@HVUpzDY8$=nhD9ovv|o7QeWPgCER;$(&k zF0-h7*I&~P-)-lK-(hTMn$0bZKzH_%?eC3WG_9#gSjL%1+xx*`D^+CHPMANza+1mY)n&$4L{BeDSXQIfKk9h7K{K-n(NtdpP-`sP8lttDZ`g`2X^9}*@BVJjc1+heq z-@Lx9uHOjsf^zU3yoH&E;6;eOJ6d4%37o6!Cp4GeqP{Iv=EYXQ2v0V?sO!fb9t>6$ zCTkhpd6tSl)uM>WBIgx@d5u8LHJyv0m!F-{;%slr_}WjHA{caHe>ZHIb$Mfw{_d@m zPZ-3{y!C%PtqV{Aim>A%Xf0hbXmHde&jwezQ%%ccIC~urF(ESFY=icj4jbxjptsqc zIZSRLL$}Hs>df?T4?jt>;ZtlzWy{pdGQW$~OxxTzI zzzPb^r^#hdfuXPj{Pz+JNhP$to!}+2psWyL$-F6%?MKlqlUH8X$$CF8BrdwhHLZEk zxA04hbN7#EAUv%3J9o{{XIP2%CZ5aMQp-b65_ ztI;`6R&D~JY6p#j*P>^V7;3e#EtZ&ao`c^A_=*o#g@1ZeUAX;DIcR4v0Z;~M5@Q8v z0u*Lnv~=H3d=NjBOaRxsQJS|3U^O%uZIDUOxHXmT@D z(N~K%KMN5ivpgzzRK9%W+A}8!GVLtDMJ&_tfHtcG)!Qm@(iq#&(LakyiOOqIe>42r zyS+X_UgNL;XUW@rZ;<~^kyi$8U8>*WUv#Tij_lIsuZ`hmL8C>Fp~`>tR6j&fKHto_ z*&CUY)-IyPxU1MB5EEcfVMl1q1G3uR2m^k1Z`b0SGu-c$v&1!ipLpuS4T@kCT?zs7 z^QN32TF3yo?!(uoha%*?S!5}!yB14*Cp^m*5AVeT^({C9`?JGkiaFV-ZLuJL>El#d zh}7`4OGT~4OvS>u;-C??SB_cR`vkkJiWyDH4ge#+A8ivlg39n0KYAJ(M1~U=v01W5 zyY=<;q0P%4OfQuGU?@Wg!l!EjjVC3qCnsEG;ND-h&~4e#GU~QY*mDObl=}GMv>zUy zKVRFroZL@On-aC}2+z%4f6nMFWx}C?3eX)wW+}fA?qj&ST>ie%dE9R_}N{h>+kiCf@k93&35-(hp|)>ZpoyWFpEg$Dds?y|Ow0V}!EWLSrx#W;U~YQ#K45y7f(g&-&1cWGaYY1Rso_adT`+OIzYKW^-7Mz5q}a|K&?AVWAcP^BX>kYcWOk07_i+FgygNTUUs4r3$y$1p34{<9Ewh4% zWhk+2@w3Kp?}7dhQbuZvY%`J=MM+Xac*-y~NGNWcdm$f<<3XqIVYo( z&+!9EK&SJw71iSx2e7wV7j=Rlq4MB#>CJj_gwFDJkqXS3%laJqNarh!483v1CBHqM zbFlCuMBu%@`oy&^v}>g*ez;OLUh1O$ESV5-plo(ak-*cpLkOt4Cj0VozqCE+q1TV{ zYW)(eUDlLpBH31W{T$7$B5O1Hn&zU@A1&Z_PX(u46aUwNo@zuHS0H01Rc&U;wkgfx zm!H~Kxnszhr+_5=Wf`_y6&Y8Uw~ZMJrp8 z|744_UH2^pt`3}L7D2)J1RX}nEiO7+AaFDDS@4lBfwrMHOVz||kc5+ac3#4hKWlXK zPlL-9CXF)>f-0$_A?8{u4RDjCSn}kn>jNQ zaf`ta3u4Fz>3mQrp2t%VThWVztf~Q(bAVd8k&^cfW3n{NzBodhsps)>e1*8C?|;~*2rJ(P4UsJ z<=$`tMT^5!(Tcz97i4&Oa?-Z#obA*$nypVisL&iVex>xyEPXNlyir(1m_}Dj$#Leh zl=(>u%OiY5L)7X*!wk*e!-4bPe!oLuM#l-lfo!}p5E^(MGx7B}rHm3Cw&Cv`POCNY z8_!iu#wCUPg8I64cOzwP7NbJ3l?IK=liy_l4U#DcpkF(o7gkqsB@8mXH6#1@p1vY^J_P&a{L77=yGZ`X z&U9+C9`VyQ9zI}S4dL_f$iYr@mq>_x{qZ^wBGlc>54|G2z)v#0~Q`jAMv zD>}yCZ*1(>0-Ir5edN{EFE`BV0wFX5+wBB=Enjj|F@gloEW@W6=h)Y`wIpm^d{XX^ z!Ry}%d<}G5+P22mXaT*SV?4!EGVNHF@?_w?=vS2po55mQQ<~|(4qe7MHbvR&%PGH~ zvBU(;Q=ILR$4Xl;hj;hc+C82r&VLQ0?4Ctw@H)H;JWm{`Yd=rC&8eL)Mv2mEK;^0*Z>#)a`z zDQ)+*1+E@CP2pqX4l8-6DNVpu>}=XNN1X3H0SH`Wl}XJ`{>av{v-9CX_B*$f>-)#t zNrcb2#_$<@EE+*1A6b0o7lsU?Lw9VKdyJK%&>pDo!-CgmR}Z{gF)@{cJ{DO^)@zYaS(yC@Gb{*yTI1k zN1ck9rvRi~L`QA4GO-e!uSCkr$HuTBU)|_I?4wtgl$fyRXf*OR0-4UQN$g+PVZ)&8 z=_(a%nG+H(Ev>clM43$F0=BFCYbNhPlH3S9rl06~FTEuA%6k0T`n%wr#q~>{Hy&=n z%L8a4=6&bATVq%^4a09gJRO8#`;u~35se1s-VV|M2yQ1KGrwym0$hMOIg1p4-k+q8 zn0>dl1y0{Rcy9WnlF+p{NOXfM($(X7aEfKw*7*|lCU|b(?*@dG{w=8rp9?zBcJJE? z=-qc7*XTzH({4_K*F0tZ6;`OPdx>s=%#Zc?4C%u2bV~}Ugh^X(?t}YoVYSYGgzU2*Z%yLrD zbfU0~evgG^{U#lgDM$uSTTw1tjeUo3aR-nsN{VxjIbGbu`;(Vz`E6t4Y@*xMos*gO zPhEGN((mw_!=fC+#c?@&v<zT5KzW4ctArV0Z$6z!LV@=o17mh- zF6YX_;~G06>#m-#UJj2blvK{mz2Iv8-nk#J%nqjbugU3^qS5HS3|94Mrp=dcdo9SfLy^It0v>=89w%6}H|zPX_K^q*09dSSs+$dwGF^uM)-86U~vO#!4iz{KIC7-Vlf3RN5*zv42@ zi)$BhPMd2jE;5zzFolkW_!`8J2FHAGd<^81bAon}M}b{h2$){0!vUgQ912z9y1Or)E+nj9_BK@mv*~^}XI^Y4VYE*L+s&=HFu{-E*BtQ1zJG<;5)% z){GJd*lNB$T)(PVLdQNX`Ad2}`&-pFQ=H0;G^&s7Xh;a>+_Et0_j5t8->|9=Nuo{_ zeT2kBmUp-&x7|Lj51rU4<)F}i-hfP^+w41eN^WlljJo(PnUW*vISdeQQnBE+`dnHY zUZ}4dm%Mt*7McLCmWG5;U&*!A7S*MS_@YLdBV`ua_YOvm_+Bhx&`sCCU-zplIt>K< z!dLa-8kpaT|IH=(D+tE7QDb1%R<~c8Y&FzK`-cmStl05YaZW2&+_YEITvK_w9kL-4 zxpC15gFmTSG+e6mjki$-7?ql-#_ zh!9i-ATEDC^0)xcZ488|Wb<0$sig>VG_%c(@qhA7JokKg87Uk*?`XysB41=cuXJ}i z{X#wMj|IEmH=d8nE2FC{R@R56Z-o@^%oIy6ky?MaNKn%QTmLpuGz2A)6j}{^27b7U zKJU_d0$V>cHA{emrij=e%&Na3_ABA#w-=NdW$#G#1cJ-k*<7PzYHL{~?Bf9BMQiKD zFI=zsDyxynI$5u8pAaYTTg_qaecIh!v;-D5U9OWw^!hrGL;I}!x?c%QB-r#}}Wg5-a zlGRd;=^jL={>|NfG#@fJ+H1MdDG@PuoP2$7xvYd3rscnsy84mMZ=KUpUf%jJ&4S=s zx*e6cm^2q~-=r0vF!B1)XTP=Zcz;c4b!bm8^q0m_XoTB)sk*xRpw^e(tLv_QL(e=F zm)^V6@A9{LS8eEE+Zny<@BPf7Kh|dJ6l{ClmMQCH{}5e=_rDww1C|`wJi+XHv;`dR&ptb1R0j~O<{d%sc*msTR zt=1*{$C0dscdZSk3D9YISKG{cPh=}sY8&4M$wM#Rl5qda@wYCj-=&T2BA{Vc%r)=t9~!WyFi^F2@KWMMrg?LEeaOl3gR>+_K|M^uepA)K zw=%h=!Ka_d;dVaje6vF-_jPd&{cY^03E|~AtvHFtt24f=eBZ2;X(89Cnu&cl4F378 zfc>=bE#inkw@v!quGIVLq-ajfSn(6fSPXzVLB9WaB-vYqaZYVU-zHmi`pR;^#Pryd zy@$cLmZr`D3nJ2p@Y=0ArNQUmq84kQ4|C4&kEZNTR~{Q?Ue5CBvbD7w4}}mFApfzi z3_LOjjz5fk;tnB81nch^uw|M;vN4gY;@Xq1ilpoC+4^m;=B`_?V30yNg0uFs;|AC1 zdL0KbF>$5E&B5b}^SW6}pW7RCy|c zMYJWLYd-4i2s`o$M-UJ=lBV*sBQYQzkVN<)##+26*Y{#0vvAXDJ}gb*->w_Ump-|b z+Iq80Pk6mIr7dx02ODFxQ1KhM6PKhmSz4grZ5&BnM^Y@Q?#m#sfNEJOqGt~5Mr&_R zGacL3Yy!3`i%8#_uV<=MBYVC|-lkmZdK`z|hr3O=PmuZP{V?;o78Eya{bO=A#{S6W z0hEhuvBFFzg)OWp%dW~|h0gxgw56SOL_FcAZf^V-Qbea2_4@Tw-;XhZNNUMHAGHTL zE8kz|0oWsphiq`SFZ`u6C=8HLi}G%(!sJ1xW{H!=bO1gGH^r zQ{`cn+Yeqj%cxZlubIMw7cD+6uo0if#>UXx{q3@7X8QrqURE`+rveuaOCIol+GW0o z$Z5I8R&xo0pa$%>Wh?DyBg!+mn9r*y!=1fXo58pSg85 zk-UD(;GZ>%UQ8#bJ&=iO`7~bQgpk?>*bR}O1+xw8&H3+e4J?!h8d&2TC$c^VMOGb9 z?zOpe

};Qy)|~Jaozs$Ee2{pZfaq2$pY^s0_5)1- zxVP{d*vf~=N4;g9^%T$ML?4JdMSMRbVEp&gEn{-_1*T_VrEr@wqKEpQHH75u-WXigy_2HLt zCgr#tUCSl-MwCdTTWX-JNpVzsVEj|l1-cFbz$C4_8N8Vp8P@K1NNzkq*Za(=ZQOLd zBAgj4)#)t3x51*zwSDgjjxB&$VU<+yMT)U2jXG>M)^?5B@EC+pC%bOaYAZRLal-== z^qLBYX{ib_tRfk2G~VQWo-#)!%BYWIi~zc7QYEQ1saJbxl&5!!5L)A`v=w600{E$6D0Y+s5n9QM5blh8sG zq41)=gS(9;6p@;mhiI~LI7CxNGalzl4^Msg_qyT@rhk)=ni~Z?*{@m%zM4Occ#2=s z!S2P6n!6bNBVDmi=wCgG{*gaCiu00#e1GFng!+ij3+KZOh-43BA}^CHShO@{#}mvD z!SQwjK4F&#f=v>k^wcO7NKd^U`$KxF{Rx{T_GQRXBnhL{dP{sVJuo--+30Vs`qfKW za?O;L?zcbGZ>F5;vX5*DdV10b8PB`W6d_MlD!pLP$J?LlTPbw%F->Y8fMHeQK(JC^ zzf_0|<#7~~`chE$N6>?Igxp5_Yc6r+pln}- z+awDI9+B$lt5c}P25o{d-!J2HZ^mCqKX{r(c>~}^P*+RtA4>+$^5R`6}eg{1D!5&SAr7^T@J5C4%?=7bSDdPafDJe-O6*&K7wD z*!p{0q!!h8TjY2IG%tW`k^hcc`zxW`12|L{B7uY zuL5k8BJ}9L{+eIT%3Y2lXXUA#SGvCt5I@Y7W5Gw}O3Bo|)gLnL?<6iSuH2t)&tEuP zamKj5(W}}CEi;iy0$hGBOA*PZEqQ+oHLz+Ws4Ru$Aog$l)D@vRfE~+euYq^~20&D> z6G>9{su#o81(sRCpNGc-Z?#Q@MoZDjkwDG;or(+DazRT0c{ic&ZZiU_=JzchQ2CQ3 z{P%jI?V^g;%|J7Y>%274{7>yxch?k0-o9#Yo;YX(TZ`6Dp`_9^Hl6cTfqo6PU=T!x zJ{qYj#a8b0PdCrIFD^VA)wv_5j!;Yg@Aw*7-Yq-p9iM@)FOY#kSqSf`K)q`&iU);v zBzr0$koeF9jWPr%Pj<{1C6#!*(KO^|JGqeE?R>OGxzw=mXJKdCN9C&EH_H5-E6VB; zzN*=BQnsH~pmn?HUkvc1xFFt6d4MMAI$~z^azZ{02sbuhp!s0X_*wmgH{X29sr6-M z2A;jE-oPnX23SLlZ^7^HIS$Z%zpI_Uc34cBo5cQ}A<7=FmD-6l;>1v&Hgt1u(t?Mu z8uT!e=!-9GZpO=LTelmTO{c@XgI*u@G568S+Wr8rXn3uw(}}kD(*dRE z*Ug>s$?D}#)x%fHUD}8JzqM0VL-U-~ljEPk-iyBOLX!Q@qxFaD%QisbPD-kL)(VnO z{-0ZiJed8eKLfFB!q-RFTrm_vzyUpv1Q=&?Bs-O6PgVN}ur7;dax*gmynsrbUwe%*^eJ`rB7*1Cfm`y0!epU8YZNKu4Q# zmXwn^Z72kX1IEWJ7<9F3x`Sm2NkLLKt!EZWgohK9d4B^OcGC-p4mmUtM1y^kf1+ep zDSwz9_~z2wIoQ}T8@`LYQNMVwzqc4yX?rn^J>1=^)JaVP=i8c&VQ?EY=vajMMYU*P z1x+Nl6oI73OL=tiY7`)wHt3SANG61)v0iWP2uy!cd=;TG{Q+xYj0^*(>n_@tBlUNAS zg4wo=>#WC#tUJwpR>!?Tq!9R8mjQv>9;{bEb0EzsR-)D?6lndo_Tg5o`Uv>sP!LSC z1YyM&W7=Q_8AqQMUFs8zPZNt#BMh_v6Ac$(+(2jaAKHTNw6T?CW~FgH+L(dLx zedmA@yAS@}$py}{(RX6Hjw_*|Ic;T?xR|N}{-GI!`v8%82h*tbMgR4DXV{4ZAC5B`)W!fV0UAK}@6#b5|1 zixV>Bj)0T5?^iHDhFkgz@_pz zpJKYE4KD|6%;$-*K0&T+SA00gT0+lc_5R@nM11Zb5%9>}0_Rov!vaV+gMm2&ZL>k0 z#V&ckqd{x)+JxfZ)3_qL{}v-&Gw`Ozp^_@!n`so@%S|hC06joEanA* zH1bG)-IQnv8c2OjJ}cHNJ$st_My}rT2gq~c@W{a!Li+9ob9pU{z5tzyg%9jKBMi@{ zt?5#Y>{&!24eU5KsF8}%cb)=|gJTP4FGx9>c+X2QqdqEiZ}lBkI=|_oidGY9)h5Oj zMxvrN2l#VRqgp#g_5fWT+nqDgz(xMRs{&{KLVMLQw`XT6ozcEO#MqYxtpJ!t7N(NI zQY<@&4x3AK$-b^WgksolkS~yeTZ@M#On^WpLnGIDV(eYKFE%@2-cg}hNlEMR9`sh{ z1@7ZTl|Dkn<27CYBOqsJs{mm!!);R$5^#+1>Ry0_Q8e!L3J_0Q%-TS|{Rx}_2@1~# z$UGOI`albfMrY2ib6L1ux{7=$D3g~Er+g>vqXqW5%Z&_`X`CMh$TD{<`Vm{shvKTH zE%Jk)4%HF0K9L5`^!eKgYc0x%dsqhRDGvC85#8&+Z<*EV_L#ux zv@b@^90mx0G|VieWjaF`8EOg*EJA=%vOey7{GL4)b--S~8C?3&@`1|Uv!ahz0Fmua z?bV&ULwYVC?qM2vNL~paP8SgDlRHX?RniXW7OP@FfDjb6f6RmNo19DGP69NFUjX3w z`kkD3u~U|Hp3(F9-D$;TAV)d|F)Y|SRGpy=SV>e)AjFZVf?>%qx$NL}*7WAP{}Pf= zft9iu`3Ekr<68|t;iZYD4+BMZLeD&y<<}j1bvnW&F2`N{L=J>oHDZ87(^c2+NISRh zdpM5xWn&Nmj)XdzKdRL>cFA#Xn!TW9M)M{gAU`s5h9qiN08a1BapI@_wO)|W2??8K z0Z``x&cIPaQt<~I1?N9*Q)V+$%?=1nf^O~kS7|~~vlxp%Suu})=C{JC4f2{VKVz4} zE8p|nIKpA9&dz*<{Fl)O4xhWe@LPcfA_Pp>LmwYeg*&~>K<#++a)Pr2&f3gZz%&G! zE9g$3|7*kt{sbjxhXVQ2N>u-dh)cCG!SHlkmL)x5GrsK7|rCK z&ZRLB7Apc)bLIsz$I96|fbWI3nKSi|puw)cwQ@0#^?@2%Bu>q;(#+4%n}aIr&Yj;z zrU&xQ^2<-q^6MZT<5JMiM5@VUC!h+*q00e`bGiZhE~He+mh&N+O!4bnX}%x6JD_nk zit7B>?Xjo(UqaS>_lLTbwPLPcUm3y9_F@wHj`wDx(uls2V`coldbwObhnbdxjtk_N z05?k!AyTuE+YWM3V3+lx)PE05$Kvk&fml_~df((YY#NJv~}2+vXIW< zztzU~*4Ms?7Jwq$(s=KEFHq}zKYHp;dc9<2_VaNAU|Ha7@?Xu~Es3u8EqlB6$x~PH z%TBpJKhn(oZ8E4%t~%O@NuAyX)LRgF1<~a1TMO|*kJ5hCsZ;xk2ayaz1*0Dnj~C8l zM5u(N$DW+j`H}-RTs?4NnUIWBmxc(+-cGO?|rNph~uFw5qUB!A6Wpl?aB3(z1nM>2z|tdU;reM+O(3N-X`vj z_h0I_DC*NS6CzvICJMhR&!)?OZ4i1CAn@T7OC%Gv!>S|@9dZ4uHQe$F7HwK8TsHGj zWq(bX(j_dnU)z5~JCvM&qOsIIt!H6mf13Mj3<~FqnC|?RH<^iKn`+!}`$Fdha0!7s zFAa?75~zUve^tOhK0d950QsMiO8G_rg76w|%OsrwMjmL#qfWL$PuZxapK9cJ1jAa7 z5hM)HE3Um$?XE-v5CApFSuD!eG#YHv@B*dGo@mhXmK$9(uxQA-2V_L)qN@OofS-%p z8K7g*ovw)>E%MjL;)Gc}U?X9uWGwBuj~^KVmR=G_WA7rfd)OZpu(2?fMuv(qSYJEo zHw6n3h|}Q%j@to~KP)T!Uv!(75-BPD^({k~BxUb}p-ZlP*`f6+hUS z_g0deHFpf?@Oo`cvCs`GYkrsYkuRY8d3Bh}9^WND2KdYCV~^ue^1UAQktX*w@q%mc zEG2?Jf+7&8=_F_`_cdVc5}#MPf}8Ae)L?Kd*E%*kt!8GpzzoHW5zY@LCtSldJ8M2I zoC0__LENbmk|UeejGzf7a`O1&8j?SOoC%NDd?frI?7mi|L}$;C&UDhH#CKU80r2>1 z6hOb{+wcdo@85L5rceHN6?AAK>J5^Ho6!hghOC_n2{#aeqa8Gc@M-~6ANnxG)8b%k zmE%Lx^2!-GxhQyj%MfJQmI_&mZJ>uorFe&N?uwc{xySMytnV9_>t7zcJ z2N0+=9pcKGz69Q^9IFA0NH1E63#nBd6Zpx0fm8I`5ri#BkL68T#I)07TpAsK;YT%; zkU|m}wWArAKxkfOOeiy?KnciPxN8ntikLdY9kjglh;@yPnkZe=5ZxKgHZJ`P?A;*7 zy=WUv1X-wxfW1@hX-j2v9|{$Haxp^P05gp_x^-C(DY}lwF(ySp8@J{p-4pVgE4Hlo*f5h=kZvNxwTN=R|cE5^D-pOsL@gwgDC$NtBt?n^@e2 zcL+unt+Ifb+js#My3J+n!Rgc-L5QpA2APSUz_u^wPU0{Y{m7${IH4Wyxtpex>1m+R zxVVq;B6QlkJEY|P5myg$!sjOfJEojqt^QeQbXs;up4gFEWz&h#N9Yi$a%xeGu-GD1 zT{|muUU1nVVtmnT0Gub0-lPH0goqe)(||JKQNw{rt;HP-CqHlgDXYtbCbVRa17;9H z@cT+gdLb>uSOlo2a8Hi=?Lz1ZU^;E1v{o`I&2oM9j9jRjTTg>5Lw}4%s3L>YqcNnz zpcRkPYo=4N*!0t3=b~u^@tjQr;ZxPZx6ZTBHm%IIJXk))rE}hRz?F^3V3|C$hOACG z`r(QC&zDlnWNMv`Ko=W#c22_!@X2~{+fR1~a2;XooynzLX&vjPaIR}*!zW>)Y%HF}abh7F4x3)#PCj%t3i2f?Pfr^oUF9F)#I|6-&yiSj{jT`%XxXMPk?8Yv z^C7FJ!77weRKXm(wN3Y~Beo`qk#^`eV0mPY3`HyBdy_ygy|YG}5=St-Y6vg=8Jd=x z#r{l`fvNv@f(2xDgi6C?0O$dGK6qZ4ctXMeC4sVMzaSHKEbgxw3+QveaX>l@_djlMOuZ*9kAAH2&mzog-|dBSN4UPA zU(L+EIj1n`QA1_UaUm_kg*nJi>~Q?G)R9I6eJcMyX(T;h`~kTzbvmf5GOE7*ZRe_v z{`mI|hc89IGWtKo-gsX0{z2*4KP>fxcg6B(GWg|RU0}8T@y~*o#HeK%10vm<;mTB< z+r(?Zs%Mvaf{dyMs2Oiv@KBb%c(7w*bPDyd&%esq+$(h_4o<-7_7-#C!K- z$%|*TW7d7bzCm|C7}{8^Mf1Xoiv!fHf9t((TUxh|9ZjA5Chxk|;zP-PC>*Hni*Y?g zpxaH}Wq>%rDQKp;l%Mq6eMbU#TR-bBJ}C@P$DDijBD1_`vdV4t(>QV5?B?5@*dFQ| zYN^AzqxWV8`P$d&tnjL z!uR+&bql!pi*V*Q=(G@M3*6|Ixz|N?hx22hFZ&bE_lUSeYSw>T^ZYzUNQiLVK0GG6 zlIgi|<0B)=)c8=B@#E*7(N$>&sg$$qD9INL1ZZ4wrN__goMp!2W@u+`8!&x%YoILb z%gqAEJ-Jat@k9j|o?*m$m7dW=cKQ}vK@hjLQ8xs>ReG=Mgxu;E;fZ>z+yx8kh= z)KG=QaEiF$W!LD!+T9St<}h>hz`|hHeO1UxSi|j(mznsKO>&Pcrn6R!}2~( zbTuX00zKTMfvcFzxT2(E86Al z!X5z}(WynBUl`Xb;D=wSb%{<4K6|Uj^trZ2$zlIMB`o!>K;X7{z4xf@_?Ia=QL{QZ zt2q!iyW=+5$W8vbb~ixe)NfCd;l1dN2(cJgWMXzEe<;Q+U zxNwq1>^CkOF{f#R3fwopO%>}a?k-9A9AFrX!_wq$erh-O)uJV8di9DjJFC_?^5CT7 zTLp`*nC;X;0mJt58AOBPAt6ywT5Y;_4qD3Es*QJ^IQ4t8MYU7$*E;(2R+i>hhs{PO z@WYT@1>UfmZ*Z~ky}PXP@|L}LC+zDRn@w7@_M0obPJ~viv6Dd|cyhdA0mO-miWYc| z*zj>;BTEX%WYT&+h3r)W>2?1$Fs)O=(!~)cd9<-)@(h zX2bQo|6tg>C%MM$kaIY_IJ8o@Hi1B)2{)Q7^Rg39L({ao%R{fq?uH=QDE`2c-z?06 z7_2oVk1m);cGnupH(i;A-sbpkbH9|@wDEeIQYQWArRrJTsb$%7`ybzKm#vS)8h#|k zt1xiXyZhi%w3DCxwd*rWqn;br($W&63axhQg7+?0-8h(O4eJ?YFb-(lG_+v|dP5pb zBfr_m-7eTIEYR|D=E*Wy7lG&M<~{+cA6f4fYjDLR^cJk0r<*&&ZQ-kW7c7TvHbGPH zq3=du^Aso9a#q)MynF1v?^isSnKLt|z0zqn+g&3hW}E5lYc`#!T~E11%E1kVYpE|1!e6dnZdYg6t7z zw%gEPie`?FGAl){v;WmQ^RM-|QqN_oQXKiZ}>q47f^5B*D5 z0{uR=EE8&_WZp&*N^(rwPdxV^o_bJl&)}j2K5Pl@aRC<=q-Um^8=)rIfX91U0;Z0` z;!fgz%e`>rC2I&qInW^}(Sk(q&>1~oPxXOog7-PkDN#j1Db~=l`CZc&=J(h#UxnUl zncobW)?;yuQ&Z|c6{t=X+V7|17x*M*v_nKdjha0Ulaln2T)`&*?;aj+v7vq!Au2)# zkE21weVsz#j1=*_-ZMFr>H_g2m1Jh3*o=|OC)oH8gz{frzkybECAB4{#)BDHHF;dr zzl#6-w2sD{*zNH2r}u9xuo5@sH5BH$U66=x(>Pt+&)i8nv$;>W86if7s;I3CnY(b2 z_CrEaM%b2T6!fvl0OQ*~ti%@BdqfbP+zm2Y*RB70OO?y(+BN)JB^8WFD;}~t_QT*N zg~j2GiciYHLoh@K1i6QvniCCuW!+QF&@_tA6v08lfvzp=sy~=E9wUls!8jLY+jsfS zkaX2-_^mgRGWpizvRk#U-%N{b5}YaoYYamzo8FSga}D)7^@EaH`^9BE*i-@|DgdYM zFrS|{0Vi6w{_~x;5|Y=0Ri!+2ZVq`1e1JC~I>_-Zax_sA!u2SKI0cENxt{aLir}-y zsI=hLDJLuHXmApLZoeneIL`9Y;^k5qh;fA<@6xy_pyqTR4;}-nH<%o` z<|fl3q(@Dj^2Ef@m^eZ$G7N>mg$cLgDL(1LqPvXN1g^wV;lDIT<0|%~Pk5PFGk&eY zE#P2D{6MC%7=`7~Qr)-bZHUmjR^t2SkP)`U`LO}>KCH(;_QIXh{)N6ZmFxtT93oL# zFEv;!&b_g(K4w#jp-ywF42X$f2p~L@5lBc)#sK3I59&VkZPucIysK}s|`*CaU*CY~a2M{8I+%6;=N;I&A zGO<&h)6ncAxDtUwP4a0qkTCP?NTlUb_&_K&f)E{8QKO({82IHLE}VnL*y_2z2glMU zFUg$pX(B;n)HV?+WOQFsHcPpr-|Nf(<72If5ZX6Pj{^vsY6yHoX zJF10XX%4QWqE{-7Ci^?IZjCmS($p9q*nZsWw9-MkOqFY>dF5D&<&Z>#TyTkxvQ>2b z%42seBaQf7nOk2HquVtVHUBX+erh;TbQ}%!4 zdAB%TxJ6hoG`nMA>eFs`hd5p79%l-wp$QL8qK0Q@k|k-%HWJiWCE;${OXf7ypFlwO zh+7Y5`eOTST;;lNkC_>XXnN10vT1o6!_&`N@Nt>Yq*8j|I&+>@c~8c9oQ61((pvU} zk2clT7qlTmYDZ9rB^Lplq&H6VoCPyu#(lF#$FgF2&%ko(-}T46Q{r?!ONjvY2v@Gs z5IdDi__(gycbU+QAJ1cmwJ8^0KA_ycMF^+i;w-pNu9ta@L=m^fy7#hx?6YZbrG{3Y zIZuA~WTJ&414>eoF~sg^on@o#Ou*_2^E?&VE4e$&zDiO>Meq8DmRAVioV;t|cTeUh zawuBRDG9A*=Pn9ms*>S1USd=E95QBfOtW_G|MYYv;83k^{1{;v%UEVCWteD?LDTIb z*+x;4qM`}a%vg%-AvLlTVMYomS#OAlnn9SXqp`%rEfh^-jS{kp3G+Yd{_lC7^PJ~= z=X>ApoNqbr@BNZ<@Q@J8Yh!4x5!Se8cVX!Ff5EMK2-mUHkRJ7O9Ke|x*LGBfsHZ{$ti3g6heoAWi zFNj&QdofKQg}}E+p`WfeY|t4f#=eOnk%IoJAx4hhvX%21u<+wC_WK}hjfD#nU?SOJ zEs~@i7BsEOI~bS;U!!9FI#GYSbWhb;|H0rpccRQyc`7dxJi);4$e3M+6SJsu?Q#ft z6_bF87n>K*@LAZy=fv$^)sZH2L8ol5&nLuDrbY$2m=V;1?%9rF8Jpd^eb^APEq0_H zRGlxwnU*X&z1k?Zc6Yke+)n*FAOVL8=kfgv$=oAbYgAP6T5Mug!snp^kRPq(%MV%j z##BO7j>cNR=BT?H_V$>LfNZn|abjQz{b(C*r;OgcZhJy*^d-pKI|Nyqb~ONLAv%Zi zUMX3K(i$aue{`cXNA%Z-j1wyN0C}@~bB50WS1!d{JaO}MJL5<1`E%MHey1Zwrd3dX zI+q@Onci3>ki4!gjbyjbltY~~UG&a>K{-UxHP1}tk5lQ@?NbJRU3%~4F#%>MGk)Gb z+w#|}cVYn&8v9P92}0oX?>^&Igr0c9Z-PV%Z6nzi_rA0EPKu(#k*e6OGqSY20dA_} zP&_$2x9WD~VwD|sWgrkj66pQ5d}k)BZ@Dvw&Ik^Hxh-40=H+)oVZ)0L^_H#j?657V zSZQ_;JAZX*o~?=n08)}BU`_ylrkyu{NIC#STk^m{0RRdEi(UMJZ`3(GTOKon0I5zs z!8g<%(?j_qW9>XEwj}V+RmTtS$4+j31*`v3^-JnMVNfUJw`vFgNB+74FTdIl0&2s5 zga=%654^4~3$xK4KRRCQEETaaF|!25FntZh6nIQLM@f(b8o8DZ`S)WFo;PjFB%JXB4n$!G5(yY*&qHY;8rowYVwx|7lW@DS z04j-|=5;ZItp$UT=1&9B&D|jfwE6u9L^Q0^F_L_nizPms%F=Ql&I(uIBF7c={=NKW z_-qKf#N+w_^7RWIt+Mj_&pIgAm0Tq|*i{$0;GaQ74g+aG8cGwSM>K02zPpBh}Zs9Z<4EL)YR!>z-z|^tg zer!Bq#vI9=^dpN8^`$duE(dB}U4+AM5oA1k8y+4Q*X(_MV?)qA_2o;6&P1bxgoI3! z2%J7HIO*BLjH(Ed=Q!xr_>@|@r)nI}B8^tAtbAQ{n|Sia!%SOjzRUaFWsB6g3hK|h zmG>j?l2J9ord-G9?k+VB|8^{b zPs9hT@oYKw0#IRXU2#GskYG_L5z)i7P)*Lg#f?HvbvOGCR?UhP7$_SD8XFt2V7Cz| znn_w|ZewMs&Xu$6db+gZM?`7V1C&jfl-3*~_m%5aJHnET!ED8JSe#%o=&@7dY}v|l zil;6IsUssK!2XGL5nv4Szs=0(V2eQRm=-4?8-u^g&WE#3CF`B9Pj6tC78c6m3am>= z!=`EOv9Yn&uIV$I9uuHo_8#QvB`Y;Zjql^W?Jg!8(~@iv+PGMa?W*&PW|z7D=o0f@ z8zF-b$`c|T7M!n<%0a#g&CM2^e_kgd(^FO-_EYlp>gpP7@7-+UgWW$=%dIp zxhga*d37azH+6o`+~nb zjj~SNT8emb)JviI(3SP(#$i>+cVg?dW)YmI7R|-@T;00e!rHe^XXndks={h=IS zJ(*(~vB5hRlTz`tUYtg42tFKXD^YOeV24;-j1QI#;ZF+P%xQ4`;Pa7MSGeoDsUf$$ zf=u*2nNBNIVGF?&+WvA8B)`@7Kw`mxwf!})7GewqrOgv)cUc%toq~rY+Kf=15xo5VxdN`v@Me-MUOoGdZqMP2uO`Z#WpscT zOS0Ez>DmsWkO9-CSkPt$_e7VOVC(s-id1_4E}*QoWD0X1JEVX#m~R@jB22@96I$4t z+@1&YY6?^yc~$CwI1Du4_I<&co(qFb76i~N7aQH5A>DBOK_IcKzGU z!L{`2K@tIIbah;RR&0l=if;A+ix5a)x@+oZtJ_}wj~8NOV&2f?g-4*&oF literal 0 HcmV?d00001 diff --git a/doc/windowspecific/kwin-window-matching.png b/doc/windowspecific/kwin-window-matching.png new file mode 100644 index 0000000000000000000000000000000000000000..50162f9805d98583a6de57481ab3f624cd4d1b4e GIT binary patch literal 53362 zcmV)tK$pLXP)01;@wuk4>zW08+cUpU~9otcH#SQm;Z53q|K@emW5D|)fQ`xoKZae0@$vsx4kwH`v zLRmBLJijL^mx_#v%!u>mIVXd^efihW&|qq6YPPklZEb5?+uGK)wiOeM^f%XEbMsR( zIx=EDeE48Ke*9>D|NXbGq~E`)J{ChCL-g^h=#P!+kF5G*Q}kyc`qQUBH&vh3(#w-}`etNQMpc|YJ-^oHuWw49REcZ)sR(l3-|jxK+5OQ!9{l0gADgC++4ON^ z`uKGeV84oDbH6q>d^Mio+OMvznB4HNX=!dY7DT;$`^HR9O_;HfoEaH@omXxs(&$Dt zG8}0vvvO-1wKZOlM%FZDYr?D1q{fq)OlT~xiM*yhYdon*Thoyy@|vz`b$lj z*)KH{*Ys!2#Wnk-Wrjwe-A=C&Imi@VQOS#gbSnxhJuu>LVxsHg;}l( zQ{!PU6%JCCoFIiVgA~dRQYtq{AseKS1yUZEnziHsDdY#K&4np1NG-WRs?=a=3xUcH zRkqyTzU&?pkC(&;Pg@C4r3X?qA!^YR_oCQ&VAcft<-eae;LlkmDl*I>LWq>Hk%?%B*^n#R%>oX$tiXf`8 zvXTyMd7@su>@!VI9-F$_YEg};t*I7OnVRZHYpQx^svlLFs)rBEqsj--wwHY`GJB;s z*MOX>yQ{3?A|C;pf?J; z_Ihx@ym-;QW><5=W}v^{yy)pRuV1~yUTzqqu;YURFU^bYF7vX#FA970%d$t-U&;2- z-0((03W%Y;!ULTte9#9!Y}6YCI7xkbO;=~T>Fa%AhKF9WWS#A@KInufMEyvGf}#F) z_3Bjy0_&hVw{Oz2nbAELqH%B72Zw_uPOk_+3``YgJ! z{**!N69b=!CF|32GqHc@^{asJ9@_Nq-nLgNONj4naEPxIVafwks~+!E08sfnRDP(k z!OEYH7mzQvNqI4L-ijZb{P5)VKm`Gm-wTCofGTN-3J>Wa4@CW^d84qw9qnz@CDqPy zuJ-4x*zU{_1&p+}wes7XHvD;Os~LVhm>Ht5`)zIO@xg@fK?@v_8Ki*6wr4HtmfiZS z*$m2Yw2Mphdl(9k;%Ub^;N01Y2$niJI!t$WkICgmKkN1DA=4#<$oAuPw~Fq&y+vRhtzmbCnr#?Y>{`$4^-K`RNd0I_x}77zt0I(=%deLrON)&!zL&H zW=i6Qqdet_BvFBI|v6w-dd{gNS^7vzcJT!las$8BXu4`qj_BIHH^I?r03 z7G!6iK5YVC3I-_vrbWI}P}xmSfR{o+3VTm|3grWMqELt;+D3f}D!Wmlas&OnE|8*M z`ax=DW>$!>&2)Bl3pn*8)Y|&objkMC=iR2UwToj51}QFXLw&8>tg}>hU1Ledo|BvW zab1nL3}0`D_=*tX3xp{KIZKY@bN0nQD^T&-2~+!j`G?$7Lwt^O{OBS5jBA$t4C0ir z5$_b=3-nI8fogJM!d$*^&io=k^V81n%;^)y#6{le3HfFBQmObYYT(QM`HPvD7$@fl z0AL?WRY7nyH#1|dT)bfR?%oyEzvAR^+~bA3fy((i-(ij&I%u9fZHfaF`z);&!fTiD_i)+%sBO#k{bE{Mn$2n!fU+TqbB1`K z_`cH%#lE(d7Cx(3XS)xg+#ux-=W(3N6BPhaB^}bkMv|VBu(N#k?yZ~Vz`nia?(JLh zy@`aKZFpRVo%TW$HvLIMT{atDUjrZbAPPHAJ+krvT;T<&VF5+zp=ERJ)5>MLmR(m{ z1vG99q|To`W5&iNOiN2^A>vpIq+Br7)$ts6pj7xBH<(Wx#{fd?G7zM`F_@bCw}&aj zAZH~DTiz)fq&Q9!(48QKZr!*h1I7~uWa9DBu|v|SSZ@}u=j|KU8PF*&vv*1jhd&%z z*2r&Pj_N-wKzs4r*@C7MpmL#IVQDG!b#4Ev(eB?*+bE1qNPh;>2@y~)1y$-z z?gTk0Wn!>eo<7xUkk%KR`A+~ToIv>^pp5zj}pP zvd(rNM7crApIFXu9xqg>K~#81??q3S>3H6ju(Ra-)K*tTOKK0s?vapIQiAf7o$PBrK!p6{_%S}p3(X(F+Aki@j331 z;&WBygXq|Q?%6G!uw8*l?u$_XA*$1ZU+=~Y$08ma@fwwzpMTgHEhl9iqth!%o2TUd z$2iT-&dB~9v{Pib%J%NzUQ6{-{o|(}30Tzsjno4mVI80# z(OZFJetw=oDQf4Vhsx;397~z&3gFSDey$VXseLkYenYzOq&}OI~!+zRdkm{{Q2_eaCp9%6p-b zAT=&^J$L#P!N?IZ?%jC~O{{6wA(XUc9zkcapeIohY~LY;E-;9!Omf zg&pUZMA@|Q$WSs53Kr;-?Xj|XNsaC6d84rNseDjhEB$}hkJ88QJNTdyzHmYmd0JJE z9wf@1kv3*ry>{gy5GMBGnmHol)a2wuqU@@N_X&G$45WmO*8Q8Cn-fpem}Jrq3v~Tu zPLv<>ffR94p}@=01sxt7u+-i0+}f6tAHa=vwhB3`cMV=so1Lu5j_m1iBeGw1+CF;o%C{b_Kwab^}$*m1T&Y(yrNe#2q>l_0P zFhHAqX~?ZM)phk;UA}N0DADgLU|DLG(A6FZO9FJ2_kNZ9gO>>)xqjucB)UEa0DubW z$ad%X^)s-eK%}2}k_I$+(}-}Ip5vmNn|<0w@z5J}%MA|!r*bd7HFs{_P*8HilIlaM zC?7xp5~!PkQ=)xU3kwTCfnFE(TXN+*QTK)Ud8g-zHc#G*=i*)`j23O8?S<#bYG(zh z+c&QPP;86c=au=g&z%LHP%=brgOpt70lZN3U6zvXGLQhN^A)G1OfCBX*H8O$w68|T zgGy^WaCo8OpJ$^``E&6)0NK1ncBJfp*9+x;J8z3}qQW6c6U^~AIqfVVzlxJ5Olxzq zWoPkv<^)I-qEZsf<(Mn+vSlo~D2|k)X0X3ES|5}=q%)^Z%6N4qX~VBwA-uL;f;o2H zs&}>AC;9+&KvEJa@7+z|gUfhI%z3+AO5nz5Xnp+D|bE+!kYCf3zROm5hk{H)Qu#%)bS8p~@UO#)JJP1@29 zHWSxWTsCOUqUkR+WoZu0sHUA_ka_c2Gigc-XQ- zHZWBN`B4}LLLw8=wrpP_#6a1x!JN++7WS>k67ZoMi~)68kR=9%CC73c>w5%x=xCgS zCF^Wok##yE_vz6?2TWtbV}XV;j0gjkh5-`>6oZFhCM-r6hY%T0&;ZHma%3#^=^rD@ zlJhBOkyn){kKdpMd97ZHR!ETJ=FepTIEj9(AVsH#G7~!I04X|tcXxN>jbb8CU3kj+4R3Ah?QR#E?B{y|SN%b zYT_$-uBMPD6gZt%hMXz}Ge;PZfs^nUpZSgXFred!^BuBtCFlScO(2~?n9eA%?%TL( ztv7&lz{gO;!Ad_)F2^h}_P$35V2BA4MVhIBZ zSRn=n`XC^{bR(Q!y;htX(Bw0q85zsGp~Zm4%hZsd8Y}f)sni34^mE)>qMy}}+HVWm zQn(>M3HYM^3Lt59)O`v(D#yBiALrv7l!?(-=t6)Vp~q%6Ed0DsKG+jb#>wtAy zvbY}0J#F3R+6NS*0K@h2o!F;d;vaYXoly7}FN;L=Wt{`vIY*L<3ohsbZgzT|B)6C>3F%J|Vb>aLOmz~9%N_z{q z^n77wUq6xA*{% z8XFs>?OCTiq`jxjqx<(PK7cQHpiXCxZaLA@?-^ay|zOWyF4+>|;lh7+W4k6jZ|Zfd9#a&0xwpEY^a z)7fU8)YlRcq-@-DUl*oO(O?RVd6Lw}1Ar=X7|y~Cg>-pD)F zQ>G5046x)=@i_&kNRs3j<}6{IyeM)c*lvMT>~k7OQ73_)U!yudxc6%Uq}caRIxvGa zo+I8^)7&cZZ!jK&>sWu<_=I)BZ~6wHm9$Q1lfi?!kW7qmK6t=U++LTyFbUX_ z>wt&M1xS{s$d$jO?L=4)&(!|Cd$@lS?MrT0e{U}#V|ns_rq_l2j<@P;F<~MeRSW>$ z8J@@qdnkZ&FXH{x;5$3GD$gX%K}p^(S67*NWd9uPfi>o{94TTp@hnV~pq28&H4FJ_>X?drR z!c;g=rRIewP|eIt^Ate%1J8{$IO;SC00wy}DR1Y{p=ew7VTf60$vSPR^HQaOlqz8y zW@l%$PQXiS-#oCx(-!-pklkBZSK~7}nj@(hqqi0olqKtGwDMY1up%TzQZL78t`u;Q z2nFJ4BYFMAu_K&cJrb0~po3v#Dg$!8G?^W6ksO5+fE;C5hFtTn9FS#w-|+?^IVdFE zTlH56;dk7)mZ+odBYBkVl%Kk;%j9Q!eQxb~0nTEC?{J^!J^|?#V20~O=#>zv!jVTS zacR3)?L@v6Fa_{TO-!&4(H@L(ginAt1t#`ca?R^Ro={hNI|F@m9S+Lxz~iqF&Ul=- z7iE1NuakmPyf0bPf~e+__c`}n8c6x)eX6n737db85OI#P;0t)~rD(svGK&+$g8TVq_aPx;!g1j;%OPA&zwFfuNAhK$2AX~>W~XlA;(t8vAo`> z5M{$u5kh?><((>;hbrxVrKyLCft4qFIv<9KeF{?gnL!le&oMf6f+Yq|_OYbQ{Q7)+ zIt2!IWF@V~)-fc6Za5#_4=opAj7VZ-A7LX5ni?OBpFV}N&XRSu^EplYF3!bqS|$cp zjiu%Zv5&WHfS!0rQ0vKz-sI@0A*NjNfwaBV%fV+g>U`!nReT+Ih<{&U%M$Iw#Ftus zzC(_Y0tj`Y-t0p>u_~<4j-S@9Y3jNzlb`K%o_;wmZAWezhF@Xm_o(fn-;t=>szcsc z!tHjGbLjn>@M>{BASxc3W4k4LDgv~KfIiC6@>tht2`E#qKW(gVjq>;5n&MM`&|Z8; zw`o5?uKS#IwkP_EE!}5_-r267DM#CkbMQUw$2^4SdEY0;1L{f}uutz_g{OG<4(Q{) z`63vWA&_rsm@fKvg|Cj^KtE?xSi#}iDQ}Cz&B|o|Dr|gH)Z>6 zxR#B*XZnLt2Sll`^Vs_gd;m|t74StC_Fx-1*@-e=bai0AV`U#_`UAMg68ESJq*M@C zs;b0|USD4yd8eMr94Vchou<9LooxmEn4z^vv7$-APRdhuOH+estgAA0@`_>ggS)0y zmaMbA{K&!RxQ55IxEpC`OM#)j5bxBM=1vtKsQfup;d4_3&75wB{@ef}#3AmzQ z_Si4qIZ;5_`^Ag?xUQPF0keRVUldp?E<^I604U9o>X&#v>&@cH%6Wc_#w+g6Ao3jvOrZhSg91O zLf}e03$D;$cF01@4%qSlmmj7oJ3w40HqM6!salyTzy@QKmqgO?KGFau(OxM&i?rQ4xoY1I-xZ$ zM5&mHD9Y)SdB1*=GE!~0Y-fLMO#~xeh*DwivHM3OKG@;n1NZ@s(5VT$f=x)oC&Wjn z@ka-Hd@tMC&vBX?6$nzcn7r^=OS6z&o$2fAGcEGDO_F+Bnx7Jfq_PFU6pi&vzQY?= zG|29sexPw=`6KHj%Si%nRHmcB!@X0*2ddE`162{cRK

p6rZF!w#AA#uAJPyM$C`}(mS_H?`4nhV9057 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 z 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 MQ@N3O{jTT#0|kKHWdHyG literal 0 HcmV?d00001 diff --git a/doc/windowspecific/window-matching-knotes.png b/doc/windowspecific/window-matching-knotes.png new file mode 100644 index 0000000000000000000000000000000000000000..7fc4f8e75013211c89ea5b1d7a626a1b6c0dadb1 GIT binary patch literal 37378 zcmdSAWmH^E(M~i`pr4?mq$FkUhb68NyrD=hMWS@J zSoZl__o~mI=u61c)hj&(AG{V+gfU;aV7(H)@q3OH(O(e3d<884{~-rJ6JgBvkVN3S zLtKASB-nledaRAFGGRsH7W$i z>N4ufo?GO5Keq~u`DsE&WFi#_@xVgJ`#UNS`J@}s#pJDwX$CMTME{F8kPUMAKz~|E zuP60_lmz+&WNnL43l+(r|MaV{qKz_ZD6ypoxbRP18F>VV-z7Zx3#>8)>JEx0hbk(ybD z>?V)_<=4h|%_(P!0+z(DwC;H4XNriuMhO`-F8_9pjKHyZCJBidQxn5&E19KLm+hYR z`bZe&z@%_BQ18IBRjqO6dy6f|TZ)CCL^tVnO>It~{aJXk0TQ{NLqZuviqNNcmxq{G z-H%SZC9dQV&$6gf6Qc{pD0q7W1T)k}qwVHf*ULWFrApe%p|rxb5l&YaYBFUN_B+34 z)0PAT9uLyyr?mCVH5O*2XBvn#lQ`;X`F$R(Z|`1Rlupbobr7B$xyTDD(D9ssF0Hn* zemJ4YQV(mJo0?w{|1v3CqD=}}h?Hf7Rji5$0#mxp_+^c=rfV{^HhJ9D9`hfx7u!9x zJL7MQjc7v-;9en4eNFe2!1gqc3#)%ke^8x89)Zb!DO-|T~z@NV?Dc zA(v`yUM8|~6xJ)_+A96Ku-|h(rfNJeNVeb1J|3a@IHB1nP1r1X06APv1KUr0e`%)F z9Z3xg;y<-Tr7TWxiP>+A{0X9;nc?E3KKc_bgAd?eU^J~mu> zRJ`C{|9U|2GuTGY+_YG;%qg&|r(y!Hd@v7ki1>~AwWQfX1$w3Ts{@yjIW%R_WV?w7 z40zYaVK0N)l~%eoPi;?cMdYA@O0Q!wJd=a4%deM_vIj%J{>!iPx~-p12U~7R;xrS= z(eRh@jH4}jP{G`KHeWqVJWOU5CFN5!Lz6pO+Q@k8^rSvUaNtpD;$#3nJ*U`Oo~*{= z$uP=BXCgDNbj0F0e^$hjC?`fYOD2!FdK%eQ>StHssJp(t_IbED%}@7c^^ZvIHipUv zgTz^Z%LBeP=>b4g5+OD(7YBvb+bR?Rw@W6c{UdlJE$3s&E9+qpDiZau&O_eP`xC1l z$w@>k9;?;mf1uRC?}x07=UQ;d0wVM{95%PK)lTw0=e>6^{oKLP*%Z6YQ~)=k(}I@5 zXwd4KMx(Qy{;$*$S)n`Gt ztH%7}VAj%4=H1(qLG^X!D^-p&05za4IBm)n#$^eCVh0d8~B`7 zuf>K%1Uz;LyjDEkCAGo%AvHKRV}1$QDfyLC+>(91QZXR|-{YLM#Xt}Wlhtzh9x=;L zoQc52n3It$r63oXm3{Sfj`7wXv#vMb2NS7i;#v|JEQ#lkZ;>U3bhD3_>c?LyHPXFq zPW%ulEe#&-_RC)313ThKxw}7%p?Qg`qEV*-c$_;}bL&vl>itePQkZcJ1!8VF)obEr z6K|!>HdEYV+fU){vQJIzZB=Bo14iV~w^Cgk7IFbek2;+22o`G_x6-e>avA&!Jf8Z| zTEAaJ#l@+g{eeW!wwu6~*!3LLKzYkaei_Z^OYCF>Mu`|kVwaa{T4X$bNEB9g1Fu1o z$Ne<571!=;_?*keD-n0KqlZlkp7-INHyfH4oGq9OMiOFQv^=&`CIM@5&n4pSYrdX& zb;;ZB6~Aw0Y4&tKMu931t64F@GU(%-WU^j?6-_ImM$02hSQ1*2`B*v+59cEifqq#3 zVV;Gb-6Kd`hObly=`?tJZi6r3lxv!Y#{0Q0rOKUUI_x3c`3P#m&SQ9(#pllTmt8TnkeWfrVA`?CA-bYvey@j3f&x^9$n84x<62??yn;)F|pUnSM$BTFRpeP6$YG7GLw5hvvo7kdM@0nd)8P#<-rKT^O1F&~BK zR$SjxHZ^LP)S(Ai{lMk?wQh^7(+m{`V#6k0LOeX&C!b>N2D=bl_!Jx*9u3$^8X*?m z=|7TP<>fdlu2bx-VLPeqI~rrykKmKMEeL*P?gsnPx1*BB|pfV6DU6|yNOJ| zjcins*zB+z&P7W_smuO}_X>aNhvus&+KkD4S-2T8YHFxOAdei?>UtTUxp7p(>AhcX zmbsJ8=fj~v{Ugl*$o&?FgHUOlO+Ky3k}iYf-G_EXU;I_O=M(}6+WZaG6mRABsNLrr zHiljOZDy~Vi=b|zs+l{i@)>?Fu1TTY9$Dn+)10sEy_zQQ9TT$49K>({kRnkFcU# zp+354{h;rXqfbLx*Nug{P??;>xTO6guiJ*DLqjP6+5`%h2ag9oJx_?ZCIlLhmx;gQ zc9z=x(avVGWTf!vwIETIzNr7F26ts__v{bTW;=yq*|8V1Gm|E%!EH3kp$as z28*d}FY>oc)i^a_5EB)@XpzRYB}y~6SKssC@^ZdX7I6u9ohGB7IQq`ml`S-P3L~~q zFbM2ud~y>vaghhx%_a}^5x5KzO@H%kIofRk6VND+(`od?x_pY5wZE8jd%_I%o`_F< z(B45{#%0uL=_tW3GW35BhllVEKQE|j)KPDe!4TbtG+VfKy^BU`rhQJqy@Tv^^+h(uQ? z==G!@1_JpPjQNw=GVWe3=LZ=0qN>}-VSGaiOMB#G45(8#EJXd;_7vMv;Bk(nc{6#a}>8wwKjOW za^@e~@ZR-l8AoD1(E4e?<3I%{Vfe5u_{?$!SF_5@d?QI^s7it;o|Sgr8e?zQH%pxM_|6P3oy!^yGn{Lt02mN;r2 zT)0hgL#;d;qNZjTW?YjY+0&{*Aq&Lx7mxI#=w{ZfT0J>cZpOy^Xd4p(LJ>uR=9Pd@tOKJVFUbEV!3lnO%K^Q#BnuidJ*Ix?6l!`Z3qBcV`Zq642YAVS z1iwJ}Px3!u{-2=#ff@LJf_~`<9pP_6`~MD{#HH@D^YvJ47;1v_@Ru8LEcUY&N%=xE zXDBf8t+xngYyBm>=|zR!yIJ8Qx~)15ZXpQY9{ZuBiH7Lu5A~{Z<>IBcdX_ZXQeIim zLGeHd*zwX<6B=E?D_dSuezm<;lY%#CDoA*lQSbPOduo0Q#!5jE4Ln6%JLodqpqLEs z#~&WDIr@l13!nwqeQ?)AzRZXw!BqnOrD)I#`CcZABKi^h^7ogX(U8A{hEDZw{R@Rq zC=Lkeisl?6?k{eo+t2Ao(rl($*}mS%oO2S3%JSypfuL*R2+2pVEk{{|Aggsr$O;)ybvm4SetsT> z;byF`DBHB)oW7E%4K$m5eLhjSc=aJe>ll{*9*qjiBT)@)sqM1RYcc?~slep9!xNG5 z-JDG{6giF{yY*?S?uKvatP*c%*$wwO@_HuhMOE z3%6StjZv$&RsGfqFYDf!R}v5$X}{wOrL9w#{F8F_RzS$NylPMTeK}-Wqqio5Jct-1 zxqOjY)rXh~jvIfF85&-9sf0lkA{tP~%N1_G+1(tL(~V|D&b-$LY&>NeHJnTZ^bNUJ zH)=7Y94*f)iq9RLV_x6XvXt3#( zx*N{7;?i%FJrW(Pml^|F$ttY2t@rdJ;DaN|SpdWGN7EK|5?3j8$Fw||1hEd+OOLcA z1xR$%da{ays1v`;-ump#RoNf~RhadpEk9JOo{sl_TrUW7Sa6^5qoAL$v7RezS*`Oi zXScD#^;vW{0kX9r41Hd0P%EC_=(Qc{wvzo^OCkDBZ6DpPi+<*g2H+8jq}2kZM}8&w zoJkR53NpP(6s4AGmF=aH={no)X8h2dpAl$@Og)kq{z2GP5rVZ&rP-ve)(*BFvq$-+ z^xn0~Zr(bAERoX#c0vq=(_v{XUBz%+tx+J?ME4r}H2z~D!HhB<`6geQi@GEBjJoLJQf==0gf=JOh-Sxn zhZQ87yAV$>u=IPR#NjB93oR0wjK& zHtAODqg}Jkh07C*`lrHrNTOt?kq5Cjb^jdv=@~53JU_9`T)Nym6fmHX690qO;z;DW zzRBYl7Q4Ng*hOmXYS9CbbS&t}?#x@|bF+hdwB=KdJ$S>Lr;G@}{SQR#5qdK#OT z$?1GBH?R_U`uTVGg%xQ>P7IQq0D&FrTgQ!Nlk{xz zxa90xRM-D^!4BUgjHiAdJmrP9Q*?Znp`6-sG%$cF!D`k>8NV^NBjIo?3!EKBB17O7v#{ggWF^|=FN?MSRE-pVuLSHYZ0iql2wkcNw1)SP4c&NumW&! z31}ljkS-0=9YsDHM0HMp&IrJc3o*W+8@Xyhbo*uL<$#jF0=cbjOS&hZ5RF;XYCq|_ zeZp%zJ~EwJdE8$X$OAab`+nMWT zZLdH?LpUo%o8|(RTUhT(tmDa23dduRx6Nyt*acsW zXuTBwcy1yEesZP3xq8Xcd1K?5EtK!|`r-eTYaOfobSaM)sRD#Oz&$dKoV@n5` zhz^P#bRzzxoP?4kAtN(#R1fZQtr2O|LFoZj!Xc#NV;;wdUc^~!ic?KW7EDp+i0kUN zRpi(foK_S_9lH6$$?{vvsz3ov#Vu9BS`GQgR52^?V-TSxLL|_D@^wJtrzQ+MlT9{y zBR&JEepOeivNKF}^3pf6tP!?4hsV8!50gov5|M0X$*Je?I;Q_6t+aZXiGweHkJTw{ z{yMC!R@PKNwZlfMff`DD+2W4THC;(lcBde~Pm5NJD-&eO-^zi|!-e`Mv5Ra#wC0eA z7NAJ-7NNY$uWX`AT73{~=0?eaTuU2`&U%FH8Y+JjMFS36yA4-wxySgr5W-@$^#xbT z_mD_bBBG*=Y7_gO4VN7Ao%M1}el1FX6Uvfa2OL^mS{=|76YZ52}Mtm+JjbO zQAn=BjG`I>;uspH-;jCrW)q_X#2}-lu^D;6Dn2k%-uP`K?805gPl5bAlz3l%6!YPX zu|{36R`}+)vz!-0!7tE%(GQW=!RT|h*ajw4u*cB zI5|a?_-B36ng4n;?AIv^Wn+QEj5Vfjst8-JXui}YV-4p&%2ElSQ1k{}9o4FeUX;n- zm+$tl;JIk>_3yg>72X-MHsEX*KJf*}z!6=Eykv+cP^9|157%p5%2h4V5>1j0vMLz7 zW|th0*rO`YvH?Voqo#zZy@H1i`!*Q=3=4M)4;Se*?@?t?tnKSqj9LfQJvKVR`|KY1 zzS#O89tIknZnAqOiz3kIo9u2mLq>$u3cGX8;U4{3AM%v?;|tC6!6)P_0-SHhjAZen zD4h$9BfexCgU6bGYzaibpaOmTAKZ*`Z&>`&yS{2Li+cX(!$haIzf7**t?y&wPS>NY zFv7kn`zenUwj8`tj6eK!-hoGxnuy!o*%m}VWOdHfK^+bKh%UH%+%KE!up6cBIYY!rV>g}KijxB}ri|y= zB7c8RAK{BP*Tnmo!}a;D4nq^G+Qb9)v3EzVu91-=e&P|Po^fnXcrIxX+tMY&MItIC zVk_^X&J}_5{@BIZUgNlp-+3sgF7TY^ncYiBJ0eSl}-=A1pnyh~(qv(0D8Wa_Q zmWkUTgfY0rb(3?tU{K&nWx;FF0Z7D`uvNBSKs%vxCGRV;O7H{?pzP{e9SF-@{bC1< zKB0{Y6n9_|Wx{;1UTRvY{U3UT&qKJefY&A873g(vH16)?coRaVLI3(Vj}#?9X?BG& z{UA&`F^RDJg`F5^03FO8=>i^d%{|44j4~r(eRaX8!H9u+L<=7?-v4>EZh$AaaSuJD zAk5u1C9(x+kFF-r$`pRCJ!E$n_FOtA3&N6SmL>l!`z4u!L8rYvp1-EgXSNA-yB19V zef@cSAmH!a7#rJp;=n?%#3mIwuX7?kph6k=2j82kC4XOJ=Jf)doA-4?T2?O$g9-nS zjVMS4XjIM?5b#`Q{s@_n0y2whwQ)Bx=&JASa5ta}jObR5a|6&(vSocDMHPTBLBe1d zm>^h`P9+)u3=V=27+NL-_Ju&pULJl?T0j^YfG-p(42&xf`b49^U@s(5(cK&9|Fz2 z6qUoFz{9{mhl6Q{gN_Xy6=tpEs%tj&+F12lzB-xs0>49*1602F}#6!~GEyB6J}5g%Q-rd03; zfv2E=GeIEharVQ*jgrz9D*1FGT&$@*7dv`}tX;HW-Nv)s?d?75<@g{Li=`~ogi_#u zb)89XD9eoX!uT14lHUE2#h|y*deWpJ><>x+^&3?Wj5YP$2L3mOE;+1`w5Yh2o zokdOfaJ_RsJ9|#%4Y1&{WI^e~sf7G;J2O1^U67vMa#Gbz6X6{Ig?d(pVz3@h^e?`_ zsGYW}XR6}C1BX-ga@J4H;?hl*#31{F-}e|K7LjdReMer1mvx?2*+dTR#_?*#n;T3T zl_D06*Z8)Zcd{jJRnp%i$7+Q;B}dZQ>gTp+mQ?0Vz0&f`an%L6Xc#k`VSqs`B>cV}CNNc@F$P%L=u zK8q+-szGJ!B_Vrrea^uq>iYe0TRMx$Q<96|C_xp7NxLBr@qCKQWe+d7XMeHW4SI+b zy@=+tnKyg4H#)SuGudJ>;_ZcqHedA|Jsqd6!tkY0q^uiyZe7R5uF0#dy=q@IXw39J zyXl*_Y^DJOgFlQa07bwmDh<`=;k8|~RMpIUZd?3}o{xw8hXm461JxkMjrxAN1hu&} zY!&#=KsIpy5`4%&KfS{_Dhf*YrKk$vuEJTeP;Htv;?snQ1);Wag~PEQmI2k?*5|Uv z7B|3p*P+RvX1P;O8eKwDR;5%A&m@a6I3k@@)Mfj8#&o14mbaTix5hel;LxoPaK;T>H+g@Wff$2*?$Sh+nvP80AdT*qx}sPI05EJEpj`P2mlDQFLRxQvTg7Vl9>D>nm|`lp~8vU zeZ&tGRPC%)+dQYeCCPln>KU$SdWxl;19@MGBGXc@~u4Tn)E0H+vGTkBwnXA)9;x z2JhO6LS@WW6dx1W?tIg=D$HC$3X{&00UOa$Vw-EjlezcWMbihk)1g$44_uj-PAC+! zN2cYVqXiE=>pZWA#d{1wdO)g4K`eSBE(fPT2TerCH-o|7V|T_l-F~8yMPEwOyb#TL zblX6VxA}4yw1w6ok_169aX+(6N?toG9Ze4;C9CCwvaBlH7aa+@9M7HWr{|U5g(sBk zO&2|Tka!laza#nuf=Uh=Kqg4w?joGn3t?|GD2mB+>oq;FfzWv0=Se!MeQ{82WtJaPWzFnoeyGH z)b%PM<@R{WQUp@w=DPDWvQ|H_M7V`N*#ks3Gpoo1x!6A>ve@o?Oby+|4C;81RtiA6 zuY(6aT?B75n&#it|Fbz#Pj zay0rX60&Op8=IORMBesAE#6B+rWk$+$sDoXVMS%!&!1{QH|X3tG{+j_JjZ36=0M;g_ZxfMDCjSQla zR5-YI$}>Ulc)X?5wRgUJ(y!D;bvHUYL#@s|5?`KjA|}fHhoVpX0BxEK1LO}p`rgvC zP&S$?ZMoa0Ae=BBqA&Y3IhDGHoXW7X=+j%P?V$=k$H2(1%r)5~>V83#<7F}tbowNj z9z`rDew|D5i!fPIEaJzUd|Q2s0I<@22%%)A0pV>hx&gZ)aOj5NYeg}`DCN&P%a4zc zU!_;OokgF`ybD4y9k}jPLJLJgSOzKH+Rg#S&rq!Nr{gO+++Qlm4Sti=o=|MTh!A%Axug zszYI5P&!{CabA=y4Zz~R4WUim{?iazsrFApsQ%S}{ih*RSHJ#`qW=Fw!*+{M2+RYA z{CA=PMVF)Hh=;~(zo#Z@-iZs|QDN_SW}O!N)aZ+L+_LL&D${_Sn;hun2)+~lTb`0fn7>Fl=D8IX zrFd)d>F8!rF!Hu33=F+e019o?U4PjS?}-9DqHo}_H*-bG+s#d|!gZ|JsHOd5o!qef zP{Ck6udHurm8|&s*^~-uP&xkkiRZWMhZ9oOVPlq1ym)RiolXMZ5mNiAB`A335f^GF zp@`lPeUy>1KH%^9Bl>K)$&%)uCQX9^|562yBjGG}bhbHgX|mtE4EJKM`9c&lWKuY- zp8eqOlm{5S+FrkyYcN4EKT@;H>%AX?<84BMWRqAqC@8ALkL#d(p#aPP$)yT-oli5K zOEX%a9oM~pUdJG|acb-6)O{j3cg3;VXR|e3yqxyAQ=^T@*@$Zm2EhsvA z`fVxsYC#+c?F>-2Y;qo#ms^oVS>J?7h`1QXs!@_aL2wpgBvA2ha7B5=2Hj_-gA z9C#hn#eWAlm@IMmF<@1k#A+~{ubcR7V+yDHp2R!L2c_y)&P5A`pm(;zH%JzCiB0QP zn_4D)#N6B*8AZ3}&8Z1gtKDCmR|N!rQ^SSsoFG{CHV^=hN%t|e#liObdkeUm<%`+n zxh8}8+Q=%a6lh|#Zn^wOu5%FHx|b}#%gf5SG~m9J)Te7?tO}3saUT-5jTaBeUXMy` zUD?>0np^t171eZnfA16Ze7vDq?AlSom0T-qyp2b0@CJYTCJD6+C*kw866*&6Kb@h= zh48Z9l)1uhH-B)btVXmw-v8OSIIt*d^OMyVilZLUreiGA&+;MRJ>1$9w4E}P8yXa$ zi#XoeM65A|XlQH6_w@=mJ+Tr^RGo8j)+q#w{Wi7DOmGnjr4xKSK+TM)m=4S=dVW~` z8dZ8!6hth;ESeYMc759y)KS4bvK01I_$?jELG1$mb?o#GmA|C;69 zKIud@(6*#_Qjh2m0N-WVo?E#7Q z;H6`VNr%P5NN@EMh6l5A)?}D4`R3);5XmZ-ZY*B&{|bScKGe+*{(&HGYVts`Kr-^c zik7Ajw4J@(PIoXURA1qjjb%r5v5n<8DL3EeWpGMu=E2^sN9RzTmGvP51LOKn&_!k3 z1m1zl1sN3?esBGArsPDWeZe#rr*8e4Scec$ObK{?$+h%*1#3dl!uIr}o= zr!zYk2GcEQ2A+PNw}~6UmpIOq0{i;}tq}Y+6OUfn%rsX4RgNLw!=Kpcz>Y7Lt4oN) z-CYpw9WoeWeEbx>fN5n@h_Cj}N0crOu7!i0y{-?k-v&S#tHl>rviC~m4))8li=wj= zTte$4azNFo8gIYM!s6m5=`tJ_=S%K-*Vgl+ri|`CH-Wk8>l5;VL08tTmr)o354J8Y z7*q;@erc@vvUrW1BxP++!IWSICMH@XTKY0RkFhinZ7+)EOvE||dwM$BdoRr3P|V=J zkH%a9&6&}hcK}bucqRS5k7J-ef)kt?QgA4LJoq!vAicfAZJSq3rV(2DW=20+-}CFy z+B9EAE%@&jBWM50`Qr53&+kVrh=t>E+vfp~zUZP}1_wMi#GJLd?~3N(}{a+H&e8DWZRi(A)-tdUwy&O}mYMq%lIHDCQGIu!42y9!?W{p1vJP5)}|* z@`Qn8?xP~*pSExCvp&n58i;=a?SW$|=Inc@axm%r5NuimaB4OLB}8V+!^ zi*u22IZ{CZPAJ|=$PB!aYM}6fFj*+>-1QX(O=5ZlK`j*C|}hbYj9J~4Qs|FC%IKqSd9w5{}_ z#nEwDzvNmZcLTPOU_me;m{<_kgFX{b`vSegL`2K)V{23SQr}viJgZ;{i4rehV}kHW ztbwkb!O2GGc(jJ?M2XAyz>10*D_J1W2_781=1WPDBr52ZiDY2xOpFL`hiHKaUmD(% z?v79-lMyI{p!3E#H94j;*X9#&(Aop#M@S~TJUd}Z;&_jdK*sD}=`iKbw*K%|@*SDr zPmpW$D}4okuOw?@^CZ!Fv544nF(N6L&cxm9tCAS3aVOjAIkX`=9Hc| z13MB8d0;3Rd`+_72jvw^J~X@_o<|M3=&Q1ft%espz`b-0)%rAJS^TBTDNa z<#Zj^S2c0%v-2}``UY2x&vP9P%)&9zVRow4LS-R*Eop3AFVg7A0xC0BEoq1Ub5B>U zQ7LvU>Er7N<;b@|+)ek<8B&SsD=V1kA;X;+n};ro(l9|9eJdwOuVsN8PL~_A8JRPY zbql4)0rC>gI$|=-{jYEN|FfH$F#?(-j?zxM_oaa!(~ehg)88Y%o70---E+pL%CJ+j zURAWB$~Ojv_0R|*7RNX-OLv$TA_YmHG^1)1LszXo5>^A0+ERt6ruGJb_GdkPA`TwE zF(FBScSZoqsn##L)7OQT8~;w&E!%wH9V(*zWDEfU`_CUst!BEFynZ}U;W9A~7B)AE zto%RjJLUxwRbwi`k6)q9rs^S*eP_cvK+@SUlsW8%VdzD&bG+PW(-M>xSu;`kOeG(F zqWcC1_i|dx#scC?jjhb8!SnOz*lAp=RV<81tV!K(~ zTzk;dZ1^FHFA*^PyPwJQMSvSWQNW%vBPlj5UA&5}Z@Ryobu*Il3JVL1i+Am%jKIOc z-MFjXdeB3=kdp_VN~1)%D`P=!b_HO`uw0o-TUL$zY0nN1$=j5mTL%mJjX74EijIkn z){DeDyH*oJTBpA{(l7ZziPFAvFNl`CepE3XFpu+fcxT0F(RP{3!p9;|SvB>+R0x~_ zddQ=e{|52hwh^>EzAEV)lxliD7`i`4S?{b$8Y28|z0n%entFBs` zO}Ta$(P6O|Z}qXccGqlet#I2q-N6VxY`!}zdafVuSspHYGHoueoM!aVA;70)9uH?M zvs$>igA4NO{pbP`*L3ojEhankn0ss^uG#Q8+}Sa8^E#IU)mYN1USD0X*Xx7=;n4-+b3xb6%l4c-vtFXA+Rmp9L zO-Wu7vA_YaYDPxT>-eXaz9Q5RhqF?sOGtI$N&ort9kec1Zn43I^2Ha4Wlew+tO%q5 z;2@xMzC39FL0EbbkQX*n07`%ta8V5E1i6il-z6IDCc!);2<*?*S`q6&t=Obf2g)SXxD8_ zviBd###!}im#kZ1NduuixAl&Fr9c+jKfgkIcbp004tlVI-%10+TOPlqe<(tSUgK9y znk0STvww(7SVlpSPjB03ip4@u^n^MrNd&yibmj^6m^P*t@1d@vIC}~vJh$uB^P4_4 zm^cZ{?#Uzv7M;EX8^H^QKcQ*Bi^>T|% zz^_LQh^co)pxx@u#V|F@Rv3>#0_kgXIm#_k5^zJr+rRqCzVo-D%e~PNCqTI8W^KE zcKbSrNnUhBcRm_4Z8K+#oGmU84=UDxghoZ)Yu7DN=5EH>EHxZAd2*E`Z z&RG8WnV{=y|D#fzc53MJus(w1GcR)|>6jQAVGwYBpypb3fUF$xn(MUP z`wj_FZx$bX89iSNk!L!bY1*BnpoVbU-7S!KT+!1oV53RclT}j(u6_-_!0n>mX8N*~ zw-Wnl;y3l7k!ii>W>{Ta9Xh`0==ls4wuFfOxGxz+Cxn9L9H!ap8Vo(u@%>ab9$b~T zx7T#LZH&~q!@%!ACakk8D=ZXo+Z()HcG+L7IwmcgtGR|r_J=@nTL(_)I06HMvTvag zfeptiSWLg4WAjJ0lB%(0u!HmUyN)_{1PwQOMs$r3WD>#H&F+s>=wE4|7oWK+V!+~@ z93w+DHC47FnRI?#5#HNdqNeNEEcuaMeNXj3SRu=$T2u{M20CK`I>n8HAn7OiX$4Y4 zY$W`%_qg8`xm`}fSR>4-Ws)j9mOCE&$v?lJ#sBk8~^iXJ&SI;K@f zMl?unnxpWnJp)rhCW>~yhW5L!w`qde5b#dD&#eh zKHGGWxL=@si(q14B4bfd`aEq45vo0$Yw8{Nrc-W<@GI$Eu8Xp}VLzD9^&!q%yTwkL zeZek`t5x-Qp{aM0Tp$yV&wK5SwL%jC0n?Y}oz2Z$AeLW+rM5)$i387f{#e zBNCDbMIejSbaPgge(wr2K%#1k5&O}0qhcCj zI1c*k*CYa(t>#44!cxBFeypr?68OQCZO2Q9oeO1Rn0gA>U%tvLx?_+!7N`tpB?6!4`{hl{@{FyE z3xK^iG@L6?&a${Ba12zTj~u{5`9`0U@4F4&(5T}2A5TfTwcT*_4`9!GtX~N3IjV+A9R4 zI!-$B?_CIzCM$%>;K+Fwv(I9?#{#Z_EDID|S4GzDa=><2X%(`}RX#dH-%(Vs>kfH~ zc!2!e;wWh}blLJwvc3lTwT~)}u|x%SY%0mgNl-Azh`BO)B4@y2`LYBT1f_Z)Af5V_9fnX;Uc~5|ySm^V))PHAWHV#MWJc%A`^XqMIc6LYQn={Vr?-EhfVtk=JyU zsFo|&(V+Lxvr4&_*JOR)D|>OEc6bBv% zy=vo*C2{};jK4^by5&hsTHJ!j3Y`~{+%lEdio^Np6uNvOc=P}!a_JAgnr(w{l{tNB zbLy#i{LGc=zVRIto#cR5IHUF!?7H@Mvo!V}gWF*blJt-}(ZJCgD_D?<=C_sL^XFM! z)aCkAuWwJn-wFl?0kmug1A~JybZl$|WtehRX7xZh0){1ecNr41i%UVgv2;3_?!FW5JEiy(Qd z-3O^t!9u{X)n{}=ng1el76e2H?tu7%AN2KrdxAFKKejYwq6OFpzyktacftTVm0_UI zf3hC`&BBw1OO9X@4mOFl`|M2fdEP;WK-d#(&xgydaRFWXo4U|kEL&yr?FC#gY_#$X z6%U24;f*r0qs2Iz?&c;QdxNl=wXpT7)~D|0%5vA5kIWYZs2dwx=dcI>c%iaa$Qv64 znZ--4CvO{R@%;{<7S%ZGb~yMz`U_mcYjfGXV}f?V3pdma9M6ifVN^8e^bBc&|J$RH zqfyvX3Kohj^aFt2fWSOp1Ve90V1Uq=1ehC=zeONuJOa!K&)*_F=|>m!LIm3IdZ%cG1;wc$km8pPGRX!JxT51f`Mr9Dm3B%88x0JQ*WW^8T0kWq)4?lxJ*+8q6ei} zr8;#5t*fy*$C=(ZZ!0UnzuqgZQy&XDs(huE*ZugqP(UrjO(_H{CYzXL!D2!p=+@XU zh^6?3({ia4pO7xGSu%K4t3*OIDZ#&(z?}c zJ8kV}A3nFGius-NC5c97In5L8jo0nn5E$5S8&tyUsJh?;a9C*en#q|$%+Oojsq61M zoU2O5hQ^JgwV5XQpF)3!gG2ZuDK_P}_C6%*dz;N-W^HT7#sINO+V~j?;g53^l&J=N zDH+$CYK{iW!2TpCN+Lu(Fw)9gIcwaOBi!hJmJrjR!KqdYrstNiH zB6JUd0OH(e^o9sJW*Xp~-im%3OCTPTwkm~=7knjV7Z1PHDk#Fo`$}Z!V&yANU%@VC zlsTyca`@Mp{%^YF8kyJQQ~Tx%+Z%69nIHSDTx7j$6T{AQ5h^WJyzlrCTA(#YnOQ@X0rm%{{TAqnfvB+W-N|d{ zmevDGS&Y|f#f;xY?5ZFw4Wx~3&E+`3+8el&Zmwv#&t6oU=7w>V})GDr7~fg+6e zu_nW`Oy=)|tsI9qUk*W8x%8q3Pe)!c?x^e;m{}O2TILz;X_PN)Tc4^eiaVYVEU$YI zK+PI~nTz7(XRT@yzN&U!yPq}Ep1HSWHv&uI3x5xN1jA2%lM1aAttTdI|5tNs36z;O+aSM3y_poXgQzjk*uVQ6~!ksx*ET7`JGuVzjAo1h7_vO7Qc;AD2I__P@0^PC}q;N9+MjU84*jBiQ|3I7*rO!*OF zkL1vcyf0PYs+Xi>H(|je zy5HI0G(;wyi`*O*f;Nv_~lWC@!D$1w(&8!IaD%y!r;0S&LHohUorDrn9$isHn8S%ARd= zzYa_PAGFhjUhO7lDMh(G(8(2M87z^{D;{j`i_(0G-P zP}PYF3?A6z-P?ZZ?12x7vYe=xA8fEI)Onojh7XBk-NY*V2@RA(!^fgbO5t`=>)7Wy za@s5oT)ao$^Danp`d^g21z42L_dkq)luJr14bq6Pl!Qo1ODQ3e3rKf20?Q)Z-5?+# zsnQ6GfOL0BBi%^9_o~nHz5c)V_kI7@%U&0I-}juIIdf*_e9p{0b58VU$}bx=XEY)& zJcM2L@^EQHeTj0@TQ5Ct%NIv~j&O5xx5nmB0dj~j{v(H{7kHkh59C9VVRvK0rDUVH z^qUn*fFnhFPwZdWqmRicI9R02+tk>sXCA~Arg4RU3HhPGR-e=uV1Yr@bC%HxO!RJS z>#@A1<<{od+s8Q<|KJ;4@qT%-C5R5XIBd*v!`Q+4VAC8xmN9-8TiNRNm)yA>D&yHv zL@Jrhs;nO`fLF^46^31*aFr~b7KkqAf|?kxFf;BlHacFUtPVd8f1Y5r=ik;Q8&wUb zFM1IbQ(#-V&}ai-!gR~dX23(}bm>ONHNpAa@_aW1-DX#*8lGoDea&J%@hiVV?hg$O zjc36&**Q6P_ZJxm`4v!+J*dLDJ$0wyQ~_^vUL(vP<~gpBbl;;VuH~;AxCH~|YhARk za}p~bd>{fK>&9mb&*cP{`F6T*rUN2MoWT0^w8p#Li(0nudw2hseYj)UTrIGxiDwEv zeH>cV-P_gY1_llytT~qfmF2zWHfZ+cr36tte!X*AgAT`TvYD&8-uIpRzB=e6tTui` z77k@vmQt%63zOpo0h+K41^4zLoQ-O;i!iR*q30C95_6)C&eZ82SjDe4u=JR{EqQ`{ zDiGj~hBIl)q!K%^TD`KeFaS1PHpg{vwfURJtQ?n)>!cO6gu4p@esU-XMy(A8=Ny{r z{y6JB9!1B}YtD=6T&dMr#(|jl;{)$}644<5r{cUA^{1Ozs9)zhG6vC&TuTn-Hbto8 z?wU8sFv-g+YDqd#_?pweQ*Y{7aTm-j<)p`CPY6EftY|3f_fWtug887*3@?Gz?jh9+4^v8Nl0cctwZkF%I`t_1$@ZMbNaj%xg`CTPj)t3{hYI=bt zZ#zA6RPN!ap)w(NPG!wgBDY2XU<-)U5R6omU%|j@ zhzuU%Ncp6MTiQ$0MculO?TdaNdUyUFEZ9p-LG*SxCZ`A`ardS-at(S$yd2BbdYSH}VLG{&h~M+qk-4o89IK zTqogk{-sh)vB8wD&HMJD>5qpWE!thKHQc)`3E(o|*F#uR4B^|ejczg#HBX8w=bF3$ z#KX~m!K7aV@y_2E>iR+*lx*g<_QKgYAGHZNOqXUy?*}Pr`9f?DhZDAjd3f3d?@G5v zcPEFuOe)j8_gM}x_!BocDl3)G=P`9uhOiZ1iUY7RsuOUpN%P|Z$2RS(NX_;n!@BEP zYiDl+H~G$0zLk}LZG+31|J7NZikQI{OKObUi(hy94H~H)=PKFy`kSl1)~vLdn0JUX zgUoD9x{Cn#kyJis!js1W>Wvf9QvX@dms&26>>C3i;;`Yjd79VgJnY2Y8ggVuA1?TE zOLoeoD%D_)(AY9zY`}3dlM=HS(|8+kM9UD)A$!@rvYC_VmGZf=w8bv~qVC>J0X2u^ z5CXzsPw3)Z6ZN49tn|92r(2F2_$usq2pW@B$?Qw6CNKs=?PFhPs%h(J`7NI|jcp0s zTLsQu_vM!ebWO6Be~|pe1Z-I{X_r|BZ2_i9O#z-h#@g5#3WR=(bKp!HsMn?he1;&8eKS0cpw`_ z9=7o=;*Dk+wb=^=3Mp;tQ>jknDe8U$De&i=r)u?UeyGq{xf1#6&)8m#QO#vdh503C zHfQW?Em|+$qullYZ4M&ItWrQ$QqZx zWtGJ#(dkuv(o?jB6kx4VB}2}!c2ntaI1D~xihuPk~vB|I4G zclNYqi?U(5;d0}N*yJ}mD>tr8T>Ou@JvZiF9zJGLvRqB4BGnOcVCU`apZ6;doI?6p zE=Z5p#6M-QIZl%=0=k(#oD4mfoea2|9Vo^9XkJi%9nv>E8^QhD>s_VF zA;0}cyTh$&>K#FYo90-dS~7n7M6scX7Lg&E{F%BqwM)4Lgu(Oq@nKx=@Ppx(KJTsS z67-K$7(f%hrOcQ8!Z?xG!i`e*#WdMFChB8jr>tDm9K~GGWpsLnuYwLGCP^Y{gQR?HGXhV- zIGfLJWaBdfiSPv9rFvh_Maf2H(L5I5c?>7Qc(g$MG^gSNOcfUH`c4D{z0b86D&%qZ z8zh;Mg)FZK*R7oUj|lPgx$k=)2S9!h zEhaTQhu*4A()ACq-IIL&?1>34IoK?H!mpGf1`_ZvRh7^r5clB(s?p^0k;8@njIYBo zT026*f*v?JI>dO!uF;h><+s#s8T(3~dsCF*T-yGhuq%wm>XjyAAy_rhx2+NbK|K?|-ePI5?ofV8p^e3DxOO`e zqfzuUw)eX>;+s`Rli;fh5s(Ds8y4Y+ z9?y^M)t?Q*y7Z2Xn`6K?9+M9Pad}QwV?aZ;#ak&JYyNU_bDaDpdg~2X=L8_XCbV0jpndT zwSH=$=@$duhtcE{vjp8Z8h79wm4k^;#t-$^et^RC{8wS3F(tAPqq4o@+-K`7X9N)f zo2q`L8qephveqzyUI=1q?eAd7r~W{Ng3fa=jfrZaJ89K(&?gMhj-}4x%s8>|JE=Y$ zAD+-EfLDS;O}8G~^l-5f`N}MhfO8pEl70y2l$y95c45~%_PC(J)2=OFIDXW1a9im{ z+Hd0}Pjwp{33%uH^H>`}F{LhqJA_;^()h_)h=JLFnBowBYKM!HL7yAXaP?It z-STzgT*4QYH%=qN))~={BRG%y-)9NFE5{F9xX?tjY@&nf+;%CM)Nf}CfF1KF&}epv zb!Pbjl>AVN5n z6pY?u#qkNpgJrD1(wK%P3J@qA0=-w$;?qi}jX?J^Qi2#q#|v|ymsH#<7icMr=<(@G zc!2_?TiOztKddjP{x&6ITC&bVGQRii)Nubh&Np3d(iA2^3?zd`2{(L~Gb6aYb5o4m z`A4wU=NT_sBA#)6kDHL*XNqn-FQz^~L16hDkZldRnYqc|+Bdhf2lv)$CI&l*zNG)u zdh<A2Tk_O%j zcxbK#HF3-LFgqC7SE8qxRwWN0Sdp|m?34c1BX74B{2M?UjLd7I?)JSNZ^sQD3E^W# zcz;4*15!s2K;@oEy>2gQ+1KB9#m%LpW(LXQ&~Kl8zA6Q_^dTVf4}g1laO}6X{DDb= z*3}=9M7Oo-#L#z;_xnUpIUu#_2jSj5ddZl~GMjq+wV($`K`V2O4A+d60JIKj3O4%lOA4=nfHljL%-r}UN|1v zTX#o7+wa3km~v7*^2f7(%~t-3_bMR?nqpmWxh2Bjx7|2b>O(wW#`!$*U8v0DFQ2f3 z_U2Cyp(mgHLHQq>`8O)fdJLmK5ZbLiFG`72%7=~RY2h{`juesemORXal%|SUALKXl zova#E2${C)xy`56E?@^f_z?fe#%A`Zh{UoU3n+9X6jC1vVMI#E{=(6Z;=B1n?@(hj zFY)W%S9KSQQpPqIKiRe(G}tMrevGF`p0P{Js&)P5a?;qxT^v|HoD^@ze`G4n!#242 zfX-k?=WRp_1 zd#R&!S@4c|uUdO{eDrgIjO%&WLJsV%Q;=Eu#(a0%*m-TNP!+AOpmO$`}QW``iL#R5Z4@Or)DK0v#98Gngj(! z+PwWobkZMVW{90$45q3IoZCiS#-ybb%3QK)l0Ed)vJZvr-HVfq(FfHph1iQ53?&?m z`gIU#ayWWVSN87lcVlnVs+FyoYVpgqVgmcX51tg$X7%obJwGI5jw7OcrTUyY?NWx> z&;5bd0CwQ?Qcb9aq_inTMm8TB!-5y#(h{sT=T4rz6Sn)%_lQ7K`;Je99cErHjxCzG z%N`4)em$ld4BWGCg|*(O7|?qB5`$0C7XJvFkNd1#OJ~SQ!vLtay!*e03Lo!;`)wippwYi88wnxZY4xi$L{tnCmC^NQWRf))IUlANvq{T815^JVjmgp;JN?f}TyCeu zQ^^_cD}|#+eB>t9?K>UWgNa?8G!VT%gw_Sdp(IZOo&xrlckCY>I3{p8%4M~5xMQ{D zZok5lall|Us8l0;JjdEBdji+P#tYfowX3>&K-WmdwLnw2j#;fTv7uH1_jBY4#)b_y z!WNsz6q^JWZ8wba>@O=;y-gk@d{YG5GfG5H>?qiS-O0=a5U)xx_w4l%?(5B%1Q?tI zlu1tOBkg72jJFAZ29jC-t%4RPyN%$q<2*&HW)w5~vjr*Zc4-n1rUKZ?>r!pH!%Du6 zo4?X*4aSe~!VYv76FaWuc6a>tnLy*bCrI-1ygMqjCl=cBgsC04eU)>vjmtK^JrZZ4 z-7UB(e@fV&$n;a~bHN3@8s^Eu8Rti0VdX&yt)z(kzr26gWBszUM5s>=tvEbWtS#vF zg0nR9#`7bvHd@^TVV(D0IBZ25J%kGKanoWP5bkwRKnDu}HCeg+yA$~?oL|4$?A@#7 zwKjF!w>x@)lfF}%G@;A(nVd0+zbg*)*BIe}%ZpR<_fiGLeu4&uDy77i?$~HCtkG#j zP)Kh8&xuG*%2xum(Qlbx$N z6ScK)od{s?US-#iGafP>;O@IPvT(~KZgf(mdZSK z9z=O}=4a+Be-dlrg}kMdyTae>7EjN|Qbf`w-`moK3v4U@sA<87!)#M(-7?lru?MqM zt$I%VQx9Nh-|-;E3MRlDpk7ZQG(dO?W0$?B#{T7)*ARHL2RxI|mg@smWWl3{SDwzJJ0pluGmo?6_z%8umwp`tc zp6cw3mqlYq{ry!q092d&pT1ljtM}u`sIvFsc-3E|+pJ2OAMBEsX8s!G8d$N3L*124 zYZFp4I{`4$lN4Io@^7KVo}P6%36N;F0Yjf#_wnMi7m^u92l2(8$HmYmG+VN;N>s8( zzb$|e$Oi`nY=zAiMvW8tkH}xSYfYcN$LXRSAS-P%$n1Z~G5Wa3qN|@1^>FNlPnur{ z9z{!TcE3NlL_oimXe#En^3jBAoAP#t{iScdu;3>A5!XL9xt zdo$p$eMg{2MfV9$boyh<#fN4iytdbAfhjjDpMW}(63P@ZA);5>ew}Uj**Kv z82&gPwUO!3kP9GrX<00z$ES(c$$Y+HR_LgXZFb~?+3}%u=i`hr<0^nT_~(}&#SB=} z<@*NZB={{;%jZZpRqI{Kl&i%M zpw}R|5Kw?ECNLthso}pSV;so7CYxgi2--Ag!%5zv%@P4f(psWcI(j~Cz(p#c|Ih<_ znfEE1CIKEOowT&k+^Tdw*{`>laN?#eGoAs|Je-{6x4k*Dqw<P8#Bp5PZFJM7P8v42F@YFA2?IHhGw2~|sB|?he{^eJw7Haa3hRZbTgVh_p z`LHCrsF9<MbuZqLDA3iItXEhEOPwP9-<#`84k&CrA2W+oQe8Or0d{X0e2D*9yS)8d|{We3I7H7Re8v0jiT zZ^WSyJ_mpFstR$%SBW|kFx0+$$=mclnx6`3V4)^QbHstOXGPT19a!#XW%Weif;Xl zeB$|s+z^;Q9YAh~B*&5f6JWsn=|Na%^jK(!w-6wa9)$iU5sVPW0v;iW#s5ROP4}N< zpmcI%nbC3i#U7Ps>QlFg2Soz^mw_ zEGy*GZJ>eo508+(u2GEbxCje=(B)de@i(H5@(ag@sO|DS&?^S@tDlh^F#kbpiQkom z9|Tjr2&!~_L)Hi4{K@tH5sR_Vy~yzN1#*bfrx>?j#ni;=3^&hMUqr2w8si(GD2}j; zGvCkFKB`)suU4Ao)%|?cpQ??KYh0@->DJv}6*_c%#K&{7lv;~zIX5xErq8Bfb;dGVfdT{z-cKAO|Lp1Ots zK12N-Kl^qffF>IZk^Cc<(w+0EF^8RTmkAR4`@j&`qLL=66eP!gO)I?~g7}7ZuJ&k? z1?1s3K2Am-UPKn~r;muq&o;5|udL^ID0RdWd8^tdtfDfWjv z?&Q#j1NvJAE3zwspx$nZ%v!GT^tGnSazf!wPbi$gHC$9;!3U;phs(G{&x$;uW`ln6 z8`-A26u-70jrYkb5lTmr0U#0$U7vXT;|UcoDzzBYJrK?lhJhFqJHZnQXZxoXHj~rS zevMB{`Yz|@-`9QOx9t-Xd4Ly9M#J`@c6b!}LHVTyq42UQ+lNq=tAxz3%X^f$iGG(_ zM3N_IEazBK4>#Q&8pTJVwPmfd*E0%-kd&C!+;tYrHFQD@Ud>rta(Nx4auIo&YpbJ) zLCs6MRr7V|{t=-EtxV2~8RF_IyN{}mjkL3~-KQO&ffbCCf1Nl&-_Ybx*B&o2B zU*G#j8Dtc0Xce)4fxr$n4kvGIA0@=lbZf+;`a_U;^#0Cc0T<>^4f~@W zh5w;aek&iO9x=BT>S*BnPi{k!V^1}6V$`sfzw-P?t0Ikk#H>l zJE-_66!4b`Ck01a*%p_%9y8q3sLihC_g|yGq=&C^2=X}knq|v3`a^ubFqF&}TQ<}) zs5Lw2%SoIke8xUu>YCwalGE3{FCZ~fRAN{Bz;Xl!`rHpti%8BUiE2!#DKs0Z5>H!l zZzSDnM;YhE!Cu3s`Favek~g2sTCF`oZb5MPnf;=_k;uM8O;Ut0fy%Hlv1#T?Tg0r?!f$V6eDRm--&^sjbj<3 zh8L6waiqi{u$;KC7==E+vEuPPJt`sO&oZ@1&X`xMJDsg>O^l1VwCB?*@l774a{Pk8 z9A*N5k?j3X6D_9-(XX--TYT4`;sW@lEfr%9&N(8K=!o9Y^>Gfkxxj8S5iI!nZ!;c_ z*Ye-)zr%aB5+sY$6!Rj^9S!>7(^8dopfKk7@-EjVWPV$@BXq2K1{KGZ3JkPpczMyE z-mr}5|4}8Lqgs^zNR~0jSvsyZ3z8eOc~r983cXHz|HWs!hH)u`KTtZO;vKK(Onm<< zJlJ2LN_O)!`VSBR4}lYA06xJX6@dgWdgDJW@mm`Itpl|XU?BCAgCrQ)XnzFom23mu`&wJ4j-NTL z_GdS1-Q*(%M-`OjhLr87_oJGyxAGEce){LD-Qv<}lWJ5v(G7qMVzd9@anQ9))P&6K zuHHnGtFoN2i@1h&?v=92uf9hy6z@DJm`H=(xADhX za=z~_1z-3z?7&s!#lTTtrJ~qmqS~_iHNOhei>b6qzoDcNUXg0{GMa-;wuO}`I-t$M zi@1^H5{l2c3|QbND^6Fq{2VUJp2}RK0QZi>vfj5smC!N_YGqFv^rV*8AZ0fKPQW@TN!SL# zN@hDDw?CdJ@fp@~Q9|YGxv@BR0L3=l2LNLss{Ad4Ae95zh;Rt(Za0*b#}-ej;d2%Q z#(>*UtIVW)KJ&KJU$l5-iKTNK>g`vGw`{-y!oB&zC3ZRZeb^sSQ)6-kgO2lCb@gQ_ zX==me+QS#NlqA4dN;F6d?PYK!zwOU;N@3O*jpJ-#g}b-X2=j_!*6y+TVph&HlE(KB ze|2@l-_|hR3PWf4xEKXh9i^iHERX5l{dAqw#yzTzKQ#f-3N#VIb`o3YxF-1 zPGqY9d=MZz0dim=+X~}9l)n@ITC>mX0QK;np96XQRks%@b!8=|yGm<(KgHo+W&S~t zlpJmPW+F(@=p6<^weUo- zK4AK0=4}15{5`rWn8VRk1@zt7L}4sbXSy5uVIE+j@EtX`6DmFSx+?qh5n~JI#$q6P zx5)5ljQ{A5$;$M2R8@b#kfQUR>S*wFV`R&G_;pxwdVxiIjDk`~bllDK?P|8RkIMA8 zQtzfu7l6%!CVxN*8p-~RAZTd%^x1i9zFHJyep5d6K-&hbo6p>h_ALGxdiDgk`lq+MR4KTXRy%6X_7x zrDHU_3bSV8RsrevF}n4zr&bIYJ@H< zryg@m#k_D&5OU>F<``Q(eAKsVQu=C43os^2;1KmhdK~UT_J)x!zTjb#D7&h9afxB3=V0G$l|Xs<#;e~0x&v1dIb(ZiitNm1k3v|TREx&}y%4?B&W~M= zw~RT%gIS#*nO}f|?c}^f2zNK?GgNznyA(d~IRA6Z!`w~f82?DQ7m2(_KPs377M9aW z%T|xm2=B;#*>3#8xyS<2o~>qo)jPxQqc$);Ob8akg3>wX{#F`$$HY((BX%(J;J*M2 zn32gC|J!Wx=f}{0O^(0R{}*KcZ)!rOO8rE?KY|{F`FGrx!s&L^ifn=@s=ih|=B9@o z48cch9v6OX-T+=j{u$!#em>oN_?ITeuRr!T*RfqM-0#S}K?U@S;O};aL&C*oqlrvD zXccz=v+wPQc)0cYzUpe_D!26L>n0aJQ5&JuYB8&l&-Gc5U0q^%?a0>!*REz|OQU1n z8cD+Vcw4a-6b{2l);e8DN4>@&fvw1&A(204v>WE8xw$ci71?G`7z&w&qg3!aM_i!EZ<=aQ? zVEBL~rMU$@Yts-l=kHl%^ZSRa1r`fgWZfJ6zZEAOvhU0Qs?+@_)*#Gjq)M{{s10V; z3e}K~zN5CFq0~sVZ#XwEV+F>UmfMTJ!rN59TGu`myY)t7M{4hv>URN4_h_!hGnTo` zYkae8JP0`(0JLu*B{X^Gc2_#4P_yIH!j@{=5vFV*Thq{ePFyX^uiHHtd@|swNFiqn zU4$xJijCklx0t=j&EcE(SqKR?S7voa=LsQFaRVbQ(-d|v_b5eoN^MiJ4&9;Upr0eY z_Z?}moeK*tQ{z;|mAe$XTCx5lIp6)q=UBgOT1fv6SS&b47_|3R=DE|zC$@F~bpIGD z?-XM=fd`Hu+#!y zTzuyyPkCEx$Za>FFa{{Vx_pN0P37xdDOpCgo)i!JNrRt7{shlfDTJSL%SQ%Ih3*QS z9TEDd+ZqFtzbK0yLMUs*B4G#qmp8r7A2|R=www$!P!K4@?WF+{zO4kwhOZ4>)-zci zguB`&K6?OS067(|)1LXeE^mv1LLk6vyG%kEu}3iA1s$Obj2ub;e+HKM{Xx?5B7prF z?*OWB$YXhJ+27A&vD&cO!huz541gH9^hXCmK#3Lih%~Hza4wNZ|!A0ql?%=#aP^DWU0lPzOfKJ zTPMOP=Aq+Tx6l4YdA^k%3u7#ODaB(;T3w@n`2$Kq79$9?*oXy@n=+2P( z+Uk*+AjM3m4=*?(ORU*1H$)?baK6SX(6LFobi4))(dgodw%Cw@czZMx?B6)K{2dEZ zy=Xkq7uQ40&aI%jE@GMj(HK;;b$mg2^9(A!ay8AoeSW9?eph~0_envcAxn%}8Gn+W zG;scv1deub9maXBUOK*muT#NN9SnFc;aIm+{o~aHE$EJy#y^+-fXl)hh+$HYKf;jj z#%!GJS!Q9Y5{_V>+K z7biMD#e&1BE)Q-g4}SeT0z4YuT3KG5Pb_5l9T|JP(a0u%zvNZ{-WUUG%pwN_a27?a zvZsDOi-m!Wqw|LvTcbZyj*>J>r&=Ul%aCT>iUK}A6tQrlUKD=FscG3}EKCJkj{BR;L#Zf}7 z(ik>f4}Z5Y37lDxk?9;87x?bvb&7bS2Ob|PY0mlSX+POBVRs^-9l`gNwT61S4~yUNh-8{zWxzz^@dak@&UDaOL~b^ce2F zG0IkXwx1vxVxnDDHbD+VRxsmZ`k(D36OOav^R4FP3%AGET}2bNFlXl*w1tJnp28u~ z>8%=%@HP5ylrTUltuK3ZK0QA#0>T&F{@Q9ejZ`MFb;xj}B7jwT%y1$Co1SK4F`ztf zI?J=8BkTbD%C049F2DJ7WWz-OL@+ed?C*njFlWMmExCtsJ-4I;f_KQNwA3}x@5Xo@%_hCm=?^eES}K3(vAD^ zbOV^AdLW3-=XP;9aojaB>#MiVs%P|a7OP?pyErJhq_QrEJ4``=GA;Ek)ZT!DO|m?) zf1&QMuR-KheZg8{&EDagA^KM|)x*WyX#el=*9v9ZSjgsIK+z85=T`#e{CN!rvHwj9oC%_Oe6f2SOjWAeaOc+X=VnCTjGKe!R(}%^H0n}D z)%8WOrKN*lr*bCX);EV^BIKnRwV*c@m!{rJfj1pgK@b#zhBIme;Sac6Hk0}?`h@Ky z;?BLaeU;}V=oPNZn&XXSGXYEw!*zoysrd`~(-pt4?*^()%e7eSO;)_YjGXj?LDQSd zebzX2yBqB-C()_DcMM|6ia@(TK;9Y<8NrQfAh>B7U&g)qY7$hAmiHcafs;x$k#!7r z>Ef##4~DMdNuO#gwA8~XB_tkdmbN_e#L{i@JX!70x+-s@Yl{pIiCdmIqj}cJEPlhn zm>)a1mDJS3werw~f--9bYn^^kO}ah5L#INirJeoa5W*|ide8q+g>xlemHpcjRpL`l z1@5LWA5IZP#$#d3xBGMl;>B4lkq~#-cg)xqBiirF9)Kq4OdlnS%YuP(byuZ7&{*$d zBHk%J@2_*gWH7`nkax4dY~7-gF?lXxjaw3)fO^MmkW)_BvSGdRU6QQ8Zk!hw%pe|T_eEMn^e0i> zH~ZU!5bZ-RORQ9#nR>__yZM9fKNoe&t$%nh&m?>>4!-yH$)T6bve6ex@pX0oV^rv5 z!z<$I-k=s0c6`)#A&T+T1We!5r@HoSUN*#D}84E?w@L&>Z_gq!e1oy%m6*d9)htdsew+_T{c+G_dOT4lfgz(c90{eMSnZB?sdTKVm#jY=( zsvtK&;llBf0GZl1Z}KAyah_RWs7Z=Wyo9xWxtqrCc`wuaeDj0~TxeH`Uu)C3d+i7s zdTWaV#j;=w`1zxxp*n#Mly2>Y#lX%?6N-9hI{Zodga3%Y7P&hNib4(?&&SdK1(-sR z?FBUbF}C-TYVzbN#Ne<&X}WC$^qWi9CH7-@aa)u)_{Ax`Bo?g75e*86Yaj><=S~Z6 zsWAr%W@~Lx{@6PRIwWK;7yqs+{p*oApwpe2*W8;BEWmho2ik_6^OXyMB8)=1utw6@ zcL`i?iOX#Ss(PRWZ;FG})8C>_$*4&xg%H;>=fCGhZSI!>+af_5dYJ{55nSmbv=DM% z`89Okj1|#>c`l=Bl*8aG|I6gQdebXH(b2yXel`*navh&k^|0_eilET$5-OsC)if=Pi2X5@-3UtEyMu8(qH2_7tRq(# zed=WXlYRBq`gc?fFT4C9EAkzv)&;=S;)#QsGsGS&uO|q?*p7?|=tW_uB3z~i)gX1C zD)*q>K30NoMfr21r#`3NkzyFo3Qsm>l=vSvA3Osw2Dc!90|P}WY1Tc!-9m(?O)Tdo zKj{m5 zl7{du!#l{6Ud8U2!BLJ?QIoRwjh=OApYHm5ue2TYinB!N;Mc_w55bATp==2%*8d4z`V#a85cFa=X! z?cODV_rvowaGu(?^&JqrXndmb}`oVClHeOk*Cs%4b9d3cEj!g~twJ8+D~i2wxoole>>9%fN9qd)*3Vy^uKI5VnhLhaA?hBeF(!spL! z*KUE(ik{1cKeCz^is@d!VbVsHmSlpija3rK=%NVm3N{(}(qbW|-4`wNg^`>clLPHP z)Rs6UVhrls5;7BiE3ms1f$Mj^Ne$ztniaO}(`ZW&YE*7}LL6V2IO|E}9o#~|1oHto z1Z7FWFSQ6UBC%)@4fM@ef#;ILgCCTqy7FIy8j!2E78x-oi|!n{l2E*uzq4e5GmBM< zj*6BDr4nAd32v(X5kaHXwqfwvbUx~@rJj;f+1uMY%XtI$a`s7hQY^qGJ+fUWD_r~_ zDo)U@*dvO%^XSxvz|<3m96ngQDEbzueH#H! zxHz3V5i}Z#PDT!ohGLP!32rF=Ttq`*v`C8IWT1n^BlZXN(qeTq>b3k_3$k_OG9E5A zf(ErD2yLmR#^Ns#mXdJ#XQuiR)-Q?~8InC)KgvIsA8^&iDat9zuWzRBjirB2b~Q0p zbM9IO6x6D?Uz9TC98VrW<{yw28=o2zlhR(LW_yO$l`YT}?cz=la9aZ}SUcs+q&ERe z#!BGj<@K63ViC_0+eS3^1tIwYVxE5g9paVH93VkPfBh566%Mcs+?s&@rP0^u>+Ldr zqw-OVkgt|4rS9BQ<76Qe$RmgU>2mCd??`HFW9@cWR_K%hyYnc$3w|1pR z%m+4G9I!-rKtB8czaXjq<6krsxCLbWul#vH+`E60toWOn!P-k@D#bFJs9+Py719-#7quTVSjqfZrH#bd>xU# zWRSNGuGITfT`HiAYU*XABa6+*%iv~n_)CI9kDoIc`uPe%ww(bE1b~v zDvIUOwH#_<`es6auL>$9l;zrmtkHLf3Aj@wUFB0_6{pLY(hVq08bRk3 zt%Y03*(#@_UpTd{t)d>FQ&7S`E0S3#9SEOj&sfL>toc!XFjjOVCi) z)ErBG#$!AD=`fUCGj+!6$Nn!i-fftG>5}?p8qWP29e=5a6v*mi7q9icxPSahowHl? z^yGL+Y}(2lSDfr?Ah?LN?fJ*bqLj=^3n5ubK5OTfPWKvE^4|{;;S>&J?j5JirJ5L2 zz4Yh~7g~939ga!M7ry48YHj^%{9$WU^T0Ko=nwt5@LTZ61x@x-`F(x6uMa0$l5w#zx(P#6a z+?R#QTWlfGP-haL;|CJH3s%X5f(l@GoW>X+lF7V-75XZ&^H}twRX4FGPWRHE9Rl&7 ztChqOyiHjeF{55f(}FFs_#R(wK%#Q~M(tH>3bZu!mhm?6}vIl!p> zmM#|;KckJi&0MEfH9{t^IyrXL-qYu5>dHB^-%N_3CK&ToMSioGL5Ywo;JnhC46NEY!%oP9T%+{mquwwxVH)~&2v4=6b=&^Cw7Gt9aySDyQ~sMs zeWV6zqeUvn$F93V70{kjAl{H(z2nnS$ajJ7vVgb0nJaz^28M^1w4IRe&*!lR%EY5m zaBFBI9~cisory}TIj~XNV(jeT)$lqmnEg;4IWj#((e7IYc->Y%m7AoQK*Zf$npJBv zL`8ooFR3_Qy6{|{1$7q+?rnq2fL;@Hr8lBRMH_+Bd*XvTMxFqY1x&xgG5gc)sn$Tu zBdxty0E-5YI%I`}d1=UrsT@Xufv@_?z<&x9?rt|%*ZCedmemQ(qoBW5_$6wWT*hM% zoU}tZ{R?os*D16X_Uv)`+R)jXX*w4X$Kn*emMsXQoFP%y_IViFdkx)q@~d@Xw_P!l zG?f&MmOFLmNocQ=W<}ffYe@sdt;x|0m8F8$p!Vl{Kj#BGpW^`@e%oq#z482T(JrBu z_nHC6g|=&E_9V9SpK%%ZGRqvQXDoIoVde12Nx`O%>?Yu52B2#sbDwMXTa`pYqZvs# zwf#M!MUwA}e}Py8lKUJ>c-&cB*T2?&n==Bx^S(y9M-xi)ei4ZB@x<;kN?oXcL~_=K z4VdxAn}-uG;@oJz>zMj7RX-V`qlr1*;I%(G)%)}<+zhfFlZy1QE0n#(LCn0v}|UHK5Sjvwcp+QL-aC9K&ZhAUXD`sJz5WI5YT; z@oQjxQQv#@WM;3~P8XlPFQ(j%_^4=+EgU^si~{hij25IOT-Onkrs5PKR^FL6E9zZ$v>mc=-H@YRL#u^sth1}o@$n=pmWj8ks<+%HLbS_M60MMbNbZjF-Qi)|uu zp|sva{_u#aW}Df@{6lXMnF0s_xptf5Kh&ZWeyM1Gy>%b3fqsVtYA*zQ9RRU@h_imL z5d#tVfN($Y>3?oiz5g%N{}8nN%KbmH`6ogAzm)@N27>#E1KzIwCjWrw5*K+kS5HKCo&E z6@}P9b0?@r(|SNy9|mN?>I*Ogl!D~zPho$*kPGO5&+|X)=)a>D0#eHUQ)M7K>z$2M z9;@B*xWc4o7&Fs#bZE5Bl3#2V)Mh4u5Oe8=&*d<5W=2!}ABL-;iCaw|iZ2Q>=bH~f zbqzIkzIw(ltva(~)MjQi`>~6Pt)82(h7a8l?>A`Z`ILHEUPS&2XkW}5^WpHR)%0jk zV_m)mG#n45N?w23zsAk^=K3A>xzn&o(7`QgOF(@OTLbp_d6EIZAI`F0KC(4&q=1-Z zD8z6CP=jp9b2;Qq$GFXm+roMaQ%_=2?r);Jg0jVFN;{#IcgAN=e!b%zXfWSAPQft- z#}z7n-KZ+z(TXAS%PQ+BtKS_N_*&ek|JidVq%h-TtVS%p5&}qv*R*fyEW6FX>nP)H zSYoO6{}TfQ{QI{)%jbfHixtLDe)##<{SH3-qffucK$fpu^~KlU9(d?czy3aHm%`Yv zadTSjgO5M6hfz`h3geVB&VBKf*9RW|W73o}&bi=_Bah38c(S8%@Q7ni%xaqB2ON6T zITu{o^zW5E%kA|y-+s{H#~gO_@mYg&WZCkS*%vwVsN?hIx$~O){7X4;&yR9-AVZwpH?z#05B$+r-+Z4fiflz>f1|a6q3!|#0ZkNJnX=@+2hmjxUO3K8$(=)eR zyz|fdFh2Nr*ZVN?V%D5_NvzC_!YCsEg%RI;|6}?f%Vh4&+_a^;vAH#uMSlM6kF1)x z9FQv=k3ap~@P(0*IPI+Sa$Clh?w&Pk*X3+J%kB9WU-9gVuT=gGRW-GFF)<3`$6tO+ zI%Iz$8x@(T?DP!!U6RP1p1C}fIZ+s81fVeDm*4+9_rgna=R{KD^s_H$ZSPD^+S-@x@o)CM-2Bk1Y zo~{8D1^^1f9>&;E7yu{?g`qG2V9Kny-IZr?mPv*@dFGr!3nRzNIU$o~%vKlxjGET2 zh4bqgOSPtEuo-e~Lvwpa=fI!VHMf;>LT1me`E$}_g#p0m$m13oTibKuo=x(S$YARl zTUuJ%^0cdg6Ospi&97_734M7cB!it(Rr}j-zh_fNVE~LKk&(4FC-hlhN+N^JkSmYq z9yBuYo}AF_kPnlm9smFU07*qoM6N<$g4K@& AZ2$lO literal 0 HcmV?d00001 diff --git a/doc/windowspecific/window-matching-kopete-chat.png b/doc/windowspecific/window-matching-kopete-chat.png new file mode 100644 index 0000000000000000000000000000000000000000..d4e6cd84424d4765223be78398154d8dfa7fdb38 GIT binary patch literal 52380 zcmZs?1ymeCw=Fz42`<5%2@u>J24@HsEI0%S?(P~a_z>I@G`PDvgFC^K;O@@An|$}) z_tty=-)r?+-F<5MoT{m+vup3_s1GXgSg*-m0{{Rl1+a`d0DuUEpHJwh@F(~oOqT$_ zn3;l%q^1|b(ZYZSy*x3R)8uWKGpPl>hG#qqs&lA$V}uODIVaDg@R)bdZvx;-w?53Z z4VXGdFKs2@c`l;$qVKO5yKBW7*cZa)J1=peB$pH#^7$8 z-BKq@FC#T~G*4h$h$XbBQZ_sBqwvw_>+j*Lzvt|&GeM3{%#lK-WnujiWqM;}fxan+ zu7=MO5=o8#94-Q*++LtDi8-W6eM&1(|*u9ABnLj z*IZZ@IGb$pl|1#<3ED$%xY`qj=kUj2kIio@5X;!xcu-*LkkYRqU)y8X7sdGGH%5fE zykBSZWaBlN_}bs%%@8-`gUh>jm#hK0UdUaPOA#9^k7x1?RorK;`wD)Zcd~v?oL_v= z@|qiY!xS$S4AK-gSM9JZE#0%&4+I@Z1ue+-4v6&b=??Bm?JRby6Vr$EEb!*%xS64E zz7=UZdA-l`!=2xMeK4D8rOY>#WAiGzguhjTGBd5IFm5hyHH(6KdN$2U;rfeA3;o=F z+PKWD+%^ApT;EPLRD622PTZW{q>8dZ?c~b6hM#raStr*0Lu|vNa9gR2K2y@y>`g-# zO(qR-eA_=C%7epnuXuGNhE$VpIP%CARYR^uqQrA266 zKUl^SY#gF-%!_R+eEujiL>XlK`Z8_vP(pA1{p{yHZCdT*8<&BF`knbmE=rVLgT zu-@4x&wVLqEo{xVE(|H@wAT9Wo&L@4o3#qVJcP3_IVu}XX5l^9N*-4Who*ciN$87K z-Z-sH$N?+j`$WRuO(kOD4NUiZWU}LAg>R6ngk@?Xm+~{tz~Ni-N~W(&mAQO0Gv769 zh3+9`(0cRM#n%ce5gWWW!d+6M#^rm<(LC%cZBWYIBCjT<>VlNP-q92m=RBftYUBNf z@@=#y9ceo;H0rP^SNcQtK8vum=8EcMw44t^Nb}2u?o|l`S7h@f(l@cM?9CA+Dmk=s z^j?km>DYhM-=R*UjN}vc1+T}8n>l9e^UjEP6*I-RJ6=3b`E^5%e5urSfBcO8z`GKH zyZ)eJ<*%vs-BCWTy^n|DK<)JkWKuVy0V-~7UR42$Zi95a^|kQjt(|p*+5+(oqCe!) zJ@|Y}M;M{CcTjzAhobjHQ{OKc34)fm3h9i$VRf9Vsa-aW;Tj|{I|{DVaiGy>qQy51 zAp!-}tZE@yn4yd|Fqr+vj8k8)6+&h>sB>XJq-dL|Q^`;IAA|3Awr>aMGOtM#bH+cF zEYtj_a1596aQCmPbY3DB_g<3!l)FTBfq|gg>m859Da`H$ zk4YV_zh}&~Z|_ot#CPcp#tj7r7f9?^27fi$uQXOP)(ovqjB|LZmmLMVSHfpF7?^+sd!emLs*RegH- zvzh0&N;p$r{XfSZ(O;g6ffhyW{!K zI(eN|8!?GzE0g~|ra44a#`J5|SKD1x{b0XoB|ZkgdjE6kb*Fxjif!!(bcp!_L{3U+Sy#*%XKEV%PD8E z$K{GI9`6#f8PnfTiGF?J>Y2hg<)IMuk>cg$i!RL&<_cK0bH2MP!6_XhZ#u@uNUe~2 zM*oDDlfZ6sK(^;gt<|BD-9b|{p-aMa@@3z(|6Wmh!!@1lAz~;xTDvk@dPpVm>4M8t z$AC;?FrM?TANNC0drq6z(w|_<31h+`m2BCY{^=yk$ow@?MKZ~Y*D8?Al}`TQlf%U- z*_A~czw?86+w)mfP)kmkqs=~62*o!heVyt<{s(0_NpBj6fv`^=32x%|t)6LxJi9UA zx-~%C;Np$Uiu_%+Jyt)kHQK=gAQj!!dMdGy`@BgBC_bI zeo|M%letxVZ>hwHr#b}fM)JlJP5Wi`-9z6uTBav|--x}}INUa*sdB5QL$^ORj+Sew z%o6uhlL1ME=CCHW7b%s#}!p1O}rP}5_ z(Q=pF;gv23%6#}1pN`oI^ZRpFgZk=OzKiy6ccNLd2qLH#6JMDu7<}RQqL^rY9{+68 zX=P(C0KP~hD38wLb!@UpV%c+FfY4p<`5^9sji zYVjJ(EeQC8E2lR|*sV28@~}!mHqdaWqdNOc8*+gj0_J*HE;ZRZcjIYII*@(aVX?tp z0pPv;0e0>3>M}!ZwA`>p0|{Ps*4y`T%3j6CO+I}eYpfqns0y9ojUmg83XPoz8b&@q zamK!;j>iT4=;o86Um3b?dFQvkWqtc*DHnTJ!6QThWi2rP;yxXx?=%`ap zH>6?7@!?|XmnN6#ljae6o7RLFT6|=UhD89b>-_l2?XR?!rxx`H(K_AZp1V-HA9WJW zj7p(EWA91fnqg)NY+)=AgTmkXmgv^h#;iq*k)bbN(Mo_0@rS*%Njnb1YYJwEl^xj- zlK|KgZ{yJ_Ia8If2(V>?)0nrho)maLb$>`}Jh$R;SMT+3RT9m#DA1H(0hTa7AkUOp z+EXxver?D>CVly^ucK3aREp)7N;+xE3FB^hH~9KGIL&r<`)y zp!J*Lggb@5jRW8^IY`N0yv=W-ybQYj;TF#`$)=OR^eS2KaWfLTrf1%QK5l`h?jj>4 zT!hU0Vk_Vfy>iRkA2quR9`a6s9_<{rC4BVucZ<@HP=I9DYf(-bHJ6V-O@(cH)X!Cg zrZilmwOT+)U*f$I=msb2g)p)$)2(({^(^MBFUGtf;5C^W{OauIneS&+nhk>0(&%S~$HmfC`y}!zHj$eT9B)#xw1!_%xvTYC z-F8af-3s4O2(m8Nt=inpZa`Con$0%)R>Szo*;I9=-#Gu0n0h|9ZV5M%Y5@5N)ATit zpd1cXh^V|28@tS?`eZ%dk`vi}<6?N5>5;f4K8St^U98J0TXa+t5T8Tq!rLt3q*BNg z|6PAN;+{E|)=oP_W6jBrj%17-RYb*}jjI}uf|Kpeuk9=?BhDW9FgVeH)?XoVOADY$Epkji z*(tLWL4!1d{%x@;@G>0u(y{TunAa;|YlTg9Aja(0-?^q#SJeALrrqye{poHqUX)?h z=Iyw|Z@m(j?PVzit5FUA2QnfeR?#eocUVnz`AWNCj)aLyA``n|2eyJo?;9>=@-R#; z3*zXbC`#WO#hDseXp+UmcB3r*j8f&@b_2^%dCzbOl>yUwG%Mv@14Z z^iNFRMByDa*$pW#2?}W-Csc18Nkw+hLBHTcMrv<))eAR#+@j7Kmx|EGqfnWR8twHu z-m#D5a?s)@8u}6=ny=FLSA7n#&2Jp_g0puT2Sd0?ym^<+s3%W3r&;&gXG)LAKxnH5 zL!_}$$qeHT(?Pu$_d6t*;@_mtdnn~=^~w(D9)c(DW}g0TEnU7pp~jQ%m~JxQrQ41& zt+f=IW&yq}_WFXCqN`JRMUa}3KY~Tt>kwOA6)rc+zSk3FENUa#zi8!)-oXxI~TG#IDE5sJ;6>@xe65u8|jEv4rWDa74rRq51VHa z`4giBcDu?9AYe5*LJ;)Q|Z)uum$jc zerl;#4tU%7cp`F?q!7)E6cXh}$Df5NiG9}|7G{#96xa)rV%LnP0wu~Y4MvIh%(#JT zH^Y&4Mh4z^zX5vh8kws)=^@|C?DVH4F_?Q)WKB5dVbpAsp#>7LjR6u`CW`H(%`~#E z7*ujcP2*$yX*^nnvEj3J-@67CjG5oBb9v3!F$B2HGc@eYFScwl!bn!+kLrZh=*9?s z0bG|~{h;gmS)h~}z{xo6fG$w%R%6}rizwf?vM<|djDg>7DR}G^HMNiY0_PPX4fKJO z^Bzkxxgs|5`Yr;)+cn zGT~j*L@RD72$O-J?;*rROJ*1TLuF|M04D+ge zQ110TWpBuHkA>Av`&%FEL$2StxE}+=o_s`(skW!}fkI%Tu7^O#6NtNqw)3a0%_{|n zrBNV*3SS_7=gT=`1aMJ+k0s>kxM{=G1wnyANc(S2*L72E9|QX;gEj@;VXjpW>&L^0GB0gWbYfw}Nfue^{%MIO z2qzVgihniBh#rZf1E36P$Q(y=_trQ~Ze0&f)ct^}D-qZ%^r~2;(xp@tMIrBH`!|zY z@KSUDRTA0o>sr);@JaQ93s*$TFNaS|A{^2OoX7GV3HwNUIifTSx?$ULlzWQbSeV?_ zvjLZgFd{(*6?CrPAM0skHF(a~mwXUE=kZYsClYdW4G!9s4Mo+M%>Ed^W4YqJiKe5M zBLSr%z9h?*cj)O=+KL-*WVZz_rwI6nyHyMWFe36 z#nfUI>SJDGjSx_Q%Kf|q9dz}9x9hHl78=V|LHUY%y+Af2xZ9m(WRv)lPY;I)5s_pM z1IxR%?z4EXk|kF%<7d3(cot_O&Eo|iDQWF!Ns6P@i&kZKSUJBpupxrupPx) zJn|3DMn3!$m5a;8*UZHQR}MD>FlA!*i6UgXY>!5v$w=Z~I8D0IAjr}W+B6j#buTOH zmy~(fLdp-ZdL&Qqe~R1;4zG9|hXK3?^Bt$y_f#L>jBsE6C9Xxyj43Qz@J=y~;DEpo zy^Ddiql*rl4_Ma+d$&!U(t>LRYi`v3WuN+_R-DHpGdoeDdJ9YrvQXswlQTf z&K(0)#jNY7|NYDCAMWp!Xv`)-#-}%Je%Gi?;NcxFx27svhhb)BZSLZc`|W(g&7fVb zriS~rJL%8@mAO?1!K2PO9yU#27QKHWX9lroSMoDmKj9DA zo8Td{ijFuaLa?!(NRbzTLb?qBNe3GKeWrIYgdC58leM^}j}fA_J@}nUJ(r?~>BQ0a zH}UXwTu;R%t+E|ijlKKRkV<+F9~bvCrqPT?m=W_V_>0)YTdGIpl8p_I=#9!lm5XL5v9=QP$U6{r} zcKl?H=n?-b7!r}6yIDN>HFm{gr0g7QsA|M5xig31FZKGTz}D~-MmHfMq{ z`ZfhgcS-F<_Gh@dk-ylQjQl)SR>*yNr8>#c!;9%kqID8D;xlD@b(uycq2#7-iuBrf zyIyON0KdU}UU>52@Jx18o4$;{yrj%Bsc2P}en?WKB$Yj$)s>{y#!_XqivmBaO+Zp; z3gx;`%vxkQUwS3A?%Y%ZdA~27f3VSTF_YVDC)Dm>pbFM;jjS*j{;_TPT`SK>9Hj_3 zpMwBXpk9uRv`YTdQG2!l z4>^q#t+hvCPKOn|f5A8)w3>YGQ}I%_KwtzV$R!gyPQ6@@_6TPk8dbUqam@r2428rV zy&93w`}GyVzO69H;=FHJG!{Xe%W3%M3ug9#6BrXYv`X=VgG%j;%lu_KVPPlh2SW`s zYg&704ge&k<*SFQ6gE6>jUcsz(j?$4dJ1DY<$?Xs(XNZf5=hpK7M$-cP{?_yh9HDNKc*+%R>BOxN7xR|jn0{brfK=e1Xb2aQ z!p&?tf-EE-KUtJUGzSY`Kvy`6TMC}@YG>rejQ5ZTSSaN{eXJ2x;hg+jZ}O>Hb4|Dh zh}nNBxxOEJ#moOVMxS#`keN|2h-aO+p{rZmDQ$*cSM8M%FziTo8mnAcgj zpD|Vf-X)q|=oMT(QlIZ~pg!c4R`$?9<8#Fc1WrFPt+r~vbJ(cD_hc>|FcflLO-g$4 z7vjtn4>U1?daWSW%yF3J?#aKtF zs>s%)#{wtwT2&WqwYLWPImR8q?#zqdN&MiD=qoT#yqh?=fk9y>$$`VD9XFmz>@Sa_ z-$1XI!sKRrJ$Saa_b+`A1GFc%h6I<2MU_3ef1{Z@nZS8z?^Ra#2tofBoYJhi9gdxi zT8%+mXSO@PPcl+ut?D~OWefIyH5(T%YpTJcY&$ncNsH1x4hq_l+_|r})7iIwjIljy zL$*Nv3me3#_cQ^Y8+!d)nATH^F_QV*gH-R%yvdB15|EpX=H>)0jIZzJG8rq^I^{Fq z^`%B&E-jVyF!3hNsUcyuubwn+r(?V$`aUN5H9Y?Q(}*_HXoz*tL%6%CPmm>Rb=Y7* zsH*D^k&(7V zEd9O%<(1xLHjz5&l{fr$8`kJynwFiHie)Hz!wYv!2Atiq&WZp9?ee5dZqyY(tw`P@OyO`mtrs~;5UJc zi@ep^En*NW0Ibrq2R?a=1^_6jLVR5kF#!NVVvlWkZU7*1>%OIPs%0RZ^UtO6NI z<=yhf1ps6&@(xzvjwFCMVM-e>Z}xEz0YR>i6-G-s(tlQo;rf_=QKm(0{UCuR|Hu3Y zY}!$Udy)V)#lv#I0oM=8&2HO1g8nxY7~Mbehw4-Zs$L+@p|>2#ZeK4T?f?t`faq4&7ZUKCc_NOPHsGo2{#gd`AyxVoGcf(PxR<>A+3 zg+rZ7v`5y2RPxh-p+)mlX)@X2yrJQ=sC+XH!^=Z5;SCqn8^nYS|yKQ+tsFkVO` zYYp@83-#x~ct8$=S|vWaWp0O!D_z(5jb|+#vA}cg#?!ysEC-V>Oh0qPeT!7Gh055C zTOKNcx8&og%fueJrGm|?j?}TBZ8zpW2icZ?1`@OXcvh=UE7k01)N8ks9sA+1R>Pu6 z-)=WxqgCnRuJzsOomPW|;qG-r97X=~)h(jj?XxbpsY;KHZ6p0hcZmP#(k0P><#u(` zGdUW&D={no#jhIM_|0EG&9^B&ToSV@c#Z8j6KK__Z^w%B+s>;{iu&=E*m|C>8NcDs zP@@!aK8OxBm#5ChuQhz!+<2zcJ`iv)IUr&+Ds{o8_(~(htdRmvsV?D(d|^CS*g=Gcd1^5kMMBy(`b;n=YQ%*FX(=Cr|>R{f&O2|c;EChi4I)! zja6Uav-#)DW8Yiqy+}i6N?rZ$K3DtHB1LA4R5r_FOSwV6Td^`VcO5#$I?3G0e!kYS z`PtJaUvf(g8l1>(a;jn0_)*HN*Y8n@nu>gxt%tqXj;I~TASy*!cO<$uji(|-{P9_v>?5L zoX&X;up+i17)o?99exGln?uCYAwks#GMQz!y*!_5@-$wcBCR@)!;EI*by^PR1&dlE z-K}L-FjcVe0f`3XpLZogRT+AXF3JWQPAYp8FxouuM$qTB({JwN0_1|*!i6K+YWagI zxWz@;o2>KopY>g_eY8pqKF=$}vs}}LV5kM2RJpsPE>v(|iG7qO7q+3H;PPo-NQOK- ziwUjQCp$CiRzjFHYM6%*#fEV^7B-Dlu&DX?9mXCpNy_u}#|7j7k%dA!#CN_Z@g z1k0_eUtcPSu{d9a+0x%0i`*igdR{pX9(nKCXSFYNnaqBiki_}frxIRl8z-J;t9&Ky zhO%U~iHb%3?qMPz$2LdyV*m+x)Sig@mk@EXK(+Gv=_OgX92hHPrnK?u9ne=EANycl`JH`zXV@$6=n}tkOk??+-X+4DQP~xBWPR+j|S~=#YGfMS?+BFNZoR zG^n5C*zVULz~n=vQQO2!V&|a9^>ZIuq(SfRtlt>GOR$ zLc?K?ETQqhj99Vmeeecz^Kzk zSqj`Z`s-B^))K2jlO86FV7Itv7@1RrM4^at z>CdyoO!p7^U%RYVfpIF6CRx^#>oImEn?h3B)sB1gwQ`n-w~Pn$!ZAkzj(lbVHaM+A zjVZPSInq%N2NHcZXKv%u^3$FSS@?;Ttb3t*AO7Hfo?lmTjk|eA4*1|URzgqsiI<*5 z-VW=x4HA$*;AH%M7`LtaHOW+@sE`qw+YY<~B5z*bA7_w5mMjqEnOB#wGTq5m-#<~> zJB>_NQ2(1$$7x!umvRd+Ap(wT*nYLK8nhij_E=I4*GMhK*@EtRm~t> z{7X!!D(&cU^0ruApoDU){M_xIfn=D{* zTE&tCZB%_E3_y(_F&c#3KAqpd5E|XlKmpt^o<9N;kxbG4m;i;is#|eEt~|_ zMM11Q>R=&gH&x=KPi)2}jeUeE2imL$-8UIk1@fLATz`TOMO&m0=Xx2V<4Z=BbZjtK zEKZ;@XLvl}Prft~u{oL-I_D31K`C;B1e~Gsl@N6y#%f&aUGHxJzC93yav3WjqE89J zp9aIt0RvRp%TW$X#|Sw`SBVIr5u=V5f2O5V9iYE3Fx7@tZS>uwW};1AX|wH;q#5q*4|V+Z|K%fneD2s^6#AoT4(a_Le1$?0-t>m6dG`ykkfP?W`bHifZRF| zHBj3*p4B)?NVljr?~Z&dmIb$UU^Cm!*IW2E(@x|3fx#veG((*MmxD4H)F5vu-qc|? z8ld7b>gVUYw*@iPl#27+oNMjw`ZLV+&c}8sc-9*m7=!}~4^rISqXo1~%s3L>E4>XD z%px&V`y?J>wdmxKV|45wZl_^%WAGp;Ai6Q>F9z=Lh{^yUFY3U%a~TdMF?IMat)BiOLmI1iAAOOo+dqY3sWLV4D1yWDsR zX++{YpKe4NCE5|$%=CKXM#=A*pN2e&y5FgM+apjDr?Z)Hpj%iUPMM>3bkzLnj|dIC zx1`N8%){tr3`AP#`}O`c0*c^ouO^3AO66BK0t#RTy=6|KbZRaOQI|r;a*_mw-@e#G zqhDe}I?UA>l9J$^?VS7jZF@JYQXIeMblMW|2==Av2!ajcwjcy`Q{3?K8oFNN8Pa!j zi=zP;rc8^Eeq952)l?$loN&y0k#S*Lx+z@Ol&-I7z{_!aJVBFny8--bn-IjdoiKCr z-me&Cp0$&-@(}?AAyR(9P;82f*t6LlaNUii55~Yc> zUSI$g0{}l^5H2V9!FH5`2A&@xwD}#t9;0y-b}rJ#J}D6(a-iY~SL<&y=y1>tZbgF& z@xUt~jK#OqS^-tMO^Jmn>#xy}FWOn(B`S*)XEgp1Y>)f4ZMv*LE$VM(%}WbJS~WtG z&8ehRKy4PM2NFmQr4zuu^|~q$vQ}du#v%&TUSVAgzXzb;ehuA{>`92BXON$pIBcMY zZe6a_;yA>ULnvF{a^hh^VIj2X{AZX+p3INvufNA%O`n#LNF=cOuWWYrtGgvI6$!iu zd3CkL-*mM3QBq!Yx{dHvhwV8(yR+TH6T3v6Kb6n0xu)B2Cer9-pD!{tC4vfqibAwh z70GNjm*tG&6%kXiKY(X4=W)p`UXl4IqbXkIyd~oLGj#+79G`WD|HU(TD}BK`<4r^6)S3V4}JXt$jdSEn0hD~j9WwJ)%*aSfK9NPM!SbCw}dK=QtR;JY9D zAqa8=)6yOOpdZ}AfH->?NI!I+vr7LJ)y+!*vwZlniJIm230DXM zc?vBzYfHVoW5Ruyz}!0dP3Q{-(6uX&kjW-6FHY(;@^M}jREj${fu8y8VxHw3&9xscNljkz7!?OZ;7F( zXFR+sWyOmTod;Io5#$gq-MRpRMf0QKChXPRpYdD3x>kDoAvx_5wMDxsgE}Ll89Gqz zPdb8>r$fC9|8JtkSJX@R{%Egtk8vpx4G`>?vAb*adSvDc>HR$7>IEnA1C=oGCAoL= zx>gm98l@+oQnkJY>Yo+yZS zh_qurN$%gHph+gJ&RXyN@~%#*YeXnMTcu_N|KNyBEl;g$yPAVY-bm_OP6-(=>0ooC z6M7%4awHJR#~BJ_yL%OGf8c;=8o$F)oBuAmw+uC1ksdyg<;&F>C0z3B7cv zIi|SrRrlFN2<{-MRkx6DaFp*$U)>Yay*G(PWhj?ri1afphsV92a?o(BQu{CpN&+|v zu*&)19d7R3r#~3nv|4y7$R`mAlS$RRxb}69m->q1I5>;VfDvHa^hsTgFy*ZhKI}*Gzl2UtqUIfb1B8bZ9*~p_H8oI8qCm?iOye3_je>30!X-a$6$z8(oFMm z!l8@24>pf*XrWF2>|icsUauH0ZnSi+PRF5#NAVrRI-lgIEd2WRLT(@6u%X&tgNX2j z^Ieh_DqKm#2!xhlty8bc)1a*k>HJ8&Ldi~nzLtm|rwfwE?3-#Ze~7H4Wm)ca4Wl|c zT0Z3l#1%lMJ(d~0kFY8KgsFAMUseyaD~9;L4lz&1Jb$9c$Z@XBW(A52zJ2SV{1E9c zhgx2`ob)FB?(MhDz+Y_`yG*r#fBL#WoBpRAg^O$cZ#);)*04N17u`LzR}CTMzD|pu z4m}(?4K;~Fzeq9wM>Vumxp;~r_7%wPwwzjvcQ*3_WVxSwWiI*(O;ebVX&Xg8KAprjpilb2oPUT)U0x4mF)b>D2S#rb;k=CJMs7)Sx(|*$g&LVE z$d3`V^H^WJi-NN#wvL?yLm$V;244jG_2zVZNzJn460;^J7X19L3BtkQfw0k(eH@~DEqksVoSET%vNd4z9yWdo zOf;a7sdWmT{jDPn?Av(J?YoLF>UfOYg8gH0DBvtk^LbIG%zqdjGdROTN17Pp)Ke1< zMoo+X%Qb(HkK@CIMsD2}DT!gxCkkwM3y9O80=^fdq^1^UYqcF10*5A2fxQ60r{iUJ zYhB$IIFlBHB;)uMPITe^hc^31ZXp3AC}X#bI%MIr8yY^eY27*XA5SMi*P^jo`Hvk- zY~PsIHnvsfCbXSg?{2gCi&$|)g~27({~6miCp;f9laJ{Kl*@CSf@ zb%clZzb~0`z!SmnXk@srC&TQWp2VS7Te*dDD z)#LfPYho;Gty;JBavkmOx!y+V#VrV&>;Eh}z0!4caL5Ph!j`$9^xVkJ@!4s4dY;JE z+7D5z9HwR|R27G@`k!xW55b<$e>3I=@EdyVYwdSU*dbn6c=@DQwYABi-4)P2nB)Jb@*WuY0=wN`5II!{4Uc0Y*=TjwKls%k zZ!_2Ke$#LWva)I)@>7r&z1iDTEwpuP=WA#=o3ZPn{mt%BG=Dof9sh^&Xyzxv<1J6u|vM-5Pl`OMn z1^u75Q|}w`Jd#J|`sq7~1Bl#ZQA(2D_QKp&fD zL9ldq@oDEH`|(+S%iG37uQ@(w#{>584Xo|7OVx92rSf+~5c=62EsaCh*$m74 zMpg7>O|L%&oK!zVNNjgsxnT`qE~R(fE}h>ue!h&U_a060YuD}y_Lj-de6t_OBPIP{hz=NLq?wU|y6GQFUo9jRMT{TB>Wg2P%hmL>H%Mr(T?#+nBQY6I} zpL9IFi5V(C{#!Vz^57C3uB<bYr_%5q+yC4MYk8 zSE9*W$WsSGWT#5`Y->3m7hgo_EnHl&G8#uxUiCL}J(BT6a`E^s`XAGmUM}qlo*$wo zQq+y|$z3EbuIq3qPeoKYmWuw9)VA013!%Y zv`}Qu@%>ZiSKj_N)a_$v5z5L?jGdo79rJC36|$@Nv-{`IokW5uCjGbUDXd?dR5o|^ zx3!Hy29Z?ukBBvsw@s&h@Jf6J-kUjgz#Ll77eqEaIs*mPJS_S31IDifhHm)&Yp#TA z7$n@HhF(irOUG%nao5En9w+7Jx64LiRL<^e%1r|>Rxlb4=&zCs>)KB)NM^mAJ||V( z4366oWGzSB)gv~MqRRh_)gxjej0aj{&nDv%@OS;9_To-&2-BzM=pfiOM!Q0fk_U=6 z>K$2WRL4)9%i|d^rZjY{{l+PV!h({p!0{G zmjtUhzUSL+_w$REO_mcmnd9snEG(0QT~DptDRzXE(3)`!j;;(t_sza|5OiU(Q^4CM zX2@sp*N{$GR+q=0jIDts+aZsdEZVq^7oj$__AXO8p>+-97lqiZvpbE+f|Pk@>bgB= zP+sk6P|yB~W7oK;kcDHL+o=|Ap4h`x{JGun+5F;0R@Gzdsdg5al#%KAtPf{Ni?LSm z_wgeGdDmm-jkAmCYFtgey)n(88N&;NrE)wj@z(~Wh(}TF#n(JK@8L7G{Twu5^C~=^ zKS-!JdZnDZC0Vl@Lkx`orhBbPA> z`Gi52kmebgEY?&IM~*SLe1_bJ})5#cIFC5ZvIl@uui~pu4g1T^F*O$xurwqBpk@OpKJHY?*{dY+J z<1$z9|IeKP{{PtjAD0J`z$TkDId~*A5FEEzWr2Sxrc5n7#_tStEzji&|APIW@Zipx zRk*9`N}0#?tC^Iu#R(4DTP}$T8V(l<#e8p5P9{;{_1XD|=CWO?H9uX0bI|^VMY~W4 zgQ&>;aB*%)Z($ds z9B=#Ke^HtAJ#6`|?X}EBYy$-x*jW~=`QAu%;0+xuCMU~3%9o}{;k`75z#;8Fcnd#e zxc@=qxiE0&pFwh49!M~hF5Sn%rG)-J)-+Lfxu^ReAG3=&h&=ayN^kA=Mn3@Kmhk1d za|3R?aiIwRUV#f`HELyT=-8{pZIb1lO9prT_()Hhrb$Vm)#f4H!BtX_$eO?bM>2N} zzi+IG;gJO^XsB#}12&I6XW2Yg{Z}V*yFRJeOx1rLU%6k-aZ4(i8wayj-O=#bEtLMW z_q{qv9aQ%FIf3e;93N#a$d|bK-6VfG7Vdb~1{3lMM=axc8#H+bx;Y7HiM9RHcX#WR zzTt~Dx*DD?^g8qZsI5`&b~sa3(p|4XNm+?nP?x{I<`*W%o{BIa?Rr(S@$!_vZDH<`kB?=sJi3)>z1_VHIyT~Q zm^fjNu$RH+1S~Er&b(VV9wXjJ;fL#e{Q8Z@Obdw=6n%*|cu-lK|K!y7Kw|g`v>YMMoPqb4l`W)mDO{YK`Um3-eDqepy%xRZkgHE4u%?e$Ct+lopTEuv63V(}3bv>%ggP!aJ|H_BeAk;*{>o+-%p4!BygoZ`^Qg zcDC{7cUR*|b$8cChr9)h72l|>!ST(pFA5st@t|j`%DbvE{Ljo8wu_F0hv1Z_6OIQk zYUEbhFC9A4$OSO=9BX6YKF_CWeUSf6yY&p+I$V;3!dsy(zj?V6t|47!gCshc)dt}* z^k@tPkND)uXlO+I7+eiGK7R8>*%W^)Oi^2DUhn$IsHWOju=UC+O0@E2^GCgfNW03R>CFk_`u@9&!8ADEmkw7rnZB$&9|}06|4OkR zZz>$kJa8x5TiyC<9-HH_KfMy((pmJ^fb02e+U!!fHbX4Z*4-vVfZIbcj%|<{nFyw>)wIvuJ`jcD{!{WUf%V1P}%dv2!?uq@@36vadA$3 z(O$%}4JN$KRW;d_1E((XuMnZxb#S#M1I|OGoxa!RgrQ>1-`CUM_^7nTn+n~n78R`X zWVc?|?-r?FFQ43fUK<=2&K3SR(AX&-Mh3l|nHk@GcD?Sy0X5tA$;H*|%8dFv>0SF3 zY2J?%`6ZKCEPd57kVNx$XIR44&;)VMgsK ztd#HA#Ye6PQC{+hPVUclYa%^ulD5Pxztw=5InpA@B%;?MD7kV8YsaN?=t7Vq#L9qt z3y=GQ>&&mZzJXzjQ5u#6m()72%Ep! zTbslr6P8-JHYPr;b-w&)7%_0}3PGR~3J0^Rci&{657VW33s#9EJ|2iz! zipc=w+dJgQq$#wwzzd?*;~VqfKdblAN{dwf^=s6?KEB#giMk=bgRz$C#CEuP#MU;o zFx-&x=E%n`taRb?@q;ou#BsH?0>)3W2CG_|;+L(HQ-R?v7_Gp1gyv&NK~P>BqC+V) zS2Oq~aH@-^LoPyx2R$E56zAVu96Ievqrodz+WRib5mQX3>o7D!;O(uv-kBpR5qnL% z8S~=LBwKTD*dQC(JNI3sp&za#?ootLvb6z;2OF`?dqeC)oLL4kapZ9Yw6 zyYFd0NB4AXo%^H8BW%j$_#t*|3NIfv$-+`KZ6(GSd(>*b;(6qfn3bGte+%+`7ImuE z17YA5>oa{J&}&+(&3v6_O{=$fv(2JS01d-|gA6+FCOYQw{VIAPueN}Oa${Os@8y`K zuc8C#8hLiIVL#SDy>b_y*me(Z$aLq(&+93`&IRBF7ePK)PoY4f^_F^1nY~X^j+{e$ zw%})ZNouhG)21qe4i)-JJtE48^{w3n$JO?VyV=@o> z83(iZOx#k7&ei-{QO)6>#k`J~g>SgHF1u+GdNGODy<+J4eOU}J=(LG7Jel|WPAcXA zNy%0*^krUOyI&+k5Noso8ogk-PXOt8R>UXuJx^@J?mRNE^FF!7GYUIqeEkk1OQsgL zDepjC_BbllS-7co8z-WHT!)V)&_grc=WoNelcYNn6sN)Sf%VtO5P`WTvw^adY#z(W zHgKocer+d?kH+_W|BO6B3MdYhXnp=b%`1C1OdrX-!$q;mb>3p)NFkxyW6fwr)f{}N z1TP)DNp z7vuV#N0ELXC;TfSl9RK|P_ao(G6ou%$G{8dH9NqzXKwbG&Hb^&EXRH0t_C~cTY>g1`&Cw^9x~j?U1Vz);ADXX5r_T{@7pl^ z)(lf1-VJdsJoA|YR@UhtZ-O$qGNy_`|0>Z3Emsu3Vq^#11kr6gE%){*+xbLAo2SKE`P45cInjE9E-@- zb{b`eDA*w zAPq}*mk2CNODQ0TbV!$iz|u%J!qVLh->mxk{on6@&U?=5QP1wP&&)G3J9FRHb=PL+ zFMc*-yY*?nfp9%d)Tn%6c;&cebF+$wrb(e>DLj$4b3Z|*qTfR=@Kbk3l6|hr(Drh) z=)|E$!;`nYm4R_hV=o$^)C@fVsD8`adUZ~d-Jw#>7k;wOd!F0->wK(F45C=7nD!Ff zr+YBWX}lEfd~5RgNHvuDXJOXM+C*ejR8)=&X@v2qa9A8SI4R>3+#D>LkOq*9moavd zYA)$$pim-7`{=T3u|7T0*asTH)eglkvTC))do1i0ophS4dw#3_ak!{cGY=R0^V1X% zz+v9pr2;$3dM468z?9=g6?d>)evKo11HdoV*>%BE`_3YDqkRi6h2q(M>v$lWMq6`0 znAea$SJ?w^%``sxN}av~lQNCfbp0VavsV)XcsyZK(pjf(U-9ZxnMW$L=BgU?X5Wb6 z)%&@w`dFo}aB+Rx3|&6m$a2xxX?t2>Ii1t0(N<%1f=e7Oyy9fg-C%gv#l6yqIxzcD zm!su}A0KHRh&om1*;LbsTV|RK7DP|#WvIyESr?qDCO*_K$UpkV(qN0)QCsX(pIA`{ zVcl*YT=lNhfwxO|>YOfoD?IbnElpL`P}!>+BD%#8ypenQw%bK_IJ02%iegC?Z<*Qj ztKoQ0FA$4WG5%mDclD|I*VRG=4~xTP%I{B~KD|-u=Km!NdK774S^#|^d^gCE$3v$i zIj8?)XqD~uFDBOGW>kL`CaO&6A;NiMis~jRJxzomJM574+Wd^wPcCrGX_=AET;lI2 zZog=KlKl0J>aPb{6Bpc$X&3&TG0UYBZt4N>890ApG%>JshZwWEUW;0zi09QSX6qB- zeL*A17cX~x+?@nzGNyy^ZiHDH0%wwZSgN7@+FcPulj}d6b%m6rM_4x zENn+}{>avnryxhg-qHNC1!AE=t(H7rp^vUB2uMRtJ?hin8)KcYT??QQmmjt)4l~9K za&C6+5{)^GfLldj1{0On!-_ASzQqQ}4&aa@vXCaD^6O3*z6(yQRyhS~i(02NiuHIR zkcdM}EE|DCkvKYfq7*2)=*o!_kM%CXq1}LQ zgNWRQbFqIz*ttAk-|V{u92S46EEvguU%D_OjSP)K(Y4@;50nEl0BK08yxspi?teWw z^aF;z|KZ$!nL3s@If^F)JpnFCLo~l?2SQ14!E4sh#a#-N@1Lu!KU=9VkKE#59mUKL zV?+4Y_O$e=Cs|4G8{! zSH)#II$O0X%80WQSXg~SNSQ=Te*Nn23XA94tt}|KoH!;r5i0e;O0Kc}e%^V5AOtPU zO50Y^bRn&x4C4oheOFi?KUU!nj$ja4rlob&)x3sYJ3Z{$Y`D~w#wsbbKl|hnb!qC# z6h-WUDv&sxzJtWl5e$XFC@y_Dt$hxIFSaCEX}7c?n}Q&?9e3x+aOY&RJmwhuF8=$y zgM*9N_Dxj(4#b~R;nLVWc{;UXiq-o77;S;eiw%jbs<@;3V>4fRm>6?vwsawhWK^oY|QlN@BkzDypWwJ+Gq`g}+Y}F-?uTyf3!% z-Z<}d>D2vj7{N#o!=Fb)N1YX39x!=ZLmFK6SD4mPcH`GYulrnPnd*&WikN`E>%|iG zEglCQ9r0Dmua`w7s)U^jPMUP1O|n4|mocx=(d7{HZ>wReLN>IQCx=N)=ArBznzU~e z!&7ckbHB>^MZ>3e;}{Q55~5|?8Gtg}EoXRs4K;t`#_T|%eWA{xZKz;Qs7vdv zC&*7;EqfDetanqekOT$%GuOqO*6kvclse==SgXSnU2MXLa1o0{v7G9Nu>BA17F6W+BJ5&cw$|_#wTB49pz_ar(XlmMNn;7 z*762^ckGQ1`?vU6i0(oPrz;h%$H$msL;C`C(hUt2qN%OqWZ`oQR?QWIt06IE4sy=( zVJ8;G?|94WEl+?tVwC+>Oh>J-1eZ#nchmiC@;B~)2O zz1nwYzRY0vMRtK*bk@7q`$=DDZnQ?Gir<(X&ZD{R20-}dS_wU+Lg7X~p%xMpdY?&L zdlauMJmsFkXoi=)eCCyi*Za4O;?0xJYR|54PzT)aRa^;7WFV*aCY|jd78II`0Cn# z(s+kvzTIKria)?;bh#8Gf*u3Hjw(r{IuP_X1MOn??-+wl^@u&07Vb) z`*ZO89EP7cN6q~XjgS4jHGue(vy>ssusrsA{3Uc{J{`sfB#8fp2C*)pgZ; zUNMK+l2R|G?3XIZXv093g~qWsO~YA1P`eEzEfSn6!dFu?dLGb&c{x9Hs)T!;Clvqd z*4oMzA>)#`L#s+M>qG0&M~p?SO34mt&;s3T7DmAc_JHP4zJ~k_A2z=0`hr4IdNKzf z6#*OfDiRyM7sL5XAA~huOTa2$7jwp$j=?!`;leU$KxlVm2K@xtJ`D=egNNsJRG_ow<{Zk5BhKygiL)PROEl--| zlvDO%Ydpsw#tLah7DO+bdCd+d9~X6@)IB|R_Fg4TT|=EkaP^yFAC5FSmW;^>ljtkm zvXc{VvgJvDV&MqcOk+1DKBGM-x$}wh@xaxhrIWaP?WUU=%Qc5gHTeKddh3pt{|n(SKNzDFJ;D_4S#_30&`2R zxBf@VtHx;M1ZLOqh)KiFBh-rI(dlLbiD#jge={(jHQO|tb9kLBb8?7G*xy2?1z%V` zZS}r5&c68&o_ED8;_jV0X%>ro+Or8LjviiFQDKv(mM%!1`6jOVqglBUAgtJ=K@zer zV=aph8Q+WA%%!WOwAi=ZWp`p|OT$mF*4@b%Uu4kwY+T|qE;Ktd+sjRV7M<@8}ZN+3RQWcdi8^kzGvzrqZlN*CE9EW zTenejd`^-{m5%Y5r^nfVSzm#M&@Vo2({3FCLR3M5G2y0jAHU4UOXEbAJ&oHvcZ?#7 zAAJ}k8O|_M2 zEDE>rb_0mD8!K9tlNF$f1#(KLyL8+r*qDQJ z7V(|2=(W$&<@RLfvjsStBDpt8E>ChVjFoljyfl{U5qDXERnl=7@1r>Z>79yAQ3SW< z8dC?pSto6nI`G6;ydIZur_!hsh7v zwsllo##l0~=9}>F|~l=CwyacZ)?g` zzKP#+?FScqj(Wu>VV9$~_n_smWEDrVVPq(v*ogNi*dB{-1uDfmO5STGilf14Wke8* zyUGbL6OxSQb*!IJ?t%wXt>kk#yo69%Y4<25J z#K*>mZJG~I0L%N3o1rraqW^$i(&@^Ot=T}f3)MQ)9BBt23 zKZ8Vew(oa%eNkE~FlJ(SX*XQag9*o*8 zo<$jP^u{wVZ{!Dh6mt*{)xxrQaO$IVy1(fgA?=q4Xa_!B>n<3)Pn z9$t3CJxAKW zNUe5j?9|#qsdcMFyBmVlBW4#=JG1Vlq%6?eoK=-RYv@JBKljkR&Gf1Cn&3GNgyWcT zo~sq&SKMNb^P3+W@#ge7i9yfs>~2vNRW0U3)ODj*M0%CdtsL#NC0-;S^WqSat_#WZ zyH7%K7GAdQnM31@9IKMW51#V34;;?)QSEH^ma;rMa1^x)2rv#HK@OS=H72bG8MjDS z%|02zx_w%Xt-snASE1ts=oi`{frWOo+9>|*M0&WJuH8h5~P z?MIh?!+S=#Bb~HIG3D$N>9+1h1esG{cxcY8))g;ZG;B`xqlHcOK?ofwJY^f*UpN30 zey7XhVgP)d2TbC5r!mj5SBH62((15o<41qwM>W6PoTyX1Z#Ald3*WuXdCEq`mzuX) zn=E|eclg!EvCeUJ!t%0&(cJC$nXRq|yOO5#c9Bb<1|Y$S%!N~JleN~(Q{cRCiik`T zZ_>OCAz!rZe2jAd6YiN$;4{CMTBM|#YiBsW;^>pFs5RxZKIt9e^}v#aT6=h(|C$lU z1fsJ=;vwGymx#=|uG?gVYHq;fNFRO3v1YW>ta>4_KZv5jAKB(7rjVNqb`GJI0& z=ltl(%1Y0v+v$n7aznhKk%FcA)W$Y562^>-#HZL2t5bo?zX?$Jaeh3s>)zqG^2LXu zoX{7Szl(l8#NE3b(n>uYu;>eY1ew0Q2IVM!>UFlmV|Nd+ce)EH_nN+a=d|?A1q4tz z5tUy`E{vFz)MY_a1%c%#kEhQEhg!b{jPie#7eEUl`DdrTS+KfhfBee9gZ%WBLjs8% z)!C=Gv!2cxMJyJHuFjP9OECGxMhl`HjoUOKt%FaUt{sr0e898)!9N_}IZjqpdzC_s z0{{KJzDRNZTiFHuxviIhofdijV|yj7hxf&6=N$$|CsVq*yVvFDvHD9(UzuuFIKH?D zHExUEMxoOBG+yZtjlVNvFK1O%d157^^V3I)L1(hK6ZhatZp~s3^J{b(G|1zKR**ux z_|3i9tL4}GVaj|jMzD^<(DsXb7;}L9&gHvz?=aDnuD}?LH3Pz6puik~5d1$Q0Blph zofta?0$2cwN~%B9d7SgsoX&s>Jbcq8N_X#i$>}$QqQ_ZjXWHsKb|rIdEW4?xX`(er zdvt5NhlC!UMMxV^ps<=>{3bw8N;9At5o8 zk--)SA3%6s`krssTzkX#d#cL4(DB{+i-Yla3Nkq3>n^wO$0=9r0r$?;%{Is>=NT`) zA&aD^qv3>6%f^^OP?PS5_CMT?+Xwl^z#iWm%j!wh$}>8LFP8w_D~F`_1ps~Yd*=1_ z=}HI@3+!Sp?LvEs{S^TV1l6T=X-So>e3_naukCCxS}s@2?z*+5UT!*#4Fg052Rs3( zo&P9&hZiMFzyi~)wG4dzRkwBOoP%Tr*v4NJ+S$_ynVCb8nt8ew;3yUYx~&DsCbz_H zLT(zrd#dN5#*N(9x%Gn<1D%x7WO#jVjB8{EKfI`O!l8jFATM`}-i%kiNgA{o>HIqI z#RB+uzbQthdGs{^V%gWK58$hgN)d2Pb(K)9zk6;`HOCC+*Kr?nDeR#StM9!8!2|y5 zhZ|B$yw`x6$;fKVxjds)RsQ;SC0&O)BW2Bsw4teLsJTqvZraxY;sB}D4T+OhQ=*~e z#NO!_`8tzXSyGby{zpG!>8P^_Fkyr?2wOy{Pe`(yB3_+MmeR7x&*ZS!&}S zP~?^32P=vD*XzwVL5u>{^)y}ix$Z>tAJ^S3hZ6ulo| ziLQ@f@gvY=hI^mXr&~=|jq~)YLV#_3FyDQ4zzjWwdYwA#b%y5R9H}TRsYjqS3-xNw z8KKHf9l`wq*yixZ{z~P-lWcXlj)ZTbH z)~IIRxLeIc@+e0{)*}f?e;3Akh}r5P>%fW@@&mt5-k5u_kBv^k>uII~5&9}uI0S{h z&*nza&vP_fI9}qJ?YN#OErKhcl)1NdOe7Brg;Iq-S}}8Gl`=khtA{36ZI>)_UCo2f zQy8_3gE)bvb+$*CID`gr8Qz<(gHKh^6OoAEo5BtOJRol1`$2*C-tKyhv-UG`v(Nd= zFw$@u^|!2RF;UN24hEBiKHaY5L#>55wE#*M?Kpg~HptSIRBnCPQW}|?1~Rd`QY#wA zoFnWPr^rIwKd)T;lnFU`^7^IuYq!vTo|XHS=GooJj`LahW22R!8i<8nQTb{L_so<`kDGk>lG&ygju{zD03J+CNg?kf>JX}U!V2C#mKQ`q*eqBGciAPR zfW=E8NJn0{UhxVg6gS}uucyq`xo2;7Z+QrRpL)AxjdfF&+Uc4r(Wti<1=^k!8Ingv zVU%817CcgK5jlPsZQ8R$b8c@&$(5s6Ys6Op%%k#1W2s5DfeppAP;HF^5xW&)Bz>9Y zS4L}2)}90HTC8)^_o1w%l4cJyOghV>vwIMaHD+F3?q}bT4+@JhUVughcy;0&)_Lj` z-#WLqdf!x*Xg}e?zQ@$5CE@o)G=~P5o6uL9hNN-cb@zZwv79C+AK7OrQouqG%4-V;s-g z?)WKOkGdSdrQptiL`Qabg?J%{l^Oh$1Ij+9HEESW*zDwmj*~-fEwV6H%N^|4^aNtf z1~2tHOOjP}MrB4^$Jq&WecPoP&+O=1M#MVB9&3D}Xf0^evpsrsZca&{s56k{xUNk= z-Tjy1nNvN}NwX*mp~(bNgRC@}Ns7mRiSHaOxX47uimu%prEy?ACJ>ubLVx)3U~Yc# zmHjqav0?e{8^&6Q`I8Ny4p}IY9cU1WIwUdlhK(iy3Dn`ceUoIlK1IP`3&(427NpfA7 z5YZ}gU^gS>CSs94mce}FclF}}pYEe)mGayNxFV|?Ivrgdcb$z`1IRI_l^k76Nx|Wm zURhL+TP4m6wIy*a;A^q5~x}CoQZ5(@0^I6$5a% z!Q%b>4@?QR0}95x3kP2Z<_TdTP-F&E-&Y+)F~nW@o0(=8^MraJ|eq=$oDQg zp4)UC(@xC%qu;s`D-iRnzipaYRny*S+srNG4~Z1Gf3ZGpe{i4^_!T5H(G0vXopx^U zJ^%q+7Gmxfvd2mNcYfKYJzJYhAPjw_}du$v3>P z_-caP^*nA(XJExVtv7r?^Twn~H^W3XU^(+{&xsMj)XFHeZfvBImIGoKOIM#mjKR(IXwrNp5F?AtHSffZIo)71SrfNWdD7J zH3vWjvV#VrLtTr3+6Pfb=@8m&9z=nJ1WdPTA?zfk$;_I5L&{|-4wSdIiv2o#z-rxq zyKW`su-)>>mkRMY^{oaO_~=`r{esqHP}kbk&C`_`!llo5hbK(WJ0fFqvNytvkfKcr zeIGxuS#6iZetOvALm*_dyLPN(?^$gZc46;v((Xi`AEzeD7*M&7#O-4a4WS3rLvl|DHF;k{**Us^s)<L=*KlklV$pB_KSd>BG0m{zel z!C={;69dAZw4Uy;I9qzzf1|r^H@|uJnd9lIm@PCj=6byl*i6UH5s)f_j20w%2OkbK z9#&18kq=fTrRDzsC2PzZ8hl8a$RE?i0;i z-}CjyDBxqS(zJ>|5V4$e3aEZuM0ji>fgYsFIT@d=A?ryeL%01C6202WmmwoG=nRAY zIQj5nA;dtkVq}zldJ_w5;fkN*bgieUs(6)5ZTaRH)%MG%lR(Jj^69(s(J)@P@&Y-{ z(^E2zKHYBls<$#hnF2^-ZjsRC4vbw91a6F<_-|HRaE5a$lRf? zi}l`d%bX3Rz}<4PPR|z7e6TDv!YJv14%pSZJc&PFl)AK=2NiIrZ=*W-c@xgd@X>B3 zc_P-w;X*cEdD{NsCSxHJa0+thj=RmpmUGB)s~w}1>nyHwnlo|S@*xPmen~_7$dbY+ zZaInSB(LUBR(*?xPj_s(cDrEw%d5;{lL>O$2u`3#F1w>3t*y?nka$JJ)6}nvXaxtv z(w+2QpOnPc8Ia2%HxZNdnC~{Qu^&^wp-~6pYuGq6e zq;9z`xU7q{+EF_F`dPg#K^gK|Q8|U#4?Vm-Q73er|0;xpu!|1`tN^Sw+QI>>YxOs2 z;xl6g0F;~y6b9$SNina`a)Dd<-bXj zx(tsYyfDEVr?BJr`s5h!(|YB_;QhC3)w;U&7qmmf&uKjl>(Xeo79xeagHj&%tG1!U|V<=8W2j zjd88PS2ueq9U}P%X^b8#G}5w;4;tzac6)#B&4LnF-ZL$X^dM#WB`(YouZE=Pxw6fM ziW(cX9#QtLA8EB#r4A`@ezzWhbq{}PQFvpljj(R3gT0-F{F$B~(JXpd8d=wNApkRj~ zb&I<7AkKXH5T6E&c;J;$NaWS1S^2ssZXla@EyxH!p=Xf$&Cd?8#j2`R-S`O zMKaf2HZT0otd}yBhL2jME(|7Xr}f5*bh*dahxb@@O4OAm++zF_Yu$9Swc3bHIWpCxW4rj zmg7;?2oOrRKQ^fgENDztKFRk~S?TY>8xY34A??KwoMW`~@ntSVW8q2yl+&+G!N3dl z2ysj*>2bES$U}1n6&A-Ec^lPe^PcGG&ceAU>o@rXsuL2{YV8CF+d3XtOoJ2HXloDb zfIXqX;FHtN=X0n4h55pRQ(6I!nB4KQH%W9v5m87t;gN{qqzBHvcU2Q5qrV8gVZu#% zM#(x_&#d%sGxHWAINJ#WzM#gZLw;9>C!ugSUOzR5amaRsBCcQAc@JzobVy@XjfqF> zhELG(S0fPIDr05Q8lyiXAlC_sN$4KL1>?H9HB=&%RRrK&p;Ubwfj;-CtdD6bNhp=x z{md1zIdejf_P`c!VO46BVF+V%TJhm;5}LB@##tAe%X%Nm`E-v)y0BZ0bWvA3OVax3 zpuo$$E3O{mDAK;Rnas&I5PkB4>S*-PUii@`Brz~BmbO1wq3r7BLwTsuN56$L`vAqL zV9ADA-^YBY`S@mmIOC@gZ9f-f@vwo!5;W95%0TZ$_i{23M_x@<5(c|x|pB3iLgO2cRMt1R)opzL?&Tl6+Z zOLi}EUR$MuBC#_y=DnE;MYWsKls2EXAhTQ@;%CrHaS`us0h@stnMWt{ghOgtMQ^zb z>3+YAI{o%$Tm_O?LiTHJbM=tGK1-RMmRznm5-tf_U=iP$qEODHB%F7CX%3Tg-w?MC z+{^2A(_XUE41k+KXoVHG#N^FkWGtR+)8jk|TA3PK7lw?zL<(-MP1m@LtE#G4?7HJu zI|Fkr5lqf~vYxZP(33=_i-JGA3nPZp6PU}{DIhEff}i7!F{~Fbuc_#N6zmTVgS-l{ ziS$PWyTlif|6KS8dG(K8!Kmj`{9#CAb%6xIf85GO4LJv>SBy0HK^w1h`Fp^W4eSJf z+#$K!>ma*MWGwH({xLm_(7|v%K+Dv5AtDcXRgm2Bb|Q$|Flu&x5w?FU4t!W3f=vB= zad%y_aQZqRdN(cvBSBOhQHmx%?I!o}?`epTYz+2Qri;m0!Kvls{Xr;F%xMKPu}X2b zon}~%Z!?;e!mmp9`te^H>s9|MRlWO1)1k;4V0ajE9{4F49l^kklKk^d7D>!sT>OuzQ=Ef| z{~_uBVdWGGNP5h?Fb&lMA&HLHA!s{QLflA3FhvfI&b`|8@iuD6<$=L9YGhGjbXxKFmURob91hlEu3wz{4p5lZ8D$Gz^NG z#(IOU<*7hSSNU8;+E}Qf_hlMr*>G`^dz%H61xDL^>wYy)0tV7|DK`+R1r{N~9L=P_ z2H;ZIRS~j$le$L-2j{0xH|Kh!o|R}-Y^p`v!HgEeg#jTh0;c?YI0)_DI|}`{aUM)| zVDx$Aq5IU2-fEoS_eCJqthzSOAI3GH@C{-!{Y?>L4Eei{;z|3ww~7$tOt79ZAkkh& z$65JTi>qb<#AuV)8gc5V&V#DhFn-l!lK*FQ{ulz_%j8<+n`KT6Mt<`k!fed%r;<*< zvD1sVTu?v$1F$uI2oo@!_SW}4EARt$2uCXrkaq}i_jOzy9R5&Y7MWbqqwj4qU014Z z#3_s_cwShXuUqro>+4fN1e#3O*sc3a0plF0PyyD_!-yheQzC`zSOzF*Mo7m z@bldluUZodthFL`SGtn)CzUh{svn*VS9_X@c^+v0@hH@M{Fo2Q8oJ%>+{9)hwqO3b11PI~ojKTD|U^muj3S%Cfe-JnH|z2W#bQ>J#j$MDz>``o88 z5n7<8SsccLcJeV^DC8RpDI_^CI7a?$Y7^8$2MgQZ;RUYAw{D)Lck-F);=y?t`n>mIGj=6t$oHs3FfQOGrKe7zIZ6cG!+ z;q@E7FS>$at3?~z(b>s2HKyt|vHFbKv7fvyD{e;16-<;p7vsD*w1;~+FH@HeZI)6q z)Wo9dPa+Zxj27~F_YbUQDS?H9;q!#+OD5^YAn9L0{BO?rf+ zXXa2zw2sd1Dg$<&(FDS1e}Wa~8(&3TAriA7YmS25b;Ivx3=8=*Sc^&lGcwK510gb^ z3&5^?Ayhx>;p=X1-O&@?TLmgjS-f||%vOjCz3=K^($3^R!?0N%YjeXS9fZ4X3E9Yv z!q!2Aom^V*dGVTJ(|Hf{7JhoS=?)9L9+Z9@V!xtNmMr>I)aA5QQwd&EaGqzL(&BV9 zw5oP~q_CR5`jKCq55?e4QvRu*I(X%E>t2b;`kb7jhNfjji&ahKPF!bh`Km)4bY|s)_{iQ3_Lzf=?)KDZM&!k>@=(NX*bRDS02y*6!98Kzb%(C-8$ayxgZ5yj+x@dZHZ3#&VY1x_@QxM9{?|C(<>XSNR?MK5Kg$|NLbYzRfp+x=R<3^2tl)(!6B%89?WOkU3E9y6rc+9~}}Ms1$cDby+>C4sqkt_p=7J4IK{;&czqSVopn2XJRq~ zsS0R@gC|pBM-#;)rmGfG^Y3!62OF*TGrjyWEFsu>zO!0bpWM^+W|1a|=zWW7>H-a6 zw@#7p!YM){{8Wh)Zu^_Q=f`IgY4T&q=&UXWsq-_zs+LU08Ep-fx>wvGrBBW)nasGj zp=eLr@=XaG=h2NWyN|~PEVRA2+`|lR61)%_vE5G{T)BG%2JtlZlX-($lex|p0zM4l zcBI2D!VCgt1>bx9_T6<%WTb}K*@m0evgX1IT=Zj=dU$sZUsdfUNMw}%re6&DSQqSE zkzF{kCk>rCJaCkf+iD+VI=^_gu4sp}Bb6hsXl2Ss9k7_|t2pzTAEC8A^BZ)UF8MTf zR=iQgUY{z**;DE)*RMHP6|r1*CgGERcG2{M*>!ZNiI=Aa0oQ&!)>n|>Ir@l5Xl?hr zVKMyf8jfq3m{pSUj9k(#BNkknfI~7QzAR7n;%8MPA?kGcV!%Xj%t^%==g||pMeC`+ zb%sSrr&sLrUgX1B8lu~85oZHL>j?}W^+Hg+5CVbYM)`|#M3Q8QRx`_J3vtunln?MN$hLW!J|fnY_mv( zTehc6f$z1zs`c4$P0>KaWc@Df;T4mnsl~(>T-L1{bsmZSqFBEN#=BVtaWzx$+&!N7 zm#6DWKfu8%_-?hRbPKD@`QxF`i~~+D86K*mf{74&LicjcYh_GT9pP~{Tj4{nc+9jP z)RM&e%`{c3HfIV8>+Kf;?8d&xhKDY*UX4VgY{Rj{x7Jfh>n;n1dDS;$mXBk)H_q0j zM#+2!o>wgH*I-rc4u&y$k<~kM@|lN?J@`z|c`Ex0JoB@r_;5Lu^mQua^7Mz3 zQm@Os{WOWjXPc7Chr?@P>%XlISL4scxJNyFc72VN#6Hryq8<_>tOAoW0|xU z-_ut(bjaw%jXU4N5pM8Y=+~Pl(BumeL3MgzeG&)bilLLVQtv#8NUi(fxEgEYpZSr4 zx49j)aNnbgG(BC@Y7l^~HwpLQl@9H?_}!H`AJ`Zguzm9bPa^@6F2y_$e`* zikgGmRFxd_Rl^Y6utp(L!Q-K*URBW+wl5kUP~Gx<#B?{)J>tW=l*nV>*54$$R|96h zyb{^q`$$Eo)KS4;GOkak8D)h#(aRVtb(DO5+Y!3D{E)$W(H9}^GOH~FZ=uwuOStM# zn|oxrO4=A6{A*U`c`_Xl()ULBliI_WH>(#@sS96C_{iesd~i@sh5Ys}0%CDi&@h}p zE*-&%g=iWd4t!kUMdeGnXTOs<*v_zAsb~g!H*3jy>DHX_pUG5EFUDi{;`H;}X;F5L zj?3F02M3PqXGbHV|3aqIAxxU|z96;m+4f3@52;%ArtL?P5wXLz2#3VEmit=~?)1Wu z-&u1wEOCyAzmAl@B1uhzCKDn*b7CXeWlA7Xx4tjVa)+}930}OE6)3REJ;u@KB_Grc!nyvKcS``br*qT#CS&exPy zox@~9P3IjQ=C%l&gTWfrBj;p|QVB}r++Yfv{!xciDnli;x)Aed`>-WZoZvj^8tq=6 z*MPshouu6dJ0`owc3IIk{g&jWuv_UhCr>=reRsd_+UncA&tRs-+K@8esgpESGq8%p zQ60m#B}^93zXqyWG6cLX@K*DLMKV8Hqg8D{_{}kibLwhuDO`sa=QQLz$-WNl3qP8YlM0f=7A*F#)U8xzC;iEO6zQ$S?F=R+ zeUczbKjVoPGf^=_cUw_PxX8pQe6@rz#s~IXt;8FNey!hs>O@>cO$ag8_Tn2Y6cESF zkzRR(|NVJs9A1ZA@Ol_s84oGvVe@zNO{=1}b6)t^af(Uk*%aHj{ApB!30+iHqo&Ey zxW4wiXz6KOQ@WGzbhj%;ul?ene0BRV1F=I!;9V>K zA4i9Oq}OtTFB;@ufba1ECxq=|M9!SPia-Qy1Pd1l+QzAC?Mb@=n9@pJ_DlUYL|ZP@ z`mtCLHn{Pt83mPlry=w&b^^P)+zFr@rq^GZ7BdtDu_@OPBXgLnnr6k??Uar?MJTibMd>$o& zBDQ|p;l6ww?bshTbkO-05!U%|y+@j0Mui>;BRc+>U8zQsynNzpeRRPn<{Znns3-Zx zdFi~TlF2cIhGroCrGD>Q*MnrTN)5?3i9N6^FDJ?4!#@XD;u@59wO*+{@-4hB&JEd(P%d?b zcGR9ahHaPL7J@!1(=#_o^_Glqw~~AL6}yojWK=RO3y1WTT3oP2b-5>UCB!b6~0d;yxiw^EtJ#k}^Col-l}~r&V|b?jdqP#&T@>OS|p4 zY2WQ0j^K%>4}5G*n4sZ}ISY)3p+XV|0S)g&aC?o_L z2Z2#Si2j^w%%@0Plph*`V6J+GIKzTr_B;fm?*80@X$r0&xCxORxTwP@2&Tcm#^6Rw zcmEzNkMQvRzc*F_gZuMf1UEDei5VFfBjyU|;a@*sh+u<4t&c&kP1(E-8u5jyOA{)S zr4!Xnk&G>5b(e`surAx|!pj}C;o+3G4(ru>F1U{IA)mq(r_I|X{tY^w0GR}VA+*&b zf1GbtJ>1Ay9IrNgUEw_GHeVAOW-*ya*DxkB-s6QnsCQkDlG%x{XR688gA(FQJ!ze` zyF9eCINbTJxjq6Q(vH4X>1Cdn^Qv(VzcxBF92%lwStE`(@d#-vP=&I_BKr5Il8xhJALh*B4ijKCFwyDi-zgs7wl zp)28D?$+z1!y;^)91vCQXLBWw+kKhRF7A0gXFyyLPoh&k4po}25=^_&Y47_n0YSWm zC2cv}V2_h9pW68CDpv#z*;iI)QAWrd{B?WFnPUw`mDNZcGKzTeI}!~Z-i?3N%SV9H zwm>+qtf*q8ej_7)=y;6ZP`BP0%=iOTLwIz4VLkWTTBYj%D3>TZLDyx`;oM4kW)tb( zEY3fE)Y#oOv&&x4l$u9E0WMdCLwcQ}% z(wV5QDAby;C0Q(1orYh=ibW6_;(@bF&HGs^<{I*n1m=hS`l>@4^|7xHVJUDmZekz- z(dlH>R5jxu!uwIgli`;eQi|TeVorAFH<{$32;&#K_otB`)*{JL#K|hgnznYiQc!6T zL@2QIhU3zwIW%L%;Wb@#{#6B)(fFu|upRwdtrrzi^9jKVu6&#DW1OJlxw8I|s$A`! z^ovYpmnE+PE8u`YYtGs=&6su7_rEANJXNRHEBAdieaW}(ilMl*O2aaaG$;Rv@g}sopKhifWN>>{(BF@PH1*Jbf4*>NZbq?{ffEcTUN03`Ot*<|dFE z(qs6{jATfXZk+iz@6KnQGp~db@li1q?`})E&j}TnB7Cq6d=jdUb`yUwi5P{ByR2EI zPWm-+ZdV!nqx_*iAr{NuwT&6yBA$k#(uQG+Zu>&DrTY8q}4nEXxv{< z{T>wGk1fR{*&t%u>Ay@I15D6CY=`IYRT+gw= z7&iOvjv}_ZI^w-pyMySP%v;1sCCt1KoDi)2D6$5KRQyj{n^%9d*N(A-dQMFW7yPCxH#+x2U9#JiH|U;Q5VnJ0Hu53q6cs^*FHNg(A;5O<52Ktv1MOw`9UgFVy8aHT%XGYFFH`SAfL0|iq>BEl z-hhItk5+ESA{sGyVe_!^eSopg)h%r0mjS^R1P;wU#;4WTMW|!(y9r4hSA?luu`4Uv zx!2sw9fKinDu&_g`q3JLB&hRpW^i%Rup$jtud5A6g5C z5&PEYuV5WE(USQIF0(o5Dm}eo*Yq&#@a9eI0^(b3)8&$@2~%M*WS2-yQuHGk0WRw1 z(Xv>&QBEFzm)~JDomjT->#vk7i>-Xuzf(CA?e#|bQ3Hi(V~vE;f3>mzA(BZ0yUQm_ zUkQ=FOdA4K(FLj) zuI6hHR*(6o^Mpn~Yf|D)G&Q6eH$e&~-t{r(p<5M8^5 zi<0?2fx#iuJ!wR;$3N_VAr}CUV0!#>zW>1$BxL5sA2Rj_Cj5mMf8hqW?SGp5fhLK# zC>REmVDJP`$A0Kq4^PflZV*byVH=NKQbfaC1(&Pi-xl7j#*u*)13+RZyAY_}cetoCEWUBK+bb@w zojz$5pMO~gGKT4eE(RgE9-rQZO8&zMVm9N6WRrlVmZTKv*6qeyOtK8-iy7}bnp&T& zq*{#kg{NqC)#q%6gcg_|?9ABXp}Go#Bj{w{{j;JwHavF!@Khx8@pK#n7+QuLhv$E4 zmmA$7Kt-)cnZsu;LUVhH2n}Y`G`xpCJ9OCsWD_i-n5442G4Edpa6}37@wPu&k$m{? zO7T)bE_1ZpnLW6 zYf?uSXY5I2Zo`(XRgvo=0POThj92@QdmXSHJRus|1Qh$+(t8Q*YCW5NYqNRX{j^%D zeD{n~JB*IzK)d}8z$HeD1@Ql`wzm$e>TB19L0TG=ZWf?Y(%m2nDG?Cq25DF{(nv`! zxmqQ^(hmP5h|=dz+V!&|&w zYmA90$s0gE>3q!!H?5X)*eNeKD%dvJc6_V2Bcc<@3uBf5x1;YMAJc1gs`+mg%hMBw zf^TH)oZ2jU_ER0sT`_}-4(8uLd<{^@87FpM<24LP&TIDdbBxCOFNc-02Nq%e78D3- ztH<@YI7y0JNNm}*Sa~U1hQY=N;&;A=*4>|0fryQN@BF;UyA$ae$lsh@e=}oq1`=C) z9pCqb!?BXN6SuJ$5r}&QsQHoneR*k&&r)o}KmR2BCG!yxpCYxvQwsgAtMWEmiqqz^ zwk3ir1>pQh-?bc0&Uk0~5nTTuHE_8*%Vf*7rm>-R(nHkhm6M~%@V_1U(Lej%jN_Gp zzV(Ey;QXIRPbcD(jqyGEbS_9y_%;ibC`wZMis8|)bBTsSp74{X+>+bXzuA+LGgGUx z^fQjK>(jowdU;;zMJ<~8d6oO*={a%NgEC7*~nuc&CJ_M}23Y|@9vE>HL;qLs-6 zH(o^61!GNrF<0@K6$xO>nQm@m0!Mx(M{w({aDJymjn?Pz28f(S&K&F<+>FQk5Qz>6 zckZS3eQei&3}gH62zOSTllu%Stqyre$f~$#r$SwwI1JLNT@m*FfOE zPKKm04N!~|1SbpYQOp_KBf0(5ac5>?%VE>shg{>VYh6K)pNHY&y7VtEZ4$o)>(d0` z8T7WIMpatXpp~xGi$hdb1j(SWEF~yS{;RS~5|Dhr*rg!DzDspA$hl^rXwK7#0ZBvf2jR%e+QZK%;E`iwsYkRWwT?lak_FR=$3q5IPiM1jM)^12XQSRB)1ST;>G zX1^m#r0?%PcToU)Q9ZEa7-pEv%?yZE5Jq;4^BcEkg2yi+pPjn9`*le-^Hf`Nx}3Q| zOtSx|oxmjn2J|uppgSa2En5v=-m5r3uj5&yF^5`Aexvs3jM@_OUtRW7FR>iwtl6Db z)$mykCgvN?@iq=U_RWbaFC1XE-mN_j+_r1h##vBP$Mh+g0tW z5q}fB~Rz9;duf*sCmM`_-^JmnC`qbhq;g3G` zOqoz4lK2H$vZ?`H0GKyynaW2?E;o$`22pp|L2UZdV-1_X=hN>W!%sv08EY!Z#LCqnMRnbo-Va36%nPITbWr?!!Uf?CLXQZs!1l(lAch zSl{F=H48)oUQ;z()Wey8r&ZQ2|9QJmSqv{4ee3fu$)tom1i%WzDz0rt6Spd9w4A z0&`Sj8YqzIyhRWw2tV@x;wcA7F@zz-y;2}+GM{fdMjM}ugTRdiwIUy^@Avnnptp5< zH+!d3b6SR`v7GvIu|cXd_@@c5KGIpCt&x9KLN4e|bx7tS;=CvVth?#~Cq_gJ1(rzL zg4w1Gtfe|-D;swAu<|$7b_h_yNLaa}M!o;hQj1qZQ_?n3(Sp@lCsN_pyP!;~85pYT z-G14=0iXR1%T=eC&(Uz=Zu42|?elxeFS~B+cS-?1VZu=^MmfPPgg}lkR@#3Rm|(|1 zFhg%pvuwwoR+rb+z6B3TY?n^doqY>$qoElwn!hFjxVq1poke=(fCLkp*!nszDv#-u z;3CLckPLmLerM$akUV*e*ykx^K63cVb@dzj?b+-!<&AH!1hLY^=636a7sb$?K=E1fojwokTY>Z3)5IG$+=^> z&bqqUk*N1Lmvby5?ca4@`P}N=KO4{1SjEW(!p-3CZtM!^8md?UyqAOfZSbR+D%GzX zFh|z(Kvz?mwguy_(fuEbC%xl5uCI}LnO!NqrTt_YTAs=l`pw5HzMhT~lnow#@$0hrgi^C|60ZhE;8BCV$0mm)|R1-Sd%bsAW0zB~m zOrm2C?-SS|mZS#W@YX{8Qiewsb`WjL$exB&oHGRR9b%_* zuznOSOz{sps$Lpq56ax^^>=M2@HVOck?Ys1*j=EcRAxi=hG#4y^&Z;E{Dy)w6>E7C z)kStd$I?_>tlZ&dnU90k{nw|dlBGHAJ$QL=lxM%;m=C4ssO6#e&6B*<`w5{l881P{ zd}J)SiuNT_k!qbuo3_KyYD1r}M*UV`$LvE|7ru6yHJ@}#ds*YKZvUsvF(eGNP9}Bh z_H2Yb`wbN)RCBa*lvi~m#)61Z$7l-=116EzC9*mLp?3!*60B%tYw5w=|Z75ME)PCtMiLRB@c7(STM$ppQD5 zNyC8^3(|)4DHBr7lvNGNekTIXvnn^!E)ZvKaxy{9F&Y+qMt*6&`gI9{i7>ckgKf5K zi1KdHQGdUZ&zlRZ^pX*KHNd7zs4I9iQUcs{iOZZ>>oOrioaV%I)Kh#5zFbU4^7@Gt z4Y-&z69Ul2>gr=?sy5)G%(pLH(v!`2&Dj>8AT?&wkW9V-mWkF1V5PnnPE2fGu6&73 zYpD>IuJQyS&*`4^BwQaBP=aGxUbH4>-B9CP44{Z9@ zazIQ5p#A3ap~4im*^LlYm)$qb;J61^zbAr&OiSXf+AZ8f1cGrqA3qF%ya z#@va)h<88*v*kAAo}g#-`*wdQFIY*KaYd5ThWZ8|Jj}WEkDlddCZ1uO{{}2d5gJ_^ z677U26skXeGC>uDd&@WzAf5h(25id)IL{*yjeU{M<+LxyeYlL#Qd@|T1PJ0lkXRO1 znTL*!P;$w^u)+0$$sNdTUvShsU;e_#TGCk`>7VVlV&-Ab?|rE4;{)M+4&>A@M#e{R zYmz-M01iHGf8BZbu$H?Kw`1t{*Y8%6NbRr`@* z>{&!(@vopd+L>`Pvgowa{dym5ZQrZGTSvVAW@hoHC@uKaDCLKZId`$6IH2<%I!-#P z5N9O7-g!wta;qg=MNI%&vqsv%!o@EEHZy4R0_!gAKhc=^k|M#m3#+`(Y;N3&T&CFbqH1W(hcf6Xf&fQ(lsze?~yc>Tp)afWZGA8x)GGA@3 zL@SrVm`)R*ZwCn5>q|n!es{Y=9qsz#6HC_|Ka>6l38t$I^aT?E02g?$YMQ%#^2(^g zjrHY9jcWFBx$ev#VNpmAw@ZK~G5}PJ(IEi$f*#I?0Vf56MS^GJ$++!OwVe0j8QG@dFz4!1U2Qvy>4KKrc2h8#Byt-E=#x@OSa?CDi z&%&$!{1U;u*bCSL-&_CU-t{;oTzUbuq6WHI0gT(e&-FG-C zERcapHNj4iRp_)|1|sbtyzb}P%0tfzEyadIl>iSWvS)XAe|%PyYg{;WV7+Utk%}Ud zDoI$3&->cTcvxzL2YCF+xm}nyUQLFC4OJW%Q-~=~-D+_;Mz(c;d zNK>~s2tjkaE}C`cOprH8yq@LdRMct^;JaHKmDwCb>ry9&$%6L;)e&-pZbO+na%|c8rm?qW&pM$M8_6L)2Ko^>%i&%9(e& z$@4nYa>y2cg^z`y9u-$wqca_Ptd=%h~T6{B_P-J?ai ziB-3qM<=uOxT=ZXwb?;PN`GV_|!l+<*3br$kuw0sV}x1COZE|pQH z?q7Co`-*L`!Aza&W#<)7Bw8<)%0X3YpljG_IurT5X6|a;mb|hDvu2N3rOivQ;BGC_Rhg3@^2p$SDOx!}3>?jpbOqMoOKDYJZ!V zG2FJaQ>UkOv1?_Laa+Z{66R%?ng1$v&TTQ3Fj{?IobMHdm~UJvfxj>&YP-$WY)smz zK9eZwn!P2!ql~t2UGgFu*W~AQ6nU!2`F&6L_bIGHF#?y@M;Yk!L#~6xM5@CL46&HPA-Y9+R?tj*=wC`5WslCkMa+fk%e!e)G zW0eWlbo_<;JO0jY_H&X$2U7kM(ZBZV6fj=*XA_zfLo*djZT#9u6b}4Wb2XA6KI`A2 zbC-)3Rp$fjBTMH#&!izN>HJ4a%2THng&9he9ylylNsapVESO`t%B|W?a?;X=pDCFt zw&gQK(xy-UWX>=>@?26yD@c`#EQ``yxv0fE2j1#?fw{dg=#vz-+PzWqdJc6SzdYS$ z+xfVxfJFao$m--clPu@ZeYOlL1I(GiSh3 zl?whS(QGUU1mC&7)F%br5ipI4#q6)6=zF>h3Aw(*=7Jq2eV2wTRnI!yC+$F3eojJm zBADp~sF)KpJC1Sj{~X%w@lcf3C0#SnnWYwntvX=-wT7cYbnfd7HVz4)v!Wv!8|KiD zSPhkKpnWznm`wHN@W#YAD3Jn&-*I@AAnL=dt(s=jCkyH4-B=cl^}*(_(&L6^3SYXU z#nZc{UNa65K8USKtr<(PL7mZTDC{~q&`@fpZ~J&GOTp`}dhl$;%57`aQw z`^}g%u`wNFo8RC_gh*4;a$W%?a^k+%IVGN3d@_&_B(jyn5Vt)qTk>S8j27v?Ch4wQ zB#r4G_x-Q-O96O3|J6MGH~aMlw*`Ou7davT;{8Cy`C#?1{)P*ve{CTWyb*x70uL6a z1V=3YuSekl41kagH#7j+HIMTDe9d>lzwQ0|op7UwK$kom`fu(1J^Js$4hA z0O0_m3iJ};|F z_js-Huv!-l5zmi8tgzX9%;#U>z~D$=aK^DOYX5%p4ergwi1Oy@X65FOEc({R!2XwX z(c7I$r^Dgz`Zo=q4{`f?M`re0aN`(T5+5%(RwKu-K$oP|X%f}n<%A`&-M@HNnx_DO zD#!s0sF&>Cx?b<~Vp?vFww;#RP`+>+(`J2KkUj_1#^V-6Y4g zdO!?9$9dd23(zFMopov7V1Gz(V|K`MtfH9=82SHGP}zQ;1K?ubm+T`>y2AO}a`*vS zwr5aW3^8wW5Ou-kzd6{|2HcpNW0Z?xX~-#j7Hx|%Dy7FHl)OI;wre%H>~*?tj~d%@ zEXlj!ueP3e?sdNc@E-K_G7syQT1B(zG`%;Ds;_g*MaG zlffrxbvVy9UyelWY|Ko}ht1DUbBg!f^O`QT#ZERC>>>)LIaP)%50GdhS&$^-;Bu1n zY{SS{UNM8#mW}qJ3uhgM4SrF}iQ6TuIWGDr6U`F#U28eQ?8?HAzi=JvZ*Ij_vYWq2 z&HLPdOxK>~B&@npi@r8=2(>*wJ(qW~ZW_7Bdkv9<%9B1u*Q8mf;PO#}jtp~s&#RNJZES30pna}+9>M6QWgst)X(ag! z_6e#Wch~Zq?RmVB1NuuD@aYb z?{wuWNT+J7*yj^1Hja@KMzIXI_weQRc6Z}VJC!Iu{W+aPwKt)5vAXOCB(M_-@~B?`QJDpVTnk@bR_k+C zKwhp}ijEkU=g1rMJh~D3%CV>2E+x>Z;=!2dQW7OoC&&I}h7PRJX z@LqFffmERw+2{S2@*(kV#WPjvrU6|_DJ8v6T0CzHmYe)*rCxKc5-f-Et`b4KmQMu6 zY^`k*?juvfsZDs!mQ=ICQ-!7EAy(R{(3OxsBn;{5JUn}G+C;a+CpI8gvr3w0FM9X^Qqrc03-7L zZ1+|5rZiE(nOm0H2bL;5=>EyG-u@%HrI z7pvyr>ULfO5u07gg4@-Ng%0))qDC6WUbSDXK|M6%x= zk3PMDVlYgNLKWz~8pV0i&6k-5-(2mW7%>#~NH=@kBN}09x;%%SW|E(H4(%MFUP|8F ztXZKyHyZf)jj0OQB~BU1vb^ZflUWj5b6>Bk4??F6u#QURS8w>}K~vb{Z`@xD4G+7y zJKHbELlyD9xOmRm)y}|shbzySJ!we6L^EMK@9-Qp#^davX>!7@S6?zC_xzoS4<+)c zx?8sGH`H6|drEd{(Wi!@J4opQs4k?hsIxUo_uIQA!?Cq3<@;W|>|V67f8SD9MKNRW za^ak*7uCNgG{Q9swDOUFlF*!ZaYw>f%inpOq^i2QFEs=VuB6)E*&1Fs%@mVz!k6-m z!G2ZrQ;wpdYY=MPBupD;A}PmP4T{Z4AiBJ6?cRDDc7@8&1le1D8m}nxymNiG@z=&{ zXv~-lxwxmwyR7XfKD&`plxH))py7v#GcJPR+9SiJL5WeHGh03Ana8c)%!EK9^imZQ zO7)_swHU&#aVVW@LG;Fvw|;N223AS+dcH=!ffB~VQxc@b3Hw}j3?cZVRprlPG|KWxj2RbJMT&QIdaG6eH93PJZ&MsatwGFX($V9jb>O@v)by zf&Snz=A#ho#BPoi9J#?hc5zJ<6VeIFKOAv_(yzMd#>FFPyLNYXwV=O)Ietw~e-<@~ zR|)cqwY1JfeWa`PPBa@t&Z>;gko*R!$PPn3Ezq!=^^f~f-1k_`y}hfrhpjQegf&wM z8eYWNfnIL*Mv)UlG9?*&sGr$7nrvc{r&8iq^xV?=_xYdAA;@BnLlKhL#UIB%y$L@q`~Ylnm-ZO`NcKcdvNs$qF7$ha{LH09p1Pc)l_L~|Am z+QvK=<>P)PXJcvkikuE>$vKb1*2>DU7gSqayY1k~M>7+`{DPVGIwaj8!K8id!TD5d zG9;(9Iz4r+&9fNu%ENet`jn@AjHkL)9xBGi+w0S%*o7jOCl};ts1%%HGD_L%L73Gm z@S@PlUWFq2{m=#>$%{K$%r~{+ya^Jz%)M*}S6TD8TLsSPJoCXTE@V-FEmDbp@8sm1 z72(iRDeBJev0Swz8gUr}T3OCJR5dFIcaygwpC@Gd74Sq-ScDOVNQ-$m^+H0-b0@n1 zZEtT=eI@UysQpDG5jvamW||%h4uBWHd@iE$9J3hVBSc-`&Q9 zI#hxJ+P)fv%7Z_VY^MMl4r-%~#u8)VS*HZtan?xc-7iTrs@tC@sCgYy_omVG0}LN8 zDzQ>hwVf_sdAuU(t;93#w|578f5+_`XedCf)q~-^NO7ey=qsc|b0-4DqLIMU1@Bg< zTdA}xIk{q*M5-wHrD*DW;ky{b&mGkJG53t72xqy-DmndUr*{&jtD1P%q=isK7wv*u zW?dsn4Y}(^&j9-ml225p(?PDaA))f0ChL)Fc>}VSK}OToc2kx2Woi@So50*^{)9|F z{$&*jdUM~UvR&9m{R`EK*Sc$t%}MCFj9i5+eT&zA7IF})0CUDYF zPvbaiUzU+^@D_^2$yw>_#c6w<6rUc@+O}wd{hc)0lofU%x+wbR5JRqjn~sTZNWxNE zON#;Euh8`k7GTM@xRPw@>S9d8TXF7rM$QJ;r8>&j6_>QgkBp3HM>KUha(ow)F1a%6 z(B#HXyFa8-|BbIgX0kxrEG6bNfZ6&Ra4~|W*@SiHL~=}npeD>lvEpB!!yasWGE~9a z>8r6UBkYrOcY{6@v3q~X%1^dlU#!qq?!WIBx{aO(I??77S@(+UQm&fm-v$CE%H{A5Q_e8+d#V!%rwF#|Rf3KCT0}w1=V3 z>926`2r|JyZ{b(PnH;stLpVF5!qF+X_7eZO@^3H<9}1wIBpvAWRWK$kef&RXVMX;V z(1axqYLp)*|P0O7Mz3_`y?}}$al5S`^>LsAYD}>)$?vA64cPS~4 z1cis$R=sRv!XhJ;LPH}G=jZjJFcjTozP~TB_#1y$OkGYSg>pJfTbG}n3q|aTIjP-U zm2yDij40IwI&UrzrFWDX8X7xi#NI!iZ>Cn4^FCkt_Jk;rhWDvH8mh_Ca6ADeu^2>7 z2@H8I1(>hZwQclV?}Z;P-3jp{CZkgtJj!!SX?U^SQTA7FyOkN!hi(>=IATF7_wmCO zoVjspP}X<(g@w5{`0#i#RR((N-=2df}AeeZ=YK@ z2y=UZz!N0MtJk*)kbiWryS0R8a!Sd)v-SQNebjKih+l7iSujn-vcE?DtX0$}KXLc| zs8iHivDpk*2SIj5r)W{|k`EnX&BY0RQ;oZD^YZka&0U-HDzNjM$E@V4@sb$>L1l$$ zw{5bZRf)&0KLc}i{&$%{y*ifrs|Dm8Tpk|<9skkMaoCB#JY?fe3gfYMcEyyN7NWxIv-|F+!eXTupW|2T6F zGseKu&0e+VoC8!3==vZVpy9`CXyDOo&%-$I9~J_ezhN-CVtF zPXEE%%&DwzOxfS7bxU!td8VK*Dw}V|Htc}~&n1&+cYfvQEWS#)<&?H7b&t*hjc1Ex zeTPX{JFl?YjoPmMe_inO-IqHi48*J`QQRp2`y7B;+I!#^_z8N+j)o@bOrC^6<&CH> zR$`-J!5Nu8F#A={N#-SyW;s>0&-lk-+`BVc+&&UeqJ~;;avyOz;iJl_dH`l2Sq+Rz zc$6q&&fP@GeB-E9*vc;|AV@z&>HVTh zHM>syL{6_-g;>uZaL%M+0M^Iul!;PRUYGrZANP|97JrW7w0TJ*~YGr)ZBwn^=Igc61-ZsrC>3}bVT zM$+`{U$x+xJNYF1CDUzPhST}&7dpe*d(`&Xb(;tvdU+#6(?oTt@nTpcxXwTn1?v?Nj zPagyd*SEV+cdRD8Ru#4%X)==m(*T@agM%*tNwd@CXFGS{RJ%A0w?qFnqc0=GP`|;g zln~5$1WPF?`h(JmV1JX6PF2PFPxYcirP!ZLuB7TW*v%|bKM1ADh$nF$A-V?IUMXev z7)$vvP5T?80*{dg6GjAG^Yb1_=64SAT@>Wo^>9b=l_ZHuEuA&G&!QikSip5^4>l=B ze0;M?v;`1$9M&BV-HYsD!T_LX&XVrRJSu(hFd6CNgESgqxnjpmaT2^IHI%-)3-`x) z8v{iNXBb4Va!YDc+hgu}Y488Ij2*L$n8t@QwPl9L?fAc#=*MC5G;i$A?ZCRH% z(Z~Ee>QHTn0~IS4mD|eek-^i|z@X_Aa5!f;@4?5?$dqR`2kiTzC~Gf$!#T`35n<(S zv(d5#t?^)xaGp(KYf6-N^1Wi~(E$bXs!P7PK=Mi8YTH+Wm8%)QRke05?42u#3No9l zlALujW8ni5D~fh0#nz`qQy~?a2+(SFQf@0EXt5@^Cc6L8&#zI5Pm+HEoH`!joEbx! z%Yn#~2Mv#g-B@Amv3vmXj6Qg9fE^qN)(3YTCwq7qIy^VjGo*h`gN^j%GF6LdS2hE$ zf%1gf9d${KL|qPpiuFn<{o@%W`X>|NpDCzm+wWoYiE%T5Pw^ zqgmp*-$H(Vj29O<|8rdGgSXi|p<`aPB>AqSt6FE_XphU7MaJkz^cDY~^Zw*;amZG% zm7D8f|BKNA;-^R%Pn{5D+DM>Sfz~GEii(P!9o@oobHva?opWMn>Y9Kih#)L&))UJ@ ztu(sdd3yXR2$i?qmNffN$HKyfXREh&kVlj;TmK|r1Mv_m6_~r@EFf(#Bd`uaLA*7U zxB7|(^5HW;FWvfmU0|#E6StJJjAX05nFiRzKTHkv6Tzjq#a3nzC$FL08dwXEX&(t^ zB7LtUGVqAi0Z3LzaXbB$fSnRFmv)vP#_}5YFv; ze6K7_|Ha|bg_iW$E+J@tx^f|`(r@K%E-w%H69)aaU!F?TD$0=Qt1?{C%VA$FgyfH5 z32AQbGU_itBW(I$-o8e$2tvV^ZLa80M3|lk%*G~%i8N%dxSulyq|@x8tE38(ef^sc zY`cR2Ly_t1A@leX;-@=}`Lf%}iig?&2L)!?|D2Wj*2ycNl_{A|U!0%diE!^(fEX|! zcXXA})_`CV8sXzfZZAMcS~o%Vs`8l8D}K}|OR|}sgMkvldBvfhBy|{RNuzj9EvI~Q zES<594c{t4-oMXfA2A!0P{n3hA=O}IKlo0LhS-K9x@^SZxZ+j>fN{SjQTC((4QZB5 zq9}TY#Tj?)xZnD&E^=s=dlb*Biu+zY+UU&Jqe+&za!6)k=`H?x9x*X?>__zm-EE!& z?0w!ogxpJ}Bfim+g|@eKm+Z+4{+gJ{pmphDkbJoQO=01xYEa;q7(a)?Rf^xB2fb}C zNhzI!2@7=uLKG=x+NX1hX$ zA+StXd<094g!5^xAQVeN601VXJp8pm*aG19%A6Ti!zU-$CB~7?vjv$AzAmRZ06`sM z&*K$AB2wP}&}6KOqpgfUCRWf&F=!C>6ak0oHHsKYnR3g;E8)*W5J}{#xo{~?-=1L0 zKf3v7=?5jv*Q(bjo<|PkdJDgzv5=8sd)T7c?dcwgE6f8f!I$o8Q2(TY%2&Su67TR6 zDQbcl4|<6geu%UzVBs$lH2|}b04CZS;*<}&Xm(isO0{@+qPUZnWU{G<`S)Hs78?RF zuHl*71NpIMAMOAyvHXd;Alwb#)i}?pmKQ!Wkr)OzD^-{g^i~G5&HPKw>cw4OQ!%NP z3A~&ZZ{#jnyNZ{cMzZctzLVs9**Dc`{ll$=M&qSYZC!Ny0BUpu*?~od?UJ46P7qp> zeI^YlGi&2c&01Th_38W(OSXb2Cvr;6-Lsux(WXWcyHx&Sq)BXHrsS7v2WE1|7UXNH zM7J_;zou+~xh>tIrVtJq#is)QL!)cDHVJYvAtI+jlOS(g)=VfRAixk7A$Va*1IS3ayzpQ*4gFdGt3(t}(Yc(J?L z&Nfwl0G=yIne}UC=;_)k_}dysDW#0k5Tr71W1kqTZNAT|4;7d-DUjVBQ#8$ZIJg9$ zeT~r(m4>8^%sm0BgMX`tf8WVS#2B~1gPkw{mjPuZ>=+&XNl}5PQLd3_w&KO_nS~g0 zM_{VCFDAwSfU98uN%wdK;bQYACqQP#W-;RrW}^nUY-ztTWgMYY8x?oxt-5XLj5Rde zFG_rZ?A-PuNz-&Ms0qvv0rD^ACWS7BiUi<`y?H(^SCn{z&HKPt45#IVbN4(j@+bpr zJpjM#U$S9%J?<#9h|XO9C#4`pa=#|{a@Y)Z`RlI|KW{rdEf=phiW2RR9`R~ zE+DGAiIW2GsciDUcEuMw?{i=v=z4d z)&t18{|?(Rd{o~xC<8*eB?0u(CzKD8>U-TPwgWBe7rp--wp!YHIsOs>VKFL3n3L0z zq#D6@yVf=r!20~YIpvO8W;N8}w7xUtbUiq?C9qJJY_{Q=Q#`{&3T)Mq{3bB*>d4Bj zN%tBcZpQBc$i9`*Z4(i7nmp@?dq0j$Ic~hz5N<4EP8W7$obx$|Mobg*VjSpaSL?5z zKYGeM{0krlRfpd@!sVDhvI(U%b32(l>=m$^;!P(0+=0csuCre~v7jK;PETJ=jgIj0 zR2AgfgU2NdIeD)L+6M!SKQvPPWk!S5qPi78Jsx3zzJ6{qS^|`a4--qbJ-j5==e;lh z#$qVPrBNycBdtzbq_m;qBK#_yOo-p<)p1RZP>IM;{?q|L0ZHzQk@X=mMt#!WRk9 zQ1-h*nTM|dL48Q5c!7f|ATgWjo&)2Lu88CtF|~*{*O8q}W`c*IYzF9DT$Q9qjb;-h zz)DQ1o<}u_bl^RDwbE|H9O0c^#_z3o!(FclE3_2ZVP$Xg_14O>Cr2jSLcD~~5%nL1 zUdO3@2juIKnZopyUXQ-J-qZS;pBQ5CWQLy%qQhX%2cg>Ds5zf*s$c1;ML)y#~A21iJGDSCuutU+SB;AIag)hw#Z37bBub%`JHR3lsg^o z(3kpLj(LIyowz{PTp&Vx{AB5RLD2o({OH(NpQKflbTds4?;XWP_GuQjs`EI54 zMvHly&sOFQZ}YbsU9Fr88r(ruA8A^F>nLhyXh=v1Eh(aV3&YzY zm2#uXskHG@y;_x^2&&TkKCipwK(%-3u)BcZQ~XclwYEZ7unFCqKBx!yhEkf23Z~lKXMG!X{beuUy%io8mrz(;_6 z*RVC_39X1?%~lMd>{mk8e(E@R(K;IDvC#)I6-K8XC$6MLhUUR0$R=q&JGmY~v?T;s z>B*~i2i0|tzqy@Kf)+dz$IS*xn$b*Vvp2WAUiDqTxIoNILnL9!uj8x9dFAadB$K(l zYJkeGdSv=b|N17+Q>}5ESvvY|B?7xsQODbW@{!{1L9fGVAOnMRyye3t)A@a00Kl!~ zX9WTB_YO#bZ9Q}6o80NMj#&wwrutHvsFq6nAm?|AYcB_$MFGN`hNXJ6B)K7$P=69N zVrf72$TnZn*z3TW$SQB>P^0NCBu#N5XN zye!RoQvmG7K3EEpGDADHpaW)vi@Vtt-DI<2`<&&4?bGOTdQ!^F4}vu9fQvFcIuXF> ze*L=7X|bRrpF{F!5#QM4*THWBe9DQnEU|6{@Swd2C^2F4*R<^$Hs(?%GAJ&!kRR7n z&cZcpL~?U)tX31Oq@X!nWHy<1D2Tk`K6(Me@oD)K$53H3;PEX2FVaWpsrnctCX5sd zCM_*}f+Gb1EN~*f^O+-M&ST85U+*Mx-#wBd2Je`|j6PnE$Jd9ao5&gixMVQ&x7cUV zX$?++HwP6o%qSJ-6p z>-;W00KEXcw}c^(9{PNVXAp6C(V?bUYf<8iDp{pUkW!PR z@eEPJXX|?%|LXQ+z2Ft}o}}D@hF7)wVs4`^K&5s}AY5uLiD3%lt05?HE!ITPIX?6j z59;N4n?^(cSf;gAIuM^zer2XlXmm~+(W8pu_TwV0Mu}H>;z9eDYlRvwM%DZ70%Wr_ z?rHfG!^z5mSLt7UT=0{+3g7KZWdFVABv3ynb#%@aWHPI>ME!(qo7dXmNgRH(g9wG? zn{B5S)57ts^b4+ymeGwQEAI=H;wjdfHL0jT_RpioH#awd*=)L-Z^3$%-j$E15Z}I6 z`zo~`q3!A~HGZmed>p!mzK^p(%31)n- zhgT$$oRRd=L&MEo;kauBt!5^c98<`MiWb* zOy}+b9W;?0z@lxpOZtZMV|s1%sX;m^+|A@|tE;`+`jVEME=6fCQB3K=$6Q#DR>CDY zU%QYSyM0jD8sTgP@8nmubE{o)WZa!!099XDneBVAqLpiX^Acb)F7Unh_<#S!q|#2E zFA#l1dpOp4b=P{6Sv^DW&*$Qrf@q2n$Li()U~ED{c)JUt({`$1^AhmRN;g8)z>xzA)Z-S$jtZAW#m1&QO}JR{XfI12{4Kmu z6Zg1xV$FT6P#MDU@HIj`=slS}x45v@Jjo-!r3F}v;frUuqV$DVdkGB9wQ|sN)X?Mi z3^5`JpTB=BVA)AvNnj+Z&e?i9nl#?95WR!21;t8Q@U6+ZOe*06k=@#ilCTWuPe?=C z_bm)-k;NmnGP32C9a|#$EP7cL zKj_~`He9~`JY{jZg{3)Rqr&7|M~2JJ*edr9m!S_Kngai1 z2Q3;-?Meue?2LU;;7UrGWnUBEl literal 0 HcmV?d00001 diff --git a/doc/windowspecific/window-matching-kopete.png b/doc/windowspecific/window-matching-kopete.png new file mode 100644 index 0000000000000000000000000000000000000000..4c34ee67536df12184770ed2c81fc02c25312ef4 GIT binary patch literal 50418 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 BDDD6N literal 0 HcmV?d00001 diff --git a/doc/windowspecific/window-matching-ready-akregator.png b/doc/windowspecific/window-matching-ready-akregator.png new file mode 100644 index 0000000000000000000000000000000000000000..35ccf297f431270c8e7c0ba2b913e56caae23632 GIT binary patch literal 50090 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 R4!|!_6l7IpN~BDK{|`HmFgE}I literal 0 HcmV?d00001 diff --git a/doc/windowspecific/window-matching-tbird-main.png b/doc/windowspecific/window-matching-tbird-main.png new file mode 100644 index 0000000000000000000000000000000000000000..98a0df802fafd541f99eca9801c47bd8766be4ea GIT binary patch literal 55370 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 T-LcQq00000NkvXXu0mjfXPdXi literal 0 HcmV?d00001 diff --git a/doc/windowspecific/window-matching-tbird-reminder.png b/doc/windowspecific/window-matching-tbird-reminder.png new file mode 100644 index 0000000000000000000000000000000000000000..c32a280720c55bc9e54a8374b5b835adea60751e GIT binary patch literal 55508 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 + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +// own +#include "effectloader.h" +// KWin +#include +#include +#include "effects/effect_builtins.h" +#include "scripting/scriptedeffect.h" +#include "utils.h" +// KDE +#include +#include +#include +#include +// Qt +#include +#include +#include +#include +#include + +namespace KWin +{ + +AbstractEffectLoader::AbstractEffectLoader(QObject *parent) + : QObject(parent) +{ +} + +AbstractEffectLoader::~AbstractEffectLoader() +{ +} + +void AbstractEffectLoader::setConfig(KSharedConfig::Ptr config) +{ + m_config = config; +} + +LoadEffectFlags AbstractEffectLoader::readConfig(const QString &effectName, bool defaultValue) const +{ + Q_ASSERT(m_config); + KConfigGroup plugins(m_config, QStringLiteral("Plugins")); + + const QString key = effectName + QStringLiteral("Enabled"); + + // do we have a key for the effect? + if (plugins.hasKey(key)) { + // we have a key in the config, so read the enabled state + const bool load = plugins.readEntry(key, defaultValue); + return load ? LoadEffectFlags(LoadEffectFlag::Load) : LoadEffectFlags(); + } + // we don't have a key, so we just use the enabled by default value + if (defaultValue) { + return LoadEffectFlag::Load | LoadEffectFlag::CheckDefaultFunction; + } + return LoadEffectFlags(); +} + +BuiltInEffectLoader::BuiltInEffectLoader(QObject *parent) + : AbstractEffectLoader(parent) + , m_queue(new EffectLoadQueue(this)) +{ +} + +BuiltInEffectLoader::~BuiltInEffectLoader() +{ +} + +bool BuiltInEffectLoader::hasEffect(const QString &name) const +{ + return BuiltInEffects::available(internalName(name)); +} + +bool BuiltInEffectLoader::isEffectSupported(const QString &name) const +{ + return BuiltInEffects::supported(BuiltInEffects::builtInForName(internalName(name))); +} + +QStringList BuiltInEffectLoader::listOfKnownEffects() const +{ + return BuiltInEffects::availableEffectNames(); +} + +bool BuiltInEffectLoader::loadEffect(const QString &name) +{ + return loadEffect(name, BuiltInEffects::builtInForName(internalName(name)), LoadEffectFlag::Load); +} + +void BuiltInEffectLoader::queryAndLoadAll() +{ + const QList effects = BuiltInEffects::availableEffects(); + for (BuiltInEffect effect : effects) { + // check whether it is already loaded + if (m_loadedEffects.contains(effect)) { + continue; + } + const QString key = BuiltInEffects::nameForEffect(effect); + const LoadEffectFlags flags = readConfig(key, BuiltInEffects::enabledByDefault(effect)); + if (flags.testFlag(LoadEffectFlag::Load)) { + m_queue->enqueue(qMakePair(effect, flags)); + } + } +} + +bool BuiltInEffectLoader::loadEffect(BuiltInEffect effect, LoadEffectFlags flags) +{ + return loadEffect(BuiltInEffects::nameForEffect(effect), effect, flags); +} + +bool BuiltInEffectLoader::loadEffect(const QString &name, BuiltInEffect effect, LoadEffectFlags flags) +{ + if (effect == BuiltInEffect::Invalid) { + return false; + } + if (!flags.testFlag(LoadEffectFlag::Load)) { + qCDebug(KWIN_CORE) << "Loading flags disable effect: " << name; + return false; + } + // check that it is not already loaded + if (m_loadedEffects.contains(effect)) { + return false; + } + + // supported might need a context +#ifndef KWIN_UNIT_TEST + effects->makeOpenGLContextCurrent(); +#endif + if (!BuiltInEffects::supported(effect)) { + qCDebug(KWIN_CORE) << "Effect is not supported: " << name; + return false; + } + + if (flags.testFlag(LoadEffectFlag::CheckDefaultFunction)) { + if (!BuiltInEffects::checkEnabledByDefault(effect)) { + qCDebug(KWIN_CORE) << "Enabled by default function disables effect: " << name; + return false; + } + } + + // ok, now we can try to create the Effect + Effect *e = BuiltInEffects::create(effect); + if (!e) { + qCDebug(KWIN_CORE) << "Failed to create effect: " << name; + return false; + } + // insert in our loaded effects + m_loadedEffects.insert(effect, e); + connect(e, &Effect::destroyed, this, + [this, effect]() { + m_loadedEffects.remove(effect); + } + ); + qCDebug(KWIN_CORE) << "Successfully loaded built-in effect: " << name; + emit effectLoaded(e, name); + return true; +} + +QString BuiltInEffectLoader::internalName(const QString& name) const +{ + return name.toLower(); +} + +void BuiltInEffectLoader::clear() +{ + m_queue->clear(); +} + +static const QString s_nameProperty = QStringLiteral("X-KDE-PluginInfo-Name"); +static const QString s_jsConstraint = QStringLiteral("[X-Plasma-API] == 'javascript'"); +static const QString s_serviceType = QStringLiteral("KWin/Effect"); + +ScriptedEffectLoader::ScriptedEffectLoader(QObject *parent) + : AbstractEffectLoader(parent) + , m_queue(new EffectLoadQueue(this)) +{ +} + +ScriptedEffectLoader::~ScriptedEffectLoader() +{ +} + +bool ScriptedEffectLoader::hasEffect(const QString &name) const +{ + return findEffect(name).isValid(); +} + +bool ScriptedEffectLoader::isEffectSupported(const QString &name) const +{ + // scripted effects are in general supported + if (!ScriptedEffect::supported()) { + return false; + } + return hasEffect(name); +} + +QStringList ScriptedEffectLoader::listOfKnownEffects() const +{ + const auto effects = findAllEffects(); + QStringList result; + for (const auto &service : effects) { + result << service.pluginId(); + } + return result; +} + +bool ScriptedEffectLoader::loadEffect(const QString &name) +{ + auto effect = findEffect(name); + if (!effect.isValid()) { + return false; + } + return loadEffect(effect, LoadEffectFlag::Load); +} + +bool ScriptedEffectLoader::loadEffect(const KPluginMetaData &effect, LoadEffectFlags flags) +{ + const QString name = effect.pluginId(); + if (!flags.testFlag(LoadEffectFlag::Load)) { + qCDebug(KWIN_CORE) << "Loading flags disable effect: " << name; + return false; + } + if (m_loadedEffects.contains(name)) { + qCDebug(KWIN_CORE) << name << "already loaded"; + return false; + } + + if (!ScriptedEffect::supported()) { + qCDebug(KWIN_CORE) << "Effect is not supported: " << name; + return false; + } + + ScriptedEffect *e = ScriptedEffect::create(effect); + if (!e) { + qCDebug(KWIN_CORE) << "Could not initialize scripted effect: " << name; + return false; + } + connect(e, &ScriptedEffect::destroyed, this, + [this, name]() { + m_loadedEffects.removeAll(name); + } + ); + + qCDebug(KWIN_CORE) << "Successfully loaded scripted effect: " << name; + emit effectLoaded(e, name); + m_loadedEffects << name; + return true; +} + +void ScriptedEffectLoader::queryAndLoadAll() +{ + if (m_queryConnection) { + return; + } + // perform querying for the services in a thread + QFutureWatcher> *watcher = new QFutureWatcher>(this); + m_queryConnection = connect(watcher, &QFutureWatcher>::finished, this, + [this, watcher]() { + const auto effects = watcher->result(); + for (auto effect : effects) { + const LoadEffectFlags flags = readConfig(effect.pluginId(), effect.isEnabledByDefault()); + if (flags.testFlag(LoadEffectFlag::Load)) { + m_queue->enqueue(qMakePair(effect, flags)); + } + } + watcher->deleteLater(); + m_queryConnection = QMetaObject::Connection(); + }, + Qt::QueuedConnection); + watcher->setFuture(QtConcurrent::run(this, &ScriptedEffectLoader::findAllEffects)); +} + +QList ScriptedEffectLoader::findAllEffects() const +{ + return KPackage::PackageLoader::self()->listPackages(s_serviceType, QStringLiteral("kwin/effects")); +} + +KPluginMetaData ScriptedEffectLoader::findEffect(const QString &name) const +{ + const auto plugins = KPackage::PackageLoader::self()->findPackages(s_serviceType, QStringLiteral("kwin/effects"), + [name] (const KPluginMetaData &metadata) { + return metadata.pluginId().compare(name, Qt::CaseInsensitive) == 0; + } + ); + if (!plugins.isEmpty()) { + return plugins.first(); + } + return KPluginMetaData(); +} + + +void ScriptedEffectLoader::clear() +{ + disconnect(m_queryConnection); + m_queryConnection = QMetaObject::Connection(); + m_queue->clear(); +} + +PluginEffectLoader::PluginEffectLoader(QObject *parent) + : AbstractEffectLoader(parent) + , m_queue(new EffectLoadQueue< PluginEffectLoader, KPluginMetaData>(this)) + , m_pluginSubDirectory(QStringLiteral("kwin/effects/plugins/")) +{ +} + +PluginEffectLoader::~PluginEffectLoader() +{ +} + +bool PluginEffectLoader::hasEffect(const QString &name) const +{ + const auto info = findEffect(name); + return info.isValid(); +} + +KPluginMetaData PluginEffectLoader::findEffect(const QString &name) const +{ + const auto plugins = KPluginLoader::findPlugins(m_pluginSubDirectory, + [name] (const KPluginMetaData &data) { + return data.pluginId().compare(name, Qt::CaseInsensitive) == 0 && data.serviceTypes().contains(s_serviceType); + } + ); + if (plugins.isEmpty()) { + return KPluginMetaData(); + } + return plugins.first(); +} + +bool PluginEffectLoader::isEffectSupported(const QString &name) const +{ + if (EffectPluginFactory *effectFactory = factory(findEffect(name))) { + return effectFactory->isSupported(); + } + return false; +} + +EffectPluginFactory *PluginEffectLoader::factory(const KPluginMetaData &info) const +{ + if (!info.isValid()) { + return nullptr; + } + KPluginLoader loader(info.fileName()); + if (loader.pluginVersion() != KWIN_EFFECT_API_VERSION) { + qCDebug(KWIN_CORE) << info.pluginId() << " has not matching plugin version, expected " << KWIN_EFFECT_API_VERSION << "got " << loader.pluginVersion(); + return nullptr; + } + KPluginFactory *factory = loader.factory(); + if (!factory) { + qCDebug(KWIN_CORE) << "Did not get KPluginFactory for " << info.pluginId(); + return nullptr; + } + return dynamic_cast< EffectPluginFactory* >(factory); +} + +QStringList PluginEffectLoader::listOfKnownEffects() const +{ + const auto plugins = findAllEffects(); + QStringList result; + for (const auto &plugin : plugins) { + result << plugin.pluginId(); + } + qCDebug(KWIN_CORE) << result; + return result; +} + +bool PluginEffectLoader::loadEffect(const QString &name) +{ + const auto info = findEffect(name); + if (!info.isValid()) { + return false; + } + return loadEffect(info, LoadEffectFlag::Load); +} + +bool PluginEffectLoader::loadEffect(const KPluginMetaData &info, LoadEffectFlags flags) +{ + if (!info.isValid()) { + qCDebug(KWIN_CORE) << "Plugin info is not valid"; + return false; + } + const QString name = info.pluginId(); + if (!flags.testFlag(LoadEffectFlag::Load)) { + qCDebug(KWIN_CORE) << "Loading flags disable effect: " << name; + return false; + } + if (m_loadedEffects.contains(name)) { + qCDebug(KWIN_CORE) << name << " already loaded"; + return false; + } + EffectPluginFactory *effectFactory = factory(info); + if (!effectFactory) { + qCDebug(KWIN_CORE) << "Couldn't get an EffectPluginFactory for: " << name; + return false; + } + +#ifndef KWIN_UNIT_TEST + effects->makeOpenGLContextCurrent(); +#endif + if (!effectFactory->isSupported()) { + qCDebug(KWIN_CORE) << "Effect is not supported: " << name; + return false; + } + + if (flags.testFlag(LoadEffectFlag::CheckDefaultFunction)) { + if (!effectFactory->enabledByDefault()) { + qCDebug(KWIN_CORE) << "Enabled by default function disables effect: " << name; + return false; + } + } + + // ok, now we can try to create the Effect + Effect *e = effectFactory->createEffect(); + if (!e) { + qCDebug(KWIN_CORE) << "Failed to create effect: " << name; + return false; + } + // insert in our loaded effects + m_loadedEffects << name; + connect(e, &Effect::destroyed, this, + [this, name]() { + m_loadedEffects.removeAll(name); + } + ); + qCDebug(KWIN_CORE) << "Successfully loaded plugin effect: " << name; + emit effectLoaded(e, name); + return true; +} + +void PluginEffectLoader::queryAndLoadAll() +{ + if (m_queryConnection) { + return; + } + // perform querying for the services in a thread + QFutureWatcher> *watcher = new QFutureWatcher>(this); + m_queryConnection = connect(watcher, &QFutureWatcher>::finished, this, + [this, watcher]() { + const auto effects = watcher->result(); + for (const auto &effect : effects) { + const LoadEffectFlags flags = readConfig(effect.pluginId(), effect.isEnabledByDefault()); + if (flags.testFlag(LoadEffectFlag::Load)) { + m_queue->enqueue(qMakePair(effect, flags)); + } + } + watcher->deleteLater(); + m_queryConnection = QMetaObject::Connection(); + }, + Qt::QueuedConnection); + watcher->setFuture(QtConcurrent::run(this, &PluginEffectLoader::findAllEffects)); +} + +QVector PluginEffectLoader::findAllEffects() const +{ + return KPluginLoader::findPlugins(m_pluginSubDirectory, [] (const KPluginMetaData &data) { return data.serviceTypes().contains(s_serviceType); }); +} + +void PluginEffectLoader::setPluginSubDirectory(const QString &directory) +{ + m_pluginSubDirectory = directory; +} + +void PluginEffectLoader::clear() +{ + disconnect(m_queryConnection); + m_queryConnection = QMetaObject::Connection(); + m_queue->clear(); +} + +EffectLoader::EffectLoader(QObject *parent) + : AbstractEffectLoader(parent) +{ + m_loaders << new BuiltInEffectLoader(this) + << new ScriptedEffectLoader(this) + << new PluginEffectLoader(this); + for (auto it = m_loaders.constBegin(); it != m_loaders.constEnd(); ++it) { + connect(*it, &AbstractEffectLoader::effectLoaded, this, &AbstractEffectLoader::effectLoaded); + } +} + +EffectLoader::~EffectLoader() +{ +} + +#define BOOL_MERGE( method ) \ + bool EffectLoader::method(const QString &name) const \ + { \ + for (auto it = m_loaders.constBegin(); it != m_loaders.constEnd(); ++it) { \ + if ((*it)->method(name)) { \ + return true; \ + } \ + } \ + return false; \ + } + +BOOL_MERGE(hasEffect) +BOOL_MERGE(isEffectSupported) + +#undef BOOL_MERGE + +QStringList EffectLoader::listOfKnownEffects() const +{ + QStringList result; + for (auto it = m_loaders.constBegin(); it != m_loaders.constEnd(); ++it) { + result << (*it)->listOfKnownEffects(); + } + return result; +} + +bool EffectLoader::loadEffect(const QString &name) +{ + for (auto it = m_loaders.constBegin(); it != m_loaders.constEnd(); ++it) { + if ((*it)->loadEffect(name)) { + return true; + } + } + return false; +} + +void EffectLoader::queryAndLoadAll() +{ + for (auto it = m_loaders.constBegin(); it != m_loaders.constEnd(); ++it) { + (*it)->queryAndLoadAll(); + } +} + +void EffectLoader::setConfig(KSharedConfig::Ptr config) +{ + AbstractEffectLoader::setConfig(config); + for (auto it = m_loaders.constBegin(); it != m_loaders.constEnd(); ++it) { + (*it)->setConfig(config); + } +} + +void EffectLoader::clear() +{ + for (auto it = m_loaders.constBegin(); it != m_loaders.constEnd(); ++it) { + (*it)->clear(); + } +} + +} // namespace KWin diff --git a/effectloader.h b/effectloader.h new file mode 100644 index 0000000..31b0be6 --- /dev/null +++ b/effectloader.h @@ -0,0 +1,366 @@ +/* + 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 +*/ +#ifndef KWIN_EFFECT_LOADER_H +#define KWIN_EFFECT_LOADER_H +#include +// KDE +#include +#include +// Qt +#include +#include +#include +#include +#include + +namespace KWin +{ +class Effect; +class EffectPluginFactory; +enum class BuiltInEffect; + +/** + * @brief Flags defining how a Loader should load an Effect. + * + * These Flags are only used internally when querying the configuration on whether + * an Effect should be loaded. + * + * @see AbstractEffectLoader::readConfig() + */ +enum class LoadEffectFlag { + Load = 1 << 0, ///< Effect should be loaded + CheckDefaultFunction = 1 << 2 ///< The Check Default Function needs to be invoked if the Effect provides it +}; +Q_DECLARE_FLAGS(LoadEffectFlags, LoadEffectFlag) + +/** + * @brief Interface to describe how an effect loader has to function. + * + * The AbstractEffectLoader specifies the methods a concrete loader has to implement and how + * those methods are expected to perform. Also it provides an interface to the outside world + * (that is EffectsHandlerImpl). + * + * The abstraction is used because there are multiple types of Effects which need to be loaded: + * @li Built-In Effects + * @li Scripted Effects + * @li Binary Plugin Effects + * + * Serving all of them with one Effect Loader is rather complex given that different stores need + * to be queried at the same time. Thus the idea is to have one implementation per type and one + * implementation which makes use of all of them and combines the loading. + */ +class KWIN_EXPORT AbstractEffectLoader : public QObject +{ + Q_OBJECT +public: + ~AbstractEffectLoader() override; + + /** + * @brief The KSharedConfig this EffectLoader should operate on. + * + * Important: a valid KSharedConfig must be provided before trying to load any effects! + * + * @param config + * @internal + */ + virtual void setConfig(KSharedConfig::Ptr config); + + /** + * @brief Whether this Effect Loader can load the Effect with the given @p name. + * + * The Effect Loader determines whether it knows or can find an Effect called @p name, + * and thus whether it can attempt to load the Effect. + * + * @param name The name of the Effect to look for. + * @return bool @c true if the Effect Loader knows this effect, false otherwise + */ + virtual bool hasEffect(const QString &name) const = 0; + + /** + * @brief All the Effects this loader knows of. + * + * The implementation should re-query its store whenever this method is invoked. + * It's possible that the store of effects changed (e.g. a new one got installed) + * + * @return QStringList The internal names of the known Effects + */ + virtual QStringList listOfKnownEffects() const = 0; + + /** + * @brief Synchronous loading of the Effect with the given @p name. + * + * Loads the Effect without checking any configuration value or any enabled by default + * function provided by the Effect. + * + * The loader is expected to apply the following checks: + * If the Effect is already loaded, the Effect should not get loaded again. Thus the loader + * is expected to track which Effects it has loaded, and which of those have been destroyed. + * The loader should check whether the Effect is supported. If the Effect indicates it is + * not supported, it should not get loaded. + * + * If the Effect loaded successfully the signal effectLoaded(KWin::Effect*,const QString&) + * must be emitted. Otherwise the user of the loader is not able to get the loaded Effect. + * It's not returning the Effect as queryAndLoadAll() is working async and thus the users + * of the loader are expected to be prepared for async loading. + * + * @param name The internal name of the Effect which should be loaded + * @return bool @c true if the effect could be loaded, @c false in error case + * @see queryAndLoadAll() + * @see effectLoaded(KWin::Effect*,const QString&) + */ + virtual bool loadEffect(const QString &name) = 0; + + /** + * @brief The Effect Loader should query its store for all available effects and try to load them. + * + * The Effect Loader is supposed to perform this operation in a highly async way. If there is + * IO which needs to be performed this should be done in a background thread and a queue should + * be used to load the effects. The loader should make sure to not load more than one Effect + * in one event cycle. Loading the Effect has to be performed in the Compositor thread and + * thus blocks the Compositor. Therefore after loading one Effect all events should get + * processed first, so that the Compositor can perform a painting pass if needed. To simplify + * this operation one can use the EffectLoadQueue. This requires to add another loadEffect + * method with the custom loader specific type to refer to an Effect and LoadEffectFlags. + * + * The LoadEffectFlags have to be determined by querying the configuration with readConfig(). + * If the Load flag is set the loading can proceed and all the checks from + * loadEffect(const QString &) have to be applied. + * In addition if the CheckDefaultFunction flag is set and the Effect provides such a method, + * it should be queried to determine whether the Effect is enabled by default. If such a method + * returns @c false the Effect should not get loaded. If the Effect does not provide a way to + * query whether it's enabled by default at runtime the flag can get ignored. + * + * If the Effect loaded successfully the signal effectLoaded(KWin::Effect*,const QString&) + * must be emitted. + * + * @see loadEffect(const QString &) + * @see effectLoaded(KWin::Effect*,const QString&) + */ + virtual void queryAndLoadAll() = 0; + + /** + * @brief Whether the Effect with the given @p name is supported by the compositing backend. + * + * @param name The name of the Effect to check. + * @return bool @c true if it is supported, @c false otherwise + */ + virtual bool isEffectSupported(const QString &name) const = 0; + + /** + * @brief Clears the load queue, that is all scheduled Effects are discarded from loading. + */ + virtual void clear() = 0; + +Q_SIGNALS: + /** + * @brief The loader emits this signal when it successfully loaded an effect. + * + * @param effect The created Effect + * @param name The internal name of the loaded Effect + * @return void + */ + void effectLoaded(KWin::Effect *effect, const QString &name); + +protected: + explicit AbstractEffectLoader(QObject *parent = nullptr); + /** + * @brief Checks the configuration for the Effect identified by @p effectName. + * + * For each Effect there could be a key called "Enabled". If there is such a key + * the returned flags will contain Load in case it's @c true. If the key does not exist the + * @p defaultValue determines whether the Effect should be loaded. A value of @c true means + * that Load | CheckDefaultFunction is returned, in case of @c false no Load flags are returned. + * + * @param effectName The name of the Effect to look for in the configuration + * @param defaultValue Whether the Effect is enabled by default or not. + * @returns Flags indicating whether the Effect should be loaded and how it should be loaded + */ + LoadEffectFlags readConfig(const QString &effectName, bool defaultValue) const; + +private: + KSharedConfig::Ptr m_config; +}; + +/** + * @brief Helper class to queue the loading of Effects. + * + * Loading an Effect has to be done in the compositor thread and thus the Compositor is blocked + * while the Effect loads. To not block the compositor for several frames the loading of all + * Effects need to be queued. By invoking the slot dequeue() through a QueuedConnection the queue + * can ensure that events are processed between the loading of two Effects and thus the compositor + * doesn't block. + * + * As it needs to be a slot, the queue must subclass QObject, but it also needs to be templated as + * the information to load an Effect is specific to the Effect Loader. Thus there is the + * AbstractEffectLoadQueue providing the slots as pure virtual functions and the templated + * EffectLoadQueue inheriting from AbstractEffectLoadQueue. + * + * The queue operates like a normal queue providing enqueue and a scheduleDequeue instead of dequeue. + * + */ +class AbstractEffectLoadQueue : public QObject +{ + Q_OBJECT +public: + explicit AbstractEffectLoadQueue(QObject *parent = nullptr) + : QObject(parent) + { + } +protected Q_SLOTS: + virtual void dequeue() = 0; +}; + +template +class EffectLoadQueue : public AbstractEffectLoadQueue +{ +public: + explicit EffectLoadQueue(Loader *parent) + : AbstractEffectLoadQueue(parent) + , m_effectLoader(parent) + , m_dequeueScheduled(false) + { + } + void enqueue(const QPair value) + { + m_queue.enqueue(value); + scheduleDequeue(); + } + void clear() + { + m_queue.clear(); + m_dequeueScheduled = false; + } +protected: + void dequeue() override + { + if (m_queue.isEmpty()) { + return; + } + m_dequeueScheduled = false; + const auto pair = m_queue.dequeue(); + m_effectLoader->loadEffect(pair.first, pair.second); + scheduleDequeue(); + } +private: + void scheduleDequeue() + { + if (m_queue.isEmpty() || m_dequeueScheduled) { + return; + } + m_dequeueScheduled = true; + QMetaObject::invokeMethod(this, "dequeue", Qt::QueuedConnection); + } + Loader *m_effectLoader; + bool m_dequeueScheduled; + QQueue> m_queue; +}; + +/** + * @brief Can load the Built-In-Effects + */ +class BuiltInEffectLoader : public AbstractEffectLoader +{ + Q_OBJECT +public: + explicit BuiltInEffectLoader(QObject *parent = nullptr); + ~BuiltInEffectLoader() override; + + bool hasEffect(const QString &name) const override; + bool isEffectSupported(const QString &name) const override; + QStringList listOfKnownEffects() const override; + + void clear() override; + void queryAndLoadAll() override; + bool loadEffect(const QString& name) override; + bool loadEffect(BuiltInEffect effect, LoadEffectFlags flags); + +private: + bool loadEffect(const QString &name, BuiltInEffect effect, LoadEffectFlags flags); + QString internalName(const QString &name) const; + EffectLoadQueue *m_queue; + QMap m_loadedEffects; +}; + +/** + * @brief Can load scripted Effects + */ +class KWIN_EXPORT ScriptedEffectLoader : public AbstractEffectLoader +{ + Q_OBJECT +public: + explicit ScriptedEffectLoader(QObject* parent = nullptr); + ~ScriptedEffectLoader() override; + + bool hasEffect(const QString &name) const override; + bool isEffectSupported(const QString &name) const override; + QStringList listOfKnownEffects() const override; + + void clear() override; + void queryAndLoadAll() override; + bool loadEffect(const QString &name) override; + bool loadEffect(const KPluginMetaData &effect, LoadEffectFlags flags); + +private: + QList findAllEffects() const; + KPluginMetaData findEffect(const QString &name) const; + QStringList m_loadedEffects; + EffectLoadQueue< ScriptedEffectLoader, KPluginMetaData > *m_queue; + QMetaObject::Connection m_queryConnection; +}; + +class PluginEffectLoader : public AbstractEffectLoader +{ + Q_OBJECT +public: + explicit PluginEffectLoader(QObject *parent = nullptr); + ~PluginEffectLoader() override; + + bool hasEffect(const QString &name) const override; + bool isEffectSupported(const QString &name) const override; + QStringList listOfKnownEffects() const override; + + void clear() override; + void queryAndLoadAll() override; + bool loadEffect(const QString &name) override; + bool loadEffect(const KPluginMetaData &info, LoadEffectFlags flags); + + void setPluginSubDirectory(const QString &directory); + +private: + QVector findAllEffects() const; + KPluginMetaData findEffect(const QString &name) const; + EffectPluginFactory *factory(const KPluginMetaData &info) const; + QStringList m_loadedEffects; + EffectLoadQueue< PluginEffectLoader, KPluginMetaData> *m_queue; + QString m_pluginSubDirectory; + QMetaObject::Connection m_queryConnection; +}; + +class EffectLoader : public AbstractEffectLoader +{ + Q_OBJECT +public: + explicit EffectLoader(QObject *parent = nullptr); + ~EffectLoader() override; + bool hasEffect(const QString &name) const override; + bool isEffectSupported(const QString &name) const override; + QStringList listOfKnownEffects() const override; + bool loadEffect(const QString &name) override; + void queryAndLoadAll() override; + void setConfig(KSharedConfig::Ptr config) override; + void clear() override; + +private: + QList m_loaders; +}; + +} +Q_DECLARE_OPERATORS_FOR_FLAGS(KWin::LoadEffectFlags) + +#endif diff --git a/effects.cpp b/effects.cpp new file mode 100644 index 0000000..2a98725 --- /dev/null +++ b/effects.cpp @@ -0,0 +1,2407 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2010, 2011 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "effects.h" + +#include "effectsadaptor.h" +#include "effectloader.h" +#ifdef KWIN_BUILD_ACTIVITIES +#include "activities.h" +#endif +#include "deleted.h" +#include "x11client.h" +#include "cursor.h" +#include "group.h" +#include "internal_client.h" +#include "osd.h" +#include "pointer_input.h" +#include "unmanaged.h" +#ifdef KWIN_BUILD_TABBOX +#include "tabbox.h" +#endif +#include "screenedge.h" +#include "scripting/scriptedeffect.h" +#include "screens.h" +#include "screenlockerwatcher.h" +#include "thumbnailitem.h" +#include "virtualdesktops.h" +#include "window_property_notify_x11_filter.h" +#include "workspace.h" +#include "kwinglutils.h" +#include "kwineffectquickview.h" + +#include + +#include + +#include "composite.h" +#include "xcbutils.h" +#include "platform.h" +#include "waylandclient.h" +#include "wayland_server.h" + +#include "decorations/decorationbridge.h" +#include + +namespace KWin +{ +//--------------------- +// Static + +static QByteArray readWindowProperty(xcb_window_t win, xcb_atom_t atom, xcb_atom_t type, int format) +{ + if (win == XCB_WINDOW_NONE) { + return QByteArray(); + } + uint32_t len = 32768; + for (;;) { + Xcb::Property prop(false, win, atom, XCB_ATOM_ANY, 0, len); + if (prop.isNull()) { + // get property failed + return QByteArray(); + } + if (prop->bytes_after > 0) { + len *= 2; + continue; + } + return prop.toByteArray(format, type); + } +} + +static void deleteWindowProperty(Window win, long int atom) +{ + if (win == XCB_WINDOW_NONE) { + return; + } + xcb_delete_property(kwinApp()->x11Connection(), win, atom); +} + +static xcb_atom_t registerSupportProperty(const QByteArray &propertyName) +{ + auto c = kwinApp()->x11Connection(); + if (!c) { + return XCB_ATOM_NONE; + } + // get the atom for the propertyName + ScopedCPointer atomReply(xcb_intern_atom_reply(c, + xcb_intern_atom_unchecked(c, false, propertyName.size(), propertyName.constData()), + nullptr)); + if (atomReply.isNull()) { + return XCB_ATOM_NONE; + } + // announce property on root window + unsigned char dummy = 0; + xcb_change_property(c, XCB_PROP_MODE_REPLACE, kwinApp()->x11RootWindow(), atomReply->atom, atomReply->atom, 8, 1, &dummy); + // TODO: add to _NET_SUPPORTED + return atomReply->atom; +} + +//--------------------- + +EffectsHandlerImpl::EffectsHandlerImpl(Compositor *compositor, Scene *scene) + : EffectsHandler(scene->compositingType()) + , keyboard_grab_effect(nullptr) + , fullscreen_effect(nullptr) + , next_window_quad_type(EFFECT_QUAD_TYPE_START) + , m_compositor(compositor) + , m_scene(scene) + , m_desktopRendering(false) + , m_currentRenderedDesktop(0) + , m_effectLoader(new EffectLoader(this)) + , m_trackingCursorChanges(0) +{ + qRegisterMetaType>(); + connect(m_effectLoader, &AbstractEffectLoader::effectLoaded, this, + [this](Effect *effect, const QString &name) { + effect_order.insert(effect->requestedEffectChainPosition(), EffectPair(name, effect)); + loaded_effects << EffectPair(name, effect); + effectsChanged(); + } + ); + m_effectLoader->setConfig(kwinApp()->config()); + new EffectsAdaptor(this); + QDBusConnection dbus = QDBusConnection::sessionBus(); + dbus.registerObject(QStringLiteral("/Effects"), this); + // init is important, otherwise causes crashes when quads are build before the first painting pass start + m_currentBuildQuadsIterator = m_activeEffects.constEnd(); + + Workspace *ws = Workspace::self(); + VirtualDesktopManager *vds = VirtualDesktopManager::self(); + connect(ws, &Workspace::showingDesktopChanged, + this, &EffectsHandlerImpl::showingDesktopChanged); + connect(ws, &Workspace::currentDesktopChanged, this, + [this](int old, AbstractClient *c) { + const int newDesktop = VirtualDesktopManager::self()->current(); + if (old != 0 && newDesktop != old) { + emit desktopChanged(old, newDesktop, c ? c->effectWindow() : nullptr); + // TODO: remove in 4.10 + emit desktopChanged(old, newDesktop); + } + } + ); + connect(ws, &Workspace::desktopPresenceChanged, this, + [this](AbstractClient *c, int old) { + if (!c->effectWindow()) { + return; + } + emit desktopPresenceChanged(c->effectWindow(), old, c->desktop()); + } + ); + connect(ws, &Workspace::clientAdded, this, + [this](AbstractClient *c) { + if (c->readyForPainting()) + slotClientShown(c); + else + connect(c, &Toplevel::windowShown, this, &EffectsHandlerImpl::slotClientShown); + } + ); + connect(ws, &Workspace::unmanagedAdded, this, + [this](Unmanaged *u) { + // it's never initially ready but has synthetic 50ms delay + connect(u, &Toplevel::windowShown, this, &EffectsHandlerImpl::slotUnmanagedShown); + } + ); + connect(ws, &Workspace::internalClientAdded, this, + [this](InternalClient *client) { + setupClientConnections(client); + emit windowAdded(client->effectWindow()); + } + ); + connect(ws, &Workspace::clientActivated, this, + [this](KWin::AbstractClient *c) { + emit windowActivated(c ? c->effectWindow() : nullptr); + } + ); + connect(ws, &Workspace::deletedRemoved, this, + [this](KWin::Deleted *d) { + emit windowDeleted(d->effectWindow()); + elevated_windows.removeAll(d->effectWindow()); + } + ); + connect(ws->sessionManager(), &SessionManager::stateChanged, this, + &KWin::EffectsHandler::sessionStateChanged); + connect(vds, &VirtualDesktopManager::countChanged, this, &EffectsHandler::numberDesktopsChanged); + connect(Cursors::self()->mouse(), &Cursor::mouseChanged, this, &EffectsHandler::mouseChanged); + connect(screens(), &Screens::countChanged, this, &EffectsHandler::numberScreensChanged); + connect(screens(), &Screens::sizeChanged, this, &EffectsHandler::virtualScreenSizeChanged); + connect(screens(), &Screens::geometryChanged, this, &EffectsHandler::virtualScreenGeometryChanged); +#ifdef KWIN_BUILD_ACTIVITIES + if (Activities *activities = Activities::self()) { + connect(activities, &Activities::added, this, &EffectsHandler::activityAdded); + connect(activities, &Activities::removed, this, &EffectsHandler::activityRemoved); + connect(activities, &Activities::currentChanged, this, &EffectsHandler::currentActivityChanged); + } +#endif + connect(ws, &Workspace::stackingOrderChanged, this, &EffectsHandler::stackingOrderChanged); +#ifdef KWIN_BUILD_TABBOX + TabBox::TabBox *tabBox = TabBox::TabBox::self(); + connect(tabBox, &TabBox::TabBox::tabBoxAdded, this, &EffectsHandler::tabBoxAdded); + connect(tabBox, &TabBox::TabBox::tabBoxUpdated, this, &EffectsHandler::tabBoxUpdated); + connect(tabBox, &TabBox::TabBox::tabBoxClosed, this, &EffectsHandler::tabBoxClosed); + connect(tabBox, &TabBox::TabBox::tabBoxKeyEvent, this, &EffectsHandler::tabBoxKeyEvent); +#endif + connect(ScreenEdges::self(), &ScreenEdges::approaching, this, &EffectsHandler::screenEdgeApproaching); + connect(ScreenLockerWatcher::self(), &ScreenLockerWatcher::locked, this, &EffectsHandler::screenLockingChanged); + connect(ScreenLockerWatcher::self(), &ScreenLockerWatcher::aboutToLock, this, &EffectsHandler::screenAboutToLock); + + connect(kwinApp(), &Application::x11ConnectionChanged, this, + [this] { + registered_atoms.clear(); + for (auto it = m_propertiesForEffects.keyBegin(); it != m_propertiesForEffects.keyEnd(); it++) { + const auto atom = registerSupportProperty(*it); + if (atom == XCB_ATOM_NONE) { + continue; + } + m_compositor->keepSupportProperty(atom); + m_managedProperties.insert(*it, atom); + registerPropertyType(atom, true); + } + if (kwinApp()->x11Connection()) { + m_x11WindowPropertyNotify = std::make_unique(this); + } else { + m_x11WindowPropertyNotify.reset(); + } + emit xcbConnectionChanged(); + } + ); + + if (kwinApp()->x11Connection()) { + m_x11WindowPropertyNotify = std::make_unique(this); + } + + // connect all clients + for (X11Client *c : ws->clientList()) { + setupClientConnections(c); + } + for (Unmanaged *u : ws->unmanagedList()) { + setupUnmanagedConnections(u); + } + for (InternalClient *client : ws->internalClients()) { + setupClientConnections(client); + } + if (waylandServer()) { + const auto clients = waylandServer()->clients(); + for (AbstractClient *c : clients) { + if (c->readyForPainting()) { + setupClientConnections(c); + } else { + connect(c, &Toplevel::windowShown, this, &EffectsHandlerImpl::slotClientShown); + } + } + } + reconfigure(); +} + +EffectsHandlerImpl::~EffectsHandlerImpl() +{ + unloadAllEffects(); +} + +void EffectsHandlerImpl::unloadAllEffects() +{ + for (const EffectPair &pair : loaded_effects) { + destroyEffect(pair.second); + } + + effect_order.clear(); + m_effectLoader->clear(); + + effectsChanged(); +} + +void EffectsHandlerImpl::setupClientConnections(AbstractClient* c) +{ + connect(c, &AbstractClient::windowClosed, this, &EffectsHandlerImpl::slotWindowClosed); + connect(c, static_cast(&AbstractClient::clientMaximizedStateChanged), + this, &EffectsHandlerImpl::slotClientMaximized); + connect(c, &AbstractClient::clientStartUserMovedResized, this, + [this](AbstractClient *c) { + emit windowStartUserMovedResized(c->effectWindow()); + } + ); + connect(c, &AbstractClient::clientStepUserMovedResized, this, + [this](AbstractClient *c, const QRect &geometry) { + emit windowStepUserMovedResized(c->effectWindow(), geometry); + } + ); + connect(c, &AbstractClient::clientFinishUserMovedResized, this, + [this](AbstractClient *c) { + emit windowFinishUserMovedResized(c->effectWindow()); + } + ); + connect(c, &AbstractClient::opacityChanged, this, &EffectsHandlerImpl::slotOpacityChanged); + connect(c, &AbstractClient::clientMinimized, this, + [this](AbstractClient *c, bool animate) { + // TODO: notify effects even if it should not animate? + if (animate) { + emit windowMinimized(c->effectWindow()); + } + } + ); + connect(c, &AbstractClient::clientUnminimized, this, + [this](AbstractClient* c, bool animate) { + // TODO: notify effects even if it should not animate? + if (animate) { + emit windowUnminimized(c->effectWindow()); + } + } + ); + connect(c, &AbstractClient::modalChanged, this, &EffectsHandlerImpl::slotClientModalityChanged); + connect(c, &AbstractClient::geometryShapeChanged, this, &EffectsHandlerImpl::slotGeometryShapeChanged); + connect(c, &AbstractClient::frameGeometryChanged, this, &EffectsHandlerImpl::slotFrameGeometryChanged); + connect(c, &AbstractClient::damaged, this, &EffectsHandlerImpl::slotWindowDamaged); + connect(c, &AbstractClient::paddingChanged, this, &EffectsHandlerImpl::slotPaddingChanged); + connect(c, &AbstractClient::unresponsiveChanged, this, + [this, c](bool unresponsive) { + emit windowUnresponsiveChanged(c->effectWindow(), unresponsive); + } + ); + connect(c, &AbstractClient::windowShown, this, + [this](Toplevel *c) { + emit windowShown(c->effectWindow()); + } + ); + connect(c, &AbstractClient::windowHidden, this, + [this](Toplevel *c) { + emit windowHidden(c->effectWindow()); + } + ); + connect(c, &AbstractClient::keepAboveChanged, this, + [this, c](bool above) { + Q_UNUSED(above) + emit windowKeepAboveChanged(c->effectWindow()); + } + ); + connect(c, &AbstractClient::keepBelowChanged, this, + [this, c](bool below) { + Q_UNUSED(below) + emit windowKeepBelowChanged(c->effectWindow()); + } + ); + connect(c, &AbstractClient::fullScreenChanged, this, + [this, c]() { + emit windowFullScreenChanged(c->effectWindow()); + } + ); +} + +void EffectsHandlerImpl::setupUnmanagedConnections(Unmanaged* u) +{ + connect(u, &Unmanaged::windowClosed, this, &EffectsHandlerImpl::slotWindowClosed); + connect(u, &Unmanaged::opacityChanged, this, &EffectsHandlerImpl::slotOpacityChanged); + connect(u, &Unmanaged::geometryShapeChanged, this, &EffectsHandlerImpl::slotGeometryShapeChanged); + connect(u, &Unmanaged::frameGeometryChanged, this, &EffectsHandlerImpl::slotFrameGeometryChanged); + connect(u, &Unmanaged::paddingChanged, this, &EffectsHandlerImpl::slotPaddingChanged); + connect(u, &Unmanaged::damaged, this, &EffectsHandlerImpl::slotWindowDamaged); +} + +void EffectsHandlerImpl::reconfigure() +{ + m_effectLoader->queryAndLoadAll(); +} + +// the idea is that effects call this function again which calls the next one +void EffectsHandlerImpl::prePaintScreen(ScreenPrePaintData& data, int time) +{ + if (m_currentPaintScreenIterator != m_activeEffects.constEnd()) { + (*m_currentPaintScreenIterator++)->prePaintScreen(data, time); + --m_currentPaintScreenIterator; + } + // no special final code +} + +void EffectsHandlerImpl::paintScreen(int mask, const QRegion ®ion, ScreenPaintData& data) +{ + if (m_currentPaintScreenIterator != m_activeEffects.constEnd()) { + (*m_currentPaintScreenIterator++)->paintScreen(mask, region, data); + --m_currentPaintScreenIterator; + } else + m_scene->finalPaintScreen(mask, region, data); +} + +void EffectsHandlerImpl::paintDesktop(int desktop, int mask, QRegion region, ScreenPaintData &data) +{ + if (desktop < 1 || desktop > numberOfDesktops()) { + return; + } + m_currentRenderedDesktop = desktop; + m_desktopRendering = true; + // save the paint screen iterator + EffectsIterator savedIterator = m_currentPaintScreenIterator; + m_currentPaintScreenIterator = m_activeEffects.constBegin(); + effects->paintScreen(mask, region, data); + // restore the saved iterator + m_currentPaintScreenIterator = savedIterator; + m_desktopRendering = false; +} + +void EffectsHandlerImpl::postPaintScreen() +{ + if (m_currentPaintScreenIterator != m_activeEffects.constEnd()) { + (*m_currentPaintScreenIterator++)->postPaintScreen(); + --m_currentPaintScreenIterator; + } + // no special final code +} + +void EffectsHandlerImpl::prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time) +{ + if (m_currentPaintWindowIterator != m_activeEffects.constEnd()) { + (*m_currentPaintWindowIterator++)->prePaintWindow(w, data, time); + --m_currentPaintWindowIterator; + } + // no special final code +} + +void EffectsHandlerImpl::paintWindow(EffectWindow* w, int mask, const QRegion ®ion, WindowPaintData& data) +{ + if (m_currentPaintWindowIterator != m_activeEffects.constEnd()) { + (*m_currentPaintWindowIterator++)->paintWindow(w, mask, region, data); + --m_currentPaintWindowIterator; + } else + m_scene->finalPaintWindow(static_cast(w), mask, region, data); +} + +void EffectsHandlerImpl::paintEffectFrame(EffectFrame* frame, const QRegion ®ion, double opacity, double frameOpacity) +{ + if (m_currentPaintEffectFrameIterator != m_activeEffects.constEnd()) { + (*m_currentPaintEffectFrameIterator++)->paintEffectFrame(frame, region, opacity, frameOpacity); + --m_currentPaintEffectFrameIterator; + } else { + const EffectFrameImpl* frameImpl = static_cast(frame); + frameImpl->finalRender(region, opacity, frameOpacity); + } +} + +void EffectsHandlerImpl::postPaintWindow(EffectWindow* w) +{ + if (m_currentPaintWindowIterator != m_activeEffects.constEnd()) { + (*m_currentPaintWindowIterator++)->postPaintWindow(w); + --m_currentPaintWindowIterator; + } + // no special final code +} + +Effect *EffectsHandlerImpl::provides(Effect::Feature ef) +{ + for (int i = 0; i < loaded_effects.size(); ++i) + if (loaded_effects.at(i).second->provides(ef)) + return loaded_effects.at(i).second; + return nullptr; +} + +void EffectsHandlerImpl::drawWindow(EffectWindow* w, int mask, const QRegion ®ion, WindowPaintData& data) +{ + if (m_currentDrawWindowIterator != m_activeEffects.constEnd()) { + (*m_currentDrawWindowIterator++)->drawWindow(w, mask, region, data); + --m_currentDrawWindowIterator; + } else + m_scene->finalDrawWindow(static_cast(w), mask, region, data); +} + +void EffectsHandlerImpl::buildQuads(EffectWindow* w, WindowQuadList& quadList) +{ + static bool initIterator = true; + if (initIterator) { + m_currentBuildQuadsIterator = m_activeEffects.constBegin(); + initIterator = false; + } + if (m_currentBuildQuadsIterator != m_activeEffects.constEnd()) { + (*m_currentBuildQuadsIterator++)->buildQuads(w, quadList); + --m_currentBuildQuadsIterator; + } + if (m_currentBuildQuadsIterator == m_activeEffects.constBegin()) + initIterator = true; +} + +bool EffectsHandlerImpl::hasDecorationShadows() const +{ + return false; +} + +bool EffectsHandlerImpl::decorationsHaveAlpha() const +{ + return true; +} + +bool EffectsHandlerImpl::decorationSupportsBlurBehind() const +{ + return Decoration::DecorationBridge::self()->needsBlur(); +} + +// start another painting pass +void EffectsHandlerImpl::startPaint() +{ + m_activeEffects.clear(); + m_activeEffects.reserve(loaded_effects.count()); + for(QVector< KWin::EffectPair >::const_iterator it = loaded_effects.constBegin(); it != loaded_effects.constEnd(); ++it) { + if (it->second->isActive()) { + m_activeEffects << it->second; + } + } + m_currentDrawWindowIterator = m_activeEffects.constBegin(); + m_currentPaintWindowIterator = m_activeEffects.constBegin(); + m_currentPaintScreenIterator = m_activeEffects.constBegin(); + m_currentPaintEffectFrameIterator = m_activeEffects.constBegin(); +} + +void EffectsHandlerImpl::slotClientMaximized(KWin::AbstractClient *c, MaximizeMode maxMode) +{ + bool horizontal = false; + bool vertical = false; + switch (maxMode) { + case MaximizeHorizontal: + horizontal = true; + break; + case MaximizeVertical: + vertical = true; + break; + case MaximizeFull: + horizontal = true; + vertical = true; + break; + case MaximizeRestore: // fall through + default: + // default - nothing to do + break; + } + if (EffectWindowImpl *w = c->effectWindow()) { + emit windowMaximizedStateChanged(w, horizontal, vertical); + } +} + +void EffectsHandlerImpl::slotOpacityChanged(Toplevel *t, qreal oldOpacity) +{ + if (t->opacity() == oldOpacity || !t->effectWindow()) { + return; + } + emit windowOpacityChanged(t->effectWindow(), oldOpacity, (qreal)t->opacity()); +} + +void EffectsHandlerImpl::slotClientShown(KWin::Toplevel *t) +{ + Q_ASSERT(qobject_cast(t)); + AbstractClient *c = static_cast(t); + disconnect(c, &Toplevel::windowShown, this, &EffectsHandlerImpl::slotClientShown); + setupClientConnections(c); + emit windowAdded(c->effectWindow()); +} + +void EffectsHandlerImpl::slotUnmanagedShown(KWin::Toplevel *t) +{ // regardless, unmanaged windows are -yet?- not synced anyway + Q_ASSERT(qobject_cast(t)); + Unmanaged *u = static_cast(t); + setupUnmanagedConnections(u); + emit windowAdded(u->effectWindow()); +} + +void EffectsHandlerImpl::slotWindowClosed(KWin::Toplevel *c, KWin::Deleted *d) +{ + c->disconnect(this); + if (d) { + emit windowClosed(c->effectWindow()); + } +} + +void EffectsHandlerImpl::slotClientModalityChanged() +{ + emit windowModalityChanged(static_cast(sender())->effectWindow()); +} + +void EffectsHandlerImpl::slotCurrentTabAboutToChange(EffectWindow *from, EffectWindow *to) +{ + emit currentTabAboutToChange(from, to); +} + +void EffectsHandlerImpl::slotTabAdded(EffectWindow* w, EffectWindow* to) +{ + emit tabAdded(w, to); +} + +void EffectsHandlerImpl::slotTabRemoved(EffectWindow *w, EffectWindow* leaderOfFormerGroup) +{ + emit tabRemoved(w, leaderOfFormerGroup); +} + +void EffectsHandlerImpl::slotWindowDamaged(Toplevel* t, const QRect& r) +{ + if (!t->effectWindow()) { + // can happen during tear down of window + return; + } + emit windowDamaged(t->effectWindow(), r); +} + +void EffectsHandlerImpl::slotGeometryShapeChanged(Toplevel* t, const QRect& old) +{ + // during late cleanup effectWindow() may be already NULL + // in some functions that may still call this + if (t == nullptr || t->effectWindow() == nullptr) + return; + emit windowGeometryShapeChanged(t->effectWindow(), old); +} + +void EffectsHandlerImpl::slotFrameGeometryChanged(Toplevel *toplevel, const QRect &oldGeometry) +{ + // effectWindow() might be nullptr during tear down of the client. + if (toplevel->effectWindow()) { + emit windowFrameGeometryChanged(toplevel->effectWindow(), oldGeometry); + } +} + +void EffectsHandlerImpl::slotPaddingChanged(Toplevel* t, const QRect& old) +{ + // during late cleanup effectWindow() may be already NULL + // in some functions that may still call this + if (t == nullptr || t->effectWindow() == nullptr) + return; + emit windowPaddingChanged(t->effectWindow(), old); +} + +void EffectsHandlerImpl::setActiveFullScreenEffect(Effect* e) +{ + if (fullscreen_effect == e) { + return; + } + const bool activeChanged = (e == nullptr || fullscreen_effect == nullptr); + fullscreen_effect = e; + emit activeFullScreenEffectChanged(); + if (activeChanged) { + emit hasActiveFullScreenEffectChanged(); + } +} + +Effect* EffectsHandlerImpl::activeFullScreenEffect() const +{ + return fullscreen_effect; +} + +bool EffectsHandlerImpl::hasActiveFullScreenEffect() const +{ + return fullscreen_effect; +} + +bool EffectsHandlerImpl::grabKeyboard(Effect* effect) +{ + if (keyboard_grab_effect != nullptr) + return false; + if (!doGrabKeyboard()) { + return false; + } + keyboard_grab_effect = effect; + return true; +} + +bool EffectsHandlerImpl::doGrabKeyboard() +{ + return true; +} + +void EffectsHandlerImpl::ungrabKeyboard() +{ + Q_ASSERT(keyboard_grab_effect != nullptr); + doUngrabKeyboard(); + keyboard_grab_effect = nullptr; +} + +void EffectsHandlerImpl::doUngrabKeyboard() +{ +} + +void EffectsHandlerImpl::grabbedKeyboardEvent(QKeyEvent* e) +{ + if (keyboard_grab_effect != nullptr) + keyboard_grab_effect->grabbedKeyboardEvent(e); +} + +void EffectsHandlerImpl::startMouseInterception(Effect *effect, Qt::CursorShape shape) +{ + if (m_grabbedMouseEffects.contains(effect)) { + return; + } + m_grabbedMouseEffects.append(effect); + if (m_grabbedMouseEffects.size() != 1) { + return; + } + doStartMouseInterception(shape); +} + +void EffectsHandlerImpl::doStartMouseInterception(Qt::CursorShape shape) +{ + input()->pointer()->setEffectsOverrideCursor(shape); +} + +void EffectsHandlerImpl::stopMouseInterception(Effect *effect) +{ + if (!m_grabbedMouseEffects.contains(effect)) { + return; + } + m_grabbedMouseEffects.removeAll(effect); + if (m_grabbedMouseEffects.isEmpty()) { + doStopMouseInterception(); + } +} + +void EffectsHandlerImpl::doStopMouseInterception() +{ + input()->pointer()->removeEffectsOverrideCursor(); +} + +bool EffectsHandlerImpl::isMouseInterception() const +{ + return m_grabbedMouseEffects.count() > 0; +} + + +bool EffectsHandlerImpl::touchDown(qint32 id, const QPointF &pos, quint32 time) +{ + // TODO: reverse call order? + for (auto it = loaded_effects.constBegin(); it != loaded_effects.constEnd(); ++it) { + if (it->second->touchDown(id, pos, time)) { + return true; + } + } + return false; +} + +bool EffectsHandlerImpl::touchMotion(qint32 id, const QPointF &pos, quint32 time) +{ + // TODO: reverse call order? + for (auto it = loaded_effects.constBegin(); it != loaded_effects.constEnd(); ++it) { + if (it->second->touchMotion(id, pos, time)) { + return true; + } + } + return false; +} + +bool EffectsHandlerImpl::touchUp(qint32 id, quint32 time) +{ + // TODO: reverse call order? + for (auto it = loaded_effects.constBegin(); it != loaded_effects.constEnd(); ++it) { + if (it->second->touchUp(id, time)) { + return true; + } + } + return false; +} + +void EffectsHandlerImpl::registerGlobalShortcut(const QKeySequence &shortcut, QAction *action) +{ + input()->registerShortcut(shortcut, action); +} + +void EffectsHandlerImpl::registerPointerShortcut(Qt::KeyboardModifiers modifiers, Qt::MouseButton pointerButtons, QAction *action) +{ + input()->registerPointerShortcut(modifiers, pointerButtons, action); +} + +void EffectsHandlerImpl::registerAxisShortcut(Qt::KeyboardModifiers modifiers, PointerAxisDirection axis, QAction *action) +{ + input()->registerAxisShortcut(modifiers, axis, action); +} + +void EffectsHandlerImpl::registerTouchpadSwipeShortcut(SwipeDirection direction, QAction *action) +{ + input()->registerTouchpadSwipeShortcut(direction, action); +} + +void* EffectsHandlerImpl::getProxy(QString name) +{ + for (QVector< EffectPair >::const_iterator it = loaded_effects.constBegin(); it != loaded_effects.constEnd(); ++it) + if ((*it).first == name) + return (*it).second->proxy(); + + return nullptr; +} + +void EffectsHandlerImpl::startMousePolling() +{ + if (Cursors::self()->mouse()) + Cursors::self()->mouse()->startMousePolling(); +} + +void EffectsHandlerImpl::stopMousePolling() +{ + if (Cursors::self()->mouse()) + Cursors::self()->mouse()->stopMousePolling(); +} + +bool EffectsHandlerImpl::hasKeyboardGrab() const +{ + return keyboard_grab_effect != nullptr; +} + +void EffectsHandlerImpl::desktopResized(const QSize &size) +{ + m_scene->screenGeometryChanged(size); + emit screenGeometryChanged(size); +} + +void EffectsHandlerImpl::registerPropertyType(long atom, bool reg) +{ + if (reg) + ++registered_atoms[ atom ]; // initialized to 0 if not present yet + else { + if (--registered_atoms[ atom ] == 0) + registered_atoms.remove(atom); + } +} + +xcb_atom_t EffectsHandlerImpl::announceSupportProperty(const QByteArray &propertyName, Effect *effect) +{ + PropertyEffectMap::iterator it = m_propertiesForEffects.find(propertyName); + if (it != m_propertiesForEffects.end()) { + // property has already been registered for an effect + // just append Effect and return the atom stored in m_managedProperties + if (!it.value().contains(effect)) { + it.value().append(effect); + } + return m_managedProperties.value(propertyName, XCB_ATOM_NONE); + } + m_propertiesForEffects.insert(propertyName, QList() << effect); + const auto atom = registerSupportProperty(propertyName); + if (atom == XCB_ATOM_NONE) { + return atom; + } + m_compositor->keepSupportProperty(atom); + m_managedProperties.insert(propertyName, atom); + registerPropertyType(atom, true); + return atom; +} + +void EffectsHandlerImpl::removeSupportProperty(const QByteArray &propertyName, Effect *effect) +{ + PropertyEffectMap::iterator it = m_propertiesForEffects.find(propertyName); + if (it == m_propertiesForEffects.end()) { + // property is not registered - nothing to do + return; + } + if (!it.value().contains(effect)) { + // property is not registered for given effect - nothing to do + return; + } + it.value().removeAll(effect); + if (!it.value().isEmpty()) { + // property still registered for another effect - nothing further to do + return; + } + const xcb_atom_t atom = m_managedProperties.take(propertyName); + registerPropertyType(atom, false); + m_propertiesForEffects.remove(propertyName); + m_compositor->removeSupportProperty(atom); // delayed removal +} + +QByteArray EffectsHandlerImpl::readRootProperty(long atom, long type, int format) const +{ + if (!kwinApp()->x11Connection()) { + return QByteArray(); + } + return readWindowProperty(kwinApp()->x11RootWindow(), atom, type, format); +} + +void EffectsHandlerImpl::activateWindow(EffectWindow* c) +{ + if (auto cl = qobject_cast(static_cast(c)->window())) { + Workspace::self()->activateClient(cl, true); + } +} + +EffectWindow* EffectsHandlerImpl::activeWindow() const +{ + return Workspace::self()->activeClient() ? Workspace::self()->activeClient()->effectWindow() : nullptr; +} + +void EffectsHandlerImpl::moveWindow(EffectWindow* w, const QPoint& pos, bool snap, double snapAdjust) +{ + auto cl = qobject_cast(static_cast(w)->window()); + if (!cl || !cl->isMovable()) + return; + + if (snap) + cl->move(Workspace::self()->adjustClientPosition(cl, pos, true, snapAdjust)); + else + cl->move(pos); +} + +void EffectsHandlerImpl::windowToDesktop(EffectWindow* w, int desktop) +{ + auto cl = qobject_cast(static_cast(w)->window()); + if (cl && !cl->isDesktop() && !cl->isDock()) { + Workspace::self()->sendClientToDesktop(cl, desktop, true); + } +} + +void EffectsHandlerImpl::windowToDesktops(EffectWindow *w, const QVector &desktopIds) +{ + AbstractClient* cl = qobject_cast< AbstractClient* >(static_cast(w)->window()); + if (!cl || cl->isDesktop() || cl->isDock()) { + return; + } + QVector desktops; + desktops.reserve(desktopIds.count()); + for (uint x11Id: desktopIds) { + if (x11Id > VirtualDesktopManager::self()->count()) { + continue; + } + VirtualDesktop *d = VirtualDesktopManager::self()->desktopForX11Id(x11Id); + Q_ASSERT(d); + if (desktops.contains(d)) { + continue; + } + desktops << d; + } + cl->setDesktops(desktops); +} + +void EffectsHandlerImpl::windowToScreen(EffectWindow* w, int screen) +{ + auto cl = qobject_cast(static_cast(w)->window()); + if (cl && !cl->isDesktop() && !cl->isDock()) + Workspace::self()->sendClientToScreen(cl, screen); +} + +void EffectsHandlerImpl::setShowingDesktop(bool showing) +{ + Workspace::self()->setShowingDesktop(showing); +} + +QString EffectsHandlerImpl::currentActivity() const +{ +#ifdef KWIN_BUILD_ACTIVITIES + if (!Activities::self()) { + return QString(); + } + return Activities::self()->current(); +#else + return QString(); +#endif +} + +int EffectsHandlerImpl::currentDesktop() const +{ + return VirtualDesktopManager::self()->current(); +} + +int EffectsHandlerImpl::numberOfDesktops() const +{ + return VirtualDesktopManager::self()->count(); +} + +void EffectsHandlerImpl::setCurrentDesktop(int desktop) +{ + VirtualDesktopManager::self()->setCurrent(desktop); +} + +void EffectsHandlerImpl::setNumberOfDesktops(int desktops) +{ + VirtualDesktopManager::self()->setCount(desktops); +} + +QSize EffectsHandlerImpl::desktopGridSize() const +{ + return VirtualDesktopManager::self()->grid().size(); +} + +int EffectsHandlerImpl::desktopGridWidth() const +{ + return desktopGridSize().width(); +} + +int EffectsHandlerImpl::desktopGridHeight() const +{ + return desktopGridSize().height(); +} + +int EffectsHandlerImpl::workspaceWidth() const +{ + return desktopGridWidth() * screens()->size().width(); +} + +int EffectsHandlerImpl::workspaceHeight() const +{ + return desktopGridHeight() * screens()->size().height(); +} + +int EffectsHandlerImpl::desktopAtCoords(QPoint coords) const +{ + if (auto vd = VirtualDesktopManager::self()->grid().at(coords)) { + return vd->x11DesktopNumber(); + } + return 0; +} + +QPoint EffectsHandlerImpl::desktopGridCoords(int id) const +{ + return VirtualDesktopManager::self()->grid().gridCoords(id); +} + +QPoint EffectsHandlerImpl::desktopCoords(int id) const +{ + QPoint coords = VirtualDesktopManager::self()->grid().gridCoords(id); + if (coords.x() == -1) + return QPoint(-1, -1); + const QSize displaySize = screens()->size(); + return QPoint(coords.x() * displaySize.width(), coords.y() * displaySize.height()); +} + +int EffectsHandlerImpl::desktopAbove(int desktop, bool wrap) const +{ + return getDesktop(desktop, wrap); +} + +int EffectsHandlerImpl::desktopToRight(int desktop, bool wrap) const +{ + return getDesktop(desktop, wrap); +} + +int EffectsHandlerImpl::desktopBelow(int desktop, bool wrap) const +{ + return getDesktop(desktop, wrap); +} + +int EffectsHandlerImpl::desktopToLeft(int desktop, bool wrap) const +{ + return getDesktop(desktop, wrap); +} + +QString EffectsHandlerImpl::desktopName(int desktop) const +{ + return VirtualDesktopManager::self()->name(desktop); +} + +bool EffectsHandlerImpl::optionRollOverDesktops() const +{ + return options->isRollOverDesktops(); +} + +double EffectsHandlerImpl::animationTimeFactor() const +{ + return options->animationTimeFactor(); +} + +WindowQuadType EffectsHandlerImpl::newWindowQuadType() +{ + return WindowQuadType(next_window_quad_type++); +} + +EffectWindow* EffectsHandlerImpl::findWindow(WId id) const +{ + if (X11Client *w = Workspace::self()->findClient(Predicate::WindowMatch, id)) + return w->effectWindow(); + if (Unmanaged* w = Workspace::self()->findUnmanaged(id)) + return w->effectWindow(); + if (waylandServer()) { + if (AbstractClient *w = waylandServer()->findClient(id)) { + return w->effectWindow(); + } + } + return nullptr; +} + +EffectWindow* EffectsHandlerImpl::findWindow(KWaylandServer::SurfaceInterface *surf) const +{ + if (waylandServer()) { + if (AbstractClient *w = waylandServer()->findClient(surf)) { + return w->effectWindow(); + } + } + return nullptr; +} + +EffectWindow *EffectsHandlerImpl::findWindow(QWindow *w) const +{ + if (Toplevel *toplevel = workspace()->findInternal(w)) { + return toplevel->effectWindow(); + } + return nullptr; +} + +EffectWindow *EffectsHandlerImpl::findWindow(const QUuid &id) const +{ + if (const auto client = workspace()->findAbstractClient([&id] (const AbstractClient *c) { return c->internalId() == id; })) { + return client->effectWindow(); + } + if (const auto unmanaged = workspace()->findUnmanaged([&id] (const Unmanaged *c) { return c->internalId() == id; })) { + return unmanaged->effectWindow(); + } + return nullptr; +} + +EffectWindowList EffectsHandlerImpl::stackingOrder() const +{ + QList list = Workspace::self()->xStackingOrder(); + EffectWindowList ret; + for (Toplevel *t : list) { + if (EffectWindow *w = effectWindow(t)) + ret.append(w); + } + return ret; +} + +void EffectsHandlerImpl::setElevatedWindow(KWin::EffectWindow* w, bool set) +{ + elevated_windows.removeAll(w); + if (set) + elevated_windows.append(w); +} + +void EffectsHandlerImpl::setTabBoxWindow(EffectWindow* w) +{ +#ifdef KWIN_BUILD_TABBOX + if (auto c = qobject_cast(static_cast(w)->window())) { + TabBox::TabBox::self()->setCurrentClient(c); + } +#else + Q_UNUSED(w) +#endif +} + +void EffectsHandlerImpl::setTabBoxDesktop(int desktop) +{ +#ifdef KWIN_BUILD_TABBOX + TabBox::TabBox::self()->setCurrentDesktop(desktop); +#else + Q_UNUSED(desktop) +#endif +} + +EffectWindowList EffectsHandlerImpl::currentTabBoxWindowList() const +{ +#ifdef KWIN_BUILD_TABBOX + const auto clients = TabBox::TabBox::self()->currentClientList(); + EffectWindowList ret; + ret.reserve(clients.size()); + std::transform(std::cbegin(clients), std::cend(clients), + std::back_inserter(ret), + [](auto client) { return client->effectWindow(); }); + return ret; +#else + return EffectWindowList(); +#endif +} + +void EffectsHandlerImpl::refTabBox() +{ +#ifdef KWIN_BUILD_TABBOX + TabBox::TabBox::self()->reference(); +#endif +} + +void EffectsHandlerImpl::unrefTabBox() +{ +#ifdef KWIN_BUILD_TABBOX + TabBox::TabBox::self()->unreference(); +#endif +} + +void EffectsHandlerImpl::closeTabBox() +{ +#ifdef KWIN_BUILD_TABBOX + TabBox::TabBox::self()->close(); +#endif +} + +QList< int > EffectsHandlerImpl::currentTabBoxDesktopList() const +{ +#ifdef KWIN_BUILD_TABBOX + return TabBox::TabBox::self()->currentDesktopList(); +#else + return QList< int >(); +#endif +} + +int EffectsHandlerImpl::currentTabBoxDesktop() const +{ +#ifdef KWIN_BUILD_TABBOX + return TabBox::TabBox::self()->currentDesktop(); +#else + return -1; +#endif +} + +EffectWindow* EffectsHandlerImpl::currentTabBoxWindow() const +{ +#ifdef KWIN_BUILD_TABBOX + if (auto c = TabBox::TabBox::self()->currentClient()) + return c->effectWindow(); +#endif + return nullptr; +} + +void EffectsHandlerImpl::addRepaintFull() +{ + m_compositor->addRepaintFull(); +} + +void EffectsHandlerImpl::addRepaint(const QRect& r) +{ + m_compositor->addRepaint(r); +} + +void EffectsHandlerImpl::addRepaint(const QRegion& r) +{ + m_compositor->addRepaint(r); +} + +void EffectsHandlerImpl::addRepaint(int x, int y, int w, int h) +{ + m_compositor->addRepaint(x, y, w, h); +} + +int EffectsHandlerImpl::activeScreen() const +{ + return screens()->current(); +} + +int EffectsHandlerImpl::numScreens() const +{ + return screens()->count(); +} + +int EffectsHandlerImpl::screenNumber(const QPoint& pos) const +{ + return screens()->number(pos); +} + +QRect EffectsHandlerImpl::clientArea(clientAreaOption opt, int screen, int desktop) const +{ + return Workspace::self()->clientArea(opt, screen, desktop); +} + +QRect EffectsHandlerImpl::clientArea(clientAreaOption opt, const EffectWindow* c) const +{ + const Toplevel* t = static_cast< const EffectWindowImpl* >(c)->window(); + if (const auto *cl = qobject_cast(t)) { + return Workspace::self()->clientArea(opt, cl); + } else { + return Workspace::self()->clientArea(opt, t->frameGeometry().center(), VirtualDesktopManager::self()->current()); + } +} + +QRect EffectsHandlerImpl::clientArea(clientAreaOption opt, const QPoint& p, int desktop) const +{ + return Workspace::self()->clientArea(opt, p, desktop); +} + +QRect EffectsHandlerImpl::virtualScreenGeometry() const +{ + return screens()->geometry(); +} + +QSize EffectsHandlerImpl::virtualScreenSize() const +{ + return screens()->size(); +} + +void EffectsHandlerImpl::defineCursor(Qt::CursorShape shape) +{ + input()->pointer()->setEffectsOverrideCursor(shape); +} + +bool EffectsHandlerImpl::checkInputWindowEvent(QMouseEvent *e) +{ + if (m_grabbedMouseEffects.isEmpty()) { + return false; + } + foreach (Effect *effect, m_grabbedMouseEffects) { + effect->windowInputMouseEvent(e); + } + return true; +} + +bool EffectsHandlerImpl::checkInputWindowEvent(QWheelEvent *e) +{ + if (m_grabbedMouseEffects.isEmpty()) { + return false; + } + foreach (Effect *effect, m_grabbedMouseEffects) { + effect->windowInputMouseEvent(e); + } + return true; +} + +void EffectsHandlerImpl::connectNotify(const QMetaMethod &signal) +{ + if (signal == QMetaMethod::fromSignal(&EffectsHandler::cursorShapeChanged)) { + if (!m_trackingCursorChanges) { + connect(Cursors::self()->mouse(), &Cursor::cursorChanged, this, &EffectsHandler::cursorShapeChanged); + Cursors::self()->mouse()->startCursorTracking(); + } + ++m_trackingCursorChanges; + } + EffectsHandler::connectNotify(signal); +} + +void EffectsHandlerImpl::disconnectNotify(const QMetaMethod &signal) +{ + if (signal == QMetaMethod::fromSignal(&EffectsHandler::cursorShapeChanged)) { + Q_ASSERT(m_trackingCursorChanges > 0); + if (!--m_trackingCursorChanges) { + Cursors::self()->mouse()->stopCursorTracking(); + disconnect(Cursors::self()->mouse(), &Cursor::cursorChanged, this, &EffectsHandler::cursorShapeChanged); + } + } + EffectsHandler::disconnectNotify(signal); +} + + +void EffectsHandlerImpl::checkInputWindowStacking() +{ + if (m_grabbedMouseEffects.isEmpty()) { + return; + } + doCheckInputWindowStacking(); +} + +void EffectsHandlerImpl::doCheckInputWindowStacking() +{ +} + +QPoint EffectsHandlerImpl::cursorPos() const +{ + return Cursors::self()->mouse()->pos(); +} + +void EffectsHandlerImpl::reserveElectricBorder(ElectricBorder border, Effect *effect) +{ + ScreenEdges::self()->reserve(border, effect, "borderActivated"); +} + +void EffectsHandlerImpl::unreserveElectricBorder(ElectricBorder border, Effect *effect) +{ + ScreenEdges::self()->unreserve(border, effect); +} + +void EffectsHandlerImpl::registerTouchBorder(ElectricBorder border, QAction *action) +{ + ScreenEdges::self()->reserveTouch(border, action); +} + +void EffectsHandlerImpl::unregisterTouchBorder(ElectricBorder border, QAction *action) +{ + ScreenEdges::self()->unreserveTouch(border, action); +} + +unsigned long EffectsHandlerImpl::xrenderBufferPicture() +{ + return m_scene->xrenderBufferPicture(); +} + +QPainter *EffectsHandlerImpl::scenePainter() +{ + return m_scene->scenePainter(); +} + +void EffectsHandlerImpl::toggleEffect(const QString& name) +{ + if (isEffectLoaded(name)) + unloadEffect(name); + else + loadEffect(name); +} + +QStringList EffectsHandlerImpl::loadedEffects() const +{ + QStringList listModules; + listModules.reserve(loaded_effects.count()); + std::transform(loaded_effects.constBegin(), loaded_effects.constEnd(), + std::back_inserter(listModules), + [](const EffectPair &pair) { return pair.first; }); + return listModules; +} + +QStringList EffectsHandlerImpl::listOfEffects() const +{ + return m_effectLoader->listOfKnownEffects(); +} + +bool EffectsHandlerImpl::loadEffect(const QString& name) +{ + makeOpenGLContextCurrent(); + m_compositor->addRepaintFull(); + + return m_effectLoader->loadEffect(name); +} + +void EffectsHandlerImpl::unloadEffect(const QString& name) +{ + auto it = std::find_if(effect_order.begin(), effect_order.end(), + [name](EffectPair &pair) { + return pair.first == name; + } + ); + if (it == effect_order.end()) { + qCDebug(KWIN_CORE) << "EffectsHandler::unloadEffect : Effect not loaded :" << name; + return; + } + + qCDebug(KWIN_CORE) << "EffectsHandler::unloadEffect : Unloading Effect :" << name; + destroyEffect((*it).second); + effect_order.erase(it); + effectsChanged(); + + m_compositor->addRepaintFull(); +} + +void EffectsHandlerImpl::destroyEffect(Effect *effect) +{ + makeOpenGLContextCurrent(); + + if (fullscreen_effect == effect) { + setActiveFullScreenEffect(nullptr); + } + + if (keyboard_grab_effect == effect) { + ungrabKeyboard(); + } + + stopMouseInterception(effect); + + const QList properties = m_propertiesForEffects.keys(); + for (const QByteArray &property : properties) { + removeSupportProperty(property, effect); + } + + delete effect; +} + +void EffectsHandlerImpl::reconfigureEffect(const QString& name) +{ + for (QVector< EffectPair >::const_iterator it = loaded_effects.constBegin(); it != loaded_effects.constEnd(); ++it) + if ((*it).first == name) { + kwinApp()->config()->reparseConfiguration(); + makeOpenGLContextCurrent(); + (*it).second->reconfigure(Effect::ReconfigureAll); + return; + } +} + +bool EffectsHandlerImpl::isEffectLoaded(const QString& name) const +{ + auto it = std::find_if(loaded_effects.constBegin(), loaded_effects.constEnd(), + [&name](const EffectPair &pair) { return pair.first == name; }); + return it != loaded_effects.constEnd(); +} + +bool EffectsHandlerImpl::isEffectSupported(const QString &name) +{ + // If the effect is loaded, it is obviously supported. + if (isEffectLoaded(name)) { + return true; + } + + // next checks might require a context + makeOpenGLContextCurrent(); + m_compositor->addRepaintFull(); + + return m_effectLoader->isEffectSupported(name); + +} + +QList EffectsHandlerImpl::areEffectsSupported(const QStringList &names) +{ + QList retList; + retList.reserve(names.count()); + std::transform(names.constBegin(), names.constEnd(), + std::back_inserter(retList), + [this](const QString &name) { + return isEffectSupported(name); + }); + return retList; +} + +void EffectsHandlerImpl::reloadEffect(Effect *effect) +{ + QString effectName; + for (QVector< EffectPair >::const_iterator it = loaded_effects.constBegin(); it != loaded_effects.constEnd(); ++it) { + if ((*it).second == effect) { + effectName = (*it).first; + break; + } + } + if (!effectName.isNull()) { + unloadEffect(effectName); + m_effectLoader->loadEffect(effectName); + } +} + +void EffectsHandlerImpl::effectsChanged() +{ + loaded_effects.clear(); + m_activeEffects.clear(); // it's possible to have a reconfigure and a quad rebuild between two paint cycles - bug #308201 + + loaded_effects.reserve(effect_order.count()); + std::copy(effect_order.constBegin(), effect_order.constEnd(), + std::back_inserter(loaded_effects)); + + m_activeEffects.reserve(loaded_effects.count()); +} + +QStringList EffectsHandlerImpl::activeEffects() const +{ + QStringList ret; + for(QVector< KWin::EffectPair >::const_iterator it = loaded_effects.constBegin(), + end = loaded_effects.constEnd(); it != end; ++it) { + if (it->second->isActive()) { + ret << it->first; + } + } + return ret; +} + +KWaylandServer::Display *EffectsHandlerImpl::waylandDisplay() const +{ + if (waylandServer()) { + return waylandServer()->display(); + } + return nullptr; +} + +EffectFrame* EffectsHandlerImpl::effectFrame(EffectFrameStyle style, bool staticSize, const QPoint& position, Qt::Alignment alignment) const +{ + return new EffectFrameImpl(style, staticSize, position, alignment); +} + + +QVariant EffectsHandlerImpl::kwinOption(KWinOption kwopt) +{ + switch (kwopt) { + case CloseButtonCorner: + // TODO: this could become per window and be derived from the actual position in the deco + return Decoration::DecorationBridge::self()->settings()->decorationButtonsLeft().contains(KDecoration2::DecorationButtonType::Close) ? Qt::TopLeftCorner : Qt::TopRightCorner; + case SwitchDesktopOnScreenEdge: + return ScreenEdges::self()->isDesktopSwitching(); + case SwitchDesktopOnScreenEdgeMovingWindows: + return ScreenEdges::self()->isDesktopSwitchingMovingClients(); + default: + return QVariant(); // an invalid one + } +} + +QString EffectsHandlerImpl::supportInformation(const QString &name) const +{ + auto it = std::find_if(loaded_effects.constBegin(), loaded_effects.constEnd(), + [name](const EffectPair &pair) { return pair.first == name; }); + if (it == loaded_effects.constEnd()) { + return QString(); + } + + QString support((*it).first + QLatin1String(":\n")); + const QMetaObject *metaOptions = (*it).second->metaObject(); + for (int i=0; ipropertyCount(); ++i) { + const QMetaProperty property = metaOptions->property(i); + if (qstrcmp(property.name(), "objectName") == 0) { + continue; + } + support += QString::fromUtf8(property.name()) + QLatin1String(": ") + (*it).second->property(property.name()).toString() + QLatin1Char('\n'); + } + + return support; +} + + +bool EffectsHandlerImpl::isScreenLocked() const +{ + return ScreenLockerWatcher::self()->isLocked(); +} + +QString EffectsHandlerImpl::debug(const QString& name, const QString& parameter) const +{ + QString internalName = name.toLower();; + for (QVector< EffectPair >::const_iterator it = loaded_effects.constBegin(); it != loaded_effects.constEnd(); ++it) { + if ((*it).first == internalName) { + return it->second->debug(parameter); + } + } + return QString(); +} + +bool EffectsHandlerImpl::makeOpenGLContextCurrent() +{ + return m_scene->makeOpenGLContextCurrent(); +} + +void EffectsHandlerImpl::doneOpenGLContextCurrent() +{ + m_scene->doneOpenGLContextCurrent(); +} + +bool EffectsHandlerImpl::animationsSupported() const +{ + static const QByteArray forceEnvVar = qgetenv("KWIN_EFFECTS_FORCE_ANIMATIONS"); + if (!forceEnvVar.isEmpty()) { + static const int forceValue = forceEnvVar.toInt(); + return forceValue == 1; + } + return m_scene->animationsSupported(); +} + +void EffectsHandlerImpl::highlightWindows(const QVector &windows) +{ + Effect *e = provides(Effect::HighlightWindows); + if (!e) { + return; + } + e->perform(Effect::HighlightWindows, QVariantList{QVariant::fromValue(windows)}); +} + +PlatformCursorImage EffectsHandlerImpl::cursorImage() const +{ + return kwinApp()->platform()->cursorImage(); +} + +void EffectsHandlerImpl::hideCursor() +{ + kwinApp()->platform()->hideCursor(); +} + +void EffectsHandlerImpl::showCursor() +{ + kwinApp()->platform()->showCursor(); +} + +void EffectsHandlerImpl::startInteractiveWindowSelection(std::function callback) +{ + kwinApp()->platform()->startInteractiveWindowSelection( + [callback] (KWin::Toplevel *t) { + if (t && t->effectWindow()) { + callback(t->effectWindow()); + } else { + callback(nullptr); + } + } + ); +} + +void EffectsHandlerImpl::startInteractivePositionSelection(std::function callback) +{ + kwinApp()->platform()->startInteractivePositionSelection(callback); +} + +void EffectsHandlerImpl::showOnScreenMessage(const QString &message, const QString &iconName) +{ + OSD::show(message, iconName); +} + +void EffectsHandlerImpl::hideOnScreenMessage(OnScreenMessageHideFlags flags) +{ + OSD::HideFlags osdFlags; + if (flags.testFlag(OnScreenMessageHideFlag::SkipsCloseAnimation)) { + osdFlags |= OSD::HideFlag::SkipCloseAnimation; + } + OSD::hide(osdFlags); +} + +KSharedConfigPtr EffectsHandlerImpl::config() const +{ + return kwinApp()->config(); +} + +KSharedConfigPtr EffectsHandlerImpl::inputConfig() const +{ + return InputConfig::self()->inputConfig(); +} + +Effect *EffectsHandlerImpl::findEffect(const QString &name) const +{ + auto it = std::find_if(loaded_effects.constBegin(), loaded_effects.constEnd(), + [name] (const EffectPair &pair) { + return pair.first == name; + } + ); + if (it == loaded_effects.constEnd()) { + return nullptr; + } + return (*it).second; +} + +void EffectsHandlerImpl::renderEffectQuickView(EffectQuickView *w) const +{ + if (!w->isVisible()) { + return; + } + scene()->paintEffectQuickView(w); +} + +SessionState EffectsHandlerImpl::sessionState() const +{ + return Workspace::self()->sessionManager()->state(); +} + +//**************************************** +// EffectWindowImpl +//**************************************** + +EffectWindowImpl::EffectWindowImpl(Toplevel *toplevel) + : EffectWindow(toplevel) + , toplevel(toplevel) + , sw(nullptr) +{ + // Deleted windows are not managed. So, when windowClosed signal is + // emitted, effects can't distinguish managed windows from unmanaged + // windows(e.g. combo box popups, popup menus, etc). Save value of the + // managed property during construction of EffectWindow. At that time, + // parent can be Client, XdgShellClient, or Unmanaged. So, later on, when + // an instance of Deleted becomes parent of the EffectWindow, effects + // can still figure out whether it is/was a managed window. + managed = toplevel->isClient(); + + waylandClient = qobject_cast(toplevel) != nullptr; + x11Client = qobject_cast(toplevel) != nullptr || + qobject_cast(toplevel) != nullptr; +} + +EffectWindowImpl::~EffectWindowImpl() +{ + QVariant cachedTextureVariant = data(LanczosCacheRole); + if (cachedTextureVariant.isValid()) { + GLTexture *cachedTexture = static_cast< GLTexture*>(cachedTextureVariant.value()); + delete cachedTexture; + } +} + +bool EffectWindowImpl::isPaintingEnabled() +{ + return sceneWindow()->isPaintingEnabled(); +} + +void EffectWindowImpl::enablePainting(int reason) +{ + sceneWindow()->enablePainting(reason); +} + +void EffectWindowImpl::disablePainting(int reason) +{ + sceneWindow()->disablePainting(reason); +} + +void EffectWindowImpl::addRepaint(const QRect &r) +{ + toplevel->addRepaint(r); +} + +void EffectWindowImpl::addRepaint(int x, int y, int w, int h) +{ + toplevel->addRepaint(x, y, w, h); +} + +void EffectWindowImpl::addRepaintFull() +{ + toplevel->addRepaintFull(); +} + +void EffectWindowImpl::addLayerRepaint(const QRect &r) +{ + toplevel->addLayerRepaint(r); +} + +void EffectWindowImpl::addLayerRepaint(int x, int y, int w, int h) +{ + toplevel->addLayerRepaint(x, y, w, h); +} + +const EffectWindowGroup* EffectWindowImpl::group() const +{ + if (auto c = qobject_cast(toplevel)) { + return c->group()->effectGroup(); + } + return nullptr; // TODO +} + +void EffectWindowImpl::refWindow() +{ + if (auto d = qobject_cast(toplevel)) { + return d->refWindow(); + } + abort(); // TODO +} + +void EffectWindowImpl::unrefWindow() +{ + if (auto d = qobject_cast(toplevel)) { + return d->unrefWindow(); // delays deletion in case + } + abort(); // TODO +} + +#define TOPLEVEL_HELPER( rettype, prototype, toplevelPrototype) \ + rettype EffectWindowImpl::prototype ( ) const \ + { \ + return toplevel->toplevelPrototype(); \ + } + +TOPLEVEL_HELPER(double, opacity, opacity) +TOPLEVEL_HELPER(bool, hasAlpha, hasAlpha) +TOPLEVEL_HELPER(int, x, x) +TOPLEVEL_HELPER(int, y, y) +TOPLEVEL_HELPER(int, width, width) +TOPLEVEL_HELPER(int, height, height) +TOPLEVEL_HELPER(QPoint, pos, pos) +TOPLEVEL_HELPER(QSize, size, size) +TOPLEVEL_HELPER(int, screen, screen) +TOPLEVEL_HELPER(QRect, geometry, frameGeometry) +TOPLEVEL_HELPER(QRect, frameGeometry, frameGeometry) +TOPLEVEL_HELPER(QRect, bufferGeometry, bufferGeometry) +TOPLEVEL_HELPER(QRect, expandedGeometry, visibleRect) +TOPLEVEL_HELPER(QRect, rect, rect) +TOPLEVEL_HELPER(int, desktop, desktop) +TOPLEVEL_HELPER(bool, isDesktop, isDesktop) +TOPLEVEL_HELPER(bool, isDock, isDock) +TOPLEVEL_HELPER(bool, isToolbar, isToolbar) +TOPLEVEL_HELPER(bool, isMenu, isMenu) +TOPLEVEL_HELPER(bool, isNormalWindow, isNormalWindow) +TOPLEVEL_HELPER(bool, isDialog, isDialog) +TOPLEVEL_HELPER(bool, isSplash, isSplash) +TOPLEVEL_HELPER(bool, isUtility, isUtility) +TOPLEVEL_HELPER(bool, isDropdownMenu, isDropdownMenu) +TOPLEVEL_HELPER(bool, isPopupMenu, isPopupMenu) +TOPLEVEL_HELPER(bool, isTooltip, isTooltip) +TOPLEVEL_HELPER(bool, isNotification, isNotification) +TOPLEVEL_HELPER(bool, isCriticalNotification, isCriticalNotification) +TOPLEVEL_HELPER(bool, isOnScreenDisplay, isOnScreenDisplay) +TOPLEVEL_HELPER(bool, isComboBox, isComboBox) +TOPLEVEL_HELPER(bool, isDNDIcon, isDNDIcon) +TOPLEVEL_HELPER(bool, isDeleted, isDeleted) +TOPLEVEL_HELPER(bool, hasOwnShape, shape) +TOPLEVEL_HELPER(QString, windowRole, windowRole) +TOPLEVEL_HELPER(QStringList, activities, activities) +TOPLEVEL_HELPER(bool, skipsCloseAnimation, skipsCloseAnimation) +TOPLEVEL_HELPER(KWaylandServer::SurfaceInterface *, surface, surface) +TOPLEVEL_HELPER(bool, isPopupWindow, isPopupWindow) +TOPLEVEL_HELPER(bool, isOutline, isOutline) +TOPLEVEL_HELPER(pid_t, pid, pid) + +#undef TOPLEVEL_HELPER + +#define CLIENT_HELPER_WITH_DELETED( rettype, prototype, propertyname, defaultValue ) \ + rettype EffectWindowImpl::prototype ( ) const \ + { \ + auto client = qobject_cast(toplevel); \ + if (client) { \ + return client->propertyname(); \ + } \ + auto deleted = qobject_cast(toplevel); \ + if (deleted) { \ + return deleted->propertyname(); \ + } \ + return defaultValue; \ + } + +CLIENT_HELPER_WITH_DELETED(bool, isMinimized, isMinimized, false) +CLIENT_HELPER_WITH_DELETED(bool, isModal, isModal, false) +CLIENT_HELPER_WITH_DELETED(bool, isFullScreen, isFullScreen, false) +CLIENT_HELPER_WITH_DELETED(bool, keepAbove, keepAbove, false) +CLIENT_HELPER_WITH_DELETED(bool, keepBelow, keepBelow, false) +CLIENT_HELPER_WITH_DELETED(QString, caption, caption, QString()); +CLIENT_HELPER_WITH_DELETED(QVector, desktops, x11DesktopIds, QVector()); + +#undef CLIENT_HELPER_WITH_DELETED + +// legacy from tab groups, can be removed when no effects use this any more. +bool EffectWindowImpl::isCurrentTab() const +{ + return true; +} + +QString EffectWindowImpl::windowClass() const +{ + return toplevel->resourceName() + QLatin1Char(' ') + toplevel->resourceClass(); +} + +QRect EffectWindowImpl::contentsRect() const +{ + return QRect(toplevel->clientPos(), toplevel->clientSize()); +} + +NET::WindowType EffectWindowImpl::windowType() const +{ + return toplevel->windowType(); +} + +#define CLIENT_HELPER( rettype, prototype, propertyname, defaultValue ) \ + rettype EffectWindowImpl::prototype ( ) const \ + { \ + auto client = qobject_cast(toplevel); \ + if (client) { \ + return client->propertyname(); \ + } \ + return defaultValue; \ + } + +CLIENT_HELPER(bool, isMovable, isMovable, false) +CLIENT_HELPER(bool, isMovableAcrossScreens, isMovableAcrossScreens, false) +CLIENT_HELPER(bool, isUserMove, isMove, false) +CLIENT_HELPER(bool, isUserResize, isResize, false) +CLIENT_HELPER(QRect, iconGeometry, iconGeometry, QRect()) +CLIENT_HELPER(bool, isSpecialWindow, isSpecialWindow, true) +CLIENT_HELPER(bool, acceptsFocus, wantsInput, true) // We don't actually know... +CLIENT_HELPER(QIcon, icon, icon, QIcon()) +CLIENT_HELPER(bool, isSkipSwitcher, skipSwitcher, false) +CLIENT_HELPER(bool, decorationHasAlpha, decorationHasAlpha, false) +CLIENT_HELPER(bool, isUnresponsive, unresponsive, false) + +#undef CLIENT_HELPER + +QSize EffectWindowImpl::basicUnit() const +{ + if (auto client = qobject_cast(toplevel)){ + return client->basicUnit(); + } + return QSize(1,1); +} + +void EffectWindowImpl::setWindow(Toplevel* w) +{ + toplevel = w; + setParent(w); +} + +void EffectWindowImpl::setSceneWindow(Scene::Window* w) +{ + sw = w; +} + +QRegion EffectWindowImpl::shape() const +{ + if (isX11Client() && sceneWindow()) { + return sceneWindow()->bufferShape(); + } + return toplevel->rect(); +} + +QRect EffectWindowImpl::decorationInnerRect() const +{ + auto client = qobject_cast(toplevel); + return client ? client->transparentRect() : contentsRect(); +} + +QByteArray EffectWindowImpl::readProperty(long atom, long type, int format) const +{ + if (!kwinApp()->x11Connection()) { + return QByteArray(); + } + return readWindowProperty(window()->window(), atom, type, format); +} + +void EffectWindowImpl::deleteProperty(long int atom) const +{ + if (kwinApp()->x11Connection()) { + deleteWindowProperty(window()->window(), atom); + } +} + +EffectWindow* EffectWindowImpl::findModal() +{ + auto client = qobject_cast(toplevel); + if (!client) { + return nullptr; + } + + AbstractClient *modal = client->findModal(); + if (modal) { + return modal->effectWindow(); + } + + return nullptr; +} + +EffectWindow* EffectWindowImpl::transientFor() +{ + auto client = qobject_cast(toplevel); + if (!client) { + return nullptr; + } + + AbstractClient *transientFor = client->transientFor(); + if (transientFor) { + return transientFor->effectWindow(); + } + + return nullptr; +} + +QWindow *EffectWindowImpl::internalWindow() const +{ + auto client = qobject_cast(toplevel); + if (!client) { + return nullptr; + } + return client->internalWindow(); +} + +template +EffectWindowList getMainWindows(T *c) +{ + const auto mainclients = c->mainClients(); + EffectWindowList ret; + ret.reserve(mainclients.size()); + std::transform(std::cbegin(mainclients), std::cend(mainclients), + std::back_inserter(ret), + [](auto client) { return client->effectWindow(); }); + return ret; +} + +EffectWindowList EffectWindowImpl::mainWindows() const +{ + if (auto client = qobject_cast(toplevel)) { + return getMainWindows(client); + } + if (auto deleted = qobject_cast(toplevel)) { + return getMainWindows(deleted); + } + return {}; +} + +WindowQuadList EffectWindowImpl::buildQuads(bool force) const +{ + return sceneWindow()->buildQuads(force); +} + +void EffectWindowImpl::setData(int role, const QVariant &data) +{ + if (!data.isNull()) + dataMap[ role ] = data; + else + dataMap.remove(role); + emit effects->windowDataChanged(this, role); +} + +QVariant EffectWindowImpl::data(int role) const +{ + return dataMap.value(role); +} + +EffectWindow* effectWindow(Toplevel* w) +{ + EffectWindowImpl* ret = w->effectWindow(); + return ret; +} + +EffectWindow* effectWindow(Scene::Window* w) +{ + EffectWindowImpl* ret = w->window()->effectWindow(); + ret->setSceneWindow(w); + return ret; +} + +void EffectWindowImpl::elevate(bool elevate) +{ + effects->setElevatedWindow(this, elevate); +} + +void EffectWindowImpl::registerThumbnail(AbstractThumbnailItem *item) +{ + if (WindowThumbnailItem *thumb = qobject_cast(item)) { + insertThumbnail(thumb); + connect(thumb, SIGNAL(destroyed(QObject*)), SLOT(thumbnailDestroyed(QObject*))); + connect(thumb, &WindowThumbnailItem::wIdChanged, this, &EffectWindowImpl::thumbnailTargetChanged); + } else if (DesktopThumbnailItem *desktopThumb = qobject_cast(item)) { + m_desktopThumbnails.append(desktopThumb); + connect(desktopThumb, SIGNAL(destroyed(QObject*)), SLOT(desktopThumbnailDestroyed(QObject*))); + } +} + +void EffectWindowImpl::thumbnailDestroyed(QObject *object) +{ + // we know it is a ThumbnailItem + m_thumbnails.remove(static_cast(object)); +} + +void EffectWindowImpl::thumbnailTargetChanged() +{ + if (WindowThumbnailItem *item = qobject_cast(sender())) { + insertThumbnail(item); + } +} + +void EffectWindowImpl::insertThumbnail(WindowThumbnailItem *item) +{ + EffectWindow *w = effects->findWindow(item->wId()); + if (w) { + m_thumbnails.insert(item, QPointer(static_cast(w))); + } else { + m_thumbnails.insert(item, QPointer()); + } +} + +void EffectWindowImpl::desktopThumbnailDestroyed(QObject *object) +{ + // we know it is a DesktopThumbnailItem + m_desktopThumbnails.removeAll(static_cast(object)); +} + +void EffectWindowImpl::minimize() +{ + if (auto client = qobject_cast(toplevel)) { + client->minimize(); + } +} + +void EffectWindowImpl::unminimize() +{ + if (auto client = qobject_cast(toplevel)) { + client->unminimize(); + } +} + +void EffectWindowImpl::closeWindow() +{ + if (auto client = qobject_cast(toplevel)) { + client->closeWindow(); + } +} + +void EffectWindowImpl::referencePreviousWindowPixmap() +{ + if (sw) { + sw->referencePreviousPixmap(); + } +} + +void EffectWindowImpl::unreferencePreviousWindowPixmap() +{ + if (sw) { + sw->unreferencePreviousPixmap(); + } +} + +bool EffectWindowImpl::isManaged() const +{ + return managed; +} + +bool EffectWindowImpl::isWaylandClient() const +{ + return waylandClient; +} + +bool EffectWindowImpl::isX11Client() const +{ + return x11Client; +} + + +//**************************************** +// EffectWindowGroupImpl +//**************************************** + + +EffectWindowList EffectWindowGroupImpl::members() const +{ + const auto memberList = group->members(); + EffectWindowList ret; + ret.reserve(memberList.size()); + std::transform(std::cbegin(memberList), std::cend(memberList), + std::back_inserter(ret), + [](auto toplevel) { return toplevel->effectWindow(); }); + return ret; +} + +//**************************************** +// EffectFrameImpl +//**************************************** + +EffectFrameImpl::EffectFrameImpl(EffectFrameStyle style, bool staticSize, QPoint position, Qt::Alignment alignment) + : QObject(nullptr) + , EffectFrame() + , m_style(style) + , m_static(staticSize) + , m_point(position) + , m_alignment(alignment) + , m_shader(nullptr) + , m_theme(new Plasma::Theme(this)) +{ + if (m_style == EffectFrameStyled) { + m_frame.setImagePath(QStringLiteral("widgets/background")); + m_frame.setCacheAllRenderedFrames(true); + connect(m_theme, SIGNAL(themeChanged()), this, SLOT(plasmaThemeChanged())); + } + m_selection.setImagePath(QStringLiteral("widgets/viewitem")); + m_selection.setElementPrefix(QStringLiteral("hover")); + m_selection.setCacheAllRenderedFrames(true); + m_selection.setEnabledBorders(Plasma::FrameSvg::AllBorders); + + m_sceneFrame = Compositor::self()->scene()->createEffectFrame(this); +} + +EffectFrameImpl::~EffectFrameImpl() +{ + delete m_sceneFrame; +} + +const QFont& EffectFrameImpl::font() const +{ + return m_font; +} + +void EffectFrameImpl::setFont(const QFont& font) +{ + if (m_font == font) { + return; + } + m_font = font; + QRect oldGeom = m_geometry; + if (!m_text.isEmpty()) { + autoResize(); + } + if (oldGeom == m_geometry) { + // Wasn't updated in autoResize() + m_sceneFrame->freeTextFrame(); + } +} + +void EffectFrameImpl::free() +{ + m_sceneFrame->free(); +} + +const QRect& EffectFrameImpl::geometry() const +{ + return m_geometry; +} + +void EffectFrameImpl::setGeometry(const QRect& geometry, bool force) +{ + QRect oldGeom = m_geometry; + m_geometry = geometry; + if (m_geometry == oldGeom && !force) { + return; + } + effects->addRepaint(oldGeom); + effects->addRepaint(m_geometry); + if (m_geometry.size() == oldGeom.size() && !force) { + return; + } + + if (m_style == EffectFrameStyled) { + qreal left, top, right, bottom; + m_frame.getMargins(left, top, right, bottom); // m_geometry is the inner geometry + m_frame.resizeFrame(m_geometry.adjusted(-left, -top, right, bottom).size()); + } + + free(); +} + +const QIcon& EffectFrameImpl::icon() const +{ + return m_icon; +} + +void EffectFrameImpl::setIcon(const QIcon& icon) +{ + m_icon = icon; + if (isCrossFade()) { + m_sceneFrame->crossFadeIcon(); + } + if (m_iconSize.isEmpty() && !m_icon.availableSizes().isEmpty()) { // Set a size if we don't already have one + setIconSize(m_icon.availableSizes().first()); + } + m_sceneFrame->freeIconFrame(); +} + +const QSize& EffectFrameImpl::iconSize() const +{ + return m_iconSize; +} + +void EffectFrameImpl::setIconSize(const QSize& size) +{ + if (m_iconSize == size) { + return; + } + m_iconSize = size; + autoResize(); + m_sceneFrame->freeIconFrame(); +} + +void EffectFrameImpl::plasmaThemeChanged() +{ + free(); +} + +void EffectFrameImpl::render(const QRegion ®ion, double opacity, double frameOpacity) +{ + if (m_geometry.isEmpty()) { + return; // Nothing to display + } + m_shader = nullptr; + setScreenProjectionMatrix(static_cast(effects)->scene()->screenProjectionMatrix()); + effects->paintEffectFrame(this, region, opacity, frameOpacity); +} + +void EffectFrameImpl::finalRender(QRegion region, double opacity, double frameOpacity) const +{ + region = infiniteRegion(); // TODO: Old region doesn't seem to work with OpenGL + + m_sceneFrame->render(region, opacity, frameOpacity); +} + +Qt::Alignment EffectFrameImpl::alignment() const +{ + return m_alignment; +} + + +void +EffectFrameImpl::align(QRect &geometry) +{ + if (m_alignment & Qt::AlignLeft) + geometry.moveLeft(m_point.x()); + else if (m_alignment & Qt::AlignRight) + geometry.moveLeft(m_point.x() - geometry.width()); + else + geometry.moveLeft(m_point.x() - geometry.width() / 2); + if (m_alignment & Qt::AlignTop) + geometry.moveTop(m_point.y()); + else if (m_alignment & Qt::AlignBottom) + geometry.moveTop(m_point.y() - geometry.height()); + else + geometry.moveTop(m_point.y() - geometry.height() / 2); +} + + +void EffectFrameImpl::setAlignment(Qt::Alignment alignment) +{ + m_alignment = alignment; + align(m_geometry); + setGeometry(m_geometry); +} + +void EffectFrameImpl::setPosition(const QPoint& point) +{ + m_point = point; + QRect geometry = m_geometry; // this is important, setGeometry need call repaint for old & new geometry + align(geometry); + setGeometry(geometry); +} + +const QString& EffectFrameImpl::text() const +{ + return m_text; +} + +void EffectFrameImpl::setText(const QString& text) +{ + if (m_text == text) { + return; + } + if (isCrossFade()) { + m_sceneFrame->crossFadeText(); + } + m_text = text; + QRect oldGeom = m_geometry; + autoResize(); + if (oldGeom == m_geometry) { + // Wasn't updated in autoResize() + m_sceneFrame->freeTextFrame(); + } +} + +void EffectFrameImpl::setSelection(const QRect& selection) +{ + if (selection == m_selectionGeometry) { + return; + } + m_selectionGeometry = selection; + if (m_selectionGeometry.size() != m_selection.frameSize().toSize()) { + m_selection.resizeFrame(m_selectionGeometry.size()); + } + // TODO; optimize to only recreate when resizing + m_sceneFrame->freeSelection(); +} + +void EffectFrameImpl::autoResize() +{ + if (m_static) + return; // Not automatically resizing + + QRect geometry; + // Set size + if (!m_text.isEmpty()) { + QFontMetrics metrics(m_font); + geometry.setSize(metrics.size(0, m_text)); + } + if (!m_icon.isNull() && !m_iconSize.isEmpty()) { + geometry.setLeft(-m_iconSize.width()); + if (m_iconSize.height() > geometry.height()) + geometry.setHeight(m_iconSize.height()); + } + + align(geometry); + setGeometry(geometry); +} + +QColor EffectFrameImpl::styledTextColor() +{ + return m_theme->color(Plasma::Theme::TextColor); +} + +} // namespace diff --git a/effects.h b/effects.h new file mode 100644 index 0000000..d7d54d7 --- /dev/null +++ b/effects.h @@ -0,0 +1,663 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2010, 2011 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_EFFECTSIMPL_H +#define KWIN_EFFECTSIMPL_H + +#include "kwineffects.h" + +#include "scene.h" + +#include +#include + +#include + +namespace Plasma { +class Theme; +} + +namespace KWaylandServer +{ +class Display; +} + +class QDBusPendingCallWatcher; +class QDBusServiceWatcher; + + +namespace KWin +{ + +class AbstractThumbnailItem; +class DesktopThumbnailItem; +class WindowThumbnailItem; + +class AbstractClient; +class Compositor; +class Deleted; +class EffectLoader; +class Group; +class Toplevel; +class Unmanaged; +class WindowPropertyNotifyX11Filter; + +class KWIN_EXPORT EffectsHandlerImpl : public EffectsHandler +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.kwin.Effects") + Q_PROPERTY(QStringList activeEffects READ activeEffects) + Q_PROPERTY(QStringList loadedEffects READ loadedEffects) + Q_PROPERTY(QStringList listOfEffects READ listOfEffects) +public: + EffectsHandlerImpl(Compositor *compositor, Scene *scene); + ~EffectsHandlerImpl() override; + void prePaintScreen(ScreenPrePaintData& data, int time) override; + void paintScreen(int mask, const QRegion ®ion, ScreenPaintData& data) override; + /** + * Special hook to perform a paintScreen but just with the windows on @p desktop. + */ + void paintDesktop(int desktop, int mask, QRegion region, ScreenPaintData& data); + void postPaintScreen() override; + void prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time) override; + void paintWindow(EffectWindow* w, int mask, const QRegion ®ion, WindowPaintData& data) override; + void postPaintWindow(EffectWindow* w) override; + void paintEffectFrame(EffectFrame* frame, const QRegion ®ion, double opacity, double frameOpacity) override; + + Effect *provides(Effect::Feature ef); + + void drawWindow(EffectWindow* w, int mask, const QRegion ®ion, WindowPaintData& data) override; + + void buildQuads(EffectWindow* w, WindowQuadList& quadList) override; + + void activateWindow(EffectWindow* c) override; + EffectWindow* activeWindow() const override; + void moveWindow(EffectWindow* w, const QPoint& pos, bool snap = false, double snapAdjust = 1.0) override; + void windowToDesktop(EffectWindow* w, int desktop) override; + void windowToScreen(EffectWindow* w, int screen) override; + void setShowingDesktop(bool showing) override; + + QString currentActivity() const override; + int currentDesktop() const override; + int numberOfDesktops() const override; + void setCurrentDesktop(int desktop) override; + void setNumberOfDesktops(int desktops) override; + QSize desktopGridSize() const override; + int desktopGridWidth() const override; + int desktopGridHeight() const override; + int workspaceWidth() const override; + int workspaceHeight() const override; + int desktopAtCoords(QPoint coords) const override; + QPoint desktopGridCoords(int id) const override; + QPoint desktopCoords(int id) const override; + int desktopAbove(int desktop = 0, bool wrap = true) const override; + int desktopToRight(int desktop = 0, bool wrap = true) const override; + int desktopBelow(int desktop = 0, bool wrap = true) const override; + int desktopToLeft(int desktop = 0, bool wrap = true) const override; + QString desktopName(int desktop) const override; + bool optionRollOverDesktops() const override; + + QPoint cursorPos() const override; + bool grabKeyboard(Effect* effect) override; + void ungrabKeyboard() override; + // not performing XGrabPointer + void startMouseInterception(Effect *effect, Qt::CursorShape shape) override; + void stopMouseInterception(Effect *effect) override; + bool isMouseInterception() const; + void registerGlobalShortcut(const QKeySequence &shortcut, QAction *action) override; + void registerPointerShortcut(Qt::KeyboardModifiers modifiers, Qt::MouseButton pointerButtons, QAction *action) override; + void registerAxisShortcut(Qt::KeyboardModifiers modifiers, PointerAxisDirection axis, QAction *action) override; + void registerTouchpadSwipeShortcut(SwipeDirection direction, QAction *action) override; + void* getProxy(QString name) override; + void startMousePolling() override; + void stopMousePolling() override; + EffectWindow* findWindow(WId id) const override; + EffectWindow* findWindow(KWaylandServer::SurfaceInterface *surf) const override; + EffectWindow *findWindow(QWindow *w) const override; + EffectWindow *findWindow(const QUuid &id) const override; + EffectWindowList stackingOrder() const override; + void setElevatedWindow(KWin::EffectWindow* w, bool set) override; + + void setTabBoxWindow(EffectWindow*) override; + void setTabBoxDesktop(int) override; + EffectWindowList currentTabBoxWindowList() const override; + void refTabBox() override; + void unrefTabBox() override; + void closeTabBox() override; + QList< int > currentTabBoxDesktopList() const override; + int currentTabBoxDesktop() const override; + EffectWindow* currentTabBoxWindow() const override; + + void setActiveFullScreenEffect(Effect* e) override; + Effect* activeFullScreenEffect() const override; + bool hasActiveFullScreenEffect() const override; + + void addRepaintFull() override; + void addRepaint(const QRect& r) override; + void addRepaint(const QRegion& r) override; + void addRepaint(int x, int y, int w, int h) override; + int activeScreen() const override; + int numScreens() const override; + int screenNumber(const QPoint& pos) const override; + QRect clientArea(clientAreaOption, int screen, int desktop) const override; + QRect clientArea(clientAreaOption, const EffectWindow* c) const override; + QRect clientArea(clientAreaOption, const QPoint& p, int desktop) const override; + QSize virtualScreenSize() const override; + QRect virtualScreenGeometry() const override; + double animationTimeFactor() const override; + WindowQuadType newWindowQuadType() override; + + void defineCursor(Qt::CursorShape shape) override; + bool checkInputWindowEvent(QMouseEvent *e); + bool checkInputWindowEvent(QWheelEvent *e); + void checkInputWindowStacking(); + + void reserveElectricBorder(ElectricBorder border, Effect *effect) override; + void unreserveElectricBorder(ElectricBorder border, Effect *effect) override; + + void registerTouchBorder(ElectricBorder border, QAction *action) override; + void unregisterTouchBorder(ElectricBorder border, QAction *action) override; + + unsigned long xrenderBufferPicture() override; + QPainter* scenePainter() override; + void reconfigure() override; + QByteArray readRootProperty(long atom, long type, int format) const override; + xcb_atom_t announceSupportProperty(const QByteArray& propertyName, Effect* effect) override; + void removeSupportProperty(const QByteArray& propertyName, Effect* effect) override; + + bool hasDecorationShadows() const override; + + bool decorationsHaveAlpha() const override; + + bool decorationSupportsBlurBehind() const override; + + EffectFrame* effectFrame(EffectFrameStyle style, bool staticSize, const QPoint& position, Qt::Alignment alignment) const override; + + QVariant kwinOption(KWinOption kwopt) override; + bool isScreenLocked() const override; + + bool makeOpenGLContextCurrent() override; + void doneOpenGLContextCurrent() override; + + xcb_connection_t *xcbConnection() const override; + xcb_window_t x11RootWindow() const override; + + // internal (used by kwin core or compositing code) + void startPaint(); + void grabbedKeyboardEvent(QKeyEvent* e); + bool hasKeyboardGrab() const; + void desktopResized(const QSize &size); + + void reloadEffect(Effect *effect) override; + QStringList loadedEffects() const; + QStringList listOfEffects() const; + void unloadAllEffects(); + + QList elevatedWindows() const; + QStringList activeEffects() const; + + /** + * @returns Whether we are currently in a desktop rendering process triggered by paintDesktop hook + */ + bool isDesktopRendering() const { + return m_desktopRendering; + } + /** + * @returns the desktop currently being rendered in the paintDesktop hook. + */ + int currentRenderedDesktop() const { + return m_currentRenderedDesktop; + } + + KWaylandServer::Display *waylandDisplay() const override; + + bool animationsSupported() const override; + + PlatformCursorImage cursorImage() const override; + + void hideCursor() override; + void showCursor() override; + + void startInteractiveWindowSelection(std::function callback) override; + void startInteractivePositionSelection(std::function callback) override; + + void showOnScreenMessage(const QString &message, const QString &iconName = QString()) override; + void hideOnScreenMessage(OnScreenMessageHideFlags flags = OnScreenMessageHideFlags()) override; + + KSharedConfigPtr config() const override; + KSharedConfigPtr inputConfig() const override; + + Scene *scene() const { + return m_scene; + } + + bool touchDown(qint32 id, const QPointF &pos, quint32 time); + bool touchMotion(qint32 id, const QPointF &pos, quint32 time); + bool touchUp(qint32 id, quint32 time); + + void highlightWindows(const QVector &windows); + + bool isPropertyTypeRegistered(xcb_atom_t atom) const { + return registered_atoms.contains(atom); + } + + void windowToDesktops(EffectWindow *w, const QVector &desktops) override; + + /** + * Finds an effect with the given name. + * + * @param name The name of the effect. + * @returns The effect with the given name @p name, or nullptr if there + * is no such effect loaded. + */ + Effect *findEffect(const QString &name) const; + + void renderEffectQuickView(EffectQuickView *effectQuickView) const override; + + SessionState sessionState() const override; + +public Q_SLOTS: + void slotCurrentTabAboutToChange(EffectWindow* from, EffectWindow* to); + void slotTabAdded(EffectWindow* from, EffectWindow* to); + void slotTabRemoved(EffectWindow* c, EffectWindow* newActiveWindow); + + // slots for D-Bus interface + Q_SCRIPTABLE void reconfigureEffect(const QString& name); + Q_SCRIPTABLE bool loadEffect(const QString& name); + Q_SCRIPTABLE void toggleEffect(const QString& name); + Q_SCRIPTABLE void unloadEffect(const QString& name); + Q_SCRIPTABLE bool isEffectLoaded(const QString& name) const; + Q_SCRIPTABLE bool isEffectSupported(const QString& name); + Q_SCRIPTABLE QList areEffectsSupported(const QStringList &names); + Q_SCRIPTABLE QString supportInformation(const QString& name) const; + Q_SCRIPTABLE QString debug(const QString& name, const QString& parameter = QString()) const; + +protected Q_SLOTS: + void slotClientShown(KWin::Toplevel*); + void slotUnmanagedShown(KWin::Toplevel*); + void slotWindowClosed(KWin::Toplevel *c, KWin::Deleted *d); + void slotClientMaximized(KWin::AbstractClient *c, MaximizeMode maxMode); + void slotOpacityChanged(KWin::Toplevel *t, qreal oldOpacity); + void slotClientModalityChanged(); + void slotGeometryShapeChanged(KWin::Toplevel *t, const QRect &old); + void slotFrameGeometryChanged(Toplevel *toplevel, const QRect &oldGeometry); + void slotPaddingChanged(KWin::Toplevel *t, const QRect &old); + void slotWindowDamaged(KWin::Toplevel *t, const QRect& r); + +protected: + void connectNotify(const QMetaMethod &signal) override; + void disconnectNotify(const QMetaMethod &signal) override; + void effectsChanged(); + void setupClientConnections(KWin::AbstractClient *client); + void setupUnmanagedConnections(KWin::Unmanaged *u); + + /** + * Default implementation does nothing and returns @c true. + */ + virtual bool doGrabKeyboard(); + /** + * Default implementation does nothing. + */ + virtual void doUngrabKeyboard(); + + /** + * Default implementation sets Effects override cursor on the PointerInputRedirection. + */ + virtual void doStartMouseInterception(Qt::CursorShape shape); + + /** + * Default implementation removes the Effects override cursor on the PointerInputRedirection. + */ + virtual void doStopMouseInterception(); + + /** + * Default implementation does nothing + */ + virtual void doCheckInputWindowStacking(); + + Effect* keyboard_grab_effect; + Effect* fullscreen_effect; + QList elevated_windows; + QMultiMap< int, EffectPair > effect_order; + QHash< long, int > registered_atoms; + int next_window_quad_type; + +private: + void registerPropertyType(long atom, bool reg); + void destroyEffect(Effect *effect); + + typedef QVector< Effect*> EffectsList; + typedef EffectsList::const_iterator EffectsIterator; + EffectsList m_activeEffects; + EffectsIterator m_currentDrawWindowIterator; + EffectsIterator m_currentPaintWindowIterator; + EffectsIterator m_currentPaintEffectFrameIterator; + EffectsIterator m_currentPaintScreenIterator; + EffectsIterator m_currentBuildQuadsIterator; + typedef QHash< QByteArray, QList< Effect*> > PropertyEffectMap; + PropertyEffectMap m_propertiesForEffects; + QHash m_managedProperties; + Compositor *m_compositor; + Scene *m_scene; + bool m_desktopRendering; + int m_currentRenderedDesktop; + QList m_grabbedMouseEffects; + EffectLoader *m_effectLoader; + int m_trackingCursorChanges; + std::unique_ptr m_x11WindowPropertyNotify; +}; + +class EffectWindowImpl : public EffectWindow +{ + Q_OBJECT +public: + explicit EffectWindowImpl(Toplevel *toplevel); + ~EffectWindowImpl() override; + + void enablePainting(int reason) override; + void disablePainting(int reason) override; + bool isPaintingEnabled() override; + + void addRepaint(const QRect &r) override; + void addRepaint(int x, int y, int w, int h) override; + void addRepaintFull() override; + void addLayerRepaint(const QRect &r) override; + void addLayerRepaint(int x, int y, int w, int h) override; + + void refWindow() override; + void unrefWindow() override; + + const EffectWindowGroup* group() const override; + + bool isDeleted() const override; + bool isMinimized() const override; + double opacity() const override; + bool hasAlpha() const override; + + QStringList activities() const override; + int desktop() const override; + QVector desktops() const override; + int x() const override; + int y() const override; + int width() const override; + int height() const override; + + QSize basicUnit() const override; + QRect geometry() const override; + QRect frameGeometry() const override; + QRect bufferGeometry() const override; + + QString caption() const override; + + QRect expandedGeometry() const override; + QRegion shape() const override; + int screen() const override; + bool hasOwnShape() const override; // only for shadow effect, for now + QPoint pos() const override; + QSize size() const override; + QRect rect() const override; + + bool isMovable() const override; + bool isMovableAcrossScreens() const override; + bool isUserMove() const override; + bool isUserResize() const override; + QRect iconGeometry() const override; + + bool isDesktop() const override; + bool isDock() const override; + bool isToolbar() const override; + bool isMenu() const override; + bool isNormalWindow() const override; + bool isSpecialWindow() const override; + bool isDialog() const override; + bool isSplash() const override; + bool isUtility() const override; + bool isDropdownMenu() const override; + bool isPopupMenu() const override; + bool isTooltip() const override; + bool isNotification() const override; + bool isCriticalNotification() const override; + bool isOnScreenDisplay() const override; + bool isComboBox() const override; + bool isDNDIcon() const override; + bool skipsCloseAnimation() const override; + + bool acceptsFocus() const override; + bool keepAbove() const override; + bool keepBelow() const override; + bool isModal() const override; + bool isPopupWindow() const override; + bool isOutline() const override; + + KWaylandServer::SurfaceInterface *surface() const override; + bool isFullScreen() const override; + bool isUnresponsive() const override; + + QRect contentsRect() const override; + bool decorationHasAlpha() const override; + QIcon icon() const override; + QString windowClass() const override; + NET::WindowType windowType() const override; + bool isSkipSwitcher() const override; + bool isCurrentTab() const override; + QString windowRole() const override; + + bool isManaged() const override; + bool isWaylandClient() const override; + bool isX11Client() const override; + + pid_t pid() const override; + + QRect decorationInnerRect() const override; + QByteArray readProperty(long atom, long type, int format) const override; + void deleteProperty(long atom) const override; + + EffectWindow* findModal() override; + EffectWindow* transientFor() override; + EffectWindowList mainWindows() const override; + + WindowQuadList buildQuads(bool force = false) const override; + + void minimize() override; + void unminimize() override; + void closeWindow() override; + + void referencePreviousWindowPixmap() override; + void unreferencePreviousWindowPixmap() override; + + QWindow *internalWindow() const override; + + const Toplevel* window() const; + Toplevel* window(); + + void setWindow(Toplevel* w); // internal + void setSceneWindow(Scene::Window* w); // internal + const Scene::Window* sceneWindow() const; // internal + Scene::Window* sceneWindow(); // internal + + void elevate(bool elevate); + + void setData(int role, const QVariant &data) override; + QVariant data(int role) const override; + + void registerThumbnail(AbstractThumbnailItem *item); + QHash > const &thumbnails() const { + return m_thumbnails; + } + QList const &desktopThumbnails() const { + return m_desktopThumbnails; + } + +private Q_SLOTS: + void thumbnailDestroyed(QObject *object); + void thumbnailTargetChanged(); + void desktopThumbnailDestroyed(QObject *object); +private: + void insertThumbnail(WindowThumbnailItem *item); + Toplevel* toplevel; + Scene::Window* sw; // This one is used only during paint pass. + QHash dataMap; + QHash > m_thumbnails; + QList m_desktopThumbnails; + bool managed = false; + bool waylandClient; + bool x11Client; +}; + +class EffectWindowGroupImpl + : public EffectWindowGroup +{ +public: + explicit EffectWindowGroupImpl(Group* g); + EffectWindowList members() const override; +private: + Group* group; +}; + +class KWIN_EXPORT EffectFrameImpl + : public QObject, public EffectFrame +{ + Q_OBJECT +public: + explicit EffectFrameImpl(EffectFrameStyle style, bool staticSize = true, QPoint position = QPoint(-1, -1), + Qt::Alignment alignment = Qt::AlignCenter); + ~EffectFrameImpl() override; + + void free() override; + void render(const QRegion ®ion = infiniteRegion(), double opacity = 1.0, double frameOpacity = 1.0) override; + Qt::Alignment alignment() const override; + void setAlignment(Qt::Alignment alignment) override; + const QFont& font() const override; + void setFont(const QFont& font) override; + const QRect& geometry() const override; + void setGeometry(const QRect& geometry, bool force = false) override; + const QIcon& icon() const override; + void setIcon(const QIcon& icon) override; + const QSize& iconSize() const override; + void setIconSize(const QSize& size) override; + void setPosition(const QPoint& point) override; + const QString& text() const override; + void setText(const QString& text) override; + EffectFrameStyle style() const override { + return m_style; + }; + Plasma::FrameSvg& frame() { + return m_frame; + } + bool isStatic() const { + return m_static; + }; + void finalRender(QRegion region, double opacity, double frameOpacity) const; + void setShader(GLShader* shader) override { + m_shader = shader; + } + GLShader* shader() const override { + return m_shader; + } + void setSelection(const QRect& selection) override; + const QRect& selection() const { + return m_selectionGeometry; + } + Plasma::FrameSvg& selectionFrame() { + return m_selection; + } + /** + * The foreground text color as specified by the default Plasma theme. + */ + QColor styledTextColor(); + +private Q_SLOTS: + void plasmaThemeChanged(); + +private: + Q_DISABLE_COPY(EffectFrameImpl) // As we need to use Qt slots we cannot copy this class + void align(QRect &geometry); // positions geometry around m_point respecting m_alignment + void autoResize(); // Auto-resize if not a static size + + EffectFrameStyle m_style; + Plasma::FrameSvg m_frame; // TODO: share between all EffectFrames + Plasma::FrameSvg m_selection; + + // Position + bool m_static; + QPoint m_point; + Qt::Alignment m_alignment; + QRect m_geometry; + + // Contents + QString m_text; + QFont m_font; + QIcon m_icon; + QSize m_iconSize; + QRect m_selectionGeometry; + + Scene::EffectFrame* m_sceneFrame; + GLShader* m_shader; + + Plasma::Theme *m_theme; +}; + +inline +QList EffectsHandlerImpl::elevatedWindows() const +{ + if (isScreenLocked()) + return QList(); + return elevated_windows; +} + +inline +xcb_window_t EffectsHandlerImpl::x11RootWindow() const +{ + return rootWindow(); +} + +inline +xcb_connection_t *EffectsHandlerImpl::xcbConnection() const +{ + return connection(); +} + +inline +EffectWindowGroupImpl::EffectWindowGroupImpl(Group* g) + : group(g) +{ +} + +EffectWindow* effectWindow(Toplevel* w); +EffectWindow* effectWindow(Scene::Window* w); + +inline +const Scene::Window* EffectWindowImpl::sceneWindow() const +{ + return sw; +} + +inline +Scene::Window* EffectWindowImpl::sceneWindow() +{ + return sw; +} + +inline +const Toplevel* EffectWindowImpl::window() const +{ + return toplevel; +} + +inline +Toplevel* EffectWindowImpl::window() +{ + return toplevel; +} + + +} // namespace + +#endif diff --git a/effects/CMakeLists.txt b/effects/CMakeLists.txt new file mode 100644 index 0000000..ed3e067 --- /dev/null +++ b/effects/CMakeLists.txt @@ -0,0 +1,223 @@ +# KI18N Translation Domain for this library +add_definitions(-DTRANSLATION_DOMAIN=\"kwin_effects\" -DEFFECT_BUILTINS) + +include_directories(${KWin_SOURCE_DIR}) # for xcbutils.h + +if (HAVE_ACCESSIBILITY) + include_directories(${QACCESSIBILITYCLIENT_INCLUDE_DIR}) +endif() + +set(kwin_effect_OWN_LIBS + kwineffects +) + +if (KWIN_HAVE_XRENDER_COMPOSITING) + set(kwin_effect_OWN_LIBS ${kwin_effect_OWN_LIBS} kwinxrenderutils) +endif() + +set(kwin_effect_KDE_LIBS + KF5::ConfigGui + KF5::ConfigWidgets + KF5::GlobalAccel + KF5::I18n + KF5::Notifications # screenshot effect + KF5::Plasma # screenedge effect + KF5::WindowSystem + KF5::Service # utils / screenshot effect +) + +if (HAVE_ACCESSIBILITY) + set(kwin_effect_KDE_LIBS ${kwin_effect_KDE_LIBS} ${QACCESSIBILITYCLIENT_LIBRARY}) +endif() + +set(kwin_effect_QT_LIBS + Qt5::Concurrent + Qt5::DBus + Qt5::Quick + Qt5::X11Extras +) + +set(kwin_effect_XLIB_LIBS + ${X11_X11_LIB} +) + +set(kwin_effect_XCB_LIBS + XCB::IMAGE + XCB::XCB + XCB::XFIXES +) + +if (KWIN_HAVE_XRENDER_COMPOSITING) + set(kwin_effect_XCB_LIBS ${kwin_effect_XCB_LIBS} XCB::RENDER) +endif() + +set(kwin_effect_OWN_LIBS ${kwin_effect_OWN_LIBS} kwinglutils) + +macro(KWIN4_ADD_EFFECT_BACKEND name) + add_library(${name} SHARED ${ARGN}) + target_link_libraries(${name} PRIVATE + ${kwin_effect_KDE_LIBS} + ${kwin_effect_OWN_LIBS} + ${kwin_effect_QT_LIBS} + ${kwin_effect_XCB_LIBS} + ${kwin_effect_XLIB_LIBS} + ) +endmacro() + +# Adds effect plugin with given name. Sources are given after the name +macro(KWIN4_ADD_EFFECT name) + kwin4_add_effect_backend(kwin4_effect_${name} ${ARGN}) + + set_target_properties(kwin4_effect_${name} PROPERTIES VERSION 1.0.0 SOVERSION 1) + set_target_properties(kwin4_effect_${name} PROPERTIES OUTPUT_NAME ${KWIN_NAME}4_effect_${name}) + install(TARGETS kwin4_effect_${name} ${INSTALL_TARGETS_DEFAULT_ARGS}) +endmacro() + +# Install the KWin/Effect service type +install(FILES kwineffect.desktop DESTINATION ${SERVICETYPES_INSTALL_DIR}) + +# Create initial variables +set(kwin4_effect_include_directories) + +set(kwin4_effect_builtins_sources + blur/blur.cpp + blur/blurshader.cpp + colorpicker/colorpicker.cpp + coverswitch/coverswitch.cpp + cube/cube.cpp + cube/cube_proxy.cpp + cubeslide/cubeslide.cpp + desktopgrid/desktopgrid.cpp + diminactive/diminactive.cpp + effect_builtins.cpp + flipswitch/flipswitch.cpp + glide/glide.cpp + invert/invert.cpp + logging.cpp + lookingglass/lookingglass.cpp + magiclamp/magiclamp.cpp + magnifier/magnifier.cpp + mouseclick/mouseclick.cpp + mousemark/mousemark.cpp + presentwindows/presentwindows.cpp + presentwindows/presentwindows_proxy.cpp + resize/resize.cpp + showfps/showfps.cpp + showpaint/showpaint.cpp + slide/slide.cpp + thumbnailaside/thumbnailaside.cpp + touchpoints/touchpoints.cpp + trackmouse/trackmouse.cpp + windowgeometry/windowgeometry.cpp + wobblywindows/wobblywindows.cpp + zoom/zoom.cpp + ../service_utils.cpp +) + +if (HAVE_ACCESSIBILITY) + set(kwin4_effect_builtins_sources + zoom/accessibilityintegration.cpp + ${kwin4_effect_builtins_sources} + ) +endif() + +qt5_add_resources(kwin4_effect_builtins_sources shaders.qrc) + +kconfig_add_kcfg_files(kwin4_effect_builtins_sources + blur/blurconfig.kcfgc + coverswitch/coverswitchconfig.kcfgc + cube/cubeconfig.kcfgc + cubeslide/cubeslideconfig.kcfgc + desktopgrid/desktopgridconfig.kcfgc + diminactive/diminactiveconfig.kcfgc + fallapart/fallapartconfig.kcfgc + flipswitch/flipswitchconfig.kcfgc + glide/glideconfig.kcfgc + lookingglass/lookingglassconfig.kcfgc + magiclamp/magiclampconfig.kcfgc + magnifier/magnifierconfig.kcfgc + mouseclick/mouseclickconfig.kcfgc + mousemark/mousemarkconfig.kcfgc + presentwindows/presentwindowsconfig.kcfgc + resize/resizeconfig.kcfgc + showfps/showfpsconfig.kcfgc + slide/slideconfig.kcfgc + slidingpopups/slidingpopupsconfig.kcfgc + thumbnailaside/thumbnailasideconfig.kcfgc + trackmouse/trackmouseconfig.kcfgc + windowgeometry/windowgeometryconfig.kcfgc + wobblywindows/wobblywindowsconfig.kcfgc + zoom/zoomconfig.kcfgc +) + +# scripted effects +function(install_scripted_effect name) + kpackage_install_package(${name}/package kwin4_effect_${name} effects kwin) + + # necessary so tests are found without installing + file(COPY ${name}/package/contents ${name}/package/metadata.desktop DESTINATION ${CMAKE_BINARY_DIR}/bin/kwin/effects/kwin4_effect_${name}) +endfunction() +install_scripted_effect(dialogparent) +install_scripted_effect(dimscreen) +install_scripted_effect(eyeonscreen) +install_scripted_effect(fade) +install_scripted_effect(fadedesktop) +install_scripted_effect(fadingpopups) +install_scripted_effect(frozenapp) +install_scripted_effect(fullscreen) +install_scripted_effect(login) +install_scripted_effect(logout) +install_scripted_effect(maximize) +install_scripted_effect(morphingpopups) +install_scripted_effect(scale) +install_scripted_effect(squash) +install_scripted_effect(translucency) +install_scripted_effect(windowaperture) +install_scripted_effect(sessionquit) + +############################################################################### +# Built-in effects go here + +# Common effects +add_subdirectory(desktopgrid) +add_subdirectory(diminactive) +include(fallapart/CMakeLists.txt) +include(highlightwindow/CMakeLists.txt) +include(kscreen/CMakeLists.txt) +add_subdirectory(magiclamp) +add_subdirectory(presentwindows) +add_subdirectory(resize) +include(screenedge/CMakeLists.txt) +add_subdirectory(showfps) +add_subdirectory(showpaint) +add_subdirectory(slide) +include(slideback/CMakeLists.txt) +include(slidingpopups/CMakeLists.txt) +add_subdirectory(thumbnailaside) +add_subdirectory(windowgeometry) +add_subdirectory(zoom) + +# OpenGL-specific effects +add_subdirectory(blur) +include(backgroundcontrast/CMakeLists.txt) +add_subdirectory(coverswitch) +add_subdirectory(cube) +add_subdirectory(cubeslide) +add_subdirectory(flipswitch) +add_subdirectory(glide) +add_subdirectory(invert) +add_subdirectory(lookingglass) +add_subdirectory(magnifier) +add_subdirectory(mouseclick) +add_subdirectory(mousemark) +include(screenshot/CMakeLists.txt) +include(sheet/CMakeLists.txt) +include(snaphelper/CMakeLists.txt) +include(startupfeedback/CMakeLists.txt) +add_subdirectory(trackmouse) +add_subdirectory(wobblywindows) + +############################################################################### + +# Add the builtins plugin +kwin4_add_effect(builtins ${kwin4_effect_builtins_sources}) diff --git a/effects/Messages.sh b/effects/Messages.sh new file mode 100644 index 0000000..70d65f7 --- /dev/null +++ b/effects/Messages.sh @@ -0,0 +1,4 @@ +#! /usr/bin/env bash +$EXTRACTRC `find . -name \*.ui` >> rc.cpp || exit 11 +$XGETTEXT `find . -name \*.cpp -o -name \*.h` -o $podir/kwin_effects.pot +rm -f rc.cpp diff --git a/effects/backgroundcontrast/.directory b/effects/backgroundcontrast/.directory new file mode 100644 index 0000000..a1f0a3e --- /dev/null +++ b/effects/backgroundcontrast/.directory @@ -0,0 +1,3 @@ +[Dolphin] +Timestamp=2013,12,1,20,18,54 +Version=3 diff --git a/effects/backgroundcontrast/CMakeLists.txt b/effects/backgroundcontrast/CMakeLists.txt new file mode 100644 index 0000000..c6cbcf8 --- /dev/null +++ b/effects/backgroundcontrast/CMakeLists.txt @@ -0,0 +1,8 @@ +####################################### +# Effect + +# Source files +set(kwin4_effect_builtins_sources ${kwin4_effect_builtins_sources} + backgroundcontrast/contrast.cpp + backgroundcontrast/contrastshader.cpp +) diff --git a/effects/backgroundcontrast/backgroundcontrast.kdev4 b/effects/backgroundcontrast/backgroundcontrast.kdev4 new file mode 100644 index 0000000..ce6b581 --- /dev/null +++ b/effects/backgroundcontrast/backgroundcontrast.kdev4 @@ -0,0 +1,3 @@ +[Project] +Manager=KDevCMakeManager +Name=backgroundcontrast diff --git a/effects/backgroundcontrast/contrast.cpp b/effects/backgroundcontrast/contrast.cpp new file mode 100644 index 0000000..2c20a20 --- /dev/null +++ b/effects/backgroundcontrast/contrast.cpp @@ -0,0 +1,520 @@ +/* + SPDX-FileCopyrightText: 2010 Fredrik Höglund + SPDX-FileCopyrightText: 2011 Philipp Knechtges + SPDX-FileCopyrightText: 2014 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "contrast.h" +#include "contrastshader.h" +// KConfigSkeleton + +#include +#include + +#include +#include +#include + +namespace KWin +{ + +static const QByteArray s_contrastAtomName = QByteArrayLiteral("_KDE_NET_WM_BACKGROUND_CONTRAST_REGION"); + +ContrastEffect::ContrastEffect() +{ + shader = ContrastShader::create(); + + reconfigure(ReconfigureAll); + + // ### Hackish way to announce support. + // Should be included in _NET_SUPPORTED instead. + if (shader && shader->isValid()) { + net_wm_contrast_region = effects->announceSupportProperty(s_contrastAtomName, this); + KWaylandServer::Display *display = effects->waylandDisplay(); + if (display) { + m_contrastManager = display->createContrastManager(this); + } + } else { + net_wm_contrast_region = 0; + } + + connect(effects, &EffectsHandler::windowAdded, this, &ContrastEffect::slotWindowAdded); + connect(effects, &EffectsHandler::windowDeleted, this, &ContrastEffect::slotWindowDeleted); + connect(effects, &EffectsHandler::propertyNotify, this, &ContrastEffect::slotPropertyNotify); + connect(effects, &EffectsHandler::screenGeometryChanged, this, &ContrastEffect::slotScreenGeometryChanged); + connect(effects, &EffectsHandler::xcbConnectionChanged, this, + [this] { + if (shader && shader->isValid()) { + net_wm_contrast_region = effects->announceSupportProperty(s_contrastAtomName, this); + } + } + ); + + // Fetch the contrast regions for all windows + for (EffectWindow *window: effects->stackingOrder()) { + updateContrastRegion(window); + } +} + +ContrastEffect::~ContrastEffect() +{ + delete shader; +} + +void ContrastEffect::slotScreenGeometryChanged() +{ + effects->makeOpenGLContextCurrent(); + if (!supported()) { + effects->reloadEffect(this); + return; + } + for (EffectWindow *window: effects->stackingOrder()) { + updateContrastRegion(window); + } +} + +void ContrastEffect::reconfigure(ReconfigureFlags flags) +{ + Q_UNUSED(flags) + + if (shader) + shader->init(); + + if (!shader || !shader->isValid()) { + effects->removeSupportProperty(s_contrastAtomName, this); + delete m_contrastManager; + m_contrastManager = nullptr; + } +} + +void ContrastEffect::updateContrastRegion(EffectWindow *w) +{ + QRegion region; + float colorTransform[16]; + QByteArray value; + + if (net_wm_contrast_region != XCB_ATOM_NONE) { + value = w->readProperty(net_wm_contrast_region, net_wm_contrast_region, 32); + + if (value.size() > 0 && !((value.size() - (16 * sizeof(uint32_t))) % ((4 * sizeof(uint32_t))))) { + const uint32_t *cardinals = reinterpret_cast(value.constData()); + const float *floatCardinals = reinterpret_cast(value.constData()); + unsigned int i = 0; + for (; i < ((value.size() - (16 * sizeof(uint32_t)))) / sizeof(uint32_t);) { + int x = cardinals[i++]; + int y = cardinals[i++]; + int w = cardinals[i++]; + int h = cardinals[i++]; + region += QRect(x, y, w, h); + } + + for (unsigned int j = 0; j < 16; ++j) { + colorTransform[j] = floatCardinals[i + j]; + } + + QMatrix4x4 colorMatrix(colorTransform); + m_colorMatrices[w] = colorMatrix; + } + } + + KWaylandServer::SurfaceInterface *surf = w->surface(); + + if (surf && surf->contrast()) { + region = surf->contrast()->region(); + m_colorMatrices[w] = colorMatrix(surf->contrast()->contrast(), surf->contrast()->intensity(), surf->contrast()->saturation()); + } + + if (auto internal = w->internalWindow()) { + const auto property = internal->property("kwin_background_region"); + if (property.isValid()) { + region = property.value(); + bool ok = false; + qreal contrast = internal->property("kwin_background_contrast").toReal(&ok); + if (!ok) { + contrast = 1.0; + } + qreal intensity = internal->property("kwin_background_intensity").toReal(&ok); + if (!ok) { + intensity = 1.0; + } + qreal saturation = internal->property("kwin_background_saturation").toReal(&ok); + if (!ok) { + saturation = 1.0; + } + m_colorMatrices[w] = colorMatrix(contrast, intensity, saturation); + } + } + + //!value.isNull() full window in X11 case, surf->contrast() + //valid, full window in wayland case + if (region.isEmpty() && (!value.isNull() || (surf && surf->contrast()))) { + // Set the data to a dummy value. + // This is needed to be able to distinguish between the value not + // being set, and being set to an empty region. + w->setData(WindowBackgroundContrastRole, 1); + } else + w->setData(WindowBackgroundContrastRole, region); +} + +void ContrastEffect::slotWindowAdded(EffectWindow *w) +{ + KWaylandServer::SurfaceInterface *surf = w->surface(); + + if (surf) { + m_contrastChangedConnections[w] = connect(surf, &KWaylandServer::SurfaceInterface::contrastChanged, this, [this, w] () { + + if (w) { + updateContrastRegion(w); + } + }); + } + + if (auto internal = w->internalWindow()) { + internal->installEventFilter(this); + } + + updateContrastRegion(w); +} + +bool ContrastEffect::eventFilter(QObject *watched, QEvent *event) +{ + auto internal = qobject_cast(watched); + if (internal && event->type() == QEvent::DynamicPropertyChange) { + QDynamicPropertyChangeEvent *pe = static_cast(event); + if (pe->propertyName() == "kwin_background_region" || + pe->propertyName() == "kwin_background_contrast" || + pe->propertyName() == "kwin_background_intensity" || + pe->propertyName() == "kwin_background_saturation") { + if (auto w = effects->findWindow(internal)) { + updateContrastRegion(w); + } + } + } + return false; +} + +void ContrastEffect::slotWindowDeleted(EffectWindow *w) +{ + if (m_contrastChangedConnections.contains(w)) { + disconnect(m_contrastChangedConnections[w]); + m_contrastChangedConnections.remove(w); + m_colorMatrices.remove(w); + } +} + +void ContrastEffect::slotPropertyNotify(EffectWindow *w, long atom) +{ + if (w && atom == net_wm_contrast_region && net_wm_contrast_region != XCB_ATOM_NONE) { + updateContrastRegion(w); + } +} + +QMatrix4x4 ContrastEffect::colorMatrix(qreal contrast, qreal intensity, qreal saturation) +{ + QMatrix4x4 satMatrix; //saturation + QMatrix4x4 intMatrix; //intensity + QMatrix4x4 contMatrix; //contrast + + //Saturation matrix + if (!qFuzzyCompare(saturation, 1.0)) { + const qreal rval = (1.0 - saturation) * .2126; + const qreal gval = (1.0 - saturation) * .7152; + const qreal bval = (1.0 - saturation) * .0722; + + satMatrix = QMatrix4x4(rval + saturation, rval, rval, 0.0, + gval, gval + saturation, gval, 0.0, + bval, bval, bval + saturation, 0.0, + 0, 0, 0, 1.0); + } + + //IntensityMatrix + if (!qFuzzyCompare(intensity, 1.0)) { + intMatrix.scale(intensity, intensity, intensity); + } + + //Contrast Matrix + if (!qFuzzyCompare(contrast, 1.0)) { + const float transl = (1.0 - contrast) / 2.0; + + contMatrix = QMatrix4x4(contrast, 0, 0, 0.0, + 0, contrast, 0, 0.0, + 0, 0, contrast, 0.0, + transl, transl, transl, 1.0); + } + + QMatrix4x4 colorMatrix = contMatrix * satMatrix * intMatrix; + //colorMatrix = colorMatrix.transposed(); + + return colorMatrix; +} + +bool ContrastEffect::enabledByDefault() +{ + GLPlatform *gl = GLPlatform::instance(); + + if (gl->isIntel() && gl->chipClass() < SandyBridge) + return false; + if (gl->isSoftwareEmulation()) { + return false; + } + + return true; +} + +bool ContrastEffect::supported() +{ + bool supported = effects->isOpenGLCompositing() && GLRenderTarget::supported(); + + if (supported) { + int maxTexSize; + glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maxTexSize); + + const QSize screenSize = effects->virtualScreenSize(); + if (screenSize.width() > maxTexSize || screenSize.height() > maxTexSize) + supported = false; + } + return supported; +} + +QRegion ContrastEffect::contrastRegion(const EffectWindow *w) const +{ + QRegion region; + + const QVariant value = w->data(WindowBackgroundContrastRole); + if (value.isValid()) { + const QRegion appRegion = qvariant_cast(value); + if (!appRegion.isEmpty()) { + region |= appRegion.translated(w->contentsRect().topLeft()) & + w->decorationInnerRect(); + } else { + // An empty region means that the blur effect should be enabled + // for the whole window. + region = w->decorationInnerRect(); + } + } + + return region; +} + +void ContrastEffect::uploadRegion(QVector2D *&map, const QRegion ®ion) +{ + for (const QRect &r : region) { + const QVector2D topLeft(r.x(), r.y()); + const QVector2D topRight(r.x() + r.width(), r.y()); + const QVector2D bottomLeft(r.x(), r.y() + r.height()); + const QVector2D bottomRight(r.x() + r.width(), r.y() + r.height()); + + // First triangle + *(map++) = topRight; + *(map++) = topLeft; + *(map++) = bottomLeft; + + // Second triangle + *(map++) = bottomLeft; + *(map++) = bottomRight; + *(map++) = topRight; + } +} + +void ContrastEffect::uploadGeometry(GLVertexBuffer *vbo, const QRegion ®ion) +{ + const int vertexCount = region.rectCount() * 6; + if (!vertexCount) + return; + + QVector2D *map = (QVector2D *) vbo->map(vertexCount * sizeof(QVector2D)); + uploadRegion(map, region); + vbo->unmap(); + + const GLVertexAttrib layout[] = { + { VA_Position, 2, GL_FLOAT, 0 }, + { VA_TexCoord, 2, GL_FLOAT, 0 } + }; + + vbo->setAttribLayout(layout, 2, sizeof(QVector2D)); +} + +void ContrastEffect::prePaintScreen(ScreenPrePaintData &data, int time) +{ + m_paintedArea = QRegion(); + m_currentContrast = QRegion(); + + effects->prePaintScreen(data, time); +} + +void ContrastEffect::prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time) +{ + // this effect relies on prePaintWindow being called in the bottom to top order + + effects->prePaintWindow(w, data, time); + + if (!w->isPaintingEnabled()) { + return; + } + if (!shader || !shader->isValid()) { + return; + } + + const QRegion oldPaint = data.paint; + + // we don't have to blur a region we don't see + m_currentContrast -= data.clip; + // if we have to paint a non-opaque part of this window that intersects with the + // currently blurred region (which is not cached) we have to redraw the whole region + if ((data.paint-data.clip).intersects(m_currentContrast)) { + data.paint |= m_currentContrast; + } + + // in case this window has regions to be blurred + const QRect screen = effects->virtualScreenGeometry(); + const QRegion contrastArea = contrastRegion(w).translated(w->pos()) & screen; + + // we are not caching the window + + // if this window or an window underneath the modified area is painted again we have to + // do everything + if (m_paintedArea.intersects(contrastArea) || data.paint.intersects(contrastArea)) { + data.paint |= contrastArea; + + // we have to check again whether we do not damage a blurred area + // of a window we do not cache + if (contrastArea.intersects(m_currentContrast)) { + data.paint |= m_currentContrast; + } + } + + m_currentContrast |= contrastArea; + + + // m_paintedArea keep track of all repainted areas + m_paintedArea -= data.clip; + m_paintedArea |= data.paint; +} + +bool ContrastEffect::shouldContrast(const EffectWindow *w, int mask, const WindowPaintData &data) const +{ + if (!shader || !shader->isValid()) + return false; + + if (effects->activeFullScreenEffect() && !w->data(WindowForceBackgroundContrastRole).toBool()) + return false; + + if (w->isDesktop()) + return false; + + bool scaled = !qFuzzyCompare(data.xScale(), 1.0) && !qFuzzyCompare(data.yScale(), 1.0); + bool translated = data.xTranslation() || data.yTranslation(); + + if ((scaled || (translated || (mask & PAINT_WINDOW_TRANSFORMED))) && !w->data(WindowForceBackgroundContrastRole).toBool()) + return false; + + if (!w->hasAlpha()) + return false; + + return true; +} + +void ContrastEffect::drawWindow(EffectWindow *w, int mask, const QRegion ®ion, WindowPaintData &data) +{ + const QRect screen = GLRenderTarget::virtualScreenGeometry(); + if (shouldContrast(w, mask, data)) { + QRegion shape = region & contrastRegion(w).translated(w->pos()) & screen; + + // let's do the evil parts - someone wants to blur behind a transformed window + const bool translated = data.xTranslation() || data.yTranslation(); + const bool scaled = data.xScale() != 1 || data.yScale() != 1; + if (scaled) { + QPoint pt = shape.boundingRect().topLeft(); + QRegion scaledShape; + for (QRect r : shape) { + r.moveTo(pt.x() + (r.x() - pt.x()) * data.xScale() + data.xTranslation(), + pt.y() + (r.y() - pt.y()) * data.yScale() + data.yTranslation()); + r.setWidth(r.width() * data.xScale()); + r.setHeight(r.height() * data.yScale()); + scaledShape |= r; + } + shape = scaledShape & region; + + //Only translated, not scaled + } else if (translated) { + shape = shape.translated(data.xTranslation(), data.yTranslation()); + shape = shape & region; + } + + if (!shape.isEmpty()) { + doContrast(w, shape, screen, data.opacity(), data.screenProjectionMatrix()); + } + } + + // Draw the window over the contrast area + effects->drawWindow(w, mask, region, data); +} + +void ContrastEffect::paintEffectFrame(EffectFrame *frame, const QRegion ®ion, double opacity, double frameOpacity) +{ + //FIXME: this is a no-op for now, it should figure out the right contrast, intensity, saturation + effects->paintEffectFrame(frame, region, opacity, frameOpacity); +} + +void ContrastEffect::doContrast(EffectWindow *w, const QRegion& shape, const QRect& screen, const float opacity, const QMatrix4x4 &screenProjection) +{ + const QRegion actualShape = shape & screen; + const QRect r = actualShape.boundingRect(); + + qreal scale = GLRenderTarget::virtualScreenScale(); + + // Upload geometry for the horizontal and vertical passes + GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer(); + vbo->reset(); + uploadGeometry(vbo, actualShape); + vbo->bindArrays(); + + // Create a scratch texture and copy the area in the back buffer that we're + // going to blur into it + GLTexture scratch(GL_RGBA8, r.width() * scale, r.height() * scale); + scratch.setFilter(GL_LINEAR); + scratch.setWrapMode(GL_CLAMP_TO_EDGE); + scratch.bind(); + + const QRect sg = GLRenderTarget::virtualScreenGeometry(); + glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, (r.x() - sg.x()) * scale, (sg.height() - (r.y() - sg.y() + r.height())) * scale, + scratch.width(), scratch.height()); + + // Draw the texture on the offscreen framebuffer object, while blurring it horizontally + + shader->setColorMatrix(m_colorMatrices.value(w)); + shader->bind(); + + + shader->setOpacity(opacity); + // Set up the texture matrix to transform from screen coordinates + // to texture coordinates. + QMatrix4x4 textureMatrix; + textureMatrix.scale(1.0 / r.width(), -1.0 / r.height(), 1); + textureMatrix.translate(-r.x(), -r.height() - r.y(), 0); + shader->setTextureMatrix(textureMatrix); + shader->setModelViewProjectionMatrix(screenProjection); + + vbo->draw(GL_TRIANGLES, 0, actualShape.rectCount() * 6); + + scratch.unbind(); + scratch.discard(); + + vbo->unbindArrays(); + + if (opacity < 1.0) { + glDisable(GL_BLEND); + } + + shader->unbind(); +} + +bool ContrastEffect::isActive() const +{ + return !effects->isScreenLocked(); +} + +} // namespace KWin + diff --git a/effects/backgroundcontrast/contrast.h b/effects/backgroundcontrast/contrast.h new file mode 100644 index 0000000..c4ca3e2 --- /dev/null +++ b/effects/backgroundcontrast/contrast.h @@ -0,0 +1,91 @@ +/* + SPDX-FileCopyrightText: 2010 Fredrik Höglund + SPDX-FileCopyrightText: 2014 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef CONTRAST_H +#define CONTRAST_H + +#include +#include +#include + +#include +#include + +namespace KWaylandServer +{ +class ContrastManagerInterface; +} + +namespace KWin +{ + +class ContrastShader; + +class ContrastEffect : public KWin::Effect +{ + Q_OBJECT +public: + ContrastEffect(); + ~ContrastEffect() override; + + static bool supported(); + static bool enabledByDefault(); + + static QMatrix4x4 colorMatrix(qreal contrast, qreal intensity, qreal saturation); + void reconfigure(ReconfigureFlags flags) override; + void prePaintScreen(ScreenPrePaintData &data, int time) override; + void prePaintWindow(EffectWindow *w, WindowPrePaintData &data, int time) override; + void drawWindow(EffectWindow *w, int mask, const QRegion ®ion, WindowPaintData &data) override; + void paintEffectFrame(EffectFrame *frame, const QRegion ®ion, double opacity, double frameOpacity) override; + + bool provides(Feature feature) override; + bool isActive() const override; + + int requestedEffectChainPosition() const override { + return 76; + } + + bool eventFilter(QObject *watched, QEvent *event) override; + +public Q_SLOTS: + void slotWindowAdded(KWin::EffectWindow *w); + void slotWindowDeleted(KWin::EffectWindow *w); + void slotPropertyNotify(KWin::EffectWindow *w, long atom); + void slotScreenGeometryChanged(); + +private: + QRegion contrastRegion(const EffectWindow *w) const; + bool shouldContrast(const EffectWindow *w, int mask, const WindowPaintData &data) const; + void updateContrastRegion(EffectWindow *w); + void doContrast(EffectWindow *w, const QRegion &shape, const QRect &screen, const float opacity, const QMatrix4x4 &screenProjection); + void uploadRegion(QVector2D *&map, const QRegion ®ion); + void uploadGeometry(GLVertexBuffer *vbo, const QRegion ®ion); + +private: + ContrastShader *shader; + long net_wm_contrast_region; + QRegion m_paintedArea; // actually painted area which is greater than m_damagedArea + QRegion m_currentContrast; // keeps track of the currently contrasted area of non-caching windows(from bottom to top) + QHash< const EffectWindow*, QMatrix4x4> m_colorMatrices; + QHash< const EffectWindow*, QMetaObject::Connection > m_contrastChangedConnections; // used only in Wayland to keep track of effect changed + KWaylandServer::ContrastManagerInterface *m_contrastManager = nullptr; +}; + +inline +bool ContrastEffect::provides(Effect::Feature feature) +{ + if (feature == Contrast) { + return true; + } + return KWin::Effect::provides(feature); +} + + +} // namespace KWin + +#endif + diff --git a/effects/backgroundcontrast/contrastshader.cpp b/effects/backgroundcontrast/contrastshader.cpp new file mode 100644 index 0000000..568db6b --- /dev/null +++ b/effects/backgroundcontrast/contrastshader.cpp @@ -0,0 +1,198 @@ +/* + SPDX-FileCopyrightText: 2010 Fredrik Höglund + SPDX-FileCopyrightText: 2014 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "contrastshader.h" + +#include +#include + +#include +#include +#include +#include + +#include + +namespace KWin +{ + +ContrastShader::ContrastShader() + : mValid(false), shader(nullptr), m_opacity(1) +{ +} + +ContrastShader::~ContrastShader() +{ + reset(); +} + +ContrastShader *ContrastShader::create() +{ + return new ContrastShader(); +} + +void ContrastShader::reset() +{ + delete shader; + shader = nullptr; + + setIsValid(false); +} + +void ContrastShader::setOpacity(float opacity) +{ + m_opacity = opacity; + + ShaderManager::instance()->pushShader(shader); + shader->setUniform(opacityLocation, opacity); + ShaderManager::instance()->popShader(); +} + +float ContrastShader::opacity() const +{ + return m_opacity; +} + +void ContrastShader::setColorMatrix(const QMatrix4x4 &matrix) +{ + if (!isValid()) + return; + + ShaderManager::instance()->pushShader(shader); + shader->setUniform(colorMatrixLocation, matrix); + ShaderManager::instance()->popShader(); +} + +void ContrastShader::setTextureMatrix(const QMatrix4x4 &matrix) +{ + if (!isValid()) + return; + + shader->setUniform(textureMatrixLocation, matrix); +} + +void ContrastShader::setModelViewProjectionMatrix(const QMatrix4x4 &matrix) +{ + if (!isValid()) + return; + + shader->setUniform(mvpMatrixLocation, matrix); +} + +void ContrastShader::bind() +{ + if (!isValid()) + return; + + ShaderManager::instance()->pushShader(shader); +} + +void ContrastShader::unbind() +{ + ShaderManager::instance()->popShader(); +} + +void ContrastShader::init() +{ + reset(); + + const bool gles = GLPlatform::instance()->isGLES(); + const bool glsl_140 = !gles && GLPlatform::instance()->glslVersion() >= kVersionNumber(1, 40); + const bool core = glsl_140 || (gles && GLPlatform::instance()->glslVersion() >= kVersionNumber(3, 0)); + + QByteArray vertexSource; + QByteArray fragmentSource; + + const QByteArray attribute = core ? "in" : "attribute"; + const QByteArray varying_in = core ? (gles ? "in" : "noperspective in") : "varying"; + const QByteArray varying_out = core ? (gles ? "out" : "noperspective out") : "varying"; + const QByteArray texture2D = core ? "texture" : "texture2D"; + const QByteArray fragColor = core ? "fragColor" : "gl_FragColor"; + + // Vertex shader + // =================================================================== + QTextStream stream(&vertexSource); + + if (gles) { + if (core) { + stream << "#version 300 es\n\n"; + } + stream << "precision highp float;\n"; + } else if (glsl_140) { + stream << "#version 140\n\n"; + } + + stream << "uniform mat4 modelViewProjectionMatrix;\n"; + stream << "uniform mat4 textureMatrix;\n"; + stream << attribute << " vec4 vertex;\n\n"; + stream << varying_out << " vec4 varyingTexCoords;\n"; + stream << "\n"; + stream << "void main(void)\n"; + stream << "{\n"; + stream << " varyingTexCoords = vec4(textureMatrix * vertex).stst;\n"; + stream << " gl_Position = modelViewProjectionMatrix * vertex;\n"; + stream << "}\n"; + stream.flush(); + + + // Fragment shader + // =================================================================== + QTextStream stream2(&fragmentSource); + + if (gles) { + if (core) { + stream2 << "#version 300 es\n\n"; + } + stream2 << "precision highp float;\n"; + } else if (glsl_140) { + stream2 << "#version 140\n\n"; + } + + stream2 << "uniform mat4 colorMatrix;\n"; + stream2 << "uniform sampler2D sampler;\n"; + stream2 << "uniform float opacity;\n"; + stream2 << varying_in << " vec4 varyingTexCoords;\n"; + + if (core) + stream2 << "out vec4 fragColor;\n\n"; + + stream2 << "void main(void)\n"; + stream2 << "{\n"; + stream2 << " vec4 tex = " << texture2D << "(sampler, varyingTexCoords.st);\n"; + + stream2 << " if (opacity >= 1.0) {\n"; + stream2 << " " << fragColor << " = tex * colorMatrix;\n"; + stream2 << " } else {\n"; + stream2 << " " << fragColor << " = tex * (opacity * colorMatrix + (1.0 - opacity) * mat4(1.0));\n"; + stream2 << " }\n"; + + stream2 << "}\n"; + stream2.flush(); + + shader = ShaderManager::instance()->loadShaderFromCode(vertexSource, fragmentSource); + + if (shader->isValid()) { + colorMatrixLocation = shader->uniformLocation("colorMatrix"); + textureMatrixLocation = shader->uniformLocation("textureMatrix"); + mvpMatrixLocation = shader->uniformLocation("modelViewProjectionMatrix"); + opacityLocation = shader->uniformLocation("opacity"); + + QMatrix4x4 modelViewProjection; + const QSize screenSize = effects->virtualScreenSize(); + modelViewProjection.ortho(0, screenSize.width(), screenSize.height(), 0, 0, 65535); + ShaderManager::instance()->pushShader(shader); + shader->setUniform(colorMatrixLocation, QMatrix4x4()); + shader->setUniform(textureMatrixLocation, QMatrix4x4()); + shader->setUniform(mvpMatrixLocation, modelViewProjection); + shader->setUniform(opacityLocation, (float)1.0); + ShaderManager::instance()->popShader(); + } + + setIsValid(shader->isValid()); +} + +} // namespace KWin diff --git a/effects/backgroundcontrast/contrastshader.h b/effects/backgroundcontrast/contrastshader.h new file mode 100644 index 0000000..8e254bc --- /dev/null +++ b/effects/backgroundcontrast/contrastshader.h @@ -0,0 +1,63 @@ +/* + SPDX-FileCopyrightText: 2010 Fredrik Höglund + SPDX-FileCopyrightText: 2014 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef CONTRASTSHADER_H +#define CONTRASTSHADER_H + +#include + +class QMatrix4x4; + +namespace KWin +{ + +class ContrastShader +{ +public: + ContrastShader(); + virtual ~ContrastShader(); + + void init(); + + static ContrastShader *create(); + + bool isValid() const { + return mValid; + } + + void setColorMatrix(const QMatrix4x4 &matrix); + + void setTextureMatrix(const QMatrix4x4 &matrix); + void setModelViewProjectionMatrix(const QMatrix4x4 &matrix); + + void bind(); + void unbind(); + + void setOpacity(float opacity); + float opacity() const; + +protected: + void setIsValid(bool value) { + mValid = value; + } + void reset(); + +private: + bool mValid; + GLShader *shader; + int mvpMatrixLocation; + int textureMatrixLocation; + int colorMatrixLocation; + int opacityLocation; + float m_opacity; +}; + + +} // namespace KWin + +#endif + diff --git a/effects/blur/CMakeLists.txt b/effects/blur/CMakeLists.txt new file mode 100644 index 0000000..35db55d --- /dev/null +++ b/effects/blur/CMakeLists.txt @@ -0,0 +1,23 @@ +####################################### +# Config +set(kwin_blur_config_SRCS blur_config.cpp) +ki18n_wrap_ui(kwin_blur_config_SRCS blur_config.ui) +kconfig_add_kcfg_files(kwin_blur_config_SRCS blurconfig.kcfgc) + +add_library(kwin_blur_config MODULE ${kwin_blur_config_SRCS}) + +target_link_libraries(kwin_blur_config + KF5::ConfigWidgets + KF5::I18n + Qt5::DBus + KWinEffectsInterface +) + +kcoreaddons_desktop_to_json(kwin_blur_config blur_config.desktop SERVICE_TYPES kcmodule.desktop) + +install( + TARGETS + kwin_blur_config + DESTINATION + ${PLUGIN_INSTALL_DIR}/kwin/effects/configs +) diff --git a/effects/blur/blur.cpp b/effects/blur/blur.cpp new file mode 100644 index 0000000..fffb6cd --- /dev/null +++ b/effects/blur/blur.cpp @@ -0,0 +1,818 @@ +/* + SPDX-FileCopyrightText: 2010 Fredrik Höglund + SPDX-FileCopyrightText: 2011 Philipp Knechtges + SPDX-FileCopyrightText: 2018 Alex Nemeth + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "blur.h" +#include "blurshader.h" +// KConfigSkeleton +#include "blurconfig.h" + +#include +#include +#include // for QGuiApplication +#include +#include +#include // for ceil() + +#include +#include +#include +#include +#include +#include + +namespace KWin +{ + +static const QByteArray s_blurAtomName = QByteArrayLiteral("_KDE_NET_WM_BLUR_BEHIND_REGION"); + +BlurEffect::BlurEffect() +{ + initConfig(); + m_shader = new BlurShader(this); + + initBlurStrengthValues(); + reconfigure(ReconfigureAll); + + // ### Hackish way to announce support. + // Should be included in _NET_SUPPORTED instead. + if (m_shader && m_shader->isValid() && m_renderTargetsValid) { + net_wm_blur_region = effects->announceSupportProperty(s_blurAtomName, this); + KWaylandServer::Display *display = effects->waylandDisplay(); + if (display) { + m_blurManager = display->createBlurManager(this); + } + } else { + net_wm_blur_region = 0; + } + + connect(effects, &EffectsHandler::windowAdded, this, &BlurEffect::slotWindowAdded); + connect(effects, &EffectsHandler::windowDeleted, this, &BlurEffect::slotWindowDeleted); + connect(effects, &EffectsHandler::propertyNotify, this, &BlurEffect::slotPropertyNotify); + connect(effects, &EffectsHandler::screenGeometryChanged, this, &BlurEffect::slotScreenGeometryChanged); + connect(effects, &EffectsHandler::xcbConnectionChanged, this, + [this] { + if (m_shader && m_shader->isValid() && m_renderTargetsValid) { + net_wm_blur_region = effects->announceSupportProperty(s_blurAtomName, this); + } + } + ); + + // Fetch the blur regions for all windows + foreach (EffectWindow *window, effects->stackingOrder()) + updateBlurRegion(window); +} + +BlurEffect::~BlurEffect() +{ + deleteFBOs(); +} + +void BlurEffect::slotScreenGeometryChanged() +{ + effects->makeOpenGLContextCurrent(); + updateTexture(); + + // Fetch the blur regions for all windows + foreach (EffectWindow *window, effects->stackingOrder()) + updateBlurRegion(window); + effects->doneOpenGLContextCurrent(); +} + +bool BlurEffect::renderTargetsValid() const +{ + return !m_renderTargets.isEmpty() && std::find_if(m_renderTargets.cbegin(), m_renderTargets.cend(), + [](const GLRenderTarget *target) { + return !target->valid(); + }) == m_renderTargets.cend(); +} + +void BlurEffect::deleteFBOs() +{ + qDeleteAll(m_renderTargets); + + m_renderTargets.clear(); + m_renderTextures.clear(); +} + +void BlurEffect::updateTexture() +{ + deleteFBOs(); + + /* Reserve memory for: + * - The original sized texture (1) + * - The downsized textures (m_downSampleIterations) + * - The helper texture (1) + */ + m_renderTargets.reserve(m_downSampleIterations + 2); + m_renderTextures.reserve(m_downSampleIterations + 2); + + GLenum textureFormat = GL_RGBA8; + + // Check the color encoding of the default framebuffer + if (!GLPlatform::instance()->isGLES()) { + GLuint prevFbo = 0; + glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, reinterpret_cast(&prevFbo)); + + if (prevFbo != 0) { + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + } + + GLenum colorEncoding = GL_LINEAR; + glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_BACK_LEFT, + GL_FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING, + reinterpret_cast(&colorEncoding)); + + if (prevFbo != 0) { + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, prevFbo); + } + + if (colorEncoding == GL_SRGB) { + textureFormat = GL_SRGB8_ALPHA8; + } + } + + for (int i = 0; i <= m_downSampleIterations; i++) { + m_renderTextures.append(GLTexture(textureFormat, effects->virtualScreenSize() / (1 << i))); + m_renderTextures.last().setFilter(GL_LINEAR); + m_renderTextures.last().setWrapMode(GL_CLAMP_TO_EDGE); + + m_renderTargets.append(new GLRenderTarget(m_renderTextures.last())); + } + + // This last set is used as a temporary helper texture + m_renderTextures.append(GLTexture(textureFormat, effects->virtualScreenSize())); + m_renderTextures.last().setFilter(GL_LINEAR); + m_renderTextures.last().setWrapMode(GL_CLAMP_TO_EDGE); + + m_renderTargets.append(new GLRenderTarget(m_renderTextures.last())); + + m_renderTargetsValid = renderTargetsValid(); + + // Prepare the stack for the rendering + m_renderTargetStack.clear(); + m_renderTargetStack.reserve(m_downSampleIterations * 2); + + // Upsample + for (int i = 1; i < m_downSampleIterations; i++) { + m_renderTargetStack.push(m_renderTargets[i]); + } + + // Downsample + for (int i = m_downSampleIterations; i > 0; i--) { + m_renderTargetStack.push(m_renderTargets[i]); + } + + // Copysample + m_renderTargetStack.push(m_renderTargets[0]); + + // Generate the noise helper texture + generateNoiseTexture(); +} + +void BlurEffect::initBlurStrengthValues() +{ + // This function creates an array of blur strength values that are evenly distributed + + // The range of the slider on the blur settings UI + int numOfBlurSteps = 15; + int remainingSteps = numOfBlurSteps; + + /* + * Explanation for these numbers: + * + * The texture blur amount depends on the downsampling iterations and the offset value. + * By changing the offset we can alter the blur amount without relying on further downsampling. + * But there is a minimum and maximum value of offset per downsample iteration before we + * get artifacts. + * + * The minOffset variable is the minimum offset value for an iteration before we + * get blocky artifacts because of the downsampling. + * + * The maxOffset value is the maximum offset value for an iteration before we + * get diagonal line artifacts because of the nature of the dual kawase blur algorithm. + * + * The expandSize value is the minimum value for an iteration before we reach the end + * of a texture in the shader and sample outside of the area that was copied into the + * texture from the screen. + */ + + // {minOffset, maxOffset, expandSize} + blurOffsets.append({1.0, 2.0, 10}); // Down sample size / 2 + blurOffsets.append({2.0, 3.0, 20}); // Down sample size / 4 + blurOffsets.append({2.0, 5.0, 50}); // Down sample size / 8 + blurOffsets.append({3.0, 8.0, 150}); // Down sample size / 16 + //blurOffsets.append({5.0, 10.0, 400}); // Down sample size / 32 + //blurOffsets.append({7.0, ?.0}); // Down sample size / 64 + + float offsetSum = 0; + + for (int i = 0; i < blurOffsets.size(); i++) { + offsetSum += blurOffsets[i].maxOffset - blurOffsets[i].minOffset; + } + + for (int i = 0; i < blurOffsets.size(); i++) { + int iterationNumber = std::ceil((blurOffsets[i].maxOffset - blurOffsets[i].minOffset) / offsetSum * numOfBlurSteps); + remainingSteps -= iterationNumber; + + if (remainingSteps < 0) { + iterationNumber += remainingSteps; + } + + float offsetDifference = blurOffsets[i].maxOffset - blurOffsets[i].minOffset; + + for (int j = 1; j <= iterationNumber; j++) { + // {iteration, offset} + blurStrengthValues.append({i + 1, blurOffsets[i].minOffset + (offsetDifference / iterationNumber) * j}); + } + } +} + +void BlurEffect::reconfigure(ReconfigureFlags flags) +{ + Q_UNUSED(flags) + + BlurConfig::self()->read(); + + int blurStrength = BlurConfig::blurStrength() - 1; + m_downSampleIterations = blurStrengthValues[blurStrength].iteration; + m_offset = blurStrengthValues[blurStrength].offset; + m_expandSize = blurOffsets[m_downSampleIterations - 1].expandSize; + m_noiseStrength = BlurConfig::noiseStrength(); + + m_scalingFactor = qMax(1.0, QGuiApplication::primaryScreen()->logicalDotsPerInch() / 96.0); + + updateTexture(); + + if (!m_shader || !m_shader->isValid()) { + effects->removeSupportProperty(s_blurAtomName, this); + delete m_blurManager; + m_blurManager = nullptr; + } + + // Update all windows for the blur to take effect + effects->addRepaintFull(); +} + +void BlurEffect::updateBlurRegion(EffectWindow *w) const +{ + QRegion region; + QByteArray value; + + if (net_wm_blur_region != XCB_ATOM_NONE) { + value = w->readProperty(net_wm_blur_region, XCB_ATOM_CARDINAL, 32); + if (value.size() > 0 && !(value.size() % (4 * sizeof(uint32_t)))) { + const uint32_t *cardinals = reinterpret_cast(value.constData()); + for (unsigned int i = 0; i < value.size() / sizeof(uint32_t);) { + int x = cardinals[i++]; + int y = cardinals[i++]; + int w = cardinals[i++]; + int h = cardinals[i++]; + region += QRect(x, y, w, h); + } + } + } + + KWaylandServer::SurfaceInterface *surf = w->surface(); + + if (surf && surf->blur()) { + region = surf->blur()->region(); + } + + if (auto internal = w->internalWindow()) { + const auto property = internal->property("kwin_blur"); + if (property.isValid()) { + region = property.value(); + } + } + + //!value.isNull() full window in X11 case, surf->blur() + //valid, full window in wayland case + if (region.isEmpty() && (!value.isNull() || (surf && surf->blur()))) { + // Set the data to a dummy value. + // This is needed to be able to distinguish between the value not + // being set, and being set to an empty region. + w->setData(WindowBlurBehindRole, 1); + } else + w->setData(WindowBlurBehindRole, region); +} + +void BlurEffect::slotWindowAdded(EffectWindow *w) +{ + KWaylandServer::SurfaceInterface *surf = w->surface(); + + if (surf) { + windowBlurChangedConnections[w] = connect(surf, &KWaylandServer::SurfaceInterface::blurChanged, this, [this, w] () { + if (w) { + updateBlurRegion(w); + } + }); + } + if (auto internal = w->internalWindow()) { + internal->installEventFilter(this); + } + + updateBlurRegion(w); +} + +void BlurEffect::slotWindowDeleted(EffectWindow *w) +{ + auto it = windowBlurChangedConnections.find(w); + if (it == windowBlurChangedConnections.end()) { + return; + } + disconnect(*it); + windowBlurChangedConnections.erase(it); +} + +void BlurEffect::slotPropertyNotify(EffectWindow *w, long atom) +{ + if (w && atom == net_wm_blur_region && net_wm_blur_region != XCB_ATOM_NONE) { + updateBlurRegion(w); + } +} + +bool BlurEffect::eventFilter(QObject *watched, QEvent *event) +{ + auto internal = qobject_cast(watched); + if (internal && event->type() == QEvent::DynamicPropertyChange) { + QDynamicPropertyChangeEvent *pe = static_cast(event); + if (pe->propertyName() == "kwin_blur") { + if (auto w = effects->findWindow(internal)) { + updateBlurRegion(w); + } + } + } + return false; +} + +bool BlurEffect::enabledByDefault() +{ + GLPlatform *gl = GLPlatform::instance(); + + if (gl->isIntel() && gl->chipClass() < SandyBridge) + return false; + if (gl->isSoftwareEmulation()) { + return false; + } + + return true; +} + +bool BlurEffect::supported() +{ + bool supported = effects->isOpenGLCompositing() && GLRenderTarget::supported() && GLRenderTarget::blitSupported(); + + if (supported) { + int maxTexSize; + glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maxTexSize); + + const QSize screenSize = effects->virtualScreenSize(); + if (screenSize.width() > maxTexSize || screenSize.height() > maxTexSize) + supported = false; + } + return supported; +} + +QRect BlurEffect::expand(const QRect &rect) const +{ + return rect.adjusted(-m_expandSize, -m_expandSize, m_expandSize, m_expandSize); +} + +QRegion BlurEffect::expand(const QRegion ®ion) const +{ + QRegion expanded; + + for (const QRect &rect : region) { + expanded += expand(rect); + } + + return expanded; +} + +QRegion BlurEffect::blurRegion(const EffectWindow *w) const +{ + QRegion region; + + const QVariant value = w->data(WindowBlurBehindRole); + if (value.isValid()) { + const QRegion appRegion = qvariant_cast(value); + if (!appRegion.isEmpty()) { + if (w->decorationHasAlpha() && effects->decorationSupportsBlurBehind()) { + region = w->shape() & w->rect(); + region -= w->decorationInnerRect(); + } + region |= appRegion.translated(w->contentsRect().topLeft()) & + w->decorationInnerRect(); + } else { + // An empty region means that the blur effect should be enabled + // for the whole window. + region = w->shape() & w->rect(); + } + } else if (w->decorationHasAlpha() && effects->decorationSupportsBlurBehind()) { + // If the client hasn't specified a blur region, we'll only enable + // the effect behind the decoration. + region = w->shape() & w->rect(); + region -= w->decorationInnerRect(); + } + + return region; +} + +void BlurEffect::uploadRegion(QVector2D *&map, const QRegion ®ion, const int downSampleIterations) +{ + for (int i = 0; i <= downSampleIterations; i++) { + const int divisionRatio = (1 << i); + + for (const QRect &r : region) { + const QVector2D topLeft( r.x() / divisionRatio, r.y() / divisionRatio); + const QVector2D topRight( (r.x() + r.width()) / divisionRatio, r.y() / divisionRatio); + const QVector2D bottomLeft( r.x() / divisionRatio, (r.y() + r.height()) / divisionRatio); + const QVector2D bottomRight((r.x() + r.width()) / divisionRatio, (r.y() + r.height()) / divisionRatio); + + // First triangle + *(map++) = topRight; + *(map++) = topLeft; + *(map++) = bottomLeft; + + // Second triangle + *(map++) = bottomLeft; + *(map++) = bottomRight; + *(map++) = topRight; + } + } +} + +void BlurEffect::uploadGeometry(GLVertexBuffer *vbo, const QRegion &blurRegion, const QRegion &windowRegion) +{ + const int vertexCount = ((blurRegion.rectCount() * (m_downSampleIterations + 1)) + windowRegion.rectCount()) * 6; + + if (!vertexCount) + return; + + QVector2D *map = (QVector2D *) vbo->map(vertexCount * sizeof(QVector2D)); + + uploadRegion(map, blurRegion, m_downSampleIterations); + uploadRegion(map, windowRegion, 0); + + vbo->unmap(); + + const GLVertexAttrib layout[] = { + { VA_Position, 2, GL_FLOAT, 0 }, + { VA_TexCoord, 2, GL_FLOAT, 0 } + }; + + vbo->setAttribLayout(layout, 2, sizeof(QVector2D)); +} + +void BlurEffect::prePaintScreen(ScreenPrePaintData &data, int time) +{ + m_paintedArea = QRegion(); + m_currentBlur = QRegion(); + + effects->prePaintScreen(data, time); +} + +void BlurEffect::prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time) +{ + // this effect relies on prePaintWindow being called in the bottom to top order + + effects->prePaintWindow(w, data, time); + + if (!w->isPaintingEnabled()) { + return; + } + if (!m_shader || !m_shader->isValid()) { + return; + } + + // to blur an area partially we have to shrink the opaque area of a window + QRegion newClip; + const QRegion oldClip = data.clip; + for (const QRect &rect : data.clip) { + newClip |= rect.adjusted(m_expandSize, m_expandSize, -m_expandSize, -m_expandSize); + } + data.clip = newClip; + + // we don't have to blur a region we don't see + m_currentBlur -= newClip; + // if we have to paint a non-opaque part of this window that intersects with the + // currently blurred region we have to redraw the whole region + if ((data.paint - oldClip).intersects(m_currentBlur)) { + data.paint |= m_currentBlur; + } + + // in case this window has regions to be blurred + const QRect screen = effects->virtualScreenGeometry(); + const QRegion blurArea = blurRegion(w).translated(w->pos()) & screen; + const QRegion expandedBlur = (w->isDock() ? blurArea : expand(blurArea)) & screen; + + // if this window or a window underneath the blurred area is painted again we have to + // blur everything + if (m_paintedArea.intersects(expandedBlur) || data.paint.intersects(blurArea)) { + data.paint |= expandedBlur; + // we have to check again whether we do not damage a blurred area + // of a window + if (expandedBlur.intersects(m_currentBlur)) { + data.paint |= m_currentBlur; + } + } + + m_currentBlur |= expandedBlur; + + m_paintedArea -= data.clip; + m_paintedArea |= data.paint; +} + +bool BlurEffect::shouldBlur(const EffectWindow *w, int mask, const WindowPaintData &data) const +{ + if (!m_renderTargetsValid || !m_shader || !m_shader->isValid()) + return false; + + if (effects->activeFullScreenEffect() && !w->data(WindowForceBlurRole).toBool()) + return false; + + if (w->isDesktop()) + return false; + + bool scaled = !qFuzzyCompare(data.xScale(), 1.0) && !qFuzzyCompare(data.yScale(), 1.0); + bool translated = data.xTranslation() || data.yTranslation(); + + if ((scaled || (translated || (mask & PAINT_WINDOW_TRANSFORMED))) && !w->data(WindowForceBlurRole).toBool()) + return false; + + bool blurBehindDecos = effects->decorationsHaveAlpha() && + effects->decorationSupportsBlurBehind(); + + if (!w->hasAlpha() && w->opacity() >= 1.0 && !(blurBehindDecos && w->hasDecoration())) + return false; + + return true; +} + +void BlurEffect::drawWindow(EffectWindow *w, int mask, const QRegion ®ion, WindowPaintData &data) +{ + const QRect screen = GLRenderTarget::virtualScreenGeometry(); + if (shouldBlur(w, mask, data)) { + QRegion shape = region & blurRegion(w).translated(w->pos()) & screen; + + // let's do the evil parts - someone wants to blur behind a transformed window + const bool translated = data.xTranslation() || data.yTranslation(); + const bool scaled = data.xScale() != 1 || data.yScale() != 1; + if (scaled) { + QPoint pt = shape.boundingRect().topLeft(); + QRegion scaledShape; + for (QRect r : shape) { + r.moveTo(pt.x() + (r.x() - pt.x()) * data.xScale() + data.xTranslation(), + pt.y() + (r.y() - pt.y()) * data.yScale() + data.yTranslation()); + r.setWidth(r.width() * data.xScale()); + r.setHeight(r.height() * data.yScale()); + scaledShape |= r; + } + shape = scaledShape & region; + + //Only translated, not scaled + } else if (translated) { + shape = shape.translated(data.xTranslation(), data.yTranslation()); + shape = shape & region; + } + + EffectWindow* modal = w->transientFor(); + const bool transientForIsDock = (modal ? modal->isDock() : false); + + if (!shape.isEmpty()) { + doBlur(shape, screen, data.opacity(), data.screenProjectionMatrix(), w->isDock() || transientForIsDock, w->geometry()); + } + } + + // Draw the window over the blurred area + effects->drawWindow(w, mask, region, data); +} + +void BlurEffect::paintEffectFrame(EffectFrame *frame, const QRegion ®ion, double opacity, double frameOpacity) +{ + const QRect screen = effects->virtualScreenGeometry(); + bool valid = m_renderTargetsValid && m_shader && m_shader->isValid(); + + QRegion shape = frame->geometry().adjusted(-borderSize, -borderSize, borderSize, borderSize) & screen; + + if (valid && !shape.isEmpty() && region.intersects(shape.boundingRect()) && frame->style() != EffectFrameNone) { + doBlur(shape, screen, opacity * frameOpacity, frame->screenProjectionMatrix(), false, frame->geometry()); + } + effects->paintEffectFrame(frame, region, opacity, frameOpacity); +} + +void BlurEffect::generateNoiseTexture() +{ + if (m_noiseStrength == 0) { + return; + } + + // Init randomness based on time + qsrand((uint)QTime::currentTime().msec()); + + QImage noiseImage(QSize(256, 256), QImage::Format_Grayscale8); + + for (int y = 0; y < noiseImage.height(); y++) { + uint8_t *noiseImageLine = (uint8_t *) noiseImage.scanLine(y); + + for (int x = 0; x < noiseImage.width(); x++) { + noiseImageLine[x] = qrand() % m_noiseStrength + (128 - m_noiseStrength / 2); + } + } + + // The noise texture looks distorted when not scaled with integer + noiseImage = noiseImage.scaled(noiseImage.size() * m_scalingFactor); + + m_noiseTexture = GLTexture(noiseImage); + m_noiseTexture.setFilter(GL_NEAREST); + m_noiseTexture.setWrapMode(GL_REPEAT); +} + +void BlurEffect::doBlur(const QRegion& shape, const QRect& screen, const float opacity, const QMatrix4x4 &screenProjection, bool isDock, QRect windowRect) +{ + // Blur would not render correctly on a secondary monitor because of wrong coordinates + // BUG: 393723 + const int xTranslate = -screen.x(); + const int yTranslate = effects->virtualScreenSize().height() - screen.height() - screen.y(); + + const QRegion expandedBlurRegion = expand(shape) & expand(screen); + + const bool useSRGB = m_renderTextures.first().internalFormat() == GL_SRGB8_ALPHA8; + + // Upload geometry for the down and upsample iterations + GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer(); + vbo->reset(); + + uploadGeometry(vbo, expandedBlurRegion.translated(xTranslate, yTranslate), shape); + vbo->bindArrays(); + + const QRect sourceRect = expandedBlurRegion.boundingRect() & screen; + const QRect destRect = sourceRect.translated(xTranslate, yTranslate); + + GLRenderTarget::pushRenderTargets(m_renderTargetStack); + int blurRectCount = expandedBlurRegion.rectCount() * 6; + + /* + * If the window is a dock or panel we avoid the "extended blur" effect. + * Extended blur is when windows that are not under the blurred area affect + * the final blur result. + * We want to avoid this on panels, because it looks really weird and ugly + * when maximized windows or windows near the panel affect the dock blur. + */ + if (isDock) { + m_renderTargets.last()->blitFromFramebuffer(sourceRect, destRect); + + if (useSRGB) { + glEnable(GL_FRAMEBUFFER_SRGB); + } + + copyScreenSampleTexture(vbo, blurRectCount, shape.translated(xTranslate, yTranslate), screenProjection); + } else { + m_renderTargets.first()->blitFromFramebuffer(sourceRect, destRect); + + if (useSRGB) { + glEnable(GL_FRAMEBUFFER_SRGB); + } + + // Remove the m_renderTargets[0] from the top of the stack that we will not use + GLRenderTarget::popRenderTarget(); + } + + downSampleTexture(vbo, blurRectCount); + upSampleTexture(vbo, blurRectCount); + + // Modulate the blurred texture with the window opacity if the window isn't opaque + if (opacity < 1.0) { + glEnable(GL_BLEND); +#if 1 // bow shape, always above y = x + float o = 1.0f-opacity; + o = 1.0f - o*o; +#else // sigmoid shape, above y = x for x > 0.5, below y = x for x < 0.5 + float o = 2.0f*opacity - 1.0f; + o = 0.5f + o / (1.0f + qAbs(o)); +#endif + glBlendColor(0, 0, 0, o); + glBlendFunc(GL_CONSTANT_ALPHA, GL_ONE_MINUS_CONSTANT_ALPHA); + } + + upscaleRenderToScreen(vbo, blurRectCount * (m_downSampleIterations + 1), shape.rectCount() * 6, screenProjection, windowRect.topLeft()); + + if (useSRGB) { + glDisable(GL_FRAMEBUFFER_SRGB); + } + + if (opacity < 1.0) { + glDisable(GL_BLEND); + } + + vbo->unbindArrays(); +} + +void BlurEffect::upscaleRenderToScreen(GLVertexBuffer *vbo, int vboStart, int blurRectCount, QMatrix4x4 screenProjection, QPoint windowPosition) +{ + glActiveTexture(GL_TEXTURE0); + m_renderTextures[1].bind(); + + if (m_noiseStrength > 0) { + m_shader->bind(BlurShader::NoiseSampleType); + m_shader->setTargetTextureSize(m_renderTextures[0].size() * GLRenderTarget::virtualScreenScale()); + m_shader->setNoiseTextureSize(m_noiseTexture.size() * GLRenderTarget::virtualScreenScale()); + m_shader->setTexturePosition(windowPosition * GLRenderTarget::virtualScreenScale()); + + glActiveTexture(GL_TEXTURE1); + m_noiseTexture.bind(); + } else { + m_shader->bind(BlurShader::UpSampleType); + m_shader->setTargetTextureSize(m_renderTextures[0].size() * GLRenderTarget::virtualScreenScale()); + } + + m_shader->setOffset(m_offset); + m_shader->setModelViewProjectionMatrix(screenProjection); + + //Render to the screen + vbo->draw(GL_TRIANGLES, vboStart, blurRectCount); + + glActiveTexture(GL_TEXTURE0); + m_shader->unbind(); +} + +void BlurEffect::downSampleTexture(GLVertexBuffer *vbo, int blurRectCount) +{ + QMatrix4x4 modelViewProjectionMatrix; + + m_shader->bind(BlurShader::DownSampleType); + m_shader->setOffset(m_offset); + + for (int i = 1; i <= m_downSampleIterations; i++) { + modelViewProjectionMatrix.setToIdentity(); + modelViewProjectionMatrix.ortho(0, m_renderTextures[i].width(), m_renderTextures[i].height(), 0 , 0, 65535); + + m_shader->setModelViewProjectionMatrix(modelViewProjectionMatrix); + m_shader->setTargetTextureSize(m_renderTextures[i].size()); + + //Copy the image from this texture + m_renderTextures[i - 1].bind(); + + vbo->draw(GL_TRIANGLES, blurRectCount * i, blurRectCount); + GLRenderTarget::popRenderTarget(); + } + + m_shader->unbind(); +} + +void BlurEffect::upSampleTexture(GLVertexBuffer *vbo, int blurRectCount) +{ + QMatrix4x4 modelViewProjectionMatrix; + + m_shader->bind(BlurShader::UpSampleType); + m_shader->setOffset(m_offset); + + for (int i = m_downSampleIterations - 1; i >= 1; i--) { + modelViewProjectionMatrix.setToIdentity(); + modelViewProjectionMatrix.ortho(0, m_renderTextures[i].width(), m_renderTextures[i].height(), 0 , 0, 65535); + + m_shader->setModelViewProjectionMatrix(modelViewProjectionMatrix); + m_shader->setTargetTextureSize(m_renderTextures[i].size()); + + //Copy the image from this texture + m_renderTextures[i + 1].bind(); + + vbo->draw(GL_TRIANGLES, blurRectCount * i, blurRectCount); + GLRenderTarget::popRenderTarget(); + } + + m_shader->unbind(); +} + +void BlurEffect::copyScreenSampleTexture(GLVertexBuffer *vbo, int blurRectCount, QRegion blurShape, QMatrix4x4 screenProjection) +{ + m_shader->bind(BlurShader::CopySampleType); + + m_shader->setModelViewProjectionMatrix(screenProjection); + m_shader->setTargetTextureSize(effects->virtualScreenSize()); + + /* + * This '1' sized adjustment is necessary do avoid windows affecting the blur that are + * right next to this window. + */ + m_shader->setBlurRect(blurShape.boundingRect().adjusted(1, 1, -1, -1), effects->virtualScreenSize()); + m_renderTextures.last().bind(); + + vbo->draw(GL_TRIANGLES, 0, blurRectCount); + GLRenderTarget::popRenderTarget(); + + m_shader->unbind(); +} + +bool BlurEffect::isActive() const +{ + return !effects->isScreenLocked(); +} + +} // namespace KWin + diff --git a/effects/blur/blur.h b/effects/blur/blur.h new file mode 100644 index 0000000..fdf5f13 --- /dev/null +++ b/effects/blur/blur.h @@ -0,0 +1,134 @@ +/* + SPDX-FileCopyrightText: 2010 Fredrik Höglund + SPDX-FileCopyrightText: 2018 Alex Nemeth + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef BLUR_H +#define BLUR_H + +#include +#include +#include + +#include +#include +#include + +namespace KWaylandServer +{ +class BlurManagerInterface; +} + +namespace KWin +{ + +static const int borderSize = 5; + +class BlurShader; + +class BlurEffect : public KWin::Effect +{ + Q_OBJECT + +public: + BlurEffect(); + ~BlurEffect() override; + + static bool supported(); + static bool enabledByDefault(); + + void reconfigure(ReconfigureFlags flags) override; + void prePaintScreen(ScreenPrePaintData &data, int time) override; + void prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time) override; + void drawWindow(EffectWindow *w, int mask, const QRegion ®ion, WindowPaintData &data) override; + void paintEffectFrame(EffectFrame *frame, const QRegion ®ion, double opacity, double frameOpacity) override; + + bool provides(Feature feature) override; + bool isActive() const override; + + int requestedEffectChainPosition() const override { + return 75; + } + + bool eventFilter(QObject *watched, QEvent *event) override; + +public Q_SLOTS: + void slotWindowAdded(KWin::EffectWindow *w); + void slotWindowDeleted(KWin::EffectWindow *w); + void slotPropertyNotify(KWin::EffectWindow *w, long atom); + void slotScreenGeometryChanged(); + +private: + QRect expand(const QRect &rect) const; + QRegion expand(const QRegion ®ion) const; + bool renderTargetsValid() const; + void deleteFBOs(); + void initBlurStrengthValues(); + void updateTexture(); + QRegion blurRegion(const EffectWindow *w) const; + bool shouldBlur(const EffectWindow *w, int mask, const WindowPaintData &data) const; + void updateBlurRegion(EffectWindow *w) const; + void doBlur(const QRegion &shape, const QRect &screen, const float opacity, const QMatrix4x4 &screenProjection, bool isDock, QRect windowRect); + void uploadRegion(QVector2D *&map, const QRegion ®ion, const int downSampleIterations); + void uploadGeometry(GLVertexBuffer *vbo, const QRegion &blurRegion, const QRegion &windowRegion); + void generateNoiseTexture(); + + void upscaleRenderToScreen(GLVertexBuffer *vbo, int vboStart, int blurRectCount, QMatrix4x4 screenProjection, QPoint windowPosition); + void downSampleTexture(GLVertexBuffer *vbo, int blurRectCount); + void upSampleTexture(GLVertexBuffer *vbo, int blurRectCount); + void copyScreenSampleTexture(GLVertexBuffer *vbo, int blurRectCount, QRegion blurShape, QMatrix4x4 screenProjection); + +private: + BlurShader *m_shader; + QVector m_renderTargets; + QVector m_renderTextures; + QStack m_renderTargetStack; + + GLTexture m_noiseTexture; + + bool m_renderTargetsValid; + long net_wm_blur_region; + QRegion m_paintedArea; // keeps track of all painted areas (from bottom to top) + QRegion m_currentBlur; // keeps track of the currently blured area of the windows(from bottom to top) + + int m_downSampleIterations; // number of times the texture will be downsized to half size + int m_offset; + int m_expandSize; + int m_noiseStrength; + int m_scalingFactor; + + struct OffsetStruct { + float minOffset; + float maxOffset; + int expandSize; + }; + + QVector blurOffsets; + + struct BlurValuesStruct { + int iteration; + float offset; + }; + + QVector blurStrengthValues; + + QMap windowBlurChangedConnections; + KWaylandServer::BlurManagerInterface *m_blurManager = nullptr; +}; + +inline +bool BlurEffect::provides(Effect::Feature feature) +{ + if (feature == Blur) { + return true; + } + return KWin::Effect::provides(feature); +} + + +} // namespace KWin + +#endif + diff --git a/effects/blur/blur.kcfg b/effects/blur/blur.kcfg new file mode 100644 index 0000000..8c21540 --- /dev/null +++ b/effects/blur/blur.kcfg @@ -0,0 +1,15 @@ + + + + + + 10 + + + 5 + + + diff --git a/effects/blur/blur_config.cpp b/effects/blur/blur_config.cpp new file mode 100644 index 0000000..573b482 --- /dev/null +++ b/effects/blur/blur_config.cpp @@ -0,0 +1,49 @@ +/* + SPDX-FileCopyrightText: 2010 Fredrik Höglund + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "blur_config.h" +// KConfigSkeleton +#include "blurconfig.h" +#include + +#include +#include +#include + +K_PLUGIN_FACTORY_WITH_JSON(BlurEffectConfigFactory, + "blur_config.json", + registerPlugin();) + +namespace KWin +{ + +BlurEffectConfig::BlurEffectConfig(QWidget *parent, const QVariantList &args) + : KCModule(KAboutData::pluginData(QStringLiteral("blur")), parent, args) +{ + ui.setupUi(this); + BlurConfig::instance(KWIN_CONFIG); + addConfig(BlurConfig::self(), this); + + load(); +} + +BlurEffectConfig::~BlurEffectConfig() +{ +} + +void BlurEffectConfig::save() +{ + KCModule::save(); + + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("blur")); +} + +} // namespace KWin + +#include "blur_config.moc" diff --git a/effects/blur/blur_config.desktop b/effects/blur/blur_config.desktop new file mode 100644 index 0000000..1d19a8b --- /dev/null +++ b/effects/blur/blur_config.desktop @@ -0,0 +1,89 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=KCModule + +X-KDE-Library=kwin_blur_config +X-KDE-ParentComponents=blur + +Name=Blur +Name[af]=Blur +Name[ar]=غشاوة +Name[az]=Yayğınlıq +Name[be]=Blur +Name[bg]=Замъгляване +Name[bn]=ব্লার +Name[bn_IN]=Blur (ব্লার) +Name[bs]=Zamućenje +Name[ca]=Difuminat +Name[ca@valencia]=Difumina +Name[cs]=Rozostření +Name[csb]=Rozmazóné +Name[da]=Slør +Name[de]=Verwischen +Name[el]=Θόλωμα +Name[en_GB]=Blur +Name[eo]=Malklarigi +Name[es]=Desenfocar +Name[et]=Hägu +Name[eu]=Lausotu +Name[fa]=محو +Name[fi]=Sumennus +Name[fr]=Flou +Name[fy]=Ferfagje +Name[ga]=Blur +Name[gl]=Desenfocar +Name[gu]=ઝાંખું +Name[he]=טשטוש +Name[hi]=धुंधला करें +Name[hne]=धुंधला करव +Name[hr]=Mrlja +Name[hsb]=Młowojty +Name[hu]=Elmosódás +Name[ia]=Obscura (Blur) +Name[id]=Buram +Name[is]=Móða +Name[it]=Sfocatura +Name[ja]=ぼかし +Name[kk]=Бұлдыр +Name[km]=ព្រិល​ +Name[kn]=ಮಾಸಲುಗೊಳಿಸು (ಬ್ಲರ್) +Name[ko]=흐리게 +Name[ku]=Blur +Name[lt]=Suliejimas +Name[lv]=Aizmiglot +Name[mai]=धुंधला करू +Name[mk]=Заматување +Name[ml]=മങ്ങിയതാക്കുക +Name[mr]=पुसट +Name[nb]=Slør +Name[nds]=Verwischen +Name[ne]=धब्बा +Name[nl]=Vervagen +Name[nn]=Uklar +Name[pa]=ਧੁੰਦਲਾ +Name[pl]=Rozmycie +Name[pt]=BlueFish +Name[pt_BR]=Borrar +Name[ro]=Estompare +Name[ru]=Размытие +Name[se]=Seagas +Name[si]=අපැහැදිලි කිරීම +Name[sk]=RozmazaÅ¥ +Name[sl]=ZabriÅ¡i +Name[sr]=Замућење +Name[sr@ijekavian]=Замућење +Name[sr@ijekavianlatin]=Zamućenje +Name[sr@latin]=Zamućenje +Name[sv]=Oskärpa +Name[ta]=மங்கலாக +Name[te]=బ్లర్ +Name[th]=ทำให้ไม่ชัดเจน +Name[tr]=Bulanıklaştırma +Name[ug]=گۇڭگا +Name[uk]=Розмивання +Name[vi]=Nhoè +Name[wa]=Flou +Name[x-test]=xxBlurxx +Name[zh_CN]=模糊 +Name[zh_TW]=模糊 + diff --git a/effects/blur/blur_config.h b/effects/blur/blur_config.h new file mode 100644 index 0000000..45a7ddc --- /dev/null +++ b/effects/blur/blur_config.h @@ -0,0 +1,33 @@ +/* + SPDX-FileCopyrightText: 2010 Fredrik Höglund + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef BLUR_CONFIG_H +#define BLUR_CONFIG_H + +#include +#include "ui_blur_config.h" + +namespace KWin +{ + +class BlurEffectConfig : public KCModule +{ + Q_OBJECT + +public: + explicit BlurEffectConfig(QWidget *parent = nullptr, const QVariantList& args = QVariantList()); + ~BlurEffectConfig() override; + + void save() override; + +private: + ::Ui::BlurEffectConfig ui; +}; + +} // namespace KWin + +#endif + diff --git a/effects/blur/blur_config.ui b/effects/blur/blur_config.ui new file mode 100644 index 0000000..283fff7 --- /dev/null +++ b/effects/blur/blur_config.ui @@ -0,0 +1,160 @@ + + + BlurEffectConfig + + + + 0 + 0 + 480 + 184 + + + + + + + Blur strength: + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + Light + + + + + + + 1 + + + 15 + + + 1 + + + 1 + + + 10 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + + + + + Strong + + + + + + + + + Noise strength: + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + Light + + + + + + + 14 + + + 5 + + + 5 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + 1 + + + + + + + Strong + + + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + + diff --git a/effects/blur/blurconfig.kcfgc b/effects/blur/blurconfig.kcfgc new file mode 100644 index 0000000..4fb3fe5 --- /dev/null +++ b/effects/blur/blurconfig.kcfgc @@ -0,0 +1,5 @@ +File=blur.kcfg +ClassName=BlurConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/effects/blur/blurshader.cpp b/effects/blur/blurshader.cpp new file mode 100644 index 0000000..fc9fc77 --- /dev/null +++ b/effects/blur/blurshader.cpp @@ -0,0 +1,432 @@ +/* + SPDX-FileCopyrightText: 2010 Fredrik Höglund + SPDX-FileCopyrightText: 2018 Alex Nemeth + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "blurshader.h" + +#include +#include +#include + +#include +#include + +namespace KWin +{ + +BlurShader::BlurShader(QObject *parent) + : QObject(parent) +{ + const bool gles = GLPlatform::instance()->isGLES(); + const bool glsl_140 = !gles && GLPlatform::instance()->glslVersion() >= kVersionNumber(1, 40); + const bool core = glsl_140 || (gles && GLPlatform::instance()->glslVersion() >= kVersionNumber(3, 0)); + + QByteArray vertexSource; + QByteArray fragmentDownSource; + QByteArray fragmentUpSource; + QByteArray fragmentCopySource; + QByteArray fragmentNoiseSource; + + const QByteArray attribute = core ? "in" : "attribute"; + const QByteArray texture2D = core ? "texture" : "texture2D"; + const QByteArray fragColor = core ? "fragColor" : "gl_FragColor"; + + QString glHeaderString; + + if (gles) { + if (core) { + glHeaderString += "#version 300 es\n\n"; + } + + glHeaderString += "precision highp float;\n"; + } else if (glsl_140) { + glHeaderString += "#version 140\n\n"; + } + + QString glUniformString = "uniform sampler2D texUnit;\n" + "uniform float offset;\n" + "uniform vec2 renderTextureSize;\n" + "uniform vec2 halfpixel;\n"; + + if (core) { + glUniformString += "out vec4 fragColor;\n\n"; + } + + // Vertex shader + QTextStream streamVert(&vertexSource); + + streamVert << glHeaderString; + + streamVert << "uniform mat4 modelViewProjectionMatrix;\n"; + streamVert << attribute << " vec4 vertex;\n\n"; + streamVert << "\n"; + streamVert << "void main(void)\n"; + streamVert << "{\n"; + streamVert << " gl_Position = modelViewProjectionMatrix * vertex;\n"; + streamVert << "}\n"; + + streamVert.flush(); + + // Fragment shader (Dual Kawase Blur) - Downsample + QTextStream streamFragDown(&fragmentDownSource); + + streamFragDown << glHeaderString << glUniformString; + + streamFragDown << "void main(void)\n"; + streamFragDown << "{\n"; + streamFragDown << " vec2 uv = vec2(gl_FragCoord.xy / renderTextureSize);\n"; + streamFragDown << " \n"; + streamFragDown << " vec4 sum = " << texture2D << "(texUnit, uv) * 4.0;\n"; + streamFragDown << " sum += " << texture2D << "(texUnit, uv - halfpixel.xy * offset);\n"; + streamFragDown << " sum += " << texture2D << "(texUnit, uv + halfpixel.xy * offset);\n"; + streamFragDown << " sum += " << texture2D << "(texUnit, uv + vec2(halfpixel.x, -halfpixel.y) * offset);\n"; + streamFragDown << " sum += " << texture2D << "(texUnit, uv - vec2(halfpixel.x, -halfpixel.y) * offset);\n"; + streamFragDown << " \n"; + streamFragDown << " " << fragColor << " = sum / 8.0;\n"; + streamFragDown << "}\n"; + + streamFragDown.flush(); + + // Fragment shader (Dual Kawase Blur) - Upsample + QTextStream streamFragUp(&fragmentUpSource); + + streamFragUp << glHeaderString << glUniformString; + + streamFragUp << "void main(void)\n"; + streamFragUp << "{\n"; + streamFragUp << " vec2 uv = vec2(gl_FragCoord.xy / renderTextureSize);\n"; + streamFragUp << " \n"; + streamFragUp << " vec4 sum = " << texture2D << "(texUnit, uv + vec2(-halfpixel.x * 2.0, 0.0) * offset);\n"; + streamFragUp << " sum += " << texture2D << "(texUnit, uv + vec2(-halfpixel.x, halfpixel.y) * offset) * 2.0;\n"; + streamFragUp << " sum += " << texture2D << "(texUnit, uv + vec2(0.0, halfpixel.y * 2.0) * offset);\n"; + streamFragUp << " sum += " << texture2D << "(texUnit, uv + vec2(halfpixel.x, halfpixel.y) * offset) * 2.0;\n"; + streamFragUp << " sum += " << texture2D << "(texUnit, uv + vec2(halfpixel.x * 2.0, 0.0) * offset);\n"; + streamFragUp << " sum += " << texture2D << "(texUnit, uv + vec2(halfpixel.x, -halfpixel.y) * offset) * 2.0;\n"; + streamFragUp << " sum += " << texture2D << "(texUnit, uv + vec2(0.0, -halfpixel.y * 2.0) * offset);\n"; + streamFragUp << " sum += " << texture2D << "(texUnit, uv + vec2(-halfpixel.x, -halfpixel.y) * offset) * 2.0;\n"; + streamFragUp << " \n"; + streamFragUp << " " << fragColor << " = sum / 12.0;\n"; + streamFragUp << "}\n"; + + streamFragUp.flush(); + + // Fragment shader - Copy texture + QTextStream streamFragCopy(&fragmentCopySource); + + streamFragCopy << glHeaderString; + + streamFragCopy << "uniform sampler2D texUnit;\n"; + streamFragCopy << "uniform vec2 renderTextureSize;\n"; + streamFragCopy << "uniform vec4 blurRect;\n"; + + if (core) { + streamFragCopy << "out vec4 fragColor;\n\n"; + } + + streamFragCopy << "void main(void)\n"; + streamFragCopy << "{\n"; + streamFragCopy << " vec2 uv = vec2(gl_FragCoord.xy / renderTextureSize);\n"; + streamFragCopy << " " << fragColor << " = " << texture2D << "(texUnit, clamp(uv, blurRect.xy, blurRect.zw));\n"; + streamFragCopy << "}\n"; + + streamFragCopy.flush(); + + // Fragment shader - Noise texture + QTextStream streamFragNoise(&fragmentNoiseSource); + + streamFragNoise << glHeaderString << glUniformString; + + streamFragNoise << "uniform sampler2D noiseTexUnit;\n"; + streamFragNoise << "uniform vec2 noiseTextureSize;\n"; + streamFragNoise << "uniform vec2 texStartPos;\n"; + + // Upsampling + Noise + streamFragNoise << "void main(void)\n"; + streamFragNoise << "{\n"; + streamFragNoise << " vec2 uv = vec2(gl_FragCoord.xy / renderTextureSize);\n"; + streamFragNoise << " vec2 uvNoise = vec2((texStartPos.xy + gl_FragCoord.xy) / noiseTextureSize);\n"; + streamFragNoise << " \n"; + streamFragNoise << " vec4 sum = " << texture2D << "(texUnit, uv + vec2(-halfpixel.x * 2.0, 0.0) * offset);\n"; + streamFragNoise << " sum += " << texture2D << "(texUnit, uv + vec2(-halfpixel.x, halfpixel.y) * offset) * 2.0;\n"; + streamFragNoise << " sum += " << texture2D << "(texUnit, uv + vec2(0.0, halfpixel.y * 2.0) * offset);\n"; + streamFragNoise << " sum += " << texture2D << "(texUnit, uv + vec2(halfpixel.x, halfpixel.y) * offset) * 2.0;\n"; + streamFragNoise << " sum += " << texture2D << "(texUnit, uv + vec2(halfpixel.x * 2.0, 0.0) * offset);\n"; + streamFragNoise << " sum += " << texture2D << "(texUnit, uv + vec2(halfpixel.x, -halfpixel.y) * offset) * 2.0;\n"; + streamFragNoise << " sum += " << texture2D << "(texUnit, uv + vec2(0.0, -halfpixel.y * 2.0) * offset);\n"; + streamFragNoise << " sum += " << texture2D << "(texUnit, uv + vec2(-halfpixel.x, -halfpixel.y) * offset) * 2.0;\n"; + streamFragNoise << " \n"; + streamFragNoise << " " << fragColor << " = sum / 12.0 - (vec4(0.5, 0.5, 0.5, 0) - vec4(" << texture2D << "(noiseTexUnit, uvNoise).rrr, 0));\n"; + streamFragNoise << "}\n"; + + streamFragNoise.flush(); + + m_shaderDownsample.reset(ShaderManager::instance()->loadShaderFromCode(vertexSource, fragmentDownSource)); + m_shaderUpsample.reset(ShaderManager::instance()->loadShaderFromCode(vertexSource, fragmentUpSource)); + m_shaderCopysample.reset(ShaderManager::instance()->loadShaderFromCode(vertexSource, fragmentCopySource)); + m_shaderNoisesample.reset(ShaderManager::instance()->loadShaderFromCode(vertexSource, fragmentNoiseSource)); + + m_valid = m_shaderDownsample->isValid() && + m_shaderUpsample->isValid() && + m_shaderCopysample->isValid() && + m_shaderNoisesample->isValid(); + + if (m_valid) { + m_mvpMatrixLocationDownsample = m_shaderDownsample->uniformLocation("modelViewProjectionMatrix"); + m_offsetLocationDownsample = m_shaderDownsample->uniformLocation("offset"); + m_renderTextureSizeLocationDownsample = m_shaderDownsample->uniformLocation("renderTextureSize"); + m_halfpixelLocationDownsample = m_shaderDownsample->uniformLocation("halfpixel"); + + m_mvpMatrixLocationUpsample = m_shaderUpsample->uniformLocation("modelViewProjectionMatrix"); + m_offsetLocationUpsample = m_shaderUpsample->uniformLocation("offset"); + m_renderTextureSizeLocationUpsample = m_shaderUpsample->uniformLocation("renderTextureSize"); + m_halfpixelLocationUpsample = m_shaderUpsample->uniformLocation("halfpixel"); + + m_mvpMatrixLocationCopysample = m_shaderCopysample->uniformLocation("modelViewProjectionMatrix"); + m_renderTextureSizeLocationCopysample = m_shaderCopysample->uniformLocation("renderTextureSize"); + m_blurRectLocationCopysample = m_shaderCopysample->uniformLocation("blurRect"); + + m_mvpMatrixLocationNoisesample = m_shaderNoisesample->uniformLocation("modelViewProjectionMatrix"); + m_offsetLocationNoisesample = m_shaderNoisesample->uniformLocation("offset"); + m_renderTextureSizeLocationNoisesample = m_shaderNoisesample->uniformLocation("renderTextureSize"); + m_noiseTextureSizeLocationNoisesample = m_shaderNoisesample->uniformLocation("noiseTextureSize"); + m_texStartPosLocationNoisesample = m_shaderNoisesample->uniformLocation("texStartPos"); + m_halfpixelLocationNoisesample = m_shaderNoisesample->uniformLocation("halfpixel"); + + QMatrix4x4 modelViewProjection; + const QSize screenSize = effects->virtualScreenSize(); + modelViewProjection.ortho(0, screenSize.width(), screenSize.height(), 0, 0, 65535); + + //Add default values to the uniforms of the shaders + ShaderManager::instance()->pushShader(m_shaderDownsample.data()); + m_shaderDownsample->setUniform(m_mvpMatrixLocationDownsample, modelViewProjection); + m_shaderDownsample->setUniform(m_offsetLocationDownsample, float(1.0)); + m_shaderDownsample->setUniform(m_renderTextureSizeLocationDownsample, QVector2D(1.0, 1.0)); + m_shaderDownsample->setUniform(m_halfpixelLocationDownsample, QVector2D(1.0, 1.0)); + ShaderManager::instance()->popShader(); + + ShaderManager::instance()->pushShader(m_shaderUpsample.data()); + m_shaderUpsample->setUniform(m_mvpMatrixLocationUpsample, modelViewProjection); + m_shaderUpsample->setUniform(m_offsetLocationUpsample, float(1.0)); + m_shaderUpsample->setUniform(m_renderTextureSizeLocationUpsample, QVector2D(1.0, 1.0)); + m_shaderUpsample->setUniform(m_halfpixelLocationUpsample, QVector2D(1.0, 1.0)); + ShaderManager::instance()->popShader(); + + ShaderManager::instance()->pushShader(m_shaderCopysample.data()); + m_shaderCopysample->setUniform(m_mvpMatrixLocationCopysample, modelViewProjection); + m_shaderCopysample->setUniform(m_renderTextureSizeLocationCopysample, QVector2D(1.0, 1.0)); + m_shaderCopysample->setUniform(m_blurRectLocationCopysample, QVector4D(1.0, 1.0, 1.0, 1.0)); + ShaderManager::instance()->popShader(); + + ShaderManager::instance()->pushShader(m_shaderNoisesample.data()); + m_shaderNoisesample->setUniform(m_mvpMatrixLocationNoisesample, modelViewProjection); + m_shaderNoisesample->setUniform(m_offsetLocationNoisesample, float(1.0)); + m_shaderNoisesample->setUniform(m_renderTextureSizeLocationNoisesample, QVector2D(1.0, 1.0)); + m_shaderNoisesample->setUniform(m_noiseTextureSizeLocationNoisesample, QVector2D(1.0, 1.0)); + m_shaderNoisesample->setUniform(m_texStartPosLocationNoisesample, QVector2D(1.0, 1.0)); + m_shaderNoisesample->setUniform(m_halfpixelLocationNoisesample, QVector2D(1.0, 1.0)); + + glUniform1i(m_shaderNoisesample->uniformLocation("texUnit"), 0); + glUniform1i(m_shaderNoisesample->uniformLocation("noiseTexUnit"), 1); + + ShaderManager::instance()->popShader(); + } +} + +BlurShader::~BlurShader() +{ +} + +void BlurShader::setModelViewProjectionMatrix(const QMatrix4x4 &matrix) +{ + if (!isValid()) { + return; + } + + switch (m_activeSampleType) { + case CopySampleType: + if (matrix == m_matrixCopysample) { + return; + } + + m_matrixCopysample = matrix; + m_shaderCopysample->setUniform(m_mvpMatrixLocationCopysample, matrix); + break; + + case UpSampleType: + if (matrix == m_matrixUpsample) { + return; + } + + m_matrixUpsample = matrix; + m_shaderUpsample->setUniform(m_mvpMatrixLocationUpsample, matrix); + break; + + case DownSampleType: + if (matrix == m_matrixDownsample) { + return; + } + + m_matrixDownsample = matrix; + m_shaderDownsample->setUniform(m_mvpMatrixLocationDownsample, matrix); + break; + + case NoiseSampleType: + if (matrix == m_matrixNoisesample) { + return; + } + + m_matrixNoisesample = matrix; + m_shaderNoisesample->setUniform(m_mvpMatrixLocationNoisesample, matrix); + break; + + default: + Q_UNREACHABLE(); + break; + } +} + +void BlurShader::setOffset(float offset) +{ + if (!isValid()) { + return; + } + + switch (m_activeSampleType) { + case UpSampleType: + if (offset == m_offsetUpsample) { + return; + } + + m_offsetUpsample = offset; + m_shaderUpsample->setUniform(m_offsetLocationUpsample, offset); + break; + + case DownSampleType: + if (offset == m_offsetDownsample) { + return; + } + + m_offsetDownsample = offset; + m_shaderDownsample->setUniform(m_offsetLocationDownsample, offset); + break; + + case NoiseSampleType: + if (offset == m_offsetNoisesample) { + return; + } + + m_offsetNoisesample = offset; + m_shaderNoisesample->setUniform(m_offsetLocationNoisesample, offset); + break; + + default: + Q_UNREACHABLE(); + break; + } +} + +void BlurShader::setTargetTextureSize(const QSize &renderTextureSize) +{ + if (!isValid()) { + return; + } + + const QVector2D texSize(renderTextureSize.width(), renderTextureSize.height()); + + switch (m_activeSampleType) { + case CopySampleType: + m_shaderCopysample->setUniform(m_renderTextureSizeLocationCopysample, texSize); + break; + + case UpSampleType: + m_shaderUpsample->setUniform(m_renderTextureSizeLocationUpsample, texSize); + m_shaderUpsample->setUniform(m_halfpixelLocationUpsample, QVector2D(0.5 / texSize.x(), 0.5 / texSize.y())); + break; + + case DownSampleType: + m_shaderDownsample->setUniform(m_renderTextureSizeLocationDownsample, texSize); + m_shaderDownsample->setUniform(m_halfpixelLocationDownsample, QVector2D(0.5 / texSize.x(), 0.5 / texSize.y())); + break; + + case NoiseSampleType: + m_shaderNoisesample->setUniform(m_renderTextureSizeLocationNoisesample, texSize); + m_shaderNoisesample->setUniform(m_halfpixelLocationNoisesample, QVector2D(0.5 / texSize.x(), 0.5 / texSize.y())); + break; + + default: + Q_UNREACHABLE(); + break; + } +} + +void BlurShader::setNoiseTextureSize(const QSize &noiseTextureSize) +{ + const QVector2D noiseTexSize(noiseTextureSize.width(), noiseTextureSize.height()); + + if (noiseTexSize != m_noiseTextureSizeNoisesample) { + m_noiseTextureSizeNoisesample = noiseTexSize; + m_shaderNoisesample->setUniform(m_noiseTextureSizeLocationNoisesample, noiseTexSize); + } +} + +void BlurShader::setTexturePosition(const QPoint &texPos) +{ + m_shaderNoisesample->setUniform(m_texStartPosLocationNoisesample, QVector2D(-texPos.x(), texPos.y())); +} + +void BlurShader::setBlurRect(const QRect &blurRect, const QSize &screenSize) +{ + if (!isValid()) { + return; + } + + const QVector4D rect( + blurRect.left() / float(screenSize.width()), + 1.0 - blurRect.bottom() / float(screenSize.height()), + blurRect.right() / float(screenSize.width()), + 1.0 - blurRect.top() / float(screenSize.height()) + ); + + m_shaderCopysample->setUniform(m_blurRectLocationCopysample, rect); +} + +void BlurShader::bind(SampleType sampleType) +{ + if (!isValid()) { + return; + } + + switch (sampleType) { + case CopySampleType: + ShaderManager::instance()->pushShader(m_shaderCopysample.data()); + break; + + case UpSampleType: + ShaderManager::instance()->pushShader(m_shaderUpsample.data()); + break; + + case DownSampleType: + ShaderManager::instance()->pushShader(m_shaderDownsample.data()); + break; + + case NoiseSampleType: + ShaderManager::instance()->pushShader(m_shaderNoisesample.data()); + break; + + default: + Q_UNREACHABLE(); + break; + } + + m_activeSampleType = sampleType; +} + +void BlurShader::unbind() +{ + ShaderManager::instance()->popShader(); +} + +} // namespace KWin diff --git a/effects/blur/blurshader.h b/effects/blur/blurshader.h new file mode 100644 index 0000000..3ecbb02 --- /dev/null +++ b/effects/blur/blurshader.h @@ -0,0 +1,103 @@ +/* + SPDX-FileCopyrightText: 2010 Fredrik Höglund + SPDX-FileCopyrightText: 2018 Alex Nemeth + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef BLURSHADER_H +#define BLURSHADER_H + +#include + +#include +#include +#include +#include +#include + +namespace KWin +{ + +class BlurShader : public QObject +{ + Q_OBJECT + +public: + BlurShader(QObject *parent = nullptr); + ~BlurShader() override; + + bool isValid() const; + + enum SampleType { + DownSampleType, + UpSampleType, + CopySampleType, + NoiseSampleType + }; + + void bind(SampleType sampleType); + void unbind(); + + void setModelViewProjectionMatrix(const QMatrix4x4 &matrix); + void setOffset(float offset); + void setTargetTextureSize(const QSize &renderTextureSize); + void setNoiseTextureSize(const QSize &noiseTextureSize); + void setTexturePosition(const QPoint &texPos); + void setBlurRect(const QRect &blurRect, const QSize &screenSize); + +private: + QScopedPointer m_shaderDownsample; + QScopedPointer m_shaderUpsample; + QScopedPointer m_shaderCopysample; + QScopedPointer m_shaderNoisesample; + + int m_mvpMatrixLocationDownsample; + int m_offsetLocationDownsample; + int m_renderTextureSizeLocationDownsample; + int m_halfpixelLocationDownsample; + + int m_mvpMatrixLocationUpsample; + int m_offsetLocationUpsample; + int m_renderTextureSizeLocationUpsample; + int m_halfpixelLocationUpsample; + + int m_mvpMatrixLocationCopysample; + int m_renderTextureSizeLocationCopysample; + int m_blurRectLocationCopysample; + + int m_mvpMatrixLocationNoisesample; + int m_offsetLocationNoisesample; + int m_renderTextureSizeLocationNoisesample; + int m_noiseTextureSizeLocationNoisesample; + int m_texStartPosLocationNoisesample; + int m_halfpixelLocationNoisesample; + + //Caching uniform values to aviod unnecessary setUniform calls + int m_activeSampleType = -1; + + float m_offsetDownsample = 0.0; + QMatrix4x4 m_matrixDownsample; + + float m_offsetUpsample = 0.0; + QMatrix4x4 m_matrixUpsample; + + QMatrix4x4 m_matrixCopysample; + + float m_offsetNoisesample = 0.0; + QVector2D m_noiseTextureSizeNoisesample; + QMatrix4x4 m_matrixNoisesample; + + bool m_valid = false; + + Q_DISABLE_COPY(BlurShader); +}; + +inline bool BlurShader::isValid() const +{ + return m_valid; +} + +} // namespace KWin + +#endif diff --git a/effects/colorpicker/colorpicker.cpp b/effects/colorpicker/colorpicker.cpp new file mode 100644 index 0000000..2a2f10a --- /dev/null +++ b/effects/colorpicker/colorpicker.cpp @@ -0,0 +1,120 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "colorpicker.h" +#include +#include +#include +#include +#include + +Q_DECLARE_METATYPE(QColor) + +QDBusArgument &operator<< (QDBusArgument &argument, const QColor &color) +{ + argument.beginStructure(); + argument << color.rgba(); + argument.endStructure(); + return argument; +} + +const QDBusArgument &operator>>(const QDBusArgument &argument, QColor &color) +{ + argument.beginStructure(); + QRgb rgba; + argument >> rgba; + argument.endStructure(); + color = QColor::fromRgba(rgba); + return argument; +} + +namespace KWin +{ + +bool ColorPickerEffect::supported() +{ + return effects->isOpenGLCompositing(); +} + +ColorPickerEffect::ColorPickerEffect() + : m_scheduledPosition(QPoint(-1, -1)) +{ + qDBusRegisterMetaType(); + QDBusConnection::sessionBus().registerObject(QStringLiteral("/ColorPicker"), this, QDBusConnection::ExportScriptableContents); +} + +ColorPickerEffect::~ColorPickerEffect() = default; + +void ColorPickerEffect::paintScreen(int mask, const QRegion ®ion, ScreenPaintData &data) +{ + m_cachedOutputGeometry = data.outputGeometry(); + effects->paintScreen(mask, region, data); +} + +void ColorPickerEffect::postPaintScreen() +{ + effects->postPaintScreen(); + + if (m_scheduledPosition != QPoint(-1, -1) && (m_cachedOutputGeometry.isEmpty() || m_cachedOutputGeometry.contains(m_scheduledPosition))) { + uint8_t data[3]; + const QRect geo = GLRenderTarget::virtualScreenGeometry(); + const QPoint screenPosition(m_scheduledPosition.x() - geo.x(), m_scheduledPosition.y() - geo.y()); + const QPoint texturePosition(screenPosition.x() * GLRenderTarget::virtualScreenScale(), (geo.height() - screenPosition.y()) * GLRenderTarget::virtualScreenScale()); + + glReadnPixels(texturePosition.x(), texturePosition.y(), 1, 1, GL_RGB, GL_UNSIGNED_BYTE, 3, data); + QDBusConnection::sessionBus().send(m_replyMessage.createReply(QColor(data[0], data[1], data[2]))); + m_picking = false; + m_scheduledPosition = QPoint(-1, -1); + } +} + +QColor ColorPickerEffect::pick() +{ + if (!calledFromDBus()) { + return QColor(); + } + if (m_picking) { + sendErrorReply(QDBusError::Failed, "Color picking is already in progress"); + return QColor(); + } + m_picking = true; + m_replyMessage = message(); + setDelayedReply(true); + showInfoMessage(); + effects->startInteractivePositionSelection( + [this] (const QPoint &p) { + hideInfoMessage(); + if (p == QPoint(-1, -1)) { + // error condition + QDBusConnection::sessionBus().send(m_replyMessage.createErrorReply(QStringLiteral("org.kde.kwin.ColorPicker.Error.Cancelled"), "Color picking got cancelled")); + m_picking = false; + } else { + m_scheduledPosition = p; + effects->addRepaintFull(); + } + } + ); + return QColor(); +} + +void ColorPickerEffect::showInfoMessage() +{ + effects->showOnScreenMessage(i18n("Select a position for color picking with left click or enter.\nEscape or right click to cancel."), QStringLiteral("color-picker")); +} + +void ColorPickerEffect::hideInfoMessage() +{ + effects->hideOnScreenMessage(); +} + +bool ColorPickerEffect::isActive() const +{ + return m_picking && ((m_scheduledPosition != QPoint(-1, -1))) && !effects->isScreenLocked(); +} + +} // namespace diff --git a/effects/colorpicker/colorpicker.h b/effects/colorpicker/colorpicker.h new file mode 100644 index 0000000..88ccd7b --- /dev/null +++ b/effects/colorpicker/colorpicker.h @@ -0,0 +1,54 @@ +/* + 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 KWIN_COLORPICKER_H +#define KWIN_COLORPICKER_H + +#include +#include +#include +#include +#include +#include + +namespace KWin +{ + +class ColorPickerEffect : public Effect, protected QDBusContext +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.kwin.ColorPicker") +public: + ColorPickerEffect(); + ~ColorPickerEffect() override; + void paintScreen(int mask, const QRegion ®ion, ScreenPaintData &data) override; + void postPaintScreen() override; + bool isActive() const override; + + int requestedEffectChainPosition() const override { + return 50; + } + + static bool supported(); + +public Q_SLOTS: + Q_SCRIPTABLE QColor pick(); + +private: + void showInfoMessage(); + void hideInfoMessage(); + + QDBusMessage m_replyMessage; + QRect m_cachedOutputGeometry; + QPoint m_scheduledPosition; + bool m_picking = false; +}; + +} // namespace + +#endif diff --git a/effects/coverswitch/CMakeLists.txt b/effects/coverswitch/CMakeLists.txt new file mode 100644 index 0000000..f344a0e --- /dev/null +++ b/effects/coverswitch/CMakeLists.txt @@ -0,0 +1,26 @@ +####################################### +# Effect + +####################################### +# Config +set(kwin_coverswitch_config_SRCS coverswitch_config.cpp) +ki18n_wrap_ui(kwin_coverswitch_config_SRCS coverswitch_config.ui) +kconfig_add_kcfg_files(kwin_coverswitch_config_SRCS coverswitchconfig.kcfgc) + +add_library(kwin_coverswitch_config MODULE ${kwin_coverswitch_config_SRCS}) + +target_link_libraries(kwin_coverswitch_config + KF5::ConfigWidgets + KF5::I18n + Qt5::DBus + KWinEffectsInterface +) + +kcoreaddons_desktop_to_json(kwin_coverswitch_config coverswitch_config.desktop SERVICE_TYPES kcmodule.desktop) + +install( + TARGETS + kwin_coverswitch_config + DESTINATION + ${PLUGIN_INSTALL_DIR}/kwin/effects/configs +) diff --git a/effects/coverswitch/coverswitch.cpp b/effects/coverswitch/coverswitch.cpp new file mode 100644 index 0000000..d92487e --- /dev/null +++ b/effects/coverswitch/coverswitch.cpp @@ -0,0 +1,994 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "coverswitch.h" +// KConfigSkeleton +#include "coverswitchconfig.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +namespace KWin +{ + +CoverSwitchEffect::CoverSwitchEffect() + : mActivated(0) + , angle(60.0) + , animation(false) + , start(false) + , stop(false) + , stopRequested(false) + , startRequested(false) + , zPosition(900.0) + , scaleFactor(0.0) + , direction(Left) + , selected_window(nullptr) + , captionFrame(nullptr) + , primaryTabBox(false) + , secondaryTabBox(false) +{ + initConfig(); + reconfigure(ReconfigureAll); + + // Caption frame + captionFont.setBold(true); + captionFont.setPointSize(captionFont.pointSize() * 2); + + if (effects->compositingType() == OpenGL2Compositing) { + m_reflectionShader = ShaderManager::instance()->generateShaderFromResources(ShaderTrait::MapTexture, QString(), QStringLiteral("coverswitch-reflection.glsl")); + } else { + m_reflectionShader = nullptr; + } + connect(effects, &EffectsHandler::windowClosed, this, &CoverSwitchEffect::slotWindowClosed); + connect(effects, &EffectsHandler::tabBoxAdded, this, &CoverSwitchEffect::slotTabBoxAdded); + connect(effects, &EffectsHandler::tabBoxClosed, this, &CoverSwitchEffect::slotTabBoxClosed); + connect(effects, &EffectsHandler::tabBoxUpdated, this, &CoverSwitchEffect::slotTabBoxUpdated); + connect(effects, &EffectsHandler::tabBoxKeyEvent, this, &CoverSwitchEffect::slotTabBoxKeyEvent); +} + +CoverSwitchEffect::~CoverSwitchEffect() +{ + delete captionFrame; + delete m_reflectionShader; +} + +bool CoverSwitchEffect::supported() +{ + return effects->isOpenGLCompositing() && effects->animationsSupported(); +} + +void CoverSwitchEffect::reconfigure(ReconfigureFlags) +{ + CoverSwitchConfig::self()->read(); + animationDuration = std::chrono::milliseconds( + animationTime(200)); + animateSwitch = CoverSwitchConfig::animateSwitch(); + animateStart = CoverSwitchConfig::animateStart(); + animateStop = CoverSwitchConfig::animateStop(); + reflection = CoverSwitchConfig::reflection(); + windowTitle = CoverSwitchConfig::windowTitle(); + zPosition = CoverSwitchConfig::zPosition(); + timeLine.setEasingCurve(QEasingCurve::InOutSine); + timeLine.setDuration(animationDuration); + + // Defined outside the ui + primaryTabBox = CoverSwitchConfig::tabBox(); + secondaryTabBox = CoverSwitchConfig::tabBoxAlternative(); + + QColor tmp = CoverSwitchConfig::mirrorFrontColor(); + mirrorColor[0][0] = tmp.redF(); + mirrorColor[0][1] = tmp.greenF(); + mirrorColor[0][2] = tmp.blueF(); + mirrorColor[0][3] = 1.0; + tmp = CoverSwitchConfig::mirrorRearColor(); + mirrorColor[1][0] = tmp.redF(); + mirrorColor[1][1] = tmp.greenF(); + mirrorColor[1][2] = tmp.blueF(); + mirrorColor[1][3] = -1.0; + +} + +void CoverSwitchEffect::prePaintScreen(ScreenPrePaintData& data, int time) +{ + if (mActivated || stop || stopRequested) { + data.mask |= Effect::PAINT_SCREEN_WITH_TRANSFORMED_WINDOWS; + if (animation || start || stop) { + timeLine.update(std::chrono::milliseconds(time)); + } + if (selected_window == nullptr) + abort(); + } + effects->prePaintScreen(data, time); +} + +void CoverSwitchEffect::paintScreen(int mask, const QRegion ®ion, ScreenPaintData& data) +{ + effects->paintScreen(mask, region, data); + + if (mActivated || stop || stopRequested) { + + QList< EffectWindow* > tempList = currentWindowList; + int index = tempList.indexOf(selected_window); + if (animation || start || stop) { + if (!start && !stop) { + if (direction == Right) + index++; + else + index--; + if (index < 0) + index = tempList.count() + index; + if (index >= tempList.count()) + index = index % tempList.count(); + } + foreach (Direction direction, scheduled_directions) { + if (direction == Right) + index++; + else + index--; + if (index < 0) + index = tempList.count() + index; + if (index >= tempList.count()) + index = index % tempList.count(); + } + } + int leftIndex = index - 1; + if (leftIndex < 0) + leftIndex = tempList.count() - 1; + int rightIndex = index + 1; + if (rightIndex == tempList.count()) + rightIndex = 0; + + EffectWindow* frontWindow = tempList[ index ]; + leftWindows.clear(); + rightWindows.clear(); + + bool evenWindows = (tempList.count() % 2 == 0) ? true : false; + int leftWindowCount = 0; + if (evenWindows) + leftWindowCount = tempList.count() / 2 - 1; + else + leftWindowCount = (tempList.count() - 1) / 2; + for (int i = 0; i < leftWindowCount; i++) { + int tempIndex = (leftIndex - i); + if (tempIndex < 0) + tempIndex = tempList.count() + tempIndex; + leftWindows.prepend(tempList[ tempIndex ]); + } + int rightWindowCount = 0; + if (evenWindows) + rightWindowCount = tempList.count() / 2; + else + rightWindowCount = (tempList.count() - 1) / 2; + for (int i = 0; i < rightWindowCount; i++) { + int tempIndex = (rightIndex + i) % tempList.count(); + rightWindows.prepend(tempList[ tempIndex ]); + } + + if (reflection) { + // no reflections during start and stop animation + // except when using a shader + if ((!start && !stop) || effects->compositingType() == OpenGL2Compositing) + paintScene(frontWindow, leftWindows, rightWindows, true); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + // we can use a huge scale factor (needed to calculate the rearground vertices) + // as we restrict with a PaintClipper painting on the current screen + float reflectionScaleFactor = 100000 * tan(60.0 * M_PI / 360.0f) / area.width(); + const float width = area.width(); + const float height = area.height(); + float vertices[] = { + -width * 0.5f, height, 0.0, + width * 0.5f, height, 0.0, + width*reflectionScaleFactor, height, -5000, + -width*reflectionScaleFactor, height, -5000 + }; + // foreground + if (start) { + mirrorColor[0][3] = timeLine.value(); + } else if (stop) { + mirrorColor[0][3] = 1.0 - timeLine.value(); + } else { + mirrorColor[0][3] = 1.0; + } + + int y = 0; + // have to adjust the y values to fit OpenGL + // in OpenGL y==0 is at bottom, in Qt at top + if (effects->numScreens() > 1) { + QRect fullArea = effects->clientArea(FullArea, 0, 1); + if (fullArea.height() != area.height()) { + if (area.y() == 0) + y = fullArea.height() - area.height(); + else + y = fullArea.height() - area.y() - area.height(); + } + } + // use scissor to restrict painting of the reflection plane to current screen + glScissor(area.x(), y, area.width(), area.height()); + glEnable(GL_SCISSOR_TEST); + + if (m_reflectionShader && m_reflectionShader->isValid()) { + ShaderManager::instance()->pushShader(m_reflectionShader); + QMatrix4x4 windowTransformation = data.projectionMatrix(); + windowTransformation.translate(area.x() + area.width() * 0.5f, 0.0, 0.0); + m_reflectionShader->setUniform(GLShader::ModelViewProjectionMatrix, windowTransformation); + m_reflectionShader->setUniform("u_frontColor", QVector4D(mirrorColor[0][0], mirrorColor[0][1], mirrorColor[0][2], mirrorColor[0][3])); + m_reflectionShader->setUniform("u_backColor", QVector4D(mirrorColor[1][0], mirrorColor[1][1], mirrorColor[1][2], mirrorColor[1][3])); + // TODO: make this one properly + QVector verts; + QVector texcoords; + verts.reserve(18); + texcoords.reserve(12); + texcoords << 1.0 << 0.0; + verts << vertices[6] << vertices[7] << vertices[8]; + texcoords << 1.0 << 0.0; + verts << vertices[9] << vertices[10] << vertices[11]; + texcoords << 0.0 << 0.0; + verts << vertices[0] << vertices[1] << vertices[2]; + texcoords << 0.0 << 0.0; + verts << vertices[0] << vertices[1] << vertices[2]; + texcoords << 0.0 << 0.0; + verts << vertices[3] << vertices[4] << vertices[5]; + texcoords << 1.0 << 0.0; + verts << vertices[6] << vertices[7] << vertices[8]; + GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer(); + vbo->reset(); + vbo->setData(6, 3, verts.data(), texcoords.data()); + vbo->render(GL_TRIANGLES); + + ShaderManager::instance()->popShader(); + } + glDisable(GL_SCISSOR_TEST); + glDisable(GL_BLEND); + } + paintScene(frontWindow, leftWindows, rightWindows); + + // Render the caption frame + if (windowTitle) { + double opacity = 1.0; + if (start) + opacity = timeLine.value(); + else if (stop) + opacity = 1.0 - timeLine.value(); + if (animation) + captionFrame->setCrossFadeProgress(timeLine.value()); + captionFrame->render(region, opacity); + } + } +} + +void CoverSwitchEffect::postPaintScreen() +{ + if ((mActivated && (animation || start)) || stop || stopRequested) { + if (timeLine.done()) { + timeLine.reset(); + if (stop) { + stop = false; + effects->setActiveFullScreenEffect(nullptr); + foreach (EffectWindow * window, referrencedWindows) { + window->unrefWindow(); + } + referrencedWindows.clear(); + currentWindowList.clear(); + if (startRequested) { + startRequested = false; + mActivated = true; + effects->refTabBox(); + currentWindowList = effects->currentTabBoxWindowList(); + if (animateStart) { + start = true; + } + } + } else if (!scheduled_directions.isEmpty()) { + direction = scheduled_directions.dequeue(); + if (start) { + animation = true; + start = false; + } + } else { + animation = false; + start = false; + if (stopRequested) { + stopRequested = false; + stop = true; + } + } + } + effects->addRepaintFull(); + } + effects->postPaintScreen(); +} + +void CoverSwitchEffect::paintScene(EffectWindow* frontWindow, const EffectWindowList& leftWindows, + const EffectWindowList& rightWindows, bool reflectedWindows) +{ + // LAYOUT + // one window in the front. Other windows left and right rotated + // for odd number of windows: left: (n-1)/2; front: 1; right: (n-1)/2 + // for even number of windows: left: n/2; front: 1; right: n/2 -1 + // + // ANIMATION + // forward (alt+tab) + // all left windows are moved to next position + // top most left window is rotated and moved to front window position + // front window is rotated and moved to next right window position + // right windows are moved to next position + // last right window becomes totally transparent in half the time + // appears transparent on left side and becomes totally opaque again + // backward (alt+shift+tab) same as forward but opposite direction + int width = area.width(); + int leftWindowCount = leftWindows.count(); + int rightWindowCount = rightWindows.count(); + + + // Problem during animation: a window which is painted after another window + // appears in front of the other + // so during animation the painting order has to be rearreanged + // paint sequence no animation: left, right, front + // paint sequence forward animation: right, front, left + + if (!animation) { + paintWindows(leftWindows, true, reflectedWindows); + paintWindows(rightWindows, false, reflectedWindows); + paintFrontWindow(frontWindow, width, leftWindowCount, rightWindowCount, reflectedWindows); + } else { + if (direction == Right) { + if (timeLine.value() < 0.5) { + // paint in normal way + paintWindows(leftWindows, true, reflectedWindows); + paintWindows(rightWindows, false, reflectedWindows); + paintFrontWindow(frontWindow, width, leftWindowCount, rightWindowCount, reflectedWindows); + } else { + paintWindows(rightWindows, false, reflectedWindows); + paintFrontWindow(frontWindow, width, leftWindowCount, rightWindowCount, reflectedWindows); + paintWindows(leftWindows, true, reflectedWindows, rightWindows.at(0)); + } + } else { + paintWindows(leftWindows, true, reflectedWindows); + if (timeLine.value() < 0.5) { + paintWindows(rightWindows, false, reflectedWindows); + paintFrontWindow(frontWindow, width, leftWindowCount, rightWindowCount, reflectedWindows); + } else { + EffectWindow* leftWindow; + if (leftWindowCount > 0) { + leftWindow = leftWindows.at(0); + paintFrontWindow(frontWindow, width, leftWindowCount, rightWindowCount, reflectedWindows); + } else + leftWindow = frontWindow; + paintWindows(rightWindows, false, reflectedWindows, leftWindow); + } + } + } +} + +void CoverSwitchEffect::paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data) +{ + if (mActivated || stop || stopRequested) { + if (!(mask & PAINT_WINDOW_TRANSFORMED) && !w->isDesktop()) { + if ((start || stop) && w->isDock()) { + data.setOpacity(1.0 - timeLine.value()); + if (stop) + data.setOpacity(timeLine.value()); + } else + return; + } + } + if ((start || stop) && (!w->isOnCurrentDesktop() || w->isMinimized())) { + if (stop) // Fade out windows not on the current desktop + data.setOpacity(1.0 - timeLine.value()); + else // Fade in Windows from other desktops when animation is started + data.setOpacity(timeLine.value()); + } + effects->paintWindow(w, mask, region, data); +} + +void CoverSwitchEffect::slotTabBoxAdded(int mode) +{ + if (effects->activeFullScreenEffect() && effects->activeFullScreenEffect() != this) + return; + if (!mActivated) { + effects->setShowingDesktop(false); + // only for windows mode + if (((mode == TabBoxWindowsMode && primaryTabBox) || + (mode == TabBoxWindowsAlternativeMode && secondaryTabBox) || + (mode == TabBoxCurrentAppWindowsMode && primaryTabBox) || + (mode == TabBoxCurrentAppWindowsAlternativeMode && secondaryTabBox)) + && effects->currentTabBoxWindowList().count() > 0) { + effects->startMouseInterception(this, Qt::ArrowCursor); + activeScreen = effects->activeScreen(); + if (!stop && !stopRequested) { + effects->refTabBox(); + effects->setActiveFullScreenEffect(this); + scheduled_directions.clear(); + selected_window = effects->currentTabBoxWindow(); + currentWindowList = effects->currentTabBoxWindowList(); + direction = Left; + mActivated = true; + if (animateStart) { + start = true; + } + + // Calculation of correct area + area = effects->clientArea(FullScreenArea, activeScreen, effects->currentDesktop()); + const QSize screenSize = effects->virtualScreenSize(); + scaleFactor = (zPosition + 1100) * 2.0 * tan(60.0 * M_PI / 360.0f) / screenSize.width(); + if (screenSize.width() - area.width() != 0) { + // one of the screens is smaller than the other (horizontal) + if (area.width() < screenSize.width() - area.width()) + scaleFactor *= (float)area.width() / (float)(screenSize.width() - area.width()); + else if (area.width() != screenSize.width() - area.width()) { + // vertical layout with different width + // but we don't want to catch screens with same width and different height + if (screenSize.height() != area.height()) + scaleFactor *= (float)area.width() / (float)(screenSize.width()); + } + } + + if (effects->numScreens() > 1) { + // unfortunatelly we have to change the projection matrix in dual screen mode + // code is adapted from SceneOpenGL2::createProjectionMatrix() + QRect fullRect = effects->clientArea(FullArea, activeScreen, effects->currentDesktop()); + float fovy = 60.0f; + float aspect = 1.0f; + float zNear = 0.1f; + float zFar = 100.0f; + + float ymax = zNear * std::tan(fovy * M_PI / 360.0f); + float ymin = -ymax; + float xmin = ymin * aspect; + float xmax = ymax * aspect; + + if (area.width() != fullRect.width()) { + if (area.x() == 0) { + // horizontal layout: left screen + xmin *= (float)area.width() / (float)fullRect.width(); + xmax *= (fullRect.width() - 0.5f * area.width()) / (0.5f * fullRect.width()); + } else { + // horizontal layout: right screen + xmin *= (fullRect.width() - 0.5f * area.width()) / (0.5f * fullRect.width()); + xmax *= (float)area.width() / (float)fullRect.width(); + } + } + if (area.height() != fullRect.height()) { + if (area.y() == 0) { + // vertical layout: top screen + ymin *= (fullRect.height() - 0.5f * area.height()) / (0.5f * fullRect.height()); + ymax *= (float)area.height() / (float)fullRect.height(); + } else { + // vertical layout: bottom screen + ymin *= (float)area.height() / (float)fullRect.height(); + ymax *= (fullRect.height() - 0.5f * area.height()) / (0.5f * fullRect.height()); + } + } + + m_projectionMatrix = QMatrix4x4(); + m_projectionMatrix.frustum(xmin, xmax, ymin, ymax, zNear, zFar); + + const float scaleFactor = 1.1f / zNear; + + // Create a second matrix that transforms screen coordinates + // to world coordinates. + QMatrix4x4 matrix; + matrix.translate(xmin * scaleFactor, ymax * scaleFactor, -1.1); + matrix.scale( (xmax - xmin) * scaleFactor / fullRect.width(), + -(ymax - ymin) * scaleFactor / fullRect.height(), + 0.001); + // Combine the matrices + m_projectionMatrix *= matrix; + + m_modelviewMatrix = QMatrix4x4(); + m_modelviewMatrix.translate(area.x(), area.y(), 0.0); + } + + // Setup caption frame geometry + if (windowTitle) { + QRect frameRect = QRect(area.width() * 0.25f + area.x(), + area.height() * 0.9f + area.y(), + area.width() * 0.5f, + QFontMetrics(captionFont).height()); + if (!captionFrame) { + captionFrame = effects->effectFrame(EffectFrameStyled); + captionFrame->setFont(captionFont); + captionFrame->enableCrossFade(true); + } + captionFrame->setGeometry(frameRect); + captionFrame->setIconSize(QSize(frameRect.height(), frameRect.height())); + // And initial contents + updateCaption(); + } + + effects->addRepaintFull(); + } else { + startRequested = true; + } + } + } +} + +void CoverSwitchEffect::slotTabBoxClosed() +{ + if (mActivated) { + if (animateStop) { + if (!animation && !start) { + stop = true; + } else if (start && scheduled_directions.isEmpty()) { + start = false; + stop = true; + timeLine.setElapsed(timeLine.duration() - timeLine.elapsed()); + } else { + stopRequested = true; + } + } else { + effects->setActiveFullScreenEffect(nullptr); + start = false; + animation = false; + timeLine.reset(); + } + mActivated = false; + effects->unrefTabBox(); + effects->stopMouseInterception(this); + effects->addRepaintFull(); + } +} + +void CoverSwitchEffect::slotTabBoxUpdated() +{ + if (mActivated) { + if (animateSwitch && currentWindowList.count() > 1) { + // determine the switch direction + if (selected_window != effects->currentTabBoxWindow()) { + if (selected_window != nullptr) { + int old_index = currentWindowList.indexOf(selected_window); + int new_index = effects->currentTabBoxWindowList().indexOf(effects->currentTabBoxWindow()); + Direction new_direction; + int distance = new_index - old_index; + if (distance > 0) + new_direction = Left; + if (distance < 0) + new_direction = Right; + if (effects->currentTabBoxWindowList().count() == 2) { + new_direction = Left; + distance = 1; + } + if (distance != 0) { + distance = abs(distance); + int tempDistance = effects->currentTabBoxWindowList().count() - distance; + if (tempDistance < abs(distance)) { + distance = tempDistance; + if (new_direction == Left) + new_direction = Right; + else + new_direction = Left; + } + if (!animation && !start) { + animation = true; + direction = new_direction; + distance--; + } + for (int i = 0; i < distance; i++) { + if (!scheduled_directions.isEmpty() && scheduled_directions.last() != new_direction) + scheduled_directions.pop_back(); + else + scheduled_directions.enqueue(new_direction); + if (scheduled_directions.count() == effects->currentTabBoxWindowList().count()) + scheduled_directions.clear(); + } + } + } + selected_window = effects->currentTabBoxWindow(); + currentWindowList = effects->currentTabBoxWindowList(); + updateCaption(); + } + } + effects->addRepaintFull(); + } +} + +void CoverSwitchEffect::paintWindowCover(EffectWindow* w, bool reflectedWindow, WindowPaintData& data) +{ + QRect windowRect = w->geometry(); + data.setYTranslation(area.height() - windowRect.y() - windowRect.height()); + data.setZTranslation(-zPosition); + if (start) { + if (w->isMinimized()) { + data.multiplyOpacity(timeLine.value()); + } else { + const QVector3D translation = data.translation() * timeLine.value(); + data.setXTranslation(translation.x()); + data.setYTranslation(translation.y()); + data.setZTranslation(translation.z()); + if (effects->numScreens() > 1) { + QRect clientRect = effects->clientArea(FullScreenArea, w->screen(), effects->currentDesktop()); + QRect fullRect = effects->clientArea(FullArea, activeScreen, effects->currentDesktop()); + if (w->screen() == activeScreen) { + if (clientRect.width() != fullRect.width() && clientRect.x() != fullRect.x()) { + data.translate(- clientRect.x() * (1.0f - timeLine.value())); + } + if (clientRect.height() != fullRect.height() && clientRect.y() != fullRect.y()) { + data.translate(0.0, - clientRect.y() * (1.0f - timeLine.value())); + } + } else { + if (clientRect.width() != fullRect.width() && clientRect.x() < area.x()) { + data.translate(- clientRect.width() * (1.0f - timeLine.value())); + } + if (clientRect.height() != fullRect.height() && clientRect.y() < area.y()) { + data.translate(0.0, - clientRect.height() * (1.0f - timeLine.value())); + } + } + } + data.setRotationAngle(data.rotationAngle() * timeLine.value()); + } + } + if (stop) { + if (w->isMinimized() && w != effects->activeWindow()) { + data.multiplyOpacity(1.0 - timeLine.value()); + } else { + const QVector3D translation = data.translation() * (1.0 - timeLine.value()); + data.setXTranslation(translation.x()); + data.setYTranslation(translation.y()); + data.setZTranslation(translation.z()); + if (effects->numScreens() > 1) { + QRect clientRect = effects->clientArea(FullScreenArea, w->screen(), effects->currentDesktop()); + QRect rect = effects->clientArea(FullScreenArea, activeScreen, effects->currentDesktop()); + QRect fullRect = effects->clientArea(FullArea, activeScreen, effects->currentDesktop()); + if (w->screen() == activeScreen) { + if (clientRect.width() != fullRect.width() && clientRect.x() != fullRect.x()) { + data.translate(- clientRect.x() * timeLine.value()); + } + if (clientRect.height() != fullRect.height() && clientRect.y() != fullRect.y()) { + data.translate(0.0, - clientRect.y() * timeLine.value()); + } + } else { + if (clientRect.width() != fullRect.width() && clientRect.x() < rect.x()) { + data.translate(- clientRect.width() * timeLine.value()); + } + if (clientRect.height() != fullRect.height() && clientRect.y() < area.y()) { + data.translate(0.0, - clientRect.height() * timeLine.value()); + } + } + } + data.setRotationAngle(data.rotationAngle() * (1.0 - timeLine.value())); + } + } + + if (reflectedWindow) { + QMatrix4x4 reflectionMatrix; + reflectionMatrix.scale(1.0, -1.0, 1.0); + data.setModelViewMatrix(reflectionMatrix*data.modelViewMatrix()); + data.setYTranslation(- area.height() - windowRect.y() - windowRect.height()); + if (start) { + data.multiplyOpacity(timeLine.value()); + } else if (stop) { + data.multiplyOpacity(1.0 - timeLine.value()); + } + effects->drawWindow(w, + PAINT_WINDOW_TRANSFORMED, + infiniteRegion(), data); + } else { + effects->paintWindow(w, + PAINT_WINDOW_TRANSFORMED, + infiniteRegion(), data); + } +} + +void CoverSwitchEffect::paintFrontWindow(EffectWindow* frontWindow, int width, int leftWindows, int rightWindows, bool reflectedWindow) +{ + if (frontWindow == nullptr) + return; + bool specialHandlingForward = false; + WindowPaintData data(frontWindow); + if (effects->numScreens() > 1) { + data.setProjectionMatrix(m_projectionMatrix); + data.setModelViewMatrix(m_modelviewMatrix); + } + data.setXTranslation(area.width() * 0.5 - frontWindow->geometry().x() - frontWindow->geometry().width() * 0.5); + if (leftWindows == 0) { + leftWindows = 1; + if (!start && !stop) + specialHandlingForward = true; + } + if (rightWindows == 0) { + rightWindows = 1; + } + if (animation) { + float distance = 0.0; + const QSize screenSize = effects->virtualScreenSize(); + if (direction == Right) { + // move to right + distance = -frontWindow->geometry().width() * 0.5f + area.width() * 0.5f + + (((float)screenSize.width() * 0.5 * scaleFactor) - (float)area.width() * 0.5f) / rightWindows; + data.translate(distance * timeLine.value()); + data.setRotationAxis(Qt::YAxis); + data.setRotationAngle(-angle * timeLine.value()); + data.setRotationOrigin(QVector3D(frontWindow->geometry().width(), 0.0, 0.0)); + } else { + // move to left + distance = frontWindow->geometry().width() * 0.5f - area.width() * 0.5f + + ((float)width * 0.5f - ((float)screenSize.width() * 0.5 * scaleFactor)) / leftWindows; + float factor = 1.0; + if (specialHandlingForward) + factor = 2.0; + data.translate(distance * timeLine.value() * factor); + data.setRotationAxis(Qt::YAxis); + data.setRotationAngle(angle * timeLine.value()); + } + } + if (specialHandlingForward && timeLine.value() < 0.5) { + data.multiplyOpacity(1.0 - timeLine.value() * 2.0); + } + paintWindowCover(frontWindow, reflectedWindow, data); +} + +void CoverSwitchEffect::paintWindows(const EffectWindowList& windows, bool left, bool reflectedWindows, EffectWindow* additionalWindow) +{ + int width = area.width(); + int windowCount = windows.count(); + EffectWindow* window; + + int rotateFactor = 1; + if (!left) { + rotateFactor = -1; + } + + const QSize screenSize = effects->virtualScreenSize(); + float xTranslate = -((float)(width) * 0.5f - ((float)screenSize.width() * 0.5 * scaleFactor)); + if (!left) + xTranslate = ((float)screenSize.width() * 0.5 * scaleFactor) - (float)width * 0.5f; + // handling for additional window from other side + // has to appear on this side after half of the time + if (animation && timeLine.value() >= 0.5 && additionalWindow != nullptr) { + WindowPaintData data(additionalWindow); + if (effects->numScreens() > 1) { + data.setProjectionMatrix(m_projectionMatrix); + data.setModelViewMatrix(m_modelviewMatrix); + } + data.setRotationAxis(Qt::YAxis); + data.setRotationAngle(angle * rotateFactor); + if (left) { + data.translate(-xTranslate - additionalWindow->geometry().x()); + } + else { + data.translate(xTranslate + area.width() - + additionalWindow->geometry().x() - additionalWindow->geometry().width()); + data.setRotationOrigin(QVector3D(additionalWindow->geometry().width(), 0.0, 0.0)); + } + data.multiplyOpacity((timeLine.value() - 0.5) * 2.0); + paintWindowCover(additionalWindow, reflectedWindows, data); + } + // normal behaviour + for (int i = 0; i < windows.count(); i++) { + window = windows.at(i); + if (window == nullptr || window->isDeleted()) { + continue; + } + WindowPaintData data(window); + if (effects->numScreens() > 1) { + data.setProjectionMatrix(m_projectionMatrix); + data.setModelViewMatrix(m_modelviewMatrix); + } + data.setRotationAxis(Qt::YAxis); + data.setRotationAngle(angle); + if (left) + data.translate(-xTranslate + xTranslate * i / windowCount - window->geometry().x()); + else + data.translate(xTranslate + width - xTranslate * i / windowCount - window->geometry().x() - window->geometry().width()); + if (animation) { + if (direction == Right) { + if ((i == windowCount - 1) && left) { + // right most window on left side -> move to front + // have to move one window distance plus half the difference between the window and the desktop size + data.translate((xTranslate / windowCount + (width - window->geometry().width()) * 0.5f) * timeLine.value()); + data.setRotationAngle(angle - angle * timeLine.value()); + } + // right most window does not have to be moved + else if (!left && (i == 0)); // do nothing + else { + // all other windows - move to next position + data.translate(xTranslate / windowCount * timeLine.value()); + } + } else { + if ((i == windowCount - 1) && !left) { + // left most window on right side -> move to front + data.translate(- (xTranslate / windowCount + (width - window->geometry().width()) * 0.5f) * timeLine.value()); + data.setRotationAngle(angle - angle * timeLine.value()); + } + // left most window does not have to be moved + else if (i == 0 && left); // do nothing + else { + // all other windows - move to next position + data.translate(- xTranslate / windowCount * timeLine.value()); + } + } + } + if (!left) + data.setRotationOrigin(QVector3D(window->geometry().width(), 0.0, 0.0)); + data.setRotationAngle(data.rotationAngle() * rotateFactor); + // make window most to edge transparent if animation + if (animation && i == 0 && ((direction == Left && left) || (direction == Right && !left))) { + // only for the first half of the animation + if (timeLine.value() < 0.5) { + data.multiplyOpacity((1.0 - timeLine.value() * 2.0)); + paintWindowCover(window, reflectedWindows, data); + } + } else { + paintWindowCover(window, reflectedWindows, data); + } + } +} + +void CoverSwitchEffect::windowInputMouseEvent(QEvent* e) +{ + if (e->type() != QEvent::MouseButtonPress) + return; + // we don't want click events during animations + if (animation) + return; + QMouseEvent* event = static_cast< QMouseEvent* >(e); + + switch (event->button()) { + case Qt::XButton1: // wheel up + selectPreviousWindow(); + break; + case Qt::XButton2: // wheel down + selectNextWindow(); + break; + case Qt::LeftButton: + case Qt::RightButton: + case Qt::MiddleButton: + default: + QPoint pos = event->pos(); + + // determine if a window has been clicked + // not interested in events above a fullscreen window (ignoring panel size) + if (pos.y() < (area.height()*scaleFactor - area.height()) * 0.5f *(1.0f / scaleFactor)) + return; + + // if there is no selected window (that is no window at all) we cannot click it + if (!selected_window) + return; + + if (pos.x() < (area.width()*scaleFactor - selected_window->width()) * 0.5f *(1.0f / scaleFactor)) { + float availableSize = (area.width() * scaleFactor - area.width()) * 0.5f * (1.0f / scaleFactor); + for (int i = 0; i < leftWindows.count(); i++) { + int windowPos = availableSize / leftWindows.count() * i; + if (pos.x() < windowPos) + continue; + if (i + 1 < leftWindows.count()) { + if (pos.x() > availableSize / leftWindows.count()*(i + 1)) + continue; + } + + effects->setTabBoxWindow(leftWindows[i]); + return; + } + } + + if (pos.x() > area.width() - (area.width()*scaleFactor - selected_window->width()) * 0.5f *(1.0f / scaleFactor)) { + float availableSize = (area.width() * scaleFactor - area.width()) * 0.5f * (1.0f / scaleFactor); + for (int i = 0; i < rightWindows.count(); i++) { + int windowPos = area.width() - availableSize / rightWindows.count() * i; + if (pos.x() > windowPos) + continue; + if (i + 1 < rightWindows.count()) { + if (pos.x() < area.width() - availableSize / rightWindows.count()*(i + 1)) + continue; + } + + effects->setTabBoxWindow(rightWindows[i]); + return; + } + } + break; + } +} + +void CoverSwitchEffect::abort() +{ + // it's possible that abort is called after tabbox has been closed + // in this case the cleanup is already done (see bug 207554) + if (mActivated) { + effects->unrefTabBox(); + effects->stopMouseInterception(this); + } + effects->setActiveFullScreenEffect(nullptr); + timeLine.reset(); + mActivated = false; + stop = false; + stopRequested = false; + effects->addRepaintFull(); + captionFrame->free(); +} + +void CoverSwitchEffect::slotWindowClosed(EffectWindow* c) +{ + if (c == selected_window) + selected_window = nullptr; + // if the list is not empty, the effect is active + if (!currentWindowList.isEmpty()) { + c->refWindow(); + referrencedWindows.append(c); + currentWindowList.removeAll(c); + leftWindows.removeAll(c); + rightWindows.removeAll(c); + } +} + +bool CoverSwitchEffect::isActive() const +{ + return (mActivated || stop || stopRequested) && !effects->isScreenLocked(); +} + +void CoverSwitchEffect::updateCaption() +{ + if (!selected_window || !windowTitle) { + return; + } + if (selected_window->isDesktop()) { + captionFrame->setText(i18nc("Special entry in alt+tab list for minimizing all windows", + "Show Desktop")); + static QPixmap pix = QIcon::fromTheme(QStringLiteral("user-desktop")).pixmap(captionFrame->iconSize()); + captionFrame->setIcon(pix); + } else { + captionFrame->setText(selected_window->caption()); + captionFrame->setIcon(selected_window->icon()); + } +} + +void CoverSwitchEffect::slotTabBoxKeyEvent(QKeyEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + switch (event->key()) { + case Qt::Key_Left: + selectPreviousWindow(); + break; + case Qt::Key_Right: + selectNextWindow(); + break; + default: + // nothing + break; + } + } +} + +void CoverSwitchEffect::selectNextOrPreviousWindow(bool forward) +{ + if (!mActivated || !selected_window) { + return; + } + const int index = effects->currentTabBoxWindowList().indexOf(selected_window); + int newIndex = index; + if (forward) { + ++newIndex; + } else { + --newIndex; + } + if (newIndex == effects->currentTabBoxWindowList().size()) { + newIndex = 0; + } else if (newIndex < 0) { + newIndex = effects->currentTabBoxWindowList().size() -1; + } + if (index == newIndex) { + return; + } + effects->setTabBoxWindow(effects->currentTabBoxWindowList().at(newIndex)); +} + +} // namespace diff --git a/effects/coverswitch/coverswitch.h b/effects/coverswitch/coverswitch.h new file mode 100644 index 0000000..d48b39b --- /dev/null +++ b/effects/coverswitch/coverswitch.h @@ -0,0 +1,155 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_COVERSWITCH_H +#define KWIN_COVERSWITCH_H + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace KWin +{ + +class CoverSwitchEffect + : public Effect +{ + Q_OBJECT + Q_PROPERTY(int animationDuration READ configuredAnimationDuration) + Q_PROPERTY(bool animateSwitch READ isAnimateSwitch) + Q_PROPERTY(bool animateStart READ isAnimateStart) + Q_PROPERTY(bool animateStop READ isAnimateStop) + Q_PROPERTY(bool reflection READ isReflection) + Q_PROPERTY(bool windowTitle READ isWindowTitle) + Q_PROPERTY(qreal zPosition READ windowZPosition) + Q_PROPERTY(bool primaryTabBox READ isPrimaryTabBox) + Q_PROPERTY(bool secondaryTabBox READ isSecondaryTabBox) + // TODO: mirror colors +public: + CoverSwitchEffect(); + ~CoverSwitchEffect() override; + + void reconfigure(ReconfigureFlags) override; + void prePaintScreen(ScreenPrePaintData &data, int time) override; + void paintScreen(int mask, const QRegion ®ion, ScreenPaintData &data) override; + void postPaintScreen() override; + void paintWindow(EffectWindow *w, int mask, QRegion region, WindowPaintData &data) override; + void windowInputMouseEvent(QEvent *e) override; + bool isActive() const override; + + static bool supported(); + + // for properties + int configuredAnimationDuration() const { + return animationDuration.count(); + } + bool isAnimateSwitch() const { + return animateSwitch; + } + bool isAnimateStart() const { + return animateStart; + } + bool isAnimateStop() const { + return animateStop; + } + bool isReflection() const { + return reflection; + } + bool isWindowTitle() const { + return windowTitle; + } + qreal windowZPosition() const { + return zPosition; + } + bool isPrimaryTabBox() const { + return primaryTabBox; + } + bool isSecondaryTabBox() const { + return secondaryTabBox; + } + + int requestedEffectChainPosition() const override { + return 50; + } + +public Q_SLOTS: + void slotWindowClosed(KWin::EffectWindow *c); + void slotTabBoxAdded(int mode); + void slotTabBoxClosed(); + void slotTabBoxUpdated(); + void slotTabBoxKeyEvent(QKeyEvent* event); + +private: + void paintScene(EffectWindow* frontWindow, const EffectWindowList& leftWindows, const EffectWindowList& rightWindows, + bool reflectedWindows = false); + void paintWindowCover(EffectWindow* w, bool reflectedWindow, WindowPaintData& data); + void paintFrontWindow(EffectWindow* frontWindow, int width, int leftWindows, int rightWindows, bool reflectedWindow); + void paintWindows(const EffectWindowList& windows, bool left, bool reflectedWindows, EffectWindow* additionalWindow = nullptr); + void selectNextOrPreviousWindow(bool forward); + inline void selectNextWindow() { selectNextOrPreviousWindow(true); } + inline void selectPreviousWindow() { selectNextOrPreviousWindow(false); } + void abort(); + /** + * Updates the caption of the caption frame. + * Taking care of rewording the desktop client. + * As well sets the icon for the caption frame. + */ + void updateCaption(); + + bool mActivated; + float angle; + bool animateSwitch; + bool animateStart; + bool animateStop; + bool animation; + bool start; + bool stop; + bool reflection; + float mirrorColor[2][4]; + bool windowTitle; + std::chrono::milliseconds animationDuration; + bool stopRequested; + bool startRequested; + TimeLine timeLine; + QRect area; + float zPosition; + float scaleFactor; + enum Direction { + Left, + Right + }; + Direction direction; + QQueue scheduled_directions; + EffectWindow* selected_window; + int activeScreen; + QList< EffectWindow* > leftWindows; + QList< EffectWindow* > rightWindows; + EffectWindowList currentWindowList; + EffectWindowList referrencedWindows; + + EffectFrame* captionFrame; + QFont captionFont; + + bool primaryTabBox; + bool secondaryTabBox; + + GLShader *m_reflectionShader; + QMatrix4x4 m_projectionMatrix; + QMatrix4x4 m_modelviewMatrix; +}; + +} // namespace + +#endif diff --git a/effects/coverswitch/coverswitch.kcfg b/effects/coverswitch/coverswitch.kcfg new file mode 100644 index 0000000..77db6d2 --- /dev/null +++ b/effects/coverswitch/coverswitch.kcfg @@ -0,0 +1,42 @@ + + + + + + 0 + + + true + + + true + + + true + + + true + + + QColor(0, 0, 0) + + + QColor(0, 0, 0) + + + true + + + 900 + + + false + + + false + + + diff --git a/effects/coverswitch/coverswitch_config.cpp b/effects/coverswitch/coverswitch_config.cpp new file mode 100644 index 0000000..7ba6981 --- /dev/null +++ b/effects/coverswitch/coverswitch_config.cpp @@ -0,0 +1,56 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "coverswitch_config.h" +// KConfigSkeleton +#include "coverswitchconfig.h" +#include + +#include +#include +#include + +#include + +K_PLUGIN_FACTORY_WITH_JSON(CoverSwitchEffectConfigFactory, + "coverswitch_config.json", + registerPlugin();) + +namespace KWin +{ + +CoverSwitchEffectConfigForm::CoverSwitchEffectConfigForm(QWidget* parent) : QWidget(parent) +{ + setupUi(this); +} + +CoverSwitchEffectConfig::CoverSwitchEffectConfig(QWidget* parent, const QVariantList& args) : + KCModule(KAboutData::pluginData(QStringLiteral("coverswitch")), parent, args) +{ + m_ui = new CoverSwitchEffectConfigForm(this); + + QVBoxLayout* layout = new QVBoxLayout(this); + + layout->addWidget(m_ui); + + CoverSwitchConfig::instance(KWIN_CONFIG); + addConfig(CoverSwitchConfig::self(), m_ui); +} + +void CoverSwitchEffectConfig::save() +{ + KCModule::save(); + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("coverswitch")); +} + +} // namespace + +#include "coverswitch_config.moc" diff --git a/effects/coverswitch/coverswitch_config.desktop b/effects/coverswitch/coverswitch_config.desktop new file mode 100644 index 0000000..7cd50ff --- /dev/null +++ b/effects/coverswitch/coverswitch_config.desktop @@ -0,0 +1,76 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=KCModule + +X-KDE-Library=kwin_coverswitch_config +X-KDE-ParentComponents=coverswitch + +Name=Cover Switch +Name[ar]=تبديل الأغلفة +Name[az]=Karusel +Name[bg]=Прелистване на страници +Name[bn]=আবরণ বদল +Name[bs]=Protočno prebacivanje +Name[ca]=Canvi de capa +Name[ca@valencia]=Canvi de capa +Name[cs]=Přehlídka oken +Name[da]=Cover-skifter +Name[de]=3D-Fenstergalerie +Name[el]=Εναλλαγή εξωφύλλου +Name[en_GB]=Cover Switch +Name[eo]=Kovra ŝanĝilo +Name[es]=Selección de ventana en modo carátula +Name[et]=Aknalülitaja +Name[eu]=Leiho-argazkien aldaketa +Name[fi]=Levykansivaihtaja +Name[fr]=Défilement circulaire +Name[fy]=Foarplaat wiksel +Name[ga]=Cover Switch +Name[gl]=Cambio en capas +Name[gu]=ફેરફાર ઢાંકો +Name[he]=מחליף כיסויים +Name[hi]=कवर स्विच +Name[hne]=कवर स्विच +Name[hr]=Pokrivač – promjena +Name[hu]=Fedett váltódoboz +Name[ia]=Commutator de copertura +Name[id]=Beralih Sampul +Name[is]=Síðuskiptir +Name[it]=Scambiafinestre circolare +Name[ja]=カバースイッチ +Name[kk]=Cover Switch +Name[km]=ប្ដូរ​​គម្រប​ +Name[kn]=ಕವರ್ ಸ್ವಿಚ್ +Name[ko]=커버 전환기 +Name[lt]=VirÅ¡elių perjungiklis +Name[lv]=Vāku pārslēdzējs +Name[ml]=കവര്‍ സ്വിച്ച് +Name[mr]=कव्हर स्विच +Name[nb]=Omslagsbytter +Name[nds]=Cover Switch +Name[nl]=Omslagtonen +Name[nn]=Omslagvekslar +Name[pa]=ਕਵਰ ਸਵਿੱਚ +Name[pl]=Przełączanie okładek +Name[pt]=Mudança de Capas +Name[pt_BR]=Seleção em capas +Name[ro]=Comutare copertă +Name[ru]=Карусель +Name[si]=කවර මාරුව +Name[sk]=PrepínaÅ¥ s gáleriou +Name[sl]=Preklapljanje - ovitki +Name[sr]=Проточно пребацивање +Name[sr@ijekavian]=Проточно пребацивање +Name[sr@ijekavianlatin]=Protočno prebacivanje +Name[sr@latin]=Protočno prebacivanje +Name[sv]=Omslagsbyte +Name[ta]=Cover Switch +Name[te]=కవర్ స్విచ్ +Name[th]=สลับหน้าต่างแบบปกเทป +Name[tr]=Kapak Pencere Seçici +Name[ug]=قاپنى ئالماشتۇر +Name[uk]=Перемикач обкладинок +Name[wa]=Discandjeu d' coviete +Name[x-test]=xxCover Switchxx +Name[zh_CN]=封面切换 +Name[zh_TW]=覆蓋切換 diff --git a/effects/coverswitch/coverswitch_config.h b/effects/coverswitch/coverswitch_config.h new file mode 100644 index 0000000..91d7649 --- /dev/null +++ b/effects/coverswitch/coverswitch_config.h @@ -0,0 +1,43 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_COVERSWITCH_CONFIG_H +#define KWIN_COVERSWITCH_CONFIG_H + +#include + +#include "ui_coverswitch_config.h" + + +namespace KWin +{ + +class CoverSwitchEffectConfigForm : public QWidget, public Ui::CoverSwitchEffectConfigForm +{ + Q_OBJECT +public: + explicit CoverSwitchEffectConfigForm(QWidget* parent); +}; + +class CoverSwitchEffectConfig : public KCModule +{ + Q_OBJECT +public: + explicit CoverSwitchEffectConfig(QWidget* parent = nullptr, const QVariantList& args = QVariantList()); + +public Q_SLOTS: + void save() override; + +private: + CoverSwitchEffectConfigForm* m_ui; +}; + +} // namespace + +#endif diff --git a/effects/coverswitch/coverswitch_config.ui b/effects/coverswitch/coverswitch_config.ui new file mode 100644 index 0000000..982caf6 --- /dev/null +++ b/effects/coverswitch/coverswitch_config.ui @@ -0,0 +1,307 @@ + + + KWin::CoverSwitchEffectConfigForm + + + + 0 + 0 + 453 + 270 + + + + + + + Display window &titles + + + + + + + + + + + + Zoom + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + Define how far away the windows should appear + + + 3000 + + + 100 + + + 500 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + 200 + + + + + + + + + Near + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Far + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + 0 + 0 + + + + 0 + + + + Animation + + + + + + Animate switch + + + + + + + Animation on tab box open + + + + + + + Animation on tab box close + + + + + + + + + Animation duration: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_Duration + + + + + + + + 0 + 0 + + + + Default + + + milliseconds + + + 9999 + + + 10 + + + + + + + + + + Reflections + + + + + + Reflections + + + + + + + QFormLayout::FieldsStayAtSizeHint + + + + + Rear color + + + + + + + + + + Front color + + + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + KColorButton + QPushButton +

kcolorbutton.h
+ + + + + + kcfg_Reflection + toggled(bool) + kcfg_MirrorFrontColor + setEnabled(bool) + + + 60 + 49 + + + 282 + 110 + + + + + kcfg_Reflection + toggled(bool) + kcfg_MirrorRearColor + setEnabled(bool) + + + 102 + 51 + + + 284 + 72 + + + + + kcfg_Reflection + toggled(bool) + label_2 + setEnabled(bool) + + + 136 + 47 + + + 202 + 78 + + + + + kcfg_Reflection + toggled(bool) + label + setEnabled(bool) + + + 175 + 49 + + + 209 + 102 + + + + + diff --git a/effects/coverswitch/coverswitchconfig.kcfgc b/effects/coverswitch/coverswitchconfig.kcfgc new file mode 100644 index 0000000..c23d13a --- /dev/null +++ b/effects/coverswitch/coverswitchconfig.kcfgc @@ -0,0 +1,5 @@ +File=coverswitch.kcfg +ClassName=CoverSwitchConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/effects/coverswitch/shaders/1.10/coverswitch-reflection.glsl b/effects/coverswitch/shaders/1.10/coverswitch-reflection.glsl new file mode 100644 index 0000000..2d67397 --- /dev/null +++ b/effects/coverswitch/shaders/1.10/coverswitch-reflection.glsl @@ -0,0 +1,9 @@ +uniform vec4 u_frontColor; +uniform vec4 u_backColor; + +varying vec2 texcoord0; + +void main() +{ + gl_FragColor = u_frontColor*(1.0-texcoord0.s) + u_backColor*texcoord0.s; +} diff --git a/effects/coverswitch/shaders/1.40/coverswitch-reflection.glsl b/effects/coverswitch/shaders/1.40/coverswitch-reflection.glsl new file mode 100644 index 0000000..720d24a --- /dev/null +++ b/effects/coverswitch/shaders/1.40/coverswitch-reflection.glsl @@ -0,0 +1,12 @@ +#version 140 +uniform vec4 u_frontColor; +uniform vec4 u_backColor; + +in vec2 texcoord0; + +out vec4 fragColor; + +void main() +{ + fragColor = u_frontColor*(1.0-texcoord0.s) + u_backColor*texcoord0.s; +} diff --git a/effects/cube/CMakeLists.txt b/effects/cube/CMakeLists.txt new file mode 100644 index 0000000..c9d9460 --- /dev/null +++ b/effects/cube/CMakeLists.txt @@ -0,0 +1,32 @@ +####################################### +# Effect + +# Data files +install(FILES data/cubecap.png DESTINATION ${DATA_INSTALL_DIR}/kwin) + +####################################### +# Config + +set(kwin_cube_config_SRCS cube_config.cpp) +ki18n_wrap_ui(kwin_cube_config_SRCS cube_config.ui) +kconfig_add_kcfg_files(kwin_cube_config_SRCS cubeconfig.kcfgc) + +add_library(kwin_cube_config MODULE ${kwin_cube_config_SRCS}) + +target_link_libraries(kwin_cube_config + KF5::ConfigWidgets + KF5::GlobalAccel + KF5::I18n + KF5::XmlGui + KF5::KIOWidgets + KWinEffectsInterface +) + +kcoreaddons_desktop_to_json(kwin_cube_config cube_config.desktop SERVICE_TYPES kcmodule.desktop) + +install( + TARGETS + kwin_cube_config + DESTINATION + ${PLUGIN_INSTALL_DIR}/kwin/effects/configs +) diff --git a/effects/cube/cube.cpp b/effects/cube/cube.cpp new file mode 100644 index 0000000..dc491b3 --- /dev/null +++ b/effects/cube/cube.cpp @@ -0,0 +1,1739 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "cube.h" +// KConfigSkeleton +#include "cubeconfig.h" + +#include "cube_inside.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +namespace KWin +{ + +CubeEffect::CubeEffect() + : activated(false) + , cube_painting(false) + , keyboard_grab(false) + , painting_desktop(1) + , frontDesktop(0) + , cubeOpacity(1.0) + , opacityDesktopOnly(true) + , displayDesktopName(false) + , desktopNameFrame(nullptr) + , reflection(true) + , desktopChangedWhileRotating(false) + , paintCaps(true) + , wallpaper(nullptr) + , texturedCaps(true) + , capTexture(nullptr) + , reflectionPainting(false) + , activeScreen(0) + , bottomCap(false) + , closeOnMouseRelease(false) + , zoom(0.0) + , zPosition(0.0) + , useForTabBox(false) + , tabBoxMode(false) + , shortcutsRegistered(false) + , mode(Cube) + , useShaders(false) + , cylinderShader(nullptr) + , sphereShader(nullptr) + , zOrderingFactor(0.0f) + , mAddedHeightCoeff1(0.0f) + , mAddedHeightCoeff2(0.0f) + , m_cubeCapBuffer(nullptr) + , m_proxy(this) + , m_cubeAction(new QAction(this)) + , m_cylinderAction(new QAction(this)) + , m_sphereAction(new QAction(this)) +{ + initConfig(); + desktopNameFont.setBold(true); + desktopNameFont.setPointSize(14); + + if (effects->compositingType() == OpenGL2Compositing) { + m_reflectionShader = ShaderManager::instance()->generateShaderFromResources(ShaderTrait::MapTexture, QString(), QStringLiteral("cube-reflection.glsl")); + m_capShader = ShaderManager::instance()->generateShaderFromResources(ShaderTrait::MapTexture, QString(), QStringLiteral("cube-cap.glsl")); + } else { + m_reflectionShader = nullptr; + m_capShader = nullptr; + } + m_textureMirrorMatrix.scale(1.0, -1.0, 1.0); + m_textureMirrorMatrix.translate(0.0, -1.0, 0.0); + connect(effects, &EffectsHandler::tabBoxAdded, this, &CubeEffect::slotTabBoxAdded); + connect(effects, &EffectsHandler::tabBoxClosed, this, &CubeEffect::slotTabBoxClosed); + connect(effects, &EffectsHandler::tabBoxUpdated, this, &CubeEffect::slotTabBoxUpdated); + connect(effects, &EffectsHandler::screenAboutToLock, this, [this]() { + // Set active(false) does not release key grabs until the animation completes + // As we know the lockscreen is trying to grab them, release them early + // all other grabs are released in the normal way + setActive(false); + if (keyboard_grab) { + effects->ungrabKeyboard(); + keyboard_grab = false; + } + }); + + reconfigure(ReconfigureAll); +} + +bool CubeEffect::supported() +{ + return effects->isOpenGLCompositing(); +} + +void CubeEffect::reconfigure(ReconfigureFlags) +{ + CubeConfig::self()->read(); + foreach (ElectricBorder border, borderActivate) { + effects->unreserveElectricBorder(border, this); + } + foreach (ElectricBorder border, borderActivateCylinder) { + effects->unreserveElectricBorder(border, this); + } + foreach (ElectricBorder border, borderActivateSphere) { + effects->unreserveElectricBorder(border, this); + } + borderActivate.clear(); + borderActivateCylinder.clear(); + borderActivateSphere.clear(); + QList borderList = QList(); + borderList.append(int(ElectricNone)); + borderList = CubeConfig::borderActivate(); + foreach (int i, borderList) { + borderActivate.append(ElectricBorder(i)); + effects->reserveElectricBorder(ElectricBorder(i), this); + } + borderList.clear(); + borderList.append(int(ElectricNone)); + borderList = CubeConfig::borderActivateCylinder(); + foreach (int i, borderList) { + borderActivateCylinder.append(ElectricBorder(i)); + effects->reserveElectricBorder(ElectricBorder(i), this); + } + borderList.clear(); + borderList.append(int(ElectricNone)); + borderList = CubeConfig::borderActivateSphere(); + foreach (int i, borderList) { + borderActivateSphere.append(ElectricBorder(i)); + effects->reserveElectricBorder(ElectricBorder(i), this); + } + + cubeOpacity = (float)CubeConfig::opacity() / 100.0f; + opacityDesktopOnly = CubeConfig::opacityDesktopOnly(); + displayDesktopName = CubeConfig::displayDesktopName(); + reflection = CubeConfig::reflection(); + // TODO: Rename rotationDuration to duration so we + // can use animationTime(500). + const int d = CubeConfig::rotationDuration() != 0 + ? CubeConfig::rotationDuration() + : 500; + rotationDuration = std::chrono::milliseconds(static_cast(animationTime(d))); + backgroundColor = CubeConfig::backgroundColor(); + capColor = CubeConfig::capColor(); + paintCaps = CubeConfig::caps(); + closeOnMouseRelease = CubeConfig::closeOnMouseRelease(); + zPosition = CubeConfig::zPosition(); + + useForTabBox = CubeConfig::tabBox(); + invertKeys = CubeConfig::invertKeys(); + invertMouse = CubeConfig::invertMouse(); + capDeformationFactor = (float)CubeConfig::capDeformation() / 100.0f; + useZOrdering = CubeConfig::zOrdering(); + delete wallpaper; + wallpaper = nullptr; + delete capTexture; + capTexture = nullptr; + texturedCaps = CubeConfig::texturedCaps(); + + timeLine.setEasingCurve(QEasingCurve::InOutSine); + timeLine.setDuration(rotationDuration); + + verticalTimeLine.setEasingCurve(QEasingCurve::InOutSine); + verticalTimeLine.setDuration(rotationDuration); + + // do not connect the shortcut if we use cylinder or sphere + if (!shortcutsRegistered) { + QAction* cubeAction = m_cubeAction; + cubeAction->setObjectName(QStringLiteral("Cube")); + cubeAction->setText(i18n("Desktop Cube")); + KGlobalAccel::self()->setDefaultShortcut(cubeAction, QList() << Qt::CTRL + Qt::Key_F11); + KGlobalAccel::self()->setShortcut(cubeAction, QList() << Qt::CTRL + Qt::Key_F11); + effects->registerGlobalShortcut(Qt::CTRL + Qt::Key_F11, cubeAction); + effects->registerPointerShortcut(Qt::ControlModifier | Qt::AltModifier, Qt::LeftButton, cubeAction); + cubeShortcut = KGlobalAccel::self()->shortcut(cubeAction); + QAction* cylinderAction = m_cylinderAction; + cylinderAction->setObjectName(QStringLiteral("Cylinder")); + cylinderAction->setText(i18n("Desktop Cylinder")); + KGlobalAccel::self()->setShortcut(cylinderAction, QList()); + effects->registerGlobalShortcut(QKeySequence(), cylinderAction); + cylinderShortcut = KGlobalAccel::self()->shortcut(cylinderAction); + QAction* sphereAction = m_sphereAction; + sphereAction->setObjectName(QStringLiteral("Sphere")); + sphereAction->setText(i18n("Desktop Sphere")); + KGlobalAccel::self()->setShortcut(sphereAction, QList()); + sphereShortcut = KGlobalAccel::self()->shortcut(sphereAction); + effects->registerGlobalShortcut(QKeySequence(), sphereAction); + connect(cubeAction, &QAction::triggered, this, &CubeEffect::toggleCube); + connect(cylinderAction, &QAction::triggered, this, &CubeEffect::toggleCylinder); + connect(sphereAction, &QAction::triggered, this, &CubeEffect::toggleSphere); + connect(KGlobalAccel::self(), &KGlobalAccel::globalShortcutChanged, this, &CubeEffect::globalShortcutChanged); + shortcutsRegistered = true; + } + + // set the cap color on the shader + if (m_capShader && m_capShader->isValid()) { + ShaderBinder binder(m_capShader); + m_capShader->setUniform(GLShader::Color, capColor); + } + + // touch borders + const QVector relevantBorders{ElectricLeft, ElectricTop, ElectricRight, ElectricBottom}; + for (auto e : relevantBorders) { + effects->unregisterTouchBorder(e, m_cubeAction); + effects->unregisterTouchBorder(e, m_sphereAction); + effects->unregisterTouchBorder(e, m_cylinderAction); + } + auto touchEdge = [&relevantBorders] (const QList touchBorders, QAction *action) { + for (int i : touchBorders) { + if (!relevantBorders.contains(ElectricBorder(i))) { + continue; + } + effects->registerTouchBorder(ElectricBorder(i), action); + } + }; + touchEdge(CubeConfig::touchBorderActivate(), m_cubeAction); + touchEdge(CubeConfig::touchBorderActivateCylinder(), m_cylinderAction); + touchEdge(CubeConfig::touchBorderActivateSphere(), m_sphereAction); +} + +CubeEffect::~CubeEffect() +{ + delete wallpaper; + delete capTexture; + delete cylinderShader; + delete sphereShader; + delete desktopNameFrame; + delete m_reflectionShader; + delete m_capShader; + delete m_cubeCapBuffer; +} + +QImage CubeEffect::loadCubeCap(const QString &capPath) +{ + if (!texturedCaps) { + return QImage(); + } + return QImage(capPath); +} + +void CubeEffect::slotCubeCapLoaded() +{ + QFutureWatcher *watcher = dynamic_cast*>(sender()); + if (!watcher) { + // not invoked from future watcher + return; + } + QImage img = watcher->result(); + if (!img.isNull()) { + effects->makeOpenGLContextCurrent(); + capTexture = new GLTexture(img); + capTexture->setFilter(GL_LINEAR); + if (!GLPlatform::instance()->isGLES()) { + capTexture->setWrapMode(GL_CLAMP_TO_BORDER); + } + // need to recreate the VBO for the cube cap + delete m_cubeCapBuffer; + m_cubeCapBuffer = nullptr; + effects->addRepaintFull(); + } + watcher->deleteLater(); +} + +QImage CubeEffect::loadWallPaper(const QString &file) +{ + return QImage(file); +} + +void CubeEffect::slotWallPaperLoaded() +{ + QFutureWatcher *watcher = dynamic_cast*>(sender()); + if (!watcher) { + // not invoked from future watcher + return; + } + QImage img = watcher->result(); + if (!img.isNull()) { + effects->makeOpenGLContextCurrent(); + wallpaper = new GLTexture(img); + effects->addRepaintFull(); + } + watcher->deleteLater(); +} + +bool CubeEffect::loadShader() +{ + effects->makeOpenGLContextCurrent(); + if (!(GLPlatform::instance()->supports(GLSL) && + (effects->compositingType() == OpenGL2Compositing))) + return false; + + cylinderShader = ShaderManager::instance()->generateShaderFromResources(ShaderTrait::MapTexture | ShaderTrait::AdjustSaturation | ShaderTrait::Modulate, QStringLiteral("cylinder.vert"), QString()); + if (!cylinderShader->isValid()) { + qCCritical(KWINEFFECTS) << "The cylinder shader failed to load!"; + return false; + } else { + ShaderBinder binder(cylinderShader); + cylinderShader->setUniform("sampler", 0); + QRect rect = effects->clientArea(FullArea, activeScreen, effects->currentDesktop()); + cylinderShader->setUniform("width", (float)rect.width() * 0.5f); + } + + sphereShader = ShaderManager::instance()->generateShaderFromResources(ShaderTrait::MapTexture | ShaderTrait::AdjustSaturation | ShaderTrait::Modulate, QStringLiteral("sphere.vert"), QString()); + if (!sphereShader->isValid()) { + qCCritical(KWINEFFECTS) << "The sphere shader failed to load!"; + return false; + } else { + ShaderBinder binder(sphereShader); + sphereShader->setUniform("sampler", 0); + QRect rect = effects->clientArea(FullArea, activeScreen, effects->currentDesktop()); + sphereShader->setUniform("width", (float)rect.width() * 0.5f); + sphereShader->setUniform("height", (float)rect.height() * 0.5f); + sphereShader->setUniform("u_offset", QVector2D(0, 0)); + } + return true; +} + +void CubeEffect::startAnimation(AnimationState state) +{ + QEasingCurve curve; + /* If this is first and only animation -> EaseInOut + * there is more -> EaseIn + * If there was an animation before, and this is the last one -> EaseOut + * there is more -> Linear */ + if (animationState == AnimationState::None) { + curve.setType(animations.empty() ? QEasingCurve::InOutSine : QEasingCurve::InCurve); + } else { + curve.setType(animations.empty() ? QEasingCurve::OutCurve : QEasingCurve::Linear); + } + timeLine.reset(); + timeLine.setEasingCurve(curve); + startAngle = currentAngle; + startFrontDesktop = frontDesktop; + animationState = state; +} + +void CubeEffect::startVerticalAnimation(VerticalAnimationState state) +{ + /* Ignore if there is nowhere to rotate */ + if ((qFuzzyIsNull(verticalCurrentAngle - 90.0f) && state == VerticalAnimationState::Upwards) || + (qFuzzyIsNull(verticalCurrentAngle + 90.0f) && state == VerticalAnimationState::Downwards)) { + return; + } + verticalTimeLine.reset(); + verticalStartAngle = verticalCurrentAngle; + verticalAnimationState = state; +} + +void CubeEffect::prePaintScreen(ScreenPrePaintData& data, int time) +{ + if (activated) { + data.mask |= PAINT_SCREEN_TRANSFORMED | Effect::PAINT_SCREEN_WITH_TRANSFORMED_WINDOWS | PAINT_SCREEN_BACKGROUND_FIRST; + if (animationState == AnimationState::None && !animations.empty()) { + startAnimation(animations.dequeue()); + } + if (verticalAnimationState == VerticalAnimationState::None && !verticalAnimations.empty()) { + startVerticalAnimation(verticalAnimations.dequeue()); + } + + if (animationState != AnimationState::None || verticalAnimationState != VerticalAnimationState::None) { + if (animationState != AnimationState::None) { + timeLine.update(std::chrono::milliseconds(time)); + } + if (verticalAnimationState != VerticalAnimationState::None) { + verticalTimeLine.update(std::chrono::milliseconds(time)); + } + rotateCube(); + } + } + effects->prePaintScreen(data, time); +} + +void CubeEffect::paintScreen(int mask, const QRegion ®ion, ScreenPaintData& data) +{ + if (activated) { + QRect rect = effects->clientArea(FullArea, activeScreen, effects->currentDesktop()); + + // background + float clearColor[4]; + glGetFloatv(GL_COLOR_CLEAR_VALUE, clearColor); + glClearColor(backgroundColor.redF(), backgroundColor.greenF(), backgroundColor.blueF(), 1.0); + glClear(GL_COLOR_BUFFER_BIT); + glClearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]); + + // wallpaper + if (wallpaper) { + ShaderBinder binder(ShaderTrait::MapTexture); + binder.shader()->setUniform(GLShader::ModelViewProjectionMatrix, data.projectionMatrix()); + wallpaper->bind(); + wallpaper->render(region, rect); + wallpaper->unbind(); + } + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + // some veriables needed for painting the caps + float cubeAngle = (float)((float)(effects->numberOfDesktops() - 2) / (float)effects->numberOfDesktops() * 180.0f); + float point = rect.width() / 2 * tan(cubeAngle * 0.5f * M_PI / 180.0f); + float zTranslate = zPosition + zoom; + if (animationState == AnimationState::Start) { + zTranslate *= timeLine.value(); + } else if (animationState == AnimationState::Stop) { + zTranslate *= (1.0 - timeLine.value()); + } + // reflection + if (reflection) { + // we can use a huge scale factor (needed to calculate the rearground vertices) + float scaleFactor = 1000000 * tan(60.0 * M_PI / 360.0f) / rect.height(); + m_reflectionMatrix.setToIdentity(); + m_reflectionMatrix.scale(1.0, -1.0, 1.0); + + double translate = 0.0; + if (mode == Cube) { + double addedHeight1 = -rect.height() * cos(verticalCurrentAngle*M_PI/180.0f) - rect.width() * sin(fabs(verticalCurrentAngle)*M_PI/180.0f)/tan(M_PI/effects->numberOfDesktops()); + double addedHeight2 = -rect.width() * sin(fabs(verticalCurrentAngle)*M_PI/180.0f)*tan(M_PI*0.5f/effects->numberOfDesktops()); + if (verticalCurrentAngle > 0.0f && effects->numberOfDesktops() & 1) + translate = cos(fabs(currentAngle)*effects->numberOfDesktops()*M_PI/360.0f) * addedHeight2 + addedHeight1 - float(rect.height()); + else + translate = sin(fabs(currentAngle)*effects->numberOfDesktops()*M_PI/360.0f) * addedHeight2 + addedHeight1 - float(rect.height()); + } else if (mode == Cylinder) { + double addedHeight1 = -rect.height() * cos(verticalCurrentAngle*M_PI/180.0f) - rect.width() * sin(fabs(verticalCurrentAngle)*M_PI/180.0f)/tan(M_PI/effects->numberOfDesktops()); + translate = addedHeight1 - float(rect.height()); + } else { + float radius = (rect.width() * 0.5) / cos(cubeAngle * 0.5 * M_PI / 180.0); + translate = -rect.height()-2*radius; + } + m_reflectionMatrix.translate(0.0f, translate, 0.0f); + + reflectionPainting = true; + glEnable(GL_CULL_FACE); + paintCap(true, -point - zTranslate, data.projectionMatrix()); + + // cube + glCullFace(GL_BACK); + paintCube(mask, region, data); + + glCullFace(GL_FRONT); + paintCube(mask, region, data); + + paintCap(false, -point - zTranslate, data.projectionMatrix()); + glDisable(GL_CULL_FACE); + reflectionPainting = false; + + const float width = rect.width(); + const float height = rect.height(); + float vertices[] = { + -width * 0.5f, height, 0.0, + width * 0.5f, height, 0.0, + width * scaleFactor, height, -5000, + -width * scaleFactor, height, -5000 + }; + // foreground + float alpha = 0.7; + if (animationState == AnimationState::Start) { + alpha = 0.3 + 0.4 * timeLine.value(); + } else if (animationState == AnimationState::Stop) { + alpha = 0.3 + 0.4 * (1.0 - timeLine.value()); + } + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + if (m_reflectionShader && m_reflectionShader->isValid()) { + // ensure blending is enabled - no attribute stack + ShaderBinder binder(m_reflectionShader); + QMatrix4x4 windowTransformation = data.projectionMatrix(); + windowTransformation.translate(rect.x() + rect.width() * 0.5f, 0.0, 0.0); + m_reflectionShader->setUniform(GLShader::ModelViewProjectionMatrix, windowTransformation); + m_reflectionShader->setUniform("u_alpha", alpha); + QVector verts; + QVector texcoords; + verts.reserve(18); + texcoords.reserve(12); + texcoords << 0.0 << 0.0; + verts << vertices[6] << vertices[7] << vertices[8]; + texcoords << 0.0 << 0.0; + verts << vertices[9] << vertices[10] << vertices[11]; + texcoords << 1.0 << 0.0; + verts << vertices[0] << vertices[1] << vertices[2]; + texcoords << 1.0 << 0.0; + verts << vertices[0] << vertices[1] << vertices[2]; + texcoords << 1.0 << 0.0; + verts << vertices[3] << vertices[4] << vertices[5]; + texcoords << 0.0 << 0.0; + verts << vertices[6] << vertices[7] << vertices[8]; + GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer(); + vbo->reset(); + vbo->setData(6, 3, verts.data(), texcoords.data()); + vbo->render(GL_TRIANGLES); + } + glDisable(GL_BLEND); + } + glEnable(GL_CULL_FACE); + // caps + paintCap(false, -point - zTranslate, data.projectionMatrix()); + + // cube + glCullFace(GL_FRONT); + paintCube(mask, region, data); + + glCullFace(GL_BACK); + paintCube(mask, region, data); + + // cap + paintCap(true, -point - zTranslate, data.projectionMatrix()); + glDisable(GL_CULL_FACE); + + glDisable(GL_BLEND); + + // desktop name box - inspired from coverswitch + if (displayDesktopName) { + double opacity = 1.0; + if (animationState == AnimationState::Start) { + opacity = timeLine.value(); + } else if (animationState == AnimationState::Stop) { + opacity = 1.0 - timeLine.value(); + } + QRect screenRect = effects->clientArea(ScreenArea, activeScreen, frontDesktop); + QRect frameRect = QRect(screenRect.width() * 0.33f + screenRect.x(), screenRect.height() * 0.95f + screenRect.y(), + screenRect.width() * 0.34f, QFontMetrics(desktopNameFont).height()); + if (!desktopNameFrame) { + desktopNameFrame = effects->effectFrame(EffectFrameStyled); + desktopNameFrame->setFont(desktopNameFont); + } + desktopNameFrame->setGeometry(frameRect); + desktopNameFrame->setText(effects->desktopName(frontDesktop)); + desktopNameFrame->render(region, opacity); + } + } else { + effects->paintScreen(mask, region, data); + } +} + +void CubeEffect::rotateCube() +{ + QRect rect = effects->clientArea(FullArea, activeScreen, effects->currentDesktop()); + m_rotationMatrix.setToIdentity(); + float internalCubeAngle = 360.0f / effects->numberOfDesktops(); + float zTranslate = zPosition + zoom; + float cubeAngle = (float)((float)(effects->numberOfDesktops() - 2) / (float)effects->numberOfDesktops() * 180.0f); + float point = rect.width() / 2 * tan(cubeAngle * 0.5f * M_PI / 180.0f); + /* Animations */ + if (animationState == AnimationState::Start) { + zTranslate *= timeLine.value(); + } else if (animationState == AnimationState::Stop) { + currentAngle = startAngle * (1.0 - timeLine.value()); + zTranslate *= (1.0 - timeLine.value()); + } else if (animationState != AnimationState::None) { + /* Left or right */ + float endAngle = animationState == AnimationState::Right ? internalCubeAngle : -internalCubeAngle; + currentAngle = startAngle + timeLine.value() * (endAngle - startAngle); + frontDesktop = startFrontDesktop; + } + /* Switching to next desktop: either by mouse or due to animation */ + if (currentAngle > internalCubeAngle * 0.5f) { + currentAngle -= internalCubeAngle; + frontDesktop--; + if (frontDesktop < 1) { + frontDesktop = effects->numberOfDesktops(); + } + } + if (currentAngle < -internalCubeAngle * 0.5f) { + currentAngle += internalCubeAngle; + frontDesktop++; + if (frontDesktop > effects->numberOfDesktops()) { + frontDesktop = 1; + } + } + /* Vertical animations */ + if (verticalAnimationState != VerticalAnimationState::None) { + float verticalEndAngle = 0.0; + if (verticalAnimationState == VerticalAnimationState::Upwards && verticalStartAngle >= 0.0) { + verticalEndAngle = 90.0; + } + if (verticalAnimationState == VerticalAnimationState::Downwards && verticalStartAngle <= 0.0) { + verticalEndAngle = -90.0; + } + // This also handles the "VerticalAnimationState::Stop" correctly, since it has endAngle = 0.0 + verticalCurrentAngle = verticalStartAngle + verticalTimeLine.value() * (verticalEndAngle - verticalStartAngle); + } + /* Updating rotation matrix */ + if (verticalAnimationState != VerticalAnimationState::None || verticalCurrentAngle != 0.0f) { + m_rotationMatrix.translate(rect.width() / 2, rect.height() / 2, -point - zTranslate); + m_rotationMatrix.rotate(verticalCurrentAngle, 1.0, 0.0, 0.0); + m_rotationMatrix.translate(-rect.width() / 2, -rect.height() / 2, point + zTranslate); + } + if (animationState != AnimationState::None || currentAngle != 0.0f) { + m_rotationMatrix.translate(rect.width() / 2, rect.height() / 2, -point - zTranslate); + m_rotationMatrix.rotate(currentAngle, 0.0, 1.0, 0.0); + m_rotationMatrix.translate(-rect.width() / 2, -rect.height() / 2, point + zTranslate); + } +} + +void CubeEffect::paintCube(int mask, QRegion region, ScreenPaintData& data) +{ + QRect rect = effects->clientArea(FullArea, activeScreen, effects->currentDesktop()); + float internalCubeAngle = 360.0f / effects->numberOfDesktops(); + cube_painting = true; + float zTranslate = zPosition + zoom; + if (animationState == AnimationState::Start) { + zTranslate *= timeLine.value(); + } else if (animationState == AnimationState::Stop) { + zTranslate *= (1.0 - timeLine.value()); + } + + // Rotation of the cube + float cubeAngle = (float)((float)(effects->numberOfDesktops() - 2) / (float)effects->numberOfDesktops() * 180.0f); + float point = rect.width() / 2 * tan(cubeAngle * 0.5f * M_PI / 180.0f); + + for (int i = 0; i < effects->numberOfDesktops(); i++) { + // start painting the cube + painting_desktop = (i + frontDesktop) % effects->numberOfDesktops(); + if (painting_desktop == 0) { + painting_desktop = effects->numberOfDesktops(); + } + QMatrix4x4 matrix; + matrix.translate(0, 0, -zTranslate); + const QVector3D origin(rect.width() / 2, 0.0, -point); + matrix.translate(origin); + matrix.rotate(internalCubeAngle * i, 0, 1, 0); + matrix.translate(-origin); + m_currentFaceMatrix = matrix; + effects->paintScreen(mask, region, data); + } + cube_painting = false; + painting_desktop = effects->currentDesktop(); +} + +void CubeEffect::paintCap(bool frontFirst, float zOffset, const QMatrix4x4 &projection) +{ + if ((!paintCaps) || effects->numberOfDesktops() <= 2) + return; + GLenum firstCull = frontFirst ? GL_FRONT : GL_BACK; + GLenum secondCull = frontFirst ? GL_BACK : GL_FRONT; + const QRect rect = effects->clientArea(FullArea, activeScreen, effects->currentDesktop()); + + // create the VBO if not yet created + if (!m_cubeCapBuffer) { + switch(mode) { + case Cube: + paintCubeCap(); + break; + case Cylinder: + paintCylinderCap(); + break; + case Sphere: + paintSphereCap(); + break; + default: + // impossible + break; + } + } + + QMatrix4x4 capMvp; + QMatrix4x4 capMatrix; + capMatrix.translate(rect.width() / 2, 0.0, zOffset); + capMatrix.rotate((1 - frontDesktop) * 360.0f / effects->numberOfDesktops(), 0.0, 1.0, 0.0); + capMatrix.translate(0.0, rect.height(), 0.0); + if (mode == Sphere) { + capMatrix.scale(1.0, -1.0, 1.0); + } + + bool capShader = false; + if (effects->compositingType() == OpenGL2Compositing && m_capShader && m_capShader->isValid()) { + capShader = true; + ShaderManager::instance()->pushShader(m_capShader); + float opacity = cubeOpacity; + if (animationState == AnimationState::Start) { + opacity *= timeLine.value(); + } else if (animationState == AnimationState::Stop) { + opacity *= (1.0 - timeLine.value()); + } + m_capShader->setUniform("u_opacity", opacity); + m_capShader->setUniform("u_mirror", 1); + if (reflectionPainting) { + capMvp = projection * m_reflectionMatrix * m_rotationMatrix; + } else { + capMvp = projection * m_rotationMatrix; + } + m_capShader->setUniform(GLShader::ModelViewProjectionMatrix, capMvp * capMatrix); + m_capShader->setUniform("u_untextured", texturedCaps ? 0 : 1); + if (texturedCaps && effects->numberOfDesktops() > 3 && capTexture) { + capTexture->bind(); + } + } + + glEnable(GL_BLEND); + glCullFace(firstCull); + m_cubeCapBuffer->render(GL_TRIANGLES); + + if (mode == Sphere) { + capMatrix.scale(1.0, -1.0, 1.0); + } + capMatrix.translate(0.0, -rect.height(), 0.0); + if (capShader) { + m_capShader->setUniform(GLShader::ModelViewProjectionMatrix, capMvp * capMatrix); + m_capShader->setUniform("u_mirror", 0); + } + glCullFace(secondCull); + m_cubeCapBuffer->render(GL_TRIANGLES); + glDisable(GL_BLEND); + + if (capShader) { + ShaderManager::instance()->popShader(); + if (texturedCaps && effects->numberOfDesktops() > 3 && capTexture) { + capTexture->unbind(); + } + } +} + +void CubeEffect::paintCubeCap() +{ + QRect rect = effects->clientArea(FullArea, activeScreen, effects->currentDesktop()); + float cubeAngle = (float)((float)(effects->numberOfDesktops() - 2) / (float)effects->numberOfDesktops() * 180.0f); + float z = rect.width() / 2 * tan(cubeAngle * 0.5f * M_PI / 180.0f); + float zTexture = rect.width() / 2 * tan(45.0f * M_PI / 180.0f); + float angle = 360.0f / effects->numberOfDesktops(); + bool texture = texturedCaps && effects->numberOfDesktops() > 3 && capTexture; + QVector verts; + QVector texCoords; + for (int i = 0; i < effects->numberOfDesktops(); i++) { + int triangleRows = effects->numberOfDesktops() * 5; + float zTriangleDistance = z / (float)triangleRows; + float widthTriangle = tan(angle * 0.5 * M_PI / 180.0) * zTriangleDistance; + float currentWidth = 0.0; + float cosValue = cos(i * angle * M_PI / 180.0); + float sinValue = sin(i * angle * M_PI / 180.0); + for (int j = 0; j < triangleRows; j++) { + float previousWidth = currentWidth; + currentWidth = tan(angle * 0.5 * M_PI / 180.0) * zTriangleDistance * (j + 1); + int evenTriangles = 0; + int oddTriangles = 0; + for (int k = 0; k < floor(currentWidth / widthTriangle * 2 - 1 + 0.5f); k++) { + float x1 = -previousWidth; + float x2 = -currentWidth; + float x3 = 0.0; + float z1 = 0.0; + float z2 = 0.0; + float z3 = 0.0; + if (k % 2 == 0) { + x1 += evenTriangles * widthTriangle * 2; + x2 += evenTriangles * widthTriangle * 2; + x3 = x2 + widthTriangle * 2; + z1 = j * zTriangleDistance; + z2 = (j + 1) * zTriangleDistance; + z3 = (j + 1) * zTriangleDistance; + float xRot = cosValue * x1 - sinValue * z1; + float zRot = sinValue * x1 + cosValue * z1; + x1 = xRot; + z1 = zRot; + xRot = cosValue * x2 - sinValue * z2; + zRot = sinValue * x2 + cosValue * z2; + x2 = xRot; + z2 = zRot; + xRot = cosValue * x3 - sinValue * z3; + zRot = sinValue * x3 + cosValue * z3; + x3 = xRot; + z3 = zRot; + evenTriangles++; + } else { + x1 += oddTriangles * widthTriangle * 2; + x2 += (oddTriangles + 1) * widthTriangle * 2; + x3 = x1 + widthTriangle * 2; + z1 = j * zTriangleDistance; + z2 = (j + 1) * zTriangleDistance; + z3 = j * zTriangleDistance; + float xRot = cosValue * x1 - sinValue * z1; + float zRot = sinValue * x1 + cosValue * z1; + x1 = xRot; + z1 = zRot; + xRot = cosValue * x2 - sinValue * z2; + zRot = sinValue * x2 + cosValue * z2; + x2 = xRot; + z2 = zRot; + xRot = cosValue * x3 - sinValue * z3; + zRot = sinValue * x3 + cosValue * z3; + x3 = xRot; + z3 = zRot; + oddTriangles++; + } + float texX1 = 0.0; + float texX2 = 0.0; + float texX3 = 0.0; + float texY1 = 0.0; + float texY2 = 0.0; + float texY3 = 0.0; + if (texture) { + if (capTexture->isYInverted()) { + texX1 = x1 / (rect.width()) + 0.5; + texY1 = 0.5 + z1 / zTexture * 0.5; + texX2 = x2 / (rect.width()) + 0.5; + texY2 = 0.5 + z2 / zTexture * 0.5; + texX3 = x3 / (rect.width()) + 0.5; + texY3 = 0.5 + z3 / zTexture * 0.5; + texCoords << texX1 << texY1; + } else { + texX1 = x1 / (rect.width()) + 0.5; + texY1 = 0.5 - z1 / zTexture * 0.5; + texX2 = x2 / (rect.width()) + 0.5; + texY2 = 0.5 - z2 / zTexture * 0.5; + texX3 = x3 / (rect.width()) + 0.5; + texY3 = 0.5 - z3 / zTexture * 0.5; + texCoords << texX1 << texY1; + } + } + verts << x1 << 0.0 << z1; + if (texture) { + texCoords << texX2 << texY2; + } + verts << x2 << 0.0 << z2; + if (texture) { + texCoords << texX3 << texY3; + } + verts << x3 << 0.0 << z3; + } + } + } + delete m_cubeCapBuffer; + m_cubeCapBuffer = new GLVertexBuffer(GLVertexBuffer::Static); + m_cubeCapBuffer->setData(verts.count() / 3, 3, verts.constData(), texture ? texCoords.constData() : nullptr); +} + +void CubeEffect::paintCylinderCap() +{ + QRect rect = effects->clientArea(FullArea, activeScreen, effects->currentDesktop()); + float cubeAngle = (float)((float)(effects->numberOfDesktops() - 2) / (float)effects->numberOfDesktops() * 180.0f); + + float radian = (cubeAngle * 0.5) * M_PI / 180; + float radius = (rect.width() * 0.5) * tan(radian); + float segment = radius / 30.0f; + + bool texture = texturedCaps && effects->numberOfDesktops() > 3 && capTexture; + QVector verts; + QVector texCoords; + for (int i = 1; i <= 30; i++) { + int steps = 72; + for (int j = 0; j <= steps; j++) { + const float azimuthAngle = (j * (360.0f / steps)) * M_PI / 180.0f; + const float azimuthAngle2 = ((j + 1) * (360.0f / steps)) * M_PI / 180.0f; + const float x1 = segment * (i - 1) * sin(azimuthAngle); + const float x2 = segment * i * sin(azimuthAngle); + const float x3 = segment * (i - 1) * sin(azimuthAngle2); + const float x4 = segment * i * sin(azimuthAngle2); + const float z1 = segment * (i - 1) * cos(azimuthAngle); + const float z2 = segment * i * cos(azimuthAngle); + const float z3 = segment * (i - 1) * cos(azimuthAngle2); + const float z4 = segment * i * cos(azimuthAngle2); + if (texture) { + if (capTexture->isYInverted()) { + texCoords << (radius + x1) / (radius * 2.0f) << (z1 + radius) / (radius * 2.0f); + texCoords << (radius + x2) / (radius * 2.0f) << (z2 + radius) / (radius * 2.0f); + texCoords << (radius + x3) / (radius * 2.0f) << (z3 + radius) / (radius * 2.0f); + texCoords << (radius + x4) / (radius * 2.0f) << (z4 + radius) / (radius * 2.0f); + texCoords << (radius + x3) / (radius * 2.0f) << (z3 + radius) / (radius * 2.0f); + texCoords << (radius + x2) / (radius * 2.0f) << (z2 + radius) / (radius * 2.0f); + } else { + texCoords << (radius + x1) / (radius * 2.0f) << 1.0f - (z1 + radius) / (radius * 2.0f); + texCoords << (radius + x2) / (radius * 2.0f) << 1.0f - (z2 + radius) / (radius * 2.0f); + texCoords << (radius + x3) / (radius * 2.0f) << 1.0f - (z3 + radius) / (radius * 2.0f); + texCoords << (radius + x4) / (radius * 2.0f) << 1.0f - (z4 + radius) / (radius * 2.0f); + texCoords << (radius + x3) / (radius * 2.0f) << 1.0f - (z3 + radius) / (radius * 2.0f); + texCoords << (radius + x2) / (radius * 2.0f) << 1.0f - (z2 + radius) / (radius * 2.0f); + } + } + verts << x1 << 0.0 << z1; + verts << x2 << 0.0 << z2; + verts << x3 << 0.0 << z3; + verts << x4 << 0.0 << z4; + verts << x3 << 0.0 << z3; + verts << x2 << 0.0 << z2; + } + } + delete m_cubeCapBuffer; + m_cubeCapBuffer = new GLVertexBuffer(GLVertexBuffer::Static); + m_cubeCapBuffer->setData(verts.count() / 3, 3, verts.constData(), texture ? texCoords.constData() : nullptr); +} + +void CubeEffect::paintSphereCap() +{ + QRect rect = effects->clientArea(FullArea, activeScreen, effects->currentDesktop()); + float cubeAngle = (float)((float)(effects->numberOfDesktops() - 2) / (float)effects->numberOfDesktops() * 180.0f); + float zTexture = rect.width() / 2 * tan(45.0f * M_PI / 180.0f); + float radius = (rect.width() * 0.5) / cos(cubeAngle * 0.5 * M_PI / 180.0); + float angle = acos((rect.height() * 0.5) / radius) * 180.0 / M_PI; + angle /= 30; + bool texture = texturedCaps && effects->numberOfDesktops() > 3 && capTexture; + QVector verts; + QVector texCoords; + for (int i = 0; i < 30; i++) { + float topAngle = angle * i * M_PI / 180.0; + float bottomAngle = angle * (i + 1) * M_PI / 180.0; + float yTop = (rect.height() * 0.5 - radius * cos(topAngle)); + yTop *= (1.0f-capDeformationFactor); + float yBottom = rect.height() * 0.5 - radius * cos(bottomAngle); + yBottom *= (1.0f - capDeformationFactor); + for (int j = 0; j < 36; j++) { + const float x1 = radius * sin(topAngle) * sin((90.0 + j * 10.0) * M_PI / 180.0); + const float z1 = radius * sin(topAngle) * cos((90.0 + j * 10.0) * M_PI / 180.0); + const float x2 = radius * sin(bottomAngle) * sin((90.0 + j * 10.0) * M_PI / 180.00); + const float z2 = radius * sin(bottomAngle) * cos((90.0 + j * 10.0) * M_PI / 180.0); + const float x3 = radius * sin(bottomAngle) * sin((90.0 + (j + 1) * 10.0) * M_PI / 180.0); + const float z3 = radius * sin(bottomAngle) * cos((90.0 + (j + 1) * 10.0) * M_PI / 180.0); + const float x4 = radius * sin(topAngle) * sin((90.0 + (j + 1) * 10.0) * M_PI / 180.0); + const float z4 = radius * sin(topAngle) * cos((90.0 + (j + 1) * 10.0) * M_PI / 180.0); + if (texture) { + if (capTexture->isYInverted()) { + texCoords << x4 / (rect.width()) + 0.5 << 0.5 + z4 / zTexture * 0.5; + texCoords << x1 / (rect.width()) + 0.5 << 0.5 + z1 / zTexture * 0.5; + texCoords << x2 / (rect.width()) + 0.5 << 0.5 + z2 / zTexture * 0.5; + texCoords << x2 / (rect.width()) + 0.5 << 0.5 + z2 / zTexture * 0.5; + texCoords << x3 / (rect.width()) + 0.5 << 0.5 + z3 / zTexture * 0.5; + texCoords << x4 / (rect.width()) + 0.5 << 0.5 + z4 / zTexture * 0.5; + } else { + texCoords << x4 / (rect.width()) + 0.5 << 0.5 - z4 / zTexture * 0.5; + texCoords << x1 / (rect.width()) + 0.5 << 0.5 - z1 / zTexture * 0.5; + texCoords << x2 / (rect.width()) + 0.5 << 0.5 - z2 / zTexture * 0.5; + texCoords << x2 / (rect.width()) + 0.5 << 0.5 - z2 / zTexture * 0.5; + texCoords << x3 / (rect.width()) + 0.5 << 0.5 - z3 / zTexture * 0.5; + texCoords << x4 / (rect.width()) + 0.5 << 0.5 - z4 / zTexture * 0.5; + } + } + verts << x4 << yTop << z4; + verts << x1 << yTop << z1; + verts << x2 << yBottom << z2; + verts << x2 << yBottom << z2; + verts << x3 << yBottom << z3; + verts << x4 << yTop << z4; + } + } + delete m_cubeCapBuffer; + m_cubeCapBuffer = new GLVertexBuffer(GLVertexBuffer::Static); + m_cubeCapBuffer->setData(verts.count() / 3, 3, verts.constData(), texture ? texCoords.constData() : nullptr); +} + +void CubeEffect::postPaintScreen() +{ + effects->postPaintScreen(); + if (!activated) + return; + + bool animation = (animationState != AnimationState::None || verticalAnimationState != VerticalAnimationState::None); + if (animationState != AnimationState::None && timeLine.done()) { + /* An animation have just finished! */ + if (animationState == AnimationState::Stop) { + /* If the stop animation is finished, we're done */ + if (keyboard_grab) + effects->ungrabKeyboard(); + keyboard_grab = false; + effects->stopMouseInterception(this); + effects->setCurrentDesktop(frontDesktop); + effects->setActiveFullScreenEffect(nullptr); + delete m_cubeCapBuffer; + m_cubeCapBuffer = nullptr; + if (desktopNameFrame) + desktopNameFrame->free(); + activated = false; + // User can press Esc several times, and several Stop animations can be added to queue. We don't want it + animationState = AnimationState::None; + animations.clear(); + verticalAnimationState = VerticalAnimationState::None; + verticalAnimations.clear(); + } else { + if (!animations.empty()) + startAnimation(animations.dequeue()); + else + animationState = AnimationState::None; + } + } + /* Vertical animation have finished */ + if (verticalAnimationState != VerticalAnimationState::None && verticalTimeLine.done()) { + if (!verticalAnimations.empty()) { + startVerticalAnimation(verticalAnimations.dequeue()); + } else { + verticalAnimationState = VerticalAnimationState::None; + } + } + /* Repaint if there is any animation */ + if (animation) { + effects->addRepaintFull(); + } +} + +void CubeEffect::prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time) +{ + if (activated) { + if (cube_painting) { + if (mode == Cylinder || mode == Sphere) { + int leftDesktop = frontDesktop - 1; + int rightDesktop = frontDesktop + 1; + if (leftDesktop == 0) + leftDesktop = effects->numberOfDesktops(); + if (rightDesktop > effects->numberOfDesktops()) + rightDesktop = 1; + if (painting_desktop == frontDesktop) + data.quads = data.quads.makeGrid(40); + else if (painting_desktop == leftDesktop || painting_desktop == rightDesktop) + data.quads = data.quads.makeGrid(100); + else + data.quads = data.quads.makeGrid(250); + } + if (w->isOnDesktop(painting_desktop)) { + QRect rect = effects->clientArea(FullArea, activeScreen, painting_desktop); + if (w->x() < rect.x()) { + data.quads = data.quads.splitAtX(-w->x()); + } + if (w->x() + w->width() > rect.x() + rect.width()) { + data.quads = data.quads.splitAtX(rect.width() - w->x()); + } + if (w->y() < rect.y()) { + data.quads = data.quads.splitAtY(-w->y()); + } + if (w->y() + w->height() > rect.y() + rect.height()) { + data.quads = data.quads.splitAtY(rect.height() - w->y()); + } + if (useZOrdering && !w->isDesktop() && !w->isDock() && !w->isOnAllDesktops()) + data.setTransformed(); + w->enablePainting(EffectWindow::PAINT_DISABLED_BY_DESKTOP); + } else { + // check for windows belonging to the previous desktop + int prev_desktop = painting_desktop - 1; + if (prev_desktop == 0) + prev_desktop = effects->numberOfDesktops(); + if (w->isOnDesktop(prev_desktop) && mode == Cube && !useZOrdering) { + QRect rect = effects->clientArea(FullArea, activeScreen, prev_desktop); + if (w->x() + w->width() > rect.x() + rect.width()) { + w->enablePainting(EffectWindow::PAINT_DISABLED_BY_DESKTOP); + data.quads = data.quads.splitAtX(rect.width() - w->x()); + if (w->y() < rect.y()) { + data.quads = data.quads.splitAtY(-w->y()); + } + if (w->y() + w->height() > rect.y() + rect.height()) { + data.quads = data.quads.splitAtY(rect.height() - w->y()); + } + data.setTransformed(); + effects->prePaintWindow(w, data, time); + return; + } + } + // check for windows belonging to the next desktop + int next_desktop = painting_desktop + 1; + if (next_desktop > effects->numberOfDesktops()) + next_desktop = 1; + if (w->isOnDesktop(next_desktop) && mode == Cube && !useZOrdering) { + QRect rect = effects->clientArea(FullArea, activeScreen, next_desktop); + if (w->x() < rect.x()) { + w->enablePainting(EffectWindow::PAINT_DISABLED_BY_DESKTOP); + data.quads = data.quads.splitAtX(-w->x()); + if (w->y() < rect.y()) { + data.quads = data.quads.splitAtY(-w->y()); + } + if (w->y() + w->height() > rect.y() + rect.height()) { + data.quads = data.quads.splitAtY(rect.height() - w->y()); + } + data.setTransformed(); + effects->prePaintWindow(w, data, time); + return; + } + } + w->disablePainting(EffectWindow::PAINT_DISABLED_BY_DESKTOP); + } + } + } + effects->prePaintWindow(w, data, time); +} + +void CubeEffect::paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data) +{ + ShaderManager *shaderManager = ShaderManager::instance(); + if (activated && cube_painting) { + region= infiniteRegion(); // we need to explicitly prevent any clipping, bug #325432 + //qCDebug(KWINEFFECTS) << w->caption(); + float opacity = cubeOpacity; + if (animationState == AnimationState::Start) { + opacity = 1.0 - (1.0 - opacity) * timeLine.value(); + if (reflectionPainting) + opacity = 0.5 + (cubeOpacity - 0.5) * timeLine.value(); + // fade in windows belonging to different desktops + if (painting_desktop == effects->currentDesktop() && (!w->isOnDesktop(painting_desktop))) + opacity = timeLine.value() * cubeOpacity; + } else if (animationState == AnimationState::Stop) { + opacity = 1.0 - (1.0 - opacity) * (1.0 - timeLine.value()); + if (reflectionPainting) + opacity = 0.5 + (cubeOpacity - 0.5) * (1.0 - timeLine.value()); + // fade out windows belonging to different desktops + if (painting_desktop == effects->currentDesktop() && (!w->isOnDesktop(painting_desktop))) + opacity = cubeOpacity * (1.0 - timeLine.value()); + } + // z-Ordering + if (!w->isDesktop() && !w->isDock() && useZOrdering && !w->isOnAllDesktops()) { + float zOrdering = (effects->stackingOrder().indexOf(w) + 1) * zOrderingFactor; + if (animationState == AnimationState::Start) { + zOrdering *= timeLine.value(); + } else if (animationState == AnimationState::Stop) { + zOrdering *= (1.0 - timeLine.value()); + } + data.translate(0.0, 0.0, zOrdering); + } + // check for windows belonging to the previous desktop + int prev_desktop = painting_desktop - 1; + if (prev_desktop == 0) + prev_desktop = effects->numberOfDesktops(); + int next_desktop = painting_desktop + 1; + if (next_desktop > effects->numberOfDesktops()) + next_desktop = 1; + if (w->isOnDesktop(prev_desktop) && (mask & PAINT_WINDOW_TRANSFORMED)) { + QRect rect = effects->clientArea(FullArea, activeScreen, prev_desktop); + WindowQuadList new_quads; + foreach (const WindowQuad & quad, data.quads) { + if (quad.right() > rect.width() - w->x()) { + new_quads.append(quad); + } + } + data.quads = new_quads; + data.setXTranslation(-rect.width()); + } + if (w->isOnDesktop(next_desktop) && (mask & PAINT_WINDOW_TRANSFORMED)) { + QRect rect = effects->clientArea(FullArea, activeScreen, next_desktop); + WindowQuadList new_quads; + foreach (const WindowQuad & quad, data.quads) { + if (w->x() + quad.right() <= rect.x()) { + new_quads.append(quad); + } + } + data.quads = new_quads; + data.setXTranslation(rect.width()); + } + QRect rect = effects->clientArea(FullArea, activeScreen, painting_desktop); + + if (animationState == AnimationState::Start || animationState == AnimationState::Stop) { + // we have to change opacity values for fade in/out of windows which are shown on front-desktop + if (prev_desktop == effects->currentDesktop() && w->x() < rect.x()) { + if (animationState == AnimationState::Start) { + opacity = timeLine.value() * cubeOpacity; + } else if (animationState == AnimationState::Stop) { + opacity = cubeOpacity * (1.0 - timeLine.value()); + } + } + if (next_desktop == effects->currentDesktop() && w->x() + w->width() > rect.x() + rect.width()) { + if (animationState == AnimationState::Start) { + opacity = timeLine.value() * cubeOpacity; + } else if (animationState == AnimationState::Stop) { + opacity = cubeOpacity * (1.0 - timeLine.value()); + } + } + } + // HACK set opacity to 0.99 in case of fully opaque to ensure that windows are painted in correct sequence + // bug #173214 + if (opacity > 0.99f) + opacity = 0.99f; + if (opacityDesktopOnly && !w->isDesktop()) + opacity = 0.99f; + data.multiplyOpacity(opacity); + + if (w->isOnDesktop(painting_desktop) && w->x() < rect.x()) { + WindowQuadList new_quads; + foreach (const WindowQuad & quad, data.quads) { + if (quad.right() > -w->x()) { + new_quads.append(quad); + } + } + data.quads = new_quads; + } + if (w->isOnDesktop(painting_desktop) && w->x() + w->width() > rect.x() + rect.width()) { + WindowQuadList new_quads; + foreach (const WindowQuad & quad, data.quads) { + if (quad.right() <= rect.width() - w->x()) { + new_quads.append(quad); + } + } + data.quads = new_quads; + } + if (w->y() < rect.y()) { + WindowQuadList new_quads; + foreach (const WindowQuad & quad, data.quads) { + if (quad.bottom() > -w->y()) { + new_quads.append(quad); + } + } + data.quads = new_quads; + } + if (w->y() + w->height() > rect.y() + rect.height()) { + WindowQuadList new_quads; + foreach (const WindowQuad & quad, data.quads) { + if (quad.bottom() <= rect.height() - w->y()) { + new_quads.append(quad); + } + } + data.quads = new_quads; + } + GLShader *currentShader = nullptr; + if (mode == Cylinder) { + shaderManager->pushShader(cylinderShader); + cylinderShader->setUniform("xCoord", (float)w->x()); + cylinderShader->setUniform("cubeAngle", (effects->numberOfDesktops() - 2) / (float)effects->numberOfDesktops() * 90.0f); + float factor = 0.0f; + if (animationState == AnimationState::Start) { + factor = 1.0f - timeLine.value(); + } else if (animationState == AnimationState::Stop) { + factor = timeLine.value(); + } + cylinderShader->setUniform("timeLine", factor); + currentShader = cylinderShader; + } + if (mode == Sphere) { + shaderManager->pushShader(sphereShader); + sphereShader->setUniform("u_offset", QVector2D(w->x(), w->y())); + sphereShader->setUniform("cubeAngle", (effects->numberOfDesktops() - 2) / (float)effects->numberOfDesktops() * 90.0f); + float factor = 0.0f; + if (animationState == AnimationState::Start) { + factor = 1.0f - timeLine.value(); + } else if (animationState == AnimationState::Stop) { + factor = timeLine.value(); + } + sphereShader->setUniform("timeLine", factor); + currentShader = sphereShader; + } + if (currentShader) { + data.shader = currentShader; + } + data.setProjectionMatrix(data.screenProjectionMatrix()); + if (reflectionPainting) { + data.setModelViewMatrix(m_reflectionMatrix * m_rotationMatrix * m_currentFaceMatrix); + } else { + data.setModelViewMatrix(m_rotationMatrix * m_currentFaceMatrix); + } + } + effects->paintWindow(w, mask, region, data); + if (activated && cube_painting) { + if (mode == Cylinder || mode == Sphere) { + shaderManager->popShader(); + } + if (w->isDesktop() && effects->numScreens() > 1 && paintCaps) { + QRect rect = effects->clientArea(FullArea, activeScreen, painting_desktop); + QRegion paint = QRegion(rect); + for (int i = 0; i < effects->numScreens(); i++) { + if (i == w->screen()) + continue; + paint = paint.subtracted(QRegion(effects->clientArea(ScreenArea, i, painting_desktop))); + } + paint = paint.subtracted(QRegion(w->geometry())); + // in case of free area in multiscreen setup fill it with cap color + if (!paint.isEmpty()) { + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + QVector verts; + float quadSize = 0.0f; + int leftDesktop = frontDesktop - 1; + int rightDesktop = frontDesktop + 1; + if (leftDesktop == 0) + leftDesktop = effects->numberOfDesktops(); + if (rightDesktop > effects->numberOfDesktops()) + rightDesktop = 1; + if (painting_desktop == frontDesktop) + quadSize = 100.0f; + else if (painting_desktop == leftDesktop || painting_desktop == rightDesktop) + quadSize = 150.0f; + else + quadSize = 250.0f; + for (const QRect &paintRect : paint) { + for (int i = 0; i <= (paintRect.height() / quadSize); i++) { + for (int j = 0; j <= (paintRect.width() / quadSize); j++) { + verts << qMin(paintRect.x() + (j + 1)*quadSize, (float)paintRect.x() + paintRect.width()) << paintRect.y() + i*quadSize; + verts << paintRect.x() + j*quadSize << paintRect.y() + i*quadSize; + verts << paintRect.x() + j*quadSize << qMin(paintRect.y() + (i + 1)*quadSize, (float)paintRect.y() + paintRect.height()); + verts << paintRect.x() + j*quadSize << qMin(paintRect.y() + (i + 1)*quadSize, (float)paintRect.y() + paintRect.height()); + verts << qMin(paintRect.x() + (j + 1)*quadSize, (float)paintRect.x() + paintRect.width()) << qMin(paintRect.y() + (i + 1)*quadSize, (float)paintRect.y() + paintRect.height()); + verts << qMin(paintRect.x() + (j + 1)*quadSize, (float)paintRect.x() + paintRect.width()) << paintRect.y() + i*quadSize; + } + } + } + bool capShader = false; + if (effects->compositingType() == OpenGL2Compositing && m_capShader && m_capShader->isValid()) { + capShader = true; + ShaderManager::instance()->pushShader(m_capShader); + m_capShader->setUniform("u_mirror", 0); + m_capShader->setUniform("u_untextured", 1); + QMatrix4x4 mvp = data.screenProjectionMatrix(); + if (reflectionPainting) { + mvp = mvp * m_reflectionMatrix * m_rotationMatrix * m_currentFaceMatrix; + } else { + mvp = mvp * m_rotationMatrix * m_currentFaceMatrix; + } + m_capShader->setUniform(GLShader::ModelViewProjectionMatrix, mvp); + } + GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer(); + vbo->reset(); + QColor color = capColor; + capColor.setAlphaF(cubeOpacity); + vbo->setColor(color); + vbo->setData(verts.size() / 2, 2, verts.constData(), nullptr); + if (!capShader || mode == Cube) { + // TODO: use sphere and cylinder shaders + vbo->render(GL_TRIANGLES); + } + if (capShader) { + ShaderManager::instance()->popShader(); + } + glDisable(GL_BLEND); + } + } + } +} + +bool CubeEffect::borderActivated(ElectricBorder border) +{ + if (!borderActivate.contains(border) && + !borderActivateCylinder.contains(border) && + !borderActivateSphere.contains(border)) + return false; + if (effects->activeFullScreenEffect() && effects->activeFullScreenEffect() != this) + return false; + if (borderActivate.contains(border)) { + if (!activated || (activated && mode == Cube)) + toggleCube(); + else + return false; + } + if (borderActivateCylinder.contains(border)) { + if (!activated || (activated && mode == Cylinder)) + toggleCylinder(); + else + return false; + } + if (borderActivateSphere.contains(border)) { + if (!activated || (activated && mode == Sphere)) + toggleSphere(); + else + return false; + } + return true; +} + +void CubeEffect::toggleCube() +{ + qCDebug(KWINEFFECTS) << "toggle cube"; + toggle(Cube); +} + +void CubeEffect::toggleCylinder() +{ + qCDebug(KWINEFFECTS) << "toggle cylinder"; + if (!useShaders) + useShaders = loadShader(); + if (useShaders) + toggle(Cylinder); +} + +void CubeEffect::toggleSphere() +{ + qCDebug(KWINEFFECTS) << "toggle sphere"; + if (!useShaders) + useShaders = loadShader(); + if (useShaders) + toggle(Sphere); +} + +void CubeEffect::toggle(CubeMode newMode) +{ + if ((effects->activeFullScreenEffect() && effects->activeFullScreenEffect() != this) || + effects->numberOfDesktops() < 2) + return; + if (!activated) { + mode = newMode; + setActive(true); + } else { + setActive(false); + } +} + +void CubeEffect::grabbedKeyboardEvent(QKeyEvent* e) +{ + // If either stop is running or is scheduled - ignore all events + if ((!animations.isEmpty() && animations.last() == AnimationState::Stop) || animationState == AnimationState::Stop) { + return; + } + // taken from desktopgrid.cpp + if (e->type() == QEvent::KeyPress) { + // check for global shortcuts + // HACK: keyboard grab disables the global shortcuts so we have to check for global shortcut (bug 156155) + if (mode == Cube && cubeShortcut.contains(e->key() + e->modifiers())) { + toggleCube(); + return; + } + if (mode == Cylinder && cylinderShortcut.contains(e->key() + e->modifiers())) { + toggleCylinder(); + return; + } + if (mode == Sphere && sphereShortcut.contains(e->key() + e->modifiers())) { + toggleSphere(); + return; + } + + int desktop = -1; + // switch by F or just + if (e->key() >= Qt::Key_F1 && e->key() <= Qt::Key_F35) + desktop = e->key() - Qt::Key_F1 + 1; + else if (e->key() >= Qt::Key_0 && e->key() <= Qt::Key_9) + desktop = e->key() == Qt::Key_0 ? 10 : e->key() - Qt::Key_0; + if (desktop != -1) { + if (desktop <= effects->numberOfDesktops()) { + // we have to rotate to chosen desktop + // and end effect when rotation finished + rotateToDesktop(desktop); + setActive(false); + } + return; + } + + int key = e->key(); + if (invertKeys) { + if (key == Qt::Key_Left) + key = Qt::Key_Right; + else if (key == Qt::Key_Right) + key = Qt::Key_Left; + else if (key == Qt::Key_Up) + key = Qt::Key_Down; + else if (key == Qt::Key_Down) + key = Qt::Key_Up; + } + + switch(key) { + // wrap only on autorepeat + case Qt::Key_Left: + qCDebug(KWINEFFECTS) << "left"; + if (animations.count() < effects->numberOfDesktops()) + animations.enqueue(AnimationState::Left); + break; + case Qt::Key_Right: + qCDebug(KWINEFFECTS) << "right"; + if (animations.count() < effects->numberOfDesktops()) + animations.enqueue(AnimationState::Right); + break; + case Qt::Key_Up: + qCDebug(KWINEFFECTS) << "up"; + verticalAnimations.enqueue(VerticalAnimationState::Upwards); + break; + case Qt::Key_Down: + qCDebug(KWINEFFECTS) << "down"; + verticalAnimations.enqueue(VerticalAnimationState::Downwards); + break; + case Qt::Key_Escape: + rotateToDesktop(effects->currentDesktop()); + setActive(false); + return; + case Qt::Key_Enter: + case Qt::Key_Return: + case Qt::Key_Space: + setActive(false); + return; + case Qt::Key_Plus: + case Qt::Key_Equal: + zoom -= 10.0; + zoom = qMax(-zPosition, zoom); + rotateCube(); + break; + case Qt::Key_Minus: + zoom += 10.0f; + rotateCube(); + break; + default: + break; + } + } + effects->addRepaintFull(); +} + +void CubeEffect::rotateToDesktop(int desktop) +{ + // all scheduled animations will be removed as a speed up + animations.clear(); + verticalAnimations.clear(); + // we want only startAnimation to finish gracefully + // all the others can be interrupted + if (animationState != AnimationState::Start) { + animationState = AnimationState::None; + } + verticalAnimationState = VerticalAnimationState::None; + // find the fastest rotation path from frontDesktop to desktop + int rightRotations = frontDesktop - desktop; + if (rightRotations < 0) { + rightRotations += effects->numberOfDesktops(); + } + int leftRotations = desktop - frontDesktop; + if (leftRotations < 0) { + leftRotations += effects->numberOfDesktops(); + } + if (leftRotations <= rightRotations) { + for (int i = 0; i < leftRotations; i++) { + animations.enqueue(AnimationState::Left); + } + } else { + for (int i = 0; i < rightRotations; i++) { + animations.enqueue(AnimationState::Right); + } + } + // we want the face of desktop to appear, it might need also vertical animation + if (verticalCurrentAngle > 0.0f) { + verticalAnimations.enqueue(VerticalAnimationState::Downwards); + } + if (verticalCurrentAngle < 0.0f) { + verticalAnimations.enqueue(VerticalAnimationState::Upwards); + } + /* Start immediately, so there is no pause: + * during that pause, actual frontDesktop might change + * if user moves his mouse fast, leading to incorrect desktop */ + if (animationState == AnimationState::None && !animations.empty()) { + startAnimation(animations.dequeue()); + } + if (verticalAnimationState == VerticalAnimationState::None && !verticalAnimations.empty()) { + startVerticalAnimation(verticalAnimations.dequeue()); + } +} + +void CubeEffect::setActive(bool active) +{ + foreach (CubeInsideEffect * inside, m_cubeInsideEffects) { + inside->setActive(true); + } + if (active) { + QString capPath = CubeConfig::capPath(); + if (texturedCaps && !capTexture && !capPath.isEmpty()) { + QFutureWatcher *watcher = new QFutureWatcher(this); + connect(watcher, &QFutureWatcher::finished, this, &CubeEffect::slotCubeCapLoaded); + watcher->setFuture(QtConcurrent::run(this, &CubeEffect::loadCubeCap, capPath)); + } + QString wallpaperPath = CubeConfig::wallpaper().toLocalFile(); + if (!wallpaper && !wallpaperPath.isEmpty()) { + QFutureWatcher *watcher = new QFutureWatcher(this); + connect(watcher, &QFutureWatcher::finished, this, &CubeEffect::slotWallPaperLoaded); + watcher->setFuture(QtConcurrent::run(this, &CubeEffect::loadWallPaper, wallpaperPath)); + } + activated = true; + activeScreen = effects->activeScreen(); + keyboard_grab = effects->grabKeyboard(this); + effects->startMouseInterception(this, Qt::OpenHandCursor); + frontDesktop = effects->currentDesktop(); + zoom = 0.0; + zOrderingFactor = zPosition / (effects->stackingOrder().count() - 1); + animations.enqueue(AnimationState::Start); + animationState = AnimationState::None; + verticalAnimationState = VerticalAnimationState::None; + effects->setActiveFullScreenEffect(this); + qCDebug(KWINEFFECTS) << "Cube is activated"; + currentAngle = 0.0; + verticalCurrentAngle = 0.0; + if (reflection) { + QRect rect = effects->clientArea(FullArea, activeScreen, effects->currentDesktop()); + float temporaryCoeff = float(rect.width()) / tan(M_PI / float(effects->numberOfDesktops())); + mAddedHeightCoeff1 = sqrt(float(rect.height()) * float(rect.height()) + temporaryCoeff * temporaryCoeff); + mAddedHeightCoeff2 = sqrt(float(rect.height()) * float(rect.height()) + float(rect.width()) * float(rect.width()) + temporaryCoeff * temporaryCoeff); + } + m_rotationMatrix.setToIdentity(); + } else { + animations.enqueue(AnimationState::Stop); + } + effects->addRepaintFull(); +} + +void CubeEffect::windowInputMouseEvent(QEvent* e) +{ + if (!activated) + return; + if (tabBoxMode) + return; + if ((!animations.isEmpty() && animations.last() == AnimationState::Stop) || animationState == AnimationState::Stop) + return; + + QMouseEvent *mouse = dynamic_cast< QMouseEvent* >(e); + if (!mouse) + return; + + static QPoint oldpos; + static QElapsedTimer dblClckTime; + static int dblClckCounter(0); + if (mouse->type() == QEvent::MouseMove && mouse->buttons().testFlag(Qt::LeftButton)) { + const QPoint pos = mouse->pos(); + QRect rect = effects->clientArea(FullArea, activeScreen, effects->currentDesktop()); + bool repaint = false; + // vertical movement only if there is not a rotation + if (verticalAnimationState == VerticalAnimationState::None) { + // display height corresponds to 180* + int deltaY = pos.y() - oldpos.y(); + float deltaVerticalDegrees = (float)deltaY / rect.height() * 180.0f; + if (invertMouse) + verticalCurrentAngle += deltaVerticalDegrees; + else + verticalCurrentAngle -= deltaVerticalDegrees; + // don't get too excited + verticalCurrentAngle = qBound(-90.0f, verticalCurrentAngle, 90.0f); + + if (deltaVerticalDegrees != 0.0) + repaint = true; + } + // horizontal movement only if there is not a rotation + if (animationState == AnimationState::None) { + // display width corresponds to sum of angles of the polyhedron + int deltaX = oldpos.x() - pos.x(); + float deltaDegrees = (float)deltaX / rect.width() * 360.0f; + if (deltaX == 0) { + if (pos.x() == 0) + deltaDegrees = 5.0f; + if (pos.x() == rect.width() - 1) + deltaDegrees = -5.0f; + } + if (invertMouse) + currentAngle += deltaDegrees; + else + currentAngle -= deltaDegrees; + if (deltaDegrees != 0.0) + repaint = true; + } + if (repaint) { + rotateCube(); + effects->addRepaintFull(); + } + oldpos = pos; + } + + else if (mouse->type() == QEvent::MouseButtonPress && mouse->button() == Qt::LeftButton) { + oldpos = mouse->pos(); + if (dblClckTime.elapsed() > QApplication::doubleClickInterval()) + dblClckCounter = 0; + if (!dblClckCounter) + dblClckTime.start(); + } + + else if (mouse->type() == QEvent::MouseButtonRelease) { + effects->defineCursor(Qt::OpenHandCursor); + if (mouse->button() == Qt::LeftButton && ++dblClckCounter == 2) { + dblClckCounter = 0; + if (dblClckTime.elapsed() < QApplication::doubleClickInterval()) { + setActive(false); + return; + } + } + else if (mouse->button() == Qt::XButton1) { + if (animations.count() < effects->numberOfDesktops()) { + if (invertMouse) + animations.enqueue(AnimationState::Right); + else + animations.enqueue(AnimationState::Left); + } + effects->addRepaintFull(); + } else if (mouse->button() == Qt::XButton2) { + if (animations.count() < effects->numberOfDesktops()) { + if (invertMouse) + animations.enqueue(AnimationState::Left); + else + animations.enqueue(AnimationState::Right); + } + effects->addRepaintFull(); + } else if (mouse->button() == Qt::RightButton || (mouse->button() == Qt::LeftButton && closeOnMouseRelease)) { + setActive(false); + } + } +} + +void CubeEffect::slotTabBoxAdded(int mode) +{ + if (activated) + return; + if (effects->activeFullScreenEffect() && effects->activeFullScreenEffect() != this) + return; + if (useForTabBox && mode == TabBoxDesktopListMode) { + effects->refTabBox(); + tabBoxMode = true; + setActive(true); + rotateToDesktop(effects->currentTabBoxDesktop()); + } +} + +void CubeEffect::slotTabBoxUpdated() +{ + if (activated) { + rotateToDesktop(effects->currentTabBoxDesktop()); + effects->addRepaintFull(); + } +} + +void CubeEffect::slotTabBoxClosed() +{ + if (activated) { + effects->unrefTabBox(); + tabBoxMode = false; + setActive(false); + } +} + +void CubeEffect::globalShortcutChanged(QAction *action, const QKeySequence &seq) +{ + if (action->objectName() == QStringLiteral("Cube")) { + cubeShortcut.clear(); + cubeShortcut.append(seq); + } else if (action->objectName() == QStringLiteral("Cylinder")) { + cylinderShortcut.clear(); + cylinderShortcut.append(seq); + } else if (action->objectName() == QStringLiteral("Sphere")) { + sphereShortcut.clear(); + sphereShortcut.append(seq); + } +} + +void* CubeEffect::proxy() +{ + return &m_proxy; +} + +void CubeEffect::registerCubeInsideEffect(CubeInsideEffect* effect) +{ + m_cubeInsideEffects.append(effect); +} + +void CubeEffect::unregisterCubeInsideEffect(CubeInsideEffect* effect) +{ + m_cubeInsideEffects.removeAll(effect); +} + +bool CubeEffect::isActive() const +{ + return activated && !effects->isScreenLocked(); +} + +} // namespace diff --git a/effects/cube/cube.h b/effects/cube/cube.h new file mode 100644 index 0000000..026f08c --- /dev/null +++ b/effects/cube/cube.h @@ -0,0 +1,252 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_CUBE_H +#define KWIN_CUBE_H + +#include +#include +#include +#include +#include +#include +#include "cube_inside.h" +#include "cube_proxy.h" + +namespace KWin +{ + +class CubeEffect + : public Effect +{ + Q_OBJECT + Q_PROPERTY(qreal cubeOpacity READ configuredCubeOpacity) + Q_PROPERTY(bool opacityDesktopOnly READ isOpacityDesktopOnly) + Q_PROPERTY(bool displayDesktopName READ isDisplayDesktopName) + Q_PROPERTY(bool reflection READ isReflection) + Q_PROPERTY(int rotationDuration READ configuredRotationDuration) + Q_PROPERTY(QColor backgroundColor READ configuredBackgroundColor) + Q_PROPERTY(QColor capColor READ configuredCapColor) + Q_PROPERTY(bool paintCaps READ isPaintCaps) + Q_PROPERTY(bool closeOnMouseRelease READ isCloseOnMouseRelease) + Q_PROPERTY(qreal zPosition READ configuredZPosition) + Q_PROPERTY(bool useForTabBox READ isUseForTabBox) + Q_PROPERTY(bool invertKeys READ isInvertKeys) + Q_PROPERTY(bool invertMouse READ isInvertMouse) + Q_PROPERTY(qreal capDeformationFactor READ configuredCapDeformationFactor) + Q_PROPERTY(bool useZOrdering READ isUseZOrdering) + Q_PROPERTY(bool texturedCaps READ isTexturedCaps) + // TODO: electric borders: not a registered type +public: + CubeEffect(); + ~CubeEffect() override; + void reconfigure(ReconfigureFlags) override; + void prePaintScreen(ScreenPrePaintData& data, int time) override; + void paintScreen(int mask, const QRegion ®ion, ScreenPaintData& data) override; + void postPaintScreen() override; + void prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time) override; + void paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data) override; + bool borderActivated(ElectricBorder border) override; + void grabbedKeyboardEvent(QKeyEvent* e) override; + void windowInputMouseEvent(QEvent* e) override; + bool isActive() const override; + + int requestedEffectChainPosition() const override { + return 50; + } + + // proxy functions + void* proxy() override; + void registerCubeInsideEffect(CubeInsideEffect* effect); + void unregisterCubeInsideEffect(CubeInsideEffect* effect); + + static bool supported(); + + // for properties + qreal configuredCubeOpacity() const { + return cubeOpacity; + } + bool isOpacityDesktopOnly() const { + return opacityDesktopOnly; + } + bool isDisplayDesktopName() const { + return displayDesktopName; + } + bool isReflection() const { + return reflection; + } + int configuredRotationDuration() const { + return rotationDuration.count(); + } + QColor configuredBackgroundColor() const { + return backgroundColor; + } + QColor configuredCapColor() const { + return capColor; + } + bool isPaintCaps() const { + return paintCaps; + } + bool isCloseOnMouseRelease() const { + return closeOnMouseRelease; + } + qreal configuredZPosition() const { + return zPosition; + } + bool isUseForTabBox() const { + return useForTabBox; + } + bool isInvertKeys() const { + return invertKeys; + } + bool isInvertMouse() const { + return invertMouse; + } + qreal configuredCapDeformationFactor() const { + return capDeformationFactor; + } + bool isUseZOrdering() const { + return useZOrdering; + } + bool isTexturedCaps() const { + return texturedCaps; + } +private Q_SLOTS: + void toggleCube(); + void toggleCylinder(); + void toggleSphere(); + // slots for global shortcut changed + // needed to toggle the effect + void globalShortcutChanged(QAction *action, const QKeySequence &seq); + void slotTabBoxAdded(int mode); + void slotTabBoxUpdated(); + void slotTabBoxClosed(); + void slotCubeCapLoaded(); + void slotWallPaperLoaded(); +private: + enum class AnimationState { + None, + Start, + Stop, + Left, + Right + }; + enum class VerticalAnimationState { + None, + Upwards, + Downwards + }; + enum CubeMode { + Cube, + Cylinder, + Sphere + }; + void toggle(CubeMode newMode = Cube); + void paintCube(int mask, QRegion region, ScreenPaintData& data); + void paintCap(bool frontFirst, float zOffset, const QMatrix4x4 &projection); + void paintCubeCap(); + void paintCylinderCap(); + void paintSphereCap(); + bool loadShader(); + void rotateCube(); + void rotateToDesktop(int desktop); + void setActive(bool active); + QImage loadCubeCap(const QString &capPath); + QImage loadWallPaper(const QString &file); + void startAnimation(AnimationState state); + void startVerticalAnimation(VerticalAnimationState state); + + bool activated; + bool cube_painting; + bool keyboard_grab; + bool schedule_close; + QList borderActivate; + QList borderActivateCylinder; + QList borderActivateSphere; + int painting_desktop; + int frontDesktop; + float cubeOpacity; + bool opacityDesktopOnly; + bool displayDesktopName; + EffectFrame* desktopNameFrame; + QFont desktopNameFont; + bool reflection; + bool rotating; + bool verticalRotating; + bool desktopChangedWhileRotating; + bool paintCaps; + QColor backgroundColor; + QColor capColor; + GLTexture* wallpaper; + bool texturedCaps; + GLTexture* capTexture; + // animations + // Horizontal/start/stop + float startAngle; + float currentAngle; + int startFrontDesktop; + AnimationState animationState; + TimeLine timeLine; + QQueue animations; + // vertical + float verticalStartAngle; + float verticalCurrentAngle; + VerticalAnimationState verticalAnimationState; + TimeLine verticalTimeLine; + QQueue verticalAnimations; + + bool reflectionPainting; + std::chrono::milliseconds rotationDuration; + int activeScreen; + bool bottomCap; + bool closeOnMouseRelease; + float zoom; + float zPosition; + bool useForTabBox; + bool invertKeys; + bool invertMouse; + bool tabBoxMode; + bool shortcutsRegistered; + CubeMode mode; + bool useShaders; + GLShader* cylinderShader; + GLShader* sphereShader; + GLShader* m_reflectionShader; + GLShader* m_capShader; + float capDeformationFactor; + bool useZOrdering; + float zOrderingFactor; + bool useList; + // needed for reflection + float mAddedHeightCoeff1; + float mAddedHeightCoeff2; + + QMatrix4x4 m_rotationMatrix; + QMatrix4x4 m_reflectionMatrix; + QMatrix4x4 m_textureMirrorMatrix; + QMatrix4x4 m_currentFaceMatrix; + GLVertexBuffer *m_cubeCapBuffer; + + // Shortcuts - needed to toggle the effect + QList cubeShortcut; + QList cylinderShortcut; + QList sphereShortcut; + + // proxy + CubeEffectProxy m_proxy; + QList< CubeInsideEffect* > m_cubeInsideEffects; + + QAction *m_cubeAction; + QAction *m_cylinderAction; + QAction *m_sphereAction; +}; + +} // namespace + +#endif diff --git a/effects/cube/cube.kcfg b/effects/cube/cube.kcfg new file mode 100644 index 0000000..f0c5fda --- /dev/null +++ b/effects/cube/cube.kcfg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + 0 + + + 80 + + + false + + + true + + + true + + + QColor(Qt::black) + + + QColor(KColorScheme(QPalette::Active, KColorScheme::Window).background().color()) + + + QStandardPaths::locate(QStandardPaths::DataLocation, QStringLiteral("cubecap.png")) + + + true + + + true + + + false + + + false + + + 100 + + + + 0 + + + false + + + false + + + false + + + diff --git a/effects/cube/cube_config.cpp b/effects/cube/cube_config.cpp new file mode 100644 index 0000000..9be4bce --- /dev/null +++ b/effects/cube/cube_config.cpp @@ -0,0 +1,110 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "cube_config.h" +// KConfigSkeleton +#include "cubeconfig.h" +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +K_PLUGIN_FACTORY_WITH_JSON(CubeEffectConfigFactory, + "cube_config.json", + registerPlugin();) + +namespace KWin +{ + +CubeEffectConfigForm::CubeEffectConfigForm(QWidget* parent) : QWidget(parent) +{ + setupUi(this); +} + +CubeEffectConfig::CubeEffectConfig(QWidget* parent, const QVariantList& args) : + KCModule(KAboutData::pluginData(QStringLiteral("cube")), parent, args) +{ + m_ui = new CubeEffectConfigForm(this); + + QVBoxLayout* layout = new QVBoxLayout(this); + + layout->addWidget(m_ui); + + m_ui->tabWidget->setTabText(0, i18nc("@title:tab Basic Settings", "Basic")); + m_ui->tabWidget->setTabText(1, i18nc("@title:tab Advanced Settings", "Advanced")); + + // Shortcut config. The shortcut belongs to the component "kwin"! + m_actionCollection = new KActionCollection(this, QStringLiteral("kwin")); + m_actionCollection->setComponentDisplayName(i18n("KWin")); + + m_actionCollection->setConfigGroup(QStringLiteral("Cube")); + m_actionCollection->setConfigGlobal(true); + + QAction* cubeAction = m_actionCollection->addAction(QStringLiteral("Cube")); + cubeAction->setText(i18n("Desktop Cube")); + cubeAction->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(cubeAction, QList() << Qt::CTRL + Qt::Key_F11); + KGlobalAccel::self()->setShortcut(cubeAction, QList() << Qt::CTRL + Qt::Key_F11); + QAction* cylinderAction = m_actionCollection->addAction(QStringLiteral("Cylinder")); + cylinderAction->setText(i18n("Desktop Cylinder")); + cylinderAction->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setShortcut(cylinderAction, QList()); + QAction* sphereAction = m_actionCollection->addAction(QStringLiteral("Sphere")); + sphereAction->setText(i18n("Desktop Sphere")); + sphereAction->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setShortcut(sphereAction, QList()); + + m_ui->editor->addCollection(m_actionCollection); + + capsSelectionChanged(); + connect(m_ui->kcfg_Caps, &QCheckBox::stateChanged, this, &CubeEffectConfig::capsSelectionChanged); + m_ui->kcfg_Wallpaper->setFilter(QStringLiteral("*.png *.jpeg *.jpg ")); + CubeConfig::instance(KWIN_CONFIG); + addConfig(CubeConfig::self(), m_ui); + load(); +} + +void CubeEffectConfig::save() +{ + KCModule::save(); + m_ui->editor->save(); + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("cube")); +} + +void CubeEffectConfig::capsSelectionChanged() +{ + if (m_ui->kcfg_Caps->checkState() == Qt::Checked) { + // activate cap color + m_ui->kcfg_CapColor->setEnabled(true); + m_ui->capColorLabel->setEnabled(true); + m_ui->kcfg_TexturedCaps->setEnabled(true); + } else { + // deactivate cap color + m_ui->kcfg_CapColor->setEnabled(false); + m_ui->capColorLabel->setEnabled(false); + m_ui->kcfg_TexturedCaps->setEnabled(false); + } +} + +} // namespace + +#include "cube_config.moc" diff --git a/effects/cube/cube_config.desktop b/effects/cube/cube_config.desktop new file mode 100644 index 0000000..7cd492a --- /dev/null +++ b/effects/cube/cube_config.desktop @@ -0,0 +1,83 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=KCModule + +X-KDE-Library=kwin_cube_config +X-KDE-ParentComponents=cube + +Name=Desktop Cube +Name[ar]=سطح مكتب مكعّب +Name[az]=İş Masası kubu +Name[be@latin]=Rabočy kub +Name[bg]=Кубичен работен плот +Name[bn]=ডেস্কটপ কিউব +Name[bs]=Kocka povrÅ¡i +Name[ca]=Cub de l'escriptori +Name[ca@valencia]=Cub d'escriptori +Name[cs]=Plochy na kostce +Name[csb]=Szescan pùltu +Name[da]=Skrivebordsterning +Name[de]=Arbeitsflächen-Würfel +Name[el]=Κύβος επιφάνειας εργασίας +Name[en_GB]=Desktop Cube +Name[eo]=Labortabla Kubo +Name[es]=Cubo de escritorio +Name[et]=Töölauakuubik +Name[eu]=Mahaigain kuboa +Name[fi]=Työpöytäkuutio +Name[fr]=Bureau en cube +Name[fy]=Buroblêdkubus +Name[ga]=Ciúb Deisce +Name[gl]=Cubo do escritorio +Name[gu]=ડેસ્કટોપ ટ્યુબ +Name[he]=שולחן עבודה בקובייה +Name[hi]=डेस्कटॉप घन +Name[hne]=डेस्कटाप घन +Name[hr]=Radna povrÅ¡ina na kocki +Name[hsb]=Kóstka +Name[hu]=Asztalkocka +Name[ia]=Cubo de scriptorio +Name[id]=Kubus Desktop +Name[is]=Skjáborðskubbur +Name[it]=Cubo dei desktop +Name[ja]=デスクトップキューブ +Name[kk]=Үстел текшесі +Name[km]=គូប​ផ្ទៃតុ​ +Name[kn]=ಗಣಕತೆರೆ ಘನಾಕೃತಿ +Name[ko]=데스크톱 큐브 +Name[ku]=Sermaseya Mîkap +Name[lt]=Darbalaukio kubas +Name[lv]=Darbvirsmas kubs +Name[mai]=डेस्कटाप घन +Name[mk]=Работна коцка +Name[ml]=പണിയിടം ക്യൂബ് +Name[mr]=डेस्कटॉप क्यूब +Name[nb]=Skrivebordsterning +Name[nds]=Wörpel-Schriefdisch +Name[nl]=Bureaubladkubus +Name[nn]=Skrivebordskube +Name[pa]=ਡੈਸਕਟਾਪ ਘਣ +Name[pl]=Sześcian pulpitu +Name[pt]=Cubo de Ecrãs +Name[pt_BR]=Cubo de áreas de trabalho +Name[ro]=Cub de birou +Name[ru]=Куб рабочих столов +Name[si]=වැඩතල ඝනකය +Name[sk]=Plochy na kocke +Name[sl]=Kocka z namizji +Name[sr]=Коцка површи +Name[sr@ijekavian]=Коцка површи +Name[sr@ijekavianlatin]=Kocka povrÅ¡i +Name[sr@latin]=Kocka povrÅ¡i +Name[sv]=Skrivbordskub +Name[ta]=பணிமேசை க்யூப் +Name[te]=‍డెస్‍క్ టాప్ క్యూబ్ +Name[th]=พื้นที่ทำงานทรงลูกบาศก์ +Name[tr]=Masaüstü Küpü +Name[ug]=ئۈستەلئۈستى كۈپى +Name[uk]=Куб стільниць +Name[vi]=Khối vuông màn hình +Name[wa]=Cube di scribannes +Name[x-test]=xxDesktop Cubexx +Name[zh_CN]=桌面立方 +Name[zh_TW]=桌面立方體 diff --git a/effects/cube/cube_config.h b/effects/cube/cube_config.h new file mode 100644 index 0000000..5bc13f6 --- /dev/null +++ b/effects/cube/cube_config.h @@ -0,0 +1,46 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_CUBE_CONFIG_H +#define KWIN_CUBE_CONFIG_H + +#include + +#include "ui_cube_config.h" + + +namespace KWin +{ + +class CubeEffectConfigForm : public QWidget, public Ui::CubeEffectConfigForm +{ + Q_OBJECT +public: + explicit CubeEffectConfigForm(QWidget* parent); +}; + +class CubeEffectConfig : public KCModule +{ + Q_OBJECT +public: + explicit CubeEffectConfig(QWidget* parent = nullptr, const QVariantList& args = QVariantList()); + +public Q_SLOTS: + void save() override; + +private Q_SLOTS: + void capsSelectionChanged(); +private: + CubeEffectConfigForm* m_ui; + KActionCollection* m_actionCollection; +}; + +} // namespace + +#endif diff --git a/effects/cube/cube_config.ui b/effects/cube/cube_config.ui new file mode 100644 index 0000000..ec79f1c --- /dev/null +++ b/effects/cube/cube_config.ui @@ -0,0 +1,569 @@ + + + KWin::CubeEffectConfigForm + + + + 0 + 0 + 747 + 566 + + + + + + + 0 + + + + Tab 1 + + + + + + Background + + + + + + Background color: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_BackgroundColor + + + + + + + + 0 + 0 + + + + + + + + Wallpaper: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_Wallpaper + + + + + + + + 0 + 0 + + + + + + + + + + + Activation + + + + + + + 0 + 200 + + + + KShortcutsEditor::GlobalAction + + + + + + + + + + Appearance + + + + + + Display desktop name + + + + + + + Reflection + + + + + + + Rotation duration: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_RotationDuration + + + + + + + + 0 + 0 + + + + + 100 + 0 + + + + Default + + + 5000 + + + 10 + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + Windows hover above cube + + + + + + + + + + Opacity + + + + + + + 200 + 0 + + + + 100 + + + 1 + + + 100 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + 10 + + + + + + + + 75 + 0 + + + + % + + + 100 + + + 100 + + + + + + + Transparent + + + + + + + Opaque + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Do not change opacity of windows + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Tab 2 + + + + + + Caps + + + + + + Show caps + + + + + + + Cap color: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_CapColor + + + + + + + + 0 + 0 + + + + + + + + Display image on caps + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + Zoom + + + + + + Near + + + + + + + Far + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Define how far away the object should appear + + + 3000 + + + 10 + + + 100 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + 100 + + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + Additional Options + + + + + + If enabled the effect will be deactivated after rotating the cube with the mouse, +otherwise it will remain active + + + Close after mouse dragging + + + + + + + Use this effect for walking through the desktops + + + + + + + Invert cursor keys + + + + + + + Invert mouse + + + + + + + + + + Sphere Cap Deformation + + + + + + 100 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + 25 + + + + + + + Sphere + + + + + + + Plane + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + + + + KColorButton + QPushButton +
kcolorbutton.h
+
+ + KUrlRequester + QFrame +
kurlrequester.h
+ 1 +
+ + KShortcutsEditor + QWidget +
KShortcutsEditor
+ 1 +
+
+ + tabWidget + kcfg_DisplayDesktopName + kcfg_Reflection + kcfg_ZOrdering + kcfg_RotationDuration + kcfg_Opacity + kcfg_OpacitySpin + kcfg_OpacityDesktopOnly + kcfg_BackgroundColor + kcfg_Wallpaper + kcfg_Caps + kcfg_CapColor + kcfg_TexturedCaps + kcfg_CloseOnMouseRelease + kcfg_TabBox + kcfg_InvertKeys + kcfg_InvertMouse + kcfg_ZPosition + kcfg_CapDeformation + + + + + kcfg_OpacitySpin + valueChanged(int) + kcfg_Opacity + setValue(int) + + + 727 + 85 + + + 611 + 88 + + + + + kcfg_Opacity + valueChanged(int) + kcfg_OpacitySpin + setValue(int) + + + 611 + 88 + + + 727 + 85 + + + + +
diff --git a/effects/cube/cube_inside.h b/effects/cube/cube_inside.h new file mode 100644 index 0000000..fe96ab7 --- /dev/null +++ b/effects/cube/cube_inside.h @@ -0,0 +1,29 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_CUBE_INSIDE_H +#define KWIN_CUBE_INSIDE_H +#include + +namespace KWin +{ + +class CubeInsideEffect : public Effect +{ +public: + CubeInsideEffect() {} + ~CubeInsideEffect() override {} + + virtual void paint() = 0; + virtual void setActive(bool active) = 0; +}; + +} // namespace + +#endif // KWIN_CUBE_INSIDE_H diff --git a/effects/cube/cube_proxy.cpp b/effects/cube/cube_proxy.cpp new file mode 100644 index 0000000..d9221b3 --- /dev/null +++ b/effects/cube/cube_proxy.cpp @@ -0,0 +1,36 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "cube_proxy.h" +#include "cube.h" +#include "cube_inside.h" + +namespace KWin +{ + +CubeEffectProxy::CubeEffectProxy(CubeEffect* effect) + : m_effect(effect) +{ +} + +CubeEffectProxy::~CubeEffectProxy() +{ +} + +void CubeEffectProxy::registerCubeInsideEffect(CubeInsideEffect* effect) +{ + m_effect->registerCubeInsideEffect(effect); +} + +void CubeEffectProxy::unregisterCubeInsideEffect(CubeInsideEffect* effect) +{ + m_effect->unregisterCubeInsideEffect(effect); +} + +} // namespace diff --git a/effects/cube/cube_proxy.h b/effects/cube/cube_proxy.h new file mode 100644 index 0000000..8084701 --- /dev/null +++ b/effects/cube/cube_proxy.h @@ -0,0 +1,34 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_CUBE_PROXY_H +#define KWIN_CUBE_PROXY_H + +namespace KWin +{ + +class CubeEffect; +class CubeInsideEffect; + +class CubeEffectProxy +{ +public: + explicit CubeEffectProxy(CubeEffect* effect); + ~CubeEffectProxy(); + + void registerCubeInsideEffect(CubeInsideEffect* effect); + void unregisterCubeInsideEffect(CubeInsideEffect* effect); + +private: + CubeEffect* m_effect; +}; + +} // namespace + +#endif // KWIN_CUBE_PROXY_H diff --git a/effects/cube/cubeconfig.kcfgc b/effects/cube/cubeconfig.kcfgc new file mode 100644 index 0000000..4ee0f80 --- /dev/null +++ b/effects/cube/cubeconfig.kcfgc @@ -0,0 +1,6 @@ +File=cube.kcfg +ClassName=CubeConfig +NameSpace=KWin +Singleton=true +Mutators=true +IncludeFiles=kcolorscheme.h diff --git a/effects/cube/data/1.10/cube-cap.glsl b/effects/cube/data/1.10/cube-cap.glsl new file mode 100644 index 0000000..8189054 --- /dev/null +++ b/effects/cube/data/1.10/cube-cap.glsl @@ -0,0 +1,29 @@ +uniform sampler2D sampler; +uniform vec4 geometryColor; +uniform float u_opacity; +uniform int u_mirror; +uniform int u_untextured; + +varying vec2 texcoord0; + +vec2 mirrorTex(vec2 coords) { + vec2 mirrored = coords; + if (u_mirror != 0) { + mirrored.t = mirrored.t * (-1.0) + 1.0; + } + return mirrored; +} + +void main() { + vec4 color = geometryColor; + vec2 texCoord = mirrorTex(texcoord0); + vec4 tex = texture2D(sampler, texCoord); + if (texCoord.s < 0.0 || texCoord.s > 1.0 || + texCoord.t < 0.0 || texCoord.t > 1.0 || u_untextured != 0) { + tex = geometryColor; + } + color.rgb = tex.rgb*tex.a + color.rgb*(1.0-tex.a); + color.a = u_opacity; + + gl_FragColor = color; +} diff --git a/effects/cube/data/1.10/cube-reflection.glsl b/effects/cube/data/1.10/cube-reflection.glsl new file mode 100644 index 0000000..d9335a2 --- /dev/null +++ b/effects/cube/data/1.10/cube-reflection.glsl @@ -0,0 +1,8 @@ +uniform float u_alpha; + +varying vec2 texcoord0; + +void main() +{ + gl_FragColor = vec4(0.0, 0.0, 0.0, u_alpha*texcoord0.s); +} diff --git a/effects/cube/data/1.10/cylinder.vert b/effects/cube/data/1.10/cylinder.vert new file mode 100644 index 0000000..eeb250d --- /dev/null +++ b/effects/cube/data/1.10/cylinder.vert @@ -0,0 +1,35 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008, 2011 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +uniform mat4 modelViewProjectionMatrix; +uniform float width; +uniform float cubeAngle; +uniform float xCoord; +uniform float timeLine; + +attribute vec4 position; +attribute vec4 texcoord; + +varying vec2 texcoord0; + +void main() +{ + texcoord0 = texcoord.st; + vec4 transformedVertex = vec4(position.x - ( width - xCoord ), position.yzw); + float radian = radians(cubeAngle); + float radius = (width)*tan(radian); + float azimuthAngle = radians(transformedVertex.x/(width)*(90.0 - cubeAngle)); + + transformedVertex.x = width - xCoord + radius * sin( azimuthAngle ); + transformedVertex.z = position.z + radius * cos( azimuthAngle ) - radius; + + vec3 diff = (position.xyz - transformedVertex.xyz)*timeLine; + transformedVertex.xyz += diff; + + gl_Position = modelViewProjectionMatrix*transformedVertex; +} diff --git a/effects/cube/data/1.10/sphere.vert b/effects/cube/data/1.10/sphere.vert new file mode 100644 index 0000000..8440444 --- /dev/null +++ b/effects/cube/data/1.10/sphere.vert @@ -0,0 +1,41 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008, 2011 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +uniform mat4 modelViewProjectionMatrix; +uniform float width; +uniform float height; +uniform float cubeAngle; +uniform vec2 u_offset; +uniform float timeLine; + +attribute vec4 position; +attribute vec4 texcoord; + +varying vec2 texcoord0; + +void main() +{ + texcoord0 = texcoord.st; + vec4 transformedVertex = position; + transformedVertex.x = transformedVertex.x - width; + transformedVertex.y = transformedVertex.y - height; + transformedVertex.xy = transformedVertex.xy + u_offset; + float radian = radians(cubeAngle); + float radius = (width)/cos(radian); + float zenithAngle = acos(transformedVertex.y/radius); + float azimuthAngle = asin(transformedVertex.x/radius); + transformedVertex.z = radius * sin( zenithAngle ) * cos( azimuthAngle ) - radius*cos( radians( 90.0 - cubeAngle ) ); + transformedVertex.x = radius * sin( zenithAngle ) * sin( azimuthAngle ); + + transformedVertex.xy += vec2( width - u_offset.x, height - u_offset.y ); + + vec3 diff = (position.xyz - transformedVertex.xyz)*timeLine; + transformedVertex.xyz += diff; + + gl_Position = modelViewProjectionMatrix*transformedVertex; +} diff --git a/effects/cube/data/1.40/cube-cap.glsl b/effects/cube/data/1.40/cube-cap.glsl new file mode 100644 index 0000000..20a8306 --- /dev/null +++ b/effects/cube/data/1.40/cube-cap.glsl @@ -0,0 +1,32 @@ +#version 140 +uniform sampler2D sampler; +uniform vec4 geometryColor; +uniform float u_opacity; +uniform int u_mirror; +uniform int u_untextured; + +in vec2 texcoord0; + +out vec4 fragColor; + +vec2 mirrorTex(vec2 coords) { + vec2 mirrored = coords; + if (u_mirror != 0) { + mirrored.t = mirrored.t * (-1.0) + 1.0; + } + return mirrored; +} + +void main() { + vec4 color = geometryColor; + vec2 texCoord = mirrorTex(texcoord0); + vec4 tex = texture(sampler, texCoord); + if (texCoord.s < 0.0 || texCoord.s > 1.0 || + texCoord.t < 0.0 || texCoord.t > 1.0 || u_untextured != 0) { + tex = geometryColor; + } + color.rgb = tex.rgb*tex.a + color.rgb*(1.0-tex.a); + color.a = u_opacity; + + fragColor = color; +} diff --git a/effects/cube/data/1.40/cube-reflection.glsl b/effects/cube/data/1.40/cube-reflection.glsl new file mode 100644 index 0000000..b70e98d --- /dev/null +++ b/effects/cube/data/1.40/cube-reflection.glsl @@ -0,0 +1,11 @@ +#version 140 +uniform float u_alpha; + +in vec2 texcoord0; + +out vec4 fragColor; + +void main() +{ + fragColor = vec4(0.0, 0.0, 0.0, u_alpha*texcoord0.s); +} diff --git a/effects/cube/data/1.40/cylinder.vert b/effects/cube/data/1.40/cylinder.vert new file mode 100644 index 0000000..d30472f --- /dev/null +++ b/effects/cube/data/1.40/cylinder.vert @@ -0,0 +1,36 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008, 2011 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#version 140 +uniform mat4 modelViewProjectionMatrix; +uniform float width; +uniform float cubeAngle; +uniform float xCoord; +uniform float timeLine; + +in vec4 position; +in vec4 texcoord; + +out vec2 texcoord0; + +void main() +{ + texcoord0 = texcoord.st; + vec4 transformedVertex = vec4(position.x - ( width - xCoord ), position.yzw); + float radian = radians(cubeAngle); + float radius = (width)*tan(radian); + float azimuthAngle = radians(transformedVertex.x/(width)*(90.0 - cubeAngle)); + + transformedVertex.x = width - xCoord + radius * sin( azimuthAngle ); + transformedVertex.z = position.z + radius * cos( azimuthAngle ) - radius; + + vec3 diff = (position.xyz - transformedVertex.xyz)*timeLine; + transformedVertex.xyz += diff; + + gl_Position = modelViewProjectionMatrix*transformedVertex; +} diff --git a/effects/cube/data/1.40/sphere.vert b/effects/cube/data/1.40/sphere.vert new file mode 100644 index 0000000..82648a2 --- /dev/null +++ b/effects/cube/data/1.40/sphere.vert @@ -0,0 +1,42 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008, 2011 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#version 140 +uniform mat4 modelViewProjectionMatrix; +uniform float width; +uniform float height; +uniform float cubeAngle; +uniform vec2 u_offset; +uniform float timeLine; + +in vec4 position; +in vec4 texcoord; + +out vec2 texcoord0; + +void main() +{ + texcoord0 = texcoord.st; + vec4 transformedVertex = position; + transformedVertex.x = transformedVertex.x - width; + transformedVertex.y = transformedVertex.y - height; + transformedVertex.xy = transformedVertex.xy + u_offset; + float radian = radians(cubeAngle); + float radius = (width)/cos(radian); + float zenithAngle = acos(transformedVertex.y/radius); + float azimuthAngle = asin(transformedVertex.x/radius); + transformedVertex.z = radius * sin( zenithAngle ) * cos( azimuthAngle ) - radius*cos( radians( 90.0 - cubeAngle ) ); + transformedVertex.x = radius * sin( zenithAngle ) * sin( azimuthAngle ); + + transformedVertex.xy += vec2( width - u_offset.x, height - u_offset.y ); + + vec3 diff = (position.xyz - transformedVertex.xyz)*timeLine; + transformedVertex.xyz += diff; + + gl_Position = modelViewProjectionMatrix*transformedVertex; +} diff --git a/effects/cube/data/cubecap.png b/effects/cube/data/cubecap.png new file mode 100644 index 0000000000000000000000000000000000000000..229fa04f2bc0f1cbff4cf4b1c38fd9446273d362 GIT binary patch literal 48777 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 + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "cubeslide.h" +// KConfigSkeleton +#include "cubeslideconfig.h" + +#include +#include + +#include + +#include + +namespace KWin +{ + +CubeSlideEffect::CubeSlideEffect() + : stickyPainting(false) + , windowMoving(false) + , desktopChangedWhileMoving(false) + , progressRestriction(0.0f) +{ + initConfig(); + connect(effects, &EffectsHandler::windowAdded, + this, &CubeSlideEffect::slotWindowAdded); + connect(effects, &EffectsHandler::windowDeleted, + this, &CubeSlideEffect::slotWindowDeleted); + connect(effects, QOverload::of(&EffectsHandler::desktopChanged), + this, &CubeSlideEffect::slotDesktopChanged); + connect(effects, &EffectsHandler::windowStepUserMovedResized, + this, &CubeSlideEffect::slotWindowStepUserMovedResized); + connect(effects, &EffectsHandler::windowFinishUserMovedResized, + this, &CubeSlideEffect::slotWindowFinishUserMovedResized); + connect(effects, &EffectsHandler::numberDesktopsChanged, + this, &CubeSlideEffect::slotNumberDesktopsChanged); + reconfigure(ReconfigureAll); +} + +CubeSlideEffect::~CubeSlideEffect() +{ +} + +bool CubeSlideEffect::supported() +{ + return effects->isOpenGLCompositing() && effects->animationsSupported(); +} + +void CubeSlideEffect::reconfigure(ReconfigureFlags) +{ + CubeSlideConfig::self()->read(); + // TODO: rename rotationDuration to duration + rotationDuration = animationTime(CubeSlideConfig::rotationDuration() != 0 ? CubeSlideConfig::rotationDuration() : 500); + timeLine.setEasingCurve(QEasingCurve::InOutSine); + timeLine.setDuration(rotationDuration); + dontSlidePanels = CubeSlideConfig::dontSlidePanels(); + dontSlideStickyWindows = CubeSlideConfig::dontSlideStickyWindows(); + usePagerLayout = CubeSlideConfig::usePagerLayout(); + useWindowMoving = CubeSlideConfig::useWindowMoving(); +} + +void CubeSlideEffect::prePaintScreen(ScreenPrePaintData& data, int time) +{ + if (isActive()) { + data.mask |= PAINT_SCREEN_TRANSFORMED | PAINT_SCREEN_WITH_TRANSFORMED_WINDOWS | PAINT_SCREEN_BACKGROUND_FIRST; + timeLine.setCurrentTime(timeLine.currentTime() + time); + if (windowMoving && timeLine.currentTime() > progressRestriction * (qreal)timeLine.duration()) + timeLine.setCurrentTime(progressRestriction * (qreal)timeLine.duration()); + } + effects->prePaintScreen(data, time); +} + +void CubeSlideEffect::paintScreen(int mask, const QRegion ®ion, ScreenPaintData& data) +{ + if (isActive()) { + glEnable(GL_CULL_FACE); + glCullFace(GL_FRONT); + paintSlideCube(mask, region, data); + glCullFace(GL_BACK); + paintSlideCube(mask, region, data); + glDisable(GL_CULL_FACE); + // Paint an extra screen with 'sticky' windows. + if (!staticWindows.isEmpty()) { + stickyPainting = true; + effects->paintScreen(mask, region, data); + stickyPainting = false; + } + } else + effects->paintScreen(mask, region, data); +} + +void CubeSlideEffect::paintSlideCube(int mask, QRegion region, ScreenPaintData& data) +{ + // slide cube only paints to desktops at a time + // first the horizontal rotations followed by vertical rotations + QRect rect = effects->clientArea(FullArea, effects->activeScreen(), effects->currentDesktop()); + float point = rect.width() / 2 * tan(45.0f * M_PI / 180.0f); + cube_painting = true; + painting_desktop = front_desktop; + + ScreenPaintData firstFaceData = data; + ScreenPaintData secondFaceData = data; + RotationDirection direction = slideRotations.head(); + int secondDesktop; + switch(direction) { + case Left: + firstFaceData.setRotationAxis(Qt::YAxis); + secondFaceData.setRotationAxis(Qt::YAxis); + if (usePagerLayout) + secondDesktop = effects->desktopToLeft(front_desktop, true); + else { + secondDesktop = front_desktop - 1; + if (secondDesktop == 0) + secondDesktop = effects->numberOfDesktops(); + } + firstFaceData.setRotationAngle(90.0f * timeLine.currentValue()); + secondFaceData.setRotationAngle(-90.0f * (1.0f - timeLine.currentValue())); + break; + case Right: + firstFaceData.setRotationAxis(Qt::YAxis); + secondFaceData.setRotationAxis(Qt::YAxis); + if (usePagerLayout) + secondDesktop = effects->desktopToRight(front_desktop, true); + else { + secondDesktop = front_desktop + 1; + if (secondDesktop > effects->numberOfDesktops()) + secondDesktop = 1; + } + firstFaceData.setRotationAngle(-90.0f * timeLine.currentValue()); + secondFaceData.setRotationAngle(90.0f * (1.0f - timeLine.currentValue())); + break; + case Upwards: + firstFaceData.setRotationAxis(Qt::XAxis); + secondFaceData.setRotationAxis(Qt::XAxis); + secondDesktop = effects->desktopAbove(front_desktop, true); + firstFaceData.setRotationAngle(-90.0f * timeLine.currentValue()); + secondFaceData.setRotationAngle(90.0f * (1.0f - timeLine.currentValue())); + point = rect.height() / 2 * tan(45.0f * M_PI / 180.0f); + break; + case Downwards: + firstFaceData.setRotationAxis(Qt::XAxis); + secondFaceData.setRotationAxis(Qt::XAxis); + secondDesktop = effects->desktopBelow(front_desktop, true); + firstFaceData.setRotationAngle(90.0f * timeLine.currentValue()); + secondFaceData.setRotationAngle(-90.0f * (1.0f - timeLine.currentValue())); + point = rect.height() / 2 * tan(45.0f * M_PI / 180.0f); + break; + default: + // totally impossible + return; + } + // front desktop + firstFaceData.setRotationOrigin(QVector3D(rect.width() / 2, rect.height() / 2, -point)); + other_desktop = secondDesktop; + firstDesktop = true; + effects->paintScreen(mask, region, firstFaceData); + // second desktop + other_desktop = painting_desktop; + painting_desktop = secondDesktop; + firstDesktop = false; + secondFaceData.setRotationOrigin(QVector3D(rect.width() / 2, rect.height() / 2, -point)); + effects->paintScreen(mask, region, secondFaceData); + cube_painting = false; + painting_desktop = effects->currentDesktop(); +} + +void CubeSlideEffect::prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time) +{ + if (stickyPainting) { + if (staticWindows.contains(w)) { + w->enablePainting(EffectWindow::PAINT_DISABLED_BY_DESKTOP); + } else { + w->disablePainting(EffectWindow::PAINT_DISABLED_BY_DESKTOP); + } + } else if (isActive() && cube_painting) { + if (staticWindows.contains(w)) { + w->disablePainting(EffectWindow::PAINT_DISABLED_BY_DESKTOP); + effects->prePaintWindow(w, data, time); + return; + } + QRect rect = effects->clientArea(FullArea, effects->activeScreen(), painting_desktop); + if (w->isOnDesktop(painting_desktop)) { + if (w->x() < rect.x()) { + data.quads = data.quads.splitAtX(-w->x()); + } + if (w->x() + w->width() > rect.x() + rect.width()) { + data.quads = data.quads.splitAtX(rect.width() - w->x()); + } + if (w->y() < rect.y()) { + data.quads = data.quads.splitAtY(-w->y()); + } + if (w->y() + w->height() > rect.y() + rect.height()) { + data.quads = data.quads.splitAtY(rect.height() - w->y()); + } + w->enablePainting(EffectWindow::PAINT_DISABLED_BY_DESKTOP); + } else if (w->isOnDesktop(other_desktop)) { + RotationDirection direction = slideRotations.head(); + bool enable = false; + if (w->x() < rect.x() && + (direction == Left || direction == Right)) { + data.quads = data.quads.splitAtX(-w->x()); + enable = true; + } + if (w->x() + w->width() > rect.x() + rect.width() && + (direction == Left || direction == Right)) { + data.quads = data.quads.splitAtX(rect.width() - w->x()); + enable = true; + } + if (w->y() < rect.y() && + (direction == Upwards || direction == Downwards)) { + data.quads = data.quads.splitAtY(-w->y()); + enable = true; + } + if (w->y() + w->height() > rect.y() + rect.height() && + (direction == Upwards || direction == Downwards)) { + data.quads = data.quads.splitAtY(rect.height() - w->y()); + enable = true; + } + if (enable) { + data.setTransformed(); + data.setTranslucent(); + w->enablePainting(EffectWindow::PAINT_DISABLED_BY_DESKTOP); + } else + w->disablePainting(EffectWindow::PAINT_DISABLED_BY_DESKTOP); + } else + w->disablePainting(EffectWindow::PAINT_DISABLED_BY_DESKTOP); + } + effects->prePaintWindow(w, data, time); +} + +void CubeSlideEffect::paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data) +{ + if (isActive() && cube_painting && !staticWindows.contains(w)) { + // filter out quads overlapping the edges + QRect rect = effects->clientArea(FullArea, effects->activeScreen(), painting_desktop); + if (w->isOnDesktop(painting_desktop)) { + if (w->x() < rect.x()) { + WindowQuadList new_quads; + foreach (const WindowQuad & quad, data.quads) { + if (quad.right() > -w->x()) { + new_quads.append(quad); + } + } + data.quads = new_quads; + } + if (w->x() + w->width() > rect.x() + rect.width()) { + WindowQuadList new_quads; + foreach (const WindowQuad & quad, data.quads) { + if (quad.right() <= rect.width() - w->x()) { + new_quads.append(quad); + } + } + data.quads = new_quads; + } + if (w->y() < rect.y()) { + WindowQuadList new_quads; + foreach (const WindowQuad & quad, data.quads) { + if (quad.bottom() > -w->y()) { + new_quads.append(quad); + } + } + data.quads = new_quads; + } + if (w->y() + w->height() > rect.y() + rect.height()) { + WindowQuadList new_quads; + foreach (const WindowQuad & quad, data.quads) { + if (quad.bottom() <= rect.height() - w->y()) { + new_quads.append(quad); + } + } + data.quads = new_quads; + } + } + // paint windows overlapping edges from other desktop + if (w->isOnDesktop(other_desktop) && (mask & PAINT_WINDOW_TRANSFORMED)) { + RotationDirection direction = slideRotations.head(); + if (w->x() < rect.x() && + (direction == Left || direction == Right)) { + WindowQuadList new_quads; + data.setXTranslation(rect.width()); + foreach (const WindowQuad & quad, data.quads) { + if (quad.right() <= -w->x()) { + new_quads.append(quad); + } + } + data.quads = new_quads; + } + if (w->x() + w->width() > rect.x() + rect.width() && + (direction == Left || direction == Right)) { + WindowQuadList new_quads; + data.setXTranslation(-rect.width()); + foreach (const WindowQuad & quad, data.quads) { + if (quad.right() > rect.width() - w->x()) { + new_quads.append(quad); + } + } + data.quads = new_quads; + } + if (w->y() < rect.y() && + (direction == Upwards || direction == Downwards)) { + WindowQuadList new_quads; + data.setYTranslation(rect.height()); + foreach (const WindowQuad & quad, data.quads) { + if (quad.bottom() <= -w->y()) { + new_quads.append(quad); + } + } + data.quads = new_quads; + } + if (w->y() + w->height() > rect.y() + rect.height() && + (direction == Upwards || direction == Downwards)) { + WindowQuadList new_quads; + data.setYTranslation(-rect.height()); + foreach (const WindowQuad & quad, data.quads) { + if (quad.bottom() > rect.height() - w->y()) { + new_quads.append(quad); + } + } + data.quads = new_quads; + } + if (firstDesktop) + data.multiplyOpacity(timeLine.currentValue()); + else + data.multiplyOpacity((1.0 - timeLine.currentValue())); + } + } + effects->paintWindow(w, mask, region, data); +} + +void CubeSlideEffect::postPaintScreen() +{ + effects->postPaintScreen(); + if (isActive()) { + if (timeLine.currentValue() == 1.0) { + RotationDirection direction = slideRotations.dequeue(); + switch(direction) { + case Left: + if (usePagerLayout) + front_desktop = effects->desktopToLeft(front_desktop, true); + else { + front_desktop--; + if (front_desktop == 0) + front_desktop = effects->numberOfDesktops(); + } + break; + case Right: + if (usePagerLayout) + front_desktop = effects->desktopToRight(front_desktop, true); + else { + front_desktop++; + if (front_desktop > effects->numberOfDesktops()) + front_desktop = 1; + } + break; + case Upwards: + front_desktop = effects->desktopAbove(front_desktop, true); + break; + case Downwards: + front_desktop = effects->desktopBelow(front_desktop, true); + break; + } + timeLine.setCurrentTime(0); + if (slideRotations.count() == 1) + timeLine.setEasingCurve(QEasingCurve::OutSine); + else + timeLine.setEasingCurve(QEasingCurve::Linear); + if (slideRotations.empty()) { + for (EffectWindow* w : staticWindows) { + w->setData(WindowForceBlurRole, QVariant()); + w->setData(WindowForceBackgroundContrastRole, QVariant()); + } + staticWindows.clear(); + effects->setActiveFullScreenEffect(nullptr); + } + } + effects->addRepaintFull(); + } +} + +void CubeSlideEffect::slotDesktopChanged(int old, int current, EffectWindow* w) +{ + Q_UNUSED(w) + if (effects->activeFullScreenEffect() && effects->activeFullScreenEffect() != this) + return; + if (old > effects->numberOfDesktops()) { + // number of desktops has been reduced -> no animation + return; + } + if (windowMoving) { + desktopChangedWhileMoving = true; + progressRestriction = 1.0 - progressRestriction; + effects->addRepaintFull(); + return; + } + bool activate = true; + if (!slideRotations.empty()) { + // last slide still in progress + activate = false; + RotationDirection direction = slideRotations.dequeue(); + slideRotations.clear(); + slideRotations.enqueue(direction); + switch(direction) { + case Left: + if (usePagerLayout) + old = effects->desktopToLeft(front_desktop, true); + else { + old = front_desktop - 1; + if (old == 0) + old = effects->numberOfDesktops(); + } + break; + case Right: + if (usePagerLayout) + old = effects->desktopToRight(front_desktop, true); + else { + old = front_desktop + 1; + if (old > effects->numberOfDesktops()) + old = 1; + } + break; + case Upwards: + old = effects->desktopAbove(front_desktop, true); + break; + case Downwards: + old = effects->desktopBelow(front_desktop, true); + break; + } + } + if (usePagerLayout) { + // calculate distance in respect to pager + QPoint diff = effects->desktopGridCoords(effects->currentDesktop()) - effects->desktopGridCoords(old); + if (qAbs(diff.x()) > effects->desktopGridWidth() / 2) { + int sign = -1 * (diff.x() / qAbs(diff.x())); + diff.setX(sign *(effects->desktopGridWidth() - qAbs(diff.x()))); + } + if (diff.x() > 0) { + for (int i = 0; i < diff.x(); i++) { + slideRotations.enqueue(Right); + } + } else if (diff.x() < 0) { + diff.setX(-diff.x()); + for (int i = 0; i < diff.x(); i++) { + slideRotations.enqueue(Left); + } + } + if (qAbs(diff.y()) > effects->desktopGridHeight() / 2) { + int sign = -1 * (diff.y() / qAbs(diff.y())); + diff.setY(sign *(effects->desktopGridHeight() - qAbs(diff.y()))); + } + if (diff.y() > 0) { + for (int i = 0; i < diff.y(); i++) { + slideRotations.enqueue(Downwards); + } + } + if (diff.y() < 0) { + diff.setY(-diff.y()); + for (int i = 0; i < diff.y(); i++) { + slideRotations.enqueue(Upwards); + } + } + } else { + // ignore pager layout + int left = old - current; + if (left < 0) + left = effects->numberOfDesktops() + left; + int right = current - old; + if (right < 0) + right = effects->numberOfDesktops() + right; + if (left < right) { + for (int i = 0; i < left; i++) { + slideRotations.enqueue(Left); + } + } else { + for (int i = 0; i < right; i++) { + slideRotations.enqueue(Right); + } + } + } + timeLine.setDuration((float)rotationDuration / (float)slideRotations.count()); + if (activate) { + startAnimation(); + front_desktop = old; + effects->addRepaintFull(); + } +} + +void CubeSlideEffect::startAnimation() { + const EffectWindowList windows = effects->stackingOrder(); + for (EffectWindow* w : windows) { + if (!shouldAnimate(w)) { + w->setData(WindowForceBlurRole, QVariant(true)); + w->setData(WindowForceBackgroundContrastRole, QVariant(true)); + staticWindows.insert(w); + } + } + if (slideRotations.count() == 1) { + timeLine.setEasingCurve(QEasingCurve::InOutSine); + } else { + timeLine.setEasingCurve(QEasingCurve::InSine); + } + effects->setActiveFullScreenEffect(this); + timeLine.setCurrentTime(0); +} + +void CubeSlideEffect::slotWindowAdded(EffectWindow* w) { + if (!isActive()) { + return; + } + if (!shouldAnimate(w)) { + staticWindows.insert(w); + w->setData(WindowForceBlurRole, QVariant(true)); + w->setData(WindowForceBackgroundContrastRole, QVariant(true)); + } +} + +void CubeSlideEffect::slotWindowDeleted(EffectWindow* w) { + staticWindows.remove(w); +} + +bool CubeSlideEffect::shouldAnimate(const EffectWindow* w) const +{ + if (w->isDock()) { + return !dontSlidePanels; + } + if (w->isOnAllDesktops()) { + if (w->isDesktop()) { + return true; + } + if (w->isSpecialWindow()) { + return false; + } + return !dontSlideStickyWindows; + } + return true; +} + +void CubeSlideEffect::slotWindowStepUserMovedResized(EffectWindow* w) +{ + if (!useWindowMoving) + return; + if (!effects->kwinOption(SwitchDesktopOnScreenEdgeMovingWindows).toBool()) + return; + if (w->isUserResize()) + return; + const QSize screenSize = effects->virtualScreenSize(); + const QPoint cursor = effects->cursorPos(); + const int horizontal = screenSize.width() * 0.1; + const int vertical = screenSize.height() * 0.1; + const QRect leftRect(0, screenSize.height() * 0.1, horizontal, screenSize.height() * 0.8); + const QRect rightRect(screenSize.width() - horizontal, screenSize.height() * 0.1, horizontal, screenSize.height() * 0.8); + const QRect topRect(horizontal, 0, screenSize.width() * 0.8, vertical); + const QRect bottomRect(horizontal, screenSize.height() - vertical, screenSize.width() - horizontal * 2, vertical); + if (leftRect.contains(cursor)) { + if (effects->desktopToLeft(effects->currentDesktop()) != effects->currentDesktop()) + windowMovingChanged(0.3 *(float)(horizontal - cursor.x()) / (float)horizontal, Left); + } else if (rightRect.contains(cursor)) { + if (effects->desktopToRight(effects->currentDesktop()) != effects->currentDesktop()) + windowMovingChanged(0.3 *(float)(cursor.x() - screenSize.width() + horizontal) / (float)horizontal, Right); + } else if (topRect.contains(cursor)) { + if (effects->desktopAbove(effects->currentDesktop()) != effects->currentDesktop()) + windowMovingChanged(0.3 *(float)(vertical - cursor.y()) / (float)vertical, Upwards); + } else if (bottomRect.contains(cursor)) { + if (effects->desktopBelow(effects->currentDesktop()) != effects->currentDesktop()) + windowMovingChanged(0.3 *(float)(cursor.y() - screenSize.height() + vertical) / (float)vertical, Downwards); + } else { + // not in one of the areas + windowMoving = false; + desktopChangedWhileMoving = false; + timeLine.setCurrentTime(0); + if (!slideRotations.isEmpty()) + slideRotations.clear(); + effects->setActiveFullScreenEffect(nullptr); + effects->addRepaintFull(); + } +} + +void CubeSlideEffect::slotWindowFinishUserMovedResized(EffectWindow* w) +{ + if (!useWindowMoving) + return; + if (!effects->kwinOption(SwitchDesktopOnScreenEdgeMovingWindows).toBool()) + return; + if (w->isUserResize()) + return; + if (!desktopChangedWhileMoving) { + if (slideRotations.isEmpty()) + return; + const RotationDirection direction = slideRotations.dequeue(); + switch(direction) { + case Left: + slideRotations.enqueue(Right); + break; + case Right: + slideRotations.enqueue(Left); + break; + case Upwards: + slideRotations.enqueue(Downwards); + break; + case Downwards: + slideRotations.enqueue(Upwards); + break; + default: + break; // impossible + } + timeLine.setCurrentTime(timeLine.duration() - timeLine.currentTime()); + } + desktopChangedWhileMoving = false; + windowMoving = false; + effects->addRepaintFull(); +} + +void CubeSlideEffect::windowMovingChanged(float progress, RotationDirection direction) +{ + if (desktopChangedWhileMoving) + progressRestriction = 1.0 - progress; + else + progressRestriction = progress; + front_desktop = effects->currentDesktop(); + if (slideRotations.isEmpty()) { + slideRotations.enqueue(direction); + windowMoving = true; + startAnimation(); + } + effects->addRepaintFull(); +} + +bool CubeSlideEffect::isActive() const +{ + return !slideRotations.isEmpty(); +} + +void CubeSlideEffect::slotNumberDesktopsChanged() +{ + // This effect animates only aftermaths of desktop switching. There is no any + // way to reference removed desktops for animation purposes. So our the best + // shot is just to do nothing. It doesn't look nice and we probaby have to + // find more proper way to handle this case. + + if (!isActive()) { + return; + } + + for (EffectWindow *w : staticWindows) { + w->setData(WindowForceBlurRole, QVariant()); + w->setData(WindowForceBackgroundContrastRole, QVariant()); + } + + slideRotations.clear(); + staticWindows.clear(); + + effects->setActiveFullScreenEffect(nullptr); +} + +} // namespace diff --git a/effects/cubeslide/cubeslide.h b/effects/cubeslide/cubeslide.h new file mode 100644 index 0000000..5625f2f --- /dev/null +++ b/effects/cubeslide/cubeslide.h @@ -0,0 +1,105 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_CUBESLIDE_H +#define KWIN_CUBESLIDE_H + +#include +#include +#include +#include +#include + +namespace KWin +{ +class CubeSlideEffect + : public Effect +{ + Q_OBJECT + Q_PROPERTY(int rotationDuration READ configuredRotationDuration) + Q_PROPERTY(bool dontSlidePanels READ isDontSlidePanels) + Q_PROPERTY(bool dontSlideStickyWindows READ isDontSlideStickyWindows) + Q_PROPERTY(bool usePagerLayout READ isUsePagerLayout) + Q_PROPERTY(bool useWindowMoving READ isUseWindowMoving) +public: + CubeSlideEffect(); + ~CubeSlideEffect() override; + void reconfigure(ReconfigureFlags) override; + void prePaintScreen(ScreenPrePaintData& data, int time) override; + void paintScreen(int mask, const QRegion ®ion, ScreenPaintData& data) override; + void postPaintScreen() override; + void prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time) override; + void paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data) override; + bool isActive() const override; + + int requestedEffectChainPosition() const override { + return 50; + } + + static bool supported(); + + // for properties + int configuredRotationDuration() const { + return rotationDuration; + } + bool isDontSlidePanels() const { + return dontSlidePanels; + } + bool isDontSlideStickyWindows() const { + return dontSlideStickyWindows; + } + bool isUsePagerLayout() const { + return usePagerLayout; + } + bool isUseWindowMoving() const { + return useWindowMoving; + } +private Q_SLOTS: + void slotWindowAdded(EffectWindow* w); + void slotWindowDeleted(EffectWindow* w); + + void slotDesktopChanged(int old, int current, EffectWindow* w); + void slotWindowStepUserMovedResized(KWin::EffectWindow *w); + void slotWindowFinishUserMovedResized(KWin::EffectWindow *w); + void slotNumberDesktopsChanged(); + +private: + enum RotationDirection { + Left, + Right, + Upwards, + Downwards + }; + void paintSlideCube(int mask, QRegion region, ScreenPaintData& data); + void windowMovingChanged(float progress, RotationDirection direction); + + bool shouldAnimate(const EffectWindow* w) const; + void startAnimation(); + + bool cube_painting; + int front_desktop; + int painting_desktop; + int other_desktop; + bool firstDesktop; + bool stickyPainting; + QSet staticWindows; + QTimeLine timeLine; + QQueue slideRotations; + bool dontSlidePanels; + bool dontSlideStickyWindows; + bool usePagerLayout; + int rotationDuration; + bool useWindowMoving; + bool windowMoving; + bool desktopChangedWhileMoving; + double progressRestriction; +}; +} + +#endif diff --git a/effects/cubeslide/cubeslide.kcfg b/effects/cubeslide/cubeslide.kcfg new file mode 100644 index 0000000..0bf2bd2 --- /dev/null +++ b/effects/cubeslide/cubeslide.kcfg @@ -0,0 +1,25 @@ + + + + + + + 0 + + + true + + + true + + + true + + + false + + + diff --git a/effects/cubeslide/cubeslide_config.cpp b/effects/cubeslide/cubeslide_config.cpp new file mode 100644 index 0000000..900e05e --- /dev/null +++ b/effects/cubeslide/cubeslide_config.cpp @@ -0,0 +1,58 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "cubeslide_config.h" +// KConfigSkeleton +#include "cubeslideconfig.h" +#include +#include + +#include +#include +#include +#include + +K_PLUGIN_FACTORY_WITH_JSON(CubeSlideEffectConfigFactory, + "cubeslide_config.json", + registerPlugin();) + +namespace KWin +{ + +CubeSlideEffectConfigForm::CubeSlideEffectConfigForm(QWidget* parent) : QWidget(parent) +{ + setupUi(this); +} + +CubeSlideEffectConfig::CubeSlideEffectConfig(QWidget* parent, const QVariantList& args) : + KCModule(KAboutData::pluginData(QStringLiteral("cubeslide")), parent, args) +{ + m_ui = new CubeSlideEffectConfigForm(this); + + QVBoxLayout* layout = new QVBoxLayout(this); + + layout->addWidget(m_ui); + + CubeSlideConfig::instance(KWIN_CONFIG); + addConfig(CubeSlideConfig::self(), m_ui); + + load(); +} + +void CubeSlideEffectConfig::save() +{ + KCModule::save(); + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("cubeslide")); +} + +} // namespace + +#include "cubeslide_config.moc" diff --git a/effects/cubeslide/cubeslide_config.desktop b/effects/cubeslide/cubeslide_config.desktop new file mode 100644 index 0000000..efb8c58 --- /dev/null +++ b/effects/cubeslide/cubeslide_config.desktop @@ -0,0 +1,74 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=KCModule + +X-KDE-Library=kwin_cubeslide_config +X-KDE-ParentComponents=cubeslide + +Name=Desktop Cube Animation +Name[az]=Animasiyalı İş Masası kubu +Name[bg]=Кубична анимация върху работен плот +Name[bs]=Animacija kocke povrÅ¡i +Name[ca]=Animació de cub per a l'escriptori +Name[ca@valencia]=Animació de cub d'escriptori +Name[cs]=Animace kostky plochy +Name[csb]=Animacëjô Szescanu pùltu +Name[da]=Animeret skrivebordsterning +Name[de]=Animation Arbeitsflächen-Würfel +Name[el]=Εφέ κύβου επιφάνειας εργασίας +Name[en_GB]=Desktop Cube Animation +Name[eo]=Movbildo de Labortabla kubo +Name[es]=Animación del cubo de escritorio +Name[et]=Töölauakuubiku animeerimine +Name[eu]=Mahaigain kuboaren animazioa +Name[fi]=Työpöytäkuutioanimaatio +Name[fr]=Animation du bureau en cube +Name[fy]=Buroblêd kubus animaasje +Name[ga]=Beochan an Chiúib Deisce +Name[gl]=Animación do cubo do escritorio +Name[gu]=ડેસ્ટોપ ક્યુબ એનિમેશન +Name[he]=הנפשת שולחן עבודה בקובייה +Name[hi]=डेस्कटॉप घन एनिमेशन +Name[hr]=Animacija kocke s radnom povrÅ¡inom +Name[hu]=Asztalváltó kocka +Name[ia]=Animation de cubo de scriptorio +Name[id]=Animasi Kubus Desktop +Name[is]=Hreyfingar á skjáborðskubbi +Name[it]=Animazione del cubo dei desktop +Name[ja]=デスクトップキューブのアニメーション +Name[kk]=Үстел текшесін анимациялау +Name[km]=ការ​ធ្វើ​ឲ្យ​គូប​ផ្ទៃតុ​មាន​ចលនា​ +Name[kn]=ಗಣಕತೆರೆ ಘನಾಕೃತಿಯ ಸಜೀವನ(ಎನಿಮೇಶನ್) +Name[ko]=데스크톱 큐브 애니메이션 +Name[lt]=Darbalaukio kubo animacija +Name[lv]=Darbvirsmas kuba animācija +Name[mk]=Анимација „Работна коцка“ +Name[ml]=പണിയിടം ക്യൂബ് നീക്കം +Name[mr]=डेस्कटॉप क्यूब ऍनीमेशन +Name[nb]=Animert skrivebordsterning +Name[nds]=Animeert Wörpel-Schriefdisch +Name[nl]=Animatie met bureaubladkubus +Name[nn]=Kubeskifte av skrivebord +Name[pa]=ਡੈਸਕਟਾਪ ਘਣ ਐਨੀਮੇਸ਼ਨ +Name[pl]=Animacja sześcianu pulpitów +Name[pt]=Animação do Cubo de Ecrãs +Name[pt_BR]=Animação do cubo de áreas de trabalho +Name[ro]=Animație Cub de birou +Name[ru]=Анимация куба рабочих столов +Name[si]=වැඩතල ඝනක සජීවීකරණය +Name[sk]=Animácia plochy na kocke +Name[sl]=Animacija kocka z namizji +Name[sr]=Анимација коцке површи +Name[sr@ijekavian]=Анимација коцке површи +Name[sr@ijekavianlatin]=Animacija kocke povrÅ¡i +Name[sr@latin]=Animacija kocke povrÅ¡i +Name[sv]=Animering med skrivbordskub +Name[th]=พื้นที่ทำงานทรงลูกบาศก์แบบเคลื่อนไหว +Name[tr]=Masaüstü Küp Animasyonu +Name[ug]=ئۈستەلئۈستى كۇب جانلاندۇرۇمى +Name[uk]=Анімація куба стільниць +Name[vi]=Hiệu ứng khối vuông màn hình +Name[wa]=AnimÃ¥cion cube do scribannes +Name[x-test]=xxDesktop Cube Animationxx +Name[zh_CN]=桌面立方动画 +Name[zh_TW]=桌面立方體動畫 diff --git a/effects/cubeslide/cubeslide_config.h b/effects/cubeslide/cubeslide_config.h new file mode 100644 index 0000000..8a49c6e --- /dev/null +++ b/effects/cubeslide/cubeslide_config.h @@ -0,0 +1,43 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_CUBESLIDE_CONFIG_H +#define KWIN_CUBESLIDE_CONFIG_H + +#include + +#include "ui_cubeslide_config.h" + + +namespace KWin +{ + +class CubeSlideEffectConfigForm : public QWidget, public Ui::CubeSlideEffectConfigForm +{ + Q_OBJECT +public: + explicit CubeSlideEffectConfigForm(QWidget* parent); +}; + +class CubeSlideEffectConfig : public KCModule +{ + Q_OBJECT +public: + explicit CubeSlideEffectConfig(QWidget* parent = nullptr, const QVariantList& args = QVariantList()); + +public Q_SLOTS: + void save() override; + +private: + CubeSlideEffectConfigForm* m_ui; +}; + +} // namespace + +#endif diff --git a/effects/cubeslide/cubeslide_config.ui b/effects/cubeslide/cubeslide_config.ui new file mode 100644 index 0000000..44acfe9 --- /dev/null +++ b/effects/cubeslide/cubeslide_config.ui @@ -0,0 +1,105 @@ + + + KWin::CubeSlideEffectConfigForm + + + + 0 + 0 + 431 + 161 + + + + + + + Do not animate windows on all desktops + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 0 + 0 + + + + + 100 + 0 + + + + Default + + + msec + + + 5000 + + + 10 + + + + + + + Do not animate panels + + + + + + + Rotation duration: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_RotationDuration + + + + + + + Use pager layout for animation + + + + + + + Start animation when moving windows towards screen edges + + + + + + + kcfg_RotationDuration + kcfg_DontSlidePanels + kcfg_DontSlideStickyWindows + + + + diff --git a/effects/cubeslide/cubeslideconfig.kcfgc b/effects/cubeslide/cubeslideconfig.kcfgc new file mode 100644 index 0000000..6059a7e --- /dev/null +++ b/effects/cubeslide/cubeslideconfig.kcfgc @@ -0,0 +1,5 @@ +File=cubeslide.kcfg +ClassName=CubeSlideConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/effects/desktopgrid/CMakeLists.txt b/effects/desktopgrid/CMakeLists.txt new file mode 100644 index 0000000..7fa4b7a --- /dev/null +++ b/effects/desktopgrid/CMakeLists.txt @@ -0,0 +1,31 @@ +####################################### +# Effect +install(FILES main.qml DESTINATION ${DATA_INSTALL_DIR}/kwin/effects/desktopgrid/) + +####################################### +# Config +set(kwin_desktopgrid_config_SRCS desktopgrid_config.cpp) +ki18n_wrap_ui(kwin_desktopgrid_config_SRCS desktopgrid_config.ui) +kconfig_add_kcfg_files(kwin_desktopgrid_config_SRCS desktopgridconfig.kcfgc) + +add_library(kwin_desktopgrid_config MODULE ${kwin_desktopgrid_config_SRCS}) + +target_link_libraries(kwin_desktopgrid_config + KF5::Completion + KF5::ConfigWidgets + KF5::GlobalAccel + KF5::I18n + KF5::XmlGui + Qt5::Quick + kwineffects + KWinEffectsInterface +) + +kcoreaddons_desktop_to_json(kwin_desktopgrid_config desktopgrid_config.desktop SERVICE_TYPES kcmodule.desktop SERVICE_TYPES kcmodule.desktop) + +install( + TARGETS + kwin_desktopgrid_config + DESTINATION + ${PLUGIN_INSTALL_DIR}/kwin/effects/configs +) diff --git a/effects/desktopgrid/desktopgrid.cpp b/effects/desktopgrid/desktopgrid.cpp new file mode 100644 index 0000000..56fe32d --- /dev/null +++ b/effects/desktopgrid/desktopgrid.cpp @@ -0,0 +1,1414 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Lubos Lunak + SPDX-FileCopyrightText: 2008 Lucas Murray + SPDX-FileCopyrightText: 2009 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "desktopgrid.h" +// KConfigSkeleton +#include "desktopgridconfig.h" + +#include "../presentwindows/presentwindows_proxy.h" +#include "../effect_builtins.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +namespace KWin +{ + +// WARNING, TODO: This effect relies on the desktop layout being EWMH-compliant. + +DesktopGridEffect::DesktopGridEffect() + : activated(false) + , timeline() + , keyboardGrab(false) + , wasWindowMove(false) + , wasWindowCopy(false) + , wasDesktopMove(false) + , isValidMove(false) + , windowMove(nullptr) + , windowMoveDiff() + , gridSize() + , orientation(Qt::Horizontal) + , activeCell(1, 1) + , scale() + , unscaledBorder() + , scaledSize() + , scaledOffset() + , m_proxy(nullptr) + , m_activateAction(new QAction(this)) +{ + initConfig(); + // Load shortcuts + QAction* a = m_activateAction; + a->setObjectName(QStringLiteral("ShowDesktopGrid")); + a->setText(i18n("Show Desktop Grid")); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::CTRL + Qt::Key_F8); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::CTRL + Qt::Key_F8); + shortcut = KGlobalAccel::self()->shortcut(a); + effects->registerGlobalShortcut(Qt::CTRL + Qt::Key_F8, a); + effects->registerTouchpadSwipeShortcut(SwipeDirection::Up, a); + connect(a, &QAction::triggered, this, &DesktopGridEffect::toggle); + connect(KGlobalAccel::self(), &KGlobalAccel::globalShortcutChanged, this, &DesktopGridEffect::globalShortcutChanged); + connect(effects, &EffectsHandler::windowAdded, this, &DesktopGridEffect::slotWindowAdded); + connect(effects, &EffectsHandler::windowClosed, this, &DesktopGridEffect::slotWindowClosed); + connect(effects, &EffectsHandler::windowDeleted, this, &DesktopGridEffect::slotWindowDeleted); + connect(effects, &EffectsHandler::numberDesktopsChanged, this, &DesktopGridEffect::slotNumberDesktopsChanged); + connect(effects, &EffectsHandler::windowFrameGeometryChanged, this, &DesktopGridEffect::slotWindowFrameGeometryChanged); + connect(effects, &EffectsHandler::numberScreensChanged, this, &DesktopGridEffect::setup); + + connect(effects, &EffectsHandler::screenAboutToLock, this, [this]() { + setActive(false); + if (keyboardGrab) { + effects->ungrabKeyboard(); + keyboardGrab = false; + } + }); + + // Load all other configuration details + reconfigure(ReconfigureAll); +} + +DesktopGridEffect::~DesktopGridEffect() +{ +} + +void DesktopGridEffect::reconfigure(ReconfigureFlags) +{ + DesktopGridConfig::self()->read(); + + foreach (ElectricBorder border, borderActivate) { + effects->unreserveElectricBorder(border, this); + } + borderActivate.clear(); + foreach (int i, DesktopGridConfig::borderActivate()) { + borderActivate.append(ElectricBorder(i)); + effects->reserveElectricBorder(ElectricBorder(i), this); + } + + // TODO: rename zoomDuration to duration + zoomDuration = animationTime(DesktopGridConfig::zoomDuration() != 0 ? DesktopGridConfig::zoomDuration() : 300); + timeline.setEasingCurve(QEasingCurve::InOutSine); + timeline.setDuration(zoomDuration); + + border = DesktopGridConfig::borderWidth(); + desktopNameAlignment = Qt::Alignment(DesktopGridConfig::desktopNameAlignment()); + layoutMode = DesktopGridConfig::layoutMode(); + customLayoutRows = DesktopGridConfig::customLayoutRows(); + m_usePresentWindows = DesktopGridConfig::presentWindows(); + + // deactivate and activate all touch border + const QVector relevantBorders{ElectricLeft, ElectricTop, ElectricRight, ElectricBottom}; + for (auto e : relevantBorders) { + effects->unregisterTouchBorder(e, m_activateAction); + } + const auto touchBorders = DesktopGridConfig::touchBorderActivate(); + for (int i : touchBorders) { + if (!relevantBorders.contains(ElectricBorder(i))) { + continue; + } + effects->registerTouchBorder(ElectricBorder(i), m_activateAction); + } +} + +//----------------------------------------------------------------------------- +// Screen painting + +void DesktopGridEffect::prePaintScreen(ScreenPrePaintData& data, int time) +{ + if (timeline.currentValue() != 0 || activated || (isUsingPresentWindows() && isMotionManagerMovingWindows())) { + if (activated) + timeline.setCurrentTime(timeline.currentTime() + time); + else + timeline.setCurrentTime(timeline.currentTime() - time); + for (int i = 0; i < effects->numberOfDesktops(); i++) { + if (i == highlightedDesktop - 1) + hoverTimeline[i]->setCurrentTime(hoverTimeline[i]->currentTime() + time); + else + hoverTimeline[i]->setCurrentTime(hoverTimeline[i]->currentTime() - time); + } + if (isUsingPresentWindows()) { + QList::iterator i; + for (i = m_managers.begin(); i != m_managers.end(); ++i) + (*i).calculate(time); + } + // PAINT_SCREEN_BACKGROUND_FIRST is needed because screen will be actually painted more than once, + // so with normal screen painting second screen paint would erase parts of the first paint + if (timeline.currentValue() != 0 || (isUsingPresentWindows() && isMotionManagerMovingWindows())) + data.mask |= PAINT_SCREEN_TRANSFORMED | PAINT_SCREEN_BACKGROUND_FIRST; + if (!activated && timeline.currentValue() == 0 && !(isUsingPresentWindows() && isMotionManagerMovingWindows())) + finish(); + } + + for (auto const &w : effects->stackingOrder()) { + w->setData(WindowForceBlurRole, QVariant(true)); + } + + effects->prePaintScreen(data, time); +} + +void DesktopGridEffect::paintScreen(int mask, const QRegion ®ion, ScreenPaintData& data) +{ + if (timeline.currentValue() == 0 && !isUsingPresentWindows()) { + effects->paintScreen(mask, region, data); + return; + } + for (int desktop = 1; desktop <= effects->numberOfDesktops(); desktop++) { + ScreenPaintData d = data; + paintingDesktop = desktop; + effects->paintScreen(mask, region, d); + } + + // paint the add desktop button + for (EffectQuickScene *view : m_desktopButtons) { + view->rootItem()->setOpacity(timeline.currentValue()); + effects->renderEffectQuickView(view); + } + + if (isUsingPresentWindows() && windowMove && wasWindowMove) { + // the moving window has to be painted on top of all desktops + QPoint diff = cursorPos() - m_windowMoveStartPoint; + QRect geo = m_windowMoveGeometry.translated(diff); + WindowPaintData d(windowMove, data.projectionMatrix()); + d *= QVector2D((qreal)geo.width() / (qreal)windowMove->width(), (qreal)geo.height() / (qreal)windowMove->height()); + d += QPoint(geo.left() - windowMove->x(), geo.top() - windowMove->y()); + effects->drawWindow(windowMove, PAINT_WINDOW_TRANSFORMED | PAINT_WINDOW_LANCZOS, infiniteRegion(), d); + } + + if (desktopNameAlignment) { + for (int screen = 0; screen < effects->numScreens(); screen++) { + QRect screenGeom = effects->clientArea(ScreenArea, screen, 0); + int desktop = 1; + foreach (EffectFrame * frame, desktopNames) { + QPointF posTL(scalePos(screenGeom.topLeft(), desktop, screen)); + QPointF posBR(scalePos(screenGeom.bottomRight(), desktop, screen)); + QRect textArea(posTL.x(), posTL.y(), posBR.x() - posTL.x(), posBR.y() - posTL.y()); + textArea.adjust(textArea.width() / 10, textArea.height() / 10, + -textArea.width() / 10, -textArea.height() / 10); + int x, y; + if (desktopNameAlignment & Qt::AlignLeft) + x = textArea.x(); + else if (desktopNameAlignment & Qt::AlignRight) + x = textArea.right(); + else + x = textArea.center().x(); + if (desktopNameAlignment & Qt::AlignTop) + y = textArea.y(); + else if (desktopNameAlignment & Qt::AlignBottom) + y = textArea.bottom(); + else + y = textArea.center().y(); + frame->setPosition(QPoint(x, y)); + frame->render(region, timeline.currentValue(), 0.7); + ++desktop; + } + } + } +} + +void DesktopGridEffect::postPaintScreen() +{ + if (activated ? timeline.currentValue() != 1 : timeline.currentValue() != 0) + effects->addRepaintFull(); // Repaint during zoom + if (isUsingPresentWindows() && isMotionManagerMovingWindows()) + effects->addRepaintFull(); + if (activated) { + for (int i = 0; i < effects->numberOfDesktops(); i++) { + if (hoverTimeline[i]->currentValue() != 0.0 && hoverTimeline[i]->currentValue() != 1.0) { + // Repaint during soft highlighting + effects->addRepaintFull(); + break; + } + } + } + + for (auto &w : effects->stackingOrder()) { + w->setData(WindowForceBlurRole, QVariant()); + } + + effects->postPaintScreen(); +} + +//----------------------------------------------------------------------------- +// Window painting + +void DesktopGridEffect::prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time) +{ + if (timeline.currentValue() != 0 || (isUsingPresentWindows() && isMotionManagerMovingWindows())) { + if (w->isOnDesktop(paintingDesktop)) { + w->enablePainting(EffectWindow::PAINT_DISABLED_BY_DESKTOP); + if (w->isMinimized() && isUsingPresentWindows()) + w->enablePainting(EffectWindow::PAINT_DISABLED_BY_MINIMIZE); + data.mask |= PAINT_WINDOW_TRANSFORMED; + + // Split windows at screen edges + for (int screen = 0; screen < effects->numScreens(); screen++) { + QRect screenGeom = effects->clientArea(ScreenArea, screen, 0); + if (w->x() < screenGeom.x()) + data.quads = data.quads.splitAtX(screenGeom.x() - w->x()); + if (w->x() + w->width() > screenGeom.x() + screenGeom.width()) + data.quads = data.quads.splitAtX(screenGeom.x() + screenGeom.width() - w->x()); + if (w->y() < screenGeom.y()) + data.quads = data.quads.splitAtY(screenGeom.y() - w->y()); + if (w->y() + w->height() > screenGeom.y() + screenGeom.height()) + data.quads = data.quads.splitAtY(screenGeom.y() + screenGeom.height() - w->y()); + } + if (windowMove && wasWindowMove && windowMove->findModal() == w) + w->disablePainting(EffectWindow::PAINT_DISABLED_BY_DESKTOP); + } else + w->disablePainting(EffectWindow::PAINT_DISABLED_BY_DESKTOP); + } + effects->prePaintWindow(w, data, time); +} + +void DesktopGridEffect::paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data) +{ + if (timeline.currentValue() != 0 || (isUsingPresentWindows() && isMotionManagerMovingWindows())) { + if (isUsingPresentWindows() && w == windowMove && wasWindowMove && + ((!wasWindowCopy && sourceDesktop == paintingDesktop) || + (sourceDesktop != highlightedDesktop && highlightedDesktop == paintingDesktop))) { + return; // will be painted on top of all other windows + } + + qreal xScale = data.xScale(); + qreal yScale = data.yScale(); + + data.multiplyBrightness(1.0 - (0.3 * (1.0 - hoverTimeline[paintingDesktop - 1]->currentValue()))); + + for (int screen = 0; screen < effects->numScreens(); screen++) { + QRect screenGeom = effects->clientArea(ScreenArea, screen, 0); + + QRectF transformedGeo = w->geometry(); + // Display all quads on the same screen on the same pass + WindowQuadList screenQuads; + bool quadsAdded = false; + if (isUsingPresentWindows()) { + WindowMotionManager& manager = m_managers[(paintingDesktop-1)*(effects->numScreens())+screen ]; + if (manager.isManaging(w)) { + foreach (const WindowQuad & quad, data.quads) + screenQuads.append(quad); + transformedGeo = manager.transformedGeometry(w); + quadsAdded = true; + if (!manager.areWindowsMoving() && timeline.currentValue() == 1.0) + mask |= PAINT_WINDOW_LANCZOS; + } else if (w->screen() != screen) + quadsAdded = true; // we don't want parts of overlapping windows on the other screen + if (w->isDesktop()) + quadsAdded = false; + } + if (!quadsAdded) { + foreach (const WindowQuad & quad, data.quads) { + QRect quadRect( + w->x() + quad.left(), w->y() + quad.top(), + quad.right() - quad.left(), quad.bottom() - quad.top() + ); + if (quadRect.intersects(screenGeom)) + screenQuads.append(quad); + } + } + if (screenQuads.isEmpty()) + continue; // Nothing is being displayed, don't bother + WindowPaintData d = data; + d.quads = screenQuads; + + QPointF newPos = scalePos(transformedGeo.topLeft().toPoint(), paintingDesktop, screen); + double progress = timeline.currentValue(); + d.setXScale(interpolate(1, xScale * scale[screen] * (float)transformedGeo.width() / (float)w->geometry().width(), progress)); + d.setYScale(interpolate(1, yScale * scale[screen] * (float)transformedGeo.height() / (float)w->geometry().height(), progress)); + d += QPoint(qRound(newPos.x() - w->x()), qRound(newPos.y() - w->y())); + + if (isUsingPresentWindows() && (w->isDock() || w->isSkipSwitcher())) { + // fade out panels if present windows is used + d.multiplyOpacity((1.0 - timeline.currentValue())); + } + if (isUsingPresentWindows() && w->isMinimized()) { + d.multiplyOpacity(timeline.currentValue()); + } + + if (effects->compositingType() == XRenderCompositing) { + // More exact clipping as XRender displays the entire window instead of just the quad + QPointF screenPosF = scalePos(screenGeom.topLeft(), paintingDesktop).toPoint(); + QPoint screenPos( + qRound(screenPosF.x()), + qRound(screenPosF.y()) + ); + QSize screenSize( + qRound(interpolate(screenGeom.width(), scaledSize[screen].width(), progress)), + qRound(interpolate(screenGeom.height(), scaledSize[screen].height(), progress)) + ); + PaintClipper pc(effects->clientArea(ScreenArea, screen, 0) & QRect(screenPos, screenSize)); + effects->paintWindow(w, mask, region, d); + } else { + if (w->isDesktop() && timeline.currentValue() == 1.0) { + // desktop windows are not in a motion manager and can always be rendered with + // lanczos sampling except for animations + mask |= PAINT_WINDOW_LANCZOS; + } + effects->paintWindow(w, mask, effects->clientArea(ScreenArea, screen, 0), d); + } + } + } else + effects->paintWindow(w, mask, region, data); +} + +//----------------------------------------------------------------------------- +// User interaction + +void DesktopGridEffect::slotWindowAdded(EffectWindow* w) +{ + if (!activated) + return; + if (isUsingPresentWindows()) { + if (!isRelevantWithPresentWindows(w)) + return; // don't add + foreach (const int i, desktopList(w)) { + WindowMotionManager& manager = m_managers[ i*effects->numScreens()+w->screen()]; + manager.manage(w); + m_proxy->calculateWindowTransformations(manager.managedWindows(), w->screen(), manager); + } + } + effects->addRepaintFull(); +} + +void DesktopGridEffect::slotWindowClosed(EffectWindow* w) +{ + if (!activated && timeline.currentValue() == 0) + return; + if (w == windowMove) { + effects->setElevatedWindow(windowMove, false); + windowMove = nullptr; + } + if (isUsingPresentWindows()) { + foreach (const int i, desktopList(w)) { + WindowMotionManager& manager = m_managers[i*effects->numScreens()+w->screen()]; + manager.unmanage(w); + m_proxy->calculateWindowTransformations(manager.managedWindows(), w->screen(), manager); + } + } + effects->addRepaintFull(); +} + +void DesktopGridEffect::slotWindowDeleted(EffectWindow* w) +{ + if (w == windowMove) + windowMove = nullptr; + if (isUsingPresentWindows()) { + for (QList::iterator it = m_managers.begin(), + end = m_managers.end(); it != end; ++it) { + it->unmanage(w); + } + } +} + +void DesktopGridEffect::slotWindowFrameGeometryChanged(EffectWindow* w, const QRect& old) +{ + Q_UNUSED(old) + if (!activated) + return; + if (w == windowMove && wasWindowMove) + return; + if (isUsingPresentWindows()) { + foreach (const int i, desktopList(w)) { + WindowMotionManager& manager = m_managers[i*effects->numScreens()+w->screen()]; + m_proxy->calculateWindowTransformations(manager.managedWindows(), w->screen(), manager); + } + } +} + +void DesktopGridEffect::windowInputMouseEvent(QEvent* e) +{ + if ((e->type() != QEvent::MouseMove + && e->type() != QEvent::MouseButtonPress + && e->type() != QEvent::MouseButtonRelease) + || timeline.currentValue() != 1) // Block user input during animations + return; + QMouseEvent* me = static_cast< QMouseEvent* >(e); + if (!(wasWindowMove || wasDesktopMove)) { + for (EffectQuickScene *view : m_desktopButtons) { + view->forwardMouseEvent(me); + if (e->isAccepted()) { + return; + } + } + } + + if (e->type() == QEvent::MouseMove) { + int d = posToDesktop(me->pos()); + if (windowMove != nullptr && + (me->pos() - dragStartPos).manhattanLength() > QApplication::startDragDistance()) { + // Handle window moving + if (!wasWindowMove) { // Activate on move + if (isUsingPresentWindows()) { + foreach (const int i, desktopList(windowMove)) { + WindowMotionManager& manager = m_managers[(i)*(effects->numScreens()) + windowMove->screen()]; + if ((i + 1) == sourceDesktop) { + const QRectF transformedGeo = manager.transformedGeometry(windowMove); + const QPointF pos = scalePos(transformedGeo.topLeft().toPoint(), sourceDesktop, windowMove->screen()); + const QSize size(scale[windowMove->screen()] *(float)transformedGeo.width(), + scale[windowMove->screen()] *(float)transformedGeo.height()); + m_windowMoveGeometry = QRect(pos.toPoint(), size); + m_windowMoveStartPoint = me->pos(); + } + manager.unmanage(windowMove); + if (EffectWindow* modal = windowMove->findModal()) { + if (manager.isManaging(modal)) + manager.unmanage(modal); + } + m_proxy->calculateWindowTransformations(manager.managedWindows(), windowMove->screen(), manager); + } + wasWindowMove = true; + } + } + if (windowMove->isMovable() && !isUsingPresentWindows()) { + wasWindowMove = true; + int screen = effects->screenNumber(me->pos()); + effects->moveWindow(windowMove, unscalePos(me->pos(), nullptr) + windowMoveDiff, true, 1.0 / scale[screen]); + } + if (wasWindowMove) { + if (effects->waylandDisplay() && (me->modifiers() & Qt::ControlModifier)) { + wasWindowCopy = true; + effects->defineCursor(Qt::DragCopyCursor); + } else { + wasWindowCopy = false; + effects->defineCursor(Qt::ClosedHandCursor); + } + if (d != highlightedDesktop) { + auto desktops = windowMove->desktops(); + if (!desktops.contains(d)) { + desktops.append(d); + } + if (highlightedDesktop != sourceDesktop || !wasWindowCopy) { + desktops.removeOne(highlightedDesktop); + } + effects->windowToDesktops(windowMove, desktops); + const int screen = effects->screenNumber(me->pos()); + if (screen != windowMove->screen()) + effects->windowToScreen(windowMove, screen); + } + effects->addRepaintFull(); + } + } else if ((me->buttons() & Qt::LeftButton) && !wasDesktopMove && + (me->pos() - dragStartPos).manhattanLength() > QApplication::startDragDistance()) { + wasDesktopMove = true; + effects->defineCursor(Qt::ClosedHandCursor); + } + if (d != highlightedDesktop) { // Highlight desktop + if ((me->buttons() & Qt::LeftButton) && isValidMove && !wasWindowMove && d <= effects->numberOfDesktops()) { + EffectWindowList windows = effects->stackingOrder(); + EffectWindowList stack[3]; + for (EffectWindowList::const_iterator it = windows.constBegin(), + end = windows.constEnd(); it != end; ++it) { + EffectWindow *w = const_cast(*it); // we're not really touching it here but below + if (w->isOnAllDesktops()) + continue; + if (w->isOnDesktop(highlightedDesktop)) + stack[0] << w; + if (w->isOnDesktop(d)) + stack[1] << w; + if (w->isOnDesktop(m_originalMovingDesktop)) + stack[2] << w; + } + const int desks[4] = {highlightedDesktop, d, m_originalMovingDesktop, highlightedDesktop}; + for (int i = 0; i < 3; ++i ) { + if (desks[i] == desks[i+1]) + continue; + foreach (EffectWindow *w, stack[i]) { + auto desktops = w->desktops(); + desktops.removeOne(desks[i]); + desktops.append(desks[i+1]); + effects->windowToDesktops(w, desktops); + + if (isUsingPresentWindows()) { + m_managers[(desks[i]-1)*(effects->numScreens()) + w->screen()].unmanage(w); + m_managers[(desks[i+1]-1)*(effects->numScreens()) + w->screen()].manage(w); + } + } + } + if (isUsingPresentWindows()) { + for (int i = 0; i < effects->numScreens(); i++) { + for (int j = 0; j < 3; ++j) { + WindowMotionManager& manager = m_managers[(desks[j]-1)*(effects->numScreens()) + i ]; + m_proxy->calculateWindowTransformations(manager.managedWindows(), i, manager); + } + } + effects->addRepaintFull(); + } + } + setHighlightedDesktop(d); + } + } + if (e->type() == QEvent::MouseButtonPress) { + if (me->buttons() == Qt::LeftButton) { + isValidMove = true; + dragStartPos = me->pos(); + sourceDesktop = posToDesktop(me->pos()); + bool isDesktop = (me->modifiers() & Qt::ShiftModifier); + EffectWindow* w = isDesktop ? nullptr : windowAt(me->pos()); + if (w != nullptr) + isDesktop = w->isDesktop(); + if (isDesktop) + m_originalMovingDesktop = posToDesktop(me->pos()); + else + m_originalMovingDesktop = 0; + if (w != nullptr && !w->isDesktop() && (w->isMovable() || w->isMovableAcrossScreens() || isUsingPresentWindows())) { + // Prepare it for moving + windowMoveDiff = w->pos() - unscalePos(me->pos(), nullptr); + windowMove = w; + effects->setElevatedWindow(windowMove, true); + } + } else if ((me->buttons() == Qt::MiddleButton || me->buttons() == Qt::RightButton) && windowMove == nullptr) { + EffectWindow* w = windowAt(me->pos()); + if (w && w->isDesktop()) { + w = nullptr; + } + if (w != nullptr) { + const int desktop = posToDesktop(me->pos()); + if (w->isOnAllDesktops()) { + effects->windowToDesktop(w, desktop); + } else { + effects->windowToDesktop(w, NET::OnAllDesktops); + } + const bool isOnAllDesktops = w->isOnAllDesktops(); + if (isUsingPresentWindows()) { + for (int i = 0; i < effects->numberOfDesktops(); i++) { + if (i != desktop - 1) { + WindowMotionManager& manager = m_managers[ i*effects->numScreens() + w->screen()]; + if (isOnAllDesktops) + manager.manage(w); + else + manager.unmanage(w); + m_proxy->calculateWindowTransformations(manager.managedWindows(), w->screen(), manager); + } + } + } + effects->addRepaintFull(); + } + } + } + if (e->type() == QEvent::MouseButtonRelease && me->button() == Qt::LeftButton) { + isValidMove = false; + if (windowMove) + effects->activateWindow(windowMove); + if (wasWindowMove || wasDesktopMove) { // reset pointer + effects->defineCursor(Qt::PointingHandCursor); + } else { // click -> exit + const int desk = posToDesktop(me->pos()); + if (desk > effects->numberOfDesktops()) + return; // don't quit when missing desktop + setCurrentDesktop(desk); + setActive(false); + } + if (windowMove) { + if (wasWindowMove && isUsingPresentWindows()) { + const int targetDesktop = posToDesktop(cursorPos()); + foreach (const int i, desktopList(windowMove)) { + WindowMotionManager& manager = m_managers[(i)*(effects->numScreens()) + windowMove->screen()]; + manager.manage(windowMove); + if (EffectWindow* modal = windowMove->findModal()) + manager.manage(modal); + if (i + 1 == targetDesktop) { + // for the desktop the window is dropped on, we use the current geometry + manager.setTransformedGeometry(windowMove, moveGeometryToDesktop(targetDesktop)); + } + m_proxy->calculateWindowTransformations(manager.managedWindows(), windowMove->screen(), manager); + } + effects->addRepaintFull(); + } + effects->setElevatedWindow(windowMove, false); + windowMove = nullptr; + } + wasWindowMove = false; + wasWindowCopy = false; + wasDesktopMove = false; + } +} + +void DesktopGridEffect::grabbedKeyboardEvent(QKeyEvent* e) +{ + if (timeline.currentValue() != 1) // Block user input during animations + return; + if (windowMove != nullptr) + return; + if (e->type() == QEvent::KeyPress) { + // check for global shortcuts + // HACK: keyboard grab disables the global shortcuts so we have to check for global shortcut (bug 156155) + if (shortcut.contains(e->key() + e->modifiers())) { + toggle(); + return; + } + + int desktop = -1; + // switch by F or just + if (e->key() >= Qt::Key_F1 && e->key() <= Qt::Key_F35) + desktop = e->key() - Qt::Key_F1 + 1; + else if (e->key() >= Qt::Key_0 && e->key() <= Qt::Key_9) + desktop = e->key() == Qt::Key_0 ? 10 : e->key() - Qt::Key_0; + if (desktop != -1) { + if (desktop <= effects->numberOfDesktops()) { + setHighlightedDesktop(desktop); + setCurrentDesktop(desktop); + setActive(false); + } + return; + } + switch(e->key()) { + // Wrap only on autorepeat + case Qt::Key_Left: + setHighlightedDesktop(desktopToLeft(highlightedDesktop, !e->isAutoRepeat())); + break; + case Qt::Key_Right: + setHighlightedDesktop(desktopToRight(highlightedDesktop, !e->isAutoRepeat())); + break; + case Qt::Key_Up: + setHighlightedDesktop(desktopUp(highlightedDesktop, !e->isAutoRepeat())); + break; + case Qt::Key_Down: + setHighlightedDesktop(desktopDown(highlightedDesktop, !e->isAutoRepeat())); + break; + case Qt::Key_Escape: + setActive(false); + return; + case Qt::Key_Enter: + case Qt::Key_Return: + case Qt::Key_Space: + setCurrentDesktop(highlightedDesktop); + setActive(false); + return; + case Qt::Key_Plus: + slotAddDesktop(); + break; + case Qt::Key_Minus: + slotRemoveDesktop(); + break; + default: + break; + } + } +} + +bool DesktopGridEffect::borderActivated(ElectricBorder border) +{ + if (!borderActivate.contains(border)) + return false; + if (effects->activeFullScreenEffect() && effects->activeFullScreenEffect() != this) + return true; + toggle(); + return true; +} + +//----------------------------------------------------------------------------- +// Helper functions + +// Transform a point to its position on the scaled grid +QPointF DesktopGridEffect::scalePos(const QPoint& pos, int desktop, int screen) const +{ + if (screen == -1) + screen = effects->screenNumber(pos); + QRect screenGeom = effects->clientArea(ScreenArea, screen, 0); + QPoint desktopCell; + if (orientation == Qt::Horizontal) { + desktopCell.setX((desktop - 1) % gridSize.width() + 1); + desktopCell.setY((desktop - 1) / gridSize.width() + 1); + } else { + desktopCell.setX((desktop - 1) / gridSize.height() + 1); + desktopCell.setY((desktop - 1) % gridSize.height() + 1); + } + + double progress = timeline.currentValue(); + QPointF point( + interpolate( + ( + (screenGeom.width() + unscaledBorder[screen]) *(desktopCell.x() - 1) + - (screenGeom.width() + unscaledBorder[screen]) *(activeCell.x() - 1) + ) + pos.x(), + ( + (scaledSize[screen].width() + border) *(desktopCell.x() - 1) + + scaledOffset[screen].x() + + (pos.x() - screenGeom.x()) * scale[screen] + ), + progress), + interpolate( + ( + (screenGeom.height() + unscaledBorder[screen]) *(desktopCell.y() - 1) + - (screenGeom.height() + unscaledBorder[screen]) *(activeCell.y() - 1) + ) + pos.y(), + ( + (scaledSize[screen].height() + border) *(desktopCell.y() - 1) + + scaledOffset[screen].y() + + (pos.y() - screenGeom.y()) * scale[screen] + ), + progress) + ); + + return point; +} + +// Detransform a point to its position on the full grid +// TODO: Doesn't correctly interpolate (Final position is correct though), don't forget to copy to posToDesktop() +QPoint DesktopGridEffect::unscalePos(const QPoint& pos, int* desktop) const +{ + int screen = effects->screenNumber(pos); + QRect screenGeom = effects->clientArea(ScreenArea, screen, 0); + + //double progress = timeline.currentValue(); + double scaledX = /*interpolate( + ( pos.x() - screenGeom.x() + unscaledBorder[screen] / 2.0 ) / ( screenGeom.width() + unscaledBorder[screen] ) + activeCell.x() - 1,*/ + (pos.x() - scaledOffset[screen].x() + double(border) / 2.0) / (scaledSize[screen].width() + border)/*, + progress )*/; + double scaledY = /*interpolate( + ( pos.y() - screenGeom.y() + unscaledBorder[screen] / 2.0 ) / ( screenGeom.height() + unscaledBorder[screen] ) + activeCell.y() - 1,*/ + (pos.y() - scaledOffset[screen].y() + double(border) / 2.0) / (scaledSize[screen].height() + border)/*, + progress )*/; + int gx = qBound(0, int(scaledX), gridSize.width() - 1); // Zero-based + int gy = qBound(0, int(scaledY), gridSize.height() - 1); + scaledX -= gx; + scaledY -= gy; + if (desktop != nullptr) { + if (orientation == Qt::Horizontal) + *desktop = gy * gridSize.width() + gx + 1; + else + *desktop = gx * gridSize.height() + gy + 1; + } + + return QPoint( + qBound( + screenGeom.x(), + qRound( + scaledX * (screenGeom.width() + unscaledBorder[screen]) + - unscaledBorder[screen] / 2.0 + + screenGeom.x() + ), + screenGeom.right() + ), + qBound( + screenGeom.y(), + qRound( + scaledY * (screenGeom.height() + unscaledBorder[screen]) + - unscaledBorder[screen] / 2.0 + + screenGeom.y() + ), + screenGeom.bottom() + ) + ); +} + +int DesktopGridEffect::posToDesktop(const QPoint& pos) const +{ + // Copied from unscalePos() + int screen = effects->screenNumber(pos); + + double scaledX = (pos.x() - scaledOffset[screen].x() + double(border) / 2.0) / (scaledSize[screen].width() + border); + double scaledY = (pos.y() - scaledOffset[screen].y() + double(border) / 2.0) / (scaledSize[screen].height() + border); + int gx = qBound(0, int(scaledX), gridSize.width() - 1); // Zero-based + int gy = qBound(0, int(scaledY), gridSize.height() - 1); + if (orientation == Qt::Horizontal) + return gy * gridSize.width() + gx + 1; + return gx * gridSize.height() + gy + 1; +} + +EffectWindow* DesktopGridEffect::windowAt(QPoint pos) const +{ + // Get stacking order top first + EffectWindowList windows = effects->stackingOrder(); + EffectWindowList::Iterator begin = windows.begin(); + EffectWindowList::Iterator end = windows.end(); + --end; + while (begin < end) + qSwap(*begin++, *end--); + + int desktop; + pos = unscalePos(pos, &desktop); + if (desktop > effects->numberOfDesktops()) + return nullptr; + if (isUsingPresentWindows()) { + const int screen = effects->screenNumber(pos); + EffectWindow *w = + m_managers.at((desktop - 1) * (effects->numScreens()) + screen).windowAtPoint(pos, false); + if (w) + return w; + foreach (EffectWindow * w, windows) { + if (w->isOnDesktop(desktop) && w->isDesktop() && w->geometry().contains(pos)) + return w; + } + } else { + foreach (EffectWindow * w, windows) { + if (w->isOnDesktop(desktop) && w->isOnCurrentActivity() && !w->isMinimized() && w->geometry().contains(pos)) + return w; + } + } + return nullptr; +} + +void DesktopGridEffect::setCurrentDesktop(int desktop) +{ + if (orientation == Qt::Horizontal) { + activeCell.setX((desktop - 1) % gridSize.width() + 1); + activeCell.setY((desktop - 1) / gridSize.width() + 1); + } else { + activeCell.setX((desktop - 1) / gridSize.height() + 1); + activeCell.setY((desktop - 1) % gridSize.height() + 1); + } + if (effects->currentDesktop() != desktop) + effects->setCurrentDesktop(desktop); +} + +void DesktopGridEffect::setHighlightedDesktop(int d) +{ + if (d == highlightedDesktop || d <= 0 || d > effects->numberOfDesktops()) + return; + if (highlightedDesktop > 0 && highlightedDesktop <= hoverTimeline.count()) + hoverTimeline[highlightedDesktop-1]->setCurrentTime(qMin(hoverTimeline[highlightedDesktop-1]->currentTime(), + hoverTimeline[highlightedDesktop-1]->duration())); + highlightedDesktop = d; + if (highlightedDesktop <= hoverTimeline.count()) + hoverTimeline[highlightedDesktop-1]->setCurrentTime(qMax(hoverTimeline[highlightedDesktop-1]->currentTime(), 0)); + effects->addRepaintFull(); +} + +int DesktopGridEffect::desktopToRight(int desktop, bool wrap) const +{ + // Copied from Workspace::desktopToRight() + int dt = desktop - 1; + if (orientation == Qt::Vertical) { + dt += gridSize.height(); + if (dt >= effects->numberOfDesktops()) { + if (wrap) + dt -= effects->numberOfDesktops(); + else + return desktop; + } + } else { + int d = (dt % gridSize.width()) + 1; + if (d >= gridSize.width()) { + if (wrap) + d -= gridSize.width(); + else + return desktop; + } + dt = dt - (dt % gridSize.width()) + d; + } + return dt + 1; +} + +int DesktopGridEffect::desktopToLeft(int desktop, bool wrap) const +{ + // Copied from Workspace::desktopToLeft() + int dt = desktop - 1; + if (orientation == Qt::Vertical) { + dt -= gridSize.height(); + if (dt < 0) { + if (wrap) + dt += effects->numberOfDesktops(); + else + return desktop; + } + } else { + int d = (dt % gridSize.width()) - 1; + if (d < 0) { + if (wrap) + d += gridSize.width(); + else + return desktop; + } + dt = dt - (dt % gridSize.width()) + d; + } + return dt + 1; +} + +int DesktopGridEffect::desktopUp(int desktop, bool wrap) const +{ + // Copied from Workspace::desktopUp() + int dt = desktop - 1; + if (orientation == Qt::Horizontal) { + dt -= gridSize.width(); + if (dt < 0) { + if (wrap) + dt += effects->numberOfDesktops(); + else + return desktop; + } + } else { + int d = (dt % gridSize.height()) - 1; + if (d < 0) { + if (wrap) + d += gridSize.height(); + else + return desktop; + } + dt = dt - (dt % gridSize.height()) + d; + } + return dt + 1; +} + +int DesktopGridEffect::desktopDown(int desktop, bool wrap) const +{ + // Copied from Workspace::desktopDown() + int dt = desktop - 1; + if (orientation == Qt::Horizontal) { + dt += gridSize.width(); + if (dt >= effects->numberOfDesktops()) { + if (wrap) + dt -= effects->numberOfDesktops(); + else + return desktop; + } + } else { + int d = (dt % gridSize.height()) + 1; + if (d >= gridSize.height()) { + if (wrap) + d -= gridSize.height(); + else + return desktop; + } + dt = dt - (dt % gridSize.height()) + d; + } + return dt + 1; +} + +//----------------------------------------------------------------------------- +// Activation + +void DesktopGridEffect::toggle() +{ + setActive(!activated); +} + +void DesktopGridEffect::setActive(bool active) +{ + if (effects->activeFullScreenEffect() && effects->activeFullScreenEffect() != this) + return; // Only one fullscreen effect at a time thanks + if (active && isMotionManagerMovingWindows()) + return; // Still moving windows from last usage - don't activate + if (activated == active) + return; // Already in that state + + activated = active; + if (activated) { + effects->setShowingDesktop(false); + if (timeline.currentValue() == 0) + setup(); + } else { + if (isUsingPresentWindows()) { + QList::iterator it; + for (it = m_managers.begin(); it != m_managers.end(); ++it) { + foreach (EffectWindow * w, (*it).managedWindows()) { + (*it).moveWindow(w, w->geometry()); + } + } + } + QTimer::singleShot(zoomDuration + 1, this, + [this] { + if (activated) + return; + for (EffectQuickScene *view : m_desktopButtons) { + view->hide(); + } + } + ); + setHighlightedDesktop(effects->currentDesktop()); // Ensure selected desktop is highlighted + } + effects->addRepaintFull(); +} + +void DesktopGridEffect::setup() +{ + if (!isActive()) + return; + if (!keyboardGrab) { + keyboardGrab = effects->grabKeyboard(this); + effects->startMouseInterception(this, Qt::PointingHandCursor); + effects->setActiveFullScreenEffect(this); + } + setHighlightedDesktop(effects->currentDesktop()); + + // Soft highlighting + qDeleteAll(hoverTimeline); + hoverTimeline.clear(); + for (int i = 0; i < effects->numberOfDesktops(); i++) { + QTimeLine *newTimeline = new QTimeLine(zoomDuration, this); + newTimeline->setEasingCurve(QEasingCurve::InOutSine); + hoverTimeline.append(newTimeline); + } + hoverTimeline[effects->currentDesktop() - 1]->setCurrentTime(hoverTimeline[effects->currentDesktop() - 1]->duration()); + + // Create desktop name textures if enabled + if (desktopNameAlignment) { + QFont font; + font.setBold(true); + font.setPointSize(12); + for (int i = 0; i < effects->numberOfDesktops(); i++) { + EffectFrame* frame = effects->effectFrame(EffectFrameUnstyled, false); + frame->setFont(font); + frame->setText(effects->desktopName(i + 1)); + frame->setAlignment(desktopNameAlignment); + desktopNames.append(frame); + } + } + setupGrid(); + setCurrentDesktop(effects->currentDesktop()); + + // setup the motion managers + if (m_usePresentWindows) + m_proxy = static_cast(effects->getProxy(BuiltInEffects::nameForEffect(BuiltInEffect::PresentWindows))); + if (isUsingPresentWindows()) { + m_proxy->reCreateGrids(); // revalidation on multiscreen, bug #351724 + for (int i = 1; i <= effects->numberOfDesktops(); i++) { + for (int j = 0; j < effects->numScreens(); j++) { + WindowMotionManager manager; + foreach (EffectWindow * w, effects->stackingOrder()) { + if (w->isOnDesktop(i) && w->screen() == j &&isRelevantWithPresentWindows(w)) { + manager.manage(w); + } + } + m_proxy->calculateWindowTransformations(manager.managedWindows(), j, manager); + m_managers.append(manager); + } + } + } + + auto it = m_desktopButtons.begin(); + const int n = DesktopGridConfig::showAddRemove() ? effects->numScreens() : 0; + for (int i = 0; i < n; ++i) { + EffectQuickScene *view; + QSize size; + if (it == m_desktopButtons.end()) { + view = new EffectQuickScene(this); + + connect(view, &EffectQuickView::repaintNeeded, this, []() { + effects->addRepaintFull(); + }); + + view->rootContext()->setContextProperty("effects", effects); + view->setSource(QUrl(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kwin/effects/desktopgrid/main.qml")))); + + QQuickItem *rootItem = view->rootItem(); + if (!rootItem) { + delete view; + continue; + } + + m_desktopButtons.append(view); + it = m_desktopButtons.end(); // changed through insert! + + size = QSize(rootItem->implicitWidth(), rootItem->implicitHeight()); + } else { + view = *it; + ++it; + size = view->size(); + } + const QRect screenRect = effects->clientArea(FullScreenArea, i, 1); + view->show(); // pseudo show must happen before geometry changes + const QPoint position(screenRect.right() - border/3 - size.width(), + screenRect.bottom() - border/3 - size.height()); + view->setGeometry(QRect(position, size)); + } + while (it != m_desktopButtons.end()) { + (*it)->deleteLater(); + it = m_desktopButtons.erase(it); + } +} + +void DesktopGridEffect::setupGrid() +{ + // We need these variables for every paint so lets cache them + int x, y; + int numDesktops = effects->numberOfDesktops(); + switch(layoutMode) { + default: + case LayoutPager: + orientation = Qt::Horizontal; + gridSize = effects->desktopGridSize(); + // sanity check: pager may report incorrect size in case of one desktop + if (numDesktops == 1) { + gridSize = QSize(1, 1); + } + break; + case LayoutAutomatic: + y = sqrt(float(numDesktops)) + 0.5; + x = float(numDesktops) / float(y) + 0.5; + if (x * y < numDesktops) + x++; + orientation = Qt::Horizontal; + gridSize.setWidth(x); + gridSize.setHeight(y); + break; + case LayoutCustom: + orientation = Qt::Horizontal; + gridSize.setWidth(ceil(effects->numberOfDesktops() / double(customLayoutRows))); + gridSize.setHeight(customLayoutRows); + break; + } + scale.clear(); + unscaledBorder.clear(); + scaledSize.clear(); + scaledOffset.clear(); + for (int i = 0; i < effects->numScreens(); i++) { + QRect geom = effects->clientArea(ScreenArea, i, 0); + double sScale; + if (gridSize.width() > gridSize.height()) + sScale = (geom.width() - border * (gridSize.width() + 1)) / double(geom.width() * gridSize.width()); + else + sScale = (geom.height() - border * (gridSize.height() + 1)) / double(geom.height() * gridSize.height()); + double sBorder = border / sScale; + QSizeF size( + double(geom.width()) * sScale, + double(geom.height()) * sScale + ); + QPointF offset( + geom.x() + (geom.width() - size.width() * gridSize.width() - border *(gridSize.width() - 1)) / 2.0, + geom.y() + (geom.height() - size.height() * gridSize.height() - border *(gridSize.height() - 1)) / 2.0 + ); + scale.append(sScale); + unscaledBorder.append(sBorder); + scaledSize.append(size); + scaledOffset.append(offset); + } +} + +void DesktopGridEffect::finish() +{ + if (desktopNameAlignment) { + qDeleteAll(desktopNames); + desktopNames.clear(); + } + + if (keyboardGrab) + effects->ungrabKeyboard(); + keyboardGrab = false; + effects->stopMouseInterception(this); + effects->setActiveFullScreenEffect(nullptr); + if (isUsingPresentWindows()) { + while (!m_managers.isEmpty()) { + m_managers.first().unmanageAll(); + m_managers.removeFirst(); + } + m_proxy = nullptr; + } +} + +void DesktopGridEffect::globalShortcutChanged(QAction *action, const QKeySequence& seq) +{ + if (action->objectName() != QStringLiteral("ShowDesktopGrid")) { + return; + } + shortcut.clear(); + shortcut.append(seq); +} + +bool DesktopGridEffect::isMotionManagerMovingWindows() const +{ + if (isUsingPresentWindows()) { + QList::const_iterator it; + for (it = m_managers.begin(); it != m_managers.end(); ++it) { + if ((*it).areWindowsMoving()) + return true; + } + } + return false; +} + +bool DesktopGridEffect::isUsingPresentWindows() const +{ + return (m_proxy != nullptr); +} + +// transforms the geometry of the moved window to a geometry on the desktop +// internal method only used when a window is dropped onto a desktop +QRectF DesktopGridEffect::moveGeometryToDesktop(int desktop) const +{ + QPointF point = unscalePos(m_windowMoveGeometry.topLeft() + cursorPos() - m_windowMoveStartPoint); + const double scaleFactor = scale[ windowMove->screen()]; + if (posToDesktop(m_windowMoveGeometry.topLeft() + cursorPos() - m_windowMoveStartPoint) != desktop) { + // topLeft is not on the desktop - check other corners + // if all corners are not on the desktop the window is bigger than the desktop - no matter what it will look strange + if (posToDesktop(m_windowMoveGeometry.topRight() + cursorPos() - m_windowMoveStartPoint) == desktop) { + point = unscalePos(m_windowMoveGeometry.topRight() + cursorPos() - m_windowMoveStartPoint) - + QPointF(m_windowMoveGeometry.width(), 0) / scaleFactor; + } else if (posToDesktop(m_windowMoveGeometry.bottomLeft() + cursorPos() - m_windowMoveStartPoint) == desktop) { + point = unscalePos(m_windowMoveGeometry.bottomLeft() + cursorPos() - m_windowMoveStartPoint) - + QPointF(0, m_windowMoveGeometry.height()) / scaleFactor; + } else if (posToDesktop(m_windowMoveGeometry.bottomRight() + cursorPos() - m_windowMoveStartPoint) == desktop) { + point = unscalePos(m_windowMoveGeometry.bottomRight() + cursorPos() - m_windowMoveStartPoint) - + QPointF(m_windowMoveGeometry.width(), m_windowMoveGeometry.height()) / scaleFactor; + } + } + return QRectF(point, m_windowMoveGeometry.size() / scaleFactor); +} + +void DesktopGridEffect::slotAddDesktop() +{ + effects->setNumberOfDesktops(effects->numberOfDesktops() + 1); +} + +void DesktopGridEffect::slotRemoveDesktop() +{ + effects->setNumberOfDesktops(effects->numberOfDesktops() - 1); +} + +void DesktopGridEffect::slotNumberDesktopsChanged(uint old) +{ + if (!activated) + return; + const uint desktop = effects->numberOfDesktops(); + if (old < desktop) + desktopsAdded(old); + else + desktopsRemoved(old); +} + +void DesktopGridEffect::desktopsAdded(int old) +{ + const int desktop = effects->numberOfDesktops(); + for (int i = old; i <= effects->numberOfDesktops(); i++) { + // add a timeline for the new desktop + QTimeLine *newTimeline = new QTimeLine(zoomDuration, this); + newTimeline->setEasingCurve(QEasingCurve::InOutSine); + hoverTimeline.append(newTimeline); + } + + // Create desktop name textures if enabled + if (desktopNameAlignment) { + QFont font; + font.setBold(true); + font.setPointSize(12); + for (int i = old; i < desktop; i++) { + EffectFrame* frame = effects->effectFrame(EffectFrameUnstyled, false); + frame->setFont(font); + frame->setText(effects->desktopName(i + 1)); + frame->setAlignment(desktopNameAlignment); + desktopNames.append(frame); + } + } + + if (isUsingPresentWindows()) { + for (int i = old+1; i <= effects->numberOfDesktops(); ++i) { + for (int j = 0; j < effects->numScreens(); ++j) { + WindowMotionManager manager; + foreach (EffectWindow * w, effects->stackingOrder()) { + if (w->isOnDesktop(i) && w->screen() == j &&isRelevantWithPresentWindows(w)) { + manager.manage(w); + } + } + m_proxy->calculateWindowTransformations(manager.managedWindows(), j, manager); + m_managers.append(manager); + } + } + } + + setupGrid(); + + // and repaint + effects->addRepaintFull(); +} + +void DesktopGridEffect::desktopsRemoved(int old) +{ + const int desktop = effects->numberOfDesktops(); + for (int i = desktop; i < old; i++) { + delete hoverTimeline.takeLast(); + if (desktopNameAlignment) { + delete desktopNames.last(); + desktopNames.removeLast(); + } + if (isUsingPresentWindows()) { + for (int j = 0; j < effects->numScreens(); ++j) { + WindowMotionManager& manager = m_managers.last(); + manager.unmanageAll(); + m_managers.removeLast(); + } + } + } + // add removed windows to the last desktop + if (isUsingPresentWindows()) { + for (int j = 0; j < effects->numScreens(); ++j) { + WindowMotionManager& manager = m_managers[(desktop-1)*(effects->numScreens())+j ]; + foreach (EffectWindow * w, effects->stackingOrder()) { + if (manager.isManaging(w)) + continue; + if (w->isOnDesktop(desktop) && w->screen() == j && isRelevantWithPresentWindows(w)) { + manager.manage(w); + } + } + m_proxy->calculateWindowTransformations(manager.managedWindows(), j, manager); + } + } + + setupGrid(); + + // and repaint + effects->addRepaintFull(); +} +//TODO: kill this function? or at least keep a consistent numeration with desktops starting from 1 +QVector DesktopGridEffect::desktopList(const EffectWindow *w) const +{ + if (w->isOnAllDesktops()) { + static QVector allDesktops; + if (allDesktops.count() != effects->numberOfDesktops()) { + allDesktops.resize(effects->numberOfDesktops()); + for (int i = 0; i < effects->numberOfDesktops(); ++i) + allDesktops[i] = i; + } + return allDesktops; + } + + QVector desks; + desks.resize(w->desktops().count()); + int i = 0; + for (const int desk : w->desktops()) { + desks[i++] = desk-1; + } + return desks; +} + +bool DesktopGridEffect::isActive() const +{ + return (timeline.currentValue() != 0 || activated || (isUsingPresentWindows() && isMotionManagerMovingWindows())) && !effects->isScreenLocked(); +} + +bool DesktopGridEffect::isRelevantWithPresentWindows(EffectWindow *w) const +{ + if (w->isSpecialWindow() || w->isUtility()) { + return false; + } + + if (w->isSkipSwitcher()) { + return false; + } + + if (w->isDeleted()) { + return false; + } + + if (!w->acceptsFocus()) { + return false; + } + + if (!w->isOnCurrentActivity()) { + return false; + } + + return true; +} + +} // namespace + diff --git a/effects/desktopgrid/desktopgrid.h b/effects/desktopgrid/desktopgrid.h new file mode 100644 index 0000000..dff82a4 --- /dev/null +++ b/effects/desktopgrid/desktopgrid.h @@ -0,0 +1,161 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Lubos Lunak + SPDX-FileCopyrightText: 2008 Lucas Murray + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_DESKTOPGRID_H +#define KWIN_DESKTOPGRID_H + +#include +#include +#include + +#include "kwineffectquickview.h" + +namespace KWin +{ + +class PresentWindowsEffectProxy; + +class DesktopGridEffect + : public Effect +{ + Q_OBJECT + Q_PROPERTY(int zoomDuration READ configuredZoomDuration) + Q_PROPERTY(int border READ configuredBorder) + Q_PROPERTY(Qt::Alignment desktopNameAlignment READ configuredDesktopNameAlignment) + Q_PROPERTY(int layoutMode READ configuredLayoutMode) + Q_PROPERTY(int customLayoutRows READ configuredCustomLayoutRows) + Q_PROPERTY(bool usePresentWindows READ isUsePresentWindows) + // TODO: electric borders +public: + DesktopGridEffect(); + ~DesktopGridEffect() override; + void reconfigure(ReconfigureFlags) override; + void prePaintScreen(ScreenPrePaintData& data, int time) override; + void paintScreen(int mask, const QRegion ®ion, ScreenPaintData& data) override; + void postPaintScreen() override; + void prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time) override; + void paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data) override; + void windowInputMouseEvent(QEvent* e) override; + void grabbedKeyboardEvent(QKeyEvent* e) override; + bool borderActivated(ElectricBorder border) override; + bool isActive() const override; + + int requestedEffectChainPosition() const override { + return 50; + } + + enum { LayoutPager, LayoutAutomatic, LayoutCustom }; // Layout modes + + // for properties + int configuredZoomDuration() const { + return zoomDuration; + } + int configuredBorder() const { + return border; + } + Qt::Alignment configuredDesktopNameAlignment() const { + return desktopNameAlignment; + } + int configuredLayoutMode() const { + return layoutMode; + } + int configuredCustomLayoutRows() const { + return customLayoutRows; + } + bool isUsePresentWindows() const { + return m_usePresentWindows; + } +private Q_SLOTS: + void toggle(); + // slots for global shortcut changed + // needed to toggle the effect + void globalShortcutChanged(QAction *action, const QKeySequence& seq); + void slotAddDesktop(); + void slotRemoveDesktop(); + void slotWindowAdded(KWin::EffectWindow* w); + void slotWindowClosed(KWin::EffectWindow *w); + void slotWindowDeleted(KWin::EffectWindow *w); + void slotNumberDesktopsChanged(uint old); + void slotWindowFrameGeometryChanged(KWin::EffectWindow *w, const QRect &old); + +private: + QPointF scalePos(const QPoint& pos, int desktop, int screen = -1) const; + QPoint unscalePos(const QPoint& pos, int* desktop = nullptr) const; + int posToDesktop(const QPoint& pos) const; + EffectWindow* windowAt(QPoint pos) const; + void setCurrentDesktop(int desktop); + void setHighlightedDesktop(int desktop); + int desktopToRight(int desktop, bool wrap = true) const; + int desktopToLeft(int desktop, bool wrap = true) const; + int desktopUp(int desktop, bool wrap = true) const; + int desktopDown(int desktop, bool wrap = true) const; + void setActive(bool active); + void setup(); + void setupGrid(); + void finish(); + bool isMotionManagerMovingWindows() const; + bool isRelevantWithPresentWindows(EffectWindow *w) const; + bool isUsingPresentWindows() const; + QRectF moveGeometryToDesktop(int desktop) const; + void desktopsAdded(int old); + void desktopsRemoved(int old); + QVector desktopList(const EffectWindow *w) const; + + QList borderActivate; + int zoomDuration; + int border; + Qt::Alignment desktopNameAlignment; + int layoutMode; + int customLayoutRows; + + bool activated; + QTimeLine timeline; + int paintingDesktop; + int highlightedDesktop; + int sourceDesktop; + int m_originalMovingDesktop; + bool keyboardGrab; + bool wasWindowMove, wasWindowCopy, wasDesktopMove, isValidMove; + EffectWindow* windowMove; + QPoint windowMoveDiff; + QPoint dragStartPos; + + // Soft highlighting + QList hoverTimeline; + + QList< EffectFrame* > desktopNames; + + QSize gridSize; + Qt::Orientation orientation; + QPoint activeCell; + // Per screen variables + QList scale; // Because the border isn't a ratio each screen is different + QList unscaledBorder; + QList scaledSize; + QList scaledOffset; + + // Shortcut - needed to toggle the effect + QList shortcut; + + PresentWindowsEffectProxy* m_proxy; + QList m_managers; + bool m_usePresentWindows; + QRect m_windowMoveGeometry; + QPoint m_windowMoveStartPoint; + + QVector m_desktopButtons; + + QAction *m_activateAction; + +}; + +} // namespace + +#endif diff --git a/effects/desktopgrid/desktopgrid.kcfg b/effects/desktopgrid/desktopgrid.kcfg new file mode 100644 index 0000000..720924d --- /dev/null +++ b/effects/desktopgrid/desktopgrid.kcfg @@ -0,0 +1,32 @@ + + + + + + + + 0 + + + 10 + + + 0 + + + 0 + + + 2 + + + true + + + true + + + diff --git a/effects/desktopgrid/desktopgrid_config.cpp b/effects/desktopgrid/desktopgrid_config.cpp new file mode 100644 index 0000000..7d3f7c8 --- /dev/null +++ b/effects/desktopgrid/desktopgrid_config.cpp @@ -0,0 +1,129 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Rivo Laks + SPDX-FileCopyrightText: 2008 Lucas Murray + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "desktopgrid_config.h" +// KConfigSkeleton +#include "desktopgridconfig.h" +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include + +K_PLUGIN_FACTORY_WITH_JSON(DesktopGridEffectConfigFactory, + "desktopgrid_config.json", + registerPlugin();) + +namespace KWin +{ + +DesktopGridEffectConfigForm::DesktopGridEffectConfigForm(QWidget* parent) : QWidget(parent) +{ + setupUi(this); +} + +DesktopGridEffectConfig::DesktopGridEffectConfig(QWidget* parent, const QVariantList& args) + : KCModule(KAboutData::pluginData(QStringLiteral("desktopgrid")), parent, args) +{ + m_ui = new DesktopGridEffectConfigForm(this); + + QVBoxLayout* layout = new QVBoxLayout(this); + + layout->addWidget(m_ui); + + // Shortcut config. The shortcut belongs to the component "kwin"! + m_actionCollection = new KActionCollection(this, QStringLiteral("kwin")); + + m_actionCollection->setComponentDisplayName(i18n("KWin")); + m_actionCollection->setConfigGroup(QStringLiteral("DesktopGrid")); + m_actionCollection->setConfigGlobal(true); + + QAction* a = m_actionCollection->addAction(QStringLiteral("ShowDesktopGrid")); + a->setText(i18n("Show Desktop Grid")); + a->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::CTRL + Qt::Key_F8); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::CTRL + Qt::Key_F8); + + m_ui->shortcutEditor->addCollection(m_actionCollection); + + + m_ui->desktopNameAlignmentCombo->addItem(i18nc("Desktop name alignment:", "Disabled"), QVariant(Qt::Alignment())); + m_ui->desktopNameAlignmentCombo->addItem(i18n("Top"), QVariant(Qt::AlignHCenter | Qt::AlignTop)); + m_ui->desktopNameAlignmentCombo->addItem(i18n("Top-Right"), QVariant(Qt::AlignRight | Qt::AlignTop)); + m_ui->desktopNameAlignmentCombo->addItem(i18n("Right"), QVariant(Qt::AlignRight | Qt::AlignVCenter)); + m_ui->desktopNameAlignmentCombo->addItem(i18n("Bottom-Right"), QVariant(Qt::AlignRight | Qt::AlignBottom)); + m_ui->desktopNameAlignmentCombo->addItem(i18n("Bottom"), QVariant(Qt::AlignHCenter | Qt::AlignBottom)); + m_ui->desktopNameAlignmentCombo->addItem(i18n("Bottom-Left"), QVariant(Qt::AlignLeft | Qt::AlignBottom)); + m_ui->desktopNameAlignmentCombo->addItem(i18n("Left"), QVariant(Qt::AlignLeft | Qt::AlignVCenter)); + m_ui->desktopNameAlignmentCombo->addItem(i18n("Top-Left"), QVariant(Qt::AlignLeft | Qt::AlignTop)); + m_ui->desktopNameAlignmentCombo->addItem(i18n("Center"), QVariant(Qt::AlignCenter)); + + DesktopGridConfig::instance(KWIN_CONFIG); + addConfig(DesktopGridConfig::self(), m_ui); + connect(m_ui->kcfg_LayoutMode, qOverload(&KComboBox::currentIndexChanged), this, &DesktopGridEffectConfig::layoutSelectionChanged); + connect(m_ui->desktopNameAlignmentCombo, qOverload(&KComboBox::currentIndexChanged), this, &DesktopGridEffectConfig::markAsChanged); + connect(m_ui->shortcutEditor, &KShortcutsEditor::keyChange, this, &DesktopGridEffectConfig::markAsChanged); + + load(); + layoutSelectionChanged(); +} + +DesktopGridEffectConfig::~DesktopGridEffectConfig() +{ + // If save() is called undoChanges() has no effect + m_ui->shortcutEditor->undoChanges(); +} + +void DesktopGridEffectConfig::save() +{ + m_ui->shortcutEditor->save(); + DesktopGridConfig::setDesktopNameAlignment(m_ui->desktopNameAlignmentCombo->itemData(m_ui->desktopNameAlignmentCombo->currentIndex()).toInt()); + KCModule::save(); + + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("desktopgrid")); +} + +void DesktopGridEffectConfig::load() +{ + KCModule::load(); + m_ui->desktopNameAlignmentCombo->setCurrentIndex(m_ui->desktopNameAlignmentCombo->findData(QVariant(DesktopGridConfig::desktopNameAlignment()))); +} + +void DesktopGridEffectConfig::layoutSelectionChanged() +{ + if (m_ui->kcfg_LayoutMode->currentIndex() == DesktopGridEffect::LayoutCustom) { + m_ui->layoutRowsLabel->setEnabled(true); + m_ui->kcfg_CustomLayoutRows->setEnabled(true); + } else { + m_ui->layoutRowsLabel->setEnabled(false); + m_ui->kcfg_CustomLayoutRows->setEnabled(false); + } +} + +void DesktopGridEffectConfig::defaults() +{ + KCModule::defaults(); + m_ui->desktopNameAlignmentCombo->setCurrentIndex(0); +} + +} // namespace + +#include "desktopgrid_config.moc" diff --git a/effects/desktopgrid/desktopgrid_config.desktop b/effects/desktopgrid/desktopgrid_config.desktop new file mode 100644 index 0000000..1978805 --- /dev/null +++ b/effects/desktopgrid/desktopgrid_config.desktop @@ -0,0 +1,85 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=KCModule + +X-KDE-Library=kwin_desktopgrid_config +X-KDE-ParentComponents=desktopgrid + +Name=Desktop Grid +Name[af]=Werkskerm rooster +Name[ar]=شبكة سطح المكتب +Name[az]=Bütün İş Masaları +Name[be]=Сетка на працоўны стол +Name[be@latin]=Rabočaja sietka +Name[bg]=Мрежест работен плот +Name[bn]=ডেস্কটপ গ্রিড +Name[bs]=Mreža povrÅ¡i +Name[ca]=Quadrícula de l'escriptori +Name[ca@valencia]=Graella d'escriptori +Name[cs]=Mřížka plochy +Name[da]=Skrivebordsgitter +Name[de]=Arbeitsflächen-Umschalter (Raster) +Name[el]=Κάνναβος επιφάνειας εργασίας +Name[en_GB]=Desktop Grid +Name[eo]=Labortabla krado +Name[es]=Rejilla del escritorio +Name[et]=Töölauavõrgustik +Name[eu]=Mahaigain sareta +Name[fa]=توری رومیزی +Name[fi]=Työpöydän ruudukko +Name[fr]=Bureau en grille +Name[fy]=Buroblêd roaster +Name[ga]=Greille na nDeasc +Name[gl]=Grade do escritorio +Name[gu]=ડેસ્કટોપ જાળી +Name[he]=רשת שולחנות עבודה +Name[hi]=डेस्कटॉप ग्रिड +Name[hne]=डेस्कटाप ग्रिड +Name[hr]=Mreža radne povrÅ¡ine +Name[hu]=Asztalrács +Name[ia]=Grillia de scriptorio +Name[id]=Kisi Desktop +Name[is]=Skjáborðsmöskvi +Name[it]=Griglia dei Desktop +Name[ja]=デスクトップグリッド +Name[kk]=Үстел торы +Name[km]=ក្រឡា​ចត្រង្គ​ផ្ទៃ​តុ​ +Name[kn]=ಗಣಕತೆರೆ ಚೌಕಳಿ +Name[ko]=데스크톱 모눈 +Name[lt]=Darbalaukio tinklelis +Name[lv]=Darbvirsmas režģis +Name[mai]=डेस्कटाप ग्रिड +Name[mk]=Мрежа на раб. површина +Name[ml]=പണിയറക്കള്ളികള്‍ +Name[mr]=डेस्कटॉप जाळे +Name[nb]=Skrivebordsrutenett +Name[nds]=Schriefdisch-Gadder +Name[ne]=डेस्कटप ग्रिड +Name[nl]=Bureaubladraster +Name[nn]=Skrivebordsoversikt +Name[pa]=ਡੈਸਕਟਾਪ ਗਰਿੱਡ +Name[pl]=Siatka pulpitu +Name[pt]=Grelha de Ecrãs +Name[pt_BR]=Grade de áreas de trabalho virtuais +Name[ro]=Grilă birou +Name[ru]=Все рабочие столы +Name[se]=Čállinbeavderuvttodat +Name[si]=වැඩතල ජාලය +Name[sk]=Plochy v mriežke +Name[sl]=Mreža namizij +Name[sr]=Мрежа површи +Name[sr@ijekavian]=Мрежа површи +Name[sr@ijekavianlatin]=Mreža povrÅ¡i +Name[sr@latin]=Mreža povrÅ¡i +Name[sv]=Skrivbordsrutnät +Name[ta]=மேல்மேசை கிரிட் +Name[te]=డెస్‍క్ టాప్ గ్రిడ్ +Name[th]=พื้นที่ทำงานเป็นตาราง +Name[tr]=Masaüstü Izgarası +Name[ug]=ئۈستەلئۈستى سېتكىسى +Name[uk]=Таблиця стільниць +Name[vi]=Lưới màn hình nền +Name[wa]=Glindisse do scribanne +Name[x-test]=xxDesktop Gridxx +Name[zh_CN]=桌面窗格 +Name[zh_TW]=桌面格 diff --git a/effects/desktopgrid/desktopgrid_config.h b/effects/desktopgrid/desktopgrid_config.h new file mode 100644 index 0000000..fe3bdba --- /dev/null +++ b/effects/desktopgrid/desktopgrid_config.h @@ -0,0 +1,51 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Rivo Laks + SPDX-FileCopyrightText: 2008 Lucas Murray + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_DESKTOPGRID_CONFIG_H +#define KWIN_DESKTOPGRID_CONFIG_H + +#include + +#include "ui_desktopgrid_config.h" +#include "desktopgrid.h" + +namespace KWin +{ + +class DesktopGridEffectConfigForm : public QWidget, public Ui::DesktopGridEffectConfigForm +{ + Q_OBJECT +public: + explicit DesktopGridEffectConfigForm(QWidget* parent); +}; + +class DesktopGridEffectConfig : public KCModule +{ + Q_OBJECT +public: + explicit DesktopGridEffectConfig(QWidget* parent = nullptr, const QVariantList& args = QVariantList()); + ~DesktopGridEffectConfig() override; + +public Q_SLOTS: + void save() override; + void load() override; + void defaults() override; + +private Q_SLOTS: + void layoutSelectionChanged(); + +private: + DesktopGridEffectConfigForm* m_ui; + KActionCollection* m_actionCollection; +}; + +} // namespace + +#endif diff --git a/effects/desktopgrid/desktopgrid_config.ui b/effects/desktopgrid/desktopgrid_config.ui new file mode 100644 index 0000000..50a73b1 --- /dev/null +++ b/effects/desktopgrid/desktopgrid_config.ui @@ -0,0 +1,250 @@ + + + KWin::DesktopGridEffectConfigForm + + + + 0 + 0 + 574 + 250 + + + + + + + Appearance + + + + + + Zoom &duration: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_ZoomDuration + + + + + + + + 0 + 0 + + + + Default + + + 5000 + + + 10 + + + + + + + Border wid&th: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_BorderWidth + + + + + + + + 0 + 0 + + + + 100 + + + 10 + + + + + + + Desktop &name alignment: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + desktopNameAlignmentCombo + + + + + + + + 0 + 0 + + + + + + + + &Layout mode: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_LayoutMode + + + + + + + + 0 + 0 + + + + + Pager + + + + + Automatic + + + + + Custom + + + + + + + + N&umber of rows: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_CustomLayoutRows + + + + + + + + 0 + 0 + + + + 1 + + + 20 + + + 2 + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + Use Present Windows effect to layout the windows + + + + + + + Show buttons to alter count of virtual desktops + + + + + + + + + + Activation + + + + + + + 0 + 0 + + + + KShortcutsEditor::GlobalAction + + + + + + + + + + + KComboBox + QComboBox +

kcombobox.h
+ + + KShortcutsEditor + QWidget +
KShortcutsEditor
+ 1 +
+ + + kcfg_ZoomDuration + kcfg_BorderWidth + desktopNameAlignmentCombo + kcfg_LayoutMode + kcfg_CustomLayoutRows + + + + diff --git a/effects/desktopgrid/desktopgridconfig.kcfgc b/effects/desktopgrid/desktopgridconfig.kcfgc new file mode 100644 index 0000000..70a3ed5 --- /dev/null +++ b/effects/desktopgrid/desktopgridconfig.kcfgc @@ -0,0 +1,5 @@ +File=desktopgrid.kcfg +ClassName=DesktopGridConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/effects/desktopgrid/main.qml b/effects/desktopgrid/main.qml new file mode 100644 index 0000000..4682fd6 --- /dev/null +++ b/effects/desktopgrid/main.qml @@ -0,0 +1,26 @@ +/* + 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 +*/ +import QtQuick 2.0 +import QtQuick.Layouts 1.2 +import org.kde.plasma.components 3.0 as Plasma + +RowLayout { + Plasma.Button { + objectName: "removeButton" + enabled: effects.desktops > 1 + icon.name: "list-remove" + onClicked: effects.desktops-- + } + Plasma.Button { + objectName: "addButton" + enabled: effects.desktops < 20 + icon.name: "list-add" + onClicked: effects.desktops++ + } +} diff --git a/effects/dialogparent/package/contents/code/main.js b/effects/dialogparent/package/contents/code/main.js new file mode 100644 index 0000000..2c56911 --- /dev/null +++ b/effects/dialogparent/package/contents/code/main.js @@ -0,0 +1,153 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +/*global effect, effects, animate, cancel, set, animationTime, Effect, QEasingCurve */ +/*jslint continue: true */ +var dialogParentEffect = { + duration: animationTime(300), + windowAdded: function (window) { + "use strict"; + if (window.modal) { + dialogParentEffect.dialogGotModality(window); + } + }, + dialogGotModality: function (window) { + "use strict"; + var mainWindows = window.mainWindows(); + for (var i = 0; i < mainWindows.length; ++i) { + dialogParentEffect.startAnimation(mainWindows[i]); + } + }, + startAnimation: function (window) { + "use strict"; + if (window.visible === false) { + return; + } + if (window.dialogParentAnimation) { + if (redirect(window.dialogParentAnimation, Effect.Forward)) { + return; + } + cancel(window.dialogParentAnimation); + } + window.dialogParentAnimation = set({ + window: window, + duration: dialogParentEffect.duration, + keepAlive: false, + animations: [{ + type: Effect.Saturation, + to: 0.4 + }, { + type: Effect.Brightness, + to: 0.6 + }] + }); + }, + windowClosed: function (window) { + "use strict"; + if (window.modal) { + dialogParentEffect.dialogLostModality(window); + } + }, + dialogLostModality: function (window) { + "use strict"; + var mainWindows = window.mainWindows(); + for (var i = 0; i < mainWindows.length; ++i) { + dialogParentEffect.cancelAnimationSmooth(mainWindows[i]); + } + }, + cancelAnimationInstant: function (window) { + "use strict"; + if (window.dialogParentAnimation) { + cancel(window.dialogParentAnimation); + delete window.dialogParentAnimation; + } + }, + cancelAnimationSmooth: function (window) { + "use strict"; + if (!window.dialogParentAnimation) { + return; + } + if (redirect(window.dialogParentAnimation, Effect.Backward)) { + return; + } + cancel(window.dialogParentAnimation); + delete window.dialogParentEffect; + }, + desktopChanged: function () { + "use strict"; + // If there is an active full screen effect, then try smoothly dim/brighten + // the main windows. Keep in mind that in order for this to work properly, this + // effect has to come after the full screen effect in the effect chain, + // otherwise this slot will be invoked before the full screen effect can mark + // itself as a full screen effect. + if (effects.hasActiveFullScreenEffect) { + return; + } + + var windows = effects.stackingOrder; + for (var i = 0; i < windows.length; ++i) { + var window = windows[i]; + dialogParentEffect.cancelAnimationInstant(window); + dialogParentEffect.restartAnimation(window); + } + }, + modalDialogChanged: function(dialog) { + "use strict"; + if (dialog.modal === false) + dialogParentEffect.dialogLostModality(dialog); + else if (dialog.modal === true) + dialogParentEffect.dialogGotModality(dialog); + }, + restartAnimation: function (window) { + "use strict"; + if (window === null || window.findModal() === null) { + return; + } + dialogParentEffect.startAnimation(window); + if (window.dialogParentAnimation) { + complete(window.dialogParentAnimation); + } + }, + activeFullScreenEffectChanged: function () { + "use strict"; + var windows = effects.stackingOrder; + for (var i = 0; i < windows.length; ++i) { + var dialog = windows[i]; + if (!dialog.modal) { + continue; + } + if (effects.hasActiveFullScreenEffect) { + dialogParentEffect.dialogLostModality(dialog); + } else { + dialogParentEffect.dialogGotModality(dialog); + } + } + }, + init: function () { + "use strict"; + var i, windows; + effects.windowAdded.connect(dialogParentEffect.windowAdded); + effects.windowClosed.connect(dialogParentEffect.windowClosed); + effects.windowMinimized.connect(dialogParentEffect.cancelAnimationInstant); + effects.windowUnminimized.connect(dialogParentEffect.restartAnimation); + effects.windowModalityChanged.connect(dialogParentEffect.modalDialogChanged) + effects['desktopChanged(int,int)'].connect(dialogParentEffect.desktopChanged); + effects.desktopPresenceChanged.connect(dialogParentEffect.cancelAnimationInstant); + effects.desktopPresenceChanged.connect(dialogParentEffect.restartAnimation); + effects.activeFullScreenEffectChanged.connect( + dialogParentEffect.activeFullScreenEffectChanged); + + // start animation + windows = effects.stackingOrder; + for (i = 0; i < windows.length; i += 1) { + dialogParentEffect.restartAnimation(windows[i]); + } + } +}; +dialogParentEffect.init(); diff --git a/effects/dialogparent/package/metadata.desktop b/effects/dialogparent/package/metadata.desktop new file mode 100644 index 0000000..e23d29a --- /dev/null +++ b/effects/dialogparent/package/metadata.desktop @@ -0,0 +1,159 @@ +[Desktop Entry] +Name=Dialog Parent +Name[af]=Voorafgaande dialoog +Name[ar]=مولدة الحوار +Name[az]=Əsas dialoq +Name[be]=Бацькоўскае акно +Name[bg]=Основен прозорец +Name[bs]=Roditelj dijaloga +Name[ca]=Diàleg principal +Name[ca@valencia]=Diàleg principal +Name[cs]=Předek dialogu +Name[da]=Dialogens forældrevindue +Name[de]=Eltern-Fenster abdunkeln +Name[el]=Γονικός διάλογος +Name[en_GB]=Dialogue Box Parent +Name[eo]=Dialoga patro +Name[es]=Padre de la ventana +Name[et]=Dialoogi eellane +Name[eu]=Elkarrizketa-koadroaren gurasoa +Name[fa]=پدر محاوره +Name[fi]=Isäikkuna +Name[fr]=Boîte de dialogue parente +Name[fy]=Dialooch eigner +Name[ga]=Máthairdialóg +Name[gl]=Pai do diálogo +Name[gu]=સંવાદ પિતૃ +Name[he]=חלון־האב של תיבת דו־שיח +Name[hi]=मूल संवाद +Name[hne]=मूल गोठ +Name[hr]=Dialog Roditelj +Name[hu]=Párbeszédablak-tartó +Name[ia]=Dialogo genitor +Name[id]=Induk Dialog +Name[is]=Dekking undir valglugga +Name[it]=Finestra madre +Name[ja]=ダイアログの親 +Name[kk]=Диалогтың аталығы +Name[km]=ប្រអប់​មេ​ +Name[kn]=ಸಂವಾದ ಪೂರ್ವಜ (ಪೇರೆಂಟ್) +Name[ko]=대화 상자 부모 +Name[lt]=Dialogo virÅ¡esnis +Name[lv]=Dialoga vecāks +Name[mai]=मूल संवाद +Name[ml]=മാതൃജാലകം +Name[mr]=मूळ संवाद +Name[nb]=Dialog-mor +Name[nds]=Överornt Finster +Name[ne]=प्रमूल संवाद +Name[nl]=Dialoogeigenaar +Name[nn]=Mørklegg foreldrevindauge +Name[pa]=ਡਾਈਲਾਗ ਪੇਰੈਂਟ +Name[pl]=Rodzic okna dialogowego +Name[pt]=Pai da Janela +Name[pt_BR]=Diálogo pai +Name[ro]=Părinte dialog +Name[ru]=Затемнение основного окна +Name[se]=LáseÅ¡váhnen +Name[si]=සංවාදය අයත් +Name[sk]=Nadradený dialóg +Name[sl]=Nadrejeno pogovorno okno +Name[sr]=Родитељ дијалога +Name[sr@ijekavian]=Родитељ дијалога +Name[sr@ijekavianlatin]=Roditelj dijaloga +Name[sr@latin]=Roditelj dijaloga +Name[sv]=Dialogrutors ägare +Name[ta]=தாய் பலகம் +Name[te]=డైలాగ్ మాత్రుక +Name[th]=กล่องโต้ตอบหลัก +Name[tr]=İletişim Kutusu Sahibi +Name[ug]=ئاتا سۆزلەشكۈ +Name[uk]=Батьківське вікно +Name[vi]=Cha hộp thoại +Name[wa]=Parint del divize +Name[x-test]=xxDialog Parentxx +Name[zh_CN]=暗淡父级对话框 +Name[zh_TW]=上層對話框 +Icon=preferences-system-windows-effect-dialog-parent +Comment=Darkens the parent window of the currently active dialog +Comment[ar]=تعتم النافذة المولدة للحوار النشط حالياً +Comment[az]=Dialoq göstərilərkən əsas pəncərənin tutqunlaşması +Comment[bg]=Затъмняване на главния от активните прозорци +Comment[bs]=Zatamnjuje roditeljski prozor trenutno aktivnog dijaloga +Comment[ca]=Enfosqueix la finestra principal del diàleg actual actiu +Comment[ca@valencia]=Enfosqueix la finestra principal del diàleg actual actiu +Comment[cs]=Ztmaví okno nadřazené aktivnímu dialogu +Comment[da]=Gør forældrevindue til aktuelt aktiv dialog mørkere +Comment[de]=Dunkelt das Eltern-Fenster des aktiven Dialogs ab. +Comment[el]=Σκίαση του γονικού παραθύρου του τρέχοντος ενεργού διαλόγου +Comment[en_GB]=Darkens the parent window of the currently active dialogue box +Comment[eo]=Malheligas patrajn fenestrojn de la aktuala aktiva dialogo +Comment[es]=Oscurece la ventana madre de los diálogos activos +Comment[et]=Tumendab aktiivse dialoogi eellasakna +Comment[eu]=Unean aktibo dagoen elkarrizketa-koadroaren leiho gurasoa iluntzen du +Comment[fi]=Himmentää aktiivisen kyselyikkunan isäntäikkunan +Comment[fr]=Assombrit la fenêtre parente de la fenêtre active courante +Comment[fy]=Fertsjustert it haadfinster fan aktive dialogen +Comment[ga]=Dorchaigh máthairfhuinneog na dialóige atá gníomhach faoi láthair +Comment[gl]=Escurece a xanela pai do diálogo activo +Comment[gu]=સક્રિય સંવાદની મુખ્ય વિન્ડોને ઘાટી બનાવે છે +Comment[he]=מחשיך את חלון האב של תיבת הדו־שיח הפעילה +Comment[hi]=वर्तमान सक्रिय संवाद के मुख्य विंडो को गाढ़ा करता है +Comment[hne]=हाल के सक्रिय गोठ के असली विंडो ल गाढ़ा करथे +Comment[hr]=Zatamnjenje prozora-roditelja trenutno aktivnog dialoga +Comment[hu]=Elsötétíti az aktív párbeszédablak szülőablakát +Comment[ia]=Il obscura le fenestra genitor del dialogo currentemente active +Comment[id]=Pergelap window induk dari dialog yang saat ini aktif +Comment[is]=Dekkir upprunaglugga (foreldri) virka gluggans +Comment[it]=Scurisce la finestra madre di quella attiva +Comment[ja]=現在アクティブなダイアログの親ウィンドウを暗くします +Comment[kk]=Белсенді диалогтың аталық терезесін күңгірттеп көрсету +Comment[km]=ធ្វើ​ឲ្យ​បង្អួច​មេ​របស់​ប្រអប់​សកម្ម​បច្ចុប្បន្ន​ងងឹត​ +Comment[kn]=ಸಕ್ರಿಯವಾಗಿರುವ ಸಂವಾದದ ಪೂರ್ವಜ ಕಿಟಕಿಯನ್ನು ಮಸುಕಾಗಿಸುತ್ತದೆ +Comment[ko]=현재 활성 대화 상자의 부모 창을 어둡게 합니다 +Comment[lt]=Užtemdo esamo aktyvaus dialogo virÅ¡esnį langą +Comment[lv]=SatumÅ¡ina aktÄ«vā loga vecāka logu +Comment[ml]=സജീവഡയലോഗിന്റെ മാതൃജാലകം കറുപ്പിക്കുക +Comment[mr]=वर्तमान सक्रिय संवादची मुख्य चौकट गडद करा +Comment[nb]=Gjør mor-vinduet til den aktive dialogen mørkere +Comment[nds]=Maakt dat Moderfinstern vun den aktiven Dialoog düüsterer +Comment[nl]=Maakt het venster dat bij de ouder van de actieve dialoog hoort donkerder +Comment[nn]=Gjer foreldrevindauget til det aktive dialogvindauget mørkare +Comment[pa]=ਮੌਜੂਦਾ ਐਕਟਿਵ ਡਾਈਲਾਗ ਉੱਤੇ ਮੁੱਢਲੇ ਵਿੰਡੋ ਦਾ ਪਰਛਾਵਾਂ +Comment[pl]=Przyciemnia nadrzędne okna obecnie aktywnego okna dialogowego +Comment[pt]=Escurece a janela-mãe da janela activa de momento +Comment[pt_BR]=Escurece as janelas pai do diálogo ativo atual +Comment[ro]=Întunecă fereastra-părinte a dialogului activ +Comment[ru]=Затемнение основного окна при показе диалога +Comment[si]=වත්මන් සක්‍රීය සංවාදය අයත් කවුළුව අඳුරු කරන්න +Comment[sk]=Stmaví nadradené okno aktívneho dialógu +Comment[sl]=Potemni nadrejeno okno za trenutno dejavno pogovorno okno +Comment[sr]=Затамњује родитељски прозор тренутно активног дијалога +Comment[sr@ijekavian]=Затамњује родитељски прозор тренутно активног дијалога +Comment[sr@ijekavianlatin]=Zatamnjuje roditeljski prozor trenutno aktivnog dijaloga +Comment[sr@latin]=Zatamnjuje roditeljski prozor trenutno aktivnog dijaloga +Comment[sv]=Gör fönstret som äger den för närvarande aktiva dialogrutan mörkare +Comment[ta]=தற்போது செயலில் உள்ள பலகத்தில் தாய் சாளரத்தை இருளாக்கும் +Comment[th]=ทำให้หน้าต่างหลักมืดลงจากกล่องโต้ตอบที่ใช้อยู่ +Comment[tr]=Etkin iletişim kutularının sahibi olan pencereleri koyulaştır +Comment[ug]=نۆۋەتتىكى ئاكتىپ كۆزنەكنىڭ ئاتا سۆزلەشكۈ كۆزنىكى خىرەلىشىدۇ +Comment[uk]=Затемнення батьківських вікон активних діалогових вікон +Comment[vi]=Làm tối cá»­a sổ cha cá»§a hộp thoại hiện đang hoạt động +Comment[wa]=Fwait pus noer li finiesse parint do purnea di dvize ovrant pol moumint +Comment[x-test]=xxDarkens the parent window of the currently active dialogxx +Comment[zh_CN]=将当前激活对话框的父窗口变暗 +Comment[zh_TW]=將目前對話框的父視窗變暗 + +Type=Service +X-Plasma-API=javascript +X-Plasma-MainScript=code/main.js +X-KDE-ServiceTypes=KWin/Effect +X-KDE-PluginInfo-Author=Rivo Laks, Martin Gräßlin +X-KDE-PluginInfo-Email=rivolaks@hot.ee, mgraesslin@kde.org +X-KDE-PluginInfo-Name=kwin4_effect_dialogparent +X-KDE-PluginInfo-Version=0.1.0 +X-KDE-PluginInfo-Category=Focus +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-EnabledByDefault=true +X-KDE-Library=kwin4_effect_dialogparent +X-KDE-Ordering=70 diff --git a/effects/diminactive/CMakeLists.txt b/effects/diminactive/CMakeLists.txt new file mode 100644 index 0000000..5d6e7d3 --- /dev/null +++ b/effects/diminactive/CMakeLists.txt @@ -0,0 +1,23 @@ +####################################### +# Config +set(kwin_diminactive_config_SRCS diminactive_config.cpp) +ki18n_wrap_ui(kwin_diminactive_config_SRCS diminactive_config.ui) +kconfig_add_kcfg_files(kwin_diminactive_config_SRCS diminactiveconfig.kcfgc) + +add_library(kwin_diminactive_config MODULE ${kwin_diminactive_config_SRCS}) + +target_link_libraries(kwin_diminactive_config + KF5::ConfigWidgets + KF5::I18n + Qt5::DBus + KWinEffectsInterface +) + +kcoreaddons_desktop_to_json(kwin_diminactive_config diminactive_config.desktop SERVICE_TYPES kcmodule.desktop) + +install( + TARGETS + kwin_diminactive_config + DESTINATION + ${PLUGIN_INSTALL_DIR}/kwin/effects/configs +) diff --git a/effects/diminactive/diminactive.cpp b/effects/diminactive/diminactive.cpp new file mode 100644 index 0000000..837421f --- /dev/null +++ b/effects/diminactive/diminactive.cpp @@ -0,0 +1,409 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Lubos Lunak + SPDX-FileCopyrightText: 2007 Christian Nitschkowski + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +// own +#include "diminactive.h" + +// KConfigSkeleton +#include "diminactiveconfig.h" + +namespace KWin +{ + +/** + * Checks if two windows belong to the same window group + * + * One possible example of a window group is an app window and app + * preferences window(e.g. Dolphin window and Dolphin Preferences window). + * + * @param w1 The first window + * @param w2 The second window + * @returns @c true if both windows belong to the same window group, @c false otherwise + */ +static inline bool belongToSameGroup(const EffectWindow *w1, const EffectWindow *w2) +{ + return w1 && w2 && w1->group() && w1->group() == w2->group(); +} + +DimInactiveEffect::DimInactiveEffect() +{ + initConfig(); + reconfigure(ReconfigureAll); + + connect(effects, &EffectsHandler::windowActivated, + this, &DimInactiveEffect::windowActivated); + connect(effects, &EffectsHandler::windowClosed, + this, &DimInactiveEffect::windowClosed); + connect(effects, &EffectsHandler::windowDeleted, + this, &DimInactiveEffect::windowDeleted); + connect(effects, &EffectsHandler::activeFullScreenEffectChanged, + this, &DimInactiveEffect::activeFullScreenEffectChanged); + connect(effects, &EffectsHandler::windowKeepAboveChanged, + this, &DimInactiveEffect::updateActiveWindow); + connect(effects, &EffectsHandler::windowFullScreenChanged, + this, &DimInactiveEffect::updateActiveWindow); +} + +DimInactiveEffect::~DimInactiveEffect() +{ +} + +void DimInactiveEffect::reconfigure(ReconfigureFlags flags) +{ + Q_UNUSED(flags) + + DimInactiveConfig::self()->read(); + + // TODO: Use normalized strength param. + m_dimStrength = DimInactiveConfig::strength() / 100.0; + m_dimPanels = DimInactiveConfig::dimPanels(); + m_dimDesktop = DimInactiveConfig::dimDesktop(); + m_dimKeepAbove = DimInactiveConfig::dimKeepAbove(); + m_dimByGroup = DimInactiveConfig::dimByGroup(); + m_dimFullScreen = DimInactiveConfig::dimFullScreen(); + + updateActiveWindow(effects->activeWindow()); + + m_activeWindowGroup = (m_dimByGroup && m_activeWindow) + ? m_activeWindow->group() + : nullptr; + + m_fullScreenTransition.timeLine.setDuration( + std::chrono::milliseconds(static_cast(animationTime(250)))); + + effects->addRepaintFull(); +} + +void DimInactiveEffect::prePaintScreen(ScreenPrePaintData &data, int time) +{ + const std::chrono::milliseconds delta(time); + + if (m_fullScreenTransition.active) { + m_fullScreenTransition.timeLine.update(delta); + } + + auto transitionIt = m_transitions.begin(); + while (transitionIt != m_transitions.end()) { + (*transitionIt).update(delta); + ++transitionIt; + } + + effects->prePaintScreen(data, time); +} + +void DimInactiveEffect::paintWindow(EffectWindow *w, int mask, QRegion region, WindowPaintData &data) +{ + auto transitionIt = m_transitions.constFind(w); + if (transitionIt != m_transitions.constEnd()) { + const qreal transitionProgress = (*transitionIt).value(); + dimWindow(data, m_dimStrength * transitionProgress); + effects->paintWindow(w, mask, region, data); + return; + } + + auto forceIt = m_forceDim.constFind(w); + if (forceIt != m_forceDim.constEnd()) { + const qreal forcedStrength = *forceIt; + dimWindow(data, forcedStrength); + effects->paintWindow(w, mask, region, data); + return; + } + + if (canDimWindow(w)) { + dimWindow(data, m_dimStrength); + } + + effects->paintWindow(w, mask, region, data); +} + +void DimInactiveEffect::postPaintScreen() +{ + if (m_fullScreenTransition.active) { + if (m_fullScreenTransition.timeLine.done()) { + m_fullScreenTransition.active = false; + } + effects->addRepaintFull(); + } + + auto transitionIt = m_transitions.begin(); + while (transitionIt != m_transitions.end()) { + EffectWindow *w = transitionIt.key(); + if ((*transitionIt).done()) { + transitionIt = m_transitions.erase(transitionIt); + } else { + ++transitionIt; + } + w->addRepaintFull(); + } + + effects->postPaintScreen(); +} + +void DimInactiveEffect::dimWindow(WindowPaintData &data, qreal strength) +{ + qreal dimFactor; + if (m_fullScreenTransition.active) { + dimFactor = 1.0 - m_fullScreenTransition.timeLine.value(); + } else if (effects->activeFullScreenEffect()) { + dimFactor = 0.0; + } else { + dimFactor = 1.0; + } + + data.multiplyBrightness(1.0 - strength * dimFactor); + data.multiplySaturation(1.0 - strength * dimFactor); +} + +bool DimInactiveEffect::canDimWindow(const EffectWindow *w) const +{ + if (m_activeWindow == w) { + return false; + } + + if (m_dimByGroup && belongToSameGroup(m_activeWindow, w)) { + return false; + } + + if (w->isDock() && !m_dimPanels) { + return false; + } + + if (w->isDesktop() && !m_dimDesktop) { + return false; + } + + if (w->keepAbove() && !m_dimKeepAbove) { + return false; + } + + if (w->isFullScreen() && !m_dimFullScreen) { + return false; + } + + if (w->isPopupWindow()) { + return false; + } + + if (w->isX11Client() && !w->isManaged()) { + return false; + } + + return w->isNormalWindow() + || w->isDialog() + || w->isUtility() + || w->isDock() + || w->isDesktop(); +} + +void DimInactiveEffect::scheduleInTransition(EffectWindow *w) +{ + TimeLine &timeLine = m_transitions[w]; + timeLine.setDuration( + std::chrono::milliseconds(static_cast(animationTime(160)))); + if (timeLine.done()) { + // If the Out animation is still active, then we're trucating + // duration of the timeline(from 250ms to 160ms). If the timeline + // is about to be finished with the old duration, then after + // changing duration it will be in the "done" state. Thus, we + // have to reset the timeline, otherwise it won't update progress. + timeLine.reset(); + } + timeLine.setDirection(TimeLine::Backward); + timeLine.setEasingCurve(QEasingCurve::InOutSine); +} + +void DimInactiveEffect::scheduleGroupInTransition(EffectWindow *w) +{ + if (!m_dimByGroup) { + scheduleInTransition(w); + return; + } + + if (!w->group()) { + scheduleInTransition(w); + return; + } + + const auto members = w->group()->members(); + for (EffectWindow *member : members) { + scheduleInTransition(member); + } +} + +void DimInactiveEffect::scheduleOutTransition(EffectWindow *w) +{ + TimeLine &timeLine = m_transitions[w]; + timeLine.setDuration( + std::chrono::milliseconds(static_cast(animationTime(250)))); + if (timeLine.done()) { + timeLine.reset(); + } + timeLine.setDirection(TimeLine::Forward); + timeLine.setEasingCurve(QEasingCurve::InOutSine); +} + +void DimInactiveEffect::scheduleGroupOutTransition(EffectWindow *w) +{ + if (!m_dimByGroup) { + scheduleOutTransition(w); + return; + } + + if (!w->group()) { + scheduleOutTransition(w); + return; + } + + const auto members = w->group()->members(); + for (EffectWindow *member : members) { + scheduleOutTransition(member); + } +} + +void DimInactiveEffect::scheduleRepaint(EffectWindow *w) +{ + if (!m_dimByGroup) { + w->addRepaintFull(); + return; + } + + if (!w->group()) { + w->addRepaintFull(); + return; + } + + const auto members = w->group()->members(); + for (EffectWindow *member : members) { + member->addRepaintFull(); + } +} + +void DimInactiveEffect::windowActivated(EffectWindow *w) +{ + if (!w) { + return; + } + + if (m_activeWindow == w) { + return; + } + + if (m_dimByGroup && belongToSameGroup(m_activeWindow, w)) { + m_activeWindow = w; + return; + } + + // WORKAROUND: Deleted windows do not belong to any of window groups. + // So, if one of windows in a window group is closed, the In transition + // will be false-triggered for the rest of the window group. In addition + // to the active window, keep track of active window group so we can + // tell whether "focus" moved from a closed window to some other window + // in a window group. + if (m_dimByGroup && w->group() && w->group() == m_activeWindowGroup) { + m_activeWindow = w; + return; + } + + EffectWindow *previousActiveWindow = m_activeWindow; + m_activeWindow = canDimWindow(w) ? w : nullptr; + + m_activeWindowGroup = (m_dimByGroup && m_activeWindow) + ? m_activeWindow->group() + : nullptr; + + if (previousActiveWindow) { + scheduleGroupOutTransition(previousActiveWindow); + scheduleRepaint(previousActiveWindow); + } + + if (m_activeWindow) { + scheduleGroupInTransition(m_activeWindow); + scheduleRepaint(m_activeWindow); + } +} + +void DimInactiveEffect::windowClosed(EffectWindow *w) +{ + // When a window is closed, we should force current dim strength that + // is applied to it to avoid flickering when some effect animates + // the disappearing of the window. If there is no such effect then + // it won't be dimmed. + qreal forcedStrength = 0.0; + bool shouldForceDim = false; + + auto transitionIt = m_transitions.find(w); + if (transitionIt != m_transitions.end()) { + forcedStrength = m_dimStrength * (*transitionIt).value(); + shouldForceDim = true; + m_transitions.erase(transitionIt); + } else if (m_activeWindow == w) { + forcedStrength = 0.0; + shouldForceDim = true; + } else if (m_dimByGroup && belongToSameGroup(m_activeWindow, w)) { + forcedStrength = 0.0; + shouldForceDim = true; + } else if (canDimWindow(w)) { + forcedStrength = m_dimStrength; + shouldForceDim = true; + } + + if (shouldForceDim) { + m_forceDim.insert(w, forcedStrength); + } + + if (m_activeWindow == w) { + m_activeWindow = nullptr; + } +} + +void DimInactiveEffect::windowDeleted(EffectWindow *w) +{ + m_forceDim.remove(w); + + // FIXME: Sometimes we can miss the window close signal because KWin + // can activate a window that is not ready for painting and the window + // gets destroyed immediately. So, we have to remove active transitions + // for that window here, otherwise we'll crash in postPaintScreen. + m_transitions.remove(w); +} + +void DimInactiveEffect::activeFullScreenEffectChanged() +{ + if (m_fullScreenTransition.timeLine.done()) { + m_fullScreenTransition.timeLine.reset(); + } + m_fullScreenTransition.timeLine.setDirection( + effects->activeFullScreenEffect() + ? TimeLine::Forward + : TimeLine::Backward + ); + m_fullScreenTransition.active = true; + + effects->addRepaintFull(); +} + +void DimInactiveEffect::updateActiveWindow(EffectWindow *w) +{ + if (effects->activeWindow() == nullptr) { + return; + } + + if (effects->activeWindow() != w) { + return; + } + + // Need to reset m_activeWindow because canDimWindow depends on it. + m_activeWindow = nullptr; + + m_activeWindow = canDimWindow(w) ? w : nullptr; +} + +} // namespace KWin diff --git a/effects/diminactive/diminactive.h b/effects/diminactive/diminactive.h new file mode 100644 index 0000000..9c01f06 --- /dev/null +++ b/effects/diminactive/diminactive.h @@ -0,0 +1,129 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Lubos Lunak + SPDX-FileCopyrightText: 2007 Christian Nitschkowski + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_DIMINACTIVE_H +#define KWIN_DIMINACTIVE_H + +// kwineffects +#include + +namespace KWin +{ + +class DimInactiveEffect : public Effect +{ + Q_OBJECT + Q_PROPERTY(int dimStrength READ dimStrength) + Q_PROPERTY(bool dimPanels READ dimPanels) + Q_PROPERTY(bool dimDesktop READ dimDesktop) + Q_PROPERTY(bool dimKeepAbove READ dimKeepAbove) + Q_PROPERTY(bool dimByGroup READ dimByGroup) + Q_PROPERTY(bool dimFullScreen READ dimFullScreen) + +public: + DimInactiveEffect(); + ~DimInactiveEffect() override; + + void reconfigure(ReconfigureFlags flags) override; + + void prePaintScreen(ScreenPrePaintData &data, int time) override; + void paintWindow(EffectWindow *w, int mask, QRegion region, WindowPaintData &data) override; + void postPaintScreen() override; + + int requestedEffectChainPosition() const override; + bool isActive() const override; + + int dimStrength() const; + bool dimPanels() const; + bool dimDesktop() const; + bool dimKeepAbove() const; + bool dimByGroup() const; + bool dimFullScreen() const; + +private Q_SLOTS: + void windowActivated(EffectWindow *w); + void windowClosed(EffectWindow *w); + void windowDeleted(EffectWindow *w); + void activeFullScreenEffectChanged(); + + void updateActiveWindow(EffectWindow *w); + +private: + void dimWindow(WindowPaintData &data, qreal strength); + bool canDimWindow(const EffectWindow *w) const; + void scheduleInTransition(EffectWindow *w); + void scheduleGroupInTransition(EffectWindow *w); + void scheduleOutTransition(EffectWindow *w); + void scheduleGroupOutTransition(EffectWindow *w); + void scheduleRepaint(EffectWindow *w); + +private: + qreal m_dimStrength; + bool m_dimPanels; + bool m_dimDesktop; + bool m_dimKeepAbove; + bool m_dimByGroup; + bool m_dimFullScreen; + + EffectWindow *m_activeWindow = nullptr; + const EffectWindowGroup *m_activeWindowGroup; + QHash m_transitions; + QHash m_forceDim; + + struct { + bool active = false; + TimeLine timeLine; + } m_fullScreenTransition; +}; + +inline int DimInactiveEffect::requestedEffectChainPosition() const +{ + return 50; +} + +inline bool DimInactiveEffect::isActive() const +{ + return true; +} + +inline int DimInactiveEffect::dimStrength() const +{ + return qRound(m_dimStrength * 100.0); +} + +inline bool DimInactiveEffect::dimPanels() const +{ + return m_dimPanels; +} + +inline bool DimInactiveEffect::dimDesktop() const +{ + return m_dimDesktop; +} + +inline bool DimInactiveEffect::dimKeepAbove() const +{ + return m_dimKeepAbove; +} + +inline bool DimInactiveEffect::dimByGroup() const +{ + return m_dimByGroup; +} + +inline bool DimInactiveEffect::dimFullScreen() const +{ + return m_dimFullScreen; +} + +} // namespace KWin + +#endif diff --git a/effects/diminactive/diminactive.kcfg b/effects/diminactive/diminactive.kcfg new file mode 100644 index 0000000..ebad90c --- /dev/null +++ b/effects/diminactive/diminactive.kcfg @@ -0,0 +1,27 @@ + + + + + + 25 + + + false + + + false + + + false + + + true + + + true + + + diff --git a/effects/diminactive/diminactive_config.cpp b/effects/diminactive/diminactive_config.cpp new file mode 100644 index 0000000..6941dc5 --- /dev/null +++ b/effects/diminactive/diminactive_config.cpp @@ -0,0 +1,54 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Christian Nitschkowski + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "diminactive_config.h" + +// KConfigSkeleton +#include "diminactiveconfig.h" +#include + +#include + +#include +#include + +K_PLUGIN_FACTORY_WITH_JSON(DimInactiveEffectConfigFactory, + "diminactive_config.json", + registerPlugin();) + +namespace KWin +{ + +DimInactiveEffectConfig::DimInactiveEffectConfig(QWidget *parent, const QVariantList &args) + : KCModule(KAboutData::pluginData(QStringLiteral("diminactive")), parent, args) +{ + m_ui.setupUi(this); + DimInactiveConfig::instance(KWIN_CONFIG); + addConfig(DimInactiveConfig::self(), this); + load(); +} + +DimInactiveEffectConfig::~DimInactiveEffectConfig() +{ +} + +void DimInactiveEffectConfig::save() +{ + KCModule::save(); + + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("diminactive")); +} + +} // namespace KWin + +#include "diminactive_config.moc" diff --git a/effects/diminactive/diminactive_config.desktop b/effects/diminactive/diminactive_config.desktop new file mode 100644 index 0000000..4680092 --- /dev/null +++ b/effects/diminactive/diminactive_config.desktop @@ -0,0 +1,81 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=KCModule + +X-KDE-Library=kwin_diminactive_config +X-KDE-ParentComponents=diminactive + +Name=Dim Inactive +Name[af]=Verdof wanneer onaktief +Name[ar]=تعتيم الخامل +Name[az]=Qeyri-aktiv pəncərələrin tutqunlaşması +Name[bg]=Затъмняване на неактивния +Name[bs]=PriguÅ¡eni neaktivni +Name[ca]=Enfosqueix les inactives +Name[ca@valencia]=Enfosqueix les inactives +Name[cs]=Ztmavit neaktivní +Name[da]=Gør inaktiv mat +Name[de]=Inaktive abdunkeln +Name[el]=Σκίαση ανενεργού +Name[en_GB]=Dim Inactive +Name[eo]=Malheligi neaktivajn +Name[es]=Oscurecer inactiva +Name[et]=Tuhm mitteaktiivne +Name[eu]=Ilundu inaktiboak +Name[fi]=Passiivisen himmennys +Name[fr]=Estompe les inactifs +Name[fy]=Net aktive dimme +Name[ga]=Dorchaigh Fuinneoga Neamhghníomhacha +Name[gl]=Escurecer as inactivas +Name[gu]=અસક્રિય ઝાંખી +Name[he]=עמעום חלונות לא פעילים +Name[hi]=असक्रिय मंद करें +Name[hne]=असक्रिय मंद करव +Name[hr]=PriguÅ¡i neaktivno +Name[hu]=Inaktív ablakok kiszürkítése +Name[ia]=Dim Inactive +Name[id]=Pergelap yang Tak Aktif +Name[is]=Dimma óvirka +Name[it]=Scurisci le inattive +Name[ja]=非アクティブを暗く +Name[kk]=Белсенді еместі күңгірттеу +Name[km]=បន្ថយ​ពន្លឺ​នៅពេល​អសកម្ម +Name[kn]=ನಿಷ್ಕ್ರಿಯವಾದದ್ದನ್ನು ಮಂಕಾಗಿಸು +Name[ko]=비활성 ì°½ 어둡게 +Name[lt]=Pasyviųjų pritemdymas +Name[lv]=TumÅ¡ināt neaktÄ«vos +Name[mai]=असक्रिय मंद करू +Name[mk]=Затемни неактивни +Name[ml]=നിര്‍ജ്ജീവമായവ മറയ്ക്കുക +Name[mr]=निष्क्रिय गडद करा +Name[nb]=Mørklegg inaktive +Name[nds]=Nich aktive bedüüstern +Name[ne]=मलिन असक्रियता +Name[nl]=Inactief dimmen +Name[nn]=Mørklegg inaktive vindauge +Name[pa]=Dim ਨਾ-ਸਰਗਰਮ +Name[pl]=Przyciemnienie nieaktywnych +Name[pt]=Escurecer as Inactivas +Name[pt_BR]=Escurecer inativas +Name[ro]=Împăienjenire inactive +Name[ru]=Затемнение неактивных окон +Name[se]=Sevnjodahte ii-aktiivalaÅ¡ lasiid +Name[si]=අක්‍රීය අඳුරු කරන්න +Name[sk]=StmaviÅ¥ neaktívne +Name[sl]=Potemni nedejavno +Name[sr]=Пригушени неактивни +Name[sr@ijekavian]=Пригушени неактивни +Name[sr@ijekavianlatin]=PriguÅ¡eni neaktivni +Name[sr@latin]=PriguÅ¡eni neaktivni +Name[sv]=Dämpa inaktiva +Name[ta]=Dim Inactive +Name[te]=Dim క్రియాహీనం +Name[th]=ลดความสว่างตัวที่ไม่ได้ใช้ +Name[tr]=Pasifleri Koyulaştır +Name[ug]=تۇتۇق ئاكتىپسىز +Name[uk]=Затемнення неактивних +Name[vi]=Làm tối cá»­a sổ không hoạt động +Name[wa]=Fé pus noer l' essocté +Name[x-test]=xxDim Inactivexx +Name[zh_CN]=暗淡未激活窗口 +Name[zh_TW]=Dim Inactive diff --git a/effects/diminactive/diminactive_config.h b/effects/diminactive/diminactive_config.h new file mode 100644 index 0000000..84d44e7 --- /dev/null +++ b/effects/diminactive/diminactive_config.h @@ -0,0 +1,37 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Christian Nitschkowski + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_DIMINACTIVE_CONFIG_H +#define KWIN_DIMINACTIVE_CONFIG_H + +#include + +#include "ui_diminactive_config.h" + +namespace KWin +{ + +class DimInactiveEffectConfig : public KCModule +{ + Q_OBJECT + +public: + explicit DimInactiveEffectConfig(QWidget *parent = nullptr, const QVariantList &args = QVariantList()); + ~DimInactiveEffectConfig() override; + + void save() override; + +private: + ::Ui::DimInactiveEffectConfig m_ui; +}; + +} // namespace KWin + +#endif diff --git a/effects/diminactive/diminactive_config.ui b/effects/diminactive/diminactive_config.ui new file mode 100644 index 0000000..f81ed44 --- /dev/null +++ b/effects/diminactive/diminactive_config.ui @@ -0,0 +1,83 @@ + + + DimInactiveEffectConfig + + + + 0 + 0 + 400 + 160 + + + + + + + Strength: + + + + + + + + 0 + 0 + + + + 100 + + + 5 + + + + + + + Dim: + + + + + + + Docks and panels + + + + + + + Desktop + + + + + + + Keep above windows + + + + + + + By window group + + + + + + + Fullscreen windows + + + + + + + + diff --git a/effects/diminactive/diminactiveconfig.kcfgc b/effects/diminactive/diminactiveconfig.kcfgc new file mode 100644 index 0000000..0e57d63 --- /dev/null +++ b/effects/diminactive/diminactiveconfig.kcfgc @@ -0,0 +1,5 @@ +File=diminactive.kcfg +ClassName=DimInactiveConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/effects/dimscreen/package/contents/code/main.js b/effects/dimscreen/package/contents/code/main.js new file mode 100644 index 0000000..f7fa9ef --- /dev/null +++ b/effects/dimscreen/package/contents/code/main.js @@ -0,0 +1,227 @@ +/* + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +"use strict"; + +var authenticationAgents = [ + "kdesu kdesu", + "kdesudo kdesudo", + "pinentry pinentry", + "polkit-kde-authentication-agent-1 polkit-kde-authentication-agent-1", + "polkit-kde-manager polkit-kde-manager", + + // On Wayland, the resource name is filename of executable. It's empty for + // authentication agents because KWayland can't get their executable paths. + " org.kde.kdesu", + " org.kde.polkit-kde-authentication-agent-1" +]; + +function activeAuthenticationAgent() { + var activeWindow = effects.activeWindow; + if (!activeWindow) { + return null; + } + + if (authenticationAgents.indexOf(activeWindow.windowClass) == -1) { + return null; + } + + return activeWindow; +} + +var dimScreenEffect = { + loadConfig: function () { + dimScreenEffect.duration = animationTime(250); + dimScreenEffect.brightness = 0.67; + dimScreenEffect.saturation = 0.67; + }, + startAnimation: function (window) { + if (!window.visible) { + return; + } + if (window.popupWindow) { + return; + } + if (window.x11Client && !window.managed) { + return; + } + if (window.dimAnimation) { + if (redirect(window.dimAnimation, Effect.Forward)) { + return; + } + cancel(window.dimAnimation); + } + window.dimAnimation = set({ + window: window, + curve: QEasingCurve.InOutSine, + duration: dimScreenEffect.duration, + keepAlive: false, + animations: [ + { + type: Effect.Saturation, + to: dimScreenEffect.saturation + }, + { + type: Effect.Brightness, + to: dimScreenEffect.brightness + } + ] + }); + }, + startAnimationSmooth: function (window) { + dimScreenEffect.startAnimation(window); + }, + startAnimationInstant: function (window) { + dimScreenEffect.startAnimation(window); + + if (window.dimAnimation) { + complete(window.dimAnimation); + } + }, + cancelAnimationSmooth: function (window) { + if (!window.dimAnimation) { + return; + } + if (redirect(window.dimAnimation, Effect.Backward)) { + return; + } + cancel(window.dimAnimation); + delete window.dimAnimation; + }, + cancelAnimationInstant: function (window) { + if (window.dimAnimation) { + cancel(window.dimAnimation); + delete window.dimAnimation; + } + }, + dimScreen: function (agent, animationFunc, cancelFunc) { + // Keep track of currently active authentication agent so we don't have + // to re-scan the stacking order in brightenScreen each time when some + // window is activated. + dimScreenEffect.authenticationAgent = agent; + + var windows = effects.stackingOrder; + for (var i = 0; i < windows.length; ++i) { + var window = windows[i]; + if (window == agent) { + // The agent might have been dimmed before (because there are + // several authentication agents on the screen), so we need to + // cancel previous animations for it if there are any. + cancelFunc(agent); + continue; + } + animationFunc(window); + } + }, + dimScreenSmooth: function (agent) { + dimScreenEffect.dimScreen( + agent, + dimScreenEffect.startAnimationSmooth, + dimScreenEffect.cancelAnimationSmooth + ); + }, + dimScreenInstant: function (agent) { + dimScreenEffect.dimScreen( + agent, + dimScreenEffect.startAnimationInstant, + dimScreenEffect.cancelAnimationInstant + ); + }, + brightenScreen: function (cancelFunc) { + if (!dimScreenEffect.authenticationAgent) { + return; + } + dimScreenEffect.authenticationAgent = null; + + var windows = effects.stackingOrder; + for (var i = 0; i < windows.length; ++i) { + cancelFunc(windows[i]); + } + }, + brightenScreenSmooth: function () { + dimScreenEffect.brightenScreen(dimScreenEffect.cancelAnimationSmooth); + }, + brightenScreenInstant: function () { + dimScreenEffect.brightenScreen(dimScreenEffect.cancelAnimationInstant); + }, + slotWindowActivated: function (window) { + if (!window) { + return; + } + if (authenticationAgents.indexOf(window.windowClass) != -1) { + dimScreenEffect.dimScreenSmooth(window); + } else { + dimScreenEffect.brightenScreenSmooth(); + } + }, + slotWindowAdded: function (window) { + // Don't dim authentication agents that just opened. + var agent = activeAuthenticationAgent(); + if (agent == window) { + return; + } + + // If a window appeared while the screen is dimmed, dim the window too. + if (agent) { + dimScreenEffect.startAnimationInstant(window); + } + }, + slotActiveFullScreenEffectChanged: function () { + // If some full screen effect has been activated, for example the desktop + // cube effect, then brighten screen back. We need to do that because the + // full screen effect can dim windows on its own. + if (effects.hasActiveFullScreenEffect) { + dimScreenEffect.brightenScreenSmooth(); + return; + } + + // If user left the full screen effect, try to dim screen back. + var agent = activeAuthenticationAgent(); + if (agent) { + dimScreenEffect.dimScreenSmooth(agent); + } + }, + slotDesktopChanged: function () { + // If there is an active full screen effect, then try smoothly dim/brighten + // the screen. Keep in mind that in order for this to work properly, this + // effect has to come after the full screen effect in the effect chain, + // otherwise this slot will be invoked before the full screen effect can mark + // itself as a full screen effect. + if (effects.hasActiveFullScreenEffect) { + return; + } + + // Try to brighten windows on the previous virtual desktop. + dimScreenEffect.brightenScreenInstant(); + + // Try to dim windows on the current virtual desktop. + var agent = activeAuthenticationAgent(); + if (agent) { + dimScreenEffect.dimScreenInstant(agent); + } + }, + restartAnimation: function (window) { + if (activeAuthenticationAgent()) { + dimScreenEffect.startAnimationInstant(window); + } + }, + init: function () { + dimScreenEffect.loadConfig(); + + effect.configChanged.connect(dimScreenEffect.loadConfig); + effects.windowActivated.connect(dimScreenEffect.slotWindowActivated); + effects.windowAdded.connect(dimScreenEffect.slotWindowAdded); + effects.windowMinimized.connect(dimScreenEffect.cancelAnimationInstant); + effects.windowUnminimized.connect(dimScreenEffect.restartAnimation); + effects.activeFullScreenEffectChanged.connect( + dimScreenEffect.slotActiveFullScreenEffectChanged); + effects['desktopChanged(int,int)'].connect(dimScreenEffect.slotDesktopChanged); + } +}; + +dimScreenEffect.init(); diff --git a/effects/dimscreen/package/metadata.desktop b/effects/dimscreen/package/metadata.desktop new file mode 100644 index 0000000..9664d67 --- /dev/null +++ b/effects/dimscreen/package/metadata.desktop @@ -0,0 +1,145 @@ +[Desktop Entry] +Comment=Darkens the entire screen when requesting root privileges +Comment[ar]=يعتم كامل الشاشة عند طلب صلاحيات الجذر +Comment[az]=Kök imtiyazı tələb edən bütün pəncərələr tutqunlaşır +Comment[bg]=Екранът затъмнява при необходимост от администраторски права +Comment[ca]=Enfosqueix tota la pantalla en demanar privilegis d'administrador +Comment[ca@valencia]=Enfosqueix tota la pantalla en demanar privilegis d'administrador +Comment[cs]=Ztmaví obrazovku, pokud jsou vyžadována oprávnění administrátora systému +Comment[da]=Gør hele skærmen mørkere nÃ¥r der bedes om root-privilegier +Comment[de]=Dunkelt den gesamten Bildschirm ab, wenn nach dem Systemverwalter-Passwort gefragt wird. +Comment[el]=Σκίαση ολόκληρης της οθόνης όταν απαιτούνται διακαιώματα ριζικού χρήστη root +Comment[en_GB]=Darkens the entire screen when requesting root privileges +Comment[eo]=Malheligi la tutan ekranon kiam ĉefuzantaj permesoj estas petitaj. +Comment[es]=Oscurece la pantalla completa cuando se requieran privilegios de root +Comment[et]=Tumendab administraatori õiguste nõudmisel kogu ekraani +Comment[eu]=Pantaila osoa iluntzen du root-aren pribilegioak eskatzean +Comment[fi]=Tummentaa koko näytön pääkäyttäjäoikeuksia kysyttäessä +Comment[fr]=Noircit la totalité du bureau quand les privilèges administrateurs sont nécessaires +Comment[fy]=It gehiele skerm donker meitsje as der om root-rjochten frege wurdt +Comment[ga]=Dorchaigh an scáileán iomlán nuair atáthar ag iarraidh ceadanna an fhorúsáideora a fháil +Comment[gl]=Escurece toda a pantalla cando se piden os privilexios de root +Comment[gu]=જ્યારે રૂટ હક્ક માંગવામાં આવે ત્યારે આખા સ્ક્રિનને ઘેરો બનાવે છે +Comment[hi]=जब रूट विशेषाधिकार हेतु निवेदन किया जाए तो संपूर्ण स्क्रीन को गाढ़ा करता है +Comment[hne]=जब रूट प्रिविलेज बर निवेदन करही तब पूरा स्क्रीन ल धुंधला कर देही +Comment[hr]=Zatamni cijeli ekran kad se zahtijevaju administratorske povlastice +Comment[hu]=Elsötétíti a képernyőt rendszergazdai jogosultság kérésekor +Comment[ia]=Il obscura le schermo integre quando il demanda privileges de super-usator (root) +Comment[id]=Pergelap seluruh layar ketika meminta hak akses root +Comment[is]=Skyggir allan skjáinn þegar beðið er um lykilorð kerfisstjóra +Comment[it]=Scurisce tutto lo schermo quando si richiedono i privilegi di root +Comment[ja]=root 権限が要求されるとスクリーン全体を暗くします +Comment[kk]=root құқығын сұрағанда бүкіл экранды күңгірттеу +Comment[km]=ធ្វើ​ឲ្យ​អេក្រង់ទាំងមូល​ងងឹត​នៅពេល​បង្ហាញ​សិទ្ធិជា​ root +Comment[kn]=ನಿರ್ವಾಹಕ ಸವಲತ್ತುಗಳನ್ನು ಕೇಳುವಾಗ ಇಡೀ ತೆರೆಯನ್ನು ಮಂಕಾಗಿಸುತ್ತದೆ +Comment[ko]=루트 권한이 필요할 때 화면을 어둡게 합니다 +Comment[lt]=Užtemdo visą ekraną, kai reikalaujama pagrindinio naudotojo (root) teisių +Comment[lv]=AptumÅ¡ina visu ekrānu, kad pieprasa root privilēģijas +Comment[mk]=Го затемнува целиот екран при побарување администраторски привилегии +Comment[ml]=മൂല (റൂട്ട്) വിശേഷാധികാരം അഭ്യര്‍ത്തിക്കുംബോള്‍ സ്ക്രീന്‍ കറുപ്പിക്കുന്നു +Comment[mr]=प्रशासकीय अधिकारांची विनंती करताना पूर्ण स्क्रीन गडद करा +Comment[nds]=Maakt den helen Schirm düüsterer, wenn na Systeempleger-Verlöven fraagt warrt +Comment[nl]=Maakt het volledige scherm donker wanneer er om root-privileges wordt verzocht +Comment[nn]=Gjer heile skjermen mørkare nÃ¥r det vert spurt om rottilgang +Comment[pa]=ਜਦੋਂ ਰੂਟ ਅਧਿਕਾਰਾਂ ਦੀ ਲੋੜ ਹੋਵੇ ਤਾਂ ਪੂਰੀ ਸਕਰੀਨ ਲਈ ਗੂੜ੍ਹਾ ਕਰੋ +Comment[pl]=Przyciemnia ekran przy żądaniu praw administratora +Comment[pt]=Escurece o ecrã inteiro ao pedir privilégios de administrador +Comment[pt_BR]=Escurece a tela inteira ao solicitar privilégios de superusuário (root) +Comment[ro]=Întunecă întregul ecran la cererea privilegiilor de root +Comment[ru]=Затемнение всего экрана при запросе привилегий суперпользователя +Comment[si]=රූට් බලතල ඉල්ලා සිටින විට සම්පූර්ණ තිරය අඳුරු කරන්න +Comment[sk]=Stmaví celú obrazovku, keď sú vyžadované rootovské oprávnenia +Comment[sl]=Potemni celoten zaslon, ko se zahteva skrbniÅ¡ka dovoljenja +Comment[sv]=Gör hela skärmen mörkare när administratörsrättigheter begärs +Comment[ta]=Darkens the entire screen when requesting root privileges +Comment[te]=రూట్ అనుమతులను అభ్యర్ధిస్తున్నప్పుడు మొత్తం తెరను చీకటిచేస్తుంది. +Comment[th]=ทำทั้งหน้าจอให้ดูมืดขึ้น เมื่อมีการร้องขอใช้สิทธิ์ของผู้บริหารระบบ (root) +Comment[tr]=Yönetici hakları isteğinde bulunulduğunda ekranı koyulaştırır +Comment[ug]=root ھوقۇقىنى ئىلتىماس قىلغاندا پۈتكۈل ئېكراننى خىرەلەشتۈرىدۇ +Comment[uk]=Притлумлює кольори усього екрана, коли працюємо з доступом root +Comment[vi]=Làm tối toàn màn hình khi yêu cầu quyền quản trị +Comment[wa]=Fwait pus noer li waitroûle etire cwand on dmande les droets da root +Comment[x-test]=xxDarkens the entire screen when requesting root privilegesxx +Comment[zh_CN]=在请求 root 权限时暗化整个屏幕 +Comment[zh_TW]=當要求 root 權限時將整個螢幕變暗 +Icon=preferences-system-windows-effect-dimscreen +Name=Dim Screen for Administrator Mode +Name[af]=Verdof vir Administrateurmodus +Name[ar]=يعتم الشاشة لنمط المدير +Name[az]=İdarəçi rejimi zamanı ekran tutqunlaşır +Name[bg]=Затъмняване на екрана в администраторски режим +Name[ca]=Enfosqueix la pantalla per al mode administrador +Name[ca@valencia]=Enfosqueix la pantalla per al mode d'administrador +Name[cs]=Ztmavit obrazovku v administrátorském režimu +Name[da]=Dæmp skærmen til administratortilstand +Name[de]=Bildschirm für Systemverwaltungsmodus abdunkeln +Name[el]=Σκίαση οθόνης σε λειτουργία διαχειριστή +Name[en_GB]=Dim Screen for Administrator Mode +Name[eo]=Malheligi la ekranon en administra reĝimo +Name[es]=Oscurecer la pantalla en el modo administrador +Name[et]=Tuhm ekraan administraatori režiimis +Name[eu]=Ilundu leihoa administratzaile modurako +Name[fi]=Himmennä näyttö pääkäyttäjätilassa +Name[fr]=Assombrir l'écran pour le mode administrateur +Name[fy]=Yn administratormodus it skerm dimme +Name[ga]=Doiléirigh an Scáileán i Mód an Riarthóra +Name[gl]=Escurecer a pantalla no modo de administración +Name[gu]=સંચાલક સ્થિતિ માટે ઝાંખો સ્ક્રિન +Name[hi]=प्रबंधक मोड के लिए स्क्रीन मंद करें +Name[hne]=प्रसासक मोड बर स्क्रीन ल धुंधला कर देव +Name[hr]=PriguÅ¡i zaslon za administrativni način +Name[hu]=Halványított képernyő rendszergazdai módban +Name[ia]=Schermo Dim pro modo de administrator +Name[id]=Pergelap Layar untuk Mode Administrator +Name[is]=Dimma skjá fyrir kerfisstjóraham +Name[it]=Scurisci lo schermo in modalità amministrativa +Name[ja]=管理者モードでスクリーンを暗く +Name[kk]=Әкімші режімде экранды күңгірттеп көрсету +Name[km]=បន្ថយ​ពន្លឺ​អេក្រង់ សម្រាប់​របៀប​ជា​អ្នក​គ្រប់គ្រង +Name[kn]=ನಿರ್ವಾಹಕ ವಿಧಾನದಲ್ಲಿ (ಅ ಡ್ಮಿನಿಸ್ಟ್ರೇಟರ್ ಮೋಡ್) ತೆರೆಯನ್ನು ಮಂಕಾಗಿಸು +Name[ko]=관리자 모드에서 화면 어둡게 하기 +Name[lt]=Ekrano pritemdymas administratoriaus veiksenoje +Name[lv]=AptumÅ¡ināt ekrānu, pieprasot administratora tiesÄ«bas +Name[mk]=Го затемнува екранот за администраторски режим +Name[ml]=അഡ്മിനിസ്ട്രേറ്റര്‍ മോഡില്‍ സ്ക്രീന്‍ തെളിച്ചം കുറയ്ക്കല്‍ +Name[mr]=प्रशासक पद्धती करिता गडद स्क्रीन +Name[nds]=In Systeemplegerbedrief Schirm bedüüstern +Name[nl]=Scherm dimmen voor administratormodus +Name[nn]=Mørklegg skjermen i administratormodus +Name[pa]=ਪਰਸ਼ਾਸ਼ਕੀ ਮੋਡ ਵਿੱਚ ਸਕਰੀਨ ਡਿਮ ਕਰੋ +Name[pl]=Przyciemnienie ekranu dla trybu administratora +Name[pt]=Escurecer o Ecrã no Modo de Administrador +Name[pt_BR]=Escurecer a tela no modo administrador +Name[ro]=Împăienjenește ecranul pentru Regimul administrator +Name[ru]=Затемнение экрана при административной задаче +Name[si]=පරිපාලක ප්‍රකාරයේදී තිර දීප්තිය අඩු කරන්න +Name[sk]=StmaviÅ¥ obrazovku v administrátorskom režime +Name[sl]=Potemnitev za način skrbnika +Name[sv]=Dämpa skärmen vid administratörsläge +Name[ta]=Dim Screen for Administrator Mode +Name[te]=నిర్వాహక రీతికొరకు తెరను డిమ్ చేస్తుంది +Name[th]=ทำให้หน้าจอมืดลงสำหรับใช้ในโหมดผู้บริหารระบบ +Name[tr]=Yönetici Kipinde Ekranı Dondur +Name[ug]=باشقۇرغۇچى ھالىتىدە ئېكراننى تۇتۇقلاشتۇر +Name[uk]=Притлумлення кольорів у режимі адміністратора +Name[vi]=Làm tối màn hình cho chế độ quản trị +Name[wa]=Fé pus noere li waitroûle pol môde Manaedjeu +Name[x-test]=xxDim Screen for Administrator Modexx +Name[zh_CN]=进入管理员模式时暗淡屏幕 +Name[zh_TW]=為管理員模式暗化螢幕 + +Type=Service +X-KDE-ParentApp= +X-KDE-PluginInfo-Author=Martin Flöser, Vlad Zahorodnii +X-KDE-PluginInfo-Category=Focus +X-KDE-PluginInfo-Email=mgraesslin@kde.org, vlad.zahorodnii@kde.org +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-Name=kwin4_effect_dimscreen +X-KDE-PluginInfo-Version=1 +X-KDE-PluginInfo-Website= +X-KDE-ServiceTypes=KWin/Effect +X-KDE-PluginInfo-EnabledByDefault=false +X-KDE-Ordering=60 +X-Plasma-API=javascript +X-Plasma-MainScript=code/main.js +X-KWin-Video-Url=https://files.kde.org/plasma/kwin/effect-videos/dim_administration.mp4 diff --git a/effects/effect_builtins.cpp b/effects/effect_builtins.cpp new file mode 100644 index 0000000..90a7554 --- /dev/null +++ b/effects/effect_builtins.cpp @@ -0,0 +1,748 @@ +/* + 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 "effect_builtins.h" +#ifdef EFFECT_BUILTINS +// common effects +#include "backgroundcontrast/contrast.h" +#include "blur/blur.h" +#include "colorpicker/colorpicker.h" +#include "kscreen/kscreen.h" +#include "presentwindows/presentwindows.h" +#include "screenedge/screenedgeeffect.h" +#include "screenshot/screenshot.h" +#include "slidingpopups/slidingpopups.h" +// Common effects only relevant to desktop +#include "desktopgrid/desktopgrid.h" +#include "diminactive/diminactive.h" +#include "fallapart/fallapart.h" +#include "highlightwindow/highlightwindow.h" +#include "magiclamp/magiclamp.h" +#include "resize/resize.h" +#include "showfps/showfps.h" +#include "showpaint/showpaint.h" +#include "slide/slide.h" +#include "slideback/slideback.h" +#include "thumbnailaside/thumbnailaside.h" +#include "touchpoints/touchpoints.h" +#include "windowgeometry/windowgeometry.h" +#include "zoom/zoom.h" +// OpenGL-specific effects for desktop +#include "coverswitch/coverswitch.h" +#include "cube/cube.h" +#include "cubeslide/cubeslide.h" +#include "flipswitch/flipswitch.h" +#include "glide/glide.h" +#include "invert/invert.h" +#include "lookingglass/lookingglass.h" +#include "magnifier/magnifier.h" +#include "mouseclick/mouseclick.h" +#include "mousemark/mousemark.h" +#include "sheet/sheet.h" +#include "snaphelper/snaphelper.h" +#include "startupfeedback/startupfeedback.h" +#include "trackmouse/trackmouse.h" +#include "wobblywindows/wobblywindows.h" +#endif + +#include +#include + +#ifndef EFFECT_BUILTINS +#define EFFECT_FALLBACK nullptr, nullptr, nullptr +#else +#define EFFECT_FALLBACK +#endif + +namespace KWin +{ + +namespace BuiltInEffects +{ + +template +inline Effect *createHelper() +{ + return new T(); +} + +static const QVector &effectData() +{ + static const QVector s_effectData = { + { + QString(), + QString(), + QString(), + QString(), + QString(), + QUrl(), + false, + false, + nullptr, + nullptr, + nullptr + }, { + QStringLiteral("blur"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Blur"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Blurs the background behind semi-transparent windows"), + QStringLiteral("Appearance"), + QString(), + QUrl(), + true, + false, +#ifdef EFFECT_BUILTINS + &createHelper, + &BlurEffect::supported, + &BlurEffect::enabledByDefault +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("colorpicker"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Color Picker"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Supports picking a color"), + QStringLiteral("Accessibility"), + QString(), + QUrl(), + true, + true, +#ifdef EFFECT_BUILTINS + &createHelper, + &ColorPickerEffect::supported, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("contrast"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Background contrast"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Improve contrast and readability behind semi-transparent windows"), + QStringLiteral("Appearance"), + QString(), + QUrl(), + true, + false, +#ifdef EFFECT_BUILTINS + &createHelper, + &ContrastEffect::supported, + &ContrastEffect::enabledByDefault +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("coverswitch"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Cover Switch"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Display a Cover Flow effect for the alt+tab window switcher"), + QStringLiteral("Window Management"), + QString(), + QUrl(QStringLiteral("https://files.kde.org/plasma/kwin/effect-videos/cover_switch.mp4")), + false, + true, +#ifdef EFFECT_BUILTINS + &createHelper, + &CoverSwitchEffect::supported, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("cube"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Desktop Cube"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Display each virtual desktop on a side of a cube"), + QStringLiteral("Window Management"), + QString(), + QUrl(QStringLiteral("https://files.kde.org/plasma/kwin/effect-videos/desktop_cube.ogv")), + false, + false, +#ifdef EFFECT_BUILTINS + &createHelper, + &CubeEffect::supported, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("cubeslide"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Desktop Cube Animation"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Animate desktop switching with a cube"), + QStringLiteral("Virtual Desktop Switching Animation"), + QStringLiteral("desktop-animations"), + QUrl(QStringLiteral("https://files.kde.org/plasma/kwin/effect-videos/desktop_cube_animation.ogv")), + false, + false, +#ifdef EFFECT_BUILTINS + &createHelper, + &CubeSlideEffect::supported, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("desktopgrid"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Desktop Grid"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Zoom out so all desktops are displayed side-by-side in a grid"), + QStringLiteral("Window Management"), + QString(), + QUrl(QStringLiteral("https://files.kde.org/plasma/kwin/effect-videos/desktop_grid.mp4")), + true, + false, +#ifdef EFFECT_BUILTINS + &createHelper, + nullptr, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("diminactive"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Dim Inactive"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Darken inactive windows"), + QStringLiteral("Focus"), + QString(), + QUrl(QStringLiteral("https://files.kde.org/plasma/kwin/effect-videos/dim_inactive.mp4")), + false, + false, +#ifdef EFFECT_BUILTINS + &createHelper, + nullptr, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("fallapart"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Fall Apart"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Closed windows fall into pieces"), + QStringLiteral("Appearance"), + QString(), + QUrl(), + false, + false, +#ifdef EFFECT_BUILTINS + &createHelper, + &FallApartEffect::supported, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("flipswitch"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Flip Switch"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Flip through windows that are in a stack for the alt+tab window switcher"), + QStringLiteral("Window Management"), + QString(), + QUrl(QStringLiteral("https://files.kde.org/plasma/kwin/effect-videos/flip_switch.mp4")), + false, + false, +#ifdef EFFECT_BUILTINS + &createHelper, + &FlipSwitchEffect::supported, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("glide"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Glide"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Glide windows as they appear or disappear"), + QStringLiteral("Window Open/Close Animation"), + QStringLiteral("toplevel-open-close-animation"), + QUrl(), + false, + false, +#ifdef EFFECT_BUILTINS + &createHelper, + &GlideEffect::supported, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("highlightwindow"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Highlight Window"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Highlight the appropriate window when hovering over taskbar entries"), + QStringLiteral("Appearance"), + QString(), + QUrl(), + true, + true, +#ifdef EFFECT_BUILTINS + &createHelper, + nullptr, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("invert"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Invert"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Inverts the color of the desktop and windows"), + QStringLiteral("Accessibility"), + QString(), + QUrl(QStringLiteral("https://files.kde.org/plasma/kwin/effect-videos/invert.mp4")), + false, + false, +#ifdef EFFECT_BUILTINS + &createHelper, + &InvertEffect::supported, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("kscreen"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Kscreen"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Helper Effect for KScreen"), + QStringLiteral("Appearance"), + QString(), + QUrl(), + true, + true, +#ifdef EFFECT_BUILTINS + &createHelper, + nullptr, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("lookingglass"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Looking Glass"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "A screen magnifier that looks like a fisheye lens"), + QStringLiteral("Accessibility"), + QStringLiteral("magnifiers"), + QUrl(QStringLiteral("https://files.kde.org/plasma/kwin/effect-videos/looking_glass.ogv")), + false, + false, +#ifdef EFFECT_BUILTINS + &createHelper, + &LookingGlassEffect::supported, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("magiclamp"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Magic Lamp"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Simulate a magic lamp when minimizing windows"), + QStringLiteral("Appearance"), + QStringLiteral("minimize"), + QUrl(QStringLiteral("https://files.kde.org/plasma/kwin/effect-videos/magic_lamp.ogv")), + false, + false, +#ifdef EFFECT_BUILTINS + &createHelper, + &MagicLampEffect::supported, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("magnifier"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Magnifier"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Magnify the section of the screen that is near the mouse cursor"), + QStringLiteral("Accessibility"), + QStringLiteral("magnifiers"), + QUrl(QStringLiteral("https://files.kde.org/plasma/kwin/effect-videos/magnifier.ogv")), + false, + false, +#ifdef EFFECT_BUILTINS + &createHelper, + &MagnifierEffect::supported, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("mouseclick"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Mouse Click Animation"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Creates an animation whenever a mouse button is clicked. This is useful for screenrecordings/presentations"), + QStringLiteral("Accessibility"), + QString(), + QUrl(QStringLiteral("https://files.kde.org/plasma/kwin/effect-videos/mouse_click.mp4")), + false, + false, +#ifdef EFFECT_BUILTINS + &createHelper, + nullptr, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("mousemark"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Mouse Mark"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Allows you to draw lines on the desktop"), + QStringLiteral("Appearance"), + QString(), + QUrl(), + false, + false, +#ifdef EFFECT_BUILTINS + &createHelper, + nullptr, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("presentwindows"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Present Windows"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Zoom out until all opened windows can be displayed side-by-side"), + QStringLiteral("Window Management"), + QString(), + QUrl(QStringLiteral("https://files.kde.org/plasma/kwin/effect-videos/present_windows.mp4")), + true, + false, +#ifdef EFFECT_BUILTINS + &createHelper, + nullptr, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("resize"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Resize Window"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Resizes windows with a fast texture scale instead of updating contents"), + QStringLiteral("Window Management"), + QString(), + QUrl(), + false, + false, +#ifdef EFFECT_BUILTINS + &createHelper, + nullptr, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("screenedge"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Screen Edge"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Highlights a screen edge when approaching"), + QStringLiteral("Appearance"), + QString(), + QUrl(), + true, + false, +#ifdef EFFECT_BUILTINS + &createHelper, + nullptr, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("screenshot"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Screenshot"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Helper effect for screenshot tools"), + QStringLiteral("Appearance"), + QString(), + QUrl(), + true, + true, +#ifdef EFFECT_BUILTINS + &createHelper, + &ScreenShotEffect::supported, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("sheet"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Sheet"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Make modal dialogs smoothly fly in and out when they are shown or hidden"), + QStringLiteral("Appearance"), + QString(), + QUrl(), + false, + false, +#ifdef EFFECT_BUILTINS + &createHelper, + &SheetEffect::supported, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("showfps"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Show FPS"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Display KWin's performance in the corner of the screen"), + QStringLiteral("Tools"), + QString(), + QUrl(), + false, + false, +#ifdef EFFECT_BUILTINS + &createHelper, + nullptr, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("showpaint"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Show Paint"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Highlight areas of the desktop that have been recently updated"), + QStringLiteral("Tools"), + QString(), + QUrl(), + false, + false, +#ifdef EFFECT_BUILTINS + &createHelper, + nullptr, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("slide"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Slide"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Slide desktops when switching virtual desktops"), + QStringLiteral("Virtual Desktop Switching Animation"), + QStringLiteral("desktop-animations"), + QUrl(QStringLiteral("https://files.kde.org/plasma/kwin/effect-videos/slide.ogv")), + true, + false, +#ifdef EFFECT_BUILTINS + &createHelper, + &SlideEffect::supported, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("slideback"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Slide Back"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Slide back windows when another window is raised"), + QStringLiteral("Focus"), + QString(), + QUrl(), + false, + false, +#ifdef EFFECT_BUILTINS + &createHelper, + nullptr, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("slidingpopups"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Sliding popups"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Sliding animation for Plasma popups"), + QStringLiteral("Appearance"), + QString(), + QUrl(QStringLiteral("https://files.kde.org/plasma/kwin/effect-videos/sliding_popups.mp4")), + true, + false, +#ifdef EFFECT_BUILTINS + &createHelper, + &SlidingPopupsEffect::supported, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("snaphelper"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Snap Helper"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Help you locate the center of the screen when moving a window"), + QStringLiteral("Accessibility"), + QString(), + QUrl(QStringLiteral("https://files.kde.org/plasma/kwin/effect-videos/snap_helper.mp4")), + false, + false, +#ifdef EFFECT_BUILTINS + &createHelper, + nullptr, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("startupfeedback"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Startup Feedback"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Helper effect for startup feedback"), + QStringLiteral("Appearance"), + QString(), + QUrl(), + true, + true, +#ifdef EFFECT_BUILTINS + &createHelper, + &StartupFeedbackEffect::supported, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("thumbnailaside"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Thumbnail Aside"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Display window thumbnails on the edge of the screen"), + QStringLiteral("Appearance"), + QString(), + QUrl(), + false, + false, +#ifdef EFFECT_BUILTINS + &createHelper, + nullptr, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("touchpoints"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Touch Points"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Visualize touch points"), + QStringLiteral("Appearance"), + QString(), + QUrl(), + false, + false, +#ifdef EFFECT_BUILTINS + &createHelper, + nullptr, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("trackmouse"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Track Mouse"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Display a mouse cursor locating effect when activated"), + QStringLiteral("Accessibility"), + QString(), + QUrl(QStringLiteral("https://files.kde.org/plasma/kwin/effect-videos/track_mouse.mp4")), + false, + false, +#ifdef EFFECT_BUILTINS + &createHelper, + nullptr, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("windowgeometry"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Window Geometry"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Display window geometries on move/resize"), + QStringLiteral("Appearance"), + QString(), + QUrl(), + false, + true, +#ifdef EFFECT_BUILTINS + &createHelper, + nullptr, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("wobblywindows"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Wobbly Windows"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Deform windows while they are moving"), + QStringLiteral("Appearance"), + QString(), + QUrl(QStringLiteral("https://files.kde.org/plasma/kwin/effect-videos/wobbly_windows.ogv")), + false, + false, +#ifdef EFFECT_BUILTINS + &createHelper, + &WobblyWindowsEffect::supported, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("zoom"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Zoom"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Magnify the entire desktop"), + QStringLiteral("Accessibility"), + QStringLiteral("magnifiers"), + QUrl(QStringLiteral("https://files.kde.org/plasma/kwin/effect-videos/zoom.ogv")), + true, + false, +#ifdef EFFECT_BUILTINS + &createHelper, + nullptr, + nullptr +#endif +EFFECT_FALLBACK + } + }; + return s_effectData; +} + +static inline int index(BuiltInEffect effect) +{ + return static_cast(effect); +} + +Effect *create(BuiltInEffect effect) +{ + const EffectData &data = effectData(effect); + if (data.createFunction == nullptr) { + return nullptr; + } + return data.createFunction(); +} + +bool available(const QString &name) +{ + auto it = std::find_if(effectData().begin(), effectData().end(), + [name](const EffectData &data) { + return data.name == name; + } + ); + return it != effectData().end(); +} + +bool supported(BuiltInEffect effect) +{ + if (effect == BuiltInEffect::Invalid) { + return false; + } + const EffectData &data = effectData(effect); + if (data.supportedFunction == nullptr) { + return true; + } + return data.supportedFunction(); +} + +bool checkEnabledByDefault(BuiltInEffect effect) +{ + if (effect == BuiltInEffect::Invalid) { + return false; + } + const EffectData &data = effectData(effect); + if (data.enabledFunction == nullptr) { + return true; + } + return data.enabledFunction(); +} + +bool enabledByDefault(BuiltInEffect effect) +{ + return effectData(effect).enabled; +} + +QStringList availableEffectNames() +{ + QStringList result; + for (const EffectData &data : effectData()) { + if (data.name.isEmpty()) { + continue; + } + result << data.name; + } + return result; +} + +QList< BuiltInEffect > availableEffects() +{ + QList result; + for (int i = index(BuiltInEffect::Invalid) + 1; i <= index(BuiltInEffect::Zoom); ++i) { + result << BuiltInEffect(i); + } + return result; +} + +BuiltInEffect builtInForName(const QString &name) +{ + auto it = std::find_if(effectData().begin(), effectData().end(), + [name](const EffectData &data) { + return data.name == name; + } + ); + if (it == effectData().end()) { + return BuiltInEffect::Invalid; + } + return BuiltInEffect(std::distance(effectData().begin(), it)); +} + +QString nameForEffect(BuiltInEffect effect) +{ + return effectData(effect).name; +} + +const EffectData &effectData(BuiltInEffect effect) +{ + return effectData().at(index(effect)); +} + +} // BuiltInEffects + +} // namespace diff --git a/effects/effect_builtins.h b/effects/effect_builtins.h new file mode 100644 index 0000000..d0dd751 --- /dev/null +++ b/effects/effect_builtins.h @@ -0,0 +1,96 @@ +/* + 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 +*/ +#ifndef KWIN_EFFECT_BUILTINS_H +#define KWIN_EFFECT_BUILTINS_H +#include +#include +#include +#include + +namespace KWin +{ +class Effect; + +/** + * Defines all the built in effects. + */ +enum class BuiltInEffect +{ + Invalid, ///< not a valid Effect + Blur, + ColorPicker, + Contrast, + CoverSwitch, + Cube, + CubeSlide, + DesktopGrid, + DimInactive, + FallApart, + FlipSwitch, + Glide, + HighlightWindow, + Invert, + Kscreen, + LookingGlass, + MagicLamp, + Magnifier, + MouseClick, + MouseMark, + PresentWindows, + Resize, + ScreenEdge, + ScreenShot, + Sheet, + ShowFps, + ShowPaint, + Slide, + SlideBack, + SlidingPopups, + SnapHelper, + StartupFeedback, + ThumbnailAside, + TouchPoints, + TrackMouse, + WindowGeometry, + WobblyWindows, + Zoom +}; + +namespace BuiltInEffects +{ + +struct EffectData { + QString name; + QString displayName; + QString comment; + QString category; + QString exclusiveCategory; + QUrl video; + bool enabled; + bool internal; + std::function createFunction; + std::function supportedFunction; + std::function enabledFunction; +}; + +KWINEFFECTS_EXPORT Effect *create(BuiltInEffect effect); +KWINEFFECTS_EXPORT bool available(const QString &name); +KWINEFFECTS_EXPORT bool supported(BuiltInEffect effect); +KWINEFFECTS_EXPORT bool checkEnabledByDefault(BuiltInEffect effect); +KWINEFFECTS_EXPORT bool enabledByDefault(BuiltInEffect effect); +KWINEFFECTS_EXPORT QString nameForEffect(BuiltInEffect effect); +KWINEFFECTS_EXPORT BuiltInEffect builtInForName(const QString &name); +KWINEFFECTS_EXPORT QStringList availableEffectNames(); +KWINEFFECTS_EXPORT QList availableEffects(); +KWINEFFECTS_EXPORT const EffectData &effectData(BuiltInEffect effect); +} + +} + +#endif diff --git a/effects/eyeonscreen/package/contents/code/main.js b/effects/eyeonscreen/package/contents/code/main.js new file mode 100644 index 0000000..201c08a --- /dev/null +++ b/effects/eyeonscreen/package/contents/code/main.js @@ -0,0 +1,152 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Thomas Lübking + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +/*global effect, effects, animate, animationTime, Effect, QEasingCurve */ + +"use strict"; + +var eyeOnScreenEffect = { + duration: animationTime(250), + loadConfig: function () { + eyeOnScreenEffect.duration = animationTime(250); + }, + delevateWindow: function(window) { + if (window.desktopWindow) { + if (window.eyeOnScreenShowsDesktop) { + window.eyeOnScreenShowsDesktop = false; + var stackingOrder = effects.stackingOrder; + for (var i = 0; i < stackingOrder.length; ++i) { + var w = stackingOrder[i]; + if (w.eyeOnScreenOpacityKeeper === undefined) + continue; + cancel(w.eyeOnScreenOpacityKeeper); + delete w.eyeOnScreenOpacityKeeper; + } + } + } else if (window.elevatedByEyeOnScreen) { + effects.setElevatedWindow(window, false); + window.elevatedByEyeOnScreen = false; + } + }, + slurp: function (showing) { + var stackingOrder = effects.stackingOrder; + var screenGeo = effects.virtualScreenGeometry; + var center = { value1: screenGeo.x + screenGeo.width/2, + value2: screenGeo.y + screenGeo.height/2 }; + for (var i = 0; i < stackingOrder.length; ++i) { + var w = stackingOrder[i]; + if (!w.visible || !(showing || w.slurpedByEyeOnScreen)) { + continue; + } + w.slurpedByEyeOnScreen = showing; + if (w.desktopWindow) { + // causes "seizures" because of opposing movements + // var zoom = showing ? 0.8 : 1.2; + var zoom = 0.8; + w.eyeOnScreenShowsDesktop = showing; + animate({ + window: w, + duration: 2*eyeOnScreenEffect.duration, // "*2 for "bumper" transition + animations: [{ + type: Effect.Scale, + curve: Effect.GaussianCurve, + to: zoom + }, { + type: Effect.Opacity, + curve: Effect.GaussianCurve, + to: 0.0 + }] + }); + if (showing) // (when not showing, pretty much everything would be above) + break; // ignore windows above the desktop + } else { + effects.setElevatedWindow(w, showing); + if (showing) { + w.elevatedByEyeOnScreen = true; + if (w.dock) { + animate({ + // this is a HACK - we need to trigger an animationEnded to delevate the dock in time, or it'll flicker :-( + // TODO? "var timer = new QTimer;" causes an error in effect scripts + window: w, + animations: [{ + type: Effect.Opacity, + curve: QEasingCurve.Linear, + duration: eyeOnScreenEffect.duration, + to: 0.9 + }] + }); + } else { + animate({ + window: w, + animations: [{ + type: Effect.Scale, + curve: QEasingCurve.InCubic, + duration: eyeOnScreenEffect.duration, + to: 0.0 + }, { + type: Effect.Position, + curve: QEasingCurve.InCubic, + duration: eyeOnScreenEffect.duration, + to: center + }] + }); + } + w.eyeOnScreenOpacityKeeper = set({ + window: w, + animations: [{ + type: Effect.Opacity, + curve: QEasingCurve.InCubic, + duration: eyeOnScreenEffect.duration, + to: 0.0 + }] + }); + } else { + w.elevatedByEyeOnScreen = false; + if (!w.dock) { + animate({ + window: w, + duration: eyeOnScreenEffect.duration, + delay: eyeOnScreenEffect.duration, + animations: [{ + type: Effect.Scale, + curve: QEasingCurve.OutCubic, + from: 0.0 + }, { + type: Effect.Position, + curve: QEasingCurve.OutCubic, + from: center + }] + }); + } + if (w.eyeOnScreenOpacityKeeper !== undefined) { + cancel(w.eyeOnScreenOpacityKeeper); + delete w.eyeOnScreenOpacityKeeper; + } + animate({ + window: w, + duration: eyeOnScreenEffect.duration, + delay: eyeOnScreenEffect.duration, + animations: [{ + type: Effect.Opacity, + curve: QEasingCurve.OutCubic, + duration: eyeOnScreenEffect.duration, + from: 0.0 + }] + }); + } + } + } + }, + init: function () { + eyeOnScreenEffect.loadConfig(); + effects.showingDesktopChanged.connect(eyeOnScreenEffect.slurp); + effect.animationEnded.connect(eyeOnScreenEffect.delevateWindow); + } +}; + +eyeOnScreenEffect.init(); diff --git a/effects/eyeonscreen/package/metadata.desktop b/effects/eyeonscreen/package/metadata.desktop new file mode 100644 index 0000000..ce8515e --- /dev/null +++ b/effects/eyeonscreen/package/metadata.desktop @@ -0,0 +1,73 @@ +[Desktop Entry] +Name=Eye on Screen +Name[az]=Pəncərənin ekranın mərkəzinə çəkilməsi +Name[ca]=Ull a la pantalla +Name[cs]=Oko na obrazovce +Name[en_GB]=Eye on Screen +Name[es]=Ojo a la pantalla +Name[et]=Eye on Screen +Name[eu]=Begirada pantailan +Name[fi]=Silmä näytöllä +Name[fr]=Jeter un oeil sur le bureau +Name[ia]=Eye On Screen (Oculo sur schermo) +Name[id]=Mata di Layar +Name[it]=Eye On Screen +Name[ko]=화면 위의 눈 +Name[lt]=Akis ekrane +Name[nl]=Oog op scherm +Name[nn]=Auge pÃ¥ skjerm +Name[pl]=Oko na ekranie +Name[pt]=Olho no Ecrã +Name[pt_BR]=Olho na tela +Name[ro]=Ochi pe ecran +Name[ru]=Втягивание окон в центр экрана +Name[sk]=Oko na obrazovke +Name[sl]=Oko na zaslonu +Name[sv]=Ögat pÃ¥ skärmen +Name[uk]=Око на екрані +Name[x-test]=xxEye on Screenxx +Name[zh_CN]=关注屏幕 +Name[zh_TW]=螢幕之眼 +Icon=preferences-system-windows-effect-eyeonscreen +Comment=Suck windows into the desktop +Comment[az]=Pəncərənin İş Masasının mərkəzinə çəkilməsi +Comment[ca]=Enganxa les finestres a l'escriptori +Comment[en_GB]=Suck windows into the desktop +Comment[es]=Aspirar las ventanas en el escritorio +Comment[et]=Akende imemine töölauale +Comment[eu]=Mahaigainak leihoak xurgatu +Comment[fi]=Imaise ikkunat työpöydälle +Comment[fr]=Faire disparaître les fenêtres dans le bureau +Comment[ia]=Suger fenestras in le scriptorio +Comment[id]=Sedot window ke desktop +Comment[it]=Risucchia le finestre nel desktop +Comment[ko]=창을 바탕 화면으로 흡수 +Comment[lt]=Ä®traukti langus į darbalaukį +Comment[nl]=Zuig vensters in het bureaublad +Comment[nn]=Sug vindauge inn i skrivebordet +Comment[pl]=Zasysa okna na pulpicie +Comment[pt]=Aspira as janelas para o ecrã +Comment[pt_BR]=Suga as janelas para a área de trabalho +Comment[ro]=Suge ferestrele în birou +Comment[ru]=Втягивание окон в центр рабочего стола +Comment[sk]=NasaÅ¥ okná na plochu +Comment[sl]=Prisesa okna na namizje +Comment[sv]=Sug in fönster i skrivbordet +Comment[uk]=Засмоктування вікон до стільниці +Comment[x-test]=xxSuck windows into the desktopxx +Comment[zh_CN]=将窗口吸收到桌面 +Comment[zh_TW]=將視窗吸進桌面 + +Type=Service +X-Plasma-API=javascript +X-Plasma-MainScript=code/main.js +X-KDE-ServiceTypes=KWin/Effect +X-KDE-PluginInfo-Author=Thomas Lübking +X-KDE-PluginInfo-Email=thomas.luebking@gmail.com +X-KDE-PluginInfo-Name=kwin4_effect_eyeonscreen +X-KDE-PluginInfo-Version=0.1.0 +X-KDE-PluginInfo-Category=Show Desktop Animation +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-EnabledByDefault=false +X-KDE-Ordering=50 +X-KWin-Exclusive-Category=show-desktop diff --git a/effects/fade/CMakeLists.txt b/effects/fade/CMakeLists.txt new file mode 100644 index 0000000..b84b58b --- /dev/null +++ b/effects/fade/CMakeLists.txt @@ -0,0 +1,8 @@ +install(DIRECTORY package/ + DESTINATION ${DATA_INSTALL_DIR}/${KWIN_NAME}/effects/kwin4_effect_fade) + +install(FILES package/metadata.desktop + DESTINATION ${SERVICES_INSTALL_DIR}/${KWIN_NAME} + RENAME kwin4_effect_fade.desktop) + +file(COPY package/ DESTINATION ${CMAKE_BINARY_DIR}/bin/kwin/effects/kwin4_effect_fade) diff --git a/effects/fade/package/contents/code/main.js b/effects/fade/package/contents/code/main.js new file mode 100644 index 0000000..d5337fb --- /dev/null +++ b/effects/fade/package/contents/code/main.js @@ -0,0 +1,106 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Philip Falkner + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +var blacklist = [ + // The logout screen has to be animated only by the logout effect. + "ksmserver ksmserver", + "ksmserver-logout-greeter ksmserver-logout-greeter", + + // The splash screen has to be animated only by the login effect. + "ksplashqml ksplashqml", + "ksplashsimple ksplashsimple", + "ksplashx ksplashx" +]; + +function isFadeWindow(w) { + if (blacklist.indexOf(w.windowClass) != -1) { + return false; + } + if (w.popupWindow) { + return false; + } + if (w.x11Client && !w.managed) { + return false; + } + if (!w.visible) { + return false; + } + if (w.outline) { + return false; + } + if (w.deleted && effect.isGrabbed(w, Effect.WindowClosedGrabRole)) { + return false; + } else if (!w.deleted && effect.isGrabbed(w, Effect.WindowAddedGrabRole)) { + return false; + } + return w.normalWindow || w.dialog; +} + +var fadeInTime, fadeOutTime, fadeWindows; +function loadConfig() { + fadeInTime = animationTime(effect.readConfig("FadeInTime", 150)); + fadeOutTime = animationTime(effect.readConfig("FadeOutTime", 150)) * 4; + fadeWindows = effect.readConfig("FadeWindows", true); +} +loadConfig(); +effect.configChanged.connect(function() { + loadConfig(); +}); +function fadeInHandler(w) { + if (effects.hasActiveFullScreenEffect) { + return; + } + if (fadeWindows && isFadeWindow(w)) { + if (w.fadeOutWindowTypeAnimation !== undefined) { + cancel(w.fadeOutWindowTypeAnimation); + w.fadeOutWindowTypeAnimation = undefined; + } + w.fadeInWindowTypeAnimation = effect.animate(w, Effect.Opacity, fadeInTime, 1.0, 0.0); + } +} +function fadeOutHandler(w) { + if (effects.hasActiveFullScreenEffect) { + return; + } + if (fadeWindows && isFadeWindow(w)) { + if (w.fadeOutWindowTypeAnimation !== undefined) { + // don't animate again as it was already animated through window hidden + return; + } + w.fadeOutWindowTypeAnimation = animate({ + window: w, + duration: fadeOutTime, + animations: [{ + type: Effect.Opacity, + curve: QEasingCurve.OutQuart, + to: 0.0 + }] + }); + } +} +effects.windowAdded.connect(fadeInHandler); +effects.windowClosed.connect(fadeOutHandler); +effects.windowDataChanged.connect(function (window, role) { + if (role == Effect.WindowAddedGrabRole) { + if (effect.isGrabbed(window, Effect.WindowAddedGrabRole)) { + if (window.fadeInWindowTypeAnimation !== undefined) { + cancel(window.fadeInWindowTypeAnimation); + window.fadeInWindowTypeAnimation = undefined; + } + } + } else if (role == Effect.WindowClosedGrabRole) { + if (effect.isGrabbed(window, Effect.WindowClosedGrabRole)) { + if (window.fadeOutWindowTypeAnimation !== undefined) { + cancel(window.fadeOutWindowTypeAnimation); + window.fadeOutWindowTypeAnimation = undefined; + } + } + } +}); diff --git a/effects/fade/package/contents/config/main.xml b/effects/fade/package/contents/config/main.xml new file mode 100644 index 0000000..2e0373f --- /dev/null +++ b/effects/fade/package/contents/config/main.xml @@ -0,0 +1,20 @@ + + + + + + + true + + + 150 + + + 150 + + + + diff --git a/effects/fade/package/metadata.desktop b/effects/fade/package/metadata.desktop new file mode 100644 index 0000000..0235da6 --- /dev/null +++ b/effects/fade/package/metadata.desktop @@ -0,0 +1,163 @@ +[Desktop Entry] +Name=Fade +Name[af]=Vervaag +Name[ar]=التلاشي +Name[az]=Solmaq +Name[be]=Павольнае знікненне +Name[bg]=Избледняване +Name[bn]=ফেড +Name[bs]=Utapanje +Name[ca]=Apagat gradual +Name[ca@valencia]=Fosa +Name[cs]=Blednutí +Name[da]=Udtone +Name[de]=Verblassen +Name[el]=Ομαλή εμφάνιση +Name[en_GB]=Fade +Name[eo]=Dissolvi +Name[es]=Fundido +Name[et]=Hääbumine +Name[eu]=Desagertzea +Name[fa]=محو کردن +Name[fi]=Häivytys +Name[fr]=Fondu +Name[fy]=Litte ferfage +Name[ga]=Céimnigh +Name[gl]=Esvaer +Name[gu]=પીગળવું +Name[he]=חשיפה והיעלמות +Name[hi]=फीका +Name[hne]=फीका +Name[hr]=Lagano pojavljivanje +Name[hu]=Fokozatos átmenet +Name[ia]=Pallidi +Name[id]=Lesap +Name[is]=Þynna út +Name[it]=Dissolvi +Name[ja]=フェード +Name[kk]=Біртіндеп +Name[km]=លេច​បន្តិច​ម្ដងៗ​ +Name[kn]=ಮಾಸು/ಮಸುಕುಗೊಳಿಸು +Name[ko]=페이드 +Name[lt]=IÅ¡nykimas/Atsiradimas +Name[lv]=Izdzist +Name[mai]=फीका करू +Name[mk]=Избледување +Name[ml]=മങ്ങുക +Name[mr]=फीके +Name[nb]=Ton ut +Name[nds]=Utblennen +Name[ne]=फेड +Name[nl]=Opkomen/vervagen +Name[nn]=Ton inn og ut +Name[pa]=ਫਿੱਕਾ +Name[pl]=Zanikanie/wyłanianie +Name[pt]=Desvanecer +Name[pt_BR]=Desvanecer +Name[ro]=Estompare +Name[ru]=Растворение +Name[se]=Rievdat Å¡earratvuođa +Name[si]=විවර්ණ +Name[sk]=ZoslabiÅ¥ +Name[sl]=Pojavljanje in pojemanje +Name[sr]=Утапање +Name[sr@ijekavian]=Утапање +Name[sr@ijekavianlatin]=Utapanje +Name[sr@latin]=Utapanje +Name[sv]=Tona +Name[ta]=வெளிர் +Name[te]=ఫేడ్ +Name[th]=ค่อย ๆ ชัด/ค่อย ๆ จางหาย +Name[tr]=Kaybolma +Name[ug]=سۇسلاشتۇر +Name[uk]=Згасання +Name[uz]=SoÊ»nish +Name[uz@cyrillic]=Сўниш +Name[vi]=Mờ dần +Name[wa]=BlÃ¥we +Name[x-test]=xxFadexx +Name[zh_CN]=淡入 +Name[zh_TW]=淡出 +Icon=preferences-system-windows-effect-fade +Comment=Make windows smoothly fade in and out when they are shown or hidden +Comment[ar]=اجعل النوافذ تظهر وتتلاشى بنعومة عند إظهاراها وإخفائها +Comment[az]=Bağlanan pəncərə getdikcə şəffaflanşır (solur) və tam yox olur +Comment[bg]=Постепенно избледняване при показване и скриване на прозорците +Comment[bs]=Prozori glatko izranjaju i utapaju se pri pojavljivanju i sakrivanju +Comment[ca]=Fa que les finestres s'encenguin o s'apaguin de manera gradual quan es mostren o s'oculten +Comment[ca@valencia]=Fa que les finestres s'encenguen o s'apaguen de manera gradual quan es mostren o s'oculten +Comment[cs]=Nechá okna plynule zmizet/objevit se, pokud jsou zobrazeny resp. skryty +Comment[da]=FÃ¥ vinduer til at tone blidt ud og ind nÃ¥r de vises eller skjules +Comment[de]=Blendet Fenster beim Öffnen/Schließen langsam ein bzw. aus. +Comment[el]=Ομαλή εμφάνιση και απόκρυψη των παραθύρων +Comment[en_GB]=Make windows smoothly fade in and out when they are shown or hidden +Comment[eo]=Fenestroj glate maldissolvi/fordissolvi kiam ili videbliĝas aÅ­ kaŝiĝas +Comment[es]=Hace que las ventanas aparezcan suavemente o se desvanezcan al mostrarlas u ocultarlas +Comment[et]=Paneb aknad sujuvalt hääbuma või tugevnema, kui need peidetakse või nähtavale tuuakse +Comment[eu]=Leihoak emeki agertzen eta desagertzen ditu haiek erakustean edo ezkutatzean +Comment[fi]=Ikkunat tulevat näkyviin tai poistuvat näkyvistä pehmeästi häivyttäen +Comment[fr]=Estompe ou fait apparaître en fondu les fenêtres lorsqu'elles sont affichées ou cachées +Comment[fy]=Lit finters útstrutsen ferfage of opkomme as se te sjen binne of ferburgen wurde +Comment[ga]=Leis seo, céimneoidh fuinneoga isteach agus amach agus iad á dtaispeáint nó á bhfolú +Comment[gl]=Esvae/Fai opacas as xanelas con suavidade ao mostralas ou agochadas +Comment[gu]=જ્યારે વિન્ડો બતાવવામાં અથવા છુપાવવામાં આવે છે ત્યારે તેમને સરળતાથી ઝાંખી અથવા પ્રકાશિત કરો +Comment[he]=חשיפה והיעלמות חלקה של חלונות בעת הצגתם או הסתרתם +Comment[hi]=जब विंडो को दिखाया या छुपाया जाता है तो उन्हें यह धीरे से फ़ीका करता है +Comment[hne]=जब विंडो ल देखाय या लुकाय जाथे तहां ये मन ल, ए धीरे से फीका करथे +Comment[hr]=Prozori će se lagano pojavljivati i nestajati kad ih se prikazuje ili sakriva +Comment[hu]=Az ablakok folyamatosan áttűnő módon lesznek elrejtve és megjelenítve +Comment[ia]=Face que fenestras pote dulcemente pallidir intra e foras quando illos es monstrate o celate +Comment[id]=Buat window melesap-muncul dan melesap-hilang ketika window ditampilkan atau disembunyikan +Comment[is]=Lætur glugga þynnast mjúklega inn eða út, þegar þeir eru endurheimtir eða faldir +Comment[it]=Fai dissolvere e comparire gradualmente le finestre quando vengono mostrate o nascoste +Comment[ja]=ウィンドウの表示/非表示の切り替えを滑らかにフェードイン/フェードアウトします +Comment[kk]=Көрсеткенде/жасырғанда терезелер біртіндеп пайда/ғайып болады +Comment[km]=ធ្វើ​ឲ្យ​បង្អួច​លេច​បន្តិច​ម្តង​/លិចបន្តិច​ម្តង​យ៉ាង​រលូន ពេល​ពួក​វា​ត្រូវ​បាន​បង្ហាញ ឬ​លាក់ +Comment[kn]=ಕಿಟಕಿಗಳನ್ನು ತೆರೆದಾಗ ಅಥವಾ ಅಡಗಿಸಿದಾಗ ಅವುಗಳು ಸುಗಮವಾಗಿ ಒಳಮಸುಳು/ಹೊರಮಸುಳುವಂತೆ ಮಾಡುತ್ತದೆ +Comment[ko]=창이 보여지거나 감춰질 때 부드러운 페이드 인/아웃을 사용합니다 +Comment[lt]=Sukuria efektą, kai langai, juos parodant ar paslepiant, glotniai pamažu atsiranda/iÅ¡nyksta +Comment[lv]=Liek logiem vienmērÄ«gi izdzist un parādÄ«ties, kad tos parāda vai noslēpj +Comment[ml]=ജാലകങ്ങള്‍ ഒളിപ്പിയ്ക്കുമ്പോഴോ കാണിയ്ക്കുമ്പോഴോ മങ്ങുന്നതും തെളിയുന്നതും പോലെ തോന്നിയ്ക്കുക +Comment[mr]=चौकटी दर्शविताना किंवा लपविताना त्यांना गडद वा फीक्या करा +Comment[nb]=Gjør at vinduer toner jevnt inn/ut nÃ¥r de vises eller skjules +Comment[nds]=Finstern bi't Wiesen oder Versteken week in- oder utblennen +Comment[nl]=Laat vensters vloeiend opkomen/vervagen als ze worden weergegeven of verborgen +Comment[nn]=Ton vindauge inn og ut nÃ¥r dei vert viste eller gøymde +Comment[pa]=ਜਦੋਂ ਵਿੰਡੋਜ਼ ਨੂੰ ਵੇਖਾਉਣਾ ਜਾਂ ਓਹਲੇ ਕਰਨਾ ਹੋਵੇ ਤਾਂ ਕੂਲੇ ਢੰਗ ਨਾਲ ਫੇਡ ਇਨ/ਆਉਟ ਕਰੋ +Comment[pl]=Okna gładko wyłaniają się przy otwieraniu i zanikają przy zamykaniu +Comment[pt]=Fazer com que as janelas apareçam/desapareçam suavemente quando aparecem ou ficam escondidas +Comment[pt_BR]=Faz as janelas aparecerem/desaparecerem suavemente quando são exibidas ou ocultadas +Comment[ro]=Face ferestrele să se (de)coloreze când sunt arătate sau ascunse +Comment[ru]=Закрывающиеся окна будут становиться всё более прозрачными, а потом совсем исчезать +Comment[si]=පෙන්වන හා සඟවන විට කවුළු විවර්‍ණය කිරීම හා නොකිරීම සිදු කරන්න +Comment[sk]=Okná sa plynule objavia/zmiznú pri ich zobrazení alebo skrytí +Comment[sl]=Okna se prikažejo in izginejo postopoma +Comment[sr]=Прозори глатко израњају и утапају се при појављивању и сакривању +Comment[sr@ijekavian]=Прозори глатко израњају и утапају се при појављивању и сакривању +Comment[sr@ijekavianlatin]=Prozori glatko izranjaju i utapaju se pri pojavljivanju i sakrivanju +Comment[sr@latin]=Prozori glatko izranjaju i utapaju se pri pojavljivanju i sakrivanju +Comment[sv]=Gör att fönster mjukt tonas in eller ut när de visas eller döljs +Comment[ta]=Make windows smoothly fade in and out when they are shown or hidden +Comment[th]=ทำให้หน้าต่างค่อย ๆ ชัดหรือค่อย ๆ จางหายเมื่อมีการแสดงหรือซ่อนหน้าต่าง +Comment[tr]=Pencereler gösterilirken pürüzsüz bir şekilde belirginleştir, gizlenirken pürüzsüz bir şekilde soldur +Comment[ug]=كۆزنەكنى كۆرسەتكەن ياكى يوشۇرغاندا كۆزنەكنى تەكشى سۇسلاشتۇر +Comment[uk]=Плавна поява або зникнення вікон +Comment[vi]=Làm cá»­a sổ mờ dần hay hiện dần khi chúng được ẩn đi hay hiện lên +Comment[wa]=Fé blawi/disblawi doûçmint les finiesses cwand on les mostere ou k' on les catche +Comment[x-test]=xxMake windows smoothly fade in and out when they are shown or hiddenxx +Comment[zh_CN]=当窗口被显示或者隐藏时,使窗口平滑地淡入淡出 +Comment[zh_TW]=顯示或隱藏視窗時以淡入、淡出方式呈現 + +Type=Service +X-Plasma-API=javascript +X-Plasma-MainScript=code/main.js +X-KDE-ServiceTypes=KWin/Effect +X-KDE-PluginInfo-Author=Philip Falkner, Martin Gräßlin +X-KDE-PluginInfo-Email=philip.falkner@gmail.com, mgraesslin@kde.org +X-KDE-PluginInfo-Name=kwin4_effect_fade +X-KDE-PluginInfo-Version=0.2.0 +X-KDE-PluginInfo-Category=Window Open/Close Animation +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-EnabledByDefault=true +X-KDE-Ordering=60 +X-KWin-Exclusive-Category=toplevel-open-close-animation diff --git a/effects/fadedesktop/CMakeLists.txt b/effects/fadedesktop/CMakeLists.txt new file mode 100644 index 0000000..22fe9f9 --- /dev/null +++ b/effects/fadedesktop/CMakeLists.txt @@ -0,0 +1,8 @@ +install(DIRECTORY package/ + DESTINATION ${DATA_INSTALL_DIR}/${KWIN_NAME}/effects/kwin4_effect_fadedesktop) + +install(FILES package/metadata.desktop + DESTINATION ${SERVICES_INSTALL_DIR}/${KWIN_NAME} + RENAME kwin4_effect_fadedesktop.desktop) + +file(COPY package/ DESTINATION ${CMAKE_BINARY_DIR}/bin/kwin/effects/kwin4_effect_fadedesktop) diff --git a/effects/fadedesktop/package/contents/code/main.js b/effects/fadedesktop/package/contents/code/main.js new file mode 100644 index 0000000..4dbc9f3 --- /dev/null +++ b/effects/fadedesktop/package/contents/code/main.js @@ -0,0 +1,125 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2012 Martin Gräßlin + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +"use strict"; + +var fadeDesktopEffect = { + duration: animationTime(250), + loadConfig: function () { + fadeDesktopEffect.duration = animationTime(250); + }, + fadeInWindow: function (window) { + if (window.fadeOutAnimation) { + if (redirect(window.fadeOutAnimation, Effect.Backward)) { + return; + } + cancel(window.fadeOutAnimation); + delete window.fadeOutAnimation; + } + if (window.fadeInAnimation) { + if (redirect(window.fadeInAnimation, Effect.Forward)) { + return; + } + cancel(window.fadeInAnimation); + } + window.fadeInAnimation = animate({ + window: window, + curve: QEasingCurve.Linear, + duration: fadeDesktopEffect.duration, + fullScreen: true, + keepAlive: false, + type: Effect.Opacity, + from: 0.0, + to: 1.0 + }); + }, + fadeOutWindow: function (window) { + if (window.fadeInAnimation) { + if (redirect(window.fadeInAnimation, Effect.Backward)) { + return; + } + cancel(window.fadeInAnimation); + delete window.fadeInAnimation; + } + if (window.fadeOutAnimation) { + if (redirect(window.fadeOutAnimation, Effect.Forward)) { + return; + } + cancel(window.fadeOutAnimation); + } + window.fadeOutAnimation = animate({ + window: window, + curve: QEasingCurve.Linear, + duration: fadeDesktopEffect.duration, + fullScreen: true, + keepAlive: false, + type: Effect.Opacity, + from: 1.0, + to: 0.0 + }); + }, + slotDesktopChanged: function (oldDesktop, newDesktop, movingWindow) { + if (effects.hasActiveFullScreenEffect && !effect.isActiveFullScreenEffect) { + return; + } + + var stackingOrder = effects.stackingOrder; + for (var i = 0; i < stackingOrder.length; ++i) { + var w = stackingOrder[i]; + + // Don't animate windows that have been moved to the current + // desktop, i.e. newDesktop. + if (w == movingWindow) { + continue; + } + + // If the window is not on the old and the new desktop or it's + // on both of them, then don't animate it. + var onOldDesktop = w.isOnDesktop(oldDesktop); + var onNewDesktop = w.isOnDesktop(newDesktop); + if (onOldDesktop == onNewDesktop) { + continue; + } + + if (w.minimized) { + continue; + } + + if (!w.isOnActivity(effects.currentActivity)){ + continue; + } + + if (onOldDesktop) { + fadeDesktopEffect.fadeOutWindow(w); + } else { + fadeDesktopEffect.fadeInWindow(w); + } + } + }, + slotIsActiveFullScreenEffectChanged: function () { + var isActiveFullScreen = effect.isActiveFullScreenEffect; + var stackingOrder = effects.stackingOrder; + for (var i = 0; i < stackingOrder.length; ++i) { + var w = stackingOrder[i]; + w.setData(Effect.WindowForceBlurRole, isActiveFullScreen); + w.setData(Effect.WindowForceBackgroundContrastRole, isActiveFullScreen); + } + }, + init: function () { + effect.configChanged.connect(fadeDesktopEffect.loadConfig); + effect.isActiveFullScreenEffectChanged.connect( + fadeDesktopEffect.slotIsActiveFullScreenEffectChanged); + effects['desktopChanged(int,int,KWin::EffectWindow*)'].connect( + fadeDesktopEffect.slotDesktopChanged); + } +}; + +fadeDesktopEffect.init(); diff --git a/effects/fadedesktop/package/metadata.desktop b/effects/fadedesktop/package/metadata.desktop new file mode 100644 index 0000000..14b3415 --- /dev/null +++ b/effects/fadedesktop/package/metadata.desktop @@ -0,0 +1,148 @@ +[Desktop Entry] +Name=Fade Desktop +Name[ar]=تلاشِ سطح المكتب +Name[az]=İş Masasının rəvan dəyişimi +Name[bg]=Избледняване на работния плот +Name[bs]=Utapanje povrÅ¡i +Name[ca]=Esvaïment de l'escriptori +Name[ca@valencia]=Fosa de l'escriptori +Name[cs]=Zeslabit plochu +Name[csb]=Pùlt fade +Name[da]=Lad skrivebord fade ud +Name[de]=Arbeitsfläche aus-/einblenden +Name[el]=Ομαλή εμφάνιση επιφάνειας εργασίας +Name[en_GB]=Fade Desktop +Name[eo]=Labortablo Dissolvo +Name[es]=Difuminar escritorio +Name[et]=Töölaua hääbumine +Name[eu]=Mahaigaina desagertzea +Name[fi]=Työpöydän häivytys +Name[fr]=Effectue un fondu du bureau +Name[fy]=Buroblêd ferfagje +Name[ga]=Céimnigh an Deasc +Name[gl]=Esvaer o escritorio +Name[gu]=ડેસ્કટોપ ઝાંખુ કરો +Name[he]=חשיפה והיעלמות בין שולחנות עבודה +Name[hr]=Preklapanje radnih povrÅ¡ina +Name[hu]=Elhalványuló váltás +Name[ia]=Pallidi scriptorio +Name[id]=Lesapkan Desktop +Name[is]=Þynna út skjáborð +Name[it]=Dissolvenza dei desktop +Name[ja]=デスクトップのフェード +Name[kk]=Біртіндеп ауысу +Name[km]=ធ្វើ​ឲ្យ​ផ្ទៃតុលេច​បន្តិចម្ដងៗ +Name[kn]=ಗಣಕತೆರೆಯನ್ನು ಮಬ್ಬಾಗಿಸು +Name[ko]=바탕 화면 페이드 +Name[lt]=Pamažu atsirandantis/iÅ¡nykstantis darbalaukis +Name[lv]=Sapludināt darbvirsmu +Name[mai]=Fade Desktop +Name[ml]=പണിയിടം മങ്ങി പോകുക +Name[mr]=फीका डेस्कटॉप +Name[nb]=Ton ut skrivebord +Name[nds]=Schriefdisch överblennen +Name[nl]=Bureaublad op laten komen +Name[nn]=Ton inn/ut skrivebord +Name[pa]=ਡੈਸਕਟਾਪ ਫਿੱਕਾ +Name[pl]=Przenikanie pulpitów +Name[pt]=Desvanecer o Ecrã +Name[pt_BR]=Desaparecimento da área de trabalho +Name[ro]=Estompare birou +Name[ru]=Плавная смена рабочих столов +Name[si]=වැඩතලය විවර්ණ කිරීම +Name[sk]=ZoslabiÅ¥ plochu +Name[sl]=Pojemanje namizja +Name[sr]=Утапање површи +Name[sr@ijekavian]=Утапање површи +Name[sr@ijekavianlatin]=Utapanje povrÅ¡i +Name[sr@latin]=Utapanje povrÅ¡i +Name[sv]=Tona skrivbord +Name[th]=ปรับพื้นที่ทำงานแบบค่อย ๆ ชัด/ค่อย ๆ จางหาย +Name[tr]=Masaüstü Kaybolması +Name[ug]=ئۈستەلئۈستىنى سۇسلاشتۇر +Name[uk]=Затемнення стільниці +Name[vi]=Làm mờ màn hình +Name[wa]=Fondou do scribanne +Name[x-test]=xxFade Desktopxx +Name[zh_CN]=淡出桌面 +Name[zh_TW]=淡出淡入桌面 +Icon=preferences-system-windows-effect-fadedesktop +Comment=Fade between virtual desktops when switching between them +Comment[ar]=يجعل أسطح المكتب تتلاشى عند التبيديل بينها +Comment[az]=Başqa İş Masasına keçdiksə fon şəklinin tədricən dəyişilməsi +Comment[bg]=Избледняване при превключване между работните плотове +Comment[bs]=Jedna virtuelna povrÅ¡ utapa se u drugu pri prebacivanju +Comment[ca]=Esvaeix entre els escriptoris virtuals quan es commuta entre ells +Comment[ca@valencia]=Fosa entre els escriptoris virtuals quan es canvien entre ells +Comment[cs]=Plochy při přepínání mění intenzitu +Comment[da]=Fade ud og ind mellem virtuelle skrivebord ved skift mellem dem +Comment[de]=Wechseln der virtuellen Arbeitsfläche durch Aus- und Einblenden der Arbeitsfläche. +Comment[el]=Εναλλαγή μεταξύ των επιφανειών εργασίας με ομαλό σβήσιμο +Comment[en_GB]=Fade between virtual desktops when switching between them +Comment[eo]=Disvolvi inter virtualaj labortabloj kiam ŝaltas inter ili. +Comment[es]=Difumina al cambiar entre escritorios virtuales +Comment[et]=Hääbumisefekt ühelt virtuaalselt töölaualt teisele lülitudes +Comment[eu]=Uneko mahaigaina desagerrarazi mahagain batetik bestera aldatzean +Comment[fi]=Häivytys virtuaalityöpöytien välillä niitä vaihdettaessa +Comment[fr]=Effectue un fondu entre les bureaux virtuels lors des changements de bureau +Comment[fy]=By it wikseljen tusken firuele buroblêden in ferfaging dwaan +Comment[ga]=Céimnigh idir deasca fíorúla agus ag malairt eatarthu +Comment[gl]=Emprega un efecto de esvaemento ao cambiar de escritorio +Comment[he]=חשיפה והיעלמות בין שולחנות עבודה וירטואליים במעבר ביניהם +Comment[hr]=Efekt preklapanja radnih povrÅ¡ina prilikom promjene među njima +Comment[hu]=Asztalváltáskor a régi asztal elhalványul, az új felerősödik +Comment[ia]=Pallidi inter scriptorio virtuales quando il commuta intra los +Comment[id]=Lesap di antara desktop virtual ketika bertukar di antara mereka +Comment[is]=Þynnir saman sýndarkjáborðum þegar skipt er á milli þeirra +Comment[it]=Passa da desktop virtuale all'altro con una dissolvenza +Comment[ja]=仮想デスクトップの切り替えにフェード効果をかけます +Comment[kk]=Виртуалды үстелді ауыстыруды біртіндеп көрсету +Comment[km]=ស្រមោល​រវាង​ផ្ទៃតុ​និម្មិត នៅពេល​ប្ដូរ​រវាង​ពួកវា +Comment[kn]=ವಾಸ್ತವಪ್ರಾಯ(ವರ್ಚುವಲ್) ಗಣಕತೆರೆಗಳ ನಡುವೆ ಬದಲಾಯಿಸುವಾಗ ಮಬ್ಬಾಗಿಸು +Comment[ko]=가상 바탕 화면 사이를 전환할 때 페이드 효과를 사용합니다 +Comment[lt]=IÅ¡blukinti, perjungiant virtualiuosius darbalaukius +Comment[lv]=PlÅ«stoÅ¡i pārslēgties starp vituālajām darbvirsmām +Comment[ml]=വെര്‍ച്വല്‍ പണിയിടങ്ങള്‍ക്കിടക്ക് സ്വിച്ച് ചെയ്യുമ്പോള്‍ മങ്ങിപ്പോകുന്നു. +Comment[mr]=आभासी डेस्कटॉप बदलताना फीका करा +Comment[nb]=Ton ut/inn mellom virtuelle skrivebord nÃ¥r det byttes mellom dem +Comment[nds]=Bi't Wesseln de Schriefdischen överenanner blennen +Comment[nl]=Laat virtuele bureaubladen vervagen en opkomen bij het wisselen +Comment[nn]=Ton inn og ut ved veksling mellom virtuelle skrivebord +Comment[pa]=ਜਦੋਂ ਵੁਰਚੁਅਲ ਡੈਸਕਟਾਪ ਵਿੱਚ ਬਦਲਣਾ ਹੋਵੇ ਤਾਂ ਉਹਨਾਂ ਨੂੰ ਫਿੱਕਾ ਕਰੋ +Comment[pl]=Przenika pulpity wirtualne przy ich przełączaniu +Comment[pt]=Desvanece entre os ecrãs virtuais, ao circular entre eles +Comment[pt_BR]=Desaparece entre as áreas de trabalho virtuais, ao alternar entre elas +Comment[ro]=Estompare între birourile virtuale la schimbarea între ele +Comment[ru]=Постепенная смена изображения при переключении на другой рабочий стол +Comment[si]=අතත්‍ය වැඩතල අතර මාරුවීමේදී ඒවා අතර විවර්ණය කරන්න +Comment[sk]=Efekt zoslabenia pri prepínaní virtuálnych plôch +Comment[sl]=Ob preklopu med namizjema postopoma skrije prejÅ¡njega in postopoma prikaže novega +Comment[sr]=Једна виртуелна површ утапа се у другу при пребацивању +Comment[sr@ijekavian]=Једна виртуелна површ утапа се у другу при пребацивању +Comment[sr@ijekavianlatin]=Jedna virtuelna povrÅ¡ utapa se u drugu pri prebacivanju +Comment[sr@latin]=Jedna virtuelna povrÅ¡ utapa se u drugu pri prebacivanju +Comment[sv]=Tona mellan virtuella skrivbord vid byte mellan dem +Comment[th]=ปรับพื้นที่ทำงานแบบค่อย ๆ ชัด/ค่อย ๆ จางหายเมื่อมีการสลับพื้นที่ทำงาน +Comment[tr]=Sanal masaüstleri arasında geçiş yapılırken kaybolma efektini kullan +Comment[ug]=مەۋھۇم ئۈستەلئۈستى ئارىسىدا ئالماشتۇرغاندا سۇسلاشتۇرۇش ئۈنۈمىنى كۆرسەت +Comment[uk]=Затемнення перед час перемикання між віртуальними стільницями +Comment[vi]=Hiệu ứng mờ khi chuyển đổi giữa các màn hình làm việc ảo +Comment[wa]=Fwait on fondou etur sicribannes cwand on passe d' onk a èn ôte +Comment[x-test]=xxFade between virtual desktops when switching between themxx +Comment[zh_CN]=在虚拟桌面间切换时呈现淡出效果 +Comment[zh_TW]=在虛擬桌面間切換時使用淡出/淡入效果 + +Type=Service +X-Plasma-API=javascript +X-Plasma-MainScript=code/main.js +X-KDE-ServiceTypes=KWin/Effect +X-KDE-PluginInfo-Author=Lucas Murray, Martin Gräßlin +X-KDE-PluginInfo-Email=lmurray@undefinedfire.com, mgraesslin@kde.org +X-KDE-PluginInfo-Name=kwin4_effect_fadedesktop +X-KDE-PluginInfo-Version=0.2.0 +X-KDE-PluginInfo-Category=Virtual Desktop Switching Animation +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-EnabledByDefault=false +X-KDE-Ordering=50 +X-KWin-Video-Url=https://files.kde.org/plasma/kwin/effect-videos/fade_desktop.ogv +X-KWin-Exclusive-Category=desktop-animations diff --git a/effects/fadingpopups/package/contents/code/main.js b/effects/fadingpopups/package/contents/code/main.js new file mode 100644 index 0000000..b49adcd --- /dev/null +++ b/effects/fadingpopups/package/contents/code/main.js @@ -0,0 +1,140 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +"use strict"; + +var blacklist = [ + // The logout screen has to be animated only by the logout effect. + "ksmserver ksmserver", + "ksmserver-logout-greeter ksmserver-logout-greeter", + + // KDE Plasma splash screen has to be animated only by the login effect. + "ksplashqml ksplashqml", + "ksplashsimple ksplashsimple", + "ksplashx ksplashx" +]; + +function isPopupWindow(window) { + // If the window is blacklisted, don't animate it. + if (blacklist.indexOf(window.windowClass) != -1) { + return false; + } + + // Animate combo box popups, tooltips, popup menus, etc. + if (window.popupWindow) { + return true; + } + + // Maybe the outline deserves its own effect. + if (window.outline) { + return true; + } + + // Override-redirect windows are usually used for user interface + // concepts that are expected to be animated by this effect, e.g. + // popups that contain window thumbnails on X11, etc. + if (window.x11Client && !window.managed) { + // Some utility windows can look like popup windows (e.g. the + // address bar dropdown in Firefox), but we don't want to fade + // them because the fade effect didn't do that. + if (window.utility) { + return false; + } + + return true; + } + + // Previously, there was a "monolithic" fade effect, which tried to + // animate almost every window that was shown or hidden. Then it was + // split into two effects: one that animates toplevel windows and + // this one. In addition to popups, this effect also animates some + // special windows(e.g. notifications) because the monolithic version + // was doing that. + if (window.dock || window.splash || window.toolbar + || window.notification || window.onScreenDisplay + || window.criticalNotification) { + return true; + } + + return false; +} + +var fadingPopupsEffect = { + loadConfig: function () { + fadingPopupsEffect.fadeInDuration = animationTime(150); + fadingPopupsEffect.fadeOutDuration = animationTime(150) * 4; + }, + slotWindowAdded: function (window) { + if (effects.hasActiveFullScreenEffect) { + return; + } + if (!isPopupWindow(window)) { + return; + } + if (!window.visible) { + return; + } + if (!effect.grab(window, Effect.WindowAddedGrabRole)) { + return; + } + window.fadeInAnimation = animate({ + window: window, + curve: QEasingCurve.Linear, + duration: fadingPopupsEffect.fadeInDuration, + type: Effect.Opacity, + from: 0.0, + to: 1.0 + }); + }, + slotWindowClosed: function (window) { + if (effects.hasActiveFullScreenEffect) { + return; + } + if (!isPopupWindow(window)) { + return; + } + if (!window.visible) { + return; + } + if (!effect.grab(window, Effect.WindowClosedGrabRole)) { + return; + } + window.fadeOutAnimation = animate({ + window: window, + curve: QEasingCurve.OutQuart, + duration: fadingPopupsEffect.fadeOutDuration, + type: Effect.Opacity, + from: 1.0, + to: 0.0 + }); + }, + slotWindowDataChanged: function (window, role) { + if (role == Effect.WindowAddedGrabRole) { + if (window.fadeInAnimation && effect.isGrabbed(window, role)) { + cancel(window.fadeInAnimation); + delete window.fadeInAnimation; + } + } else if (role == Effect.WindowClosedGrabRole) { + if (window.fadeOutAnimation && effect.isGrabbed(window, role)) { + cancel(window.fadeOutAnimation); + delete window.fadeOutAnimation; + } + } + }, + init: function () { + fadingPopupsEffect.loadConfig(); + + effect.configChanged.connect(fadingPopupsEffect.loadConfig); + effects.windowAdded.connect(fadingPopupsEffect.slotWindowAdded); + effects.windowClosed.connect(fadingPopupsEffect.slotWindowClosed); + effects.windowDataChanged.connect(fadingPopupsEffect.slotWindowDataChanged); + } +}; + +fadingPopupsEffect.init(); diff --git a/effects/fadingpopups/package/metadata.desktop b/effects/fadingpopups/package/metadata.desktop new file mode 100644 index 0000000..412c9bb --- /dev/null +++ b/effects/fadingpopups/package/metadata.desktop @@ -0,0 +1,83 @@ +[Desktop Entry] +Name=Fading Popups +Name[az]=Solğunlaşan sürüşən pəncərələr +Name[ca]=Missatges emergents esvaïts +Name[ca@valencia]=Missatges emergents que es fonen +Name[cs]=Mizející vyskakovací okna +Name[da]=Pop-op'er udtoner +Name[de]=Überblendete Aufklappfenster +Name[en_GB]=Fading Popups +Name[es]=Desvanecer ventanas emergentes +Name[et]=Hääbuvad hüpikdialoogid +Name[eu]=Itzaleztatzen diren gainerakorrak +Name[fi]=Hiipuvat ponnahdusikkunat +Name[fr]=Fondu des boîtes de dialogue +Name[gl]=Xanelas emerxentes que esvaen +Name[hu]=Halványodó felugró ablakok +Name[ia]=Popups dissolvente +Name[id]=Sembulan Melesap +Name[it]=Finestre a comparsa che si dissolvono +Name[ko]=페이드 팝업 +Name[lt]=Pamažu atsirandantys/iÅ¡nykstantys iÅ¡kylantieji langai +Name[nl]=Vervagende pop-ups +Name[nn]=Inn- og uttoning av sprettoppvindauge +Name[pl]=Zanikanie okien wysuwnych +Name[pt]=Mensagens Desvanecentes +Name[pt_BR]=Desvanecer mensagens +Name[ro]=Indicii estompate +Name[ru]=Растворяющиеся всплывающие окна +Name[sk]=Miznúce vyskakovacie okná +Name[sl]=Prehajajoča pojavna okna +Name[sv]=Borttonande meddelanderutor +Name[uk]=Інтерактивні контекстні панелі +Name[x-test]=xxFading Popupsxx +Name[zh_CN]=气泡通知渐隐渐现 +Name[zh_TW]=淡化彈出視窗 +Icon=preferences-system-windows-effect-fadingpopups +Comment=Make popups smoothly fade in and out when they are shown or hidden +Comment[az]=Sürüşən pəncərələr getdikcə şəffaflaşır və sonda tam yox olur +Comment[ca]=Fa que els missatges emergents s'encenguin o s'apaguin de manera gradual quan es mostren o s'oculten +Comment[ca@valencia]=Fa que els missatges emergents s'encenguen o s'apaguen de manera gradual quan es mostren o s'oculten +Comment[cs]=Nechá vyskakovací okna plynule zmizet/objevit se, pokud jsou zobrazeny resp. skryty +Comment[da]=FÃ¥ pop-op'er til at tone ud og ind nÃ¥r de vises eller skjules +Comment[de]=Blendet Aufklappfenster beim Öffnen/Schließen langsam ein bzw. aus +Comment[en_GB]=Make popups smoothly fade in and out when they are shown or hidden +Comment[es]=Hace que las ventanas emergentes aparezcan o se desvanezcan suavemente al mostrarlas u ocultarlas +Comment[et]=Paneb hüpikaknad sujuvalt hääbuma või tugevnema, kui need peidetakse või nähtavale tuuakse +Comment[eu]=Gainerakorrak emeki agertzen eta desagertzen ditu haiek erakustean edo ezkutatzean +Comment[fi]=Ponnahdusikkunat tulevat näkyviin tai poistuvat näkyvistä pehmeästi häivyttäen +Comment[fr]=Estompe ou fait apparaître en fondu les boîtes de dialogue lorsqu'elles sont affichées ou cachées +Comment[gl]=Esvae e fai opacas as xanelas emerxentes con suavidade ao mostralas ou agochadas +Comment[hu]=A felugró folyamatosan áttűnő módon lesznek elrejtve és megjelenítve +Comment[ia]=Face que fenestras pote dulcemente pallidir intra e foras quando illos es monstrate o celate +Comment[id]=Buat sembulan secara halus lesap-muncul dan lesap-hilang ketika ia ditampilkan atau disembunyikan +Comment[it]=Fai dissolvere e comparire gradualmente le finestre a comparsa quando vengono mostrate o nascoste +Comment[ko]=팝업이 보여지거나 감춰질 때 부드러운 페이드 인/아웃을 사용합니다 +Comment[lt]=Sukuria efektą, kai iÅ¡kylantieji langai, juos parodant ar paslepiant, glotniai pamažu atsiranda/iÅ¡nyksta +Comment[nl]=Laat pop-ups vloeiend opkomen/vervagen als ze worden weergegeven of verborgen +Comment[nn]=Ton sprettoppvindauge gradvis inn og ut nÃ¥r dei vert viste eller gøymde +Comment[pl]=Okna wysuwne gładko wyłaniają się przy otwieraniu i zanikają przy zamykaniu +Comment[pt]=Fazer com que as janelas apareçam/desapareçam suavemente quando aparecem ou ficam escondidas +Comment[pt_BR]=Faz as mensagens aparecerem/desaparecerem suavemente quando são exibidas ou ocultadas +Comment[ro]=Face indiciile să se (de)coloreze când sunt arătate sau ascunse +Comment[ru]=Всплывающие окна при закрытии будут становиться всё более прозрачными, а потом совсем исчезать +Comment[sk]=Okná sa plynule objavia/zmiznú pri ich zobrazení alebo skrytí +Comment[sl]=Okna se pojavijo in izginejo postopoma, kadar se pokažejo ali skrijejo +Comment[sv]=Gör att meddelanderutor mjukt tonas in eller ut när de visas eller döljs +Comment[uk]=Поступова поява або зникнення контекстних вікон вікон при відкритті чи закритті +Comment[x-test]=xxMake popups smoothly fade in and out when they are shown or hiddenxx +Comment[zh_CN]=当弹窗被显示或者隐藏时,使窗口平滑地淡入淡出 +Comment[zh_TW]=當彈出視窗出現或消失時,讓彈出視窗滑順的淡入和淡出 + +Type=Service +X-KDE-ServiceTypes=KWin/Effect +X-KDE-PluginInfo-Author=Vlad Zahorodnii +X-KDE-PluginInfo-Email=vlad.zahorodnii@kde.org +X-KDE-PluginInfo-Name=kwin4_effect_fadingpopups +X-KDE-PluginInfo-Version=1.0 +X-KDE-PluginInfo-Category=Appearance +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-EnabledByDefault=true +X-KDE-Ordering=60 +X-Plasma-API=javascript +X-Plasma-MainScript=code/main.js diff --git a/effects/fallapart/CMakeLists.txt b/effects/fallapart/CMakeLists.txt new file mode 100644 index 0000000..00ff7c8 --- /dev/null +++ b/effects/fallapart/CMakeLists.txt @@ -0,0 +1,7 @@ +####################################### +# Effect + +# Source files +set(kwin4_effect_builtins_sources ${kwin4_effect_builtins_sources} + fallapart/fallapart.cpp +) diff --git a/effects/fallapart/fallapart.cpp b/effects/fallapart/fallapart.cpp new file mode 100644 index 0000000..d521180 --- /dev/null +++ b/effects/fallapart/fallapart.cpp @@ -0,0 +1,192 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "fallapart.h" +// KConfigSkeleton +#include "fallapartconfig.h" + +#include + +namespace KWin +{ + +bool FallApartEffect::supported() +{ + return effects->isOpenGLCompositing() && effects->animationsSupported(); +} + +FallApartEffect::FallApartEffect() +{ + initConfig(); + reconfigure(ReconfigureAll); + connect(effects, &EffectsHandler::windowClosed, this, &FallApartEffect::slotWindowClosed); + connect(effects, &EffectsHandler::windowDeleted, this, &FallApartEffect::slotWindowDeleted); + connect(effects, &EffectsHandler::windowDataChanged, this, &FallApartEffect::slotWindowDataChanged); +} + +void FallApartEffect::reconfigure(ReconfigureFlags) +{ + FallApartConfig::self()->read(); + blockSize = FallApartConfig::blockSize(); +} + +void FallApartEffect::prePaintScreen(ScreenPrePaintData& data, int time) +{ + if (!windows.isEmpty()) + data.mask |= PAINT_SCREEN_WITH_TRANSFORMED_WINDOWS; + effects->prePaintScreen(data, time); +} + +void FallApartEffect::prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time) +{ + if (windows.contains(w) && isRealWindow(w)) { + if (windows[ w ] < 1) { + windows[ w ] += time / animationTime(1000.); + data.setTransformed(); + w->enablePainting(EffectWindow::PAINT_DISABLED_BY_DELETE); + // Request the window to be divided into cells + data.quads = data.quads.makeGrid(blockSize); + } else { + windows.remove(w); + w->unrefWindow(); + } + } + effects->prePaintWindow(w, data, time); +} + +void FallApartEffect::paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data) +{ + if (windows.contains(w) && isRealWindow(w)) { + const qreal t = windows[w]; + WindowQuadList new_quads; + int cnt = 0; + foreach (WindowQuad quad, data.quads) { // krazy:exclude=foreach + // make fragments move in various directions, based on where + // they are (left pieces generally move to the left, etc.) + QPointF p1(quad[ 0 ].x(), quad[ 0 ].y()); + double xdiff = 0; + if (p1.x() < w->width() / 2) + xdiff = -(w->width() / 2 - p1.x()) / w->width() * 100; + if (p1.x() > w->width() / 2) + xdiff = (p1.x() - w->width() / 2) / w->width() * 100; + double ydiff = 0; + if (p1.y() < w->height() / 2) + ydiff = -(w->height() / 2 - p1.y()) / w->height() * 100; + if (p1.y() > w->height() / 2) + ydiff = (p1.y() - w->height() / 2) / w->height() * 100; + double modif = t * t * 64; + srandom(cnt); // change direction randomly but consistently + xdiff += (rand() % 21 - 10); + ydiff += (rand() % 21 - 10); + for (int j = 0; + j < 4; + ++j) { + quad[ j ].move(quad[ j ].x() + xdiff * modif, quad[ j ].y() + ydiff * modif); + } + // also make the fragments rotate around their center + QPointF center((quad[ 0 ].x() + quad[ 1 ].x() + quad[ 2 ].x() + quad[ 3 ].x()) / 4, + (quad[ 0 ].y() + quad[ 1 ].y() + quad[ 2 ].y() + quad[ 3 ].y()) / 4); + double adiff = (rand() % 720 - 360) / 360. * 2 * M_PI; // spin randomly + for (int j = 0; + j < 4; + ++j) { + double x = quad[ j ].x() - center.x(); + double y = quad[ j ].y() - center.y(); + double angle = atan2(y, x); + angle += windows[ w ] * adiff; + double dist = sqrt(x * x + y * y); + x = dist * cos(angle); + y = dist * sin(angle); + quad[ j ].move(center.x() + x, center.y() + y); + } + new_quads.append(quad); + ++cnt; + } + data.quads = new_quads; + data.multiplyOpacity(interpolate(1.0, 0.0, t)); + } + effects->paintWindow(w, mask, region, data); +} + +void FallApartEffect::postPaintScreen() +{ + if (!windows.isEmpty()) + effects->addRepaintFull(); + effects->postPaintScreen(); +} + +bool FallApartEffect::isRealWindow(EffectWindow* w) +{ + // TODO: isSpecialWindow is rather generic, maybe tell windowtypes separately? + /* + qCDebug(KWINEFFECTS) << "--" << w->caption() << "--------------------------------"; + qCDebug(KWINEFFECTS) << "Tooltip:" << w->isTooltip(); + qCDebug(KWINEFFECTS) << "Toolbar:" << w->isToolbar(); + qCDebug(KWINEFFECTS) << "Desktop:" << w->isDesktop(); + qCDebug(KWINEFFECTS) << "Special:" << w->isSpecialWindow(); + qCDebug(KWINEFFECTS) << "TopMenu:" << w->isTopMenu(); + qCDebug(KWINEFFECTS) << "Notific:" << w->isNotification(); + qCDebug(KWINEFFECTS) << "Splash:" << w->isSplash(); + qCDebug(KWINEFFECTS) << "Normal:" << w->isNormalWindow(); + */ + if (w->isPopupWindow()) { + return false; + } + if (w->isX11Client() && !w->isManaged()) { + return false; + } + if (!w->isNormalWindow()) + return false; + return true; +} + +void FallApartEffect::slotWindowClosed(EffectWindow* c) +{ + if (!isRealWindow(c)) + return; + if (!c->isVisible()) + return; + const void* e = c->data(WindowClosedGrabRole).value(); + if (e && e != this) + return; + c->setData(WindowClosedGrabRole, QVariant::fromValue(static_cast(this))); + windows[ c ] = 0; + c->refWindow(); +} + +void FallApartEffect::slotWindowDeleted(EffectWindow* c) +{ + windows.remove(c); +} + +void FallApartEffect::slotWindowDataChanged(EffectWindow* w, int role) +{ + if (role != WindowClosedGrabRole) { + return; + } + + if (w->data(role).value() == this) { + return; + } + + auto it = windows.find(w); + if (it == windows.end()) { + return; + } + + it.key()->unrefWindow(); + windows.erase(it); +} + +bool FallApartEffect::isActive() const +{ + return !windows.isEmpty(); +} + +} // namespace diff --git a/effects/fallapart/fallapart.h b/effects/fallapart/fallapart.h new file mode 100644 index 0000000..9fed03b --- /dev/null +++ b/effects/fallapart/fallapart.h @@ -0,0 +1,56 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_FALLAPART_H +#define KWIN_FALLAPART_H + +#include + +namespace KWin +{ + +class FallApartEffect + : public Effect +{ + Q_OBJECT + Q_PROPERTY(int blockSize READ configuredBlockSize) +public: + FallApartEffect(); + void reconfigure(ReconfigureFlags) override; + void prePaintScreen(ScreenPrePaintData& data, int time) override; + void prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time) override; + void paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data) override; + void postPaintScreen() override; + bool isActive() const override; + + int requestedEffectChainPosition() const override { + return 70; + } + + // for properties + int configuredBlockSize() const { + return blockSize; + } + + static bool supported(); + +public Q_SLOTS: + void slotWindowClosed(KWin::EffectWindow *c); + void slotWindowDeleted(KWin::EffectWindow *w); + void slotWindowDataChanged(KWin::EffectWindow *w, int role); + +private: + QHash< EffectWindow*, double > windows; + bool isRealWindow(EffectWindow* w); + int blockSize; +}; + +} // namespace + +#endif diff --git a/effects/fallapart/fallapart.kcfg b/effects/fallapart/fallapart.kcfg new file mode 100644 index 0000000..57221a1 --- /dev/null +++ b/effects/fallapart/fallapart.kcfg @@ -0,0 +1,14 @@ + + + + + + 40 + 1 + 100000 + + + diff --git a/effects/fallapart/fallapartconfig.kcfgc b/effects/fallapart/fallapartconfig.kcfgc new file mode 100644 index 0000000..1735fb7 --- /dev/null +++ b/effects/fallapart/fallapartconfig.kcfgc @@ -0,0 +1,5 @@ +File=fallapart.kcfg +ClassName=FallApartConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/effects/flipswitch/CMakeLists.txt b/effects/flipswitch/CMakeLists.txt new file mode 100644 index 0000000..6829ea5 --- /dev/null +++ b/effects/flipswitch/CMakeLists.txt @@ -0,0 +1,24 @@ +####################################### +# Config +set(kwin_flipswitch_config_SRCS flipswitch_config.cpp) +ki18n_wrap_ui(kwin_flipswitch_config_SRCS flipswitch_config.ui) +kconfig_add_kcfg_files(kwin_flipswitch_config_SRCS flipswitchconfig.kcfgc) + +add_library(kwin_flipswitch_config MODULE ${kwin_flipswitch_config_SRCS}) + +target_link_libraries(kwin_flipswitch_config + KF5::ConfigWidgets + KF5::GlobalAccel + KF5::I18n + KF5::XmlGui + KWinEffectsInterface +) + +kcoreaddons_desktop_to_json(kwin_flipswitch_config flipswitch_config.desktop SERVICE_TYPES kcmodule.desktop) + +install( + TARGETS + kwin_flipswitch_config + DESTINATION + ${PLUGIN_INSTALL_DIR}/kwin/effects/configs +) diff --git a/effects/flipswitch/flipswitch.cpp b/effects/flipswitch/flipswitch.cpp new file mode 100644 index 0000000..15e6ccd --- /dev/null +++ b/effects/flipswitch/flipswitch.cpp @@ -0,0 +1,979 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008, 2009 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "flipswitch.h" +// KConfigSkeleton +#include "flipswitchconfig.h" + +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include + +namespace KWin +{ + +FlipSwitchEffect::FlipSwitchEffect() + : m_selectedWindow(nullptr) + , m_currentAnimationEasingCurve(QEasingCurve::InOutSine) + , m_active(false) + , m_start(false) + , m_stop(false) + , m_animation(false) + , m_hasKeyboardGrab(false) + , m_captionFrame(nullptr) +{ + initConfig(); + reconfigure(ReconfigureAll); + + // Caption frame + m_captionFont.setBold(true); + m_captionFont.setPointSize(m_captionFont.pointSize() * 2); + + QAction* flipSwitchCurrentAction = new QAction(this); + flipSwitchCurrentAction->setObjectName(QStringLiteral("FlipSwitchCurrent")); + flipSwitchCurrentAction->setText(i18n("Toggle Flip Switch (Current desktop)")); + KGlobalAccel::self()->setShortcut(flipSwitchCurrentAction, QList()); + m_shortcutCurrent = KGlobalAccel::self()->shortcut(flipSwitchCurrentAction); + effects->registerGlobalShortcut(QKeySequence(), flipSwitchCurrentAction); + connect(flipSwitchCurrentAction, &QAction::triggered, this, &FlipSwitchEffect::toggleActiveCurrent); + QAction* flipSwitchAllAction = new QAction(this); + flipSwitchAllAction->setObjectName(QStringLiteral("FlipSwitchAll")); + flipSwitchAllAction->setText(i18n("Toggle Flip Switch (All desktops)")); + KGlobalAccel::self()->setShortcut(flipSwitchAllAction, QList()); + effects->registerGlobalShortcut(QKeySequence(), flipSwitchAllAction); + m_shortcutAll = KGlobalAccel::self()->shortcut(flipSwitchAllAction); + connect(flipSwitchAllAction, &QAction::triggered, this, &FlipSwitchEffect::toggleActiveAllDesktops); + connect(KGlobalAccel::self(), &KGlobalAccel::globalShortcutChanged, this, &FlipSwitchEffect::globalShortcutChanged); + connect(effects, &EffectsHandler::windowAdded, this, &FlipSwitchEffect::slotWindowAdded); + connect(effects, &EffectsHandler::windowClosed, this, &FlipSwitchEffect::slotWindowClosed); + connect(effects, &EffectsHandler::tabBoxAdded, this, &FlipSwitchEffect::slotTabBoxAdded); + connect(effects, &EffectsHandler::tabBoxClosed, this, &FlipSwitchEffect::slotTabBoxClosed); + connect(effects, &EffectsHandler::tabBoxUpdated, this, &FlipSwitchEffect::slotTabBoxUpdated); + connect(effects, &EffectsHandler::tabBoxKeyEvent, this, &FlipSwitchEffect::slotTabBoxKeyEvent); + connect(effects, &EffectsHandler::screenAboutToLock, this, [this]() { + setActive(false, AllDesktopsMode); + setActive(false, CurrentDesktopMode); + }); +} + +FlipSwitchEffect::~FlipSwitchEffect() +{ + delete m_captionFrame; +} + +bool FlipSwitchEffect::supported() +{ + return effects->isOpenGLCompositing() && effects->animationsSupported(); +} + +void FlipSwitchEffect::reconfigure(ReconfigureFlags) +{ + FlipSwitchConfig::self()->read(); + m_tabbox = FlipSwitchConfig::tabBox(); + m_tabboxAlternative = FlipSwitchConfig::tabBoxAlternative(); + const int duration = animationTime(200); + m_timeLine.setDuration(duration); + m_startStopTimeLine.setDuration(duration); + + m_angle = FlipSwitchConfig::angle(); + m_xPosition = FlipSwitchConfig::xPosition() / 100.0f; + m_yPosition = FlipSwitchConfig::yPosition() / 100.0f; + m_windowTitle = FlipSwitchConfig::windowTitle(); +} + +void FlipSwitchEffect::prePaintScreen(ScreenPrePaintData& data, int time) +{ + if (m_active) { + data.mask |= PAINT_SCREEN_WITH_TRANSFORMED_WINDOWS; + if (m_start) + m_startStopTimeLine.setCurrentTime(m_startStopTimeLine.currentTime() + time); + if (m_stop && m_scheduledDirections.isEmpty()) + m_startStopTimeLine.setCurrentTime(m_startStopTimeLine.currentTime() - time); + if (m_animation) + m_timeLine.setCurrentTime(m_timeLine.currentTime() + time); + } + effects->prePaintScreen(data, time); +} + +void FlipSwitchEffect::paintScreen(int mask, const QRegion ®ion, ScreenPaintData& data) +{ + effects->paintScreen(mask, region, data); + if (m_active) { + EffectWindowList tempList; + if (m_mode == TabboxMode) + tempList = effects->currentTabBoxWindowList(); + else { + // we have to setup the list + // using stacking order directly is not possible + // as not each window in stacking order is shown + // TODO: store list instead of calculating in each frame? + foreach (EffectWindow * w, effects->stackingOrder()) { + if (m_windows.contains(w)) + tempList.append(w); + } + } + m_flipOrderedWindows.clear(); + int index = tempList.indexOf(m_selectedWindow); + + int tabIndex = index; + if (m_mode == TabboxMode) { + foreach (SwitchingDirection direction, m_scheduledDirections) { // krazy:exclude=foreach + if (direction == DirectionBackward) + index++; + else + index--; + if (index < 0) + index = tempList.count() + index; + if (index >= tempList.count()) + index = index % tempList.count(); + } + tabIndex = index; + EffectWindow* w = nullptr; + if (!m_scheduledDirections.isEmpty() && m_scheduledDirections.head() == DirectionBackward) { + index--; + if (index < 0) + index = tempList.count() + index; + w = tempList.at(index); + } + for (int i = index - 1; i >= 0; i--) + m_flipOrderedWindows.append(tempList.at(i)); + for (int i = effects->currentTabBoxWindowList().count() - 1; i >= index; i--) + m_flipOrderedWindows.append(tempList.at(i)); + if (w) { + m_flipOrderedWindows.removeAll(w); + m_flipOrderedWindows.append(w); + } + } else { + foreach (SwitchingDirection direction, m_scheduledDirections) { // krazy:exclude=foreach + if (direction == DirectionForward) + index++; + else + index--; + if (index < 0) + index = tempList.count() - 1; + if (index >= tempList.count()) + index = 0; + } + tabIndex = index; + EffectWindow* w = nullptr; + if (!m_scheduledDirections.isEmpty() && m_scheduledDirections.head() == DirectionBackward) { + index++; + if (index >= tempList.count()) + index = 0; + } + // sort from stacking order + for (int i = index + 1; i < tempList.count(); i++) + m_flipOrderedWindows.append(tempList.at(i)); + for (int i = 0; i <= index; i++) + m_flipOrderedWindows.append(tempList.at(i)); + if (w) { + m_flipOrderedWindows.removeAll(w); + m_flipOrderedWindows.append(w); + } + } + + + int winMask = PAINT_WINDOW_TRANSFORMED | PAINT_WINDOW_TRANSLUCENT; + // fade in/out one window at the end of the stack during animation + if (m_animation && !m_scheduledDirections.isEmpty()) { + EffectWindow* w = m_flipOrderedWindows.last(); + if (ItemInfo *info = m_windows.value(w,0)) { + WindowPaintData data(w); + if (effects->numScreens() > 1) { + data.setProjectionMatrix(m_projectionMatrix); + data.setModelViewMatrix(m_modelviewMatrix); + } + data.setRotationAxis(Qt::YAxis); + data.setRotationAngle(m_angle * m_startStopTimeLine.currentValue()); + data.setOpacity(info->opacity); + data.setBrightness(info->brightness); + data.setSaturation(info->saturation); + int distance = tempList.count() - 1; + float zDistance = 500.0f; + data.translate(- (w->x() - m_screenArea.x() + data.xTranslation()) * m_startStopTimeLine.currentValue()); + + data.translate(m_screenArea.width() * m_xPosition * m_startStopTimeLine.currentValue(), + (m_screenArea.y() + m_screenArea.height() * m_yPosition - (w->y() + w->height() + data.yTranslation())) * m_startStopTimeLine.currentValue()); + data.translate(- (m_screenArea.width() * 0.25f) * distance * m_startStopTimeLine.currentValue(), + - (m_screenArea.height() * 0.10f) * distance * m_startStopTimeLine.currentValue(), + - (zDistance * distance) * m_startStopTimeLine.currentValue()); + if (m_scheduledDirections.head() == DirectionForward) + data.multiplyOpacity(0.8 * m_timeLine.currentValue()); + else + data.multiplyOpacity(0.8 * (1.0 - m_timeLine.currentValue())); + + if (effects->numScreens() > 1) { + adjustWindowMultiScreen(w, data); + } + effects->drawWindow(w, winMask, infiniteRegion(), data); + } + } + + foreach (EffectWindow *w, m_flipOrderedWindows) { + ItemInfo *info = m_windows.value(w,0); + if (!info) + continue; + WindowPaintData data(w); + if (effects->numScreens() > 1) { + data.setProjectionMatrix(m_projectionMatrix); + data.setModelViewMatrix(m_modelviewMatrix); + } + data.setRotationAxis(Qt::YAxis); + data.setRotationAngle(m_angle * m_startStopTimeLine.currentValue()); + data.setOpacity(info->opacity); + data.setBrightness(info->brightness); + data.setSaturation(info->saturation); + int windowIndex = tempList.indexOf(w); + int distance; + if (m_mode == TabboxMode) { + if (windowIndex < tabIndex) + distance = tempList.count() - (tabIndex - windowIndex); + else if (windowIndex > tabIndex) + distance = windowIndex - tabIndex; + else + distance = 0; + } else { + distance = m_flipOrderedWindows.count() - m_flipOrderedWindows.indexOf(w) - 1; + + if (!m_scheduledDirections.isEmpty() && m_scheduledDirections.head() == DirectionBackward) { + distance--; + } + } + if (!m_scheduledDirections.isEmpty() && m_scheduledDirections.head() == DirectionBackward) { + if (w == m_flipOrderedWindows.last()) { + distance = -1; + data.multiplyOpacity(m_timeLine.currentValue()); + } + } + float zDistance = 500.0f; + data.translate(- (w->x() - m_screenArea.x() + data.xTranslation()) * m_startStopTimeLine.currentValue()); + data.translate(m_screenArea.width() * m_xPosition * m_startStopTimeLine.currentValue(), + (m_screenArea.y() + m_screenArea.height() * m_yPosition - (w->y() + w->height() + data.yTranslation())) * m_startStopTimeLine.currentValue()); + + data.translate(-(m_screenArea.width() * 0.25f) * distance * m_startStopTimeLine.currentValue(), + -(m_screenArea.height() * 0.10f) * distance * m_startStopTimeLine.currentValue(), + -(zDistance * distance) * m_startStopTimeLine.currentValue()); + if (m_animation && !m_scheduledDirections.isEmpty()) { + if (m_scheduledDirections.head() == DirectionForward) { + data.translate((m_screenArea.width() * 0.25f) * m_timeLine.currentValue(), + (m_screenArea.height() * 0.10f) * m_timeLine.currentValue(), + zDistance * m_timeLine.currentValue()); + if (distance == 0) + data.multiplyOpacity((1.0 - m_timeLine.currentValue())); + } else { + data.translate(- (m_screenArea.width() * 0.25f) * m_timeLine.currentValue(), + - (m_screenArea.height() * 0.10f) * m_timeLine.currentValue(), + - zDistance * m_timeLine.currentValue()); + } + } + data.multiplyOpacity((0.8 + 0.2 * (1.0 - m_startStopTimeLine.currentValue()))); + if (effects->numScreens() > 1) { + adjustWindowMultiScreen(w, data); + } + + effects->drawWindow(w, winMask, infiniteRegion(), data); + } + + if (m_windowTitle) { + // Render the caption frame + if (m_animation) { + m_captionFrame->setCrossFadeProgress(m_timeLine.currentValue()); + } + m_captionFrame->render(region, m_startStopTimeLine.currentValue()); + } + } +} + +void FlipSwitchEffect::postPaintScreen() +{ + if (m_active) { + if (m_start && m_startStopTimeLine.currentValue() == 1.0f) { + m_start = false; + if (!m_scheduledDirections.isEmpty()) { + m_animation = true; + m_timeLine.setCurrentTime(0); + if (m_scheduledDirections.count() == 1) { + m_currentAnimationEasingCurve = QEasingCurve::OutSine; + m_timeLine.setEasingCurve(m_currentAnimationEasingCurve); + } else { + m_currentAnimationEasingCurve = QEasingCurve::Linear; + m_timeLine.setEasingCurve(m_currentAnimationEasingCurve); + } + } + effects->addRepaintFull(); + } + if (m_stop && m_startStopTimeLine.currentValue() == 0.0f) { + m_stop = false; + m_active = false; + m_captionFrame->free(); + effects->setActiveFullScreenEffect(nullptr); + effects->addRepaintFull(); + qDeleteAll(m_windows); + m_windows.clear(); + } + if (m_animation && m_timeLine.currentValue() == 1.0f) { + m_timeLine.setCurrentTime(0); + m_scheduledDirections.dequeue(); + if (m_scheduledDirections.isEmpty()) { + m_animation = false; + effects->addRepaintFull(); + } else { + if (m_scheduledDirections.count() == 1) { + if (m_stop) + m_currentAnimationEasingCurve = QEasingCurve::Linear; + else + m_currentAnimationEasingCurve = QEasingCurve::OutSine; + } else { + m_currentAnimationEasingCurve = QEasingCurve::Linear; + } + m_timeLine.setEasingCurve(m_currentAnimationEasingCurve); + } + } + if (m_start || m_stop || m_animation) + effects->addRepaintFull(); + } + effects->postPaintScreen(); +} + +void FlipSwitchEffect::prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time) +{ + if (m_active) { + if (m_windows.contains(w)) { + data.setTransformed(); + data.setTranslucent(); + if (!w->isOnCurrentDesktop()) + w->enablePainting(EffectWindow::PAINT_DISABLED_BY_DESKTOP); + if (w->isMinimized()) + w->enablePainting(EffectWindow::PAINT_DISABLED_BY_MINIMIZE); + } else { + if ((m_start || m_stop) && !w->isDesktop() && w->isOnCurrentDesktop()) + data.setTranslucent(); + else if (!w->isDesktop()) + w->disablePainting(EffectWindow::PAINT_DISABLED_BY_DESKTOP); + } + } + effects->prePaintWindow(w, data, time); +} + +void FlipSwitchEffect::paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data) +{ + if (m_active) { + ItemInfo *info = m_windows.value(w,0); + if (info) { + info->opacity = data.opacity(); + info->brightness = data.brightness(); + info->saturation = data.saturation(); + } + + // fade out all windows not in window list except the desktops + const bool isFader = (m_start || m_stop) && !info && !w->isDesktop(); + if (isFader) + data.multiplyOpacity((1.0 - m_startStopTimeLine.currentValue())); + + // if not a fader or the desktop, skip painting here to prevent flicker + if (!(isFader || w->isDesktop())) + return; + } + effects->paintWindow(w, mask, region, data); +} + +//************************************************************* +// Tabbox handling +//************************************************************* +void FlipSwitchEffect::slotTabBoxAdded(int mode) +{ + if (effects->activeFullScreenEffect() && effects->activeFullScreenEffect() != this) + return; + // only for windows mode + effects->setShowingDesktop(false); + if (((mode == TabBoxWindowsMode && m_tabbox) || + (mode == TabBoxWindowsAlternativeMode && m_tabboxAlternative) || + (mode == TabBoxCurrentAppWindowsMode && m_tabbox) || + (mode == TabBoxCurrentAppWindowsAlternativeMode && m_tabboxAlternative)) + && (!m_active || (m_active && m_stop)) + && !effects->currentTabBoxWindowList().isEmpty()) { + setActive(true, TabboxMode); + if (m_active) + effects->refTabBox(); + } +} + +void FlipSwitchEffect::slotTabBoxClosed() +{ + if (m_active) { + setActive(false, TabboxMode); + effects->unrefTabBox(); + } +} + +void FlipSwitchEffect::slotTabBoxUpdated() +{ + if (m_active && !m_stop) { + if (!effects->currentTabBoxWindowList().isEmpty()) { + // determine the switch direction + if (m_selectedWindow != effects->currentTabBoxWindow()) { + if (m_selectedWindow != nullptr) { + int old_index = effects->currentTabBoxWindowList().indexOf(m_selectedWindow); + int new_index = effects->currentTabBoxWindowList().indexOf(effects->currentTabBoxWindow()); + SwitchingDirection new_direction; + int distance = new_index - old_index; + if (distance > 0) + new_direction = DirectionForward; + if (distance < 0) + new_direction = DirectionBackward; + if (effects->currentTabBoxWindowList().count() == 2) { + new_direction = DirectionForward; + distance = 1; + } + if (distance != 0) { + distance = abs(distance); + int tempDistance = effects->currentTabBoxWindowList().count() - distance; + if (tempDistance < abs(distance)) { + distance = tempDistance; + if (new_direction == DirectionForward) + new_direction = DirectionBackward; + else + new_direction = DirectionForward; + } + scheduleAnimation(new_direction, distance); + } + } + m_selectedWindow = effects->currentTabBoxWindow(); + updateCaption(); + } + } + effects->addRepaintFull(); + } +} + +//************************************************************* +// Window adding/removing handling +//************************************************************* + +void FlipSwitchEffect::slotWindowAdded(EffectWindow* w) +{ + if (m_active && isSelectableWindow(w)) { + m_windows[ w ] = new ItemInfo; + } +} + +void FlipSwitchEffect::slotWindowClosed(EffectWindow* w) +{ + if (m_selectedWindow == w) + m_selectedWindow = nullptr; + if (m_active) { + QHash< const EffectWindow*, ItemInfo* >::iterator it = m_windows.find(w); + if (it != m_windows.end()) { + delete *it; + m_windows.erase(it); + } + } +} + +//************************************************************* +// Activation +//************************************************************* + +void FlipSwitchEffect::setActive(bool activate, FlipSwitchMode mode) +{ + if (activate) { + // effect already active, do some sanity checks + if (m_active) { + if (m_stop) { + if (mode != m_mode) { + // only the same mode may reactivate the effect + return; + } + } else { + // active, but not scheduled for closing -> abort + return; + } + } + + m_mode = mode; + foreach (EffectWindow * w, effects->stackingOrder()) { + if (isSelectableWindow(w) && !m_windows.contains(w)) + m_windows[ w ] = new ItemInfo; + } + if (m_windows.isEmpty()) + return; + + effects->setActiveFullScreenEffect(this); + m_active = true; + m_start = true; + m_startStopTimeLine.setEasingCurve(QEasingCurve::InOutSine); + m_activeScreen = effects->activeScreen(); + m_screenArea = effects->clientArea(ScreenArea, m_activeScreen, effects->currentDesktop()); + + if (effects->numScreens() > 1) { + // unfortunatelly we have to change the projection matrix in dual screen mode + // code is copied from Coverswitch + QRect fullRect = effects->clientArea(FullArea, m_activeScreen, effects->currentDesktop()); + float fovy = 60.0f; + float aspect = 1.0f; + float zNear = 0.1f; + float zFar = 100.0f; + + float ymax = zNear * std::tan(fovy * M_PI / 360.0f); + float ymin = -ymax; + float xmin = ymin * aspect; + float xmax = ymax * aspect; + + if (m_screenArea.width() != fullRect.width()) { + if (m_screenArea.x() == 0) { + // horizontal layout: left screen + xmin *= (float)m_screenArea.width() / (float)fullRect.width(); + xmax *= (fullRect.width() - 0.5f * m_screenArea.width()) / (0.5f * fullRect.width()); + } else { + // horizontal layout: right screen + xmin *= (fullRect.width() - 0.5f * m_screenArea.width()) / (0.5f * fullRect.width()); + xmax *= (float)m_screenArea.width() / (float)fullRect.width(); + } + } + if (m_screenArea.height() != fullRect.height()) { + if (m_screenArea.y() == 0) { + // vertical layout: top screen + ymin *= (fullRect.height() - 0.5f * m_screenArea.height()) / (0.5f * fullRect.height()); + ymax *= (float)m_screenArea.height() / (float)fullRect.height(); + } else { + // vertical layout: bottom screen + ymin *= (float)m_screenArea.height() / (float)fullRect.height(); + ymax *= (fullRect.height() - 0.5f * m_screenArea.height()) / (0.5f * fullRect.height()); + } + } + + m_projectionMatrix = QMatrix4x4(); + m_projectionMatrix.frustum(xmin, xmax, ymin, ymax, zNear, zFar); + + const float scaleFactor = 1.1f / zNear; + + // Create a second matrix that transforms screen coordinates + // to world coordinates. + QMatrix4x4 matrix; + matrix.translate(xmin * scaleFactor, ymax * scaleFactor, -1.1); + matrix.scale( (xmax - xmin) * scaleFactor / fullRect.width(), + -(ymax - ymin) * scaleFactor / fullRect.height(), + 0.001); + // Combine the matrices + m_projectionMatrix *= matrix; + + m_modelviewMatrix = QMatrix4x4(); + m_modelviewMatrix.translate(m_screenArea.x(), m_screenArea.y(), 0.0); + } + + if (m_stop) { + // effect is still closing from last usage + m_stop = false; + } else { + // things to do only when there is no closing animation + m_scheduledDirections.clear(); + } + + switch(m_mode) { + case TabboxMode: + m_selectedWindow = effects->currentTabBoxWindow(); + effects->startMouseInterception(this, Qt::ArrowCursor); + break; + case CurrentDesktopMode: + m_selectedWindow = effects->activeWindow(); + effects->startMouseInterception(this, Qt::BlankCursor); + m_hasKeyboardGrab = effects->grabKeyboard(this); + break; + case AllDesktopsMode: + m_selectedWindow = effects->activeWindow(); + effects->startMouseInterception(this, Qt::BlankCursor); + m_hasKeyboardGrab = effects->grabKeyboard(this); + break; + } + + // Setup caption frame geometry + QRect frameRect = QRect(m_screenArea.width() * 0.25f + m_screenArea.x(), + m_screenArea.height() * 0.1f + m_screenArea.y() - QFontMetrics(m_captionFont).height(), + m_screenArea.width() * 0.5f, + QFontMetrics(m_captionFont).height()); + if (!m_captionFrame) { + m_captionFrame = effects->effectFrame(EffectFrameStyled); + m_captionFrame->setFont(m_captionFont); + m_captionFrame->enableCrossFade(true); + } + m_captionFrame->setGeometry(frameRect); + m_captionFrame->setIconSize(QSize(frameRect.height(), frameRect.height())); + updateCaption(); + effects->addRepaintFull(); + } else { + // only deactivate if mode is current mode + if (mode != m_mode) + return; + if (m_start && m_scheduledDirections.isEmpty()) { + m_start = false; + } + m_stop = true; + if (m_animation) { + m_startStopTimeLine.setEasingCurve(QEasingCurve::OutSine); + if (m_scheduledDirections.count() == 1) { + if (m_currentAnimationEasingCurve == QEasingCurve::InOutSine) + m_currentAnimationEasingCurve = QEasingCurve::InSine; + else if (m_currentAnimationEasingCurve == QEasingCurve::OutSine) + m_currentAnimationEasingCurve = QEasingCurve::Linear; + m_timeLine.setEasingCurve(m_currentAnimationEasingCurve); + } + } else + m_startStopTimeLine.setEasingCurve(QEasingCurve::InOutSine); + effects->stopMouseInterception(this); + if (m_hasKeyboardGrab) { + effects->ungrabKeyboard(); + m_hasKeyboardGrab = false; + } + effects->addRepaintFull(); + } +} + +void FlipSwitchEffect::toggleActiveAllDesktops() +{ + if (m_active) { + if (m_stop) { + // reactivate if stopping + setActive(true, AllDesktopsMode); + } else { + // deactivate if not stopping + setActive(false, AllDesktopsMode); + } + } else { + setActive(true, AllDesktopsMode); + } +} + +void FlipSwitchEffect::toggleActiveCurrent() +{ + if (m_active) { + if (m_stop) { + // reactivate if stopping + setActive(true, CurrentDesktopMode); + } else { + // deactivate if not stopping + setActive(false, CurrentDesktopMode); + } + } else { + setActive(true, CurrentDesktopMode); + } +} + +//************************************************************* +// Helper function +//************************************************************* + +bool FlipSwitchEffect::isSelectableWindow(EffectWindow* w) const +{ + // desktop windows might be included + if ((w->isSpecialWindow() && !w->isDesktop()) || w->isUtility()) + return false; + if (w->isDesktop()) + return (m_mode == TabboxMode && effects->currentTabBoxWindowList().contains(w)); + if (w->isDeleted()) + return false; + if (!w->acceptsFocus()) + return false; + switch(m_mode) { + case TabboxMode: + return effects->currentTabBoxWindowList().contains(w); + case CurrentDesktopMode: + return w->isOnCurrentDesktop(); + case AllDesktopsMode: + //nothing special + break; + } + return true; +} + +void FlipSwitchEffect::scheduleAnimation(const SwitchingDirection& direction, int distance) +{ + if (m_start) { + // start is still active so change the shape to have a nice transition + m_startStopTimeLine.setEasingCurve(QEasingCurve::InSine); + } + if (!m_animation && !m_start) { + m_animation = true; + m_scheduledDirections.enqueue(direction); + distance--; + // reset shape just to make sure + m_currentAnimationEasingCurve = QEasingCurve::InOutSine; + m_timeLine.setEasingCurve(m_currentAnimationEasingCurve); + } + for (int i = 0; i < distance; i++) { + if (m_scheduledDirections.count() > 1 && m_scheduledDirections.last() != direction) + m_scheduledDirections.pop_back(); + else + m_scheduledDirections.enqueue(direction); + if (m_scheduledDirections.count() == m_windows.count() + 1) { + SwitchingDirection temp = m_scheduledDirections.dequeue(); + m_scheduledDirections.clear(); + m_scheduledDirections.enqueue(temp); + } + } + if (m_scheduledDirections.count() > 1) { + QEasingCurve curve; + switch (m_currentAnimationEasingCurve.type()) { + case QEasingCurve::InOutSine: + curve = QEasingCurve::InSine; + break; + case QEasingCurve::OutSine: + curve = QEasingCurve::Linear; + break; + default: + curve = m_currentAnimationEasingCurve; + } + if (m_currentAnimationEasingCurve != curve) { + m_currentAnimationEasingCurve = curve; + m_timeLine.setEasingCurve(curve); + } + } +} + +void FlipSwitchEffect::adjustWindowMultiScreen(const KWin::EffectWindow* w, WindowPaintData& data) +{ + if (effects->numScreens() <= 1) + return; + QRect clientRect = effects->clientArea(FullScreenArea, w->screen(), effects->currentDesktop()); + QRect rect = effects->clientArea(ScreenArea, m_activeScreen, effects->currentDesktop()); + QRect fullRect = effects->clientArea(FullArea, m_activeScreen, effects->currentDesktop()); + if (w->screen() == m_activeScreen) { + if (clientRect.width() != fullRect.width() && clientRect.x() != fullRect.x()) { + data.translate(- clientRect.x()); + } + if (clientRect.height() != fullRect.height() && clientRect.y() != fullRect.y()) { + data.translate(0.0, - clientRect.y()); + } + } else { + if (clientRect.width() != fullRect.width() && clientRect.x() < rect.x()) { + data.translate(- (m_screenArea.x() - clientRect.x())); + } + if (clientRect.height() != fullRect.height() && clientRect.y() < m_screenArea.y()) { + data.translate(0.0, - (m_screenArea.y() - clientRect.y())); + } + } +} + +void FlipSwitchEffect::selectNextOrPreviousWindow(bool forward) +{ + if (!m_active || !m_selectedWindow) { + return; + } + const int index = effects->currentTabBoxWindowList().indexOf(m_selectedWindow); + int newIndex = index; + if (forward) { + ++newIndex; + } else { + --newIndex; + } + if (newIndex == effects->currentTabBoxWindowList().size()) { + newIndex = 0; + } else if (newIndex < 0) { + newIndex = effects->currentTabBoxWindowList().size() -1; + } + if (index == newIndex) { + return; + } + effects->setTabBoxWindow(effects->currentTabBoxWindowList().at(newIndex)); +} + + +//************************************************************* +// Keyboard handling +//************************************************************* +void FlipSwitchEffect::globalShortcutChanged(QAction *action, const QKeySequence &shortcut) +{ + if (action->objectName() == QStringLiteral("FlipSwitchAll")) { + m_shortcutAll.clear(); + m_shortcutAll.append(shortcut); + } else if (action->objectName() == QStringLiteral("FlipSwitchCurrent")) { + m_shortcutCurrent.clear(); + m_shortcutCurrent.append(shortcut); + } +} + +void FlipSwitchEffect::grabbedKeyboardEvent(QKeyEvent* e) +{ + if (e->type() == QEvent::KeyPress) { + // check for global shortcuts + // HACK: keyboard grab disables the global shortcuts so we have to check for global shortcut (bug 156155) + if (m_mode == CurrentDesktopMode && m_shortcutCurrent.contains(e->key() + e->modifiers())) { + toggleActiveCurrent(); + return; + } + if (m_mode == AllDesktopsMode && m_shortcutAll.contains(e->key() + e->modifiers())) { + toggleActiveAllDesktops(); + return; + } + + switch(e->key()) { + case Qt::Key_Escape: + setActive(false, m_mode); + return; + case Qt::Key_Tab: { + // find next window + if (m_windows.isEmpty()) + return; // sanity check + bool found = false; + for (int i = effects->stackingOrder().indexOf(m_selectedWindow) - 1; i >= 0; i--) { + if (isSelectableWindow(effects->stackingOrder().at(i))) { + m_selectedWindow = effects->stackingOrder().at(i); + found = true; + break; + } + } + if (!found) { + for (int i = effects->stackingOrder().count() - 1; i > effects->stackingOrder().indexOf(m_selectedWindow); i--) { + if (isSelectableWindow(effects->stackingOrder().at(i))) { + m_selectedWindow = effects->stackingOrder().at(i); + found = true; + break; + } + } + } + if (found) { + updateCaption(); + scheduleAnimation(DirectionForward); + } + break; + } + case Qt::Key_Backtab: { + // find previous window + if (m_windows.isEmpty()) + return; // sanity check + bool found = false; + for (int i = effects->stackingOrder().indexOf(m_selectedWindow) + 1; i < effects->stackingOrder().count(); i++) { + if (isSelectableWindow(effects->stackingOrder().at(i))) { + m_selectedWindow = effects->stackingOrder().at(i); + found = true; + break; + } + } + if (!found) { + for (int i = 0; i < effects->stackingOrder().indexOf(m_selectedWindow); i++) { + if (isSelectableWindow(effects->stackingOrder().at(i))) { + m_selectedWindow = effects->stackingOrder().at(i); + found = true; + break; + } + } + } + if (found) { + updateCaption(); + scheduleAnimation(DirectionBackward); + } + break; + } + case Qt::Key_Return: + case Qt::Key_Enter: + case Qt::Key_Space: + if (m_selectedWindow) + effects->activateWindow(m_selectedWindow); + setActive(false, m_mode); + break; + default: + break; + } + effects->addRepaintFull(); + } +} + +void FlipSwitchEffect::slotTabBoxKeyEvent(QKeyEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + switch (event->key()) { + case Qt::Key_Up: + case Qt::Key_Left: + selectPreviousWindow(); + break; + case Qt::Key_Down: + case Qt::Key_Right: + selectNextWindow(); + break; + default: + // nothing + break; + } + } +} + +bool FlipSwitchEffect::isActive() const +{ + return m_active && !effects->isScreenLocked(); +} + +void FlipSwitchEffect::updateCaption() +{ + if (!m_selectedWindow) { + return; + } + if (m_selectedWindow->isDesktop()) { + m_captionFrame->setText(i18nc("Special entry in alt+tab list for minimizing all windows", + "Show Desktop")); + static QPixmap pix = QIcon::fromTheme(QStringLiteral("user-desktop")).pixmap(m_captionFrame->iconSize()); + m_captionFrame->setIcon(pix); + } else { + m_captionFrame->setText(m_selectedWindow->caption()); + m_captionFrame->setIcon(m_selectedWindow->icon()); + } +} + +//************************************************************* +// Mouse handling +//************************************************************* + +void FlipSwitchEffect::windowInputMouseEvent(QEvent* e) +{ + if (e->type() != QEvent::MouseButtonPress) + return; + // we don't want click events during animations + if (m_animation) + return; + QMouseEvent* event = static_cast< QMouseEvent* >(e); + + switch (event->button()) { + case Qt::XButton1: // wheel up + selectPreviousWindow(); + break; + case Qt::XButton2: // wheel down + selectNextWindow(); + break; + case Qt::LeftButton: + case Qt::RightButton: + case Qt::MiddleButton: + default: + // TODO: Change window on mouse button click + break; + } +} + +//************************************************************* +// Item Info +//************************************************************* +FlipSwitchEffect::ItemInfo::ItemInfo() + : deleted(false) + , opacity(0.0) + , brightness(0.0) + , saturation(0.0) +{ +} + +FlipSwitchEffect::ItemInfo::~ItemInfo() +{ +} + +} // namespace + diff --git a/effects/flipswitch/flipswitch.h b/effects/flipswitch/flipswitch.h new file mode 100644 index 0000000..b45ec7d --- /dev/null +++ b/effects/flipswitch/flipswitch.h @@ -0,0 +1,154 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008, 2009 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_FLIPSWITCH_H +#define KWIN_FLIPSWITCH_H + +#include +#include +#include +#include +#include + +namespace KWin +{ + +class FlipSwitchEffect + : public Effect +{ + Q_OBJECT + Q_PROPERTY(bool tabBox READ isTabBox) + Q_PROPERTY(bool tabBoxAlternative READ isTabBoxAlternative) + Q_PROPERTY(int duration READ duration) + Q_PROPERTY(int angle READ angle) + Q_PROPERTY(qreal xPosition READ xPosition) + Q_PROPERTY(qreal yPosition READ yPosition) + Q_PROPERTY(bool windowTitle READ isWindowTitle) +public: + FlipSwitchEffect(); + ~FlipSwitchEffect() override; + + void reconfigure(ReconfigureFlags) override; + void prePaintScreen(ScreenPrePaintData& data, int time) override; + void paintScreen(int mask, const QRegion ®ion, ScreenPaintData& data) override; + void postPaintScreen() override; + void prePaintWindow(EffectWindow *w, WindowPrePaintData &data, int time) override; + void paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data) override; + void grabbedKeyboardEvent(QKeyEvent* e) override; + void windowInputMouseEvent(QEvent* e) override; + bool isActive() const override; + + int requestedEffectChainPosition() const override { + return 50; + } + + static bool supported(); + + // for properties + bool isTabBox() const { + return m_tabbox; + } + bool isTabBoxAlternative() const { + return m_tabboxAlternative; + } + int duration() const { + return m_timeLine.duration(); + } + int angle() const { + return m_angle; + } + qreal xPosition() const { + return m_xPosition; + } + qreal yPosition() const { + return m_yPosition; + } + bool isWindowTitle() const { + return m_windowTitle; + } +private Q_SLOTS: + void toggleActiveCurrent(); + void toggleActiveAllDesktops(); + void globalShortcutChanged(QAction *action, const QKeySequence &shortcut); + void slotWindowAdded(KWin::EffectWindow* w); + void slotWindowClosed(KWin::EffectWindow *w); + void slotTabBoxAdded(int mode); + void slotTabBoxClosed(); + void slotTabBoxUpdated(); + void slotTabBoxKeyEvent(QKeyEvent* event); + +private: + class ItemInfo; + enum SwitchingDirection { + DirectionForward, + DirectionBackward + }; + enum FlipSwitchMode { + TabboxMode, + CurrentDesktopMode, + AllDesktopsMode + }; + void setActive(bool activate, FlipSwitchMode mode); + bool isSelectableWindow(EffectWindow *w) const; + void scheduleAnimation(const SwitchingDirection& direction, int distance = 1); + void adjustWindowMultiScreen(const EffectWindow *w, WindowPaintData& data); + void selectNextOrPreviousWindow(bool forward); + inline void selectNextWindow() { selectNextOrPreviousWindow(true); } + inline void selectPreviousWindow() { selectNextOrPreviousWindow(false); } + /** + * Updates the caption of the caption frame. + * Taking care of rewording the desktop client. + * As well sets the icon for the caption frame. + */ + void updateCaption(); + QQueue< SwitchingDirection> m_scheduledDirections; + EffectWindow* m_selectedWindow; + QTimeLine m_timeLine; + QTimeLine m_startStopTimeLine; + QEasingCurve m_currentAnimationEasingCurve; + QRect m_screenArea; + int m_activeScreen; + bool m_active; + bool m_start; + bool m_stop; + bool m_animation; + bool m_hasKeyboardGrab; + FlipSwitchMode m_mode; + EffectFrame* m_captionFrame; + QFont m_captionFont; + EffectWindowList m_flipOrderedWindows; + QHash< const EffectWindow*, ItemInfo* > m_windows; + QMatrix4x4 m_projectionMatrix; + QMatrix4x4 m_modelviewMatrix; + // options + bool m_tabbox; + bool m_tabboxAlternative; + float m_angle; + float m_xPosition; + float m_yPosition; + bool m_windowTitle; + // Shortcuts + QList m_shortcutCurrent; + QList m_shortcutAll; +}; + +class FlipSwitchEffect::ItemInfo +{ +public: + ItemInfo(); + ~ItemInfo(); + bool deleted; + double opacity; + double brightness; + double saturation; +}; + +} // namespace + +#endif diff --git a/effects/flipswitch/flipswitch.kcfg b/effects/flipswitch/flipswitch.kcfg new file mode 100644 index 0000000..37c2e75 --- /dev/null +++ b/effects/flipswitch/flipswitch.kcfg @@ -0,0 +1,30 @@ + + + + + + false + + + false + + + 0 + + + 30 + + + 33 + + + 100 + + + true + + + diff --git a/effects/flipswitch/flipswitch_config.cpp b/effects/flipswitch/flipswitch_config.cpp new file mode 100644 index 0000000..149ba73 --- /dev/null +++ b/effects/flipswitch/flipswitch_config.cpp @@ -0,0 +1,86 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008, 2009 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "flipswitch_config.h" +// KConfigSkeleton +#include "flipswitchconfig.h" +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include + +K_PLUGIN_FACTORY_WITH_JSON(FlipSwitchEffectConfigFactory, + "flipswitch_config.json", + registerPlugin();) + +namespace KWin +{ + +FlipSwitchEffectConfigForm::FlipSwitchEffectConfigForm(QWidget* parent) : QWidget(parent) +{ + setupUi(this); +} + +FlipSwitchEffectConfig::FlipSwitchEffectConfig(QWidget* parent, const QVariantList& args) : + KCModule(KAboutData::pluginData(QStringLiteral("flipswitch")), parent, args) +{ + m_ui = new FlipSwitchEffectConfigForm(this); + + QVBoxLayout* layout = new QVBoxLayout(this); + + layout->addWidget(m_ui); + + // Shortcut config. The shortcut belongs to the component "kwin"! + m_actionCollection = new KActionCollection(this, QStringLiteral("kwin")); + QAction* a = m_actionCollection->addAction(QStringLiteral("FlipSwitchCurrent")); + a->setText(i18n("Toggle Flip Switch (Current desktop)")); + KGlobalAccel::self()->setShortcut(a, QList()); + QAction* b = m_actionCollection->addAction(QStringLiteral("FlipSwitchAll")); + b->setText(i18n("Toggle Flip Switch (All desktops)")); + KGlobalAccel::self()->setShortcut(b, QList()); + + m_actionCollection->setComponentDisplayName(i18n("KWin")); + m_actionCollection->setConfigGroup(QStringLiteral("FlipSwitch")); + m_actionCollection->setConfigGlobal(true); + + m_ui->shortcutEditor->addCollection(m_actionCollection); + + FlipSwitchConfig::instance(KWIN_CONFIG); + addConfig(FlipSwitchConfig::self(), m_ui); + + load(); +} + +FlipSwitchEffectConfig::~FlipSwitchEffectConfig() +{ +} + +void FlipSwitchEffectConfig::save() +{ + KCModule::save(); + m_ui->shortcutEditor->save(); + + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("flipswitch")); +} + + +} // namespace + +#include "flipswitch_config.moc" diff --git a/effects/flipswitch/flipswitch_config.desktop b/effects/flipswitch/flipswitch_config.desktop new file mode 100644 index 0000000..10e8b37 --- /dev/null +++ b/effects/flipswitch/flipswitch_config.desktop @@ -0,0 +1,77 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=KCModule + +X-KDE-Library=kwin_flipswitch_config +X-KDE-ParentComponents=flipswitch + +Name=Flip Switch +Name[af]=Wissel skakelaar +Name[ar]=تبديل بالتقليب +Name[az]=Səhifələmə +Name[bs]=Preklopno prebacivanje +Name[ca]=Canvi en roda +Name[ca@valencia]=Canvi en roda +Name[cs]=Kartotéka +Name[da]=Vippeskifter +Name[de]=3D-Fensterstapel +Name[el]=Εναλλαγή στοίβας +Name[en_GB]=Flip Switch +Name[eo]=Turna ŝanĝilo +Name[es]=Selección de ventana en modo cascada +Name[et]=Aknalehitseja +Name[eu]=Biratze aldaketa +Name[fi]=Kääntövaihtaja +Name[fr]=Empilement en perspective +Name[fy]=Flip wiksel +Name[ga]=Flip Switch +Name[gl]=Cambio en fila +Name[gu]=ફ્લિપ સ્વિચ +Name[he]=מחליף ערמה +Name[hi]=स्विच बदलें +Name[hne]=फ्लिप स्विच +Name[hr]=Naslagani prozori – promjena +Name[hu]=Billenős váltódoboz +Name[ia]=Commutator de colpetto (Flip) +Name[id]=Beralih Jungkir +Name[is]=Flettirofi +Name[it]=Scambiafinestre a pila +Name[ja]=フリップスイッチ +Name[kk]=Ақтарып ауыстырғыш +Name[km]=ប្ដូរ​ការ​ត្រឡប់​ +Name[kn]=ಬದಲಾವಣೆ ಗುಂಡಿ (ಫ್ಲಿಪ್ ಸ್ವಿಚ್) +Name[ko]=플립 전환기 +Name[lt]=Kartotekos pavidalo langų perjungiklis +Name[lv]=Flip pārslēdzējs +Name[mai]=स्विच पलटू +Name[ml]=ഫ്ലിപ് സ്വിച്ച് +Name[mr]=पलटी करून बदल +Name[nb]=Bla-bytter +Name[nds]=Dreihwesseln +Name[nl]=Flip Switch +Name[nn]=Stabelvekslar +Name[pa]=ਫਲਿੱਪ ਸਵਿੱਚ +Name[pl]=Przełączanie przebierane +Name[pt]=Mudança em Pilha +Name[pt_BR]=Mudança em pilha +Name[ro]=Schimbare cu întoarcere +Name[ru]=Перелистывание +Name[si]=උඩට ගැනීම් මාරුව +Name[sk]=PrepínaÅ¥ v kartotéke +Name[sl]=Preklapljanje - sklad +Name[sr]=Преклопно пребацивање +Name[sr@ijekavian]=Преклопно пребацивање +Name[sr@ijekavianlatin]=Preklopno prebacivanje +Name[sr@latin]=Preklopno prebacivanje +Name[sv]=Blädderbyte +Name[ta]=Flip Switch +Name[te]=ఫ్లిప్ స్విచ్ +Name[th]=สลับหน้าต่างพลิกซ้อนเป็นชั้น +Name[tr]=Dönen Seçici +Name[ug]=ئايلاندۇرۇپ ئالماشتۇر +Name[uk]=Тасування карток +Name[vi]=Chuyển đổi lật +Name[wa]=Discandjeu d' finiesses +Name[x-test]=xxFlip Switchxx +Name[zh_CN]=翻转切换 +Name[zh_TW]=翻轉切換 diff --git a/effects/flipswitch/flipswitch_config.h b/effects/flipswitch/flipswitch_config.h new file mode 100644 index 0000000..a81675b --- /dev/null +++ b/effects/flipswitch/flipswitch_config.h @@ -0,0 +1,46 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008, 2009 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_FLIPSWITCH_CONFIG_H +#define KWIN_FLIPSWITCH_CONFIG_H + +#include + +#include "ui_flipswitch_config.h" + +class KActionCollection; + +namespace KWin +{ + +class FlipSwitchEffectConfigForm : public QWidget, public Ui::FlipSwitchEffectConfigForm +{ + Q_OBJECT +public: + explicit FlipSwitchEffectConfigForm(QWidget* parent); +}; + +class FlipSwitchEffectConfig : public KCModule +{ + Q_OBJECT +public: + explicit FlipSwitchEffectConfig(QWidget* parent = nullptr, const QVariantList& args = QVariantList()); + ~FlipSwitchEffectConfig() override; + +public Q_SLOTS: + void save() override; + +private: + FlipSwitchEffectConfigForm* m_ui; + KActionCollection* m_actionCollection; +}; + +} // namespace + +#endif diff --git a/effects/flipswitch/flipswitch_config.ui b/effects/flipswitch/flipswitch_config.ui new file mode 100644 index 0000000..02a925f --- /dev/null +++ b/effects/flipswitch/flipswitch_config.ui @@ -0,0 +1,238 @@ + + + KWin::FlipSwitchEffectConfigForm + + + + 0 + 0 + 400 + 316 + + + + + + + Appearance + + + + + + Flip animation duration: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_Duration + + + + + + + + 0 + 0 + + + + Default + + + 5000 + + + 10 + + + + + + + Angle: + + + kcfg_Angle + + + + + + + + 0 + 0 + + + + ° + + + 360 + + + + + + + Horizontal position of front: + + + kcfg_XPosition + + + + + + + + + 100 + + + Qt::Horizontal + + + + + + + + + Left + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Right + + + + + + + + + + + Vertical position of front: + + + kcfg_YPosition + + + + + + + + + 100 + + + Qt::Vertical + + + + + + + + + Top + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Bottom + + + + + + + + + + + Display window &titles + + + + + + + + + + + + + Activation + + + + + + + 0 + 0 + + + + KShortcutsEditor::GlobalAction + + + + + + + + + + + KShortcutsEditor + QWidget +
KShortcutsEditor
+ 1 +
+
+ + kcfg_Duration + + + +
diff --git a/effects/flipswitch/flipswitchconfig.kcfgc b/effects/flipswitch/flipswitchconfig.kcfgc new file mode 100644 index 0000000..15fb710 --- /dev/null +++ b/effects/flipswitch/flipswitchconfig.kcfgc @@ -0,0 +1,5 @@ +File=flipswitch.kcfg +ClassName=FlipSwitchConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/effects/frozenapp/package/contents/code/main.js b/effects/frozenapp/package/contents/code/main.js new file mode 100644 index 0000000..95b46db --- /dev/null +++ b/effects/frozenapp/package/contents/code/main.js @@ -0,0 +1,118 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +"use strict"; + +var frozenAppEffect = { + inDuration: animationTime(1500), + outDuration: animationTime(250), + loadConfig: function () { + frozenAppEffect.inDuration = animationTime(1500); + frozenAppEffect.outDuration = animationTime(250); + }, + windowAdded: function (window) { + if (!window || !window.unresponsive) { + return; + } + frozenAppEffect.windowBecameUnresponsive(window); + }, + windowBecameUnresponsive: function (window) { + if (window.unresponsiveAnimation) { + return; + } + frozenAppEffect.startAnimation(window, frozenAppEffect.inDuration); + }, + startAnimation: function (window, duration) { + if (!window.visible) { + return; + } + window.unresponsiveAnimation = set({ + window: window, + duration: duration, + animations: [{ + type: Effect.Saturation, + to: 0.1 + }, { + type: Effect.Brightness, + to: 1.5 + }] + }); + }, + windowClosed: function (window) { + frozenAppEffect.cancelAnimation(window); + if (!window.unresponsive) { + return; + } + frozenAppEffect.windowBecameResponsive(window); + }, + windowBecameResponsive: function (window) { + if (!window.unresponsiveAnimation) { + return; + } + cancel(window.unresponsiveAnimation); + window.unresponsiveAnimation = undefined; + + animate({ + window: window, + duration: frozenAppEffect.outDuration, + animations: [{ + type: Effect.Saturation, + from: 0.1, + to: 1.0 + }, { + type: Effect.Brightness, + from: 1.5, + to: 1.0 + }] + }); + }, + cancelAnimation: function (window) { + if (window.unresponsiveAnimation) { + cancel(window.unresponsiveAnimation); + window.unresponsiveAnimation = undefined; + } + }, + desktopChanged: function () { + var windows = effects.stackingOrder; + for (var i = 0, length = windows.length; i < length; ++i) { + var window = windows[i]; + frozenAppEffect.cancelAnimation(window); + frozenAppEffect.restartAnimation(window); + } + }, + unresponsiveChanged: function (window) { + if (window.unresponsive) { + frozenAppEffect.windowBecameUnresponsive(window); + } else { + frozenAppEffect.windowBecameResponsive(window); + } + }, + restartAnimation: function (window) { + if (!window || !window.unresponsive) { + return; + } + frozenAppEffect.startAnimation(window, 1); + }, + init: function () { + effects.windowAdded.connect(frozenAppEffect.windowAdded); + effects.windowClosed.connect(frozenAppEffect.windowClosed); + effects.windowMinimized.connect(frozenAppEffect.cancelAnimation); + effects.windowUnminimized.connect(frozenAppEffect.restartAnimation); + effects.windowUnresponsiveChanged.connect(frozenAppEffect.unresponsiveChanged); + effects['desktopChanged(int,int)'].connect(frozenAppEffect.desktopChanged); + effects.desktopPresenceChanged.connect(frozenAppEffect.cancelAnimation); + effects.desktopPresenceChanged.connect(frozenAppEffect.restartAnimation); + + var windows = effects.stackingOrder; + for (var i = 0, length = windows.length; i < length; ++i) { + frozenAppEffect.restartAnimation(windows[i]); + } + } +}; +frozenAppEffect.init(); diff --git a/effects/frozenapp/package/metadata.desktop b/effects/frozenapp/package/metadata.desktop new file mode 100644 index 0000000..5b6137a --- /dev/null +++ b/effects/frozenapp/package/metadata.desktop @@ -0,0 +1,99 @@ +[Desktop Entry] +Name=Desaturate Unresponsive Applications +Name[az]=Cavab verməyən pəncərələrin rəngsizləşməsi +Name[ca]=Dessatura les aplicacions que no responen +Name[ca@valencia]=Dessatura les aplicacions que no responguen +Name[da]=Dæmp programmer som ikke svarer +Name[de]=Sättigung von nicht reagierenden Anwendungen verringern +Name[el]=Αποκορεσμός χρωμάτων μη αποκρινόμενων εφαρμογών +Name[en_GB]=Desaturate Unresponsive Applications +Name[es]=Desaturar las aplicaciones que no responden +Name[et]=Reageerimisvõimetute rakenduste muutmine kahvatuks +Name[eu]=Desasetu erantzuten ez duten aplikazioak +Name[fi]=Vähennä värikylläisyyttä sovelluksilta, jotka eivät vastaa +Name[fr]=Désature les applications qui ne répondent pas +Name[gl]=Reducir a saturación das aplicacións que non responden +Name[he]=מחשיך יישומים שאינם מגיבים +Name[hu]=Nem válaszoló alkalmazások színtelenítése +Name[ia]=Desatura applicationes non responsive +Name[id]=Desaturate-kan Aplikasi Tidak Responsif +Name[it]=Desatura le applicazioni che non rispondono +Name[ko]=응답 없는 프로그램을 무채색으로 전환 +Name[lt]=Nusodrinti nereaguojančias programas +Name[nl]=Verzadiging van niet responsieve toepassingen verminderen +Name[nn]=Fjern fargemetting pÃ¥ ikkje-responsive program +Name[pl]=Odbarwienie nieodpowiadających aplikacji +Name[pt]=Reduzir a Saturação das Aplicações Bloqueadas +Name[pt_BR]=Reduzir saturação de aplicativos que não respondem +Name[ro]=Desaturează aplicațiile ce nu răspund +Name[ru]=Обесцвечивание зависших приложений +Name[sk]=DesaturovaÅ¥ neodpovedajúce aplikácie +Name[sl]=ZmanjÅ¡aj nasičenost neodzivnih programov +Name[sr]=Посивљавање програма без одзива +Name[sr@ijekavian]=Посивљавање програма без одзива +Name[sr@ijekavianlatin]=Posivljavanje programa bez odziva +Name[sr@latin]=Posivljavanje programa bez odziva +Name[sv]=Avmätta oemottagliga program +Name[tr]=Yanıt Vermeyen Uygulamaların Yoğunluğunu Kaldır +Name[uk]=Зненасичення вікон, які не відповідають на запити +Name[x-test]=xxDesaturate Unresponsive Applicationsxx +Name[zh_CN]=对未响应应用程序降低饱和度 +Name[zh_TW]=淡化無回應的應用程式 +Icon=preferences-system-windows-effect-frozenapp +Comment=Desaturate windows of unresponsive (frozen) applications +Comment[az]=Cavab verməyən (donmuş) pəncərələrin rəngsizləşməsi +Comment[ca]=Dessatura les finestres de les aplicacions que no responen (congelades) +Comment[ca@valencia]=Dessatura les finestres de les aplicacions que no responen (congelades) +Comment[da]=Dæmp vinduerne for programmer der ikke svarer (er frosset) +Comment[de]=Verringert die Sättigung von nicht reagierenden, eingefrorenen Anwendungen +Comment[el]=Αποκορεσμός χρωμάτων παραθύρων μη αποκρινόμενων (κολλημένων) εφαρμογών +Comment[en_GB]=Desaturate windows of unresponsive (frozen) applications +Comment[es]=Desaturar las ventanas de las aplicaciones que no responden (congeladas) +Comment[et]=Reageerimisvõimetute (hangunud) rakenduste akende muutmine kahvatuks +Comment[eu]=Desasetu erantzuten ez duten aplikazioen leihoak (izoztuak) +Comment[fi]=Vähennä värikylläisyyttä sovelluksilta, jotka eivät vastaa (ovat jumittuneet) +Comment[fr]=Désature les fenêtres des applications qui ne répondent pas (gelées) +Comment[gl]=Reducir a saturación das xanelas de aplicacións que non responden (conxeladas) +Comment[he]=מחשיך חלונות של יישומים שאינם מגיבים (תקועים) +Comment[hu]=Színteleníti a nem válaszoló, lefagyott alkalmazások ablakait +Comment[ia]=Destura fenestras de applicationes non responsive (congelate) +Comment[id]=Desaturate-kan window pada aplikasi yang tidak responsif (beku) +Comment[it]=Desatura le finestre delle applicazione che non rispondono (bloccate) +Comment[ko]=응답 없는 프로그램 창을 무채색으로 전환 +Comment[lt]=Nusodrinti neatsakančių (užstrigusių) programų langus +Comment[nl]=Verzadiging van vensters van niet responsieve (bevroren) toepassingen verminderen +Comment[nn]=Fjern fargemetting pÃ¥ vindauge pÃ¥ program som ikkje lenger reagerer +Comment[pl]=Odbarwia okna nieodpowiadających (zawieszonych) aplikacji +Comment[pt]=Reduzir a saturação das janelas das aplicações sem resposta (bloqueadas) +Comment[pt_BR]=Reduzir saturação de janelas de aplicativos que não respondem (travados) +Comment[ro]=Desaturează ferestrele aplicațiilor ce nu răspund (înghețate) +Comment[ru]=Обесцвечивание окон приложений, не отвечающих на запросы +Comment[sk]=DesaturovaÅ¥ okná neodpovedajúcich aplikácií +Comment[sl]=ZmanjÅ¡aj nasičenost oken neodzivnih (zamrznjenih) programov +Comment[sr]=Прозори програма који се не одазивају (смрзнутих) бивају посивљени +Comment[sr@ijekavian]=Прозори програма који се не одазивају (смрзнутих) бивају посивљени +Comment[sr@ijekavianlatin]=Prozori programa koji se ne odazivaju (smrznutih) bivaju posivljeni +Comment[sr@latin]=Prozori programa koji se ne odazivaju (smrznutih) bivaju posivljeni +Comment[sv]=Avmätta fönster för oemottagliga (frysta) program +Comment[tr]=Yanıt vermeyen (donmuş) uygulamaların pencerelerini soldur +Comment[uk]=Зменшення насиченості кольорів вікон програм, які не відповідають на запити (повисли) +Comment[x-test]=xxDesaturate windows of unresponsive (frozen) applicationsxx +Comment[zh_CN]=将未响应 (冻结) 的应用程序窗口的饱和度降低 +Comment[zh_TW]=淡化無回應(凍結)的應用程式視窗 + +Type=Service +X-KDE-ServiceTypes=KWin/Effect,KCModule +X-KDE-PluginInfo-Author=Kai Uwe Broulik +X-KDE-PluginInfo-Email=kde@privat.broulik.de +X-KDE-PluginInfo-Name=kwin4_effect_frozenapp +X-KDE-PluginInfo-Version=1.0 +X-KDE-PluginInfo-Category=Appearance +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-EnabledByDefault=true +X-KDE-Ordering=60 +X-Plasma-API=javascript +X-Plasma-MainScript=code/main.js +X-KDE-PluginKeyword=kwin4_effect_frozenapp +X-KDE-Library=kcm_kwin4_genericscripted +X-KDE-ParentComponents=kwin4_effect_frozen +X-KWin-Config-TranslationDomain=kwin_effects diff --git a/effects/fullscreen/package/contents/code/fullscreen.js b/effects/fullscreen/package/contents/code/fullscreen.js new file mode 100644 index 0000000..778f0fb --- /dev/null +++ b/effects/fullscreen/package/contents/code/fullscreen.js @@ -0,0 +1,90 @@ +/* + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +"use strict"; + +var fullScreenEffect = { + duration: animationTime(250), + loadConfig: function () { + fullScreenEffect.duration = animationTime(250); + }, + fullScreenChanged: function (window) { + if (!window.oldGeometry) { + return; + } + window.setData(Effect.WindowForceBlurRole, true); + var oldGeometry, newGeometry; + oldGeometry = window.oldGeometry; + newGeometry = window.geometry; + if (oldGeometry.width == newGeometry.width && oldGeometry.height == newGeometry.height) + oldGeometry = window.olderGeometry; + window.olderGeometry = window.oldGeometry; + window.oldGeometry = newGeometry; + window.fullScreenAnimation1 = animate({ + window: window, + duration: fullScreenEffect.duration, + animations: [{ + type: Effect.Size, + to: { + value1: newGeometry.width, + value2: newGeometry.height + }, + from: { + value1: oldGeometry.width, + value2: oldGeometry.height + } + }, { + type: Effect.Translation, + to: { + value1: 0, + value2: 0 + }, + from: { + value1: oldGeometry.x - newGeometry.x - (newGeometry.width / 2 - oldGeometry.width / 2), + value2: oldGeometry.y - newGeometry.y - (newGeometry.height / 2 - oldGeometry.height / 2) + } + }] + }); + if (!window.resize) { + window.fullScreenAnimation2 =animate({ + window: window, + duration: fullScreenEffect.duration, + animations: [{ + type: Effect.CrossFadePrevious, + to: 1.0, + from: 0.0 + }] + }); + } + }, + restoreForceBlurState: function(window) { + window.setData(Effect.WindowForceBlurRole, null); + }, + geometryChange: function (window, oldGeometry) { + if (window.fullScreenAnimation1) { + if (window.geometry.width != window.oldGeometry.width || + window.geometry.height != window.oldGeometry.height) { + cancel(window.fullScreenAnimation1); + delete window.fullScreenAnimation1; + if (window.fullScreenAnimation2) { + cancel(window.fullScreenAnimation2); + delete window.fullScreenAnimation2; + } + } + } + window.oldGeometry = window.geometry; + window.olderGeometry = oldGeometry; + }, + init: function () { + effect.configChanged.connect(fullScreenEffect.loadConfig); + effects.windowFrameGeometryChanged.connect(fullScreenEffect.geometryChange); + effects.windowFullScreenChanged.connect(fullScreenEffect.fullScreenChanged); + effect.animationEnded.connect(fullScreenEffect.restoreForceBlurState); + } +}; +fullScreenEffect.init(); diff --git a/effects/fullscreen/package/metadata.desktop b/effects/fullscreen/package/metadata.desktop new file mode 100644 index 0000000..e7d5442 --- /dev/null +++ b/effects/fullscreen/package/metadata.desktop @@ -0,0 +1,62 @@ +[Desktop Entry] +Comment=Animation for a window going to and leaving full screen mode +Comment[ca]=Animació per a una finestra que entra o abandona el mode de pantalla completa +Comment[en_GB]=Animation for a window going to and leaving full screen mode +Comment[es]=Animación para una ventana que entra en el modo de pantalla completa o sale de él +Comment[et]=Täisekraanirežiimi mineva või sealt väljuva akna animatsioon +Comment[eu]=Pantaila-osoko modutik/modura aldatzen den leiho baten animazioa +Comment[fi]=Animointi ikkunalle, joka siirtyy tai poistuu koko näytön tilasta +Comment[fr]=Animation pour une fenêtre basculant vers ou sortant du mode « Plein écran » +Comment[ia]=Animation per un fenestra va a e abandona modo de schermo plen +Comment[it]=Animazione per una finestra che entra e esce dalla modalità a schermo intero +Comment[ko]=창이 전체 화면 모드로 진입하거나 벗어날 때 사용할 애니메이션 +Comment[nl]=Animatie voor een venster dat gaat naar volledig scherm en deze verlaat +Comment[nn]=Animasjon for vindauge som gÃ¥r til eller frÃ¥ fullskjermsmodus +Comment[pl]=Animacja dla okna wchodzącego i wychodzącego z trybu pełnego ekranu +Comment[pt]=Animação para uma janela que vai entrar/sair do modo de ecrã completo +Comment[pt_BR]=Animação para uma janela indo e saindo do modo de tela cheia +Comment[ro]=Animație pentru o fereastră ce intră sau iese din regim de ecran complet +Comment[sk]=Animácia pre okno smerujúce do a opúšťajúce režim celej obrazovky +Comment[sl]=Animacija za razpenjanje ali zapuščanje čez zaslon razpetega okna +Comment[sv]=Animering för ett fönster till och frÃ¥n fullskärmsläge +Comment[uk]=Анімація для вікон під час входу до повноекранного режиму і виходу з нього +Comment[x-test]=xxAnimation for a window going to and leaving full screen modexx +Icon=preferences-system-windows-effect-fullscreen +Name=Full Screen +Name[ca]=Pantalla completa +Name[cs]=Celá obrazovka +Name[de]=Vollbild +Name[en_GB]=Full Screen +Name[es]=Pantalla completa +Name[et]=Täisekraan +Name[eu]=Pantaila-betea +Name[fi]=Koko näyttö +Name[fr]=Plein écran +Name[ia]=Schermo plen +Name[it]=Schermo intero +Name[ko]=전체 화면 +Name[nl]=Volledig scherm +Name[nn]=Fullskjerm +Name[pl]=Pełny ekran +Name[pt]=Ecrã Completo +Name[pt_BR]=Tela inteira +Name[ro]=Ecran complet +Name[sk]=Celá obrazovka +Name[sl]=Celotni zaslon +Name[sv]=Fullskärm +Name[uk]=Повноекранний режим +Name[x-test]=xxFull Screenxx +Type=Service +X-KDE-ParentApp= +X-KDE-PluginInfo-Author=Martin Gräßlin +X-KDE-PluginInfo-Category=Appearance +X-KDE-PluginInfo-Email=mgraesslin@kde.org +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-Name=kwin4_effect_fullscreen +X-KDE-PluginInfo-Version=1 +X-KDE-PluginInfo-Website= +X-KDE-ServiceTypes=KWin/Effect +X-KDE-PluginInfo-EnabledByDefault=true +X-KDE-Ordering=60 +X-Plasma-API=javascript +X-Plasma-MainScript=code/fullscreen.js diff --git a/effects/glide/CMakeLists.txt b/effects/glide/CMakeLists.txt new file mode 100644 index 0000000..cd0a69c --- /dev/null +++ b/effects/glide/CMakeLists.txt @@ -0,0 +1,24 @@ +####################################### +# Config +set(kwin_glide_config_SRCS glide_config.cpp) +ki18n_wrap_ui(kwin_glide_config_SRCS glide_config.ui) +kconfig_add_kcfg_files(kwin_glide_config_SRCS glideconfig.kcfgc) + +add_library(kwin_glide_config MODULE ${kwin_glide_config_SRCS}) + +target_link_libraries(kwin_glide_config + KF5::ConfigWidgets + KF5::I18n + Qt5::DBus + KWinEffectsInterface +) + +kcoreaddons_desktop_to_json(kwin_glide_config glide_config.desktop SERVICE_TYPES kcmodule.desktop) + +install( + TARGETS + kwin_glide_config + DESTINATION + ${PLUGIN_INSTALL_DIR}/kwin/effects/configs +) + diff --git a/effects/glide/glide.cpp b/effects/glide/glide.cpp new file mode 100644 index 0000000..0b0c18d --- /dev/null +++ b/effects/glide/glide.cpp @@ -0,0 +1,326 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Philip Falkner + SPDX-FileCopyrightText: 2009 Martin Gräßlin + SPDX-FileCopyrightText: 2010 Alexandre Pereira + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +// own +#include "glide.h" + +// KConfigSkeleton +#include "glideconfig.h" + +// Qt +#include +#include + +namespace KWin +{ + +static const QSet s_blacklist { + QStringLiteral("ksmserver ksmserver"), + QStringLiteral("ksmserver-logout-greeter ksmserver-logout-greeter"), + QStringLiteral("ksplashqml ksplashqml"), + QStringLiteral("ksplashsimple ksplashsimple"), + QStringLiteral("ksplashx ksplashx") +}; + +GlideEffect::GlideEffect() +{ + initConfig(); + reconfigure(ReconfigureAll); + + connect(effects, &EffectsHandler::windowAdded, this, &GlideEffect::windowAdded); + connect(effects, &EffectsHandler::windowClosed, this, &GlideEffect::windowClosed); + connect(effects, &EffectsHandler::windowDeleted, this, &GlideEffect::windowDeleted); + connect(effects, &EffectsHandler::windowDataChanged, this, &GlideEffect::windowDataChanged); +} + +GlideEffect::~GlideEffect() = default; + +void GlideEffect::reconfigure(ReconfigureFlags flags) +{ + Q_UNUSED(flags) + + GlideConfig::self()->read(); + m_duration = std::chrono::milliseconds(animationTime(160)); + + m_inParams.edge = static_cast(GlideConfig::inRotationEdge()); + m_inParams.angle.from = GlideConfig::inRotationAngle(); + m_inParams.angle.to = 0.0; + m_inParams.distance.from = GlideConfig::inDistance(); + m_inParams.distance.to = 0.0; + m_inParams.opacity.from = GlideConfig::inOpacity(); + m_inParams.opacity.to = 1.0; + + m_outParams.edge = static_cast(GlideConfig::outRotationEdge()); + m_outParams.angle.from = 0.0; + m_outParams.angle.to = GlideConfig::outRotationAngle(); + m_outParams.distance.from = 0.0; + m_outParams.distance.to = GlideConfig::outDistance(); + m_outParams.opacity.from = 1.0; + m_outParams.opacity.to = GlideConfig::outOpacity(); +} + +void GlideEffect::prePaintScreen(ScreenPrePaintData &data, int time) +{ + const std::chrono::milliseconds delta(time); + + auto animationIt = m_animations.begin(); + while (animationIt != m_animations.end()) { + (*animationIt).update(delta); + ++animationIt; + } + + data.mask |= PAINT_SCREEN_WITH_TRANSFORMED_WINDOWS; + + effects->prePaintScreen(data, time); +} + +void GlideEffect::prePaintWindow(EffectWindow *w, WindowPrePaintData &data, int time) +{ + if (m_animations.contains(w)) { + data.setTransformed(); + w->enablePainting(EffectWindow::PAINT_DISABLED_BY_DELETE); + } + + effects->prePaintWindow(w, data, time); +} + +void GlideEffect::paintWindow(EffectWindow *w, int mask, QRegion region, WindowPaintData &data) +{ + auto animationIt = m_animations.constFind(w); + if (animationIt == m_animations.constEnd()) { + effects->paintWindow(w, mask, region, data); + return; + } + + // Perspective projection distorts objects near edges + // of the viewport. This is critical because distortions + // near edges of the viewport are not desired with this effect. + // To fix this, the center of the window will be moved to the origin, + // after applying perspective projection, the center is moved back + // to its "original" projected position. Overall, this is how the window + // will be transformed: + // [move to the origin] -> [rotate] -> [translate] -> + // -> [perspective projection] -> [reverse "move to the origin"] + const QMatrix4x4 oldProjMatrix = data.screenProjectionMatrix(); + const QRectF windowGeo = w->geometry(); + const QVector3D invOffset = oldProjMatrix.map(QVector3D(windowGeo.center())); + QMatrix4x4 invOffsetMatrix; + invOffsetMatrix.translate(invOffset.x(), invOffset.y()); + data.setProjectionMatrix(invOffsetMatrix * oldProjMatrix); + + // Move the center of the window to the origin. + const QRectF screenGeo = effects->virtualScreenGeometry(); + const QPointF offset = screenGeo.center() - windowGeo.center(); + data.translate(offset.x(), offset.y()); + + const GlideParams params = w->isDeleted() ? m_outParams : m_inParams; + const qreal t = (*animationIt).value(); + + switch (params.edge) { + case RotationEdge::Top: + data.setRotationAxis(Qt::XAxis); + data.setRotationOrigin(QVector3D(0, 0, 0)); + data.setRotationAngle(-interpolate(params.angle.from, params.angle.to, t)); + break; + + case RotationEdge::Right: + data.setRotationAxis(Qt::YAxis); + data.setRotationOrigin(QVector3D(w->width(), 0, 0)); + data.setRotationAngle(-interpolate(params.angle.from, params.angle.to, t)); + break; + + case RotationEdge::Bottom: + data.setRotationAxis(Qt::XAxis); + data.setRotationOrigin(QVector3D(0, w->height(), 0)); + data.setRotationAngle(interpolate(params.angle.from, params.angle.to, t)); + break; + + case RotationEdge::Left: + data.setRotationAxis(Qt::YAxis); + data.setRotationOrigin(QVector3D(0, 0, 0)); + data.setRotationAngle(interpolate(params.angle.from, params.angle.to, t)); + break; + + default: + // Fallback to Top. + data.setRotationAxis(Qt::XAxis); + data.setRotationOrigin(QVector3D(0, 0, 0)); + data.setRotationAngle(-interpolate(params.angle.from, params.angle.to, t)); + break; + } + + data.setZTranslation(-interpolate(params.distance.from, params.distance.to, t)); + data.multiplyOpacity(interpolate(params.opacity.from, params.opacity.to, t)); + + effects->paintWindow(w, mask, region, data); +} + +void GlideEffect::postPaintScreen() +{ + auto animationIt = m_animations.begin(); + while (animationIt != m_animations.end()) { + if ((*animationIt).done()) { + EffectWindow *w = animationIt.key(); + if (w->isDeleted()) { + w->unrefWindow(); + } + animationIt = m_animations.erase(animationIt); + } else { + ++animationIt; + } + } + + effects->addRepaintFull(); + effects->postPaintScreen(); +} + +bool GlideEffect::isActive() const +{ + return !m_animations.isEmpty(); +} + +bool GlideEffect::supported() +{ + return effects->isOpenGLCompositing() + && effects->animationsSupported(); +} + +void GlideEffect::windowAdded(EffectWindow *w) +{ + if (effects->activeFullScreenEffect()) { + return; + } + + if (!isGlideWindow(w)) { + return; + } + + if (!w->isVisible()) { + return; + } + + const void *addGrab = w->data(WindowAddedGrabRole).value(); + if (addGrab && addGrab != this) { + return; + } + + w->setData(WindowAddedGrabRole, QVariant::fromValue(static_cast(this))); + + TimeLine &timeLine = m_animations[w]; + timeLine.reset(); + timeLine.setDirection(TimeLine::Forward); + timeLine.setDuration(m_duration); + timeLine.setEasingCurve(QEasingCurve::InCurve); + + effects->addRepaintFull(); +} + +void GlideEffect::windowClosed(EffectWindow *w) +{ + if (effects->activeFullScreenEffect()) { + return; + } + + if (!isGlideWindow(w)) { + return; + } + + if (!w->isVisible()) { + return; + } + + const void *closeGrab = w->data(WindowClosedGrabRole).value(); + if (closeGrab && closeGrab != this) { + return; + } + + w->refWindow(); + w->setData(WindowClosedGrabRole, QVariant::fromValue(static_cast(this))); + + TimeLine &timeLine = m_animations[w]; + timeLine.reset(); + timeLine.setDirection(TimeLine::Forward); + timeLine.setDuration(m_duration); + timeLine.setEasingCurve(QEasingCurve::OutCurve); + + effects->addRepaintFull(); +} + +void GlideEffect::windowDeleted(EffectWindow *w) +{ + m_animations.remove(w); +} + +void GlideEffect::windowDataChanged(EffectWindow *w, int role) +{ + if (role != WindowAddedGrabRole && role != WindowClosedGrabRole) { + return; + } + + if (w->data(role).value() == this) { + return; + } + + auto animationIt = m_animations.find(w); + if (animationIt == m_animations.end()) { + return; + } + + if (w->isDeleted() && role == WindowClosedGrabRole) { + w->unrefWindow(); + } + + m_animations.erase(animationIt); +} + +bool GlideEffect::isGlideWindow(EffectWindow *w) const +{ + // We don't want to animate most of plasmashell's windows, yet, some + // of them we want to, for example, Task Manager Settings window. + // The problem is that all those window share single window class. + // So, the only way to decide whether a window should be animated is + // to use a heuristic: if a window has decoration, then it's most + // likely a dialog or a settings window so we have to animate it. + if (w->windowClass() == QLatin1String("plasmashell plasmashell") + || w->windowClass() == QLatin1String("plasmashell org.kde.plasmashell")) { + return w->hasDecoration(); + } + + if (s_blacklist.contains(w->windowClass())) { + return false; + } + + if (w->hasDecoration()) { + return true; + } + + // Don't animate combobox popups, tooltips, popup menus, etc. + if (w->isPopupWindow()) { + return false; + } + + // Don't animate the outline because it looks very sick. + if (w->isOutline()) { + return false; + } + + // Override-redirect windows are usually used for user interface + // concepts that are not expected to be animated by this effect. + if (w->isX11Client() && !w->isManaged()) { + return false; + } + + return w->isNormalWindow() + || w->isDialog(); +} + +} // namespace KWin diff --git a/effects/glide/glide.h b/effects/glide/glide.h new file mode 100644 index 0000000..d456eeb --- /dev/null +++ b/effects/glide/glide.h @@ -0,0 +1,145 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Philip Falkner + SPDX-FileCopyrightText: 2009 Martin Gräßlin + SPDX-FileCopyrightText: 2010 Alexandre Pereira + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_GLIDE_H +#define KWIN_GLIDE_H + +// kwineffects +#include + +namespace KWin +{ + +class GlideEffect : public Effect +{ + Q_OBJECT + Q_PROPERTY(int duration READ duration) + Q_PROPERTY(RotationEdge inRotationEdge READ inRotationEdge) + Q_PROPERTY(qreal inRotationAngle READ inRotationAngle) + Q_PROPERTY(qreal inDistance READ inDistance) + Q_PROPERTY(qreal inOpacity READ inOpacity) + Q_PROPERTY(RotationEdge outRotationEdge READ outRotationEdge) + Q_PROPERTY(qreal outRotationAngle READ outRotationAngle) + Q_PROPERTY(qreal outDistance READ outDistance) + Q_PROPERTY(qreal outOpacity READ outOpacity) + +public: + GlideEffect(); + ~GlideEffect() override; + + void reconfigure(ReconfigureFlags flags) override; + + void prePaintScreen(ScreenPrePaintData &data, int time) override; + void prePaintWindow(EffectWindow *w, WindowPrePaintData &data, int time) override; + void paintWindow(EffectWindow *w, int mask, QRegion region, WindowPaintData &data) override; + void postPaintScreen() override; + + bool isActive() const override; + int requestedEffectChainPosition() const override; + + static bool supported(); + + enum RotationEdge { + Top = 0, + Right = 1, + Bottom = 2, + Left = 3 + }; + Q_ENUM(RotationEdge) + + int duration() const; + RotationEdge inRotationEdge() const; + qreal inRotationAngle() const; + qreal inDistance() const; + qreal inOpacity() const; + RotationEdge outRotationEdge() const; + qreal outRotationAngle() const; + qreal outDistance() const; + qreal outOpacity() const; + +private Q_SLOTS: + void windowAdded(EffectWindow *w); + void windowClosed(EffectWindow *w); + void windowDeleted(EffectWindow *w); + void windowDataChanged(EffectWindow *w, int role); + +private: + bool isGlideWindow(EffectWindow *w) const; + + std::chrono::milliseconds m_duration; + QHash m_animations; + + struct GlideParams { + RotationEdge edge; + struct { + qreal from; + qreal to; + } angle, distance, opacity; + }; + + GlideParams m_inParams; + GlideParams m_outParams; +}; + +inline int GlideEffect::requestedEffectChainPosition() const +{ + return 50; +} + +inline int GlideEffect::duration() const +{ + return m_duration.count(); +} + +inline GlideEffect::RotationEdge GlideEffect::inRotationEdge() const +{ + return m_inParams.edge; +} + +inline qreal GlideEffect::inRotationAngle() const +{ + return m_inParams.angle.from; +} + +inline qreal GlideEffect::inDistance() const +{ + return m_inParams.distance.from; +} + +inline qreal GlideEffect::inOpacity() const +{ + return m_inParams.opacity.from; +} + +inline GlideEffect::RotationEdge GlideEffect::outRotationEdge() const +{ + return m_outParams.edge; +} + +inline qreal GlideEffect::outRotationAngle() const +{ + return m_outParams.angle.to; +} + +inline qreal GlideEffect::outDistance() const +{ + return m_outParams.distance.to; +} + +inline qreal GlideEffect::outOpacity() const +{ + return m_outParams.opacity.to; +} + +} // namespace KWin + +#endif diff --git a/effects/glide/glide.kcfg b/effects/glide/glide.kcfg new file mode 100644 index 0000000..917f725 --- /dev/null +++ b/effects/glide/glide.kcfg @@ -0,0 +1,40 @@ + + + + + + 0 + + + 0 + + + 3.0 + + + 30.0 + + + 0.4 + 0.0 + 1.0 + + + 2 + + + 3.0 + + + 30.0 + + + 0.0 + 0.0 + 1.0 + + + diff --git a/effects/glide/glide_config.cpp b/effects/glide/glide_config.cpp new file mode 100644 index 0000000..65a174c --- /dev/null +++ b/effects/glide/glide_config.cpp @@ -0,0 +1,48 @@ +/* + SPDX-FileCopyrightText: 2010 Fredrik Höglund + SPDX-FileCopyrightText: 2010 Alexandre Pereira + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "glide_config.h" +// KConfigSkeleton +#include "glideconfig.h" +#include + +#include +#include +#include + +K_PLUGIN_FACTORY_WITH_JSON(GlideEffectConfigFactory, + "glide_config.json", + registerPlugin();) + +namespace KWin +{ + +GlideEffectConfig::GlideEffectConfig(QWidget *parent, const QVariantList &args) + : KCModule(KAboutData::pluginData(QStringLiteral("glide")), parent, args) +{ + ui.setupUi(this); + GlideConfig::instance(KWIN_CONFIG); + addConfig(GlideConfig::self(), this); + load(); +} + +GlideEffectConfig::~GlideEffectConfig() +{ +} + +void GlideEffectConfig::save() +{ + KCModule::save(); + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("glide")); +} + +} // namespace KWin + +#include "glide_config.moc" diff --git a/effects/glide/glide_config.desktop b/effects/glide/glide_config.desktop new file mode 100644 index 0000000..d023674 --- /dev/null +++ b/effects/glide/glide_config.desktop @@ -0,0 +1,69 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=KCModule + +X-KDE-Library=kwin_glide_config +X-KDE-ParentComponents=glide + +Name=Glide +Name[ar]=الطيران +Name[az]=Sürüşmə +Name[bg]=Приплъзване +Name[bs]=Uletanje +Name[ca]=Lliscament +Name[ca@valencia]=Lliscament +Name[cs]=Klouzání +Name[da]=Svæv +Name[de]=Gleiten +Name[el]=Ολίσθηση +Name[en_GB]=Glide +Name[es]=Glide +Name[et]=Liuglemine +Name[eu]=Irristatzea +Name[fi]=Ikkunaliuku +Name[fr]=Glisser +Name[ga]=Sleamhnaigh +Name[gl]=Deslizamento +Name[he]=גלישה +Name[hi]=ग्लाइड +Name[hr]=Klizanje +Name[hu]=Siklás +Name[ia]=Glissa +Name[id]=Luncuran +Name[is]=Svífa +Name[it]=Plana +Name[ja]=グライド +Name[kk]=Сырғанау +Name[km]=សំកាំង +Name[kn]=ಜಾರು +Name[ko]=글라이드 +Name[lt]=Sklandymas +Name[lv]=SlÄ«dēt +Name[mr]=घसरणे +Name[nb]=Skyv +Name[nds]=Glieden +Name[nl]=Schuiven +Name[nn]=Skliding +Name[pa]=ਗਲਾਈਡ +Name[pl]=Obracanie na zawiasie +Name[pt]=Deslizar +Name[pt_BR]=Deslizar +Name[ro]=Scurgere +Name[ru]=Скольжение +Name[si]=ලිස්සන්න +Name[sk]=KĺzaÅ¥ +Name[sl]=Drsenje +Name[sr]=Улетање +Name[sr@ijekavian]=Улетање +Name[sr@ijekavianlatin]=Uletanje +Name[sr@latin]=Uletanje +Name[sv]=Glid +Name[th]=ร่อนหน้าต่าง +Name[tr]=Kaydır +Name[ug]=سىيرىل +Name[uk]=Плин +Name[vi]=Trượt +Name[wa]=Ridaedje +Name[x-test]=xxGlidexx +Name[zh_CN]=滑行 +Name[zh_TW]=滑動 diff --git a/effects/glide/glide_config.h b/effects/glide/glide_config.h new file mode 100644 index 0000000..2527512 --- /dev/null +++ b/effects/glide/glide_config.h @@ -0,0 +1,34 @@ +/* + SPDX-FileCopyrightText: 2010 Fredrik Höglund + SPDX-FileCopyrightText: 2010 Alexandre Pereira + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef GLIDE_CONFIG_H +#define GLIDE_CONFIG_H + +#include +#include "ui_glide_config.h" + +namespace KWin +{ + +class GlideEffectConfig : public KCModule +{ + Q_OBJECT + +public: + explicit GlideEffectConfig(QWidget *parent = nullptr, const QVariantList &args = QVariantList()); + ~GlideEffectConfig() override; + + void save() override; + +private: + ::Ui::GlideEffectConfig ui; +}; + +} // namespace KWin + +#endif + diff --git a/effects/glide/glide_config.ui b/effects/glide/glide_config.ui new file mode 100644 index 0000000..f5bdef3 --- /dev/null +++ b/effects/glide/glide_config.ui @@ -0,0 +1,260 @@ + + + GlideEffectConfig + + + + 0 + 0 + 440 + 375 + + + + + + + + + Duration: + + + + + + + + 0 + 0 + + + + Default + + + milliseconds + + + 9999 + + + 5 + + + + + + + + + Window Open Animation + + + + + + Rotation edge: + + + + + + + + 0 + 0 + + + + + Top + + + + + Right + + + + + Bottom + + + + + Left + + + + + + + + Rotation angle: + + + + + + + + 0 + 0 + + + + + + + -360 + + + 360 + + + + + + + Distance: + + + + + + + + 0 + 0 + + + + -5000 + + + 5000 + + + 5 + + + + + + + + + + Window Close Animation + + + + + + Rotation edge: + + + + + + + + 0 + 0 + + + + + Top + + + + + Right + + + + + Bottom + + + + + Left + + + + + + + + Rotation angle: + + + + + + + Distance: + + + + + + + + 0 + 0 + + + + + + + -360 + + + 360 + + + + + + + + 0 + 0 + + + + -5000 + + + 5000 + + + 5 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/effects/glide/glideconfig.kcfgc b/effects/glide/glideconfig.kcfgc new file mode 100644 index 0000000..3266f2c --- /dev/null +++ b/effects/glide/glideconfig.kcfgc @@ -0,0 +1,5 @@ +File=glide.kcfg +ClassName=GlideConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/effects/highlightwindow/CMakeLists.txt b/effects/highlightwindow/CMakeLists.txt new file mode 100644 index 0000000..712c4a6 --- /dev/null +++ b/effects/highlightwindow/CMakeLists.txt @@ -0,0 +1,7 @@ +####################################### +# Effect + +# Source files +set(kwin4_effect_builtins_sources ${kwin4_effect_builtins_sources} + highlightwindow/highlightwindow.cpp +) diff --git a/effects/highlightwindow/highlightwindow.cpp b/effects/highlightwindow/highlightwindow.cpp new file mode 100644 index 0000000..28594c4 --- /dev/null +++ b/effects/highlightwindow/highlightwindow.cpp @@ -0,0 +1,300 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Lucas Murray + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "highlightwindow.h" + +namespace KWin +{ + +HighlightWindowEffect::HighlightWindowEffect() + : m_finishing(false) + , m_fadeDuration(float(animationTime(150))) + , m_monitorWindow(nullptr) +{ + m_atom = effects->announceSupportProperty("_KDE_WINDOW_HIGHLIGHT", this); + connect(effects, &EffectsHandler::windowAdded, this, &HighlightWindowEffect::slotWindowAdded); + connect(effects, &EffectsHandler::windowClosed, this, &HighlightWindowEffect::slotWindowClosed); + connect(effects, &EffectsHandler::windowDeleted, this, &HighlightWindowEffect::slotWindowDeleted); + connect(effects, &EffectsHandler::propertyNotify, this, + [this](EffectWindow *w, long atom) { + slotPropertyNotify(w, atom, nullptr); + } + ); + connect(effects, &EffectsHandler::xcbConnectionChanged, this, + [this] { + m_atom = effects->announceSupportProperty("_KDE_WINDOW_HIGHLIGHT", this); + } + ); +} + +HighlightWindowEffect::~HighlightWindowEffect() +{ +} + +static bool isInitiallyHidden(EffectWindow* w) +{ + // Is the window initially hidden until it is highlighted? + return w->isMinimized() || !w->isOnCurrentDesktop(); +} + +void HighlightWindowEffect::prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time) +{ + // Calculate window opacities + QHash::iterator opacity = m_windowOpacity.find(w); + if (!m_highlightedWindows.isEmpty()) { + // Initial fade out and changing highlight animation + if (opacity == m_windowOpacity.end()) + opacity = m_windowOpacity.insert(w, 0.0f); + float oldOpacity = *opacity; + if (m_highlightedWindows.contains(w)) + *opacity = qMin(1.0f, oldOpacity + time / m_fadeDuration); + else if (w->isNormalWindow() || w->isDialog()) // Only fade out windows + *opacity = qMax(isInitiallyHidden(w) ? 0.0f : 0.15f, oldOpacity - time / m_fadeDuration); + + if (*opacity < 0.98f) + data.setTranslucent(); + if (oldOpacity != *opacity) + effects->addRepaint(w->expandedGeometry()); + } else if (m_finishing && m_windowOpacity.contains(w)) { + // Final fading back in animation + if (opacity == m_windowOpacity.end()) + opacity = m_windowOpacity.insert(w, 0.0f); + float oldOpacity = *opacity; + if (isInitiallyHidden(w)) + *opacity = qMax(0.0f, oldOpacity - time / m_fadeDuration); + else + *opacity = qMin(1.0f, oldOpacity + time / m_fadeDuration); + + if (*opacity < 0.98f) + data.setTranslucent(); + if (oldOpacity != *opacity) + effects->addRepaint(w->expandedGeometry()); + + if (*opacity > 0.98f || *opacity < 0.02f) { + m_windowOpacity.remove(w); // We default to 1.0 + opacity = m_windowOpacity.end(); + } + } + + // Show tabbed windows and windows on other desktops if highlighted + if (opacity != m_windowOpacity.end() && *opacity > 0.01) { + if (w->isMinimized()) + w->enablePainting(EffectWindow::PAINT_DISABLED_BY_MINIMIZE); + if (!w->isOnCurrentDesktop()) + w->enablePainting(EffectWindow::PAINT_DISABLED_BY_DESKTOP); + } + + effects->prePaintWindow(w, data, time); +} + +void HighlightWindowEffect::paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data) +{ + data.multiplyOpacity(m_windowOpacity.value(w, 1.0f)); + effects->paintWindow(w, mask, region, data); +} + +void HighlightWindowEffect::slotWindowAdded(EffectWindow* w) +{ + if (!m_highlightedWindows.isEmpty()) { + // The effect is activated thus we need to add it to the opacity hash + foreach (const WId id, m_highlightedIds) { + if (w == effects->findWindow(id)) { + m_windowOpacity[w] = 1.0; // this window was demanded to be highlighted before it appeared + return; + } + } + m_windowOpacity[w] = 0.15; // this window is not currently highlighted + } + slotPropertyNotify(w, m_atom, w); // Check initial value +} + +void HighlightWindowEffect::slotWindowClosed(EffectWindow* w) +{ + if (m_monitorWindow == w) // The monitoring window was destroyed + finishHighlighting(); +} + +void HighlightWindowEffect::slotWindowDeleted(EffectWindow* w) +{ + m_windowOpacity.remove(w); +} + +void HighlightWindowEffect::slotPropertyNotify(EffectWindow* w, long a, EffectWindow *addedWindow) +{ + if (a != m_atom || m_atom == XCB_ATOM_NONE) + return; // Not our atom + + // if the window is null, the property was set on the root window - see events.cpp + QByteArray byteData = w ? w->readProperty(m_atom, m_atom, 32) : + effects->readRootProperty(m_atom, m_atom, 32); + if (byteData.length() < 1) { + // Property was removed, clearing highlight + if (!addedWindow || w != addedWindow) + finishHighlighting(); + return; + } + auto* data = reinterpret_cast(byteData.data()); + + if (!data[0]) { + // Purposely clearing highlight by issuing a NULL target + finishHighlighting(); + return; + } + m_monitorWindow = w; + bool found = false; + int length = byteData.length() / sizeof(data[0]); + //foreach ( EffectWindow* e, m_highlightedWindows ) + // effects->setElevatedWindow( e, false ); + m_highlightedWindows.clear(); + m_highlightedIds.clear(); + for (int i = 0; i < length; i++) { + m_highlightedIds << data[i]; + EffectWindow* foundWin = effects->findWindow(data[i]); + if (!foundWin) { + qCDebug(KWINEFFECTS) << "Invalid window targetted for highlight. Requested:" << data[i]; + continue; // might come in later. + } + m_highlightedWindows.append(foundWin); + // TODO: We cannot just simply elevate the window as this will elevate it over + // Plasma tooltips and other such windows as well + //effects->setElevatedWindow( foundWin, true ); + found = true; + } + if (!found) { + finishHighlighting(); + return; + } + prepareHighlighting(); + if (w) + m_windowOpacity[w] = 1.0; // Because it's not in stackingOrder() yet + + /* TODO: Finish thumbnails of offscreen windows, not sure if it's worth it though + if ( !m_highlightedWindow->isOnCurrentDesktop() ) + { // Window is offscreen, determine thumbnail position + QRect screenArea = effects->clientArea( MaximizeArea ); // Workable area of the active screen + QRect outerArea = outerArea.adjusted( outerArea.width() / 10, outerArea.height() / 10, + -outerArea.width() / 10, -outerArea.height() / 10 ); // Add 10% margin around the edge + QRect innerArea = outerArea.adjusted( outerArea.width() / 40, outerArea.height() / 40, + -outerArea.width() / 40, -outerArea.height() / 40 ); // Outer edge of the thumbnail border (2.5%) + QRect thumbArea = outerArea.adjusted( 20, 20, -20, -20 ); // Outer edge of the thumbnail (20px) + + // Determine the maximum size that we can make the thumbnail within the innerArea + double areaAspect = double( thumbArea.width() ) / double( thumbArea.height() ); + double windowAspect = aspectRatio( m_highlightedWindow ); + QRect thumbRect; // Position doesn't matter right now, but it will later + if ( windowAspect > areaAspect ) + // Top/bottom will touch first + thumbRect = QRect( 0, 0, widthForHeight( thumbArea.height() ), thumbArea.height() ); + else // Left/right will touch first + thumbRect = QRect( 0, 0, thumbArea.width(), heightForWidth( thumbArea.width() )); + if ( thumbRect.width() >= m_highlightedWindow->width() ) + // Area is larger than the window, just use the window's size + thumbRect = m_highlightedWindow->geometry(); + + // Determine position of desktop relative to the current one + QPoint direction = effects->desktopGridCoords( m_highlightedWindow->desktop() ) - + effects->desktopGridCoords( effects->currentDesktop() ); + + // Draw a line from the center of the current desktop to the center of the target desktop. + QPointF desktopLine( 0, 0, direction.x() * screenArea.width(), direction.y() * screenArea.height() ); + desktopLeft.translate( screenArea.width() / 2, screenArea.height() / 2 ); // Move to the screen center + + // Take the point where the line crosses the outerArea, this will be the tip of our arrow + QPointF arrowTip; + QLineF testLine( // Top + outerArea.x(), outerArea.y(), + outerArea.x() + outerArea.width(), outerArea.y() ); + if ( desktopLine.intersect( testLine, &arrowTip ) != QLineF::BoundedIntersection ) + { + testLine = QLineF( // Right + outerArea.x() + outerArea.width(), outerArea.y(), + outerArea.x() + outerArea.width(), outerArea.y() + outerArea.height() ); + if ( desktopLine.intersect( testLine, &arrowTip ) != QLineF::BoundedIntersection ) + { + testLine = QLineF( // Bottom + outerArea.x() + outerArea.width(), outerArea.y() + outerArea.height(), + outerArea.x(), outerArea.y() + outerArea.height() ); + if ( desktopLine.intersect( testLine, &arrowTip ) != QLineF::BoundedIntersection ) + { + testLine = QLineF( // Left + outerArea.x(), outerArea.y() + outerArea.height(), + outerArea.x(), outerArea.y() ); + desktopLine.intersect( testLine, &arrowTip ); // Should never fail + } + } + } + m_arrowTip = arrowTip.toPoint(); + } */ +} + +void HighlightWindowEffect::prepareHighlighting() +{ + // Create window data for every window. Just calling [w] creates it. + m_finishing = false; + foreach (EffectWindow * w, effects->stackingOrder()) { + if (!m_windowOpacity.contains(w)) // Just in case we are still finishing from last time + m_windowOpacity.insert(w, isInitiallyHidden(w) ? 0.0 : 1.0); + if (!m_highlightedWindows.isEmpty()) + m_highlightedWindows.at(0)->addRepaintFull(); + } +} + +void HighlightWindowEffect::finishHighlighting() +{ + m_finishing = true; + m_monitorWindow = nullptr; + m_highlightedWindows.clear(); + if (!m_windowOpacity.isEmpty()) + m_windowOpacity.constBegin().key()->addRepaintFull(); +} + +void HighlightWindowEffect::highlightWindows(const QVector &windows) +{ + if (windows.isEmpty()) { + finishHighlighting(); + return; + } + + m_monitorWindow = nullptr; + m_highlightedWindows.clear(); + m_highlightedIds.clear(); + for (auto w : windows) { + m_highlightedWindows << w; + } + prepareHighlighting(); +} + +bool HighlightWindowEffect::isActive() const +{ + return !(m_windowOpacity.isEmpty() || effects->isScreenLocked()); +} + +bool HighlightWindowEffect::provides(Feature feature) +{ + switch (feature) { + case HighlightWindows: + return true; + default: + return false; + } +} + +bool HighlightWindowEffect::perform(Feature feature, const QVariantList &arguments) +{ + if (feature != HighlightWindows) { + return false; + } + if (arguments.size() != 1) { + return false; + } + highlightWindows(arguments.first().value>()); + return true; +} + +} // namespace diff --git a/effects/highlightwindow/highlightwindow.h b/effects/highlightwindow/highlightwindow.h new file mode 100644 index 0000000..b8b80d3 --- /dev/null +++ b/effects/highlightwindow/highlightwindow.h @@ -0,0 +1,76 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Lucas Murray + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_HIGHLIGHTWINDOW_H +#define KWIN_HIGHLIGHTWINDOW_H + +#include + +namespace KWin +{ + +class HighlightWindowEffect + : public Effect +{ + Q_OBJECT +public: + HighlightWindowEffect(); + ~HighlightWindowEffect() override; + + void prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time) override; + void paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data) override; + bool isActive() const override; + + int requestedEffectChainPosition() const override { + return 70; + } + + bool provides(Feature feature) override; + bool perform(Feature feature, const QVariantList &arguments) override; + +public Q_SLOTS: + void slotWindowAdded(KWin::EffectWindow* w); + void slotWindowClosed(KWin::EffectWindow *w); + void slotWindowDeleted(KWin::EffectWindow *w); + void slotPropertyNotify(KWin::EffectWindow* w, long atom, EffectWindow *addedWindow = nullptr); + +private: + void prepareHighlighting(); + void finishHighlighting(); + + void highlightWindows(const QVector &windows); + + bool m_finishing; + + float m_fadeDuration; + QHash m_windowOpacity; + + long m_atom; + QList m_highlightedWindows; + EffectWindow* m_monitorWindow; + QList m_highlightedIds; + + // Offscreen position cache + /*QRect m_thumbArea; // Thumbnail area + QPoint m_arrowTip; // Position of the arrow's tip + QPoint m_arrowA; // Arrow vertex position at the base (First) + QPoint m_arrowB; // Arrow vertex position at the base (Second) + + // Helper functions + inline double aspectRatio( EffectWindow *w ) + { return w->width() / double( w->height() ); } + inline int widthForHeight( EffectWindow *w, int height ) + { return int(( height / double( w->height() )) * w->width() ); } + inline int heightForWidth( EffectWindow *w, int width ) + { return int(( width / double( w->width() )) * w->height() ); }*/ +}; + +} // namespace + +#endif diff --git a/effects/invert/CMakeLists.txt b/effects/invert/CMakeLists.txt new file mode 100644 index 0000000..09dac3f --- /dev/null +++ b/effects/invert/CMakeLists.txt @@ -0,0 +1,25 @@ +####################################### +# Effect + +####################################### +# Config +set(kwin_invert_config_SRCS invert_config.cpp) + +add_library(kwin_invert_config MODULE ${kwin_invert_config_SRCS}) + +target_link_libraries(kwin_invert_config + KF5::ConfigWidgets + KF5::GlobalAccel + KF5::I18n + KF5::XmlGui + KWinEffectsInterface +) + +kcoreaddons_desktop_to_json(kwin_invert_config invert_config.desktop SERVICE_TYPES kcmodule.desktop) + +install( + TARGETS + kwin_invert_config + DESTINATION + ${PLUGIN_INSTALL_DIR}/kwin/effects/configs +) diff --git a/effects/invert/data/1.10/invert.frag b/effects/invert/data/1.10/invert.frag new file mode 100644 index 0000000..49d7861 --- /dev/null +++ b/effects/invert/data/1.10/invert.frag @@ -0,0 +1,22 @@ +uniform sampler2D sampler; +uniform vec4 modulation; +uniform float saturation; + +varying vec2 texcoord0; + +void main() +{ + vec4 tex = texture2D(sampler, texcoord0); + + if (saturation != 1.0) { + vec3 desaturated = tex.rgb * vec3( 0.30, 0.59, 0.11 ); + desaturated = vec3( dot( desaturated, tex.rgb )); + tex.rgb = tex.rgb * vec3( saturation ) + desaturated * vec3( 1.0 - saturation ); + } + + tex.rgb = vec3(1.0) - tex.rgb; + tex *= modulation; + tex.rgb *= tex.a; + + gl_FragColor = tex; +} diff --git a/effects/invert/data/1.40/invert.frag b/effects/invert/data/1.40/invert.frag new file mode 100644 index 0000000..632f0a3 --- /dev/null +++ b/effects/invert/data/1.40/invert.frag @@ -0,0 +1,25 @@ +#version 140 +uniform sampler2D sampler; +uniform vec4 modulation; +uniform float saturation; + +in vec2 texcoord0; + +out vec4 fragColor; + +void main() +{ + vec4 tex = texture(sampler, texcoord0); + + if (saturation != 1.0) { + vec3 desaturated = tex.rgb * vec3( 0.30, 0.59, 0.11 ); + desaturated = vec3( dot( desaturated, tex.rgb )); + tex.rgb = tex.rgb * vec3( saturation ) + desaturated * vec3( 1.0 - saturation ); + } + + tex.rgb = vec3(1.0) - tex.rgb; + tex *= modulation; + tex.rgb *= tex.a; + + fragColor = tex; +} diff --git a/effects/invert/invert.cpp b/effects/invert/invert.cpp new file mode 100644 index 0000000..8eec12f --- /dev/null +++ b/effects/invert/invert.cpp @@ -0,0 +1,140 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Rivo Laks + SPDX-FileCopyrightText: 2008 Lucas Murray + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "invert.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace KWin +{ + +InvertEffect::InvertEffect() + : m_inited(false), + m_valid(true), + m_shader(nullptr), + m_allWindows(false) +{ + QAction* a = new QAction(this); + a->setObjectName(QStringLiteral("Invert")); + a->setText(i18n("Toggle Invert Effect")); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::CTRL + Qt::META + Qt::Key_I); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::CTRL + Qt::META + Qt::Key_I); + effects->registerGlobalShortcut(Qt::CTRL + Qt::META + Qt::Key_I, a); + connect(a, &QAction::triggered, this, &InvertEffect::toggleScreenInversion); + + QAction* b = new QAction(this); + b->setObjectName(QStringLiteral("InvertWindow")); + b->setText(i18n("Toggle Invert Effect on Window")); + KGlobalAccel::self()->setDefaultShortcut(b, QList() << Qt::CTRL + Qt::META + Qt::Key_U); + KGlobalAccel::self()->setShortcut(b, QList() << Qt::CTRL + Qt::META + Qt::Key_U); + effects->registerGlobalShortcut(Qt::CTRL + Qt::META + Qt::Key_U, b); + connect(b, &QAction::triggered, this, &InvertEffect::toggleWindow); + + connect(effects, &EffectsHandler::windowClosed, this, &InvertEffect::slotWindowClosed); +} + +InvertEffect::~InvertEffect() +{ + delete m_shader; +} + +bool InvertEffect::supported() +{ + return effects->compositingType() == OpenGL2Compositing; +} + +bool InvertEffect::loadData() +{ + m_inited = true; + + m_shader = ShaderManager::instance()->generateShaderFromResources(ShaderTrait::MapTexture, QString(), QStringLiteral("invert.frag")); + if (!m_shader->isValid()) { + qCCritical(KWINEFFECTS) << "The shader failed to load!"; + return false; + } + + return true; +} + +void InvertEffect::drawWindow(EffectWindow* w, int mask, const QRegion ®ion, WindowPaintData& data) +{ + // Load if we haven't already + if (m_valid && !m_inited) + m_valid = loadData(); + + bool useShader = m_valid && (m_allWindows != m_windows.contains(w)); + if (useShader) { + ShaderManager *shaderManager = ShaderManager::instance(); + shaderManager->pushShader(m_shader); + + data.shader = m_shader; + } + + effects->drawWindow(w, mask, region, data); + + if (useShader) { + ShaderManager::instance()->popShader(); + } +} + +void InvertEffect::paintEffectFrame(KWin::EffectFrame* frame, const QRegion ®ion, double opacity, double frameOpacity) +{ + if (m_valid && m_allWindows) { + frame->setShader(m_shader); + ShaderBinder binder(m_shader); + effects->paintEffectFrame(frame, region, opacity, frameOpacity); + } else { + effects->paintEffectFrame(frame, region, opacity, frameOpacity); + } +} + +void InvertEffect::slotWindowClosed(EffectWindow* w) +{ + m_windows.removeOne(w); +} + +void InvertEffect::toggleScreenInversion() +{ + m_allWindows = !m_allWindows; + effects->addRepaintFull(); +} + +void InvertEffect::toggleWindow() +{ + if (!effects->activeWindow()) { + return; + } + if (!m_windows.contains(effects->activeWindow())) + m_windows.append(effects->activeWindow()); + else + m_windows.removeOne(effects->activeWindow()); + effects->activeWindow()->addRepaintFull(); +} + +bool InvertEffect::isActive() const +{ + return m_valid && (m_allWindows || !m_windows.isEmpty()); +} + +bool InvertEffect::provides(Feature f) +{ + return f == ScreenInversion; +} + +} // namespace + diff --git a/effects/invert/invert.h b/effects/invert/invert.h new file mode 100644 index 0000000..aa23f73 --- /dev/null +++ b/effects/invert/invert.h @@ -0,0 +1,64 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Rivo Laks + SPDX-FileCopyrightText: 2008 Lucas Murray + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_INVERT_H +#define KWIN_INVERT_H + +#include + +namespace KWin +{ + +class GLShader; + +/** + * Inverts desktop's colors + */ +class InvertEffect + : public Effect +{ + Q_OBJECT +public: + InvertEffect(); + ~InvertEffect() override; + + void drawWindow(EffectWindow* w, int mask, const QRegion ®ion, WindowPaintData& data) override; + void paintEffectFrame(KWin::EffectFrame* frame, const QRegion ®ion, double opacity, double frameOpacity) override; + bool isActive() const override; + bool provides(Feature) override; + + int requestedEffectChainPosition() const override; + + static bool supported(); + +public Q_SLOTS: + void toggleScreenInversion(); + void toggleWindow(); + void slotWindowClosed(KWin::EffectWindow *w); + +protected: + bool loadData(); + +private: + bool m_inited; + bool m_valid; + GLShader* m_shader; + bool m_allWindows; + QList m_windows; +}; + +inline int InvertEffect::requestedEffectChainPosition() const +{ + return 99; +} + +} // namespace + +#endif diff --git a/effects/invert/invert_config.cpp b/effects/invert/invert_config.cpp new file mode 100644 index 0000000..502ef40 --- /dev/null +++ b/effects/invert/invert_config.cpp @@ -0,0 +1,96 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Rivo Laks + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "invert_config.h" +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include + +K_PLUGIN_FACTORY_WITH_JSON(InvertEffectConfigFactory, + "invert_config.json", + registerPlugin();) + +namespace KWin +{ + +InvertEffectConfig::InvertEffectConfig(QWidget* parent, const QVariantList& args) : + KCModule(KAboutData::pluginData(QStringLiteral("invert")), parent, args) +{ + QVBoxLayout* layout = new QVBoxLayout(this); + + // Shortcut config. The shortcut belongs to the component "kwin"! + KActionCollection *actionCollection = new KActionCollection(this, QStringLiteral("kwin")); + actionCollection->setComponentDisplayName(i18n("KWin")); + + QAction* a = actionCollection->addAction(QStringLiteral("Invert")); + a->setText(i18n("Toggle Invert Effect")); + a->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::CTRL + Qt::META + Qt::Key_I); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::CTRL + Qt::META + Qt::Key_I); + + QAction* b = actionCollection->addAction(QStringLiteral("InvertWindow")); + b->setText(i18n("Toggle Invert Effect on Window")); + b->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(b, QList() << Qt::CTRL + Qt::META + Qt::Key_U); + KGlobalAccel::self()->setShortcut(b, QList() << Qt::CTRL + Qt::META + Qt::Key_U); + + mShortcutEditor = new KShortcutsEditor(actionCollection, this, + KShortcutsEditor::GlobalAction, KShortcutsEditor::LetterShortcutsDisallowed); + connect(mShortcutEditor, &KShortcutsEditor::keyChange, this, &InvertEffectConfig::markAsChanged); + layout->addWidget(mShortcutEditor); + + load(); +} + +InvertEffectConfig::~InvertEffectConfig() +{ + // Undo (only) unsaved changes to global key shortcuts + mShortcutEditor->undoChanges(); +} + +void InvertEffectConfig::load() +{ + KCModule::load(); + + emit changed(false); +} + +void InvertEffectConfig::save() +{ + KCModule::save(); + + mShortcutEditor->save(); // undo() will restore to this state from now on + + emit changed(false); + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("invert")); +} + +void InvertEffectConfig::defaults() +{ + mShortcutEditor->allDefault(); + + emit changed(true); +} + + +} // namespace + +#include "invert_config.moc" diff --git a/effects/invert/invert_config.desktop b/effects/invert/invert_config.desktop new file mode 100644 index 0000000..a975a40 --- /dev/null +++ b/effects/invert/invert_config.desktop @@ -0,0 +1,90 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=KCModule + +X-KDE-Library=kwin_invert_config +X-KDE-ParentComponents=invert + +Name=Invert +Name[af]=Keer om +Name[ar]=اعكس +Name[az]=Neqativ +Name[be]=Інвертаваць +Name[be@latin]=Invercyja +Name[bg]=Обръщане на цвета +Name[bs]=Izvrtanje +Name[ca]=Inverteix +Name[ca@valencia]=Inverteix +Name[cs]=Invertovat +Name[csb]=Inwertëje +Name[da]=Invertér +Name[de]=Invertieren +Name[el]=Αντιστροφή +Name[en_GB]=Invert +Name[eo]=Inversigi +Name[es]=Invertir +Name[et]=Teistpidi +Name[eu]=Alderantzikatu +Name[fa]=وارونه +Name[fi]=Käänteiset värit +Name[fr]=Inverse +Name[fy]=Omdraaie +Name[ga]=Invert +Name[gl]=Inverter +Name[gu]=ઉલ્ટું +Name[he]=היפוך צבעים +Name[hi]=उलटें +Name[hne]=उलटव +Name[hr]=Izokretanje +Name[hu]=Invertálás +Name[ia]=Inverte +Name[id]=Kebalikan +Name[is]=Umhverfa +Name[it]=Inverti +Name[ja]=色調反転 +Name[kk]=Терістеу +Name[km]=ដាក់​បញ្ច្រាស +Name[kn]=ವಿಲೋಮಗೊಳಿಸು (ಇನ್ವರ್ಟ್) +Name[ko]=반전 +Name[ku]=Vajî +Name[lt]=Invertavimas +Name[lv]=Invertēt +Name[mai]=उनटू +Name[mk]=Инвертирање +Name[ml]=വര്‍ണ്ണങ്ങളെ തലതിരിക്കുക +Name[mr]=उलट +Name[nb]=Snu om +Name[nds]=Ümdreihen +Name[ne]=उल्टाउनुहोस् +Name[nl]=Omkeren +Name[nn]=Snu fargane +Name[oc]=Enversar +Name[pa]=ਉਲਟ +Name[pl]=Odwróć +Name[pt]=Inverter +Name[pt_BR]=Inverter +Name[ro]=Inversare +Name[ru]=Инверсия +Name[se]=Jorgalahte +Name[si]=යටිකුරු කිරීම +Name[sk]=InvertovaÅ¥ +Name[sl]=Obrni +Name[sr]=Извртање +Name[sr@ijekavian]=Извртање +Name[sr@ijekavianlatin]=Izvrtanje +Name[sr@latin]=Izvrtanje +Name[sv]=Invertera +Name[ta]=திருப்பு +Name[te]=ఇన్వర్‍ట్ +Name[th]=กลับค่าสี +Name[tr]=Negatifleştir +Name[ug]=ئەكسى رەڭگە ئالماشتۇر +Name[uk]=Інверсія +Name[uz]=Teskari +Name[uz@cyrillic]=Тескари +Name[vi]=Đảo ngược +Name[wa]=Å rvier +Name[x-test]=xxInvertxx +Name[zh_CN]=反向选择 +Name[zh_TW]=反轉 + diff --git a/effects/invert/invert_config.h b/effects/invert/invert_config.h new file mode 100644 index 0000000..421878b --- /dev/null +++ b/effects/invert/invert_config.h @@ -0,0 +1,38 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Rivo Laks + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_INVERT_CONFIG_H +#define KWIN_INVERT_CONFIG_H + +#include + +class KShortcutsEditor; + +namespace KWin +{ + +class InvertEffectConfig : public KCModule +{ + Q_OBJECT +public: + explicit InvertEffectConfig(QWidget* parent = nullptr, const QVariantList& args = QVariantList()); + ~InvertEffectConfig() override; + +public Q_SLOTS: + void save() override; + void load() override; + void defaults() override; + +private: + KShortcutsEditor* mShortcutEditor; +}; + +} // namespace + +#endif diff --git a/effects/kscreen/CMakeLists.txt b/effects/kscreen/CMakeLists.txt new file mode 100644 index 0000000..a6f8653 --- /dev/null +++ b/effects/kscreen/CMakeLists.txt @@ -0,0 +1,10 @@ +####################################### +# Effect + +# Source files +set(kwin4_effect_builtins_sources ${kwin4_effect_builtins_sources} + kscreen/kscreen.cpp +) + +kconfig_add_kcfg_files(kwin4_effect_builtins_sources kscreen/kscreenconfig.kcfgc) + diff --git a/effects/kscreen/kscreen.cpp b/effects/kscreen/kscreen.cpp new file mode 100644 index 0000000..affa73d --- /dev/null +++ b/effects/kscreen/kscreen.cpp @@ -0,0 +1,181 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +// own +#include "kscreen.h" +// KConfigSkeleton +#include "kscreenconfig.h" + +/** + * How this effect works: + * + * Effect announces that it is around through property _KDE_KWIN_KSCREEN_SUPPORT on the root window. + * + * KScreen watches for this property and when it wants to adjust screens, KScreen goes + * through the following protocol: + * 1. KScreen sets the property value to 1 + * 2. Effect starts to fade out all windows + * 3. When faded out the effect sets property value to 2 + * 4. KScreen adjusts the screens + * 5. KScreen sets property value to 3 + * 6. Effect starts to fade in all windows again + * 7. Effect sets back property value to 0 + * + * The property has type 32 bits cardinal. To test it use: + * xprop -root -f _KDE_KWIN_KSCREEN_SUPPORT 32c -set _KDE_KWIN_KSCREEN_SUPPORT 1 + * + * The states are: + * 0: normal + * 1: fading out + * 2: faded out + * 3: fading in + */ + +namespace KWin +{ + +KscreenEffect::KscreenEffect() + : Effect() + , m_state(StateNormal) + , m_atom(effects->announceSupportProperty("_KDE_KWIN_KSCREEN_SUPPORT", this)) +{ + initConfig(); + connect(effects, &EffectsHandler::propertyNotify, this, &KscreenEffect::propertyNotify); + connect(effects, &EffectsHandler::xcbConnectionChanged, this, + [this] { + m_atom = effects->announceSupportProperty(QByteArrayLiteral("_KDE_KWIN_KSCREEN_SUPPORT"), this); + } + ); + reconfigure(ReconfigureAll); +} + +KscreenEffect::~KscreenEffect() +{ +} + +void KscreenEffect::reconfigure(ReconfigureFlags flags) +{ + Q_UNUSED(flags) + + KscreenConfig::self()->read(); + m_timeLine.setDuration( + std::chrono::milliseconds(animationTime(250))); +} + +void KscreenEffect::prePaintScreen(ScreenPrePaintData &data, int time) +{ + if (m_state == StateFadingIn || m_state == StateFadingOut) { + m_timeLine.update(std::chrono::milliseconds(time)); + if (m_timeLine.done()) { + switchState(); + } + } + effects->prePaintScreen(data, time); +} + +void KscreenEffect::postPaintScreen() +{ + if (m_state == StateFadingIn || m_state == StateFadingOut) { + effects->addRepaintFull(); + } +} + +void KscreenEffect::prePaintWindow(EffectWindow *w, WindowPrePaintData &data, int time) +{ + if (m_state != StateNormal) { + data.setTranslucent(); + } + effects->prePaintWindow(w, data, time); +} + +void KscreenEffect::paintWindow(EffectWindow *w, int mask, QRegion region, WindowPaintData &data) +{ + //fade to black and fully opaque + switch (m_state) { + case StateFadingOut: + data.setOpacity(data.opacity() + (1.0 - data.opacity()) * m_timeLine.value()); + data.multiplyBrightness(1.0 - m_timeLine.value()); + break; + case StateFadedOut: + data.multiplyOpacity(0.0); + data.multiplyBrightness(0.0); + break; + case StateFadingIn: + data.setOpacity(data.opacity() + (1.0 - data.opacity()) * (1.0 - m_timeLine.value())); + data.multiplyBrightness(m_timeLine.value()); + break; + default: + // no adjustment + break; + } + effects->paintWindow(w, mask, region, data); +} + +void KscreenEffect::propertyNotify(EffectWindow *window, long int atom) +{ + if (window || atom != m_atom || m_atom == XCB_ATOM_NONE) { + return; + } + QByteArray byteData = effects->readRootProperty(m_atom, XCB_ATOM_CARDINAL, 32); + const uint32_t *data = byteData.isEmpty() ? nullptr : reinterpret_cast(byteData.data()); + if (!data // Property was deleted + || data[0] == 0) { // normal state - KWin should have switched to it + if (m_state != StateNormal) { + m_state = StateNormal; + effects->addRepaintFull(); + } + return; + } + if (data[0] == 2) { + // faded out state - KWin should have switched to it + if (m_state != StateFadedOut) { + m_state = StateFadedOut; + effects->addRepaintFull(); + } + return; + } + if (data[0] == 1) { + // kscreen wants KWin to fade out all windows + m_state = StateFadingOut; + m_timeLine.reset(); + effects->addRepaintFull(); + return; + } + if (data[0] == 3) { + // kscreen wants KWin to fade in again + m_state = StateFadingIn; + m_timeLine.reset(); + effects->addRepaintFull(); + return; + } + qCDebug(KWINEFFECTS) << "Incorrect Property state, immediate stop: " << data[0]; + m_state = StateNormal; + effects->addRepaintFull(); +} + +void KscreenEffect::switchState() +{ + long value = -1l; + if (m_state == StateFadingOut) { + m_state = StateFadedOut; + value = 2l; + } else if (m_state == StateFadingIn) { + m_state = StateNormal; + value = 0l; + } + if (value != -1l && m_atom != XCB_ATOM_NONE) { + xcb_change_property(xcbConnection(), XCB_PROP_MODE_REPLACE, x11RootWindow(), m_atom, XCB_ATOM_CARDINAL, 32, 1, &value); + } +} + +bool KscreenEffect::isActive() const +{ + return m_state != StateNormal; +} + +} // namespace KWin diff --git a/effects/kscreen/kscreen.h b/effects/kscreen/kscreen.h new file mode 100644 index 0000000..8307423 --- /dev/null +++ b/effects/kscreen/kscreen.h @@ -0,0 +1,55 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_KSCREEN_H +#define KWIN_KSCREEN_H + +#include + +namespace KWin +{ + +class KscreenEffect : public Effect +{ + Q_OBJECT + +public: + KscreenEffect(); + ~KscreenEffect() override; + + void prePaintScreen(ScreenPrePaintData &data, int time) override; + void postPaintScreen() override; + void prePaintWindow(EffectWindow *w, WindowPrePaintData &data, int time) override; + void paintWindow(EffectWindow *w, int mask, QRegion region, WindowPaintData &data) override; + + void reconfigure(ReconfigureFlags flags) override; + bool isActive() const override; + + int requestedEffectChainPosition() const override { + return 99; + } + +private Q_SLOTS: + void propertyNotify(KWin::EffectWindow *window, long atom); + +private: + void switchState(); + enum FadeOutState { + StateNormal, + StateFadingOut, + StateFadedOut, + StateFadingIn + }; + TimeLine m_timeLine; + FadeOutState m_state; + xcb_atom_t m_atom; +}; + + +} // namespace KWin +#endif // KWIN_KSCREEN_H diff --git a/effects/kscreen/kscreen.kcfg b/effects/kscreen/kscreen.kcfg new file mode 100644 index 0000000..9aebc06 --- /dev/null +++ b/effects/kscreen/kscreen.kcfg @@ -0,0 +1,12 @@ + + + + + + 0 + + + diff --git a/effects/kscreen/kscreenconfig.kcfgc b/effects/kscreen/kscreenconfig.kcfgc new file mode 100644 index 0000000..c2b8bbb --- /dev/null +++ b/effects/kscreen/kscreenconfig.kcfgc @@ -0,0 +1,5 @@ +File=kscreen.kcfg +ClassName=KscreenConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/effects/kwineffect.desktop b/effects/kwineffect.desktop new file mode 100644 index 0000000..b74b53c --- /dev/null +++ b/effects/kwineffect.desktop @@ -0,0 +1,107 @@ +[Desktop Entry] +Type=ServiceType +X-KDE-ServiceType=KWin/Effect + +Comment=KWin Effect +Comment[af]=KWin-effek +Comment[ar]=تأثيرات كوين +Comment[az]=KWin Effekti +Comment[be]=Эфекты KWin +Comment[be@latin]=Efekty akońnika „KWin” +Comment[bg]=Ефект KWin +Comment[bn_IN]=KWin ইফেক্ট +Comment[bs]=Efekti K‑vina +Comment[ca]=Efecte del KWin +Comment[ca@valencia]=Efecte de KWin +Comment[cs]=KWin efekt +Comment[csb]=Efektë òknów +Comment[da]=KWin-effekt +Comment[de]=KWin-Effekt +Comment[el]=Εφέ KWin +Comment[en_GB]=KWin Effect +Comment[eo]=KWin efekto +Comment[es]=Efecto de KWin +Comment[et]=KWin'i efektid +Comment[eu]=KWin efektua +Comment[fa]=جلوه‌های KWin +Comment[fi]=KWin-tehoste +Comment[fr]=Effet KWin +Comment[fy]=KWin Effekt +Comment[ga]=Maisíocht KWin +Comment[gl]=Efecto de KWin +Comment[gu]=KWin અસરો +Comment[he]=אפקט של KWin +Comment[hi]=के-विन प्रभाव +Comment[hne]=के-विन प्रभाव +Comment[hr]=KWin efekt +Comment[hu]=KWin-effekt +Comment[ia]=Effecto de KWin +Comment[id]=Efek KWin +Comment[is]=KWin áhrif +Comment[it]=Effetto di KWin +Comment[ja]=KWin 効果 +Comment[kk]=KWin эффекті +Comment[km]=បែបផែន KWin +Comment[kn]=ಕೆವಿನ್ ಪರಿಣಾಮ +Comment[ko]=KWin 효과 +Comment[ku]=Efektê KWin +Comment[lt]=KWin efektas +Comment[lv]=KWin efekts +Comment[mai]=के-विन प्रभाव +Comment[mk]=Ефект за KWin +Comment[ml]=കെവിന്‍ പ്രഭാവം +Comment[mr]=के-विन परिणाम +Comment[nb]=KWin-effekt +Comment[nds]=KWin-Effekt +Comment[ne]=केडीई विन प्रभाव +Comment[nl]=KWin-effect +Comment[nn]=KWin-effekt +Comment[pa]=KWin ਪਰਭਾਵ +Comment[pl]=Efekty KWin +Comment[pt]=Efeito do KWin +Comment[pt_BR]=Efeito do KWin +Comment[ro]=Efect KWin +Comment[ru]=Эффект диспетчера окон +Comment[se]=KWin-effeavttat +Comment[si]=KWin සැරසිලි +Comment[sk]=Efekty KWin +Comment[sl]=Učinek KWin +Comment[sr]=Ефекти К‑вина +Comment[sr@ijekavian]=Ефекти К‑вина +Comment[sr@ijekavianlatin]=Efekti KWina +Comment[sr@latin]=Efekti KWina +Comment[sv]=Kwin-effekt +Comment[ta]=கேவின் தாக்கங்கள் +Comment[te]=KWin ప్రభావం +Comment[th]=ลูกเล่นของ KWin +Comment[tr]=KWin Efekti +Comment[ug]=KWin ئۈنۈمى +Comment[uk]=Ефект KWin +Comment[uz]=KWin effektlari +Comment[uz@cyrillic]=KWin эффектлари +Comment[vi]=Hiệu ứng KWin +Comment[wa]=Efet KWin +Comment[x-test]=xxKWin Effectxx +Comment[zh_CN]=KWin 效果 +Comment[zh_TW]=KWin 效果 + +[PropertyDef::X-KDE-Ordering] +Type=int + +[PropertyDef::X-KWin-Requires-OpenGL] +Type=bool + +[PropertyDef::X-KWin-Requires-OpenGL2] +Type=bool + +[PropertyDef::X-KWin-Requires-Shaders] +Type=bool + +[PropertyDef::X-KWin-Video-Url] +Type=QString + +[PropertyDef::X-KWin-Exclusive-Category] +Type=QString + +[PropertyDef::X-KWin-Internal] +Type=bool diff --git a/effects/logging.cpp b/effects/logging.cpp new file mode 100644 index 0000000..78f5b35 --- /dev/null +++ b/effects/logging.cpp @@ -0,0 +1,10 @@ +/* + 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 +Q_LOGGING_CATEGORY(KWINEFFECTS, "kwineffects", QtCriticalMsg) diff --git a/effects/login/package/contents/code/main.js b/effects/login/package/contents/code/main.js new file mode 100644 index 0000000..a4b91a8 --- /dev/null +++ b/effects/login/package/contents/code/main.js @@ -0,0 +1,79 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Lubos Lunak + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +"use strict"; + +var loginEffect = { + duration: animationTime(1000), + isFadeToBlack: false, + loadConfig: function () { + loginEffect.isFadeToBlack = effect.readConfig("FadeToBlack", false); + }, + isLoginSplash: function (window) { + var windowClass = window.windowClass; + if (windowClass === "ksplashx ksplashx") { + return true; + } + if (windowClass === "ksplashsimple ksplashsimple") { + return true; + } + if (windowClass === "ksplashqml ksplashqml") { + return true; + } + return false; + }, + fadeOut: function (window) { + animate({ + window: window, + duration: loginEffect.duration, + type: Effect.Opacity, + from: 1.0, + to: 0.0 + }); + }, + fadeToBlack: function (window) { + animate({ + window: window, + duration: loginEffect.duration / 2, + animations: [{ + type: Effect.Brightness, + from: 1.0, + to: 0.0 + }, { + type: Effect.Opacity, + from: 1.0, + to: 0.0, + delay: loginEffect.duration / 2 + }, { + // TODO: is there a better way to keep brightness constant? + type: Effect.Brightness, + from: 0.0, + to: 0.0, + delay: loginEffect.duration / 2 + }] + }); + }, + closed: function (window) { + if (!loginEffect.isLoginSplash(window)) { + return; + } + if (loginEffect.isFadeToBlack === true) { + loginEffect.fadeToBlack(window); + } else { + loginEffect.fadeOut(window); + } + }, + init: function () { + effect.configChanged.connect(loginEffect.loadConfig); + effects.windowClosed.connect(loginEffect.closed); + loginEffect.loadConfig(); + } +}; +loginEffect.init(); diff --git a/effects/login/package/contents/config/main.xml b/effects/login/package/contents/config/main.xml new file mode 100644 index 0000000..626a7f0 --- /dev/null +++ b/effects/login/package/contents/config/main.xml @@ -0,0 +1,12 @@ + + + + + + false + + + diff --git a/effects/login/package/contents/ui/config.ui b/effects/login/package/contents/ui/config.ui new file mode 100644 index 0000000..07cb5f1 --- /dev/null +++ b/effects/login/package/contents/ui/config.ui @@ -0,0 +1,38 @@ + + + KWin::LoginEffectConfigForm + + + + 0 + 0 + 400 + 160 + + + + + + + Fade to black (fullscreen splash screens only) + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/effects/login/package/metadata.desktop b/effects/login/package/metadata.desktop new file mode 100644 index 0000000..07b3f57 --- /dev/null +++ b/effects/login/package/metadata.desktop @@ -0,0 +1,173 @@ +[Desktop Entry] +Name=Login +Name[af]=Aanteken +Name[ar]=ولوج +Name[ast]=Aniciu de sesión +Name[az]=Sistemə Giriş +Name[be]=Уваход +Name[be@latin]=Uvachod +Name[bg]=Вход +Name[bn]=লগ-ইন +Name[bn_IN]=লগ-ইন করুন +Name[bs]=Prijava +Name[ca]=Entrada +Name[ca@valencia]=Entrada +Name[cs]=Přihlášení +Name[csb]=Logòwanié +Name[da]=Login +Name[de]=Anmelden +Name[el]=Σύνδεση +Name[en_GB]=Login +Name[eo]=Ensaluto +Name[es]=Acceso +Name[et]=Sisselogimine +Name[eu]=Saio-hasiera +Name[fi]=Sisäänkirjautuminen +Name[fr]=Connexion +Name[fy]=Ynlogge +Name[ga]=Logáil Isteach +Name[gl]=Acceso +Name[gu]=પ્રવેશ +Name[he]=כניסה +Name[hi]=लॉगइन +Name[hne]=लागइन +Name[hr]=Prijava +Name[hsb]=Přizjewjenje +Name[hu]=Bejelentkezés +Name[ia]=Accesso de identification +Name[id]=Login +Name[is]=Innskráning +Name[it]=Accesso +Name[ja]=ログイン +Name[kk]=Кіру +Name[km]=ចូល +Name[kn]=ಪ್ರವೇಶಿಸು (ಲಾಗಿನ್) +Name[ko]=로그인 +Name[ku]=Têketin +Name[lt]=Prisijungimas +Name[lv]=Pieteikties +Name[mai]=लागिन +Name[mk]=Најавување +Name[ml]=അകത്തുകയറുക +Name[mr]=प्रवेश +Name[nb]=Logg inn +Name[nds]=Anmellen +Name[ne]=लगइन गर्नुहोस् +Name[nl]=Aanmelden +Name[nn]=Innlogging +Name[oc]=Connexion +Name[or]=ଲଗଇନ +Name[pa]=ਲਾਗਇਨ +Name[pl]=Logowanie +Name[pt]=Arranque +Name[pt_BR]=Início de sessão +Name[ro]=Autentificare +Name[ru]=Вход в систему +Name[se]=Sisačáliheapmi +Name[si]=පිවිසුම +Name[sk]=Prihlásenie +Name[sl]=Prijava +Name[sr]=Пријава +Name[sr@ijekavian]=Пријава +Name[sr@ijekavianlatin]=Prijava +Name[sr@latin]=Prijava +Name[sv]=Inloggning +Name[ta]=நுழைக +Name[te]=లాగిన్ +Name[tg]=Воридшавӣ +Name[th]=ลงบันทึกเข้าระบบ +Name[tr]=Giriş +Name[ug]=كىرىش +Name[uk]=Вхід +Name[uz]=Kirish +Name[uz@cyrillic]=Кириш +Name[vi]=Đăng nhập +Name[wa]=Elodjaedje +Name[x-test]=xxLoginxx +Name[zh_CN]=登录时 +Name[zh_TW]=登入 +Icon=preferences-system-windows-effect-login +Comment=Smoothly fade to the desktop when logging in +Comment[ar]=اظهار سطح المكتب بنعومة عند الولوج +Comment[az]=Sistemə giriş zamanı İş Masasının tədricən görünməsi +Comment[bg]=Плавен преход към работния плот при влизане +Comment[bs]=Glatko pretapa na povrÅ¡ pri prijavljivanju +Comment[ca]=Transició suau a l'escriptori en connectar-se +Comment[ca@valencia]=Transició suau a l'escriptori en connectar-se +Comment[cs]=Plynule zobrazit plochu po přihlášení +Comment[da]=Toner blidt til skrivebordet nÃ¥r der logges ind +Comment[de]=Blendet die Arbeitsfläche nach der Anmeldung langsam ein. +Comment[el]=Ομαλή εμφάνιση της επιφάνειας εργασίας κατά τη σύνδεση +Comment[en_GB]=Smoothly fade to the desktop when logging in +Comment[es]=Desvanece el escritorio cuando se inicia la sesión +Comment[et]=Töölaua sujuv ilmumine sisselogimisel +Comment[eu]=Emeki desagertu mahaigainerantz saio-hastean +Comment[fi]=Häivytä pehmeästi työpöydälle kirjauduttaessa sisään +Comment[fr]=Effectue un dégradé progressif vers le bureau lors de la connexion +Comment[fy]=Lit it opstartskerm ferdizenje nei it buroblêd ûnder it oanmelden +Comment[ga]=Céimnigh go dtí an deasc go réidh ag am logála isteach +Comment[gl]=Suaviza a entrada no escritorio cun efecto de esvaecemento +Comment[gu]=જ્યારે પ્રવેશ કરવામાં આવે ત્યારે ડેસ્કટોપને સરળ રીતે ઝાંખું કરે છે +Comment[he]=עמעום המסך בהדרגה בעת הכניסה למערכת +Comment[hne]=जब लागइन होथे तब डेस्कटाप मं धीरे से फेड होथे +Comment[hr]=Lagano pojavljivanje radne povrÅ¡ine prilikom prijave na sustav +Comment[hu]=Folyamatos átmenet az asztalra bejelentkezéskor +Comment[ia]=Dulcemente pallidi le scriptorio quando tu accede in identification +Comment[id]=Melesap dengan halus ke desktop ketika memasuki log +Comment[is]=Láta skjáborð koma mjúklega í ljós við innstimplun +Comment[it]=Dissolvenza graduale del desktop all'accesso +Comment[ja]=ログイン時に滑らかにデスクトップを表示します +Comment[kk]=Кіргенде үстелге біртіндеп ауысу +Comment[km]=លិច​​បន្តិចម្ដងៗ​ទៅ​ផ្ទៃតុ​នៅពេល​ចូល +Comment[kn]=ಪ್ರವೇಶಿಸುವಾಗ (ಲಾಗಿಂಗ್ ಇನ್), ನಾಜೂಕಾಗಿ ಗಣಕತೆರೆಗೆ ಮಸುಕುಗೊಳಿಸು +Comment[ko]=로그인할 때 부드럽게 바탕 화면을 보여 줍니다 +Comment[lt]=Prisijungiant, glotniai pamažu iÅ¡ryÅ¡kina darbalaukį +Comment[lv]=Gludeni parādÄ«t darbvirsmu, piesakoties +Comment[ml]=അകത്തേക്ക് കടക്കുംബോള്‍ പണിയിടത്തിലേക്ക് തനിയെ സ്മൂത് ആയി കടക്കുന്നു +Comment[mr]=प्रवेश करताना डेस्कटॉप फीका करा +Comment[nb]=Ton jevnt inn til skrivebordet nÃ¥r det logges inn +Comment[nds]=Bi't Anmellen week na den Schriefdisch överblennen +Comment[nl]=Laat het opstartscherm vervagen naar het opkomende bureaublad tijdens het aanmelden +Comment[nn]=Ton inn skrivebordet ved innlogging +Comment[pa]=ਜਦੋਂ ਲਾਗਇਨ ਕਰਨਾ ਹੋਵੇ ਤਾਂ ਹੌਲੀ ਹੌਲੀ ਡੈਸਕਟਾਪ ਨੂੰ ਫਿੱਕਾ ਕਰੋ +Comment[pl]=Płynne rozjaśnianie do pulpitu podczas logowania +Comment[pt]=Desvanecer suavemente para o ecrã ao ligar-se +Comment[pt_BR]=Suaviza o desaparecimento da área de trabalho ao fazer a autenticação +Comment[ro]=Estompează lin biroul la autentificare +Comment[ru]=Плавное проявление рабочего стола при входе в систему +Comment[si]=පිවිසීමේදී වැඩතලයට සුමුදු අවපැහැකිරීමක් ලබාදෙන්න +Comment[sk]=Plynule zobrazí plochu pri prihlásení +Comment[sl]=Pri prijavi se namizje prikaže postopoma +Comment[sr]=Глатко претапа на површ при пријављивању +Comment[sr@ijekavian]=Глатко претапа на површ при пријављивању +Comment[sr@ijekavianlatin]=Glatko pretapa na povrÅ¡ pri prijavljivanju +Comment[sr@latin]=Glatko pretapa na povrÅ¡ pri prijavljivanju +Comment[sv]=Tona mjukt till skrivbordet vid inloggning +Comment[ta]=Smoothly fade to the desktop when logging in +Comment[te]=లాగిన్ అవుతున్నప్పుడు రంగస్థలమునకు సున్నితంగా ఫేడ్ చేయుము +Comment[th]=ค่อย ๆ ปรับภาพพื้นที่ทำงานให้ชัดขึ้นอย่างนุ่มนวลเมื่อทำการล็อกอิน +Comment[tr]=Giriş yapılırken masaüstünü pürüzsüzce belirginleştir +Comment[ug]=تىزىمغا كىرگەندە ئۈستەلئۈستىگە تەكشى سۇسلاشتۇر +Comment[uk]=Плавна поява стільниці під час входу +Comment[vi]=Làm mờ dần màn hình khi đăng nhập +Comment[wa]=Dous fondou viè l' sicribanne a l' elodjaedje +Comment[x-test]=xxSmoothly fade to the desktop when logging inxx +Comment[zh_CN]=登录时平滑淡入到桌面 +Comment[zh_TW]=登入時平順地淡入桌面 + +Type=Service +X-KDE-ServiceTypes=KWin/Effect,KCModule +X-KDE-PluginInfo-Author=Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin +X-KDE-PluginInfo-Email=l.lunak@kde.org, kde@privat.broulik.de, mgraesslin@kde.org +X-KDE-PluginInfo-Name=kwin4_effect_login +X-KDE-PluginInfo-Version=0.2.0 +X-KDE-PluginInfo-Category=Appearance +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-EnabledByDefault=true +X-KDE-Ordering=40 +X-Plasma-API=javascript +X-Plasma-MainScript=code/main.js +X-KDE-PluginKeyword=kwin4_effect_login +X-KDE-Library=kcm_kwin4_genericscripted +X-KDE-ParentComponents=kwin4_effect_login +X-KWin-Config-TranslationDomain=kwin_effects diff --git a/effects/logout/package/contents/code/main.js b/effects/logout/package/contents/code/main.js new file mode 100644 index 0000000..a80cf1c --- /dev/null +++ b/effects/logout/package/contents/code/main.js @@ -0,0 +1,71 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Lubos Lunak + SPDX-FileCopyrightText: 2013 Martin Gräßlin + SPDX-FileCopyrightText: 2017 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +"use strict"; + +var logoutEffect = { + inDuration: animationTime(800), + outDuration: animationTime(400), + loadConfig: function () { + logoutEffect.inDuration = animationTime(800); + logoutEffect.outDuration = animationTime(400); + }, + isLogoutWindow: function (window) { + if (window.windowClass === "ksmserver ksmserver") { + return true; + } + if (window.windowClass === "ksmserver-logout-greeter ksmserver-logout-greeter") { + return true; + } + return false; + }, + opened: function (window) { + if (!logoutEffect.isLogoutWindow(window)) { + return; + } + // If the Out animation is still active, kill it. + if (window.outAnimation !== undefined) { + cancel(window.outAnimation); + delete window.outAnimation; + } + window.inAnimation = animate({ + window: window, + duration: logoutEffect.inDuration, + type: Effect.Opacity, + from: 0.0, + to: 1.0 + }); + }, + closed: function (window) { + if (!logoutEffect.isLogoutWindow(window)) { + return; + } + // If the In animation is still active, kill it. + if (window.inAnimation !== undefined) { + cancel(window.inAnimation); + delete window.inAnimation; + } + window.outAnimation = animate({ + window: window, + duration: logoutEffect.outDuration, + type: Effect.Opacity, + from: 1.0, + to: 0.0 + }); + }, + init: function () { + logoutEffect.loadConfig(); + effects.windowAdded.connect(logoutEffect.opened); + effects.windowClosed.connect(logoutEffect.closed); + } +}; +logoutEffect.init(); + diff --git a/effects/logout/package/metadata.desktop b/effects/logout/package/metadata.desktop new file mode 100644 index 0000000..3cb9c11 --- /dev/null +++ b/effects/logout/package/metadata.desktop @@ -0,0 +1,86 @@ +[Desktop Entry] +Name=Logout +Name[ast]=Zarru de sesión +Name[az]=Sistemdən Çıxış +Name[ca]=Sortida +Name[ca@valencia]=Eixida +Name[cs]=Odhlášení +Name[da]=Log ud +Name[de]=Abmeldung +Name[el]=Αποσύνδεση +Name[en_GB]=Logout +Name[es]=Cerrar la sesión +Name[et]=Väljalogimine +Name[eu]=Saio-ixtea +Name[fi]=Kirjaudu ulos +Name[fr]=Déconnexion +Name[gl]=Saír +Name[hu]=Kijelentkezés +Name[ia]=Clausura de session +Name[id]=Logout +Name[it]=Uscita +Name[ko]=로그아웃 +Name[lt]=Atsijungimas +Name[nl]=Afmelden +Name[nn]=Logg ut +Name[pl]=Wylogowywanie +Name[pt]=Encerrar +Name[pt_BR]=Encerrar sessão +Name[ro]=Ieșire din sistem +Name[ru]=Завершение работы +Name[sk]=OdhlásiÅ¥ sa +Name[sl]=Odjava +Name[sv]=Logga ut +Name[uk]=Вихід +Name[x-test]=xxLogoutxx +Name[zh_CN]=注销 +Name[zh_TW]=登出 +Icon=preferences-system-windows-effect-logout +Comment=Smoothly fade to the logout screen +Comment[az]=Sesiyadan çıxış ekranının tədricən görünmsəi +Comment[ca]=Transició suau a la pantalla de sortida +Comment[ca@valencia]=Transició suau a la pantalla d'eixida +Comment[cs]=Plynule přejít na odhlaÅ¡ovací obrazovku +Comment[da]=Toner blidt til log ud-skærmen +Comment[de]=Blendet den Abmeldungsdialog langsam ein. +Comment[el]=Ομαλή εμφάνιση της οθόνης αποσύνδεσης +Comment[en_GB]=Smoothly fade to the logout screen +Comment[es]=Desvanecer suavemente hasta la pantalla de cierre de sesión +Comment[et]=Sujuv hääbumine sisselogimisekraani ilmumiseni +Comment[eu]=Emeki desagertu saio-ixteko pantailarantz +Comment[fi]=Häivytä pehmeästi kirjautumisikkunaan +Comment[fr]=Effectue un dégradé progressif vers l'écran de déconnexion +Comment[gl]=Suaviza a aparición da pantalla de saída +Comment[hu]=Folyamatos átmenet a kijelentkező képernyőre +Comment[ia]=Dulcemente pallidi al schermo de abandono (logout) +Comment[id]=Melesap secara halus ke layar logout +Comment[it]=Dissolvenza graduale alla schermata di uscita +Comment[ko]=로그아웃 화면으로 부드럽게 전환합니다 +Comment[lt]=Glotniai pamažu parodo atsijungimo ekraną +Comment[nl]=Langzaam naar het afmeldscherm vervagen +Comment[nn]=Ton ut til utloggingsbiletet +Comment[pl]=Płynne zanikanie do ekranu wylogowywania +Comment[pt]=Desvanecer suavemente para o ecrã de encerramento +Comment[pt_BR]=Suaviza o desaparecimento para a tela de encerramento da sessão +Comment[ro]=Estompează lin spre ecranul de ieșire +Comment[ru]=Плавное появление экрана завершения работы +Comment[sk]=Plynule zobrazí plochu pri odhlásení +Comment[sl]=Postopoma okno zbledi v zaslon za odjavo +Comment[sv]=Tona mjukt till utloggningsskärmen +Comment[uk]=Плавний перехід до вікна виходу з системи +Comment[x-test]=xxSmoothly fade to the logout screenxx +Comment[zh_CN]=平滑地淡出到注销屏幕 +Comment[zh_TW]=平順地淡入登出畫面 + +Type=Service +X-KDE-ServiceTypes=KWin/Effect +X-KDE-PluginInfo-Author=Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin +X-KDE-PluginInfo-Email=l.lunak@kde.org, kde@privat.broulik.de, mgraesslin@kde.org, mart@kde.org +X-KDE-PluginInfo-Name=kwin4_effect_logout +X-KDE-PluginInfo-Version=0.2.0 +X-KDE-PluginInfo-Category=Appearance +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-EnabledByDefault=true +X-KDE-Ordering=40 +X-Plasma-API=javascript +X-Plasma-MainScript=code/main.js diff --git a/effects/lookingglass/CMakeLists.txt b/effects/lookingglass/CMakeLists.txt new file mode 100644 index 0000000..381a790 --- /dev/null +++ b/effects/lookingglass/CMakeLists.txt @@ -0,0 +1,27 @@ +####################################### +# Effect + +####################################### +# Config +set(kwin_lookingglass_config_SRCS lookingglass_config.cpp) +ki18n_wrap_ui(kwin_lookingglass_config_SRCS lookingglass_config.ui) +kconfig_add_kcfg_files(kwin_lookingglass_config_SRCS lookingglassconfig.kcfgc) + +add_library(kwin_lookingglass_config MODULE ${kwin_lookingglass_config_SRCS}) + +target_link_libraries(kwin_lookingglass_config + KF5::ConfigWidgets + KF5::GlobalAccel + KF5::I18n + KF5::XmlGui + KWinEffectsInterface +) + +kcoreaddons_desktop_to_json(kwin_lookingglass_config lookingglass_config.desktop SERVICE_TYPES kcmodule.desktop) + +install( + TARGETS + kwin_lookingglass_config + DESTINATION + ${PLUGIN_INSTALL_DIR}/kwin/effects/configs +) diff --git a/effects/lookingglass/data/1.10/lookingglass.frag b/effects/lookingglass/data/1.10/lookingglass.frag new file mode 100644 index 0000000..992c9d4 --- /dev/null +++ b/effects/lookingglass/data/1.10/lookingglass.frag @@ -0,0 +1,25 @@ +uniform sampler2D sampler; +uniform vec2 u_cursor; +uniform float u_zoom; +uniform float u_radius; +uniform vec2 u_textureSize; + +varying vec2 texcoord0; + +#define PI 3.14159 + +void main() +{ + vec2 d = u_cursor - texcoord0; + float dist = sqrt(d.x*d.x + d.y*d.y); + vec2 texcoord = texcoord0; + if (dist < u_radius) { + float disp = sin(dist / u_radius * PI) * (u_zoom - 1.0) * 20.0; + texcoord += d / dist * disp; + } + + texcoord = texcoord/u_textureSize; + texcoord.t = 1.0 - texcoord.t; + gl_FragColor = texture2D(sampler, texcoord); +} + diff --git a/effects/lookingglass/data/1.40/lookingglass.frag b/effects/lookingglass/data/1.40/lookingglass.frag new file mode 100644 index 0000000..56cf3ce --- /dev/null +++ b/effects/lookingglass/data/1.40/lookingglass.frag @@ -0,0 +1,28 @@ +#version 140 +uniform sampler2D sampler; +uniform vec2 u_cursor; +uniform float u_zoom; +uniform float u_radius; +uniform vec2 u_textureSize; + +in vec2 texcoord0; + +out vec4 fragColor; + +#define PI 3.14159 + +void main() +{ + vec2 d = u_cursor - texcoord0; + float dist = sqrt(d.x*d.x + d.y*d.y); + vec2 texcoord = texcoord0; + if (dist < u_radius) { + float disp = sin(dist / u_radius * PI) * (u_zoom - 1.0) * 20.0; + texcoord += d / dist * disp; + } + + texcoord = texcoord/u_textureSize; + texcoord.t = 1.0 - texcoord.t; + fragColor = texture(sampler, texcoord); +} + diff --git a/effects/lookingglass/lookingglass.cpp b/effects/lookingglass/lookingglass.cpp new file mode 100644 index 0000000..283c89d --- /dev/null +++ b/effects/lookingglass/lookingglass.cpp @@ -0,0 +1,246 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Rivo Laks + SPDX-FileCopyrightText: 2007 Christian Nitschkowski + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "lookingglass.h" + +// KConfigSkeleton +#include "lookingglassconfig.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include + +namespace KWin +{ + +LookingGlassEffect::LookingGlassEffect() + : zoom(1.0f) + , target_zoom(1.0f) + , polling(false) + , m_texture(nullptr) + , m_fbo(nullptr) + , m_vbo(nullptr) + , m_shader(nullptr) + , m_enabled(false) + , m_valid(false) +{ + initConfig(); + QAction* a; + a = KStandardAction::zoomIn(this, SLOT(zoomIn()), this); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::META + Qt::Key_Equal); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::META + Qt::Key_Equal); + effects->registerGlobalShortcut(Qt::META + Qt::Key_Equal, a); + + a = KStandardAction::zoomOut(this, SLOT(zoomOut()), this); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::META + Qt::Key_Minus); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::META + Qt::Key_Minus); + effects->registerGlobalShortcut(Qt::META + Qt::Key_Minus, a); + + a = KStandardAction::actualSize(this, SLOT(toggle()), this); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::META + Qt::Key_0); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::META + Qt::Key_0); + effects->registerGlobalShortcut(Qt::META + Qt::Key_0, a); + + connect(effects, &EffectsHandler::mouseChanged, this, &LookingGlassEffect::slotMouseChanged); + + reconfigure(ReconfigureAll); +} + +LookingGlassEffect::~LookingGlassEffect() +{ + delete m_texture; + delete m_fbo; + delete m_shader; + delete m_vbo; +} + +bool LookingGlassEffect::supported() +{ + return effects->compositingType() == OpenGL2Compositing && !GLPlatform::instance()->supports(LimitedNPOT); +} + +void LookingGlassEffect::reconfigure(ReconfigureFlags) +{ + LookingGlassConfig::self()->read(); + initialradius = LookingGlassConfig::radius(); + radius = initialradius; + qCDebug(KWINEFFECTS) << "Radius from config:" << radius; + m_valid = loadData(); +} + +bool LookingGlassEffect::loadData() +{ + const QSize screenSize = effects->virtualScreenSize(); + int texw = screenSize.width(); + int texh = screenSize.height(); + + // Create texture and render target + const int levels = std::log2(qMin(texw, texh)) + 1; + m_texture = new GLTexture(GL_RGBA8, texw, texh, levels); + m_texture->setFilter(GL_LINEAR_MIPMAP_LINEAR); + m_texture->setWrapMode(GL_CLAMP_TO_EDGE); + + m_fbo = new GLRenderTarget(*m_texture); + if (!m_fbo->valid()) { + return false; + } + + m_shader = ShaderManager::instance()->generateShaderFromResources(ShaderTrait::MapTexture, QString(), QStringLiteral("lookingglass.frag")); + if (m_shader->isValid()) { + ShaderBinder binder(m_shader); + m_shader->setUniform("u_textureSize", QVector2D(screenSize.width(), screenSize.height())); + } else { + qCCritical(KWINEFFECTS) << "The shader failed to load!"; + return false; + } + + m_vbo = new GLVertexBuffer(GLVertexBuffer::Static); + QVector verts; + QVector texcoords; + texcoords << screenSize.width() << 0.0; + verts << screenSize.width() << 0.0; + texcoords << 0.0 << 0.0; + verts << 0.0 << 0.0; + texcoords << 0.0 << screenSize.height(); + verts << 0.0 << screenSize.height(); + texcoords << 0.0 << screenSize.height(); + verts << 0.0 << screenSize.height(); + texcoords << screenSize.width() << screenSize.height(); + verts << screenSize.width() << screenSize.height(); + texcoords << screenSize.width() << 0.0; + verts << screenSize.width() << 0.0; + m_vbo->setData(6, 2, verts.constData(), texcoords.constData()); + return true; +} + +void LookingGlassEffect::toggle() +{ + if (target_zoom == 1.0f) { + target_zoom = 2.0f; + if (!polling) { + polling = true; + effects->startMousePolling(); + } + m_enabled = true; + } else { + target_zoom = 1.0f; + if (polling) { + polling = false; + effects->stopMousePolling(); + } + if (zoom == target_zoom) { + m_enabled = false; + } + } + effects->addRepaint(cursorPos().x() - radius, cursorPos().y() - radius, 2 * radius, 2 * radius); +} + +void LookingGlassEffect::zoomIn() +{ + target_zoom = qMin(7.0, target_zoom + 0.5); + m_enabled = true; + if (!polling) { + polling = true; + effects->startMousePolling(); + } + effects->addRepaint(cursorPos().x() - radius, cursorPos().y() - radius, 2 * radius, 2 * radius); +} + +void LookingGlassEffect::zoomOut() +{ + target_zoom -= 0.5; + if (target_zoom < 1) { + target_zoom = 1; + if (polling) { + polling = false; + effects->stopMousePolling(); + } + if (zoom == target_zoom) { + m_enabled = false; + } + } + effects->addRepaint(cursorPos().x() - radius, cursorPos().y() - radius, 2 * radius, 2 * radius); +} + +void LookingGlassEffect::prePaintScreen(ScreenPrePaintData& data, int time) +{ + if (zoom != target_zoom) { + double diff = time / animationTime(500.0); + if (target_zoom > zoom) + zoom = qMin(zoom * qMax(1.0 + diff, 1.2), target_zoom); + else + zoom = qMax(zoom * qMin(1.0 - diff, 0.8), target_zoom); + qCDebug(KWINEFFECTS) << "zoom is now " << zoom; + radius = qBound((double)initialradius, initialradius * zoom, 3.5 * initialradius); + + if (zoom <= 1.0f) { + m_enabled = false; + } + + effects->addRepaint(cursorPos().x() - radius, cursorPos().y() - radius, 2 * radius, 2 * radius); + } + if (m_valid && m_enabled) { + data.mask |= PAINT_SCREEN_WITH_TRANSFORMED_WINDOWS; + // Start rendering to texture + GLRenderTarget::pushRenderTarget(m_fbo); + } + + effects->prePaintScreen(data, time); +} + +void LookingGlassEffect::slotMouseChanged(const QPoint& pos, const QPoint& old, Qt::MouseButtons, + Qt::MouseButtons, Qt::KeyboardModifiers, Qt::KeyboardModifiers) +{ + if (pos != old && m_enabled) { + effects->addRepaint(pos.x() - radius, pos.y() - radius, 2 * radius, 2 * radius); + effects->addRepaint(old.x() - radius, old.y() - radius, 2 * radius, 2 * radius); + } +} + +void LookingGlassEffect::paintScreen(int mask, const QRegion ®ion, ScreenPaintData &data) +{ + // Call the next effect. + effects->paintScreen(mask, region, data); + if (m_valid && m_enabled) { + // Disable render texture + GLRenderTarget* target = GLRenderTarget::popRenderTarget(); + Q_ASSERT(target == m_fbo); + Q_UNUSED(target); + m_texture->bind(); + m_texture->generateMipmaps(); + + // Use the shader + ShaderBinder binder(m_shader); + m_shader->setUniform("u_zoom", (float)zoom); + m_shader->setUniform("u_radius", (float)radius); + m_shader->setUniform("u_cursor", QVector2D(cursorPos().x(), cursorPos().y())); + m_shader->setUniform(GLShader::ModelViewProjectionMatrix, data.projectionMatrix()); + m_vbo->render(GL_TRIANGLES); + m_texture->unbind(); + } +} + +bool LookingGlassEffect::isActive() const +{ + return m_valid && m_enabled; +} + +} // namespace + diff --git a/effects/lookingglass/lookingglass.h b/effects/lookingglass/lookingglass.h new file mode 100644 index 0000000..463c3dc --- /dev/null +++ b/effects/lookingglass/lookingglass.h @@ -0,0 +1,72 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Rivo Laks + SPDX-FileCopyrightText: 2007 Christian Nitschkowski + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_LOOKINGGLASS_H +#define KWIN_LOOKINGGLASS_H + +#include + +namespace KWin +{ + +class GLRenderTarget; +class GLShader; +class GLTexture; +class GLVertexBuffer; + +/** + * Enhanced magnifier + */ +class LookingGlassEffect : public Effect +{ + Q_OBJECT + Q_PROPERTY(int initialRadius READ initialRadius) +public: + LookingGlassEffect(); + ~LookingGlassEffect() override; + + void reconfigure(ReconfigureFlags) override; + + void prePaintScreen(ScreenPrePaintData& data, int time) override; + void paintScreen(int mask, const QRegion ®ion, ScreenPaintData &data) override; + bool isActive() const override; + + static bool supported(); + + // for properties + int initialRadius() const { + return initialradius; + } +public Q_SLOTS: + void toggle(); + void zoomIn(); + void zoomOut(); + void slotMouseChanged(const QPoint& pos, const QPoint& old, + Qt::MouseButtons buttons, Qt::MouseButtons oldbuttons, + Qt::KeyboardModifiers modifiers, Qt::KeyboardModifiers oldmodifiers); + +private: + bool loadData(); + double zoom; + double target_zoom; + bool polling; // Mouse polling + int radius; + int initialradius; + GLTexture *m_texture; + GLRenderTarget *m_fbo; + GLVertexBuffer *m_vbo; + GLShader *m_shader; + bool m_enabled; + bool m_valid; +}; + +} // namespace + +#endif diff --git a/effects/lookingglass/lookingglass.kcfg b/effects/lookingglass/lookingglass.kcfg new file mode 100644 index 0000000..236befd --- /dev/null +++ b/effects/lookingglass/lookingglass.kcfg @@ -0,0 +1,12 @@ + + + + + + 200 + + + diff --git a/effects/lookingglass/lookingglass_config.cpp b/effects/lookingglass/lookingglass_config.cpp new file mode 100644 index 0000000..df71fd5 --- /dev/null +++ b/effects/lookingglass/lookingglass_config.cpp @@ -0,0 +1,108 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Christian Nitschkowski + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "lookingglass_config.h" + +// KConfigSkeleton +#include "lookingglassconfig.h" +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +K_PLUGIN_FACTORY_WITH_JSON(LookingGlassEffectConfigFactory, + "lookingglass_config.json", + registerPlugin();) + +namespace KWin +{ + +LookingGlassEffectConfigForm::LookingGlassEffectConfigForm(QWidget* parent) : QWidget(parent) +{ + setupUi(this); +} + +LookingGlassEffectConfig::LookingGlassEffectConfig(QWidget* parent, const QVariantList& args) : + KCModule(KAboutData::pluginData(QStringLiteral("lookingglass")), parent, args) +{ + m_ui = new LookingGlassEffectConfigForm(this); + + QVBoxLayout* layout = new QVBoxLayout(this); + + layout->addWidget(m_ui); + + LookingGlassConfig::instance(KWIN_CONFIG); + addConfig(LookingGlassConfig::self(), m_ui); + connect(m_ui->editor, &KShortcutsEditor::keyChange, this, &LookingGlassEffectConfig::markAsChanged); + + // Shortcut config. The shortcut belongs to the component "kwin"! + m_actionCollection = new KActionCollection(this, QStringLiteral("kwin")); + + m_actionCollection->setComponentDisplayName(i18n("KWin")); + m_actionCollection->setConfigGroup(QStringLiteral("LookingGlass")); + m_actionCollection->setConfigGlobal(true); + + QAction* a; + a = m_actionCollection->addAction(KStandardAction::ZoomIn); + a->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::META + Qt::Key_Equal); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::META + Qt::Key_Equal); + + a = m_actionCollection->addAction(KStandardAction::ZoomOut); + a->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::META + Qt::Key_Minus); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::META + Qt::Key_Minus); + + a = m_actionCollection->addAction(KStandardAction::ActualSize); + a->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::META + Qt::Key_0); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::META + Qt::Key_0); + + m_ui->editor->addCollection(m_actionCollection); +} + +LookingGlassEffectConfig::~LookingGlassEffectConfig() +{ + // Undo (only) unsaved changes to global key shortcuts + m_ui->editor->undoChanges(); +} + +void LookingGlassEffectConfig::save() +{ + qDebug() << "Saving config of LookingGlass" ; + KCModule::save(); + + m_ui->editor->save(); // undo() will restore to this state from now on + + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("lookingglass")); +} + +void LookingGlassEffectConfig::defaults() +{ + m_ui->editor->allDefault(); + KCModule::defaults(); +} + +} // namespace + +#include "lookingglass_config.moc" diff --git a/effects/lookingglass/lookingglass_config.desktop b/effects/lookingglass/lookingglass_config.desktop new file mode 100644 index 0000000..f65a1c2 --- /dev/null +++ b/effects/lookingglass/lookingglass_config.desktop @@ -0,0 +1,83 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=KCModule + +X-KDE-Library=kwin_lookingglass_config +X-KDE-ParentComponents=lookingglass + +Name=Looking Glass +Name[af]=Vergrootglas +Name[ar]=عدسة عين السمكة +Name[az]=Linza +Name[be@latin]=Looking Glass +Name[bg]=Лупа +Name[bs]=Lupa +Name[ca]=Aspecte de vidre +Name[ca@valencia]=Aspecte de vidre +Name[cs]=Lupa +Name[csb]=Lupa +Name[da]=Kikkert +Name[de]=Bildschirmlupe +Name[el]=Μεγεθυντικός φακός +Name[en_GB]=Looking Glass +Name[eo]=Looking Glass +Name[es]=Espejo +Name[et]=Suurendusklaas +Name[eu]=Pantaila-lupa +Name[fi]=Suurennuslasi +Name[fr]=Loupe +Name[fy]=Looking Glass +Name[ga]=Looking Glass +Name[gl]=Espello +Name[gu]=જોવાનો કાચ +Name[he]=משקף +Name[hi]=लुकिंग ग्लास +Name[hne]=लुकिंग ग्लास +Name[hr]=Povećalo +Name[hu]=Nagyító +Name[ia]=Looking Glass +Name[id]=Kaca Pembesar +Name[is]=Spegilgler +Name[it]=Specchio +Name[ja]=拡大鏡 +Name[kk]=Лупа +Name[km]=កញ្ចក់​មើល +Name[kn]=ಲುಕಿಂಗ್ ಗ್ಲಾಸ್ +Name[ko]=들여다보는 돋보기 +Name[lt]=Veidrodis +Name[lv]=Skatāmais stikls +Name[mai]=लुकिंग ग्लास +Name[mk]=Лупа +Name[ml]=മായകണ്ണാടി. +Name[mr]=भिंग दृश्य +Name[nb]=Looking Glass +Name[nds]=Kiekglas +Name[ne]=हेर्ने ऐना +Name[nl]=Vergrootglas +Name[nn]=Forstørringsglas +Name[pa]=ਸ਼ੀਸ਼ਾ ਵੇਖਣਾ +Name[pl]=Lupa +Name[pt]=Aparência de Vidro +Name[pt_BR]=Espelho +Name[ro]=Lentilă +Name[ru]=Линза +Name[se]=Stuoridanláse +Name[si]=වීදුරු ලෙස පෙනෙන +Name[sk]=Å oÅ¡ovka +Name[sl]=Povečevalno steklo +Name[sr]=Лупа +Name[sr@ijekavian]=Лупа +Name[sr@ijekavianlatin]=Lupa +Name[sr@latin]=Lupa +Name[sv]=Förstoringsglas +Name[ta]=Looking Glass +Name[te]=లుకింగ్ గ్లాస్ +Name[th]=เลนส์ตาปลา +Name[tr]=Büyüteç +Name[ug]=كۆزىتىش ئەينەك +Name[uk]=Збільшувальне скло +Name[vi]=Thấu kính +Name[wa]=Berikes +Name[x-test]=xxLooking Glassxx +Name[zh_CN]=窥镜 +Name[zh_TW]=鏡射 diff --git a/effects/lookingglass/lookingglass_config.h b/effects/lookingglass/lookingglass_config.h new file mode 100644 index 0000000..786f359 --- /dev/null +++ b/effects/lookingglass/lookingglass_config.h @@ -0,0 +1,46 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Christian Nitschkowski + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_LOOKINGGLASS_CONFIG_H +#define KWIN_LOOKINGGLASS_CONFIG_H + +#include + +#include "ui_lookingglass_config.h" + +class KActionCollection; + +namespace KWin +{ + +class LookingGlassEffectConfigForm : public QWidget, public Ui::LookingGlassEffectConfigForm +{ + Q_OBJECT +public: + explicit LookingGlassEffectConfigForm(QWidget* parent); +}; + +class LookingGlassEffectConfig : public KCModule +{ + Q_OBJECT +public: + explicit LookingGlassEffectConfig(QWidget* parent = nullptr, const QVariantList& args = QVariantList()); + ~LookingGlassEffectConfig() override; + + void save() override; + void defaults() override; + +private: + LookingGlassEffectConfigForm* m_ui; + KActionCollection* m_actionCollection; +}; + +} // namespace + +#endif diff --git a/effects/lookingglass/lookingglass_config.ui b/effects/lookingglass/lookingglass_config.ui new file mode 100644 index 0000000..7fec55e --- /dev/null +++ b/effects/lookingglass/lookingglass_config.ui @@ -0,0 +1,59 @@ + + + KWin::LookingGlassEffectConfigForm + + + + 0 + 0 + 275 + 185 + + + + + + + KShortcutsEditor::GlobalAction + + + + + + + &Radius: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_Radius + + + + + + + + 0 + 0 + + + + 9999 + + + + + + + + KShortcutsEditor + QWidget +
KShortcutsEditor
+ 1 +
+
+ + +
diff --git a/effects/lookingglass/lookingglassconfig.kcfgc b/effects/lookingglass/lookingglassconfig.kcfgc new file mode 100644 index 0000000..6b35b53 --- /dev/null +++ b/effects/lookingglass/lookingglassconfig.kcfgc @@ -0,0 +1,5 @@ +File=lookingglass.kcfg +ClassName=LookingGlassConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/effects/magiclamp/CMakeLists.txt b/effects/magiclamp/CMakeLists.txt new file mode 100644 index 0000000..b1ea9c9 --- /dev/null +++ b/effects/magiclamp/CMakeLists.txt @@ -0,0 +1,23 @@ +####################################### +# Config +set(kwin_magiclamp_config_SRCS magiclamp_config.cpp) +ki18n_wrap_ui(kwin_magiclamp_config_SRCS magiclamp_config.ui) +kconfig_add_kcfg_files(kwin_magiclamp_config_SRCS magiclampconfig.kcfgc) + +add_library(kwin_magiclamp_config MODULE ${kwin_magiclamp_config_SRCS}) + +target_link_libraries(kwin_magiclamp_config + KF5::ConfigWidgets + KF5::I18n + Qt5::DBus + KWinEffectsInterface +) + +kcoreaddons_desktop_to_json(kwin_magiclamp_config magiclamp_config.desktop SERVICE_TYPES kcmodule.desktop) + +install( + TARGETS + kwin_magiclamp_config + DESTINATION + ${PLUGIN_INSTALL_DIR}/kwin/effects/configs +) diff --git a/effects/magiclamp/magiclamp.cpp b/effects/magiclamp/magiclamp.cpp new file mode 100644 index 0000000..4c7c0fc --- /dev/null +++ b/effects/magiclamp/magiclamp.cpp @@ -0,0 +1,363 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +// based on minimize animation by Rivo Laks + +#include "magiclamp.h" +// KConfigSkeleton +#include "magiclampconfig.h" + +namespace KWin +{ + +MagicLampEffect::MagicLampEffect() +{ + initConfig(); + reconfigure(ReconfigureAll); + connect(effects, &EffectsHandler::windowDeleted, this, &MagicLampEffect::slotWindowDeleted); + connect(effects, &EffectsHandler::windowMinimized, this, &MagicLampEffect::slotWindowMinimized); + connect(effects, &EffectsHandler::windowUnminimized, this, &MagicLampEffect::slotWindowUnminimized); +} + +bool MagicLampEffect::supported() +{ + return effects->isOpenGLCompositing() && effects->animationsSupported(); +} + +void MagicLampEffect::reconfigure(ReconfigureFlags) +{ + MagicLampConfig::self()->read(); + + // TODO: Rename animationDuration to duration so we can use + // animationTime(250). + const int d = MagicLampConfig::animationDuration() != 0 + ? MagicLampConfig::animationDuration() + : 250; + m_duration = std::chrono::milliseconds(static_cast(animationTime(d))); +} + +void MagicLampEffect::prePaintScreen(ScreenPrePaintData& data, int time) +{ + const std::chrono::milliseconds delta(time); + + auto animationIt = m_animations.begin(); + while (animationIt != m_animations.end()) { + (*animationIt).update(delta); + ++animationIt; + } + + // We need to mark the screen windows as transformed. Otherwise the + // whole screen won't be repainted, resulting in artefacts. + data.mask |= PAINT_SCREEN_WITH_TRANSFORMED_WINDOWS; + + effects->prePaintScreen(data, time); +} + +void MagicLampEffect::prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time) +{ + // Schedule window for transformation if the animation is still in + // progress + if (m_animations.contains(w)) { + // We'll transform this window + data.setTransformed(); + data.quads = data.quads.makeGrid(40); + w->enablePainting(EffectWindow::PAINT_DISABLED_BY_MINIMIZE); + } + + effects->prePaintWindow(w, data, time); +} + +void MagicLampEffect::paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data) +{ + auto animationIt = m_animations.constFind(w); + if (animationIt != m_animations.constEnd()) { + // 0 = not minimized, 1 = fully minimized + const qreal progress = (*animationIt).value(); + + QRect geo = w->geometry(); + QRect icon = w->iconGeometry(); + IconPosition position = Top; + // If there's no icon geometry, minimize to the center of the screen + if (!icon.isValid()) { + QRect extG = geo; + QPoint pt = cursorPos(); + // focussing inside the window is no good, leads to ugly artefacts, find nearest border + if (extG.contains(pt)) { + const int d[2][2] = { {pt.x() - extG.x(), extG.right() - pt.x()}, + {pt.y() - extG.y(), extG.bottom() - pt.y()} + }; + int di = d[1][0]; + position = Top; + if (d[0][0] < di) { + di = d[0][0]; + position = Left; + } + if (d[1][1] < di) { + di = d[1][1]; + position = Bottom; + } + if (d[0][1] < di) + position = Right; + switch(position) { + case Top: pt.setY(extG.y()); break; + case Left: pt.setX(extG.x()); break; + case Bottom: pt.setY(extG.bottom()); break; + case Right: pt.setX(extG.right()); break; + } + } else { + if (pt.y() < geo.y()) + position = Top; + else if (pt.x() < geo.x()) + position = Left; + else if (pt.y() > geo.bottom()) + position = Bottom; + else if (pt.x() > geo.right()) + position = Right; + } + icon = QRect(pt, QSize(0, 0)); + } else { + // Assumption: there is a panel containing the icon position + EffectWindow* panel = nullptr; + foreach (EffectWindow * window, effects->stackingOrder()) { + if (!window->isDock()) + continue; + // we have to use intersects as there seems to be a Plasma bug + // the published icon geometry might be bigger than the panel + if (window->geometry().intersects(icon)) { + panel = window; + break; + } + } + if (panel) { + // Assumption: width of horizonal panel is greater than its height and vice versa + // The panel has to border one screen edge, so get it's screen area + QRect panelScreen = effects->clientArea(ScreenArea, panel); + if (panel->width() >= panel->height()) { + // horizontal panel + if (panel->y() <= panelScreen.height()/2) + position = Top; + else + position = Bottom; + } else { + // vertical panel + if (panel->x() <= panelScreen.width()/2) + position = Left; + else + position = Right; + } + } else { + // we did not find a panel, so it might be autohidden + QRect iconScreen = effects->clientArea(ScreenArea, icon.topLeft(), effects->currentDesktop()); + // as the icon geometry could be overlap a screen edge we use an intersection + QRect rect = iconScreen.intersected(icon); + // here we need a different assumption: icon geometry borders one screen edge + // this assumption might be wrong for e.g. task applet being the only applet in panel + // in this case the icon borders two screen edges + // there might be a wrong animation, but not distorted + if (rect.x() == iconScreen.x()) { + position = Left; + } else if (rect.x() + rect.width() == iconScreen.x() + iconScreen.width()) { + position = Right; + } else if (rect.y() == iconScreen.y()) { + position = Top; + } else { + position = Bottom; + } + } + } + +#define SANITIZE_PROGRESS if (p_progress[0] < 0)\ + p_progress[0] = -p_progress[0];\ + if (p_progress[1] < 0)\ + p_progress[1] = -p_progress[1] +#define SET_QUADS(_SET_A_, _A_, _DA_, _SET_B_, _B_, _O0_, _O1_, _O2_, _O3_) quad[0]._SET_A_((icon._A_() + icon._DA_()*(quad[0]._A_() / geo._DA_()) - (quad[0]._A_() + geo._A_()))*p_progress[_O0_] + quad[0]._A_());\ + quad[1]._SET_A_((icon._A_() + icon._DA_()*(quad[1]._A_() / geo._DA_()) - (quad[1]._A_() + geo._A_()))*p_progress[_O1_] + quad[1]._A_());\ + quad[2]._SET_A_((icon._A_() + icon._DA_()*(quad[2]._A_() / geo._DA_()) - (quad[2]._A_() + geo._A_()))*p_progress[_O2_] + quad[2]._A_());\ + quad[3]._SET_A_((icon._A_() + icon._DA_()*(quad[3]._A_() / geo._DA_()) - (quad[3]._A_() + geo._A_()))*p_progress[_O3_] + quad[3]._A_());\ + \ + quad[0]._SET_B_(quad[0]._B_() + offset[_O0_]);\ + quad[1]._SET_B_(quad[1]._B_() + offset[_O1_]);\ + quad[2]._SET_B_(quad[2]._B_() + offset[_O2_]);\ + quad[3]._SET_B_(quad[3]._B_() + offset[_O3_]) + + WindowQuadList newQuads; + newQuads.reserve(data.quads.count()); + float quadFactor; // defines how fast a quad is vertically moved: y coordinates near to window top are slowed down + // it is used as quadFactor^3/windowHeight^3 + // quadFactor is the y position of the quad but is changed towards becomming the window height + // by that the factor becomes 1 and has no influence any more + float offset[2] = {0,0}; // how far has a quad to be moved? Distance between icon and window multiplied by the progress and by the quadFactor + float p_progress[2] = {0,0}; // the factor which defines how far the x values have to be changed + // factor is the current moved y value diveded by the distance between icon and window + WindowQuad lastQuad(WindowQuadError); + lastQuad[0].setX(-1); + lastQuad[0].setY(-1); + lastQuad[1].setX(-1); + lastQuad[1].setY(-1); + lastQuad[2].setX(-1); + lastQuad[2].setY(-1); + + if (position == Bottom) { + float height_cube = float(geo.height()) * float(geo.height()) * float(geo.height()); + foreach (WindowQuad quad, data.quads) { // krazy:exclude=foreach + + if (quad[0].y() != lastQuad[0].y() || quad[2].y() != lastQuad[2].y()) { + quadFactor = quad[0].y() + (geo.height() - quad[0].y()) * progress; + offset[0] = (icon.y() + quad[0].y() - geo.y()) * progress * ((quadFactor * quadFactor * quadFactor) / height_cube); + quadFactor = quad[2].y() + (geo.height() - quad[2].y()) * progress; + offset[1] = (icon.y() + quad[2].y() - geo.y()) * progress * ((quadFactor * quadFactor * quadFactor) / height_cube); + p_progress[1] = qMin(offset[1] / (icon.y() + icon.height() - geo.y() - float(quad[2].y())), 1.0f); + p_progress[0] = qMin(offset[0] / (icon.y() + icon.height() - geo.y() - float(quad[0].y())), 1.0f); + } else + lastQuad = quad; + + SANITIZE_PROGRESS; + // x values are moved towards the center of the icon + SET_QUADS(setX, x, width, setY, y, 0,0,1,1); + + newQuads.append(quad); + } + } else if (position == Top) { + float height_cube = float(geo.height()) * float(geo.height()) * float(geo.height()); + foreach (WindowQuad quad, data.quads) { // krazy:exclude=foreach + + if (quad[0].y() != lastQuad[0].y() || quad[2].y() != lastQuad[2].y()) { + quadFactor = geo.height() - quad[0].y() + (quad[0].y()) * progress; + offset[0] = (geo.y() - icon.height() + geo.height() + quad[0].y() - icon.y()) * progress * ((quadFactor * quadFactor * quadFactor) / height_cube); + quadFactor = geo.height() - quad[2].y() + (quad[2].y()) * progress; + offset[1] = (geo.y() - icon.height() + geo.height() + quad[2].y() - icon.y()) * progress * ((quadFactor * quadFactor * quadFactor) / height_cube); + p_progress[0] = qMin(offset[0] / (geo.y() - icon.height() + geo.height() - icon.y() - float(geo.height() - quad[0].y())), 1.0f); + p_progress[1] = qMin(offset[1] / (geo.y() - icon.height() + geo.height() - icon.y() - float(geo.height() - quad[2].y())), 1.0f); + } else + lastQuad = quad; + + offset[0] = -offset[0]; + offset[1] = -offset[1]; + + SANITIZE_PROGRESS; + // x values are moved towards the center of the icon + SET_QUADS(setX, x, width, setY, y, 0,0,1,1); + + newQuads.append(quad); + } + } else if (position == Left) { + float width_cube = float(geo.width()) * float(geo.width()) * float(geo.width()); + foreach (WindowQuad quad, data.quads) { // krazy:exclude=foreach + + if (quad[0].x() != lastQuad[0].x() || quad[1].x() != lastQuad[1].x()) { + quadFactor = geo.width() - quad[0].x() + (quad[0].x()) * progress; + offset[0] = (geo.x() - icon.width() + geo.width() + quad[0].x() - icon.x()) * progress * ((quadFactor * quadFactor * quadFactor) / width_cube); + quadFactor = geo.width() - quad[1].x() + (quad[1].x()) * progress; + offset[1] = (geo.x() - icon.width() + geo.width() + quad[1].x() - icon.x()) * progress * ((quadFactor * quadFactor * quadFactor) / width_cube); + p_progress[0] = qMin(offset[0] / (geo.x() - icon.width() + geo.width() - icon.x() - float(geo.width() - quad[0].x())), 1.0f); + p_progress[1] = qMin(offset[1] / (geo.x() - icon.width() + geo.width() - icon.x() - float(geo.width() - quad[1].x())), 1.0f); + } else + lastQuad = quad; + + offset[0] = -offset[0]; + offset[1] = -offset[1]; + + SANITIZE_PROGRESS; + // y values are moved towards the center of the icon + SET_QUADS(setY, y, height, setX, x, 0,1,1,0); + + newQuads.append(quad); + } + } else if (position == Right) { + float width_cube = float(geo.width()) * float(geo.width()) * float(geo.width()); + foreach (WindowQuad quad, data.quads) { // krazy:exclude=foreach + + if (quad[0].x() != lastQuad[0].x() || quad[1].x() != lastQuad[1].x()) { + quadFactor = quad[0].x() + (geo.width() - quad[0].x()) * progress; + offset[0] = (icon.x() + quad[0].x() - geo.x()) * progress * ((quadFactor * quadFactor * quadFactor) / width_cube); + quadFactor = quad[1].x() + (geo.width() - quad[1].x()) * progress; + offset[1] = (icon.x() + quad[1].x() - geo.x()) * progress * ((quadFactor * quadFactor * quadFactor) / width_cube); + p_progress[0] = qMin(offset[0] / (icon.x() + icon.width() - geo.x() - float(quad[0].x())), 1.0f); + p_progress[1] = qMin(offset[1] / (icon.x() + icon.width() - geo.x() - float(quad[1].x())), 1.0f); + } else + lastQuad = quad; + + SANITIZE_PROGRESS; + // y values are moved towards the center of the icon + SET_QUADS(setY, y, height, setX, x, 0,1,1,0); + + newQuads.append(quad); + } + } + data.quads = newQuads; + } + + // Call the next effect. + effects->paintWindow(w, mask, region, data); +} + +void MagicLampEffect::postPaintScreen() +{ + auto animationIt = m_animations.begin(); + while (animationIt != m_animations.end()) { + if ((*animationIt).done()) { + animationIt = m_animations.erase(animationIt); + } else { + ++animationIt; + } + } + + effects->addRepaintFull(); + + // Call the next effect. + effects->postPaintScreen(); +} + +void MagicLampEffect::slotWindowDeleted(EffectWindow* w) +{ + m_animations.remove(w); +} + +void MagicLampEffect::slotWindowMinimized(EffectWindow* w) +{ + if (effects->activeFullScreenEffect()) + return; + + TimeLine &timeLine = m_animations[w]; + + if (timeLine.running()) { + timeLine.toggleDirection(); + } else { + timeLine.setDirection(TimeLine::Forward); + timeLine.setDuration(m_duration); + timeLine.setEasingCurve(QEasingCurve::Linear); + } + + effects->addRepaintFull(); +} + +void MagicLampEffect::slotWindowUnminimized(EffectWindow* w) +{ + if (effects->activeFullScreenEffect()) + return; + + TimeLine &timeLine = m_animations[w]; + + if (timeLine.running()) { + timeLine.toggleDirection(); + } else { + timeLine.setDirection(TimeLine::Backward); + timeLine.setDuration(m_duration); + timeLine.setEasingCurve(QEasingCurve::Linear); + } + + effects->addRepaintFull(); +} + +bool MagicLampEffect::isActive() const +{ + return !m_animations.isEmpty(); +} + +} // namespace diff --git a/effects/magiclamp/magiclamp.h b/effects/magiclamp/magiclamp.h new file mode 100644 index 0000000..408236b --- /dev/null +++ b/effects/magiclamp/magiclamp.h @@ -0,0 +1,58 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_MAGICLAMP_H +#define KWIN_MAGICLAMP_H + +#include + +namespace KWin +{ + +class MagicLampEffect + : public Effect +{ + Q_OBJECT + +public: + MagicLampEffect(); + + void reconfigure(ReconfigureFlags) override; + void prePaintScreen(ScreenPrePaintData& data, int time) override; + void prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time) override; + void paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data) override; + void postPaintScreen() override; + bool isActive() const override; + + int requestedEffectChainPosition() const override { + return 50; + } + + static bool supported(); + +public Q_SLOTS: + void slotWindowDeleted(KWin::EffectWindow *w); + void slotWindowMinimized(KWin::EffectWindow *w); + void slotWindowUnminimized(KWin::EffectWindow *w); + +private: + std::chrono::milliseconds m_duration; + QHash m_animations; + + enum IconPosition { + Top, + Bottom, + Left, + Right + }; +}; + +} // namespace + +#endif diff --git a/effects/magiclamp/magiclamp.kcfg b/effects/magiclamp/magiclamp.kcfg new file mode 100644 index 0000000..67c8ba0 --- /dev/null +++ b/effects/magiclamp/magiclamp.kcfg @@ -0,0 +1,12 @@ + + + + + + 0 + + + diff --git a/effects/magiclamp/magiclamp_config.cpp b/effects/magiclamp/magiclamp_config.cpp new file mode 100644 index 0000000..688abc5 --- /dev/null +++ b/effects/magiclamp/magiclamp_config.cpp @@ -0,0 +1,60 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "magiclamp_config.h" +// KConfigSkeleton +#include "magiclampconfig.h" +#include + +#include + +#include +#include +#include + +#include + +K_PLUGIN_FACTORY_WITH_JSON(MagicLampEffectConfigFactory, + "magiclamp_config.json", + registerPlugin();) + +namespace KWin +{ + +MagicLampEffectConfigForm::MagicLampEffectConfigForm(QWidget* parent) : QWidget(parent) +{ + setupUi(this); +} + +MagicLampEffectConfig::MagicLampEffectConfig(QWidget* parent, const QVariantList& args) : + KCModule(KAboutData::pluginData(QStringLiteral("magiclamp")), parent, args) +{ + m_ui = new MagicLampEffectConfigForm(this); + + QVBoxLayout* layout = new QVBoxLayout(this); + + layout->addWidget(m_ui); + + MagicLampConfig::instance(KWIN_CONFIG); + addConfig(MagicLampConfig::self(), m_ui); + + load(); +} + +void MagicLampEffectConfig::save() +{ + KCModule::save(); + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("magiclamp")); +} + +} // namespace + +#include "magiclamp_config.moc" diff --git a/effects/magiclamp/magiclamp_config.desktop b/effects/magiclamp/magiclamp_config.desktop new file mode 100644 index 0000000..7b7db0e --- /dev/null +++ b/effects/magiclamp/magiclamp_config.desktop @@ -0,0 +1,80 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=KCModule + +X-KDE-Library=kwin_magiclamp_config +X-KDE-ParentComponents=magiclamp + +Name=Magic Lamp +Name[ar]=المصباح السحري +Name[ast]=Llámpara máxica +Name[az]=Sehirli Lampa +Name[be@latin]=Mahičnaja lampa +Name[bg]=Вълшебна лампа +Name[bs]=Magična lampa +Name[ca]=Làmpada màgica +Name[ca@valencia]=Làmpada màgica +Name[cs]=Magická lampa +Name[da]=Magisk lampe +Name[de]=Wunderlampe +Name[el]=Μαγικό λυχνάρι +Name[en_GB]=Magic Lamp +Name[eo]=Magia Lampo +Name[es]=Lámpara mágica +Name[et]=Imelamp +Name[eu]=Lanpara magikoa +Name[fi]=Taikalamppu +Name[fr]=Lampe Magique +Name[fy]=Magyske lamp +Name[ga]=Lampa Draíochtach +Name[gl]=Lámpada máxica +Name[gu]=જાદુઇ દિવો +Name[he]=מנורת קסמים +Name[hi]=जादुई बल्ब +Name[hne]=मैजिक लेम्प +Name[hr]=Čarobna svjetiljka +Name[hu]=Varázslámpa +Name[ia]=Lampa Magic +Name[id]=Lampu Ajaib +Name[is]=Töfralampi +Name[it]=Lampada magica +Name[ja]=魔法のランプ +Name[kk]=Сиқырлы шырақ +Name[km]=អំពូល​មន្តអាគមន៍ +Name[kn]=ಮಾಂತ್ರಿಕ ದೀಪ +Name[ko]=요술 램프 +Name[lt]=MagiÅ¡ka lempa +Name[lv]=MaÄ£iskā lampa +Name[mai]=जादुइ चिराग +Name[mk]=Магична ламба +Name[ml]=മാന്ത്രിക വിളക്ക് +Name[mr]=जादुई दिवा +Name[nb]=Magic Lamp +Name[nds]=Töverlamp +Name[nl]=Magische lamp +Name[nn]=Magisk lampe +Name[pa]=ਮੈਜਿਕ ਲੈਂਪ +Name[pl]=Magiczna lampa +Name[pt]=Lâmpada Mágica +Name[pt_BR]=Lâmpada mágica +Name[ro]=Lampă fermecată +Name[ru]=Волшебная лампа +Name[si]=මැජික් ලාම්පුව +Name[sk]=Magická lampa +Name[sl]=Čarobna svetilka +Name[sr]=Магична лампа +Name[sr@ijekavian]=Магична лампа +Name[sr@ijekavianlatin]=Magična lampa +Name[sr@latin]=Magična lampa +Name[sv]=Magisk lanterna +Name[ta]=Magic Lamp +Name[te]=మాజిక్ లాంప్ +Name[th]=ตะเกียงวิเศษ +Name[tr]=Sihirli Lamba +Name[ug]=سېھىرلىك چىراغ +Name[uk]=Чарівна лампа +Name[vi]=Đèn thần +Name[wa]=Madjike lampe +Name[x-test]=xxMagic Lampxx +Name[zh_CN]=魔灯 +Name[zh_TW]=魔法燈 diff --git a/effects/magiclamp/magiclamp_config.h b/effects/magiclamp/magiclamp_config.h new file mode 100644 index 0000000..a389d9a --- /dev/null +++ b/effects/magiclamp/magiclamp_config.h @@ -0,0 +1,43 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_MAGICLAMP_CONFIG_H +#define KWIN_MAGICLAMP_CONFIG_H + +#include + +#include "ui_magiclamp_config.h" + + +namespace KWin +{ + +class MagicLampEffectConfigForm : public QWidget, public Ui::MagicLampEffectConfigForm +{ + Q_OBJECT +public: + explicit MagicLampEffectConfigForm(QWidget* parent); +}; + +class MagicLampEffectConfig : public KCModule +{ + Q_OBJECT +public: + explicit MagicLampEffectConfig(QWidget* parent = nullptr, const QVariantList& args = QVariantList()); + +public Q_SLOTS: + void save() override; + +private: + MagicLampEffectConfigForm* m_ui; +}; + +} // namespace + +#endif diff --git a/effects/magiclamp/magiclamp_config.ui b/effects/magiclamp/magiclamp_config.ui new file mode 100644 index 0000000..9e1063a --- /dev/null +++ b/effects/magiclamp/magiclamp_config.ui @@ -0,0 +1,53 @@ + + + KWin::MagicLampEffectConfigForm + + + + 0 + 0 + 400 + 300 + + + + + + + Animation duration: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_AnimationDuration + + + + + + + + 0 + 0 + + + + Default + + + milliseconds + + + 5000 + + + 10 + + + + + + + + diff --git a/effects/magiclamp/magiclampconfig.kcfgc b/effects/magiclamp/magiclampconfig.kcfgc new file mode 100644 index 0000000..fcea95d --- /dev/null +++ b/effects/magiclamp/magiclampconfig.kcfgc @@ -0,0 +1,5 @@ +File=magiclamp.kcfg +ClassName=MagicLampConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/effects/magnifier/CMakeLists.txt b/effects/magnifier/CMakeLists.txt new file mode 100644 index 0000000..cd3fe05 --- /dev/null +++ b/effects/magnifier/CMakeLists.txt @@ -0,0 +1,24 @@ +####################################### +# Config +set(kwin_magnifier_config_SRCS magnifier_config.cpp) +ki18n_wrap_ui(kwin_magnifier_config_SRCS magnifier_config.ui) +kconfig_add_kcfg_files(kwin_magnifier_config_SRCS magnifierconfig.kcfgc) + +add_library(kwin_magnifier_config MODULE ${kwin_magnifier_config_SRCS}) + +target_link_libraries(kwin_magnifier_config + KF5::ConfigWidgets + KF5::GlobalAccel + KF5::I18n + KF5::XmlGui + KWinEffectsInterface +) + +kcoreaddons_desktop_to_json(kwin_magnifier_config magnifier_config.desktop SERVICE_TYPES kcmodule.desktop) + +install( + TARGETS + kwin_magnifier_config + DESTINATION + ${PLUGIN_INSTALL_DIR}/kwin/effects/configs +) diff --git a/effects/magnifier/magnifier.cpp b/effects/magnifier/magnifier.cpp new file mode 100644 index 0000000..8882ac1 --- /dev/null +++ b/effects/magnifier/magnifier.cpp @@ -0,0 +1,331 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Lubos Lunak + SPDX-FileCopyrightText: 2007 Christian Nitschkowski + SPDX-FileCopyrightText: 2011 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "magnifier.h" +// KConfigSkeleton +#include "magnifierconfig.h" + +#include +#include +#include + +#include +#ifdef KWIN_HAVE_XRENDER_COMPOSITING +#include +#include +#endif +#include + +namespace KWin +{ + +const int FRAME_WIDTH = 5; + +MagnifierEffect::MagnifierEffect() + : zoom(1) + , target_zoom(1) + , polling(false) + , m_texture(nullptr) + , m_fbo(nullptr) +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + , m_pixmap(XCB_PIXMAP_NONE) +#endif +{ + initConfig(); + QAction* a; + a = KStandardAction::zoomIn(this, SLOT(zoomIn()), this); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::META + Qt::Key_Equal); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::META + Qt::Key_Equal); + effects->registerGlobalShortcut(Qt::META + Qt::Key_Equal, a); + + a = KStandardAction::zoomOut(this, SLOT(zoomOut()), this); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::META + Qt::Key_Minus); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::META + Qt::Key_Minus); + effects->registerGlobalShortcut(Qt::META + Qt::Key_Minus, a); + + a = KStandardAction::actualSize(this, SLOT(toggle()), this); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::META + Qt::Key_0); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::META + Qt::Key_0); + effects->registerGlobalShortcut(Qt::META + Qt::Key_0, a); + + connect(effects, &EffectsHandler::mouseChanged, this, &MagnifierEffect::slotMouseChanged); + + reconfigure(ReconfigureAll); +} + +MagnifierEffect::~MagnifierEffect() +{ + delete m_fbo; + delete m_texture; + destroyPixmap(); + // Save the zoom value. + MagnifierConfig::setInitialZoom(target_zoom); + MagnifierConfig::self()->save(); +} + +void MagnifierEffect::destroyPixmap() +{ +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + if (effects->compositingType() != XRenderCompositing) { + return; + } + m_picture.reset(); + if (m_pixmap != XCB_PIXMAP_NONE) { + xcb_free_pixmap(xcbConnection(), m_pixmap); + m_pixmap = XCB_PIXMAP_NONE; + } +#endif +} + +bool MagnifierEffect::supported() +{ + return effects->compositingType() == XRenderCompositing || + (effects->isOpenGLCompositing() && GLRenderTarget::blitSupported()); +} + +void MagnifierEffect::reconfigure(ReconfigureFlags) +{ + MagnifierConfig::self()->read(); + int width, height; + width = MagnifierConfig::width(); + height = MagnifierConfig::height(); + magnifier_size = QSize(width, height); + // Load the saved zoom value. + target_zoom = MagnifierConfig::initialZoom(); + if (target_zoom != zoom) + toggle(); +} + +void MagnifierEffect::prePaintScreen(ScreenPrePaintData& data, int time) +{ + if (zoom != target_zoom) { + double diff = time / animationTime(500.0); + if (target_zoom > zoom) + zoom = qMin(zoom * qMax(1 + diff, 1.2), target_zoom); + else { + zoom = qMax(zoom * qMin(1 - diff, 0.8), target_zoom); + if (zoom == 1.0) { + // zoom ended - delete FBO and texture + delete m_fbo; + delete m_texture; + m_fbo = nullptr; + m_texture = nullptr; + destroyPixmap(); + } + } + } + effects->prePaintScreen(data, time); + if (zoom != 1.0) + data.paint |= magnifierArea().adjusted(-FRAME_WIDTH, -FRAME_WIDTH, FRAME_WIDTH, FRAME_WIDTH); +} + +void MagnifierEffect::paintScreen(int mask, const QRegion ®ion, ScreenPaintData& data) +{ + effects->paintScreen(mask, region, data); // paint normal screen + if (zoom != 1.0) { + // get the right area from the current rendered screen + const QRect area = magnifierArea(); + const QPoint cursor = cursorPos(); + + QRect srcArea(cursor.x() - (double)area.width() / (zoom*2), + cursor.y() - (double)area.height() / (zoom*2), + (double)area.width() / zoom, (double)area.height() / zoom); + if (effects->isOpenGLCompositing()) { + m_fbo->blitFromFramebuffer(srcArea); + // paint magnifier + m_texture->bind(); + auto s = ShaderManager::instance()->pushShader(ShaderTrait::MapTexture); + QMatrix4x4 mvp; + const QSize size = effects->virtualScreenSize(); + mvp.ortho(0, size.width(), size.height(), 0, 0, 65535); + mvp.translate(area.x(), area.y()); + s->setUniform(GLShader::ModelViewProjectionMatrix, mvp); + m_texture->render(infiniteRegion(), area); + ShaderManager::instance()->popShader(); + m_texture->unbind(); + QVector verts; + GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer(); + vbo->reset(); + vbo->setColor(QColor(0, 0, 0)); + const QRectF areaF = area; + // top frame + verts << areaF.right() + FRAME_WIDTH << areaF.top() - FRAME_WIDTH; + verts << areaF.left() - FRAME_WIDTH << areaF.top() - FRAME_WIDTH; + verts << areaF.left() - FRAME_WIDTH << areaF.top(); + verts << areaF.left() - FRAME_WIDTH << areaF.top(); + verts << areaF.right() + FRAME_WIDTH << areaF.top(); + verts << areaF.right() + FRAME_WIDTH << areaF.top() - FRAME_WIDTH; + // left frame + verts << areaF.left() << areaF.top() - FRAME_WIDTH; + verts << areaF.left() - FRAME_WIDTH << areaF.top() - FRAME_WIDTH; + verts << areaF.left() - FRAME_WIDTH << areaF.bottom() + FRAME_WIDTH; + verts << areaF.left() - FRAME_WIDTH << areaF.bottom() + FRAME_WIDTH; + verts << areaF.left() << areaF.bottom() + FRAME_WIDTH; + verts << areaF.left() << areaF.top() - FRAME_WIDTH; + // right frame + verts << areaF.right() + FRAME_WIDTH << areaF.top() - FRAME_WIDTH; + verts << areaF.right() << areaF.top() - FRAME_WIDTH; + verts << areaF.right() << areaF.bottom() + FRAME_WIDTH; + verts << areaF.right() << areaF.bottom() + FRAME_WIDTH; + verts << areaF.right() + FRAME_WIDTH << areaF.bottom() + FRAME_WIDTH; + verts << areaF.right() + FRAME_WIDTH << areaF.top() - FRAME_WIDTH; + // bottom frame + verts << areaF.right() + FRAME_WIDTH << areaF.bottom(); + verts << areaF.left() - FRAME_WIDTH << areaF.bottom(); + verts << areaF.left() - FRAME_WIDTH << areaF.bottom() + FRAME_WIDTH; + verts << areaF.left() - FRAME_WIDTH << areaF.bottom() + FRAME_WIDTH; + verts << areaF.right() + FRAME_WIDTH << areaF.bottom() + FRAME_WIDTH; + verts << areaF.right() + FRAME_WIDTH << areaF.bottom(); + vbo->setData(verts.size() / 2, 2, verts.constData(), nullptr); + + ShaderBinder binder(ShaderTrait::UniformColor); + binder.shader()->setUniform(GLShader::ModelViewProjectionMatrix, data.projectionMatrix()); + vbo->render(GL_TRIANGLES); + } + if (effects->compositingType() == XRenderCompositing) { +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + if (m_pixmap == XCB_PIXMAP_NONE || m_pixmapSize != srcArea.size()) { + destroyPixmap(); + m_pixmap = xcb_generate_id(xcbConnection()); + m_pixmapSize = srcArea.size(); + xcb_create_pixmap(xcbConnection(), 32, m_pixmap, x11RootWindow(), m_pixmapSize.width(), m_pixmapSize.height()); + m_picture.reset(new XRenderPicture(m_pixmap, 32)); + } +#define DOUBLE_TO_FIXED(d) ((xcb_render_fixed_t) ((d) * 65536)) + static const xcb_render_transform_t identity = { + DOUBLE_TO_FIXED(1), DOUBLE_TO_FIXED(0), DOUBLE_TO_FIXED(0), + DOUBLE_TO_FIXED(0), DOUBLE_TO_FIXED(1), DOUBLE_TO_FIXED(0), + DOUBLE_TO_FIXED(0), DOUBLE_TO_FIXED(0), DOUBLE_TO_FIXED(1) + }; + static xcb_render_transform_t xform = { + DOUBLE_TO_FIXED(1), DOUBLE_TO_FIXED(0), DOUBLE_TO_FIXED(0), + DOUBLE_TO_FIXED(0), DOUBLE_TO_FIXED(1), DOUBLE_TO_FIXED(0), + DOUBLE_TO_FIXED(0), DOUBLE_TO_FIXED(0), DOUBLE_TO_FIXED(1) + }; + xcb_render_composite(xcbConnection(), XCB_RENDER_PICT_OP_SRC, effects->xrenderBufferPicture(), 0, *m_picture, + srcArea.x(), srcArea.y(), 0, 0, 0, 0, srcArea.width(), srcArea.height()); + xcb_flush(xcbConnection()); + xform.matrix11 = DOUBLE_TO_FIXED(1.0/zoom); + xform.matrix22 = DOUBLE_TO_FIXED(1.0/zoom); +#undef DOUBLE_TO_FIXED + xcb_render_set_picture_transform(xcbConnection(), *m_picture, xform); + xcb_render_set_picture_filter(xcbConnection(), *m_picture, 4, const_cast("good"), 0, nullptr); + xcb_render_composite(xcbConnection(), XCB_RENDER_PICT_OP_SRC, *m_picture, 0, effects->xrenderBufferPicture(), + 0, 0, 0, 0, area.x(), area.y(), area.width(), area.height() ); + xcb_render_set_picture_filter(xcbConnection(), *m_picture, 4, const_cast("fast"), 0, nullptr); + xcb_render_set_picture_transform(xcbConnection(), *m_picture, identity); + const xcb_rectangle_t rects[4] = { + { int16_t(area.x()+FRAME_WIDTH), int16_t(area.y()), uint16_t(area.width()-FRAME_WIDTH), uint16_t(FRAME_WIDTH)}, + { int16_t(area.right()-FRAME_WIDTH), int16_t(area.y()+FRAME_WIDTH), uint16_t(FRAME_WIDTH), uint16_t(area.height()-FRAME_WIDTH)}, + { int16_t(area.x()), int16_t(area.bottom()-FRAME_WIDTH), uint16_t(area.width()-FRAME_WIDTH), uint16_t(FRAME_WIDTH)}, + { int16_t(area.x()), int16_t(area.y()), uint16_t(FRAME_WIDTH), uint16_t(area.height()-FRAME_WIDTH)} + }; + xcb_render_fill_rectangles(xcbConnection(), XCB_RENDER_PICT_OP_SRC, effects->xrenderBufferPicture(), + preMultiply(QColor(0,0,0,255)), 4, rects); +#endif + } + } +} + +void MagnifierEffect::postPaintScreen() +{ + if (zoom != target_zoom) { + QRect framedarea = magnifierArea().adjusted(-FRAME_WIDTH, -FRAME_WIDTH, FRAME_WIDTH, FRAME_WIDTH); + effects->addRepaint(framedarea); + } + effects->postPaintScreen(); +} + +QRect MagnifierEffect::magnifierArea(QPoint pos) const +{ + return QRect(pos.x() - magnifier_size.width() / 2, pos.y() - magnifier_size.height() / 2, + magnifier_size.width(), magnifier_size.height()); +} + +void MagnifierEffect::zoomIn() +{ + target_zoom *= 1.2; + if (!polling) { + polling = true; + effects->startMousePolling(); + } + if (effects->isOpenGLCompositing() && !m_texture) { + effects->makeOpenGLContextCurrent(); + m_texture = new GLTexture(GL_RGBA8, magnifier_size.width(), magnifier_size.height()); + m_texture->setYInverted(false); + m_fbo = new GLRenderTarget(*m_texture); + } + effects->addRepaint(magnifierArea().adjusted(-FRAME_WIDTH, -FRAME_WIDTH, FRAME_WIDTH, FRAME_WIDTH)); +} + +void MagnifierEffect::zoomOut() +{ + target_zoom /= 1.2; + if (target_zoom <= 1) { + target_zoom = 1; + if (polling) { + polling = false; + effects->stopMousePolling(); + } + if (zoom == target_zoom) { + effects->makeOpenGLContextCurrent(); + delete m_fbo; + delete m_texture; + m_fbo = nullptr; + m_texture = nullptr; + destroyPixmap(); + } + } + effects->addRepaint(magnifierArea().adjusted(-FRAME_WIDTH, -FRAME_WIDTH, FRAME_WIDTH, FRAME_WIDTH)); +} + +void MagnifierEffect::toggle() +{ + if (zoom == 1.0) { + if (target_zoom == 1.0) { + target_zoom = 2; + } + if (!polling) { + polling = true; + effects->startMousePolling(); + } + if (effects->isOpenGLCompositing() && !m_texture) { + effects->makeOpenGLContextCurrent(); + m_texture = new GLTexture(GL_RGBA8, magnifier_size.width(), magnifier_size.height()); + m_texture->setYInverted(false); + m_fbo = new GLRenderTarget(*m_texture); + } + } else { + target_zoom = 1; + if (polling) { + polling = false; + effects->stopMousePolling(); + } + } + effects->addRepaint(magnifierArea().adjusted(-FRAME_WIDTH, -FRAME_WIDTH, FRAME_WIDTH, FRAME_WIDTH)); +} + +void MagnifierEffect::slotMouseChanged(const QPoint& pos, const QPoint& old, + Qt::MouseButtons, Qt::MouseButtons, Qt::KeyboardModifiers, Qt::KeyboardModifiers) +{ + if (pos != old && zoom != 1) + // need full repaint as we might lose some change events on fast mouse movements + // see Bug 187658 + effects->addRepaintFull(); +} + +bool MagnifierEffect::isActive() const +{ + return zoom != 1.0 || zoom != target_zoom; +} + +} // namespace + diff --git a/effects/magnifier/magnifier.h b/effects/magnifier/magnifier.h new file mode 100644 index 0000000..48b87fe --- /dev/null +++ b/effects/magnifier/magnifier.h @@ -0,0 +1,71 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Lubos Lunak + SPDX-FileCopyrightText: 2011 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_MAGNIFIER_H +#define KWIN_MAGNIFIER_H + +#include + +namespace KWin +{ + +class GLRenderTarget; +class GLTexture; +class XRenderPicture; + +class MagnifierEffect + : public Effect +{ + Q_OBJECT + Q_PROPERTY(QSize magnifierSize READ magnifierSize) + Q_PROPERTY(qreal targetZoom READ targetZoom) +public: + MagnifierEffect(); + ~MagnifierEffect() override; + void reconfigure(ReconfigureFlags) override; + void prePaintScreen(ScreenPrePaintData& data, int time) override; + void paintScreen(int mask, const QRegion ®ion, ScreenPaintData& data) override; + void postPaintScreen() override; + bool isActive() const override; + static bool supported(); + + // for properties + QSize magnifierSize() const { + return magnifier_size; + } + qreal targetZoom() const { + return target_zoom; + } +private Q_SLOTS: + void zoomIn(); + void zoomOut(); + void toggle(); + void slotMouseChanged(const QPoint& pos, const QPoint& old, + Qt::MouseButtons buttons, Qt::MouseButtons oldbuttons, + Qt::KeyboardModifiers modifiers, Qt::KeyboardModifiers oldmodifiers); + void destroyPixmap(); +private: + QRect magnifierArea(QPoint pos = cursorPos()) const; + double zoom; + double target_zoom; + bool polling; // Mouse polling + QSize magnifier_size; + GLTexture *m_texture; + GLRenderTarget *m_fbo; +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + xcb_pixmap_t m_pixmap; + QSize m_pixmapSize; + QScopedPointer m_picture; +#endif +}; + +} // namespace + +#endif diff --git a/effects/magnifier/magnifier.kcfg b/effects/magnifier/magnifier.kcfg new file mode 100644 index 0000000..85e08a1 --- /dev/null +++ b/effects/magnifier/magnifier.kcfg @@ -0,0 +1,18 @@ + + + + + + 200 + + + 200 + + + 1.0 + + + diff --git a/effects/magnifier/magnifier_config.cpp b/effects/magnifier/magnifier_config.cpp new file mode 100644 index 0000000..ee7d0d4 --- /dev/null +++ b/effects/magnifier/magnifier_config.cpp @@ -0,0 +1,109 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Christian Nitschkowski + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "magnifier_config.h" +// KConfigSkeleton +#include "magnifierconfig.h" +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +K_PLUGIN_FACTORY_WITH_JSON(MagnifierEffectConfigFactory, + "magnifier_config.json", + registerPlugin();) + +namespace KWin +{ + +MagnifierEffectConfigForm::MagnifierEffectConfigForm(QWidget* parent) : QWidget(parent) +{ + setupUi(this); +} + +MagnifierEffectConfig::MagnifierEffectConfig(QWidget* parent, const QVariantList& args) : + KCModule(KAboutData::pluginData(QStringLiteral("magnifier")), parent, args) +{ + m_ui = new MagnifierEffectConfigForm(this); + + QVBoxLayout* layout = new QVBoxLayout(this); + + layout->addWidget(m_ui); + + MagnifierConfig::instance(KWIN_CONFIG); + addConfig(MagnifierConfig::self(), m_ui); + + connect(m_ui->editor, &KShortcutsEditor::keyChange, this, &MagnifierEffectConfig::markAsChanged); + + // Shortcut config. The shortcut belongs to the component "kwin"! + m_actionCollection = new KActionCollection(this, QStringLiteral("kwin")); + + m_actionCollection->setComponentDisplayName(i18n("KWin")); + m_actionCollection->setConfigGroup(QStringLiteral("Magnifier")); + m_actionCollection->setConfigGlobal(true); + + QAction* a; + a = m_actionCollection->addAction(KStandardAction::ZoomIn); + a->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::META + Qt::Key_Equal); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::META + Qt::Key_Equal); + + a = m_actionCollection->addAction(KStandardAction::ZoomOut); + a->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::META + Qt::Key_Minus); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::META + Qt::Key_Minus); + + a = m_actionCollection->addAction(KStandardAction::ActualSize); + a->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::META + Qt::Key_0); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::META + Qt::Key_0); + + m_ui->editor->addCollection(m_actionCollection); + + load(); +} + +MagnifierEffectConfig::~MagnifierEffectConfig() +{ + // Undo (only) unsaved changes to global key shortcuts + m_ui->editor->undoChanges(); +} + +void MagnifierEffectConfig::save() +{ + qDebug() << "Saving config of Magnifier" ; + + m_ui->editor->save(); // undo() will restore to this state from now on + KCModule::save(); + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("magnifier")); +} + +void MagnifierEffectConfig::defaults() +{ + m_ui->editor->allDefault(); + KCModule::defaults(); +} + +} // namespace + +#include "magnifier_config.moc" diff --git a/effects/magnifier/magnifier_config.desktop b/effects/magnifier/magnifier_config.desktop new file mode 100644 index 0000000..0862555 --- /dev/null +++ b/effects/magnifier/magnifier_config.desktop @@ -0,0 +1,90 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=KCModule + +X-KDE-Library=kwin_magnifier_config +X-KDE-ParentComponents=magnifier + +Name=Magnifier +Name[af]=Vergroter +Name[ar]=المكبّر +Name[az]=Lupa +Name[be]=Павелічальная лупа +Name[be@latin]=Lupa +Name[bg]=Увеличител +Name[bn]=ম্যাগনিফায়ার +Name[bs]=Uveličavač +Name[ca]=Lupa +Name[ca@valencia]=Lupa +Name[cs]=Lupa +Name[csb]=Zwikszanié +Name[da]=Forstørrelsesglas +Name[de]=Lupe +Name[el]=Μεγεθυντής +Name[en_GB]=Magnifier +Name[eo]=Pligrandigilo +Name[es]=Lupa +Name[et]=Suurendaja +Name[eu]=Lupa +Name[fa]=ذره‌بین +Name[fi]=Suurennuslasi +Name[fr]=Loupe +Name[fy]=Fergrutter +Name[ga]=Formhéadaitheoir +Name[gl]=Lupa +Name[gu]=મોટું કરનાર +Name[he]=זכוכית מגדלת +Name[hi]=आतिशी शीशा +Name[hne]=आतिसी सीसा +Name[hr]=Povećalo +Name[hu]=Nagyító +Name[ia]=Aggranditor +Name[id]=Suryakanta +Name[is]=Stækkunargler +Name[it]=Lente d'ingrandimento +Name[ja]=虫めがね +Name[kk]=Ұлғайтқыш +Name[km]=X Magnifier +Name[kn]=ವರ್ಧಕ (ಮಸೂರ) +Name[ko]=돋보기 +Name[ku]=Mezinker +Name[lt]=Didinamasis stiklas +Name[lv]=Palielinātājs +Name[mai]=आवर्धक +Name[mk]=Лупа +Name[ml]=ഭൂതകണ്ണാടി +Name[mr]=वर्धक +Name[nb]=Lupe +Name[nds]=Kiekglas +Name[ne]=परिमार्जक +Name[nl]=Vergrootglas +Name[nn]=Forstørr skjermdel +Name[oc]=Lópia +Name[pa]=ਵੱਡਦਰਸ਼ੀ +Name[pl]=Powiększenie +Name[pt]=Lupa +Name[pt_BR]=Lente de aumento +Name[ro]=Lupă +Name[ru]=Лупа +Name[se]=Stuorideaddji +Name[si]=විශාලකය +Name[sk]=Lupa +Name[sl]=Povečevalnik +Name[sr]=Увеличавач +Name[sr@ijekavian]=Увеличавач +Name[sr@ijekavianlatin]=Uveličavač +Name[sr@latin]=Uveličavač +Name[sv]=Förstoringsglas +Name[ta]=பெரிதாக்கி +Name[te]=మాగ్నిఫైర్ +Name[th]=แว่นขยาย +Name[tr]=Büyüteç +Name[ug]=چوڭايتقۇچ +Name[uk]=Лупа +Name[uz]=Kattalashtiruvchi +Name[uz@cyrillic]=Катталаштирувчи +Name[vi]=Kính lúp +Name[wa]=Magnifieu +Name[x-test]=xxMagnifierxx +Name[zh_CN]=放大镜 +Name[zh_TW]=放大鏡 diff --git a/effects/magnifier/magnifier_config.h b/effects/magnifier/magnifier_config.h new file mode 100644 index 0000000..313c0d9 --- /dev/null +++ b/effects/magnifier/magnifier_config.h @@ -0,0 +1,46 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Christian Nitschkowski + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_MAGNIFIER_CONFIG_H +#define KWIN_MAGNIFIER_CONFIG_H + +#include + +#include "ui_magnifier_config.h" + +class KActionCollection; + +namespace KWin +{ + +class MagnifierEffectConfigForm : public QWidget, public Ui::MagnifierEffectConfigForm +{ + Q_OBJECT +public: + explicit MagnifierEffectConfigForm(QWidget* parent); +}; + +class MagnifierEffectConfig : public KCModule +{ + Q_OBJECT +public: + explicit MagnifierEffectConfig(QWidget* parent = nullptr, const QVariantList& args = QVariantList()); + ~MagnifierEffectConfig() override; + + void save() override; + void defaults() override; + +private: + MagnifierEffectConfigForm* m_ui; + KActionCollection* m_actionCollection; +}; + +} // namespace + +#endif diff --git a/effects/magnifier/magnifier_config.ui b/effects/magnifier/magnifier_config.ui new file mode 100644 index 0000000..ea2e1f3 --- /dev/null +++ b/effects/magnifier/magnifier_config.ui @@ -0,0 +1,106 @@ + + + KWin::MagnifierEffectConfigForm + + + + 0 + 0 + 246 + 181 + + + + + + + Size + + + + + + &Width: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_Width + + + + + + + + 0 + 0 + + + + px + + + 9999 + + + 10 + + + + + + + &Height: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_Height + + + + + + + + 0 + 0 + + + + px + + + 9999 + + + 10 + + + + + + + + + + KShortcutsEditor::GlobalAction + + + + + + + + KShortcutsEditor + QWidget +
KShortcutsEditor
+ 1 +
+
+ + +
diff --git a/effects/magnifier/magnifierconfig.kcfgc b/effects/magnifier/magnifierconfig.kcfgc new file mode 100644 index 0000000..6286bc8 --- /dev/null +++ b/effects/magnifier/magnifierconfig.kcfgc @@ -0,0 +1,5 @@ +File=magnifier.kcfg +ClassName=MagnifierConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/effects/maximize/package/contents/code/maximize.js b/effects/maximize/package/contents/code/maximize.js new file mode 100644 index 0000000..31f6846 --- /dev/null +++ b/effects/maximize/package/contents/code/maximize.js @@ -0,0 +1,90 @@ +/* + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +"use strict"; + +var maximizeEffect = { + duration: animationTime(250), + loadConfig: function () { + maximizeEffect.duration = animationTime(250); + }, + maximizeChanged: function (window) { + if (!window.oldGeometry) { + return; + } + window.setData(Effect.WindowForceBlurRole, true); + var oldGeometry, newGeometry; + oldGeometry = window.oldGeometry; + newGeometry = window.geometry; + if (oldGeometry.width == newGeometry.width && oldGeometry.height == newGeometry.height) + oldGeometry = window.olderGeometry; + window.olderGeometry = window.oldGeometry; + window.oldGeometry = newGeometry; + window.maximizeAnimation1 = animate({ + window: window, + duration: maximizeEffect.duration, + animations: [{ + type: Effect.Size, + to: { + value1: newGeometry.width, + value2: newGeometry.height + }, + from: { + value1: oldGeometry.width, + value2: oldGeometry.height + } + }, { + type: Effect.Translation, + to: { + value1: 0, + value2: 0 + }, + from: { + value1: oldGeometry.x - newGeometry.x - (newGeometry.width / 2 - oldGeometry.width / 2), + value2: oldGeometry.y - newGeometry.y - (newGeometry.height / 2 - oldGeometry.height / 2) + } + }] + }); + if (!window.resize) { + window.maximizeAnimation2 =animate({ + window: window, + duration: maximizeEffect.duration, + animations: [{ + type: Effect.CrossFadePrevious, + to: 1.0, + from: 0.0 + }] + }); + } + }, + restoreForceBlurState: function(window) { + window.setData(Effect.WindowForceBlurRole, null); + }, + geometryChange: function (window, oldGeometry) { + if (window.maximizeAnimation1) { + if (window.geometry.width != window.oldGeometry.width || + window.geometry.height != window.oldGeometry.height) { + cancel(window.maximizeAnimation1); + delete window.maximizeAnimation1; + if (window.maximizeAnimation2) { + cancel(window.maximizeAnimation2); + delete window.maximizeAnimation2; + } + } + } + window.oldGeometry = window.geometry; + window.olderGeometry = oldGeometry; + }, + init: function () { + effect.configChanged.connect(maximizeEffect.loadConfig); + effects.windowFrameGeometryChanged.connect(maximizeEffect.geometryChange); + effects.windowMaximizedStateChanged.connect(maximizeEffect.maximizeChanged); + effect.animationEnded.connect(maximizeEffect.restoreForceBlurState); + } +}; +maximizeEffect.init(); diff --git a/effects/maximize/package/metadata.desktop b/effects/maximize/package/metadata.desktop new file mode 100644 index 0000000..4065a81 --- /dev/null +++ b/effects/maximize/package/metadata.desktop @@ -0,0 +1,116 @@ +[Desktop Entry] +Comment=Animation for a window going to maximize/restore from maximize +Comment[az]=Tam açıldıqdan sonra ölçüsü genişlnəcək/bərpa olunacaq pəncərənin animasiyası +Comment[bs]=Animacija za prozor koji ide u maksimiziranje/vraćanje iz maksimiziranja +Comment[ca]=Animació per a una finestra que es maximitza/restaura des de maximització +Comment[ca@valencia]=Animació per una finestra que es maximitza/restaura des de maximització +Comment[cs]=Animace okna pro maximalizaci/obnovení z maximalizace +Comment[da]=Animation til et vindue der er ved at maksimere/gendanne fra maksimering +Comment[de]=Animation für ein Fenster beim Maximieren und Wiederherstellen +Comment[el]=Εφέ κίνησης για παράθυρο που πρόκειται να εκτελέσει μεγιστοποίηση / επαναφορά από μεγιστοποίηση +Comment[en_GB]=Animation for a window going to maximise/restore from maximise +Comment[es]=Animación para una ventana que se va a maximizar o restaurar de una maximización +Comment[et]=Maksimeeritava või maksimeeritust taastatava akna animatsioon +Comment[eu]=Leihoa maximizatzera/maximizatutik leheneratzera doanerako animazioa +Comment[fi]=Animointi ikkunalle, joka suurennetaan tai palautetaan suurennuksesta +Comment[fr]=Animation pour une fenêtre passant d'un état maximisé vers un état maximisé / restauré +Comment[gl]=Animación das xanelas que se maximizan ou restauran maximizadas +Comment[he]=שינוי גודל החלון בצורה חלקה +Comment[hu]=Ablakanimáció maximalizáláskor / maximális méretről való visszaállításkor +Comment[ia]=Animation per un fenestra va maximisar/restabilir ab maximizar +Comment[id]=Animasi untuk window yang menuju ke maksimalkan/kembalikan dari maksimalkan +Comment[it]=Animazione per una finestra massimizzata o ripristinata dalla massimizzazione +Comment[kk]=Терезені кең жаю/қалпына қайтаруды анимациясы +Comment[ko]=최대화된 창이 최소화되거나 복원될 때 사용할 애니메이션 +Comment[lt]=Lango iÅ¡skleidimo/atkÅ«rimo iÅ¡ iÅ¡skleidimo animacija +Comment[mr]=चौकट मोठी/पुन्हस्थापित करताना ऍनीमेट करा +Comment[nb]=Animering for et vindu som maksimeres/tilbakestilles fra maksimering +Comment[nds]=Animeren för en Finster, dat maximeert/vun maximeert wedderherstellt warrt +Comment[nl]=Animatie voor een venster dat gaat naar maximaliseren/herstellen vanuit maximaliseren +Comment[nn]=Vindaugsanimasjon i samband med bruk av maksimering/gjenoppretting. +Comment[pl]=Okna są gładko skalowane przy maksymalizacji i przy powrocie z niej +Comment[pt]=Animação para uma janela que se vai maximizar/repor da maximização +Comment[pt_BR]=Animação para uma janela a ser maximizada/restaurada +Comment[ro]=Animație pentru o fereastră ce trece la maximizare/restabilire din maximizare +Comment[ru]=Анимация для окна, которое будет максимизировано/восстановит размер после максимизации +Comment[sk]=Animácia pre okno pri maximalizácii/obnovení z maximalizácie +Comment[sl]=Animacija za razpenjanje okna ali obnavljanje iz razpetega okna +Comment[sr]=Анимација при максимизовању и обнављању прозора +Comment[sr@ijekavian]=Анимација при максимизовању и обнављању прозора +Comment[sr@ijekavianlatin]=Animacija pri maksimizovanju i obnavljanju prozora +Comment[sr@latin]=Animacija pri maksimizovanju i obnavljanju prozora +Comment[sv]=Animering för ett fönster som ska maximeras eller Ã¥terställas frÃ¥n maximering +Comment[tr]=Pencerenin büyütülürken veya küçültülürken gösterilecek animasyon +Comment[uk]=Анімація для вікон під час максимізації/відновлення з максимізації +Comment[vi]=Hiệu ứng cho một cá»­a sổ chuẩn bị phóng to hoặc khôi phục lại từ trạng thái phóng to +Comment[x-test]=xxAnimation for a window going to maximize/restore from maximizexx +Comment[zh_CN]=窗口最大化/从最大化恢复时显示动画 +Comment[zh_TW]=視窗最大化/回復時的動畫 +Icon=preferences-system-windows-effect-maximize +Name=Maximize +Name[ast]=Maximización +Name[az]=Genişləndirmək +Name[bs]=Maksimiziraj +Name[ca]=Maximitza +Name[ca@valencia]=Maximitza +Name[cs]=Maximalizovat +Name[da]=Maksimér +Name[de]=Maximieren +Name[el]=Μεγιστοποίηση +Name[en_GB]=Maximise +Name[es]=Maximizar +Name[et]=Maksimeerimine +Name[eu]=Maximizatu +Name[fi]=Suurentaminen +Name[fr]=Maximiser +Name[ga]=Uasmhéadaigh +Name[gl]=Maximizar +Name[he]=שינוי גודל חלק +Name[hu]=Maximalizálás +Name[ia]=Maximiza +Name[id]=Maksimalkan +Name[is]=Hámarka +Name[it]=Massimizzazione +Name[ja]=最大化 +Name[kk]=Кең жаю +Name[ko]=최대화 +Name[lt]=IÅ¡skleisti +Name[mr]=मोठी करा +Name[nb]=Maksimer +Name[nds]=Maximeren +Name[nl]=Maximaliseren +Name[nn]=Maksimer +Name[pa]=ਵੱਧੋ-ਵੱਧ +Name[pl]=Animacja maksymalizacji +Name[pt]=Maximização +Name[pt_BR]=Maximizar +Name[ro]=Maximizează +Name[ru]=Максимизировать +Name[sk]=MaximalizovaÅ¥ +Name[sl]=Razpni +Name[sr]=Максимизовање +Name[sr@ijekavian]=Максимизовање +Name[sr@ijekavianlatin]=Maksimizovanje +Name[sr@latin]=Maksimizovanje +Name[sv]=Maximera +Name[tr]=Ekranı Kapla +Name[ug]=چوڭايت +Name[uk]=Максимізація +Name[x-test]=xxMaximizexx +Name[zh_CN]=最大化 +Name[zh_TW]=最大化 +Type=Service +X-KDE-ParentApp= +X-KDE-PluginInfo-Author=Martin Gräßlin +X-KDE-PluginInfo-Category=Appearance +X-KDE-PluginInfo-Email=mgraesslin@kde.org +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-Name=kwin4_effect_maximize +X-KDE-PluginInfo-Version=1 +X-KDE-PluginInfo-Website= +X-KDE-ServiceTypes=KWin/Effect +X-KDE-PluginInfo-EnabledByDefault=true +X-KDE-Ordering=60 +X-Plasma-API=javascript +X-Plasma-MainScript=code/maximize.js +X-KWin-Video-Url=https://files.kde.org/plasma/kwin/effect-videos/maximize.ogv diff --git a/effects/morphingpopups/package/contents/code/morphingpopups.js b/effects/morphingpopups/package/contents/code/morphingpopups.js new file mode 100644 index 0000000..333b7ec --- /dev/null +++ b/effects/morphingpopups/package/contents/code/morphingpopups.js @@ -0,0 +1,125 @@ +/* + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2012 Martin Gräßlin + SPDX-FileCopyrightText: 2016 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +"use strict"; + +var morphingEffect = { + duration: animationTime(150), + loadConfig: function () { + morphingEffect.duration = animationTime(150); + }, + + handleFrameGeometryChanged: function (window, oldGeometry) { + //only tooltips and notifications + if (!window.tooltip && !window.notification && !window.criticalNotification) { + return; + } + + var newGeometry = window.geometry; + + //only do the transition for near enough tooltips, + //don't cross the whole screen: ugly + var distance = Math.abs(oldGeometry.x - newGeometry.x) + Math.abs(oldGeometry.y - newGeometry.y); + + if (distance > (newGeometry.width + newGeometry.height) * 2) { + if (window.moveAnimation) { + delete window.moveAnimation; + } + if (window.fadeAnimation) { + delete window.fadeAnimation; + } + + return; + } + + //don't resize it "too much", set as four times + if ((newGeometry.width / oldGeometry.width) > 4 || + (oldGeometry.width / newGeometry.width) > 4 || + (newGeometry.height / oldGeometry.height) > 4 || + (oldGeometry.height / newGeometry.height) > 4) { + return; + } + + window.setData(Effect.WindowForceBackgroundContrastRole, true); + window.setData(Effect.WindowForceBlurRole, true); + + var couldRetarget = false; + + if (window.moveAnimation) { + if (window.moveAnimation[0]) { + couldRetarget = retarget(window.moveAnimation[0], { + value1: newGeometry.width, + value2: newGeometry.height + }, morphingEffect.duration); + } + if (couldRetarget && window.moveAnimation[1]) { + couldRetarget = retarget(window.moveAnimation[1], { + value1: newGeometry.x + newGeometry.width/2, + value2: newGeometry.y + newGeometry.height / 2 + }, morphingEffect.duration); + } + if (!couldRetarget) { + cancel(window.moveAnimation[0]); + } + + } + + if (!couldRetarget) { + window.moveAnimation = animate({ + window: window, + duration: morphingEffect.duration, + animations: [{ + type: Effect.Size, + to: { + value1: newGeometry.width, + value2: newGeometry.height + }, + from: { + value1: oldGeometry.width, + value2: oldGeometry.height + } + }, { + type: Effect.Position, + to: { + value1: newGeometry.x + newGeometry.width / 2, + value2: newGeometry.y + newGeometry.height / 2 + }, + from: { + value1: oldGeometry.x + oldGeometry.width / 2, + value2: oldGeometry.y + oldGeometry.height / 2 + } + }] + }); + + } + + couldRetarget = false; + if (window.fadeAnimation) { + couldRetarget = retarget(window.fadeAnimation[0], 1.0, morphingEffect.duration); + } + + if (!couldRetarget) { + window.fadeAnimation = animate({ + window: window, + duration: morphingEffect.duration, + animations: [{ + type: Effect.CrossFadePrevious, + to: 1.0, + from: 0.0 + }] + }); + } + }, + + init: function () { + effect.configChanged.connect(morphingEffect.loadConfig); + effects.windowFrameGeometryChanged.connect(morphingEffect.handleFrameGeometryChanged); + } +}; +morphingEffect.init(); diff --git a/effects/morphingpopups/package/metadata.desktop b/effects/morphingpopups/package/metadata.desktop new file mode 100644 index 0000000..a919f3f --- /dev/null +++ b/effects/morphingpopups/package/metadata.desktop @@ -0,0 +1,95 @@ +[Desktop Entry] +Comment=Cross fade animation when Tooltips or Notifications change their geometry +Comment[az]=Sürüşən ip ucları və ya bildirişlər pəncərəsinin forması dəyişdirilərkən onlar tədricən dartılır və yığılır +Comment[ca]=Animació d'esvaïment creuat quan els consells d'eines o les notificacions canvien la seva geometria +Comment[ca@valencia]=Animació d'esvaïment creuat quan els consells d'eines o les notificacions canvien la seua geometria +Comment[da]=Crossfade-animation nÃ¥r værktøjstip og bekendtgørelser skifter geometry +Comment[de]=Überblendende Animationen, wenn Kurzinfos oder Benachrichtigungen ihre Geometrie ändern +Comment[el]=Εφέ εναλλαγής με εξασθένιση όταν οι Υποδείξεις ή οι Ειδοποιήσεις αλλάζουν τη γεωμετρία τους +Comment[en_GB]=Cross fade animation when Tooltips or Notifications change their geometry +Comment[es]=Animación de transición suave cuando las ayudas emergentes o las notificaciones cambian su geometría +Comment[et]=Animatsioon hääbumisega, kui kohtspikrid või märguanded muudavad geomeetriat +Comment[eu]=Desagertze gurutzatua tresnen argibideek edo jakinarazpenek geometria aldatzen dutenean +Comment[fi]=Häivytysanimaatio työkaluvihjeiden tai ilmoitusten koon muuttuessa +Comment[fr]=Animation de fondu quand les infobulles ou les notifications changent de forme +Comment[gl]=Usar unha animación de esvaemento cando a xeometría das mensaxes emerxentes de axuda ou notificación cambia. +Comment[he]=משנה את גודל החלונית הקופצת בצורה חלקה (שעוברים בין חלוניות) +Comment[hu]=Átalakuló animáció a Buboréksúgók vagy Értesítések geometriájának változtatásakor +Comment[id]=Animasi lesap silang ketika Tip-alat atau Notifikasi berubah pada geometrinya +Comment[it]=Animazione in dissolvenza quando i suggerimenti e le notifiche cambiano la loro geometria +Comment[ko]=풍선 도움말이나 알림 크기가 변경될 때 크로스페이드 애니메이션 사용 +Comment[lt]=UžplÅ«dimo animacija, kai paaiÅ¡kinimai ar praneÅ¡imai keičia savo geometriją +Comment[nl]=Verwissel animatie van opkomen/vervagen wanneer tekstballonnen of meldingen hun geometrie wijzigen +Comment[nn]=Krysstoningsanimasjon nÃ¥r hjelpebobler eller varslingar endrar form +Comment[pl]=Efekt zmiany kształtu podpowiedzi i powiadomień przy zmianie ich geometrii +Comment[pt]=Animar o desvanecimento quando as dicas ou notificações mudarem de tamanho +Comment[pt_BR]=Animar a transição suave quando as dicas ou notificações mudarem de tamanho +Comment[ru]=При изменении формы всплывающих подсказок или уведомлений они плавно растягиваются или сжимаются +Comment[sk]=Prelínacia animácia pri tooltipoch alebo notifikáciách pri zmene ich geometrie +Comment[sl]=Animacija navzkrižnega pojemanja ob spremembi geometrije orodnih namigov in obvestil +Comment[sr]=Анимација претапања када облачићи или обавештења мењају геометрију +Comment[sr@ijekavian]=Анимација претапања када облачићи или обавештења мењају геометрију +Comment[sr@ijekavianlatin]=Animacija pretapanja kada oblačići ili obaveÅ¡tenja menjaju geometriju +Comment[sr@latin]=Animacija pretapanja kada oblačići ili obaveÅ¡tenja menjaju geometriju +Comment[sv]=Övertona animering när verktygstips eller underrättelser ändrar storlek +Comment[tr]=Araç İpuçları veya Bildirimler geometrilerini değiştirdiğinde çapraz solma animasyonu göster +Comment[uk]=Анімація зі зміною освітленості під час зміни геометрії панелей підказок і сповіщень +Comment[x-test]=xxCross fade animation when Tooltips or Notifications change their geometryxx +Comment[zh_CN]=在工具提示或者通知大小变化时交叉淡入淡出的动画 +Comment[zh_TW]=當工具提示或通知變更位置時交錯的淡出動畫 +Icon=preferences-system-windows-effect-morphingpopups +Name=Morphing popups +Name[az]=Sürüşən pəncərələrin əmələ gəlmə animasiyası +Name[ca]=Missatges emergents en metamorfosi +Name[ca@valencia]=Missatges emergents en metamorfosi +Name[da]=Sammensmeltende pop-op'er +Name[de]=Verformende Aufklappfenster +Name[el]=Αναδυόμενα morphing +Name[en_GB]=Morphing popups +Name[es]=Transformación de ventanas emergentes +Name[et]=Moonduvad hüpikdialoogid +Name[eu]=Eraldatzen diren gainerakorrak +Name[fi]=Ponnahdusikkunoiden muodonmuutos +Name[fr]=Fondu des menus surgissants +Name[gl]=Xanelas emerxentes cambiantes +Name[he]=החלקת חלונות קופצים +Name[hu]=Átalakuló felugró ablakok +Name[ia]=Popups de Morphing +Name[id]=Sembulan perubahan +Name[it]=Finestre a comparsa che si trasformano +Name[ko]=변형되는 팝업 +Name[lt]=Besikeičiantys iÅ¡kylantieji langai +Name[nl]=Morphing popups +Name[nn]=Formendring for sprettoppvindauge +Name[pl]=Zmiennokształtne elementy pomocnicze +Name[pt]=Mensagens com mudança de forma +Name[pt_BR]=Mensagens com mudança de forma +Name[ro]=Indicii în schimbare +Name[ru]=Анимация преобразования всплывающих окон +Name[sk]=VysúvaÅ¥ vyskakovacie okná +Name[sl]=Prehajajoča pojavna okna +Name[sr]=Претапајући искакачи +Name[sr@ijekavian]=Претапајући искакачи +Name[sr@ijekavianlatin]=Pretapajući iskakači +Name[sr@latin]=Pretapajući iskakači +Name[sv]=Föränderliga meddelanderutor +Name[tr]=Dönüşümlü pencereler +Name[uk]=Аморфні контекстні панелі +Name[x-test]=xxMorphing popupsxx +Name[zh_CN]=变形气泡通知 +Name[zh_TW]=交錯彈出視窗 +Type=Service +X-KDE-ParentApp= +X-KDE-PluginInfo-Author=Marco Martin +X-KDE-PluginInfo-Category=Appearance +X-KDE-PluginInfo-Email=mart@kde.org +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-Name=kwin4_effect_morphingpopups +X-KDE-PluginInfo-Version=1 +X-KDE-PluginInfo-Website= +X-KDE-ServiceTypes=KWin/Effect +X-KDE-PluginInfo-EnabledByDefault=true +X-KDE-Ordering=60 +X-Plasma-API=javascript +X-Plasma-MainScript=code/morphingpopups.js +X-KWin-Video-Url=https://files.kde.org/plasma/kwin/effect-videos/morphingpopups.ogv diff --git a/effects/mouseclick/CMakeLists.txt b/effects/mouseclick/CMakeLists.txt new file mode 100644 index 0000000..1b1834d --- /dev/null +++ b/effects/mouseclick/CMakeLists.txt @@ -0,0 +1,25 @@ +########################## +## configurtion dialog +########################## +set(kwin_mouseclick_config_SRCS mouseclick_config.cpp) +ki18n_wrap_ui(kwin_mouseclick_config_SRCS mouseclick_config.ui) +kconfig_add_kcfg_files(kwin_mouseclick_config_SRCS mouseclickconfig.kcfgc) + +add_library(kwin_mouseclick_config MODULE ${kwin_mouseclick_config_SRCS}) + +target_link_libraries(kwin_mouseclick_config + KF5::ConfigWidgets + KF5::GlobalAccel + KF5::I18n + KF5::XmlGui + KWinEffectsInterface +) + +kcoreaddons_desktop_to_json(kwin_mouseclick_config mouseclick_config.desktop SERVICE_TYPES kcmodule.desktop) + +install( + TARGETS + kwin_mouseclick_config + DESTINATION + ${PLUGIN_INSTALL_DIR}/kwin/effects/configs +) diff --git a/effects/mouseclick/mouseclick.cpp b/effects/mouseclick/mouseclick.cpp new file mode 100644 index 0000000..1ce5af2 --- /dev/null +++ b/effects/mouseclick/mouseclick.cpp @@ -0,0 +1,378 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2012 Filip Wieladek + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "mouseclick.h" +// KConfigSkeleton +#include "mouseclickconfig.h" + +#include +#include + +#ifdef KWIN_HAVE_XRENDER_COMPOSITING +#include +#include +#include +#endif + +#include +#include + +#include + +#include + +namespace KWin +{ + +MouseClickEffect::MouseClickEffect() +{ + initConfig(); + m_enabled = false; + + QAction* a = new QAction(this); + a->setObjectName(QStringLiteral("ToggleMouseClick")); + a->setText(i18n("Toggle Mouse Click Effect")); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::META + Qt::Key_Asterisk); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::META + Qt::Key_Asterisk); + effects->registerGlobalShortcut(Qt::META + Qt::Key_Asterisk, a); + connect(a, &QAction::triggered, this, &MouseClickEffect::toggleEnabled); + + reconfigure(ReconfigureAll); + + m_buttons[0] = new MouseButton(i18nc("Left mouse button", "Left"), Qt::LeftButton); + m_buttons[1] = new MouseButton(i18nc("Middle mouse button", "Middle"), Qt::MiddleButton); + m_buttons[2] = new MouseButton(i18nc("Right mouse button", "Right"), Qt::RightButton); +} + +MouseClickEffect::~MouseClickEffect() +{ + if (m_enabled) + effects->stopMousePolling(); + qDeleteAll(m_clicks); + m_clicks.clear(); + + for (int i = 0; i < BUTTON_COUNT; ++i) { + delete m_buttons[i]; + } +} + +void MouseClickEffect::reconfigure(ReconfigureFlags) +{ + MouseClickConfig::self()->read(); + m_colors[0] = MouseClickConfig::color1(); + m_colors[1] = MouseClickConfig::color2(); + m_colors[2] = MouseClickConfig::color3(); + m_lineWidth = MouseClickConfig::lineWidth(); + m_ringLife = MouseClickConfig::ringLife(); + m_ringMaxSize = MouseClickConfig::ringSize(); + m_ringCount = MouseClickConfig::ringCount(); + m_showText = MouseClickConfig::showText(); + m_font = MouseClickConfig::font(); +} + +void MouseClickEffect::prePaintScreen(ScreenPrePaintData& data, int time) +{ + foreach (MouseEvent* click, m_clicks) { + click->m_time += time; + } + + for (int i = 0; i < BUTTON_COUNT; ++i) { + if (m_buttons[i]->m_isPressed) { + m_buttons[i]->m_time += time; + } + } + + while (m_clicks.size() > 0) { + MouseEvent* first = m_clicks[0]; + if (first->m_time <= m_ringLife) { + break; + } + m_clicks.pop_front(); + delete first; + } + + effects->prePaintScreen(data, time); +} + +void MouseClickEffect::paintScreen(int mask, const QRegion ®ion, ScreenPaintData& data) +{ + effects->paintScreen(mask, region, data); + + paintScreenSetup(mask, region, data); + foreach (const MouseEvent* click, m_clicks) { + for (int i = 0; i < m_ringCount; ++i) { + float alpha = computeAlpha(click, i); + float size = computeRadius(click, i); + if (size > 0 && alpha > 0) { + QColor color = m_colors[click->m_button]; + color.setAlphaF(alpha); + drawCircle(color, click->m_pos.x(), click->m_pos.y(), size); + } + } + + if (m_showText && click->m_frame) { + float frameAlpha = (click->m_time * 2.0f - m_ringLife) / m_ringLife; + frameAlpha = frameAlpha < 0 ? 1 : -(frameAlpha * frameAlpha) + 1; + click->m_frame->render(infiniteRegion(), frameAlpha, frameAlpha); + } + } + paintScreenFinish(mask, region, data); +} + +void MouseClickEffect::postPaintScreen() +{ + effects->postPaintScreen(); + repaint(); +} + +float MouseClickEffect::computeRadius(const MouseEvent* click, int ring) +{ + float ringDistance = m_ringLife / (m_ringCount * 3); + if (click->m_press) { + return ((click->m_time - ringDistance * ring) / m_ringLife) * m_ringMaxSize; + } + return ((m_ringLife - click->m_time - ringDistance * ring) / m_ringLife) * m_ringMaxSize; +} + +float MouseClickEffect::computeAlpha(const MouseEvent* click, int ring) +{ + float ringDistance = m_ringLife / (m_ringCount * 3); + return (m_ringLife - (float)click->m_time - ringDistance * (ring)) / m_ringLife; +} + +void MouseClickEffect::slotMouseChanged(const QPoint& pos, const QPoint&, + Qt::MouseButtons buttons, Qt::MouseButtons oldButtons, + Qt::KeyboardModifiers, Qt::KeyboardModifiers) +{ + if (buttons == oldButtons) + return; + + MouseEvent* m = nullptr; + int i = BUTTON_COUNT; + while (--i >= 0) { + MouseButton* b = m_buttons[i]; + if (isPressed(b->m_button, buttons, oldButtons)) { + m = new MouseEvent(i, pos, 0, createEffectFrame(pos, b->m_labelDown), true); + break; + } else if (isReleased(b->m_button, buttons, oldButtons) && (!b->m_isPressed || b->m_time > m_ringLife)) { + // we might miss a press, thus also check !b->m_isPressed, bug #314762 + m = new MouseEvent(i, pos, 0, createEffectFrame(pos, b->m_labelUp), false); + break; + } + b->setPressed(b->m_button & buttons); + } + + if (m) { + m_clicks.append(m); + } + repaint(); +} + +EffectFrame* MouseClickEffect::createEffectFrame(const QPoint& pos, const QString& text) { + if (!m_showText) { + return nullptr; + } + QPoint point(pos.x() + m_ringMaxSize, pos.y()); + EffectFrame* frame = effects->effectFrame(EffectFrameStyled, false, point, Qt::AlignLeft); + frame->setFont(m_font); + frame->setText(text); + return frame; +} + +void MouseClickEffect::repaint() +{ + if (m_clicks.size() > 0) { + QRegion dirtyRegion; + const int radius = m_ringMaxSize + m_lineWidth; + foreach (MouseEvent* click, m_clicks) { + dirtyRegion |= QRect(click->m_pos.x() - radius, click->m_pos.y() - radius, 2*radius, 2*radius); + if (click->m_frame) { + // we grant the plasma style 32px padding for stuff like shadows... + dirtyRegion |= click->m_frame->geometry().adjusted(-32,-32,32,32); + } + } + effects->addRepaint(dirtyRegion); + } +} + +bool MouseClickEffect::isReleased(Qt::MouseButtons button, Qt::MouseButtons buttons, Qt::MouseButtons oldButtons) +{ + return !(button & buttons) && (button & oldButtons); +} + +bool MouseClickEffect::isPressed(Qt::MouseButtons button, Qt::MouseButtons buttons, Qt::MouseButtons oldButtons) +{ + return (button & buttons) && !(button & oldButtons); +} + +void MouseClickEffect::toggleEnabled() +{ + m_enabled = !m_enabled; + + if (m_enabled) { + connect(effects, &EffectsHandler::mouseChanged, this, &MouseClickEffect::slotMouseChanged); + effects->startMousePolling(); + } else { + disconnect(effects, &EffectsHandler::mouseChanged, this, &MouseClickEffect::slotMouseChanged); + effects->stopMousePolling(); + } + + qDeleteAll(m_clicks); + m_clicks.clear(); + + for (int i = 0; i < BUTTON_COUNT; ++i) { + m_buttons[i]->m_time = 0; + m_buttons[i]->m_isPressed = false; + } +} + +bool MouseClickEffect::isActive() const +{ + return m_enabled && (m_clicks.size() > 0); +} + +void MouseClickEffect::drawCircle(const QColor& color, float cx, float cy, float r) +{ + if (effects->isOpenGLCompositing()) + drawCircleGl(color, cx, cy, r); + if (effects->compositingType() == XRenderCompositing) + drawCircleXr(color, cx, cy, r); + if (effects->compositingType() == QPainterCompositing) + drawCircleQPainter(color, cx, cy, r); +} + +void MouseClickEffect::paintScreenSetup(int mask, QRegion region, ScreenPaintData& data) +{ + if (effects->isOpenGLCompositing()) + paintScreenSetupGl(mask, region, data); +} + +void MouseClickEffect::paintScreenFinish(int mask, QRegion region, ScreenPaintData& data) +{ + if (effects->isOpenGLCompositing()) + paintScreenFinishGl(mask, region, data); +} + +void MouseClickEffect::drawCircleGl(const QColor& color, float cx, float cy, float r) +{ + static const int num_segments = 80; + static const float theta = 2 * 3.1415926 / float(num_segments); + static const float c = cosf(theta); //precalculate the sine and cosine + static const float s = sinf(theta); + float t; + + float x = r;//we start at angle = 0 + float y = 0; + + GLVertexBuffer* vbo = GLVertexBuffer::streamingBuffer(); + vbo->reset(); + vbo->setUseColor(true); + vbo->setColor(color); + QVector verts; + verts.reserve(num_segments * 2); + + for (int ii = 0; ii < num_segments; ++ii) { + verts << x + cx << y + cy;//output vertex + //apply the rotation matrix + t = x; + x = c * x - s * y; + y = s * t + c * y; + } + vbo->setData(verts.size() / 2, 2, verts.data(), nullptr); + vbo->render(GL_LINE_LOOP); +} + +void MouseClickEffect::drawCircleXr(const QColor& color, float cx, float cy, float r) +{ +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + if (r <= m_lineWidth) + return; + + int num_segments = r+8; + float theta = 2.0 * 3.1415926 / num_segments; + float cos = cosf(theta); //precalculate the sine and cosine + float sin = sinf(theta); + float x[2] = {r, r-m_lineWidth}; + float y[2] = {0, 0}; + +#define DOUBLE_TO_FIXED(d) ((xcb_render_fixed_t) ((d) * 65536)) + QVector strip; + strip.reserve(2*num_segments+2); + + xcb_render_pointfix_t point; + point.x = DOUBLE_TO_FIXED(x[1]+cx); + point.y = DOUBLE_TO_FIXED(y[1]+cy); + strip << point; + + for (int i = 0; i < num_segments; ++i) { + //apply the rotation matrix + const float h[2] = {x[0], x[1]}; + x[0] = cos * x[0] - sin * y[0]; + x[1] = cos * x[1] - sin * y[1]; + y[0] = sin * h[0] + cos * y[0]; + y[1] = sin * h[1] + cos * y[1]; + + point.x = DOUBLE_TO_FIXED(x[0]+cx); + point.y = DOUBLE_TO_FIXED(y[0]+cy); + strip << point; + + point.x = DOUBLE_TO_FIXED(x[1]+cx); + point.y = DOUBLE_TO_FIXED(y[1]+cy); + strip << point; + } + + const float h = x[0]; + x[0] = cos * x[0] - sin * y[0]; + y[0] = sin * h + cos * y[0]; + + point.x = DOUBLE_TO_FIXED(x[0]+cx); + point.y = DOUBLE_TO_FIXED(y[0]+cy); + strip << point; + + XRenderPicture fill = xRenderFill(color); + xcb_render_tri_strip(xcbConnection(), XCB_RENDER_PICT_OP_OVER, + fill, effects->xrenderBufferPicture(), 0, + 0, 0, strip.count(), strip.constData()); +#undef DOUBLE_TO_FIXED +#else + Q_UNUSED(color) + Q_UNUSED(cx) + Q_UNUSED(cy) + Q_UNUSED(r) +#endif +} + +void MouseClickEffect::drawCircleQPainter(const QColor &color, float cx, float cy, float r) +{ + QPainter *painter = effects->scenePainter(); + painter->save(); + painter->setPen(color); + painter->drawArc(cx - r, cy - r, r * 2, r * 2, 0, 5760); + painter->restore(); +} + +void MouseClickEffect::paintScreenSetupGl(int, QRegion, ScreenPaintData &data) +{ + GLShader *shader = ShaderManager::instance()->pushShader(ShaderTrait::UniformColor); + shader->setUniform(GLShader::ModelViewProjectionMatrix, data.projectionMatrix()); + + glLineWidth(m_lineWidth); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); +} + +void MouseClickEffect::paintScreenFinishGl(int, QRegion, ScreenPaintData&) +{ + glDisable(GL_BLEND); + + ShaderManager::instance()->popShader(); +} + +} // namespace + diff --git a/effects/mouseclick/mouseclick.h b/effects/mouseclick/mouseclick.h new file mode 100644 index 0000000..ec9cd7a --- /dev/null +++ b/effects/mouseclick/mouseclick.h @@ -0,0 +1,174 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2012 Filip Wieladek + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_MOUSECLICK_H +#define KWIN_MOUSECLICK_H + +#include +#include +#include +#include +#include + +namespace KWin +{ + +#define BUTTON_COUNT 3 + +class MouseEvent +{ +public: + int m_button; + QPoint m_pos; + int m_time; + EffectFrame* m_frame; + bool m_press; +public: + MouseEvent(int button, QPoint point, int time, EffectFrame* frame, bool press) + : m_button(button), + m_pos(point), + m_time(time), + m_frame(frame), + m_press(press) + {}; + + ~MouseEvent() + { + delete m_frame; + } +}; + +class MouseButton +{ +public: + QString m_labelUp; + QString m_labelDown; + Qt::MouseButtons m_button; + bool m_isPressed; + int m_time; +public: + MouseButton(QString label, Qt::MouseButtons button) + : m_labelUp(label), + m_labelDown(label), + m_button(button), + m_isPressed(false), + m_time(0) + { + m_labelDown.append(i18n("↓")); + m_labelUp.append(i18n("↑")); + }; + + inline void setPressed(bool pressed) + { + if (m_isPressed != pressed) { + m_isPressed = pressed; + if (pressed) + m_time = 0; + } + } +}; + +class MouseClickEffect + : public Effect +{ + Q_OBJECT + Q_PROPERTY(QColor color1 READ color1) + Q_PROPERTY(QColor color2 READ color2) + Q_PROPERTY(QColor color3 READ color3) + Q_PROPERTY(qreal lineWidth READ lineWidth) + Q_PROPERTY(int ringLife READ ringLife) + Q_PROPERTY(int ringSize READ ringSize) + Q_PROPERTY(int ringCount READ ringCount) + Q_PROPERTY(bool showText READ isShowText) + Q_PROPERTY(QFont font READ font) + Q_PROPERTY(bool enabled READ isEnabled) +public: + MouseClickEffect(); + ~MouseClickEffect() override; + void reconfigure(ReconfigureFlags) override; + void prePaintScreen(ScreenPrePaintData& data, int time) override; + void paintScreen(int mask, const QRegion ®ion, ScreenPaintData& data) override; + void postPaintScreen() override; + bool isActive() const override; + + // for properties + QColor color1() const { + return m_colors[0]; + } + QColor color2() const { + return m_colors[1]; + } + QColor color3() const { + return m_colors[2]; + } + qreal lineWidth() const { + return m_lineWidth; + } + int ringLife() const { + return m_ringLife; + } + int ringSize() const { + return m_ringMaxSize; + } + int ringCount() const { + return m_ringCount; + } + bool isShowText() const { + return m_showText; + } + QFont font() const { + return m_font; + } + bool isEnabled() const { + return m_enabled; + } + +private Q_SLOTS: + void toggleEnabled(); + void slotMouseChanged(const QPoint& pos, const QPoint& old, + Qt::MouseButtons buttons, Qt::MouseButtons oldbuttons, + Qt::KeyboardModifiers modifiers, Qt::KeyboardModifiers oldmodifiers); +private: + EffectFrame* createEffectFrame(const QPoint& pos, const QString& text); + inline void drawCircle(const QColor& color, float cx, float cy, float r); + inline void paintScreenSetup(int mask, QRegion region, ScreenPaintData& data); + inline void paintScreenFinish(int mask, QRegion region, ScreenPaintData& data); + + inline bool isReleased(Qt::MouseButtons button, Qt::MouseButtons buttons, Qt::MouseButtons oldButtons); + inline bool isPressed(Qt::MouseButtons button, Qt::MouseButtons buttons, Qt::MouseButtons oldButtons); + + inline float computeRadius(const MouseEvent* click, int ring); + inline float computeAlpha(const MouseEvent* click, int ring); + + void repaint(); + + void drawCircleGl(const QColor& color, float cx, float cy, float r); + void drawCircleXr(const QColor& color, float cx, float cy, float r); + void drawCircleQPainter(const QColor& color, float cx, float cy, float r); + void paintScreenSetupGl(int mask, QRegion region, ScreenPaintData& data); + void paintScreenFinishGl(int mask, QRegion region, ScreenPaintData& data); + + QColor m_colors[BUTTON_COUNT]; + int m_ringCount; + float m_lineWidth; + float m_ringLife; + float m_ringMaxSize; + bool m_showText; + QFont m_font; + + QList m_clicks; + MouseButton* m_buttons[BUTTON_COUNT]; + + bool m_enabled; + +}; + +} // namespace + +#endif diff --git a/effects/mouseclick/mouseclick.kcfg b/effects/mouseclick/mouseclick.kcfg new file mode 100644 index 0000000..64b39b3 --- /dev/null +++ b/effects/mouseclick/mouseclick.kcfg @@ -0,0 +1,34 @@ + + + + + + QColor(Qt::red) + + + QColor(Qt::green) + + + QColor(Qt::blue) + + + 1.0 + + + 300 + + + 20 + + + 2 + + + true + + + + diff --git a/effects/mouseclick/mouseclick_config.cpp b/effects/mouseclick/mouseclick_config.cpp new file mode 100644 index 0000000..1808d7c --- /dev/null +++ b/effects/mouseclick/mouseclick_config.cpp @@ -0,0 +1,83 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2012 Filip Wieladek + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "mouseclick_config.h" +// KConfigSkeleton +#include "mouseclickconfig.h" +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include + +K_PLUGIN_FACTORY_WITH_JSON(MouseClickEffectConfigFactory, + "mouseclick_config.json", + registerPlugin();) + +namespace KWin +{ + +MouseClickEffectConfigForm::MouseClickEffectConfigForm(QWidget* parent) : QWidget(parent) +{ + setupUi(this); +} + +MouseClickEffectConfig::MouseClickEffectConfig(QWidget* parent, const QVariantList& args) : + KCModule(KAboutData::pluginData(QStringLiteral("mouseclick")), parent, args) +{ + m_ui = new MouseClickEffectConfigForm(this); + + QVBoxLayout* layout = new QVBoxLayout(this); + layout->addWidget(m_ui); + + connect(m_ui->editor, &KShortcutsEditor::keyChange, this, &MouseClickEffectConfig::markAsChanged); + + // Shortcut config. The shortcut belongs to the component "kwin"! + m_actionCollection = new KActionCollection(this, QStringLiteral("kwin")); + m_actionCollection->setComponentDisplayName(i18n("KWin")); + + QAction* a = m_actionCollection->addAction(QStringLiteral("ToggleMouseClick")); + a->setText(i18n("Toggle Mouse Click Effect")); + a->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::META + Qt::Key_Asterisk); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::META + Qt::Key_Asterisk); + + m_ui->editor->addCollection(m_actionCollection); + + MouseClickConfig::instance(KWIN_CONFIG); + addConfig(MouseClickConfig::self(), m_ui); + load(); +} + +MouseClickEffectConfig::~MouseClickEffectConfig() +{ + // Undo (only) unsaved changes to global key shortcuts + m_ui->editor->undoChanges(); +} + +void MouseClickEffectConfig::save() +{ + KCModule::save(); + m_ui->editor->save(); // undo() will restore to this state from now on + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("mouseclick")); +} + +} // namespace + +#include "mouseclick_config.moc" diff --git a/effects/mouseclick/mouseclick_config.desktop b/effects/mouseclick/mouseclick_config.desktop new file mode 100644 index 0000000..f977e09 --- /dev/null +++ b/effects/mouseclick/mouseclick_config.desktop @@ -0,0 +1,57 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=KCModule + +X-KDE-Library=kwin_mouseclick_config +X-KDE-ParentComponents=mouseclick + +Name=Mouse Click Animation +Name[ast]=Animación al calcar col mur +Name[az]=Siçan kliki animasiyası +Name[bs]=Animacija klika miÅ¡em +Name[ca]=Animació en fer clic amb el ratolí +Name[ca@valencia]=Animació de clic de ratolí +Name[cs]=Animace kliknutí myÅ¡i +Name[da]=Animation af museklik +Name[de]=Animation für Mausklicks +Name[el]=Εφέ κίνησης με κλικ του ποντικιού +Name[en_GB]=Mouse Click Animation +Name[es]=Animación del clic de ratón +Name[et]=Hiireklõpsu animeerimine +Name[eu]=Sagu-klikaren animazioa +Name[fi]=Hiiren napsautuksen animointi +Name[fr]=Animation du clic de la souris +Name[gl]=Animación ao premer o rato +Name[he]=הנפשה של לחיצה עם העכבר +Name[hu]=Egérkattintás animáció +Name[ia]=Animation de click de mus +Name[id]=Animasi Klik Mouse +Name[is]=Hreyfingar við músarsmell +Name[it]=Animazione del clic del mouse +Name[ja]=マウスクリックアニメーション +Name[kk]=Тышқанды түрту анимациясы +Name[ko]=마우스 클릭 애니메이션 +Name[lt]=Pelės spustelėjimo animacija +Name[mr]=माऊस क्लिक ऍनीमेशन +Name[nb]=Animer ved museklikk +Name[nds]=Muusklick-Animeren +Name[nl]=Animatie van muisklik +Name[nn]=Museklikkanimasjon +Name[pa]=ਮਾਊਸ ਕਲਿੱਕ ਐਨੀਮੇਸ਼ਨ +Name[pl]=Animacja kliknięcia myszą +Name[pt]=Animação do Botão do Rato +Name[pt_BR]=Animação de clique do mouse +Name[ro]=Animație la clic de maus +Name[ru]=Анимация щелчка мышью +Name[sk]=Animácia kliknutia myÅ¡ou +Name[sl]=Animacija klika z miÅ¡ko +Name[sr]=Анимација на клик мишем +Name[sr@ijekavian]=Анимација на клик мишем +Name[sr@ijekavianlatin]=Animacija na klik miÅ¡em +Name[sr@latin]=Animacija na klik miÅ¡em +Name[sv]=Animering av musklick +Name[tr]=Fare Tıklama Animasyonu +Name[uk]=Анімація за клацанням миші +Name[x-test]=xxMouse Click Animationxx +Name[zh_CN]=鼠标点击动画 +Name[zh_TW]=滑鼠點擊動畫 diff --git a/effects/mouseclick/mouseclick_config.h b/effects/mouseclick/mouseclick_config.h new file mode 100644 index 0000000..1723cb3 --- /dev/null +++ b/effects/mouseclick/mouseclick_config.h @@ -0,0 +1,45 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2012 Filip Wieladek + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_MOUSECLICK_CONFIG_H +#define KWIN_MOUSECLICK_CONFIG_H + +#include + +#include "ui_mouseclick_config.h" + +class KActionCollection; + +namespace KWin +{ + +class MouseClickEffectConfigForm : public QWidget, public Ui::MouseClickEffectConfigForm +{ + Q_OBJECT +public: + explicit MouseClickEffectConfigForm(QWidget* parent); +}; + +class MouseClickEffectConfig : public KCModule +{ + Q_OBJECT +public: + explicit MouseClickEffectConfig(QWidget* parent = nullptr, const QVariantList& args = QVariantList()); + ~MouseClickEffectConfig() override; + + void save() override; + +private: + MouseClickEffectConfigForm* m_ui; + KActionCollection* m_actionCollection; +}; + +} // namespace + +#endif diff --git a/effects/mouseclick/mouseclick_config.ui b/effects/mouseclick/mouseclick_config.ui new file mode 100644 index 0000000..28d8e16 --- /dev/null +++ b/effects/mouseclick/mouseclick_config.ui @@ -0,0 +1,282 @@ + + + KWin::MouseClickEffectConfigForm + + + + 0 + 0 + 335 + 378 + + + + + + + 0 + + + + Basic Settings + + + + + + + 0 + 0 + + + + + + + + Left Mouse Button Color: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_Color1 + + + + + + + Middle Mouse Button Color: + + + kcfg_Color2 + + + + + + + + 0 + 0 + + + + + + + + Right Mouse Button Color: + + + kcfg_Color3 + + + + + + + + 0 + 0 + + + + + + + + + Advanced Settings + + + + + + Rings + + + + + + Line Width: + + + kcfg_LineWidth + + + + + + + + 0 + 0 + + + + pixel + + + + + + + + 0 + 0 + + + + msec + + + 50 + + + 5000 + + + + + + + Ring Duration: + + + kcfg_RingLife + + + + + + + Ring Radius: + + + kcfg_RingSize + + + + + + + + 0 + 0 + + + + pixel + + + 1 + + + 1000 + + + + + + + Ring Count: + + + kcfg_RingCount + + + + + + + + 0 + 0 + + + + 1 + + + + + + + + + + Text + + + + + + Font: + + + + + + + + + + + + + + + + + Show Text: + + + kcfg_ShowText + + + + + + + + + + + + + + + 0 + 0 + + + + KShortcutsEditor::GlobalAction + + + + + + + + KColorCombo + QComboBox +
kcolorcombo.h
+
+ + KFontRequester + QWidget +
kfontrequester.h
+
+ + KShortcutsEditor + QWidget +
KShortcutsEditor
+ 1 +
+
+ + +
diff --git a/effects/mouseclick/mouseclickconfig.kcfgc b/effects/mouseclick/mouseclickconfig.kcfgc new file mode 100644 index 0000000..cdfae15 --- /dev/null +++ b/effects/mouseclick/mouseclickconfig.kcfgc @@ -0,0 +1,5 @@ +File=mouseclick.kcfg +ClassName=MouseClickConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/effects/mousemark/CMakeLists.txt b/effects/mousemark/CMakeLists.txt new file mode 100644 index 0000000..fd42575 --- /dev/null +++ b/effects/mousemark/CMakeLists.txt @@ -0,0 +1,25 @@ +####################################### +# Config +set(kwin_mousemark_config_SRCS mousemark_config.cpp) +ki18n_wrap_ui(kwin_mousemark_config_SRCS mousemark_config.ui) +kconfig_add_kcfg_files(kwin_mousemark_config_SRCS mousemarkconfig.kcfgc) + +add_library(kwin_mousemark_config MODULE ${kwin_mousemark_config_SRCS}) + +target_link_libraries(kwin_mousemark_config + KF5::ConfigWidgets + KF5::GlobalAccel + KF5::I18n + KF5::TextWidgets + KF5::XmlGui + KWinEffectsInterface +) + +kcoreaddons_desktop_to_json(kwin_mousemark_config mousemark_config.desktop SERVICE_TYPES kcmodule.desktop) + +install( + TARGETS + kwin_mousemark_config + DESTINATION + ${PLUGIN_INSTALL_DIR}/kwin/effects/configs +) diff --git a/effects/mousemark/mousemark.cpp b/effects/mousemark/mousemark.cpp new file mode 100644 index 0000000..738c187 --- /dev/null +++ b/effects/mousemark/mousemark.cpp @@ -0,0 +1,283 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2007 Christian Nitschkowski + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "mousemark.h" + +// KConfigSkeleton +#include "mousemarkconfig.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#ifdef KWIN_HAVE_XRENDER_COMPOSITING +#include +#endif + +namespace KWin +{ + +#define NULL_POINT (QPoint( -1, -1 )) // null point is (0,0), which is valid :-/ + +MouseMarkEffect::MouseMarkEffect() +{ + initConfig(); + QAction* a = new QAction(this); + a->setObjectName(QStringLiteral("ClearMouseMarks")); + a->setText(i18n("Clear All Mouse Marks")); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::SHIFT + Qt::META + Qt::Key_F11); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::SHIFT + Qt::META + Qt::Key_F11); + effects->registerGlobalShortcut(Qt::SHIFT + Qt::META + Qt::Key_F11, a); + connect(a, &QAction::triggered, this, &MouseMarkEffect::clear); + a = new QAction(this); + a->setObjectName(QStringLiteral("ClearLastMouseMark")); + a->setText(i18n("Clear Last Mouse Mark")); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::SHIFT + Qt::META + Qt::Key_F12); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::SHIFT + Qt::META + Qt::Key_F12); + effects->registerGlobalShortcut(Qt::SHIFT + Qt::META + Qt::Key_F12, a); + connect(a, &QAction::triggered, this, &MouseMarkEffect::clearLast); + + connect(effects, &EffectsHandler::mouseChanged, this, &MouseMarkEffect::slotMouseChanged); + connect(effects, &EffectsHandler::screenLockingChanged, this, &MouseMarkEffect::screenLockingChanged); + reconfigure(ReconfigureAll); + arrow_start = NULL_POINT; + effects->startMousePolling(); // We require it to detect activation as well +} + +MouseMarkEffect::~MouseMarkEffect() +{ + effects->stopMousePolling(); +} + +static int width_2 = 1; +void MouseMarkEffect::reconfigure(ReconfigureFlags) +{ + MouseMarkConfig::self()->read(); + width = MouseMarkConfig::lineWidth(); + width_2 = width / 2; + color = MouseMarkConfig::color(); + color.setAlphaF(1.0); +} + +#ifdef KWIN_HAVE_XRENDER_COMPOSITING +void MouseMarkEffect::addRect(const QPoint &p1, const QPoint &p2, xcb_rectangle_t *r, xcb_render_color_t *c) +{ + r->x = qMin(p1.x(), p2.x()) - width_2; + r->y = qMin(p1.y(), p2.y()) - width_2; + r->width = qAbs(p1.x()-p2.x()) + 1 + width_2; + r->height = qAbs(p1.y()-p2.y()) + 1 + width_2; + // fast move -> large rect, tess... interpolate a line + if (r->width > 3*width/2 && r->height > 3*width/2) { + const int n = sqrt(r->width*r->width + r->height*r->height) / width; + xcb_rectangle_t *rects = new xcb_rectangle_t[n-1]; + const int w = p1.x() < p2.x() ? r->width : -r->width; + const int h = p1.y() < p2.y() ? r->height : -r->height; + for (int i = 1; i < n; ++i) { + rects[i-1].x = p1.x() + i*w/n; + rects[i-1].y = p1.y() + i*h/n; + rects[i-1].width = rects[i-1].height = width; + } + xcb_render_fill_rectangles(xcbConnection(), XCB_RENDER_PICT_OP_SRC, effects->xrenderBufferPicture(), *c, n - 1, rects); + delete [] rects; + r->x = p1.x(); + r->y = p1.y(); + r->width = r->height = width; + } +} +#endif + +void MouseMarkEffect::paintScreen(int mask, const QRegion ®ion, ScreenPaintData& data) +{ + effects->paintScreen(mask, region, data); // paint normal screen + if (marks.isEmpty() && drawing.isEmpty()) + return; + if ( effects->isOpenGLCompositing()) { + if (!GLPlatform::instance()->isGLES()) { + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + glEnable(GL_LINE_SMOOTH); + glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); + } + glLineWidth(width); + GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer(); + vbo->reset(); + vbo->setUseColor(true); + vbo->setColor(color); + ShaderBinder binder(ShaderTrait::UniformColor); + binder.shader()->setUniform(GLShader::ModelViewProjectionMatrix, data.projectionMatrix()); + QVector verts; + foreach (const Mark & mark, marks) { + verts.clear(); + verts.reserve(mark.size() * 2); + foreach (const QPoint & p, mark) { + verts << p.x() << p.y(); + } + vbo->setData(verts.size() / 2, 2, verts.data(), nullptr); + vbo->render(GL_LINE_STRIP); + } + if (!drawing.isEmpty()) { + verts.clear(); + verts.reserve(drawing.size() * 2); + foreach (const QPoint & p, drawing) { + verts << p.x() << p.y(); + } + vbo->setData(verts.size() / 2, 2, verts.data(), nullptr); + vbo->render(GL_LINE_STRIP); + } + glLineWidth(1.0); + if (!GLPlatform::instance()->isGLES()) { + glDisable(GL_LINE_SMOOTH); + glDisable(GL_BLEND); + } + } +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + if ( effects->compositingType() == XRenderCompositing) { + xcb_render_color_t c = preMultiply(color); + for (int i = 0; i < marks.count(); ++i) { + const int n = marks[i].count() - 1; + if (n > 0) { + xcb_rectangle_t *rects = new xcb_rectangle_t[n]; + for (int j = 0; j < marks[i].count()-1; ++j) { + addRect(marks[i][j], marks[i][j+1], &rects[j], &c); + } + xcb_render_fill_rectangles(xcbConnection(), XCB_RENDER_PICT_OP_SRC, effects->xrenderBufferPicture(), c, n, rects); + delete [] rects; + } + } + const int n = drawing.count() - 1; + if (n > 0) { + xcb_rectangle_t *rects = new xcb_rectangle_t[n]; + for (int i = 0; i < n; ++i) + addRect(drawing[i], drawing[i+1], &rects[i], &c); + xcb_render_fill_rectangles(xcbConnection(), XCB_RENDER_PICT_OP_SRC, effects->xrenderBufferPicture(), c, n, rects); + delete [] rects; + } + } +#endif + if (effects->compositingType() == QPainterCompositing) { + QPainter *painter = effects->scenePainter(); + painter->save(); + QPen pen(color); + pen.setWidth(width); + painter->setPen(pen); + foreach (const Mark &mark, marks) { + drawMark(painter, mark); + } + drawMark(painter, drawing); + painter->restore(); + } +} + +void MouseMarkEffect::drawMark(QPainter *painter, const Mark &mark) +{ + if (mark.count() <= 1) { + return; + } + for (int i = 0; i < mark.count() - 1; ++i) { + painter->drawLine(mark[i], mark[i+1]); + } +} + +void MouseMarkEffect::slotMouseChanged(const QPoint& pos, const QPoint&, + Qt::MouseButtons, Qt::MouseButtons, + Qt::KeyboardModifiers modifiers, Qt::KeyboardModifiers) +{ + if (modifiers == (Qt::META | Qt::SHIFT | Qt::CTRL)) { // start/finish arrow + if (arrow_start != NULL_POINT) { + marks.append(createArrow(arrow_start, pos)); + arrow_start = NULL_POINT; + effects->addRepaintFull(); + return; + } else + arrow_start = pos; + } + if (arrow_start != NULL_POINT) + return; + // TODO the shortcuts now trigger this right before they're activated + if (modifiers == (Qt::META | Qt::SHIFT)) { // activated + if (drawing.isEmpty()) + drawing.append(pos); + if (drawing.last() == pos) + return; + QPoint pos2 = drawing.last(); + drawing.append(pos); + QRect repaint = QRect(qMin(pos.x(), pos2.x()), qMin(pos.y(), pos2.y()), + qMax(pos.x(), pos2.x()), qMax(pos.y(), pos2.y())); + repaint.adjust(-width, -width, width, width); + effects->addRepaint(repaint); + } else if (!drawing.isEmpty()) { + marks.append(drawing); + drawing.clear(); + } +} + +void MouseMarkEffect::clear() +{ + drawing.clear(); + marks.clear(); + effects->addRepaintFull(); +} + +void MouseMarkEffect::clearLast() +{ + if (arrow_start != NULL_POINT) { + arrow_start = NULL_POINT; + } else if (!drawing.isEmpty()) { + drawing.clear(); + effects->addRepaintFull(); + } else if (!marks.isEmpty()) { + marks.pop_back(); + effects->addRepaintFull(); + } +} + +MouseMarkEffect::Mark MouseMarkEffect::createArrow(QPoint arrow_start, QPoint arrow_end) +{ + Mark ret; + double angle = atan2((double)(arrow_end.y() - arrow_start.y()), (double)(arrow_end.x() - arrow_start.x())); + ret += arrow_start + QPoint(50 * cos(angle + M_PI / 6), + 50 * sin(angle + M_PI / 6)); // right one + ret += arrow_start; + ret += arrow_end; + ret += arrow_start; // it's connected lines, so go back with the middle one + ret += arrow_start + QPoint(50 * cos(angle - M_PI / 6), + 50 * sin(angle - M_PI / 6)); // left one + return ret; +} + +void MouseMarkEffect::screenLockingChanged(bool locked) +{ + if (!marks.isEmpty() || !drawing.isEmpty()) { + effects->addRepaintFull(); + } + // disable mouse polling while screen is locked. + if (locked) { + effects->stopMousePolling(); + } else { + effects->startMousePolling(); + } +} + +bool MouseMarkEffect::isActive() const +{ + return (!marks.isEmpty() || !drawing.isEmpty()) && !effects->isScreenLocked(); +} + + +} // namespace + diff --git a/effects/mousemark/mousemark.h b/effects/mousemark/mousemark.h new file mode 100644 index 0000000..e40e014 --- /dev/null +++ b/effects/mousemark/mousemark.h @@ -0,0 +1,65 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_MOUSEMARK_H +#define KWIN_MOUSEMARK_H + +#include +#include +#include + +struct xcb_render_color_t; + +namespace KWin +{ + +class MouseMarkEffect + : public Effect +{ + Q_OBJECT + Q_PROPERTY(int width READ configuredWidth) + Q_PROPERTY(QColor color READ configuredColor) +public: + MouseMarkEffect(); + ~MouseMarkEffect() override; + void reconfigure(ReconfigureFlags) override; + void paintScreen(int mask, const QRegion ®ion, ScreenPaintData& data) override; + bool isActive() const override; + + // for properties + int configuredWidth() const { + return width; + } + QColor configuredColor() const { + return color; + } +private Q_SLOTS: + void clear(); + void clearLast(); + void slotMouseChanged(const QPoint& pos, const QPoint& old, + Qt::MouseButtons buttons, Qt::MouseButtons oldbuttons, + Qt::KeyboardModifiers modifiers, Qt::KeyboardModifiers oldmodifiers); + void screenLockingChanged(bool locked); +private: + typedef QVector< QPoint > Mark; + void drawMark(QPainter *painter, const Mark &mark); + static Mark createArrow(QPoint arrow_start, QPoint arrow_end); +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + void addRect(const QPoint &p1, const QPoint &p2, xcb_rectangle_t *r, xcb_render_color_t *c); +#endif + QVector< Mark > marks; + Mark drawing; + QPoint arrow_start; + int width; + QColor color; +}; + +} // namespace + +#endif diff --git a/effects/mousemark/mousemark.kcfg b/effects/mousemark/mousemark.kcfg new file mode 100644 index 0000000..ebaf9ac --- /dev/null +++ b/effects/mousemark/mousemark.kcfg @@ -0,0 +1,15 @@ + + + + + + 3 + + + 255,0,0 + + + diff --git a/effects/mousemark/mousemark_config.cpp b/effects/mousemark/mousemark_config.cpp new file mode 100644 index 0000000..417f31e --- /dev/null +++ b/effects/mousemark/mousemark_config.cpp @@ -0,0 +1,97 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Christian Nitschkowski + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "mousemark_config.h" + +// KConfigSkeleton +#include "mousemarkconfig.h" +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include +#include + +K_PLUGIN_FACTORY_WITH_JSON(MouseMarkEffectConfigFactory, + "mousemark_config.json", + registerPlugin();) + +namespace KWin +{ + +MouseMarkEffectConfigForm::MouseMarkEffectConfigForm(QWidget* parent) : QWidget(parent) +{ + setupUi(this); +} + +MouseMarkEffectConfig::MouseMarkEffectConfig(QWidget* parent, const QVariantList& args) : + KCModule(KAboutData::pluginData(QStringLiteral("mousemark")), parent, args) +{ + m_ui = new MouseMarkEffectConfigForm(this); + + m_ui->kcfg_LineWidth->setSuffix(ki18np(" pixel", " pixels")); + + QVBoxLayout* layout = new QVBoxLayout(this); + + layout->addWidget(m_ui); + + MouseMarkConfig::instance(KWIN_CONFIG); + addConfig(MouseMarkConfig::self(), m_ui); + + // Shortcut config. The shortcut belongs to the component "kwin"! + m_actionCollection = new KActionCollection(this, QStringLiteral("kwin")); + m_actionCollection->setComponentDisplayName(i18n("KWin")); + + QAction* a = m_actionCollection->addAction(QStringLiteral("ClearMouseMarks")); + a->setText(i18n("Clear Mouse Marks")); + a->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::SHIFT + Qt::META + Qt::Key_F11); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::SHIFT + Qt::META + Qt::Key_F11); + + a = m_actionCollection->addAction(QStringLiteral("ClearLastMouseMark")); + a->setText(i18n("Clear Last Mouse Mark")); + a->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::SHIFT + Qt::META + Qt::Key_F12); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::SHIFT + Qt::META + Qt::Key_F12); + + m_ui->editor->addCollection(m_actionCollection); + + load(); +} + +MouseMarkEffectConfig::~MouseMarkEffectConfig() +{ + // Undo (only) unsaved changes to global key shortcuts + m_ui->editor->undoChanges(); +} + +void MouseMarkEffectConfig::save() +{ + qDebug() << "Saving config of MouseMark" ; + KCModule::save(); + + m_actionCollection->writeSettings(); + m_ui->editor->save(); // undo() will restore to this state from now on + + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("mousemark")); +} + +} // namespace + +#include "mousemark_config.moc" diff --git a/effects/mousemark/mousemark_config.desktop b/effects/mousemark/mousemark_config.desktop new file mode 100644 index 0000000..6b933c6 --- /dev/null +++ b/effects/mousemark/mousemark_config.desktop @@ -0,0 +1,85 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=KCModule + +X-KDE-Library=kwin_mousemark_config +X-KDE-ParentComponents=mousemark + +Name=Mouse Mark +Name[af]=Muismerk +Name[ar]=علامة الفأرة +Name[az]=Siçanla şəkil +Name[be]=Адметка мышы +Name[be@latin]=Malavańnie myššu +Name[bg]=Чертане с мишката +Name[bs]=Otisci miÅ¡a +Name[ca]=Marca amb el ratolí +Name[ca@valencia]=Marca amb el ratolí +Name[cs]=Značkovač +Name[csb]=Merk mëszë +Name[da]=Muse-tusch +Name[de]=Mausspur +Name[el]=Σήμανση ποντικιού +Name[en_GB]=Mouse Mark +Name[eo]=Musmarko +Name[es]=Marcación con el ratón +Name[et]=Hiirejälg +Name[eu]=Sagu-marka +Name[fa]=نشان موشی +Name[fi]=Hiiren jäljet +Name[fr]=Tracé à la souris +Name[fy]=Mûs markearing +Name[ga]=Mouse Mark +Name[gl]=Marca do rato +Name[gu]=માઉસ નિશાની +Name[he]=סימון בעזרת העכבר +Name[hi]=माउस मार्क +Name[hne]=मुसुवा चिनहा +Name[hr]=Oznaka miÅ¡a +Name[hu]=Egérnyom +Name[ia]=Marca de mus +Name[id]=Tandai Mouse +Name[is]=Músarspor +Name[it]=Pennarello +Name[ja]=マウスマーク +Name[kk]=Тышқан белгісі +Name[km]=សម្គាល់​កណ្តុរ​ +Name[kn]=ಮೂಷಕ (ಮೌಸ್) ಮುದ್ರೆ +Name[ko]=마우스 자취 +Name[lt]=Žymėjimas pele +Name[lv]=Peles zÄ«mēšana +Name[mai]=माउस मार्क +Name[mk]=Цртање со глушец +Name[ml]=മൌസിന്റെ അടയാളം +Name[mr]=माऊस मार्क +Name[nb]=Musemerke +Name[nds]=Muus-Mark +Name[ne]=माउस मार्क +Name[nl]=Muismarkering +Name[nn]=Muselinjer +Name[pa]=ਮਾਊਸ ਨਿਸ਼ਾਨ +Name[pl]=Znacznik myszy +Name[pt]=Marcação com o Rato +Name[pt_BR]=Anotar com o mouse +Name[ro]=Urme de maus +Name[ru]=Рисование мышью +Name[se]=Sáhpánmearka +Name[si]=මවුස ලකුණ +Name[sk]=Stopa myÅ¡i +Name[sl]=Risanje +Name[sr]=Отисци миша +Name[sr@ijekavian]=Отисци миша +Name[sr@ijekavianlatin]=Otisci miÅ¡a +Name[sr@latin]=Otisci miÅ¡a +Name[sv]=Markera med musen +Name[ta]=எலியச் சுட்டி +Name[te]=మౌస్ గుర్తు +Name[th]=วาดด้วยเมาส์ +Name[tr]=Fare İzi +Name[ug]=چاشقىنەك بەلگىسى +Name[uk]=Позначки мишкою +Name[vi]=Dấu chuột +Name[wa]=Marke di sori +Name[x-test]=xxMouse Markxx +Name[zh_CN]=鼠标标记 +Name[zh_TW]=滑鼠標記 diff --git a/effects/mousemark/mousemark_config.h b/effects/mousemark/mousemark_config.h new file mode 100644 index 0000000..8a33ea1 --- /dev/null +++ b/effects/mousemark/mousemark_config.h @@ -0,0 +1,45 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Christian Nitschkowski + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_MOUSEMARK_CONFIG_H +#define KWIN_MOUSEMARK_CONFIG_H + +#include + +#include "ui_mousemark_config.h" + +class KActionCollection; + +namespace KWin +{ + +class MouseMarkEffectConfigForm : public QWidget, public Ui::MouseMarkEffectConfigForm +{ + Q_OBJECT +public: + explicit MouseMarkEffectConfigForm(QWidget* parent); +}; + +class MouseMarkEffectConfig : public KCModule +{ + Q_OBJECT +public: + explicit MouseMarkEffectConfig(QWidget* parent = nullptr, const QVariantList& args = QVariantList()); + ~MouseMarkEffectConfig() override; + + void save() override; + +private: + MouseMarkEffectConfigForm* m_ui; + KActionCollection* m_actionCollection; +}; + +} // namespace + +#endif diff --git a/effects/mousemark/mousemark_config.ui b/effects/mousemark/mousemark_config.ui new file mode 100644 index 0000000..6bfbb3d --- /dev/null +++ b/effects/mousemark/mousemark_config.ui @@ -0,0 +1,110 @@ + + + KWin::MouseMarkEffectConfigForm + + + + 0 + 0 + 279 + 178 + + + + + + + Appearance + + + + + + Wid&th: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_LineWidth + + + + + + + &Color: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_Color + + + + + + + + 0 + 0 + + + + + + + + 1 + + + 10 + + + 3 + + + + + + + + + + + + + Draw with the mouse by holding Shift+Meta keys and moving the mouse. + + + Qt::AlignCenter + + + true + + + + + + + + KColorCombo + QComboBox +
kcolorcombo.h
+
+ + KShortcutsEditor + QWidget +
kshortcutseditor.h
+ 1 +
+ + KPluralHandlingSpinBox + QSpinBox +
kpluralhandlingspinbox.h
+
+
+ + +
diff --git a/effects/mousemark/mousemarkconfig.kcfgc b/effects/mousemark/mousemarkconfig.kcfgc new file mode 100644 index 0000000..299221d --- /dev/null +++ b/effects/mousemark/mousemarkconfig.kcfgc @@ -0,0 +1,5 @@ +File=mousemark.kcfg +ClassName=MouseMarkConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/effects/presentwindows/CMakeLists.txt b/effects/presentwindows/CMakeLists.txt new file mode 100644 index 0000000..9f86c04 --- /dev/null +++ b/effects/presentwindows/CMakeLists.txt @@ -0,0 +1,29 @@ +####################################### +# Effect +install(FILES main.qml DESTINATION ${DATA_INSTALL_DIR}/kwin/effects/presentwindows/) + +####################################### +# Config +set(kwin_presentwindows_config_SRCS presentwindows_config.cpp) +ki18n_wrap_ui(kwin_presentwindows_config_SRCS presentwindows_config.ui) +kconfig_add_kcfg_files(kwin_presentwindows_config_SRCS presentwindowsconfig.kcfgc) + +add_library(kwin_presentwindows_config MODULE ${kwin_presentwindows_config_SRCS}) + +target_link_libraries(kwin_presentwindows_config + KF5::Completion + KF5::ConfigWidgets + KF5::GlobalAccel + KF5::I18n + KF5::XmlGui + KWinEffectsInterface +) + +kcoreaddons_desktop_to_json(kwin_presentwindows_config presentwindows_config.desktop SERVICE_TYPES kcmodule.desktop) + +install( + TARGETS + kwin_presentwindows_config + DESTINATION + ${PLUGIN_INSTALL_DIR}/kwin/effects/configs +) diff --git a/effects/presentwindows/main.qml b/effects/presentwindows/main.qml new file mode 100644 index 0000000..13b4e74 --- /dev/null +++ b/effects/presentwindows/main.qml @@ -0,0 +1,18 @@ +/* + 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 +*/ +import QtQuick 2.0 +import org.kde.plasma.components 2.0 as Plasma + +Plasma.Button { + id: closeButton + iconSource: "window-close" + anchors.fill: parent + implicitWidth: units.iconSizes.medium + implicitHeight: implicitWidth +} diff --git a/effects/presentwindows/presentwindows.cpp b/effects/presentwindows/presentwindows.cpp new file mode 100644 index 0000000..15a4d13 --- /dev/null +++ b/effects/presentwindows/presentwindows.cpp @@ -0,0 +1,2007 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Rivo Laks + SPDX-FileCopyrightText: 2008 Lucas Murray + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "presentwindows.h" +//KConfigSkeleton +#include "presentwindowsconfig.h" +#include +#include +#include + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace KWin +{ + +PresentWindowsEffect::PresentWindowsEffect() + : m_proxy(this) + , m_activated(false) + , m_ignoreMinimized(false) + , m_decalOpacity(0.0) + , m_hasKeyboardGrab(false) + , m_mode(ModeCurrentDesktop) + , m_managerWindow(nullptr) + , m_needInitialSelection(false) + , m_highlightedWindow(nullptr) + , m_filterFrame(nullptr) + , m_closeView(nullptr) + , m_exposeAction(new QAction(this)) + , m_exposeAllAction(new QAction(this)) + , m_exposeClassAction(new QAction(this)) +{ + initConfig(); + + auto announceSupportProperties = [this] { + m_atomDesktop = effects->announceSupportProperty("_KDE_PRESENT_WINDOWS_DESKTOP", this); + m_atomWindows = effects->announceSupportProperty("_KDE_PRESENT_WINDOWS_GROUP", this); + }; + announceSupportProperties(); + connect(effects, &EffectsHandler::xcbConnectionChanged, this, announceSupportProperties); + + QAction* exposeAction = m_exposeAction; + exposeAction->setObjectName(QStringLiteral("Expose")); + exposeAction->setText(i18n("Toggle Present Windows (Current desktop)")); + KGlobalAccel::self()->setDefaultShortcut(exposeAction, QList() << Qt::CTRL + Qt::Key_F9); + KGlobalAccel::self()->setShortcut(exposeAction, QList() << Qt::CTRL + Qt::Key_F9); + shortcut = KGlobalAccel::self()->shortcut(exposeAction); + effects->registerGlobalShortcut(Qt::CTRL + Qt::Key_F9, exposeAction); + connect(exposeAction, &QAction::triggered, this, &PresentWindowsEffect::toggleActive); + + QAction* exposeAllAction = m_exposeAllAction; + exposeAllAction->setObjectName(QStringLiteral("ExposeAll")); + exposeAllAction->setText(i18n("Toggle Present Windows (All desktops)")); + KGlobalAccel::self()->setDefaultShortcut(exposeAllAction, QList() << Qt::CTRL + Qt::Key_F10 << Qt::Key_LaunchC); + KGlobalAccel::self()->setShortcut(exposeAllAction, QList() << Qt::CTRL + Qt::Key_F10 << Qt::Key_LaunchC); + shortcutAll = KGlobalAccel::self()->shortcut(exposeAllAction); + effects->registerGlobalShortcut(Qt::CTRL + Qt::Key_F10, exposeAllAction); + effects->registerTouchpadSwipeShortcut(SwipeDirection::Down, exposeAllAction); + connect(exposeAllAction, &QAction::triggered, this, &PresentWindowsEffect::toggleActiveAllDesktops); + + QAction* exposeClassAction = m_exposeClassAction; + exposeClassAction->setObjectName(QStringLiteral("ExposeClass")); + exposeClassAction->setText(i18n("Toggle Present Windows (Window class)")); + KGlobalAccel::self()->setDefaultShortcut(exposeClassAction, QList() << Qt::CTRL + Qt::Key_F7); + KGlobalAccel::self()->setShortcut(exposeClassAction, QList() << Qt::CTRL + Qt::Key_F7); + effects->registerGlobalShortcut(Qt::CTRL + Qt::Key_F7, exposeClassAction); + connect(exposeClassAction, &QAction::triggered, this, &PresentWindowsEffect::toggleActiveClass); + shortcutClass = KGlobalAccel::self()->shortcut(exposeClassAction); + connect(KGlobalAccel::self(), &KGlobalAccel::globalShortcutChanged, this, &PresentWindowsEffect::globalShortcutChanged); + + reconfigure(ReconfigureAll); + connect(effects, &EffectsHandler::windowAdded, this, &PresentWindowsEffect::slotWindowAdded); + connect(effects, &EffectsHandler::windowClosed, this, &PresentWindowsEffect::slotWindowClosed); + connect(effects, &EffectsHandler::windowDeleted, this, &PresentWindowsEffect::slotWindowDeleted); + connect(effects, &EffectsHandler::windowFrameGeometryChanged, this, &PresentWindowsEffect::slotWindowFrameGeometryChanged); + connect(effects, &EffectsHandler::propertyNotify, this, &PresentWindowsEffect::slotPropertyNotify); + connect(effects, &EffectsHandler::numberScreensChanged, this, + [this] { + if (isActive()) + reCreateGrids(); + } + ); + connect(effects, &EffectsHandler::screenAboutToLock, this, [this]() { + setActive(false); + }); +} + +PresentWindowsEffect::~PresentWindowsEffect() +{ + delete m_filterFrame; + delete m_closeView; +} + +void PresentWindowsEffect::reconfigure(ReconfigureFlags) +{ + PresentWindowsConfig::self()->read(); + foreach (ElectricBorder border, m_borderActivate) { + effects->unreserveElectricBorder(border, this); + } + foreach (ElectricBorder border, m_borderActivateAll) { + effects->unreserveElectricBorder(border, this); + } + m_borderActivate.clear(); + m_borderActivateAll.clear(); + + foreach (int i, PresentWindowsConfig::borderActivate()) { + m_borderActivate.append(ElectricBorder(i)); + effects->reserveElectricBorder(ElectricBorder(i), this); + } + foreach (int i, PresentWindowsConfig::borderActivateAll()) { + m_borderActivateAll.append(ElectricBorder(i)); + effects->reserveElectricBorder(ElectricBorder(i), this); + } + foreach (int i, PresentWindowsConfig::borderActivateClass()) { + m_borderActivateClass.append(ElectricBorder(i)); + effects->reserveElectricBorder(ElectricBorder(i), this); + } + + m_layoutMode = PresentWindowsConfig::layoutMode(); + m_showCaptions = PresentWindowsConfig::drawWindowCaptions(); + m_showIcons = PresentWindowsConfig::drawWindowIcons(); + m_doNotCloseWindows = !PresentWindowsConfig::allowClosingWindows(); + + if (m_doNotCloseWindows) { + delete m_closeView; + m_closeView = nullptr; + } + + m_ignoreMinimized = PresentWindowsConfig::ignoreMinimized(); + m_accuracy = PresentWindowsConfig::accuracy() * 20; + m_fillGaps = PresentWindowsConfig::fillGaps(); + m_fadeDuration = double(animationTime(150)); + m_showPanel = PresentWindowsConfig::showPanel(); + m_leftButtonWindow = (WindowMouseAction)PresentWindowsConfig::leftButtonWindow(); + m_middleButtonWindow = (WindowMouseAction)PresentWindowsConfig::middleButtonWindow(); + m_rightButtonWindow = (WindowMouseAction)PresentWindowsConfig::rightButtonWindow(); + m_leftButtonDesktop = (DesktopMouseAction)PresentWindowsConfig::leftButtonDesktop(); + m_middleButtonDesktop = (DesktopMouseAction)PresentWindowsConfig::middleButtonDesktop(); + m_rightButtonDesktop = (DesktopMouseAction)PresentWindowsConfig::rightButtonDesktop(); + + // touch screen edges + const QVector relevantBorders{ElectricLeft, ElectricTop, ElectricRight, ElectricBottom}; + for (auto e : relevantBorders) { + effects->unregisterTouchBorder(e, m_exposeAction); + effects->unregisterTouchBorder(e, m_exposeAllAction); + effects->unregisterTouchBorder(e, m_exposeClassAction); + } + auto touchEdge = [&relevantBorders] (const QList touchBorders, QAction *action) { + for (int i : touchBorders) { + if (!relevantBorders.contains(ElectricBorder(i))) { + continue; + } + effects->registerTouchBorder(ElectricBorder(i), action); + } + }; + touchEdge(PresentWindowsConfig::touchBorderActivate(), m_exposeAction); + touchEdge(PresentWindowsConfig::touchBorderActivateAll(), m_exposeAllAction); + touchEdge(PresentWindowsConfig::touchBorderActivateClass(), m_exposeClassAction); +} + +void* PresentWindowsEffect::proxy() +{ + return &m_proxy; +} + +void PresentWindowsEffect::toggleActiveClass() +{ + if (!m_activated) { + if (!effects->activeWindow()) + return; + m_mode = ModeWindowClass; + m_class = effects->activeWindow()->windowClass(); + } + setActive(!m_activated); +} + +//----------------------------------------------------------------------------- +// Screen painting + +void PresentWindowsEffect::prePaintScreen(ScreenPrePaintData &data, int time) +{ + m_motionManager.calculate(time); + + // We need to mark the screen as having been transformed otherwise there will be no repainting + if (m_activated || m_motionManager.managingWindows()) + data.mask |= Effect::PAINT_SCREEN_WITH_TRANSFORMED_WINDOWS; + + if (m_activated) + m_decalOpacity = qMin(1.0, m_decalOpacity + time / m_fadeDuration); + else + m_decalOpacity = qMax(0.0, m_decalOpacity - time / m_fadeDuration); + + effects->prePaintScreen(data, time); +} + +void PresentWindowsEffect::paintScreen(int mask, const QRegion ®ion, ScreenPaintData &data) +{ + effects->paintScreen(mask, region, data); + + // Display the filter box + if (!m_windowFilter.isEmpty()) + m_filterFrame->render(region); + + if (m_closeView) + effects->renderEffectQuickView(m_closeView); +} + +void PresentWindowsEffect::postPaintScreen() +{ + if (m_motionManager.areWindowsMoving()) + effects->addRepaintFull(); + else if (!m_activated && m_motionManager.managingWindows() && !(m_closeView && m_closeView->isVisible())) { + // We have finished moving them back, stop processing + m_motionManager.unmanageAll(); + + DataHash::iterator i = m_windowData.begin(); + while (i != m_windowData.end()) { + delete i.value().textFrame; + delete i.value().iconFrame; + ++i; + } + m_windowData.clear(); + + foreach (EffectWindow * w, effects->stackingOrder()) { + w->setData(WindowForceBlurRole, QVariant()); + w->setData(WindowForceBackgroundContrastRole, QVariant()); + } + effects->setActiveFullScreenEffect(nullptr); + effects->addRepaintFull(); + } else if (m_activated && m_needInitialSelection) { + m_needInitialSelection = false; + QMouseEvent me(QEvent::MouseMove, cursorPos(), Qt::NoButton, Qt::NoButton, Qt::NoModifier); + windowInputMouseEvent(&me); + } + + // Update windows that are changing brightness or opacity + DataHash::const_iterator i; + for (i = m_windowData.constBegin(); i != m_windowData.constEnd(); ++i) { + if (i.value().opacity > 0.0 && i.value().opacity < 1.0) + i.key()->addRepaintFull(); + if (i.key()->isDesktop() && !m_motionManager.isManaging(i.key())) { + if (i.value().highlight != 0.3) + i.key()->addRepaintFull(); + } + else if (i.value().highlight > 0.0 && i.value().highlight < 1.0) + i.key()->addRepaintFull(); + } + + effects->postPaintScreen(); +} + +//----------------------------------------------------------------------------- +// Window painting + +void PresentWindowsEffect::prePaintWindow(EffectWindow *w, WindowPrePaintData &data, int time) +{ + // TODO: We should also check to see if any windows are fading just in case fading takes longer + // than moving the windows when the effect is deactivated. + if (m_activated || m_motionManager.areWindowsMoving() || m_closeView) { + DataHash::iterator winData = m_windowData.find(w); + if (winData == m_windowData.end()) { + effects->prePaintWindow(w, data, time); + return; + } + w->enablePainting(EffectWindow::PAINT_DISABLED_BY_MINIMIZE); // Display always + w->enablePainting(EffectWindow::PAINT_DISABLED_BY_DESKTOP); + + // Calculate window's opacity + // TODO: Minimized windows or windows not on the current desktop are only 75% visible? + if (winData->visible) { + if (winData->deleted) + winData->opacity = qMax(0.0, winData->opacity - time / m_fadeDuration); + else + winData->opacity = qMin(/*(w->isMinimized() || !w->isOnCurrentDesktop()) ? 0.75 :*/ 1.0, + winData->opacity + time / m_fadeDuration); + } else + winData->opacity = qMax(0.0, winData->opacity - time / m_fadeDuration); + + if (winData->opacity <= 0.0) { + // don't disable painting for panels if show panel is set + if (!(m_showPanel && w->isDock())) + w->disablePainting(EffectWindow::PAINT_DISABLED); + } else if (winData->opacity != 1.0) + data.setTranslucent(); + + const bool isInMotion = m_motionManager.isManaging(w); + // Calculate window's brightness + if (w == m_highlightedWindow || !m_activated) + winData->highlight = qMin(1.0, winData->highlight + time / m_fadeDuration); + else if (!isInMotion && w->isDesktop()) + winData->highlight = 0.3; + else + winData->highlight = qMax(0.0, winData->highlight - time / m_fadeDuration); + + // Closed windows + if (winData->deleted) { + data.setTranslucent(); + if (winData->opacity <= 0.0 && winData->referenced) { + // it's possible that another effect has referenced the window + // we have to keep the window in the list to prevent flickering + winData->referenced = false; + w->unrefWindow(); + } else + w->enablePainting(EffectWindow::PAINT_DISABLED_BY_DELETE); + } + + // desktop windows on other desktops (Plasma activity per desktop) should not be painted + if (w->isDesktop() && !w->isOnCurrentDesktop()) + w->disablePainting(EffectWindow::PAINT_DISABLED_BY_DESKTOP); + + if (isInMotion) + data.setTransformed(); // We will be moving this window + } + effects->prePaintWindow(w, data, time); +} + +void PresentWindowsEffect::paintWindow(EffectWindow *w, int mask, QRegion region, WindowPaintData &data) +{ + if (m_activated || m_motionManager.areWindowsMoving()) { + DataHash::const_iterator winData = m_windowData.constFind(w); + if (winData == m_windowData.constEnd() || (w->isDock() && m_showPanel)) { + // in case the panel should be shown just display it without any changes + effects->paintWindow(w, mask, region, data); + return; + } + + mask |= PAINT_WINDOW_LANCZOS; + // Apply opacity and brightness + data.multiplyOpacity(winData->opacity); + data.multiplyBrightness(interpolate(0.40, 1.0, winData->highlight)); + + if (m_motionManager.isManaging(w)) { + if (w->isDesktop()) { + effects->paintWindow(w, mask, region, data); + } + m_motionManager.apply(w, data); + QRect rect = m_motionManager.transformedGeometry(w).toRect(); + + if (m_activated && winData->highlight > 0.0) { + // scale the window (interpolated by the highlight level) to at least 105% or to cover 1/16 of the screen size - yet keep it in screen bounds + QRect area = effects->clientArea(FullScreenArea, w); + + QSizeF effSize(w->width()*data.xScale(), w->height()*data.yScale()); + const float xr = area.width()/effSize.width(); + const float yr = area.height()/effSize.height(); + float tScale = 0.0; + if (xr < yr) { + tScale = qMax(xr/4.0, yr/32.0); + } else { + tScale = qMax(xr/32.0, yr/4.0); + } + if (tScale < 1.05) { + tScale = 1.05; + } + if (effSize.width()*tScale > area.width()) + tScale = area.width() / effSize.width(); + if (effSize.height()*tScale > area.height()) + tScale = area.height() / effSize.height(); + + const qreal scale = interpolate(1.0, tScale, winData->highlight); + if (scale > 1.0) { + if (scale < tScale) // don't use lanczos during transition + mask &= ~PAINT_WINDOW_LANCZOS; + + const float df = (tScale-1.0f)*0.5f; + int tx = qRound(rect.width()*df); + int ty = qRound(rect.height()*df); + QRect tRect(rect.adjusted(-tx, -ty, tx, ty)); + tx = qMax(tRect.x(), area.x()) + qMin(0, area.right()-tRect.right()); + ty = qMax(tRect.y(), area.y()) + qMin(0, area.bottom()-tRect.bottom()); + tx = qRound((tx-rect.x())*winData->highlight); + ty = qRound((ty-rect.y())*winData->highlight); + + rect.translate(tx,ty); + rect.setWidth(rect.width()*scale); + rect.setHeight(rect.height()*scale); + + data *= QVector2D(scale, scale); + data += QPoint(tx, ty); + } + } + + if (m_motionManager.areWindowsMoving()) { + mask &= ~PAINT_WINDOW_LANCZOS; + } + effects->paintWindow(w, mask, region, data); + + if (m_showIcons) { + QPoint point(rect.x() + rect.width() * 0.95, + rect.y() + rect.height() * 0.95); + winData->iconFrame->setPosition(point); + if (effects->compositingType() == KWin::OpenGL2Compositing && data.shader) { + const float a = 0.9 * data.opacity() * m_decalOpacity * 0.75; + data.shader->setUniform(GLShader::ModulationConstant, QVector4D(a, a, a, a)); + } + winData->iconFrame->render(region, 0.9 * data.opacity() * m_decalOpacity, 0.75); + } + if (m_showCaptions) { + QPoint point(rect.x() + rect.width() / 2, + rect.y() + rect.height() / 2); + winData->textFrame->setPosition(point); + if (effects->compositingType() == KWin::OpenGL2Compositing && data.shader) { + const float a = 0.9 * data.opacity() * m_decalOpacity * 0.75; + data.shader->setUniform(GLShader::ModulationConstant, QVector4D(a, a, a, a)); + } + winData->textFrame->render(region, 0.9 * data.opacity() * m_decalOpacity, 0.75); + } + } else { + effects->paintWindow(w, mask, region, data); + } + } else + effects->paintWindow(w, mask, region, data); +} + +//----------------------------------------------------------------------------- +// User interaction + +void PresentWindowsEffect::slotWindowAdded(EffectWindow *w) +{ + if (!m_activated) + return; + + WindowData *winData = &m_windowData[w]; + winData->visible = isVisibleWindow(w); + winData->opacity = 0.0; + winData->highlight = 0.0; + winData->textFrame = effects->effectFrame(EffectFrameUnstyled, false); + + QFont font; + font.setBold(true); + font.setPointSize(12); + + winData->textFrame->setFont(font); + winData->iconFrame = effects->effectFrame(EffectFrameUnstyled, false); + winData->iconFrame->setAlignment(Qt::AlignRight | Qt::AlignBottom); + winData->iconFrame->setIcon(w->icon()); + winData->iconFrame->setIconSize(QSize(32, 32)); + + if (isSelectableWindow(w)) { + m_motionManager.manage(w); + rearrangeWindows(); + } +} + +void PresentWindowsEffect::slotWindowClosed(EffectWindow *w) +{ + if (m_managerWindow == w) + m_managerWindow = nullptr; + + DataHash::iterator winData = m_windowData.find(w); + if (winData == m_windowData.end()) + return; + + winData->deleted = true; + if (!winData->referenced) { + winData->referenced = true; + w->refWindow(); + } + + if (m_highlightedWindow == w) + setHighlightedWindow(findFirstWindow()); + + rearrangeWindows(); + + foreach (EffectWindow *w, m_motionManager.managedWindows()) { + winData = m_windowData.find(w); + if (winData != m_windowData.end() && !winData->deleted) + return; // found one that is not deleted? then we go on + } + setActive(false); //else no need to keep this open +} + +void PresentWindowsEffect::slotWindowDeleted(EffectWindow *w) +{ + DataHash::iterator winData = m_windowData.find(w); + if (winData == m_windowData.end()) + return; + + delete winData->textFrame; + delete winData->iconFrame; + m_windowData.erase(winData); + m_motionManager.unmanage(w); +} + +void PresentWindowsEffect::slotWindowFrameGeometryChanged(EffectWindow* w, const QRect& old) +{ + Q_UNUSED(old) + + if (!m_activated) + return; + if (!m_windowData.contains(w)) + return; + + rearrangeWindows(); +} + +bool PresentWindowsEffect::borderActivated(ElectricBorder border) +{ + int mode = 0; + if (m_borderActivate.contains(border)) + mode |= 1; + else if (m_borderActivateAll.contains(border)) + mode |= 2; + else if (m_borderActivateClass.contains(border)) + mode |= 4; + + if (!mode) + return false; + + if (effects->activeFullScreenEffect() && effects->activeFullScreenEffect() != this) + return true; + + if (mode & 1) + toggleActive(); + else if (mode & 2) + toggleActiveAllDesktops(); + else if (mode & 4) + toggleActiveClass(); + return true; +} + +void PresentWindowsEffect::windowInputMouseEvent(QEvent *e) +{ + QMouseEvent* me = dynamic_cast< QMouseEvent* >(e); + if (!me) { + return; + } + me->setAccepted(false); + + if (m_closeView) { + const bool contains = m_closeView->geometry().contains(me->pos()); + if (!m_closeView->isVisible() && contains) { + updateCloseWindow(); + } + m_closeView->forwardMouseEvent(e); + } + if (e->isAccepted()) { + return; + } + inputEventUpdate(me->pos(), me->type(), me->button()); +} + +void PresentWindowsEffect::inputEventUpdate(const QPoint &pos, QEvent::Type type, Qt::MouseButton button) +{ + // Which window are we hovering over? Always trigger as we don't always get move events before clicking + // We cannot use m_motionManager.windowAtPoint() as the window might not be visible + EffectWindowList windows = m_motionManager.managedWindows(); + bool hovering = false; + EffectWindow *highlightCandidate = nullptr; + for (int i = 0; i < windows.size(); ++i) { + DataHash::const_iterator winData = m_windowData.constFind(windows.at(i)); + if (winData == m_windowData.constEnd()) + continue; + + if (m_motionManager.transformedGeometry(windows.at(i)).contains(pos) && + winData->visible && !winData->deleted) { + hovering = true; + if (windows.at(i) && m_highlightedWindow != windows.at(i)) + highlightCandidate = windows.at(i); + break; + } + } + + if (!hovering) + setHighlightedWindow(nullptr); + if (m_highlightedWindow && m_motionManager.transformedGeometry(m_highlightedWindow).contains(pos)) + updateCloseWindow(); + else if (m_closeView) + m_closeView->hide(); + + if (type == QEvent::MouseButtonRelease) { + if (highlightCandidate) + setHighlightedWindow(highlightCandidate); + if (button == Qt::LeftButton) { + if (hovering) { + // mouse is hovering above a window - use MouseActionsWindow + mouseActionWindow(m_leftButtonWindow); + } else { + // mouse is hovering above desktop - use MouseActionsDesktop + mouseActionDesktop(m_leftButtonDesktop); + } + } + if (button == Qt::MiddleButton) { + if (hovering) { + // mouse is hovering above a window - use MouseActionsWindow + mouseActionWindow(m_middleButtonWindow); + } else { + // mouse is hovering above desktop - use MouseActionsDesktop + mouseActionDesktop(m_middleButtonDesktop); + } + } + if (button == Qt::RightButton) { + if (hovering) { + // mouse is hovering above a window - use MouseActionsWindow + mouseActionWindow(m_rightButtonWindow); + } else { + // mouse is hovering above desktop - use MouseActionsDesktop + mouseActionDesktop(m_rightButtonDesktop); + } + } + } else if (highlightCandidate && !m_motionManager.areWindowsMoving()) + setHighlightedWindow(highlightCandidate); +} + +bool PresentWindowsEffect::touchDown(qint32 id, const QPointF &pos, quint32 time) +{ + Q_UNUSED(time) + + if (!m_activated) { + return false; + } + + // only if we don't track a touch id yet + if (!m_touch.active) { + m_touch.active = true; + m_touch.id = id; + inputEventUpdate(pos.toPoint()); + } + return true; +} + +bool PresentWindowsEffect::touchMotion(qint32 id, const QPointF &pos, quint32 time) +{ + Q_UNUSED(id) + Q_UNUSED(time) + + if (!m_activated) { + return false; + } + if (m_touch.active && m_touch.id == id) { + // only update for the touch id we track + inputEventUpdate(pos.toPoint()); + } + return true; +} + +bool PresentWindowsEffect::touchUp(qint32 id, quint32 time) +{ + Q_UNUSED(id) + Q_UNUSED(time) + + if (!m_activated) { + return false; + } + if (m_touch.active && m_touch.id == id) { + m_touch.active = false; + m_touch.id = 0; + if (m_highlightedWindow) { + mouseActionWindow(m_leftButtonWindow); + } + } + return true; +} + +void PresentWindowsEffect::mouseActionWindow(WindowMouseAction& action) +{ + switch(action) { + case WindowActivateAction: + if (m_highlightedWindow) + effects->activateWindow(m_highlightedWindow); + setActive(false); + break; + case WindowExitAction: + setActive(false); + break; + case WindowToCurrentDesktopAction: + if (m_highlightedWindow) + effects->windowToDesktop(m_highlightedWindow, effects->currentDesktop()); + break; + case WindowToAllDesktopsAction: + if (m_highlightedWindow) { + if (m_highlightedWindow->isOnAllDesktops()) + effects->windowToDesktop(m_highlightedWindow, effects->currentDesktop()); + else + effects->windowToDesktop(m_highlightedWindow, NET::OnAllDesktops); + } + break; + case WindowMinimizeAction: + if (m_highlightedWindow) { + if (m_highlightedWindow->isMinimized()) + m_highlightedWindow->unminimize(); + else + m_highlightedWindow->minimize(); + } + break; + case WindowCloseAction: + if (m_highlightedWindow) { + m_highlightedWindow->closeWindow(); + } + break; + default: + break; + } +} + +void PresentWindowsEffect::mouseActionDesktop(DesktopMouseAction& action) +{ + switch(action) { + case DesktopActivateAction: + if (m_highlightedWindow) + effects->activateWindow(m_highlightedWindow); + setActive(false); + break; + case DesktopExitAction: + setActive(false); + break; + case DesktopShowDesktopAction: + effects->setShowingDesktop(true); + setActive(false); + default: + break; + } +} + +void PresentWindowsEffect::grabbedKeyboardEvent(QKeyEvent *e) +{ + if (e->type() == QEvent::KeyPress) { + // check for global shortcuts + // HACK: keyboard grab disables the global shortcuts so we have to check for global shortcut (bug 156155) + if (m_mode == ModeCurrentDesktop && shortcut.contains(e->key() + e->modifiers())) { + toggleActive(); + return; + } + if (m_mode == ModeAllDesktops && shortcutAll.contains(e->key() + e->modifiers())) { + toggleActiveAllDesktops(); + return; + } + if (m_mode == ModeWindowClass && shortcutClass.contains(e->key() + e->modifiers())) { + toggleActiveClass(); + return; + } + + switch(e->key()) { + // Wrap only if not auto-repeating + case Qt::Key_Left: + setHighlightedWindow(relativeWindow(m_highlightedWindow, -1, 0, !e->isAutoRepeat())); + break; + case Qt::Key_Right: + setHighlightedWindow(relativeWindow(m_highlightedWindow, 1, 0, !e->isAutoRepeat())); + break; + case Qt::Key_Up: + setHighlightedWindow(relativeWindow(m_highlightedWindow, 0, -1, !e->isAutoRepeat())); + break; + case Qt::Key_Down: + setHighlightedWindow(relativeWindow(m_highlightedWindow, 0, 1, !e->isAutoRepeat())); + break; + case Qt::Key_Home: + setHighlightedWindow(relativeWindow(m_highlightedWindow, -1000, 0, false)); + break; + case Qt::Key_End: + setHighlightedWindow(relativeWindow(m_highlightedWindow, 1000, 0, false)); + break; + case Qt::Key_PageUp: + setHighlightedWindow(relativeWindow(m_highlightedWindow, 0, -1000, false)); + break; + case Qt::Key_PageDown: + setHighlightedWindow(relativeWindow(m_highlightedWindow, 0, 1000, false)); + break; + case Qt::Key_Backspace: + if (!m_windowFilter.isEmpty()) { + m_windowFilter.remove(m_windowFilter.length() - 1, 1); + updateFilterFrame(); + rearrangeWindows(); + } + return; + case Qt::Key_Escape: + setActive(false); + return; + case Qt::Key_Return: + case Qt::Key_Enter: + if (m_highlightedWindow) + effects->activateWindow(m_highlightedWindow); + setActive(false); + return; + case Qt::Key_Tab: + return; // Nothing at the moment + case Qt::Key_Delete: + if (!m_windowFilter.isEmpty()) { + m_windowFilter.clear(); + updateFilterFrame(); + rearrangeWindows(); + } + break; + case 0: + return; // HACK: Workaround for Qt bug on unbound keys (#178547) + default: + if (!e->text().isEmpty()) { + m_windowFilter.append(e->text()); + updateFilterFrame(); + rearrangeWindows(); + return; + } + break; + } + } +} + +//----------------------------------------------------------------------------- +// Atom handling +void PresentWindowsEffect::slotPropertyNotify(EffectWindow* w, long a) +{ + if (m_atomDesktop == XCB_ATOM_NONE && m_atomWindows == XCB_ATOM_NONE) { + return; + } + if (!w || (a != m_atomDesktop && a != m_atomWindows)) + return; // Not our atom + + if (a == m_atomDesktop) { + QByteArray byteData = w->readProperty(m_atomDesktop, m_atomDesktop, 32); + if (byteData.length() < 1) { + // Property was removed, end present windows + setActive(false); + return; + } + auto* data = reinterpret_cast(byteData.data()); + + if (!data[0]) { + // Purposely ending present windows by issuing a NULL target + setActive(false); + return; + } + // present windows is active so don't do anything + if (m_activated) + return; + + int desktop = data[0]; + if (desktop > effects->numberOfDesktops()) + return; + if (desktop == -1) + toggleActiveAllDesktops(); + else { + m_mode = ModeSelectedDesktop; + m_desktop = desktop; + m_managerWindow = w; + setActive(true); + } + } else if (a == m_atomWindows) { + QByteArray byteData = w->readProperty(m_atomWindows, m_atomWindows, 32); + if (byteData.length() < 1) { + // Property was removed, end present windows + setActive(false); + return; + } + auto* data = reinterpret_cast(byteData.data()); + + if (!data[0]) { + // Purposely ending present windows by issuing a NULL target + setActive(false); + return; + } + // present windows is active so don't do anything + if (m_activated) + return; + + // for security clear selected windows + m_selectedWindows.clear(); + int length = byteData.length() / sizeof(data[0]); + for (int i = 0; i < length; i++) { + EffectWindow* foundWin = effects->findWindow(data[i]); + if (!foundWin) { + qCDebug(KWINEFFECTS) << "Invalid window targetted for present windows. Requested:" << data[i]; + continue; + } + m_selectedWindows.append(foundWin); + } + m_mode = ModeWindowGroup; + m_managerWindow = w; + setActive(true); + } +} + +//----------------------------------------------------------------------------- +// Window rearranging + +void PresentWindowsEffect::rearrangeWindows() +{ + if (!m_activated) + return; + + effects->addRepaintFull(); // Trigger the first repaint + if (m_closeView) + m_closeView->hide(); + + // Work out which windows are on which screens + EffectWindowList windowlist; + QList windowlists; + for (int i = 0; i < effects->numScreens(); i++) + windowlists.append(EffectWindowList()); + + if (m_windowFilter.isEmpty()) { + windowlist = m_motionManager.managedWindows(); + foreach (EffectWindow * w, m_motionManager.managedWindows()) { + DataHash::iterator winData = m_windowData.find(w); + if (winData == m_windowData.end() || winData->deleted) + continue; // don't include closed windows + windowlists[w->screen()].append(w); + winData->visible = true; + } + } else { + // Can we move this filtering somewhere else? + foreach (EffectWindow * w, m_motionManager.managedWindows()) { + DataHash::iterator winData = m_windowData.find(w); + if (winData == m_windowData.end() || winData->deleted) + continue; // don't include closed windows + + if (w->caption().contains(m_windowFilter, Qt::CaseInsensitive) || + w->windowClass().contains(m_windowFilter, Qt::CaseInsensitive) || + w->windowRole().contains(m_windowFilter, Qt::CaseInsensitive)) { + windowlist.append(w); + windowlists[w->screen()].append(w); + winData->visible = true; + } else + winData->visible = false; + } + } + if (windowlist.isEmpty()) { + setHighlightedWindow(nullptr); + return; + } + + // We filtered out the highlighted window + if (m_highlightedWindow) { + DataHash::iterator winData = m_windowData.find(m_highlightedWindow); + if (winData != m_windowData.end() && !winData->visible) + setHighlightedWindow(findFirstWindow()); + } else + setHighlightedWindow(findFirstWindow()); + + int screens = effects->numScreens(); + for (int screen = 0; screen < screens; screen++) { + EffectWindowList windows; + windows = windowlists[screen]; + + // Don't rearrange if the grid is the same size as what it was before to prevent + // windows moving to a better spot if one was filtered out. + if (m_layoutMode == LayoutRegularGrid && + m_gridSizes[screen].columns && + m_gridSizes[screen].rows && + windows.size() < m_gridSizes[screen].columns * m_gridSizes[screen].rows && + windows.size() > (m_gridSizes[screen].columns - 1) * m_gridSizes[screen].rows && + windows.size() > m_gridSizes[screen].columns *(m_gridSizes[screen].rows - 1)) + continue; + + // No point continuing if there is no windows to process + if (!windows.count()) + continue; + + calculateWindowTransformations(windows, screen, m_motionManager); + } + + // Resize text frames if required + QFontMetrics* metrics = nullptr; // All fonts are the same + foreach (EffectWindow * w, m_motionManager.managedWindows()) { + DataHash::iterator winData = m_windowData.find(w); + if (winData == m_windowData.end()) + continue; + + if (!metrics) + metrics = new QFontMetrics(winData->textFrame->font()); + + QRect geom = m_motionManager.targetGeometry(w).toRect(); + QString string = metrics->elidedText(w->caption(), Qt::ElideRight, geom.width() * 0.9); + if (string != winData->textFrame->text()) + winData->textFrame->setText(string); + } + delete metrics; +} + +void PresentWindowsEffect::calculateWindowTransformations(EffectWindowList windowlist, int screen, + WindowMotionManager& motionManager, bool external) +{ + if (m_layoutMode == LayoutRegularGrid) + calculateWindowTransformationsClosest(windowlist, screen, motionManager); + else if (m_layoutMode == LayoutFlexibleGrid) + calculateWindowTransformationsKompose(windowlist, screen, motionManager); + else + calculateWindowTransformationsNatural(windowlist, screen, motionManager); + + // If called externally we don't need to remember this data + if (external) + m_windowData.clear(); +} + +static inline int distance(QPoint &pos1, QPoint &pos2) +{ + const int xdiff = pos1.x() - pos2.x(); + const int ydiff = pos1.y() - pos2.y(); + return int(sqrt(float(xdiff*xdiff + ydiff*ydiff))); +} + +void PresentWindowsEffect::calculateWindowTransformationsClosest(EffectWindowList windowlist, int screen, + WindowMotionManager& motionManager) +{ + // This layout mode requires at least one window visible + if (windowlist.count() == 0) + return; + + QRect area = effects->clientArea(ScreenArea, screen, effects->currentDesktop()); + if (m_showPanel) // reserve space for the panel + area = effects->clientArea(MaximizeArea, screen, effects->currentDesktop()); + int columns = int(ceil(sqrt(double(windowlist.count())))); + int rows = int(ceil(windowlist.count() / double(columns))); + + // Remember the size for later + // If we are using this layout externally we don't need to remember m_gridSizes. + if (m_gridSizes.size() != 0) { + m_gridSizes[screen].columns = columns; + m_gridSizes[screen].rows = rows; + } + + // Assign slots + int slotWidth = area.width() / columns; + int slotHeight = area.height() / rows; + QVector takenSlots; + takenSlots.resize(rows*columns); + takenSlots.fill(0); + + // precalculate all slot centers + QVector slotCenters; + slotCenters.resize(rows*columns); + for (int x = 0; x < columns; ++x) + for (int y = 0; y < rows; ++y) { + slotCenters[x + y*columns] = QPoint(area.x() + slotWidth * x + slotWidth / 2, + area.y() + slotHeight * y + slotHeight / 2); + } + + // Assign each window to the closest available slot + EffectWindowList tmpList = windowlist; // use a QLinkedList copy instead? + QPoint otherPos; + while (!tmpList.isEmpty()) { + EffectWindow *w = tmpList.first(); + int slotCandidate = -1, slotCandidateDistance = INT_MAX; + QPoint pos = w->geometry().center(); + + for (int i = 0; i < columns*rows; ++i) { // all slots + const int dist = distance(pos, slotCenters[i]); + if (dist < slotCandidateDistance) { // window is interested in this slot + EffectWindow *occupier = takenSlots[i]; + Q_ASSERT(occupier != w); + if (!occupier || dist < distance((otherPos = occupier->geometry().center()), slotCenters[i])) { + // either nobody lives here, or we're better - takeover the slot if it's our best + slotCandidate = i; + slotCandidateDistance = dist; + } + } + } + Q_ASSERT(slotCandidate != -1); + if (takenSlots[slotCandidate]) + tmpList << takenSlots[slotCandidate]; // occupier needs a new home now :p + tmpList.removeAll(w); + takenSlots[slotCandidate] = w; // ...and we rumble in =) + } + + for (int slot = 0; slot < columns*rows; ++slot) { + EffectWindow *w = takenSlots[slot]; + if (!w) // some slots might be empty + continue; + + // Work out where the slot is + QRect target( + area.x() + (slot % columns) * slotWidth, + area.y() + (slot / columns) * slotHeight, + slotWidth, slotHeight); + target.adjust(10, 10, -10, -10); // Borders + + double scale; + if (target.width() / double(w->width()) < target.height() / double(w->height())) { + // Center vertically + scale = target.width() / double(w->width()); + target.moveTop(target.top() + (target.height() - int(w->height() * scale)) / 2); + target.setHeight(int(w->height() * scale)); + } else { + // Center horizontally + scale = target.height() / double(w->height()); + target.moveLeft(target.left() + (target.width() - int(w->width() * scale)) / 2); + target.setWidth(int(w->width() * scale)); + } + // Don't scale the windows too much + if (scale > 2.0 || (scale > 1.0 && (w->width() > 300 || w->height() > 300))) { + scale = (w->width() > 300 || w->height() > 300) ? 1.0 : 2.0; + target = QRect( + target.center().x() - int(w->width() * scale) / 2, + target.center().y() - int(w->height() * scale) / 2, + scale * w->width(), scale * w->height()); + } + motionManager.moveWindow(w, target); + } +} + +void PresentWindowsEffect::calculateWindowTransformationsKompose(EffectWindowList windowlist, int screen, + WindowMotionManager& motionManager) +{ + // This layout mode requires at least one window visible + if (windowlist.count() == 0) + return; + + QRect availRect = effects->clientArea(ScreenArea, screen, effects->currentDesktop()); + if (m_showPanel) // reserve space for the panel + availRect = effects->clientArea(MaximizeArea, screen, effects->currentDesktop()); + std::sort(windowlist.begin(), windowlist.end()); // The location of the windows should not depend on the stacking order + + // Following code is taken from Kompose 0.5.4, src/komposelayout.cpp + int spacing = 10; + int rows, columns; + double parentRatio = availRect.width() / (double)availRect.height(); + // Use more columns than rows when parent's width > parent's height + if (parentRatio > 1) { + columns = (int)ceil(sqrt((double)windowlist.count())); + rows = (int)ceil((double)windowlist.count() / (double)columns); + } else { + rows = (int)ceil(sqrt((double)windowlist.count())); + columns = (int)ceil((double)windowlist.count() / (double)rows); + } + //qCDebug(KWINEFFECTS) << "Using " << rows << " rows & " << columns << " columns for " << windowlist.count() << " clients"; + + // Calculate width & height + int w = (availRect.width() - (columns + 1) * spacing) / columns; + int h = (availRect.height() - (rows + 1) * spacing) / rows; + + EffectWindowList::iterator it(windowlist.begin()); + QList geometryRects; + QList maxRowHeights; + // Process rows + for (int i = 0; i < rows; ++i) { + int xOffsetFromLastCol = 0; + int maxHeightInRow = 0; + // Process columns + for (int j = 0; j < columns; ++j) { + EffectWindow* window; + + // Check for end of List + if (it == windowlist.end()) + break; + window = *it; + + // Calculate width and height of widget + double ratio = aspectRatio(window); + + int widgetw = 100; + int widgeth = 100; + int usableW = w; + int usableH = h; + + // use width of two boxes if there is no right neighbour + if (window == windowlist.last() && j != columns - 1) { + usableW = 2 * w; + } + ++it; // We need access to the neighbour in the following + // expand if right neighbour has ratio < 1 + if (j != columns - 1 && it != windowlist.end() && aspectRatio(*it) < 1) { + int addW = w - widthForHeight(*it, h); + if (addW > 0) { + usableW = w + addW; + } + } + + if (ratio == -1) { + widgetw = w; + widgeth = h; + } else { + double widthByHeight = widthForHeight(window, usableH); + double heightByWidth = heightForWidth(window, usableW); + if ((ratio >= 1.0 && heightByWidth <= usableH) || + (ratio < 1.0 && widthByHeight > usableW)) { + widgetw = usableW; + widgeth = (int)heightByWidth; + } else if ((ratio < 1.0 && widthByHeight <= usableW) || + (ratio >= 1.0 && heightByWidth > usableH)) { + widgeth = usableH; + widgetw = (int)widthByHeight; + } + // Don't upscale large-ish windows + if (widgetw > window->width() && (window->width() > 300 || window->height() > 300)) { + widgetw = window->width(); + widgeth = window->height(); + } + } + + // Set the Widget's size + + int alignmentXoffset = 0; + int alignmentYoffset = 0; + if (i == 0 && h > widgeth) + alignmentYoffset = h - widgeth; + if (j == 0 && w > widgetw) + alignmentXoffset = w - widgetw; + QRect geom(availRect.x() + j *(w + spacing) + spacing + alignmentXoffset + xOffsetFromLastCol, + availRect.y() + i *(h + spacing) + spacing + alignmentYoffset, + widgetw, widgeth); + geometryRects.append(geom); + + // Set the x offset for the next column + if (alignmentXoffset == 0) + xOffsetFromLastCol += widgetw - w; + if (maxHeightInRow < widgeth) + maxHeightInRow = widgeth; + } + maxRowHeights.append(maxHeightInRow); + } + + int topOffset = 0; + for (int i = 0; i < rows; i++) { + for (int j = 0; j < columns; j++) { + int pos = i * columns + j; + if (pos >= windowlist.count()) + break; + + EffectWindow* window = windowlist[pos]; + QRect target = geometryRects[pos]; + target.setY(target.y() + topOffset); + // @Marrtin: any idea what this is good for? +// DataHash::iterator winData = m_windowData.find(window); +// if (winData != m_windowData.end()) +// winData->slot = pos; + motionManager.moveWindow(window, target); + + //qCDebug(KWINEFFECTS) << "Window '" << window->caption() << "' gets moved to (" << + // mWindowData[window].area.left() << "; " << mWindowData[window].area.right() << + // "), scale: " << mWindowData[window].scale << endl; + } + if (maxRowHeights[i] - h > 0) + topOffset += maxRowHeights[i] - h; + } +} + +void PresentWindowsEffect::calculateWindowTransformationsNatural(EffectWindowList windowlist, int screen, + WindowMotionManager& motionManager) +{ + // If windows do not overlap they scale into nothingness, fix by resetting. To reproduce + // just have a single window on a Xinerama screen or have two windows that do not touch. + // TODO: Work out why this happens, is most likely a bug in the manager. + foreach (EffectWindow * w, windowlist) + if (motionManager.transformedGeometry(w) == w->geometry()) + motionManager.reset(w); + + if (windowlist.count() == 1) { + // Just move the window to its original location to save time + if (effects->clientArea(FullScreenArea, windowlist[0]).contains(windowlist[0]->geometry())) { + motionManager.moveWindow(windowlist[0], windowlist[0]->geometry()); + return; + } + } + + // As we are using pseudo-random movement (See "slot") we need to make sure the list + // is always sorted the same way no matter which window is currently active. + std::sort(windowlist.begin(), windowlist.end()); + + QRect area = effects->clientArea(ScreenArea, screen, effects->currentDesktop()); + if (m_showPanel) // reserve space for the panel + area = effects->clientArea(MaximizeArea, screen, effects->currentDesktop()); + QRect bounds = area; + int direction = 0; + QHash targets; + QHash directions; + foreach (EffectWindow * w, windowlist) { + bounds = bounds.united(w->geometry()); + targets[w] = w->geometry(); + // Reuse the unused "slot" as a preferred direction attribute. This is used when the window + // is on the edge of the screen to try to use as much screen real estate as possible. + directions[w] = direction; + direction++; + if (direction == 4) + direction = 0; + } + + // Iterate over all windows, if two overlap push them apart _slightly_ as we try to + // brute-force the most optimal positions over many iterations. + bool overlap; + do { + overlap = false; + foreach (EffectWindow * w, windowlist) { + QRect *target_w = &targets[w]; + foreach (EffectWindow * e, windowlist) { + if (w == e) + continue; + + QRect *target_e = &targets[e]; + if (target_w->adjusted(-5, -5, 5, 5).intersects(target_e->adjusted(-5, -5, 5, 5))) { + overlap = true; + + // Determine pushing direction + QPoint diff(target_e->center() - target_w->center()); + // Prevent dividing by zero and non-movement + if (diff.x() == 0 && diff.y() == 0) + diff.setX(1); + // Try to keep screen aspect ratio + //if (bounds.height() / bounds.width() > area.height() / area.width()) + // diff.setY(diff.y() / 2); + //else + // diff.setX(diff.x() / 2); + // Approximate a vector of between 10px and 20px in magnitude in the same direction + diff *= m_accuracy / double(diff.manhattanLength()); + // Move both windows apart + target_w->translate(-diff); + target_e->translate(diff); + + // Try to keep the bounding rect the same aspect as the screen so that more + // screen real estate is utilised. We do this by splitting the screen into nine + // equal sections, if the window center is in any of the corner sections pull the + // window towards the outer corner. If it is in any of the other edge sections + // alternate between each corner on that edge. We don't want to determine it + // randomly as it will not produce consistant locations when using the filter. + // Only move one window so we don't cause large amounts of unnecessary zooming + // in some situations. We need to do this even when expanding later just in case + // all windows are the same size. + // (We are using an old bounding rect for this, hopefully it doesn't matter) + int xSection = (target_w->x() - bounds.x()) / (bounds.width() / 3); + int ySection = (target_w->y() - bounds.y()) / (bounds.height() / 3); + diff = QPoint(0, 0); + if (xSection != 1 || ySection != 1) { // Remove this if you want the center to pull as well + if (xSection == 1) + xSection = (directions[w] / 2 ? 2 : 0); + if (ySection == 1) + ySection = (directions[w] % 2 ? 2 : 0); + } + if (xSection == 0 && ySection == 0) + diff = QPoint(bounds.topLeft() - target_w->center()); + if (xSection == 2 && ySection == 0) + diff = QPoint(bounds.topRight() - target_w->center()); + if (xSection == 2 && ySection == 2) + diff = QPoint(bounds.bottomRight() - target_w->center()); + if (xSection == 0 && ySection == 2) + diff = QPoint(bounds.bottomLeft() - target_w->center()); + if (diff.x() != 0 || diff.y() != 0) { + diff *= m_accuracy / double(diff.manhattanLength()); + target_w->translate(diff); + } + + // Update bounding rect + bounds = bounds.united(*target_w); + bounds = bounds.united(*target_e); + } + } + } + } while (overlap); + + // Work out scaling by getting the most top-left and most bottom-right window coords. + // The 20's and 10's are so that the windows don't touch the edge of the screen. + double scale; + if (bounds == area) + scale = 1.0; // Don't add borders to the screen + else if (area.width() / double(bounds.width()) < area.height() / double(bounds.height())) + scale = (area.width() - 20) / double(bounds.width()); + else + scale = (area.height() - 20) / double(bounds.height()); + // Make bounding rect fill the screen size for later steps + bounds = QRect( + (bounds.x() * scale - (area.width() - 20 - bounds.width() * scale) / 2 - 10) / scale, + (bounds.y() * scale - (area.height() - 20 - bounds.height() * scale) / 2 - 10) / scale, + area.width() / scale, + area.height() / scale + ); + + // Move all windows back onto the screen and set their scale + QHash::iterator target = targets.begin(); + while (target != targets.end()) { + target->setRect((target->x() - bounds.x()) * scale + area.x(), + (target->y() - bounds.y()) * scale + area.y(), + target->width() * scale, + target->height() * scale + ); + ++target; + } + + // Try to fill the gaps by enlarging windows if they have the space + if (m_fillGaps) { + // Don't expand onto or over the border + QRegion borderRegion(area.adjusted(-200, -200, 200, 200)); + borderRegion ^= area.adjusted(10 / scale, 10 / scale, -10 / scale, -10 / scale); + + bool moved; + do { + moved = false; + foreach (EffectWindow * w, windowlist) { + QRect oldRect; + QRect *target = &targets[w]; + // This may cause some slight distortion if the windows are enlarged a large amount + int widthDiff = m_accuracy; + int heightDiff = heightForWidth(w, target->width() + widthDiff) - target->height(); + int xDiff = widthDiff / 2; // Also move a bit in the direction of the enlarge, allows the + int yDiff = heightDiff / 2; // center windows to be enlarged if there is gaps on the side. + + // heightDiff (and yDiff) will be re-computed after each successful enlargement attempt + // so that the error introduced in the window's aspect ratio is minimized + + // Attempt enlarging to the top-right + oldRect = *target; + target->setRect(target->x() + xDiff, + target->y() - yDiff - heightDiff, + target->width() + widthDiff, + target->height() + heightDiff + ); + if (isOverlappingAny(w, targets, borderRegion)) + *target = oldRect; + else { + moved = true; + heightDiff = heightForWidth(w, target->width() + widthDiff) - target->height(); + yDiff = heightDiff / 2; + } + + // Attempt enlarging to the bottom-right + oldRect = *target; + target->setRect( + target->x() + xDiff, + target->y() + yDiff, + target->width() + widthDiff, + target->height() + heightDiff + ); + if (isOverlappingAny(w, targets, borderRegion)) + *target = oldRect; + else { + moved = true; + heightDiff = heightForWidth(w, target->width() + widthDiff) - target->height(); + yDiff = heightDiff / 2; + } + + // Attempt enlarging to the bottom-left + oldRect = *target; + target->setRect( + target->x() - xDiff - widthDiff, + target->y() + yDiff, + target->width() + widthDiff, + target->height() + heightDiff + ); + if (isOverlappingAny(w, targets, borderRegion)) + *target = oldRect; + else { + moved = true; + heightDiff = heightForWidth(w, target->width() + widthDiff) - target->height(); + yDiff = heightDiff / 2; + } + + // Attempt enlarging to the top-left + oldRect = *target; + target->setRect( + target->x() - xDiff - widthDiff, + target->y() - yDiff - heightDiff, + target->width() + widthDiff, + target->height() + heightDiff + ); + if (isOverlappingAny(w, targets, borderRegion)) + *target = oldRect; + else + moved = true; + } + } while (moved); + + // The expanding code above can actually enlarge windows over 1.0/2.0 scale, we don't like this + // We can't add this to the loop above as it would cause a never-ending loop so we have to make + // do with the less-than-optimal space usage with using this method. + foreach (EffectWindow * w, windowlist) { + QRect *target = &targets[w]; + double scale = target->width() / double(w->width()); + if (scale > 2.0 || (scale > 1.0 && (w->width() > 300 || w->height() > 300))) { + scale = (w->width() > 300 || w->height() > 300) ? 1.0 : 2.0; + target->setRect( + target->center().x() - int(w->width() * scale) / 2, + target->center().y() - int(w->height() * scale) / 2, + w->width() * scale, + w->height() * scale); + } + } + } + + // Notify the motion manager of the targets + foreach (EffectWindow * w, windowlist) + motionManager.moveWindow(w, targets.value(w)); +} + +bool PresentWindowsEffect::isOverlappingAny(EffectWindow *w, const QHash &targets, const QRegion &border) +{ + QHash::const_iterator winTarget = targets.find(w); + if (winTarget == targets.constEnd()) + return false; + if (border.intersects(*winTarget)) + return true; + + // Is there a better way to do this? + QHash::const_iterator target; + for (target = targets.constBegin(); target != targets.constEnd(); ++target) { + if (target == winTarget) + continue; + if (winTarget->adjusted(-5, -5, 5, 5).intersects(target->adjusted(-5, -5, 5, 5))) + return true; + } + return false; +} + +//----------------------------------------------------------------------------- +// Activation + +void PresentWindowsEffect::setActive(bool active) +{ + if (effects->activeFullScreenEffect() && effects->activeFullScreenEffect() != this) + return; + if (m_activated == active) + return; + + m_activated = active; + if (m_activated) { + effects->setShowingDesktop(false); + m_needInitialSelection = true; + m_closeButtonCorner = (Qt::Corner)effects->kwinOption(KWin::CloseButtonCorner).toInt(); + m_decalOpacity = 0.0; + m_highlightedWindow = nullptr; + m_windowFilter.clear(); + + if (!(m_doNotCloseWindows || m_closeView)) { + m_closeView = new CloseWindowView(); + connect(m_closeView, &EffectQuickView::repaintNeeded, this, []() { + effects->addRepaintFull(); + }); + connect(m_closeView, &CloseWindowView::requestClose, this, &PresentWindowsEffect::closeWindow); + } + + // Add every single window to m_windowData (Just calling [w] creates it) + foreach (EffectWindow * w, effects->stackingOrder()) { + DataHash::iterator winData; + if ((winData = m_windowData.find(w)) != m_windowData.end()) { + winData->visible = isVisibleWindow(w); + continue; // Happens if we reactivate before the ending animation finishes + } + + winData = m_windowData.insert(w, WindowData()); + winData->visible = isVisibleWindow(w); + winData->deleted = false; + winData->referenced = false; + winData->opacity = 0.0; + if (w->isOnCurrentDesktop() && !w->isMinimized()) + winData->opacity = 1.0; + + winData->highlight = 1.0; + winData->textFrame = effects->effectFrame(EffectFrameUnstyled, false); + + QFont font; + font.setBold(true); + font.setPointSize(12); + + winData->textFrame->setFont(font); + winData->iconFrame = effects->effectFrame(EffectFrameUnstyled, false); + winData->iconFrame->setAlignment(Qt::AlignRight | Qt::AlignBottom); + winData->iconFrame->setIcon(w->icon()); + winData->iconFrame->setIconSize(QSize(32, 32)); + } + + // Filter out special windows such as panels and taskbars + foreach (EffectWindow * w, effects->stackingOrder()) { + if (isSelectableWindow(w)) { + m_motionManager.manage(w); + } + } + + if (m_motionManager.managedWindows().isEmpty() || + ((m_motionManager.managedWindows().count() == 1) && m_motionManager.managedWindows().first()->isOnCurrentDesktop() && + (m_ignoreMinimized || !m_motionManager.managedWindows().first()->isMinimized()))) { + // No point triggering if there is nothing to do + m_activated = false; + + DataHash::iterator i = m_windowData.begin(); + while (i != m_windowData.end()) { + delete i.value().textFrame; + delete i.value().iconFrame; + ++i; + } + m_windowData.clear(); + + m_motionManager.unmanageAll(); + return; + } + + // Create temporary input window to catch mouse events + effects->startMouseInterception(this, Qt::PointingHandCursor); + m_hasKeyboardGrab = effects->grabKeyboard(this); + effects->setActiveFullScreenEffect(this); + + reCreateGrids(); + rearrangeWindows(); + setHighlightedWindow(effects->activeWindow()); + + foreach (EffectWindow * w, effects->stackingOrder()) { + w->setData(WindowForceBlurRole, QVariant(true)); + w->setData(WindowForceBackgroundContrastRole, QVariant(true)); + } + } else { + m_needInitialSelection = false; + if (m_highlightedWindow) + effects->setElevatedWindow(m_highlightedWindow, false); + + // Fade in/out all windows + EffectWindow *activeWindow = effects->activeWindow(); + int desktop = effects->currentDesktop(); + if (activeWindow && !activeWindow->isOnAllDesktops()) + desktop = activeWindow->desktop(); + foreach (EffectWindow * w, effects->stackingOrder()) { + DataHash::iterator winData = m_windowData.find(w); + if (winData != m_windowData.end()) + winData->visible = (w->isOnDesktop(desktop) || w->isOnAllDesktops()) && + !w->isMinimized(); + } + if (m_closeView) + m_closeView->hide(); + + // Move all windows back to their original position + foreach (EffectWindow * w, m_motionManager.managedWindows()) + m_motionManager.moveWindow(w, w->geometry()); + if (m_filterFrame) { + m_filterFrame->free(); + } + m_windowFilter.clear(); + m_selectedWindows.clear(); + + effects->stopMouseInterception(this); + if (m_hasKeyboardGrab) + effects->ungrabKeyboard(); + m_hasKeyboardGrab = false; + + // destroy atom on manager window + if (m_managerWindow) { + if (m_mode == ModeSelectedDesktop && m_atomDesktop != XCB_ATOM_NONE) + m_managerWindow->deleteProperty(m_atomDesktop); + else if (m_mode == ModeWindowGroup && m_atomWindows != XCB_ATOM_NONE) + m_managerWindow->deleteProperty(m_atomWindows); + m_managerWindow = nullptr; + } + } + + effects->addRepaintFull(); // Trigger the first repaint +} + +//----------------------------------------------------------------------------- +// Filter box + +void PresentWindowsEffect::updateFilterFrame() +{ + QRect area = effects->clientArea(ScreenArea, effects->activeScreen(), effects->currentDesktop()); + if (!m_filterFrame) { + m_filterFrame = effects->effectFrame(EffectFrameStyled, false); + QFont font; + font.setPointSize(font.pointSize() * 2); + font.setBold(true); + m_filterFrame->setFont(font); + } + m_filterFrame->setPosition(QPoint(area.x() + area.width() / 2, area.y() + area.height() / 2)); + m_filterFrame->setText(i18n("Filter:\n%1", m_windowFilter)); +} + +//----------------------------------------------------------------------------- +// Helper functions + +bool PresentWindowsEffect::isSelectableWindow(EffectWindow *w) +{ + if (!w->isOnCurrentActivity()) + return false; + if (w->isSpecialWindow() || w->isUtility()) + return false; + if (w->isDeleted()) + return false; + if (!w->acceptsFocus()) + return false; + if (w->isSkipSwitcher()) + return false; + if (m_ignoreMinimized && w->isMinimized()) + return false; + + switch(m_mode) { + default: + case ModeAllDesktops: + return true; + case ModeCurrentDesktop: + return w->isOnCurrentDesktop(); + case ModeSelectedDesktop: + return w->isOnDesktop(m_desktop); + case ModeWindowGroup: + return m_selectedWindows.contains(w); + case ModeWindowClass: + return m_class == w->windowClass(); + } +} + +bool PresentWindowsEffect::isVisibleWindow(EffectWindow *w) +{ + if (w->isDesktop()) + return true; + return isSelectableWindow(w); +} + +void PresentWindowsEffect::setHighlightedWindow(EffectWindow *w) +{ + if (w == m_highlightedWindow || (w != nullptr && !m_motionManager.isManaging(w))) + return; + + if (m_closeView) + m_closeView->hide(); + if (m_highlightedWindow) { + effects->setElevatedWindow(m_highlightedWindow, false); + m_highlightedWindow->addRepaintFull(); // Trigger the first repaint + } + m_highlightedWindow = w; + if (m_highlightedWindow) { + effects->setElevatedWindow(m_highlightedWindow, true); + m_highlightedWindow->addRepaintFull(); // Trigger the first repaint + } + + updateCloseWindow(); +} + +void PresentWindowsEffect::updateCloseWindow() +{ + if (!m_closeView || m_doNotCloseWindows) + return; + + if (!m_activated || !m_highlightedWindow || m_highlightedWindow->isDesktop()) { + m_closeView->hide(); + return; + } + if (m_closeView->isVisible()) + return; + + const QRectF rect(m_motionManager.targetGeometry(m_highlightedWindow)); + if (2*m_closeView->geometry().width() > rect.width() && 2*m_closeView->geometry().height() > rect.height()) { + // not for tiny windows (eg. with many windows) - they might become unselectable + m_closeView->hide(); + return; + } + + QRect cvr(QPoint(0,0), m_closeView->size()); + switch (m_closeButtonCorner) + { + case Qt::TopLeftCorner: + default: + cvr.moveTopLeft(rect.topLeft().toPoint()); break; + case Qt::TopRightCorner: + cvr.moveTopRight(rect.topRight().toPoint()); break; + case Qt::BottomLeftCorner: + cvr.moveBottomLeft(rect.bottomLeft().toPoint()); break; + case Qt::BottomRightCorner: + cvr.moveBottomRight(rect.bottomRight().toPoint()); break; + } + + m_closeView->setGeometry(cvr); + + if (rect.contains(effects->cursorPos())) { + m_closeView->show(); + m_closeView->disarm(); + } + else + m_closeView->hide(); +} + +void PresentWindowsEffect::closeWindow() +{ + if (m_highlightedWindow) + m_highlightedWindow->closeWindow(); +} + +EffectWindow* PresentWindowsEffect::relativeWindow(EffectWindow *w, int xdiff, int ydiff, bool wrap) const +{ + if (!w) + return m_motionManager.managedWindows().first(); + + // TODO: Is it possible to select hidden windows? + EffectWindow* next; + QRect area = effects->clientArea(FullArea, 0, effects->currentDesktop()); + QRect detectRect; + + // Detect across the width of the desktop + if (xdiff != 0) { + if (xdiff > 0) { + // Detect right + for (int i = 0; i < xdiff; i++) { + QRectF wArea = m_motionManager.transformedGeometry(w); + detectRect = QRect(0, wArea.y(), area.width(), wArea.height()); + next = nullptr; + + foreach (EffectWindow * e, m_motionManager.managedWindows()) { + DataHash::const_iterator winData = m_windowData.find(e); + if (winData == m_windowData.end() || !winData->visible) + continue; + + QRectF eArea = m_motionManager.transformedGeometry(e); + if (eArea.intersects(detectRect) && + eArea.x() > wArea.x()) { + if (next == nullptr) + next = e; + else { + QRectF nArea = m_motionManager.transformedGeometry(next); + if (eArea.x() < nArea.x()) + next = e; + } + } + } + if (next == nullptr) { + if (wrap) // We are at the right-most window, now get the left-most one to wrap + return relativeWindow(w, -1000, 0, false); + break; // No more windows to the right + } + w = next; + } + return w; + } else { + // Detect left + for (int i = 0; i < -xdiff; i++) { + QRectF wArea = m_motionManager.transformedGeometry(w); + detectRect = QRect(0, wArea.y(), area.width(), wArea.height()); + next = nullptr; + + foreach (EffectWindow * e, m_motionManager.managedWindows()) { + DataHash::const_iterator winData = m_windowData.find(e); + if (winData == m_windowData.end() || !winData->visible) + continue; + + QRectF eArea = m_motionManager.transformedGeometry(e); + if (eArea.intersects(detectRect) && + eArea.x() + eArea.width() < wArea.x() + wArea.width()) { + if (next == nullptr) + next = e; + else { + QRectF nArea = m_motionManager.transformedGeometry(next); + if (eArea.x() + eArea.width() > nArea.x() + nArea.width()) + next = e; + } + } + } + if (next == nullptr) { + if (wrap) // We are at the left-most window, now get the right-most one to wrap + return relativeWindow(w, 1000, 0, false); + break; // No more windows to the left + } + w = next; + } + return w; + } + } + + // Detect across the height of the desktop + if (ydiff != 0) { + if (ydiff > 0) { + // Detect down + for (int i = 0; i < ydiff; i++) { + QRectF wArea = m_motionManager.transformedGeometry(w); + detectRect = QRect(wArea.x(), 0, wArea.width(), area.height()); + next = nullptr; + + foreach (EffectWindow * e, m_motionManager.managedWindows()) { + DataHash::const_iterator winData = m_windowData.find(e); + if (winData == m_windowData.end() || !winData->visible) + continue; + + QRectF eArea = m_motionManager.transformedGeometry(e); + if (eArea.intersects(detectRect) && + eArea.y() > wArea.y()) { + if (next == nullptr) + next = e; + else { + QRectF nArea = m_motionManager.transformedGeometry(next); + if (eArea.y() < nArea.y()) + next = e; + } + } + } + if (next == nullptr) { + if (wrap) // We are at the bottom-most window, now get the top-most one to wrap + return relativeWindow(w, 0, -1000, false); + break; // No more windows to the bottom + } + w = next; + } + return w; + } else { + // Detect up + for (int i = 0; i < -ydiff; i++) { + QRectF wArea = m_motionManager.transformedGeometry(w); + detectRect = QRect(wArea.x(), 0, wArea.width(), area.height()); + next = nullptr; + + foreach (EffectWindow * e, m_motionManager.managedWindows()) { + DataHash::const_iterator winData = m_windowData.find(e); + if (winData == m_windowData.end() || !winData->visible) + continue; + + QRectF eArea = m_motionManager.transformedGeometry(e); + if (eArea.intersects(detectRect) && + eArea.y() + eArea.height() < wArea.y() + wArea.height()) { + if (next == nullptr) + next = e; + else { + QRectF nArea = m_motionManager.transformedGeometry(next); + if (eArea.y() + eArea.height() > nArea.y() + nArea.height()) + next = e; + } + } + } + if (next == nullptr) { + if (wrap) // We are at the top-most window, now get the bottom-most one to wrap + return relativeWindow(w, 0, 1000, false); + break; // No more windows to the top + } + w = next; + } + return w; + } + } + + abort(); // Should never get here +} + +EffectWindow* PresentWindowsEffect::findFirstWindow() const +{ + EffectWindow *topLeft = nullptr; + QRectF topLeftGeometry; + + foreach (EffectWindow * w, m_motionManager.managedWindows()) { + DataHash::const_iterator winData = m_windowData.find(w); + if (winData == m_windowData.end()) + continue; + QRectF geometry = m_motionManager.transformedGeometry(w); + if (winData->visible == false) + continue; // Not visible + if (winData->deleted) + continue; // Window has been closed + if (topLeft == nullptr) { + topLeft = w; + topLeftGeometry = geometry; + } else if (geometry.x() < topLeftGeometry.x() || geometry.y() < topLeftGeometry.y()) { + topLeft = w; + topLeftGeometry = geometry; + } + } + return topLeft; +} + +void PresentWindowsEffect::globalShortcutChanged(QAction *action, const QKeySequence& seq) +{ + if (action->objectName() == QStringLiteral("Expose")) { + shortcut.clear(); + shortcut.append(seq); + } else if (action->objectName() == QStringLiteral("ExposeAll")) { + shortcutAll.clear(); + shortcutAll.append(seq); + } else if (action->objectName() == QStringLiteral("ExposeClass")) { + shortcutClass.clear(); + shortcutClass.append(seq); + } +} + +bool PresentWindowsEffect::isActive() const +{ + return (m_activated || m_motionManager.managingWindows()) && !effects->isScreenLocked(); +} + +void PresentWindowsEffect::reCreateGrids() +{ + m_gridSizes.clear(); + for (int i = 0; i < effects->numScreens(); ++i) { + m_gridSizes.append(GridSize()); + } + rearrangeWindows(); +} + +CloseWindowView::CloseWindowView(QObject *parent) + : EffectQuickScene(parent) +{ + setSource(QUrl(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kwin/effects/presentwindows/main.qml")))); + if (QQuickItem *item = rootItem()) { + connect(item, SIGNAL(clicked()), this, SLOT(clicked())); + setGeometry(QRect(QPoint(), QSize(item->implicitWidth(), item->implicitHeight()))); + } + m_armTimer.restart(); +} + +void CloseWindowView::clicked() +{ + // 50ms until the window is elevated (seen!) and 300ms more to be "realized" by the user. + if (m_armTimer.hasExpired(350)) { + emit requestClose(); + } +} + +void CloseWindowView::disarm() +{ + m_armTimer.restart(); +} + + +} // namespace diff --git a/effects/presentwindows/presentwindows.h b/effects/presentwindows/presentwindows.h new file mode 100644 index 0000000..bd54144 --- /dev/null +++ b/effects/presentwindows/presentwindows.h @@ -0,0 +1,323 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Rivo Laks + SPDX-FileCopyrightText: 2008 Lucas Murray + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_PRESENTWINDOWS_H +#define KWIN_PRESENTWINDOWS_H + +#include "presentwindows_proxy.h" + +#include +#include + +#include + +class QMouseEvent; +class QQuickView; + +namespace KWin +{ +class CloseWindowView : public EffectQuickScene +{ + Q_OBJECT +public: + explicit CloseWindowView(QObject *parent = nullptr); + void disarm(); +Q_SIGNALS: + void requestClose(); +private Q_SLOTS: + void clicked(); +private: + QElapsedTimer m_armTimer; +}; + +/** + * Expose-like effect which shows all windows on current desktop side-by-side, + * letting the user select active window. + */ +class PresentWindowsEffect + : public Effect +{ + Q_OBJECT + Q_PROPERTY(int layoutMode READ layoutMode) + Q_PROPERTY(bool showCaptions READ isShowCaptions) + Q_PROPERTY(bool showIcons READ isShowIcons) + Q_PROPERTY(bool doNotCloseWindows READ isDoNotCloseWindows) + Q_PROPERTY(bool ignoreMinimized READ isIgnoreMinimized) + Q_PROPERTY(int accuracy READ accuracy) + Q_PROPERTY(bool fillGaps READ isFillGaps) + Q_PROPERTY(int fadeDuration READ fadeDuration) + Q_PROPERTY(bool showPanel READ isShowPanel) + Q_PROPERTY(int leftButtonWindow READ leftButtonWindow) + Q_PROPERTY(int rightButtonWindow READ rightButtonWindow) + Q_PROPERTY(int middleButtonWindow READ middleButtonWindow) + Q_PROPERTY(int leftButtonDesktop READ leftButtonDesktop) + Q_PROPERTY(int middleButtonDesktop READ middleButtonDesktop) + Q_PROPERTY(int rightButtonDesktop READ rightButtonDesktop) + // TODO: electric borders +private: + // Structures + struct WindowData { + bool visible; + bool deleted; + bool referenced; + double opacity; + double highlight; + EffectFrame* textFrame; + EffectFrame* iconFrame; + }; + typedef QHash DataHash; + struct GridSize { + int columns; + int rows; + }; + +public: + PresentWindowsEffect(); + ~PresentWindowsEffect() override; + + void reconfigure(ReconfigureFlags) override; + void* proxy() override; + + // Screen painting + void prePaintScreen(ScreenPrePaintData &data, int time) override; + void paintScreen(int mask, const QRegion ®ion, ScreenPaintData &data) override; + void postPaintScreen() override; + + // Window painting + void prePaintWindow(EffectWindow *w, WindowPrePaintData &data, int time) override; + void paintWindow(EffectWindow *w, int mask, QRegion region, WindowPaintData &data) override; + + // User interaction + bool borderActivated(ElectricBorder border) override; + void windowInputMouseEvent(QEvent *e) override; + void grabbedKeyboardEvent(QKeyEvent *e) override; + bool isActive() const override; + + bool touchDown(qint32 id, const QPointF &pos, quint32 time) override; + bool touchMotion(qint32 id, const QPointF &pos, quint32 time) override; + bool touchUp(qint32 id, quint32 time) override; + + int requestedEffectChainPosition() const override { + return 70; + } + + enum { LayoutNatural, LayoutRegularGrid, LayoutFlexibleGrid }; // Layout modes + enum PresentWindowsMode { + ModeAllDesktops, // Shows windows of all desktops + ModeCurrentDesktop, // Shows windows on current desktop + ModeSelectedDesktop, // Shows windows of selected desktop via property (m_desktop) + ModeWindowGroup, // Shows windows selected via property + ModeWindowClass // Shows all windows of same class as selected class + }; + enum WindowMouseAction { + WindowNoAction = 0, // Nothing + WindowActivateAction = 1, // Activates the window and deactivates the effect + WindowExitAction = 2, // Deactivates the effect without activating new window + WindowToCurrentDesktopAction = 3, // Brings window to current desktop + WindowToAllDesktopsAction = 4, // Brings window to all desktops + WindowMinimizeAction = 5, // Minimizes the window + WindowCloseAction = 6 // Closes the window + }; + enum DesktopMouseAction { + DesktopNoAction = 0, // nothing + DesktopActivateAction = 1, // Activates the window and deactivates the effect + DesktopExitAction = 2, // Deactivates the effect without activating new window + DesktopShowDesktopAction = 3 // Minimizes all windows + }; + + // for properties + int layoutMode() const { + return m_layoutMode; + } + bool isShowCaptions() const { + return m_showCaptions; + } + bool isShowIcons() const { + return m_showIcons; + } + bool isDoNotCloseWindows() const { + return m_doNotCloseWindows; + } + bool isIgnoreMinimized() const { + return m_ignoreMinimized; + } + int accuracy() const { + return m_accuracy; + } + bool isFillGaps() const { + return m_fillGaps; + } + int fadeDuration() const { + return m_fadeDuration; + } + bool isShowPanel() const { + return m_showPanel; + } + int leftButtonWindow() const { + return m_leftButtonWindow; + } + int rightButtonWindow() const { + return m_rightButtonWindow; + } + int middleButtonWindow() const { + return m_middleButtonWindow; + } + int leftButtonDesktop() const { + return m_leftButtonDesktop; + } + int middleButtonDesktop() const { + return m_middleButtonDesktop; + } + int rightButtonDesktop() const { + return m_rightButtonDesktop; + } +public Q_SLOTS: + void setActive(bool active); + void toggleActive() { + m_mode = ModeCurrentDesktop; + setActive(!m_activated); + } + void toggleActiveAllDesktops() { + m_mode = ModeAllDesktops; + setActive(!m_activated); + } + void toggleActiveClass(); + + // slots for global shortcut changed + // needed to toggle the effect + void globalShortcutChanged(QAction *action, const QKeySequence &seq); + // EffectsHandler + void slotWindowAdded(KWin::EffectWindow *w); + void slotWindowClosed(KWin::EffectWindow *w); + void slotWindowDeleted(KWin::EffectWindow *w); + void slotWindowFrameGeometryChanged(KWin::EffectWindow *w, const QRect &old); + // atoms + void slotPropertyNotify(KWin::EffectWindow* w, long atom); + +private Q_SLOTS: + void closeWindow(); + +protected: + // Window rearranging + void rearrangeWindows(); + void reCreateGrids(); + void calculateWindowTransformations(EffectWindowList windowlist, int screen, + WindowMotionManager& motionManager, bool external = false); + void calculateWindowTransformationsClosest(EffectWindowList windowlist, int screen, + WindowMotionManager& motionManager); + void calculateWindowTransformationsKompose(EffectWindowList windowlist, int screen, + WindowMotionManager& motionManager); + void calculateWindowTransformationsNatural(EffectWindowList windowlist, int screen, + WindowMotionManager& motionManager); + + // Helper functions for window rearranging + inline double aspectRatio(EffectWindow *w) { + return w->width() / double(w->height()); + } + inline int widthForHeight(EffectWindow *w, int height) { + return int((height / double(w->height())) * w->width()); + } + inline int heightForWidth(EffectWindow *w, int width) { + return int((width / double(w->width())) * w->height()); + } + bool isOverlappingAny(EffectWindow *w, const QHash &targets, const QRegion &border); + + // Filter box + void updateFilterFrame(); + + // Helper functions + bool isSelectableWindow(EffectWindow *w); + bool isVisibleWindow(EffectWindow *w); + void setHighlightedWindow(EffectWindow *w); + EffectWindow* relativeWindow(EffectWindow *w, int xdiff, int ydiff, bool wrap) const; + EffectWindow* findFirstWindow() const; + void updateCloseWindow(); + + // Helper functions for mouse actions + void mouseActionWindow(WindowMouseAction& action); + void mouseActionDesktop(DesktopMouseAction& action); + void inputEventUpdate(const QPoint &pos, QEvent::Type type = QEvent::None, Qt::MouseButton button = Qt::NoButton); + +private: + PresentWindowsEffectProxy m_proxy; + friend class PresentWindowsEffectProxy; + + // User configuration settings + QList m_borderActivate; + QList m_borderActivateAll; + QList m_borderActivateClass; + int m_layoutMode; + bool m_showCaptions; + bool m_showIcons; + bool m_doNotCloseWindows; + int m_accuracy; + bool m_fillGaps; + double m_fadeDuration; + bool m_showPanel; + + // Activation + bool m_activated; + bool m_ignoreMinimized; + double m_decalOpacity; + bool m_hasKeyboardGrab; + PresentWindowsMode m_mode; + int m_desktop; + EffectWindowList m_selectedWindows; + EffectWindow *m_managerWindow; + QString m_class; + bool m_needInitialSelection; + + // Window data + WindowMotionManager m_motionManager; + DataHash m_windowData; + EffectWindow *m_highlightedWindow; + + // Grid layout info + QList m_gridSizes; + + // Filter box + EffectFrame* m_filterFrame; + QString m_windowFilter; + + // Shortcut - needed to toggle the effect + QList shortcut; + QList shortcutAll; + QList shortcutClass; + + // Atoms + // Present windows for all windows of given desktop + // -1 for all desktops + long m_atomDesktop; + // Present windows for group of window ids + long m_atomWindows; + + // Mouse Actions + WindowMouseAction m_leftButtonWindow; + WindowMouseAction m_middleButtonWindow; + WindowMouseAction m_rightButtonWindow; + DesktopMouseAction m_leftButtonDesktop; + DesktopMouseAction m_middleButtonDesktop; + DesktopMouseAction m_rightButtonDesktop; + + CloseWindowView* m_closeView; + Qt::Corner m_closeButtonCorner; + struct { + qint32 id = 0; + bool active = false; + } m_touch; + + QAction *m_exposeAction; + QAction *m_exposeAllAction; + QAction *m_exposeClassAction; +}; + +} // namespace + +#endif diff --git a/effects/presentwindows/presentwindows.kcfg b/effects/presentwindows/presentwindows.kcfg new file mode 100644 index 0000000..e8bfc70 --- /dev/null +++ b/effects/presentwindows/presentwindows.kcfg @@ -0,0 +1,59 @@ + + + + + + 0 + + + true + + + true + + + true + + + false + + + false + + + 1 + + + true + + + 1 + + + 0 + + + 2 + + + 2 + + + 0 + + + 0 + + + + QList<int>() << int(ElectricTopLeft) + + + + + + + diff --git a/effects/presentwindows/presentwindows_config.cpp b/effects/presentwindows/presentwindows_config.cpp new file mode 100644 index 0000000..9560264 --- /dev/null +++ b/effects/presentwindows/presentwindows_config.cpp @@ -0,0 +1,107 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Rivo Laks + SPDX-FileCopyrightText: 2008 Lucas Murray + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "presentwindows_config.h" +// KConfigSkeleton +#include "presentwindowsconfig.h" +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +K_PLUGIN_FACTORY_WITH_JSON(PresentWindowsEffectConfigFactory, + "presentwindows_config.json", + registerPlugin();) + +namespace KWin +{ + +PresentWindowsEffectConfigForm::PresentWindowsEffectConfigForm(QWidget* parent) : QWidget(parent) +{ + setupUi(this); +} + +PresentWindowsEffectConfig::PresentWindowsEffectConfig(QWidget* parent, const QVariantList& args) + : KCModule(KAboutData::pluginData(QStringLiteral("presentwindows")), parent, args) +{ + m_ui = new PresentWindowsEffectConfigForm(this); + + QVBoxLayout* layout = new QVBoxLayout(this); + + layout->addWidget(m_ui); + + // Shortcut config. The shortcut belongs to the component "kwin"! + m_actionCollection = new KActionCollection(this, QStringLiteral("kwin")); + + m_actionCollection->setComponentDisplayName(i18n("KWin")); + m_actionCollection->setConfigGroup(QStringLiteral("PresentWindows")); + m_actionCollection->setConfigGlobal(true); + + QAction* a = m_actionCollection->addAction(QStringLiteral("ExposeAll")); + a->setText(i18n("Toggle Present Windows (All desktops)")); + a->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::CTRL + Qt::Key_F10 << Qt::Key_LaunchC); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::CTRL + Qt::Key_F10 << Qt::Key_LaunchC); + + QAction* b = m_actionCollection->addAction(QStringLiteral("Expose")); + b->setText(i18n("Toggle Present Windows (Current desktop)")); + b->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(b, QList() << Qt::CTRL + Qt::Key_F9); + KGlobalAccel::self()->setShortcut(b, QList() << Qt::CTRL + Qt::Key_F9); + + QAction* c = m_actionCollection->addAction(QStringLiteral("ExposeClass")); + c->setText(i18n("Toggle Present Windows (Window class)")); + c->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(c, QList() << Qt::CTRL + Qt::Key_F7); + KGlobalAccel::self()->setShortcut(c, QList() << Qt::CTRL + Qt::Key_F7); + + m_ui->shortcutEditor->addCollection(m_actionCollection); + + connect(m_ui->shortcutEditor, &KShortcutsEditor::keyChange, this, &PresentWindowsEffectConfig::markAsChanged); + + PresentWindowsConfig::instance(KWIN_CONFIG); + addConfig(PresentWindowsConfig::self(), m_ui); + + load(); +} + +PresentWindowsEffectConfig::~PresentWindowsEffectConfig() +{ + // If save() is called undoChanges() has no effect + m_ui->shortcutEditor->undoChanges(); +} + +void PresentWindowsEffectConfig::save() +{ + KCModule::save(); + m_ui->shortcutEditor->save(); + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("presentwindows")); +} + +void PresentWindowsEffectConfig::defaults() +{ + m_ui->shortcutEditor->allDefault(); + KCModule::defaults(); +} + +} // namespace + +#include "presentwindows_config.moc" diff --git a/effects/presentwindows/presentwindows_config.desktop b/effects/presentwindows/presentwindows_config.desktop new file mode 100644 index 0000000..a332492 --- /dev/null +++ b/effects/presentwindows/presentwindows_config.desktop @@ -0,0 +1,86 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=KCModule + +X-KDE-Library=kwin_presentwindows_config +X-KDE-ParentComponents=presentwindows + +Name=Present Windows +Name[af]=Wys vensters +Name[ar]=النوافذ الحاضرة +Name[ast]=Presentación de ventanes +Name[az]=Bütün Pəncərələr +Name[be]=Існуючыя вокны +Name[be@latin]=NajaÅ­nyja vokny +Name[bg]=Представяне на прозорци +Name[bs]=Predstava prozora +Name[ca]=Presenta les finestres +Name[ca@valencia]=Presenta les finestres +Name[cs]=Prezentace oken +Name[csb]=Prezentëjë òkna +Name[da]=Præsentér vinduer +Name[de]=Fenster zeigen +Name[el]=Παρουσίαση παραθύρων +Name[en_GB]=Present Windows +Name[eo]=Prezenti fenestrojn +Name[es]=Presenta las ventanas +Name[et]=Olemasolevad aknad +Name[eu]=Aurkeztu leihoak +Name[fa]=پنجره‌های موجود +Name[fi]=Ikkunoiden esittäminen +Name[fr]=Présentation des fenêtres +Name[fy]=Hjoeddeiske finsters +Name[ga]=Fuinneoga Gníomhacha +Name[gl]=Dispor as xanelas +Name[gu]=હાજર રહેલ વિન્ડોસ +Name[he]=הצגת חלונות +Name[hi]=मौजूद विंडो +Name[hne]=मौजूद विंडो +Name[hr]=Prikaz prozora +Name[hu]=Ablakáttekintő +Name[ia]=Fenestras actual +Name[id]=Window Hadir +Name[is]=Núverandi gluggar +Name[it]=Presenta le finestre +Name[ja]=ウィンドウを並べて表示 +Name[kk]=Терезелерді көрсету +Name[km]=បង្ហាញ​បង្អួច​ +Name[kn]=ಪ್ರಸಕ್ತ ಕಿಟಕಿಗಳು +Name[ko]=ì°½ 진열하기 +Name[lt]=Langų pateikimas +Name[lv]=ParādÄ«t logus +Name[mai]=मोजुद विंडो +Name[mk]=Презентација на прозорци +Name[ml]=നിലവിലുള്ള ജാലകങ്ങള്‍ +Name[mr]=वर्तमान चौकटी +Name[nb]=Vis vinduer +Name[nds]=Vörhannen Finstern +Name[ne]=हालको सञ्झ्याल +Name[nl]=Vensters presenteren +Name[nn]=Presenter vindauge +Name[pa]=ਮੌਜੂਦਾ ਵਿੰਡੋਜ਼ +Name[pl]=Prezentacja okien +Name[pt]=Apresentar as Janelas +Name[pt_BR]=Apresentar janelas +Name[ro]=Prezintă ferestrele +Name[ru]=Все окна +Name[se]=Presentere lásiid +Name[si]=පවතින කවුළුව +Name[sk]=Súčasné okná +Name[sl]=Predstavitev oken +Name[sr]=Представа прозора +Name[sr@ijekavian]=Представа прозора +Name[sr@ijekavianlatin]=Predstava prozora +Name[sr@latin]=Predstava prozora +Name[sv]=Befintliga fönster +Name[ta]=தற்போதைய சாளரம் +Name[te]=ప్రస్తుత విండోలు +Name[th]=แสดงหน้าต่างทั้งหมด +Name[tr]=Şimdiki Pencereler +Name[ug]=كۆزنەكلەرنى كۆرسەت +Name[uk]=Показ вікон +Name[vi]=Sắp xếp cá»­a sổ +Name[wa]=Prezinter finiesses +Name[x-test]=xxPresent Windowsxx +Name[zh_CN]=展现窗口 +Name[zh_TW]=展示視窗 diff --git a/effects/presentwindows/presentwindows_config.h b/effects/presentwindows/presentwindows_config.h new file mode 100644 index 0000000..d2b9b47 --- /dev/null +++ b/effects/presentwindows/presentwindows_config.h @@ -0,0 +1,46 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Rivo Laks + SPDX-FileCopyrightText: 2008 Lucas Murray + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_PRESENTWINDOWS_CONFIG_H +#define KWIN_PRESENTWINDOWS_CONFIG_H + +#include + +#include "ui_presentwindows_config.h" + +namespace KWin +{ + +class PresentWindowsEffectConfigForm : public QWidget, public Ui::PresentWindowsEffectConfigForm +{ + Q_OBJECT +public: + explicit PresentWindowsEffectConfigForm(QWidget* parent); +}; + +class PresentWindowsEffectConfig : public KCModule +{ + Q_OBJECT +public: + explicit PresentWindowsEffectConfig(QWidget* parent = nullptr, const QVariantList& args = QVariantList()); + ~PresentWindowsEffectConfig() override; + +public Q_SLOTS: + void save() override; + void defaults() override; + +private: + PresentWindowsEffectConfigForm* m_ui; + KActionCollection* m_actionCollection; +}; + +} // namespace + +#endif diff --git a/effects/presentwindows/presentwindows_config.ui b/effects/presentwindows/presentwindows_config.ui new file mode 100644 index 0000000..acd8730 --- /dev/null +++ b/effects/presentwindows/presentwindows_config.ui @@ -0,0 +1,492 @@ + + + KWin::PresentWindowsEffectConfigForm + + + + 0 + 0 + 595 + 441 + + + + + + + Activation + + + + + + + 0 + 0 + + + + KShortcutsEditor::GlobalAction + + + + + + + + + + Natural Layout Settings + + + + + + Fill &gaps + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Faster + + + + + + + + 130 + 0 + + + + 1 + + + 8 + + + 1 + + + 1 + + + 1 + + + true + + + Qt::Horizontal + + + true + + + false + + + QSlider::TicksBelow + + + + + + + Nicer + + + + + + + + + + Windows + + + + + + Left button: + + + kcfg_LeftButtonWindow + + + + + + + + No action + + + + + Activate window + + + + + End effect + + + + + Bring window to current desktop + + + + + Send window to all desktops + + + + + (Un-)Minimize window + + + + + + + + Middle button: + + + kcfg_MiddleButtonWindow + + + + + + + + No action + + + + + Activate window + + + + + End effect + + + + + Bring window to current desktop + + + + + Send window to all desktops + + + + + (Un-)Minimize window + + + + + Close window + + + + + + + + Right button: + + + kcfg_RightButtonWindow + + + + + + + + No action + + + + + Activate window + + + + + End effect + + + + + Bring window to current desktop + + + + + Send window to all desktops + + + + + (Un-)Minimize window + + + + + Close window + + + + + + + + + + + Desktop + + + + QFormLayout::ExpandingFieldsGrow + + + + + Left button: + + + kcfg_LeftButtonDesktop + + + + + + + + No action + + + + + Activate window + + + + + End effect + + + + + Show desktop + + + + + + + + Middle button: + + + kcfg_MiddleButtonDesktop + + + + + + + + No action + + + + + Activate window + + + + + End effect + + + + + Show desktop + + + + + + + + Right button: + + + kcfg_RightButtonDesktop + + + + + + + + No action + + + + + Activate window + + + + + End effect + + + + + Show desktop + + + + + + + + + + + Appearance + + + + + + Layout mode: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_LayoutMode + + + + + + + Display window &titles + + + + + + + Display window &icons + + + + + + + Ignore &minimized windows + + + + + + + Show &panels + + + + + + + + 0 + 0 + + + + + Natural + + + + + Regular Grid + + + + + Flexible Grid + + + + + + + + Provide buttons to close the windows + + + + + + + + + + + KComboBox + QComboBox +
kcombobox.h
+
+ + KShortcutsEditor + QWidget +
KShortcutsEditor
+ 1 +
+
+ + kcfg_LayoutMode + kcfg_DrawWindowCaptions + kcfg_DrawWindowIcons + kcfg_AllowClosingWindows + kcfg_IgnoreMinimized + kcfg_Accuracy + kcfg_FillGaps + + + +
diff --git a/effects/presentwindows/presentwindows_proxy.cpp b/effects/presentwindows/presentwindows_proxy.cpp new file mode 100644 index 0000000..9b978ec --- /dev/null +++ b/effects/presentwindows/presentwindows_proxy.cpp @@ -0,0 +1,36 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Lucas Murray + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "presentwindows_proxy.h" +#include "presentwindows.h" + +namespace KWin +{ + +PresentWindowsEffectProxy::PresentWindowsEffectProxy(PresentWindowsEffect* effect) + : m_effect(effect) +{ +} + +PresentWindowsEffectProxy::~PresentWindowsEffectProxy() +{ +} + +void PresentWindowsEffectProxy::calculateWindowTransformations(EffectWindowList windows, int screen, + WindowMotionManager& manager) +{ + return m_effect->calculateWindowTransformations(windows, screen, manager, true); +} + +void PresentWindowsEffectProxy::reCreateGrids() +{ + m_effect->reCreateGrids(); +} + +} // namespace diff --git a/effects/presentwindows/presentwindows_proxy.h b/effects/presentwindows/presentwindows_proxy.h new file mode 100644 index 0000000..f48f7eb --- /dev/null +++ b/effects/presentwindows/presentwindows_proxy.h @@ -0,0 +1,35 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Lucas Murray + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_PRESENTWINDOWS_PROXY_H +#define KWIN_PRESENTWINDOWS_PROXY_H +#include + +namespace KWin +{ + +class PresentWindowsEffect; + +class PresentWindowsEffectProxy +{ +public: + explicit PresentWindowsEffectProxy(PresentWindowsEffect* effect); + ~PresentWindowsEffectProxy(); + + void calculateWindowTransformations(EffectWindowList windows, int screen, WindowMotionManager& manager); + + void reCreateGrids(); + +private: + PresentWindowsEffect* m_effect; +}; + +} // namespace + +#endif diff --git a/effects/presentwindows/presentwindowsconfig.kcfgc b/effects/presentwindows/presentwindowsconfig.kcfgc new file mode 100644 index 0000000..3e92ddf --- /dev/null +++ b/effects/presentwindows/presentwindowsconfig.kcfgc @@ -0,0 +1,6 @@ +File=presentwindows.kcfg +ClassName=PresentWindowsConfig +NameSpace=KWin +Singleton=true +Mutators=true +IncludeFiles=kwinglobals.h diff --git a/effects/resize/CMakeLists.txt b/effects/resize/CMakeLists.txt new file mode 100644 index 0000000..5b43935 --- /dev/null +++ b/effects/resize/CMakeLists.txt @@ -0,0 +1,23 @@ +####################################### +# Config +set(kwin_resize_config_SRCS resize_config.cpp) +ki18n_wrap_ui(kwin_resize_config_SRCS resize_config.ui) +kconfig_add_kcfg_files(kwin_resize_config_SRCS resizeconfig.kcfgc) + +add_library(kwin_resize_config MODULE ${kwin_resize_config_SRCS}) + +target_link_libraries(kwin_resize_config + KF5::ConfigWidgets + KF5::I18n + Qt5::DBus + KWinEffectsInterface +) + +kcoreaddons_desktop_to_json(kwin_resize_config resize_config.desktop SERVICE_TYPES kcmodule.desktop) + +install( + TARGETS + kwin_resize_config + DESTINATION + ${PLUGIN_INSTALL_DIR}/kwin/effects/configs +) diff --git a/effects/resize/resize.cpp b/effects/resize/resize.cpp new file mode 100644 index 0000000..b3e32d2 --- /dev/null +++ b/effects/resize/resize.cpp @@ -0,0 +1,166 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "resize.h" +// KConfigSkeleton +#include "resizeconfig.h" + +#include +#ifdef KWIN_HAVE_XRENDER_COMPOSITING +#include "kwinxrenderutils.h" +#endif + +#include + +#include +#include + +namespace KWin +{ + +ResizeEffect::ResizeEffect() + : AnimationEffect() + , m_active(false) + , m_resizeWindow(nullptr) +{ + initConfig(); + reconfigure(ReconfigureAll); + connect(effects, &EffectsHandler::windowStartUserMovedResized, this, &ResizeEffect::slotWindowStartUserMovedResized); + connect(effects, &EffectsHandler::windowStepUserMovedResized, this, &ResizeEffect::slotWindowStepUserMovedResized); + connect(effects, &EffectsHandler::windowFinishUserMovedResized, this, &ResizeEffect::slotWindowFinishUserMovedResized); +} + +ResizeEffect::~ResizeEffect() +{ +} + +void ResizeEffect::prePaintScreen(ScreenPrePaintData& data, int time) +{ + if (m_active) { + data.mask |= PAINT_SCREEN_WITH_TRANSFORMED_WINDOWS; + } + AnimationEffect::prePaintScreen(data, time); +} + +void ResizeEffect::prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time) +{ + if (m_active && w == m_resizeWindow) + data.mask |= PAINT_WINDOW_TRANSFORMED; + AnimationEffect::prePaintWindow(w, data, time); +} + +void ResizeEffect::paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data) +{ + if (m_active && w == m_resizeWindow) { + if (m_features & TextureScale) { + data += (m_currentGeometry.topLeft() - m_originalGeometry.topLeft()); + data *= QVector2D(float(m_currentGeometry.width())/m_originalGeometry.width(), + float(m_currentGeometry.height())/m_originalGeometry.height()); + } + effects->paintWindow(w, mask, region, data); + + if (m_features & Outline) { + QRegion intersection = m_originalGeometry.intersected(m_currentGeometry); + QRegion paintRegion = QRegion(m_originalGeometry).united(m_currentGeometry).subtracted(intersection); + float alpha = 0.8f; + QColor color = KColorScheme(QPalette::Normal, KColorScheme::Selection).background().color(); + + if (effects->isOpenGLCompositing()) { + GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer(); + vbo->reset(); + vbo->setUseColor(true); + ShaderBinder binder(ShaderTrait::UniformColor); + binder.shader()->setUniform(GLShader::ModelViewProjectionMatrix, data.screenProjectionMatrix()); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + color.setAlphaF(alpha); + vbo->setColor(color); + QVector verts; + verts.reserve(paintRegion.rectCount() * 12); + for (const QRect &r : paintRegion) { + verts << r.x() + r.width() << r.y(); + verts << r.x() << r.y(); + verts << r.x() << r.y() + r.height(); + verts << r.x() << r.y() + r.height(); + verts << r.x() + r.width() << r.y() + r.height(); + verts << r.x() + r.width() << r.y(); + } + vbo->setData(verts.count() / 2, 2, verts.data(), nullptr); + vbo->render(GL_TRIANGLES); + glDisable(GL_BLEND); + } + +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + if (effects->compositingType() == XRenderCompositing) { + QVector rects; + for (const QRect &r : paintRegion) { + xcb_rectangle_t rect = {int16_t(r.x()), int16_t(r.y()), uint16_t(r.width()), uint16_t(r.height())}; + rects << rect; + } + xcb_render_fill_rectangles(xcbConnection(), XCB_RENDER_PICT_OP_OVER, + effects->xrenderBufferPicture(), preMultiply(color, alpha), + rects.count(), rects.constData()); + } +#endif + if (effects->compositingType() == QPainterCompositing) { + QPainter *painter = effects->scenePainter(); + painter->save(); + color.setAlphaF(alpha); + for (const QRect &r : paintRegion) { + painter->fillRect(r, color); + } + painter->restore(); + } + } + } else { + AnimationEffect::paintWindow(w, mask, region, data); + } +} + +void ResizeEffect::reconfigure(ReconfigureFlags) +{ + m_features = 0; + ResizeConfig::self()->read(); + if (ResizeConfig::textureScale()) + m_features |= TextureScale; + if (ResizeConfig::outline()) + m_features |= Outline; +} + +void ResizeEffect::slotWindowStartUserMovedResized(EffectWindow *w) +{ + if (w->isUserResize() && !w->isUserMove()) { + m_active = true; + m_resizeWindow = w; + m_originalGeometry = w->geometry(); + m_currentGeometry = w->geometry(); + w->addRepaintFull(); + } +} + +void ResizeEffect::slotWindowFinishUserMovedResized(EffectWindow *w) +{ + if (m_active && w == m_resizeWindow) { + m_active = false; + m_resizeWindow = nullptr; + if (m_features & TextureScale) + animate(w, CrossFadePrevious, 0, 150, FPx2(1.0)); + effects->addRepaintFull(); + } +} + +void ResizeEffect::slotWindowStepUserMovedResized(EffectWindow *w, const QRect &geometry) +{ + if (m_active && w == m_resizeWindow) { + m_currentGeometry = geometry; + effects->addRepaintFull(); + } +} + +} // namespace diff --git a/effects/resize/resize.h b/effects/resize/resize.h new file mode 100644 index 0000000..a0150ee --- /dev/null +++ b/effects/resize/resize.h @@ -0,0 +1,62 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_RESIZE_H +#define KWIN_RESIZE_H + +#include + +namespace KWin +{ + +class ResizeEffect + : public AnimationEffect +{ + Q_OBJECT + Q_PROPERTY(bool textureScale READ isTextureScale) + Q_PROPERTY(bool outline READ isOutline) +public: + ResizeEffect(); + ~ResizeEffect() override; + inline bool provides(Effect::Feature ef) override { + return ef == Effect::Resize; + } + inline bool isActive() const override { return m_active || AnimationEffect::isActive(); } + void prePaintScreen(ScreenPrePaintData& data, int time) override; + void prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time) override; + void paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data) override; + void reconfigure(ReconfigureFlags) override; + + int requestedEffectChainPosition() const override { + return 60; + } + + bool isTextureScale() const { + return m_features & TextureScale; + } + bool isOutline() const { + return m_features & Outline; + } + +public Q_SLOTS: + void slotWindowStartUserMovedResized(KWin::EffectWindow *w); + void slotWindowStepUserMovedResized(KWin::EffectWindow *w, const QRect &geometry); + void slotWindowFinishUserMovedResized(KWin::EffectWindow *w); + +private: + enum Feature { TextureScale = 1 << 0, Outline = 1 << 1 }; + bool m_active; + int m_features; + EffectWindow* m_resizeWindow; + QRect m_currentGeometry, m_originalGeometry; +}; + +} + +#endif diff --git a/effects/resize/resize.kcfg b/effects/resize/resize.kcfg new file mode 100644 index 0000000..a02a166 --- /dev/null +++ b/effects/resize/resize.kcfg @@ -0,0 +1,15 @@ + + + + + + true + + + false + + + diff --git a/effects/resize/resize_config.cpp b/effects/resize/resize_config.cpp new file mode 100644 index 0000000..075b700 --- /dev/null +++ b/effects/resize/resize_config.cpp @@ -0,0 +1,59 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2010 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "resize_config.h" +// KConfigSkeleton +#include "resizeconfig.h" +#include +#include + +#include +#include +#include + +#include + +K_PLUGIN_FACTORY_WITH_JSON(ResizeEffectConfigFactory, + "resize_config.json", + registerPlugin();) + +namespace KWin +{ + +ResizeEffectConfigForm::ResizeEffectConfigForm(QWidget* parent) : QWidget(parent) +{ + setupUi(this); +} + +ResizeEffectConfig::ResizeEffectConfig(QWidget* parent, const QVariantList& args) : + KCModule(KAboutData::pluginData(QStringLiteral("resize")), parent, args) +{ + m_ui = new ResizeEffectConfigForm(this); + + QVBoxLayout* layout = new QVBoxLayout(this); + + layout->addWidget(m_ui); + + ResizeConfig::instance(KWIN_CONFIG); + addConfig(ResizeConfig::self(), m_ui); + + load(); +} + +void ResizeEffectConfig::save() +{ + KCModule::save(); + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("resize")); +} + +} // namespace + +#include "resize_config.moc" diff --git a/effects/resize/resize_config.desktop b/effects/resize/resize_config.desktop new file mode 100644 index 0000000..c878f9f --- /dev/null +++ b/effects/resize/resize_config.desktop @@ -0,0 +1,76 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=KCModule + +X-KDE-Library=kwin_resize_config +X-KDE-ParentComponents=resize + +Name=Resize Window +Name[ar]=غير حجم النافذة +Name[ast]=Redimensión de ventanes +Name[az]=Pəncərənin Ölçüsünü Dəyişmək +Name[bg]=Преоразмеряване на прозорци +Name[bs]=Preuveličanje prozora +Name[ca]=Redimensionament de les finestres +Name[ca@valencia]=Redimensionament de les finestres +Name[cs]=Změnit velikost okna +Name[csb]=Zmieni miarã òkna +Name[da]=Ændr størrelse pÃ¥ vindue +Name[de]=Fenstergröße ändern +Name[el]=Αλλαγή μεγέθους παραθύρου +Name[en_GB]=Resize Window +Name[eo]=Regrandigi fenestrojn +Name[es]=Redimensionar ventana +Name[et]=Akna suuruse muutmine +Name[eu]=Tamainaz aldatu leihoa +Name[fa]=تغییر اندازۀ پنجره +Name[fi]=Ikkunan koon muuttaminen +Name[fr]=Redimensionner la fenêtre +Name[fy]=Finstergrutte feroarje +Name[ga]=Athraigh Méid na Fuinneoige +Name[gl]=Cambiar o tamaño da xanela +Name[gu]=વિન્ડોનું માપ બદલો +Name[he]=שינוי גודל חלון +Name[hi]=विंडो आकार बदलें +Name[hr]=Promjena veličine prozora +Name[hu]=Ablak átméretezése +Name[ia]=Redimensiona fenestra +Name[id]=Ubah-ukuran Window +Name[is]=Breyta stærð glugga +Name[it]=Ridimensiona la finestra +Name[ja]=ウィンドウのリサイズ +Name[kk]=Терезе өлшемін өзгерту +Name[km]=ផ្លាស់ប្ដូរ​ទំហំ​បង្អួច +Name[kn]=ಕಿಟಕಿಯ ಗಾತ್ರ ಬದಲಿಸು +Name[ko]=ì°½ 크기 조정 +Name[lt]=Keisti lango dydį +Name[lv]=MainÄ«t loga izmēru +Name[mk]=Големина на прозорец +Name[ml]=ജാലകത്തിന്റെ വലിപ്പം മാറ്റുക +Name[mr]=चौकट आकार बदल +Name[nb]=Endre størrelse pÃ¥ vinduet +Name[nds]=Finstergrött ännern +Name[nl]=Afmeting venster aanpassen +Name[nn]=Skaler vindauge +Name[pa]=ਵਿੰਡੋ ਮੁੜ-ਆਕਾਰ +Name[pl]=Zmiana rozmiaru okien +Name[pt]=Dimensionar a Janela +Name[pt_BR]=Redimensionar janela +Name[ro]=Redimensionează fereastra +Name[ru]=Изменение размера окна +Name[si]=කවුළුව ප්‍රතිප්‍රමාණ කරන්න +Name[sk]=ZmeniÅ¥ veľkosÅ¥ okna +Name[sl]=Spreminjanje velikosti okna +Name[sr]=Преувеличање прозора +Name[sr@ijekavian]=Преувеличање прозора +Name[sr@ijekavianlatin]=Preuveličanje prozora +Name[sr@latin]=Preuveličanje prozora +Name[sv]=Ändra fönsterstorlek +Name[th]=ปรับขนาดหน้าต่าง +Name[tr]=Pencereyi Yeniden Boyutlandır +Name[ug]=كۆزنەك چوڭلۇقىنى ئۆزگەرت +Name[uk]=Зміна розмірів вікон +Name[wa]=Candjî l' grandeu del finiesse +Name[x-test]=xxResize Windowxx +Name[zh_CN]=更改窗口大小 +Name[zh_TW]=重新調整視窗大小 diff --git a/effects/resize/resize_config.h b/effects/resize/resize_config.h new file mode 100644 index 0000000..c5286bc --- /dev/null +++ b/effects/resize/resize_config.h @@ -0,0 +1,43 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2010 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_RESIZE_CONFIG_H +#define KWIN_RESIZE_CONFIG_H + +#include + +#include "ui_resize_config.h" + + +namespace KWin +{ + +class ResizeEffectConfigForm : public QWidget, public Ui::ResizeEffectConfigForm +{ + Q_OBJECT +public: + explicit ResizeEffectConfigForm(QWidget* parent = nullptr); +}; + +class ResizeEffectConfig : public KCModule +{ + Q_OBJECT +public: + explicit ResizeEffectConfig(QWidget* parent = nullptr, const QVariantList& args = QVariantList()); + +public Q_SLOTS: + void save() override; + +private: + ResizeEffectConfigForm* m_ui; +}; + +} // namespace + +#endif diff --git a/effects/resize/resize_config.ui b/effects/resize/resize_config.ui new file mode 100644 index 0000000..0dcc62b --- /dev/null +++ b/effects/resize/resize_config.ui @@ -0,0 +1,45 @@ + + + KWin::ResizeEffectConfigForm + + + + 0 + 0 + 400 + 300 + + + + + + + Scale window + + + + + + + Show outline + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/effects/resize/resizeconfig.kcfgc b/effects/resize/resizeconfig.kcfgc new file mode 100644 index 0000000..d6ade9d --- /dev/null +++ b/effects/resize/resizeconfig.kcfgc @@ -0,0 +1,5 @@ +File=resize.kcfg +ClassName=ResizeConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/effects/scale/package/contents/code/main.js b/effects/scale/package/contents/code/main.js new file mode 100644 index 0000000..f088f8c --- /dev/null +++ b/effects/scale/package/contents/code/main.js @@ -0,0 +1,169 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +"use strict"; + +var blacklist = [ + // The logout screen has to be animated only by the logout effect. + "ksmserver ksmserver", + "ksmserver-logout-greeter ksmserver-logout-greeter", + + // KDE Plasma splash screen has to be animated only by the login effect. + "ksplashqml ksplashqml", + "ksplashsimple ksplashsimple", + "ksplashx ksplashx" +]; + +var scaleEffect = { + loadConfig: function (window) { + var defaultDuration = 160; + var duration = effect.readConfig("Duration", defaultDuration) || defaultDuration; + scaleEffect.duration = animationTime(duration); + scaleEffect.inScale = effect.readConfig("InScale", 0.96); + scaleEffect.inOpacity = effect.readConfig("InOpacity", 0.4); + scaleEffect.outScale = effect.readConfig("OutScale", 0.96); + scaleEffect.outOpacity = effect.readConfig("OutOpacity", 0.0); + }, + isScaleWindow: function (window) { + // We don't want to animate most of plasmashell's windows, yet, some + // of them we want to, for example, Task Manager Settings window. + // The problem is that all those window share single window class. + // So, the only way to decide whether a window should be animated is + // to use a heuristic: if a window has decoration, then it's most + // likely a dialog or a settings window so we have to animate it. + if (window.windowClass == "plasmashell plasmashell" + || window.windowClass == "plasmashell org.kde.plasmashell") { + return window.hasDecoration; + } + + if (blacklist.indexOf(window.windowClass) != -1) { + return false; + } + + if (window.hasDecoration) { + return true; + } + + // Don't animate combobox popups, tooltips, popup menus, etc. + if (window.popupWindow) { + return false; + } + + // Dont't animate the outline because it looks very sick. + if (window.outline) { + return false; + } + + // Override-redirect windows are usually used for user interface + // concepts that are not expected to be animated by this effect. + if (window.x11Client && !window.managed) { + return false; + } + + return window.normalWindow || window.dialog; + }, + setupForcedRoles: function (window) { + window.setData(Effect.WindowForceBackgroundContrastRole, true); + window.setData(Effect.WindowForceBlurRole, true); + }, + cleanupForcedRoles: function (window) { + window.setData(Effect.WindowForceBackgroundContrastRole, null); + window.setData(Effect.WindowForceBlurRole, null); + }, + slotWindowAdded: function (window) { + if (effects.hasActiveFullScreenEffect) { + return; + } + if (!scaleEffect.isScaleWindow(window)) { + return; + } + if (!window.visible) { + return; + } + if (!effect.grab(window, Effect.WindowAddedGrabRole)) { + return; + } + scaleEffect.setupForcedRoles(window); + window.scaleInAnimation = animate({ + window: window, + curve: QEasingCurve.InOutSine, + duration: scaleEffect.duration, + animations: [ + { + type: Effect.Scale, + from: scaleEffect.inScale + }, + { + type: Effect.Opacity, + from: scaleEffect.inOpacity + } + ] + }); + }, + slotWindowClosed: function (window) { + if (effects.hasActiveFullScreenEffect) { + return; + } + if (!scaleEffect.isScaleWindow(window)) { + return; + } + if (!window.visible) { + return; + } + if (!effect.grab(window, Effect.WindowClosedGrabRole)) { + return; + } + if (window.scaleInAnimation) { + cancel(window.scaleInAnimation); + delete window.scaleInAnimation; + } + scaleEffect.setupForcedRoles(window); + window.scaleOutAnimation = animate({ + window: window, + curve: QEasingCurve.InOutSine, + duration: scaleEffect.duration, + animations: [ + { + type: Effect.Scale, + to: scaleEffect.outScale + }, + { + type: Effect.Opacity, + to: scaleEffect.outOpacity + } + ] + }); + }, + slotWindowDataChanged: function (window, role) { + if (role == Effect.WindowAddedGrabRole) { + if (window.scaleInAnimation && effect.isGrabbed(window, role)) { + cancel(window.scaleInAnimation); + delete window.scaleInAnimation; + scaleEffect.cleanupForcedRoles(window); + } + } else if (role == Effect.WindowClosedGrabRole) { + if (window.scaleOutAnimation && effect.isGrabbed(window, role)) { + cancel(window.scaleOutAnimation); + delete window.scaleOutAnimation; + scaleEffect.cleanupForcedRoles(window); + } + } + }, + init: function () { + scaleEffect.loadConfig(); + + effect.configChanged.connect(scaleEffect.loadConfig); + effect.animationEnded.connect(scaleEffect.cleanupForcedRoles); + effects.windowAdded.connect(scaleEffect.slotWindowAdded); + effects.windowClosed.connect(scaleEffect.slotWindowClosed); + effects.windowDataChanged.connect(scaleEffect.slotWindowDataChanged); + } +}; + +scaleEffect.init(); diff --git a/effects/scale/package/contents/config/main.xml b/effects/scale/package/contents/config/main.xml new file mode 100644 index 0000000..4f9152a --- /dev/null +++ b/effects/scale/package/contents/config/main.xml @@ -0,0 +1,28 @@ + + + + + + 0 + + + 0.96 + + + 0.4 + 0.0 + 1.0 + + + 0.96 + + + 0.0 + 0.0 + 1.0 + + + diff --git a/effects/scale/package/contents/ui/config.ui b/effects/scale/package/contents/ui/config.ui new file mode 100644 index 0000000..97b43b5 --- /dev/null +++ b/effects/scale/package/contents/ui/config.ui @@ -0,0 +1,93 @@ + + + ScaleEffectConfig + + + + 0 + 0 + 455 + 177 + + + + + + + Duration: + + + + + + + + 0 + 0 + + + + Default + + + milliseconds + + + 9999 + + + 5 + + + + + + + Window open scale: + + + + + + + Window close scale: + + + + + + + + 0 + 0 + + + + 9.990000000000000 + + + 0.050000000000000 + + + + + + + + 0 + 0 + + + + 9.990000000000000 + + + 0.050000000000000 + + + + + + + + diff --git a/effects/scale/package/metadata.desktop b/effects/scale/package/metadata.desktop new file mode 100644 index 0000000..74aba0f --- /dev/null +++ b/effects/scale/package/metadata.desktop @@ -0,0 +1,88 @@ +[Desktop Entry] +Name=Scale +Name[az]=Miqyas +Name[ca]=Escala +Name[ca@valencia]=Escala +Name[cs]=Měřítko +Name[da]=Skalér +Name[de]=Skalieren +Name[el]=Κλιμάκωση +Name[en_GB]=Scale +Name[es]=Escalar +Name[et]=Skaleerimine +Name[eu]=Eskalatu +Name[fi]=Skaalaa +Name[fr]=Échelle +Name[gl]=Cambiar as dimensións +Name[hu]=Nagyítás +Name[ia]=Scala +Name[id]=Skala +Name[it]=Scala +Name[ko]=크기 조정 +Name[lt]=Mastelio keitimas +Name[nl]=Schalen +Name[nn]=Skalering +Name[pl]=Skalowanie +Name[pt]=Escala +Name[pt_BR]=Escala +Name[ro]=Scalare +Name[ru]=Масштабирование +Name[sk]=Å kálovaÅ¥ +Name[sl]=Merilo +Name[sv]=Skala +Name[uk]=Масштабування +Name[x-test]=xxScalexx +Name[zh_CN]=比例 +Name[zh_TW]=縮放 +Icon=preferences-system-windows-effect-scale +Comment=Make windows smoothly scale in and out when they are shown or hidden +Comment[az]=Böyüdülərkən və ya kiçildilərkən pəncərələrin miqyasının rəvan şəkildə dəyişməsi +Comment[ca]=Fa que les finestres entrin o surtin volant quan es mostren o s'oculten +Comment[ca@valencia]=Fa que les finestres entrin o isquen volant quan es mostren o s'oculten +Comment[cs]=Nechá okna plynule zvětÅ¡it/zmenÅ¡it se, pokud jsou zobrazeny resp. skryty +Comment[da]=FÃ¥ vinduer til at skalere blidt ud og ind nÃ¥r de vises eller skjules +Comment[de]=Ändert die Fenstergröße langsam beim Ein- oder Ausblenden +Comment[en_GB]=Make windows smoothly scale in and out when they are shown or hidden +Comment[es]=Hace que las ventanas se agranden o se encojan suavemente al mostrarlas u ocultarlas +Comment[et]=Skaleerib aknaid sujuvalt, kui need peidetakse või nähtavale tuuakse +Comment[eu]=Leihoak emeki eskalatu barrura eta kanpora haiek erakutsi edo ezkutatzean +Comment[fi]=Ikkunat ilmestyvät näkyviin tai poistuvat näkyvistä hiljalleen +Comment[fr]=Échelonne les fenêtres lorsqu'elles sont affichées ou cachées +Comment[gl]=Facer que as xanelas crezan ou decrezan suavemente cando se mostran ou agochan +Comment[ia]=Face que fenestras pote dulcemente scalar intra e foras quando illos es monstrate o celate +Comment[id]=Buat window menskala besar atau kecil secara mulus ketika ia ditampilkan atau disembunyikan +Comment[it]=Ridimensiona le finestre dolcemente quando sono mostrate o nascoste +Comment[ko]=창이 보여지거나 감춰질 때 부드러운 크기 조정을 사용합니다 +Comment[lt]=Glotniai didinti ar mažinti langų mastelį, juos parodant ar paslepiant +Comment[nl]=Laat vensters vloeiend kleiner en groter schalen als ze worden getoond of verborgen +Comment[nn]=Skaler vindauge jamt inn og ut nÃ¥r dei vert viste eller gøymde +Comment[pl]=Okna gładko pomniejszają się przy otwieraniu i powiększają przy zamykaniu +Comment[pt]=Fazer com que as janelas apareçam/desapareçam suavemente quando aparecem ou ficam escondidas +Comment[pt_BR]=Faz com que as janelas aumentem ou reduzam o seu tamanho de forma suave ao serem exibidas ou ocultadas +Comment[ro]=Face ferestrele să se scaleze lin când sunt arătate sau ascunse +Comment[ru]=Плавное увеличение или уменьшение окон при их появлении и скрытии +Comment[sk]=Okná sa plynule objavia/zmiznú pri ich zobrazení alebo skrytí +Comment[sl]=Okna se pojavijo in izginejo postopoma kadar se prikažejo ali skrijejo +Comment[sv]=Gör att fönster mjukt skalas in eller ut när de visas eller döljs +Comment[uk]=Плавне масштабування вікон при появі або приховуванні +Comment[x-test]=xxMake windows smoothly scale in and out when they are shown or hiddenxx +Comment[zh_CN]=窗口显示或隐藏时平滑缩放 +Comment[zh_TW]=顯示或隱藏視窗時以平順的比例縮放方式呈現。 + +Type=Service +X-KDE-ServiceTypes=KWin/Effect,KCModule +X-KDE-PluginInfo-Author=Vlad Zahorodnii +X-KDE-PluginInfo-Email=vlad.zahorodnii@kde.org +X-KDE-PluginInfo-Name=kwin4_effect_scale +X-KDE-PluginInfo-Version=1 +X-KDE-PluginInfo-Category=Window Open/Close Animation +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-EnabledByDefault=false +X-KDE-Ordering=60 +X-Plasma-API=javascript +X-Plasma-MainScript=code/main.js +X-KDE-PluginKeyword=kwin4_effect_scale +X-KDE-Library=kcm_kwin4_genericscripted +X-KDE-ParentComponents=kwin4_effect_scale +X-KWin-Config-TranslationDomain=kwin_effects +X-KWin-Exclusive-Category=toplevel-open-close-animation diff --git a/effects/screenedge/CMakeLists.txt b/effects/screenedge/CMakeLists.txt new file mode 100644 index 0000000..3504971 --- /dev/null +++ b/effects/screenedge/CMakeLists.txt @@ -0,0 +1,7 @@ +####################################### +# Effect + +# Source files +set(kwin4_effect_builtins_sources ${kwin4_effect_builtins_sources} + screenedge/screenedgeeffect.cpp +) diff --git a/effects/screenedge/screenedgeeffect.cpp b/effects/screenedge/screenedgeeffect.cpp new file mode 100644 index 0000000..4058fa9 --- /dev/null +++ b/effects/screenedge/screenedgeeffect.cpp @@ -0,0 +1,362 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "screenedgeeffect.h" +// KWin +#include +#include +#include +// KDE +#include +// Qt +#include +#include +#include +// xcb +#ifdef KWIN_HAVE_XRENDER_COMPOSITING +#include +#endif + +namespace KWin { + +ScreenEdgeEffect::ScreenEdgeEffect() + : Effect() + , m_cleanupTimer(new QTimer(this)) +{ + connect(effects, &EffectsHandler::screenEdgeApproaching, this, &ScreenEdgeEffect::edgeApproaching); + m_cleanupTimer->setInterval(5000); + m_cleanupTimer->setSingleShot(true); + connect(m_cleanupTimer, &QTimer::timeout, this, &ScreenEdgeEffect::cleanup); + connect(effects, &EffectsHandler::screenLockingChanged, this, + [this] (bool locked) { + if (locked) { + cleanup(); + } + } + ); +} + +ScreenEdgeEffect::~ScreenEdgeEffect() +{ + cleanup(); +} + +void ScreenEdgeEffect::ensureGlowSvg() +{ + if (!m_glow) { + m_glow = new Plasma::Svg(this); + m_glow->setImagePath(QStringLiteral("widgets/glowbar")); + } +} + +void ScreenEdgeEffect::cleanup() +{ + for (QHash::iterator it = m_borders.begin(); + it != m_borders.end(); + ++it) { + effects->addRepaint((*it)->geometry); + } + qDeleteAll(m_borders); + m_borders.clear(); +} + +void ScreenEdgeEffect::prePaintScreen(ScreenPrePaintData &data, int time) +{ + effects->prePaintScreen(data, time); + for (QHash::iterator it = m_borders.begin(); + it != m_borders.end(); + ++it) { + if ((*it)->strength == 0.0) { + continue; + } + data.paint += (*it)->geometry; + } +} + +void ScreenEdgeEffect::paintScreen(int mask, const QRegion ®ion, ScreenPaintData &data) +{ + effects->paintScreen(mask, region, data); + for (QHash::iterator it = m_borders.begin(); + it != m_borders.end(); + ++it) { + const qreal opacity = (*it)->strength; + if (opacity == 0.0) { + continue; + } + if (effects->isOpenGLCompositing()) { + GLTexture *texture = (*it)->texture.data(); + glEnable(GL_BLEND); + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + texture->bind(); + ShaderBinder binder(ShaderTrait::MapTexture | ShaderTrait::Modulate); + const QVector4D constant(opacity, opacity, opacity, opacity); + binder.shader()->setUniform(GLShader::ModulationConstant, constant); + QMatrix4x4 mvp = data.projectionMatrix(); + mvp.translate((*it)->geometry.x(), (*it)->geometry.y()); + binder.shader()->setUniform(GLShader::ModelViewProjectionMatrix, mvp); + texture->render(infiniteRegion(), (*it)->geometry); + texture->unbind(); + glDisable(GL_BLEND); + } else if (effects->compositingType() == XRenderCompositing) { +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + const QRect &rect = (*it)->geometry; + const QSize &size = (*it)->pictureSize; + int x = rect.x(); + int y = rect.y(); + int width = rect.width(); + int height = rect.height(); + switch ((*it)->border) { + case ElectricTopRight: + x = rect.x() + rect.width() - size.width(); + break; + case ElectricBottomRight: + x = rect.x() + rect.width() - size.width(); + y = rect.y() + rect.height() - size.height(); + break; + case ElectricBottomLeft: + y = rect.y() + rect.height() - size.height(); + break; + default: + // nothing + break; + } + xcb_render_composite(xcbConnection(), XCB_RENDER_PICT_OP_OVER, *(*it)->picture.data(), + xRenderBlendPicture(opacity), effects->xrenderBufferPicture(), + 0, 0, 0, 0, x, y, width, height); +#endif + } else if (effects->compositingType() == QPainterCompositing) { + QImage tmp((*it)->image->size(), QImage::Format_ARGB32_Premultiplied); + tmp.fill(Qt::transparent); + QPainter p(&tmp); + p.drawImage(0, 0, *(*it)->image.data()); + QColor color(Qt::transparent); + color.setAlphaF(opacity); + p.setCompositionMode(QPainter::CompositionMode_DestinationIn); + p.fillRect(QRect(QPoint(0, 0), tmp.size()), color); + p.end(); + + QPainter *painter = effects->scenePainter(); + const QRect &rect = (*it)->geometry; + const QSize &size = (*it)->pictureSize; + int x = rect.x(); + int y = rect.y(); + switch ((*it)->border) { + case ElectricTopRight: + x = rect.x() + rect.width() - size.width(); + break; + case ElectricBottomRight: + x = rect.x() + rect.width() - size.width(); + y = rect.y() + rect.height() - size.height(); + break; + case ElectricBottomLeft: + y = rect.y() + rect.height() - size.height(); + break; + default: + // nothing + break; + } + painter->drawImage(QPoint(x, y), tmp); + } + } +} + +void ScreenEdgeEffect::edgeApproaching(ElectricBorder border, qreal factor, const QRect &geometry) +{ + QHash::iterator it = m_borders.find(border); + if (it != m_borders.end()) { + // need to update + effects->addRepaint((*it)->geometry); + (*it)->strength = factor; + if ((*it)->geometry != geometry) { + (*it)->geometry = geometry; + effects->addRepaint((*it)->geometry); + if (border == ElectricLeft || border == ElectricRight || border == ElectricTop || border == ElectricBottom) { + if (effects->isOpenGLCompositing()) { + (*it)->texture.reset(createEdgeGlow(border, geometry.size())); + } else if (effects->compositingType() == XRenderCompositing) { +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + (*it)->picture.reset(createEdgeGlow(border, geometry.size())); +#endif + } else if (effects->compositingType() == QPainterCompositing) { + (*it)->image.reset(createEdgeGlow(border, geometry.size())); + } + } + } + if (factor == 0.0) { + m_cleanupTimer->start(); + } else { + m_cleanupTimer->stop(); + } + } else if (factor != 0.0) { + // need to generate new Glow + Glow *glow = createGlow(border, factor, geometry); + if (glow) { + m_borders.insert(border, glow); + effects->addRepaint(glow->geometry); + } + } +} + +Glow *ScreenEdgeEffect::createGlow(ElectricBorder border, qreal factor, const QRect &geometry) +{ + Glow *glow = new Glow(); + glow->border = border; + glow->strength = factor; + glow->geometry = geometry; + + // render the glow image + if (effects->isOpenGLCompositing()) { + effects->makeOpenGLContextCurrent(); + if (border == ElectricTopLeft || border == ElectricTopRight || border == ElectricBottomRight || border == ElectricBottomLeft) { + glow->texture.reset(createCornerGlow(border)); + } else { + glow->texture.reset(createEdgeGlow(border, geometry.size())); + } + if (!glow->texture.isNull()) { + glow->texture->setWrapMode(GL_CLAMP_TO_EDGE); + } + if (glow->texture.isNull()) { + delete glow; + return nullptr; + } + } else if (effects->compositingType() == XRenderCompositing) { +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + if (border == ElectricTopLeft || border == ElectricTopRight || border == ElectricBottomRight || border == ElectricBottomLeft) { + glow->pictureSize = cornerGlowSize(border); + glow->picture.reset(createCornerGlow(border)); + } else { + glow->pictureSize = geometry.size(); + glow->picture.reset(createEdgeGlow(border, geometry.size())); + } + if (glow->picture.isNull()) { + delete glow; + return nullptr; + } +#endif + } else if (effects->compositingType() == QPainterCompositing) { + if (border == ElectricTopLeft || border == ElectricTopRight || border == ElectricBottomRight || border == ElectricBottomLeft) { + glow->image.reset(createCornerGlow(border)); + glow->pictureSize = cornerGlowSize(border); + } else { + glow->image.reset(createEdgeGlow(border, geometry.size())); + glow->pictureSize = geometry.size(); + } + if (glow->image.isNull()) { + delete glow; + return nullptr; + } + } + + return glow; +} + +template +T *ScreenEdgeEffect::createCornerGlow(ElectricBorder border) +{ + ensureGlowSvg(); + + switch (border) { + case ElectricTopLeft: + return new T(m_glow->pixmap(QStringLiteral("bottomright")).toImage()); + case ElectricTopRight: + return new T(m_glow->pixmap(QStringLiteral("bottomleft")).toImage()); + case ElectricBottomRight: + return new T(m_glow->pixmap(QStringLiteral("topleft")).toImage()); + case ElectricBottomLeft: + return new T(m_glow->pixmap(QStringLiteral("topright")).toImage()); + default: + return nullptr; + } +} + +QSize ScreenEdgeEffect::cornerGlowSize(ElectricBorder border) +{ + ensureGlowSvg(); + + switch (border) { + case ElectricTopLeft: + return m_glow->elementSize(QStringLiteral("bottomright")); + case ElectricTopRight: + return m_glow->elementSize(QStringLiteral("bottomleft")); + case ElectricBottomRight: + return m_glow->elementSize(QStringLiteral("topleft")); + case ElectricBottomLeft: + return m_glow->elementSize(QStringLiteral("topright")); + default: + return QSize(); + } +} + +template +T *ScreenEdgeEffect::createEdgeGlow(ElectricBorder border, const QSize &size) +{ + ensureGlowSvg(); + + const bool stretchBorder = m_glow->hasElement(QStringLiteral("hint-stretch-borders")); + + QPoint pixmapPosition(0, 0); + QPixmap l, r, c; + switch (border) { + case ElectricTop: + l = m_glow->pixmap(QStringLiteral("bottomleft")); + r = m_glow->pixmap(QStringLiteral("bottomright")); + c = m_glow->pixmap(QStringLiteral("bottom")); + break; + case ElectricBottom: + l = m_glow->pixmap(QStringLiteral("topleft")); + r = m_glow->pixmap(QStringLiteral("topright")); + c = m_glow->pixmap(QStringLiteral("top")); + pixmapPosition = QPoint(0, size.height() - c.height()); + break; + case ElectricLeft: + l = m_glow->pixmap(QStringLiteral("topright")); + r = m_glow->pixmap(QStringLiteral("bottomright")); + c = m_glow->pixmap(QStringLiteral("right")); + break; + case ElectricRight: + l = m_glow->pixmap(QStringLiteral("topleft")); + r = m_glow->pixmap(QStringLiteral("bottomleft")); + c = m_glow->pixmap(QStringLiteral("left")); + pixmapPosition = QPoint(size.width() - c.width(), 0); + break; + default: + return nullptr; + } + QPixmap image(size); + image.fill(Qt::transparent); + QPainter p; + p.begin(&image); + if (border == ElectricBottom || border == ElectricTop) { + p.drawPixmap(pixmapPosition, l); + const QRect cRect(l.width(), pixmapPosition.y(), size.width() - l.width() - r.width(), c.height()); + if (stretchBorder) { + p.drawPixmap(cRect, c); + } else { + p.drawTiledPixmap(cRect, c); + } + p.drawPixmap(QPoint(size.width() - r.width(), pixmapPosition.y()), r); + } else { + p.drawPixmap(pixmapPosition, l); + const QRect cRect(pixmapPosition.x(), l.height(), c.width(), size.height() - l.height() - r.height()); + if (stretchBorder) { + p.drawPixmap(cRect, c); + } else { + p.drawTiledPixmap(cRect, c); + } + p.drawPixmap(QPoint(pixmapPosition.x(), size.height() - r.height()), r); + } + p.end(); + return new T(image.toImage()); +} + +bool ScreenEdgeEffect::isActive() const +{ + return !m_borders.isEmpty() && !effects->isScreenLocked(); +} + +} // namespace diff --git a/effects/screenedge/screenedgeeffect.h b/effects/screenedge/screenedgeeffect.h new file mode 100644 index 0000000..14a4d57 --- /dev/null +++ b/effects/screenedge/screenedgeeffect.h @@ -0,0 +1,68 @@ +/* + 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 KWIN_SCREEN_EDGE_EFFECT_H +#define KWIN_SCREEN_EDGE_EFFECT_H +#include + +class QTimer; +namespace Plasma { + class Svg; +} + +namespace KWin { +class Glow; +class GLTexture; + +class ScreenEdgeEffect : public Effect +{ + Q_OBJECT +public: + ScreenEdgeEffect(); + ~ScreenEdgeEffect() override; + void prePaintScreen(ScreenPrePaintData &data, int time) override; + void paintScreen(int mask, const QRegion ®ion, ScreenPaintData &data) override; + bool isActive() const override; + + int requestedEffectChainPosition() const override { + return 90; + } + +private Q_SLOTS: + void edgeApproaching(ElectricBorder border, qreal factor, const QRect &geometry); + void cleanup(); +private: + void ensureGlowSvg(); + Glow *createGlow(ElectricBorder border, qreal factor, const QRect &geometry); + template + T *createCornerGlow(ElectricBorder border); + template + T *createEdgeGlow(ElectricBorder border, const QSize &size); + QSize cornerGlowSize(ElectricBorder border); + Plasma::Svg *m_glow = nullptr; + QHash m_borders; + QTimer *m_cleanupTimer; +}; + +class Glow +{ +public: + QScopedPointer texture; + QScopedPointer image; +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + QScopedPointer picture; +#endif + QSize pictureSize; + qreal strength; + QRect geometry; + ElectricBorder border; +}; + +} + +#endif diff --git a/effects/screenshot/CMakeLists.txt b/effects/screenshot/CMakeLists.txt new file mode 100644 index 0000000..2b02cf5 --- /dev/null +++ b/effects/screenshot/CMakeLists.txt @@ -0,0 +1,8 @@ +####################################### +# Effect + +# Source files +set(kwin4_effect_builtins_sources ${kwin4_effect_builtins_sources} + ../service_utils.cpp + screenshot/screenshot.cpp +) diff --git a/effects/screenshot/screenshot.cpp b/effects/screenshot/screenshot.cpp new file mode 100644 index 0000000..4b87c9b --- /dev/null +++ b/effects/screenshot/screenshot.cpp @@ -0,0 +1,801 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2010 Martin Gräßlin + SPDX-FileCopyrightText: 2010 Nokia Corporation and /or its subsidiary(-ies) + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "screenshot.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include "../service_utils.h" + +class ComparableQPoint : public QPoint +{ +public: + ComparableQPoint(QPoint& point): QPoint(point.x(), point.y()) + {} + + ComparableQPoint(QPoint point): QPoint(point.x(), point.y()) + {} + + ComparableQPoint(): QPoint() + {} + + // utility class that allows using QMap to sort its keys when they are QPoint + // so that the bottom and right points are after the top left ones + bool operator<(const ComparableQPoint &other) const { + return x() < other.x() || y() < other.y(); + } +}; + + +namespace KWin +{ + +const static QString s_errorAlreadyTaking = QStringLiteral("org.kde.kwin.Screenshot.Error.AlreadyTaking"); +const static QString s_errorAlreadyTakingMsg = QStringLiteral("A screenshot is already been taken"); +const static QString s_errorNotAuthorized = QStringLiteral("org.kde.kwin.Screenshot.Error.NoAuthorized"); +const static QString s_errorNotAuthorizedMsg = QStringLiteral("The process is not authorized to take a screenshot"); +const static QString s_errorFd = QStringLiteral("org.kde.kwin.Screenshot.Error.FileDescriptor"); +const static QString s_errorFdMsg = QStringLiteral("No valid file descriptor"); +const static QString s_errorCancelled = QStringLiteral("org.kde.kwin.Screenshot.Error.Cancelled"); +const static QString s_errorCancelledMsg = QStringLiteral("Screenshot got cancelled"); +const static QString s_errorInvalidArea = QStringLiteral("org.kde.kwin.Screenshot.Error.InvalidArea"); +const static QString s_errorInvalidAreaMsg = QStringLiteral("Invalid area requested"); +const static QString s_errorInvalidScreen = QStringLiteral("org.kde.kwin.Screenshot.Error.InvalidScreen"); +const static QString s_errorInvalidScreenMsg = QStringLiteral("Invalid screen requested"); +const static QString s_dbusInterfaceName = QStringLiteral("org.kde.kwin.Screenshot"); + +bool ScreenShotEffect::supported() +{ + return effects->compositingType() == XRenderCompositing || + (effects->isOpenGLCompositing() && GLRenderTarget::supported()); +} + +ScreenShotEffect::ScreenShotEffect() + : m_scheduledScreenshot(nullptr) +{ + connect(effects, &EffectsHandler::windowClosed, this, &ScreenShotEffect::windowClosed); + QDBusConnection::sessionBus().registerObject(QStringLiteral("/Screenshot"), this, QDBusConnection::ExportScriptableContents); +} + +ScreenShotEffect::~ScreenShotEffect() +{ + QDBusConnection::sessionBus().unregisterObject(QStringLiteral("/Screenshot")); +} + +#ifdef KWIN_HAVE_XRENDER_COMPOSITING +static QImage xPictureToImage(xcb_render_picture_t srcPic, const QRect &geometry, xcb_image_t **xImage) +{ + xcb_connection_t *c = effects->xcbConnection(); + xcb_pixmap_t xpix = xcb_generate_id(c); + xcb_create_pixmap(c, 32, xpix, effects->x11RootWindow(), geometry.width(), geometry.height()); + XRenderPicture pic(xpix, 32); + xcb_render_composite(c, XCB_RENDER_PICT_OP_SRC, srcPic, XCB_RENDER_PICTURE_NONE, pic, + geometry.x(), geometry.y(), 0, 0, 0, 0, geometry.width(), geometry.height()); + xcb_flush(c); + *xImage = xcb_image_get(c, xpix, 0, 0, geometry.width(), geometry.height(), ~0, XCB_IMAGE_FORMAT_Z_PIXMAP); + QImage img((*xImage)->data, (*xImage)->width, (*xImage)->height, (*xImage)->stride, QImage::Format_ARGB32_Premultiplied); + // TODO: byte order might need swapping + xcb_free_pixmap(c, xpix); + return img.copy(); +} +#endif + +static QSize pickWindowSize(const QImage &image) +{ + xcb_connection_t *c = effects->xcbConnection(); + + // This will implicitly enable BIG-REQUESTS extension. + const uint32_t maximumRequestSize = xcb_get_maximum_request_length(c); + const xcb_setup_t *setup = xcb_get_setup(c); + + uint32_t requestSize = sizeof(xcb_put_image_request_t); + + // With BIG-REQUESTS extension an additional 32-bit field is inserted into + // the request so we better take it into account. + if (setup->maximum_request_length < maximumRequestSize) { + requestSize += 4; + } + + const uint32_t maximumDataSize = 4 * maximumRequestSize - requestSize; + const uint32_t bytesPerPixel = image.depth() >> 3; + const uint32_t bytesPerLine = image.bytesPerLine(); + + if (image.sizeInBytes() <= maximumDataSize) { + return image.size(); + } + + if (maximumDataSize < bytesPerLine) { + return QSize(maximumDataSize / bytesPerPixel, 1); + } + + return QSize(image.width(), maximumDataSize / bytesPerLine); +} + +static xcb_pixmap_t xpixmapFromImage(const QImage &image) +{ + xcb_connection_t *c = effects->xcbConnection(); + + xcb_pixmap_t pixmap = xcb_generate_id(c); + xcb_gcontext_t gc = xcb_generate_id(c); + + xcb_create_pixmap(c, image.depth(), pixmap, effects->x11RootWindow(), + image.width(), image.height()); + xcb_create_gc(c, gc, pixmap, 0, nullptr); + + const int bytesPerPixel = image.depth() >> 3; + + // Figure out how much data we can send with one invocation of xcb_put_image. + // In contrast to XPutImage, xcb_put_image doesn't implicitly split the data. + const QSize window = pickWindowSize(image); + + for (int i = 0; i < image.height(); i += window.height()) { + const int targetHeight = qMin(image.height() - i, window.height()); + const uint8_t *line = image.scanLine(i); + + for (int j = 0; j < image.width(); j += window.width()) { + const int targetWidth = qMin(image.width() - j, window.width()); + const uint8_t *bytes = line + j * bytesPerPixel; + const uint32_t byteCount = targetWidth * targetHeight * bytesPerPixel; + + xcb_put_image(c, XCB_IMAGE_FORMAT_Z_PIXMAP, pixmap, + gc, targetWidth, targetHeight, j, i, 0, image.depth(), + byteCount, bytes); + } + } + + xcb_flush(c); + xcb_free_gc(c, gc); + + return pixmap; +} + +void ScreenShotEffect::paintScreen(int mask, const QRegion ®ion, ScreenPaintData &data) +{ + m_cachedOutputGeometry = data.outputGeometry(); + // When taking a non-nativeSize fullscreenshot, pretend we have a uniform 1.0 ratio + // so the screenshot size will match the virtualGeometry + m_cachedScale = m_nativeSize ? data.screenScale() : 1.0; + effects->paintScreen(mask, region, data); +} + +/** + * Translates the Point coordinates in m_cacheOutputsImages keys from the virtualGeometry + * To a new geometry taking into account the real size of the images (virtualSize * dpr), + * moving the siblings images on the right bottom of the image as necessary + */ +void ScreenShotEffect::computeCoordinatesAfterScaling() +{ + // prepare a translation table that will store oldPoint -> newPoint in the new coordinates + QMap translationMap; + for (auto i = m_cacheOutputsImages.keyBegin(); i != m_cacheOutputsImages.keyEnd(); ++i) { + translationMap.insert(*i, *i); + } + + for (auto i = m_cacheOutputsImages.constBegin(); i != m_cacheOutputsImages.constEnd(); ++i) { + const auto p = i.key(); + const auto img = i.value(); + const auto dpr = img.devicePixelRatio(); + if (!qFuzzyCompare(dpr, 1.0)) { + // must update all coordinates of next rects + const int deltaX = img.width() - (img.width() / dpr); + const int deltaY = img.height() - (img.height() / dpr); + + // for the next images on the right or bottom + // thanks to ComparableQPoint + for (auto i2 = m_cacheOutputsImages.constFind(p); + i2 != m_cacheOutputsImages.constEnd(); ++i2) { + + const auto point = i2.key(); + auto finalPoint = translationMap.value(point); + + if (point.x() >= img.width() + p.x() - deltaX) { + finalPoint.setX(finalPoint.x() + deltaX); + } + if (point.y() >= img.height() + p.y() - deltaY) { + finalPoint.setY(finalPoint.y() + deltaY); + } + // update final position point with the necessary deltas + translationMap.insert(point, finalPoint); + } + } + } + + // make the new coordinates effective + for (auto i = translationMap.keyBegin(); i != translationMap.keyEnd(); ++i) { + const auto key = *i; + const auto img = m_cacheOutputsImages.take(key); + m_cacheOutputsImages.insert(translationMap.value(key), img); + } +} + +void ScreenShotEffect::postPaintScreen() +{ + effects->postPaintScreen(); + if (m_scheduledScreenshot) { + WindowPaintData d(m_scheduledScreenshot); + double left = 0; + double top = 0; + double right = m_scheduledScreenshot->width(); + double bottom = m_scheduledScreenshot->height(); + if (m_scheduledScreenshot->hasDecoration() && m_type & INCLUDE_DECORATION) { + for (const WindowQuad &quad : qAsConst(d.quads)) { + // we need this loop to include the decoration padding + left = qMin(left, quad.left()); + top = qMin(top, quad.top()); + right = qMax(right, quad.right()); + bottom = qMax(bottom, quad.bottom()); + } + } else if (m_scheduledScreenshot->hasDecoration()) { + WindowQuadList newQuads; + left = m_scheduledScreenshot->width(); + top = m_scheduledScreenshot->height(); + right = 0; + bottom = 0; + for (const WindowQuad &quad : qAsConst(d.quads)) { + if (quad.type() == WindowQuadContents) { + newQuads << quad; + left = qMin(left, quad.left()); + top = qMin(top, quad.top()); + right = qMax(right, quad.right()); + bottom = qMax(bottom, quad.bottom()); + } + } + d.quads = newQuads; + } + const int width = right - left; + const int height = bottom - top; + bool validTarget = true; + QScopedPointer offscreenTexture; + QScopedPointer target; + if (effects->isOpenGLCompositing()) { + offscreenTexture.reset(new GLTexture(GL_RGBA8, width, height)); + offscreenTexture->setFilter(GL_LINEAR); + offscreenTexture->setWrapMode(GL_CLAMP_TO_EDGE); + target.reset(new GLRenderTarget(*offscreenTexture)); + validTarget = target->valid(); + } + if (validTarget) { + d.setXTranslation(-m_scheduledScreenshot->x() - left); + d.setYTranslation(-m_scheduledScreenshot->y() - top); + + // render window into offscreen texture + int mask = PAINT_WINDOW_TRANSFORMED | PAINT_WINDOW_TRANSLUCENT; + QImage img; + if (effects->isOpenGLCompositing()) { + GLRenderTarget::pushRenderTarget(target.data()); + glClearColor(0.0, 0.0, 0.0, 0.0); + glClear(GL_COLOR_BUFFER_BIT); + glClearColor(0.0, 0.0, 0.0, 1.0); + + QMatrix4x4 projection; + projection.ortho(QRect(0, 0, offscreenTexture->width(), offscreenTexture->height())); + d.setProjectionMatrix(projection); + + effects->drawWindow(m_scheduledScreenshot, mask, infiniteRegion(), d); + + // copy content from framebuffer into image + img = QImage(QSize(width, height), QImage::Format_ARGB32); + glReadnPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, img.sizeInBytes(), (GLvoid*)img.bits()); + GLRenderTarget::popRenderTarget(); + ScreenShotEffect::convertFromGLImage(img, width, height); + } +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + xcb_image_t *xImage = nullptr; + if (effects->compositingType() == XRenderCompositing) { + setXRenderOffscreen(true); + effects->drawWindow(m_scheduledScreenshot, mask, QRegion(0, 0, width, height), d); + if (xRenderOffscreenTarget()) { + img = xPictureToImage(xRenderOffscreenTarget(), QRect(0, 0, width, height), &xImage); + } + setXRenderOffscreen(false); + } +#endif + + if (m_type & INCLUDE_CURSOR) { + grabPointerImage(img, m_scheduledScreenshot->x() + left, m_scheduledScreenshot->y() + top); + } + + if (m_windowMode == WindowMode::Xpixmap) { + const xcb_pixmap_t xpix = xpixmapFromImage(img); + emit screenshotCreated(xpix); + m_windowMode = WindowMode::NoCapture; + } else if (m_windowMode == WindowMode::File || m_windowMode == WindowMode::FileDescriptor) { + sendReplyImage(img); + } +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + if (xImage) { + xcb_image_destroy(xImage); + } +#endif + } + m_scheduledScreenshot = nullptr; + } + + if (!m_scheduledGeometry.isNull()) { + if (!m_cachedOutputGeometry.isNull()) { + // special handling for per-output geometry rendering + const QRect intersection = m_scheduledGeometry.intersected(m_cachedOutputGeometry); + if (intersection.isEmpty()) { + // doesn't intersect, not going onto this screenshot + return; + } + QImage img = blitScreenshot(intersection, m_cachedScale); + if (img.size() == (m_scheduledGeometry.size() * m_cachedScale)) { + // we are done + sendReplyImage(img); + return; + } + img.setDevicePixelRatio(m_cachedScale); + + m_cacheOutputsImages.insert(ComparableQPoint(m_cachedOutputGeometry.topLeft()), img); + + m_multipleOutputsRendered = m_multipleOutputsRendered.united(intersection); + if (m_multipleOutputsRendered.boundingRect() == m_scheduledGeometry) { + + // Recompute coordinates + if (m_nativeSize) { + computeCoordinatesAfterScaling(); + } + + // find the output image size + int width = 0; + int height = 0; + QMap::const_iterator i; + for (i = m_cacheOutputsImages.constBegin(); i != m_cacheOutputsImages.constEnd(); ++i) { + const auto pos = i.key(); + const auto img = i.value(); + + width = qMax(width, pos.x() + img.width()); + height = qMax(height, pos.y() + img.height()); + } + + QImage multipleOutputsImage = QImage(width, height, QImage::Format_ARGB32); + + QPainter p; + p.begin(&multipleOutputsImage); + + // reassemble images together + for (i = m_cacheOutputsImages.constBegin(); i != m_cacheOutputsImages.constEnd(); ++i) { + auto pos = i.key(); + auto img = i.value(); + // disable dpr rendering, we already took care of this + img.setDevicePixelRatio(1.0); + p.drawImage(pos, img); + } + p.end(); + + sendReplyImage(multipleOutputsImage); + } + + } else { + const QImage img = blitScreenshot(m_scheduledGeometry); + sendReplyImage(img); + } + } +} + +void ScreenShotEffect::sendReplyImage(const QImage &img) +{ + if (m_fd != -1) { + QtConcurrent::run( + [] (int fd, const QImage &img) { + QFile file; + if (file.open(fd, QIODevice::WriteOnly, QFileDevice::AutoCloseHandle)) { + QDataStream ds(&file); + ds << img; + file.close(); + } else { + close(fd); + } + }, m_fd, img); + m_fd = -1; + } else { + QDBusConnection::sessionBus().send(m_replyMessage.createReply(saveTempImage(img))); + } + m_scheduledGeometry = QRect(); + m_multipleOutputsRendered = QRegion(); + m_captureCursor = false; + m_windowMode = WindowMode::NoCapture; + m_cacheOutputsImages.clear(); + m_cachedOutputGeometry = QRect(); + m_nativeSize = false; +} + +QString ScreenShotEffect::saveTempImage(const QImage &img) +{ + if (img.isNull()) { + return QString(); + } + QTemporaryFile temp(QDir::tempPath() + QDir::separator() + QLatin1String("kwin_screenshot_XXXXXX.png")); + temp.setAutoRemove(false); + if (!temp.open()) { + return QString(); + } + img.save(&temp); + temp.close(); + KNotification::event(KNotification::Notification, + i18nc("Notification caption that a screenshot got saved to file", "Screenshot"), + i18nc("Notification with path to screenshot file", "Screenshot saved to %1", temp.fileName()), + QStringLiteral("spectacle")); + return temp.fileName(); +} + +void ScreenShotEffect::screenshotWindowUnderCursor(int mask) +{ + if (isTakingScreenshot()) { + sendErrorReply(s_errorAlreadyTaking, s_errorAlreadyTakingMsg); + return; + } + m_type = (ScreenShotType)mask; + const QPoint cursor = effects->cursorPos(); + EffectWindowList order = effects->stackingOrder(); + EffectWindowList::const_iterator it = order.constEnd(), first = order.constBegin(); + while( it != first ) { + m_scheduledScreenshot = *(--it); + if (m_scheduledScreenshot->isOnCurrentDesktop() && + !m_scheduledScreenshot->isMinimized() && !m_scheduledScreenshot->isDeleted() && + m_scheduledScreenshot->geometry().contains(cursor)) + break; + m_scheduledScreenshot = nullptr; + } + if (m_scheduledScreenshot) { + m_windowMode = WindowMode::Xpixmap; + m_scheduledScreenshot->addRepaintFull(); + } +} + +void ScreenShotEffect::screenshotForWindow(qulonglong winid, int mask) +{ + m_type = (ScreenShotType) mask; + EffectWindow* w = effects->findWindow(winid); + if(w && !w->isMinimized() && !w->isDeleted()) { + m_windowMode = WindowMode::Xpixmap; + m_scheduledScreenshot = w; + m_scheduledScreenshot->addRepaintFull(); + } +} + +bool ScreenShotEffect::checkCall() const +{ + if (!calledFromDBus()) { + return false; + } + + const QDBusReply reply = connection().interface()->servicePid(message().service()); + if (reply.isValid()) { + const uint pid = reply.value(); + const auto interfaces = KWin::fetchRestrictedDBusInterfacesFromPid(pid); + if (!interfaces.contains(s_dbusInterfaceName)) { + sendErrorReply(s_errorNotAuthorized, s_errorNotAuthorizedMsg); + qCWarning(KWINEFFECTS) << "Process " << pid << " tried to take a screenshot without being granted to DBus interface" << s_dbusInterfaceName; + return false; + } + } else { + return false; + } + + if (isTakingScreenshot()) { + sendErrorReply(s_errorAlreadyTaking, s_errorAlreadyTakingMsg); + return false; + } + return true; +} + +QString ScreenShotEffect::interactive(int mask) +{ + if (!calledFromDBus()) { + return QString(); + } + if (isTakingScreenshot()) { + sendErrorReply(s_errorAlreadyTaking, s_errorAlreadyTakingMsg); + return QString(); + } + + m_type = (ScreenShotType) mask; + m_windowMode = WindowMode::File; + m_replyMessage = message(); + setDelayedReply(true); + effects->startInteractiveWindowSelection( + [this] (EffectWindow *w) { + hideInfoMessage(); + if (!w) { + QDBusConnection::sessionBus().send(m_replyMessage.createErrorReply(s_errorCancelled, s_errorCancelledMsg)); + m_windowMode = WindowMode::NoCapture; + return; + } else { + m_scheduledScreenshot = w; + m_scheduledScreenshot->addRepaintFull(); + } + }); + + showInfoMessage(InfoMessageMode::Window); + return QString(); +} + +void ScreenShotEffect::interactive(QDBusUnixFileDescriptor fd, int mask) +{ + if (!calledFromDBus()) { + return; + } + if (isTakingScreenshot()) { + sendErrorReply(s_errorAlreadyTaking, s_errorAlreadyTakingMsg); + return; + } + + m_fd = dup(fd.fileDescriptor()); + if (m_fd == -1) { + sendErrorReply(s_errorFd, s_errorFdMsg); + return; + } + m_type = (ScreenShotType) mask; + m_windowMode = WindowMode::FileDescriptor; + + effects->startInteractiveWindowSelection( + [this] (EffectWindow *w) { + hideInfoMessage(); + if (!w) { + close(m_fd); + m_fd = -1; + m_windowMode = WindowMode::NoCapture; + return; + } else { + m_scheduledScreenshot = w; + m_scheduledScreenshot->addRepaintFull(); + } + }); + + showInfoMessage(InfoMessageMode::Window); +} + +void ScreenShotEffect::showInfoMessage(InfoMessageMode mode) +{ + QString text; + switch (mode) { + case InfoMessageMode::Window: + text = i18n("Select window to screen shot with left click or enter.\nEscape or right click to cancel."); + break; + case InfoMessageMode::Screen: + text = i18n("Create screen shot with left click or enter.\nEscape or right click to cancel."); + break; + } + effects->showOnScreenMessage(text, QStringLiteral("spectacle")); +} + +void ScreenShotEffect::hideInfoMessage() +{ + effects->hideOnScreenMessage(EffectsHandler::OnScreenMessageHideFlag::SkipsCloseAnimation); +} + +QString ScreenShotEffect::screenshotFullscreen(bool captureCursor) +{ + if (!checkCall()) { + return QString(); + } + m_replyMessage = message(); + setDelayedReply(true); + m_scheduledGeometry = effects->virtualScreenGeometry(); + m_captureCursor = captureCursor; + effects->addRepaintFull(); + return QString(); +} + +void ScreenShotEffect::screenshotFullscreen(QDBusUnixFileDescriptor fd, bool captureCursor, bool shouldReturnNativeSize) +{ + if (!checkCall()) { + return; + } + m_fd = dup(fd.fileDescriptor()); + if (m_fd == -1) { + sendErrorReply(s_errorFd, s_errorFdMsg); + return; + } + m_captureCursor = captureCursor; + m_nativeSize = shouldReturnNativeSize; + + m_scheduledGeometry = effects->virtualScreenGeometry(); + effects->addRepaint(m_scheduledGeometry); +} + +QString ScreenShotEffect::screenshotScreen(int screen, bool captureCursor) +{ + if (!checkCall()) { + return QString(); + } + m_scheduledGeometry = effects->clientArea(FullScreenArea, screen, 0); + if (m_scheduledGeometry.isNull()) { + sendErrorReply(s_errorInvalidScreen, s_errorInvalidScreenMsg); + return QString(); + } + m_captureCursor = captureCursor; + m_nativeSize = true; + m_replyMessage = message(); + setDelayedReply(true); + effects->addRepaint(m_scheduledGeometry); + return QString(); +} + +void ScreenShotEffect::screenshotScreen(QDBusUnixFileDescriptor fd, bool captureCursor) +{ + if (!checkCall()) { + return; + } + m_fd = dup(fd.fileDescriptor()); + if (m_fd == -1) { + sendErrorReply(s_errorFd, s_errorFdMsg); + return; + } + m_captureCursor = captureCursor; + m_nativeSize = true; + + showInfoMessage(InfoMessageMode::Screen); + effects->startInteractivePositionSelection( + [this] (const QPoint &p) { + hideInfoMessage(); + if (p == QPoint(-1, -1)) { + // error condition + close(m_fd); + m_fd = -1; + } else { + m_scheduledGeometry = effects->clientArea(FullScreenArea, effects->screenNumber(p), 0); + if (m_scheduledGeometry.isNull()) { + close(m_fd); + m_fd = -1; + return; + } + effects->addRepaint(m_scheduledGeometry); + } + } + ); +} + +QString ScreenShotEffect::screenshotArea(int x, int y, int width, int height, bool captureCursor) +{ + if (!checkCall()) { + return QString(); + } + m_scheduledGeometry = QRect(x, y, width, height); + if (m_scheduledGeometry.isNull() || m_scheduledGeometry.isEmpty()) { + m_scheduledGeometry = QRect(); + sendErrorReply(s_errorInvalidArea, s_errorInvalidAreaMsg); + return QString(); + } + m_captureCursor = captureCursor; + m_replyMessage = message(); + setDelayedReply(true); + effects->addRepaint(m_scheduledGeometry); + return QString(); +} + +QImage ScreenShotEffect::blitScreenshot(const QRect &geometry, const qreal scale) +{ + QImage img; + if (effects->isOpenGLCompositing()) + { + const QSize nativeSize = geometry.size() * scale; + + if (GLRenderTarget::blitSupported() && !GLPlatform::instance()->isGLES()) { + + img = QImage(nativeSize.width(), nativeSize.height(), QImage::Format_ARGB32); + GLTexture tex(GL_RGBA8, nativeSize.width(), nativeSize.height()); + GLRenderTarget target(tex); + target.blitFromFramebuffer(geometry); + // copy content from framebuffer into image + tex.bind(); + glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, static_cast(img.bits())); + tex.unbind(); + } else { + img = QImage(nativeSize.width(), nativeSize.height(), QImage::Format_ARGB32); + glReadPixels(0, 0, nativeSize.width(), nativeSize.height(), GL_RGBA, GL_UNSIGNED_BYTE, (GLvoid*)img.bits()); + } + ScreenShotEffect::convertFromGLImage(img, nativeSize.width(), nativeSize.height()); + } + +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + if (effects->compositingType() == XRenderCompositing) { + xcb_image_t *xImage = nullptr; + img = xPictureToImage(effects->xrenderBufferPicture(), geometry, &xImage); + if (xImage) { + xcb_image_destroy(xImage); + } + } +#endif + + if (m_captureCursor) { + grabPointerImage(img, geometry.x() * scale, geometry.y() * scale); + } + + return img; +} + +void ScreenShotEffect::grabPointerImage(QImage& snapshot, int offsetx, int offsety) +{ + const auto cursor = effects->cursorImage(); + if (cursor.image().isNull()) + return; + + QPainter painter(&snapshot); + painter.drawImage(effects->cursorPos() - cursor.hotSpot() - QPoint(offsetx, offsety), cursor.image()); +} + +void ScreenShotEffect::convertFromGLImage(QImage &img, int w, int h) +{ + // from QtOpenGL/qgl.cpp + // SPDX-FileCopyrightText: 2010 Nokia Corporation and /or its subsidiary(-ies) + // see https://github.com/qt/qtbase/blob/dev/src/opengl/qgl.cpp + if (QSysInfo::ByteOrder == QSysInfo::BigEndian) { + // OpenGL gives RGBA; Qt wants ARGB + uint *p = reinterpret_cast(img.bits()); + uint *end = p + w * h; + while (p < end) { + uint a = *p << 24; + *p = (*p >> 8) | a; + p++; + } + } else { + // OpenGL gives ABGR (i.e. RGBA backwards); Qt wants ARGB + for (int y = 0; y < h; y++) { + uint *q = reinterpret_cast(img.scanLine(y)); + for (int x = 0; x < w; ++x) { + const uint pixel = *q; + *q = ((pixel << 16) & 0xff0000) | ((pixel >> 16) & 0xff) + | (pixel & 0xff00ff00); + + q++; + } + } + + } + img = img.mirrored(); +} + +bool ScreenShotEffect::isActive() const +{ + return (m_scheduledScreenshot != nullptr || !m_scheduledGeometry.isNull()) && !effects->isScreenLocked(); +} + +void ScreenShotEffect::windowClosed( EffectWindow* w ) +{ + if (w == m_scheduledScreenshot) { + m_scheduledScreenshot = nullptr; + screenshotWindowUnderCursor(m_type); + } +} + +bool ScreenShotEffect::isTakingScreenshot() const +{ + if (!m_scheduledGeometry.isNull()) { + return true; + } + if (m_windowMode != WindowMode::NoCapture) { + return true; + } + if (m_fd != -1) { + return true; + } + return false; +} + +} // namespace diff --git a/effects/screenshot/screenshot.h b/effects/screenshot/screenshot.h new file mode 100644 index 0000000..d714ff8 --- /dev/null +++ b/effects/screenshot/screenshot.h @@ -0,0 +1,170 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2010 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_SCREENSHOT_H +#define KWIN_SCREENSHOT_H + +#include +#include +#include +#include +#include +#include +#include + +class ComparableQPoint; +namespace KWin +{ + +class ScreenShotEffect : public Effect, protected QDBusContext +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.kwin.Screenshot") +public: + enum ScreenShotType { + INCLUDE_DECORATION = 1 << 0, + INCLUDE_CURSOR = 1 << 1 + }; + ScreenShotEffect(); + ~ScreenShotEffect() override; + + void paintScreen(int mask, const QRegion ®ion, ScreenPaintData &data) override; + void postPaintScreen() override; + bool isActive() const override; + + int requestedEffectChainPosition() const override { + return 50; + } + + static bool supported(); + static void convertFromGLImage(QImage &img, int w, int h); +public Q_SLOTS: + Q_SCRIPTABLE void screenshotForWindow(qulonglong winid, int mask = 0); + /** + * Starts an interactive window screenshot session. The user can select a window to + * screenshot. + * + * Once the window is selected the screenshot is saved into a file and the path gets + * returned to the DBus peer. + * + * @param mask The mask for what to include in the screenshot + */ + Q_SCRIPTABLE QString interactive(int mask = 0); + /** + * Starts an interactive window screenshot session. The user can select a window to + * screenshot. + * + * Once the window is selected the screenshot is saved into the @p fd passed to the + * method. It is intended to be used with a pipe, so that the invoking side can just + * read from the pipe. The image gets written into the fd using a QDataStream. + * + * @param fd File descriptor into which the screenshot should be saved + * @param mask The mask for what to include in the screenshot + */ + Q_SCRIPTABLE void interactive(QDBusUnixFileDescriptor fd, int mask = 0); + Q_SCRIPTABLE void screenshotWindowUnderCursor(int mask = 0); + /** + * Saves a screenshot of all screen into a file and returns the path to the file. + * Functionality requires hardware support, if not available a null string is returned. + * @param captureCursor Whether to include the cursor in the image + * @returns Path to stored screenshot, or null string in failure case. + */ + Q_SCRIPTABLE QString screenshotFullscreen(bool captureCursor = false); + /** + * Starts an interactive screenshot session. + * + * The user is asked to confirm that a screenshot is taken by having to actively + * click and giving the possibility to cancel. + * + * Once the screenshot is taken it gets saved into the @p fd passed to the + * method. It is intended to be used with a pipe, so that the invoking side can just + * read from the pipe. The image gets written into the fd using a QDataStream. + * + * @param fd File descriptor into which the screenshot should be saved + * @param captureCursor Whether to include the mouse cursor + * @param shouldReturnNativeSize Whether to return an image according to the virtualGeometry, or according to pixel on screen size + */ + Q_SCRIPTABLE void screenshotFullscreen(QDBusUnixFileDescriptor fd, bool captureCursor = false, bool shouldReturnNativeSize = false); + /** + * Saves a screenshot of the screen identified by @p screen into a file and returns the path to the file. + * Functionality requires hardware support, if not available a null string is returned. + * @param screen Number of screen as numbered by QDesktopWidget + * @param captureCursor Whether to include the cursor in the image + * @returns Path to stored screenshot, or null string in failure case. + */ + Q_SCRIPTABLE QString screenshotScreen(int screen, bool captureCursor = false); + /** + * Starts an interactive screenshot of a screen session. + * + * The user is asked to select the screen to screenshot. + * + * Once the screenshot is taken it gets saved into the @p fd passed to the + * method. It is intended to be used with a pipe, so that the invoking side can just + * read from the pipe. The image gets written into the fd using a QDataStream. + * + * @param fd File descriptor into which the screenshot should be saved + * @param captureCursor Whether to include the mouse cursor + */ + Q_SCRIPTABLE void screenshotScreen(QDBusUnixFileDescriptor fd, bool captureCursor = false); + /** + * Saves a screenshot of the selected geometry into a file and returns the path to the file. + * Functionality requires hardware support, if not available a null string is returned. + * @param x Left upper x coord of region + * @param y Left upper y coord of region + * @param width Width of the region to screenshot + * @param height Height of the region to screenshot + * @param captureCursor Whether to include the cursor in the image + * @returns Path to stored screenshot, or null string in failure case. + */ + Q_SCRIPTABLE QString screenshotArea(int x, int y, int width, int height, bool captureCursor = false); + +Q_SIGNALS: + Q_SCRIPTABLE void screenshotCreated(qulonglong handle); + +private Q_SLOTS: + void windowClosed( KWin::EffectWindow* w ); + +private: + void grabPointerImage(QImage& snapshot, int offsetx, int offsety); + QImage blitScreenshot(const QRect &geometry, const qreal scale = 1.0); + QString saveTempImage(const QImage &img); + void sendReplyImage(const QImage &img); + enum class InfoMessageMode { + Window, + Screen + }; + void showInfoMessage(InfoMessageMode mode); + void hideInfoMessage(); + bool isTakingScreenshot() const; + void computeCoordinatesAfterScaling(); + bool checkCall() const; + + EffectWindow *m_scheduledScreenshot; + ScreenShotType m_type; + QRect m_scheduledGeometry; + QDBusMessage m_replyMessage; + QRect m_cachedOutputGeometry; + QRegion m_multipleOutputsRendered; + QMap m_cacheOutputsImages; + bool m_captureCursor = false; + bool m_nativeSize = false; + enum class WindowMode { + NoCapture, + Xpixmap, + File, + FileDescriptor + }; + WindowMode m_windowMode = WindowMode::NoCapture; + int m_fd = -1; + qreal m_cachedScale; +}; + +} // namespace + +#endif // KWIN_SCREENSHOT_H diff --git a/effects/sessionquit/package/contents/code/main.js b/effects/sessionquit/package/contents/code/main.js new file mode 100644 index 0000000..6b73fd9 --- /dev/null +++ b/effects/sessionquit/package/contents/code/main.js @@ -0,0 +1,32 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 David Edmundson + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +"use strict"; + +var quitEffect = { + closed: function (window) { + if (!window.desktopWindow || effects.sessionState != Globals.Quitting) { + return; + } + if (!effect.grab(window, Effect.WindowClosedGrabRole)) { + return; + } + window.outAnimation = animate({ + window: window, + duration: 30 * 1000, // 30 seconds should be long enough for any shutdown + type: Effect.Generic, // do nothing, just hold a reference + from: 0.0, + to: 0.0 + }); + }, + init: function () { + effects.windowClosed.connect(quitEffect.closed); + } +}; +quitEffect.init(); diff --git a/effects/sessionquit/package/metadata.desktop b/effects/sessionquit/package/metadata.desktop new file mode 100644 index 0000000..56d05f1 --- /dev/null +++ b/effects/sessionquit/package/metadata.desktop @@ -0,0 +1,77 @@ +[Desktop Entry] +Name=Session Quit +Name[az]=Sesiyadan çıxış +Name[ca]=Sortida de la sessió +Name[cs]=Sezení bylo ukončeno +Name[da]=Afslutning af session +Name[de]=Sitzung beenden +Name[en_GB]=Session Quit +Name[es]=Cierre de sesión +Name[et]=Seansist väljumine +Name[eu]=Saiotik irtetea +Name[fi]=Istunnon lopetus +Name[fr]=Fin de session +Name[gl]=Saída de sesión +Name[ia]=Abandona session +Name[id]=Sesi Berhenti +Name[it]=Chiusura sessione +Name[ko]=세션 종료 +Name[lt]=Seanso baigimas +Name[nl]=Afsluiten van sessie +Name[nn]=Avslutt økt +Name[pl]=Opuszczenie sesji +Name[pt]=Saída da Sessão +Name[pt_BR]=Saída da sessão +Name[ro]=Părăsirea sesiunii +Name[ru]=Завершение сеанса +Name[sk]=Ukončenie sedenia +Name[sl]=Zapri sejo +Name[sv]=Avsluta session +Name[uk]=Вихід з сеансу +Name[x-test]=xxSession Quitxx +Name[zh_CN]=会话退出 +Name[zh_TW]=離開工作階段 +Icon=preferences-system-windows-effect-logout +Comment=Keep the desktop background alive during logout until the end +Comment[az]=Seansdan çıxışda sona qədər İş Masası fon şəklinin saxlanılması +Comment[ca]=Manté viu el fons de l'escriptori durant la sortida de la sessió fins el final +Comment[da]=Hold skrivebordsbaggrunden i live under log ud helt til sidst +Comment[en_GB]=Keep the desktop background alive during logout until the end +Comment[es]=Mantener el fondo del escritorio activo durante el cierre de sesión hasta el final +Comment[et]=Töölaua taust hoitakse väljalogimisel alles kuni lõpuni +Comment[eu]=Eutsi mahaigaineko atzeko-planoa bizirik saio-ixtean bukaera arte +Comment[fi]=Pidä työpöydän tausta käynnissä aina uloskirjautumisen loppuun +Comment[fr]=Conservez l'arriére plan du bureau visible jusqu'à la fin de la déconnexion +Comment[gl]=Manter vivo o fondo de escritorio durante a saída ata o final +Comment[id]=Jaga latarbelakang desktop tetap nyala selama logout sampai akhir +Comment[it]=Mantieni attivo lo sfondo del desktop durante la chiusura della sessione fino alla fine +Comment[ko]=로그아웃이 끝날 때까지 데스크톱 ë°°ê²½ 그림 유지 +Comment[lt]=Atsijungiant, iÅ¡laikyti darbalaukio foną reaguojantį iki pat galo +Comment[nl]=De achtergrond van het bureaublad tot het einde levend houden gedurende afmelden +Comment[nn]=Hald skrivebordsbakgrunnen i live heilt til utlogginga er fullført +Comment[pl]=Utrzymuj tło pulpitu podczas wylogowania, aż do końca +Comment[pt]=Manter o fundo do ecrã activo durante a saída da sessão até ao fim +Comment[pt_BR]=Mantém ativo o fundo da área de trabalho até o fim do fechamento da sessão +Comment[ro]=Păstrează viu fundalul biroului în timpul ieșirii din sesiune până la capăt +Comment[ru]=Сохранение фона рабочего стола до окончания завершения сеанса +Comment[sk]=PonechaÅ¥ pozadie plochy aktívne počas odhlásenia do skončenia +Comment[sl]=Pusti ozadje namizja živo ob odjavi do konca +Comment[sv]=BehÃ¥ll skrivbordets bakgrund levande under utloggning till slutet +Comment[uk]=Не вимикати тло стільниці до кінця процедури виходу із облікового запису +Comment[x-test]=xxKeep the desktop background alive during logout until the endxx +Comment[zh_CN]=注销时保持桌面背景,直到完全退出 +Comment[zh_TW]=直到登出完成前皆保留桌面背景圖片 + +Type=Service +X-KDE-ServiceTypes=KWin/Effect +X-KDE-PluginInfo-Author=David Edmundson +X-KDE-PluginInfo-Email=davidedmundson@kde.org +X-KDE-PluginInfo-Name=kwin4_effect_sessionquit +X-KDE-PluginInfo-Version=0.2.0 +X-KDE-PluginInfo-Category=Appearance +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-EnabledByDefault=true +X-KDE-Ordering=40 +X-Plasma-API=javascript +X-KWin-Internal=true +X-Plasma-MainScript=code/main.js diff --git a/effects/shaders.qrc b/effects/shaders.qrc new file mode 100644 index 0000000..a196dd8 --- /dev/null +++ b/effects/shaders.qrc @@ -0,0 +1,23 @@ + + + coverswitch/shaders/1.10/coverswitch-reflection.glsl + cube/data/1.10/cube-cap.glsl + cube/data/1.10/cube-reflection.glsl + cube/data/1.10/cylinder.vert + cube/data/1.10/sphere.vert + invert/data/1.10/invert.frag + lookingglass/data/1.10/lookingglass.frag + startupfeedback/data/1.10/blinking-startup-fragment.glsl + + + coverswitch/shaders/1.40/coverswitch-reflection.glsl + cube/data/1.40/cube-cap.glsl + cube/data/1.40/cube-reflection.glsl + cube/data/1.40/cylinder.vert + cube/data/1.40/sphere.vert + invert/data/1.40/invert.frag + lookingglass/data/1.40/lookingglass.frag + startupfeedback/data/1.40/blinking-startup-fragment.glsl + + + diff --git a/effects/sheet/CMakeLists.txt b/effects/sheet/CMakeLists.txt new file mode 100644 index 0000000..fc680bc --- /dev/null +++ b/effects/sheet/CMakeLists.txt @@ -0,0 +1,8 @@ +####################################### +# Effect + +# Source files +set(kwin4_effect_builtins_sources ${kwin4_effect_builtins_sources} + sheet/sheet.cpp +) +kconfig_add_kcfg_files(kwin4_effect_builtins_sources sheet/sheetconfig.kcfgc) diff --git a/effects/sheet/sheet.cpp b/effects/sheet/sheet.cpp new file mode 100644 index 0000000..d2cb7ad --- /dev/null +++ b/effects/sheet/sheet.cpp @@ -0,0 +1,219 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Philip Falkner + SPDX-FileCopyrightText: 2009 Martin Gräßlin + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +// own +#include "sheet.h" + +// KConfigSkeleton +#include "sheetconfig.h" + +// Qt +#include + +namespace KWin +{ + +SheetEffect::SheetEffect() +{ + initConfig(); + reconfigure(ReconfigureAll); + + connect(effects, &EffectsHandler::windowAdded, this, &SheetEffect::slotWindowAdded); + connect(effects, &EffectsHandler::windowClosed, this, &SheetEffect::slotWindowClosed); + connect(effects, &EffectsHandler::windowDeleted, this, &SheetEffect::slotWindowDeleted); +} + +void SheetEffect::reconfigure(ReconfigureFlags flags) +{ + Q_UNUSED(flags) + + SheetConfig::self()->read(); + + // TODO: Rename AnimationTime config key to Duration. + const int d = animationTime(SheetConfig::animationTime() != 0 + ? SheetConfig::animationTime() + : 300); + m_duration = std::chrono::milliseconds(static_cast(d)); +} + +void SheetEffect::prePaintScreen(ScreenPrePaintData &data, int time) +{ + const std::chrono::milliseconds delta(time); + + auto animationIt = m_animations.begin(); + while (animationIt != m_animations.end()) { + (*animationIt).timeLine.update(delta); + ++animationIt; + } + + data.mask |= PAINT_SCREEN_WITH_TRANSFORMED_WINDOWS; + + effects->prePaintScreen(data, time); +} + +void SheetEffect::prePaintWindow(EffectWindow *w, WindowPrePaintData &data, int time) +{ + if (m_animations.contains(w)) { + data.setTransformed(); + w->enablePainting(EffectWindow::PAINT_DISABLED_BY_DELETE); + } + + effects->prePaintWindow(w, data, time); +} + +void SheetEffect::paintWindow(EffectWindow *w, int mask, QRegion region, WindowPaintData &data) +{ + auto animationIt = m_animations.constFind(w); + if (animationIt == m_animations.constEnd()) { + effects->paintWindow(w, mask, region, data); + return; + } + + // Perspective projection distorts objects near edges of the viewport + // in undesired way. To fix this, the center of the window will be + // moved to the origin, after applying perspective projection, the + // center is moved back to its "original" projected position. Overall, + // this is how the window will be transformed: + // [move to the origin] -> [scale] -> [rotate] -> [translate] -> + // -> [perspective projection] -> [reverse "move to the origin"] + const QMatrix4x4 oldProjMatrix = data.screenProjectionMatrix(); + const QRectF windowGeo = w->geometry(); + const QVector3D invOffset = oldProjMatrix.map(QVector3D(windowGeo.center())); + QMatrix4x4 invOffsetMatrix; + invOffsetMatrix.translate(invOffset.x(), invOffset.y()); + data.setProjectionMatrix(invOffsetMatrix * oldProjMatrix); + + // Move the center of the window to the origin. + const QRectF screenGeo = effects->virtualScreenGeometry(); + const QPointF offset = screenGeo.center() - windowGeo.center(); + data.translate(offset.x(), offset.y()); + + const qreal t = (*animationIt).timeLine.value(); + data.setRotationAxis(Qt::XAxis); + data.setRotationAngle(interpolate(60.0, 0.0, t)); + data *= QVector3D(1.0, t, t); + data.translate(0.0, -interpolate(w->y() - (*animationIt).parentY, 0.0, t)); + + data.multiplyOpacity(t); + + effects->paintWindow(w, mask, region, data); +} + +void SheetEffect::postPaintWindow(EffectWindow *w) +{ + auto animationIt = m_animations.begin(); + while (animationIt != m_animations.end()) { + EffectWindow *w = animationIt.key(); + w->addRepaintFull(); + if ((*animationIt).timeLine.done()) { + if (w->isDeleted()) { + w->unrefWindow(); + } + animationIt = m_animations.erase(animationIt); + } else { + ++animationIt; + } + } + + if (m_animations.isEmpty()) { + effects->addRepaintFull(); + } + + effects->postPaintWindow(w); +} + +bool SheetEffect::isActive() const +{ + return !m_animations.isEmpty(); +} + +bool SheetEffect::supported() +{ + return effects->isOpenGLCompositing() + && effects->animationsSupported(); +} + +void SheetEffect::slotWindowAdded(EffectWindow *w) +{ + if (effects->activeFullScreenEffect()) { + return; + } + + if (!isSheetWindow(w)) { + return; + } + + Animation &animation = m_animations[w]; + animation.parentY = 0; + animation.timeLine.reset(); + animation.timeLine.setDuration(m_duration); + animation.timeLine.setDirection(TimeLine::Forward); + animation.timeLine.setEasingCurve(QEasingCurve::Linear); + + const auto windows = effects->stackingOrder(); + auto parentIt = std::find_if(windows.constBegin(), windows.constEnd(), + [w](EffectWindow *p) { + return p->findModal() == w; + }); + if (parentIt != windows.constEnd()) { + animation.parentY = (*parentIt)->y(); + } + + w->setData(WindowAddedGrabRole, QVariant::fromValue(static_cast(this))); + + w->addRepaintFull(); +} + +void SheetEffect::slotWindowClosed(EffectWindow *w) +{ + if (effects->activeFullScreenEffect()) { + return; + } + + if (!isSheetWindow(w)) { + return; + } + + w->refWindow(); + + Animation &animation = m_animations[w]; + + animation.timeLine.reset(); + animation.parentY = 0; + animation.timeLine.setDuration(m_duration); + animation.timeLine.setDirection(TimeLine::Backward); + animation.timeLine.setEasingCurve(QEasingCurve::Linear); + + const auto windows = effects->stackingOrder(); + auto parentIt = std::find_if(windows.constBegin(), windows.constEnd(), + [w](EffectWindow *p) { + return p->findModal() == w; + }); + if (parentIt != windows.constEnd()) { + animation.parentY = (*parentIt)->y(); + } + + w->setData(WindowClosedGrabRole, QVariant::fromValue(static_cast(this))); + + w->addRepaintFull(); +} + +void SheetEffect::slotWindowDeleted(EffectWindow *w) +{ + m_animations.remove(w); +} + +bool SheetEffect::isSheetWindow(EffectWindow *w) const +{ + return w->isModal(); +} + +} // namespace KWin diff --git a/effects/sheet/sheet.h b/effects/sheet/sheet.h new file mode 100644 index 0000000..3a23657 --- /dev/null +++ b/effects/sheet/sheet.h @@ -0,0 +1,74 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Philip Falkner + SPDX-FileCopyrightText: 2009 Martin Gräßlin + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_SHEET_H +#define KWIN_SHEET_H + +// kwineffects +#include + +namespace KWin +{ + +class SheetEffect : public Effect +{ + Q_OBJECT + Q_PROPERTY(int duration READ duration) + +public: + SheetEffect(); + + void reconfigure(ReconfigureFlags flags) override; + + void prePaintScreen(ScreenPrePaintData &data, int time) override; + void prePaintWindow(EffectWindow *w, WindowPrePaintData &data, int time) override; + void paintWindow(EffectWindow *w, int mask, QRegion region, WindowPaintData &data) override; + void postPaintWindow(EffectWindow *w) override; + + bool isActive() const override; + int requestedEffectChainPosition() const override; + + static bool supported(); + + int duration() const; + +private Q_SLOTS: + void slotWindowAdded(EffectWindow *w); + void slotWindowClosed(EffectWindow *w); + void slotWindowDeleted(EffectWindow *w); + +private: + bool isSheetWindow(EffectWindow *w) const; + +private: + std::chrono::milliseconds m_duration; + + struct Animation { + TimeLine timeLine; + int parentY; + }; + + QHash m_animations; +}; + +inline int SheetEffect::requestedEffectChainPosition() const +{ + return 60; +} + +inline int SheetEffect::duration() const +{ + return m_duration.count(); +} + +} // namespace KWin + +#endif diff --git a/effects/sheet/sheet.kcfg b/effects/sheet/sheet.kcfg new file mode 100644 index 0000000..25574a5 --- /dev/null +++ b/effects/sheet/sheet.kcfg @@ -0,0 +1,12 @@ + + + + + + 0 + + + diff --git a/effects/sheet/sheetconfig.kcfgc b/effects/sheet/sheetconfig.kcfgc new file mode 100644 index 0000000..239872e --- /dev/null +++ b/effects/sheet/sheetconfig.kcfgc @@ -0,0 +1,5 @@ +File=sheet.kcfg +ClassName=SheetConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/effects/showfps/CMakeLists.txt b/effects/showfps/CMakeLists.txt new file mode 100644 index 0000000..e217e68 --- /dev/null +++ b/effects/showfps/CMakeLists.txt @@ -0,0 +1,24 @@ +####################################### +# Config +set(kwin_showfps_config_SRCS showfps_config.cpp) +ki18n_wrap_ui(kwin_showfps_config_SRCS showfps_config.ui) +kconfig_add_kcfg_files(kwin_showfps_config_SRCS showfpsconfig.kcfgc) + +add_library(kwin_showfps_config MODULE ${kwin_showfps_config_SRCS}) + +target_link_libraries(kwin_showfps_config + KF5::Completion + KF5::ConfigWidgets + KF5::I18n + Qt5::DBus + KWinEffectsInterface +) + +kcoreaddons_desktop_to_json(kwin_showfps_config showfps_config.desktop SERVICE_TYPES kcmodule.desktop) + +install( + TARGETS + kwin_showfps_config + DESTINATION + ${PLUGIN_INSTALL_DIR}/kwin/effects/configs +) diff --git a/effects/showfps/showfps.cpp b/effects/showfps/showfps.cpp new file mode 100644 index 0000000..0551035 --- /dev/null +++ b/effects/showfps/showfps.cpp @@ -0,0 +1,540 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "showfps.h" + +// KConfigSkeleton +#include "showfpsconfig.h" + +#include + +#include +#ifdef KWIN_HAVE_XRENDER_COMPOSITING +#include +#include +#endif + +#include + +#include +#include +#include + +#include + +namespace KWin +{ + +const int FPS_WIDTH = 10; +const int MAX_TIME = 100; + +ShowFpsEffect::ShowFpsEffect() + : paints_pos(0) + , frames_pos(0) + , m_noBenchmark(effects->effectFrame(EffectFrameUnstyled, false)) +{ + initConfig(); + for (int i = 0; + i < NUM_PAINTS; + ++i) { + paints[ i ] = 0; + paint_size[ i ] = 0; + } + for (int i = 0; + i < MAX_FPS; + ++i) + frames[ i ] = 0; + m_noBenchmark->setAlignment(Qt::AlignTop | Qt::AlignRight); + m_noBenchmark->setText(i18n("This effect is not a benchmark")); + reconfigure(ReconfigureAll); +} + +void ShowFpsEffect::reconfigure(ReconfigureFlags) +{ + ShowFpsConfig::self()->read(); + alpha = ShowFpsConfig::alpha(); + x = ShowFpsConfig::x(); + y = ShowFpsConfig::y(); + const QSize screenSize = effects->virtualScreenSize(); + if (x == -10000) // there's no -0 :( + x = screenSize.width() - 2 * NUM_PAINTS - FPS_WIDTH; + else if (x < 0) + x = screenSize.width() - 2 * NUM_PAINTS - FPS_WIDTH - x; + if (y == -10000) + y = screenSize.height() - MAX_TIME; + else if (y < 0) + y = screenSize.height() - MAX_TIME - y; + fps_rect = QRect(x, y, FPS_WIDTH + 2 * NUM_PAINTS, MAX_TIME); + m_noBenchmark->setPosition(fps_rect.bottomRight() + QPoint(-6, 6)); + + int textPosition = ShowFpsConfig::textPosition(); + textFont = ShowFpsConfig::textFont(); + textColor = ShowFpsConfig::textColor(); + double textAlpha = ShowFpsConfig::textAlpha(); + + if (!textColor.isValid()) + textColor = QPalette().color(QPalette::Active, QPalette::WindowText); + textColor.setAlphaF(textAlpha); + + switch(textPosition) { + case TOP_LEFT: + fpsTextRect = QRect(0, 0, 100, 100); + textAlign = Qt::AlignTop | Qt::AlignLeft; + break; + case TOP_RIGHT: + fpsTextRect = QRect(screenSize.width() - 100, 0, 100, 100); + textAlign = Qt::AlignTop | Qt::AlignRight; + break; + case BOTTOM_LEFT: + fpsTextRect = QRect(0, screenSize.height() - 100, 100, 100); + textAlign = Qt::AlignBottom | Qt::AlignLeft; + break; + case BOTTOM_RIGHT: + fpsTextRect = QRect(screenSize.width() - 100, screenSize.height() - 100, 100, 100); + textAlign = Qt::AlignBottom | Qt::AlignRight; + break; + case NOWHERE: + fpsTextRect = QRect(); + break; + case INSIDE_GRAPH: + default: + fpsTextRect = QRect(x, y, FPS_WIDTH + NUM_PAINTS, MAX_TIME); + textAlign = Qt::AlignTop | Qt::AlignRight; + break; + } +} + +void ShowFpsEffect::prePaintScreen(ScreenPrePaintData& data, int time) +{ + frames[ frames_pos ] = QDateTime::currentMSecsSinceEpoch(); + if (++frames_pos == MAX_FPS) + frames_pos = 0; + effects->prePaintScreen(data, time); + data.paint += fps_rect; + + paint_size[ paints_pos ] = 0; + t.restart(); +} + +void ShowFpsEffect::paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data) +{ + effects->paintWindow(w, mask, region, data); + + // Take intersection of region and actual window's rect, minus the fps area + // (since we keep repainting it) and count the pixels. + QRegion r2 = region & QRect(w->x(), w->y(), w->width(), w->height()); + r2 -= fps_rect; + int winsize = 0; + for (const QRect &r : r2) { + winsize += r.width() * r.height(); + } + paint_size[ paints_pos ] += winsize; +} + +void ShowFpsEffect::paintScreen(int mask, const QRegion ®ion, ScreenPaintData& data) +{ + effects->paintScreen(mask, region, data); + int lastFrame = frames_pos - 1; + if (lastFrame < 0) + lastFrame = MAX_FPS - 1; + const qint64 lastTimestamp = frames[lastFrame]; + int fps = 0; + for (int i = 0; + i < MAX_FPS; + ++i) + if (abs(lastTimestamp - frames[ i ]) < 1000) + ++fps; // count all frames in the last second + if (fps > MAX_TIME) + fps = MAX_TIME; // keep it the same height + if (effects->isOpenGLCompositing()) { + paintGL(fps, data.projectionMatrix()); + glFinish(); // make sure all rendering is done + } +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + if (effects->compositingType() == XRenderCompositing) { + paintXrender(fps); + xcb_flush(xcbConnection()); // make sure all rendering is done + } +#endif + if (effects->compositingType() == QPainterCompositing) { + paintQPainter(fps); + } + m_noBenchmark->render(infiniteRegion(), 1.0, alpha); +} + +void ShowFpsEffect::paintGL(int fps, const QMatrix4x4 &projectionMatrix) +{ + int x = this->x; + int y = this->y; + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + // TODO painting first the background white and then the contents + // means that the contents also blend with the background, I guess + ShaderBinder binder(ShaderTrait::UniformColor); + binder.shader()->setUniform(GLShader::ModelViewProjectionMatrix, projectionMatrix); + GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer(); + vbo->reset(); + QColor color(255, 255, 255); + color.setAlphaF(alpha); + vbo->setColor(color); + QVector verts; + verts.reserve(12); + verts << x + 2 * NUM_PAINTS + FPS_WIDTH << y; + verts << x << y; + verts << x << y + MAX_TIME; + verts << x << y + MAX_TIME; + verts << x + 2 * NUM_PAINTS + FPS_WIDTH << y + MAX_TIME; + verts << x + 2 * NUM_PAINTS + FPS_WIDTH << y; + vbo->setData(6, 2, verts.constData(), nullptr); + vbo->render(GL_TRIANGLES); + y += MAX_TIME; // paint up from the bottom + color.setRed(0); + color.setGreen(0); + vbo->setColor(color); + verts.clear(); + verts << x + FPS_WIDTH << y - fps; + verts << x << y - fps; + verts << x << y; + verts << x << y; + verts << x + FPS_WIDTH << y; + verts << x + FPS_WIDTH << y - fps; + vbo->setData(6, 2, verts.constData(), nullptr); + vbo->render(GL_TRIANGLES); + + + color.setBlue(0); + vbo->setColor(color); + QVector vertices; + for (int i = 10; + i < MAX_TIME; + i += 10) { + vertices << x << y - i; + vertices << x + FPS_WIDTH << y - i; + } + vbo->setData(vertices.size() / 2, 2, vertices.constData(), nullptr); + vbo->render(GL_LINES); + x += FPS_WIDTH; + + // Paint FPS graph + paintFPSGraph(x, y); + x += NUM_PAINTS; + + // Paint amount of rendered pixels graph + paintDrawSizeGraph(x, y); + + // Paint FPS numerical value + if (fpsTextRect.isValid()) { + fpsText.reset(new GLTexture(fpsTextImage(fps))); + fpsText->bind(); + ShaderBinder binder(ShaderTrait::MapTexture); + QMatrix4x4 mvp = projectionMatrix; + mvp.translate(fpsTextRect.x(), fpsTextRect.y()); + binder.shader()->setUniform(GLShader::ModelViewProjectionMatrix, mvp); + fpsText->render(QRegion(fpsTextRect), fpsTextRect); + fpsText->unbind(); + effects->addRepaint(fpsTextRect); + } + + // Paint paint sizes + glDisable(GL_BLEND); +} + +#ifdef KWIN_HAVE_XRENDER_COMPOSITING +/* + Differences between OpenGL and XRender: + - differently specified rectangles (X: width/height, O: x2,y2) + - XRender uses pre-multiplied alpha +*/ +void ShowFpsEffect::paintXrender(int fps) +{ + xcb_pixmap_t pixmap = xcb_generate_id(xcbConnection()); + xcb_create_pixmap(xcbConnection(), 32, pixmap, x11RootWindow(), FPS_WIDTH, MAX_TIME); + XRenderPicture p(pixmap, 32); + xcb_free_pixmap(xcbConnection(), pixmap); + xcb_render_color_t col; + col.alpha = int(alpha * 0xffff); + col.red = int(alpha * 0xffff); // white + col.green = int(alpha * 0xffff); + col.blue = int(alpha * 0xffff); + xcb_rectangle_t rect = {0, 0, FPS_WIDTH, MAX_TIME}; + xcb_render_fill_rectangles(xcbConnection(), XCB_RENDER_PICT_OP_SRC, p, col, 1, &rect); + col.red = 0; // blue + col.green = 0; + col.blue = int(alpha * 0xffff); + rect.y = MAX_TIME - fps; + rect.width = FPS_WIDTH; + rect.height = fps; + xcb_render_fill_rectangles(xcbConnection(), XCB_RENDER_PICT_OP_SRC, p, col, 1, &rect); + col.red = 0; // black + col.green = 0; + col.blue = 0; + QVector rects; + for (int i = 10; + i < MAX_TIME; + i += 10) { + xcb_rectangle_t rect = {0, int16_t(MAX_TIME - i), uint16_t(FPS_WIDTH), 1}; + rects << rect; + } + xcb_render_fill_rectangles(xcbConnection(), XCB_RENDER_PICT_OP_SRC, p, col, rects.count(), rects.constData()); + xcb_render_composite(xcbConnection(), alpha != 1.0 ? XCB_RENDER_PICT_OP_OVER : XCB_RENDER_PICT_OP_SRC, p, XCB_RENDER_PICTURE_NONE, + effects->xrenderBufferPicture(), 0, 0, 0, 0, x, y, FPS_WIDTH, MAX_TIME); + + + // Paint FPS graph + paintFPSGraph(x + FPS_WIDTH, y); + + // Paint amount of rendered pixels graph + paintDrawSizeGraph(x + FPS_WIDTH + MAX_TIME, y); + + // Paint FPS numerical value + if (fpsTextRect.isValid()) { + QImage textImg(fpsTextImage(fps)); + XRenderPicture textPic(textImg); + xcb_render_composite(xcbConnection(), XCB_RENDER_PICT_OP_OVER, textPic, XCB_RENDER_PICTURE_NONE, + effects->xrenderBufferPicture(), 0, 0, 0, 0, fpsTextRect.x(), fpsTextRect.y(), textImg.width(), textImg.height()); + effects->addRepaint(fpsTextRect); + } +} +#endif + +void ShowFpsEffect::paintQPainter(int fps) +{ + QPainter *painter = effects->scenePainter(); + painter->save(); + + QColor color(255, 255, 255); + color.setAlphaF(alpha); + + painter->setCompositionMode(QPainter::CompositionMode_SourceOver); + painter->fillRect(x, y, 2 * NUM_PAINTS + FPS_WIDTH, MAX_TIME, color); + color.setRed(0); + color.setGreen(0); + painter->fillRect(x, y + MAX_TIME - fps, FPS_WIDTH, fps, color); + + color.setBlue(0); + for (int i = 10; i < MAX_TIME; i += 10) { + painter->setPen(color); + painter->drawLine(x, y + MAX_TIME - i, x + FPS_WIDTH, y + MAX_TIME - i); + } + + // Paint FPS graph + paintFPSGraph(x + FPS_WIDTH, y + MAX_TIME - 1); + + // Paint amount of rendered pixels graph + paintDrawSizeGraph(x + FPS_WIDTH + NUM_PAINTS, y + MAX_TIME - 1); + + // Paint FPS numerical value + painter->setPen(Qt::black); + painter->drawText(fpsTextRect, textAlign, QString::number(fps)); + + painter->restore(); +} + +void ShowFpsEffect::paintFPSGraph(int x, int y) +{ + // Paint FPS graph + QList lines; + lines << 10 << 20 << 50; + QList values; + for (int i = 0; + i < NUM_PAINTS; + ++i) { + values.append(paints[(i + paints_pos) % NUM_PAINTS ]); + } + paintGraph(x, y, values, lines, true); +} + +void ShowFpsEffect::paintDrawSizeGraph(int x, int y) +{ + int max_drawsize = 0; + for (int i = 0; i < NUM_PAINTS; i++) + max_drawsize = qMax(max_drawsize, paint_size[ i ]); + + // Log of min/max values shown on graph + const float max_pixels_log = 7.2f; + const float min_pixels_log = 2.0f; + const int minh = 5; // Minimum height of the bar when value > 0 + + float drawscale = (MAX_TIME - minh) / (max_pixels_log - min_pixels_log); + QList drawlines; + + for (int logh = (int)min_pixels_log; logh <= max_pixels_log; logh++) + drawlines.append((int)((logh - min_pixels_log) * drawscale) + minh); + + QList drawvalues; + for (int i = 0; + i < NUM_PAINTS; + ++i) { + int value = paint_size[(i + paints_pos) % NUM_PAINTS ]; + int h = 0; + if (value > 0) { + h = (int)((log10((double)value) - min_pixels_log) * drawscale); + h = qMin(qMax(0, h) + minh, MAX_TIME); + } + drawvalues.append(h); + } + paintGraph(x, y, drawvalues, drawlines, false); +} + +void ShowFpsEffect::paintGraph(int x, int y, QList values, QList lines, bool colorize) +{ + if (effects->isOpenGLCompositing()) { + QColor color(0, 0, 0); + color.setAlphaF(alpha); + GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer(); + vbo->reset(); + vbo->setColor(color); + QVector verts; + // First draw the lines + foreach (int h, lines) { + verts << x << y - h; + verts << x + values.count() << y - h; + } + vbo->setData(verts.size() / 2, 2, verts.constData(), nullptr); + vbo->render(GL_LINES); + // Then the graph values + int lastValue = 0; + verts.clear(); + for (int i = 0; i < values.count(); i++) { + int value = values[ i ]; + if (colorize && value != lastValue) { + if (!verts.isEmpty()) { + vbo->setData(verts.size() / 2, 2, verts.constData(), nullptr); + vbo->render(GL_LINES); + } + verts.clear(); + if (value <= 10) { + color = QColor(0, 255, 0); + } else if (value <= 20) { + color = QColor(255, 255, 0); + } else if (value <= 50) { + color = QColor(255, 0, 0); + } else { + color = QColor(0, 0, 0); + } + vbo->setColor(color); + } + verts << x + values.count() - i << y; + verts << x + values.count() - i << y - value; + lastValue = value; + } + if (!verts.isEmpty()) { + vbo->setData(verts.size() / 2, 2, verts.constData(), nullptr); + vbo->render(GL_LINES); + } + } +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + if (effects->compositingType() == XRenderCompositing) { + xcb_pixmap_t pixmap = xcb_generate_id(xcbConnection()); + xcb_create_pixmap(xcbConnection(), 32, pixmap, x11RootWindow(), values.count(), MAX_TIME); + XRenderPicture p(pixmap, 32); + xcb_free_pixmap(xcbConnection(), pixmap); + xcb_render_color_t col; + col.alpha = int(alpha * 0xffff); + + // Draw background + col.red = col.green = col.blue = int(alpha * 0xffff); // white + xcb_rectangle_t rect = {0, 0, uint16_t(values.count()), uint16_t(MAX_TIME)}; + xcb_render_fill_rectangles(xcbConnection(), XCB_RENDER_PICT_OP_SRC, p, col, 1, &rect); + + // Then the values + col.red = col.green = col.blue = int(alpha * 0x8000); // grey + for (int i = 0; i < values.count(); i++) { + int value = values[ i ]; + if (colorize) { + if (value <= 10) { + // green + col.red = 0; + col.green = int(alpha * 0xffff); + col.blue = 0; + } else if (value <= 20) { + // yellow + col.red = int(alpha * 0xffff); + col.green = int(alpha * 0xffff); + col.blue = 0; + } else if (value <= 50) { + // red + col.red = int(alpha * 0xffff); + col.green = 0; + col.blue = 0; + } else { + // black + col.red = 0; + col.green = 0; + col.blue = 0; + } + } + xcb_rectangle_t rect = {int16_t(values.count() - i), int16_t(MAX_TIME - value), 1, uint16_t(value)}; + xcb_render_fill_rectangles(xcbConnection(), XCB_RENDER_PICT_OP_SRC, p, col, 1, &rect); + } + + // Then the lines + col.red = col.green = col.blue = 0; // black + QVector rects; + foreach (int h, lines) { + xcb_rectangle_t rect = {0, int16_t(MAX_TIME - h), uint16_t(values.count()), 1}; + rects << rect; + } + xcb_render_fill_rectangles(xcbConnection(), XCB_RENDER_PICT_OP_SRC, p, col, rects.count(), rects.constData()); + + // Finally render the pixmap onto screen + xcb_render_composite(xcbConnection(), alpha != 1.0 ? XCB_RENDER_PICT_OP_OVER : XCB_RENDER_PICT_OP_SRC, p, + XCB_RENDER_PICTURE_NONE, effects->xrenderBufferPicture(), 0, 0, 0, 0, x, y, values.count(), MAX_TIME); + } +#endif + if (effects->compositingType() == QPainterCompositing) { + QPainter *painter = effects->scenePainter(); + painter->setPen(Qt::black); + // First draw the lines + foreach (int h, lines) { + painter->drawLine(x, y - h, x + values.count(), y - h); + } + QColor color(0, 0, 0); + color.setAlphaF(alpha); + for (int i = 0; i < values.count(); i++) { + int value = values[ i ]; + if (colorize) { + if (value <= 10) { + color = QColor(0, 255, 0); + } else if (value <= 20) { + color = QColor(255, 255, 0); + } else if (value <= 50) { + color = QColor(255, 0, 0); + } else { + color = QColor(0, 0, 0); + } + } + painter->setPen(color); + painter->drawLine(x + values.count() - i, y, x + values.count() - i, y - value); + } + } +} + +void ShowFpsEffect::postPaintScreen() +{ + effects->postPaintScreen(); + paints[ paints_pos ] = t.elapsed(); + if (++paints_pos == NUM_PAINTS) + paints_pos = 0; + effects->addRepaint(fps_rect); +} + +QImage ShowFpsEffect::fpsTextImage(int fps) +{ + QImage im(100, 100, QImage::Format_ARGB32); + im.fill(Qt::transparent); + QPainter painter(&im); + painter.setFont(textFont); + painter.setPen(textColor); + painter.drawText(QRect(0, 0, 100, 100), textAlign, QString::number(fps)); + painter.end(); + return im; +} + +} // namespace diff --git a/effects/showfps/showfps.h b/effects/showfps/showfps.h new file mode 100644 index 0000000..e82ec84 --- /dev/null +++ b/effects/showfps/showfps.h @@ -0,0 +1,98 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_SHOWFPS_H +#define KWIN_SHOWFPS_H + +#include +#include + +#include + + +namespace KWin +{ +class GLTexture; + +class ShowFpsEffect + : public Effect +{ + Q_OBJECT + Q_PROPERTY(qreal alpha READ configuredAlpha) + Q_PROPERTY(int x READ configuredX) + Q_PROPERTY(int y READ configuredY) + Q_PROPERTY(QRect fpsTextRect READ configuredFpsTextRect) + Q_PROPERTY(int textAlign READ configuredTextAlign) + Q_PROPERTY(QFont textFont READ configuredTextFont) + Q_PROPERTY(QColor textColor READ configuredTextColor) +public: + ShowFpsEffect(); + void reconfigure(ReconfigureFlags) override; + void prePaintScreen(ScreenPrePaintData& data, int time) override; + void paintScreen(int mask, const QRegion ®ion, ScreenPaintData& data) override; + void paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data) override; + void postPaintScreen() override; + enum { INSIDE_GRAPH, NOWHERE, TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT }; // fps text position + + // for properties + qreal configuredAlpha() const { + return alpha; + } + int configuredX() const { + return x; + } + int configuredY() const { + return y; + } + QRect configuredFpsTextRect() const { + return fpsTextRect; + } + int configuredTextAlign() const { + return textAlign; + } + QFont configuredTextFont() const { + return textFont; + } + QColor configuredTextColor() const { + return textColor; + } +private: + void paintGL(int fps, const QMatrix4x4 &projectionMatrix); +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + void paintXrender(int fps); +#endif + void paintQPainter(int fps); + void paintFPSGraph(int x, int y); + void paintDrawSizeGraph(int x, int y); + void paintGraph(int x, int y, QList values, QList lines, bool colorize); + QImage fpsTextImage(int fps); + QElapsedTimer t; + enum { NUM_PAINTS = 100 }; // remember time needed to paint this many paints + int paints[ NUM_PAINTS ]; // time needed to paint + int paint_size[ NUM_PAINTS ]; // number of pixels painted + int paints_pos; // position in the queue + enum { MAX_FPS = 200 }; + qint64 frames[ MAX_FPS ]; // the time when the frame was done + int frames_pos; // position in the queue + double alpha; + int x; + int y; + QRect fps_rect; + QScopedPointer fpsText; + int textPosition; + QFont textFont; + QColor textColor; + QRect fpsTextRect; + int textAlign; + QScopedPointer m_noBenchmark; +}; + +} // namespace + +#endif diff --git a/effects/showfps/showfps.kcfg b/effects/showfps/showfps.kcfg new file mode 100644 index 0000000..b0cd463 --- /dev/null +++ b/effects/showfps/showfps.kcfg @@ -0,0 +1,28 @@ + + + + + + 0 + + + + invalid + + + 1.0 + + + 0.5 + + + -10000 + + + 0 + + + diff --git a/effects/showfps/showfps_config.cpp b/effects/showfps/showfps_config.cpp new file mode 100644 index 0000000..1c5f9a3 --- /dev/null +++ b/effects/showfps/showfps_config.cpp @@ -0,0 +1,56 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Rivo Laks + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "showfps_config.h" + +// KConfigSkeleton +#include "showfpsconfig.h" +#include +#include + +#include +#include +#include + +K_PLUGIN_FACTORY_WITH_JSON(ShowFpsEffectConfigFactory, + "showfps_config.json", + registerPlugin();) + +namespace KWin +{ + +ShowFpsEffectConfig::ShowFpsEffectConfig(QWidget* parent, const QVariantList& args) : + KCModule(KAboutData::pluginData(QStringLiteral("showfps")), parent, args) +{ + m_ui = new Ui::ShowFpsEffectConfigForm; + m_ui->setupUi(this); + + ShowFpsConfig::instance(KWIN_CONFIG); + addConfig(ShowFpsConfig::self(), this); + + load(); +} + +ShowFpsEffectConfig::~ShowFpsEffectConfig() +{ + delete m_ui; +} + +void ShowFpsEffectConfig::save() +{ + KCModule::save(); + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("showfps")); +} + +} // namespace + +#include "showfps_config.moc" diff --git a/effects/showfps/showfps_config.desktop b/effects/showfps/showfps_config.desktop new file mode 100644 index 0000000..13da638 --- /dev/null +++ b/effects/showfps/showfps_config.desktop @@ -0,0 +1,86 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=KCModule + +X-KDE-Library=kwin_showfps_config +X-KDE-ParentComponents=showfps + +Name=Show FPS +Name[af]=Wys FPS +Name[ar]=أظهر عدد الإطارات بالثانية +Name[az]=Saniyədəki kadrlar +Name[be@latin]=VyjaÅ­leńnie „FPS” +Name[bg]=Показване на кад/сек +Name[bn_IN]=FPS প্রদর্শন করা হবে +Name[bs]=Kadrovi/sekundi +Name[ca]=Mostra els FPS +Name[ca@valencia]=Mostra els FPS +Name[cs]=Zobrazit FPS +Name[csb]=Wëskrzëni FPS +Name[da]=Vis FPS +Name[de]=Bilder pro Sekunde anzeigen +Name[el]=Εμφάνιση καρέ ανά δευτερόλεπτο +Name[en_GB]=Show FPS +Name[eo]=Montri FPS +Name[es]=Muestra FPS +Name[et]=FPS-i näitamine +Name[eu]=Erakutsi FPS +Name[fa]=نمایش FPS +Name[fi]=FPS-näyttö +Name[fr]=Afficher le nombre de trames par seconde +Name[fy]=FPS sjen litte +Name[ga]=Taispeáin FSS +Name[gl]=Mostrar os FPS +Name[gu]=FPS બતાવો +Name[he]=הצג מספר תמונות לשנייה +Name[hi]=एफ़पीएस दिखायें +Name[hne]=एफपीएस देखाव +Name[hr]=Prikaz FPS-a (broj okvira po sekundi) +Name[hu]=Képkockaszámláló +Name[ia]=Monstra FPS +Name[id]=Tampilkan FPS +Name[is]=Sýna FPS (rammar á sekúndu) +Name[it]=Mostra fotogrammi al secondo +Name[ja]=フレームレート (FPS) 表示 +Name[kk]=FPS-ты көрсету +Name[km]=បង្ហាញ FPS +Name[kn]=FPS ತೋರಿಸು +Name[ko]=FPS 표시 +Name[ku]=FPS'ê nîşan bide +Name[lt]=Rodyti kadr./sek. +Name[lv]=RādÄ«t kadrus/sek. +Name[mai]=एफपीएस देखाबू +Name[mk]=Прикажи рамки/сек +Name[ml]=എഫ്‌പിഎസ് കാണിക്കുക +Name[mr]=एफपीएस दर्शवा +Name[nb]=Vis FPS +Name[nds]=Bps wiesen +Name[ne]=एफपीएस देखाउनुहोस् +Name[nl]=FPS tonen +Name[nn]=Vis talet pÃ¥ bilete per sekund +Name[pa]=FPS ਵੇਖੋ +Name[pl]=Pokaż ilość klatek na sekundę +Name[pt]=Mostrar as IPS +Name[pt_BR]=Mostrar FPS +Name[ro]=Arată CPS +Name[ru]=График производительности +Name[se]=Čájet rámmaid sekunddas +Name[si]=FPS පෙන්වන්න +Name[sk]=ZobraziÅ¥ FPS +Name[sl]=Prikaži sličice na sekundo +Name[sr]=Кадрови/секунди +Name[sr@ijekavian]=Кадрови/секунди +Name[sr@ijekavianlatin]=Kadrovi/sekundi +Name[sr@latin]=Kadrovi/sekundi +Name[sv]=Visa ramar/s +Name[ta]=Show FPS +Name[te]=FPS ను చూపుము +Name[th]=แสดงอัตราเฟรมต่อวินาที +Name[tr]=FPS Göster +Name[ug]=FPS نى كۆرسەت +Name[uk]=Показ частоти кадрів +Name[vi]=Hiện FPS +Name[wa]=Mostrer FPS +Name[x-test]=xxShow FPSxx +Name[zh_CN]=显示 FPS +Name[zh_TW]=顯示 FPS 畫格數 diff --git a/effects/showfps/showfps_config.h b/effects/showfps/showfps_config.h new file mode 100644 index 0000000..2bff6ce --- /dev/null +++ b/effects/showfps/showfps_config.h @@ -0,0 +1,36 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Rivo Laks + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_SHOWFPS_CONFIG_H +#define KWIN_SHOWFPS_CONFIG_H + +#include + +#include "ui_showfps_config.h" + +namespace KWin +{ + +class ShowFpsEffectConfig : public KCModule +{ + Q_OBJECT +public: + explicit ShowFpsEffectConfig(QWidget* parent = nullptr, const QVariantList& args = QVariantList()); + ~ShowFpsEffectConfig() override; + +public Q_SLOTS: + void save() override; + +private: + Ui::ShowFpsEffectConfigForm *m_ui; +}; + +} // namespace + +#endif diff --git a/effects/showfps/showfps_config.ui b/effects/showfps/showfps_config.ui new file mode 100644 index 0000000..0a5eb31 --- /dev/null +++ b/effects/showfps/showfps_config.ui @@ -0,0 +1,175 @@ + + + KWin::ShowFpsEffectConfigForm + + + + 0 + 0 + 356 + 180 + + + + + + + Text + + + + + + Text position: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_TextPosition + + + + + + + + 0 + 0 + + + + + Inside Graph + + + + + Nowhere + + + + + Top Left + + + + + Top Right + + + + + Bottom Left + + + + + Bottom Right + + + + + + + + Text font: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + + + + Text color: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_TextColor + + + + + + + + 0 + 0 + + + + + + + + Text alpha: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_TextAlpha + + + + + + + + 0 + 0 + + + + 2 + + + 1.000000000000000 + + + 0.100000000000000 + + + 1.000000000000000 + + + + + + + + + + + KColorButton + QPushButton +
kcolorbutton.h
+
+ + KFontRequester + QWidget +
kfontrequester.h
+
+ + KComboBox + QComboBox +
kcombobox.h
+
+
+ + +
diff --git a/effects/showfps/showfpsconfig.kcfgc b/effects/showfps/showfpsconfig.kcfgc new file mode 100644 index 0000000..c08427e --- /dev/null +++ b/effects/showfps/showfpsconfig.kcfgc @@ -0,0 +1,5 @@ +File=showfps.kcfg +ClassName=ShowFpsConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/effects/showpaint/CMakeLists.txt b/effects/showpaint/CMakeLists.txt new file mode 100644 index 0000000..db12760 --- /dev/null +++ b/effects/showpaint/CMakeLists.txt @@ -0,0 +1,24 @@ +####################################### + +# Config +set(kwin_showpaint_config_SRCS showpaint_config.cpp) +ki18n_wrap_ui(kwin_showpaint_config_SRCS showpaint_config.ui) + +add_library(kwin_showpaint_config MODULE ${kwin_showpaint_config_SRCS}) + +target_link_libraries(kwin_showpaint_config + KF5::ConfigWidgets + KF5::GlobalAccel + KF5::I18n + KF5::XmlGui +) + +kcoreaddons_desktop_to_json(kwin_showpaint_config showpaint_config.desktop SERVICE_TYPES kcmodule.desktop) + +install( + TARGETS + kwin_showpaint_config + + DESTINATION + ${PLUGIN_INSTALL_DIR}/kwin/effects/configs +) diff --git a/effects/showpaint/showpaint.cpp b/effects/showpaint/showpaint.cpp new file mode 100644 index 0000000..ad8cc5a --- /dev/null +++ b/effects/showpaint/showpaint.cpp @@ -0,0 +1,142 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Lubos Lunak + SPDX-FileCopyrightText: 2010 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "showpaint.h" + +#include +#ifdef KWIN_HAVE_XRENDER_COMPOSITING +#include +#endif + +#include +#include + +#include +#include + +namespace KWin +{ + +static const qreal s_alpha = 0.2; +static const QVector s_colors { + Qt::red, + Qt::green, + Qt::blue, + Qt::cyan, + Qt::magenta, + Qt::yellow, + Qt::gray +}; + +ShowPaintEffect::ShowPaintEffect() +{ + auto *toggleAction = new QAction(this); + toggleAction->setObjectName(QStringLiteral("Toggle")); + toggleAction->setText(i18n("Toggle Show Paint")); + KGlobalAccel::self()->setDefaultShortcut(toggleAction, {}); + KGlobalAccel::self()->setShortcut(toggleAction, {}); + effects->registerGlobalShortcut({}, toggleAction); + + connect(toggleAction, &QAction::triggered, this, &ShowPaintEffect::toggle); +} + +void ShowPaintEffect::paintScreen(int mask, const QRegion ®ion, ScreenPaintData &data) +{ + m_painted = QRegion(); + effects->paintScreen(mask, region, data); + if (effects->isOpenGLCompositing()) { + paintGL(data.projectionMatrix()); + } +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + if (effects->compositingType() == XRenderCompositing) { + paintXrender(); + } +#endif + if (effects->compositingType() == QPainterCompositing) { + paintQPainter(); + } + if (++m_colorIndex == s_colors.count()) { + m_colorIndex = 0; + } +} + +void ShowPaintEffect::paintWindow(EffectWindow *w, int mask, QRegion region, WindowPaintData &data) +{ + m_painted |= region; + effects->paintWindow(w, mask, region, data); +} + +void ShowPaintEffect::paintGL(const QMatrix4x4 &projection) +{ + GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer(); + vbo->reset(); + vbo->setUseColor(true); + ShaderBinder binder(ShaderTrait::UniformColor); + binder.shader()->setUniform(GLShader::ModelViewProjectionMatrix, projection); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + QColor color = s_colors[m_colorIndex]; + color.setAlphaF(s_alpha); + vbo->setColor(color); + QVector verts; + verts.reserve(m_painted.rectCount() * 12); + for (const QRect &r : m_painted) { + verts << r.x() + r.width() << r.y(); + verts << r.x() << r.y(); + verts << r.x() << r.y() + r.height(); + verts << r.x() << r.y() + r.height(); + verts << r.x() + r.width() << r.y() + r.height(); + verts << r.x() + r.width() << r.y(); + } + vbo->setData(verts.count() / 2, 2, verts.data(), nullptr); + vbo->render(GL_TRIANGLES); + glDisable(GL_BLEND); +} + +void ShowPaintEffect::paintXrender() +{ +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + xcb_render_color_t col; + const QColor &color = s_colors[m_colorIndex]; + col.alpha = int(s_alpha * 0xffff); + col.red = int(s_alpha * 0xffff * color.red() / 255); + col.green = int(s_alpha * 0xffff * color.green() / 255); + col.blue = int(s_alpha * 0xffff * color.blue() / 255); + QVector rects; + rects.reserve(m_painted.rectCount()); + for (const QRect &r : m_painted) { + xcb_rectangle_t rect = {int16_t(r.x()), int16_t(r.y()), uint16_t(r.width()), uint16_t(r.height())}; + rects << rect; + } + xcb_render_fill_rectangles(xcbConnection(), XCB_RENDER_PICT_OP_OVER, effects->xrenderBufferPicture(), col, rects.count(), rects.constData()); +#endif +} + +void ShowPaintEffect::paintQPainter() +{ + QColor color = s_colors[m_colorIndex]; + color.setAlphaF(s_alpha); + for (const QRect &r : m_painted) { + effects->scenePainter()->fillRect(r, color); + } +} + +bool ShowPaintEffect::isActive() const +{ + return m_active; +} + +void ShowPaintEffect::toggle() +{ + m_active = !m_active; + effects->addRepaintFull(); +} + +} // namespace KWin diff --git a/effects/showpaint/showpaint.h b/effects/showpaint/showpaint.h new file mode 100644 index 0000000..eac9a0f --- /dev/null +++ b/effects/showpaint/showpaint.h @@ -0,0 +1,45 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_SHOWPAINT_H +#define KWIN_SHOWPAINT_H + +#include + +namespace KWin +{ + +class ShowPaintEffect : public Effect +{ + Q_OBJECT + +public: + ShowPaintEffect(); + + void paintScreen(int mask, const QRegion ®ion, ScreenPaintData &data) override; + void paintWindow(EffectWindow *w, int mask, QRegion region, WindowPaintData &data) override; + + bool isActive() const override; + +private Q_SLOTS: + void toggle(); + +private: + void paintGL(const QMatrix4x4 &projection); + void paintXrender(); + void paintQPainter(); + + bool m_active = false; + QRegion m_painted; // what's painted in one pass + int m_colorIndex = 0; +}; + +} // namespace KWin + +#endif diff --git a/effects/showpaint/showpaint_config.cpp b/effects/showpaint/showpaint_config.cpp new file mode 100644 index 0000000..80af1a2 --- /dev/null +++ b/effects/showpaint/showpaint_config.cpp @@ -0,0 +1,76 @@ +/* + 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 "showpaint_config.h" + +#include +#include +#include +#include +#include +#include + +#include + +K_PLUGIN_FACTORY_WITH_JSON(ShowPaintEffectConfigFactory, + "showpaint_config.json", + registerPlugin();) + +namespace KWin +{ + +ShowPaintEffectConfig::ShowPaintEffectConfig(QWidget *parent, const QVariantList &args) + : KCModule(KAboutData::pluginData(QStringLiteral("showpaint")), parent, args) + , m_ui(new Ui::ShowPaintEffectConfig) +{ + m_ui->setupUi(this); + + auto *actionCollection = new KActionCollection(this, QStringLiteral("kwin")); + + actionCollection->setComponentDisplayName(i18n("KWin")); + actionCollection->setConfigGroup(QStringLiteral("ShowPaint")); + actionCollection->setConfigGlobal(true); + + QAction *toggleAction = actionCollection->addAction(QStringLiteral("Toggle")); + toggleAction->setText(i18n("Toggle Show Paint")); + toggleAction->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(toggleAction, {}); + KGlobalAccel::self()->setShortcut(toggleAction, {}); + + m_ui->shortcutsEditor->addCollection(actionCollection); + + connect(m_ui->shortcutsEditor, &KShortcutsEditor::keyChange, + this, &ShowPaintEffectConfig::markAsChanged); + + load(); +} + +ShowPaintEffectConfig::~ShowPaintEffectConfig() +{ + // If save() is called, undoChanges() has no effect. + m_ui->shortcutsEditor->undoChanges(); + + delete m_ui; +} + +void ShowPaintEffectConfig::save() +{ + KCModule::save(); + m_ui->shortcutsEditor->save(); +} + +void ShowPaintEffectConfig::defaults() +{ + m_ui->shortcutsEditor->allDefault(); + KCModule::defaults(); +} + +} // namespace KWin + +#include "showpaint_config.moc" diff --git a/effects/showpaint/showpaint_config.desktop b/effects/showpaint/showpaint_config.desktop new file mode 100644 index 0000000..1fa8112 --- /dev/null +++ b/effects/showpaint/showpaint_config.desktop @@ -0,0 +1,75 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=KCModule + +X-KDE-Library=kwin_showpaint_config +X-KDE-ParentComponents=showpaint + +Name=Show Paint +Name[af]=Wys inkleur +Name[ar]=أظهر الطلاء +Name[az]=Yenilənən sahə +Name[bg]=Показване на боята +Name[bn_IN]=Paint প্রদর্শন করা হবে +Name[ca]=Mostra el pintat +Name[ca@valencia]=Mostra el pintat +Name[cs]=Zobrazit kreslení +Name[csb]=Pòkôżë Paint +Name[da]=Vis tegning +Name[de]=Zeichnungsbereiche hervorheben +Name[el]=Εμφάνιση σχεδίασης +Name[en_GB]=Show Paint +Name[eo]=Montri farbon +Name[es]=Muestra pintura +Name[et]=Joonistatud alade näitamine +Name[eu]=Erakutsi pintatutakoa +Name[fi]=Näytä näytönpiirto +Name[fr]=Affiche les zones peintes +Name[fy]=Paint sjen litte +Name[ga]=Taispeáin Péint +Name[gl]=Mostrar o pintado +Name[gu]=રંગ બતાવો +Name[hi]=पेंट दिखाएं +Name[hne]=पेंट देखाव +Name[hr]=Prikaz nedavnih promjena +Name[hu]=Megmutatja a kirajzolt területeket +Name[ia]=Monstra pictura +Name[id]=Tampilkan Lukisan +Name[is]=Sýna teikningu +Name[it]=Mostra il ridisegno +Name[ja]=描画領域を表示 +Name[kk]=Бояулау +Name[km]=បង្ហាញគំនូរ +Name[kn]=ಕೆಪೈಂಟ್ ತೋರಿಸು +Name[ko]=그리기 영역 표시 +Name[lt]=PieÅ¡imo rodymas +Name[lv]=RādÄ«t zÄ«mēto +Name[mai]=पेंट देखाउ +Name[mk]=Приказ на нацртано +Name[ml]=പെയിന്റ് കാണിക്കുക +Name[mr]=रंग दर्शवा +Name[nds]=Klöör wiesen +Name[ne]=रङ देखाउनुहोस् +Name[nl]=Intekening tonen +Name[nn]=Vis oppteikning +Name[pa]=ਪੇਂਟ ਵੇਖੋ +Name[pl]=Pokaż rysowane +Name[pt]=Mostrar a Pintura +Name[pt_BR]=Mostrar pintura +Name[ro]=Arată vopseaua +Name[ru]=Подсвечивать отрисовку +Name[se]=Čájet málema +Name[si]=ඇඳීම් පෙන්වන්න +Name[sk]=ZobraziÅ¥ kresbu +Name[sl]=Izrisovanje +Name[sv]=Visa uppritning +Name[ta]=வண்ணங் காட்டு +Name[te]=పెయింట్ చూపుము +Name[th]=แสดงพื้นที่ +Name[tr]=Boyamayı Göster +Name[ug]=سىزىش دائىرىسىنى كۆرسەت +Name[uk]=Показ малювання +Name[wa]=Mostrer pondaedje +Name[x-test]=xxShow Paintxx +Name[zh_CN]=显示绘制区域 +Name[zh_TW]=顯示繪製 diff --git a/effects/showpaint/showpaint_config.h b/effects/showpaint/showpaint_config.h new file mode 100644 index 0000000..f1c8c36 --- /dev/null +++ b/effects/showpaint/showpaint_config.h @@ -0,0 +1,35 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "ui_showpaint_config.h" + +namespace KWin +{ + +class ShowPaintEffectConfig : public KCModule +{ + Q_OBJECT + +public: + explicit ShowPaintEffectConfig(QWidget *parent = nullptr, const QVariantList &args = {}); + ~ShowPaintEffectConfig() override; + +public Q_SLOTS: + void save() override; + void defaults() override; + +private: + Ui::ShowPaintEffectConfig *m_ui; +}; + +} // namespace KWin diff --git a/effects/showpaint/showpaint_config.ui b/effects/showpaint/showpaint_config.ui new file mode 100644 index 0000000..93e77ba --- /dev/null +++ b/effects/showpaint/showpaint_config.ui @@ -0,0 +1,39 @@ + + + ShowPaintEffectConfig + + + + 0 + 0 + 452 + 246 + + + + + + + + 0 + 0 + + + + KShortcutsEditor::GlobalAction + + + + + + + + KShortcutsEditor + QWidget +
KShortcutsEditor
+ 1 +
+
+ + +
diff --git a/effects/slide/CMakeLists.txt b/effects/slide/CMakeLists.txt new file mode 100644 index 0000000..a579bb6 --- /dev/null +++ b/effects/slide/CMakeLists.txt @@ -0,0 +1,23 @@ +####################################### +# Config +set(kwin_slide_config_SRCS slide_config.cpp) +ki18n_wrap_ui(kwin_slide_config_SRCS slide_config.ui) +kconfig_add_kcfg_files(kwin_slide_config_SRCS slideconfig.kcfgc) + +add_library(kwin_slide_config MODULE ${kwin_slide_config_SRCS}) + +target_link_libraries(kwin_slide_config + KF5::ConfigWidgets + KF5::I18n + Qt5::DBus + KWinEffectsInterface +) + +kcoreaddons_desktop_to_json(kwin_slide_config slide_config.desktop SERVICE_TYPES kcmodule.desktop) + +install( + TARGETS + kwin_slide_config + DESTINATION + ${PLUGIN_INSTALL_DIR}/kwin/effects/configs +) diff --git a/effects/slide/slide.cpp b/effects/slide/slide.cpp new file mode 100644 index 0000000..a73836e --- /dev/null +++ b/effects/slide/slide.cpp @@ -0,0 +1,447 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Lubos Lunak + SPDX-FileCopyrightText: 2008 Lucas Murray + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +// own +#include "slide.h" + +// KConfigSkeleton +#include "slideconfig.h" + +namespace KWin +{ + +SlideEffect::SlideEffect() +{ + initConfig(); + reconfigure(ReconfigureAll); + + m_timeLine.setEasingCurve(QEasingCurve::OutCubic); + + connect(effects, QOverload::of(&EffectsHandler::desktopChanged), + this, &SlideEffect::desktopChanged); + connect(effects, &EffectsHandler::windowAdded, + this, &SlideEffect::windowAdded); + connect(effects, &EffectsHandler::windowDeleted, + this, &SlideEffect::windowDeleted); + connect(effects, &EffectsHandler::numberDesktopsChanged, + this, &SlideEffect::numberDesktopsChanged); + connect(effects, &EffectsHandler::numberScreensChanged, + this, &SlideEffect::numberScreensChanged); +} + +SlideEffect::~SlideEffect() +{ + if (m_active) { + stop(); + } +} + +bool SlideEffect::supported() +{ + return effects->animationsSupported(); +} + +void SlideEffect::reconfigure(ReconfigureFlags) +{ + SlideConfig::self()->read(); + + m_timeLine.setDuration( + std::chrono::milliseconds(animationTime(500))); + + m_hGap = SlideConfig::horizontalGap(); + m_vGap = SlideConfig::verticalGap(); + m_slideDocks = SlideConfig::slideDocks(); + m_slideBackground = SlideConfig::slideBackground(); +} + +void SlideEffect::prePaintScreen(ScreenPrePaintData &data, int time) +{ + m_timeLine.update(std::chrono::milliseconds(time)); + + data.mask |= PAINT_SCREEN_TRANSFORMED + | PAINT_SCREEN_BACKGROUND_FIRST; + + effects->prePaintScreen(data, time); +} + +/** + * Wrap vector @p diff around grid @p w x @p h. + * + * Wrapping is done in such a way that magnitude of x and y component of vector + * @p diff is less than half of @p w and half of @p h, respectively. This will + * result in having the "shortest" path between two points. + * + * @param diff Vector between two points + * @param w Width of the desktop grid + * @param h Height of the desktop grid + */ +inline void wrapDiff(QPoint &diff, int w, int h) +{ + if (diff.x() > w/2) { + diff.setX(diff.x() - w); + } else if (diff.x() < -w/2) { + diff.setX(diff.x() + w); + } + + if (diff.y() > h/2) { + diff.setY(diff.y() - h); + } else if (diff.y() < -h/2) { + diff.setY(diff.y() + h); + } +} + +inline QRegion buildClipRegion(const QPoint &pos, int w, int h) +{ + const QSize screenSize = effects->virtualScreenSize(); + QRegion r = QRect(pos, screenSize); + if (effects->optionRollOverDesktops()) { + r |= (r & QRect(-w, 0, w, h)).translated(w, 0); // W + r |= (r & QRect(w, 0, w, h)).translated(-w, 0); // E + + r |= (r & QRect(0, -h, w, h)).translated(0, h); // N + r |= (r & QRect(0, h, w, h)).translated(0, -h); // S + + r |= (r & QRect(-w, -h, w, h)).translated(w, h); // NW + r |= (r & QRect(w, -h, w, h)).translated(-w, h); // NE + r |= (r & QRect(w, h, w, h)).translated(-w, -h); // SE + r |= (r & QRect(-w, h, w, h)).translated(w, -h); // SW + } + return r; +} + +void SlideEffect::paintScreen(int mask, const QRegion ®ion, ScreenPaintData &data) +{ + const bool wrap = effects->optionRollOverDesktops(); + const int w = workspaceWidth(); + const int h = workspaceHeight(); + + QPoint currentPos = m_startPos + m_diff * m_timeLine.value(); + + // When "Desktop navigation wraps around" checkbox is checked, currentPos + // can be outside the rectangle Rect{x:-w, y:-h, width:2*w, height: 2*h}, + // so we map currentPos back to the rect. + if (wrap) { + currentPos.setX(currentPos.x() % w); + currentPos.setY(currentPos.y() % h); + } + + QVector visibleDesktops; + visibleDesktops.reserve(4); // 4 - maximum number of visible desktops + const QRegion clipRegion = buildClipRegion(currentPos, w, h); + for (int i = 1; i <= effects->numberOfDesktops(); i++) { + const QRect desktopGeo = desktopGeometry(i); + if (!clipRegion.contains(desktopGeo)) { + continue; + } + visibleDesktops << i; + } + + // When we enter a virtual desktop that has a window in fullscreen mode, + // stacking order is fine. When we leave a virtual desktop that has + // a window in fullscreen mode, stacking order is no longer valid + // because panels are raised above the fullscreen window. Construct + // a list of fullscreen windows, so we can decide later whether + // docks should be visible on different virtual desktops. + if (m_slideDocks) { + const auto windows = effects->stackingOrder(); + m_paintCtx.fullscreenWindows.clear(); + for (EffectWindow *w : windows) { + if (!w->isFullScreen()) { + continue; + } + m_paintCtx.fullscreenWindows << w; + } + } + + // If screen is painted with either PAINT_SCREEN_TRANSFORMED or + // PAINT_SCREEN_WITH_TRANSFORMED_WINDOWS there is no clipping!! + // Push the screen geometry to the paint clipper so everything outside + // of the screen geometry is clipped. + PaintClipper pc(QRegion(effects->virtualScreenGeometry())); + + // Screen is painted in several passes. Each painting pass paints + // a single virtual desktop. There could be either 2 or 4 painting + // passes, depending how an user moves between virtual desktops. + // Windows, such as docks or keep-above windows, are painted in + // the last pass so they are above other windows. + m_paintCtx.firstPass = true; + const int lastDesktop = visibleDesktops.last(); + for (int desktop : qAsConst(visibleDesktops)) { + m_paintCtx.desktop = desktop; + m_paintCtx.lastPass = (lastDesktop == desktop); + m_paintCtx.translation = desktopCoords(desktop) - currentPos; + if (wrap) { + wrapDiff(m_paintCtx.translation, w, h); + } + effects->paintScreen(mask, region, data); + m_paintCtx.firstPass = false; + } +} + +/** + * Decide whether given window @p w should be transformed/translated. + * @returns @c true if given window @p w should be transformed, otherwise @c false + */ +bool SlideEffect::isTranslated(const EffectWindow *w) const +{ + if (w->isOnAllDesktops()) { + if (w->isDock()) { + return m_slideDocks; + } + if (w->isDesktop()) { + return m_slideBackground; + } + return false; + } else if (w == m_movingWindow) { + return false; + } else if (w->isOnDesktop(m_paintCtx.desktop)) { + return true; + } + return false; +} + +/** + * Decide whether given window @p w should be painted. + * @returns @c true if given window @p w should be painted, otherwise @c false + */ +bool SlideEffect::isPainted(const EffectWindow *w) const +{ + if (w->isOnAllDesktops()) { + if (w->isDock()) { + if (!m_slideDocks) { + return m_paintCtx.lastPass; + } + for (const EffectWindow *fw : qAsConst(m_paintCtx.fullscreenWindows)) { + if (fw->isOnDesktop(m_paintCtx.desktop) + && fw->screen() == w->screen()) { + return false; + } + } + return true; + } + if (w->isDesktop()) { + // If desktop background is not being slided, draw it only + // in the first pass. Otherwise, desktop backgrounds from + // follow-up virtual desktops will be drawn above windows + // from previous virtual desktops. + return m_slideBackground || m_paintCtx.firstPass; + } + // In order to make sure that 'keep above' windows are above + // other windows during transition to another virtual desktop, + // they should be painted in the last pass. + if (w->keepAbove()) { + return m_paintCtx.lastPass; + } + return true; + } else if (w == m_movingWindow) { + return m_paintCtx.lastPass; + } else if (w->isOnDesktop(m_paintCtx.desktop)) { + return true; + } + return false; +} + +void SlideEffect::prePaintWindow(EffectWindow *w, WindowPrePaintData &data, int time) +{ + const bool painted = isPainted(w); + if (painted) { + w->enablePainting(EffectWindow::PAINT_DISABLED_BY_DESKTOP); + } else { + w->disablePainting(EffectWindow::PAINT_DISABLED_BY_DESKTOP); + } + if (painted && isTranslated(w)) { + data.setTransformed(); + } + effects->prePaintWindow(w, data, time); +} + +void SlideEffect::paintWindow(EffectWindow *w, int mask, QRegion region, WindowPaintData &data) +{ + if (isTranslated(w)) { + data += m_paintCtx.translation; + } + effects->paintWindow(w, mask, region, data); +} + +void SlideEffect::postPaintScreen() +{ + if (m_timeLine.done()) { + stop(); + } + + effects->addRepaintFull(); + effects->postPaintScreen(); +} + +/** + * Get position of the top-left corner of desktop @p id within desktop grid with gaps. + * @param id ID of a virtual desktop + */ +QPoint SlideEffect::desktopCoords(int id) const +{ + QPoint c = effects->desktopCoords(id); + const QPoint gridPos = effects->desktopGridCoords(id); + c.setX(c.x() + m_hGap * gridPos.x()); + c.setY(c.y() + m_vGap * gridPos.y()); + return c; +} + +/** + * Get geometry of desktop @p id within desktop grid with gaps. + * @param id ID of a virtual desktop + */ +QRect SlideEffect::desktopGeometry(int id) const +{ + QRect g = effects->virtualScreenGeometry(); + g.translate(desktopCoords(id)); + return g; +} + +/** + * Get width of a virtual desktop grid. + */ +int SlideEffect::workspaceWidth() const +{ + int w = effects->workspaceWidth(); + w += m_hGap * effects->desktopGridWidth(); + return w; +} + +/** + * Get height of a virtual desktop grid. + */ +int SlideEffect::workspaceHeight() const +{ + int h = effects->workspaceHeight(); + h += m_vGap * effects->desktopGridHeight(); + return h; +} + +bool SlideEffect::shouldElevate(const EffectWindow *w) const +{ + // Static docks(i.e. this effect doesn't slide docks) should be elevated + // so they can properly animate themselves when an user enters or leaves + // a virtual desktop with a window in fullscreen mode. + return w->isDock() && !m_slideDocks; +} + +void SlideEffect::start(int old, int current, EffectWindow *movingWindow) +{ + m_movingWindow = movingWindow; + + const bool wrap = effects->optionRollOverDesktops(); + const int w = workspaceWidth(); + const int h = workspaceHeight(); + + if (m_active) { + QPoint passed = m_diff * m_timeLine.value(); + QPoint currentPos = m_startPos + passed; + QPoint delta = desktopCoords(current) - desktopCoords(old); + if (wrap) { + wrapDiff(delta, w, h); + } + m_diff += delta - passed; + m_startPos = currentPos; + // TODO: Figure out how to smooth movement. + m_timeLine.reset(); + return; + } + + const auto windows = effects->stackingOrder(); + for (EffectWindow *w : windows) { + if (shouldElevate(w)) { + effects->setElevatedWindow(w, true); + m_elevatedWindows << w; + } + w->setData(WindowForceBackgroundContrastRole, QVariant(true)); + w->setData(WindowForceBlurRole, QVariant(true)); + } + + m_diff = desktopCoords(current) - desktopCoords(old); + if (wrap) { + wrapDiff(m_diff, w, h); + } + m_startPos = desktopCoords(old); + m_timeLine.reset(); + m_active = true; + effects->setActiveFullScreenEffect(this); + effects->addRepaintFull(); +} + +void SlideEffect::stop() +{ + const EffectWindowList windows = effects->stackingOrder(); + for (EffectWindow *w : windows) { + w->setData(WindowForceBackgroundContrastRole, QVariant()); + w->setData(WindowForceBlurRole, QVariant()); + } + + for (EffectWindow *w : m_elevatedWindows) { + effects->setElevatedWindow(w, false); + } + m_elevatedWindows.clear(); + + m_paintCtx.fullscreenWindows.clear(); + m_movingWindow = nullptr; + m_active = false; + effects->setActiveFullScreenEffect(nullptr); +} + +void SlideEffect::desktopChanged(int old, int current, EffectWindow *with) +{ + if (effects->activeFullScreenEffect() && effects->activeFullScreenEffect() != this) { + return; + } + start(old, current, with); +} + +void SlideEffect::windowAdded(EffectWindow *w) +{ + if (!m_active) { + return; + } + if (shouldElevate(w)) { + effects->setElevatedWindow(w, true); + m_elevatedWindows << w; + } + w->setData(WindowForceBackgroundContrastRole, QVariant(true)); + w->setData(WindowForceBlurRole, QVariant(true)); +} + +void SlideEffect::windowDeleted(EffectWindow *w) +{ + if (!m_active) { + return; + } + if (w == m_movingWindow) { + m_movingWindow = nullptr; + } + m_elevatedWindows.removeAll(w); + m_paintCtx.fullscreenWindows.removeAll(w); +} + +void SlideEffect::numberDesktopsChanged(uint) +{ + if (!m_active) { + return; + } + stop(); +} + +void SlideEffect::numberScreensChanged() +{ + if (!m_active) { + return; + } + stop(); +} + +} // namespace KWin diff --git a/effects/slide/slide.h b/effects/slide/slide.h new file mode 100644 index 0000000..fc64803 --- /dev/null +++ b/effects/slide/slide.h @@ -0,0 +1,131 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Lubos Lunak + SPDX-FileCopyrightText: 2008 Lucas Murray + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_SLIDE_H +#define KWIN_SLIDE_H + +// kwineffects +#include + +namespace KWin +{ + +class SlideEffect : public Effect +{ + Q_OBJECT + Q_PROPERTY(int duration READ duration) + Q_PROPERTY(int horizontalGap READ horizontalGap) + Q_PROPERTY(int verticalGap READ verticalGap) + Q_PROPERTY(bool slideDocks READ slideDocks) + Q_PROPERTY(bool slideBackground READ slideBackground) + +public: + SlideEffect(); + ~SlideEffect() override; + + void reconfigure(ReconfigureFlags) override; + + void prePaintScreen(ScreenPrePaintData &data, int time) override; + void paintScreen(int mask, const QRegion ®ion, ScreenPaintData &data) override; + void postPaintScreen() override; + + void prePaintWindow(EffectWindow *w, WindowPrePaintData &data, int time) override; + void paintWindow(EffectWindow *w, int mask, QRegion region, WindowPaintData &data) override; + + bool isActive() const override { + return m_active; + } + + int requestedEffectChainPosition() const override { + return 50; + } + + static bool supported(); + + int duration() const; + int horizontalGap() const; + int verticalGap() const; + bool slideDocks() const; + bool slideBackground() const; + +private Q_SLOTS: + void desktopChanged(int old, int current, EffectWindow *with); + void windowAdded(EffectWindow *w); + void windowDeleted(EffectWindow *w); + + void numberDesktopsChanged(uint old); + void numberScreensChanged(); + +private: + QPoint desktopCoords(int id) const; + QRect desktopGeometry(int id) const; + int workspaceWidth() const; + int workspaceHeight() const; + + bool isTranslated(const EffectWindow *w) const; + bool isPainted(const EffectWindow *w) const; + bool shouldElevate(const EffectWindow *w) const; + + void start(int old, int current, EffectWindow *movingWindow = nullptr); + void stop(); + +private: + int m_hGap; + int m_vGap; + bool m_slideDocks; + bool m_slideBackground; + + bool m_active = false; + TimeLine m_timeLine; + QPoint m_startPos; + QPoint m_diff; + EffectWindow *m_movingWindow = nullptr; + + struct { + int desktop; + bool firstPass; + bool lastPass; + QPoint translation; + + EffectWindowList fullscreenWindows; + } m_paintCtx; + + EffectWindowList m_elevatedWindows; +}; + +inline int SlideEffect::duration() const +{ + return m_timeLine.duration().count(); +} + +inline int SlideEffect::horizontalGap() const +{ + return m_hGap; +} + +inline int SlideEffect::verticalGap() const +{ + return m_vGap; +} + +inline bool SlideEffect::slideDocks() const +{ + return m_slideDocks; +} + +inline bool SlideEffect::slideBackground() const +{ + return m_slideBackground; +} + +} // namespace KWin + +#endif diff --git a/effects/slide/slide.kcfg b/effects/slide/slide.kcfg new file mode 100644 index 0000000..b214980 --- /dev/null +++ b/effects/slide/slide.kcfg @@ -0,0 +1,25 @@ + + + + + + + 0 + + + 45 + + + 20 + + + false + + + true + + + diff --git a/effects/slide/slide_config.cpp b/effects/slide/slide_config.cpp new file mode 100644 index 0000000..e4d6b8e --- /dev/null +++ b/effects/slide/slide_config.cpp @@ -0,0 +1,52 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017, 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "slide_config.h" +// KConfigSkeleton +#include "slideconfig.h" +#include + +#include + +#include +#include + +K_PLUGIN_FACTORY_WITH_JSON(SlideEffectConfigFactory, + "slide_config.json", + registerPlugin();) + +namespace KWin +{ + +SlideEffectConfig::SlideEffectConfig(QWidget *parent, const QVariantList &args) + : KCModule(KAboutData::pluginData(QStringLiteral("slide")), parent, args) +{ + m_ui.setupUi(this); + SlideConfig::instance(KWIN_CONFIG); + addConfig(SlideConfig::self(), this); + load(); +} + +SlideEffectConfig::~SlideEffectConfig() +{ +} + +void SlideEffectConfig::save() +{ + KCModule::save(); + + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("slide")); +} + +} // namespace KWin + +#include "slide_config.moc" diff --git a/effects/slide/slide_config.desktop b/effects/slide/slide_config.desktop new file mode 100644 index 0000000..21b3405 --- /dev/null +++ b/effects/slide/slide_config.desktop @@ -0,0 +1,74 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=KCModule + +X-KDE-Library=kwin_slide_config +X-KDE-ParentComponents=slide + +Name=Slide +Name[ar]=أزْلِق +Name[az]=Sürüşdürmə +Name[bg]=Приплъзване +Name[ca]=Diapositiva +Name[ca@valencia]=Diapositiva +Name[cs]=Sklouznutí +Name[csb]=Pùrganié +Name[da]=Glid +Name[de]=Gleiten +Name[el]=Κύλιση +Name[en_GB]=Slide +Name[eo]=Lumbildo +Name[es]=Deslizar +Name[et]=Liuglemine +Name[eu]=Irristatu +Name[fi]=Liuku +Name[fr]=Glisser +Name[fy]=Glydzje +Name[ga]=Sleamhnaigh +Name[gl]=Deslizar +Name[gu]=સ્લાઇડ +Name[hi]=स्लाइड +Name[hne]=स्लाइड +Name[hr]=Pomak +Name[hu]=Csúsztatott váltás +Name[ia]=Glissa +Name[id]=Geser +Name[is]=Renna til +Name[it]=Scivola +Name[ja]=スライド +Name[kk]=Сырғанату +Name[km]=ស្លាយ +Name[kn]=ಜಾರು +Name[ko]=슬라이드 +Name[ku]=Xîş Bike +Name[lt]=Slydimas +Name[lv]=SlÄ«dēt +Name[mai]=स्लाइड +Name[ml]=തെന്നിമാറുക +Name[mr]=सरकणे +Name[nds]=Glieden +Name[nl]=Schuiven +Name[nn]=Skliding +Name[pa]=ਸਲਾਈਡ +Name[pl]=Slajd +Name[pt]=Deslizar +Name[pt_BR]=Deslizar +Name[ro]=Alunecă +Name[ru]=Прокрутка +Name[si]=ලිස්සන්න +Name[sk]=PosúvaÅ¥ +Name[sl]=Drsenje +Name[sr]=Клизање +Name[sr@ijekavian]=Клизање +Name[sr@ijekavianlatin]=Klizanje +Name[sr@latin]=Klizanje +Name[sv]=Skjut +Name[ta]=நழுவு +Name[th]=เลื่อนหน้าต่าง +Name[tr]=Kaydır +Name[ug]=تام تەسۋىر +Name[uk]=Ковзання +Name[wa]=Rider +Name[x-test]=xxSlidexx +Name[zh_CN]=滑动 +Name[zh_TW]=滑動 diff --git a/effects/slide/slide_config.h b/effects/slide/slide_config.h new file mode 100644 index 0000000..929e310 --- /dev/null +++ b/effects/slide/slide_config.h @@ -0,0 +1,36 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017, 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + + +#ifndef SLIDE_CONFIG_H +#define SLIDE_CONFIG_H + +#include +#include "ui_slide_config.h" + +namespace KWin +{ + +class SlideEffectConfig : public KCModule +{ + Q_OBJECT + +public: + explicit SlideEffectConfig(QWidget *parent = nullptr, const QVariantList &args = QVariantList()); + ~SlideEffectConfig() override; + + void save() override; + +private: + ::Ui::SlideEffectConfig m_ui; +}; + +} // namespace KWin + +#endif diff --git a/effects/slide/slide_config.ui b/effects/slide/slide_config.ui new file mode 100644 index 0000000..4c9cb31 --- /dev/null +++ b/effects/slide/slide_config.ui @@ -0,0 +1,133 @@ + + + SlideEffectConfig + + + + 0 + 0 + 400 + 250 + + + + + + + + + Duration: + + + + + + + + 0 + 0 + + + + Default + + + milliseconds + + + 9999 + + + 10 + + + + + + + + + Gap between desktops + + + + + + Horizontal: + + + + + + + + 0 + 0 + + + + 1000 + + + 5 + + + + + + + Vertical: + + + + + + + + 0 + 0 + + + + 1000 + + + 5 + + + + + + + + + + Slide docks + + + + + + + Slide desktop background + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/effects/slide/slideconfig.kcfgc b/effects/slide/slideconfig.kcfgc new file mode 100644 index 0000000..680f393 --- /dev/null +++ b/effects/slide/slideconfig.kcfgc @@ -0,0 +1,5 @@ +File=slide.kcfg +ClassName=SlideConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/effects/slideback/CMakeLists.txt b/effects/slideback/CMakeLists.txt new file mode 100644 index 0000000..aa865fb --- /dev/null +++ b/effects/slideback/CMakeLists.txt @@ -0,0 +1,7 @@ +####################################### +# Effect + +# Source files +set(kwin4_effect_builtins_sources ${kwin4_effect_builtins_sources} + slideback/slideback.cpp +) diff --git a/effects/slideback/slideback.cpp b/effects/slideback/slideback.cpp new file mode 100644 index 0000000..e6a8914 --- /dev/null +++ b/effects/slideback/slideback.cpp @@ -0,0 +1,330 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Michael Zanetti + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "slideback.h" + +namespace KWin +{ + +SlideBackEffect::SlideBackEffect() +{ + m_tabboxActive = 0; + m_justMapped = m_upmostWindow = nullptr; + connect(effects, &EffectsHandler::windowAdded, this, &SlideBackEffect::slotWindowAdded); + connect(effects, &EffectsHandler::windowDeleted, this, &SlideBackEffect::slotWindowDeleted); + connect(effects, &EffectsHandler::windowUnminimized, this, &SlideBackEffect::slotWindowUnminimized); + connect(effects, &EffectsHandler::tabBoxAdded, this, &SlideBackEffect::slotTabBoxAdded); + connect(effects, &EffectsHandler::stackingOrderChanged, this, &SlideBackEffect::slotStackingOrderChanged); + connect(effects, &EffectsHandler::tabBoxClosed, this, &SlideBackEffect::slotTabBoxClosed); +} + +void SlideBackEffect::slotStackingOrderChanged() +{ + if (effects->activeFullScreenEffect() || m_tabboxActive) { + oldStackingOrder = effects->stackingOrder(); + usableOldStackingOrder = usableWindows(oldStackingOrder); + return; + } + + EffectWindowList newStackingOrder = effects->stackingOrder(), + usableNewStackingOrder = usableWindows(newStackingOrder); + if (usableNewStackingOrder == usableOldStackingOrder || usableNewStackingOrder.isEmpty()) { + oldStackingOrder = newStackingOrder; + usableOldStackingOrder = usableNewStackingOrder; + return; + } + + m_upmostWindow = usableNewStackingOrder.last(); + + if (m_upmostWindow == m_justMapped ) // a window was added, got on top, stacking changed. Nothing impressive + m_justMapped = nullptr; + else if (!usableOldStackingOrder.isEmpty() && m_upmostWindow != usableOldStackingOrder.last()) + windowRaised(m_upmostWindow); + + oldStackingOrder = newStackingOrder; + usableOldStackingOrder = usableNewStackingOrder; + +} + +void SlideBackEffect::windowRaised(EffectWindow *w) +{ + // Determine all windows on top of the activated one + bool currentFound = false; + foreach (EffectWindow * tmp, oldStackingOrder) { + if (!currentFound) { + if (tmp == w) { + currentFound = true; + } + } else { + if (isWindowUsable(tmp) && tmp->isOnCurrentDesktop() && w->isOnCurrentDesktop()) { + // Do we have to move it? + if (intersects(w, tmp->geometry())) { + QRect slideRect; + slideRect = getSlideDestination(getModalGroupGeometry(w), tmp->geometry()); + effects->setElevatedWindow(tmp, true); + elevatedList.append(tmp); + motionManager.manage(tmp); + motionManager.moveWindow(tmp, slideRect); + destinationList.insert(tmp, slideRect); + coveringWindows.append(tmp); + } else { + //Does it intersect with a moved (elevated) window and do we have to elevate it too? + foreach (EffectWindow * elevatedWindow, elevatedList) { + if (tmp->geometry().intersects(elevatedWindow->geometry())) { + effects->setElevatedWindow(tmp, true); + elevatedList.append(tmp); + break; + } + } + + } + } + if (tmp->isDock() || tmp->keepAbove()) { + effects->setElevatedWindow(tmp, true); + elevatedList.append(tmp); + } + } + } + // If a window is minimized it could happen that the panels stay elevated without any windows sliding. + // clear all elevation settings + if (!motionManager.managingWindows()) { + foreach (EffectWindow * tmp, elevatedList) { + effects->setElevatedWindow(tmp, false); + } + } +} + +QRect SlideBackEffect::getSlideDestination(const QRect &windowUnderGeometry, const QRect &windowOverGeometry) +{ + // Determine the shortest way: + int leftSlide = windowUnderGeometry.left() - windowOverGeometry.right() - 20; + int rightSlide = windowUnderGeometry.right() - windowOverGeometry.left() + 20; + int upSlide = windowUnderGeometry.top() - windowOverGeometry.bottom() - 20; + int downSlide = windowUnderGeometry.bottom() - windowOverGeometry.top() + 20; + + int horizSlide = leftSlide; + if (qAbs(horizSlide) > qAbs(rightSlide)) { + horizSlide = rightSlide; + } + int vertSlide = upSlide; + if (qAbs(vertSlide) > qAbs(downSlide)) { + vertSlide = downSlide; + } + + QRect slideRect = windowOverGeometry; + if (qAbs(horizSlide) < qAbs(vertSlide)) { + slideRect.moveLeft(slideRect.x() + horizSlide); + } else { + slideRect.moveTop(slideRect.y() + vertSlide); + } + return slideRect; +} + +void SlideBackEffect::prePaintScreen(ScreenPrePaintData &data, int time) +{ + if (motionManager.managingWindows()) { + motionManager.calculate(time); + data.mask |= Effect::PAINT_SCREEN_WITH_TRANSFORMED_WINDOWS; + } + + for (auto const &w : effects->stackingOrder()) { + w->setData(WindowForceBlurRole, QVariant(true)); + } + + effects->prePaintScreen(data, time); +} + +void SlideBackEffect::postPaintScreen() +{ + if (motionManager.areWindowsMoving()) { + effects->addRepaintFull(); + } + + for (auto &w : effects->stackingOrder()) { + w->setData(WindowForceBlurRole, QVariant()); + } + + effects->postPaintScreen(); +} + +void SlideBackEffect::prePaintWindow(EffectWindow *w, WindowPrePaintData &data, int time) +{ + if (motionManager.isManaging(w)) { + data.setTransformed(); + } + + effects->prePaintWindow(w, data, time); +} + +void SlideBackEffect::paintWindow(EffectWindow *w, int mask, QRegion region, WindowPaintData &data) +{ + if (motionManager.isManaging(w)) { + motionManager.apply(w, data); + } + foreach (const QRegion &r, clippedRegions) { + region = region.intersected(r); + } + effects->paintWindow(w, mask, region, data); + for (int i = clippedRegions.count() - 1; i > -1; --i) + PaintClipper::pop(clippedRegions.at(i)); + clippedRegions.clear(); +} + +void SlideBackEffect::postPaintWindow(EffectWindow* w) +{ + if (motionManager.isManaging(w)) { + if (destinationList.contains(w)) { + if (!motionManager.isWindowMoving(w)) { // has window reched its destination? + // If we are still intersecting with the upmostWindow it is moving. slide to somewhere else + // restore the stacking order of all windows not intersecting any more except panels + if (coveringWindows.contains(w)) { + EffectWindowList tmpList; + foreach (EffectWindow * tmp, elevatedList) { + QRect elevatedGeometry = tmp->geometry(); + if (motionManager.isManaging(tmp)) { + elevatedGeometry = motionManager.transformedGeometry(tmp).toAlignedRect(); + } + if (m_upmostWindow && !tmp->isDock() && !tmp->keepAbove() && m_upmostWindow->geometry().intersects(elevatedGeometry)) { + QRect newDestination; + newDestination = getSlideDestination(getModalGroupGeometry(m_upmostWindow), elevatedGeometry); + if (!motionManager.isManaging(tmp)) { + motionManager.manage(tmp); + } + motionManager.moveWindow(tmp, newDestination); + destinationList[tmp] = newDestination; + } else { + if (!tmp->isDock()) { + bool keepElevated = false; + foreach (EffectWindow * elevatedWindow, tmpList) { + if (tmp->geometry().intersects(elevatedWindow->geometry())) { + keepElevated = true; + } + } + if (!keepElevated) { + effects->setElevatedWindow(tmp, false); + elevatedList.removeAll(tmp); + } + } + } + tmpList.append(tmp); + } + } else { + // Move the window back where it belongs + motionManager.moveWindow(w, w->geometry()); + destinationList.remove(w); + } + } + } else { + // is window back at its original position? + if (!motionManager.isWindowMoving(w)) { + motionManager.unmanage(w); + effects->addRepaintFull(); + } + } + if (coveringWindows.contains(w)) { + // It could happen that there is no aciveWindow() here if the user clicks the close-button on an inactive window. + // Just skip... the window will be removed in windowDeleted() later + if (m_upmostWindow && !intersects(m_upmostWindow, motionManager.transformedGeometry(w).toAlignedRect())) { + coveringWindows.removeAll(w); + if (coveringWindows.isEmpty()) { + // Restore correct stacking order + foreach (EffectWindow * tmp, elevatedList) { + effects->setElevatedWindow(tmp, false); + } + elevatedList.clear(); + } + } + } + } + effects->postPaintWindow(w); +} + +void SlideBackEffect::slotWindowDeleted(EffectWindow* w) +{ + if (w == m_upmostWindow) + m_upmostWindow = nullptr; + if (w == m_justMapped) + m_justMapped = nullptr; + usableOldStackingOrder.removeAll(w); + oldStackingOrder.removeAll(w); + coveringWindows.removeAll(w); + elevatedList.removeAll(w); + if (motionManager.isManaging(w)) { + motionManager.unmanage(w); + } +} + +void SlideBackEffect::slotWindowAdded(EffectWindow *w) +{ + m_justMapped = w; +} + +void SlideBackEffect::slotWindowUnminimized(EffectWindow* w) +{ + // SlideBack should not be triggered on an unminimized window. For this we need to store the last unminimized window. + m_justMapped = w; + // the stackingOrderChanged() signal came before the window turned an effect window + // usually this is no problem as the change shall not be caught anyway, but + // the window may have changed its stack position, bug #353745 + slotStackingOrderChanged(); +} + +void SlideBackEffect::slotTabBoxAdded() +{ + ++m_tabboxActive; +} + +void SlideBackEffect::slotTabBoxClosed() +{ + m_tabboxActive = qMax(m_tabboxActive-1, 0); +} + +bool SlideBackEffect::isWindowUsable(EffectWindow* w) +{ + return w && (w->isNormalWindow() || w->isDialog()) && !w->keepAbove() && !w->isDeleted() && !w->isMinimized() + && w->isPaintingEnabled(); +} + +bool SlideBackEffect::intersects(EffectWindow* windowUnder, const QRect &windowOverGeometry) +{ + QRect windowUnderGeometry = getModalGroupGeometry(windowUnder); + return windowUnderGeometry.intersects(windowOverGeometry); +} + +EffectWindowList SlideBackEffect::usableWindows(const EffectWindowList & allWindows) +{ + EffectWindowList retList; + auto isWindowVisible = [] (const EffectWindow *window) { + return window && effects->virtualScreenGeometry().intersects(window->geometry()); + }; + foreach (EffectWindow * tmp, allWindows) { + if (isWindowUsable(tmp) && isWindowVisible(tmp)) { + retList.append(tmp); + } + } + return retList; +} + +QRect SlideBackEffect::getModalGroupGeometry(EffectWindow *w) +{ + QRect modalGroupGeometry = w->geometry(); + if (w->isModal()) { + foreach (EffectWindow * modalWindow, w->mainWindows()) { + modalGroupGeometry = modalGroupGeometry.united(getModalGroupGeometry(modalWindow)); + } + } + return modalGroupGeometry; +} + +bool SlideBackEffect::isActive() const +{ + return motionManager.managingWindows(); +} + +} //Namespace diff --git a/effects/slideback/slideback.h b/effects/slideback/slideback.h new file mode 100644 index 0000000..c10b7e5 --- /dev/null +++ b/effects/slideback/slideback.h @@ -0,0 +1,69 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Michael Zanetti + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_SLIDEBACK_H +#define KWIN_SLIDEBACK_H + +// Include with base class for effects. +#include + +namespace KWin +{ + +class SlideBackEffect + : public Effect +{ + Q_OBJECT +public: + SlideBackEffect(); + + void prePaintWindow(EffectWindow *w, WindowPrePaintData &data, int time) override; + void paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data) override; + void postPaintWindow(EffectWindow* w) override; + + void prePaintScreen(ScreenPrePaintData &data, int time) override; + void postPaintScreen() override; + bool isActive() const override; + + int requestedEffectChainPosition() const override { + return 50; + } + +public Q_SLOTS: + void slotWindowAdded(KWin::EffectWindow *w); + void slotWindowDeleted(KWin::EffectWindow *w); + void slotWindowUnminimized(KWin::EffectWindow *w); + void slotStackingOrderChanged(); + void slotTabBoxAdded(); + void slotTabBoxClosed(); + +private: + + WindowMotionManager motionManager; + EffectWindowList usableOldStackingOrder; + EffectWindowList oldStackingOrder; + EffectWindowList coveringWindows; + EffectWindowList elevatedList; + EffectWindow *m_justMapped, *m_upmostWindow; + QHash destinationList; + int m_tabboxActive; + QList clippedRegions; + + QRect getSlideDestination(const QRect &windowUnderGeometry, const QRect &windowOverGeometry); + bool isWindowUsable(EffectWindow *w); + bool intersects(EffectWindow *windowUnder, const QRect &windowOverGeometry); + EffectWindowList usableWindows(const EffectWindowList &allWindows); + QRect getModalGroupGeometry(EffectWindow *w); + void windowRaised(EffectWindow *w); + +}; + +} // namespace + +#endif diff --git a/effects/slidingpopups/CMakeLists.txt b/effects/slidingpopups/CMakeLists.txt new file mode 100644 index 0000000..7924669 --- /dev/null +++ b/effects/slidingpopups/CMakeLists.txt @@ -0,0 +1,7 @@ +####################################### +# Effect + +# Source files +set(kwin4_effect_builtins_sources ${kwin4_effect_builtins_sources} + slidingpopups/slidingpopups.cpp +) diff --git a/effects/slidingpopups/slidingpopups.cpp b/effects/slidingpopups/slidingpopups.cpp new file mode 100644 index 0000000..d82f00c --- /dev/null +++ b/effects/slidingpopups/slidingpopups.cpp @@ -0,0 +1,533 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Marco Martin notmart @gmail.com + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "slidingpopups.h" +#include "slidingpopupsconfig.h" + +#include +#include +#include + +#include +#include +#include + +#include + +Q_DECLARE_METATYPE(KWindowEffects::SlideFromLocation) + +namespace KWin +{ + +SlidingPopupsEffect::SlidingPopupsEffect() +{ + initConfig(); + KWaylandServer::Display *display = effects->waylandDisplay(); + if (display) { + display->createSlideManager(this)->create(); + } + + m_slideLength = QFontMetrics(qApp->font()).height() * 8; + + m_atom = effects->announceSupportProperty("_KDE_SLIDE", this); + connect(effects, &EffectsHandler::windowAdded, this, &SlidingPopupsEffect::slotWindowAdded); + connect(effects, &EffectsHandler::windowClosed, this, &SlidingPopupsEffect::slideOut); + connect(effects, &EffectsHandler::windowDeleted, this, &SlidingPopupsEffect::slotWindowDeleted); + connect(effects, &EffectsHandler::propertyNotify, this, &SlidingPopupsEffect::slotPropertyNotify); + connect(effects, &EffectsHandler::windowShown, this, &SlidingPopupsEffect::slideIn); + connect(effects, &EffectsHandler::windowHidden, this, &SlidingPopupsEffect::slideOut); + connect(effects, &EffectsHandler::xcbConnectionChanged, this, + [this] { + m_atom = effects->announceSupportProperty(QByteArrayLiteral("_KDE_SLIDE"), this); + } + ); + connect(effects, qOverload(&EffectsHandler::desktopChanged), + this, &SlidingPopupsEffect::stopAnimations); + connect(effects, &EffectsHandler::activeFullScreenEffectChanged, + this, &SlidingPopupsEffect::stopAnimations); + + reconfigure(ReconfigureAll); +} + +SlidingPopupsEffect::~SlidingPopupsEffect() +{ +} + +bool SlidingPopupsEffect::supported() +{ + return effects->animationsSupported(); +} + +void SlidingPopupsEffect::reconfigure(ReconfigureFlags flags) +{ + Q_UNUSED(flags) + SlidingPopupsConfig::self()->read(); + m_slideInDuration = std::chrono::milliseconds( + static_cast(animationTime(SlidingPopupsConfig::slideInTime() != 0 ? SlidingPopupsConfig::slideInTime() : 150))); + m_slideOutDuration = std::chrono::milliseconds( + static_cast(animationTime(SlidingPopupsConfig::slideOutTime() != 0 ? SlidingPopupsConfig::slideOutTime() : 250))); + + auto animationIt = m_animations.begin(); + while (animationIt != m_animations.end()) { + const auto duration = ((*animationIt).kind == AnimationKind::In) + ? m_slideInDuration + : m_slideOutDuration; + (*animationIt).timeLine.setDuration(duration); + ++animationIt; + } + + auto dataIt = m_animationsData.begin(); + while (dataIt != m_animationsData.end()) { + (*dataIt).slideInDuration = m_slideInDuration; + (*dataIt).slideOutDuration = m_slideOutDuration; + ++dataIt; + } +} + +void SlidingPopupsEffect::prePaintWindow(EffectWindow *w, WindowPrePaintData &data, int time) +{ + auto animationIt = m_animations.find(w); + if (animationIt == m_animations.end()) { + effects->prePaintWindow(w, data, time); + return; + } + + (*animationIt).timeLine.update(std::chrono::milliseconds(time)); + data.setTransformed(); + w->enablePainting(EffectWindow::PAINT_DISABLED | EffectWindow::PAINT_DISABLED_BY_DELETE); + + effects->prePaintWindow(w, data, time); +} + +void SlidingPopupsEffect::paintWindow(EffectWindow *w, int mask, QRegion region, WindowPaintData &data) +{ + auto animationIt = m_animations.constFind(w); + if (animationIt == m_animations.constEnd()) { + effects->paintWindow(w, mask, region, data); + return; + } + + const AnimationData &animData = m_animationsData[w]; + const int slideLength = (animData.slideLength > 0) ? animData.slideLength : m_slideLength; + + const QRect screenRect = effects->clientArea(FullScreenArea, w->screen(), effects->currentDesktop()); + int splitPoint = 0; + const QRect geo = w->expandedGeometry(); + const qreal t = (*animationIt).timeLine.value(); + + switch (animData.location) { + case Location::Left: + if (slideLength < geo.width()) { + data.multiplyOpacity(t); + } + data.translate(-interpolate(qMin(geo.width(), slideLength), 0.0, t)); + splitPoint = geo.width() - (geo.x() + geo.width() - screenRect.x() - animData.offset); + region &= QRegion(geo.x() + splitPoint, geo.y(), geo.width() - splitPoint, geo.height()); + break; + case Location::Top: + if (slideLength < geo.height()) { + data.multiplyOpacity(t); + } + data.translate(0.0, -interpolate(qMin(geo.height(), slideLength), 0.0, t)); + splitPoint = geo.height() - (geo.y() + geo.height() - screenRect.y() - animData.offset); + region &= QRegion(geo.x(), geo.y() + splitPoint, geo.width(), geo.height() - splitPoint); + break; + case Location::Right: + if (slideLength < geo.width()) { + data.multiplyOpacity(t); + } + data.translate(interpolate(qMin(geo.width(), slideLength), 0.0, t)); + splitPoint = screenRect.x() + screenRect.width() - geo.x() - animData.offset; + region &= QRegion(geo.x(), geo.y(), splitPoint, geo.height()); + break; + case Location::Bottom: + default: + if (slideLength < geo.height()) { + data.multiplyOpacity(t); + } + data.translate(0.0, interpolate(qMin(geo.height(), slideLength), 0.0, t)); + splitPoint = screenRect.y() + screenRect.height() - geo.y() - animData.offset; + region &= QRegion(geo.x(), geo.y(), geo.width(), splitPoint); + } + + effects->paintWindow(w, mask, region, data); +} + +void SlidingPopupsEffect::postPaintWindow(EffectWindow *w) +{ + auto animationIt = m_animations.find(w); + if (animationIt != m_animations.end()) { + if ((*animationIt).timeLine.done()) { + if (w->isDeleted()) { + w->unrefWindow(); + } else { + w->setData(WindowForceBackgroundContrastRole, QVariant()); + w->setData(WindowForceBlurRole, QVariant()); + } + m_animations.erase(animationIt); + } + w->addRepaintFull(); + } + + effects->postPaintWindow(w); +} + +void SlidingPopupsEffect::slotWindowAdded(EffectWindow *w) +{ + //X11 + if (m_atom != XCB_ATOM_NONE) { + slotPropertyNotify(w, m_atom); + } + + //Wayland + if (auto surf = w->surface()) { + slotWaylandSlideOnShowChanged(w); + connect(surf, &KWaylandServer::SurfaceInterface::slideOnShowHideChanged, this, [this, surf] { + slotWaylandSlideOnShowChanged(effects->findWindow(surf)); + }); + } + + if (auto internal = w->internalWindow()) { + internal->installEventFilter(this); + setupInternalWindowSlide(w); + } + + slideIn(w); +} + +void SlidingPopupsEffect::slotWindowDeleted(EffectWindow *w) +{ + m_animations.remove(w); + m_animationsData.remove(w); +} + +void SlidingPopupsEffect::slotPropertyNotify(EffectWindow *w, long atom) +{ + if (!w || atom != m_atom || m_atom == XCB_ATOM_NONE) { + return; + } + + // _KDE_SLIDE atom format(each field is an uint32_t): + // [] [] [] + // + // If offset is equal to -1, this effect will decide what offset to use + // given edge of the screen, from which the window has to slide. + // + // If slide in duration is equal to 0 milliseconds, the default slide in + // duration will be used. Same with the slide out duration. + // + // NOTE: If only slide in duration has been provided, then it will be + // also used as slide out duration. I.e. if you provided only slide in + // duration, then slide in duration == slide out duration. + + const QByteArray rawAtomData = w->readProperty(m_atom, m_atom, 32); + + if (rawAtomData.isEmpty()) { + // Property was removed, thus also remove the effect for window + if (w->data(WindowClosedGrabRole).value() == this) { + w->setData(WindowClosedGrabRole, QVariant()); + } + m_animations.remove(w); + m_animationsData.remove(w); + return; + } + + // Offset and location are required. + if (static_cast(rawAtomData.size()) < sizeof(uint32_t) * 2) { + return; + } + + const auto *atomData = reinterpret_cast(rawAtomData.data()); + AnimationData &animData = m_animationsData[w]; + animData.offset = atomData[0]; + + switch (atomData[1]) { + case 0: // West + animData.location = Location::Left; + break; + case 1: // North + animData.location = Location::Top; + break; + case 2: // East + animData.location = Location::Right; + break; + case 3: // South + default: + animData.location = Location::Bottom; + break; + } + + if (static_cast(rawAtomData.size()) >= sizeof(uint32_t) * 3) { + animData.slideInDuration = std::chrono::milliseconds(atomData[2]); + if (static_cast(rawAtomData.size()) >= sizeof(uint32_t) * 4) { + animData.slideOutDuration = std::chrono::milliseconds(atomData[3]); + } else { + animData.slideOutDuration = animData.slideInDuration; + } + } else { + animData.slideInDuration = m_slideInDuration; + animData.slideOutDuration = m_slideOutDuration; + } + + if (static_cast(rawAtomData.size()) >= sizeof(uint32_t) * 5) { + animData.slideLength = atomData[4]; + } else { + animData.slideLength = 0; + } + + setupAnimData(w); +} + +void SlidingPopupsEffect::setupAnimData(EffectWindow *w) +{ + const QRect screenRect = effects->clientArea(FullScreenArea, w->screen(), effects->currentDesktop()); + const QRect windowGeo = w->geometry(); + AnimationData &animData = m_animationsData[w]; + + if (animData.offset == -1) { + switch (animData.location) { + case Location::Left: + animData.offset = qMax(windowGeo.left() - screenRect.left(), 0); + break; + case Location::Top: + animData.offset = qMax(windowGeo.top() - screenRect.top(), 0); + break; + case Location::Right: + animData.offset = qMax(screenRect.right() - windowGeo.right(), 0); + break; + case Location::Bottom: + default: + animData.offset = qMax(screenRect.bottom() - windowGeo.bottom(), 0); + break; + } + } + // sanitize + switch (animData.location) { + case Location::Left: + animData.offset = qMax(windowGeo.left() - screenRect.left(), animData.offset); + break; + case Location::Top: + animData.offset = qMax(windowGeo.top() - screenRect.top(), animData.offset); + break; + case Location::Right: + animData.offset = qMax(screenRect.right() - windowGeo.right(), animData.offset); + break; + case Location::Bottom: + default: + animData.offset = qMax(screenRect.bottom() - windowGeo.bottom(), animData.offset); + break; + } + + animData.slideInDuration = (animData.slideInDuration.count() != 0) + ? animData.slideInDuration + : m_slideInDuration; + + animData.slideOutDuration = (animData.slideOutDuration.count() != 0) + ? animData.slideOutDuration + : m_slideOutDuration; + + // Grab the window, so other windowClosed effects will ignore it + w->setData(WindowClosedGrabRole, QVariant::fromValue(static_cast(this))); +} + +void SlidingPopupsEffect::slotWaylandSlideOnShowChanged(EffectWindow* w) +{ + if (!w) { + return; + } + + KWaylandServer::SurfaceInterface *surf = w->surface(); + if (!surf) { + return; + } + + if (surf->slideOnShowHide()) { + AnimationData &animData = m_animationsData[w]; + + animData.offset = surf->slideOnShowHide()->offset(); + + switch (surf->slideOnShowHide()->location()) { + case KWaylandServer::SlideInterface::Location::Top: + animData.location = Location::Top; + break; + case KWaylandServer::SlideInterface::Location::Left: + animData.location = Location::Left; + break; + case KWaylandServer::SlideInterface::Location::Right: + animData.location = Location::Right; + break; + case KWaylandServer::SlideInterface::Location::Bottom: + default: + animData.location = Location::Bottom; + break; + } + animData.slideLength = 0; + animData.slideInDuration = m_slideInDuration; + animData.slideOutDuration = m_slideOutDuration; + + setupAnimData(w); + } +} + +void SlidingPopupsEffect::setupInternalWindowSlide(EffectWindow *w) +{ + if (!w) { + return; + } + auto internal = w->internalWindow(); + if (!internal) { + return; + } + const QVariant slideProperty = internal->property("kwin_slide"); + if (!slideProperty.isValid()) { + return; + } + Location location; + switch (slideProperty.value()) { + case KWindowEffects::BottomEdge: + location = Location::Bottom; + break; + case KWindowEffects::TopEdge: + location = Location::Top; + break; + case KWindowEffects::RightEdge: + location = Location::Right; + break; + case KWindowEffects::LeftEdge: + location = Location::Left; + break; + default: + return; + } + AnimationData &animData = m_animationsData[w]; + animData.location = location; + bool intOk = false; + animData.offset = internal->property("kwin_slide_offset").toInt(&intOk); + if (!intOk) { + animData.offset = -1; + } + animData.slideLength = 0; + animData.slideInDuration = m_slideInDuration; + animData.slideOutDuration = m_slideOutDuration; + + setupAnimData(w); +} + +bool SlidingPopupsEffect::eventFilter(QObject *watched, QEvent *event) +{ + auto internal = qobject_cast(watched); + if (internal && event->type() == QEvent::DynamicPropertyChange) { + QDynamicPropertyChangeEvent *pe = static_cast(event); + if (pe->propertyName() == "kwin_slide" || pe->propertyName() == "kwin_slide_offset") { + if (auto w = effects->findWindow(internal)) { + setupInternalWindowSlide(w); + } + } + } + return false; +} + +void SlidingPopupsEffect::slideIn(EffectWindow *w) +{ + if (effects->activeFullScreenEffect()) { + return; + } + + if (!w->isVisible()) { + return; + } + + auto dataIt = m_animationsData.constFind(w); + if (dataIt == m_animationsData.constEnd()) { + return; + } + + Animation &animation = m_animations[w]; + animation.kind = AnimationKind::In; + animation.timeLine.setDirection(TimeLine::Forward); + animation.timeLine.setDuration((*dataIt).slideInDuration); + animation.timeLine.setEasingCurve(QEasingCurve::OutCubic); + + // If the opposite animation (Out) was active and it had shorter duration, + // at this point, the timeline can end up in the "done" state. Thus, we have + // to reset it. + if (animation.timeLine.done()) { + animation.timeLine.reset(); + } + + w->setData(WindowAddedGrabRole, QVariant::fromValue(static_cast(this))); + w->setData(WindowForceBackgroundContrastRole, QVariant(true)); + w->setData(WindowForceBlurRole, QVariant(true)); + + w->addRepaintFull(); +} + +void SlidingPopupsEffect::slideOut(EffectWindow *w) +{ + if (effects->activeFullScreenEffect()) { + return; + } + + if (!w->isVisible()) { + return; + } + + auto dataIt = m_animationsData.constFind(w); + if (dataIt == m_animationsData.constEnd()) { + return; + } + + if (w->isDeleted()) { + w->refWindow(); + } + + Animation &animation = m_animations[w]; + animation.kind = AnimationKind::Out; + animation.timeLine.setDirection(TimeLine::Backward); + animation.timeLine.setDuration((*dataIt).slideOutDuration); + // this is effectively InCubic because the direction is reversed + animation.timeLine.setEasingCurve(QEasingCurve::OutCubic); + + // If the opposite animation (In) was active and it had shorter duration, + // at this point, the timeline can end up in the "done" state. Thus, we have + // to reset it. + if (animation.timeLine.done()) { + animation.timeLine.reset(); + } + + w->setData(WindowClosedGrabRole, QVariant::fromValue(static_cast(this))); + w->setData(WindowForceBackgroundContrastRole, QVariant(true)); + w->setData(WindowForceBlurRole, QVariant(true)); + + w->addRepaintFull(); +} + +void SlidingPopupsEffect::stopAnimations() +{ + for (auto it = m_animations.constBegin(); it != m_animations.constEnd(); ++it) { + EffectWindow *w = it.key(); + + if (w->isDeleted()) { + w->unrefWindow(); + } else { + w->setData(WindowForceBackgroundContrastRole, QVariant()); + w->setData(WindowForceBlurRole, QVariant()); + } + } + + m_animations.clear(); +} + +bool SlidingPopupsEffect::isActive() const +{ + return !m_animations.isEmpty(); +} + +} // namespace diff --git a/effects/slidingpopups/slidingpopups.h b/effects/slidingpopups/slidingpopups.h new file mode 100644 index 0000000..c6c2c3e --- /dev/null +++ b/effects/slidingpopups/slidingpopups.h @@ -0,0 +1,107 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Marco Martin notmart @gmail.com + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_SLIDINGPOPUPS_H +#define KWIN_SLIDINGPOPUPS_H + +// Include with base class for effects. +#include + +namespace KWin +{ + +class SlidingPopupsEffect : public Effect +{ + Q_OBJECT + Q_PROPERTY(int slideInDuration READ slideInDuration) + Q_PROPERTY(int slideOutDuration READ slideOutDuration) + +public: + SlidingPopupsEffect(); + ~SlidingPopupsEffect() override; + + void prePaintWindow(EffectWindow *w, WindowPrePaintData &data, int time) override; + void paintWindow(EffectWindow *w, int mask, QRegion region, WindowPaintData &data) override; + void postPaintWindow(EffectWindow *w) override; + void reconfigure(ReconfigureFlags flags) override; + bool isActive() const override; + + int requestedEffectChainPosition() const override { + return 40; + } + + static bool supported(); + + int slideInDuration() const; + int slideOutDuration() const; + + bool eventFilter(QObject *watched, QEvent *event) override; + +private Q_SLOTS: + void slotWindowAdded(EffectWindow *w); + void slotWindowDeleted(EffectWindow *w); + void slotPropertyNotify(EffectWindow *w, long atom); + void slotWaylandSlideOnShowChanged(EffectWindow *w); + + void slideIn(EffectWindow *w); + void slideOut(EffectWindow *w); + void stopAnimations(); + +private: + void setupAnimData(EffectWindow *w); + void setupInternalWindowSlide(EffectWindow *w); + + long m_atom; + + int m_slideLength; + std::chrono::milliseconds m_slideInDuration; + std::chrono::milliseconds m_slideOutDuration; + + enum class AnimationKind { + In, + Out + }; + + struct Animation { + AnimationKind kind; + TimeLine timeLine; + }; + QHash m_animations; + + enum class Location { + Left, + Top, + Right, + Bottom + }; + + struct AnimationData { + int offset; + Location location; + std::chrono::milliseconds slideInDuration; + std::chrono::milliseconds slideOutDuration; + int slideLength; + }; + QHash m_animationsData; +}; + +inline int SlidingPopupsEffect::slideInDuration() const +{ + return m_slideInDuration.count(); +} + +inline int SlidingPopupsEffect::slideOutDuration() const +{ + return m_slideOutDuration.count(); +} + +} // namespace + +#endif diff --git a/effects/slidingpopups/slidingpopups.kcfg b/effects/slidingpopups/slidingpopups.kcfg new file mode 100644 index 0000000..aa0a3ad --- /dev/null +++ b/effects/slidingpopups/slidingpopups.kcfg @@ -0,0 +1,17 @@ + + + + + + + 0 + + + 0 + + + + diff --git a/effects/slidingpopups/slidingpopupsconfig.kcfgc b/effects/slidingpopups/slidingpopupsconfig.kcfgc new file mode 100644 index 0000000..6a8c915 --- /dev/null +++ b/effects/slidingpopups/slidingpopupsconfig.kcfgc @@ -0,0 +1,5 @@ +File=slidingpopups.kcfg +ClassName=SlidingPopupsConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/effects/snaphelper/CMakeLists.txt b/effects/snaphelper/CMakeLists.txt new file mode 100644 index 0000000..73172eb --- /dev/null +++ b/effects/snaphelper/CMakeLists.txt @@ -0,0 +1,7 @@ +####################################### +# Effect + +# Source files +set(kwin4_effect_builtins_sources ${kwin4_effect_builtins_sources} + snaphelper/snaphelper.cpp +) diff --git a/effects/snaphelper/snaphelper.cpp b/effects/snaphelper/snaphelper.cpp new file mode 100644 index 0000000..0fd783c --- /dev/null +++ b/effects/snaphelper/snaphelper.cpp @@ -0,0 +1,324 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "snaphelper.h" + +#include + +#ifdef KWIN_HAVE_XRENDER_COMPOSITING +#include +#include +#endif + +#include + +namespace KWin +{ + +static const int s_lineWidth = 4; +static const QColor s_lineColor = QColor(128, 128, 128, 128); + +static QRegion computeDirtyRegion(const QRect &windowRect) +{ + const QMargins outlineMargins( + s_lineWidth / 2, + s_lineWidth / 2, + s_lineWidth / 2, + s_lineWidth / 2 + ); + + QRegion dirtyRegion; + for (int i = 0; i < effects->numScreens(); ++i) { + const QRect screenRect = effects->clientArea(ScreenArea, i, 0); + + QRect screenWindowRect = windowRect; + screenWindowRect.moveCenter(screenRect.center()); + + QRect verticalBarRect(0, 0, s_lineWidth, screenRect.height()); + verticalBarRect.moveCenter(screenRect.center()); + verticalBarRect.adjust(-1, -1, 1, 1); + dirtyRegion += verticalBarRect; + + QRect horizontalBarRect(0, 0, screenRect.width(), s_lineWidth); + horizontalBarRect.moveCenter(screenRect.center()); + horizontalBarRect.adjust(-1, -1, 1, 1); + dirtyRegion += horizontalBarRect; + + const QRect outlineOuterRect = screenWindowRect + .marginsAdded(outlineMargins) + .adjusted(-1, -1, 1, 1); + const QRect outlineInnerRect = screenWindowRect + .marginsRemoved(outlineMargins) + .adjusted(1, 1, -1, -1); + dirtyRegion += QRegion(outlineOuterRect) - QRegion(outlineInnerRect); + } + + return dirtyRegion; +} + +SnapHelperEffect::SnapHelperEffect() +{ + reconfigure(ReconfigureAll); + + connect(effects, &EffectsHandler::windowClosed, this, &SnapHelperEffect::slotWindowClosed); + connect(effects, &EffectsHandler::windowStartUserMovedResized, this, &SnapHelperEffect::slotWindowStartUserMovedResized); + connect(effects, &EffectsHandler::windowFinishUserMovedResized, this, &SnapHelperEffect::slotWindowFinishUserMovedResized); + connect(effects, &EffectsHandler::windowFrameGeometryChanged, this, &SnapHelperEffect::slotWindowFrameGeometryChanged); +} + +SnapHelperEffect::~SnapHelperEffect() +{ +} + +void SnapHelperEffect::reconfigure(ReconfigureFlags flags) +{ + Q_UNUSED(flags) + + m_animation.timeLine.setDuration( + std::chrono::milliseconds(static_cast(animationTime(250)))); +} + +void SnapHelperEffect::prePaintScreen(ScreenPrePaintData &data, int time) +{ + if (m_animation.active) { + m_animation.timeLine.update(std::chrono::milliseconds(time)); + } + + effects->prePaintScreen(data, time); +} + +void SnapHelperEffect::paintScreen(int mask, const QRegion ®ion, ScreenPaintData &data) +{ + effects->paintScreen(mask, region, data); + + const qreal opacityFactor = m_animation.active + ? m_animation.timeLine.value() + : 1.0; + + // Display the guide + if (effects->isOpenGLCompositing()) { + GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer(); + vbo->reset(); + vbo->setUseColor(true); + ShaderBinder binder(ShaderTrait::UniformColor); + binder.shader()->setUniform(GLShader::ModelViewProjectionMatrix, data.projectionMatrix()); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + QColor color = s_lineColor; + color.setAlphaF(color.alphaF() * opacityFactor); + vbo->setColor(color); + + glLineWidth(s_lineWidth); + QVector verts; + verts.reserve(effects->numScreens() * 24); + for (int i = 0; i < effects->numScreens(); ++i) { + const QRect rect = effects->clientArea(ScreenArea, i, 0); + const int midX = rect.x() + rect.width() / 2; + const int midY = rect.y() + rect.height() / 2 ; + const int halfWidth = m_geometry.width() / 2; + const int halfHeight = m_geometry.height() / 2; + + // Center vertical line. + verts << rect.x() + rect.width() / 2 << rect.y(); + verts << rect.x() + rect.width() / 2 << rect.y() + rect.height(); + + // Center horizontal line. + verts << rect.x() << rect.y() + rect.height() / 2; + verts << rect.x() + rect.width() << rect.y() + rect.height() / 2; + + // Top edge of the window outline. + verts << midX - halfWidth - s_lineWidth / 2 << midY - halfHeight; + verts << midX + halfWidth + s_lineWidth / 2 << midY - halfHeight; + + // Right edge of the window outline. + verts << midX + halfWidth << midY - halfHeight + s_lineWidth / 2; + verts << midX + halfWidth << midY + halfHeight - s_lineWidth / 2; + + // Bottom edge of the window outline. + verts << midX + halfWidth + s_lineWidth / 2 << midY + halfHeight; + verts << midX - halfWidth - s_lineWidth / 2 << midY + halfHeight; + + // Left edge of the window outline. + verts << midX - halfWidth << midY + halfHeight - s_lineWidth / 2; + verts << midX - halfWidth << midY - halfHeight + s_lineWidth / 2; + } + vbo->setData(verts.count() / 2, 2, verts.data(), nullptr); + vbo->render(GL_LINES); + + glDisable(GL_BLEND); + glLineWidth(1.0); + } + if (effects->compositingType() == XRenderCompositing) { +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + for (int i = 0; i < effects->numScreens(); ++i) { + const QRect rect = effects->clientArea(ScreenArea, i, 0); + const int midX = rect.x() + rect.width() / 2; + const int midY = rect.y() + rect.height() / 2 ; + const int halfWidth = m_geometry.width() / 2; + const int halfHeight = m_geometry.height() / 2; + + xcb_rectangle_t rects[6]; + + // Center vertical line. + rects[0].x = rect.x() + rect.width() / 2 - s_lineWidth / 2; + rects[0].y = rect.y(); + rects[0].width = s_lineWidth; + rects[0].height = rect.height(); + + // Center horizontal line. + rects[1].x = rect.x(); + rects[1].y = rect.y() + rect.height() / 2 - s_lineWidth / 2; + rects[1].width = rect.width(); + rects[1].height = s_lineWidth; + + // Top edge of the window outline. + rects[2].x = midX - halfWidth - s_lineWidth / 2; + rects[2].y = midY - halfHeight - s_lineWidth / 2; + rects[2].width = 2 * halfWidth + s_lineWidth; + rects[2].height = s_lineWidth; + + // Right edge of the window outline. + rects[3].x = midX + halfWidth - s_lineWidth / 2; + rects[3].y = midY - halfHeight + s_lineWidth / 2; + rects[3].width = s_lineWidth; + rects[3].height = 2 * halfHeight - s_lineWidth; + + // Bottom edge of the window outline. + rects[4].x = midX - halfWidth - s_lineWidth / 2; + rects[4].y = midY + halfHeight - s_lineWidth / 2; + rects[4].width = 2 * halfWidth + s_lineWidth; + rects[4].height = s_lineWidth; + + // Left edge of the window outline. + rects[5].x = midX - halfWidth - s_lineWidth / 2; + rects[5].y = midY - halfHeight + s_lineWidth / 2; + rects[5].width = s_lineWidth; + rects[5].height = 2 * halfHeight - s_lineWidth; + + QColor color = s_lineColor; + color.setAlphaF(color.alphaF() * opacityFactor); + + xcb_render_fill_rectangles(xcbConnection(), XCB_RENDER_PICT_OP_OVER, effects->xrenderBufferPicture(), + preMultiply(color), 6, rects); + } +#endif + } + if (effects->compositingType() == QPainterCompositing) { + QPainter *painter = effects->scenePainter(); + painter->save(); + QColor color = s_lineColor; + color.setAlphaF(color.alphaF() * opacityFactor); + QPen pen(color); + pen.setWidth(s_lineWidth); + painter->setPen(pen); + painter->setBrush(Qt::NoBrush); + + for (int i = 0; i < effects->numScreens(); ++i) { + const QRect rect = effects->clientArea(ScreenArea, i, 0); + // Center lines. + painter->drawLine(rect.center().x(), rect.y(), rect.center().x(), rect.y() + rect.height()); + painter->drawLine(rect.x(), rect.center().y(), rect.x() + rect.width(), rect.center().y()); + + // Window outline. + QRect outlineRect(0, 0, m_geometry.width(), m_geometry.height()); + outlineRect.moveCenter(rect.center()); + painter->drawRect(outlineRect); + } + painter->restore(); + } +} + +void SnapHelperEffect::postPaintScreen() +{ + if (m_animation.active) { + effects->addRepaint(computeDirtyRegion(m_geometry)); + } + + if (m_animation.timeLine.done()) { + m_animation.active = false; + } + + effects->postPaintScreen(); +} + +void SnapHelperEffect::slotWindowClosed(EffectWindow *w) +{ + if (w != m_window) { + return; + } + + m_window = nullptr; + + m_animation.active = true; + m_animation.timeLine.setDirection(TimeLine::Backward); + + if (m_animation.timeLine.done()) { + m_animation.timeLine.reset(); + } + + effects->addRepaint(computeDirtyRegion(m_geometry)); +} + +void SnapHelperEffect::slotWindowStartUserMovedResized(EffectWindow *w) +{ + if (!w->isMovable()) { + return; + } + + m_window = w; + m_geometry = w->geometry(); + + m_animation.active = true; + m_animation.timeLine.setDirection(TimeLine::Forward); + + if (m_animation.timeLine.done()) { + m_animation.timeLine.reset(); + } + + effects->addRepaint(computeDirtyRegion(m_geometry)); +} + +void SnapHelperEffect::slotWindowFinishUserMovedResized(EffectWindow *w) +{ + if (w != m_window) { + return; + } + + m_window = nullptr; + m_geometry = w->geometry(); + + m_animation.active = true; + m_animation.timeLine.setDirection(TimeLine::Backward); + + if (m_animation.timeLine.done()) { + m_animation.timeLine.reset(); + } + + effects->addRepaint(computeDirtyRegion(m_geometry)); +} + +void SnapHelperEffect::slotWindowFrameGeometryChanged(EffectWindow *w, const QRect &old) +{ + if (w != m_window) { + return; + } + + m_geometry = w->geometry(); + + effects->addRepaint(computeDirtyRegion(old)); +} + +bool SnapHelperEffect::isActive() const +{ + return m_window != nullptr || m_animation.active; +} + +} // namespace KWin diff --git a/effects/snaphelper/snaphelper.h b/effects/snaphelper/snaphelper.h new file mode 100644 index 0000000..4e6c09b --- /dev/null +++ b/effects/snaphelper/snaphelper.h @@ -0,0 +1,55 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_SNAPHELPER_H +#define KWIN_SNAPHELPER_H + +#include + +namespace KWin +{ + +class SnapHelperEffect : public Effect +{ + Q_OBJECT + +public: + SnapHelperEffect(); + ~SnapHelperEffect() override; + + void reconfigure(ReconfigureFlags flags) override; + + void prePaintScreen(ScreenPrePaintData &data, int time) override; + void paintScreen(int mask, const QRegion ®ion, ScreenPaintData &data) override; + void postPaintScreen() override; + + bool isActive() const override; + +private Q_SLOTS: + void slotWindowClosed(EffectWindow *w); + void slotWindowStartUserMovedResized(EffectWindow *w); + void slotWindowFinishUserMovedResized(EffectWindow *w); + void slotWindowFrameGeometryChanged(EffectWindow *w, const QRect &old); + +private: + QRect m_geometry; + EffectWindow *m_window = nullptr; + + struct Animation { + bool active = false; + TimeLine timeLine; + }; + + Animation m_animation; +}; + +} // namespace KWin + +#endif diff --git a/effects/squash/package/contents/code/main.js b/effects/squash/package/contents/code/main.js new file mode 100644 index 0000000..dae246c --- /dev/null +++ b/effects/squash/package/contents/code/main.js @@ -0,0 +1,155 @@ +/* + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +"use strict"; + +var squashEffect = { + duration: animationTime(250), + loadConfig: function () { + squashEffect.duration = animationTime(250); + }, + slotWindowMinimized: function (window) { + if (effects.hasActiveFullScreenEffect) { + return; + } + + // If the window doesn't have an icon in the task manager, + // don't animate it. + var iconRect = window.iconGeometry; + if (iconRect.width == 0 || iconRect.height == 0) { + return; + } + + if (window.unminimizeAnimation) { + if (redirect(window.unminimizeAnimation, Effect.Backward)) { + return; + } + cancel(window.unminimizeAnimation); + delete window.unminimizeAnimation; + } + + if (window.minimizeAnimation) { + if (redirect(window.minimizeAnimation, Effect.Forward)) { + return; + } + cancel(window.minimizeAnimation); + } + + var windowRect = window.geometry; + + window.minimizeAnimation = animate({ + window: window, + curve: QEasingCurve.InOutSine, + duration: squashEffect.duration, + animations: [ + { + type: Effect.Size, + from: { + value1: windowRect.width, + value2: windowRect.height + }, + to: { + value1: iconRect.width, + value2: iconRect.height + } + }, + { + type: Effect.Translation, + from: { + value1: 0.0, + value2: 0.0 + }, + to: { + value1: iconRect.x - windowRect.x - + (windowRect.width - iconRect.width) / 2, + value2: iconRect.y - windowRect.y - + (windowRect.height - iconRect.height) / 2, + } + }, + { + type: Effect.Opacity, + from: 1.0, + to: 0.0 + } + ] + }); + }, + slotWindowUnminimized: function (window) { + if (effects.hasActiveFullScreenEffect) { + return; + } + + // If the window doesn't have an icon in the task manager, + // don't animate it. + var iconRect = window.iconGeometry; + if (iconRect.width == 0 || iconRect.height == 0) { + return; + } + + if (window.minimizeAnimation) { + if (redirect(window.minimizeAnimation, Effect.Backward)) { + return; + } + cancel(window.minimizeAnimation); + delete window.minimizeAnimation; + } + + if (window.unminimizeAnimation) { + if (redirect(window.unminimizeAnimation, Effect.Forward)) { + return; + } + cancel(window.unminimizeAnimation); + } + + var windowRect = window.geometry; + + window.unminimizeAnimation = animate({ + window: window, + curve: QEasingCurve.InOutSine, + duration: squashEffect.duration, + animations: [ + { + type: Effect.Size, + from: { + value1: iconRect.width, + value2: iconRect.height + }, + to: { + value1: windowRect.width, + value2: windowRect.height + } + }, + { + type: Effect.Translation, + from: { + value1: iconRect.x - windowRect.x - + (windowRect.width - iconRect.width) / 2, + value2: iconRect.y - windowRect.y - + (windowRect.height - iconRect.height) / 2, + }, + to: { + value1: 0.0, + value2: 0.0 + } + }, + { + type: Effect.Opacity, + from: 0.0, + to: 1.0 + } + ] + }); + }, + init: function () { + effect.configChanged.connect(squashEffect.loadConfig); + effects.windowMinimized.connect(squashEffect.slotWindowMinimized); + effects.windowUnminimized.connect(squashEffect.slotWindowUnminimized); + } +}; + +squashEffect.init(); diff --git a/effects/squash/package/metadata.desktop b/effects/squash/package/metadata.desktop new file mode 100644 index 0000000..90d44b3 --- /dev/null +++ b/effects/squash/package/metadata.desktop @@ -0,0 +1,83 @@ +[Desktop Entry] +Comment=Squash windows when they are minimized +Comment[az]=Yığılarkən pəncrəni sıxlaşdıqmaq +Comment[ca]=Amuntega les finestres quan estan minimitzades +Comment[ca@valencia]=Amuntega les finestres quan estan minimitzades +Comment[da]=Mas vinduer nÃ¥r de minimeres +Comment[de]=Quetscht Fenster beim Minimieren zusammen +Comment[en_GB]=Squash windows when they are minimised +Comment[es]=Aplastar las ventanas al minimizarlas +Comment[et]=Minimeeritud akende taas üleshüpitamine +Comment[eu]=Zanpatu leihoak haiek ikonotzean +Comment[fi]=Litistä ikkunat, kun ne pienennetään +Comment[fr]=Écrase les fenêtres lorsqu'elles sont minimisées +Comment[gl]=Xuntar as xanelas cando estean minimizadas +Comment[ia]=Deforma fenestras durante que illes es minimisate +Comment[id]=Sesakkan window ketika mereka diminimalkan +Comment[it]=Schiaccia le finestre quando vengono minimizzate +Comment[ko]=창을 최소화할 때 압축시킵니다 +Comment[lt]=SutraiÅ¡kyti langus, juos suskleidžiant +Comment[nl]=Krimp vensters wanneer ze geminimaliseerd zijn +Comment[nn]=Skvis vindauge nÃ¥r dei vert minimerte +Comment[pl]=Ściąga okna przy ich minimalizacji +Comment[pt]=Esmagar as janelas quando são minimizadas +Comment[pt_BR]=Achatar as janelas quando são minimizadas +Comment[ro]=Strivește ferestrele când sunt minimizate +Comment[ru]=Сжатие окна при сворачивании +Comment[sk]=Deformuje okná pri ich minimalizovaní +Comment[sl]=Zmečkaj okna, ko jih strneÅ¡ +Comment[sv]=Kläm fönster när de minimeras +Comment[uk]=Складує вікна, якщо їх мінімізовано +Comment[x-test]=xxSquash windows when they are minimizedxx +Comment[zh_CN]=最小化时压扁窗口 +Comment[zh_TW]=壓縮最小化的視窗 +Icon=preferences-system-windows-effect-squash +Name=Squash +Name[az]=Sıxılma +Name[ca]=Amuntega +Name[ca@valencia]=Amuntega +Name[da]=Mas +Name[de]=Quetschen +Name[en_GB]=Squash +Name[es]=Aplastar +Name[et]=Üleshüpe +Name[eu]=Zanpatu +Name[fi]=Litistä +Name[fr]=Écraser +Name[gl]=Xuntar +Name[ia]=Squash +Name[id]=Sesakkan +Name[it]=Schiaccia +Name[ko]=압축 +Name[lt]=SutraiÅ¡kymas +Name[nl]=Krimpen +Name[nn]=Skvis +Name[pl]=Ściąganie +Name[pt]=Esmagar +Name[pt_BR]=Achatar +Name[ro]=Strivire +Name[ru]=Сжатие +Name[sk]=RozpučiÅ¥ +Name[sl]=Zmečkaj +Name[sv]=Kläm +Name[uk]=Складування +Name[x-test]=xxSquashxx +Name[zh_CN]=压扁 +Name[zh_TW]=壓縮 + +Type=Service +X-KDE-ParentApp= +X-KDE-PluginInfo-Author=Rivo Laks, Vlad Zahorodnii +X-KDE-PluginInfo-Category=Appearance +X-KDE-PluginInfo-Email=rivolaks@hot.ee, vlad.zahorodnii@kde.org +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-Name=kwin4_effect_squash +X-KDE-PluginInfo-Version=1 +X-KDE-PluginInfo-Website= +X-KDE-ServiceTypes=KWin/Effect +X-KDE-PluginInfo-EnabledByDefault=true +X-KDE-Ordering=60 +X-Plasma-API=javascript +X-Plasma-MainScript=code/main.js +X-KWin-Exclusive-Category=minimize +X-KWin-Video-Url=https://files.kde.org/plasma/kwin/effect-videos/minimize.ogv diff --git a/effects/startupfeedback/CMakeLists.txt b/effects/startupfeedback/CMakeLists.txt new file mode 100644 index 0000000..da311aa --- /dev/null +++ b/effects/startupfeedback/CMakeLists.txt @@ -0,0 +1,7 @@ +####################################### +# Effect + +# Source files +set(kwin4_effect_builtins_sources ${kwin4_effect_builtins_sources} + startupfeedback/startupfeedback.cpp +) diff --git a/effects/startupfeedback/data/1.10/blinking-startup-fragment.glsl b/effects/startupfeedback/data/1.10/blinking-startup-fragment.glsl new file mode 100644 index 0000000..3229f58 --- /dev/null +++ b/effects/startupfeedback/data/1.10/blinking-startup-fragment.glsl @@ -0,0 +1,13 @@ +uniform sampler2D sampler; +uniform vec4 geometryColor; + +varying vec2 texcoord0; + +void main() +{ + vec4 tex = texture2D(sampler, texcoord0); + if (tex.a != 1.0) { + tex = geometryColor; + } + gl_FragColor = tex; +} diff --git a/effects/startupfeedback/data/1.40/blinking-startup-fragment.glsl b/effects/startupfeedback/data/1.40/blinking-startup-fragment.glsl new file mode 100644 index 0000000..88c7219 --- /dev/null +++ b/effects/startupfeedback/data/1.40/blinking-startup-fragment.glsl @@ -0,0 +1,16 @@ +#version 140 +uniform sampler2D sampler; +uniform vec4 geometryColor; + +in vec2 texcoord0; + +out vec4 fragColor; + +void main() +{ + vec4 tex = texture(sampler, texcoord0); + if (tex.a != 1.0) { + tex = geometryColor; + } + fragColor = tex; +} diff --git a/effects/startupfeedback/startupfeedback.cpp b/effects/startupfeedback/startupfeedback.cpp new file mode 100644 index 0000000..065d5dd --- /dev/null +++ b/effects/startupfeedback/startupfeedback.cpp @@ -0,0 +1,388 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2010 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "startupfeedback.h" +// Qt +#include +#include +#include +#include +#include +#include +// KDE +#include +#include +#include +#include +#include +// KWin +#include + +// based on StartupId in KRunner by Lubos Lunak +// SPDX-FileCopyrightText: 2001 Lubos Lunak + +namespace KWin +{ + +// number of key frames for bouncing animation +static const int BOUNCE_FRAMES = 20; +// duration between two key frames in msec +static const int BOUNCE_FRAME_DURATION = 30; +// duration of one bounce animation +static const int BOUNCE_DURATION = BOUNCE_FRAME_DURATION * BOUNCE_FRAMES; +// number of key frames for blinking animation +static const int BLINKING_FRAMES = 5; +// duration between two key frames in msec +static const int BLINKING_FRAME_DURATION = 100; +// duration of one blinking animation +static const int BLINKING_DURATION = BLINKING_FRAME_DURATION * BLINKING_FRAMES; +//const int color_to_pixmap[] = { 0, 1, 2, 3, 2, 1 }; +static const int FRAME_TO_BOUNCE_YOFFSET[] = { + -5, -1, 2, 5, 8, 10, 12, 13, 15, 15, 15, 15, 14, 12, 10, 8, 5, 2, -1, -5 +}; +static const QSize BOUNCE_SIZES[] = { + QSize(16, 16), QSize(14, 18), QSize(12, 20), QSize(18, 14), QSize(20, 12) +}; +static const int FRAME_TO_BOUNCE_TEXTURE[] = { + 0, 0, 0, 1, 2, 2, 1, 0, 3, 4, 4, 3, 0, 1, 2, 2, 1, 0, 0, 0 +}; +static const int FRAME_TO_BLINKING_COLOR[] = { + 0, 1, 2, 3, 2, 1 +}; +static const QColor BLINKING_COLORS[] = { + Qt::black, Qt::darkGray, Qt::lightGray, Qt::white, Qt::white +}; +static const int s_startupDefaultTimeout = 5; + +StartupFeedbackEffect::StartupFeedbackEffect() + : m_bounceSizesRatio(1.0) + , m_startupInfo(new KStartupInfo(KStartupInfo::CleanOnCantDetect, this)) + , m_selection(nullptr) + , m_active(false) + , m_frame(0) + , m_progress(0) + , m_texture(nullptr) + , m_type(BouncingFeedback) + , m_blinkingShader(nullptr) + , m_cursorSize(24) + , m_configWatcher(KConfigWatcher::create(KSharedConfig::openConfig("klaunchrc", KConfig::NoGlobals))) +{ + for (int i = 0; i < 5; ++i) { + m_bouncingTextures[i] = nullptr; + } + if (KWindowSystem::isPlatformX11()) { + m_selection = new KSelectionOwner("_KDE_STARTUP_FEEDBACK", xcbConnection(), x11RootWindow(), this); + m_selection->claim(true); + } + connect(m_startupInfo, &KStartupInfo::gotNewStartup, this, &StartupFeedbackEffect::gotNewStartup); + connect(m_startupInfo, &KStartupInfo::gotRemoveStartup, this, &StartupFeedbackEffect::gotRemoveStartup); + connect(m_startupInfo, &KStartupInfo::gotStartupChange, this, &StartupFeedbackEffect::gotStartupChange); + connect(effects, &EffectsHandler::mouseChanged, this, &StartupFeedbackEffect::slotMouseChanged); + connect(m_configWatcher.data(), &KConfigWatcher::configChanged, this, [this]() { + reconfigure(ReconfigureAll); + }); + reconfigure(ReconfigureAll); + +} + +StartupFeedbackEffect::~StartupFeedbackEffect() +{ + if (m_active) { + effects->stopMousePolling(); + } + for (int i = 0; i < 5; ++i) { + delete m_bouncingTextures[i]; + } + delete m_texture; + delete m_blinkingShader; +} + +bool StartupFeedbackEffect::supported() +{ + return effects->isOpenGLCompositing(); +} + +void StartupFeedbackEffect::reconfigure(Effect::ReconfigureFlags flags) +{ + Q_UNUSED(flags) + KConfigGroup c = m_configWatcher->config()->group("FeedbackStyle"); + const bool busyCursor = c.readEntry("BusyCursor", true); + + c = m_configWatcher->config()->group("BusyCursorSettings"); + m_startupInfo->setTimeout(c.readEntry("Timeout", s_startupDefaultTimeout)); + const bool busyBlinking = c.readEntry("Blinking", false); + const bool busyBouncing = c.readEntry("Bouncing", true); + if (!busyCursor) + m_type = NoFeedback; + else if (busyBouncing) + m_type = BouncingFeedback; + else if (busyBlinking) { + m_type = BlinkingFeedback; + if (effects->compositingType() == OpenGL2Compositing) { + delete m_blinkingShader; + m_blinkingShader = ShaderManager::instance()->generateShaderFromResources(ShaderTrait::MapTexture, QString(), QStringLiteral("blinking-startup-fragment.glsl")); + if (m_blinkingShader->isValid()) { + qCDebug(KWINEFFECTS) << "Blinking Shader is valid"; + } else { + qCDebug(KWINEFFECTS) << "Blinking Shader is not valid"; + } + } + } else + m_type = PassiveFeedback; + if (m_active) { + stop(); + start(m_startups[ m_currentStartup ]); + } +} + +void StartupFeedbackEffect::prePaintScreen(ScreenPrePaintData& data, int time) +{ + if (m_active) { + // need the unclipped version + switch(m_type) { + case BouncingFeedback: + m_progress = (m_progress + time) % BOUNCE_DURATION; + m_frame = qRound((qreal)m_progress / (qreal)BOUNCE_FRAME_DURATION) % BOUNCE_FRAMES; + m_currentGeometry = feedbackRect(); // bounce alters geometry with m_frame + data.paint = data.paint.united(m_currentGeometry); + break; + case BlinkingFeedback: + m_progress = (m_progress + time) % BLINKING_DURATION; + m_frame = qRound((qreal)m_progress / (qreal)BLINKING_FRAME_DURATION) % BLINKING_FRAMES; + break; + default: + break; // nothing + } + } + effects->prePaintScreen(data, time); +} + +void StartupFeedbackEffect::paintScreen(int mask, const QRegion ®ion, ScreenPaintData& data) +{ + effects->paintScreen(mask, region, data); + if (m_active) { + GLTexture* texture; + switch(m_type) { + case BouncingFeedback: + texture = m_bouncingTextures[ FRAME_TO_BOUNCE_TEXTURE[ m_frame ]]; + break; + case BlinkingFeedback: // fall through + case PassiveFeedback: + texture = m_texture; + break; + default: + return; // safety + } + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + texture->bind(); + if (m_type == BlinkingFeedback && m_blinkingShader && m_blinkingShader->isValid()) { + const QColor& blinkingColor = BLINKING_COLORS[ FRAME_TO_BLINKING_COLOR[ m_frame ]]; + ShaderManager::instance()->pushShader(m_blinkingShader); + m_blinkingShader->setUniform(GLShader::Color, blinkingColor); + } else { + ShaderManager::instance()->pushShader(ShaderTrait::MapTexture); + } + QMatrix4x4 mvp = data.projectionMatrix(); + mvp.translate(m_currentGeometry.x(), m_currentGeometry.y()); + ShaderManager::instance()->getBoundShader()->setUniform(GLShader::ModelViewProjectionMatrix, mvp); + texture->render(m_currentGeometry, m_currentGeometry); + ShaderManager::instance()->popShader(); + texture->unbind(); + glDisable(GL_BLEND); + } +} + +void StartupFeedbackEffect::postPaintScreen() +{ + if (m_active) { + m_dirtyRect = m_currentGeometry; // ensure the now dirty region is cleaned on the next pass + if (m_type == BlinkingFeedback || m_type == BouncingFeedback) + effects->addRepaint(m_dirtyRect); // we also have to trigger a repaint + } + effects->postPaintScreen(); +} + +void StartupFeedbackEffect::slotMouseChanged(const QPoint& pos, const QPoint& oldpos, Qt::MouseButtons buttons, + Qt::MouseButtons oldbuttons, Qt::KeyboardModifiers modifiers, Qt::KeyboardModifiers oldmodifiers) +{ + Q_UNUSED(pos) + Q_UNUSED(oldpos) + Q_UNUSED(buttons) + Q_UNUSED(oldbuttons) + Q_UNUSED(modifiers) + Q_UNUSED(oldmodifiers) + if (m_active) { + m_dirtyRect |= m_currentGeometry; + m_currentGeometry = feedbackRect(); + m_dirtyRect |= m_currentGeometry; + effects->addRepaint(m_dirtyRect); + } +} + +void StartupFeedbackEffect::gotNewStartup(const KStartupInfoId& id, const KStartupInfoData& data) +{ + const QString& icon = data.findIcon(); + m_currentStartup = id; + m_startups[ id ] = icon; + start(icon); +} + +void StartupFeedbackEffect::gotRemoveStartup(const KStartupInfoId& id, const KStartupInfoData& data) +{ + Q_UNUSED( data ) + m_startups.remove(id); + if (m_startups.count() == 0) { + m_currentStartup = KStartupInfoId(); // null + stop(); + return; + } + m_currentStartup = m_startups.begin().key(); + start(m_startups[ m_currentStartup ]); +} + +void StartupFeedbackEffect::gotStartupChange(const KStartupInfoId& id, const KStartupInfoData& data) +{ + if (m_currentStartup == id) { + const QString& icon = data.findIcon(); + if (!icon.isEmpty() && icon != m_startups[ m_currentStartup ]) { + m_startups[ id ] = icon; + start(icon); + } + } +} + +void StartupFeedbackEffect::start(const QString& icon) +{ + if (m_type == NoFeedback) + return; + if (!m_active) + effects->startMousePolling(); + m_active = true; + auto readCursorSize = []() -> int { + // read details about the mouse-cursor theme define per default + KConfigGroup mousecfg(effects->inputConfig(), "Mouse"); + int cursorSize = mousecfg.readEntry("cursorSize", 24); + return cursorSize; + }; + m_cursorSize = readCursorSize(); + int iconSize = m_cursorSize / 1.5; + if (!iconSize) { + iconSize = QApplication::style()->pixelMetric(QStyle::PM_SmallIconSize); + } + // get ratio for bouncing cursor so we don't need to manually calculate the sizes for each icon size + if (m_type == BouncingFeedback) + m_bounceSizesRatio = iconSize / 16.0; + const QPixmap iconPixmap = QIcon::fromTheme(icon, QIcon::fromTheme(QStringLiteral("system-run"))).pixmap(iconSize); + prepareTextures(iconPixmap); + m_dirtyRect = m_currentGeometry = feedbackRect(); + effects->addRepaint(m_dirtyRect); +} + +void StartupFeedbackEffect::stop() +{ + if (m_active) + effects->stopMousePolling(); + m_active = false; + effects->makeOpenGLContextCurrent(); + switch(m_type) { + case BouncingFeedback: + for (int i = 0; i < 5; ++i) { + delete m_bouncingTextures[i]; + m_bouncingTextures[i] = nullptr; + } + break; + case BlinkingFeedback: + case PassiveFeedback: + delete m_texture; + m_texture = nullptr; + break; + case NoFeedback: + return; // don't want the full repaint + default: + break; // impossible + } + effects->addRepaintFull(); +} + +void StartupFeedbackEffect::prepareTextures(const QPixmap& pix) +{ + effects->makeOpenGLContextCurrent(); + switch(m_type) { + case BouncingFeedback: + for (int i = 0; i < 5; ++i) { + delete m_bouncingTextures[i]; + m_bouncingTextures[i] = new GLTexture(scalePixmap(pix, BOUNCE_SIZES[i])); + } + break; + case BlinkingFeedback: + case PassiveFeedback: + m_texture = new GLTexture(pix); + break; + default: + // for safety + m_active = false; + break; + } +} + +QImage StartupFeedbackEffect::scalePixmap(const QPixmap& pm, const QSize& size) const +{ + const QSize& adjustedSize = size * m_bounceSizesRatio; + QImage scaled = pm.toImage().scaled(adjustedSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + if (scaled.format() != QImage::Format_ARGB32_Premultiplied && scaled.format() != QImage::Format_ARGB32) + scaled = scaled.convertToFormat(QImage::Format_ARGB32); + + QImage result(20 * m_bounceSizesRatio, 20 * m_bounceSizesRatio, QImage::Format_ARGB32); + QPainter p(&result); + p.setCompositionMode(QPainter::CompositionMode_Source); + p.fillRect(result.rect(), Qt::transparent); + p.drawImage((20 * m_bounceSizesRatio - adjustedSize.width()) / 2, (20*m_bounceSizesRatio - adjustedSize.height()) / 2, scaled, 0, 0, adjustedSize.width(), adjustedSize.height() * m_bounceSizesRatio); + return result; +} + +QRect StartupFeedbackEffect::feedbackRect() const +{ + int xDiff; + if (m_cursorSize <= 16) + xDiff = 8 + 7; + else if (m_cursorSize <= 32) + xDiff = 16 + 7; + else if (m_cursorSize <= 48) + xDiff = 24 + 7; + else + xDiff = 32 + 7; + int yDiff = xDiff; + GLTexture* texture = nullptr; + int yOffset = 0; + switch(m_type) { + case BouncingFeedback: + texture = m_bouncingTextures[ FRAME_TO_BOUNCE_TEXTURE[ m_frame ]]; + yOffset = FRAME_TO_BOUNCE_YOFFSET[ m_frame ] * m_bounceSizesRatio; + break; + case BlinkingFeedback: // fall through + case PassiveFeedback: + texture = m_texture; + break; + default: + // nothing + break; + } + const QPoint cursorPos = effects->cursorPos() + QPoint(xDiff, yDiff + yOffset); + QRect rect; + if( texture ) + rect = QRect(cursorPos, texture->size()); + return rect; +} + +bool StartupFeedbackEffect::isActive() const +{ + return m_active; +} + +} // namespace diff --git a/effects/startupfeedback/startupfeedback.h b/effects/startupfeedback/startupfeedback.h new file mode 100644 index 0000000..81070eb --- /dev/null +++ b/effects/startupfeedback/startupfeedback.h @@ -0,0 +1,84 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2010 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_STARTUPFEEDBACK_H +#define KWIN_STARTUPFEEDBACK_H +#include +#include +#include +#include + +class KSelectionOwner; +namespace KWin +{ +class GLTexture; + +class StartupFeedbackEffect + : public Effect +{ + Q_OBJECT + Q_PROPERTY(int type READ type) +public: + StartupFeedbackEffect(); + ~StartupFeedbackEffect() override; + + void reconfigure(ReconfigureFlags flags) override; + void prePaintScreen(ScreenPrePaintData& data, int time) override; + void paintScreen(int mask, const QRegion ®ion, ScreenPaintData& data) override; + void postPaintScreen() override; + bool isActive() const override; + + int requestedEffectChainPosition() const override { + return 90; + } + + int type() const { + return int(m_type); + } + + static bool supported(); + +private Q_SLOTS: + void gotNewStartup(const KStartupInfoId& id, const KStartupInfoData& data); + void gotRemoveStartup(const KStartupInfoId& id, const KStartupInfoData& data); + void gotStartupChange(const KStartupInfoId& id, const KStartupInfoData& data); + void slotMouseChanged(const QPoint& pos, const QPoint& oldpos, Qt::MouseButtons buttons, Qt::MouseButtons oldbuttons, Qt::KeyboardModifiers modifiers, Qt::KeyboardModifiers oldmodifiers); + +private: + enum FeedbackType { + NoFeedback, + BouncingFeedback, + BlinkingFeedback, + PassiveFeedback + }; + void start(const QString& icon); + void stop(); + QImage scalePixmap(const QPixmap& pm, const QSize& size) const; + void prepareTextures(const QPixmap& pix); + QRect feedbackRect() const; + + qreal m_bounceSizesRatio; + KStartupInfo* m_startupInfo; + KSelectionOwner* m_selection; + KStartupInfoId m_currentStartup; + QMap< KStartupInfoId, QString > m_startups; // QString == pixmap + bool m_active; + int m_frame; + int m_progress; + GLTexture* m_bouncingTextures[5]; + GLTexture* m_texture; // for passive and blinking + FeedbackType m_type; + QRect m_currentGeometry, m_dirtyRect; + GLShader *m_blinkingShader; + int m_cursorSize; + KConfigWatcher::Ptr m_configWatcher; +}; +} // namespace + +#endif diff --git a/effects/thumbnailaside/CMakeLists.txt b/effects/thumbnailaside/CMakeLists.txt new file mode 100644 index 0000000..dacfb2a --- /dev/null +++ b/effects/thumbnailaside/CMakeLists.txt @@ -0,0 +1,24 @@ +####################################### +# Config +set(kwin_thumbnailaside_config_SRCS thumbnailaside_config.cpp) +ki18n_wrap_ui(kwin_thumbnailaside_config_SRCS thumbnailaside_config.ui) +kconfig_add_kcfg_files(kwin_thumbnailaside_config_SRCS thumbnailasideconfig.kcfgc) + +add_library(kwin_thumbnailaside_config MODULE ${kwin_thumbnailaside_config_SRCS}) + +target_link_libraries(kwin_thumbnailaside_config + KF5::ConfigWidgets + KF5::GlobalAccel + KF5::I18n + KF5::XmlGui + KWinEffectsInterface +) + +kcoreaddons_desktop_to_json(kwin_thumbnailaside_config thumbnailaside_config.desktop SERVICE_TYPES kcmodule.desktop) + +install( + TARGETS + kwin_thumbnailaside_config + DESTINATION + ${PLUGIN_INSTALL_DIR}/kwin/effects/configs +) diff --git a/effects/thumbnailaside/thumbnailaside.cpp b/effects/thumbnailaside/thumbnailaside.cpp new file mode 100644 index 0000000..9bbda90 --- /dev/null +++ b/effects/thumbnailaside/thumbnailaside.cpp @@ -0,0 +1,185 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Lubos Lunak + SPDX-FileCopyrightText: 2007 Christian Nitschkowski + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "thumbnailaside.h" +// KConfigSkeleton +#include "thumbnailasideconfig.h" + +#include +#include + +#include +#include + +namespace KWin +{ + +ThumbnailAsideEffect::ThumbnailAsideEffect() +{ + initConfig(); + QAction* a = new QAction(this); + a->setObjectName(QStringLiteral("ToggleCurrentThumbnail")); + a->setText(i18n("Toggle Thumbnail for Current Window")); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::META + Qt::CTRL + Qt::Key_T); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::META + Qt::CTRL + Qt::Key_T); + effects->registerGlobalShortcut(Qt::META + Qt::CTRL + Qt::Key_T, a); + connect(a, &QAction::triggered, this, &ThumbnailAsideEffect::toggleCurrentThumbnail); + + connect(effects, &EffectsHandler::windowClosed, this, &ThumbnailAsideEffect::slotWindowClosed); + connect(effects, &EffectsHandler::windowFrameGeometryChanged, this, &ThumbnailAsideEffect::slotWindowFrameGeometryChanged); + connect(effects, &EffectsHandler::windowDamaged, this, &ThumbnailAsideEffect::slotWindowDamaged); + connect(effects, &EffectsHandler::screenLockingChanged, this, &ThumbnailAsideEffect::repaintAll); + reconfigure(ReconfigureAll); +} + +void ThumbnailAsideEffect::reconfigure(ReconfigureFlags) +{ + ThumbnailAsideConfig::self()->read(); + maxwidth = ThumbnailAsideConfig::maxWidth(); + spacing = ThumbnailAsideConfig::spacing(); + opacity = ThumbnailAsideConfig::opacity()/100.0; + screen = ThumbnailAsideConfig::screen(); // Xinerama screen TODO add gui option + arrange(); +} + +void ThumbnailAsideEffect::paintScreen(int mask, const QRegion ®ion, ScreenPaintData& data) +{ + painted = QRegion(); + effects->paintScreen(mask, region, data); + + const QMatrix4x4 projectionMatrix = data.projectionMatrix(); + foreach (const Data & d, windows) { + if (painted.intersects(d.rect)) { + WindowPaintData data(d.window, projectionMatrix); + data.multiplyOpacity(opacity); + QRect region; + setPositionTransformations(data, region, d.window, d.rect, Qt::KeepAspectRatio); + effects->drawWindow(d.window, PAINT_WINDOW_OPAQUE | PAINT_WINDOW_TRANSLUCENT | PAINT_WINDOW_TRANSFORMED | PAINT_WINDOW_LANCZOS, + region, data); + } + } +} + +void ThumbnailAsideEffect::paintWindow(EffectWindow *w, int mask, QRegion region, WindowPaintData &data) +{ + effects->paintWindow(w, mask, region, data); + painted |= region; +} + +void ThumbnailAsideEffect::slotWindowDamaged(EffectWindow* w, const QRect&) +{ + foreach (const Data & d, windows) { + if (d.window == w) + effects->addRepaint(d.rect); + } +} + +void ThumbnailAsideEffect::slotWindowFrameGeometryChanged(EffectWindow* w, const QRect& old) +{ + foreach (const Data & d, windows) { + if (d.window == w) { + if (w->size() == old.size()) + effects->addRepaint(d.rect); + else + arrange(); + return; + } + } +} + +void ThumbnailAsideEffect::slotWindowClosed(EffectWindow* w) +{ + removeThumbnail(w); +} + +void ThumbnailAsideEffect::toggleCurrentThumbnail() +{ + EffectWindow* active = effects->activeWindow(); + if (active == nullptr) + return; + if (windows.contains(active)) + removeThumbnail(active); + else + addThumbnail(active); +} + +void ThumbnailAsideEffect::addThumbnail(EffectWindow* w) +{ + repaintAll(); // repaint old areas + Data d; + d.window = w; + d.index = windows.count(); + windows[ w ] = d; + arrange(); +} + +void ThumbnailAsideEffect::removeThumbnail(EffectWindow* w) +{ + if (!windows.contains(w)) + return; + repaintAll(); // repaint old areas + int index = windows[ w ].index; + windows.remove(w); + for (QHash< EffectWindow*, Data >::Iterator it = windows.begin(); + it != windows.end(); + ++it) { + Data& d = *it; + if (d.index > index) + --d.index; + } + arrange(); +} + +void ThumbnailAsideEffect::arrange() +{ + if (windows.size() == 0) + return; + int height = 0; + QVector< int > pos(windows.size()); + int mwidth = 0; + foreach (const Data & d, windows) { + height += d.window->height(); + mwidth = qMax(mwidth, d.window->width()); + pos[ d.index ] = d.window->height(); + } + QRect area = effects->clientArea(MaximizeArea, screen, effects->currentDesktop()); + double scale = area.height() / double(height); + scale = qMin(scale, maxwidth / double(mwidth)); // don't be wider than maxwidth pixels + int add = 0; + for (int i = 0; + i < windows.size(); + ++i) { + pos[ i ] = int(pos[ i ] * scale); + pos[ i ] += spacing + add; // compute offset of each item + add = pos[ i ]; + } + for (QHash< EffectWindow*, Data >::Iterator it = windows.begin(); + it != windows.end(); + ++it) { + Data& d = *it; + int width = int(d.window->width() * scale); + d.rect = QRect(area.right() - width, area.bottom() - pos[ d.index ], width, int(d.window->height() * scale)); + } + repaintAll(); +} + +void ThumbnailAsideEffect::repaintAll() +{ + foreach (const Data & d, windows) + effects->addRepaint(d.rect); +} + +bool ThumbnailAsideEffect::isActive() const +{ + return !windows.isEmpty() && !effects->isScreenLocked(); +} + +} // namespace + diff --git a/effects/thumbnailaside/thumbnailaside.h b/effects/thumbnailaside/thumbnailaside.h new file mode 100644 index 0000000..ef3f004 --- /dev/null +++ b/effects/thumbnailaside/thumbnailaside.h @@ -0,0 +1,80 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Lubos Lunak + SPDX-FileCopyrightText: 2007 Christian Nitschkowski + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +/* + + Testing of painting a window more than once. + +*/ + +#ifndef KWIN_THUMBNAILASIDE_H +#define KWIN_THUMBNAILASIDE_H + +#include + +#include + +namespace KWin +{ + +class ThumbnailAsideEffect + : public Effect +{ + Q_OBJECT + Q_PROPERTY(int maxWidth READ configuredMaxWidth) + Q_PROPERTY(int spacing READ configuredSpacing) + Q_PROPERTY(qreal opacity READ configuredOpacity) + Q_PROPERTY(int screen READ configuredScreen) +public: + ThumbnailAsideEffect(); + void reconfigure(ReconfigureFlags) override; + void paintScreen(int mask, const QRegion ®ion, ScreenPaintData& data) override; + void paintWindow(EffectWindow *w, int mask, QRegion region, WindowPaintData &data) override; + + // for properties + int configuredMaxWidth() const { + return maxwidth; + } + int configuredSpacing() const { + return spacing; + } + qreal configuredOpacity() const { + return opacity; + } + int configuredScreen() const { + return screen; + } +private Q_SLOTS: + void toggleCurrentThumbnail(); + void slotWindowClosed(KWin::EffectWindow *w); + void slotWindowFrameGeometryChanged(KWin::EffectWindow *w, const QRect &old); + void slotWindowDamaged(KWin::EffectWindow* w, const QRect& damage); + bool isActive() const override; + void repaintAll(); +private: + void addThumbnail(EffectWindow* w); + void removeThumbnail(EffectWindow* w); + void arrange(); + struct Data { + EffectWindow* window; // the same like the key in the hash (makes code simpler) + int index; + QRect rect; + }; + QHash< EffectWindow*, Data > windows; + int maxwidth; + int spacing; + double opacity; + int screen; + QRegion painted; +}; + +} // namespace + +#endif diff --git a/effects/thumbnailaside/thumbnailaside.kcfg b/effects/thumbnailaside/thumbnailaside.kcfg new file mode 100644 index 0000000..a44fb55 --- /dev/null +++ b/effects/thumbnailaside/thumbnailaside.kcfg @@ -0,0 +1,21 @@ + + + + + + 200 + + + 10 + + + 50 + + + -1 + + + diff --git a/effects/thumbnailaside/thumbnailaside_config.cpp b/effects/thumbnailaside/thumbnailaside_config.cpp new file mode 100644 index 0000000..466f6bb --- /dev/null +++ b/effects/thumbnailaside/thumbnailaside_config.cpp @@ -0,0 +1,90 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Christian Nitschkowski + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "thumbnailaside_config.h" +// KConfigSkeleton +#include "thumbnailasideconfig.h" +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +K_PLUGIN_FACTORY_WITH_JSON(ThumbnailAsideEffectConfigFactory, + "thumbnailaside_config.json", + registerPlugin();) + +namespace KWin +{ + +ThumbnailAsideEffectConfigForm::ThumbnailAsideEffectConfigForm(QWidget* parent) : QWidget(parent) +{ + setupUi(this); +} + +ThumbnailAsideEffectConfig::ThumbnailAsideEffectConfig(QWidget* parent, const QVariantList& args) : + KCModule(KAboutData::pluginData(QStringLiteral("thumbnailaside")), parent, args) +{ + m_ui = new ThumbnailAsideEffectConfigForm(this); + + QVBoxLayout* layout = new QVBoxLayout(this); + + layout->addWidget(m_ui); + + connect(m_ui->editor, &KShortcutsEditor::keyChange, this, &ThumbnailAsideEffectConfig::markAsChanged); + + ThumbnailAsideConfig::instance(KWIN_CONFIG); + addConfig(ThumbnailAsideConfig::self(), this); + + // Shortcut config. The shortcut belongs to the component "kwin"! + m_actionCollection = new KActionCollection(this, QStringLiteral("kwin")); + + m_actionCollection->setComponentDisplayName(i18n("KWin")); + m_actionCollection->setConfigGroup(QStringLiteral("ThumbnailAside")); + m_actionCollection->setConfigGlobal(true); + + QAction* a = m_actionCollection->addAction(QStringLiteral("ToggleCurrentThumbnail")); + a->setText(i18n("Toggle Thumbnail for Current Window")); + a->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::META + Qt::CTRL + Qt::Key_T); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::META + Qt::CTRL + Qt::Key_T); + + m_ui->editor->addCollection(m_actionCollection); + + load(); +} + +ThumbnailAsideEffectConfig::~ThumbnailAsideEffectConfig() +{ + // Undo (only) unsaved changes to global key shortcuts + m_ui->editor->undoChanges(); +} + +void ThumbnailAsideEffectConfig::save() +{ + KCModule::save(); + m_ui->editor->save(); + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("thumbnailaside")); +} + +} // namespace + +#include "thumbnailaside_config.moc" diff --git a/effects/thumbnailaside/thumbnailaside_config.desktop b/effects/thumbnailaside/thumbnailaside_config.desktop new file mode 100644 index 0000000..8b268e1 --- /dev/null +++ b/effects/thumbnailaside/thumbnailaside_config.desktop @@ -0,0 +1,83 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=KCModule + +X-KDE-Library=kwin_thumbnailaside_config +X-KDE-ParentComponents=thumbnailaside + +Name=Thumbnail Aside +Name[af]=Duimnael langsaan +Name[ar]=مصغرات على الجانب +Name[az]=Yandakı miniatür pəncərəsi +Name[be@latin]=Padhlady akon +Name[bg]=Странични миниатюри +Name[bs]=Sličica postrance +Name[ca]=Miniatures al costat +Name[ca@valencia]=Miniatures de costat +Name[cs]=Postranní miniatura +Name[csb]=Miniaturka Aside +Name[da]=Væk med miniature +Name[de]=Seitliche Vorschaubilder +Name[el]=Εικόνα επισκόπησης στο πλάι +Name[en_GB]=Thumbnail Aside +Name[eo]=Miniaturojn flanke +Name[es]=Miniaturas laterales +Name[et]=Pisipildid kõrval +Name[eu]=Koadro txikiak alboan +Name[fi]=Esikatselukuva vieressä +Name[fr]=Vignettes sur le côté +Name[fy]=Miniatuer der neist +Name[ga]=Thumbnail Aside +Name[gl]=Miniatura a un lado +Name[gu]=થમ્બનીલ બાજુમાં +Name[he]=תמונות ממוזערות בצד +Name[hi]=लघुछवि बाजू में +Name[hne]=चिनहा बाजू में +Name[hr]=Pokrajnja sličica +Name[hu]=Ablakbetekintő oldalt +Name[ia]=Miniatura a parte +Name[id]=Gambar-mini ke Samping +Name[is]=Smámynd til hliðar +Name[it]=Miniature a fianco +Name[ja]=サムネイルをわきに表示 +Name[kk]=Нобайды шеттеу +Name[km]=រូបភាព​តូចនៅ​ខាង +Name[kn]=ಸೂಚ್ಯಚಿತ್ರ ಬದಿಯಲ್ಲಿ +Name[ko]=옆쪽에 축소판 +Name[lt]=MiniatiÅ«ra Å¡one +Name[lv]=SÄ«ktēli malā +Name[mai]=लघुछवि बाजू मे +Name[mk]=Сликички на работ +Name[ml]=അടുത്തുള്ള നഖചിത്രം +Name[mr]=लघुप्रतिमा बाजूला करा +Name[nb]=Minibilde til side +Name[nds]=Vöransicht kantsiets +Name[ne]=थम्बनेल अलग गर्नुहोस् +Name[nl]=Miniatuur ernaast +Name[nn]=Miniatyrbilete ved skjermkanten +Name[pa]=ਥੰਮਨੇਲ ਏ-ਸਾਇਡ +Name[pl]=Miniatura z boku +Name[pt]=Miniaturas Lado-a-Lado +Name[pt_BR]=Miniatura de lado +Name[ro]=Miniatură lateral +Name[ru]=Показать миниатюру окна с краю экрана +Name[se]=Minigovva bálddas +Name[si]=පසෙකින් කුඩා රුවක් +Name[sk]=Bočný náhľad +Name[sl]=Sličica ob strani +Name[sr]=Сличица постранце +Name[sr@ijekavian]=Сличица постранце +Name[sr@ijekavianlatin]=Sličica postrance +Name[sr@latin]=Sličica postrance +Name[sv]=Miniatyrbild vid sidan om +Name[ta]=சுட்டி பக்கத்தில் +Name[te]=థంబ్‌నెయిల్ ఎసైడ్ +Name[th]=ภาพตัวอย่างแบบย่อตามด้าน +Name[tr]=Yan Küçük Resimcik +Name[ug]=كىچىك سۈرەت يانى +Name[uk]=Мініатюри збоку +Name[vi]=Hình nhỏ ra bên +Name[wa]=Prévoeyaedje a costé +Name[x-test]=xxThumbnail Asidexx +Name[zh_CN]=缩略图置边 +Name[zh_TW]=縮圖在旁邊 diff --git a/effects/thumbnailaside/thumbnailaside_config.h b/effects/thumbnailaside/thumbnailaside_config.h new file mode 100644 index 0000000..5f8ab08 --- /dev/null +++ b/effects/thumbnailaside/thumbnailaside_config.h @@ -0,0 +1,45 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Christian Nitschkowski + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_THUMBNAILASIDE_CONFIG_H +#define KWIN_THUMBNAILASIDE_CONFIG_H + +#include + +#include "ui_thumbnailaside_config.h" + +class KActionCollection; + +namespace KWin +{ + +class ThumbnailAsideEffectConfigForm : public QWidget, public Ui::ThumbnailAsideEffectConfigForm +{ + Q_OBJECT +public: + explicit ThumbnailAsideEffectConfigForm(QWidget* parent); +}; + +class ThumbnailAsideEffectConfig : public KCModule +{ + Q_OBJECT +public: + explicit ThumbnailAsideEffectConfig(QWidget* parent = nullptr, const QVariantList& args = QVariantList()); + ~ThumbnailAsideEffectConfig() override; + + void save() override; + +private: + ThumbnailAsideEffectConfigForm* m_ui; + KActionCollection* m_actionCollection; +}; + +} // namespace + +#endif diff --git a/effects/thumbnailaside/thumbnailaside_config.ui b/effects/thumbnailaside/thumbnailaside_config.ui new file mode 100644 index 0000000..439805c --- /dev/null +++ b/effects/thumbnailaside/thumbnailaside_config.ui @@ -0,0 +1,138 @@ + + + KWin::ThumbnailAsideEffectConfigForm + + + + 0 + 0 + 400 + 300 + + + + + + + Appearance + + + + + + Maximum &width: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_MaxWidth + + + + + + + &Spacing: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_Spacing + + + + + + + + 0 + 0 + + + + pixels + + + 30 + + + 10 + + + + + + + &Opacity: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_Opacity + + + + + + + + 0 + 0 + + + + % + + + 100 + + + 50 + + + + + + + + 0 + 0 + + + + pixels + + + 9999 + + + 200 + + + + + + + + + + KShortcutsEditor::GlobalAction + + + + + + + + KShortcutsEditor + QWidget +
KShortcutsEditor
+ 1 +
+
+ + +
diff --git a/effects/thumbnailaside/thumbnailasideconfig.kcfgc b/effects/thumbnailaside/thumbnailasideconfig.kcfgc new file mode 100644 index 0000000..21e6ca7 --- /dev/null +++ b/effects/thumbnailaside/thumbnailasideconfig.kcfgc @@ -0,0 +1,5 @@ +File=thumbnailaside.kcfg +ClassName=ThumbnailAsideConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/effects/touchpoints/touchpoints.cpp b/effects/touchpoints/touchpoints.cpp new file mode 100644 index 0000000..9af04c9 --- /dev/null +++ b/effects/touchpoints/touchpoints.cpp @@ -0,0 +1,314 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2012 Filip Wieladek + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "touchpoints.h" + +#include +#include + +#ifdef KWIN_HAVE_XRENDER_COMPOSITING +#include +#include +#include +#endif + +#include +#include + +#include + +#include + +namespace KWin +{ + +TouchPointsEffect::TouchPointsEffect() + : Effect() +{ +} + +TouchPointsEffect::~TouchPointsEffect() = default; + +static const Qt::GlobalColor s_colors[] = { + Qt::blue, + Qt::red, + Qt::green, + Qt::cyan, + Qt::magenta, + Qt::yellow, + Qt::gray, + Qt::darkBlue, + Qt::darkRed, + Qt::darkGreen +}; + +Qt::GlobalColor TouchPointsEffect::colorForId(quint32 id) +{ + auto it = m_colors.constFind(id); + if (it != m_colors.constEnd()) { + return it.value(); + } + static int s_colorIndex = -1; + s_colorIndex = (s_colorIndex + 1) % 10; + m_colors.insert(id, s_colors[s_colorIndex]); + return s_colors[s_colorIndex]; +} + +bool TouchPointsEffect::touchDown(qint32 id, const QPointF &pos, quint32 time) +{ + Q_UNUSED(time) + TouchPoint point; + point.pos = pos; + point.press = true; + point.color = colorForId(id); + m_points << point; + m_latestPositions.insert(id, pos); + repaint(); + return false; +} + +bool TouchPointsEffect::touchMotion(qint32 id, const QPointF &pos, quint32 time) +{ + Q_UNUSED(time) + TouchPoint point; + point.pos = pos; + point.press = true; + point.color = colorForId(id); + m_points << point; + m_latestPositions.insert(id, pos); + repaint(); + return false; +} + +bool TouchPointsEffect::touchUp(qint32 id, quint32 time) +{ + Q_UNUSED(time) + auto it = m_latestPositions.constFind(id); + if (it != m_latestPositions.constEnd()) { + TouchPoint point; + point.pos = it.value(); + point.press = false; + point.color = colorForId(id); + m_points << point; + } + return false; +} + +void TouchPointsEffect::prePaintScreen(ScreenPrePaintData& data, int time) +{ + auto it = m_points.begin(); + while (it != m_points.end()) { + it->time += time; + if (it->time > m_ringLife) { + it = m_points.erase(it); + } else { + it++; + } + } + + effects->prePaintScreen(data, time); +} + +void TouchPointsEffect::paintScreen(int mask, const QRegion ®ion, ScreenPaintData& data) +{ + effects->paintScreen(mask, region, data); + + paintScreenSetup(mask, region, data); + for (auto it = m_points.constBegin(), end = m_points.constEnd(); it != end; ++it) { + for (int i = 0; i < m_ringCount; ++i) { + float alpha = computeAlpha(it->time, i); + float size = computeRadius(it->time, it->press, i); + if (size > 0 && alpha > 0) { + QColor color = it->color; + color.setAlphaF(alpha); + drawCircle(color, it->pos.x(), it->pos.y(), size); + } + } + } + paintScreenFinish(mask, region, data); +} + +void TouchPointsEffect::postPaintScreen() +{ + effects->postPaintScreen(); + repaint(); +} + +float TouchPointsEffect::computeRadius(int time, bool press, int ring) +{ + float ringDistance = m_ringLife / (m_ringCount * 3); + if (press) { + return ((time - ringDistance * ring) / m_ringLife) * m_ringMaxSize; + } + return ((m_ringLife - time - ringDistance * ring) / m_ringLife) * m_ringMaxSize; +} + +float TouchPointsEffect::computeAlpha(int time, int ring) +{ + float ringDistance = m_ringLife / (m_ringCount * 3); + return (m_ringLife - (float)time - ringDistance * (ring)) / m_ringLife; +} + +void TouchPointsEffect::repaint() +{ + if (!m_points.isEmpty()) { + QRegion dirtyRegion; + const int radius = m_ringMaxSize + m_lineWidth; + for (auto it = m_points.constBegin(), end = m_points.constEnd(); it != end; ++it) { + dirtyRegion |= QRect(it->pos.x() - radius, it->pos.y() - radius, 2*radius, 2*radius); + } + effects->addRepaint(dirtyRegion); + } +} + +bool TouchPointsEffect::isActive() const +{ + return !m_points.isEmpty(); +} + +void TouchPointsEffect::drawCircle(const QColor& color, float cx, float cy, float r) +{ + if (effects->isOpenGLCompositing()) + drawCircleGl(color, cx, cy, r); + if (effects->compositingType() == XRenderCompositing) + drawCircleXr(color, cx, cy, r); + if (effects->compositingType() == QPainterCompositing) + drawCircleQPainter(color, cx, cy, r); +} + +void TouchPointsEffect::paintScreenSetup(int mask, QRegion region, ScreenPaintData& data) +{ + if (effects->isOpenGLCompositing()) + paintScreenSetupGl(mask, region, data); +} + +void TouchPointsEffect::paintScreenFinish(int mask, QRegion region, ScreenPaintData& data) +{ + if (effects->isOpenGLCompositing()) + paintScreenFinishGl(mask, region, data); +} + +void TouchPointsEffect::drawCircleGl(const QColor& color, float cx, float cy, float r) +{ + static const int num_segments = 80; + static const float theta = 2 * 3.1415926 / float(num_segments); + static const float c = cosf(theta); //precalculate the sine and cosine + static const float s = sinf(theta); + float t; + + float x = r;//we start at angle = 0 + float y = 0; + + GLVertexBuffer* vbo = GLVertexBuffer::streamingBuffer(); + vbo->reset(); + vbo->setUseColor(true); + vbo->setColor(color); + QVector verts; + verts.reserve(num_segments * 2); + + for (int ii = 0; ii < num_segments; ++ii) { + verts << x + cx << y + cy;//output vertex + //apply the rotation matrix + t = x; + x = c * x - s * y; + y = s * t + c * y; + } + vbo->setData(verts.size() / 2, 2, verts.data(), nullptr); + vbo->render(GL_LINE_LOOP); +} + +void TouchPointsEffect::drawCircleXr(const QColor& color, float cx, float cy, float r) +{ +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + if (r <= m_lineWidth) + return; + + int num_segments = r+8; + float theta = 2.0 * 3.1415926 / num_segments; + float cos = cosf(theta); //precalculate the sine and cosine + float sin = sinf(theta); + float x[2] = {r, r-m_lineWidth}; + float y[2] = {0, 0}; + +#define DOUBLE_TO_FIXED(d) ((xcb_render_fixed_t) ((d) * 65536)) + QVector strip; + strip.reserve(2*num_segments+2); + + xcb_render_pointfix_t point; + point.x = DOUBLE_TO_FIXED(x[1]+cx); + point.y = DOUBLE_TO_FIXED(y[1]+cy); + strip << point; + + for (int i = 0; i < num_segments; ++i) { + //apply the rotation matrix + const float h[2] = {x[0], x[1]}; + x[0] = cos * x[0] - sin * y[0]; + x[1] = cos * x[1] - sin * y[1]; + y[0] = sin * h[0] + cos * y[0]; + y[1] = sin * h[1] + cos * y[1]; + + point.x = DOUBLE_TO_FIXED(x[0]+cx); + point.y = DOUBLE_TO_FIXED(y[0]+cy); + strip << point; + + point.x = DOUBLE_TO_FIXED(x[1]+cx); + point.y = DOUBLE_TO_FIXED(y[1]+cy); + strip << point; + } + + const float h = x[0]; + x[0] = cos * x[0] - sin * y[0]; + y[0] = sin * h + cos * y[0]; + + point.x = DOUBLE_TO_FIXED(x[0]+cx); + point.y = DOUBLE_TO_FIXED(y[0]+cy); + strip << point; + + XRenderPicture fill = xRenderFill(color); + xcb_render_tri_strip(xcbConnection(), XCB_RENDER_PICT_OP_OVER, + fill, effects->xrenderBufferPicture(), 0, + 0, 0, strip.count(), strip.constData()); +#undef DOUBLE_TO_FIXED +#else + Q_UNUSED(color) + Q_UNUSED(cx) + Q_UNUSED(cy) + Q_UNUSED(r) +#endif +} + +void TouchPointsEffect::drawCircleQPainter(const QColor &color, float cx, float cy, float r) +{ + QPainter *painter = effects->scenePainter(); + painter->save(); + painter->setPen(color); + painter->drawArc(cx - r, cy - r, r * 2, r * 2, 0, 5760); + painter->restore(); +} + +void TouchPointsEffect::paintScreenSetupGl(int, QRegion, ScreenPaintData &data) +{ + GLShader *shader = ShaderManager::instance()->pushShader(ShaderTrait::UniformColor); + shader->setUniform(GLShader::ModelViewProjectionMatrix, data.projectionMatrix()); + + glLineWidth(m_lineWidth); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); +} + +void TouchPointsEffect::paintScreenFinishGl(int, QRegion, ScreenPaintData&) +{ + glDisable(GL_BLEND); + + ShaderManager::instance()->popShader(); +} + +} // namespace + diff --git a/effects/touchpoints/touchpoints.h b/effects/touchpoints/touchpoints.h new file mode 100644 index 0000000..2ff4626 --- /dev/null +++ b/effects/touchpoints/touchpoints.h @@ -0,0 +1,88 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2012 Filip Wieladek + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_TOUCHPOINTS_H +#define KWIN_TOUCHPOINTS_H + +#include + +namespace KWin +{ + +class TouchPointsEffect + : public Effect +{ + Q_OBJECT + Q_PROPERTY(qreal lineWidth READ lineWidth) + Q_PROPERTY(int ringLife READ ringLife) + Q_PROPERTY(int ringSize READ ringSize) + Q_PROPERTY(int ringCount READ ringCount) +public: + TouchPointsEffect(); + ~TouchPointsEffect() override; + void prePaintScreen(ScreenPrePaintData& data, int time) override; + void paintScreen(int mask, const QRegion ®ion, ScreenPaintData& data) override; + void postPaintScreen() override; + bool isActive() const override; + bool touchDown(qint32 id, const QPointF &pos, quint32 time) override; + bool touchMotion(qint32 id, const QPointF &pos, quint32 time) override; + bool touchUp(qint32 id, quint32 time) override; + + // for properties + qreal lineWidth() const { + return m_lineWidth; + } + int ringLife() const { + return m_ringLife; + } + int ringSize() const { + return m_ringMaxSize; + } + int ringCount() const { + return m_ringCount; + } + +private: + inline void drawCircle(const QColor& color, float cx, float cy, float r); + inline void paintScreenSetup(int mask, QRegion region, ScreenPaintData& data); + inline void paintScreenFinish(int mask, QRegion region, ScreenPaintData& data); + + void repaint(); + + float computeAlpha(int time, int ring); + float computeRadius(int time, bool press, int ring); + void drawCircleGl(const QColor& color, float cx, float cy, float r); + void drawCircleXr(const QColor& color, float cx, float cy, float r); + void drawCircleQPainter(const QColor& color, float cx, float cy, float r); + void paintScreenSetupGl(int mask, QRegion region, ScreenPaintData& data); + void paintScreenFinishGl(int mask, QRegion region, ScreenPaintData& data); + + Qt::GlobalColor colorForId(quint32 id); + + int m_ringCount = 2; + float m_lineWidth = 1.0; + int m_ringLife = 300; + float m_ringMaxSize = 20.0; + + struct TouchPoint { + QPointF pos; + int time = 0; + bool press; + QColor color; + }; + QVector m_points; + QHash m_latestPositions; + QHash m_colors; + +}; + +} // namespace + +#endif diff --git a/effects/trackmouse/CMakeLists.txt b/effects/trackmouse/CMakeLists.txt new file mode 100644 index 0000000..c44223a --- /dev/null +++ b/effects/trackmouse/CMakeLists.txt @@ -0,0 +1,29 @@ +####################################### +# Effect +# Data files +install(FILES data/tm_inner.png data/tm_outer.png DESTINATION ${DATA_INSTALL_DIR}/kwin) + +####################################### +# Config +set(kwin_trackmouse_config_SRCS trackmouse_config.cpp) +ki18n_wrap_ui(kwin_trackmouse_config_SRCS trackmouse_config.ui) +kconfig_add_kcfg_files(kwin_trackmouse_config_SRCS trackmouseconfig.kcfgc) + +add_library(kwin_trackmouse_config MODULE ${kwin_trackmouse_config_SRCS}) + +target_link_libraries(kwin_trackmouse_config + KF5::ConfigWidgets + KF5::GlobalAccel + KF5::I18n + KF5::XmlGui + KWinEffectsInterface +) + +kcoreaddons_desktop_to_json(kwin_trackmouse_config trackmouse_config.desktop SERVICE_TYPES kcmodule.desktop) + +install( + TARGETS + kwin_trackmouse_config + DESTINATION + ${PLUGIN_INSTALL_DIR}/kwin/effects/configs +) diff --git a/effects/trackmouse/data/tm_inner.png b/effects/trackmouse/data/tm_inner.png new file mode 100644 index 0000000000000000000000000000000000000000..a6fb166e4905d1275565e85bbf980bdc8c0366aa GIT binary patch literal 1247 zcmV<51R(o~P)>e&1Ul)Nh`~~@H{B#pFDOXp8$9u={ErD zB-inL{sVvoNqYh8CHXdhBa-Hd_z}nPho0vFz|~YgiuOU0^gQoU9LMi(Q#O=z*S78Z zVHmFF9o|XO_x(e|;qW-glah-3@!$6Q{U0ViyUZv`((*j-qNMjEy-YF&@RMa(H`mtI z`h_1jq96#|;c)n=q+^np!E>0V(Ktx*vZQ@Ee0XY% zIkVjqQh1OVm!xM^!vR3Q-~YpLoYN$K&*8%!N$0jJt86VgE0MH4&%3Z)pC6ZHSr^7v zJX5#etR-pK>-E0Q(~@x$)dK@6`ej0NU;L6G@jz zruHe4r$(=90|-1Hcu4-3W+Q3I_x%G~*R=u!0Nb{2k{qVlBpYu4H3aN*I+3Ki8D{kb z1OVH%@28bZ+8+dg3)B=a48zrAF+Wj*!Qe1ZQ@~^q{~*nVWPMu@0FrIkM3I~aY77Wq zJ*_Y~i>)tUvVl%h?OSjoqx@B%-hf#mP;bD6jPk#M8UrSWR%zMDK6VEdWW{gTdgd ztT+Y9b`(YTM(^!-z^$WuNpEJ^8e?vZhwOHAY@*d_%_S|D2X3X??S3(SeJmS$JD50cEy=7U~vjUM-%NVm_j5$*zPYsx2qHi{v z??^gZHu*KLUPwCS$jj-v?g^5oc3nd3xOQbq^Ca!cr1YJt{{V@EZb^9iav}f#002ov JPDHLkV1n3pQGoye literal 0 HcmV?d00001 diff --git a/effects/trackmouse/data/tm_outer.png b/effects/trackmouse/data/tm_outer.png new file mode 100644 index 0000000000000000000000000000000000000000..dd5454290f3238e4b9cf25dfb048551f6806affd GIT binary patch literal 1311 zcmV+)1>pLLP)8 zq67F3IT=^tMl|p-)?-(ZN*fh<4s75}Ea>=s953VDek-m@7hYkid8GXB!$bP)ja4!5 zhM;^Up2w-jTX9tk{3(ZK30~E`rS_yM27a1THxJL@w7eBp#lSDIJEwLYo|N;RSQP{R zk_L-!a_X1h{yf5}7zn@OL41%?zY?n=QB|G;J6Vmp#SLg2kKl`5f~pz_qj*Ye;z8Vn zv2KM`>cX9Dlpb3mW@1gR!YUgGZ{xijx=V3Ew}Pr02rpu5plRsQ3VRti6F1{poZWY5 z<=`JYjvoWvMOfbXW4A$M6>fFxyznzT)_;tpU@2Y*G&^x8CQa*Xfmh&OQzzAz;QE3A z{@?jZnz3YYjvqdJ($*4XyxH`ZLO;Zw@F^%SOd?O=7GKV6NmeatR!s5 zM}cOZHE=W#e{T2(kaiABIcG6SGH^B)1>%hZ2&!yEIWNNO zCKhUYuIa+x*fgl9O2j59_g*xxu!*@rMVoPGP*IhLLsITp%xz*$Aj<1mam(Trj9Iu~^DEiwzt#aJTr--Q@*?Dicwz zyG*QBw=X6{t?+8uduN7)v!q?8NeFjw~?hqfc>1yCMJ$9nEabcf9m4$P0 zS)duywoj*JdHC0-PgKoamhOB)tLAj zFA`k|Sc_`{O-tG%{#|E5)^ObtZ@4ZytHtJ#HkVE@k7}P(8kKHU5eKE+`(3HhcVpVQ zNir{bWEq!EulIaDu9|^O>7I90PW>w~&I%u?uu5Dfzj^EMP9&<5firM>PVHvAlt)-4 z1D8n;%n@UFLVVZ;)iQ8hrCC=ls$}4p^yQ9TY?emm + SPDX-FileCopyrightText: 2010 Jorge Mata + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "trackmouse.h" + +// KConfigSkeleton +#include "trackmouseconfig.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include + +namespace KWin +{ + +TrackMouseEffect::TrackMouseEffect() + : m_angle(0) +{ + initConfig(); + m_texture[0] = m_texture[1] = nullptr; +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + m_picture[0] = m_picture[1] = nullptr; + if ( effects->compositingType() == XRenderCompositing) + m_angleBase = 1.57079632679489661923; // Pi/2 +#endif + if ( effects->isOpenGLCompositing() || effects->compositingType() == QPainterCompositing) + m_angleBase = 90.0; + m_mousePolling = false; + + m_action = new QAction(this); + m_action->setObjectName(QStringLiteral("TrackMouse")); + m_action->setText(i18n("Track mouse")); + KGlobalAccel::self()->setDefaultShortcut(m_action, QList()); + KGlobalAccel::self()->setShortcut(m_action, QList()); + effects->registerGlobalShortcut(QKeySequence(), m_action); + + connect(m_action, &QAction::triggered, this, &TrackMouseEffect::toggle); + + connect(effects, &EffectsHandler::mouseChanged, this, &TrackMouseEffect::slotMouseChanged); + reconfigure(ReconfigureAll); +} + +TrackMouseEffect::~TrackMouseEffect() +{ + if (m_mousePolling) + effects->stopMousePolling(); + for (int i = 0; i < 2; ++i) { + delete m_texture[i]; m_texture[i] = nullptr; +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + delete m_picture[i]; m_picture[i] = nullptr; +#endif + } +} + +void TrackMouseEffect::reconfigure(ReconfigureFlags) +{ + m_modifiers = Qt::KeyboardModifiers(); + TrackMouseConfig::self()->read(); + if (TrackMouseConfig::shift()) + m_modifiers |= Qt::ShiftModifier; + if (TrackMouseConfig::alt()) + m_modifiers |= Qt::AltModifier; + if (TrackMouseConfig::control()) + m_modifiers |= Qt::ControlModifier; + if (TrackMouseConfig::meta()) + m_modifiers |= Qt::MetaModifier; + + if (m_modifiers) { + if (!m_mousePolling) + effects->startMousePolling(); + m_mousePolling = true; + } else if (m_mousePolling) { + effects->stopMousePolling(); + m_mousePolling = false; + } +} + +void TrackMouseEffect::prePaintScreen(ScreenPrePaintData& data, int time) +{ + QTime t = QTime::currentTime(); + m_angle = ((t.second() % 4) * m_angleBase) + (t.msec() / 1000.0 * m_angleBase); + m_lastRect[0].moveCenter(cursorPos()); + m_lastRect[1].moveCenter(cursorPos()); + data.paint |= m_lastRect[0].adjusted(-1,-1,1,1); + + effects->prePaintScreen(data, time); +} + +void TrackMouseEffect::paintScreen(int mask, const QRegion ®ion, ScreenPaintData& data) +{ + effects->paintScreen(mask, region, data); // paint normal screen + + if ( effects->isOpenGLCompositing() && m_texture[0] && m_texture[1]) { + ShaderBinder binder(ShaderTrait::MapTexture); + GLShader *shader(binder.shader()); + if (!shader) { + return; + } + glEnable(GL_BLEND); + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + QMatrix4x4 matrix(data.projectionMatrix()); + const QPointF p = m_lastRect[0].topLeft() + QPoint(m_lastRect[0].width()/2.0, m_lastRect[0].height()/2.0); + const float x = p.x()*data.xScale() + data.xTranslation(); + const float y = p.y()*data.yScale() + data.yTranslation(); + for (int i = 0; i < 2; ++i) { + matrix.translate(x, y, 0.0); + matrix.rotate(i ? -2*m_angle : m_angle, 0, 0, 1.0); + matrix.translate(-x, -y, 0.0); + QMatrix4x4 mvp(matrix); + mvp.translate(m_lastRect[i].x(), m_lastRect[i].y()); + shader->setUniform(GLShader::ModelViewProjectionMatrix, mvp); + m_texture[i]->bind(); + m_texture[i]->render(region, m_lastRect[i]); + m_texture[i]->unbind(); + } + glDisable(GL_BLEND); + } +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + if ( effects->compositingType() == XRenderCompositing && m_picture[0] && m_picture[1]) { + float sine = sin(m_angle); + const float cosine = cos(m_angle); + for (int i = 0; i < 2; ++i) { + if (i) sine = -sine; + const float dx = m_size[i].width()/2.0; + const float dy = m_size[i].height()/2.0; + const xcb_render_picture_t picture = *m_picture[i]; +#define DOUBLE_TO_FIXED(d) ((xcb_render_fixed_t) ((d) * 65536)) + xcb_render_transform_t xform = { + DOUBLE_TO_FIXED( cosine ), DOUBLE_TO_FIXED( -sine ), DOUBLE_TO_FIXED( dx - cosine*dx + sine*dy ), + DOUBLE_TO_FIXED( sine ), DOUBLE_TO_FIXED( cosine ), DOUBLE_TO_FIXED( dy - sine*dx - cosine*dy ), + DOUBLE_TO_FIXED( 0.0 ), DOUBLE_TO_FIXED( 0.0 ), DOUBLE_TO_FIXED( 1.0 ) + }; +#undef DOUBLE_TO_FIXED + xcb_render_set_picture_transform(xcbConnection(), picture, xform); + xcb_render_set_picture_filter(xcbConnection(), picture, 8, "bilinear", 0, nullptr); + const QRect &rect = m_lastRect[i]; + xcb_render_composite(xcbConnection(), XCB_RENDER_PICT_OP_OVER, picture, XCB_RENDER_PICTURE_NONE, + effects->xrenderBufferPicture(), 0, 0, 0, 0, + qRound((rect.x()+rect.width()/2.0)*data.xScale() - rect.width()/2.0 + data.xTranslation()), + qRound((rect.y()+rect.height()/2.0)*data.yScale() - rect.height()/2.0 + data.yTranslation()), + rect.width(), rect.height()); + } + } +#endif + if (effects->compositingType() == QPainterCompositing && !m_image[0].isNull() && !m_image[1].isNull()) { + QPainter *painter = effects->scenePainter(); + const QPointF p = m_lastRect[0].topLeft() + QPoint(m_lastRect[0].width()/2.0, m_lastRect[0].height()/2.0); + for (int i = 0; i < 2; ++i) { + painter->save(); + painter->translate(p.x(), p.y()); + painter->rotate(i ? -2*m_angle : m_angle); + painter->translate(-p.x(), -p.y()); + painter->drawImage(m_lastRect[i], m_image[i]); + painter->restore(); + } + } +} + +void TrackMouseEffect::postPaintScreen() +{ + effects->addRepaint(m_lastRect[0].adjusted(-1,-1,1,1)); + effects->postPaintScreen(); +} + +bool TrackMouseEffect::init() +{ + effects->makeOpenGLContextCurrent(); +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + if (!(m_texture[0] || m_picture[0] || !m_image[0].isNull())) { + loadTexture(); + if (!(m_texture[0] || m_picture[0] || !m_image[0].isNull())) + return false; + } +#else + if (!m_texture[0] || m_image[0].isNull()) { + loadTexture(); + if (!m_texture[0] || m_image[0].isNull()) + return false; + } +#endif + m_lastRect[0].moveCenter(cursorPos()); + m_lastRect[1].moveCenter(cursorPos()); + m_angle = 0; + return true; +} + +void TrackMouseEffect::toggle() +{ + switch (m_state) { + case State::ActivatedByModifiers: + m_state = State::ActivatedByShortcut; + break; + + case State::ActivatedByShortcut: + m_state = State::Inactive; + break; + + case State::Inactive: + if (!init()) { + return; + } + m_state = State::ActivatedByShortcut; + break; + + default: + Q_UNREACHABLE(); + break; + } + + effects->addRepaint(m_lastRect[0].adjusted(-1, -1, 1, 1)); +} + +void TrackMouseEffect::slotMouseChanged(const QPoint&, const QPoint&, + Qt::MouseButtons, Qt::MouseButtons, + Qt::KeyboardModifiers modifiers, Qt::KeyboardModifiers) +{ + if (!m_mousePolling) { // we didn't ask for it but maybe someone else did... + return; + } + + switch (m_state) { + case State::ActivatedByModifiers: + if (modifiers == m_modifiers) { + return; + } + m_state = State::Inactive; + break; + + case State::ActivatedByShortcut: + return; + + case State::Inactive: + if (modifiers != m_modifiers) { + return; + } + if (!init()) { + return; + } + m_state = State::ActivatedByModifiers; + break; + + default: + Q_UNREACHABLE(); + break; + } + + effects->addRepaint(m_lastRect[0].adjusted(-1, -1, 1, 1)); +} + +void TrackMouseEffect::loadTexture() +{ + QString f[2] = {QStandardPaths::locate(QStandardPaths::DataLocation, QStringLiteral("tm_outer.png")), + QStandardPaths::locate(QStandardPaths::DataLocation, QStringLiteral("tm_inner.png"))}; + if (f[0].isEmpty() || f[1].isEmpty()) + return; + + for (int i = 0; i < 2; ++i) { + if ( effects->isOpenGLCompositing()) { + QImage img(f[i]); + m_texture[i] = new GLTexture(img); + m_lastRect[i].setSize(img.size()); + } +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + if ( effects->compositingType() == XRenderCompositing) { + QImage pixmap(f[i]); + m_picture[i] = new XRenderPicture(pixmap); + m_size[i] = pixmap.size(); + m_lastRect[i].setSize(pixmap.size()); + } +#endif + if (effects->compositingType() == QPainterCompositing) { + m_image[i] = QImage(f[i]); + m_lastRect[i].setSize(m_image[i].size()); + } + } +} + +bool TrackMouseEffect::isActive() const +{ + return m_state != State::Inactive; +} + +} // namespace diff --git a/effects/trackmouse/trackmouse.h b/effects/trackmouse/trackmouse.h new file mode 100644 index 0000000..d35b087 --- /dev/null +++ b/effects/trackmouse/trackmouse.h @@ -0,0 +1,76 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2010 Jorge Mata + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_TRACKMOUSE_H +#define KWIN_TRACKMOUSE_H + +#include + +class QAction; + +namespace KWin +{ +class GLTexture; + +class TrackMouseEffect + : public Effect +{ + Q_OBJECT + Q_PROPERTY(Qt::KeyboardModifiers modifiers READ modifiers) + Q_PROPERTY(bool mousePolling READ isMousePolling) +public: + TrackMouseEffect(); + ~TrackMouseEffect() override; + void prePaintScreen(ScreenPrePaintData& data, int time) override; + void paintScreen(int mask, const QRegion ®ion, ScreenPaintData& data) override; + void postPaintScreen() override; + void reconfigure(ReconfigureFlags) override; + bool isActive() const override; + + // for properties + Qt::KeyboardModifiers modifiers() const { + return m_modifiers; + } + bool isMousePolling() const { + return m_mousePolling; + } +private Q_SLOTS: + void toggle(); + void slotMouseChanged(const QPoint& pos, const QPoint& old, + Qt::MouseButtons buttons, Qt::MouseButtons oldbuttons, + Qt::KeyboardModifiers modifiers, Qt::KeyboardModifiers oldmodifiers); +private: + bool init(); + void loadTexture(); + QRect m_lastRect[2]; + bool m_mousePolling; + float m_angle; + float m_angleBase; + GLTexture* m_texture[2]; +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + QSize m_size[2]; + XRenderPicture *m_picture[2]; +#endif + QAction* m_action; + QImage m_image[2]; + Qt::KeyboardModifiers m_modifiers; + + enum class State { + ActivatedByModifiers, + ActivatedByShortcut, + Inactive + }; + State m_state = State::Inactive; +}; + +} // namespace + +#endif diff --git a/effects/trackmouse/trackmouse.kcfg b/effects/trackmouse/trackmouse.kcfg new file mode 100644 index 0000000..6be7493 --- /dev/null +++ b/effects/trackmouse/trackmouse.kcfg @@ -0,0 +1,21 @@ + + + + + + true + + + true + + + false + + + false + + + diff --git a/effects/trackmouse/trackmouse_config.cpp b/effects/trackmouse/trackmouse_config.cpp new file mode 100644 index 0000000..7411530 --- /dev/null +++ b/effects/trackmouse/trackmouse_config.cpp @@ -0,0 +1,113 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Rivo Laks + SPDX-FileCopyrightText: 2010 Jorge Mata + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include "trackmouse_config.h" + +// KConfigSkeleton +#include "trackmouseconfig.h" + +K_PLUGIN_FACTORY_WITH_JSON(TrackMouseEffectConfigFactory, + "trackmouse_config.json", + registerPlugin();) + +namespace KWin +{ + +static const QString s_toggleTrackMouseActionName = QStringLiteral("TrackMouse"); + +TrackMouseEffectConfigForm::TrackMouseEffectConfigForm(QWidget* parent) : QWidget(parent) +{ + setupUi(this); +} + +TrackMouseEffectConfig::TrackMouseEffectConfig(QWidget* parent, const QVariantList& args) : + KCModule(KAboutData::pluginData(QStringLiteral("trackmouse")), parent, args) +{ + TrackMouseConfig::instance(KWIN_CONFIG); + m_ui = new TrackMouseEffectConfigForm(this); + QVBoxLayout* layout = new QVBoxLayout(this); + layout->addWidget(m_ui); + + addConfig(TrackMouseConfig::self(), m_ui); + + m_actionCollection = new KActionCollection(this, QStringLiteral("kwin")); + m_actionCollection->setComponentDisplayName(i18n("KWin")); + m_actionCollection->setConfigGroup(QStringLiteral("TrackMouse")); + m_actionCollection->setConfigGlobal(true); + + QAction *a = m_actionCollection->addAction(s_toggleTrackMouseActionName); + a->setText(i18n("Track mouse")); + a->setProperty("isConfigurationAction", true); + + KGlobalAccel::self()->setDefaultShortcut(a, QList()); + KGlobalAccel::self()->setShortcut(a, QList()); + + connect(m_ui->shortcut, &KKeySequenceWidget::keySequenceChanged, + this, &TrackMouseEffectConfig::shortcutChanged); + + load(); +} + +TrackMouseEffectConfig::~TrackMouseEffectConfig() +{ +} + +void TrackMouseEffectConfig::load() +{ + KCModule::load(); + + if (QAction *a = m_actionCollection->action(s_toggleTrackMouseActionName)) { + auto shortcuts = KGlobalAccel::self()->shortcut(a); + if (!shortcuts.isEmpty()) { + m_ui->shortcut->setKeySequence(shortcuts.first()); + } + } +} + +void TrackMouseEffectConfig::save() +{ + KCModule::save(); + m_actionCollection->writeSettings(); + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("trackmouse")); +} + +void TrackMouseEffectConfig::defaults() +{ + KCModule::defaults(); + m_ui->shortcut->clearKeySequence(); +} + +void TrackMouseEffectConfig::shortcutChanged(const QKeySequence &seq) +{ + if (QAction *a = m_actionCollection->action(QStringLiteral("TrackMouse"))) { + KGlobalAccel::self()->setShortcut(a, QList() << seq, KGlobalAccel::NoAutoloading); + } + emit changed(true); +} + +} // namespace + +#include "trackmouse_config.moc" diff --git a/effects/trackmouse/trackmouse_config.desktop b/effects/trackmouse/trackmouse_config.desktop new file mode 100644 index 0000000..42e4747 --- /dev/null +++ b/effects/trackmouse/trackmouse_config.desktop @@ -0,0 +1,86 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=KCModule + +X-KDE-Library=kwin_trackmouse_config +X-KDE-ParentComponents=trackmouse + +Name=Track Mouse +Name[af]=Volg die Muis +Name[ar]=تتبع الفارة +Name[az]=Kursorun izi +Name[be]=Адследжваць мыш +Name[be@latin]=VyjaÅ­leńnie kursora +Name[bg]=Проследяване на мишката +Name[bn]=মাউস ট্র্যাক করো +Name[bs]=Praćenje miÅ¡a +Name[ca]=Seguiment del ratolí +Name[ca@valencia]=Seguiment del ratolí +Name[cs]=Sledování myÅ¡i +Name[csb]=Nalezénié pòłożenia mëszë +Name[da]=Sporing af mus +Name[de]=Maus-Position finden +Name[el]=Ανίχνευση ποντικιού +Name[en_GB]=Track Mouse +Name[eo]=Spuri muson +Name[es]=Seguir el ratón +Name[et]=Hiire jälgimine +Name[eu]=Jarraitu saguari +Name[fa]=ردگیری موشی +Name[fi]=Hiiren jäljitys +Name[fr]=Repérer la souris +Name[fy]=Track Mûs +Name[ga]=Lorg an Luch +Name[gl]=Seguir o rato +Name[gu]=માઉસની ખબર રાખો +Name[he]=מעקב אחרי העכבר +Name[hi]=माउस ट्रैक करें +Name[hne]=मुसुवा ट्रैक करव +Name[hr]=Praćenje miÅ¡a +Name[hu]=Egérkövetés +Name[ia]=Tracia mus +Name[id]=Lacak Mouse +Name[is]=Elta mús +Name[it]=Trova il mouse +Name[ja]=マウス追跡 +Name[kk]=Тышқандың ізі +Name[km]=ដាន​កណ្តុរ​ +Name[kn]=ಮೂಷಕ ಹಿಂಬಾಲಕ +Name[ko]=마우스 추적 +Name[lt]=Pelės sekimas +Name[lv]=Sekot pelei +Name[mai]=माउस ट्रैक करू +Name[mk]=Следење на глушецот +Name[ml]=മൌസിനെ നിരീക്ഷിക്കുക +Name[mr]=माऊसचा मागोवा घ्या +Name[nb]=Spor mus +Name[nds]=Muusspoor +Name[ne]=ट्रयाक माउस +Name[nl]=Muis volgen +Name[nn]=Følg mus +Name[pa]=ਮਾਊਸ ਟਰੈਕ +Name[pl]=Śledzenie myszy +Name[pt]=Seguir o Rato +Name[pt_BR]=Seguir o mouse +Name[ro]=Urmărește mausul +Name[ru]=Поиск курсора мыши на экране +Name[se]=Čuovo sáhpána +Name[si]=මවුසය සොයන්න +Name[sk]=SledovaÅ¥ myÅ¡ +Name[sl]=Sledenje miÅ¡ki +Name[sr]=Праћење миша +Name[sr@ijekavian]=Праћење миша +Name[sr@ijekavianlatin]=Praćenje miÅ¡a +Name[sr@latin]=Praćenje miÅ¡a +Name[sv]=Följ musen +Name[ta]=எலியத்தை கவனி +Name[te]=ట్రాక్ మౌస్ +Name[th]=ติดตามเมาส์ +Name[tr]=Fareyi İzle +Name[ug]=چاشقىنەكنى ئىزلا +Name[uk]=Сліди мишки +Name[vi]=Vết chuột +Name[wa]=Shut l' sori +Name[x-test]=xxTrack Mousexx +Name[zh_CN]=跟踪鼠标 +Name[zh_TW]=追蹤滑鼠 diff --git a/effects/trackmouse/trackmouse_config.h b/effects/trackmouse/trackmouse_config.h new file mode 100644 index 0000000..9701806 --- /dev/null +++ b/effects/trackmouse/trackmouse_config.h @@ -0,0 +1,50 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Rivo Laks + SPDX-FileCopyrightText: 2010 Jorge Mata + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_TRACKMOUSE_CONFIG_H +#define KWIN_TRACKMOUSE_CONFIG_H + +#include + +#include "ui_trackmouse_config.h" + +class KActionCollection; + +namespace KWin +{ + +class TrackMouseEffectConfigForm : public QWidget, public Ui::TrackMouseEffectConfigForm +{ + Q_OBJECT +public: + explicit TrackMouseEffectConfigForm(QWidget* parent); +}; + +class TrackMouseEffectConfig : public KCModule +{ + Q_OBJECT +public: + explicit TrackMouseEffectConfig(QWidget* parent = nullptr, const QVariantList& args = QVariantList()); + ~TrackMouseEffectConfig() override; + +public Q_SLOTS: + void save() override; + void load() override; + void defaults() override; +private Q_SLOTS: + void shortcutChanged(const QKeySequence &seq); +private: + TrackMouseEffectConfigForm* m_ui; + KActionCollection* m_actionCollection; +}; + +} // namespace + +#endif diff --git a/effects/trackmouse/trackmouse_config.ui b/effects/trackmouse/trackmouse_config.ui new file mode 100644 index 0000000..dff595d --- /dev/null +++ b/effects/trackmouse/trackmouse_config.ui @@ -0,0 +1,104 @@ + + + KWin::TrackMouseEffectConfigForm + + + + 0 + 0 + 345 + 112 + + + + + QFormLayout::FieldsStayAtSizeHint + + + + + + 75 + true + + + + Trigger effect with: + + + + + + + Keyboard shortcut: + + + + + + + + + + Modifier keys: + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Alt + + + + + + + Ctrl + + + + + + + Shift + + + + + + + Meta + + + + + + + + + + + KKeySequenceWidget + QWidget +
kkeysequencewidget.h
+
+
+ + +
diff --git a/effects/trackmouse/trackmouseconfig.kcfgc b/effects/trackmouse/trackmouseconfig.kcfgc new file mode 100644 index 0000000..3aa4117 --- /dev/null +++ b/effects/trackmouse/trackmouseconfig.kcfgc @@ -0,0 +1,5 @@ +File=trackmouse.kcfg +ClassName=TrackMouseConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/effects/translucency/package/contents/code/main.js b/effects/translucency/package/contents/code/main.js new file mode 100644 index 0000000..927cde1 --- /dev/null +++ b/effects/translucency/package/contents/code/main.js @@ -0,0 +1,219 @@ +/* + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +/*global effect, effects, animate, cancel, set, animationTime, Effect, QEasingCurve */ + +"use strict"; + +var translucencyEffect = { + activeWindow: effects.activeWindow, + settings: { + duration: animationTime(250), + moveresize: 100, + dialogs: 100, + inactive: 100, + comboboxpopups: 100, + menus: 100, + dropdownmenus: 100, + popupmenus: 100, + tornoffmenus: 100 + }, + loadConfig: function () { + var i, individualMenu, windows; + // TODO: add animation duration + translucencyEffect.settings.moveresize = effect.readConfig("MoveResize", 80); + translucencyEffect.settings.dialogs = effect.readConfig("Dialogs", 100); + translucencyEffect.settings.inactive = effect.readConfig("Inactive", 100); + translucencyEffect.settings.comboboxpopups = effect.readConfig("ComboboxPopups", 100); + translucencyEffect.settings.menus = effect.readConfig("Menus", 100); + individualMenu = effect.readConfig("IndividualMenuConfig", false); + if (individualMenu === true) { + translucencyEffect.settings.dropdownmenus = effect.readConfig("DropdownMenus", 100); + translucencyEffect.settings.popupmenus = effect.readConfig("PopupMenus", 100); + translucencyEffect.settings.tornoffmenus = effect.readConfig("TornOffMenus", 100); + } else { + translucencyEffect.settings.dropdownmenus = translucencyEffect.settings.menus; + translucencyEffect.settings.popupmenus = translucencyEffect.settings.menus; + translucencyEffect.settings.tornoffmenus = translucencyEffect.settings.menus; + } + + windows = effects.stackingOrder; + for (i = 0; i < windows.length; i += 1) { + // stop all existing animations + translucencyEffect.cancelAnimations(windows[i]); + // schedule new animations based on new settings + translucencyEffect.startAnimation(windows[i]); + if (windows[i] !== effects.activeWindow) { + translucencyEffect.inactive.animate(windows[i]); + } + } + }, + /** + * @brief Starts the set animations depending on window type + * + */ + startAnimation: function (window) { + var checkWindow = function (window, value) { + if (value !== 100) { + var ids = set({ + window: window, + duration: 1, + animations: [{ + type: Effect.Opacity, + from: value / 100.0, + to: value / 100.0 + }] + }); + if (window.translucencyWindowTypeAnimation !== undefined) { + cancel(window.translucencyWindowTypeAnimation); + } + window.translucencyWindowTypeAnimation = ids; + } + }; + if (window.desktopWindow === true || window.dock === true || window.visible === false) { + return; + } + if (window.dialog === true) { + checkWindow(window, translucencyEffect.settings.dialogs); + } else if (window.dropdownMenu === true) { + checkWindow(window, translucencyEffect.settings.dropdownmenus); + } else if (window.popupMenu === true) { + checkWindow(window, translucencyEffect.settings.popupmenus); + } else if (window.comboBox === true) { + checkWindow(window, translucencyEffect.settings.comboboxpopups); + } else if (window.menu === true) { + checkWindow(window, translucencyEffect.settings.tornoffmenus); + } + }, + /** + * @brief Cancels all animations for window type and inactive window + * + */ + cancelAnimations: function (window) { + if (window.translucencyWindowTypeAnimation !== undefined) { + cancel(window.translucencyWindowTypeAnimation); + window.translucencyWindowTypeAnimation = undefined; + } + if (window.translucencyInactiveAnimation !== undefined) { + cancel(window.translucencyInactiveAnimation); + window.translucencyInactiveAnimation = undefined; + } + }, + moveResize: { + start: function (window) { + var ids; + if (translucencyEffect.settings.moveresize === 100) { + return; + } + ids = set({ + window: window, + duration: translucencyEffect.settings.duration, + animations: [{ + type: Effect.Opacity, + to: translucencyEffect.settings.moveresize / 100.0 + }] + }); + window.translucencyMoveResizeAnimations = ids; + }, + finish: function (window) { + if (window.translucencyMoveResizeAnimations !== undefined) { + // start revert animation + animate({ + window: window, + duration: translucencyEffect.settings.duration, + animations: [{ + type: Effect.Opacity, + from: translucencyEffect.settings.moveresize / 100.0 + }] + }); + // and cancel previous animation + cancel(window.translucencyMoveResizeAnimations); + window.translucencyMoveResizeAnimations = undefined; + } + } + }, + inactive: { + activated: function (window) { + if (translucencyEffect.settings.inactive === 100) { + return; + } + translucencyEffect.inactive.animate(translucencyEffect.activeWindow); + translucencyEffect.activeWindow = window; + if (window === null) { + return; + } + if (window.translucencyInactiveAnimation !== undefined) { + // start revert animation + animate({ + window: window, + duration: translucencyEffect.settings.duration, + animations: [{ + type: Effect.Opacity, + from: translucencyEffect.settings.inactive / 100.0 + }] + }); + // and cancel previous animation + cancel(window.translucencyInactiveAnimation); + window.translucencyInactiveAnimation = undefined; + } + }, + animate: function (window) { + var ids; + if (translucencyEffect.settings.inactive === 100) { + return; + } + if (window === null) { + return; + } + if (window === effects.activeWindow || + window.popup === true || + (window.x11Client === true && window.managed === false) || + window.desktopWindow === true || + window.dock === true || + window.visible === false || + window.deleted === true) { + return; + } + ids = set({ + window: window, + duration: translucencyEffect.settings.duration, + animations: [{ + type: Effect.Opacity, + to: translucencyEffect.settings.inactive / 100.0 + }] + }); + window.translucencyInactiveAnimation = ids; + } + }, + desktopChanged: function () { + var i, windows; + windows = effects.stackingOrder; + for (i = 0; i < windows.length; i += 1) { + translucencyEffect.cancelAnimations(windows[i]); + translucencyEffect.startAnimation(windows[i]); + if (windows[i] !== effects.activeWindow) { + translucencyEffect.inactive.animate(windows[i]); + } + } + }, + init: function () { + effect.configChanged.connect(translucencyEffect.loadConfig); + effects.desktopPresenceChanged.connect(translucencyEffect.cancelAnimations); + effects.desktopPresenceChanged.connect(translucencyEffect.startAnimation); + effects.windowAdded.connect(translucencyEffect.startAnimation); + effects.windowUnminimized.connect(translucencyEffect.startAnimation); + effects.windowClosed.connect(translucencyEffect.cancelAnimations); + effects.windowMinimized.connect(translucencyEffect.cancelAnimations); + effects.windowUnminimized.connect(translucencyEffect.inactive.animate); + effects.windowStartUserMovedResized.connect(translucencyEffect.moveResize.start); + effects.windowFinishUserMovedResized.connect(translucencyEffect.moveResize.finish); + effects.windowActivated.connect(translucencyEffect.inactive.activated); + effects['desktopChanged(int,int)'].connect(translucencyEffect.desktopChanged); + translucencyEffect.loadConfig(); + } +}; +translucencyEffect.init(); diff --git a/effects/translucency/package/contents/config/main.xml b/effects/translucency/package/contents/config/main.xml new file mode 100644 index 0000000..3112442 --- /dev/null +++ b/effects/translucency/package/contents/config/main.xml @@ -0,0 +1,36 @@ + + + + + + 80 + + + 100 + + + 100 + + + 100 + + + 100 + + + false + + + 100 + + + 100 + + + 100 + + + diff --git a/effects/translucency/package/contents/ui/config.ui b/effects/translucency/package/contents/ui/config.ui new file mode 100644 index 0000000..30d517e --- /dev/null +++ b/effects/translucency/package/contents/ui/config.ui @@ -0,0 +1,473 @@ + + + KWin::TranslucencyEffectConfigForm + + + + 0 + 0 + 643 + 269 + + + + Translucency + + + + + + General Translucency Settings + + + + + + 10 + + + 100 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + 10 + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + 0 + 0 + + + + Combobox popups: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_ComboboxPopups + + + + + + + 10 + + + 100 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + 10 + + + + + + + 10 + + + 100 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + 10 + + + + + + + + 0 + 0 + + + + Opaque + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + Dialogs: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_Dialogs + + + + + + + + 0 + 0 + + + + Transparent + + + + + + + 10 + + + 100 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + 10 + + + + + + + + 0 + 0 + + + + Menus: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_Menus + + + + + + + + 0 + 0 + + + + Moving windows: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_MoveResize + + + + + + + + 0 + 0 + + + + Inactive windows: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_Inactive + + + + + + + + 170 + 0 + + + + 10 + + + 100 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + 10 + + + + + + + + + + Set menu translucency independently + + + true + + + false + + + + + + + 0 + 0 + + + + Dropdown menus: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_DropdownMenus + + + + + + + + 170 + 0 + + + + 10 + + + 100 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + 10 + + + + + + + + 0 + 0 + + + + Popup menus: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_PopupMenus + + + + + + + 10 + + + 100 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + 10 + + + + + + + + 0 + 0 + + + + Torn-off menus: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_TornOffMenus + + + + + + + 10 + + + 100 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + 10 + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + 0 + 0 + + + + Transparent + + + + + + + + 0 + 0 + + + + Opaque + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + kcfg_Inactive + kcfg_MoveResize + kcfg_Dialogs + kcfg_ComboboxPopups + kcfg_Menus + kcfg_IndividualMenuConfig + kcfg_DropdownMenus + kcfg_PopupMenus + kcfg_TornOffMenus + + + + + kcfg_IndividualMenuConfig + toggled(bool) + kcfg_Menus + setDisabled(bool) + + + 109 + 316 + + + 212 + 220 + + + + + diff --git a/effects/translucency/package/metadata.desktop b/effects/translucency/package/metadata.desktop new file mode 100644 index 0000000..485a4e4 --- /dev/null +++ b/effects/translucency/package/metadata.desktop @@ -0,0 +1,172 @@ +[Desktop Entry] +Name=Translucency +Name[af]=Deursigtigheid +Name[ar]=شبه الشفافية +Name[az]=Yarımşəffaflıq +Name[be]=Празрыстасць +Name[be@latin]=Prazrystaść +Name[bg]=Прозрачност +Name[bn]=সমস্বচ্ছতা +Name[br]=Treuzwel +Name[bs]=Prozirnost +Name[ca]=Translucidesa +Name[ca@valencia]=Translucidesa +Name[cs]=Průhlednost +Name[csb]=Przezérnota +Name[da]=Gennemsigtighed +Name[de]=Transparenz +Name[el]=Διαφάνεια +Name[en_GB]=Translucency +Name[eo]=Diafaneco +Name[es]=Transparencia +Name[et]=Läbipaistvus +Name[eu]=Zeharrargitasuna +Name[fi]=Läpikuultavuus +Name[fr]=Translucidité +Name[fy]=Trochsichtichheid +Name[ga]=Tréshoilseacht +Name[gl]=Transparencia +Name[gu]=પારદર્શકતા +Name[he]=שקיפות חלקית +Name[hi]=अल्पपारदर्शिता +Name[hne]=पारभासी +Name[hr]=Prozirnost +Name[hu]=Áttetszőség +Name[ia]=Translucentia +Name[id]=Bening +Name[is]=Hálfgegnsæi +Name[it]=Translucenza +Name[ja]=半透明 +Name[ka]=ნახევრადგამჭირვალეობა +Name[kk]=Мөлдірлік +Name[km]=ភាព​ល្អក់ +Name[kn]=ಪಾರದೀಪಕತೆ (ಟ್ರಾನ್ಸಲುಸೆಂಸಿ) +Name[ko]=반투명 +Name[lt]=Dalinis permatomumas +Name[lv]=CaurspÄ«dÄ«gums +Name[mai]=अल्पपारदर्शिता +Name[mk]=Провидност +Name[ml]=സുതാര്യം +Name[mr]=अर्द्ध - पारदर्शकता +Name[nb]=Gjennomskinnelighet +Name[nds]=Dörschienen +Name[ne]=पारभासकता +Name[nl]=Transparatie +Name[nn]=Gjennomsikt +Name[pa]=ਬਲੌਰੀ (ਟਰੈਨਜ਼ਲੂਸਨਟ) +Name[pl]=Prześwitywanie +Name[pt]=Translucidez +Name[pt_BR]=Transparência +Name[ro]=Transluciditate +Name[ru]=Полупрозрачность +Name[se]=Čađačuovgi +Name[si]=පාරභාසකතාව +Name[sk]=PriesvitnosÅ¥ +Name[sl]=Prosojnost +Name[sr]=Прозирност +Name[sr@ijekavian]=Прозирност +Name[sr@ijekavianlatin]=Prozirnost +Name[sr@latin]=Prozirnost +Name[sv]=Genomlysning +Name[ta]=ஒளிகசிவு +Name[te]=ట్రాన్‍స్‌లుసెన్సీ +Name[th]=ความโปร่งแสง +Name[tr]=Şeffaflık +Name[ug]=يېرىم سۈزۈكلۈك +Name[uk]=Прозорість +Name[uz]=Shaffoflik +Name[uz@cyrillic]=Шаффофлик +Name[vi]=Trong mờ +Name[wa]=Transparince +Name[x-test]=xxTranslucencyxx +Name[zh_CN]=透明度 +Name[zh_TW]=半透明 +Icon=preferences-system-windows-effect-translucency +Comment=Make windows translucent under different conditions +Comment[ar]=يجعل النوافذ شبه شفافة في شروط مختلفة +Comment[az]=Müxtəlif əməllərdə pəncərənin yarımşəffaf olması +Comment[be@latin]=Robić vokny prazrystymi dla peÅ­nych umovaÅ­ +Comment[bg]=Прозрачност на прозорците при определени условия +Comment[bs]=Prozori se provide pod različitim uslovima +Comment[ca]=Torna translúcides les finestres en diverses condicions +Comment[ca@valencia]=Torna translúcides les finestres en diverses condicions +Comment[cs]=Zobrazuje okna různě průhledná +Comment[csb]=Robi òkna przezérnëmi przë ùstalonëch reglach +Comment[da]=Gør vinduer gennemsigtige under forskellige omstændigheder +Comment[de]=Lässt Fenster unter festgelegten Bedingungen durchscheinen. +Comment[el]=Εμφάνιση ημιδιαφανών παραθύρων σε διάφορες περιπτώσεις +Comment[en_GB]=Make windows translucent under different conditions +Comment[es]=Hace la ventana translúcida en distintas condiciones +Comment[et]=Akende muutmine läbipaistvaks teatavatel tingimustel +Comment[eu]=Leihoak zeharrargi bihurtzen ditu baldintzen arabera +Comment[fi]=Tee ikkunoista läpikuultavia eri ehtojen alaisina +Comment[fr]=Affiche des fenêtres translucides en fonction de diverses conditions +Comment[fy]=Meitsje finsters trochsichtich ûnder oare kondysjes +Comment[ga]=Déan fuinneoga tréshoilseach uaireanta difriúla +Comment[gl]=Fai translúcidas as xanelas en distintos casos +Comment[gu]=જુદી જુદી સ્થિતિઓમાં વિન્ડોસને પારદર્શક બનાવે છે +Comment[he]=הפיכת חלונות לשקופים חלקית תחת תנאים שונים +Comment[hi]=विभिन्न परिस्थितियों में विंडो को अल्पपारदर्शी करें +Comment[hne]=अलग अलग स्थिति मं विंडो ल अर्धपारदर्सी बनाव +Comment[hr]=Učini prozore prozirnima pod raznim uvjetima +Comment[hu]=Áttetszővé tesz ablakokat bizonyos feltételek teljesülése esetén +Comment[ia]=On face fenestra translucente sub conditiones differente +Comment[id]=Buat window bening di bawah kondisi yang berbeda +Comment[is]=Gerir glugga hálfgegnsæa við ýmis tilefni +Comment[it]=Rende le finestre translucide in certe condizioni +Comment[ja]=さまざまな状況でウィンドウを半透明にします +Comment[kk]=Түрлі жағдайда терезелерді мөлдір қылады +Comment[km]=ធ្វើ​ឲ្យ​បង្អួច​ថ្លា​ក្រោមលក្ខខណ្ឌ​ផ្សេងៗ​គ្នា +Comment[kn]=ವಿವಿಧ ಪರಿಸ್ಥಿತಿಗಳಲ್ಲಿ ಕಿಟಕಿಗಳನ್ನು ಅರೆಪಾರದರ್ಶಕಗೊಳಿಸು (ಟ್ರಾನ್ಸ್ಲೂಸೆಂಟ್) +Comment[ko]=다른 ì¡°ê±´ 하에서 창을 투명하게 만듭니다 +Comment[lt]=Esant tam tikroms sąlygoms paverčia langus dalinai permatomais +Comment[lv]=Noteiktos apstākļos padara logus caurspÄ«dÄ«gus +Comment[mk]=Ги прави прозорците провидни во различни ситуации +Comment[ml]=ജാലകങ്ങള്‍ പ്രത്യേക സാഹചര്യങ്ങളില്‍ അര്‍ദ്ധ-സുതാര്യമാക്കുന്നു +Comment[mr]=विविध परिस्थितींमध्ये चौकटी अर्द्ध - पारदर्शक करा +Comment[nb]=Gjør vinduer gjennomskinnelige under forskjellige forhold +Comment[nds]=Finstern bi verscheden Bedingen dörschienen maken +Comment[nl]=Maakt vensters doorschijnend onder andere condities +Comment[nn]=Gjer vindauge i visse tilfelle gjennomsiktige +Comment[pa]=ਵੱਖ ਵੱਖ ਹਾਲਤਾਂ ਵਿੱਚ ਵਿੰਡੋਜ਼ ਬਲੌਰੀ ਬਣਾਓ +Comment[pl]=Okna są prześwitujące w zależności od różnych warunków +Comment[pt]=Torna as janelas translúcidas sob determinadas condições +Comment[pt_BR]=Torna as janelas transparentes em determinadas condições +Comment[ro]=Face ferestrele translucide în diferite condiții +Comment[ru]=Использование полупрозрачности окон при разных событиях +Comment[si]=වෙනස් තත්ව යටතේ කවුළු පාරභාසක කරන්න +Comment[sk]=Zobrazí priesvitné okná za určitých okolností +Comment[sl]=Pod različnimi pogoji postanejo okna prosojna +Comment[sr]=Прозори се провиде под различитим условима +Comment[sr@ijekavian]=Прозори се провиде под различитим условима +Comment[sr@ijekavianlatin]=Prozori se provide pod različitim uslovima +Comment[sr@latin]=Prozori se provide pod različitim uslovima +Comment[sv]=Gör fönster halvgenomskinliga enligt olika villkor +Comment[ta]=Make windows translucent under different conditions +Comment[te]=విభీన్న స్థితులలో విండోను ట్రాన్‍స్‌లూసెంట్‌గా (స్పష్టంగా) వుంచుము +Comment[th]=ทำให้หน้าต่างดูโปร่งแสงภายใต้เงื่อนไขที่แตกต่างกัน +Comment[tr]=Farklı durumlarda pencereleri şeffaflaştır +Comment[ug]=كۆزنەك مەلۇم شارائىتتا يېرىم سۈزۈك ئۈنۈمدە كۆرۈنىدۇ +Comment[uk]=Додавання прозорості до вікон за різних умов +Comment[vi]=Làm trong suốt cá»­a sổ trong các điều kiện khác nhau +Comment[wa]=Rind les purneas voeyoute sorlon diferinnès condicions +Comment[x-test]=xxMake windows translucent under different conditionsxx +Comment[zh_CN]=使窗口在某些条件下呈现半透明效果 +Comment[zh_TW]=在不同的情況下讓視窗變半透明 + +Type=Service +X-KDE-ServiceTypes=KWin/Effect,KCModule +X-KDE-PluginInfo-Author=LuboÅ¡ Luňák, Martin Gräßlin +X-KDE-PluginInfo-Email=l.lunak@kde.org, mgraesslin@kde.org +X-KDE-PluginInfo-Name=kwin4_effect_translucency +X-KDE-PluginInfo-Version=0.1.0 +X-KDE-PluginInfo-Category=Appearance +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-EnabledByDefault=true +X-KDE-Ordering=50 +X-Plasma-API=javascript +X-Plasma-MainScript=code/main.js +X-KDE-PluginKeyword=kwin4_effect_translucency +X-KDE-Library=kcm_kwin4_genericscripted +X-KDE-ParentComponents=kwin4_effect_translucency +X-KWin-Config-TranslationDomain=kwin_effects diff --git a/effects/windowaperture/package/contents/code/main.js b/effects/windowaperture/package/contents/code/main.js new file mode 100644 index 0000000..8d930e7 --- /dev/null +++ b/effects/windowaperture/package/contents/code/main.js @@ -0,0 +1,192 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Thomas Lübking + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +/*global effect, effects, animate, animationTime, Effect, QEasingCurve */ + +"use strict"; + +var badBadWindowsEffect = { + duration: animationTime(250), + loadConfig: function () { + badBadWindowsEffect.duration = animationTime(250); + }, + offToCorners: function (showing) { + var stackingOrder = effects.stackingOrder; + var screenGeo = effects.virtualScreenGeometry; + var xOffset = screenGeo.width / 16; + var yOffset = screenGeo.height / 16; + if (showing) { + var closestWindows = [ undefined, undefined, undefined, undefined ]; + var movedWindowsCount = 0; + for (var i = 0; i < stackingOrder.length; ++i) { + var w = stackingOrder[i]; + + // ignore windows above the desktop + // (when not showing, pretty much everything would be) + if (w.desktopWindow) + break; + + // ignore invisible windows and such that do not have to be restored + if (!w.visible) + continue; + + // we just fade out docks - moving panels into edges looks dull + if (w.dock) { + w.offToCornerId = set({ + window: w, + duration: badBadWindowsEffect.duration, + animations: [{ + type: Effect.Opacity, + to: 0.0 + }] + }); + continue; + } + + // calculate the corner distances + var geo = w.geometry; + var dl = geo.x + geo.width - screenGeo.x; + var dr = screenGeo.x + screenGeo.width - geo.x; + var dt = geo.y + geo.height - screenGeo.y; + var db = screenGeo.y + screenGeo.height - geo.y; + w.apertureDistances = [ dl + dt, dr + dt, dr + db, dl + db ]; + movedWindowsCount += 1; + + // if this window is the closest one to any corner, set it as preferred there + var nearest = 0; + for (var j = 1; j < 4; ++j) { + if (w.apertureDistances[j] < w.apertureDistances[nearest] || + (w.apertureDistances[j] == w.apertureDistances[nearest] && closestWindows[j] === undefined)) { + nearest = j; + } + } + if (closestWindows[nearest] === undefined || + closestWindows[nearest].apertureDistances[nearest] > w.apertureDistances[nearest]) + closestWindows[nearest] = w; + } + + // second pass, select corners + + // 1st off, move the nearest windows to their nearest corners + // this will ensure that if there's only on window in the lower right + // it won't be moved out to the upper left + var movedWindowsDec = [ 0, 0, 0, 0 ]; + for (var i = 0; i < 4; ++i) { + if (closestWindows[i] === undefined) + continue; + closestWindows[i].apertureCorner = i; + delete closestWindows[i].apertureDistances; + movedWindowsDec[i] = 1; + } + + // 2nd, distribute the remainders according to their preferences + // this doesn't exactly have heapsort performance ;-) + movedWindowsCount = Math.floor((movedWindowsCount + 3) / 4); + for (var i = 0; i < 4; ++i) { + for (var j = 0; j < movedWindowsCount - movedWindowsDec[i]; ++j) { + var bestWindow = undefined; + for (var k = 0; k < stackingOrder.length; ++k) { + if (stackingOrder[k].apertureDistances === undefined) + continue; + if (bestWindow === undefined || + stackingOrder[k].apertureDistances[i] < bestWindow.apertureDistances[i]) + bestWindow = stackingOrder[k]; + } + if (bestWindow === undefined) + break; + bestWindow.apertureCorner = i; + delete bestWindow.apertureDistances; + } + } + + } + + // actually re/move windows from/to assigned corners + for (var i = 0; i < stackingOrder.length; ++i) { + var w = stackingOrder[i]; + if (w.apertureCorner === undefined && w.offToCornerId === undefined) + continue; + + // keep windows above the desktop visually + effects.setElevatedWindow(w, showing); + + if (!showing && w.dock) { + cancel(w.offToCornerId); + delete w.offToCornerId; + delete w.apertureCorner; // should not exist, but better safe than sorry. + animate({ + window: w, + duration: badBadWindowsEffect.duration, + animations: [{ + type: Effect.Opacity, + from: 0.0 + }] + }); + continue; + } + + var anchor, tx, ty; + var geo = w.geometry; + if (w.apertureCorner == 1 || w.apertureCorner == 2) { + tx = screenGeo.x + screenGeo.width - xOffset; + anchor = Effect.Left; + } else { + tx = xOffset; + anchor = Effect.Right; + } + if (w.apertureCorner > 1) { + ty = screenGeo.y + screenGeo.height - yOffset; + anchor |= Effect.Top; + } else { + ty = yOffset; + anchor |= Effect.Bottom; + } + + if (showing) { + w.offToCornerId = set({ + window: w, + duration: badBadWindowsEffect.duration, + curve: QEasingCurve.InOutCubic, + animations: [{ + type: Effect.Position, + targetAnchor: anchor, + to: { value1: tx, value2: ty } + },{ + type: Effect.Opacity, + to: 0.2 + }] + }); + } else { + cancel(w.offToCornerId); + delete w.offToCornerId; + delete w.apertureCorner; + if (w.visible) { // could meanwhile have been hidden + animate({ + window: w, + duration: badBadWindowsEffect.duration, + curve: QEasingCurve.InOutCubic, + animations: [{ + type: Effect.Position, + sourceAnchor: anchor, + from: { value1: tx, value2: ty } + },{ + type: Effect.Opacity, + from: 0.2 + }] + }); + } + } + } + }, + init: function () { + badBadWindowsEffect.loadConfig(); + effects.showingDesktopChanged.connect(badBadWindowsEffect.offToCorners); + } +}; + +badBadWindowsEffect.init(); diff --git a/effects/windowaperture/package/metadata.desktop b/effects/windowaperture/package/metadata.desktop new file mode 100644 index 0000000..1b65492 --- /dev/null +++ b/effects/windowaperture/package/metadata.desktop @@ -0,0 +1,87 @@ +[Desktop Entry] +Name=Window Aperture +Name[ast]=Apertura de ventanes +Name[az]=Pəncərələrin səpələnməsi +Name[ca]=Obertura de la finestra +Name[ca@valencia]=Obertura de la finestra +Name[cs]=Mřížka oken +Name[da]=Vinduesblænding +Name[de]=Fensteröffnung +Name[el]=Διάφραγμα παραθύρου +Name[en_GB]=Window Aperture +Name[es]=Apertura de ventanas +Name[et]=Aknaava +Name[eu]=Leihoa irekitzea +Name[fi]=Ikkunoiden siirto näytön kulmiin +Name[fr]=Ouverture de la fenêtre +Name[gl]=Apertura das xanelas +Name[hu]=Ablakrekeszek +Name[ia]=Aperturas de fenestra +Name[id]=Window Melubang +Name[it]=Apertura delle finestre +Name[ko]=조리개 모양 배치 +Name[lt]=Lango anga +Name[nb]=VindusÃ¥pning +Name[nl]=Vensteropening +Name[nn]=Vindaugsflukt +Name[pl]=Przesłona okna +Name[pt]=Aperto das Janelas +Name[pt_BR]=Aperto das janelas +Name[ro]=Diafragmă fereastră +Name[ru]=Разбрасывание окон в стороны +Name[sk]=Otvor okien +Name[sl]=Zaslonka okna +Name[sr]=Бленда прозора +Name[sr@ijekavian]=Бленда прозора +Name[sr@ijekavianlatin]=Blenda prozora +Name[sr@latin]=Blenda prozora +Name[sv]=Fönsteröppning +Name[tr]=Pencere Açıklığı +Name[uk]=Апертура вікна +Name[x-test]=xxWindow Aperturexx +Name[zh_CN]=窗口光圈 +Name[zh_TW]=視窗光圈 +Icon=preferences-system-windows-effect-windowaperture +Comment=Move windows into screen corners +Comment[az]=Pəncərələrin ekranın künclərinə çəkilməsi +Comment[ca]=Mou les finestres cap a les cantonades de la pantalla +Comment[cs]=Přesunout okna do rohů obrazovky +Comment[en_GB]=Move windows into screen corners +Comment[es]=Mover las ventanas a las esquinas de la pantalla +Comment[et]=Akende liigutamine ekraani nurkadesse +Comment[eu]=Eraman leihoak pantailako bazterretara +Comment[fi]=Siirrä ikkunat näytön kulmiin +Comment[fr]=Déplace les fenêtres dans les coins de l'écran +Comment[ia]=Move fenestra in angulos de schermo +Comment[id]=Pindahkan window ke sudut layar +Comment[it]=Sposta le finestre negli angoli dello schermo +Comment[ko]=창을 화면 꼭짓점으로 이동 +Comment[lt]=Perkelti langus į kampus +Comment[nl]=Verplaats vensters in de hoeken van het scherm +Comment[nn]=Flytt vindauge til skjermhjørne +Comment[pl]=Rozsuwa okna w narożniki ekranu +Comment[pt]=Mover as janelas para os cantos do ecrã +Comment[pt_BR]=Move as janelas para os cantos da tela +Comment[ro]=Mută ferestrele în colțurile ecranului +Comment[ru]=Перемещение окон в углы экрана +Comment[sk]=Presunúť okná do rohov obrazovky +Comment[sl]=Pomakne okna v kote zaslona +Comment[sv]=Flytta fönster till skärmhörn +Comment[uk]=Пересування вікон до кутів екрана +Comment[x-test]=xxMove windows into screen cornersxx +Comment[zh_CN]=将窗口移动到屏幕角 +Comment[zh_TW]=將視窗移至螢幕角落 + +Type=Service +X-Plasma-API=javascript +X-Plasma-MainScript=code/main.js +X-KDE-ServiceTypes=KWin/Effect +X-KDE-PluginInfo-Author=Thomas Lübking +X-KDE-PluginInfo-Email=thomas.luebking@gmail.com +X-KDE-PluginInfo-Name=kwin4_effect_windowaperture +X-KDE-PluginInfo-Version=0.1.0 +X-KDE-PluginInfo-Category=Show Desktop Animation +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-EnabledByDefault=true +X-KDE-Ordering=50 +X-KWin-Exclusive-Category=show-desktop diff --git a/effects/windowgeometry/CMakeLists.txt b/effects/windowgeometry/CMakeLists.txt new file mode 100644 index 0000000..4a46042 --- /dev/null +++ b/effects/windowgeometry/CMakeLists.txt @@ -0,0 +1,24 @@ +####################################### +# Config +set(kwin_windowgeometry_config_SRCS windowgeometry_config.cpp) +ki18n_wrap_ui(kwin_windowgeometry_config_SRCS windowgeometry_config.ui) +kconfig_add_kcfg_files(kwin_windowgeometry_config_SRCS windowgeometryconfig.kcfgc) + +add_library(kwin_windowgeometry_config MODULE ${kwin_windowgeometry_config_SRCS}) + +target_link_libraries(kwin_windowgeometry_config + KF5::ConfigWidgets + KF5::GlobalAccel + KF5::I18n + KF5::XmlGui + KWinEffectsInterface +) + +kcoreaddons_desktop_to_json(kwin_windowgeometry_config windowgeometry_config.desktop SERVICE_TYPES kcmodule.desktop) + +install( + TARGETS + kwin_windowgeometry_config + DESTINATION + ${PLUGIN_INSTALL_DIR}/kwin/effects/configs +) diff --git a/effects/windowgeometry/windowgeometry.cpp b/effects/windowgeometry/windowgeometry.cpp new file mode 100644 index 0000000..328b345 --- /dev/null +++ b/effects/windowgeometry/windowgeometry.cpp @@ -0,0 +1,229 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2010 Thomas Lübking + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "windowgeometry.h" +// KConfigSkeleton +#include "windowgeometryconfig.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace KWin +{ + +WindowGeometry::WindowGeometry() +{ + initConfig(); + iAmActivated = true; + iAmActive = false; + myResizeWindow = nullptr; +#define myResizeString "Window geometry display, %1 and %2 are the new size," \ + " %3 and %4 are pixel increments - avoid reformatting or suffixes like 'px'", \ + "Width: %1 (%3)\nHeight: %2 (%4)" +#define myCoordString_0 "Window geometry display, %1 and %2 are the cartesian x and y coordinates" \ + " - avoid reformatting or suffixes like 'px'", \ + "X: %1\nY: %2" +#define myCoordString_1 "Window geometry display, %1 and %2 are the cartesian x and y coordinates," \ + " %3 and %4 are the resp. increments - avoid reformatting or suffixes like 'px'", \ + "X: %1 (%3)\nY: %2 (%4)" + reconfigure(ReconfigureAll); + QAction* a = new QAction(this); + a->setObjectName(QStringLiteral("WindowGeometry")); + a->setText(i18n("Toggle window geometry display (effect only)")); + + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::CTRL + Qt::SHIFT + Qt::Key_F11); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::CTRL + Qt::SHIFT + Qt::Key_F11); + effects->registerGlobalShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_F11, a); + + connect(a, &QAction::triggered, this, &WindowGeometry::toggle); + + connect(effects, &EffectsHandler::windowStartUserMovedResized, this, &WindowGeometry::slotWindowStartUserMovedResized); + connect(effects, &EffectsHandler::windowFinishUserMovedResized, this, &WindowGeometry::slotWindowFinishUserMovedResized); + connect(effects, &EffectsHandler::windowStepUserMovedResized, this, &WindowGeometry::slotWindowStepUserMovedResized); +} + +WindowGeometry::~WindowGeometry() +{ + for (int i = 0; i < 3; ++i) + delete myMeasure[i]; +} + +void WindowGeometry::createFrames() +{ + if (myMeasure[0]) { + return; + } + QFont fnt; fnt.setBold(true); fnt.setPointSize(12); + for (int i = 0; i < 3; ++i) { + myMeasure[i] = effects->effectFrame(EffectFrameUnstyled, false); + myMeasure[i]->setFont(fnt); + } + myMeasure[0]->setAlignment(Qt::AlignLeft | Qt::AlignTop); + myMeasure[1]->setAlignment(Qt::AlignCenter); + myMeasure[2]->setAlignment(Qt::AlignRight | Qt::AlignBottom); + +} + +void WindowGeometry::reconfigure(ReconfigureFlags) +{ + WindowGeometryConfiguration::self()->read(); + iHandleMoves = WindowGeometryConfiguration::move(); + iHandleResizes = WindowGeometryConfiguration::resize(); +} + +void WindowGeometry::paintScreen(int mask, const QRegion ®ion, ScreenPaintData &data) +{ + effects->paintScreen(mask, region, data); + if (iAmActivated && iAmActive) { + for (int i = 0; i < 3; ++i) + myMeasure[i]->render(infiniteRegion(), 1.0, .66); + } +} + +void WindowGeometry::toggle() +{ + iAmActivated = !iAmActivated; + createFrames(); +} + +void WindowGeometry::slotWindowStartUserMovedResized(EffectWindow *w) +{ + if (!iAmActivated) + return; + if (w->isUserResize() && !iHandleResizes) + return; + if (w->isUserMove() && !iHandleMoves) + return; + + createFrames(); + iAmActive = true; + myResizeWindow = w; + myOriginalGeometry = w->geometry(); + myCurrentGeometry = w->geometry(); + slotWindowStepUserMovedResized(w, w->geometry()); +} + +void WindowGeometry::slotWindowFinishUserMovedResized(EffectWindow *w) +{ + if (iAmActive && w == myResizeWindow) { + iAmActive = false; + myResizeWindow = nullptr; + w->addRepaintFull(); + if (myExtraDirtyArea.isValid()) + w->addLayerRepaint(myExtraDirtyArea); + myExtraDirtyArea = QRect(); + } +} + +static inline QString number(int n) +{ + QLocale locale; + QString sign; + if (n >= 0) { + sign = locale.positiveSign(); + if (sign.isEmpty()) sign = QStringLiteral("+"); + } + else { + n = -n; + sign = locale.negativeSign(); + if (sign.isEmpty()) sign = QStringLiteral("-"); + } + return sign + QString::number(n); +} + + +void WindowGeometry::slotWindowStepUserMovedResized(EffectWindow *w, const QRect &geometry) +{ + if (iAmActivated && iAmActive && w == myResizeWindow) { + if (myExtraDirtyArea.isValid()) + effects->addRepaint(myExtraDirtyArea); + + myExtraDirtyArea = QRect(); + + myCurrentGeometry = geometry; + QPoint center = geometry.center(); + const QRect &r = geometry; + const QRect &r2 = myOriginalGeometry; + const QRect screen = effects->clientArea(ScreenArea, w); + QRect expandedGeometry = w->expandedGeometry(); + expandedGeometry = geometry.adjusted(expandedGeometry.x() - w->x(), + expandedGeometry.y() - w->y(), + expandedGeometry.right() - w->geometry().right(), + expandedGeometry.bottom() - w->geometry().bottom()); + + // sufficient for moves, resizes calculated otherwise + int dx = r.x() - r2.x(); + int dy = r.y() - r2.y(); + + // upper left ---------------------- + if (w->isUserResize()) + myMeasure[0]->setText( i18nc(myCoordString_1, r.x(), r.y(), number(dx), number(dy) ) ); + else + myMeasure[0]->setText( i18nc(myCoordString_0, r.x(), r.y() ) ); + QPoint pos = expandedGeometry.topLeft(); + pos = QPoint(qMax(pos.x(), screen.x()), qMax(pos.y(), screen.y())); + myMeasure[0]->setPosition(pos + QPoint(6,6)); // "6" is magic number because the unstyled effectframe has 5px padding + + // center ---------------------- + if (w->isUserResize()) { + // calc width for center element, otherwise the current dx/dx remains right + dx = r.width() - r2.width(); + dy = r.height() - r2.height(); + + const QSize baseInc = w->basicUnit(); + if (baseInc != QSize(1,1)) { + Q_ASSERT(baseInc.width() && baseInc.height()); + const QSize csz = w->contentsRect().size(); + myMeasure[1]->setText( i18nc(myResizeString, csz.width()/baseInc.width(), csz.height()/baseInc.height(), number(dx/baseInc.width()), number(dy/baseInc.height()) ) ); + } else + myMeasure[1]->setText( i18nc(myResizeString, r.width(), r.height(), number(dx), number(dy) ) ); + + // calc width for bottomright element, superfluous otherwise + dx = r.right() - r2.right(); + dy = r.bottom() - r2.bottom(); + } else + myMeasure[1]->setText( i18nc(myCoordString_0, number(dx), number(dy) ) ); + const int cdx = myMeasure[1]->geometry().width() / 2 + 3; // "3" = 6/2 is magic number because + const int cdy = myMeasure[1]->geometry().height() / 2 + 3; // the unstyled effectframe has 5px padding + center = QPoint(qMax(center.x(), screen.x() + cdx), + qMax(center.y(), screen.y() + cdy)); + center = QPoint(qMin(center.x(), screen.right() - cdx), + qMin(center.y(), screen.bottom() - cdy)); + myMeasure[1]->setPosition(center); + + // lower right ---------------------- + if (w->isUserResize()) + myMeasure[2]->setText( i18nc(myCoordString_1, r.right(), r.bottom(), number(dx), number(dy) ) ); + else + myMeasure[2]->setText( i18nc(myCoordString_0, r.right(), r.bottom() ) ); + pos = expandedGeometry.bottomRight(); + pos = QPoint(qMin(pos.x(), screen.right()), qMin(pos.y(), screen.bottom())); + myMeasure[2]->setPosition(pos - QPoint(6,6)); // "6" is magic number because the unstyled effectframe has 5px padding + + myExtraDirtyArea |= myMeasure[0]->geometry(); + myExtraDirtyArea |= myMeasure[1]->geometry(); + myExtraDirtyArea |= myMeasure[2]->geometry(); + myExtraDirtyArea.adjust(-6,-6,6,6); + + if (myExtraDirtyArea.isValid()) + effects->addRepaint(myExtraDirtyArea); + } +} + +bool WindowGeometry::isActive() const +{ + return iAmActive; +} + +} // namespace KWin diff --git a/effects/windowgeometry/windowgeometry.h b/effects/windowgeometry/windowgeometry.h new file mode 100644 index 0000000..c52815c --- /dev/null +++ b/effects/windowgeometry/windowgeometry.h @@ -0,0 +1,62 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2010 Thomas Lübking + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef WINDOWGEOMETRY_H +#define WINDOWGEOMETRY_H + +#include + +namespace KWin +{ + +class WindowGeometry : public Effect +{ + Q_OBJECT + Q_PROPERTY(bool handlesMoves READ isHandlesMoves) + Q_PROPERTY(bool handlesResizes READ isHandlesResizes) +public: + WindowGeometry(); + ~WindowGeometry() override; + + inline bool provides(Effect::Feature ef) override { + return ef == Effect::GeometryTip; + } + void reconfigure(ReconfigureFlags) override; + void paintScreen(int mask, const QRegion ®ion, ScreenPaintData &data) override; + bool isActive() const override; + + int requestedEffectChainPosition() const override { + return 90; + } + + // for properties + bool isHandlesMoves() const { + return iHandleMoves; + } + bool isHandlesResizes() const { + return iHandleResizes; + } +private Q_SLOTS: + void toggle(); + void slotWindowStartUserMovedResized(KWin::EffectWindow *w); + void slotWindowFinishUserMovedResized(KWin::EffectWindow *w); + void slotWindowStepUserMovedResized(KWin::EffectWindow *w, const QRect &geometry); +private: + void createFrames(); + EffectWindow *myResizeWindow; + EffectFrame *myMeasure[3] = {nullptr, nullptr, nullptr}; + QRect myOriginalGeometry, myCurrentGeometry; + QRect myExtraDirtyArea; + bool iAmActive, iAmActivated, iHandleMoves, iHandleResizes; + QString myCoordString[2], myResizeString; +}; + +} // namespace + +#endif diff --git a/effects/windowgeometry/windowgeometry.kcfg b/effects/windowgeometry/windowgeometry.kcfg new file mode 100644 index 0000000..4b79adb --- /dev/null +++ b/effects/windowgeometry/windowgeometry.kcfg @@ -0,0 +1,15 @@ + + + + + + true + + + true + + + diff --git a/effects/windowgeometry/windowgeometry_config.cpp b/effects/windowgeometry/windowgeometry_config.cpp new file mode 100644 index 0000000..99210a9 --- /dev/null +++ b/effects/windowgeometry/windowgeometry_config.cpp @@ -0,0 +1,84 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2010 Thomas Lübking + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "windowgeometry_config.h" +// KConfigSkeleton +#include "windowgeometryconfig.h" +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +K_PLUGIN_FACTORY_WITH_JSON(WindowGeometryEffectConfigFactory, + "windowgeometry_config.json", + registerPlugin();) + +namespace KWin +{ + +WindowGeometryConfigForm::WindowGeometryConfigForm(QWidget* parent) : QWidget(parent) +{ + setupUi(this); +} + +WindowGeometryConfig::WindowGeometryConfig(QWidget* parent, const QVariantList& args) + : KCModule(KAboutData::pluginData(QStringLiteral("windowgeometry")), parent, args) +{ + WindowGeometryConfiguration::instance(KWIN_CONFIG); + QVBoxLayout* layout = new QVBoxLayout(this); + layout->addWidget(myUi = new WindowGeometryConfigForm(this)); + + // Shortcut config. The shortcut belongs to the component "kwin"! + myActionCollection = new KActionCollection(this, QStringLiteral("kwin")); + myActionCollection->setComponentDisplayName(i18n("KWin")); + QAction* a = myActionCollection->addAction(QStringLiteral("WindowGeometry")); + a->setText(i18n("Toggle KWin composited geometry display")); + a->setProperty("isConfigurationAction", true); + + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::CTRL + Qt::SHIFT + Qt::Key_F11); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::CTRL + Qt::SHIFT + Qt::Key_F11); + myUi->shortcuts->addCollection(myActionCollection); + + connect(myUi->shortcuts, &KShortcutsEditor::keyChange, this, &WindowGeometryConfig::markAsChanged); + + addConfig(WindowGeometryConfiguration::self(), myUi); + + load(); +} + +WindowGeometryConfig::~WindowGeometryConfig() +{ + // Undo (only) unsaved changes to global key shortcuts + myUi->shortcuts->undoChanges(); +} + +void WindowGeometryConfig::save() +{ + KCModule::save(); + myUi->shortcuts->save(); // undo() will restore to this state from now on + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("windowgeometry")); +} + +void WindowGeometryConfig::defaults() +{ + myUi->shortcuts->allDefault(); + emit changed(true); +} + +} //namespace +#include "windowgeometry_config.moc" diff --git a/effects/windowgeometry/windowgeometry_config.desktop b/effects/windowgeometry/windowgeometry_config.desktop new file mode 100644 index 0000000..0327cde --- /dev/null +++ b/effects/windowgeometry/windowgeometry_config.desktop @@ -0,0 +1,64 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=KCModule + +X-KDE-Library=kwin_windowgeometry_config +X-KDE-ParentComponents=windowgeometry + +Name=WindowGeometry +Name[az]=Pəncərənin mövqeyi +Name[bg]=Геометрия на прозореца +Name[bs]=Geometrija prozora +Name[ca]=Geometria de la finestra +Name[ca@valencia]=WindowGeometry +Name[cs]=Geometrie okna +Name[da]=Vinduesgeometri +Name[de]=Fenstergeometrie +Name[el]=Γεωμετρία παραθύρου +Name[en_GB]=WindowGeometry +Name[es]=Geometría de la ventana +Name[et]=Akna geomeetria +Name[eu]=Leihoaren geometria +Name[fi]=Ikkunan mitat +Name[fr]=Géométrie de la fenêtre +Name[gl]=Xeometría da xanela +Name[he]=גדלי חלונות +Name[hi]=विंडोज्यामिति +Name[hr]=GeometrijaProzora +Name[hu]=Ablakgeometria +Name[ia]=WindowGeometry +Name[id]=Geometri Window +Name[is]=GluggaLögun +Name[it]=Geometria delle finestre +Name[ja]=WindowGeometry +Name[kk]=Терезенің өлшемдері +Name[km]=WindowGeometry +Name[ko]=ì°½ 크기 +Name[lt]=Lango geometrija +Name[lv]=LogaÄ¢eometrija +Name[mr]=चौकट भूमिती +Name[nb]=Vindusgeometri +Name[nds]=Finsterafmeten +Name[nl]=VensterGeometry +Name[nn]=Vindaugsgeometri +Name[pa]=ਵਿੰਡੋਜੁਮੈਟਰੀ +Name[pl]=Geometria okna +Name[pt]=Geometria da Janela +Name[pt_BR]=Geometria da Janela +Name[ro]=GeometrieFereastră +Name[ru]=Геометрия окна +Name[sk]=Geometria okna +Name[sl]=Geometrija okna +Name[sr]=Геометрија прозора +Name[sr@ijekavian]=Геометрија прозора +Name[sr@ijekavianlatin]=Geometrija prozora +Name[sr@latin]=Geometrija prozora +Name[sv]=Fönstergeometri +Name[th]=มิติขนาดของหน้าต่าง +Name[tr]=PencereGeometrisi +Name[ug]=WindowGeometry +Name[uk]=Розміри вікна +Name[wa]=Djeyometreye del finiesse +Name[x-test]=xxWindowGeometryxx +Name[zh_CN]=窗口形状 +Name[zh_TW]=視窗位置 diff --git a/effects/windowgeometry/windowgeometry_config.h b/effects/windowgeometry/windowgeometry_config.h new file mode 100644 index 0000000..ce5ca7d --- /dev/null +++ b/effects/windowgeometry/windowgeometry_config.h @@ -0,0 +1,46 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2010 Thomas Lübking + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef WINDOWGEOMETRY_CONFIG_H +#define WINDOWGEOMETRY_CONFIG_H + +#include + +#include "ui_windowgeometry_config.h" + + +namespace KWin +{ + +class WindowGeometryConfigForm : public QWidget, public Ui::WindowGeometryConfigForm +{ + Q_OBJECT +public: + explicit WindowGeometryConfigForm(QWidget* parent); +}; + +class WindowGeometryConfig : public KCModule +{ + Q_OBJECT +public: + explicit WindowGeometryConfig(QWidget* parent = nullptr, const QVariantList& args = QVariantList()); + ~WindowGeometryConfig() override; + +public Q_SLOTS: + void save() override; + void defaults() override; + +private: + WindowGeometryConfigForm* myUi; + KActionCollection* myActionCollection; +}; + +} // namespace + +#endif diff --git a/effects/windowgeometry/windowgeometry_config.ui b/effects/windowgeometry/windowgeometry_config.ui new file mode 100644 index 0000000..5a4977b --- /dev/null +++ b/effects/windowgeometry/windowgeometry_config.ui @@ -0,0 +1,47 @@ + + + KWin::WindowGeometryConfigForm + + + + 0 + 0 + 430 + 187 + + + + + + + Display for moving windows + + + + + + + Display for resizing windows + + + + + + + KShortcutsEditor::GlobalAction + + + + + + + + KShortcutsEditor + QWidget +
KShortcutsEditor
+ 1 +
+
+ + +
diff --git a/effects/windowgeometry/windowgeometryconfig.kcfgc b/effects/windowgeometry/windowgeometryconfig.kcfgc new file mode 100644 index 0000000..5fd7287 --- /dev/null +++ b/effects/windowgeometry/windowgeometryconfig.kcfgc @@ -0,0 +1,5 @@ +File=windowgeometry.kcfg +ClassName=WindowGeometryConfiguration +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/effects/wobblywindows/CMakeLists.txt b/effects/wobblywindows/CMakeLists.txt new file mode 100644 index 0000000..fb2e038 --- /dev/null +++ b/effects/wobblywindows/CMakeLists.txt @@ -0,0 +1,23 @@ +####################################### +# Config +set(kwin_wobblywindows_config_SRCS wobblywindows_config.cpp) +ki18n_wrap_ui(kwin_wobblywindows_config_SRCS wobblywindows_config.ui) +kconfig_add_kcfg_files(kwin_wobblywindows_config_SRCS wobblywindowsconfig.kcfgc) + +add_library(kwin_wobblywindows_config MODULE ${kwin_wobblywindows_config_SRCS}) + +target_link_libraries(kwin_wobblywindows_config + KF5::ConfigWidgets + KF5::I18n + Qt5::DBus + KWinEffectsInterface +) + +kcoreaddons_desktop_to_json(kwin_wobblywindows_config wobblywindows_config.desktop SERVICE_TYPES kcmodule.desktop) + +install( + TARGETS + kwin_wobblywindows_config + DESTINATION + ${PLUGIN_INSTALL_DIR}/kwin/effects/configs +) diff --git a/effects/wobblywindows/wobblywindows.cpp b/effects/wobblywindows/wobblywindows.cpp new file mode 100644 index 0000000..3113ed0 --- /dev/null +++ b/effects/wobblywindows/wobblywindows.cpp @@ -0,0 +1,1093 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008 Cédric Borgese + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + + +#include "wobblywindows.h" +#include "wobblywindowsconfig.h" + +#include + +//#define COMPUTE_STATS + +// if you enable it and run kwin in a terminal from the session it manages, +// be sure to redirect the output of kwin in a file or +// you'll propably get deadlocks. +//#define VERBOSE_MODE + +#if defined COMPUTE_STATS && !defined VERBOSE_MODE +# ifdef __GNUC__ +# warning "You enable COMPUTE_STATS without VERBOSE_MODE, computed stats will not be printed." +# endif +#endif + +namespace KWin +{ + +struct ParameterSet { + qreal stiffness; + qreal drag; + qreal move_factor; + + qreal xTesselation; + qreal yTesselation; + + qreal minVelocity; + qreal maxVelocity; + qreal stopVelocity; + qreal minAcceleration; + qreal maxAcceleration; + qreal stopAcceleration; +}; + +static const ParameterSet set_0 = { + 0.15, + 0.80, + 0.10, + 20.0, + 20.0, + 0.0, + 1000.0, + 0.5, + 0.0, + 1000.0, + 0.5, +}; + +static const ParameterSet set_1 = { + 0.10, + 0.85, + 0.10, + 20.0, + 20.0, + 0.0, + 1000.0, + 0.5, + 0.0, + 1000.0, + 0.5, +}; + +static const ParameterSet set_2 = { + 0.06, + 0.90, + 0.10, + 20.0, + 20.0, + 0.0, + 1000.0, + 0.5, + 0.0, + 1000.0, + 0.5, +}; + +static const ParameterSet set_3 = { + 0.03, + 0.92, + 0.20, + 20.0, + 20.0, + 0.0, + 1000.0, + 0.5, + 0.0, + 1000.0, + 0.5, +}; + +static const ParameterSet set_4 = { + 0.01, + 0.97, + 0.25, + 20.0, + 20.0, + 0.0, + 1000.0, + 0.5, + 0.0, + 1000.0, + 0.5, +}; + +static const ParameterSet pset[5] = { set_0, set_1, set_2, set_3, set_4 }; + +WobblyWindowsEffect::WobblyWindowsEffect() +{ + initConfig(); + reconfigure(ReconfigureAll); + connect(effects, &EffectsHandler::windowStartUserMovedResized, this, &WobblyWindowsEffect::slotWindowStartUserMovedResized); + connect(effects, &EffectsHandler::windowStepUserMovedResized, this, &WobblyWindowsEffect::slotWindowStepUserMovedResized); + connect(effects, &EffectsHandler::windowFinishUserMovedResized, this, &WobblyWindowsEffect::slotWindowFinishUserMovedResized); + connect(effects, &EffectsHandler::windowMaximizedStateChanged, this, &WobblyWindowsEffect::slotWindowMaximizeStateChanged); +} + +WobblyWindowsEffect::~WobblyWindowsEffect() +{ + if (!windows.empty()) { + // we should be empty at this point... + // emit a warning and clean the list. + qCDebug(KWINEFFECTS) << "Windows list not empty. Left items : " << windows.count(); + QHash< const EffectWindow*, WindowWobblyInfos >::iterator i; + for (i = windows.begin(); i != windows.end(); ++i) { + freeWobblyInfo(i.value()); + } + } +} + +void WobblyWindowsEffect::reconfigure(ReconfigureFlags) +{ + WobblyWindowsConfig::self()->read(); + + QString settingsMode = WobblyWindowsConfig::settings(); + if (settingsMode != QStringLiteral("Custom")) { + unsigned int wobblynessLevel = WobblyWindowsConfig::wobblynessLevel(); + if (wobblynessLevel > 4) { + qCDebug(KWINEFFECTS) << "Wrong value for \"WobblynessLevel\" : " << wobblynessLevel; + wobblynessLevel = 4; + } + setParameterSet(pset[wobblynessLevel]); + + if (WobblyWindowsConfig::advancedMode()) { + m_stiffness = WobblyWindowsConfig::stiffness() / 100.0; + m_drag = WobblyWindowsConfig::drag() / 100.0; + m_move_factor = WobblyWindowsConfig::moveFactor() / 100.0; + } + } else { // Custom method, read all values from config file. + m_stiffness = WobblyWindowsConfig::stiffness() / 100.0; + m_drag = WobblyWindowsConfig::drag() / 100.0; + m_move_factor = WobblyWindowsConfig::moveFactor() / 100.0; + + m_xTesselation = WobblyWindowsConfig::xTesselation(); + m_yTesselation = WobblyWindowsConfig::yTesselation(); + + m_minVelocity = WobblyWindowsConfig::minVelocity(); + m_maxVelocity = WobblyWindowsConfig::maxVelocity(); + m_stopVelocity = WobblyWindowsConfig::stopVelocity(); + m_minAcceleration = WobblyWindowsConfig::minAcceleration(); + m_maxAcceleration = WobblyWindowsConfig::maxAcceleration(); + m_stopAcceleration = WobblyWindowsConfig::stopAcceleration(); + } + + m_moveWobble = WobblyWindowsConfig::moveWobble(); + m_resizeWobble = WobblyWindowsConfig::resizeWobble(); + +#if defined VERBOSE_MODE + qCDebug(KWINEFFECTS) << "Parameters :\n" << + "grid(" << m_stiffness << ", " << m_drag << ", " << m_move_factor << ")\n" << + "velocity(" << m_minVelocity << ", " << m_maxVelocity << ", " << m_stopVelocity << ")\n" << + "acceleration(" << m_minAcceleration << ", " << m_maxAcceleration << ", " << m_stopAcceleration << ")\n" << + "tesselation(" << m_xTesselation << ", " << m_yTesselation << ")"; +#endif +} + +bool WobblyWindowsEffect::supported() +{ + return effects->isOpenGLCompositing() && effects->animationsSupported(); +} + +void WobblyWindowsEffect::setParameterSet(const ParameterSet& pset) +{ + m_stiffness = pset.stiffness; + m_drag = pset.drag; + m_move_factor = pset.move_factor; + + m_xTesselation = pset.xTesselation; + m_yTesselation = pset.yTesselation; + + m_minVelocity = pset.minVelocity; + m_maxVelocity = pset.maxVelocity; + m_stopVelocity = pset.stopVelocity; + m_minAcceleration = pset.minAcceleration; + m_maxAcceleration = pset.maxAcceleration; + m_stopAcceleration = pset.stopAcceleration; +} + +void WobblyWindowsEffect::setVelocityThreshold(qreal m_minVelocity) +{ + this->m_minVelocity = m_minVelocity; +} + +void WobblyWindowsEffect::setMoveFactor(qreal factor) +{ + m_move_factor = factor; +} + +void WobblyWindowsEffect::setStiffness(qreal stiffness) +{ + m_stiffness = stiffness; +} + +void WobblyWindowsEffect::setDrag(qreal drag) +{ + m_drag = drag; +} + +void WobblyWindowsEffect::prePaintScreen(ScreenPrePaintData& data, int time) +{ + // We need to mark the screen windows as transformed. Otherwise the whole + // screen won't be repainted, resulting in artefacts. + // Could we just set a subset of the screen to be repainted ? + if (windows.count() != 0) { + m_updateRegion = QRegion(); + } + + effects->prePaintScreen(data, time); +} +const qreal maxTime = 10.0; +void WobblyWindowsEffect::prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time) +{ + if (windows.contains(w)) { + data.setTransformed(); + data.quads = data.quads.makeRegularGrid(m_xTesselation, m_yTesselation); + bool stop = false; + qreal updateTime = time; + + // We have to reset the clip region in order to render clients below + // opaque wobbly windows. + data.clip = QRegion(); + + while (!stop && (updateTime > maxTime)) { +#if defined VERBOSE_MODE + qCDebug(KWINEFFECTS) << "loop time " << updateTime << " / " << time; +#endif + stop = !updateWindowWobblyDatas(w, maxTime); + updateTime -= maxTime; + } + if (!stop && updateTime > 0) { + updateWindowWobblyDatas(w, updateTime); + } + } + + effects->prePaintWindow(w, data, time); +} + +void WobblyWindowsEffect::paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data) +{ + if (!(mask & PAINT_SCREEN_TRANSFORMED) && windows.contains(w)) { + WindowWobblyInfos& wwi = windows[w]; + int tx = w->geometry().x(); + int ty = w->geometry().y(); + double left = 0.0; + double top = 0.0; + double right = w->width(); + double bottom = w->height(); + for (int i = 0; i < data.quads.count(); ++i) { + for (int j = 0; j < 4; ++j) { + WindowVertex& v = data.quads[i][j]; + Pair oldPos = {tx + v.x(), ty + v.y()}; + Pair newPos = computeBezierPoint(wwi, oldPos); + v.move(newPos.x - tx, newPos.y - ty); + } + left = qMin(left, data.quads[i].left()); + top = qMin(top, data.quads[i].top()); + right = qMax(right, data.quads[i].right()); + bottom = qMax(bottom, data.quads[i].bottom()); + } + QRectF dirtyRect( + left * data.xScale() + w->x() + data.xTranslation(), + top * data.yScale() + w->y() + data.yTranslation(), + (right - left + 1.0) * data.xScale(), + (bottom - top + 1.0) * data.yScale()); + // Expand the dirty region by 1px to fix potential round/floor issues. + dirtyRect.adjust(-1.0, -1.0, 1.0, 1.0); + m_updateRegion = m_updateRegion.united(dirtyRect.toRect()); + } + + // Call the next effect. + effects->paintWindow(w, mask, region, data); +} + +void WobblyWindowsEffect::postPaintScreen() +{ + if (!windows.isEmpty()) { + effects->addRepaint(m_updateRegion); + } + + // Call the next effect. + effects->postPaintScreen(); +} + +void WobblyWindowsEffect::slotWindowStartUserMovedResized(EffectWindow *w) +{ + if (w->isSpecialWindow()) { + return; + } + + if ((w->isUserMove() && m_moveWobble) || (w->isUserResize() && m_resizeWobble)) { + startMovedResized(w); + } +} + +void WobblyWindowsEffect::slotWindowStepUserMovedResized(EffectWindow *w, const QRect &geometry) +{ + Q_UNUSED(geometry) + if (windows.contains(w)) { + WindowWobblyInfos& wwi = windows[w]; + QRect rect = w->geometry(); + if (rect.y() != wwi.resize_original_rect.y()) wwi.can_wobble_top = true; + if (rect.x() != wwi.resize_original_rect.x()) wwi.can_wobble_left = true; + if (rect.right() != wwi.resize_original_rect.right()) wwi.can_wobble_right = true; + if (rect.bottom() != wwi.resize_original_rect.bottom()) wwi.can_wobble_bottom = true; + } +} + +void WobblyWindowsEffect::slotWindowFinishUserMovedResized(EffectWindow *w) +{ + if (windows.contains(w)) { + WindowWobblyInfos& wwi = windows[w]; + wwi.status = Free; + QRect rect = w->geometry(); + if (rect.y() != wwi.resize_original_rect.y()) wwi.can_wobble_top = true; + if (rect.x() != wwi.resize_original_rect.x()) wwi.can_wobble_left = true; + if (rect.right() != wwi.resize_original_rect.right()) wwi.can_wobble_right = true; + if (rect.bottom() != wwi.resize_original_rect.bottom()) wwi.can_wobble_bottom = true; + } +} + +void WobblyWindowsEffect::slotWindowMaximizeStateChanged(EffectWindow *w, bool horizontal, bool vertical) +{ + Q_UNUSED(horizontal) + Q_UNUSED(vertical) + if (w->isUserMove() || w->isSpecialWindow()) { + return; + } + + if (m_moveWobble && m_resizeWobble) { + stepMovedResized(w); + } + + if (windows.contains(w)) { + WindowWobblyInfos& wwi = windows[w]; + QRect rect = w->geometry(); + if (rect.y() != wwi.resize_original_rect.y()) wwi.can_wobble_top = true; + if (rect.x() != wwi.resize_original_rect.x()) wwi.can_wobble_left = true; + if (rect.right() != wwi.resize_original_rect.right()) wwi.can_wobble_right = true; + if (rect.bottom() != wwi.resize_original_rect.bottom()) wwi.can_wobble_bottom = true; + } +} + +void WobblyWindowsEffect::startMovedResized(EffectWindow* w) +{ + if (!windows.contains(w)) { + WindowWobblyInfos new_wwi; + initWobblyInfo(new_wwi, w->geometry()); + windows[w] = new_wwi; + } + + WindowWobblyInfos& wwi = windows[w]; + wwi.status = Moving; + const QRectF& rect = w->geometry(); + + qreal x_increment = rect.width() / (wwi.width - 1.0); + qreal y_increment = rect.height() / (wwi.height - 1.0); + + Pair picked = {static_cast(cursorPos().x()), static_cast(cursorPos().y())}; + int indx = (picked.x - rect.x()) / x_increment + 0.5; + int indy = (picked.y - rect.y()) / y_increment + 0.5; + int pickedPointIndex = indy * wwi.width + indx; + if (pickedPointIndex < 0) { + qCDebug(KWINEFFECTS) << "Picked index == " << pickedPointIndex << " with (" << cursorPos().x() << "," << cursorPos().y() << ")"; + pickedPointIndex = 0; + } else if (static_cast(pickedPointIndex) > wwi.count - 1) { + qCDebug(KWINEFFECTS) << "Picked index == " << pickedPointIndex << " with (" << cursorPos().x() << "," << cursorPos().y() << ")"; + pickedPointIndex = wwi.count - 1; + } +#if defined VERBOSE_MODE + qCDebug(KWINEFFECTS) << "Original Picked point -- x : " << picked.x << " - y : " << picked.y; +#endif + wwi.constraint[pickedPointIndex] = true; + + if (w->isUserResize()) { + // on a resize, do not allow any edges to wobble until it has been moved from + // its original location + wwi.can_wobble_top = wwi.can_wobble_left = wwi.can_wobble_right = wwi.can_wobble_bottom = false; + wwi.resize_original_rect = w->geometry(); + } else { + wwi.can_wobble_top = wwi.can_wobble_left = wwi.can_wobble_right = wwi.can_wobble_bottom = true; + } +} + +void WobblyWindowsEffect::stepMovedResized(EffectWindow* w) +{ + QRect new_geometry = w->geometry(); + if (!windows.contains(w)) { + WindowWobblyInfos new_wwi; + initWobblyInfo(new_wwi, new_geometry); + windows[w] = new_wwi; + } + + WindowWobblyInfos& wwi = windows[w]; + wwi.status = Free; + + QRect maximized_area = effects->clientArea(MaximizeArea, w); + bool throb_direction_out = (new_geometry.top() == maximized_area.top() && new_geometry.bottom() == maximized_area.bottom()) || + (new_geometry.left() == maximized_area.left() && new_geometry.right() == maximized_area.right()); + qreal magnitude = throb_direction_out ? 10 : -30; // a small throb out when maximized, a larger throb inwards when restored + for (unsigned int j = 0; j < wwi.height; ++j) { + for (unsigned int i = 0; i < wwi.width; ++i) { + Pair v = { magnitude*(i / qreal(wwi.width - 1) - 0.5), magnitude*(j / qreal(wwi.height - 1) - 0.5) }; + wwi.velocity[j*wwi.width+i] = v; + } + } + + // constrain the middle of the window, so that any asymetry wont cause it to drift off-center + for (unsigned int j = 1; j < wwi.height - 1; ++j) { + for (unsigned int i = 1; i < wwi.width - 1; ++i) { + wwi.constraint[j*wwi.width+i] = true; + } + } +} + +void WobblyWindowsEffect::initWobblyInfo(WindowWobblyInfos& wwi, QRect geometry) const +{ + wwi.count = 4 * 4; + wwi.width = 4; + wwi.height = 4; + + wwi.bezierWidth = m_xTesselation; + wwi.bezierHeight = m_yTesselation; + wwi.bezierCount = m_xTesselation * m_yTesselation; + + wwi.origin = new Pair[wwi.count]; + wwi.position = new Pair[wwi.count]; + wwi.velocity = new Pair[wwi.count]; + wwi.acceleration = new Pair[wwi.count]; + wwi.buffer = new Pair[wwi.count]; + wwi.constraint = new bool[wwi.count]; + + wwi.bezierSurface = new Pair[wwi.bezierCount]; + + wwi.status = Moving; + + qreal x = geometry.x(), y = geometry.y(); + qreal width = geometry.width(), height = geometry.height(); + + Pair initValue = {x, y}; + static const Pair nullPair = {0.0, 0.0}; + + qreal x_increment = width / (wwi.width - 1.0); + qreal y_increment = height / (wwi.height - 1.0); + + for (unsigned int j = 0; j < 4; ++j) { + for (unsigned int i = 0; i < 4; ++i) { + unsigned int idx = j * 4 + i; + wwi.origin[idx] = initValue; + wwi.position[idx] = initValue; + wwi.velocity[idx] = nullPair; + wwi.constraint[idx] = false; + if (i != 4 - 2) { // x grid count - 2, i.e. not the last point + initValue.x += x_increment; + } else { + initValue.x = width + x; + } + initValue.x = initValue.x; + } + initValue.x = x; + initValue.x = initValue.x; + if (j != 4 - 2) { // y grid count - 2, i.e. not the last point + initValue.y += y_increment; + } else { + initValue.y = height + y; + } + initValue.y = initValue.y; + } +} + +void WobblyWindowsEffect::freeWobblyInfo(WindowWobblyInfos& wwi) const +{ + delete[] wwi.origin; + delete[] wwi.position; + delete[] wwi.velocity; + delete[] wwi.acceleration; + delete[] wwi.buffer; + delete[] wwi.constraint; + + delete[] wwi.bezierSurface; +} + +WobblyWindowsEffect::Pair WobblyWindowsEffect::computeBezierPoint(const WindowWobblyInfos& wwi, Pair point) const +{ + // compute the input value + Pair topleft = wwi.origin[0]; + Pair bottomright = wwi.origin[wwi.count-1]; + + qreal tx = (point.x - topleft.x) / (bottomright.x - topleft.x); + qreal ty = (point.y - topleft.y) / (bottomright.y - topleft.y); + + // compute polynomial coeff + + qreal px[4]; + px[0] = (1 - tx) * (1 - tx) * (1 - tx); + px[1] = 3 * (1 - tx) * (1 - tx) * tx; + px[2] = 3 * (1 - tx) * tx * tx; + px[3] = tx * tx * tx; + + qreal py[4]; + py[0] = (1 - ty) * (1 - ty) * (1 - ty); + py[1] = 3 * (1 - ty) * (1 - ty) * ty; + py[2] = 3 * (1 - ty) * ty * ty; + py[3] = ty * ty * ty; + + Pair res = {0.0, 0.0}; + + for (unsigned int j = 0; j < 4; ++j) { + for (unsigned int i = 0; i < 4; ++i) { + // this assume the grid is 4*4 + res.x += px[i] * py[j] * wwi.position[i + j * wwi.width].x; + res.y += px[i] * py[j] * wwi.position[i + j * wwi.width].y; + } + } + + return res; +} + +namespace +{ + +static inline void fixVectorBounds(WobblyWindowsEffect::Pair& vec, qreal min, qreal max) +{ + if (fabs(vec.x) < min) { + vec.x = 0.0; + } else if (fabs(vec.x) > max) { + if (vec.x > 0.0) { + vec.x = max; + } else { + vec.x = -max; + } + } + + if (fabs(vec.y) < min) { + vec.y = 0.0; + } else if (fabs(vec.y) > max) { + if (vec.y > 0.0) { + vec.y = max; + } else { + vec.y = -max; + } + } +} + +#if defined COMPUTE_STATS +static inline void computeVectorBounds(WobblyWindowsEffect::Pair& vec, WobblyWindowsEffect::Pair& bound) +{ + if (fabs(vec.x) < bound.x) { + bound.x = fabs(vec.x); + } else if (fabs(vec.x) > bound.y) { + bound.y = fabs(vec.x); + } + if (fabs(vec.y) < bound.x) { + bound.x = fabs(vec.y); + } else if (fabs(vec.y) > bound.y) { + bound.y = fabs(vec.y); + } +} +#endif + +} // close the anonymous namespace + +bool WobblyWindowsEffect::updateWindowWobblyDatas(EffectWindow* w, qreal time) +{ + QRectF rect = w->geometry(); + WindowWobblyInfos& wwi = windows[w]; + + qreal x_length = rect.width() / (wwi.width - 1.0); + qreal y_length = rect.height() / (wwi.height - 1.0); + +#if defined VERBOSE_MODE + qCDebug(KWINEFFECTS) << "time " << time; + qCDebug(KWINEFFECTS) << "increment x " << x_length << " // y" << y_length; +#endif + + Pair origine = {rect.x(), rect.y()}; + + for (unsigned int j = 0; j < wwi.height; ++j) { + for (unsigned int i = 0; i < wwi.width; ++i) { + wwi.origin[wwi.width*j + i] = origine; + if (i != wwi.width - 2) { + origine.x += x_length; + } else { + origine.x = rect.width() + rect.x(); + } + } + origine.x = rect.x(); + if (j != wwi.height - 2) { + origine.y += y_length; + } else { + origine.y = rect.height() + rect.y(); + } + } + + Pair neibourgs[4]; + Pair acceleration; + + qreal acc_sum = 0.0; + qreal vel_sum = 0.0; + + // compute acceleration, velocity and position for each point + + // for corners + + // top-left + + if (wwi.constraint[0]) { + Pair window_pos = wwi.origin[0]; + Pair current_pos = wwi.position[0]; + Pair move = {window_pos.x - current_pos.x, window_pos.y - current_pos.y}; + Pair accel = {move.x*m_stiffness, move.y*m_stiffness}; + wwi.acceleration[0] = accel; + } else { + Pair& pos = wwi.position[0]; + neibourgs[0] = wwi.position[1]; + neibourgs[1] = wwi.position[wwi.width]; + + acceleration.x = ((neibourgs[0].x - pos.x) - x_length) * m_stiffness + (neibourgs[1].x - pos.x) * m_stiffness; + acceleration.y = ((neibourgs[1].y - pos.y) - y_length) * m_stiffness + (neibourgs[0].y - pos.y) * m_stiffness; + + acceleration.x /= 2; + acceleration.y /= 2; + + wwi.acceleration[0] = acceleration; + } + + // top-right + + if (wwi.constraint[wwi.width-1]) { + Pair window_pos = wwi.origin[wwi.width-1]; + Pair current_pos = wwi.position[wwi.width-1]; + Pair move = {window_pos.x - current_pos.x, window_pos.y - current_pos.y}; + Pair accel = {move.x*m_stiffness, move.y*m_stiffness}; + wwi.acceleration[wwi.width-1] = accel; + } else { + Pair& pos = wwi.position[wwi.width-1]; + neibourgs[0] = wwi.position[wwi.width-2]; + neibourgs[1] = wwi.position[2*wwi.width-1]; + + acceleration.x = (x_length - (pos.x - neibourgs[0].x)) * m_stiffness + (neibourgs[1].x - pos.x) * m_stiffness; + acceleration.y = ((neibourgs[1].y - pos.y) - y_length) * m_stiffness + (neibourgs[0].y - pos.y) * m_stiffness; + + acceleration.x /= 2; + acceleration.y /= 2; + + wwi.acceleration[wwi.width-1] = acceleration; + } + + // bottom-left + + if (wwi.constraint[wwi.width*(wwi.height-1)]) { + Pair window_pos = wwi.origin[wwi.width*(wwi.height-1)]; + Pair current_pos = wwi.position[wwi.width*(wwi.height-1)]; + Pair move = {window_pos.x - current_pos.x, window_pos.y - current_pos.y}; + Pair accel = {move.x*m_stiffness, move.y*m_stiffness}; + wwi.acceleration[wwi.width*(wwi.height-1)] = accel; + } else { + Pair& pos = wwi.position[wwi.width*(wwi.height-1)]; + neibourgs[0] = wwi.position[wwi.width*(wwi.height-1)+1]; + neibourgs[1] = wwi.position[wwi.width*(wwi.height-2)]; + + acceleration.x = ((neibourgs[0].x - pos.x) - x_length) * m_stiffness + (neibourgs[1].x - pos.x) * m_stiffness; + acceleration.y = (y_length - (pos.y - neibourgs[1].y)) * m_stiffness + (neibourgs[0].y - pos.y) * m_stiffness; + + acceleration.x /= 2; + acceleration.y /= 2; + + wwi.acceleration[wwi.width*(wwi.height-1)] = acceleration; + } + + // bottom-right + + if (wwi.constraint[wwi.count-1]) { + Pair window_pos = wwi.origin[wwi.count-1]; + Pair current_pos = wwi.position[wwi.count-1]; + Pair move = {window_pos.x - current_pos.x, window_pos.y - current_pos.y}; + Pair accel = {move.x*m_stiffness, move.y*m_stiffness}; + wwi.acceleration[wwi.count-1] = accel; + } else { + Pair& pos = wwi.position[wwi.count-1]; + neibourgs[0] = wwi.position[wwi.count-2]; + neibourgs[1] = wwi.position[wwi.width*(wwi.height-1)-1]; + + acceleration.x = (x_length - (pos.x - neibourgs[0].x)) * m_stiffness + (neibourgs[1].x - pos.x) * m_stiffness; + acceleration.y = (y_length - (pos.y - neibourgs[1].y)) * m_stiffness + (neibourgs[0].y - pos.y) * m_stiffness; + + acceleration.x /= 2; + acceleration.y /= 2; + + wwi.acceleration[wwi.count-1] = acceleration; + } + + + // for borders + + // top border + for (unsigned int i = 1; i < wwi.width - 1; ++i) { + if (wwi.constraint[i]) { + Pair window_pos = wwi.origin[i]; + Pair current_pos = wwi.position[i]; + Pair move = {window_pos.x - current_pos.x, window_pos.y - current_pos.y}; + Pair accel = {move.x*m_stiffness, move.y*m_stiffness}; + wwi.acceleration[i] = accel; + } else { + Pair& pos = wwi.position[i]; + neibourgs[0] = wwi.position[i-1]; + neibourgs[1] = wwi.position[i+1]; + neibourgs[2] = wwi.position[i+wwi.width]; + + acceleration.x = (x_length - (pos.x - neibourgs[0].x)) * m_stiffness + ((neibourgs[1].x - pos.x) - x_length) * m_stiffness + (neibourgs[2].x - pos.x) * m_stiffness; + acceleration.y = ((neibourgs[2].y - pos.y) - y_length) * m_stiffness + (neibourgs[0].y - pos.y) * m_stiffness + (neibourgs[1].y - pos.y) * m_stiffness; + + acceleration.x /= 3; + acceleration.y /= 3; + + wwi.acceleration[i] = acceleration; + } + } + + // bottom border + for (unsigned int i = wwi.width * (wwi.height - 1) + 1; i < wwi.count - 1; ++i) { + if (wwi.constraint[i]) { + Pair window_pos = wwi.origin[i]; + Pair current_pos = wwi.position[i]; + Pair move = {window_pos.x - current_pos.x, window_pos.y - current_pos.y}; + Pair accel = {move.x*m_stiffness, move.y*m_stiffness}; + wwi.acceleration[i] = accel; + } else { + Pair& pos = wwi.position[i]; + neibourgs[0] = wwi.position[i-1]; + neibourgs[1] = wwi.position[i+1]; + neibourgs[2] = wwi.position[i-wwi.width]; + + acceleration.x = (x_length - (pos.x - neibourgs[0].x)) * m_stiffness + ((neibourgs[1].x - pos.x) - x_length) * m_stiffness + (neibourgs[2].x - pos.x) * m_stiffness; + acceleration.y = (y_length - (pos.y - neibourgs[2].y)) * m_stiffness + (neibourgs[0].y - pos.y) * m_stiffness + (neibourgs[1].y - pos.y) * m_stiffness; + + acceleration.x /= 3; + acceleration.y /= 3; + + wwi.acceleration[i] = acceleration; + } + } + + // left border + for (unsigned int i = wwi.width; i < wwi.width*(wwi.height - 1); i += wwi.width) { + if (wwi.constraint[i]) { + Pair window_pos = wwi.origin[i]; + Pair current_pos = wwi.position[i]; + Pair move = {window_pos.x - current_pos.x, window_pos.y - current_pos.y}; + Pair accel = {move.x*m_stiffness, move.y*m_stiffness}; + wwi.acceleration[i] = accel; + } else { + Pair& pos = wwi.position[i]; + neibourgs[0] = wwi.position[i+1]; + neibourgs[1] = wwi.position[i-wwi.width]; + neibourgs[2] = wwi.position[i+wwi.width]; + + acceleration.x = ((neibourgs[0].x - pos.x) - x_length) * m_stiffness + (neibourgs[1].x - pos.x) * m_stiffness + (neibourgs[2].x - pos.x) * m_stiffness; + acceleration.y = (y_length - (pos.y - neibourgs[1].y)) * m_stiffness + ((neibourgs[2].y - pos.y) - y_length) * m_stiffness + (neibourgs[0].y - pos.y) * m_stiffness; + + acceleration.x /= 3; + acceleration.y /= 3; + + wwi.acceleration[i] = acceleration; + } + } + + // right border + for (unsigned int i = 2 * wwi.width - 1; i < wwi.count - 1; i += wwi.width) { + if (wwi.constraint[i]) { + Pair window_pos = wwi.origin[i]; + Pair current_pos = wwi.position[i]; + Pair move = {window_pos.x - current_pos.x, window_pos.y - current_pos.y}; + Pair accel = {move.x*m_stiffness, move.y*m_stiffness}; + wwi.acceleration[i] = accel; + } else { + Pair& pos = wwi.position[i]; + neibourgs[0] = wwi.position[i-1]; + neibourgs[1] = wwi.position[i-wwi.width]; + neibourgs[2] = wwi.position[i+wwi.width]; + + acceleration.x = (x_length - (pos.x - neibourgs[0].x)) * m_stiffness + (neibourgs[1].x - pos.x) * m_stiffness + (neibourgs[2].x - pos.x) * m_stiffness; + acceleration.y = (y_length - (pos.y - neibourgs[1].y)) * m_stiffness + ((neibourgs[2].y - pos.y) - y_length) * m_stiffness + (neibourgs[0].y - pos.y) * m_stiffness; + + acceleration.x /= 3; + acceleration.y /= 3; + + wwi.acceleration[i] = acceleration; + } + } + + // for the inner points + for (unsigned int j = 1; j < wwi.height - 1; ++j) { + for (unsigned int i = 1; i < wwi.width - 1; ++i) { + unsigned int index = i + j * wwi.width; + + if (wwi.constraint[index]) { + Pair window_pos = wwi.origin[index]; + Pair current_pos = wwi.position[index]; + Pair move = {window_pos.x - current_pos.x, window_pos.y - current_pos.y}; + Pair accel = {move.x*m_stiffness, move.y*m_stiffness}; + wwi.acceleration[index] = accel; + } else { + Pair& pos = wwi.position[index]; + neibourgs[0] = wwi.position[index-1]; + neibourgs[1] = wwi.position[index+1]; + neibourgs[2] = wwi.position[index-wwi.width]; + neibourgs[3] = wwi.position[index+wwi.width]; + + acceleration.x = ((neibourgs[0].x - pos.x) - x_length) * m_stiffness + + (x_length - (pos.x - neibourgs[1].x)) * m_stiffness + + (neibourgs[2].x - pos.x) * m_stiffness + + (neibourgs[3].x - pos.x) * m_stiffness; + acceleration.y = (y_length - (pos.y - neibourgs[2].y)) * m_stiffness + + ((neibourgs[3].y - pos.y) - y_length) * m_stiffness + + (neibourgs[0].y - pos.y) * m_stiffness + + (neibourgs[1].y - pos.y) * m_stiffness; + + acceleration.x /= 4; + acceleration.y /= 4; + + wwi.acceleration[index] = acceleration; + } + } + } + + heightRingLinearMean(&wwi.acceleration, wwi); + +#if defined COMPUTE_STATS + Pair accBound = {m_maxAcceleration, m_minAcceleration}; + Pair velBound = {m_maxVelocity, m_minVelocity}; +#endif + + // compute the new velocity of each vertex. + for (unsigned int i = 0; i < wwi.count; ++i) { + Pair acc = wwi.acceleration[i]; + fixVectorBounds(acc, m_minAcceleration, m_maxAcceleration); + +#if defined COMPUTE_STATS + computeVectorBounds(acc, accBound); +#endif + + Pair& vel = wwi.velocity[i]; + vel.x = acc.x * time + vel.x * m_drag; + vel.y = acc.y * time + vel.y * m_drag; + + acc_sum += fabs(acc.x) + fabs(acc.y); + } + + heightRingLinearMean(&wwi.velocity, wwi); + + // compute the new pos of each vertex. + for (unsigned int i = 0; i < wwi.count; ++i) { + Pair& pos = wwi.position[i]; + Pair& vel = wwi.velocity[i]; + + fixVectorBounds(vel, m_minVelocity, m_maxVelocity); +#if defined COMPUTE_STATS + computeVectorBounds(vel, velBound); +#endif + + pos.x += vel.x * time * m_move_factor; + pos.y += vel.y * time * m_move_factor; + + vel_sum += fabs(vel.x) + fabs(vel.y); + +#if defined VERBOSE_MODE + if (wwi.constraint[i]) { + qCDebug(KWINEFFECTS) << "Constraint point ** vel : " << vel.x << "," << vel.y << " ** move : " << vel.x*time << "," << vel.y*time; + } +#endif + } + + if (!wwi.can_wobble_top) { + for (unsigned int i = 0; i < wwi.width; ++i) + for (unsigned j = 0; j < wwi.width - 1; ++j) + wwi.position[i+wwi.width*j].y = wwi.origin[i+wwi.width*j].y; + } + if (!wwi.can_wobble_bottom) { + for (unsigned int i = wwi.width * (wwi.height - 1); i < wwi.count; ++i) + for (unsigned j = 0; j < wwi.width - 1; ++j) + wwi.position[i-wwi.width*j].y = wwi.origin[i-wwi.width*j].y; + } + if (!wwi.can_wobble_left) { + for (unsigned int i = 0; i < wwi.count; i += wwi.width) + for (unsigned j = 0; j < wwi.width - 1; ++j) + wwi.position[i+j].x = wwi.origin[i+j].x; + } + if (!wwi.can_wobble_right) { + for (unsigned int i = wwi.width - 1; i < wwi.count; i += wwi.width) + for (unsigned j = 0; j < wwi.width - 1; ++j) + wwi.position[i-j].x = wwi.origin[i-j].x; + } + +#if defined VERBOSE_MODE +# if defined COMPUTE_STATS + qCDebug(KWINEFFECTS) << "Acceleration bounds (" << accBound.x << ", " << accBound.y << ")"; + qCDebug(KWINEFFECTS) << "Velocity bounds (" << velBound.x << ", " << velBound.y << ")"; +# endif + qCDebug(KWINEFFECTS) << "sum_acc : " << acc_sum << " *** sum_vel :" << vel_sum; +#endif + + if (wwi.status != Moving && acc_sum < m_stopAcceleration && vel_sum < m_stopVelocity) { + freeWobblyInfo(wwi); + windows.remove(w); + if (windows.isEmpty()) + effects->addRepaintFull(); + return false; + } + + return true; +} + +void WobblyWindowsEffect::heightRingLinearMean(Pair** data_pointer, WindowWobblyInfos& wwi) +{ + Pair* data = *data_pointer; + Pair neibourgs[8]; + + // for corners + + // top-left + { + Pair& res = wwi.buffer[0]; + Pair vit = data[0]; + neibourgs[0] = data[1]; + neibourgs[1] = data[wwi.width]; + neibourgs[2] = data[wwi.width+1]; + + res.x = (neibourgs[0].x + neibourgs[1].x + neibourgs[2].x + 3.0 * vit.x) / 6.0; + res.y = (neibourgs[0].y + neibourgs[1].y + neibourgs[2].y + 3.0 * vit.y) / 6.0; + } + + + // top-right + { + Pair& res = wwi.buffer[wwi.width-1]; + Pair vit = data[wwi.width-1]; + neibourgs[0] = data[wwi.width-2]; + neibourgs[1] = data[2*wwi.width-1]; + neibourgs[2] = data[2*wwi.width-2]; + + res.x = (neibourgs[0].x + neibourgs[1].x + neibourgs[2].x + 3.0 * vit.x) / 6.0; + res.y = (neibourgs[0].y + neibourgs[1].y + neibourgs[2].y + 3.0 * vit.y) / 6.0; + } + + + // bottom-left + { + Pair& res = wwi.buffer[wwi.width*(wwi.height-1)]; + Pair vit = data[wwi.width*(wwi.height-1)]; + neibourgs[0] = data[wwi.width*(wwi.height-1)+1]; + neibourgs[1] = data[wwi.width*(wwi.height-2)]; + neibourgs[2] = data[wwi.width*(wwi.height-2)+1]; + + res.x = (neibourgs[0].x + neibourgs[1].x + neibourgs[2].x + 3.0 * vit.x) / 6.0; + res.y = (neibourgs[0].y + neibourgs[1].y + neibourgs[2].y + 3.0 * vit.y) / 6.0; + } + + + // bottom-right + { + Pair& res = wwi.buffer[wwi.count-1]; + Pair vit = data[wwi.count-1]; + neibourgs[0] = data[wwi.count-2]; + neibourgs[1] = data[wwi.width*(wwi.height-1)-1]; + neibourgs[2] = data[wwi.width*(wwi.height-1)-2]; + + res.x = (neibourgs[0].x + neibourgs[1].x + neibourgs[2].x + 3.0 * vit.x) / 6.0; + res.y = (neibourgs[0].y + neibourgs[1].y + neibourgs[2].y + 3.0 * vit.y) / 6.0; + } + + + // for borders + + // top border + for (unsigned int i = 1; i < wwi.width - 1; ++i) { + Pair& res = wwi.buffer[i]; + Pair vit = data[i]; + neibourgs[0] = data[i-1]; + neibourgs[1] = data[i+1]; + neibourgs[2] = data[i+wwi.width]; + neibourgs[3] = data[i+wwi.width-1]; + neibourgs[4] = data[i+wwi.width+1]; + + res.x = (neibourgs[0].x + neibourgs[1].x + neibourgs[2].x + neibourgs[3].x + neibourgs[4].x + 5.0 * vit.x) / 10.0; + res.y = (neibourgs[0].y + neibourgs[1].y + neibourgs[2].y + neibourgs[3].y + neibourgs[4].y + 5.0 * vit.y) / 10.0; + } + + // bottom border + for (unsigned int i = wwi.width * (wwi.height - 1) + 1; i < wwi.count - 1; ++i) { + Pair& res = wwi.buffer[i]; + Pair vit = data[i]; + neibourgs[0] = data[i-1]; + neibourgs[1] = data[i+1]; + neibourgs[2] = data[i-wwi.width]; + neibourgs[3] = data[i-wwi.width-1]; + neibourgs[4] = data[i-wwi.width+1]; + + res.x = (neibourgs[0].x + neibourgs[1].x + neibourgs[2].x + neibourgs[3].x + neibourgs[4].x + 5.0 * vit.x) / 10.0; + res.y = (neibourgs[0].y + neibourgs[1].y + neibourgs[2].y + neibourgs[3].y + neibourgs[4].y + 5.0 * vit.y) / 10.0; + } + + // left border + for (unsigned int i = wwi.width; i < wwi.width*(wwi.height - 1); i += wwi.width) { + Pair& res = wwi.buffer[i]; + Pair vit = data[i]; + neibourgs[0] = data[i+1]; + neibourgs[1] = data[i-wwi.width]; + neibourgs[2] = data[i+wwi.width]; + neibourgs[3] = data[i-wwi.width+1]; + neibourgs[4] = data[i+wwi.width+1]; + + res.x = (neibourgs[0].x + neibourgs[1].x + neibourgs[2].x + neibourgs[3].x + neibourgs[4].x + 5.0 * vit.x) / 10.0; + res.y = (neibourgs[0].y + neibourgs[1].y + neibourgs[2].y + neibourgs[3].y + neibourgs[4].y + 5.0 * vit.y) / 10.0; + } + + // right border + for (unsigned int i = 2 * wwi.width - 1; i < wwi.count - 1; i += wwi.width) { + Pair& res = wwi.buffer[i]; + Pair vit = data[i]; + neibourgs[0] = data[i-1]; + neibourgs[1] = data[i-wwi.width]; + neibourgs[2] = data[i+wwi.width]; + neibourgs[3] = data[i-wwi.width-1]; + neibourgs[4] = data[i+wwi.width-1]; + + res.x = (neibourgs[0].x + neibourgs[1].x + neibourgs[2].x + neibourgs[3].x + neibourgs[4].x + 5.0 * vit.x) / 10.0; + res.y = (neibourgs[0].y + neibourgs[1].y + neibourgs[2].y + neibourgs[3].y + neibourgs[4].y + 5.0 * vit.y) / 10.0; + } + + // for the inner points + for (unsigned int j = 1; j < wwi.height - 1; ++j) { + for (unsigned int i = 1; i < wwi.width - 1; ++i) { + unsigned int index = i + j * wwi.width; + + Pair& res = wwi.buffer[index]; + Pair& vit = data[index]; + neibourgs[0] = data[index-1]; + neibourgs[1] = data[index+1]; + neibourgs[2] = data[index-wwi.width]; + neibourgs[3] = data[index+wwi.width]; + neibourgs[4] = data[index-wwi.width-1]; + neibourgs[5] = data[index-wwi.width+1]; + neibourgs[6] = data[index+wwi.width-1]; + neibourgs[7] = data[index+wwi.width+1]; + + res.x = (neibourgs[0].x + neibourgs[1].x + neibourgs[2].x + neibourgs[3].x + neibourgs[4].x + neibourgs[5].x + neibourgs[6].x + neibourgs[7].x + 8.0 * vit.x) / 16.0; + res.y = (neibourgs[0].y + neibourgs[1].y + neibourgs[2].y + neibourgs[3].y + neibourgs[4].y + neibourgs[5].y + neibourgs[6].y + neibourgs[7].y + 8.0 * vit.y) / 16.0; + } + } + + Pair* tmp = data; + *data_pointer = wwi.buffer; + wwi.buffer = tmp; +} + +bool WobblyWindowsEffect::isActive() const +{ + return !windows.isEmpty(); +} + +} // namespace KWin diff --git a/effects/wobblywindows/wobblywindows.h b/effects/wobblywindows/wobblywindows.h new file mode 100644 index 0000000..c4848e3 --- /dev/null +++ b/effects/wobblywindows/wobblywindows.h @@ -0,0 +1,192 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008 Cédric Borgese + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_WOBBLYWINDOWS_H +#define KWIN_WOBBLYWINDOWS_H + +// Include with base class for effects. +#include + +namespace KWin +{ + +struct ParameterSet; + +/** + * Effect which wobble windows + */ +class WobblyWindowsEffect : public Effect +{ + Q_OBJECT + Q_PROPERTY(qreal stiffness READ stiffness) + Q_PROPERTY(qreal drag READ drag) + Q_PROPERTY(qreal moveFactor READ moveFactor) + Q_PROPERTY(qreal xTesselation READ xTesselation) + Q_PROPERTY(qreal yTesselation READ yTesselation) + Q_PROPERTY(qreal minVelocity READ minVelocity) + Q_PROPERTY(qreal maxVelocity READ maxVelocity) + Q_PROPERTY(qreal stopVelocity READ stopVelocity) + Q_PROPERTY(qreal minAcceleration READ minAcceleration) + Q_PROPERTY(qreal maxAcceleration READ maxAcceleration) + Q_PROPERTY(qreal stopAcceleration READ stopAcceleration) + Q_PROPERTY(bool moveWobble READ isMoveWobble) + Q_PROPERTY(bool resizeWobble READ isResizeWobble) +public: + + WobblyWindowsEffect(); + ~WobblyWindowsEffect() override; + + void reconfigure(ReconfigureFlags) override; + void prePaintScreen(ScreenPrePaintData& data, int time) override; + void prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time) override; + void paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data) override; + void postPaintScreen() override; + bool isActive() const override; + + int requestedEffectChainPosition() const override { + // Please notice that the Wobbly Windows effect has to be placed + // after the Maximize effect in the effect chain, otherwise there + // can be visual artifacts when dragging maximized windows. + return 70; + } + + // Wobbly model parameters + void setStiffness(qreal stiffness); + void setDrag(qreal drag); + void setVelocityThreshold(qreal velocityThreshold); + void setMoveFactor(qreal factor); + + struct Pair { + qreal x; + qreal y; + }; + + enum WindowStatus { + Free, + Moving, + }; + + static bool supported(); + + // for properties + qreal stiffness() const { + return m_stiffness; + } + qreal drag() const { + return m_drag; + } + qreal moveFactor() const { + return m_move_factor; + } + qreal xTesselation() const { + return m_xTesselation; + } + qreal yTesselation() const { + return m_yTesselation; + } + qreal minVelocity() const { + return m_minVelocity; + } + qreal maxVelocity() const { + return m_maxVelocity; + } + qreal stopVelocity() const { + return m_stopVelocity; + } + qreal minAcceleration() const { + return m_minAcceleration; + } + qreal maxAcceleration() const { + return m_maxAcceleration; + } + qreal stopAcceleration() const { + return m_stopAcceleration; + } + bool isMoveWobble() const { + return m_moveWobble; + } + bool isResizeWobble() const { + return m_resizeWobble; + } + +public Q_SLOTS: + void slotWindowStartUserMovedResized(KWin::EffectWindow *w); + void slotWindowStepUserMovedResized(KWin::EffectWindow *w, const QRect &geometry); + void slotWindowFinishUserMovedResized(KWin::EffectWindow *w); + void slotWindowMaximizeStateChanged(KWin::EffectWindow *w, bool horizontal, bool vertical); + +private: + void startMovedResized(EffectWindow* w); + void stepMovedResized(EffectWindow* w); + bool updateWindowWobblyDatas(EffectWindow* w, qreal time); + + struct WindowWobblyInfos { + Pair* origin; + Pair* position; + Pair* velocity; + Pair* acceleration; + Pair* buffer; + + // if true, the physics system moves this point based only on it "normal" destination + // given by the window position, ignoring neighbour points. + bool* constraint; + + unsigned int width; + unsigned int height; + unsigned int count; + + Pair* bezierSurface; + unsigned int bezierWidth; + unsigned int bezierHeight; + unsigned int bezierCount; + + WindowStatus status; + + // for resizing. Only sides that have moved will wobble + bool can_wobble_top, can_wobble_left, can_wobble_right, can_wobble_bottom; + QRect resize_original_rect; + }; + + QHash< const EffectWindow*, WindowWobblyInfos > windows; + + QRegion m_updateRegion; + + qreal m_stiffness; + qreal m_drag; + qreal m_move_factor; + + // the default tesselation for windows + // use qreal instead of int as I really often need + // these values as real to do divisions. + qreal m_xTesselation; + qreal m_yTesselation; + + qreal m_minVelocity; + qreal m_maxVelocity; + qreal m_stopVelocity; + qreal m_minAcceleration; + qreal m_maxAcceleration; + qreal m_stopAcceleration; + + bool m_moveWobble; + bool m_resizeWobble; + + void initWobblyInfo(WindowWobblyInfos& wwi, QRect geometry) const; + void freeWobblyInfo(WindowWobblyInfos& wwi) const; + + WobblyWindowsEffect::Pair computeBezierPoint(const WindowWobblyInfos& wwi, Pair point) const; + + static void heightRingLinearMean(Pair** data_pointer, WindowWobblyInfos& wwi); + + void setParameterSet(const ParameterSet& pset); +}; + +} // namespace KWin + +#endif // WOBBLYWINDOWS_H diff --git a/effects/wobblywindows/wobblywindows.kcfg b/effects/wobblywindows/wobblywindows.kcfg new file mode 100644 index 0000000..cacab17 --- /dev/null +++ b/effects/wobblywindows/wobblywindows.kcfg @@ -0,0 +1,57 @@ + + + + + + 0 + + + Auto + + + true + + + true + + + false + + + 15 + + + 80 + + + 10 + + + 20 + + + 20 + + + 0.0 + + + 1000.0 + + + 0.5 + + + 0.0 + + + 1000.0 + + + 5.0 + + + diff --git a/effects/wobblywindows/wobblywindows_config.cpp b/effects/wobblywindows/wobblywindows_config.cpp new file mode 100644 index 0000000..a94cf50 --- /dev/null +++ b/effects/wobblywindows/wobblywindows_config.cpp @@ -0,0 +1,109 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008 Cédric Borgese + SPDX-FileCopyrightText: 2008 Lucas Murray + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "wobblywindows_config.h" +// KConfigSkeleton +#include "wobblywindowsconfig.h" +#include +#include + +#include +#include +#include +#include + +K_PLUGIN_FACTORY_WITH_JSON(WobblyWindowsEffectConfigFactory, + "wobblywindows_config.json", + registerPlugin();) + +namespace KWin +{ + +//----------------------------------------------------------------------------- +// WARNING: This is (kinda) copied from wobblywindows.cpp + +struct ParameterSet { + int stiffness; + int drag; + int move_factor; +}; + +static const ParameterSet set_0 = { + 15, + 80, + 10 +}; + +static const ParameterSet set_1 = { + 10, + 85, + 10 +}; + +static const ParameterSet set_2 = { + 6, + 90, + 10 +}; + +static const ParameterSet set_3 = { + 3, + 92, + 20 +}; + +static const ParameterSet set_4 = { + 1, + 97, + 25 +}; + +ParameterSet pset[5] = { set_0, set_1, set_2, set_3, set_4 }; + +//----------------------------------------------------------------------------- + +WobblyWindowsEffectConfig::WobblyWindowsEffectConfig(QWidget* parent, const QVariantList& args) : + KCModule(KAboutData::pluginData(QStringLiteral("wobblywindows")), parent, args) +{ + WobblyWindowsConfig::instance(KWIN_CONFIG); + m_ui.setupUi(this); + + addConfig(WobblyWindowsConfig::self(), this); + connect(m_ui.kcfg_WobblynessLevel, &QSlider::valueChanged, this, &WobblyWindowsEffectConfig::wobblinessChanged); + + load(); +} + +WobblyWindowsEffectConfig::~WobblyWindowsEffectConfig() +{ +} + +void WobblyWindowsEffectConfig::save() +{ + KCModule::save(); + + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("wobblywindows")); +} + +void WobblyWindowsEffectConfig::wobblinessChanged() +{ + ParameterSet preset = pset[m_ui.kcfg_WobblynessLevel->value()]; + + m_ui.kcfg_Stiffness->setValue(preset.stiffness); + m_ui.kcfg_Drag->setValue(preset.drag); + m_ui.kcfg_MoveFactor->setValue(preset.move_factor); +} + +} // namespace + +#include "wobblywindows_config.moc" diff --git a/effects/wobblywindows/wobblywindows_config.desktop b/effects/wobblywindows/wobblywindows_config.desktop new file mode 100644 index 0000000..07ca918 --- /dev/null +++ b/effects/wobblywindows/wobblywindows_config.desktop @@ -0,0 +1,82 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=KCModule + +X-KDE-Library=kwin_wobblywindows_config +X-KDE-ParentComponents=wobblywindows + +Name=Wobbly Windows +Name[af]=Wobbly Windows +Name[ar]=نوافذ متذبذبة +Name[ast]=Ventanes cimblantes +Name[az]=Titrək pəncərələr +Name[be@latin]=SkryÅ­leńnie akon +Name[bg]=Желирани прозорци +Name[bs]=Lelujavi prozori +Name[ca]=Finestres sacsejades +Name[ca@valencia]=Finestres sacsejades +Name[cs]=Chvějící se okna +Name[csb]=Òkna Wobbly +Name[da]=Blævrende vinduer +Name[de]=Wabernde Fenster +Name[el]=Ταλαντευόμενα παράθυρα +Name[en_GB]=Wobbly Windows +Name[eo]=Tremaj fenestroj +Name[es]=Ventanas gelatinosas +Name[et]=Võbisevad aknad +Name[eu]=Leiho dardartiak +Name[fi]=Heiluvat ikkunat +Name[fr]=Fenêtres en gélatine +Name[fy]=Wobbly Windows +Name[ga]=Fuinneoga Creathacha +Name[gl]=Xanelas a tremer +Name[gu]=વોબલી વિન્ડોઝ +Name[he]=חלונות מתנדנדים +Name[hi]=हिलता-डुलता विंडो +Name[hne]=कांपत विंडो +Name[hr]=Klimavi prozori +Name[hu]=Tekergő ablakok +Name[ia]=Fenestras Tremulante +Name[id]=Window Bergoyang +Name[is]=Linir gluggar +Name[it]=Finestre tremolanti +Name[ja]=揺れるウィンドウ +Name[kk]=Майысқақ терезелер +Name[km]=បង្អួច​រំញ័រ +Name[kn]=ಅಲ್ಲಾಡುವ ಕಿಟಕಿಗಳು +Name[ko]=흔들리는 ì°½ +Name[ku]=Paceyên Dihejin +Name[lt]=Svirduliuojantys langai +Name[lv]=Ä»odzÄ«gie logi +Name[mai]=वोबली विंडो +Name[ml]=ചാഞ്ചാടുന്ന ജാലകങ്ങള്‍ +Name[mr]=थरथरणाऱ्या चौकटी +Name[nb]=Vaklende vinduer +Name[nds]=Wabbelig Finstern +Name[nl]=Wiebelende vensters +Name[nn]=Vaklande vindauge +Name[pa]=ਕੰਬਦੀਆਂ ਵਿੰਡੋਜ਼ +Name[pl]=Chwiejne okna +Name[pt]=Janelas Trémulas +Name[pt_BR]=Janelas instáveis +Name[ro]=Ferestre tremurătoare +Name[ru]=Колышущиеся окна +Name[si]=කවුළු සොලවන්න +Name[sk]=Zvlnené okná +Name[sl]=Majava okna +Name[sr]=Лелујави прозори +Name[sr@ijekavian]=Лелујави прозори +Name[sr@ijekavianlatin]=Lelujavi prozori +Name[sr@latin]=Lelujavi prozori +Name[sv]=Ostadiga fönster +Name[ta]=அசைவுடைய சாளரம் +Name[te]=వూబ్లీ విండోస్ +Name[th]=หน้าต่างพริ้วไหว +Name[tr]=Sallanan Pencereler +Name[ug]=تەۋرەنگەن كۆزنەكلەر +Name[uk]=Желейні вікна +Name[wa]=Molès fniesses +Name[x-test]=xxWobbly Windowsxx +Name[zh_CN]=摆动窗口 +Name[zh_TW]=變形視窗 + diff --git a/effects/wobblywindows/wobblywindows_config.h b/effects/wobblywindows/wobblywindows_config.h new file mode 100644 index 0000000..4228d99 --- /dev/null +++ b/effects/wobblywindows/wobblywindows_config.h @@ -0,0 +1,41 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008 Cédric Borgese + SPDX-FileCopyrightText: 2008 Lucas Murray + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_WOBBLYWINDOWS_CONFIG_H +#define KWIN_WOBBLYWINDOWS_CONFIG_H + +#include + +#include "ui_wobblywindows_config.h" + + +namespace KWin +{ + +class WobblyWindowsEffectConfig : public KCModule +{ + Q_OBJECT +public: + explicit WobblyWindowsEffectConfig(QWidget* parent = nullptr, const QVariantList& args = QVariantList()); + ~WobblyWindowsEffectConfig() override; + +public Q_SLOTS: + void save() override; + +private Q_SLOTS: + void wobblinessChanged(); + +private: + ::Ui::WobblyWindowsEffectConfigForm m_ui; +}; + +} // namespace + +#endif diff --git a/effects/wobblywindows/wobblywindows_config.ui b/effects/wobblywindows/wobblywindows_config.ui new file mode 100644 index 0000000..0451021 --- /dev/null +++ b/effects/wobblywindows/wobblywindows_config.ui @@ -0,0 +1,373 @@ + + + WobblyWindowsEffectConfigForm + + + + 0 + 0 + 399 + 229 + + + + + + + false + + + Advanced + + + + + + &Stiffness: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_Stiffness + + + + + + + 1 + + + 50 + + + 15 + + + Qt::Horizontal + + + + + + + 1 + + + 50 + + + 15 + + + + + + + Dra&g: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_Drag + + + + + + + &Move factor: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_MoveFactor + + + + + + + 50 + + + 100 + + + 85 + + + Qt::Horizontal + + + + + + + 50 + + + 100 + + + 85 + + + + + + + 1 + + + 25 + + + 10 + + + Qt::Horizontal + + + + + + + 1 + + + 25 + + + 10 + + + + + + + + + + Wo&bble when moving + + + + + + + Wobble when &resizing + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + Enable &advanced mode + + + + + + + true + + + &Wobbliness + + + false + + + + + + Less + + + + + + + + 120 + 0 + + + + 4 + + + Qt::Horizontal + + + + + + + More + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + + + kcfg_WobblynessLevel + kcfg_MoveWobble + kcfg_ResizeWobble + kcfg_AdvancedMode + kcfg_Stiffness + stiffnessSpin + kcfg_Drag + dragSpin + kcfg_MoveFactor + moveFactorSpin + + + + + kcfg_Stiffness + valueChanged(int) + stiffnessSpin + setValue(int) + + + 304 + 149 + + + 364 + 150 + + + + + stiffnessSpin + valueChanged(int) + kcfg_Stiffness + setValue(int) + + + 364 + 150 + + + 304 + 149 + + + + + kcfg_Drag + valueChanged(int) + dragSpin + setValue(int) + + + 304 + 177 + + + 378 + 180 + + + + + dragSpin + valueChanged(int) + kcfg_Drag + setValue(int) + + + 378 + 180 + + + 304 + 177 + + + + + kcfg_MoveFactor + valueChanged(int) + moveFactorSpin + setValue(int) + + + 304 + 205 + + + 378 + 208 + + + + + moveFactorSpin + valueChanged(int) + kcfg_MoveFactor + setValue(int) + + + 378 + 208 + + + 304 + 205 + + + + + kcfg_AdvancedMode + toggled(bool) + advancedGroup + setEnabled(bool) + + + 249 + 80 + + + 220 + 131 + + + + + diff --git a/effects/wobblywindows/wobblywindowsconfig.kcfgc b/effects/wobblywindows/wobblywindowsconfig.kcfgc new file mode 100644 index 0000000..b678137 --- /dev/null +++ b/effects/wobblywindows/wobblywindowsconfig.kcfgc @@ -0,0 +1,5 @@ +File=wobblywindows.kcfg +ClassName=WobblyWindowsConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/effects/zoom/CMakeLists.txt b/effects/zoom/CMakeLists.txt new file mode 100644 index 0000000..b6b47f2 --- /dev/null +++ b/effects/zoom/CMakeLists.txt @@ -0,0 +1,24 @@ +####################################### +# Config +set(kwin_zoom_config_SRCS zoom_config.cpp) +ki18n_wrap_ui(kwin_zoom_config_SRCS zoom_config.ui) +kconfig_add_kcfg_files(kwin_zoom_config_SRCS zoomconfig.kcfgc) + +add_library(kwin_zoom_config MODULE ${kwin_zoom_config_SRCS}) + +target_link_libraries(kwin_zoom_config + KF5::ConfigWidgets + KF5::GlobalAccel + KF5::I18n + KF5::XmlGui + KWinEffectsInterface +) + +kcoreaddons_desktop_to_json(kwin_zoom_config zoom_config.desktop SERVICE_TYPES kcmodule.desktop) + +install( + TARGETS + kwin_zoom_config + DESTINATION + ${PLUGIN_INSTALL_DIR}/kwin/effects/configs +) diff --git a/effects/zoom/accessibilityintegration.cpp b/effects/zoom/accessibilityintegration.cpp new file mode 100644 index 0000000..140d0fe --- /dev/null +++ b/effects/zoom/accessibilityintegration.cpp @@ -0,0 +1,99 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "accessibilityintegration.h" + +using namespace QAccessibleClient; // Whatever, sue me... + +namespace KWin +{ + +ZoomAccessibilityIntegration::ZoomAccessibilityIntegration(QObject *parent) + : QObject(parent) +{ +} + +void ZoomAccessibilityIntegration::setFocusTrackingEnabled(bool enabled) +{ + if (m_isFocusTrackingEnabled == enabled) { + return; + } + m_isFocusTrackingEnabled = enabled; + updateAccessibilityRegistry(); +} + +bool ZoomAccessibilityIntegration::isFocusTrackingEnabled() const +{ + return m_isFocusTrackingEnabled; +} + +void ZoomAccessibilityIntegration::setTextCaretTrackingEnabled(bool enabled) +{ + if (m_isTextCaretTrackingEnabled == enabled) { + return; + } + m_isTextCaretTrackingEnabled = enabled; + updateAccessibilityRegistry(); +} + +bool ZoomAccessibilityIntegration::isTextCaretTrackingEnabled() const +{ + return m_isTextCaretTrackingEnabled; +} + +void ZoomAccessibilityIntegration::updateAccessibilityRegistry() +{ + Registry::EventListeners eventListeners = Registry::NoEventListeners; + + if (isTextCaretTrackingEnabled()) { + eventListeners |= Registry::TextCaretMoved; + } + if (isFocusTrackingEnabled()) { + eventListeners |= Registry::Focus; + } + + if (eventListeners == Registry::NoEventListeners) { + destroyAccessibilityRegistry(); + return; + } + if (!m_accessibilityRegistry) { + createAccessibilityRegistry(); + } + + m_accessibilityRegistry->subscribeEventListeners(eventListeners); +} + +void ZoomAccessibilityIntegration::createAccessibilityRegistry() +{ + m_accessibilityRegistry = new Registry(this); + + connect(m_accessibilityRegistry, &Registry::textCaretMoved, + this, &ZoomAccessibilityIntegration::slotFocusChanged); + connect(m_accessibilityRegistry, &Registry::focusChanged, + this, &ZoomAccessibilityIntegration::slotFocusChanged); +} + +void ZoomAccessibilityIntegration::destroyAccessibilityRegistry() +{ + if (!m_accessibilityRegistry) { + return; + } + + disconnect(m_accessibilityRegistry, nullptr, this, nullptr); + + m_accessibilityRegistry->deleteLater(); + m_accessibilityRegistry = nullptr; +} + +void ZoomAccessibilityIntegration::slotFocusChanged(const AccessibleObject &object) +{ + emit focusPointChanged(object.focusPoint()); +} + +} // namespace KWin diff --git a/effects/zoom/accessibilityintegration.h b/effects/zoom/accessibilityintegration.h new file mode 100644 index 0000000..ebe50ed --- /dev/null +++ b/effects/zoom/accessibilityintegration.h @@ -0,0 +1,46 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +namespace KWin +{ + +class ZoomAccessibilityIntegration : public QObject +{ + Q_OBJECT + +public: + explicit ZoomAccessibilityIntegration(QObject *parent = nullptr); + + void setFocusTrackingEnabled(bool enabled); + bool isFocusTrackingEnabled() const; + + void setTextCaretTrackingEnabled(bool enabled); + bool isTextCaretTrackingEnabled() const; + +Q_SIGNALS: + void focusPointChanged(const QPoint &point); + +private Q_SLOTS: + void slotFocusChanged(const QAccessibleClient::AccessibleObject &object); + +private: + void createAccessibilityRegistry(); + void destroyAccessibilityRegistry(); + void updateAccessibilityRegistry(); + + QAccessibleClient::Registry *m_accessibilityRegistry = nullptr; + bool m_isFocusTrackingEnabled = false; + bool m_isTextCaretTrackingEnabled = false; +}; + +} // namespace KWin diff --git a/effects/zoom/zoom.cpp b/effects/zoom/zoom.cpp new file mode 100644 index 0000000..7ef0132 --- /dev/null +++ b/effects/zoom/zoom.cpp @@ -0,0 +1,529 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2010 Sebastian Sauer + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "zoom.h" +// KConfigSkeleton +#include "zoomconfig.h" + +#if HAVE_ACCESSIBILITY +#include "accessibilityintegration.h" +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#ifdef KWIN_HAVE_XRENDER_COMPOSITING +#include +#include +#endif + +namespace KWin +{ + +ZoomEffect::ZoomEffect() + : Effect() + , zoom(1) + , target_zoom(1) + , polling(false) + , zoomFactor(1.25) + , mouseTracking(MouseTrackingProportional) + , mousePointer(MousePointerScale) + , focusDelay(350) // in milliseconds + , imageWidth(0) + , imageHeight(0) + , isMouseHidden(false) + , xMove(0) + , yMove(0) + , moveFactor(20.0) +{ + initConfig(); + QAction* a = nullptr; + a = KStandardAction::zoomIn(this, SLOT(zoomIn()), this); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::META + Qt::Key_Equal); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::META + Qt::Key_Equal); + effects->registerGlobalShortcut(Qt::META + Qt::Key_Equal, a); + effects->registerAxisShortcut(Qt::ControlModifier | Qt::MetaModifier, PointerAxisDown, a); + + a = KStandardAction::zoomOut(this, SLOT(zoomOut()), this); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::META + Qt::Key_Minus); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::META + Qt::Key_Minus); + effects->registerGlobalShortcut(Qt::META + Qt::Key_Minus, a); + effects->registerAxisShortcut(Qt::ControlModifier | Qt::MetaModifier, PointerAxisUp, a); + + a = KStandardAction::actualSize(this, SLOT(actualSize()), this); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::META + Qt::Key_0); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::META + Qt::Key_0); + effects->registerGlobalShortcut(Qt::META + Qt::Key_0, a); + + a = new QAction(this); + a->setObjectName(QStringLiteral("MoveZoomLeft")); + a->setText(i18n("Move Zoomed Area to Left")); + KGlobalAccel::self()->setDefaultShortcut(a, QList()); + KGlobalAccel::self()->setShortcut(a, QList()); + effects->registerGlobalShortcut(QKeySequence(), a); + connect(a, &QAction::triggered, this, &ZoomEffect::moveZoomLeft); + + a = new QAction(this); + a->setObjectName(QStringLiteral("MoveZoomRight")); + a->setText(i18n("Move Zoomed Area to Right")); + KGlobalAccel::self()->setDefaultShortcut(a, QList()); + KGlobalAccel::self()->setShortcut(a, QList()); + effects->registerGlobalShortcut(QKeySequence(), a); + connect(a, &QAction::triggered, this, &ZoomEffect::moveZoomRight); + + a = new QAction(this); + a->setObjectName(QStringLiteral("MoveZoomUp")); + a->setText(i18n("Move Zoomed Area Upwards")); + KGlobalAccel::self()->setDefaultShortcut(a, QList()); + KGlobalAccel::self()->setShortcut(a, QList()); + effects->registerGlobalShortcut(QKeySequence(), a); + connect(a, &QAction::triggered, this, &ZoomEffect::moveZoomUp); + + a = new QAction(this); + a->setObjectName(QStringLiteral("MoveZoomDown")); + a->setText(i18n("Move Zoomed Area Downwards")); + KGlobalAccel::self()->setDefaultShortcut(a, QList()); + KGlobalAccel::self()->setShortcut(a, QList()); + effects->registerGlobalShortcut(QKeySequence(), a); + connect(a, &QAction::triggered, this, &ZoomEffect::moveZoomDown); + + // TODO: these two actions don't belong into the effect. They need to be moved into KWin core + a = new QAction(this); + a->setObjectName(QStringLiteral("MoveMouseToFocus")); + a->setText(i18n("Move Mouse to Focus")); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::META + Qt::Key_F5); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::META + Qt::Key_F5); + effects->registerGlobalShortcut(Qt::META + Qt::Key_F5, a); + connect(a, &QAction::triggered, this, &ZoomEffect::moveMouseToFocus); + + a = new QAction(this); + a->setObjectName(QStringLiteral("MoveMouseToCenter")); + a->setText(i18n("Move Mouse to Center")); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::META + Qt::Key_F6); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::META + Qt::Key_F6); + effects->registerGlobalShortcut(Qt::META + Qt::Key_F6, a); + connect(a, &QAction::triggered, this, &ZoomEffect::moveMouseToCenter); + + timeline.setDuration(350); + timeline.setFrameRange(0, 100); + connect(&timeline, &QTimeLine::frameChanged, this, &ZoomEffect::timelineFrameChanged); + connect(effects, &EffectsHandler::mouseChanged, this, &ZoomEffect::slotMouseChanged); + +#if HAVE_ACCESSIBILITY + m_accessibilityIntegration = new ZoomAccessibilityIntegration(this); + connect(m_accessibilityIntegration, &ZoomAccessibilityIntegration::focusPointChanged, this, &ZoomEffect::moveFocus); +#endif + + source_zoom = -1; // used to trigger initialZoom reading + reconfigure(ReconfigureAll); +} + +ZoomEffect::~ZoomEffect() +{ + // switch off and free resources + showCursor(); + // Save the zoom value. + ZoomConfig::setInitialZoom(target_zoom); + ZoomConfig::self()->save(); +} + +bool ZoomEffect::isFocusTrackingEnabled() const +{ +#if HAVE_ACCESSIBILITY + return m_accessibilityIntegration->isFocusTrackingEnabled(); +#else + return false; +#endif +} + +bool ZoomEffect::isTextCaretTrackingEnabled() const +{ +#if HAVE_ACCESSIBILITY + return m_accessibilityIntegration->isTextCaretTrackingEnabled(); +#else + return false; +#endif +} + +void ZoomEffect::showCursor() +{ + if (isMouseHidden) { + disconnect(effects, &EffectsHandler::cursorShapeChanged, this, &ZoomEffect::recreateTexture); + // show the previously hidden mouse-pointer again and free the loaded texture/picture. + effects->showCursor(); + texture.reset(); +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + xrenderPicture.reset(); +#endif + isMouseHidden = false; + } +} + +void ZoomEffect::hideCursor() +{ + if (mouseTracking == MouseTrackingProportional && mousePointer == MousePointerKeep) + return; // don't replace the actual cursor by a static image for no reason. + if (!isMouseHidden) { + // try to load the cursor-theme into a OpenGL texture and if successful then hide the mouse-pointer + recreateTexture(); + bool shouldHide = false; + if (effects->isOpenGLCompositing()) { + shouldHide = !texture.isNull(); + } else if (effects->compositingType() == XRenderCompositing) { +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + shouldHide = !xrenderPicture.isNull(); +#endif + } + if (shouldHide) { + effects->hideCursor(); + connect(effects, &EffectsHandler::cursorShapeChanged, this, &ZoomEffect::recreateTexture); + isMouseHidden = true; + } + } +} + +void ZoomEffect::recreateTexture() +{ + effects->makeOpenGLContextCurrent(); + const auto cursor = effects->cursorImage(); + if (!cursor.image().isNull()) { + imageWidth = cursor.image().width(); + imageHeight = cursor.image().height(); + cursorHotSpot = cursor.hotSpot(); + if (effects->isOpenGLCompositing()) { + texture.reset(new GLTexture(cursor.image())); + texture->setWrapMode(GL_CLAMP_TO_EDGE); + } +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + if (effects->compositingType() == XRenderCompositing) + xrenderPicture.reset(new XRenderPicture(cursor.image())); +#endif + } + else { + qCDebug(KWINEFFECTS) << "Falling back to proportional mouse tracking!"; + mouseTracking = MouseTrackingProportional; + } +} + +void ZoomEffect::reconfigure(ReconfigureFlags) +{ + ZoomConfig::self()->read(); + // On zoom-in and zoom-out change the zoom by the defined zoom-factor. + zoomFactor = qMax(0.1, ZoomConfig::zoomFactor()); + // Visibility of the mouse-pointer. + mousePointer = MousePointerType(ZoomConfig::mousePointer()); + // Track moving of the mouse. + mouseTracking = MouseTrackingType(ZoomConfig::mouseTracking()); +#if HAVE_ACCESSIBILITY + // Enable tracking of the focused location. + m_accessibilityIntegration->setFocusTrackingEnabled(ZoomConfig::enableFocusTracking()); + // Enable tracking of the text caret. + m_accessibilityIntegration->setTextCaretTrackingEnabled(ZoomConfig::enableTextCaretTracking()); +#endif + // The time in milliseconds to wait before a focus-event takes away a mouse-move. + focusDelay = qMax(uint(0), ZoomConfig::focusDelay()); + // The factor the zoom-area will be moved on touching an edge on push-mode or using the navigation KAction's. + moveFactor = qMax(0.1, ZoomConfig::moveFactor()); + if (source_zoom < 0) { + // Load the saved zoom value. + source_zoom = 1.0; + target_zoom = ZoomConfig::initialZoom(); + if (target_zoom > 1.0) + zoomIn(target_zoom); + } else { + source_zoom = 1.0; + } +} + +void ZoomEffect::prePaintScreen(ScreenPrePaintData& data, int time) +{ + if (zoom != target_zoom) { + const float zoomDist = qAbs(target_zoom - source_zoom); + if (target_zoom > zoom) + zoom = qMin(zoom + ((zoomDist * time) / animationTime(150*zoomFactor)), target_zoom); + else + zoom = qMax(zoom - ((zoomDist * time) / animationTime(150*zoomFactor)), target_zoom); + } + + if (zoom == 1.0) { + showCursor(); + } else { + hideCursor(); + data.mask |= PAINT_SCREEN_TRANSFORMED; + } + + effects->prePaintScreen(data, time); +} + +void ZoomEffect::paintScreen(int mask, const QRegion ®ion, ScreenPaintData& data) +{ + if (zoom != 1.0) { + data *= QVector2D(zoom, zoom); + const QSize screenSize = effects->virtualScreenSize(); + + // mouse-tracking allows navigation of the zoom-area using the mouse. + switch(mouseTracking) { + case MouseTrackingProportional: + data.setXTranslation(- int(cursorPoint.x() * (zoom - 1.0))); + data.setYTranslation(- int(cursorPoint.y() * (zoom - 1.0))); + prevPoint = cursorPoint; + break; + case MouseTrackingCentred: + prevPoint = cursorPoint; + // fall through + case MouseTrackingDisabled: + data.setXTranslation(qMin(0, qMax(int(screenSize.width() - screenSize.width() * zoom), int(screenSize.width() / 2 - prevPoint.x() * zoom)))); + data.setYTranslation(qMin(0, qMax(int(screenSize.height() - screenSize.height() * zoom), int(screenSize.height() / 2 - prevPoint.y() * zoom)))); + break; + case MouseTrackingPush: { + // touching an edge of the screen moves the zoom-area in that direction. + int x = cursorPoint.x() * zoom - prevPoint.x() * (zoom - 1.0); + int y = cursorPoint.y() * zoom - prevPoint.y() * (zoom - 1.0); + int threshold = 4; + xMove = yMove = 0; + if (x < threshold) + xMove = (x - threshold) / zoom; + else if (x + threshold > screenSize.width()) + xMove = (x + threshold - screenSize.width()) / zoom; + if (y < threshold) + yMove = (y - threshold) / zoom; + else if (y + threshold > screenSize.height()) + yMove = (y + threshold - screenSize.height()) / zoom; + if (xMove) + prevPoint.setX(qMax(0, qMin(screenSize.width(), prevPoint.x() + xMove))); + if (yMove) + prevPoint.setY(qMax(0, qMin(screenSize.height(), prevPoint.y() + yMove))); + data.setXTranslation(- int(prevPoint.x() * (zoom - 1.0))); + data.setYTranslation(- int(prevPoint.y() * (zoom - 1.0))); + break; + } + } + + // use the focusPoint if focus tracking is enabled + if (isFocusTrackingEnabled() || isTextCaretTrackingEnabled()) { + bool acceptFocus = true; + if (mouseTracking != MouseTrackingDisabled && focusDelay > 0) { + // Wait some time for the mouse before doing the switch. This serves as threshold + // to prevent the focus from jumping around to much while working with the mouse. + const int msecs = lastMouseEvent.msecsTo(lastFocusEvent); + acceptFocus = msecs > focusDelay; + } + if (acceptFocus) { + data.setXTranslation(- int(focusPoint.x() * (zoom - 1.0))); + data.setYTranslation(- int(focusPoint.y() * (zoom - 1.0))); + prevPoint = focusPoint; + } + } + } + + effects->paintScreen(mask, region, data); + + if (zoom != 1.0 && mousePointer != MousePointerHide) { + // Draw the mouse-texture at the position matching to zoomed-in image of the desktop. Hiding the + // previous mouse-cursor and drawing our own fake mouse-cursor is needed to be able to scale the + // mouse-cursor up and to re-position those mouse-cursor to match to the chosen zoom-level. + int w = imageWidth; + int h = imageHeight; + if (mousePointer == MousePointerScale) { + w *= zoom; + h *= zoom; + } + const QPoint p = effects->cursorPos() - cursorHotSpot; + QRect rect(p.x() * zoom + data.xTranslation(), p.y() * zoom + data.yTranslation(), w, h); + + if (texture) { + texture->bind(); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + auto s = ShaderManager::instance()->pushShader(ShaderTrait::MapTexture); + QMatrix4x4 mvp = data.projectionMatrix(); + mvp.translate(rect.x(), rect.y()); + s->setUniform(GLShader::ModelViewProjectionMatrix, mvp); + texture->render(region, rect); + ShaderManager::instance()->popShader(); + texture->unbind(); + glDisable(GL_BLEND); + } +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + if (xrenderPicture) { +#define DOUBLE_TO_FIXED(d) ((xcb_render_fixed_t) ((d) * 65536)) + static const xcb_render_transform_t xrenderIdentity = { + DOUBLE_TO_FIXED(1), DOUBLE_TO_FIXED(0), DOUBLE_TO_FIXED(0), + DOUBLE_TO_FIXED(0), DOUBLE_TO_FIXED(1), DOUBLE_TO_FIXED(0), + DOUBLE_TO_FIXED(0), DOUBLE_TO_FIXED(0), DOUBLE_TO_FIXED(1) + }; + if (mousePointer == MousePointerScale) { + xcb_render_set_picture_filter(xcbConnection(), *xrenderPicture, 4, const_cast("good"), 0, nullptr); + const xcb_render_transform_t xform = { + DOUBLE_TO_FIXED(1.0 / zoom), DOUBLE_TO_FIXED(0), DOUBLE_TO_FIXED(0), + DOUBLE_TO_FIXED(0), DOUBLE_TO_FIXED(1.0 / zoom), DOUBLE_TO_FIXED(0), + DOUBLE_TO_FIXED(0), DOUBLE_TO_FIXED(0), DOUBLE_TO_FIXED(1) + }; + xcb_render_set_picture_transform(xcbConnection(), *xrenderPicture, xform); + } + xcb_render_composite(xcbConnection(), XCB_RENDER_PICT_OP_OVER, *xrenderPicture, XCB_RENDER_PICTURE_NONE, + effects->xrenderBufferPicture(), 0, 0, 0, 0, rect.x(), rect.y(), rect.width(), rect.height()); + if (mousePointer == MousePointerScale) + xcb_render_set_picture_transform(xcbConnection(), *xrenderPicture, xrenderIdentity); +#undef DOUBLE_TO_FIXED + } +#endif + } +} + +void ZoomEffect::postPaintScreen() +{ + if (zoom != target_zoom) + effects->addRepaintFull(); + effects->postPaintScreen(); +} + +void ZoomEffect::zoomIn(double to) +{ + source_zoom = zoom; + if (to < 0.0) + target_zoom *= zoomFactor; + else + target_zoom = to; + if (!polling) { + polling = true; + effects->startMousePolling(); + } + cursorPoint = effects->cursorPos(); + if (mouseTracking == MouseTrackingDisabled) + prevPoint = cursorPoint; + effects->addRepaintFull(); +} + +void ZoomEffect::zoomOut() +{ + source_zoom = zoom; + target_zoom /= zoomFactor; + if ((zoomFactor > 1 && target_zoom < 1.01) || (zoomFactor < 1 && target_zoom > 0.99)) { + target_zoom = 1; + if (polling) { + polling = false; + effects->stopMousePolling(); + } + } + if (mouseTracking == MouseTrackingDisabled) + prevPoint = effects->cursorPos(); + effects->addRepaintFull(); +} + +void ZoomEffect::actualSize() +{ + source_zoom = zoom; + target_zoom = 1; + if (polling) { + polling = false; + effects->stopMousePolling(); + } + effects->addRepaintFull(); +} + +void ZoomEffect::timelineFrameChanged(int /* frame */) +{ + const QSize screenSize = effects->virtualScreenSize(); + prevPoint.setX(qMax(0, qMin(screenSize.width(), prevPoint.x() + xMove))); + prevPoint.setY(qMax(0, qMin(screenSize.height(), prevPoint.y() + yMove))); + cursorPoint = prevPoint; + effects->addRepaintFull(); +} + +void ZoomEffect::moveZoom(int x, int y) +{ + if (timeline.state() == QTimeLine::Running) + timeline.stop(); + + const QSize screenSize = effects->virtualScreenSize(); + if (x < 0) + xMove = - qMax(1.0, screenSize.width() / zoom / moveFactor); + else if (x > 0) + xMove = qMax(1.0, screenSize.width() / zoom / moveFactor); + else + xMove = 0; + + if (y < 0) + yMove = - qMax(1.0, screenSize.height() / zoom / moveFactor); + else if (y > 0) + yMove = qMax(1.0, screenSize.height() / zoom / moveFactor); + else + yMove = 0; + + timeline.start(); +} + +void ZoomEffect::moveZoomLeft() +{ + moveZoom(-1, 0); +} + +void ZoomEffect::moveZoomRight() +{ + moveZoom(1, 0); +} + +void ZoomEffect::moveZoomUp() +{ + moveZoom(0, -1); +} + +void ZoomEffect::moveZoomDown() +{ + moveZoom(0, 1); +} + +void ZoomEffect::moveMouseToFocus() +{ + QCursor::setPos(focusPoint.x(), focusPoint.y()); +} + +void ZoomEffect::moveMouseToCenter() +{ + const QRect r = effects->virtualScreenGeometry(); + QCursor::setPos(r.x() + r.width() / 2, r.y() + r.height() / 2); +} + +void ZoomEffect::slotMouseChanged(const QPoint& pos, const QPoint& old, Qt::MouseButtons, + Qt::MouseButtons, Qt::KeyboardModifiers, Qt::KeyboardModifiers) +{ + if (zoom == 1.0) + return; + cursorPoint = pos; + if (pos != old) { + lastMouseEvent = QTime::currentTime(); + effects->addRepaintFull(); + } +} + +void ZoomEffect::moveFocus(const QPoint &point) +{ + if (zoom == 1.0) + return; + focusPoint = point; + lastFocusEvent = QTime::currentTime(); + effects->addRepaintFull(); +} + +bool ZoomEffect::isActive() const +{ + return zoom != 1.0 || zoom != target_zoom; +} + +} // namespace + diff --git a/effects/zoom/zoom.h b/effects/zoom/zoom.h new file mode 100644 index 0000000..da754a9 --- /dev/null +++ b/effects/zoom/zoom.h @@ -0,0 +1,126 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2010 Sebastian Sauer + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_ZOOM_H +#define KWIN_ZOOM_H + +#include + +#include +#include +#include + +namespace KWin +{ + +#if HAVE_ACCESSIBILITY +class ZoomAccessibilityIntegration; +#endif + +class GLTexture; +class XRenderPicture; + +class ZoomEffect + : public Effect +{ + Q_OBJECT + Q_PROPERTY(qreal zoomFactor READ configuredZoomFactor) + Q_PROPERTY(int mousePointer READ configuredMousePointer) + Q_PROPERTY(int mouseTracking READ configuredMouseTracking) + Q_PROPERTY(bool focusTrackingEnabled READ isFocusTrackingEnabled) + Q_PROPERTY(bool textCaretTrackingEnabled READ isTextCaretTrackingEnabled) + Q_PROPERTY(int focusDelay READ configuredFocusDelay) + Q_PROPERTY(qreal moveFactor READ configuredMoveFactor) + Q_PROPERTY(qreal targetZoom READ targetZoom) +public: + ZoomEffect(); + ~ZoomEffect() override; + void reconfigure(ReconfigureFlags flags) override; + void prePaintScreen(ScreenPrePaintData& data, int time) override; + void paintScreen(int mask, const QRegion ®ion, ScreenPaintData& data) override; + void postPaintScreen() override; + bool isActive() const override; + // for properties + qreal configuredZoomFactor() const { + return zoomFactor; + } + int configuredMousePointer() const { + return mousePointer; + } + int configuredMouseTracking() const { + return mouseTracking; + } + bool isFocusTrackingEnabled() const; + bool isTextCaretTrackingEnabled() const; + int configuredFocusDelay() const { + return focusDelay; + } + qreal configuredMoveFactor() const { + return moveFactor; + } + qreal targetZoom() const { + return target_zoom; + } +private Q_SLOTS: + inline void zoomIn() { zoomIn(-1.0); }; + void zoomIn(double to); + void zoomOut(); + void actualSize(); + void moveZoomLeft(); + void moveZoomRight(); + void moveZoomUp(); + void moveZoomDown(); + void moveMouseToFocus(); + void moveMouseToCenter(); + void timelineFrameChanged(int frame); + void moveFocus(const QPoint &point); + void slotMouseChanged(const QPoint& pos, const QPoint& old, + Qt::MouseButtons buttons, Qt::MouseButtons oldbuttons, + Qt::KeyboardModifiers modifiers, Qt::KeyboardModifiers oldmodifiers); + void recreateTexture(); +private: + void showCursor(); + void hideCursor(); + void moveZoom(int x, int y); +private: +#if HAVE_ACCESSIBILITY + ZoomAccessibilityIntegration *m_accessibilityIntegration = nullptr; +#endif + double zoom; + double target_zoom; + double source_zoom; + bool polling; // Mouse polling + double zoomFactor; + enum MouseTrackingType { MouseTrackingProportional = 0, MouseTrackingCentred = 1, MouseTrackingPush = 2, MouseTrackingDisabled = 3 }; + MouseTrackingType mouseTracking; + enum MousePointerType { MousePointerScale = 0, MousePointerKeep = 1, MousePointerHide = 2 }; + MousePointerType mousePointer; + int focusDelay; + QPoint cursorPoint; + QPoint cursorHotSpot; + QPoint focusPoint; + QPoint prevPoint; + QTime lastMouseEvent; + QTime lastFocusEvent; + QScopedPointer texture; +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + QScopedPointer xrenderPicture; +#endif + int imageWidth; + int imageHeight; + bool isMouseHidden; + QTimeLine timeline; + int xMove, yMove; + double moveFactor; +}; + +} // namespace + +#endif diff --git a/effects/zoom/zoom.kcfg b/effects/zoom/zoom.kcfg new file mode 100644 index 0000000..97a022d --- /dev/null +++ b/effects/zoom/zoom.kcfg @@ -0,0 +1,33 @@ + + + + + + 1.2 + + + 0 + + + 0 + + + false + + + false + + + 350 + + + 20.0 + + + 1.0 + + + diff --git a/effects/zoom/zoom_config.cpp b/effects/zoom/zoom_config.cpp new file mode 100644 index 0000000..59f83f1 --- /dev/null +++ b/effects/zoom/zoom_config.cpp @@ -0,0 +1,144 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Rivo Laks + SPDX-FileCopyrightText: 2010 Sebastian Sauer + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "zoom_config.h" +// KConfigSkeleton +#include "zoomconfig.h" +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include + +K_PLUGIN_FACTORY_WITH_JSON(ZoomEffectConfigFactory, + "zoom_config.json", + registerPlugin();) + +namespace KWin +{ + +ZoomEffectConfigForm::ZoomEffectConfigForm(QWidget* parent) : QWidget(parent) +{ + setupUi(this); +} + +ZoomEffectConfig::ZoomEffectConfig(QWidget* parent, const QVariantList& args) : + KCModule(KAboutData::pluginData(QStringLiteral("zoom")), parent, args) +{ + ZoomConfig::instance(KWIN_CONFIG); + m_ui = new ZoomEffectConfigForm(this); + + QVBoxLayout* layout = new QVBoxLayout(this); + layout->addWidget(m_ui); + + addConfig(ZoomConfig::self(), m_ui); + + connect(m_ui->editor, &KShortcutsEditor::keyChange, this, &ZoomEffectConfig::markAsChanged); + +#if !HAVE_ACCESSIBILITY + m_ui->kcfg_EnableFocusTracking->setVisible(false); + m_ui->kcfg_EnableTextCaretTracking->setVisible(false); +#endif + + // Shortcut config. The shortcut belongs to the component "kwin"! + KActionCollection *actionCollection = new KActionCollection(this, QStringLiteral("kwin")); + actionCollection->setComponentDisplayName(i18n("KWin")); + actionCollection->setConfigGroup(QStringLiteral("Zoom")); + actionCollection->setConfigGlobal(true); + + QAction* a; + a = actionCollection->addAction(KStandardAction::ZoomIn); + a->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::META + Qt::Key_Equal); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::META + Qt::Key_Equal); + + a = actionCollection->addAction(KStandardAction::ZoomOut); + a->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::META + Qt::Key_Minus); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::META + Qt::Key_Minus); + + a = actionCollection->addAction(KStandardAction::ActualSize); + a->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::META + Qt::Key_0); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::META + Qt::Key_0); + + a = actionCollection->addAction(QStringLiteral("MoveZoomLeft")); + a->setIcon(QIcon::fromTheme(QStringLiteral("go-previous"))); + a->setText(i18n("Move Left")); + a->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::META + Qt::CTRL + Qt::Key_Left); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::META + Qt::CTRL + Qt::Key_Left); + + a = actionCollection->addAction(QStringLiteral("MoveZoomRight")); + a->setIcon(QIcon::fromTheme(QStringLiteral("go-next"))); + a->setText(i18n("Move Right")); + a->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::META + Qt::CTRL + Qt::Key_Right); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::META + Qt::CTRL + Qt::Key_Right); + + a = actionCollection->addAction(QStringLiteral("MoveZoomUp")); + a->setIcon(QIcon::fromTheme(QStringLiteral("go-up"))); + a->setText(i18n("Move Up")); + a->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::META + Qt::CTRL + Qt::Key_Up); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::META + Qt::CTRL + Qt::Key_Up); + + a = actionCollection->addAction(QStringLiteral("MoveZoomDown")); + a->setIcon(QIcon::fromTheme(QStringLiteral("go-down"))); + a->setText(i18n("Move Down")); + a->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::META + Qt::CTRL + Qt::Key_Down); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::META + Qt::CTRL + Qt::Key_Down); + + a = actionCollection->addAction(QStringLiteral("MoveMouseToFocus")); + a->setIcon(QIcon::fromTheme(QStringLiteral("view-restore"))); + a->setText(i18n("Move Mouse to Focus")); + a->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::META + Qt::Key_F5); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::META + Qt::Key_F5); + + a = actionCollection->addAction(QStringLiteral("MoveMouseToCenter")); + a->setIcon(QIcon::fromTheme(QStringLiteral("view-restore"))); + a->setText(i18n("Move Mouse to Center")); + a->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << Qt::META + Qt::Key_F6); + KGlobalAccel::self()->setShortcut(a, QList() << Qt::META + Qt::Key_F6); + + m_ui->editor->addCollection(actionCollection); + + load(); +} + +ZoomEffectConfig::~ZoomEffectConfig() +{ + // Undo (only) unsaved changes to global key shortcuts + m_ui->editor->undoChanges(); +} + +void ZoomEffectConfig::save() +{ + m_ui->editor->save(); // undo() will restore to this state from now on + KCModule::save(); + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("zoom")); +} + +} // namespace + +#include "zoom_config.moc" diff --git a/effects/zoom/zoom_config.desktop b/effects/zoom/zoom_config.desktop new file mode 100644 index 0000000..7f2bf62 --- /dev/null +++ b/effects/zoom/zoom_config.desktop @@ -0,0 +1,93 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=KCModule + +X-KDE-Library=kwin_zoom_config +X-KDE-ParentComponents=zoom + +Name=Zoom +Name[af]=Zoem +Name[ar]=تكبير +Name[as]=ডাঙৰকৈ প্ৰদৰ্শন +Name[ast]=Zoom +Name[az]=Miqyas +Name[be]=Маштабаванне +Name[be@latin]=MaÅ¡tab +Name[bg]=Мащабиране +Name[bn_IN]=বড় করে প্রদর্শন +Name[br]=Zoom +Name[bs]=Uveličanje +Name[ca]=Zoom +Name[ca@valencia]=Zoom +Name[cs]=ZvětÅ¡ení +Name[csb]=Zwikasznié +Name[da]=Zoom +Name[de]=Arbeitsflächen-Vergrößerung +Name[el]=Εστίαση +Name[en_GB]=Zoom +Name[eo]=Zomi +Name[es]=Ampliación +Name[et]=Suurendus +Name[eu]=Handiagotu +Name[fa]=بزرگ‌نمایی +Name[fi]=Zoomaus +Name[fr]=Zoom +Name[fy]=Zoome +Name[ga]=Súmáil +Name[gl]=Ampliación +Name[gu]=મોટું કરો +Name[he]=מגדיל +Name[hi]=ज़ूम +Name[hne]=जूम +Name[hr]=Povećanje +Name[hu]=Kinagyítás +Name[ia]=Zoom +Name[id]=Zoom +Name[is]=Aðdráttur +Name[it]=Ingrandimento +Name[ja]=ズーム +Name[kk]=Ұлғайту +Name[km]=ពង្រីក +Name[kn]=ಹಿಗ್ಗಿಸು +Name[ko]=확대/축소 +Name[ku]=Mezinkirin +Name[lt]=Didinimas +Name[lv]=Palielināšana +Name[mai]=जूम +Name[mk]=Зум +Name[ml]=വലുതാക്കുക +Name[mr]=झूम +Name[nb]=Skalering +Name[nds]=Ansichtgrött +Name[ne]=जुम गर्नुहोस् +Name[nl]=Zoomen +Name[nn]=Forstørr skrivebordet +Name[oc]=Zoom +Name[pa]=ਜ਼ੂਮ +Name[pl]=Powiększanie +Name[pt]=Ampliação +Name[pt_BR]=Zoom +Name[ro]=Apropiere +Name[ru]=Масштаб +Name[se]=Stuorrudit +Name[si]=විශාලණය +Name[sk]=Lupa +Name[sl]=Približanje +Name[sr]=Увеличање +Name[sr@ijekavian]=Увеличање +Name[sr@ijekavianlatin]=Uveličanje +Name[sr@latin]=Uveličanje +Name[sv]=Zoom +Name[ta]=Zoom +Name[te]=జూమ్ +Name[th]=ดูย่อ/ดูขยาย +Name[tr]=Büyüt +Name[ug]=كېڭەيت-تارايت +Name[uk]=Масштабування +Name[uz]=Kattalashtirish +Name[uz@cyrillic]=Катталаштириш +Name[vi]=Thu/Phóng +Name[wa]=Zoum +Name[x-test]=xxZoomxx +Name[zh_CN]=缩放 +Name[zh_TW]=縮放 diff --git a/effects/zoom/zoom_config.h b/effects/zoom/zoom_config.h new file mode 100644 index 0000000..a51b529 --- /dev/null +++ b/effects/zoom/zoom_config.h @@ -0,0 +1,46 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Rivo Laks + SPDX-FileCopyrightText: 2010 Sebastian Sauer + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_ZOOM_CONFIG_H +#define KWIN_ZOOM_CONFIG_H + +#include + +#include "ui_zoom_config.h" + + +namespace KWin +{ + +class ZoomEffectConfigForm : public QWidget, public Ui::ZoomEffectConfigForm +{ + Q_OBJECT +public: + explicit ZoomEffectConfigForm(QWidget* parent = nullptr); +}; + +class ZoomEffectConfig : public KCModule +{ + Q_OBJECT +public: + explicit ZoomEffectConfig(QWidget* parent = nullptr, const QVariantList& args = QVariantList()); + ~ZoomEffectConfig() override; + +public Q_SLOTS: + void save() override; + +private: + ZoomEffectConfigForm* m_ui; + enum MouseTracking { MouseCentred = 0, MouseProportional = 1, MouseDisabled = 2 }; +}; + +} // namespace + +#endif diff --git a/effects/zoom/zoom_config.ui b/effects/zoom/zoom_config.ui new file mode 100644 index 0000000..df941a7 --- /dev/null +++ b/effects/zoom/zoom_config.ui @@ -0,0 +1,192 @@ + + + KWin::ZoomEffectConfigForm + + + + 0 + 0 + 304 + 288 + + + + + + + + + + + + + + + On zoom-in and zoom-out change the zoom by the defined zoom-factor. + + + Zoom Factor: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_ZoomFactor + + + + + + + On zoom-in and zoom-out change the zoom by the defined zoom-factor. + + + + + + 2 + + + 9999.000000000000000 + + + 0.050000000000000 + + + 1.250000000000000 + + + + + + + + + + Enable tracking of the focused location. This needs QAccessible to be enabled per application ("export QT_LINUX_ACCESSIBILITY_ALWAYS_ON=1"). + + + Enable Focus Tracking + + + + + + + Enable tracking of the text cursor. This needs QAccessible to be enabled per application ("export QT_LINUX_ACCESSIBILITY_ALWAYS_ON=1"). + + + Enable Text Cursor Tracking + + + + + + + Mouse Pointer: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_MousePointer + + + + + + + Visibility of the mouse-pointer. + + + + Scale + + + + + Keep + + + + + Hide + + + + + + + + Track moving of the mouse. + + + + Proportional + + + + + Centered + + + + + Push + + + + + Disabled + + + + + + + + Mouse Tracking: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_MouseTracking + + + + + + + + + + + + + 0 + 0 + + + + + + + + + KShortcutsEditor + QWidget +
kshortcutseditor.h
+ 1 +
+
+ + kcfg_ZoomFactor + kcfg_MousePointer + kcfg_MouseTracking + kcfg_EnableFocusTracking + kcfg_EnableTextCaretTracking + + + +
diff --git a/effects/zoom/zoomconfig.kcfgc b/effects/zoom/zoomconfig.kcfgc new file mode 100644 index 0000000..9979583 --- /dev/null +++ b/effects/zoom/zoomconfig.kcfgc @@ -0,0 +1,5 @@ +File=zoom.kcfg +ClassName=ZoomConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/egl_context_attribute_builder.cpp b/egl_context_attribute_builder.cpp new file mode 100644 index 0000000..a099c59 --- /dev/null +++ b/egl_context_attribute_builder.cpp @@ -0,0 +1,71 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "egl_context_attribute_builder.h" +#include + +namespace KWin +{ +std::vector EglContextAttributeBuilder::build() const +{ + std::vector attribs; + if (isVersionRequested()) { + attribs.emplace_back(EGL_CONTEXT_MAJOR_VERSION_KHR); + attribs.emplace_back(majorVersion()); + attribs.emplace_back(EGL_CONTEXT_MINOR_VERSION_KHR); + attribs.emplace_back(minorVersion()); + } + int contextFlags = 0; + if (isRobust()) { + attribs.emplace_back(EGL_CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY_KHR); + attribs.emplace_back(EGL_LOSE_CONTEXT_ON_RESET_KHR); + contextFlags |= EGL_CONTEXT_OPENGL_ROBUST_ACCESS_BIT_KHR; + } + if (isForwardCompatible()) { + contextFlags |= EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE_BIT_KHR; + } + if (contextFlags != 0) { + attribs.emplace_back(EGL_CONTEXT_FLAGS_KHR); + attribs.emplace_back(contextFlags); + } + if (isCoreProfile() || isCompatibilityProfile()) { + attribs.emplace_back(EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR); + if (isCoreProfile()) { + attribs.emplace_back(EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT_KHR); + } else if (isCompatibilityProfile()) { + attribs.emplace_back(EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT_KHR); + } + } + if (isHighPriority()) { + attribs.emplace_back(EGL_CONTEXT_PRIORITY_LEVEL_IMG); + attribs.emplace_back(EGL_CONTEXT_PRIORITY_HIGH_IMG); + } + attribs.emplace_back(EGL_NONE); + return attribs; +} + +std::vector EglOpenGLESContextAttributeBuilder::build() const +{ + std::vector attribs; + attribs.emplace_back(EGL_CONTEXT_CLIENT_VERSION); + attribs.emplace_back(majorVersion()); + if (isRobust()) { + attribs.emplace_back(EGL_CONTEXT_OPENGL_ROBUST_ACCESS_EXT); + attribs.emplace_back(EGL_TRUE); + attribs.emplace_back(EGL_CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY_EXT); + attribs.emplace_back(EGL_LOSE_CONTEXT_ON_RESET_EXT); + } + if (isHighPriority()) { + attribs.emplace_back(EGL_CONTEXT_PRIORITY_LEVEL_IMG); + attribs.emplace_back(EGL_CONTEXT_PRIORITY_HIGH_IMG); + } + attribs.emplace_back(EGL_NONE); + return attribs; +} + +} diff --git a/egl_context_attribute_builder.h b/egl_context_attribute_builder.h new file mode 100644 index 0000000..15c8ee5 --- /dev/null +++ b/egl_context_attribute_builder.h @@ -0,0 +1,28 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include "abstract_opengl_context_attribute_builder.h" +#include + +namespace KWin +{ + +class KWIN_EXPORT EglContextAttributeBuilder : public AbstractOpenGLContextAttributeBuilder +{ +public: + std::vector build() const override; +}; + +class KWIN_EXPORT EglOpenGLESContextAttributeBuilder : public AbstractOpenGLContextAttributeBuilder +{ +public: + std::vector build() const override; +}; + +} diff --git a/events.cpp b/events.cpp new file mode 100644 index 0000000..2e8885d --- /dev/null +++ b/events.cpp @@ -0,0 +1,1403 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +/* + + This file contains things relevant to handling incoming events. + +*/ + +#include "x11client.h" +#include "cursor.h" +#include "focuschain.h" +#include "netinfo.h" +#include "workspace.h" +#include "atoms.h" +#ifdef KWIN_BUILD_TABBOX +#include "tabbox.h" +#endif +#include "group.h" +#include "rules.h" +#include "unmanaged.h" +#include "useractions.h" +#include "effects.h" +#include "screens.h" +#include "xcbutils.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#ifdef XCB_ICCCM_FOUND +#include +#endif + +#include "composite.h" +#include "x11eventfilter.h" + +#include "wayland_server.h" +#include + +#ifndef XCB_GE_GENERIC +#define XCB_GE_GENERIC 35 +typedef struct xcb_ge_generic_event_t { + uint8_t response_type; /**< */ + uint8_t extension; /**< */ + uint16_t sequence; /**< */ + uint32_t length; /**< */ + uint16_t event_type; /**< */ + uint8_t pad0[22]; /**< */ + uint32_t full_sequence; /**< */ +} xcb_ge_generic_event_t; +#endif + +namespace KWin +{ + +// **************************************** +// Workspace +// **************************************** + +static xcb_window_t findEventWindow(xcb_generic_event_t *event) +{ + const uint8_t eventType = event->response_type & ~0x80; + switch(eventType) { + case XCB_KEY_PRESS: + case XCB_KEY_RELEASE: + return reinterpret_cast(event)->event; + case XCB_BUTTON_PRESS: + case XCB_BUTTON_RELEASE: + return reinterpret_cast(event)->event; + case XCB_MOTION_NOTIFY: + return reinterpret_cast(event)->event; + case XCB_ENTER_NOTIFY: + case XCB_LEAVE_NOTIFY: + return reinterpret_cast(event)->event; + case XCB_FOCUS_IN: + case XCB_FOCUS_OUT: + return reinterpret_cast(event)->event; + case XCB_EXPOSE: + return reinterpret_cast(event)->window; + case XCB_GRAPHICS_EXPOSURE: + return reinterpret_cast(event)->drawable; + case XCB_NO_EXPOSURE: + return reinterpret_cast(event)->drawable; + case XCB_VISIBILITY_NOTIFY: + return reinterpret_cast(event)->window; + case XCB_CREATE_NOTIFY: + return reinterpret_cast(event)->window; + case XCB_DESTROY_NOTIFY: + return reinterpret_cast(event)->window; + case XCB_UNMAP_NOTIFY: + return reinterpret_cast(event)->window; + case XCB_MAP_NOTIFY: + return reinterpret_cast(event)->window; + case XCB_MAP_REQUEST: + return reinterpret_cast(event)->window; + case XCB_REPARENT_NOTIFY: + return reinterpret_cast(event)->window; + case XCB_CONFIGURE_NOTIFY: + return reinterpret_cast(event)->window; + case XCB_CONFIGURE_REQUEST: + return reinterpret_cast(event)->window; + case XCB_GRAVITY_NOTIFY: + return reinterpret_cast(event)->window; + case XCB_RESIZE_REQUEST: + return reinterpret_cast(event)->window; + case XCB_CIRCULATE_NOTIFY: + case XCB_CIRCULATE_REQUEST: + return reinterpret_cast(event)->window; + case XCB_PROPERTY_NOTIFY: + return reinterpret_cast(event)->window; + case XCB_COLORMAP_NOTIFY: + return reinterpret_cast(event)->window; + case XCB_CLIENT_MESSAGE: + return reinterpret_cast(event)->window; + default: + // extension handling + if (eventType == Xcb::Extensions::self()->shapeNotifyEvent()) { + return reinterpret_cast(event)->affected_window; + } + if (eventType == Xcb::Extensions::self()->damageNotifyEvent()) { + return reinterpret_cast(event)->drawable; + } + return XCB_WINDOW_NONE; + } +} + +QVector s_xcbEerrors({ + QByteArrayLiteral("Success"), + QByteArrayLiteral("BadRequest"), + QByteArrayLiteral("BadValue"), + QByteArrayLiteral("BadWindow"), + QByteArrayLiteral("BadPixmap"), + QByteArrayLiteral("BadAtom"), + QByteArrayLiteral("BadCursor"), + QByteArrayLiteral("BadFont"), + QByteArrayLiteral("BadMatch"), + QByteArrayLiteral("BadDrawable"), + QByteArrayLiteral("BadAccess"), + QByteArrayLiteral("BadAlloc"), + QByteArrayLiteral("BadColor"), + QByteArrayLiteral("BadGC"), + QByteArrayLiteral("BadIDChoice"), + QByteArrayLiteral("BadName"), + QByteArrayLiteral("BadLength"), + QByteArrayLiteral("BadImplementation"), + QByteArrayLiteral("Unknown")}); + + +void Workspace::registerEventFilter(X11EventFilter *filter) +{ + if (filter->isGenericEvent()) { + m_genericEventFilters.append(new X11EventFilterContainer(filter)); + } else { + m_eventFilters.append(new X11EventFilterContainer(filter)); + } +} + +static X11EventFilterContainer *takeEventFilter(X11EventFilter *eventFilter, + QList> &list) +{ + for (int i = 0; i < list.count(); ++i) { + X11EventFilterContainer *container = list.at(i); + if (container->filter() == eventFilter) { + return list.takeAt(i); + } + } + return nullptr; +} + +void Workspace::unregisterEventFilter(X11EventFilter *filter) +{ + X11EventFilterContainer *container = nullptr; + if (filter->isGenericEvent()) { + container = takeEventFilter(filter, m_genericEventFilters); + } else { + container = takeEventFilter(filter, m_eventFilters); + } + delete container; +} + + +/** + * Handles workspace specific XCB event + */ +bool Workspace::workspaceEvent(xcb_generic_event_t *e) +{ + const uint8_t eventType = e->response_type & ~0x80; + if (!eventType) { + // let's check whether it's an error from one of the extensions KWin uses + xcb_generic_error_t *error = reinterpret_cast(e); + const QVector extensions = Xcb::Extensions::self()->extensions(); + for (const auto &extension : extensions) { + if (error->major_code == extension.majorOpcode) { + QByteArray errorName; + if (error->error_code < s_xcbEerrors.size()) { + errorName = s_xcbEerrors.at(error->error_code); + } else if (error->error_code >= extension.errorBase) { + const int index = error->error_code - extension.errorBase; + if (index >= 0 && index < extension.errorCodes.size()) { + errorName = extension.errorCodes.at(index); + } + } + if (errorName.isEmpty()) { + errorName = QByteArrayLiteral("Unknown"); + } + qCWarning(KWIN_CORE, "XCB error: %d (%s), sequence: %d, resource id: %d, major code: %d (%s), minor code: %d (%s)", + int(error->error_code), errorName.constData(), + int(error->sequence), int(error->resource_id), + int(error->major_code), extension.name.constData(), + int(error->minor_code), + extension.opCodes.size() > error->minor_code ? extension.opCodes.at(error->minor_code).constData() : "Unknown"); + return true; + } + } + return false; + } + + if (eventType == XCB_GE_GENERIC) { + xcb_ge_generic_event_t *ge = reinterpret_cast(e); + + // We need to make a shadow copy of the event filter list because an activated event + // filter may mutate it by removing or installing another event filter. + const auto eventFilters = m_genericEventFilters; + + for (X11EventFilterContainer *container : eventFilters) { + if (!container) { + continue; + } + X11EventFilter *filter = container->filter(); + if (filter->extension() == ge->extension && filter->genericEventTypes().contains(ge->event_type) && filter->event(e)) { + return true; + } + } + } else { + // We need to make a shadow copy of the event filter list because an activated event + // filter may mutate it by removing or installing another event filter. + const auto eventFilters = m_eventFilters; + + for (X11EventFilterContainer *container : eventFilters) { + if (!container) { + continue; + } + X11EventFilter *filter = container->filter(); + if (filter->eventTypes().contains(eventType) && filter->event(e)) { + return true; + } + } + } + + if (effects && static_cast< EffectsHandlerImpl* >(effects)->hasKeyboardGrab() + && (eventType == XCB_KEY_PRESS || eventType == XCB_KEY_RELEASE)) + return false; // let Qt process it, it'll be intercepted again in eventFilter() + + // events that should be handled before Clients can get them + switch (eventType) { + case XCB_CONFIGURE_NOTIFY: + if (reinterpret_cast(e)->event == rootWindow()) + markXStackingOrderAsDirty(); + break; + }; + + const xcb_window_t eventWindow = findEventWindow(e); + if (eventWindow != XCB_WINDOW_NONE) { + if (X11Client *c = findClient(Predicate::WindowMatch, eventWindow)) { + if (c->windowEvent(e)) + return true; + } else if (X11Client *c = findClient(Predicate::WrapperIdMatch, eventWindow)) { + if (c->windowEvent(e)) + return true; + } else if (X11Client *c = findClient(Predicate::FrameIdMatch, eventWindow)) { + if (c->windowEvent(e)) + return true; + } else if (X11Client *c = findClient(Predicate::InputIdMatch, eventWindow)) { + if (c->windowEvent(e)) + return true; + } else if (Unmanaged* c = findUnmanaged(eventWindow)) { + if (c->windowEvent(e)) + return true; + } + } + + switch (eventType) { + case XCB_CREATE_NOTIFY: { + const auto *event = reinterpret_cast(e); + if (event->parent == rootWindow() && + !QWidget::find(event->window) && + !event->override_redirect) { + // see comments for allowClientActivation() + updateXTime(); + const xcb_timestamp_t t = xTime(); + xcb_change_property(connection(), XCB_PROP_MODE_REPLACE, event->window, atoms->kde_net_wm_user_creation_time, XCB_ATOM_CARDINAL, 32, 1, &t); + } + break; + } + case XCB_UNMAP_NOTIFY: { + const auto *event = reinterpret_cast(e); + return (event->event != event->window); // hide wm typical event from Qt + } + case XCB_REPARENT_NOTIFY: { + //do not confuse Qt with these events. After all, _we_ are the + //window manager who does the reparenting. + return true; + } + case XCB_MAP_REQUEST: { + updateXTime(); + + const auto *event = reinterpret_cast(e); + if (X11Client *c = findClient(Predicate::WindowMatch, event->window)) { + // e->xmaprequest.window is different from e->xany.window + // TODO this shouldn't be necessary now + c->windowEvent(e); + FocusChain::self()->update(c, FocusChain::Update); + } else if ( true /*|| e->xmaprequest.parent != root */ ) { + // NOTICE don't check for the parent being the root window, this breaks when some app unmaps + // a window, changes something and immediately maps it back, without giving KWin + // a chance to reparent it back to root + // since KWin can get MapRequest only for root window children and + // children of WindowWrapper (=clients), the check is AFAIK useless anyway + // NOTICE: The save-set support in X11Client::mapRequestEvent() actually requires that + // this code doesn't check the parent to be root. + if (!createClient(event->window, false)) { + xcb_map_window(connection(), event->window); + const uint32_t values[] = { XCB_STACK_MODE_ABOVE }; + xcb_configure_window(connection(), event->window, XCB_CONFIG_WINDOW_STACK_MODE, values); + } + } + return true; + } + case XCB_MAP_NOTIFY: { + const auto *event = reinterpret_cast(e); + if (event->override_redirect) { + Unmanaged* c = findUnmanaged(event->window); + if (c == nullptr) + c = createUnmanaged(event->window); + if (c) { + // if hasScheduledRelease is true, it means a unamp and map sequence has occurred. + // since release is scheduled after map notify, this old Unmanaged will get released + // before KWIN has chance to remanage it again. so release it right now. + if (c->hasScheduledRelease()) { + c->release(); + c = createUnmanaged(event->window); + } + if (c) + return c->windowEvent(e); + } + } + return (event->event != event->window); // hide wm typical event from Qt + } + + case XCB_CONFIGURE_REQUEST: { + const auto *event = reinterpret_cast(e); + if (event->parent == rootWindow()) { + uint32_t values[5] = { 0, 0, 0, 0, 0}; + const uint32_t value_mask = event->value_mask + & (XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y | XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT | XCB_CONFIG_WINDOW_BORDER_WIDTH); + int i = 0; + if (value_mask & XCB_CONFIG_WINDOW_X) { + values[i++] = event->x; + } + if (value_mask & XCB_CONFIG_WINDOW_Y) { + values[i++] = event->y; + } + if (value_mask & XCB_CONFIG_WINDOW_WIDTH) { + values[i++] = event->width; + } + if (value_mask & XCB_CONFIG_WINDOW_HEIGHT) { + values[i++] = event->height; + } + if (value_mask & XCB_CONFIG_WINDOW_BORDER_WIDTH) { + values[i++] = event->border_width; + } + xcb_configure_window(connection(), event->window, value_mask, values); + return true; + } + break; + } + case XCB_FOCUS_IN: { + const auto *event = reinterpret_cast(e); + if (event->event == rootWindow() + && (event->detail == XCB_NOTIFY_DETAIL_NONE || event->detail == XCB_NOTIFY_DETAIL_POINTER_ROOT || event->detail == XCB_NOTIFY_DETAIL_INFERIOR)) { + Xcb::CurrentInput currentInput; + updateXTime(); // focusToNull() uses xTime(), which is old now (FocusIn has no timestamp) + // it seems we can "loose" focus reversions when the closing client hold a grab + // => catch the typical pattern (though we don't want the focus on the root anyway) #348935 + const bool lostFocusPointerToRoot = currentInput->focus == rootWindow() && event->detail == XCB_NOTIFY_DETAIL_INFERIOR; + if (!currentInput.isNull() && (currentInput->focus == XCB_WINDOW_NONE || currentInput->focus == XCB_INPUT_FOCUS_POINTER_ROOT || lostFocusPointerToRoot)) { + //kWarning( 1212 ) << "X focus set to None/PointerRoot, reseting focus" ; + AbstractClient *c = mostRecentlyActivatedClient(); + if (c != nullptr) + requestFocus(c, true); + else if (activateNextClient(nullptr)) + ; // ok, activated + else + focusToNull(); + } + } + } + // fall through + case XCB_FOCUS_OUT: + return true; // always eat these, they would tell Qt that KWin is the active app + default: + break; + } + return false; +} + +// Used only to filter events that need to be processed by Qt first +// (e.g. keyboard input to be composed), otherwise events are +// handle by the XEvent filter above +bool Workspace::workspaceEvent(QEvent* e) +{ + if ((e->type() == QEvent::KeyPress || e->type() == QEvent::KeyRelease || e->type() == QEvent::ShortcutOverride) + && effects && static_cast< EffectsHandlerImpl* >(effects)->hasKeyboardGrab()) { + static_cast< EffectsHandlerImpl* >(effects)->grabbedKeyboardEvent(static_cast< QKeyEvent* >(e)); + return true; + } + return false; +} + +// **************************************** +// Client +// **************************************** + +/** + * General handler for XEvents concerning the client window + */ +bool X11Client::windowEvent(xcb_generic_event_t *e) +{ + if (findEventWindow(e) == window()) { // avoid doing stuff on frame or wrapper + NET::Properties dirtyProperties; + NET::Properties2 dirtyProperties2; + double old_opacity = opacity(); + info->event(e, &dirtyProperties, &dirtyProperties2); // pass through the NET stuff + + if ((dirtyProperties & NET::WMName) != 0) + fetchName(); + if ((dirtyProperties & NET::WMIconName) != 0) + fetchIconicName(); + if ((dirtyProperties & NET::WMStrut) != 0 + || (dirtyProperties2 & NET::WM2ExtendedStrut) != 0) { + workspace()->updateClientArea(); + } + if ((dirtyProperties & NET::WMIcon) != 0) + getIcons(); + // Note there's a difference between userTime() and info->userTime() + // info->userTime() is the value of the property, userTime() also includes + // updates of the time done by KWin (ButtonPress on windowrapper etc.). + if ((dirtyProperties2 & NET::WM2UserTime) != 0) { + workspace()->setWasUserInteraction(); + updateUserTime(info->userTime()); + } + if ((dirtyProperties2 & NET::WM2StartupId) != 0) + startupIdChanged(); + if (dirtyProperties2 & NET::WM2Opacity) { + if (compositing()) { + addRepaintFull(); + emit opacityChanged(this, old_opacity); + } else { + // forward to the frame if there's possibly another compositing manager running + NETWinInfo i(connection(), frameId(), rootWindow(), NET::Properties(), NET::Properties2()); + i.setOpacity(info->opacity()); + } + } + if (dirtyProperties2 & NET::WM2FrameOverlap) { + // ### Inform the decoration + } + if (dirtyProperties2.testFlag(NET::WM2WindowRole)) { + emit windowRoleChanged(); + } + if (dirtyProperties2.testFlag(NET::WM2WindowClass)) { + getResourceClass(); + } + if (dirtyProperties2.testFlag(NET::WM2BlockCompositing)) { + setBlockingCompositing(info->isBlockingCompositing()); + } + if (dirtyProperties2.testFlag(NET::WM2GroupLeader)) { + checkGroup(); + updateAllowedActions(); // Group affects isMinimizable() + } + if (dirtyProperties2.testFlag(NET::WM2Urgency)) { + updateUrgency(); + } + if (dirtyProperties2 & NET::WM2OpaqueRegion) { + getWmOpaqueRegion(); + } + if (dirtyProperties2 & NET::WM2DesktopFileName) { + setDesktopFileName(QByteArray(info->desktopFileName())); + } + if (dirtyProperties2 & NET::WM2GTKFrameExtents) { + setClientFrameExtents(info->gtkFrameExtents()); + } + } + + const uint8_t eventType = e->response_type & ~0x80; + switch(eventType) { + case XCB_UNMAP_NOTIFY: + unmapNotifyEvent(reinterpret_cast(e)); + break; + case XCB_DESTROY_NOTIFY: + destroyNotifyEvent(reinterpret_cast(e)); + break; + case XCB_MAP_REQUEST: + // this one may pass the event to workspace + return mapRequestEvent(reinterpret_cast(e)); + case XCB_CONFIGURE_REQUEST: + configureRequestEvent(reinterpret_cast(e)); + break; + case XCB_PROPERTY_NOTIFY: + propertyNotifyEvent(reinterpret_cast(e)); + break; + case XCB_KEY_PRESS: + updateUserTime(reinterpret_cast(e)->time); + break; + case XCB_BUTTON_PRESS: { + const auto *event = reinterpret_cast(e); + updateUserTime(event->time); + buttonPressEvent(event->event, event->detail, event->state, + event->event_x, event->event_y, event->root_x, event->root_y, event->time); + break; + } + case XCB_KEY_RELEASE: + // don't update user time on releases + // e.g. if the user presses Alt+F2, the Alt release + // would appear as user input to the currently active window + break; + case XCB_BUTTON_RELEASE: { + const auto *event = reinterpret_cast(e); + // don't update user time on releases + // e.g. if the user presses Alt+F2, the Alt release + // would appear as user input to the currently active window + buttonReleaseEvent(event->event, event->detail, event->state, + event->event_x, event->event_y, event->root_x, event->root_y); + break; + } + case XCB_MOTION_NOTIFY: { + const auto *event = reinterpret_cast(e); + motionNotifyEvent(event->event, event->state, + event->event_x, event->event_y, event->root_x, event->root_y); + workspace()->updateFocusMousePosition(QPoint(event->root_x, event->root_y)); + break; + } + case XCB_ENTER_NOTIFY: { + auto *event = reinterpret_cast(e); + enterNotifyEvent(event); + // MotionNotify is guaranteed to be generated only if the mouse + // move start and ends in the window; for cases when it only + // starts or only ends there, Enter/LeaveNotify are generated. + // Fake a MotionEvent in such cases to make handle of mouse + // events simpler (Qt does that too). + motionNotifyEvent(event->event, event->state, + event->event_x, event->event_y, event->root_x, event->root_y); + workspace()->updateFocusMousePosition(QPoint(event->root_x, event->root_y)); + break; + } + case XCB_LEAVE_NOTIFY: { + auto *event = reinterpret_cast(e); + motionNotifyEvent(event->event, event->state, + event->event_x, event->event_y, event->root_x, event->root_y); + leaveNotifyEvent(event); + // not here, it'd break following enter notify handling + // workspace()->updateFocusMousePosition( QPoint( e->xcrossing.x_root, e->xcrossing.y_root )); + break; + } + case XCB_FOCUS_IN: + focusInEvent(reinterpret_cast(e)); + break; + case XCB_FOCUS_OUT: + focusOutEvent(reinterpret_cast(e)); + break; + case XCB_REPARENT_NOTIFY: + break; + case XCB_CLIENT_MESSAGE: + clientMessageEvent(reinterpret_cast(e)); + break; + case XCB_EXPOSE: { + xcb_expose_event_t *event = reinterpret_cast(e); + if (event->window == frameId() && !Compositor::self()->isActive()) { + // TODO: only repaint required areas + triggerDecorationRepaint(); + } + break; + } + default: + if (eventType == Xcb::Extensions::self()->shapeNotifyEvent() && reinterpret_cast(e)->affected_window == window()) { + detectShape(window()); // workaround for #19644 + updateShape(); + } + if (eventType == Xcb::Extensions::self()->damageNotifyEvent() && reinterpret_cast(e)->drawable == frameId()) + damageNotifyEvent(); + break; + } + return true; // eat all events +} + +/** + * Handles map requests of the client window + */ +bool X11Client::mapRequestEvent(xcb_map_request_event_t *e) +{ + if (e->window != window()) { + // Special support for the save-set feature, which is a bit broken. + // If there's a window from one client embedded in another one, + // e.g. using XEMBED, and the embedder suddenly loses its X connection, + // save-set will reparent the embedded window to its closest ancestor + // that will remains. Unfortunately, with reparenting window managers, + // this is not the root window, but the frame (or in KWin's case, + // it's the wrapper for the client window). In this case, + // the wrapper will get ReparentNotify for a window it won't know, + // which will be ignored, and then it gets MapRequest, as save-set + // always maps. Returning true here means that Workspace::workspaceEvent() + // will handle this MapRequest and manage this window (i.e. act as if + // it was reparented to root window). + if (e->parent == wrapperId()) + return false; + return true; // no messing with frame etc. + } + // also copied in clientMessage() + if (isMinimized()) + unminimize(); + if (isShade()) + setShade(ShadeNone); + if (!isOnCurrentDesktop()) { + if (workspace()->allowClientActivation(this)) + workspace()->activateClient(this); + else + demandAttention(); + } + return true; +} + +/** + * Handles unmap notify events of the client window + */ +void X11Client::unmapNotifyEvent(xcb_unmap_notify_event_t *e) +{ + if (e->window != window()) + return; + if (e->event != wrapperId()) { + // most probably event from root window when initially reparenting + bool ignore = true; + if (e->event == rootWindow() && (e->response_type & 0x80)) + ignore = false; // XWithdrawWindow() + if (ignore) + return; + } + + // check whether this is result of an XReparentWindow - client then won't be parented by wrapper + // in this case do not release the client (causes reparent to root, removal from saveSet and what not) + // but just destroy the client + Xcb::Tree tree(m_client); + xcb_window_t daddy = tree.parent(); + if (daddy == m_wrapper) { + releaseWindow(); // unmapped from a regular client state + } else { + destroyClient(); // the client was moved to some other parent + } +} + +void X11Client::destroyNotifyEvent(xcb_destroy_notify_event_t *e) +{ + if (e->window != window()) + return; + destroyClient(); +} + + +/** + * Handles client messages for the client window + */ +void X11Client::clientMessageEvent(xcb_client_message_event_t *e) +{ + Toplevel::clientMessageEvent(e); + if (e->window != window()) + return; // ignore frame/wrapper + // WM_STATE + if (e->type == atoms->wm_change_state) { + if (e->data.data32[0] == XCB_ICCCM_WM_STATE_ICONIC) + minimize(); + return; + } +} + + +/** + * Handles configure requests of the client window + */ +void X11Client::configureRequestEvent(xcb_configure_request_event_t *e) +{ + if (e->window != window()) + return; // ignore frame/wrapper + if (isResize() || isMove()) + return; // we have better things to do right now + + if (m_fullscreenMode == FullScreenNormal) { // refuse resizing of fullscreen windows + // but allow resizing fullscreen hacks in order to let them cancel fullscreen mode + sendSyntheticConfigureNotify(); + return; + } + if (isSplash()) { // no manipulations with splashscreens either + sendSyntheticConfigureNotify(); + return; + } + + if (e->value_mask & XCB_CONFIG_WINDOW_BORDER_WIDTH) { + // first, get rid of a window border + m_client.setBorderWidth(0); + } + + if (e->value_mask & (XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y | XCB_CONFIG_WINDOW_HEIGHT | XCB_CONFIG_WINDOW_WIDTH)) + configureRequest(e->value_mask, e->x, e->y, e->width, e->height, 0, false); + + if (e->value_mask & XCB_CONFIG_WINDOW_STACK_MODE) + restackWindow(e->sibling, e->stack_mode, NET::FromApplication, userTime(), false); + + // Sending a synthetic configure notify always is fine, even in cases where + // the ICCCM doesn't require this - it can be though of as 'the WM decided to move + // the window later'. The client should not cause that many configure request, + // so this should not have any significant impact. With user moving/resizing + // the it should be optimized though (see also X11Client::setGeometry()/plainResize()/move()). + sendSyntheticConfigureNotify(); + + // SELI TODO accept configure requests for isDesktop windows (because kdesktop + // may get XRANDR resize event before kwin), but check it's still at the bottom? +} + + +/** + * Handles property changes of the client window + */ +void X11Client::propertyNotifyEvent(xcb_property_notify_event_t *e) +{ + Toplevel::propertyNotifyEvent(e); + if (e->window != window()) + return; // ignore frame/wrapper + switch(e->atom) { + case XCB_ATOM_WM_NORMAL_HINTS: + getWmNormalHints(); + break; + case XCB_ATOM_WM_NAME: + fetchName(); + break; + case XCB_ATOM_WM_ICON_NAME: + fetchIconicName(); + break; + case XCB_ATOM_WM_TRANSIENT_FOR: + readTransient(); + break; + case XCB_ATOM_WM_HINTS: + getIcons(); // because KWin::icon() uses WMHints as fallback + break; + default: + if (e->atom == atoms->motif_wm_hints) { + getMotifHints(); + } else if (e->atom == atoms->net_wm_sync_request_counter) + getSyncCounter(); + else if (e->atom == atoms->activities) + checkActivities(); + else if (e->atom == atoms->kde_first_in_window_list) + updateFirstInTabBox(); + else if (e->atom == atoms->kde_color_sheme) + updateColorScheme(); + else if (e->atom == atoms->kde_screen_edge_show) + updateShowOnScreenEdge(); + else if (e->atom == atoms->kde_net_wm_appmenu_service_name) + checkApplicationMenuServiceName(); + else if (e->atom == atoms->kde_net_wm_appmenu_object_path) + checkApplicationMenuObjectPath(); + break; + } +} + + +void X11Client::enterNotifyEvent(xcb_enter_notify_event_t *e) +{ + if (e->event != frameId()) + return; // care only about entering the whole frame + +#define MOUSE_DRIVEN_FOCUS (!options->focusPolicyIsReasonable() || \ + (options->focusPolicy() == Options::FocusFollowsMouse && options->isNextFocusPrefersMouse())) + if (e->mode == XCB_NOTIFY_MODE_NORMAL || (e->mode == XCB_NOTIFY_MODE_UNGRAB && MOUSE_DRIVEN_FOCUS)) { +#undef MOUSE_DRIVEN_FOCUS + + enterEvent(QPoint(e->root_x, e->root_y)); + return; + } +} + +void X11Client::leaveNotifyEvent(xcb_leave_notify_event_t *e) +{ + if (e->event != frameId()) + return; // care only about leaving the whole frame + if (e->mode == XCB_NOTIFY_MODE_NORMAL) { + if (!isMoveResizePointerButtonDown()) { + setMoveResizePointerMode(PositionCenter); + updateCursor(); + } + bool lostMouse = !rect().contains(QPoint(e->event_x, e->event_y)); + // 'lostMouse' wouldn't work with e.g. B2 or Keramik, which have non-rectangular decorations + // (i.e. the LeaveNotify event comes before leaving the rect and no LeaveNotify event + // comes after leaving the rect) - so lets check if the pointer is really outside the window + + // TODO this still sucks if a window appears above this one - it should lose the mouse + // if this window is another client, but not if it's a popup ... maybe after KDE3.1 :( + // (repeat after me 'AARGHL!') + if (!lostMouse && e->detail != XCB_NOTIFY_DETAIL_INFERIOR) { + Xcb::Pointer pointer(frameId()); + if (!pointer || !pointer->same_screen || pointer->child == XCB_WINDOW_NONE) { + // really lost the mouse + lostMouse = true; + } + } + if (lostMouse) { + leaveEvent(); + if (isDecorated()) { + // sending a move instead of a leave. With leave we need to send proper coords, with move it's handled internally + QHoverEvent leaveEvent(QEvent::HoverMove, QPointF(-1, -1), QPointF(-1, -1), Qt::NoModifier); + QCoreApplication::sendEvent(decoration(), &leaveEvent); + } + } + if (options->focusPolicy() == Options::FocusStrictlyUnderMouse && isActive() && lostMouse) { + workspace()->requestDelayFocus(nullptr); + } + return; + } +} + +static uint16_t x11CommandAllModifier() +{ + switch (options->commandAllModifier()) { + case Qt::MetaModifier: + return KKeyServer::modXMeta(); + case Qt::AltModifier: + return KKeyServer::modXAlt(); + default: + return 0; + } +} + +#define XCapL KKeyServer::modXLock() +#define XNumL KKeyServer::modXNumLock() +#define XScrL KKeyServer::modXScrollLock() +void X11Client::establishCommandWindowGrab(uint8_t button) +{ + // Unfortunately there are a lot of possible modifier combinations that we need to take into + // account. We tackle that problem in a kind of smart way. First, we grab the button with all + // possible modifiers, then we ungrab the ones that are relevant only to commandAllx(). + + m_wrapper.grabButton(XCB_GRAB_MODE_SYNC, XCB_GRAB_MODE_ASYNC, XCB_MOD_MASK_ANY, button); + + uint16_t x11Modifier = x11CommandAllModifier(); + + unsigned int mods[ 8 ] = { + 0, XCapL, XNumL, XNumL | XCapL, + XScrL, XScrL | XCapL, + XScrL | XNumL, XScrL | XNumL | XCapL + }; + for (int i = 0; + i < 8; + ++i) + m_wrapper.ungrabButton(x11Modifier | mods[ i ], button); +} + +void X11Client::establishCommandAllGrab(uint8_t button) +{ + uint16_t x11Modifier = x11CommandAllModifier(); + + unsigned int mods[ 8 ] = { + 0, XCapL, XNumL, XNumL | XCapL, + XScrL, XScrL | XCapL, + XScrL | XNumL, XScrL | XNumL | XCapL + }; + for (int i = 0; + i < 8; + ++i) + m_wrapper.grabButton(XCB_GRAB_MODE_SYNC, XCB_GRAB_MODE_ASYNC, x11Modifier | mods[ i ], button); +} +#undef XCapL +#undef XNumL +#undef XScrL + +void X11Client::updateMouseGrab() +{ + xcb_ungrab_button(connection(), XCB_BUTTON_INDEX_ANY, m_wrapper, XCB_MOD_MASK_ANY); + + if (TabBox::TabBox::self()->forcedGlobalMouseGrab()) { // see TabBox::establishTabBoxGrab() + m_wrapper.grabButton(XCB_GRAB_MODE_SYNC, XCB_GRAB_MODE_ASYNC); + return; + } + + // When a passive grab is activated or deactivated, the X server will generate crossing + // events as if the pointer were suddenly to warp from its current position to some position + // in the grab window. Some /broken/ X11 clients do get confused by such EnterNotify and + // LeaveNotify events so we release the passive grab for the active window. + // + // The passive grab below is established so the window can be raised or activated when it + // is clicked. + if ((options->focusPolicyIsReasonable() && !isActive()) || + (options->isClickRaise() && !isMostRecentlyRaised())) { + if (options->commandWindow1() != Options::MouseNothing) { + establishCommandWindowGrab(XCB_BUTTON_INDEX_1); + } + if (options->commandWindow2() != Options::MouseNothing) { + establishCommandWindowGrab(XCB_BUTTON_INDEX_2); + } + if (options->commandWindow3() != Options::MouseNothing) { + establishCommandWindowGrab(XCB_BUTTON_INDEX_3); + } + if (options->commandWindowWheel() != Options::MouseNothing) { + establishCommandWindowGrab(XCB_BUTTON_INDEX_4); + establishCommandWindowGrab(XCB_BUTTON_INDEX_5); + } + } + + // We want to grab + buttons no matter what state the window is in. The + // client will receive funky EnterNotify and LeaveNotify events, but there is nothing that + // we can do about it, unfortunately. + + if (!workspace()->globalShortcutsDisabled()) { + if (options->commandAll1() != Options::MouseNothing) { + establishCommandAllGrab(XCB_BUTTON_INDEX_1); + } + if (options->commandAll2() != Options::MouseNothing) { + establishCommandAllGrab(XCB_BUTTON_INDEX_2); + } + if (options->commandAll3() != Options::MouseNothing) { + establishCommandAllGrab(XCB_BUTTON_INDEX_3); + } + if (options->commandAllWheel() != Options::MouseWheelNothing) { + establishCommandAllGrab(XCB_BUTTON_INDEX_4); + establishCommandAllGrab(XCB_BUTTON_INDEX_5); + } + } +} + +static bool modKeyDown(int state) { + const uint keyModX = (options->keyCmdAllModKey() == Qt::Key_Meta) ? + KKeyServer::modXMeta() : KKeyServer::modXAlt(); + return keyModX && (state & KKeyServer::accelModMaskX()) == keyModX; +} + + +// return value matters only when filtering events before decoration gets them +bool X11Client::buttonPressEvent(xcb_window_t w, int button, int state, int x, int y, int x_root, int y_root, xcb_timestamp_t time) +{ + if (isMoveResizePointerButtonDown()) { + if (w == wrapperId()) + xcb_allow_events(connection(), XCB_ALLOW_SYNC_POINTER, XCB_TIME_CURRENT_TIME); //xTime()); + return true; + } + + if (w == wrapperId() || w == frameId() || w == inputId()) { + // FRAME neco s tohohle by se melo zpracovat, nez to dostane dekorace + updateUserTime(time); + const bool bModKeyHeld = modKeyDown(state); + + if (isSplash() + && button == XCB_BUTTON_INDEX_1 && !bModKeyHeld) { + // hide splashwindow if the user clicks on it + hideClient(true); + if (w == wrapperId()) + xcb_allow_events(connection(), XCB_ALLOW_SYNC_POINTER, XCB_TIME_CURRENT_TIME); //xTime()); + return true; + } + + Options::MouseCommand com = Options::MouseNothing; + bool was_action = false; + if (bModKeyHeld) { + was_action = true; + switch(button) { + case XCB_BUTTON_INDEX_1: + com = options->commandAll1(); + break; + case XCB_BUTTON_INDEX_2: + com = options->commandAll2(); + break; + case XCB_BUTTON_INDEX_3: + com = options->commandAll3(); + break; + case XCB_BUTTON_INDEX_4: + case XCB_BUTTON_INDEX_5: + com = options->operationWindowMouseWheel(button == XCB_BUTTON_INDEX_4 ? 120 : -120); + break; + } + } else { + if (w == wrapperId()) { + if (button < 4) { + com = getMouseCommand(x11ToQtMouseButton(button), &was_action); + } else if (button < 6) { + com = getWheelCommand(Qt::Vertical, &was_action); + } + } + } + if (was_action) { + bool replay = performMouseCommand(com, QPoint(x_root, y_root)); + + if (isSpecialWindow()) + replay = true; + + if (w == wrapperId()) // these can come only from a grab + xcb_allow_events(connection(), replay ? XCB_ALLOW_REPLAY_POINTER : XCB_ALLOW_SYNC_POINTER, XCB_TIME_CURRENT_TIME); //xTime()); + return true; + } + } + + if (w == wrapperId()) { // these can come only from a grab + xcb_allow_events(connection(), XCB_ALLOW_REPLAY_POINTER, XCB_TIME_CURRENT_TIME); //xTime()); + return true; + } + if (w == inputId()) { + x = x_root - frameGeometry().x(); + y = y_root - frameGeometry().y(); + // New API processes core events FIRST and only passes unused ones to the decoration + QMouseEvent ev(QMouseEvent::MouseButtonPress, QPoint(x, y), QPoint(x_root, y_root), + x11ToQtMouseButton(button), x11ToQtMouseButtons(state), Qt::KeyboardModifiers()); + return processDecorationButtonPress(&ev, true); + } + if (w == frameId() && isDecorated()) { + if (button >= 4 && button <= 7) { + const Qt::KeyboardModifiers modifiers = x11ToQtKeyboardModifiers(state); + // Logic borrowed from qapplication_x11.cpp + const int delta = 120 * ((button == 4 || button == 6) ? 1 : -1); + const bool hor = (((button == 4 || button == 5) && (modifiers & Qt::AltModifier)) + || (button == 6 || button == 7)); + + const QPoint angle = hor ? QPoint(delta, 0) : QPoint(0, delta); + QWheelEvent event(QPointF(x, y), + QPointF(x_root, y_root), + QPoint(), + angle, + delta, + hor ? Qt::Horizontal : Qt::Vertical, + x11ToQtMouseButtons(state), + modifiers); + event.setAccepted(false); + QCoreApplication::sendEvent(decoration(), &event); + if (!event.isAccepted() && !hor) { + if (titlebarPositionUnderMouse()) { + performMouseCommand(options->operationTitlebarMouseWheel(delta), QPoint(x_root, y_root)); + } + } + } else { + QMouseEvent event(QEvent::MouseButtonPress, QPointF(x, y), QPointF(x_root, y_root), + x11ToQtMouseButton(button), x11ToQtMouseButtons(state), x11ToQtKeyboardModifiers(state)); + event.setAccepted(false); + QCoreApplication::sendEvent(decoration(), &event); + if (!event.isAccepted()) { + processDecorationButtonPress(&event); + } + } + return true; + } + return true; +} + +// return value matters only when filtering events before decoration gets them +bool X11Client::buttonReleaseEvent(xcb_window_t w, int button, int state, int x, int y, int x_root, int y_root) +{ + if (w == frameId() && isDecorated()) { + // wheel handled on buttonPress + if (button < 4 || button > 7) { + QMouseEvent event(QEvent::MouseButtonRelease, + QPointF(x, y), + QPointF(x_root, y_root), + x11ToQtMouseButton(button), + x11ToQtMouseButtons(state) & ~x11ToQtMouseButton(button), + x11ToQtKeyboardModifiers(state)); + event.setAccepted(false); + QCoreApplication::sendEvent(decoration(), &event); + if (event.isAccepted() || !titlebarPositionUnderMouse()) { + invalidateDecorationDoubleClickTimer(); // click was for the deco and shall not init a doubleclick + } + } + } + if (w == wrapperId()) { + xcb_allow_events(connection(), XCB_ALLOW_SYNC_POINTER, XCB_TIME_CURRENT_TIME); //xTime()); + return true; + } + if (w != frameId() && w != inputId() && w != moveResizeGrabWindow()) + return true; + if (w == frameId() && workspace()->userActionsMenu() && workspace()->userActionsMenu()->isShown()) { + const_cast(workspace()->userActionsMenu())->grabInput(); + } + x = this->x(); // translate from grab window to local coords + y = this->y(); + + // Check whether other buttons are still left pressed + int buttonMask = XCB_BUTTON_MASK_1 | XCB_BUTTON_MASK_2 | XCB_BUTTON_MASK_3; + if (button == XCB_BUTTON_INDEX_1) + buttonMask &= ~XCB_BUTTON_MASK_1; + else if (button == XCB_BUTTON_INDEX_2) + buttonMask &= ~XCB_BUTTON_MASK_2; + else if (button == XCB_BUTTON_INDEX_3) + buttonMask &= ~XCB_BUTTON_MASK_3; + + if ((state & buttonMask) == 0) { + endMoveResize(); + } + return true; +} + +// return value matters only when filtering events before decoration gets them +bool X11Client::motionNotifyEvent(xcb_window_t w, int state, int x, int y, int x_root, int y_root) +{ + if (w == frameId() && isDecorated() && !isMinimized()) { + // TODO Mouse move event dependent on state + QHoverEvent event(QEvent::HoverMove, QPointF(x, y), QPointF(x, y)); + QCoreApplication::instance()->sendEvent(decoration(), &event); + } + if (w != frameId() && w != inputId() && w != moveResizeGrabWindow()) + return true; // care only about the whole frame + if (!isMoveResizePointerButtonDown()) { + if (w == inputId()) { + int x = x_root - frameGeometry().x();// + padding_left; + int y = y_root - frameGeometry().y();// + padding_top; + + if (isDecorated()) { + QHoverEvent event(QEvent::HoverMove, QPointF(x, y), QPointF(x, y)); + QCoreApplication::instance()->sendEvent(decoration(), &event); + } + } + Position newmode = modKeyDown(state) ? PositionCenter : mousePosition(); + if (newmode != moveResizePointerMode()) { + setMoveResizePointerMode(newmode); + updateCursor(); + } + return false; + } + if (w == moveResizeGrabWindow()) { + x = this->x(); // translate from grab window to local coords + y = this->y(); + } + + handleMoveResize(QPoint(x, y), QPoint(x_root, y_root)); + return true; +} + +void X11Client::focusInEvent(xcb_focus_in_event_t *e) +{ + if (e->event != window()) + return; // only window gets focus + if (e->mode == XCB_NOTIFY_MODE_UNGRAB) + return; // we don't care + if (e->detail == XCB_NOTIFY_DETAIL_POINTER) + return; // we don't care + if (!isShown(false) || !isOnCurrentDesktop()) // we unmapped it, but it got focus meanwhile -> + return; // activateNextClient() already transferred focus elsewhere + workspace()->forEachClient([](X11Client *client) { + client->cancelFocusOutTimer(); + }); + // check if this client is in should_get_focus list or if activation is allowed + bool activate = workspace()->allowClientActivation(this, -1U, true); + workspace()->gotFocusIn(this); // remove from should_get_focus list + if (activate) { + setActive(true); + } else { + if (workspace()->restoreFocus()) { + demandAttention(); + } else { + qCWarning(KWIN_CORE, "Failed to restore focus. Activating 0x%x", windowId()); + setActive(true); + } + } +} + +void X11Client::focusOutEvent(xcb_focus_out_event_t *e) +{ + if (e->event != window()) + return; // only window gets focus + if (e->mode == XCB_NOTIFY_MODE_GRAB) + return; // we don't care + if (isShade()) + return; // here neither + if (e->detail != XCB_NOTIFY_DETAIL_NONLINEAR + && e->detail != XCB_NOTIFY_DETAIL_NONLINEAR_VIRTUAL) + // SELI check all this + return; // hack for motif apps like netscape + if (QApplication::activePopupWidget()) + return; + + // When a client loses focus, FocusOut events are usually immediatelly + // followed by FocusIn events for another client that gains the focus + // (unless the focus goes to another screen, or to the nofocus widget). + // Without this check, the former focused client would have to be + // deactivated, and after that, the new one would be activated, with + // a short time when there would be no active client. This can cause + // flicker sometimes, e.g. when a fullscreen is shown, and focus is transferred + // from it to its transient, the fullscreen would be kept in the Active layer + // at the beginning and at the end, but not in the middle, when the active + // client would be temporarily none (see X11Client::belongToLayer() ). + // Therefore the setActive(false) call is moved to the end of the current + // event queue. If there is a matching FocusIn event in the current queue + // this will be processed before the setActive(false) call and the activation + // of the Client which gained FocusIn will automatically deactivate the + // previously active client. + if (!m_focusOutTimer) { + m_focusOutTimer = new QTimer(this); + m_focusOutTimer->setSingleShot(true); + m_focusOutTimer->setInterval(0); + connect(m_focusOutTimer, &QTimer::timeout, [this]() { + setActive(false); + }); + } + m_focusOutTimer->start(); +} + +// performs _NET_WM_MOVERESIZE +void X11Client::NETMoveResize(int x_root, int y_root, NET::Direction direction) +{ + if (direction == NET::Move) { + // move cursor to the provided position to prevent the window jumping there on first movement + // the expectation is that the cursor is already at the provided position, + // thus it's more a safety measurement + Cursors::self()->mouse()->setPos(QPoint(x_root, y_root)); + performMouseCommand(Options::MouseMove, QPoint(x_root, y_root)); + } else if (isMoveResize() && direction == NET::MoveResizeCancel) { + finishMoveResize(true); + setMoveResizePointerButtonDown(false); + updateCursor(); + } else if (direction >= NET::TopLeft && direction <= NET::Left) { + static const Position convert[] = { + PositionTopLeft, + PositionTop, + PositionTopRight, + PositionRight, + PositionBottomRight, + PositionBottom, + PositionBottomLeft, + PositionLeft + }; + if (!isResizable() || isShade()) + return; + if (isMoveResize()) + finishMoveResize(false); + setMoveResizePointerButtonDown(true); + setMoveOffset(QPoint(x_root - x(), y_root - y())); // map from global + setInvertedMoveOffset(rect().bottomRight() - moveOffset()); + setUnrestrictedMoveResize(false); + setMoveResizePointerMode(convert[ direction ]); + if (!startMoveResize()) + setMoveResizePointerButtonDown(false); + updateCursor(); + } else if (direction == NET::KeyboardMove) { + // ignore mouse coordinates given in the message, mouse position is used by the moving algorithm + Cursors::self()->mouse()->setPos(frameGeometry().center()); + performMouseCommand(Options::MouseUnrestrictedMove, frameGeometry().center()); + } else if (direction == NET::KeyboardSize) { + // ignore mouse coordinates given in the message, mouse position is used by the resizing algorithm + Cursors::self()->mouse()->setPos(frameGeometry().bottomRight()); + performMouseCommand(Options::MouseUnrestrictedResize, frameGeometry().bottomRight()); + } +} + +void X11Client::keyPressEvent(uint key_code, xcb_timestamp_t time) +{ + updateUserTime(time); + AbstractClient::keyPressEvent(key_code); +} + +// **************************************** +// Unmanaged +// **************************************** + +bool Unmanaged::windowEvent(xcb_generic_event_t *e) +{ + double old_opacity = opacity(); + NET::Properties dirtyProperties; + NET::Properties2 dirtyProperties2; + info->event(e, &dirtyProperties, &dirtyProperties2); // pass through the NET stuff + if (dirtyProperties2 & NET::WM2Opacity) { + if (compositing()) { + addRepaintFull(); + emit opacityChanged(this, old_opacity); + } + } + if (dirtyProperties2 & NET::WM2OpaqueRegion) { + getWmOpaqueRegion(); + } + if (dirtyProperties2.testFlag(NET::WM2WindowRole)) { + emit windowRoleChanged(); + } + if (dirtyProperties2.testFlag(NET::WM2WindowClass)) { + getResourceClass(); + } + const uint8_t eventType = e->response_type & ~0x80; + switch (eventType) { + case XCB_DESTROY_NOTIFY: + release(ReleaseReason::Destroyed); + break; + case XCB_UNMAP_NOTIFY:{ + workspace()->updateFocusMousePosition(Cursors::self()->mouse()->pos()); // may cause leave event + + // unmap notify might have been emitted due to a destroy notify + // but unmap notify gets emitted before the destroy notify, nevertheless at this + // point the window is already destroyed. This means any XCB request with the window + // will cause an error. + // To not run into these errors we try to wait for the destroy notify. For this we + // generate a round trip to the X server and wait a very short time span before + // handling the release. + updateXTime(); + // using 1 msec to not just move it at the end of the event loop but add an very short + // timespan to cover cases like unmap() followed by destroy(). The only other way to + // ensure that the window is not destroyed when we do the release handling is to grab + // the XServer which we do not want to do for an Unmanaged. The timespan of 1 msec is + // short enough to not cause problems in the close window animations. + // It's of course still possible that we miss the destroy in which case non-fatal + // X errors are reported to the event loop and logged by Qt. + m_scheduledRelease = true; + QTimer::singleShot(1, this, SLOT(release())); + break; + } + case XCB_CONFIGURE_NOTIFY: + configureNotifyEvent(reinterpret_cast(e)); + break; + case XCB_PROPERTY_NOTIFY: + propertyNotifyEvent(reinterpret_cast(e)); + break; + case XCB_CLIENT_MESSAGE: + clientMessageEvent(reinterpret_cast(e)); + break; + default: { + if (eventType == Xcb::Extensions::self()->shapeNotifyEvent()) { + detectShape(window()); + addRepaintFull(); + addWorkspaceRepaint(frameGeometry()); // in case shape change removes part of this window + emit geometryShapeChanged(this, frameGeometry()); + } + if (eventType == Xcb::Extensions::self()->damageNotifyEvent()) + damageNotifyEvent(); + break; + } + } + return false; // don't eat events, even our own unmanaged widgets are tracked +} + +void Unmanaged::configureNotifyEvent(xcb_configure_notify_event_t *e) +{ + if (effects) + static_cast(effects)->checkInputWindowStacking(); // keep them on top + QRect newgeom(e->x, e->y, e->width, e->height); + if (newgeom != m_frameGeometry) { + addWorkspaceRepaint(visibleRect()); // damage old area + QRect old = m_frameGeometry; + m_clientGeometry = newgeom; + m_frameGeometry = newgeom; + emit frameGeometryChanged(this, old); // update shadow region + addRepaintFull(); + if (old.size() != m_frameGeometry.size()) + discardWindowPixmap(); + emit geometryShapeChanged(this, old); + } +} + +// **************************************** +// Toplevel +// **************************************** + +void Toplevel::propertyNotifyEvent(xcb_property_notify_event_t *e) +{ + if (e->window != window()) + return; // ignore frame/wrapper + switch(e->atom) { + default: + if (e->atom == atoms->wm_client_leader) + getWmClientLeader(); + else if (e->atom == atoms->kde_net_wm_shadow) + updateShadow(); + else if (e->atom == atoms->kde_skip_close_animation) + getSkipCloseAnimation(); + break; + } +} + +void Toplevel::clientMessageEvent(xcb_client_message_event_t *e) +{ + if (e->type == atoms->wl_surface_id) { + m_surfaceId = e->data.data32[0]; + if (auto w = waylandServer()) { + if (auto s = KWaylandServer::SurfaceInterface::get(m_surfaceId, w->xWaylandConnection())) { + setSurface(s); + } + } + emit surfaceIdChanged(m_surfaceId); + } +} + +} // namespace diff --git a/fixqopengl.h b/fixqopengl.h new file mode 100644 index 0000000..7c9ce7b --- /dev/null +++ b/fixqopengl.h @@ -0,0 +1,26 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018 Bhushan Shah + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef FIXQOPENGL_H +#define FIXQOPENGL_H + +// qopengl.h declares GLdouble as a typedef of float when Qt is built +// with GLES support. This conflicts with the epoxy/gl_generated.h +// declaration, so we have to prevent the Qt header from being #included. +#define QOPENGL_H + +#ifndef QOPENGLF_APIENTRY +#define QOPENGLF_APIENTRY GLAPIENTRY +#endif + +#ifndef QOPENGLF_APIENTRYP +#define QOPENGLF_APIENTRYP GLAPIENTRYP +#endif + +#endif //FIXQOPENGL_H diff --git a/focuschain.cpp b/focuschain.cpp new file mode 100644 index 0000000..66d2097 --- /dev/null +++ b/focuschain.cpp @@ -0,0 +1,248 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "focuschain.h" +#include "abstract_client.h" +#include "screens.h" + +namespace KWin +{ + +KWIN_SINGLETON_FACTORY_VARIABLE(FocusChain, s_manager) + +FocusChain::FocusChain(QObject *parent) + : QObject(parent) + , m_separateScreenFocus(false) + , m_activeClient(nullptr) + , m_currentDesktop(0) +{ +} + +FocusChain::~FocusChain() +{ + s_manager = nullptr; +} + +void FocusChain::remove(AbstractClient *client) +{ + for (auto it = m_desktopFocusChains.begin(); + it != m_desktopFocusChains.end(); + ++it) { + it.value().removeAll(client); + } + m_mostRecentlyUsed.removeAll(client); +} + +void FocusChain::resize(uint previousSize, uint newSize) +{ + for (uint i = previousSize + 1; i <= newSize; ++i) { + m_desktopFocusChains.insert(i, Chain()); + } + for (uint i = previousSize; i > newSize; --i) { + m_desktopFocusChains.remove(i); + } +} + +AbstractClient *FocusChain::getForActivation(uint desktop) const +{ + return getForActivation(desktop, screens()->current()); +} + +AbstractClient *FocusChain::getForActivation(uint desktop, int screen) const +{ + auto it = m_desktopFocusChains.constFind(desktop); + if (it == m_desktopFocusChains.constEnd()) { + return nullptr; + } + const auto &chain = it.value(); + for (int i = chain.size() - 1; i >= 0; --i) { + auto tmp = chain.at(i); + // TODO: move the check into Client + if (tmp->isShown(false) && tmp->isOnCurrentActivity() + && ( !m_separateScreenFocus || tmp->screen() == screen)) { + return tmp; + } + } + return nullptr; +} + +void FocusChain::update(AbstractClient *client, FocusChain::Change change) +{ + if (!client->wantsTabFocus()) { + // Doesn't want tab focus, remove + remove(client); + return; + } + + if (client->isOnAllDesktops()) { + // Now on all desktops, add it to focus chains it is not already in + for (auto it = m_desktopFocusChains.begin(); + it != m_desktopFocusChains.end(); + ++it) { + auto &chain = it.value(); + // Making first/last works only on current desktop, don't affect all desktops + if (it.key() == m_currentDesktop + && (change == MakeFirst || change == MakeLast)) { + if (change == MakeFirst) { + makeFirstInChain(client, chain); + } else { + makeLastInChain(client, chain); + } + } else { + insertClientIntoChain(client, chain); + } + } + } else { + // Now only on desktop, remove it anywhere else + for (auto it = m_desktopFocusChains.begin(); + it != m_desktopFocusChains.end(); + ++it) { + auto &chain = it.value(); + if (client->isOnDesktop(it.key())) { + updateClientInChain(client, change, chain); + } else { + chain.removeAll(client); + } + } + } + + // add for most recently used chain + updateClientInChain(client, change, m_mostRecentlyUsed); +} + +void FocusChain::updateClientInChain(AbstractClient *client, FocusChain::Change change, Chain &chain) +{ + if (change == MakeFirst) { + makeFirstInChain(client, chain); + } else if (change == MakeLast) { + makeLastInChain(client, chain); + } else { + insertClientIntoChain(client, chain); + } +} + +void FocusChain::insertClientIntoChain(AbstractClient *client, Chain &chain) +{ + if (chain.contains(client)) { + return; + } + if (m_activeClient && m_activeClient != client && + !chain.empty() && chain.last() == m_activeClient) { + // Add it after the active client + chain.insert(chain.size() - 1, client); + } else { + // Otherwise add as the first one + chain.append(client); + } +} + +void FocusChain::moveAfterClient(AbstractClient *client, AbstractClient *reference) +{ + if (!client->wantsTabFocus()) { + return; + } + + for (auto it = m_desktopFocusChains.begin(); + it != m_desktopFocusChains.end(); + ++it) { + if (!client->isOnDesktop(it.key())) { + continue; + } + moveAfterClientInChain(client, reference, it.value()); + } + moveAfterClientInChain(client, reference, m_mostRecentlyUsed); +} + +void FocusChain::moveAfterClientInChain(AbstractClient *client, AbstractClient *reference, Chain &chain) +{ + if (!chain.contains(reference)) { + return; + } + if (AbstractClient::belongToSameApplication(reference, client)) { + chain.removeAll(client); + chain.insert(chain.indexOf(reference), client); + } else { + chain.removeAll(client); + for (int i = chain.size() - 1; i >= 0; --i) { + if (AbstractClient::belongToSameApplication(reference, chain.at(i))) { + chain.insert(i, client); + break; + } + } + } +} + +AbstractClient *FocusChain::firstMostRecentlyUsed() const +{ + if (m_mostRecentlyUsed.isEmpty()) { + return nullptr; + } + return m_mostRecentlyUsed.first(); +} + +AbstractClient *FocusChain::nextMostRecentlyUsed(AbstractClient *reference) const +{ + if (m_mostRecentlyUsed.isEmpty()) { + return nullptr; + } + const int index = m_mostRecentlyUsed.indexOf(reference); + if (index == -1) { + return m_mostRecentlyUsed.first(); + } + if (index == 0) { + return m_mostRecentlyUsed.last(); + } + return m_mostRecentlyUsed.at(index - 1); +} + +// copied from activation.cpp +bool FocusChain::isUsableFocusCandidate(AbstractClient *c, AbstractClient *prev) const +{ + return c != prev && + c->isShown(false) && c->isOnCurrentDesktop() && c->isOnCurrentActivity() && + (!m_separateScreenFocus || c->isOnScreen(prev ? prev->screen() : screens()->current())); +} + +AbstractClient *FocusChain::nextForDesktop(AbstractClient *reference, uint desktop) const +{ + auto it = m_desktopFocusChains.constFind(desktop); + if (it == m_desktopFocusChains.constEnd()) { + return nullptr; + } + const auto &chain = it.value(); + for (int i = chain.size() - 1; i >= 0; --i) { + auto client = chain.at(i); + if (isUsableFocusCandidate(client, reference)) { + return client; + } + } + return nullptr; +} + +void FocusChain::makeFirstInChain(AbstractClient *client, Chain &chain) +{ + chain.removeAll(client); + chain.append(client); +} + +void FocusChain::makeLastInChain(AbstractClient *client, Chain &chain) +{ + chain.removeAll(client); + chain.prepend(client); +} + +bool FocusChain::contains(AbstractClient *client, uint desktop) const +{ + auto it = m_desktopFocusChains.constFind(desktop); + if (it == m_desktopFocusChains.constEnd()) { + return false; + } + return it.value().contains(client); +} + +} // namespace diff --git a/focuschain.h b/focuschain.h new file mode 100644 index 0000000..8baf3ea --- /dev/null +++ b/focuschain.h @@ -0,0 +1,242 @@ +/* + 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 +*/ +#ifndef KWIN_FOCUS_CHAIN_H +#define KWIN_FOCUS_CHAIN_H +// KWin +#include +// Qt +#include +#include + +namespace KWin +{ +// forward declarations +class AbstractClient; + +/** + * @brief Singleton class to handle the various focus chains. + * + * A focus chain is a list of Clients containing information on which Client should be activated. + * + * Internally this FocusChain holds multiple independent chains. There is one chain of most recently + * used Clients which is primarily used by TabBox to build up the list of Clients for navigation. + * The chains are organized as a normal QList of Clients with the most recently used Client being the + * last item of the list, that is a LIFO like structure. + * + * In addition there is one chain for each virtual desktop which is used to determine which Client + * should get activated when the user switches to another virtual desktop. + * + * Furthermore this class contains various helper methods for the two different kind of chains. + */ +class FocusChain : public QObject +{ + Q_OBJECT +public: + enum Change { + MakeFirst, + MakeLast, + Update + }; + ~FocusChain() override; + /** + * @brief Updates the position of the @p client according to the requested @p change in the + * focus chain. + * + * This method affects both the most recently used focus chain and the per virtual desktop focus + * chain. + * + * In case the client does no longer want to get focus, it is removed from all chains. In case + * the client is on all virtual desktops it is ensured that it is present in each of the virtual + * desktops focus chain. In case it's on exactly one virtual desktop it is ensured that it is only + * in the focus chain for that virtual desktop. + * + * Depending on @p change the Client is inserted at different positions in the focus chain. In case + * of @c MakeFirst it is moved to the first position of the chain, in case of + * @c MakeLast it is moved to the last position of the chain. In all other cases it + * depends on whether the @p client is the currently active Client. If it is the active Client it + * becomes the first Client in the chain, otherwise it is inserted at the second position that is + * directly after the currently active Client. + * + * @param client The Client which should be moved inside the chains. + * @param change Where to move the Client + * @return void + */ + void update(AbstractClient *client, Change change); + /** + * @brief Moves @p client behind the @p reference Client in all focus chains. + * + * @param client The Client to move in the chains + * @param reference The Client behind which the @p client should be moved + * @return void + */ + void moveAfterClient(AbstractClient *client, AbstractClient *reference); + /** + * @brief Finds the best Client to become the new active Client in the focus chain for the given + * virtual @p desktop. + * + * In case that separate screen focus is used only Clients on the current screen are considered. + * If no Client for activation is found @c null is returned. + * + * @param desktop The virtual desktop to look for a Client for activation + * @return :X11Client *The Client which could be activated or @c null if there is none. + */ + AbstractClient *getForActivation(uint desktop) const; + /** + * @brief Finds the best Client to become the new active Client in the focus chain for the given + * virtual @p desktop on the given @p screen. + * + * This method makes only sense to use if separate screen focus is used. If separate screen focus + * is disabled the @p screen is ignored. + * If no Client for activation is found @c null is returned. + * + * @param desktop The virtual desktop to look for a Client for activation + * @param screen The screen to constrain the search on with separate screen focus + * @return :X11Client *The Client which could be activated or @c null if there is none. + */ + AbstractClient *getForActivation(uint desktop, int screen) const; + + /** + * @brief Checks whether the most recently used focus chain contains the given @p client. + * + * Does not consider the per-desktop focus chains. + * @param client The Client to look for. + * @return bool @c true if the most recently used focus chain contains @p client, @c false otherwise. + */ + bool contains(AbstractClient *client) const; + /** + * @brief Checks whether the focus chain for the given @p desktop contains the given @p client. + * + * Does not consider the most recently used focus chain. + * + * @param client The Client to look for. + * @param desktop The virtual desktop whose focus chain should be used + * @return bool @c true if the focus chain for @p desktop contains @p client, @c false otherwise. + */ + bool contains(AbstractClient *client, uint desktop) const; + /** + * @brief Queries the most recently used focus chain for the next Client after the given + * @p reference Client. + * + * The navigation wraps around the borders of the chain. That is if the @p reference Client is + * the last item of the focus chain, the first Client will be returned. + * + * If the @p reference Client cannot be found in the focus chain, the first element of the focus + * chain is returned. + * + * @param reference The start point in the focus chain to search + * @return :X11Client *The relatively next Client in the most recently used chain. + */ + AbstractClient *nextMostRecentlyUsed(AbstractClient *reference) const; + /** + * @brief Queries the focus chain for @p desktop for the next Client in relation to the given + * @p reference Client. + * + * The method finds the first usable Client which is not the @p reference Client. If no Client + * can be found @c null is returned + * + * @param reference The reference Client which should not be returned + * @param desktop The virtual desktop whose focus chain should be used + * @return :X11Client *The next usable Client or @c null if none can be found. + */ + AbstractClient *nextForDesktop(AbstractClient *reference, uint desktop) const; + /** + * @brief Returns the first Client in the most recently used focus chain. First Client in this + * case means really the first Client in the chain and not the most recently used Client. + * + * @return :X11Client *The first Client in the most recently used chain. + */ + AbstractClient *firstMostRecentlyUsed() const; + +public Q_SLOTS: + /** + * @brief Resizes the per virtual desktop focus chains from @p previousSize to @p newSize. + * This means that for each virtual desktop between previous and new size a new focus chain is + * created and in case the number is reduced the focus chains are destroyed. + * + * @param previousSize The previous number of virtual desktops + * @param newSize The new number of virtual desktops + * @return void + */ + void resize(uint previousSize, uint newSize); + /** + * @brief Removes @p client from all focus chains. + * + * @param client The Client to remove from all focus chains. + * @return void + */ + void remove(KWin::AbstractClient *client); + void setSeparateScreenFocus(bool enabled); + void setActiveClient(KWin::AbstractClient *client); + void setCurrentDesktop(uint previous, uint newDesktop); + bool isUsableFocusCandidate(AbstractClient *c, AbstractClient *prev) const; + +private: + using Chain = QList; + /** + * @brief Makes @p client the first Client in the given focus @p chain. + * + * This means the existing position of @p client is dropped and @p client is appended to the + * @p chain which makes it the first item. + * + * @param client The Client to become the first in @p chain + * @param chain The focus chain to operate on + * @return void + */ + void makeFirstInChain(AbstractClient *client, Chain &chain); + /** + * @brief Makes @p client the last Client in the given focus @p chain. + * + * This means the existing position of @p client is dropped and @p client is prepended to the + * @p chain which makes it the last item. + * + * @param client The Client to become the last in @p chain + * @param chain The focus chain to operate on + * @return void + */ + void makeLastInChain(AbstractClient *client, Chain &chain); + void moveAfterClientInChain(AbstractClient *client, AbstractClient *reference, Chain &chain); + void updateClientInChain(AbstractClient *client, Change change, Chain &chain); + void insertClientIntoChain(AbstractClient *client, Chain &chain); + Chain m_mostRecentlyUsed; + QHash m_desktopFocusChains; + bool m_separateScreenFocus; + AbstractClient *m_activeClient; + uint m_currentDesktop; + + KWIN_SINGLETON_VARIABLE(FocusChain, s_manager) +}; + +inline +bool FocusChain::contains(AbstractClient *client) const +{ + return m_mostRecentlyUsed.contains(client); +} + +inline +void FocusChain::setSeparateScreenFocus(bool enabled) +{ + m_separateScreenFocus = enabled; +} + +inline +void FocusChain::setActiveClient(AbstractClient *client) +{ + m_activeClient = client; +} + +inline +void FocusChain::setCurrentDesktop(uint previous, uint newDesktop) +{ + Q_UNUSED(previous) + m_currentDesktop = newDesktop; +} + +} // namespace + +#endif // KWIN_FOCUS_CHAIN_H diff --git a/geometrytip.cpp b/geometrytip.cpp new file mode 100644 index 0000000..9545ea6 --- /dev/null +++ b/geometrytip.cpp @@ -0,0 +1,64 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2003 Karol Szwed + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "geometrytip.h" + +namespace KWin +{ + +GeometryTip::GeometryTip(const Xcb::GeometryHints* xSizeHints): + QLabel(nullptr) +{ + setObjectName(QLatin1String("kwingeometry")); + setMargin(1); + setIndent(0); + setLineWidth(1); + setFrameStyle(QFrame::Raised | QFrame::StyledPanel); + setAlignment(Qt::AlignCenter | Qt::AlignTop); + setWindowFlags(Qt::X11BypassWindowManagerHint); + sizeHints = xSizeHints; +} + +GeometryTip::~GeometryTip() +{ +} + +static QString numberWithSign(int n) +{ + const QLocale locale; + const QChar sign = n >= 0 ? locale.positiveSign() : locale.negativeSign(); + return sign + QString::number(std::abs(n)); +} + +void GeometryTip::setGeometry(const QRect& geom) +{ + int w = geom.width(); + int h = geom.height(); + + if (sizeHints) { + if (sizeHints->hasResizeIncrements()) { + w = (w - sizeHints->baseSize().width()) / sizeHints->resizeIncrements().width(); + h = (h - sizeHints->baseSize().height()) / sizeHints->resizeIncrements().height(); + } + } + + h = qMax(h, 0); // in case of isShade() and PBaseSize + const QString pos = QStringLiteral("%1,%2
(%3 x %4)") + .arg(numberWithSign(geom.x())) + .arg(numberWithSign(geom.y())) + .arg(w) + .arg(h); + setText(pos); + adjustSize(); + move(geom.x() + ((geom.width() - width()) / 2), + geom.y() + ((geom.height() - height()) / 2)); +} + +} // namespace + diff --git a/geometrytip.h b/geometrytip.h new file mode 100644 index 0000000..6ca580d --- /dev/null +++ b/geometrytip.h @@ -0,0 +1,33 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2003 Karol Szwed + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_GEOMETRY_TIP_H +#define KWIN_GEOMETRY_TIP_H +#include "xcbutils.h" + +#include + +namespace KWin +{ + +class GeometryTip: public QLabel +{ + Q_OBJECT +public: + GeometryTip(const Xcb::GeometryHints* xSizeHints); + ~GeometryTip() override; + void setGeometry(const QRect& geom); + +private: + const Xcb::GeometryHints* sizeHints; +}; + +} // namespace + +#endif diff --git a/gestures.cpp b/gestures.cpp new file mode 100644 index 0000000..391bb2c --- /dev/null +++ b/gestures.cpp @@ -0,0 +1,207 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "gestures.h" + +#include +#include +#include + +namespace KWin +{ + +Gesture::Gesture(QObject *parent) + : QObject(parent) +{ +} + +Gesture::~Gesture() = default; + +SwipeGesture::SwipeGesture(QObject *parent) + : Gesture(parent) +{ +} + +SwipeGesture::~SwipeGesture() = default; + +void SwipeGesture::setStartGeometry(const QRect &geometry) +{ + setMinimumX(geometry.x()); + setMinimumY(geometry.y()); + setMaximumX(geometry.x() + geometry.width()); + setMaximumY(geometry.y() + geometry.height()); + + Q_ASSERT(m_maximumX >= m_minimumX); + Q_ASSERT(m_maximumY >= m_minimumY); +} + +qreal SwipeGesture::minimumDeltaReachedProgress(const QSizeF &delta) const +{ + if (!m_minimumDeltaRelevant || m_minimumDelta.isNull()) { + return 1.0; + } + switch (m_direction) { + case Direction::Up: + case Direction::Down: + return std::min(std::abs(delta.height()) / std::abs(m_minimumDelta.height()), 1.0); + case Direction::Left: + case Direction::Right: + return std::min(std::abs(delta.width()) / std::abs(m_minimumDelta.width()), 1.0); + default: + Q_UNREACHABLE(); + } +} + +bool SwipeGesture::minimumDeltaReached(const QSizeF &delta) const +{ + return minimumDeltaReachedProgress(delta) >= 1.0; +} + +GestureRecognizer::GestureRecognizer(QObject *parent) + : QObject(parent) +{ +} + +GestureRecognizer::~GestureRecognizer() = default; + +void GestureRecognizer::registerGesture(KWin::Gesture* gesture) +{ + Q_ASSERT(!m_gestures.contains(gesture)); + auto connection = connect(gesture, &QObject::destroyed, this, std::bind(&GestureRecognizer::unregisterGesture, this, gesture)); + m_destroyConnections.insert(gesture, connection); + m_gestures << gesture; +} + +void GestureRecognizer::unregisterGesture(KWin::Gesture* gesture) +{ + auto it = m_destroyConnections.find(gesture); + if (it != m_destroyConnections.end()) { + disconnect(it.value()); + m_destroyConnections.erase(it); + } + m_gestures.removeAll(gesture); + if (m_activeSwipeGestures.removeOne(gesture)) { + emit gesture->cancelled(); + } +} + +int GestureRecognizer::startSwipeGesture(uint fingerCount, const QPointF &startPos, StartPositionBehavior startPosBehavior) +{ + int count = 0; + // TODO: verify that no gesture is running + for (Gesture *gesture : qAsConst(m_gestures)) { + SwipeGesture *swipeGesture = qobject_cast(gesture); + if (!gesture) { + continue; + } + if (swipeGesture->minimumFingerCountIsRelevant()) { + if (swipeGesture->minimumFingerCount() > fingerCount) { + continue; + } + } + if (swipeGesture->maximumFingerCountIsRelevant()) { + if (swipeGesture->maximumFingerCount() < fingerCount) { + continue; + } + } + if (startPosBehavior == StartPositionBehavior::Relevant) { + if (swipeGesture->minimumXIsRelevant()) { + if (swipeGesture->minimumX() > startPos.x()) { + continue; + } + } + if (swipeGesture->maximumXIsRelevant()) { + if (swipeGesture->maximumX() < startPos.x()) { + continue; + } + } + if (swipeGesture->minimumYIsRelevant()) { + if (swipeGesture->minimumY() > startPos.y()) { + continue; + } + } + if (swipeGesture->maximumYIsRelevant()) { + if (swipeGesture->maximumY() < startPos.y()) { + continue; + } + } + } + // direction doesn't matter yet + m_activeSwipeGestures << swipeGesture; + count++; + emit swipeGesture->started(); + } + return count; +} + +void GestureRecognizer::updateSwipeGesture(const QSizeF &delta) +{ + m_swipeUpdates << delta; + if (std::abs(delta.width()) < 1 && std::abs(delta.height()) < 1) { + // some (touch) devices report sub-pixel movement on screen edges + // this often cancels gestures -> ignore these movements + return; + } + // determine the direction of the swipe + if (delta.width() == delta.height()) { + // special case of diagonal, this is not yet supported, thus cancel all gestures + cancelActiveSwipeGestures(); + return; + } + SwipeGesture::Direction direction; + if (std::abs(delta.width()) > std::abs(delta.height())) { + // horizontal + direction = delta.width() < 0 ? SwipeGesture::Direction::Left : SwipeGesture::Direction::Right; + } else { + // vertical + direction = delta.height() < 0 ? SwipeGesture::Direction::Up : SwipeGesture::Direction::Down; + } + const QSizeF combinedDelta = std::accumulate(m_swipeUpdates.constBegin(), m_swipeUpdates.constEnd(), QSizeF(0, 0)); + for (auto it = m_activeSwipeGestures.begin(); it != m_activeSwipeGestures.end();) { + auto g = qobject_cast(*it); + if (g->direction() == direction) { + if (g->isMinimumDeltaRelevant()) { + emit g->progress(g->minimumDeltaReachedProgress(combinedDelta)); + } + it++; + } else { + emit g->cancelled(); + it = m_activeSwipeGestures.erase(it); + } + } +} + +void GestureRecognizer::cancelActiveSwipeGestures() +{ + for (auto g : qAsConst(m_activeSwipeGestures)) { + emit g->cancelled(); + } + m_activeSwipeGestures.clear(); +} + +void GestureRecognizer::cancelSwipeGesture() +{ + cancelActiveSwipeGestures(); + m_swipeUpdates.clear(); +} + +void GestureRecognizer::endSwipeGesture() +{ + const QSizeF delta = std::accumulate(m_swipeUpdates.constBegin(), m_swipeUpdates.constEnd(), QSizeF(0, 0)); + for (auto g : qAsConst(m_activeSwipeGestures)) { + if (static_cast(g)->minimumDeltaReached(delta)) { + emit g->triggered(); + } else { + emit g->cancelled(); + } + } + m_activeSwipeGestures.clear(); + m_swipeUpdates.clear(); +} + +} diff --git a/gestures.h b/gestures.h new file mode 100644 index 0000000..9fc2fad --- /dev/null +++ b/gestures.h @@ -0,0 +1,210 @@ +/* + 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 +*/ +#ifndef KWIN_GESTURES_H +#define KWIN_GESTURES_H + +#include + +#include +#include +#include +#include +#include + +namespace KWin +{ + +class Gesture : public QObject +{ + Q_OBJECT +public: + ~Gesture() override; +protected: + explicit Gesture(QObject *parent); + +Q_SIGNALS: + /** + * Matching of a gesture started and this Gesture might match. + * On further evaluation either the signal @ref triggered or + * @ref cancelled will get emitted. + */ + void started(); + /** + * Gesture matching ended and this Gesture matched. + */ + void triggered(); + /** + * This Gesture no longer matches. + */ + void cancelled(); +}; + +class SwipeGesture : public Gesture +{ + Q_OBJECT +public: + enum class Direction { + Down, + Left, + Up, + Right + }; + + explicit SwipeGesture(QObject *parent = nullptr); + ~SwipeGesture() override; + + bool minimumFingerCountIsRelevant() const { + return m_minimumFingerCountRelevant; + } + void setMinimumFingerCount(uint count) { + m_minimumFingerCount = count; + m_minimumFingerCountRelevant = true; + } + uint minimumFingerCount() const { + return m_minimumFingerCount; + } + + bool maximumFingerCountIsRelevant() const { + return m_maximumFingerCountRelevant; + } + void setMaximumFingerCount(uint count) { + m_maximumFingerCount = count; + m_maximumFingerCountRelevant = true; + } + uint maximumFingerCount() const { + return m_maximumFingerCount; + } + + Direction direction() const { + return m_direction; + } + void setDirection(Direction direction) { + m_direction = direction; + } + + void setMinimumX(int x) { + m_minimumX = x; + m_minimumXRelevant = true; + } + int minimumX() const { + return m_minimumX; + } + bool minimumXIsRelevant() const { + return m_minimumXRelevant; + } + void setMinimumY(int y) { + m_minimumY = y; + m_minimumYRelevant = true; + } + int minimumY() const { + return m_minimumY; + } + bool minimumYIsRelevant() const { + return m_minimumYRelevant; + } + + void setMaximumX(int x) { + m_maximumX = x; + m_maximumXRelevant = true; + } + int maximumX() const { + return m_maximumX; + } + bool maximumXIsRelevant() const { + return m_maximumXRelevant; + } + void setMaximumY(int y) { + m_maximumY = y; + m_maximumYRelevant = true; + } + int maximumY() const { + return m_maximumY; + } + bool maximumYIsRelevant() const { + return m_maximumYRelevant; + } + void setStartGeometry(const QRect &geometry); + + QSizeF minimumDelta() const { + return m_minimumDelta; + } + void setMinimumDelta(const QSizeF &delta) { + m_minimumDelta = delta; + m_minimumDeltaRelevant = true; + } + bool isMinimumDeltaRelevant() const { + return m_minimumDeltaRelevant; + } + + qreal minimumDeltaReachedProgress(const QSizeF &delta) const; + bool minimumDeltaReached(const QSizeF &delta) const; + +Q_SIGNALS: + /** + * The progress of the gesture if a minimumDelta is set. + * The progress is reported in [0.0,1.0] + */ + void progress(qreal); + +private: + bool m_minimumFingerCountRelevant = false; + uint m_minimumFingerCount = 0; + bool m_maximumFingerCountRelevant = false; + uint m_maximumFingerCount = 0; + Direction m_direction = Direction::Down; + bool m_minimumXRelevant = false; + int m_minimumX = 0; + bool m_minimumYRelevant = false; + int m_minimumY = 0; + bool m_maximumXRelevant = false; + int m_maximumX = 0; + bool m_maximumYRelevant = false; + int m_maximumY = 0; + bool m_minimumDeltaRelevant = false; + QSizeF m_minimumDelta; +}; + +class KWIN_EXPORT GestureRecognizer : public QObject +{ + Q_OBJECT +public: + GestureRecognizer(QObject *parent = nullptr); + ~GestureRecognizer() override; + + void registerGesture(Gesture *gesture); + void unregisterGesture(Gesture *gesture); + + int startSwipeGesture(uint fingerCount) { + return startSwipeGesture(fingerCount, QPointF(), StartPositionBehavior::Irrelevant); + } + int startSwipeGesture(const QPointF &startPos) { + return startSwipeGesture(1, startPos, StartPositionBehavior::Relevant); + } + void updateSwipeGesture(const QSizeF &delta); + void cancelSwipeGesture(); + void endSwipeGesture(); + +private: + void cancelActiveSwipeGestures(); + enum class StartPositionBehavior { + Relevant, + Irrelevant + }; + int startSwipeGesture(uint fingerCount, const QPointF &startPos, StartPositionBehavior startPosBehavior); + QVector m_gestures; + QVector m_activeSwipeGestures; + QMap m_destroyConnections; + QVector m_swipeUpdates; +}; + +} + +Q_DECLARE_METATYPE(KWin::SwipeGesture::Direction) + +#endif diff --git a/globalshortcuts.cpp b/globalshortcuts.cpp new file mode 100644 index 0000000..6452143 --- /dev/null +++ b/globalshortcuts.cpp @@ -0,0 +1,301 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +// own +#include "globalshortcuts.h" +// kwin +#include +#include "main.h" +#include "gestures.h" +#include "utils.h" +// KDE +#include +#include +// Qt +#include + +namespace KWin +{ + +uint qHash(SwipeDirection direction) +{ + return uint(direction); +} + +GlobalShortcut::GlobalShortcut(const QKeySequence &shortcut) + : m_shortcut(shortcut) + , m_pointerModifiers(Qt::NoModifier) + , m_pointerButtons(Qt::NoButton) +{ +} + +GlobalShortcut::GlobalShortcut(Qt::KeyboardModifiers pointerButtonModifiers, Qt::MouseButtons pointerButtons) + : m_shortcut(QKeySequence()) + , m_pointerModifiers(pointerButtonModifiers) + , m_pointerButtons(pointerButtons) +{ +} + +GlobalShortcut::GlobalShortcut(Qt::KeyboardModifiers modifiers) + : m_shortcut(QKeySequence()) + , m_pointerModifiers(modifiers) + , m_pointerButtons(Qt::NoButton) +{ +} + +GlobalShortcut::GlobalShortcut(SwipeDirection direction) + : m_shortcut(QKeySequence()) + , m_pointerModifiers(Qt::NoModifier) + , m_pointerButtons(Qt::NoButton) + , m_swipeDirection(direction) +{ +} + +GlobalShortcut::~GlobalShortcut() +{ +} + +InternalGlobalShortcut::InternalGlobalShortcut(Qt::KeyboardModifiers modifiers, const QKeySequence &shortcut, QAction *action) + : GlobalShortcut(shortcut) + , m_action(action) +{ + Q_UNUSED(modifiers) +} + +InternalGlobalShortcut::InternalGlobalShortcut(Qt::KeyboardModifiers pointerButtonModifiers, Qt::MouseButtons pointerButtons, QAction *action) + : GlobalShortcut(pointerButtonModifiers, pointerButtons) + , m_action(action) +{ +} + +InternalGlobalShortcut::InternalGlobalShortcut(Qt::KeyboardModifiers axisModifiers, PointerAxisDirection axis, QAction *action) + : GlobalShortcut(axisModifiers) + , m_action(action) +{ + Q_UNUSED(axis) +} + +static SwipeGesture::Direction toSwipeDirection(SwipeDirection direction) +{ + switch (direction) { + case SwipeDirection::Up: + return SwipeGesture::Direction::Up; + case SwipeDirection::Down: + return SwipeGesture::Direction::Down; + case SwipeDirection::Left: + return SwipeGesture::Direction::Left; + case SwipeDirection::Right: + return SwipeGesture::Direction::Right; + case SwipeDirection::Invalid: + default: + Q_UNREACHABLE(); + } +} + +InternalGlobalShortcut::InternalGlobalShortcut(Qt::KeyboardModifiers swipeModifier, SwipeDirection direction, QAction *action) + : GlobalShortcut(direction) + , m_action(action) + , m_swipe(new SwipeGesture) +{ + Q_UNUSED(swipeModifier) + m_swipe->setDirection(toSwipeDirection(direction)); + m_swipe->setMinimumFingerCount(4); + m_swipe->setMaximumFingerCount(4); + QObject::connect(m_swipe.data(), &SwipeGesture::triggered, m_action, &QAction::trigger, Qt::QueuedConnection); +} + +InternalGlobalShortcut::~InternalGlobalShortcut() +{ +} + +void InternalGlobalShortcut::invoke() +{ + // using QueuedConnection so that we finish the even processing first + QMetaObject::invokeMethod(m_action, "trigger", Qt::QueuedConnection); +} + +GlobalShortcutsManager::GlobalShortcutsManager(QObject *parent) + : QObject(parent) + , m_gestureRecognizer(new GestureRecognizer(this)) +{ +} + +template +void clearShortcuts(T &shortcuts) +{ + for (auto it = shortcuts.begin(); it != shortcuts.end(); ++it) { + qDeleteAll((*it)); + } +} + +GlobalShortcutsManager::~GlobalShortcutsManager() +{ + clearShortcuts(m_pointerShortcuts); + clearShortcuts(m_axisShortcuts); + clearShortcuts(m_swipeShortcuts); +} + +void GlobalShortcutsManager::init() +{ + if (kwinApp()->shouldUseWaylandForCompositing()) { + qputenv("KGLOBALACCELD_PLATFORM", QByteArrayLiteral("org.kde.kwin")); + m_kglobalAccel = new KGlobalAccelD(this); + if (!m_kglobalAccel->init()) { + qCDebug(KWIN_CORE) << "Init of kglobalaccel failed"; + delete m_kglobalAccel; + m_kglobalAccel = nullptr; + } else { + qCDebug(KWIN_CORE) << "KGlobalAcceld inited"; + } + } +} + +template +void handleDestroyedAction(QObject *object, T &shortcuts) +{ + for (auto it = shortcuts.begin(); it != shortcuts.end(); ++it) { + auto &list = it.value(); + auto it2 = list.begin(); + while (it2 != list.end()) { + if (InternalGlobalShortcut *shortcut = dynamic_cast(it2.value())) { + if (shortcut->action() == object) { + it2 = list.erase(it2); + delete shortcut; + continue; + } + } + ++it2; + } + } +} + +void GlobalShortcutsManager::objectDeleted(QObject *object) +{ + handleDestroyedAction(object, m_pointerShortcuts); + handleDestroyedAction(object, m_axisShortcuts); + handleDestroyedAction(object, m_swipeShortcuts); +} + +template +GlobalShortcut *addShortcut(T &shortcuts, QAction *action, Qt::KeyboardModifiers modifiers, R value) +{ + GlobalShortcut *cut = new InternalGlobalShortcut(modifiers, value, action); + auto it = shortcuts.find(modifiers); + if (it != shortcuts.end()) { + // TODO: check if shortcut already exists + (*it).insert(value, cut); + } else { + QHash s; + s.insert(value, cut); + shortcuts.insert(modifiers, s); + } + return cut; +} + +void GlobalShortcutsManager::registerPointerShortcut(QAction *action, Qt::KeyboardModifiers modifiers, Qt::MouseButtons pointerButtons) +{ + addShortcut(m_pointerShortcuts, action, modifiers, pointerButtons); + connect(action, &QAction::destroyed, this, &GlobalShortcutsManager::objectDeleted); +} + +void GlobalShortcutsManager::registerAxisShortcut(QAction *action, Qt::KeyboardModifiers modifiers, PointerAxisDirection axis) +{ + addShortcut(m_axisShortcuts, action, modifiers, axis); + connect(action, &QAction::destroyed, this, &GlobalShortcutsManager::objectDeleted); +} + +void GlobalShortcutsManager::registerTouchpadSwipe(QAction *action, SwipeDirection direction) +{ + auto shortcut = addShortcut(m_swipeShortcuts, action, Qt::NoModifier, direction); + connect(action, &QAction::destroyed, this, &GlobalShortcutsManager::objectDeleted); + m_gestureRecognizer->registerGesture(static_cast(shortcut)->swipeGesture()); +} + +template +bool processShortcut(Qt::KeyboardModifiers mods, T key, U &shortcuts) +{ + auto it = shortcuts.find(mods); + if (it == shortcuts.end()) { + return false; + } + auto it2 = (*it).find(key); + if (it2 == (*it).end()) { + return false; + } + it2.value()->invoke(); + return true; +} + +bool GlobalShortcutsManager::processKey(Qt::KeyboardModifiers mods, int keyQt) +{ + if (m_kglobalAccelInterface) { + if (!keyQt && !mods) { + return false; + } + auto check = [this] (Qt::KeyboardModifiers mods, int keyQt) { + bool retVal = false; + QMetaObject::invokeMethod(m_kglobalAccelInterface, + "checkKeyPressed", + Qt::DirectConnection, + Q_RETURN_ARG(bool, retVal), + Q_ARG(int, int(mods) | keyQt)); + return retVal; + }; + if (check(mods, keyQt)) { + return true; + } else if (keyQt == Qt::Key_Backtab) { + // KGlobalAccel on X11 has some workaround for Backtab + // see kglobalaccel/src/runtime/plugins/xcb/kglobalccel_x11.cpp method x11KeyPress + // Apparently KKeySequenceWidget captures Shift+Tab instead of Backtab + // thus if the key is backtab we should adjust to add shift again and use tab + // in addition KWin registers the shortcut incorrectly as Alt+Shift+Backtab + // this should be changed to either Alt+Backtab or Alt+Shift+Tab to match KKeySequenceWidget + // trying the variants + if (check(mods | Qt::ShiftModifier, keyQt)) { + return true; + } + if (check(mods | Qt::ShiftModifier, Qt::Key_Tab)) { + return true; + } + } + } + return false; +} + +bool GlobalShortcutsManager::processPointerPressed(Qt::KeyboardModifiers mods, Qt::MouseButtons pointerButtons) +{ + return processShortcut(mods, pointerButtons, m_pointerShortcuts); +} + +bool GlobalShortcutsManager::processAxis(Qt::KeyboardModifiers mods, PointerAxisDirection axis) +{ + return processShortcut(mods, axis, m_axisShortcuts); +} + +void GlobalShortcutsManager::processSwipeStart(uint fingerCount) +{ + m_gestureRecognizer->startSwipeGesture(fingerCount); +} + +void GlobalShortcutsManager::processSwipeUpdate(const QSizeF &delta) +{ + m_gestureRecognizer->updateSwipeGesture(delta); +} + +void GlobalShortcutsManager::processSwipeCancel() +{ + m_gestureRecognizer->cancelSwipeGesture(); +} + +void GlobalShortcutsManager::processSwipeEnd() +{ + m_gestureRecognizer->endSwipeGesture(); + // TODO: cancel on Wayland Seat if one triggered +} + +} // namespace diff --git a/globalshortcuts.h b/globalshortcuts.h new file mode 100644 index 0000000..a650b28 --- /dev/null +++ b/globalshortcuts.h @@ -0,0 +1,181 @@ +/* + 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 KWIN_GLOBALSHORTCUTS_H +#define KWIN_GLOBALSHORTCUTS_H +// KWin +#include +// Qt +#include + +class QAction; +class KGlobalAccelD; +class KGlobalAccelInterface; + +namespace KWin +{ + +class GlobalShortcut; +class SwipeGesture; +class GestureRecognizer; + +/** + * @brief Manager for the global shortcut system inside KWin. + * + * This class is responsible for holding all the global shortcuts and to process a key press event. + * That is trigger a shortcut if there is a match. + * + * For internal shortcut handling (those which are delivered inside KWin) QActions are used and + * triggered if the shortcut matches. For external shortcut handling a DBus interface is used. + */ +class GlobalShortcutsManager : public QObject +{ + Q_OBJECT +public: + explicit GlobalShortcutsManager(QObject *parent = nullptr); + ~GlobalShortcutsManager() override; + void init(); + + /** + * @brief Registers an internal global pointer shortcut + * + * @param action The action to trigger if the shortcut is pressed + * @param modifiers The modifiers which need to be hold to trigger the action + * @param pointerButtons The pointer button which needs to be pressed + */ + void registerPointerShortcut(QAction *action, Qt::KeyboardModifiers modifiers, Qt::MouseButtons pointerButtons); + /** + * @brief Registers an internal global axis shortcut + * + * @param action The action to trigger if the shortcut is triggered + * @param modifiers The modifiers which need to be hold to trigger the action + * @param axis The pointer axis + */ + void registerAxisShortcut(QAction *action, Qt::KeyboardModifiers modifiers, PointerAxisDirection axis); + + void registerTouchpadSwipe(QAction *action, SwipeDirection direction); + + /** + * @brief Processes a key event to decide whether a shortcut needs to be triggered. + * + * If a shortcut triggered this method returns @c true to indicate to the caller that the event + * should not be further processed. If there is no shortcut which triggered for the key, then + * @c false is returned. + * + * @param modifiers The current hold modifiers + * @param keyQt The Qt::Key which got pressed + * @return @c true if a shortcut triggered, @c false otherwise + */ + bool processKey(Qt::KeyboardModifiers modifiers, int keyQt); + bool processPointerPressed(Qt::KeyboardModifiers modifiers, Qt::MouseButtons pointerButtons); + /** + * @brief Processes a pointer axis event to decide whether a shortcut needs to be triggered. + * + * If a shortcut triggered this method returns @c true to indicate to the caller that the event + * should not be further processed. If there is no shortcut which triggered for the key, then + * @c false is returned. + * + * @param modifiers The current hold modifiers + * @param axis The axis direction which has triggered this event + * @return @c true if a shortcut triggered, @c false otherwise + */ + bool processAxis(Qt::KeyboardModifiers modifiers, PointerAxisDirection axis); + + void processSwipeStart(uint fingerCount); + void processSwipeUpdate(const QSizeF &delta); + void processSwipeCancel(); + void processSwipeEnd(); + + void setKGlobalAccelInterface(KGlobalAccelInterface *interface) { + m_kglobalAccelInterface = interface; + } + +private: + void objectDeleted(QObject *object); + QHash > m_pointerShortcuts; + QHash > m_axisShortcuts; + QHash > m_swipeShortcuts; + KGlobalAccelD *m_kglobalAccel = nullptr; + KGlobalAccelInterface *m_kglobalAccelInterface = nullptr; + GestureRecognizer *m_gestureRecognizer; +}; + +class GlobalShortcut +{ +public: + virtual ~GlobalShortcut(); + + const QKeySequence &shortcut() const; + Qt::KeyboardModifiers pointerButtonModifiers() const; + Qt::MouseButtons pointerButtons() const; + SwipeDirection swipeDirection() const { + return m_swipeDirection; + } + virtual void invoke() = 0; + +protected: + GlobalShortcut(const QKeySequence &shortcut); + GlobalShortcut(Qt::KeyboardModifiers pointerButtonModifiers, Qt::MouseButtons pointerButtons); + GlobalShortcut(Qt::KeyboardModifiers axisModifiers); + GlobalShortcut(SwipeDirection direction); + +private: + QKeySequence m_shortcut; + Qt::KeyboardModifiers m_pointerModifiers; + Qt::MouseButtons m_pointerButtons; + SwipeDirection m_swipeDirection = SwipeDirection::Invalid;; +}; + +class InternalGlobalShortcut : public GlobalShortcut +{ +public: + InternalGlobalShortcut(Qt::KeyboardModifiers modifiers, const QKeySequence &shortcut, QAction *action); + InternalGlobalShortcut(Qt::KeyboardModifiers pointerButtonModifiers, Qt::MouseButtons pointerButtons, QAction *action); + InternalGlobalShortcut(Qt::KeyboardModifiers axisModifiers, PointerAxisDirection axis, QAction *action); + InternalGlobalShortcut(Qt::KeyboardModifiers swipeModifier, SwipeDirection direction, QAction *action); + ~InternalGlobalShortcut() override; + + void invoke() override; + + QAction *action() const; + + SwipeGesture *swipeGesture() const { + return m_swipe.data(); + } +private: + QAction *m_action; + QScopedPointer m_swipe; +}; + +inline +QAction *InternalGlobalShortcut::action() const +{ + return m_action; +} + +inline +const QKeySequence &GlobalShortcut::shortcut() const +{ + return m_shortcut; +} + +inline +Qt::KeyboardModifiers GlobalShortcut::pointerButtonModifiers() const +{ + return m_pointerModifiers; +} + +inline +Qt::MouseButtons GlobalShortcut::pointerButtons() const +{ + return m_pointerButtons; +} + +} // namespace + +#endif diff --git a/group.cpp b/group.cpp new file mode 100644 index 0000000..395ca08 --- /dev/null +++ b/group.cpp @@ -0,0 +1,125 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +//#define QT_CLEAN_NAMESPACE + +#include "group.h" +#include "workspace.h" +#include "x11client.h" +#include "effects.h" + +#include +#include + +namespace KWin +{ + +//******************************************** +// Group +//******************************************** + +Group::Group(xcb_window_t leader_P) + : leader_client(nullptr), + leader_wid(leader_P), + leader_info(nullptr), + user_time(-1U), + refcount(0) +{ + if (leader_P != XCB_WINDOW_NONE) { + leader_client = workspace()->findClient(Predicate::WindowMatch, leader_P); + leader_info = new NETWinInfo(connection(), leader_P, rootWindow(), + NET::Properties(), NET::WM2StartupId); + } + effect_group = new EffectWindowGroupImpl(this); + workspace()->addGroup(this); +} + +Group::~Group() +{ + delete leader_info; + delete effect_group; +} + +QIcon Group::icon() const +{ + if (leader_client != nullptr) + return leader_client->icon(); + else if (leader_wid != XCB_WINDOW_NONE) { + QIcon ic; + NETWinInfo info(connection(), leader_wid, rootWindow(), NET::WMIcon, NET::WM2IconPixmap); + auto readIcon = [&ic, &info, this](int size, bool scale = true) { + const QPixmap pix = KWindowSystem::icon(leader_wid, size, size, scale, KWindowSystem::NETWM | KWindowSystem::WMHints, &info); + if (!pix.isNull()) { + ic.addPixmap(pix); + } + }; + readIcon(16); + readIcon(32); + readIcon(48, false); + readIcon(64, false); + readIcon(128, false); + return ic; + } + return QIcon(); +} + +void Group::addMember(X11Client *member_P) +{ + _members.append(member_P); +// qDebug() << "GROUPADD:" << this << ":" << member_P; +// qDebug() << kBacktrace(); +} + +void Group::removeMember(X11Client *member_P) +{ +// qDebug() << "GROUPREMOVE:" << this << ":" << member_P; +// qDebug() << kBacktrace(); + Q_ASSERT(_members.contains(member_P)); + _members.removeAll(member_P); +// there are cases when automatic deleting of groups must be delayed, +// e.g. when removing a member and doing some operation on the possibly +// other members of the group (which would be however deleted already +// if there were no other members) + if (refcount == 0 && _members.isEmpty()) { + workspace()->removeGroup(this); + delete this; + } +} + +void Group::ref() +{ + ++refcount; +} + +void Group::deref() +{ + if (--refcount == 0 && _members.isEmpty()) { + workspace()->removeGroup(this); + delete this; + } +} + +void Group::gotLeader(X11Client *leader_P) +{ + Q_ASSERT(leader_P->window() == leader_wid); + leader_client = leader_P; +} + +void Group::lostLeader() +{ + Q_ASSERT(!_members.contains(leader_client)); + leader_client = nullptr; + if (_members.isEmpty()) { + workspace()->removeGroup(this); + delete this; + } +} + +} // namespace diff --git a/group.h b/group.h new file mode 100644 index 0000000..f850527 --- /dev/null +++ b/group.h @@ -0,0 +1,86 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_GROUP_H +#define KWIN_GROUP_H + +#include "utils.h" +#include + +namespace KWin +{ + +class EffectWindowGroupImpl; +class X11Client; + +class Group +{ +public: + Group(xcb_window_t leader); + ~Group(); + xcb_window_t leader() const; + const X11Client *leaderClient() const; + X11Client *leaderClient(); + const QList &members() const; + QIcon icon() const; + void addMember(X11Client *member); + void removeMember(X11Client *member); + void gotLeader(X11Client *leader); + void lostLeader(); + void updateUserTime(xcb_timestamp_t time); + xcb_timestamp_t userTime() const; + void ref(); + void deref(); + EffectWindowGroupImpl* effectGroup(); +private: + void startupIdChanged(); + QList _members; + X11Client *leader_client; + xcb_window_t leader_wid; + NETWinInfo* leader_info; + xcb_timestamp_t user_time; + int refcount; + EffectWindowGroupImpl* effect_group; +}; + +inline xcb_window_t Group::leader() const +{ + return leader_wid; +} + +inline const X11Client *Group::leaderClient() const +{ + return leader_client; +} + +inline X11Client *Group::leaderClient() +{ + return leader_client; +} + +inline const QList &Group::members() const +{ + return _members; +} + +inline xcb_timestamp_t Group::userTime() const +{ + return user_time; +} + +inline +EffectWindowGroupImpl* Group::effectGroup() +{ + return effect_group; +} + +} // namespace + +#endif diff --git a/helpers/CMakeLists.txt b/helpers/CMakeLists.txt new file mode 100644 index 0000000..94e0c0b --- /dev/null +++ b/helpers/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(killer) diff --git a/helpers/killer/CMakeLists.txt b/helpers/killer/CMakeLists.txt new file mode 100644 index 0000000..79812b3 --- /dev/null +++ b/helpers/killer/CMakeLists.txt @@ -0,0 +1,15 @@ +########### next target ############### + +set(kwin_killer_helper_SRCS killer.cpp) + +add_executable(kwin_killer_helper ${kwin_killer_helper_SRCS}) + +target_link_libraries(kwin_killer_helper + KF5::AuthCore + KF5::I18n + KF5::WidgetsAddons + Qt5::Widgets + Qt5::X11Extras +) + +install(TARGETS kwin_killer_helper DESTINATION ${LIBEXEC_INSTALL_DIR}) diff --git a/helpers/killer/killer.cpp b/helpers/killer/killer.cpp new file mode 100644 index 0000000..095abc3 --- /dev/null +++ b/helpers/killer/killer.cpp @@ -0,0 +1,116 @@ +/* + SPDX-FileCopyrightText: 2003 Lubos Lunak + + SPDX-License-Identifier: MIT + +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +int main(int argc, char* argv[]) +{ + KLocalizedString::setApplicationDomain("kwin"); + qputenv("QT_QPA_PLATFORM", QByteArrayLiteral("xcb")); + QApplication app(argc, argv); + app.setAttribute(Qt::AA_UseHighDpiPixmaps, true); + QApplication::setWindowIcon(QIcon::fromTheme(QStringLiteral("dialog-warning"))); + QCoreApplication::setApplicationName(QStringLiteral("kwin_killer_helper")); + QCoreApplication::setOrganizationDomain(QStringLiteral("kde.org")); + QApplication::setApplicationDisplayName(i18n("Window Manager")); + QCoreApplication::setApplicationVersion(QStringLiteral("1.0")); + + + QCommandLineOption pidOption(QStringLiteral("pid"), + i18n("PID of the application to terminate"), i18n("pid")); + QCommandLineOption hostNameOption(QStringLiteral("hostname"), + i18n("Hostname on which the application is running"), i18n("hostname")); + QCommandLineOption windowNameOption(QStringLiteral("windowname"), + i18n("Caption of the window to be terminated"), i18n("caption")); + QCommandLineOption applicationNameOption(QStringLiteral("applicationname"), + i18n("Name of the application to be terminated"), i18n("name")); + QCommandLineOption widOption(QStringLiteral("wid"), + i18n("ID of resource belonging to the application"), i18n("id")); + QCommandLineOption timestampOption(QStringLiteral("timestamp"), + i18n("Time of user action causing termination"), i18n("time")); + QCommandLineParser parser; + parser.setApplicationDescription(i18n("KWin helper utility")); + parser.addHelpOption(); + parser.addVersionOption(); + + parser.addOption(pidOption); + parser.addOption(hostNameOption); + parser.addOption(windowNameOption); + parser.addOption(applicationNameOption); + parser.addOption(widOption); + parser.addOption(timestampOption); + + parser.process(app); + + QString hostname = parser.value(hostNameOption); + bool pid_ok = false; + pid_t pid = parser.value(pidOption).toULong(&pid_ok); + QString caption = parser.value(windowNameOption); + QString appname = parser.value(applicationNameOption); + bool id_ok = false; + xcb_window_t id = parser.value(widOption).toULong(&id_ok); + bool time_ok = false; + xcb_timestamp_t timestamp = parser.value(timestampOption).toULong(&time_ok); + if (!pid_ok || pid == 0 || !id_ok || id == XCB_WINDOW_NONE || !time_ok || timestamp == XCB_TIME_CURRENT_TIME + || hostname.isEmpty() || caption.isEmpty() || appname.isEmpty()) { + fprintf(stdout, "%s\n", qPrintable(i18n("This helper utility is not supposed to be called directly."))); + parser.showHelp(1); + } + bool isLocal = hostname == QStringLiteral("localhost"); + + caption = caption.toHtmlEscaped(); + appname = appname.toHtmlEscaped(); + hostname = hostname.toHtmlEscaped(); + QString pidString = QString::number(pid); // format pid ourself as it does not make sense to format an ID according to locale settings + + QString question = i18nc("@info", "Application \"%1\" is not responding", appname); + question += isLocal + ? xi18nc("@info", "

You tried to close window \"%1\" from application \"%2\" (Process ID: %3) but the application is not responding.

", + caption, appname, pidString) + : xi18nc("@info", "

You tried to close window \"%1\" from application \"%2\" (Process ID: %3), running on host \"%4\", but the application is not responding.

", + caption, appname, pidString, hostname); + question += xi18nc("@info", + "

Do you want to terminate this application?

" + "

Terminating the application will close all of its child windows. Any unsaved data will be lost.

" + ); + + KGuiItem continueButton = KGuiItem(i18n("&Terminate Application %1", appname), QStringLiteral("edit-bomb")); + KGuiItem cancelButton = KGuiItem(i18n("Wait Longer"), QStringLiteral("chronometer")); + QX11Info::setAppUserTime(timestamp); + if (KMessageBox::warningContinueCancelWId(id, question, QString(), continueButton, cancelButton) == KMessageBox::Continue) { + if (!isLocal) { + QStringList lst; + lst << hostname << QStringLiteral("kill") << QString::number(pid); + QProcess::startDetached(QStringLiteral("xon"), lst); + } else { + if (::kill(pid, SIGKILL) && errno == EPERM) { + KAuth::Action killer(QStringLiteral("org.kde.ksysguard.processlisthelper.sendsignal")); + killer.setHelperId(QStringLiteral("org.kde.ksysguard.processlisthelper")); + killer.addArgument(QStringLiteral("pid0"), pid); + killer.addArgument(QStringLiteral("pidcount"), 1); + killer.addArgument(QStringLiteral("signal"), SIGKILL); + if (killer.isValid()) { + qDebug() << "Using KAuth to kill pid: " << pid; + killer.execute(); + } else { + qDebug() << "KWin process killer action not valid"; + } + } + } + } +} diff --git a/idle_inhibition.cpp b/idle_inhibition.cpp new file mode 100644 index 0000000..9f00833 --- /dev/null +++ b/idle_inhibition.cpp @@ -0,0 +1,110 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Flöser + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "idle_inhibition.h" +#include "abstract_client.h" +#include "deleted.h" +#include "workspace.h" + +#include +#include + +#include +#include + +using KWaylandServer::SurfaceInterface; + +namespace KWin +{ + +IdleInhibition::IdleInhibition(IdleInterface *idle) + : QObject(idle) + , m_idle(idle) +{ + // Workspace is created after the wayland server is initialized. + connect(kwinApp(), &Application::workspaceCreated, this, &IdleInhibition::slotWorkspaceCreated); +} + +IdleInhibition::~IdleInhibition() = default; + +void IdleInhibition::registerClient(AbstractClient *client) +{ + auto updateInhibit = [this, client] { + update(client); + }; + + m_connections[client] = connect(client->surface(), &SurfaceInterface::inhibitsIdleChanged, this, updateInhibit); + connect(client, &AbstractClient::desktopChanged, this, updateInhibit); + connect(client, &AbstractClient::clientMinimized, this, updateInhibit); + connect(client, &AbstractClient::clientUnminimized, this, updateInhibit); + connect(client, &AbstractClient::windowHidden, this, updateInhibit); + connect(client, &AbstractClient::windowShown, this, updateInhibit); + connect(client, &AbstractClient::windowClosed, this, + [this, client] { + uninhibit(client); + auto it = m_connections.find(client); + if (it != m_connections.end()) { + disconnect(it.value()); + m_connections.erase(it); + } + } + ); + + updateInhibit(); +} + +void IdleInhibition::inhibit(AbstractClient *client) +{ + if (isInhibited(client)) { + // already inhibited + return; + } + m_idleInhibitors << client; + m_idle->inhibit(); + // TODO: notify powerdevil? +} + +void IdleInhibition::uninhibit(AbstractClient *client) +{ + auto it = std::find(m_idleInhibitors.begin(), m_idleInhibitors.end(), client); + if (it == m_idleInhibitors.end()) { + // not inhibited + return; + } + m_idleInhibitors.erase(it); + m_idle->uninhibit(); +} + +void IdleInhibition::update(AbstractClient *client) +{ + if (client->isInternal()) { + return; + } + + // TODO: Don't honor the idle inhibitor object if the shell client is not + // on the current activity (currently, activities are not supported). + const bool visible = client->isShown(true) && client->isOnCurrentDesktop(); + if (visible && client->surface() && client->surface()->inhibitsIdle()) { + inhibit(client); + } else { + uninhibit(client); + } +} + +void IdleInhibition::slotWorkspaceCreated() +{ + connect(workspace(), &Workspace::currentDesktopChanged, this, &IdleInhibition::slotDesktopChanged); +} + +void IdleInhibition::slotDesktopChanged() +{ + workspace()->forEachAbstractClient([this] (AbstractClient *c) { update(c); }); +} + +} diff --git a/idle_inhibition.h b/idle_inhibition.h new file mode 100644 index 0000000..dc70b1c --- /dev/null +++ b/idle_inhibition.h @@ -0,0 +1,56 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Flöser + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include +#include +#include + +namespace KWaylandServer +{ +class IdleInterface; +} + +using KWaylandServer::IdleInterface; + +namespace KWin +{ +class AbstractClient; + +class IdleInhibition : public QObject +{ + Q_OBJECT +public: + explicit IdleInhibition(IdleInterface *idle); + ~IdleInhibition() override; + + void registerClient(AbstractClient *client); + + bool isInhibited() const { + return !m_idleInhibitors.isEmpty(); + } + bool isInhibited(AbstractClient *client) const { + return m_idleInhibitors.contains(client); + } + +private Q_SLOTS: + void slotWorkspaceCreated(); + void slotDesktopChanged(); + +private: + void inhibit(AbstractClient *client); + void uninhibit(AbstractClient *client); + void update(AbstractClient *client); + + IdleInterface *m_idle; + QVector m_idleInhibitors; + QMap m_connections; +}; +} diff --git a/input.cpp b/input.cpp new file mode 100644 index 0000000..4211ae3 --- /dev/null +++ b/input.cpp @@ -0,0 +1,2766 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + SPDX-FileCopyrightText: 2018 Roman Gilg + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "input.h" +#include "effects.h" +#include "gestures.h" +#include "globalshortcuts.h" +#include "input_event.h" +#include "input_event_spy.h" +#include "keyboard_input.h" +#include "logind.h" +#include "main.h" +#include "pointer_input.h" +#include "tablet_input.h" +#include "touch_hide_cursor_spy.h" +#include "touch_input.h" +#include "x11client.h" +#ifdef KWIN_BUILD_TABBOX +#include "tabbox/tabbox.h" +#endif +#include "internal_client.h" +#include "libinput/connection.h" +#include "libinput/device.h" +#include "platform.h" +#include "popup_input_filter.h" +#include "screenedge.h" +#include "screens.h" +#include "unmanaged.h" +#include "wayland_server.h" +#include "workspace.h" +#include "xwl/xwayland_interface.h" +#include "cursor.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +//screenlocker +#include +// Qt +#include + +#include + +namespace KWin +{ + +InputEventFilter::InputEventFilter() = default; + +InputEventFilter::~InputEventFilter() +{ + if (input()) { + input()->uninstallInputEventFilter(this); + } +} + +bool InputEventFilter::pointerEvent(QMouseEvent *event, quint32 nativeButton) +{ + Q_UNUSED(event) + Q_UNUSED(nativeButton) + return false; +} + +bool InputEventFilter::wheelEvent(QWheelEvent *event) +{ + Q_UNUSED(event) + return false; +} + +bool InputEventFilter::keyEvent(QKeyEvent *event) +{ + Q_UNUSED(event) + return false; +} + +bool InputEventFilter::touchDown(qint32 id, const QPointF &point, quint32 time) +{ + Q_UNUSED(id) + Q_UNUSED(point) + Q_UNUSED(time) + return false; +} + +bool InputEventFilter::touchMotion(qint32 id, const QPointF &point, quint32 time) +{ + Q_UNUSED(id) + Q_UNUSED(point) + Q_UNUSED(time) + return false; +} + +bool InputEventFilter::touchUp(qint32 id, quint32 time) +{ + Q_UNUSED(id) + Q_UNUSED(time) + return false; +} + +bool InputEventFilter::pinchGestureBegin(int fingerCount, quint32 time) +{ + Q_UNUSED(fingerCount) + Q_UNUSED(time) + return false; +} + +bool InputEventFilter::pinchGestureUpdate(qreal scale, qreal angleDelta, const QSizeF &delta, quint32 time) +{ + Q_UNUSED(scale) + Q_UNUSED(angleDelta) + Q_UNUSED(delta) + Q_UNUSED(time) + return false; +} + +bool InputEventFilter::pinchGestureEnd(quint32 time) +{ + Q_UNUSED(time) + return false; +} + +bool InputEventFilter::pinchGestureCancelled(quint32 time) +{ + Q_UNUSED(time) + return false; +} + +bool InputEventFilter::swipeGestureBegin(int fingerCount, quint32 time) +{ + Q_UNUSED(fingerCount) + Q_UNUSED(time) + return false; +} + +bool InputEventFilter::swipeGestureUpdate(const QSizeF &delta, quint32 time) +{ + Q_UNUSED(delta) + Q_UNUSED(time) + return false; +} + +bool InputEventFilter::swipeGestureEnd(quint32 time) +{ + Q_UNUSED(time) + return false; +} + +bool InputEventFilter::swipeGestureCancelled(quint32 time) +{ + Q_UNUSED(time) + return false; +} + +bool InputEventFilter::switchEvent(SwitchEvent *event) +{ + Q_UNUSED(event) + return false; +} + +bool InputEventFilter::tabletToolEvent(TabletEvent *event) +{ + Q_UNUSED(event) + return false; +} + +bool InputEventFilter::tabletToolButtonEvent(const QSet &pressedButtons) +{ + Q_UNUSED(pressedButtons) + return false; +} + +bool InputEventFilter::tabletPadButtonEvent(const QSet &pressedButtons) +{ + Q_UNUSED(pressedButtons) + return false; +} + +bool InputEventFilter::tabletPadStripEvent(int number, int position, bool isFinger) +{ + Q_UNUSED(number) + Q_UNUSED(position) + Q_UNUSED(isFinger) + return false; +} + +bool InputEventFilter::tabletPadRingEvent(int number, int position, bool isFinger) +{ + Q_UNUSED(number) + Q_UNUSED(position) + Q_UNUSED(isFinger) + return false; +} + +void InputEventFilter::passToWaylandServer(QKeyEvent *event) +{ + Q_ASSERT(waylandServer()); + if (event->isAutoRepeat()) { + return; + } + switch (event->type()) { + case QEvent::KeyPress: + waylandServer()->seat()->keyPressed(event->nativeScanCode()); + break; + case QEvent::KeyRelease: + waylandServer()->seat()->keyReleased(event->nativeScanCode()); + break; + default: + break; + } +} + +class VirtualTerminalFilter : public InputEventFilter { +public: + bool keyEvent(QKeyEvent *event) override { + // really on press and not on release? X11 switches on press. + if (event->type() == QEvent::KeyPress && !event->isAutoRepeat()) { + const xkb_keysym_t keysym = event->nativeVirtualKey(); + if (keysym >= XKB_KEY_XF86Switch_VT_1 && keysym <= XKB_KEY_XF86Switch_VT_12) { + LogindIntegration::self()->switchVirtualTerminal(keysym - XKB_KEY_XF86Switch_VT_1 + 1); + return true; + } + } + return false; + } +}; + +class TerminateServerFilter : public InputEventFilter { +public: + bool keyEvent(QKeyEvent *event) override { + if (event->type() == QEvent::KeyPress && !event->isAutoRepeat()) { + if (event->nativeVirtualKey() == XKB_KEY_Terminate_Server) { + qCWarning(KWIN_CORE) << "Request to terminate server"; + QMetaObject::invokeMethod(qApp, "quit", Qt::QueuedConnection); + return true; + } + } + return false; + } +}; + +class LockScreenFilter : public InputEventFilter { +public: + bool pointerEvent(QMouseEvent *event, quint32 nativeButton) override { + if (!waylandServer()->isScreenLocked()) { + return false; + } + auto seat = waylandServer()->seat(); + seat->setTimestamp(event->timestamp()); + if (event->type() == QEvent::MouseMove) { + if (pointerSurfaceAllowed()) { + // TODO: should the pointer position always stay in sync, i.e. not do the check? + seat->setPointerPos(event->screenPos().toPoint()); + } + } else if (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonRelease) { + if (pointerSurfaceAllowed()) { + // TODO: can we leak presses/releases here when we move the mouse in between from an allowed surface to + // disallowed one or vice versa? + event->type() == QEvent::MouseButtonPress ? seat->pointerButtonPressed(nativeButton) : seat->pointerButtonReleased(nativeButton); + } + } + return true; + } + bool wheelEvent(QWheelEvent *event) override { + if (!waylandServer()->isScreenLocked()) { + return false; + } + auto seat = waylandServer()->seat(); + if (pointerSurfaceAllowed()) { + seat->setTimestamp(event->timestamp()); + const Qt::Orientation orientation = event->angleDelta().x() == 0 ? Qt::Vertical : Qt::Horizontal; + seat->pointerAxis(orientation, orientation == Qt::Horizontal ? event->angleDelta().x() : event->angleDelta().y()); + } + return true; + } + bool keyEvent(QKeyEvent * event) override { + if (!waylandServer()->isScreenLocked()) { + return false; + } + if (event->isAutoRepeat()) { + // wayland client takes care of it + return true; + } + // send event to KSldApp for global accel + // if event is set to accepted it means a whitelisted shortcut was triggered + // in that case we filter it out and don't process it further + event->setAccepted(false); + QCoreApplication::sendEvent(ScreenLocker::KSldApp::self(), event); + if (event->isAccepted()) { + return true; + } + + // continue normal processing + input()->keyboard()->update(); + auto seat = waylandServer()->seat(); + seat->setTimestamp(event->timestamp()); + if (!keyboardSurfaceAllowed()) { + // don't pass event to seat + return true; + } + switch (event->type()) { + case QEvent::KeyPress: + seat->keyPressed(event->nativeScanCode()); + break; + case QEvent::KeyRelease: + seat->keyReleased(event->nativeScanCode()); + break; + default: + break; + } + return true; + } + bool touchDown(qint32 id, const QPointF &pos, quint32 time) override { + if (!waylandServer()->isScreenLocked()) { + return false; + } + auto seat = waylandServer()->seat(); + seat->setTimestamp(time); + if (touchSurfaceAllowed()) { + input()->touch()->insertId(id, seat->touchDown(pos)); + } + return true; + } + bool touchMotion(qint32 id, const QPointF &pos, quint32 time) override { + if (!waylandServer()->isScreenLocked()) { + return false; + } + auto seat = waylandServer()->seat(); + seat->setTimestamp(time); + if (touchSurfaceAllowed()) { + const qint32 kwaylandId = input()->touch()->mappedId(id); + if (kwaylandId != -1) { + seat->touchMove(kwaylandId, pos); + } + } + return true; + } + bool touchUp(qint32 id, quint32 time) override { + if (!waylandServer()->isScreenLocked()) { + return false; + } + auto seat = waylandServer()->seat(); + seat->setTimestamp(time); + if (touchSurfaceAllowed()) { + const qint32 kwaylandId = input()->touch()->mappedId(id); + if (kwaylandId != -1) { + seat->touchUp(kwaylandId); + input()->touch()->removeId(id); + } + } + return true; + } + bool pinchGestureBegin(int fingerCount, quint32 time) override { + Q_UNUSED(fingerCount) + Q_UNUSED(time) + // no touchpad multi-finger gestures on lock screen + return waylandServer()->isScreenLocked(); + } + bool pinchGestureUpdate(qreal scale, qreal angleDelta, const QSizeF &delta, quint32 time) override { + Q_UNUSED(scale) + Q_UNUSED(angleDelta) + Q_UNUSED(delta) + Q_UNUSED(time) + // no touchpad multi-finger gestures on lock screen + return waylandServer()->isScreenLocked(); + } + bool pinchGestureEnd(quint32 time) override { + Q_UNUSED(time) + // no touchpad multi-finger gestures on lock screen + return waylandServer()->isScreenLocked(); + } + bool pinchGestureCancelled(quint32 time) override { + Q_UNUSED(time) + // no touchpad multi-finger gestures on lock screen + return waylandServer()->isScreenLocked(); + } + + bool swipeGestureBegin(int fingerCount, quint32 time) override { + Q_UNUSED(fingerCount) + Q_UNUSED(time) + // no touchpad multi-finger gestures on lock screen + return waylandServer()->isScreenLocked(); + } + bool swipeGestureUpdate(const QSizeF &delta, quint32 time) override { + Q_UNUSED(delta) + Q_UNUSED(time) + // no touchpad multi-finger gestures on lock screen + return waylandServer()->isScreenLocked(); + } + bool swipeGestureEnd(quint32 time) override { + Q_UNUSED(time) + // no touchpad multi-finger gestures on lock screen + return waylandServer()->isScreenLocked(); + } + bool swipeGestureCancelled(quint32 time) override { + Q_UNUSED(time) + // no touchpad multi-finger gestures on lock screen + return waylandServer()->isScreenLocked(); + } +private: + bool surfaceAllowed(KWaylandServer::SurfaceInterface *(KWaylandServer::SeatInterface::*method)() const) const { + if (KWaylandServer::SurfaceInterface *s = (waylandServer()->seat()->*method)()) { + if (Toplevel *t = waylandServer()->findClient(s)) { + return t->isLockScreen() || t->isInputMethod(); + } + return false; + } + return true; + } + bool pointerSurfaceAllowed() const { + return surfaceAllowed(&KWaylandServer::SeatInterface::focusedPointerSurface); + } + bool keyboardSurfaceAllowed() const { + return surfaceAllowed(&KWaylandServer::SeatInterface::focusedKeyboardSurface); + } + bool touchSurfaceAllowed() const { + return surfaceAllowed(&KWaylandServer::SeatInterface::focusedTouchSurface); + } +}; + +class EffectsFilter : public InputEventFilter { +public: + bool pointerEvent(QMouseEvent *event, quint32 nativeButton) override { + Q_UNUSED(nativeButton) + if (!effects) { + return false; + } + return static_cast(effects)->checkInputWindowEvent(event); + } + bool wheelEvent(QWheelEvent *event) override { + if (!effects) { + return false; + } + return static_cast(effects)->checkInputWindowEvent(event); + } + bool keyEvent(QKeyEvent *event) override { + if (!effects || !static_cast< EffectsHandlerImpl* >(effects)->hasKeyboardGrab()) { + return false; + } + waylandServer()->seat()->setFocusedKeyboardSurface(nullptr); + passToWaylandServer(event); + static_cast< EffectsHandlerImpl* >(effects)->grabbedKeyboardEvent(event); + return true; + } + bool touchDown(qint32 id, const QPointF &pos, quint32 time) override { + if (!effects) { + return false; + } + return static_cast< EffectsHandlerImpl* >(effects)->touchDown(id, pos, time); + } + bool touchMotion(qint32 id, const QPointF &pos, quint32 time) override { + if (!effects) { + return false; + } + return static_cast< EffectsHandlerImpl* >(effects)->touchMotion(id, pos, time); + } + bool touchUp(qint32 id, quint32 time) override { + if (!effects) { + return false; + } + return static_cast< EffectsHandlerImpl* >(effects)->touchUp(id, time); + } +}; + +class MoveResizeFilter : public InputEventFilter { +public: + bool pointerEvent(QMouseEvent *event, quint32 nativeButton) override { + Q_UNUSED(nativeButton) + AbstractClient *c = workspace()->moveResizeClient(); + if (!c) { + return false; + } + switch (event->type()) { + case QEvent::MouseMove: + c->updateMoveResize(event->screenPos().toPoint()); + break; + case QEvent::MouseButtonRelease: + if (event->buttons() == Qt::NoButton) { + c->endMoveResize(); + } + break; + default: + break; + } + return true; + } + bool wheelEvent(QWheelEvent *event) override { + Q_UNUSED(event) + // filter out while moving a window + return workspace()->moveResizeClient() != nullptr; + } + bool keyEvent(QKeyEvent *event) override { + AbstractClient *c = workspace()->moveResizeClient(); + if (!c) { + return false; + } + if (event->type() == QEvent::KeyPress) { + c->keyPressEvent(event->key() | event->modifiers()); + if (c->isMove() || c->isResize()) { + // only update if mode didn't end + c->updateMoveResize(input()->globalPointer()); + } + } + return true; + } + + bool touchDown(qint32 id, const QPointF &pos, quint32 time) override { + Q_UNUSED(id) + Q_UNUSED(pos) + Q_UNUSED(time) + AbstractClient *c = workspace()->moveResizeClient(); + if (!c) { + return false; + } + return true; + } + + bool touchMotion(qint32 id, const QPointF &pos, quint32 time) override { + Q_UNUSED(time) + AbstractClient *c = workspace()->moveResizeClient(); + if (!c) { + return false; + } + if (!m_set) { + m_id = id; + m_set = true; + } + if (m_id == id) { + c->updateMoveResize(pos.toPoint()); + } + return true; + } + + bool touchUp(qint32 id, quint32 time) override { + Q_UNUSED(time) + AbstractClient *c = workspace()->moveResizeClient(); + if (!c) { + return false; + } + if (m_id == id || !m_set) { + c->endMoveResize(); + m_set = false; + // pass through to update decoration filter later on + return false; + } + m_set = false; + return true; + } +private: + qint32 m_id = 0; + bool m_set = false; +}; + +class WindowSelectorFilter : public InputEventFilter { +public: + bool pointerEvent(QMouseEvent *event, quint32 nativeButton) override { + Q_UNUSED(nativeButton) + if (!m_active) { + return false; + } + switch (event->type()) { + case QEvent::MouseButtonRelease: + if (event->buttons() == Qt::NoButton) { + if (event->button() == Qt::RightButton) { + cancel(); + } else { + accept(event->globalPos()); + } + } + break; + default: + break; + } + return true; + } + bool wheelEvent(QWheelEvent *event) override { + Q_UNUSED(event) + // filter out while selecting a window + return m_active; + } + bool keyEvent(QKeyEvent *event) override { + Q_UNUSED(event) + if (!m_active) { + return false; + } + waylandServer()->seat()->setFocusedKeyboardSurface(nullptr); + passToWaylandServer(event); + + if (event->type() == QEvent::KeyPress) { + // x11 variant does this on key press, so do the same + if (event->key() == Qt::Key_Escape) { + cancel(); + } else if (event->key() == Qt::Key_Enter || + event->key() == Qt::Key_Return || + event->key() == Qt::Key_Space) { + accept(input()->globalPointer()); + } + if (input()->supportsPointerWarping()) { + int mx = 0; + int my = 0; + if (event->key() == Qt::Key_Left) { + mx = -10; + } + if (event->key() == Qt::Key_Right) { + mx = 10; + } + if (event->key() == Qt::Key_Up) { + my = -10; + } + if (event->key() == Qt::Key_Down) { + my = 10; + } + if (event->modifiers() & Qt::ControlModifier) { + mx /= 10; + my /= 10; + } + input()->warpPointer(input()->globalPointer() + QPointF(mx, my)); + } + } + // filter out while selecting a window + return true; + } + + bool touchDown(qint32 id, const QPointF &pos, quint32 time) override { + Q_UNUSED(time) + if (!isActive()) { + return false; + } + m_touchPoints.insert(id, pos); + return true; + } + + bool touchMotion(qint32 id, const QPointF &pos, quint32 time) override { + Q_UNUSED(time) + if (!isActive()) { + return false; + } + auto it = m_touchPoints.find(id); + if (it != m_touchPoints.end()) { + *it = pos; + } + return true; + } + + bool touchUp(qint32 id, quint32 time) override { + Q_UNUSED(time) + if (!isActive()) { + return false; + } + auto it = m_touchPoints.find(id); + if (it != m_touchPoints.end()) { + const auto pos = it.value(); + m_touchPoints.erase(it); + if (m_touchPoints.isEmpty()) { + accept(pos); + } + } + return true; + } + + bool isActive() const { + return m_active; + } + void start(std::function callback) { + Q_ASSERT(!m_active); + m_active = true; + m_callback = callback; + input()->keyboard()->update(); + input()->cancelTouch(); + } + void start(std::function callback) { + Q_ASSERT(!m_active); + m_active = true; + m_pointSelectionFallback = callback; + input()->keyboard()->update(); + input()->cancelTouch(); + } +private: + void deactivate() { + m_active = false; + m_callback = std::function(); + m_pointSelectionFallback = std::function(); + input()->pointer()->removeWindowSelectionCursor(); + input()->keyboard()->update(); + m_touchPoints.clear(); + } + void cancel() { + if (m_callback) { + m_callback(nullptr); + } + if (m_pointSelectionFallback) { + m_pointSelectionFallback(QPoint(-1, -1)); + } + deactivate(); + } + void accept(const QPoint &pos) { + if (m_callback) { + // TODO: this ignores shaped windows + m_callback(input()->findToplevel(pos)); + } + if (m_pointSelectionFallback) { + m_pointSelectionFallback(pos); + } + deactivate(); + } + void accept(const QPointF &pos) { + accept(pos.toPoint()); + } + bool m_active = false; + std::function m_callback; + std::function m_pointSelectionFallback; + QMap m_touchPoints; +}; + +class GlobalShortcutFilter : public InputEventFilter { +public: + GlobalShortcutFilter() { + m_powerDown = new QTimer; + m_powerDown->setSingleShot(true); + m_powerDown->setInterval(1000); + } + ~GlobalShortcutFilter() { + delete m_powerDown; + } + + bool pointerEvent(QMouseEvent *event, quint32 nativeButton) override { + Q_UNUSED(nativeButton); + if (event->type() == QEvent::MouseButtonPress) { + if (input()->shortcuts()->processPointerPressed(event->modifiers(), event->buttons())) { + return true; + } + } + return false; + } + bool wheelEvent(QWheelEvent *event) override { + if (event->modifiers() == Qt::NoModifier) { + return false; + } + PointerAxisDirection direction = PointerAxisUp; + if (event->angleDelta().x() < 0) { + direction = PointerAxisRight; + } else if (event->angleDelta().x() > 0) { + direction = PointerAxisLeft; + } else if (event->angleDelta().y() < 0) { + direction = PointerAxisDown; + } else if (event->angleDelta().y() > 0) { + direction = PointerAxisUp; + } + return input()->shortcuts()->processAxis(event->modifiers(), direction); + } + bool keyEvent(QKeyEvent *event) override { + if (event->key() == Qt::Key_PowerOff) { + const auto modifiers = static_cast(event)->modifiersRelevantForGlobalShortcuts(); + if (event->type() == QEvent::KeyPress && !event->isAutoRepeat()) { + QObject::connect(m_powerDown, &QTimer::timeout, input()->shortcuts(), [this, modifiers] { + QObject::disconnect(m_powerDown, &QTimer::timeout, input()->shortcuts(), nullptr); + m_powerDown->stop(); + input()->shortcuts()->processKey(modifiers, Qt::Key_PowerDown); + }); + m_powerDown->start(); + return true; + } else if (event->type() == QEvent::KeyRelease) { + const bool ret = !m_powerDown->isActive() || input()->shortcuts()->processKey(modifiers, event->key()); + m_powerDown->stop(); + return ret; + } + } else if (event->type() == QEvent::KeyPress) { + if (!waylandServer()->isKeyboardShortcutsInhibited()) { + return input()->shortcuts()->processKey(static_cast(event)->modifiersRelevantForGlobalShortcuts(), event->key()); + } + } + return false; + } + bool swipeGestureBegin(int fingerCount, quint32 time) override { + Q_UNUSED(time) + input()->shortcuts()->processSwipeStart(fingerCount); + return false; + } + bool swipeGestureUpdate(const QSizeF &delta, quint32 time) override { + Q_UNUSED(time) + input()->shortcuts()->processSwipeUpdate(delta); + return false; + } + bool swipeGestureCancelled(quint32 time) override { + Q_UNUSED(time) + input()->shortcuts()->processSwipeCancel(); + return false; + } + bool swipeGestureEnd(quint32 time) override { + Q_UNUSED(time) + input()->shortcuts()->processSwipeEnd(); + return false; + } + +private: + QTimer* m_powerDown = nullptr; +}; + + +namespace { + +enum class MouseAction { + ModifierOnly, + ModifierAndWindow +}; +std::pair performClientMouseAction(QMouseEvent *event, AbstractClient *client, MouseAction action = MouseAction::ModifierOnly) +{ + Options::MouseCommand command = Options::MouseNothing; + bool wasAction = false; + if (static_cast(event)->modifiersRelevantForGlobalShortcuts() == options->commandAllModifier()) { + if (!input()->pointer()->isConstrained() && !workspace()->globalShortcutsDisabled()) { + wasAction = true; + switch (event->button()) { + case Qt::LeftButton: + command = options->commandAll1(); + break; + case Qt::MiddleButton: + command = options->commandAll2(); + break; + case Qt::RightButton: + command = options->commandAll3(); + break; + default: + // nothing + break; + } + } + } else { + if (action == MouseAction::ModifierAndWindow) { + command = client->getMouseCommand(event->button(), &wasAction); + } + } + if (wasAction) { + return std::make_pair(wasAction, !client->performMouseCommand(command, event->globalPos())); + } + return std::make_pair(wasAction, false); +} + +std::pair performClientWheelAction(QWheelEvent *event, AbstractClient *c, MouseAction action = MouseAction::ModifierOnly) +{ + bool wasAction = false; + Options::MouseCommand command = Options::MouseNothing; + if (static_cast(event)->modifiersRelevantForGlobalShortcuts() == options->commandAllModifier()) { + if (!input()->pointer()->isConstrained() && !workspace()->globalShortcutsDisabled()) { + wasAction = true; + command = options->operationWindowMouseWheel(-1 * event->angleDelta().y()); + } + } else { + if (action == MouseAction::ModifierAndWindow) { + command = c->getWheelCommand(Qt::Vertical, &wasAction); + } + } + if (wasAction) { + return std::make_pair(wasAction, !c->performMouseCommand(command, event->globalPos())); + } + return std::make_pair(wasAction, false); +} + +} + +class InternalWindowEventFilter : public InputEventFilter { + bool pointerEvent(QMouseEvent *event, quint32 nativeButton) override { + Q_UNUSED(nativeButton) + auto internal = input()->pointer()->internalWindow(); + if (!internal) { + return false; + } + // find client + switch (event->type()) + { + case QEvent::MouseButtonPress: + case QEvent::MouseButtonRelease: { + auto s = qobject_cast(workspace()->findInternal(internal)); + if (s && s->isDecorated()) { + // only perform mouse commands on decorated internal windows + const auto actionResult = performClientMouseAction(event, s); + if (actionResult.first) { + return actionResult.second; + } + } + break; + } + default: + break; + } + QMouseEvent e(event->type(), + event->pos() - internal->position(), + event->globalPos(), + event->button(), event->buttons(), event->modifiers()); + e.setAccepted(false); + QCoreApplication::sendEvent(internal.data(), &e); + return e.isAccepted(); + } + bool wheelEvent(QWheelEvent *event) override { + auto internal = input()->pointer()->internalWindow(); + if (!internal) { + return false; + } + if (event->angleDelta().y() != 0) { + auto s = qobject_cast(workspace()->findInternal(internal)); + if (s && s->isDecorated()) { + // client window action only on vertical scrolling + const auto actionResult = performClientWheelAction(event, s); + if (actionResult.first) { + return actionResult.second; + } + } + } + const QPointF localPos = event->globalPosF() - QPointF(internal->x(), internal->y()); + const Qt::Orientation orientation = (event->angleDelta().x() != 0) ? Qt::Horizontal : Qt::Vertical; + const int delta = event->angleDelta().x() != 0 ? event->angleDelta().x() : event->angleDelta().y(); + QWheelEvent e(localPos, event->globalPosF(), QPoint(), + event->angleDelta() * -1, + delta * -1, + orientation, + event->buttons(), + event->modifiers()); + e.setAccepted(false); + QCoreApplication::sendEvent(internal.data(), &e); + return e.isAccepted(); + } + bool keyEvent(QKeyEvent *event) override { + const QList &internalClients = workspace()->internalClients(); + if (internalClients.isEmpty()) { + return false; + } + QWindow *found = nullptr; + auto it = internalClients.end(); + do { + it--; + if (QWindow *w = (*it)->internalWindow()) { + if (!w->isVisible()) { + continue; + } + if (!screens()->geometry().contains(w->geometry())) { + continue; + } + if (w->property("_q_showWithoutActivating").toBool()) { + continue; + } + if (w->property("outputOnly").toBool()) { + continue; + } + if (w->flags().testFlag(Qt::ToolTip)) { + continue; + } + found = w; + break; + } + } while (it != internalClients.begin()); + if (!found) { + return false; + } + auto xkb = input()->keyboard()->xkb(); + Qt::Key key = xkb->toQtKey(xkb->toKeysym(event->nativeScanCode())); + if (key == Qt::Key_Super_L || key == Qt::Key_Super_R) { + // workaround for QTBUG-62102 + key = Qt::Key_Meta; + } + QKeyEvent internalEvent(event->type(), key, + event->modifiers(), event->nativeScanCode(), event->nativeVirtualKey(), + event->nativeModifiers(), event->text()); + internalEvent.setAccepted(false); + if (QCoreApplication::sendEvent(found, &internalEvent)) { + waylandServer()->seat()->setFocusedKeyboardSurface(nullptr); + passToWaylandServer(event); + return true; + } + return false; + } + + bool touchDown(qint32 id, const QPointF &pos, quint32 time) override { + auto seat = waylandServer()->seat(); + if (seat->isTouchSequence()) { + // something else is getting the events + return false; + } + auto touch = input()->touch(); + if (touch->internalPressId() != -1) { + // already on internal window, ignore further touch points, but filter out + return true; + } + // a new touch point + seat->setTimestamp(time); + auto internal = touch->internalWindow(); + if (!internal) { + return false; + } + touch->setInternalPressId(id); + // Qt's touch event API is rather complex, let's do fake mouse events instead + m_lastGlobalTouchPos = pos; + m_lastLocalTouchPos = pos - QPointF(internal->x(), internal->y()); + + QEnterEvent enterEvent(m_lastLocalTouchPos, m_lastLocalTouchPos, pos); + QCoreApplication::sendEvent(internal.data(), &enterEvent); + + QMouseEvent e(QEvent::MouseButtonPress, m_lastLocalTouchPos, pos, Qt::LeftButton, Qt::LeftButton, input()->keyboardModifiers()); + e.setAccepted(false); + QCoreApplication::sendEvent(internal.data(), &e); + return true; + } + bool touchMotion(qint32 id, const QPointF &pos, quint32 time) override { + auto touch = input()->touch(); + auto internal = touch->internalWindow(); + if (!internal) { + return false; + } + if (touch->internalPressId() == -1) { + return false; + } + waylandServer()->seat()->setTimestamp(time); + if (touch->internalPressId() != qint32(id)) { + // ignore, but filter out + return true; + } + m_lastGlobalTouchPos = pos; + m_lastLocalTouchPos = pos - QPointF(internal->x(), internal->y()); + + QMouseEvent e(QEvent::MouseMove, m_lastLocalTouchPos, m_lastGlobalTouchPos, Qt::LeftButton, Qt::LeftButton, input()->keyboardModifiers()); + QCoreApplication::instance()->sendEvent(internal.data(), &e); + return true; + } + bool touchUp(qint32 id, quint32 time) override { + auto touch = input()->touch(); + auto internal = touch->internalWindow(); + if (!internal) { + return false; + } + if (touch->internalPressId() == -1) { + return false; + } + waylandServer()->seat()->setTimestamp(time); + if (touch->internalPressId() != qint32(id)) { + // ignore, but filter out + return true; + } + // send mouse up + QMouseEvent e(QEvent::MouseButtonRelease, m_lastLocalTouchPos, m_lastGlobalTouchPos, Qt::LeftButton, Qt::MouseButtons(), input()->keyboardModifiers()); + e.setAccepted(false); + QCoreApplication::sendEvent(internal.data(), &e); + + QEvent leaveEvent(QEvent::Leave); + QCoreApplication::sendEvent(internal.data(), &leaveEvent); + + m_lastGlobalTouchPos = QPointF(); + m_lastLocalTouchPos = QPointF(); + input()->touch()->setInternalPressId(-1); + return true; + } +private: + QPointF m_lastGlobalTouchPos; + QPointF m_lastLocalTouchPos; +}; + +class DecorationEventFilter : public InputEventFilter { +public: + bool pointerEvent(QMouseEvent *event, quint32 nativeButton) override { + Q_UNUSED(nativeButton) + auto decoration = input()->pointer()->decoration(); + if (!decoration) { + return false; + } + const QPointF p = event->globalPos() - decoration->client()->pos(); + switch (event->type()) { + case QEvent::MouseMove: { + QHoverEvent e(QEvent::HoverMove, p, p); + QCoreApplication::instance()->sendEvent(decoration->decoration(), &e); + decoration->client()->processDecorationMove(p.toPoint(), event->globalPos()); + return true; + } + case QEvent::MouseButtonPress: + case QEvent::MouseButtonRelease: { + const auto actionResult = performClientMouseAction(event, decoration->client()); + if (actionResult.first) { + return actionResult.second; + } + QMouseEvent e(event->type(), p, event->globalPos(), event->button(), event->buttons(), event->modifiers()); + e.setAccepted(false); + QCoreApplication::sendEvent(decoration->decoration(), &e); + if (!e.isAccepted() && event->type() == QEvent::MouseButtonPress) { + decoration->client()->processDecorationButtonPress(&e); + } + if (event->type() == QEvent::MouseButtonRelease) { + decoration->client()->processDecorationButtonRelease(&e); + } + return true; + } + default: + break; + } + return false; + } + bool wheelEvent(QWheelEvent *event) override { + auto decoration = input()->pointer()->decoration(); + if (!decoration) { + return false; + } + if (event->angleDelta().y() != 0) { + // client window action only on vertical scrolling + const auto actionResult = performClientWheelAction(event, decoration->client()); + if (actionResult.first) { + return actionResult.second; + } + } + const QPointF localPos = event->globalPosF() - decoration->client()->pos(); + const Qt::Orientation orientation = (event->angleDelta().x() != 0) ? Qt::Horizontal : Qt::Vertical; + const int delta = event->angleDelta().x() != 0 ? event->angleDelta().x() : event->angleDelta().y(); + QWheelEvent e(localPos, event->globalPosF(), QPoint(), + event->angleDelta(), + delta, + orientation, + event->buttons(), + event->modifiers()); + e.setAccepted(false); + QCoreApplication::sendEvent(decoration.data(), &e); + if (e.isAccepted()) { + return true; + } + if ((orientation == Qt::Vertical) && decoration->client()->titlebarPositionUnderMouse()) { + decoration->client()->performMouseCommand(options->operationTitlebarMouseWheel(delta * -1), + event->globalPosF().toPoint()); + } + return true; + } + bool touchDown(qint32 id, const QPointF &pos, quint32 time) override { + auto seat = waylandServer()->seat(); + if (seat->isTouchSequence()) { + return false; + } + if (input()->touch()->decorationPressId() != -1) { + // already on a decoration, ignore further touch points, but filter out + return true; + } + seat->setTimestamp(time); + auto decoration = input()->touch()->decoration(); + if (!decoration) { + return false; + } + + input()->touch()->setDecorationPressId(id); + m_lastGlobalTouchPos = pos; + m_lastLocalTouchPos = pos - decoration->client()->pos(); + + QHoverEvent hoverEvent(QEvent::HoverMove, m_lastLocalTouchPos, m_lastLocalTouchPos); + QCoreApplication::sendEvent(decoration->decoration(), &hoverEvent); + + QMouseEvent e(QEvent::MouseButtonPress, m_lastLocalTouchPos, pos, Qt::LeftButton, Qt::LeftButton, input()->keyboardModifiers()); + e.setAccepted(false); + QCoreApplication::sendEvent(decoration->decoration(), &e); + if (!e.isAccepted()) { + decoration->client()->processDecorationButtonPress(&e); + } + return true; + } + bool touchMotion(qint32 id, const QPointF &pos, quint32 time) override { + Q_UNUSED(time) + auto decoration = input()->touch()->decoration(); + if (!decoration) { + return false; + } + if (input()->touch()->decorationPressId() == -1) { + return false; + } + if (input()->touch()->decorationPressId() != qint32(id)) { + // ignore, but filter out + return true; + } + m_lastGlobalTouchPos = pos; + m_lastLocalTouchPos = pos - decoration->client()->pos(); + + QHoverEvent e(QEvent::HoverMove, m_lastLocalTouchPos, m_lastLocalTouchPos); + QCoreApplication::instance()->sendEvent(decoration->decoration(), &e); + decoration->client()->processDecorationMove(m_lastLocalTouchPos.toPoint(), pos.toPoint()); + return true; + } + bool touchUp(qint32 id, quint32 time) override { + Q_UNUSED(time); + auto decoration = input()->touch()->decoration(); + if (!decoration) { + return false; + } + if (input()->touch()->decorationPressId() == -1) { + return false; + } + if (input()->touch()->decorationPressId() != qint32(id)) { + // ignore, but filter out + return true; + } + + // send mouse up + QMouseEvent e(QEvent::MouseButtonRelease, m_lastLocalTouchPos, m_lastGlobalTouchPos, Qt::LeftButton, Qt::MouseButtons(), input()->keyboardModifiers()); + e.setAccepted(false); + QCoreApplication::sendEvent(decoration->decoration(), &e); + decoration->client()->processDecorationButtonRelease(&e); + + QHoverEvent leaveEvent(QEvent::HoverLeave, QPointF(), QPointF()); + QCoreApplication::sendEvent(decoration->decoration(), &leaveEvent); + + m_lastGlobalTouchPos = QPointF(); + m_lastLocalTouchPos = QPointF(); + input()->touch()->setDecorationPressId(-1); + return true; + } +private: + QPointF m_lastGlobalTouchPos; + QPointF m_lastLocalTouchPos; +}; + +#ifdef KWIN_BUILD_TABBOX +class TabBoxInputFilter : public InputEventFilter +{ +public: + bool pointerEvent(QMouseEvent *event, quint32 button) override { + Q_UNUSED(button) + if (!TabBox::TabBox::self() || !TabBox::TabBox::self()->isGrabbed()) { + return false; + } + return TabBox::TabBox::self()->handleMouseEvent(event); + } + bool keyEvent(QKeyEvent *event) override { + if (!TabBox::TabBox::self() || !TabBox::TabBox::self()->isGrabbed()) { + return false; + } + auto seat = waylandServer()->seat(); + seat->setFocusedKeyboardSurface(nullptr); + input()->pointer()->setEnableConstraints(false); + // pass the key event to the seat, so that it has a proper model of the currently hold keys + // this is important for combinations like alt+shift to ensure that shift is not considered pressed + passToWaylandServer(event); + + if (event->type() == QEvent::KeyPress) { + TabBox::TabBox::self()->keyPress(event->modifiers() | event->key()); + } else if (static_cast(event)->modifiersRelevantForGlobalShortcuts() == Qt::NoModifier) { + TabBox::TabBox::self()->modifiersReleased(); + } + return true; + } + bool wheelEvent(QWheelEvent *event) override { + if (!TabBox::TabBox::self() || !TabBox::TabBox::self()->isGrabbed()) { + return false; + } + return TabBox::TabBox::self()->handleWheelEvent(event); + } +}; +#endif + +class ScreenEdgeInputFilter : public InputEventFilter +{ +public: + bool pointerEvent(QMouseEvent *event, quint32 nativeButton) override { + Q_UNUSED(nativeButton) + ScreenEdges::self()->isEntered(event); + // always forward + return false; + } + bool touchDown(qint32 id, const QPointF &pos, quint32 time) override { + Q_UNUSED(time) + // TODO: better check whether a touch sequence is in progress + if (m_touchInProgress || waylandServer()->seat()->isTouchSequence()) { + // cancel existing touch + ScreenEdges::self()->gestureRecognizer()->cancelSwipeGesture(); + m_touchInProgress = false; + m_id = 0; + return false; + } + if (ScreenEdges::self()->gestureRecognizer()->startSwipeGesture(pos) > 0) { + m_touchInProgress = true; + m_id = id; + m_lastPos = pos; + return true; + } + return false; + } + bool touchMotion(qint32 id, const QPointF &pos, quint32 time) override { + Q_UNUSED(time) + if (m_touchInProgress && m_id == id) { + ScreenEdges::self()->gestureRecognizer()->updateSwipeGesture(QSizeF(pos.x() - m_lastPos.x(), pos.y() - m_lastPos.y())); + m_lastPos = pos; + return true; + } + return false; + } + bool touchUp(qint32 id, quint32 time) override { + Q_UNUSED(time) + if (m_touchInProgress && m_id == id) { + ScreenEdges::self()->gestureRecognizer()->endSwipeGesture(); + m_touchInProgress = false; + return true; + } + return false; + } +private: + bool m_touchInProgress = false; + qint32 m_id = 0; + QPointF m_lastPos; +}; + +/** + * This filter implements window actions. If the event should not be passed to the + * current pointer window it will filter out the event + */ +class WindowActionInputFilter : public InputEventFilter +{ +public: + bool pointerEvent(QMouseEvent *event, quint32 nativeButton) override { + Q_UNUSED(nativeButton) + if (event->type() != QEvent::MouseButtonPress) { + return false; + } + AbstractClient *c = dynamic_cast(input()->pointer()->focus()); + if (!c) { + return false; + } + const auto actionResult = performClientMouseAction(event, c, MouseAction::ModifierAndWindow); + if (actionResult.first) { + return actionResult.second; + } + return false; + } + bool wheelEvent(QWheelEvent *event) override { + if (event->angleDelta().y() == 0) { + // only actions on vertical scroll + return false; + } + AbstractClient *c = dynamic_cast(input()->pointer()->focus()); + if (!c) { + return false; + } + const auto actionResult = performClientWheelAction(event, c, MouseAction::ModifierAndWindow); + if (actionResult.first) { + return actionResult.second; + } + return false; + } + bool touchDown(qint32 id, const QPointF &pos, quint32 time) override { + Q_UNUSED(id) + Q_UNUSED(time) + auto seat = waylandServer()->seat(); + if (seat->isTouchSequence()) { + return false; + } + AbstractClient *c = dynamic_cast(input()->touch()->focus()); + if (!c) { + return false; + } + bool wasAction = false; + const Options::MouseCommand command = c->getMouseCommand(Qt::LeftButton, &wasAction); + if (wasAction) { + return !c->performMouseCommand(command, pos.toPoint()); + } + return false; + } +}; + +/** + * The remaining default input filter which forwards events to other windows + */ +class ForwardInputFilter : public InputEventFilter +{ +public: + bool pointerEvent(QMouseEvent *event, quint32 nativeButton) override { + auto seat = waylandServer()->seat(); + seat->setTimestamp(event->timestamp()); + switch (event->type()) { + case QEvent::MouseMove: { + seat->setPointerPos(event->globalPos()); + MouseEvent *e = static_cast(event); + if (e->delta() != QSizeF()) { + seat->relativePointerMotion(e->delta(), e->deltaUnaccelerated(), e->timestampMicroseconds()); + } + break; + } + case QEvent::MouseButtonPress: + seat->pointerButtonPressed(nativeButton); + break; + case QEvent::MouseButtonRelease: + seat->pointerButtonReleased(nativeButton); + break; + default: + break; + } + return true; + } + bool wheelEvent(QWheelEvent *event) override { + auto seat = waylandServer()->seat(); + seat->setTimestamp(event->timestamp()); + auto _event = static_cast(event); + KWaylandServer::PointerAxisSource source; + switch (_event->axisSource()) { + case KWin::InputRedirection::PointerAxisSourceWheel: + source = KWaylandServer::PointerAxisSource::Wheel; + break; + case KWin::InputRedirection::PointerAxisSourceFinger: + source = KWaylandServer::PointerAxisSource::Finger; + break; + case KWin::InputRedirection::PointerAxisSourceContinuous: + source = KWaylandServer::PointerAxisSource::Continuous; + break; + case KWin::InputRedirection::PointerAxisSourceWheelTilt: + source = KWaylandServer::PointerAxisSource::WheelTilt; + break; + case KWin::InputRedirection::PointerAxisSourceUnknown: + default: + source = KWaylandServer::PointerAxisSource::Unknown; + break; + } + seat->pointerAxisV5(_event->orientation(), _event->delta(), _event->discreteDelta(), source); + return true; + } + bool keyEvent(QKeyEvent *event) override { + if (!workspace()) { + return false; + } + if (event->isAutoRepeat()) { + // handled by Wayland client + return false; + } + auto seat = waylandServer()->seat(); + input()->keyboard()->update(); + seat->setTimestamp(event->timestamp()); + passToWaylandServer(event); + return true; + } + bool touchDown(qint32 id, const QPointF &pos, quint32 time) override { + if (!workspace()) { + return false; + } + auto seat = waylandServer()->seat(); + seat->setTimestamp(time); + input()->touch()->insertId(id, seat->touchDown(pos)); + return true; + } + bool touchMotion(qint32 id, const QPointF &pos, quint32 time) override { + if (!workspace()) { + return false; + } + auto seat = waylandServer()->seat(); + seat->setTimestamp(time); + const qint32 kwaylandId = input()->touch()->mappedId(id); + if (kwaylandId != -1) { + seat->touchMove(kwaylandId, pos); + } + return true; + } + bool touchUp(qint32 id, quint32 time) override { + if (!workspace()) { + return false; + } + auto seat = waylandServer()->seat(); + seat->setTimestamp(time); + const qint32 kwaylandId = input()->touch()->mappedId(id); + if (kwaylandId != -1) { + seat->touchUp(kwaylandId); + input()->touch()->removeId(id); + } + return true; + } + bool pinchGestureBegin(int fingerCount, quint32 time) override { + if (!workspace()) { + return false; + } + auto seat = waylandServer()->seat(); + seat->setTimestamp(time); + seat->startPointerPinchGesture(fingerCount); + return true; + } + bool pinchGestureUpdate(qreal scale, qreal angleDelta, const QSizeF &delta, quint32 time) override { + if (!workspace()) { + return false; + } + auto seat = waylandServer()->seat(); + seat->setTimestamp(time); + seat->updatePointerPinchGesture(delta, scale, angleDelta); + return true; + } + bool pinchGestureEnd(quint32 time) override { + if (!workspace()) { + return false; + } + auto seat = waylandServer()->seat(); + seat->setTimestamp(time); + seat->endPointerPinchGesture(); + return true; + } + bool pinchGestureCancelled(quint32 time) override { + if (!workspace()) { + return false; + } + auto seat = waylandServer()->seat(); + seat->setTimestamp(time); + seat->cancelPointerPinchGesture(); + return true; + } + + bool swipeGestureBegin(int fingerCount, quint32 time) override { + if (!workspace()) { + return false; + } + auto seat = waylandServer()->seat(); + seat->setTimestamp(time); + seat->startPointerSwipeGesture(fingerCount); + return true; + } + bool swipeGestureUpdate(const QSizeF &delta, quint32 time) override { + if (!workspace()) { + return false; + } + auto seat = waylandServer()->seat(); + seat->setTimestamp(time); + seat->updatePointerSwipeGesture(delta); + return true; + } + bool swipeGestureEnd(quint32 time) override { + if (!workspace()) { + return false; + } + auto seat = waylandServer()->seat(); + seat->setTimestamp(time); + seat->endPointerSwipeGesture(); + return true; + } + bool swipeGestureCancelled(quint32 time) override { + if (!workspace()) { + return false; + } + auto seat = waylandServer()->seat(); + seat->setTimestamp(time); + seat->cancelPointerSwipeGesture(); + return true; + } +}; + +static KWaylandServer::SeatInterface *findSeat() +{ + auto server = waylandServer(); + if (!server) { + return nullptr; + } + return server->seat(); +} + +/** + * Handles input coming from a tablet device (e.g. wacom) often with a pen + */ +class TabletInputFilter : public QObject, public InputEventFilter +{ +public: + TabletInputFilter() + { + } + + static KWaylandServer::TabletSeatInterface *findTabletSeat() + { + auto server = waylandServer(); + if (!server) { + return nullptr; + } + KWaylandServer::TabletManagerInterface *manager = server->tabletManager(); + return manager->seat(findSeat()); + } + + void integrateDevice(LibInput::Device *device) + { + if (device->isTabletTool()) { + KWaylandServer::TabletSeatInterface *tabletSeat = findTabletSeat(); + if (!tabletSeat) { + qCCritical(KWIN_CORE) << "Could not find tablet manager"; + return; + } + struct udev_device *const udev_device = libinput_device_get_udev_device(device->device()); + const char *devnode = udev_device_get_devnode(udev_device); + tabletSeat->addTablet(device->vendor(), device->product(), device->sysName(), device->name(), {QString::fromUtf8(devnode)}); + } + } + void removeDevice(const QString &sysname) + { + KWaylandServer::TabletSeatInterface *tabletSeat = findTabletSeat(); + if (tabletSeat) + tabletSeat->removeTablet(sysname); + else + qCCritical(KWIN_CORE) << "Could not find tablet to remove" << sysname; + } + + bool tabletToolEvent(TabletEvent *event) override + { + if (!workspace()) { + return false; + } + + KWaylandServer::TabletSeatInterface *tabletSeat = findTabletSeat(); + if (!tabletSeat) { + qCCritical(KWIN_CORE) << "Could not find tablet manager"; + return false; + } + auto tool = tabletSeat->toolByHardwareSerial(event->serialId()); + if (!tool) { + using namespace KWaylandServer; + + const QVector capabilities = event->capabilities(); + const auto f = [](InputRedirection::Capability cap) { + switch (cap) { + case InputRedirection::Tilt: + return TabletToolInterface::Tilt; + case InputRedirection::Pressure: + return TabletToolInterface::Pressure; + case InputRedirection::Distance: + return TabletToolInterface::Distance; + case InputRedirection::Rotation: + return TabletToolInterface::Rotation; + case InputRedirection::Slider: + return TabletToolInterface::Slider; + case InputRedirection::Wheel: + return TabletToolInterface::Wheel; + } + return TabletToolInterface::Wheel; + }; + QVector ifaceCapabilities; + ifaceCapabilities.resize(capabilities.size()); + std::transform(capabilities.constBegin(), capabilities.constEnd(), ifaceCapabilities.begin(), f); + + TabletToolInterface::Type toolType = TabletToolInterface::Type::Pen; + switch (event->toolType()) { + case InputRedirection::Pen: + toolType = TabletToolInterface::Type::Pen; + break; + case InputRedirection::Eraser: + toolType = TabletToolInterface::Type::Eraser; + break; + case InputRedirection::Brush: + toolType = TabletToolInterface::Type::Brush; + break; + case InputRedirection::Pencil: + toolType = TabletToolInterface::Type::Pencil; + break; + case InputRedirection::Airbrush: + toolType = TabletToolInterface::Type::Airbrush; + break; + case InputRedirection::Finger: + toolType = TabletToolInterface::Type::Finger; + break; + case InputRedirection::Mouse: + toolType = TabletToolInterface::Type::Mouse; + break; + case InputRedirection::Lens: + toolType = TabletToolInterface::Type::Lens; + break; + case InputRedirection::Totem: + toolType = TabletToolInterface::Type::Totem; + break; + } + tool = tabletSeat->addTool(toolType, event->serialId(), event->uniqueId(), ifaceCapabilities); + + const auto cursor = new Cursor(tool); + Cursors::self()->addCursor(cursor); + m_cursorByTool[tool] = cursor; + + connect(tool, &TabletToolInterface::cursorChanged, cursor, &Cursor::cursorChanged); + connect(tool, &TabletToolInterface::cursorChanged, cursor, [cursor] (TabletCursor* tcursor) { + static const auto createDefaultCursor = [] { + WaylandCursorImage defaultCursor; + WaylandCursorImage::Image ret; + defaultCursor.loadThemeCursor(CursorShape(Qt::CrossCursor), &ret); + return ret; + }; + static const auto defaultCursor = createDefaultCursor(); + if (!tcursor) { + cursor->updateCursor(defaultCursor.image, defaultCursor.hotspot); + return; + } + auto cursorSurface = tcursor->surface(); + if (!cursorSurface) { + cursor->updateCursor(defaultCursor.image, defaultCursor.hotspot); + return; + } + auto buffer = cursorSurface->buffer(); + if (!buffer) { + cursor->updateCursor(defaultCursor.image, defaultCursor.hotspot); + return; + } + + QImage cursorImage; + cursorImage = buffer->data().copy(); + cursorImage.setDevicePixelRatio(cursorSurface->bufferScale()); + + cursor->updateCursor(cursorImage, tcursor->hotspot()); + }); + emit cursor->cursorChanged(); + } + + KWaylandServer::TabletInterface *tablet = tabletSeat->tabletByName(event->tabletSysName()); + + Toplevel *toplevel = input()->findToplevel(event->globalPos()); + if (!toplevel || !toplevel->surface()) { + return false; + } + + KWaylandServer::SurfaceInterface *surface = toplevel->surface(); + tool->setCurrentSurface(surface); + + if (!tool->isClientSupported() || !tablet->isSurfaceSupported(surface)) { + return emulateTabletEvent(event); + } + + switch (event->type()) { + case QEvent::TabletMove: { + const auto pos = event->globalPosF() - toplevel->bufferGeometry().topLeft(); + tool->sendMotion(pos); + m_cursorByTool[tool]->setPos(event->globalPos()); + break; + } case QEvent::TabletEnterProximity: { + tool->sendProximityIn(tablet); + break; + } case QEvent::TabletLeaveProximity: + tool->sendProximityOut(); + break; + case QEvent::TabletPress: { + const auto pos = event->globalPosF() - toplevel->bufferGeometry().topLeft(); + tool->sendMotion(pos); + m_cursorByTool[tool]->setPos(event->globalPos()); + tool->sendDown(); + break; + } + case QEvent::TabletRelease: + tool->sendUp(); + break; + default: + qCWarning(KWIN_CORE) << "Unexpected tablet event type" << event; + break; + } + const quint32 MAX_VAL = 65535; + tool->sendPressure(MAX_VAL * event->pressure()); + tool->sendFrame(event->timestamp()); + waylandServer()->simulateUserActivity(); + return true; + } + + bool emulateTabletEvent(TabletEvent *event) + { + if (!workspace()) { + return false; + } + + switch (event->type()) { + case QEvent::TabletMove: + case QEvent::TabletEnterProximity: + input()->pointer()->processMotion(event->globalPosF(), event->timestamp()); + break; + case QEvent::TabletPress: + input()->pointer()->processButton(KWin::qtMouseButtonToButton(Qt::LeftButton), + InputRedirection::PointerButtonPressed, event->timestamp()); + break; + case QEvent::TabletRelease: + input()->pointer()->processButton(KWin::qtMouseButtonToButton(Qt::LeftButton), + InputRedirection::PointerButtonReleased, event->timestamp()); + break; + case QEvent::TabletLeaveProximity: + break; + default: + qCWarning(KWIN_CORE) << "Unexpected tablet event type" << event; + break; + } + waylandServer()->simulateUserActivity(); + return true; + } + QHash m_cursorByTool; +}; + +class DragAndDropInputFilter : public InputEventFilter +{ +public: + bool pointerEvent(QMouseEvent *event, quint32 nativeButton) override { + auto seat = waylandServer()->seat(); + if (!seat->isDragPointer()) { + return false; + } + if (seat->isDragTouch()) { + return true; + } + seat->setTimestamp(event->timestamp()); + switch (event->type()) { + case QEvent::MouseMove: { + const auto pos = input()->globalPointer(); + seat->setPointerPos(pos); + + const auto eventPos = event->globalPos(); + // TODO: use InputDeviceHandler::at() here and check isClient()? + Toplevel *t = input()->findManagedToplevel(eventPos); + if (auto *xwl = xwayland()) { + const auto ret = xwl->dragMoveFilter(t, eventPos); + if (ret == Xwl::DragEventReply::Ignore) { + return false; + } else if (ret == Xwl::DragEventReply::Take) { + break; + } + } + + if (t) { + // TODO: consider decorations + if (t->surface() != seat->dragSurface()) { + if (AbstractClient *c = qobject_cast(t)) { + workspace()->activateClient(c); + } + seat->setDragTarget(t->surface(), t->inputTransformation()); + } + } else { + // no window at that place, if we have a surface we need to reset + seat->setDragTarget(nullptr); + } + break; + } + case QEvent::MouseButtonPress: + seat->pointerButtonPressed(nativeButton); + break; + case QEvent::MouseButtonRelease: + seat->pointerButtonReleased(nativeButton); + break; + default: + break; + } + // TODO: should we pass through effects? + return true; + } + + bool touchDown(qint32 id, const QPointF &pos, quint32 time) override { + auto seat = waylandServer()->seat(); + if (seat->isDragPointer()) { + return true; + } + if (!seat->isDragTouch()) { + return false; + } + if (m_touchId != id) { + return true; + } + seat->setTimestamp(time); + input()->touch()->insertId(id, seat->touchDown(pos)); + return true; + } + bool touchMotion(qint32 id, const QPointF &pos, quint32 time) override { + auto seat = waylandServer()->seat(); + if (seat->isDragPointer()) { + return true; + } + if (!seat->isDragTouch()) { + return false; + } + if (m_touchId < 0) { + // We take for now the first id appearing as a move after a drag + // started. We can optimize by specifying the id the drag is + // associated with by implementing a key-value getter in KWayland. + m_touchId = id; + } + if (m_touchId != id) { + return true; + } + seat->setTimestamp(time); + const qint32 kwaylandId = input()->touch()->mappedId(id); + if (kwaylandId == -1) { + return true; + } + + seat->touchMove(kwaylandId, pos); + + if (Toplevel *t = input()->findToplevel(pos.toPoint())) { + // TODO: consider decorations + if (t->surface() != seat->dragSurface()) { + if (AbstractClient *c = qobject_cast(t)) { + workspace()->activateClient(c); + } + seat->setDragTarget(t->surface(), pos, t->inputTransformation()); + } + } else { + // no window at that place, if we have a surface we need to reset + seat->setDragTarget(nullptr); + } + return true; + } + bool touchUp(qint32 id, quint32 time) override { + auto seat = waylandServer()->seat(); + if (!seat->isDragTouch()) { + return false; + } + seat->setTimestamp(time); + const qint32 kwaylandId = input()->touch()->mappedId(id); + if (kwaylandId != -1) { + seat->touchUp(kwaylandId); + input()->touch()->removeId(id); + } + if (m_touchId == id) { + m_touchId = -1; + } + return true; + } +private: + qint32 m_touchId = -1; +}; + +KWIN_SINGLETON_FACTORY(InputRedirection) + +static const QString s_touchpadComponent = QStringLiteral("kcm_touchpad"); + +InputRedirection::InputRedirection(QObject *parent) + : QObject(parent) + , m_keyboard(new KeyboardInputRedirection(this)) + , m_pointer(new PointerInputRedirection(this)) + , m_tablet(new TabletInputRedirection(this)) + , m_touch(new TouchInputRedirection(this)) + , m_shortcuts(new GlobalShortcutsManager(this)) +{ + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + if (Application::usesLibinput()) { + if (LogindIntegration::self()->hasSessionControl()) { + setupLibInput(); + } else { + LibInput::Connection::createThread(); + if (LogindIntegration::self()->isConnected()) { + LogindIntegration::self()->takeControl(); + } else { + connect(LogindIntegration::self(), &LogindIntegration::connectedChanged, LogindIntegration::self(), &LogindIntegration::takeControl); + } + connect(LogindIntegration::self(), &LogindIntegration::hasSessionControlChanged, this, + [this] (bool sessionControl) { + if (sessionControl) { + setupLibInput(); + } + } + ); + } + } + connect(kwinApp(), &Application::workspaceCreated, this, &InputRedirection::setupWorkspace); + reconfigure(); +} + +InputRedirection::~InputRedirection() +{ + s_self = nullptr; + qDeleteAll(m_filters); + qDeleteAll(m_spies); +} + +void InputRedirection::installInputEventFilter(InputEventFilter *filter) +{ + Q_ASSERT(!m_filters.contains(filter)); + m_filters << filter; +} + +void InputRedirection::prependInputEventFilter(InputEventFilter *filter) +{ + Q_ASSERT(!m_filters.contains(filter)); + m_filters.prepend(filter); +} + +void InputRedirection::uninstallInputEventFilter(InputEventFilter *filter) +{ + m_filters.removeOne(filter); +} + +void InputRedirection::installInputEventSpy(InputEventSpy *spy) +{ + m_spies << spy; +} + +void InputRedirection::uninstallInputEventSpy(InputEventSpy *spy) +{ + m_spies.removeOne(spy); +} + +void InputRedirection::init() +{ + m_shortcuts->init(); +} + +void InputRedirection::setupWorkspace() +{ + if (waylandServer()) { + using namespace KWaylandServer; + FakeInputInterface *fakeInput = waylandServer()->display()->createFakeInput(this); + fakeInput->create(); + connect(fakeInput, &FakeInputInterface::deviceCreated, this, + [this] (FakeInputDevice *device) { + connect(device, &FakeInputDevice::authenticationRequested, this, + [device] (const QString &application, const QString &reason) { + Q_UNUSED(application) + Q_UNUSED(reason) + // TODO: make secure + device->setAuthentication(true); + } + ); + connect(device, &FakeInputDevice::pointerMotionRequested, this, + [this] (const QSizeF &delta) { + // TODO: Fix time + m_pointer->processMotion(globalPointer() + QPointF(delta.width(), delta.height()), 0); + waylandServer()->simulateUserActivity(); + } + ); + connect(device, &FakeInputDevice::pointerMotionAbsoluteRequested, this, + [this] (const QPointF &pos) { + // TODO: Fix time + m_pointer->processMotion(pos, 0); + waylandServer()->simulateUserActivity(); + } + ); + connect(device, &FakeInputDevice::pointerButtonPressRequested, this, + [this] (quint32 button) { + // TODO: Fix time + m_pointer->processButton(button, InputRedirection::PointerButtonPressed, 0); + waylandServer()->simulateUserActivity(); + } + ); + connect(device, &FakeInputDevice::pointerButtonReleaseRequested, this, + [this] (quint32 button) { + // TODO: Fix time + m_pointer->processButton(button, InputRedirection::PointerButtonReleased, 0); + waylandServer()->simulateUserActivity(); + } + ); + connect(device, &FakeInputDevice::pointerAxisRequested, this, + [this] (Qt::Orientation orientation, qreal delta) { + // TODO: Fix time + InputRedirection::PointerAxis axis; + switch (orientation) { + case Qt::Horizontal: + axis = InputRedirection::PointerAxisHorizontal; + break; + case Qt::Vertical: + axis = InputRedirection::PointerAxisVertical; + break; + default: + Q_UNREACHABLE(); + break; + } + // TODO: Fix time + m_pointer->processAxis(axis, delta, 0, InputRedirection::PointerAxisSourceUnknown, 0); + waylandServer()->simulateUserActivity(); + } + ); + connect(device, &FakeInputDevice::touchDownRequested, this, + [this] (qint32 id, const QPointF &pos) { + // TODO: Fix time + m_touch->processDown(id, pos, 0); + waylandServer()->simulateUserActivity(); + } + ); + connect(device, &FakeInputDevice::touchMotionRequested, this, + [this] (qint32 id, const QPointF &pos) { + // TODO: Fix time + m_touch->processMotion(id, pos, 0); + waylandServer()->simulateUserActivity(); + } + ); + connect(device, &FakeInputDevice::touchUpRequested, this, + [this] (qint32 id) { + // TODO: Fix time + m_touch->processUp(id, 0); + waylandServer()->simulateUserActivity(); + } + ); + connect(device, &FakeInputDevice::touchCancelRequested, this, + [this] () { + m_touch->cancel(); + } + ); + connect(device, &FakeInputDevice::touchFrameRequested, this, + [this] () { + m_touch->frame(); + } + ); + connect(device, &FakeInputDevice::keyboardKeyPressRequested, this, + [this] (quint32 button) { + // TODO: Fix time + m_keyboard->processKey(button, InputRedirection::KeyboardKeyPressed, 0); + waylandServer()->simulateUserActivity(); + } + ); + connect(device, &FakeInputDevice::keyboardKeyReleaseRequested, this, + [this] (quint32 button) { + // TODO: Fix time + m_keyboard->processKey(button, InputRedirection::KeyboardKeyReleased, 0); + waylandServer()->simulateUserActivity(); + } + ); + } + ); + connect(workspace(), &Workspace::configChanged, this, &InputRedirection::reconfigure); + + m_keyboard->init(); + m_pointer->init(); + m_touch->init(); + m_tablet->init(); + } + setupInputFilters(); +} + +void InputRedirection::setupInputFilters() +{ + const bool hasGlobalShortcutSupport = !waylandServer() || waylandServer()->hasGlobalShortcutSupport(); + if (LogindIntegration::self()->hasSessionControl() && hasGlobalShortcutSupport) { + installInputEventFilter(new VirtualTerminalFilter); + } + if (waylandServer()) { + installInputEventSpy(new TouchHideCursorSpy); + if (hasGlobalShortcutSupport) { + installInputEventFilter(new TerminateServerFilter); + } + installInputEventFilter(new DragAndDropInputFilter); + installInputEventFilter(new LockScreenFilter); + installInputEventFilter(new PopupInputFilter); + m_windowSelector = new WindowSelectorFilter; + installInputEventFilter(m_windowSelector); + } + if (hasGlobalShortcutSupport) { + installInputEventFilter(new ScreenEdgeInputFilter); + } + installInputEventFilter(new EffectsFilter); + installInputEventFilter(new MoveResizeFilter); +#ifdef KWIN_BUILD_TABBOX + installInputEventFilter(new TabBoxInputFilter); +#endif + if (hasGlobalShortcutSupport) { + installInputEventFilter(new GlobalShortcutFilter); + } + installInputEventFilter(new DecorationEventFilter); + installInputEventFilter(new InternalWindowEventFilter); + if (waylandServer()) { + installInputEventFilter(new WindowActionInputFilter); + installInputEventFilter(new ForwardInputFilter); + + if (m_libInput) { + m_tabletSupport = new TabletInputFilter; + for (LibInput::Device *dev : m_libInput->devices()) { + m_tabletSupport->integrateDevice(dev); + } + connect(m_libInput, &LibInput::Connection::deviceAdded, m_tabletSupport, &TabletInputFilter::integrateDevice); + connect(m_libInput, &LibInput::Connection::deviceRemovedSysName, m_tabletSupport, &TabletInputFilter::removeDevice); + installInputEventFilter(m_tabletSupport); + } + } +} + +void InputRedirection::reconfigure() +{ + if (Application::usesLibinput()) { + auto inputConfig = InputConfig::self()->inputConfig(); + inputConfig->reparseConfiguration(); + const auto config = inputConfig->group(QStringLiteral("Keyboard")); + const int delay = config.readEntry("RepeatDelay", 660); + const int rate = config.readEntry("RepeatRate", 25); + const bool enabled = config.readEntry("KeyboardRepeating", 0) == 0; + + waylandServer()->seat()->setKeyRepeatInfo(enabled ? rate : 0, delay); + } +} + +void InputRedirection::setupLibInput() +{ + if (!Application::usesLibinput()) { + return; + } + if (m_libInput) { + return; + } + LibInput::Connection *conn = LibInput::Connection::create(this); + m_libInput = conn; + if (conn) { + + if (waylandServer()) { + // create relative pointer manager + waylandServer()->display()->createRelativePointerManager(KWaylandServer::RelativePointerInterfaceVersion::UnstableV1, waylandServer()->display())->create(); + } + + conn->setInputConfig(InputConfig::self()->inputConfig()); + conn->updateLEDs(m_keyboard->xkb()->leds()); + waylandServer()->updateKeyState(m_keyboard->xkb()->leds()); + connect(m_keyboard, &KeyboardInputRedirection::ledsChanged, waylandServer(), &WaylandServer::updateKeyState); + connect(m_keyboard, &KeyboardInputRedirection::ledsChanged, conn, &LibInput::Connection::updateLEDs); + connect(conn, &LibInput::Connection::eventsRead, this, + [this] { + m_libInput->processEvents(); + }, Qt::QueuedConnection + ); + conn->setup(); + connect(conn, &LibInput::Connection::pointerButtonChanged, m_pointer, &PointerInputRedirection::processButton); + connect(conn, &LibInput::Connection::pointerAxisChanged, m_pointer, &PointerInputRedirection::processAxis); + connect(conn, &LibInput::Connection::pinchGestureBegin, m_pointer, &PointerInputRedirection::processPinchGestureBegin); + connect(conn, &LibInput::Connection::pinchGestureUpdate, m_pointer, &PointerInputRedirection::processPinchGestureUpdate); + connect(conn, &LibInput::Connection::pinchGestureEnd, m_pointer, &PointerInputRedirection::processPinchGestureEnd); + connect(conn, &LibInput::Connection::pinchGestureCancelled, m_pointer, &PointerInputRedirection::processPinchGestureCancelled); + connect(conn, &LibInput::Connection::swipeGestureBegin, m_pointer, &PointerInputRedirection::processSwipeGestureBegin); + connect(conn, &LibInput::Connection::swipeGestureUpdate, m_pointer, &PointerInputRedirection::processSwipeGestureUpdate); + connect(conn, &LibInput::Connection::swipeGestureEnd, m_pointer, &PointerInputRedirection::processSwipeGestureEnd); + connect(conn, &LibInput::Connection::swipeGestureCancelled, m_pointer, &PointerInputRedirection::processSwipeGestureCancelled); + connect(conn, &LibInput::Connection::keyChanged, m_keyboard, &KeyboardInputRedirection::processKey); + connect(conn, &LibInput::Connection::pointerMotion, this, + [this] (const QSizeF &delta, const QSizeF &deltaNonAccel, uint32_t time, quint64 timeMicroseconds, LibInput::Device *device) { + m_pointer->processMotion(m_pointer->pos() + QPointF(delta.width(), delta.height()), delta, deltaNonAccel, time, timeMicroseconds, device); + } + ); + connect(conn, &LibInput::Connection::pointerMotionAbsolute, this, + [this] (QPointF orig, QPointF screen, uint32_t time, LibInput::Device *device) { + Q_UNUSED(orig) + m_pointer->processMotion(screen, time, device); + } + ); + connect(conn, &LibInput::Connection::touchDown, m_touch, &TouchInputRedirection::processDown); + connect(conn, &LibInput::Connection::touchUp, m_touch, &TouchInputRedirection::processUp); + connect(conn, &LibInput::Connection::touchMotion, m_touch, &TouchInputRedirection::processMotion); + connect(conn, &LibInput::Connection::touchCanceled, m_touch, &TouchInputRedirection::cancel); + connect(conn, &LibInput::Connection::touchFrame, m_touch, &TouchInputRedirection::frame); + auto handleSwitchEvent = [this] (SwitchEvent::State state, quint32 time, quint64 timeMicroseconds, LibInput::Device *device) { + SwitchEvent event(state, time, timeMicroseconds, device); + processSpies(std::bind(&InputEventSpy::switchEvent, std::placeholders::_1, &event)); + processFilters(std::bind(&InputEventFilter::switchEvent, std::placeholders::_1, &event)); + }; + connect(conn, &LibInput::Connection::switchToggledOn, this, + std::bind(handleSwitchEvent, SwitchEvent::State::On, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); + connect(conn, &LibInput::Connection::switchToggledOff, this, + std::bind(handleSwitchEvent, SwitchEvent::State::Off, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); + + connect(conn, &LibInput::Connection::tabletToolEvent, + m_tablet, &TabletInputRedirection::tabletToolEvent); + connect(conn, &LibInput::Connection::tabletToolButtonEvent, + m_tablet, &TabletInputRedirection::tabletToolButtonEvent); + connect(conn, &LibInput::Connection::tabletPadButtonEvent, + m_tablet, &TabletInputRedirection::tabletPadButtonEvent); + connect(conn, &LibInput::Connection::tabletPadRingEvent, + m_tablet, &TabletInputRedirection::tabletPadRingEvent); + connect(conn, &LibInput::Connection::tabletPadStripEvent, + m_tablet, &TabletInputRedirection::tabletPadStripEvent); + + if (screens()) { + setupLibInputWithScreens(); + } else { + connect(kwinApp(), &Application::screensCreated, this, &InputRedirection::setupLibInputWithScreens); + } + if (auto s = findSeat()) { + // Workaround for QTBUG-54371: if there is no real keyboard Qt doesn't request virtual keyboard + s->setHasKeyboard(true); + s->setHasPointer(conn->hasPointer()); + s->setHasTouch(conn->hasTouch()); + connect(conn, &LibInput::Connection::hasAlphaNumericKeyboardChanged, this, + [this] (bool set) { + if (m_libInput->isSuspended()) { + return; + } + // TODO: this should update the seat, only workaround for QTBUG-54371 + emit hasAlphaNumericKeyboardChanged(set); + } + ); + connect(conn, &LibInput::Connection::hasTabletModeSwitchChanged, this, + [this] (bool set) { + if (m_libInput->isSuspended()) { + return; + } + emit hasTabletModeSwitchChanged(set); + } + ); + connect(conn, &LibInput::Connection::hasPointerChanged, this, + [this, s] (bool set) { + if (m_libInput->isSuspended()) { + return; + } + s->setHasPointer(set); + } + ); + connect(conn, &LibInput::Connection::hasTouchChanged, this, + [this, s] (bool set) { + if (m_libInput->isSuspended()) { + return; + } + s->setHasTouch(set); + } + ); + } + connect(LogindIntegration::self(), &LogindIntegration::sessionActiveChanged, m_libInput, + [this] (bool active) { + if (!active) { + m_libInput->deactivate(); + } + } + ); + } + + setupTouchpadShortcuts(); +} + +void InputRedirection::setupTouchpadShortcuts() +{ + if (!m_libInput) { + return; + } + QAction *touchpadToggleAction = new QAction(this); + QAction *touchpadOnAction = new QAction(this); + QAction *touchpadOffAction = new QAction(this); + + touchpadToggleAction->setObjectName(QStringLiteral("Toggle Touchpad")); + touchpadToggleAction->setProperty("componentName", s_touchpadComponent); + touchpadOnAction->setObjectName(QStringLiteral("Enable Touchpad")); + touchpadOnAction->setProperty("componentName", s_touchpadComponent); + touchpadOffAction->setObjectName(QStringLiteral("Disable Touchpad")); + touchpadOffAction->setProperty("componentName", s_touchpadComponent); + KGlobalAccel::self()->setDefaultShortcut(touchpadToggleAction, QList{Qt::Key_TouchpadToggle}); + KGlobalAccel::self()->setShortcut(touchpadToggleAction, QList{Qt::Key_TouchpadToggle}); + KGlobalAccel::self()->setDefaultShortcut(touchpadOnAction, QList{Qt::Key_TouchpadOn}); + KGlobalAccel::self()->setShortcut(touchpadOnAction, QList{Qt::Key_TouchpadOn}); + KGlobalAccel::self()->setDefaultShortcut(touchpadOffAction, QList{Qt::Key_TouchpadOff}); + KGlobalAccel::self()->setShortcut(touchpadOffAction, QList{Qt::Key_TouchpadOff}); +#ifndef KWIN_BUILD_TESTING + registerShortcut(Qt::Key_TouchpadToggle, touchpadToggleAction); + registerShortcut(Qt::Key_TouchpadOn, touchpadOnAction); + registerShortcut(Qt::Key_TouchpadOff, touchpadOffAction); +#endif + connect(touchpadToggleAction, &QAction::triggered, m_libInput, &LibInput::Connection::toggleTouchpads); + connect(touchpadOnAction, &QAction::triggered, m_libInput, &LibInput::Connection::enableTouchpads); + connect(touchpadOffAction, &QAction::triggered, m_libInput, &LibInput::Connection::disableTouchpads); +} + +bool InputRedirection::hasAlphaNumericKeyboard() +{ + if (m_libInput) { + return m_libInput->hasAlphaNumericKeyboard(); + } + return true; +} + +bool InputRedirection::hasTabletModeSwitch() +{ + if (m_libInput) { + return m_libInput->hasTabletModeSwitch(); + } + return false; +} + +void InputRedirection::setupLibInputWithScreens() +{ + if (!screens() || !m_libInput) { + return; + } + m_libInput->setScreenSize(screens()->size()); + m_libInput->updateScreens(); + connect(screens(), &Screens::sizeChanged, this, + [this] { + m_libInput->setScreenSize(screens()->size()); + } + ); + connect(screens(), &Screens::changed, m_libInput, &LibInput::Connection::updateScreens); +} + +void InputRedirection::processPointerMotion(const QPointF &pos, uint32_t time) +{ + m_pointer->processMotion(pos, time); +} + +void InputRedirection::processPointerButton(uint32_t button, InputRedirection::PointerButtonState state, uint32_t time) +{ + m_pointer->processButton(button, state, time); +} + +void InputRedirection::processPointerAxis(InputRedirection::PointerAxis axis, qreal delta, qint32 discreteDelta, PointerAxisSource source, uint32_t time) +{ + m_pointer->processAxis(axis, delta, discreteDelta, source, time); +} + +void InputRedirection::processKeyboardKey(uint32_t key, InputRedirection::KeyboardKeyState state, uint32_t time) +{ + m_keyboard->processKey(key, state, time); +} + +void InputRedirection::processKeyboardModifiers(uint32_t modsDepressed, uint32_t modsLatched, uint32_t modsLocked, uint32_t group) +{ + m_keyboard->processModifiers(modsDepressed, modsLatched, modsLocked, group); +} + +void InputRedirection::processKeymapChange(int fd, uint32_t size) +{ + m_keyboard->processKeymapChange(fd, size); +} + +void InputRedirection::processTouchDown(qint32 id, const QPointF &pos, quint32 time) +{ + m_touch->processDown(id, pos, time); +} + +void InputRedirection::processTouchUp(qint32 id, quint32 time) +{ + m_touch->processUp(id, time); +} + +void InputRedirection::processTouchMotion(qint32 id, const QPointF &pos, quint32 time) +{ + m_touch->processMotion(id, pos, time); +} + +void InputRedirection::cancelTouch() +{ + m_touch->cancel(); +} + +void InputRedirection::touchFrame() +{ + m_touch->frame(); +} + +Qt::MouseButtons InputRedirection::qtButtonStates() const +{ + return m_pointer->buttons(); +} + +static bool acceptsInput(Toplevel *t, const QPoint &pos) +{ + const QRegion input = t->inputShape(); + if (input.isEmpty()) { + return true; + } + // TODO: What about sub-surfaces sticking outside the main surface? + const QPoint localPoint = pos - t->bufferGeometry().topLeft(); + return input.contains(localPoint); +} + +Toplevel *InputRedirection::findToplevel(const QPoint &pos) +{ + if (!Workspace::self()) { + return nullptr; + } + const bool isScreenLocked = waylandServer() && waylandServer()->isScreenLocked(); + // TODO: check whether the unmanaged wants input events at all + if (!isScreenLocked) { + // if an effect overrides the cursor we don't have a window to focus + if (effects && static_cast(effects)->isMouseInterception()) { + return nullptr; + } + const QList &unmanaged = Workspace::self()->unmanagedList(); + foreach (Unmanaged *u, unmanaged) { + if (u->inputGeometry().contains(pos) && acceptsInput(u, pos)) { + return u; + } + } + } + return findManagedToplevel(pos); +} + +Toplevel *InputRedirection::findManagedToplevel(const QPoint &pos) +{ + if (!Workspace::self()) { + return nullptr; + } + const bool isScreenLocked = waylandServer() && waylandServer()->isScreenLocked(); + const QList &stacking = Workspace::self()->stackingOrder(); + if (stacking.isEmpty()) { + return nullptr; + } + auto it = stacking.end(); + do { + --it; + Toplevel *t = (*it); + if (t->isDeleted()) { + // a deleted window doesn't get mouse events + continue; + } + if (AbstractClient *c = dynamic_cast(t)) { + if (!c->isOnCurrentActivity() || !c->isOnCurrentDesktop() || c->isMinimized() || c->isHiddenInternal()) { + continue; + } + } + if (!t->readyForPainting()) { + continue; + } + if (isScreenLocked) { + if (!t->isLockScreen() && !t->isInputMethod()) { + continue; + } + } + if (t->inputGeometry().contains(pos) && acceptsInput(t, pos)) { + return t; + } + } while (it != stacking.begin()); + return nullptr; +} + +Qt::KeyboardModifiers InputRedirection::keyboardModifiers() const +{ + return m_keyboard->modifiers(); +} + +Qt::KeyboardModifiers InputRedirection::modifiersRelevantForGlobalShortcuts() const +{ + return m_keyboard->modifiersRelevantForGlobalShortcuts(); +} + +void InputRedirection::registerShortcut(const QKeySequence &shortcut, QAction *action) +{ + Q_UNUSED(shortcut) + kwinApp()->platform()->setupActionForGlobalAccel(action); +} + +void InputRedirection::registerPointerShortcut(Qt::KeyboardModifiers modifiers, Qt::MouseButton pointerButtons, QAction *action) +{ + m_shortcuts->registerPointerShortcut(action, modifiers, pointerButtons); +} + +void InputRedirection::registerAxisShortcut(Qt::KeyboardModifiers modifiers, PointerAxisDirection axis, QAction *action) +{ + m_shortcuts->registerAxisShortcut(action, modifiers, axis); +} + +void InputRedirection::registerTouchpadSwipeShortcut(SwipeDirection direction, QAction *action) +{ + m_shortcuts->registerTouchpadSwipe(action, direction); +} + +void InputRedirection::registerGlobalAccel(KGlobalAccelInterface *interface) +{ + m_shortcuts->setKGlobalAccelInterface(interface); +} + +void InputRedirection::warpPointer(const QPointF &pos) +{ + m_pointer->warp(pos); +} + +bool InputRedirection::supportsPointerWarping() const +{ + return m_pointer->supportsWarping(); +} + +QPointF InputRedirection::globalPointer() const +{ + return m_pointer->pos(); +} + +void InputRedirection::startInteractiveWindowSelection(std::function callback, const QByteArray &cursorName) +{ + if (!m_windowSelector || m_windowSelector->isActive()) { + callback(nullptr); + return; + } + m_windowSelector->start(callback); + m_pointer->setWindowSelectionCursor(cursorName); +} + +void InputRedirection::startInteractivePositionSelection(std::function callback) +{ + if (!m_windowSelector || m_windowSelector->isActive()) { + callback(QPoint(-1, -1)); + return; + } + m_windowSelector->start(callback); + m_pointer->setWindowSelectionCursor(QByteArray()); +} + +bool InputRedirection::isSelectingWindow() const +{ + return m_windowSelector ? m_windowSelector->isActive() : false; +} + +InputDeviceHandler::InputDeviceHandler(InputRedirection *input) + : QObject(input) +{ +} + +InputDeviceHandler::~InputDeviceHandler() = default; + +void InputDeviceHandler::init() +{ + connect(workspace(), &Workspace::stackingOrderChanged, this, &InputDeviceHandler::update); + connect(workspace(), &Workspace::clientMinimizedChanged, this, &InputDeviceHandler::update); + connect(VirtualDesktopManager::self(), &VirtualDesktopManager::currentChanged, this, &InputDeviceHandler::update); +} + +bool InputDeviceHandler::setAt(Toplevel *toplevel) +{ + if (m_at.at == toplevel) { + return false; + } + auto old = m_at.at; + disconnect(m_at.surfaceCreatedConnection); + m_at.surfaceCreatedConnection = QMetaObject::Connection(); + + m_at.at = toplevel; + emit atChanged(old, toplevel); + return true; +} + +void InputDeviceHandler::setFocus(Toplevel *toplevel) +{ + m_focus.focus = toplevel; + //TODO: call focusUpdate? +} + +void InputDeviceHandler::setDecoration(QPointer decoration) +{ + auto oldDeco = m_focus.decoration; + m_focus.decoration = decoration; + cleanupDecoration(oldDeco.data(), m_focus.decoration.data()); + emit decorationChanged(); +} + +void InputDeviceHandler::setInternalWindow(QWindow *window) +{ + m_focus.internalWindow = window; + //TODO: call internalWindowUpdate? +} + +void InputDeviceHandler::updateFocus() +{ + auto oldFocus = m_focus.focus; + + if (m_at.at && !m_at.at->surface()) { + // The surface has not yet been created (special XWayland case). + // Therefore listen for its creation. + if (!m_at.surfaceCreatedConnection) { + m_at.surfaceCreatedConnection = connect(m_at.at, &Toplevel::surfaceChanged, + this, &InputDeviceHandler::update); + } + m_focus.focus = nullptr; + } else { + m_focus.focus = m_at.at; + } + + focusUpdate(oldFocus, m_focus.focus); +} + +bool InputDeviceHandler::updateDecoration() +{ + const auto oldDeco = m_focus.decoration; + m_focus.decoration = nullptr; + + auto *ac = qobject_cast(m_at.at); + if (ac && ac->decoratedClient()) { + const QRect clientRect = QRect(ac->clientPos(), ac->clientSize()).translated(ac->pos()); + if (!clientRect.contains(position().toPoint())) { + // input device above decoration + m_focus.decoration = ac->decoratedClient(); + } + } + + if (m_focus.decoration == oldDeco) { + // no change to decoration + return false; + } + cleanupDecoration(oldDeco.data(), m_focus.decoration.data()); + emit decorationChanged(); + return true; +} + +void InputDeviceHandler::updateInternalWindow(QWindow *window) +{ + if (m_focus.internalWindow == window) { + // no change + return; + } + const auto oldInternal = m_focus.internalWindow; + m_focus.internalWindow = window; + cleanupInternalWindow(oldInternal, window); +} + +void InputDeviceHandler::update() +{ + if (!m_inited) { + return; + } + + Toplevel *toplevel = nullptr; + QWindow *internalWindow = nullptr; + + if (!positionValid()) { + const auto pos = position().toPoint(); + internalWindow = findInternalWindow(pos); + if (internalWindow) { + toplevel = workspace()->findInternal(internalWindow); + } else { + toplevel = input()->findToplevel(pos); + } + } + // Always set the toplevel at the position of the input device. + setAt(toplevel); + + if (focusUpdatesBlocked()) { + return; + } + + if (internalWindow) { + if (m_focus.internalWindow != internalWindow) { + // changed internal window + updateDecoration(); + updateInternalWindow(internalWindow); + updateFocus(); + } else if (updateDecoration()) { + // went onto or off from decoration, update focus + updateFocus(); + } + return; + } + updateInternalWindow(nullptr); + + if (m_focus.focus != m_at.at) { + // focus change + updateDecoration(); + updateFocus(); + return; + } + // check if switched to/from decoration while staying on the same Toplevel + if (updateDecoration()) { + // went onto or off from decoration, update focus + updateFocus(); + } +} + +Toplevel *InputDeviceHandler::at() const +{ + return m_at.at.data(); +} + +Toplevel *InputDeviceHandler::focus() const +{ + return m_focus.focus.data(); +} + +QWindow* InputDeviceHandler::findInternalWindow(const QPoint &pos) const +{ + if (waylandServer()->isScreenLocked()) { + return nullptr; + } + + const QList &internalClients = workspace()->internalClients(); + if (internalClients.isEmpty()) { + return nullptr; + } + + auto it = internalClients.end(); + do { + --it; + QWindow *w = (*it)->internalWindow(); + if (!w || !w->isVisible()) { + continue; + } + if (!(*it)->frameGeometry().contains(pos)) { + continue; + } + // check input mask + const QRegion mask = w->mask().translated(w->geometry().topLeft()); + if (!mask.isEmpty() && !mask.contains(pos)) { + continue; + } + if (w->property("outputOnly").toBool()) { + continue; + } + return w; + } while (it != internalClients.begin()); + + return nullptr; +} + +} // namespace diff --git a/input.h b/input.h new file mode 100644 index 0000000..18dc7f1 --- /dev/null +++ b/input.h @@ -0,0 +1,527 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + SPDX-FileCopyrightText: 2018 Roman Gilg + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_INPUT_H +#define KWIN_INPUT_H +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +class KGlobalAccelInterface; +class QKeySequence; +class QMouseEvent; +class QKeyEvent; +class QWheelEvent; + +namespace KWin +{ +class GlobalShortcutsManager; +class Toplevel; +class InputEventFilter; +class InputEventSpy; +class KeyboardInputRedirection; +class PointerInputRedirection; +class TabletInputRedirection; +class TouchInputRedirection; +class WindowSelectorFilter; +class SwitchEvent; +class TabletEvent; +class TabletInputFilter; + +namespace Decoration +{ +class DecoratedClientImpl; +} + +namespace LibInput +{ + class Connection; + class Device; +} + +/** + * @brief This class is responsible for redirecting incoming input to the surface which currently + * has input or send enter/leave events. + * + * In addition input is intercepted before passed to the surfaces to have KWin internal areas + * getting input first (e.g. screen edges) and filter the input event out if we currently have + * a full input grab. + */ +class KWIN_EXPORT InputRedirection : public QObject +{ + Q_OBJECT +public: + enum PointerButtonState { + PointerButtonReleased, + PointerButtonPressed + }; + enum PointerAxis { + PointerAxisVertical, + PointerAxisHorizontal + }; + enum PointerAxisSource { + PointerAxisSourceUnknown, + PointerAxisSourceWheel, + PointerAxisSourceFinger, + PointerAxisSourceContinuous, + PointerAxisSourceWheelTilt + }; + enum KeyboardKeyState { + KeyboardKeyReleased, + KeyboardKeyPressed, + KeyboardKeyAutoRepeat + }; + enum TabletEventType { + Axis, + Proximity, + Tip + }; + enum TabletToolType { + Pen, + Eraser, + Brush, + Pencil, + Airbrush, + Finger, + Mouse, + Lens, + Totem, + }; + enum Capability { + Tilt, + Pressure, + Distance, + Rotation, + Slider, + Wheel, + }; + + ~InputRedirection() override; + void init(); + + /** + * @return const QPointF& The current global pointer position + */ + QPointF globalPointer() const; + Qt::MouseButtons qtButtonStates() const; + Qt::KeyboardModifiers keyboardModifiers() const; + Qt::KeyboardModifiers modifiersRelevantForGlobalShortcuts() const; + + void registerShortcut(const QKeySequence &shortcut, QAction *action); + /** + * @overload + * + * Like registerShortcut, but also connects QAction::triggered to the @p slot on @p receiver. + * It's recommended to use this method as it ensures that the X11 timestamp is updated prior + * to the @p slot being invoked. If not using this overload it's required to ensure that + * registerShortcut is called before connecting to QAction's triggered signal. + */ + template + void registerShortcut(const QKeySequence &shortcut, QAction *action, T *receiver, Slot slot); + void registerPointerShortcut(Qt::KeyboardModifiers modifiers, Qt::MouseButton pointerButtons, QAction *action); + void registerAxisShortcut(Qt::KeyboardModifiers modifiers, PointerAxisDirection axis, QAction *action); + void registerTouchpadSwipeShortcut(SwipeDirection direction, QAction *action); + void registerGlobalAccel(KGlobalAccelInterface *interface); + + /** + * @internal + */ + void processPointerMotion(const QPointF &pos, uint32_t time); + /** + * @internal + */ + void processPointerButton(uint32_t button, PointerButtonState state, uint32_t time); + /** + * @internal + */ + void processPointerAxis(PointerAxis axis, qreal delta, qint32 discreteDelta, PointerAxisSource source, uint32_t time); + /** + * @internal + */ + void processKeyboardKey(uint32_t key, KeyboardKeyState state, uint32_t time); + /** + * @internal + */ + void processKeyboardModifiers(uint32_t modsDepressed, uint32_t modsLatched, uint32_t modsLocked, uint32_t group); + /** + * @internal + */ + void processKeymapChange(int fd, uint32_t size); + void processTouchDown(qint32 id, const QPointF &pos, quint32 time); + void processTouchUp(qint32 id, quint32 time); + void processTouchMotion(qint32 id, const QPointF &pos, quint32 time); + void cancelTouch(); + void touchFrame(); + + bool supportsPointerWarping() const; + void warpPointer(const QPointF &pos); + + /** + * Adds the @p filter to the list of event filters and makes it the first + * event filter in processing. + * + * Note: the event filter will get events before the lock screen can get them, thus + * this is a security relevant method. + */ + void prependInputEventFilter(InputEventFilter *filter); + void uninstallInputEventFilter(InputEventFilter *filter); + + /** + * Installs the @p spy for spying on events. + */ + void installInputEventSpy(InputEventSpy *spy); + + /** + * Uninstalls the @p spy. This happens automatically when deleting an InputEventSpy. + */ + void uninstallInputEventSpy(InputEventSpy *spy); + + Toplevel *findToplevel(const QPoint &pos); + Toplevel *findManagedToplevel(const QPoint &pos); + GlobalShortcutsManager *shortcuts() const { + return m_shortcuts; + } + + /** + * Sends an event through all InputFilters. + * The method @p function is invoked on each input filter. Processing is stopped if + * a filter returns @c true for @p function. + * + * The UnaryPredicate is defined like the UnaryPredicate of std::any_of. + * The signature of the function should be equivalent to the following: + * @code + * bool function(const InputEventFilter *spy); + * @endcode + * + * The intended usage is to std::bind the method to invoke on the filter with all arguments + * bind. + */ + template + void processFilters(UnaryPredicate function) { + std::any_of(m_filters.constBegin(), m_filters.constEnd(), function); + } + + /** + * Sends an event through all input event spies. + * The @p function is invoked on each InputEventSpy. + * + * The UnaryFunction is defined like the UnaryFunction of std::for_each. + * The signature of the function should be equivalent to the following: + * @code + * void function(const InputEventSpy *spy); + * @endcode + * + * The intended usage is to std::bind the method to invoke on the spies with all arguments + * bind. + */ + template + void processSpies(UnaryFunction function) { + std::for_each(m_spies.constBegin(), m_spies.constEnd(), function); + } + + KeyboardInputRedirection *keyboard() const { + return m_keyboard; + } + PointerInputRedirection *pointer() const { + return m_pointer; + } + TabletInputRedirection *tablet() const { + return m_tablet; + } + TouchInputRedirection *touch() const { + return m_touch; + } + + bool hasAlphaNumericKeyboard(); + bool hasTabletModeSwitch(); + + void startInteractiveWindowSelection(std::function callback, const QByteArray &cursorName); + void startInteractivePositionSelection(std::function callback); + bool isSelectingWindow() const; + +Q_SIGNALS: + /** + * @brief Emitted when the global pointer position changed + * + * @param pos The new global pointer position. + */ + void globalPointerChanged(const QPointF &pos); + /** + * @brief Emitted when the state of a pointer button changed. + * + * @param button The button which changed + * @param state The new button state + */ + void pointerButtonStateChanged(uint32_t button, InputRedirection::PointerButtonState state); + /** + * @brief Emitted when a pointer axis changed + * + * @param axis The axis on which the even occurred + * @param delta The delta of the event. + */ + void pointerAxisChanged(InputRedirection::PointerAxis axis, qreal delta); + /** + * @brief Emitted when the modifiers changes. + * + * Only emitted for the mask which is provided by Qt::KeyboardModifiers, if other modifiers + * change signal is not emitted + * + * @param newMods The new modifiers state + * @param oldMods The previous modifiers state + */ + void keyboardModifiersChanged(Qt::KeyboardModifiers newMods, Qt::KeyboardModifiers oldMods); + /** + * @brief Emitted when the state of a key changed. + * + * @param keyCode The keycode of the key which changed + * @param state The new key state + */ + void keyStateChanged(quint32 keyCode, InputRedirection::KeyboardKeyState state); + + void hasAlphaNumericKeyboardChanged(bool set); + void hasTabletModeSwitchChanged(bool set); + +private: + void setupLibInput(); + void setupTouchpadShortcuts(); + void setupLibInputWithScreens(); + void setupWorkspace(); + void reconfigure(); + void setupInputFilters(); + void installInputEventFilter(InputEventFilter *filter); + KeyboardInputRedirection *m_keyboard; + PointerInputRedirection *m_pointer; + TabletInputRedirection *m_tablet; + TouchInputRedirection *m_touch; + TabletInputFilter *m_tabletSupport = nullptr; + + GlobalShortcutsManager *m_shortcuts; + + LibInput::Connection *m_libInput = nullptr; + + WindowSelectorFilter *m_windowSelector = nullptr; + + QVector m_filters; + QVector m_spies; + + KWIN_SINGLETON(InputRedirection) + friend InputRedirection *input(); + friend class DecorationEventFilter; + friend class InternalWindowEventFilter; + friend class ForwardInputFilter; +}; + +/** + * Base class for filtering input events inside InputRedirection. + * + * The idea behind the InputEventFilter is to have task oriented + * filters. E.g. there is one filter taking care of a locked screen, + * one to take care of interacting with window decorations, etc. + * + * A concrete subclass can reimplement the virtual methods and decide + * whether an event should be filtered out or not by returning either + * @c true or @c false. E.g. the lock screen filter can easily ensure + * that all events are filtered out. + * + * As soon as a filter returns @c true the processing is stopped. If + * a filter returns @c false the next one is invoked. This means a filter + * installed early gets to see more events than a filter installed later on. + * + * Deleting an instance of InputEventFilter automatically uninstalls it from + * InputRedirection. + */ +class KWIN_EXPORT InputEventFilter +{ +public: + InputEventFilter(); + virtual ~InputEventFilter(); + + /** + * Event filter for pointer events which can be described by a QMouseEvent. + * + * Please note that the button translation in QMouseEvent cannot cover all + * possible buttons. Because of that also the @p nativeButton code is passed + * through the filter. For internal areas it's fine to use @p event, but for + * passing to client windows the @p nativeButton should be used. + * + * @param event The event information about the move or button press/release + * @param nativeButton The native key code of the button, for move events 0 + * @return @c true to stop further event processing, @c false to pass to next filter + */ + virtual bool pointerEvent(QMouseEvent *event, quint32 nativeButton); + /** + * Event filter for pointer axis events. + * + * @param event The event information about the axis event + * @return @c true to stop further event processing, @c false to pass to next filter + */ + virtual bool wheelEvent(QWheelEvent *event); + /** + * Event filter for keyboard events. + * + * @param event The event information about the key event + * @return @c tru to stop further event processing, @c false to pass to next filter. + */ + virtual bool keyEvent(QKeyEvent *event); + virtual bool touchDown(qint32 id, const QPointF &pos, quint32 time); + virtual bool touchMotion(qint32 id, const QPointF &pos, quint32 time); + virtual bool touchUp(qint32 id, quint32 time); + + virtual bool pinchGestureBegin(int fingerCount, quint32 time); + virtual bool pinchGestureUpdate(qreal scale, qreal angleDelta, const QSizeF &delta, quint32 time); + virtual bool pinchGestureEnd(quint32 time); + virtual bool pinchGestureCancelled(quint32 time); + + virtual bool swipeGestureBegin(int fingerCount, quint32 time); + virtual bool swipeGestureUpdate(const QSizeF &delta, quint32 time); + virtual bool swipeGestureEnd(quint32 time); + virtual bool swipeGestureCancelled(quint32 time); + + virtual bool switchEvent(SwitchEvent *event); + + virtual bool tabletToolEvent(TabletEvent *event); + virtual bool tabletToolButtonEvent(const QSet &buttons); + virtual bool tabletPadButtonEvent(const QSet &buttons); + virtual bool tabletPadStripEvent(int number, int position, bool isFinger); + virtual bool tabletPadRingEvent(int number, int position, bool isFinger); + +protected: + void passToWaylandServer(QKeyEvent *event); +}; + +class KWIN_EXPORT InputDeviceHandler : public QObject +{ + Q_OBJECT +public: + ~InputDeviceHandler() override; + virtual void init(); + + void update(); + + /** + * @brief First Toplevel currently at the position of the input device + * according to the stacking order. + * @return Toplevel* at device position. + * + * This will be null if no toplevel is at the position + */ + Toplevel *at() const; + /** + * @brief Toplevel currently having pointer input focus (this might + * be different from the Toplevel at the position of the pointer). + * @return Toplevel* with pointer focus. + * + * This will be null if no toplevel has focus + */ + Toplevel *focus() const; + + /** + * @brief The Decoration currently receiving events. + * @return decoration with pointer focus. + */ + QPointer decoration() const { + return m_focus.decoration; + } + /** + * @brief The internal window currently receiving events. + * @return QWindow with pointer focus. + */ + QPointer internalWindow() const { + return m_focus.internalWindow; + } + + virtual QPointF position() const = 0; + + void setFocus(Toplevel *toplevel); + void setDecoration(QPointer decoration); + void setInternalWindow(QWindow *window); + +Q_SIGNALS: + void atChanged(Toplevel *old, Toplevel *now); + void decorationChanged(); + +protected: + explicit InputDeviceHandler(InputRedirection *parent); + + virtual void cleanupInternalWindow(QWindow *old, QWindow *now) = 0; + virtual void cleanupDecoration(Decoration::DecoratedClientImpl *old, Decoration::DecoratedClientImpl *now) = 0; + + virtual void focusUpdate(Toplevel *old, Toplevel *now) = 0; + + /** + * Certain input devices can be in a state of having no valid + * position. An example are touch screens when no finger/pen + * is resting on the surface (no touch point). + */ + virtual bool positionValid() const { + return false; + } + virtual bool focusUpdatesBlocked() { + return false; + } + + inline bool inited() const { + return m_inited; + } + inline void setInited(bool set) { + m_inited = set; + } + +private: + bool setAt(Toplevel *toplevel); + void updateFocus(); + bool updateDecoration(); + void updateInternalWindow(QWindow *window); + + QWindow* findInternalWindow(const QPoint &pos) const; + + struct { + QPointer at; + QMetaObject::Connection surfaceCreatedConnection; + } m_at; + + struct { + QPointer focus; + QPointer decoration; + QPointer internalWindow; + } m_focus; + + bool m_inited = false; +}; + +inline +InputRedirection *input() +{ + return InputRedirection::s_self; +} + +template +inline +void InputRedirection::registerShortcut(const QKeySequence &shortcut, QAction *action, T *receiver, Slot slot) { + registerShortcut(shortcut, action); + connect(action, &QAction::triggered, receiver, slot); +} + +} // namespace KWin + +Q_DECLARE_METATYPE(KWin::InputRedirection::KeyboardKeyState) +Q_DECLARE_METATYPE(KWin::InputRedirection::PointerButtonState) +Q_DECLARE_METATYPE(KWin::InputRedirection::PointerAxis) +Q_DECLARE_METATYPE(KWin::InputRedirection::PointerAxisSource) + +#endif // KWIN_INPUT_H diff --git a/input_event.cpp b/input_event.cpp new file mode 100644 index 0000000..e6047a1 --- /dev/null +++ b/input_event.cpp @@ -0,0 +1,70 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "input_event.h" + +namespace KWin +{ + +MouseEvent::MouseEvent(QEvent::Type type, const QPointF &pos, Qt::MouseButton button, + Qt::MouseButtons buttons, Qt::KeyboardModifiers modifiers, + quint32 timestamp, const QSizeF &delta, const QSizeF &deltaNonAccelerated, + quint64 timestampMicroseconds, LibInput::Device *device) + : QMouseEvent(type, pos, pos, button, buttons, modifiers) + , m_delta(delta) + , m_deltaUnccelerated(deltaNonAccelerated) + , m_timestampMicroseconds(timestampMicroseconds) + , m_device(device) +{ + setTimestamp(timestamp); +} + +WheelEvent::WheelEvent(const QPointF &pos, qreal delta, qint32 discreteDelta, Qt::Orientation orientation, + Qt::MouseButtons buttons, Qt::KeyboardModifiers modifiers, InputRedirection::PointerAxisSource source, + quint32 timestamp, LibInput::Device *device) + : QWheelEvent(pos, pos, QPoint(), (orientation == Qt::Horizontal) ? QPoint(delta, 0) : QPoint(0, delta), delta, orientation, buttons, modifiers) + , m_device(device) + , m_orientation(orientation) + , m_delta(delta) + , m_discreteDelta(discreteDelta) + , m_source(source) +{ + setTimestamp(timestamp); +} + +KeyEvent::KeyEvent(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers, quint32 code, quint32 keysym, + const QString &text, bool autorepeat, quint32 timestamp, LibInput::Device *device) + : QKeyEvent(type, key, modifiers, code, keysym, 0, text, autorepeat) + , m_device(device) +{ + setTimestamp(timestamp); +} + +SwitchEvent::SwitchEvent(State state, quint32 timestamp, quint64 timestampMicroseconds, LibInput::Device* device) + : QInputEvent(QEvent::User) + , m_state(state) + , m_timestampMicroseconds(timestampMicroseconds) + , m_device(device) +{ + setTimestamp(timestamp); +} + +TabletEvent::TabletEvent(Type t, const QPointF &pos, const QPointF &globalPos, + int device, int pointerType, qreal pressure, int xTilt, int yTilt, + qreal tangentialPressure, qreal rotation, int z, + Qt::KeyboardModifiers keyState, qint64 uniqueID, + Qt::MouseButton button, Qt::MouseButtons buttons, InputRedirection::TabletToolType toolType, const QVector &capabilities, quint64 serialId, const QString &tabletSysName) + : QTabletEvent(t, pos, globalPos, device, pointerType, pressure, xTilt, yTilt, tangentialPressure, rotation, z, keyState, uniqueID, button, buttons) + , m_toolType(toolType) + , m_capabilities(capabilities) + , m_serialId(serialId) + , m_tabletSysName(tabletSysName) +{ +} + +} diff --git a/input_event.h b/input_event.h new file mode 100644 index 0000000..8261f39 --- /dev/null +++ b/input_event.h @@ -0,0 +1,193 @@ +/* + 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 KWIN_INPUT_EVENT_H +#define KWIN_INPUT_EVENT_H + +#include "input.h" + +#include + +namespace KWin +{ + +namespace LibInput +{ +class Device; +} + +class MouseEvent : public QMouseEvent +{ +public: + explicit MouseEvent(QEvent::Type type, const QPointF &pos, Qt::MouseButton button, Qt::MouseButtons buttons, + Qt::KeyboardModifiers modifiers, quint32 timestamp, + const QSizeF &delta, const QSizeF &deltaNonAccelerated, quint64 timestampMicroseconds, + LibInput::Device *device); + + QSizeF delta() const { + return m_delta; + } + + QSizeF deltaUnaccelerated() const { + return m_deltaUnccelerated; + } + + quint64 timestampMicroseconds() const { + return m_timestampMicroseconds; + } + + LibInput::Device *device() const { + return m_device; + } + + Qt::KeyboardModifiers modifiersRelevantForGlobalShortcuts() const { + return m_modifiersRelevantForShortcuts; + } + + void setModifiersRelevantForGlobalShortcuts(const Qt::KeyboardModifiers &mods) { + m_modifiersRelevantForShortcuts = mods; + } + + quint32 nativeButton() const { + return m_nativeButton; + } + + void setNativeButton(quint32 button) { + m_nativeButton = button; + } + +private: + QSizeF m_delta; + QSizeF m_deltaUnccelerated; + quint64 m_timestampMicroseconds; + LibInput::Device *m_device; + Qt::KeyboardModifiers m_modifiersRelevantForShortcuts = Qt::KeyboardModifiers(); + quint32 m_nativeButton = 0; +}; + +// TODO: Don't derive from QWheelEvent, this event is quite domain specific. +class WheelEvent : public QWheelEvent +{ +public: + explicit WheelEvent(const QPointF &pos, qreal delta, qint32 discreteDelta, Qt::Orientation orientation, + Qt::MouseButtons buttons, Qt::KeyboardModifiers modifiers, InputRedirection::PointerAxisSource source, + quint32 timestamp, LibInput::Device *device); + + Qt::Orientation orientation() const { + return m_orientation; + } + + qreal delta() const { + return m_delta; + } + + qint32 discreteDelta() const { + return m_discreteDelta; + } + + InputRedirection::PointerAxisSource axisSource() const { + return m_source; + } + + LibInput::Device *device() const { + return m_device; + } + + Qt::KeyboardModifiers modifiersRelevantForGlobalShortcuts() const { + return m_modifiersRelevantForShortcuts; + } + + void setModifiersRelevantForGlobalShortcuts(const Qt::KeyboardModifiers &mods) { + m_modifiersRelevantForShortcuts = mods; + } + +private: + LibInput::Device *m_device; + Qt::Orientation m_orientation; + qreal m_delta; + qint32 m_discreteDelta; + InputRedirection::PointerAxisSource m_source; + Qt::KeyboardModifiers m_modifiersRelevantForShortcuts = Qt::KeyboardModifiers(); +}; + +class KeyEvent : public QKeyEvent +{ +public: + explicit KeyEvent(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers, quint32 code, quint32 keysym, + const QString &text, bool autorepeat, quint32 timestamp, LibInput::Device *device); + + LibInput::Device *device() const { + return m_device; + } + + Qt::KeyboardModifiers modifiersRelevantForGlobalShortcuts() const { + return m_modifiersRelevantForShortcuts; + } + + void setModifiersRelevantForGlobalShortcuts(const Qt::KeyboardModifiers &mods) { + m_modifiersRelevantForShortcuts = mods; + } + +private: + LibInput::Device *m_device; + Qt::KeyboardModifiers m_modifiersRelevantForShortcuts = Qt::KeyboardModifiers(); +}; + +class SwitchEvent : public QInputEvent +{ +public: + enum class State { + Off, + On + }; + explicit SwitchEvent(State state, quint32 timestamp, quint64 timestampMicroseconds, LibInput::Device *device); + + State state() const { + return m_state; + } + + quint64 timestampMicroseconds() const { + return m_timestampMicroseconds; + } + + LibInput::Device *device() const { + return m_device; + } + +private: + State m_state; + quint64 m_timestampMicroseconds; + LibInput::Device *m_device; +}; + +class TabletEvent : public QTabletEvent +{ +public: + TabletEvent(Type t, const QPointF &pos, const QPointF &globalPos, + int device, int pointerType, qreal pressure, int xTilt, int yTilt, + qreal tangentialPressure, qreal rotation, int z, + Qt::KeyboardModifiers keyState, qint64 uniqueID, + Qt::MouseButton button, Qt::MouseButtons buttons, InputRedirection::TabletToolType toolType, + const QVector &capabilities, + quint64 serialId, const QString &tabletSysname); + + InputRedirection::TabletToolType toolType() const { return m_toolType; } + QVector capabilities() const { return m_capabilities; } + quint64 serialId() const { return m_serialId; } + QString tabletSysName() { return m_tabletSysName; } + +private: + const InputRedirection::TabletToolType m_toolType; + const QVector m_capabilities; + const quint64 m_serialId; + const QString m_tabletSysName; +}; + +} + +#endif diff --git a/input_event_spy.cpp b/input_event_spy.cpp new file mode 100644 index 0000000..a143aa7 --- /dev/null +++ b/input_event_spy.cpp @@ -0,0 +1,141 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "input_event_spy.h" +#include "input.h" + +#include +#include + +namespace KWin +{ + +InputEventSpy::InputEventSpy() = default; + +InputEventSpy::~InputEventSpy() +{ + if (input()) { + input()->uninstallInputEventSpy(this); + } +} + +void InputEventSpy::pointerEvent(MouseEvent *event) +{ + Q_UNUSED(event) +} + +void InputEventSpy::wheelEvent(WheelEvent *event) +{ + Q_UNUSED(event) +} + +void InputEventSpy::keyEvent(KeyEvent *event) +{ + Q_UNUSED(event) +} + +void InputEventSpy::touchDown(qint32 id, const QPointF &point, quint32 time) +{ + Q_UNUSED(id) + Q_UNUSED(point) + Q_UNUSED(time) +} + +void InputEventSpy::touchMotion(qint32 id, const QPointF &point, quint32 time) +{ + Q_UNUSED(id) + Q_UNUSED(point) + Q_UNUSED(time) +} + +void InputEventSpy::touchUp(qint32 id, quint32 time) +{ + Q_UNUSED(id) + Q_UNUSED(time) +} + +void InputEventSpy::pinchGestureBegin(int fingerCount, quint32 time) +{ + Q_UNUSED(fingerCount) + Q_UNUSED(time) +} + +void InputEventSpy::pinchGestureUpdate(qreal scale, qreal angleDelta, const QSizeF &delta, quint32 time) +{ + Q_UNUSED(scale) + Q_UNUSED(angleDelta) + Q_UNUSED(delta) + Q_UNUSED(time) +} + +void InputEventSpy::pinchGestureEnd(quint32 time) +{ + Q_UNUSED(time) +} + +void InputEventSpy::pinchGestureCancelled(quint32 time) +{ + Q_UNUSED(time) +} + +void InputEventSpy::swipeGestureBegin(int fingerCount, quint32 time) +{ + Q_UNUSED(fingerCount) + Q_UNUSED(time) +} + +void InputEventSpy::swipeGestureUpdate(const QSizeF &delta, quint32 time) +{ + Q_UNUSED(delta) + Q_UNUSED(time) +} + +void InputEventSpy::swipeGestureEnd(quint32 time) +{ + Q_UNUSED(time) +} + +void InputEventSpy::swipeGestureCancelled(quint32 time) +{ + Q_UNUSED(time) +} + +void InputEventSpy::switchEvent(SwitchEvent *event) +{ + Q_UNUSED(event) +} + +void InputEventSpy::tabletToolEvent(TabletEvent *event) +{ + Q_UNUSED(event) +} + +void InputEventSpy::tabletToolButtonEvent(const QSet &pressedButtons) +{ + Q_UNUSED(pressedButtons) +} + +void InputEventSpy::tabletPadButtonEvent(const QSet &pressedButtons) +{ + Q_UNUSED(pressedButtons) +} + +void InputEventSpy::tabletPadStripEvent(int number, int position, bool isFinger) +{ + Q_UNUSED(number) + Q_UNUSED(position) + Q_UNUSED(isFinger) +} + +void InputEventSpy::tabletPadRingEvent(int number, int position, bool isFinger) +{ + Q_UNUSED(number) + Q_UNUSED(position) + Q_UNUSED(isFinger) +} +} diff --git a/input_event_spy.h b/input_event_spy.h new file mode 100644 index 0000000..6015d6b --- /dev/null +++ b/input_event_spy.h @@ -0,0 +1,87 @@ +/* + 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 KWIN_INPUT_EVENT_SPY_H +#define KWIN_INPUT_EVENT_SPY_H +#include + +#include + +class QPointF; +class QSizeF; +class QTabletEvent; + +namespace KWin +{ +class KeyEvent; +class MouseEvent; +class WheelEvent; +class SwitchEvent; +class TabletEvent; + +/** + * Base class for spying on input events inside InputRedirection. + * + * This class is quite similar to InputEventFilter, except that it does not + * support event filtering. Each InputEventSpy gets to see all input events, + * the processing happens prior to sending events through the InputEventFilters. + * + * Deleting an instance of InputEventSpy automatically uninstalls it from + * InputRedirection. + */ +class KWIN_EXPORT InputEventSpy +{ +public: + InputEventSpy(); + virtual ~InputEventSpy(); + + /** + * Event spy for pointer events which can be described by a MouseEvent. + * + * @param event The event information about the move or button press/release + */ + virtual void pointerEvent(MouseEvent *event); + /** + * Event spy for pointer axis events. + * + * @param event The event information about the axis event + */ + virtual void wheelEvent(WheelEvent *event); + /** + * Event spy for keyboard events. + * + * @param event The event information about the key event + */ + virtual void keyEvent(KeyEvent *event); + virtual void touchDown(qint32 id, const QPointF &pos, quint32 time); + virtual void touchMotion(qint32 id, const QPointF &pos, quint32 time); + virtual void touchUp(qint32 id, quint32 time); + + virtual void pinchGestureBegin(int fingerCount, quint32 time); + virtual void pinchGestureUpdate(qreal scale, qreal angleDelta, const QSizeF &delta, quint32 time); + virtual void pinchGestureEnd(quint32 time); + virtual void pinchGestureCancelled(quint32 time); + + virtual void swipeGestureBegin(int fingerCount, quint32 time); + virtual void swipeGestureUpdate(const QSizeF &delta, quint32 time); + virtual void swipeGestureEnd(quint32 time); + virtual void swipeGestureCancelled(quint32 time); + + virtual void switchEvent(SwitchEvent *event); + + virtual void tabletToolEvent(TabletEvent *event); + virtual void tabletToolButtonEvent(const QSet &pressedButtons); + virtual void tabletPadButtonEvent(const QSet &pressedButtons); + virtual void tabletPadStripEvent(int number, int position, bool isFinger); + virtual void tabletPadRingEvent(int number, int position, bool isFinger); +}; + + +} // namespace KWin + +#endif diff --git a/inputpanelv1client.cpp b/inputpanelv1client.cpp new file mode 100644 index 0000000..f4b6ac2 --- /dev/null +++ b/inputpanelv1client.cpp @@ -0,0 +1,129 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "inputpanelv1client.h" +#include "deleted.h" +#include "wayland_server.h" +#include "workspace.h" +#include "abstract_wayland_output.h" +#include "platform.h" +#include +#include +#include +#include + +using namespace KWaylandServer; + +namespace KWin +{ + +InputPanelV1Client::InputPanelV1Client(InputPanelSurfaceV1Interface *panelSurface) + : WaylandClient(panelSurface->surface()) + , m_panelSurface(panelSurface) +{ + setSkipSwitcher(true); + setSkipPager(true); + setSkipTaskbar(true); + setPositionSyncMode(SyncMode::Sync); + setSizeSyncMode(SyncMode::Sync); + + connect(surface(), &SurfaceInterface::aboutToBeDestroyed, this, &InputPanelV1Client::destroyClient); + connect(surface(), &SurfaceInterface::sizeChanged, this, &InputPanelV1Client::reposition); + connect(surface(), &SurfaceInterface::mapped, this, &InputPanelV1Client::updateDepth); + connect(surface(), &SurfaceInterface::damaged, this, QOverload::of(&WaylandClient::addRepaint)); + + connect(panelSurface, &InputPanelSurfaceV1Interface::topLevel, this, &InputPanelV1Client::showTopLevel); + connect(panelSurface, &InputPanelSurfaceV1Interface::overlayPanel, this, &InputPanelV1Client::showOverlayPanel); + connect(panelSurface, &InputPanelSurfaceV1Interface::destroyed, this, &InputPanelV1Client::destroyClient); +} + +void InputPanelV1Client::showOverlayPanel() +{ + setOutput(nullptr); + m_mode = Overlay; + reposition(); + setReadyForPainting(); +} + +void InputPanelV1Client::showTopLevel(OutputInterface *output, InputPanelSurfaceV1Interface::Position position) +{ + Q_UNUSED(position); + m_mode = Toplevel; + setOutput(output); + reposition(); + setReadyForPainting(); +} + +void KWin::InputPanelV1Client::reposition() +{ + switch (m_mode) { + case Toplevel: { + if (m_output) { + const QSize panelSize = surface()->size(); + if (!panelSize.isValid() || panelSize.isEmpty()) { + return; + } + + const auto outputGeometry = m_output->geometry(); + QRect geo(outputGeometry.topLeft(), panelSize); + geo.translate((outputGeometry.width() - panelSize.width())/2, outputGeometry.height() - panelSize.height()); + updateGeometry(geo); + } + } break; + case Overlay: { + auto textClient = waylandServer()->findClient(waylandServer()->seat()->focusedTextInputSurface()); + auto textInput = waylandServer()->seat()->focusedTextInput(); + if (textClient && textInput) { + const auto cursorRectangle = textInput->cursorRectangle(); + updateGeometry({textClient->pos() + textClient->clientPos() + cursorRectangle.bottomLeft(), surface()->size()}); + } + } break; + } +} + +void InputPanelV1Client::destroyClient() +{ + markAsZombie(); + + Deleted *deleted = Deleted::create(this); + emit windowClosed(this, deleted); + StackingUpdatesBlocker blocker(workspace()); + waylandServer()->removeClient(this); + deleted->unrefWindow(); + + delete this; +} + +NET::WindowType InputPanelV1Client::windowType(bool, int) const +{ + return NET::Utility; +} + +QRect InputPanelV1Client::inputGeometry() const +{ + if (surface()->inputIsInfinite()) { + return frameGeometry(); + } + return surface()->input().boundingRect().translated(pos()); +} + +void InputPanelV1Client::setOutput(OutputInterface *outputIface) +{ + if (m_output) { + disconnect(m_output, &AbstractWaylandOutput::geometryChanged, this, &InputPanelV1Client::reposition); + } + + m_output = waylandServer()->findOutput(outputIface); + + if (m_output) { + connect(m_output, &AbstractWaylandOutput::geometryChanged, this, &InputPanelV1Client::reposition); + } +} + +} // namespace KWin diff --git a/inputpanelv1client.h b/inputpanelv1client.h new file mode 100644 index 0000000..e47eb28 --- /dev/null +++ b/inputpanelv1client.h @@ -0,0 +1,58 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "waylandclient.h" +#include +#include + +namespace KWin +{ +class AbstractWaylandOutput; + +class InputPanelV1Client : public WaylandClient +{ + Q_OBJECT +public: + InputPanelV1Client(KWaylandServer::InputPanelSurfaceV1Interface *panelSurface); + + enum Mode { + Toplevel, + Overlay, + }; + Q_ENUM(Mode) + + void destroyClient() override; + bool isPlaceable() const override { return false; } + bool isCloseable() const override { return false; } + bool isResizable() const override { return false; } + bool isMovable() const override { return false; } + bool isMovableAcrossScreens() const override { return false; } + bool acceptsFocus() const override { return false; } + void closeWindow() override {} + bool takeFocus() override { return false; } + bool wantsInput() const override { return false; } + bool isInputMethod() const override { return true; } + bool isInitialPositionSet() const override { return true; } + NET::WindowType windowType(bool /*direct*/, int /*supported_types*/) const override; + QRect inputGeometry() const override; + +private: + void showTopLevel(KWaylandServer::OutputInterface *output, KWaylandServer::InputPanelSurfaceV1Interface::Position position); + void showOverlayPanel(); + void reposition(); + void setOutput(KWaylandServer::OutputInterface* output); + + QPointer m_output; + Mode m_mode = Toplevel; + const QPointer m_panelSurface; +}; + +} diff --git a/inputpanelv1integration.cpp b/inputpanelv1integration.cpp new file mode 100644 index 0000000..0d035f7 --- /dev/null +++ b/inputpanelv1integration.cpp @@ -0,0 +1,33 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "inputpanelv1integration.h" +#include "inputpanelv1client.h" +#include "wayland_server.h" + +#include +#include + +using namespace KWaylandServer; + +namespace KWin +{ + +InputPanelV1Integration::InputPanelV1Integration(QObject *parent) + : WaylandShellIntegration(parent) +{ + InputPanelV1Interface *shell = waylandServer()->display()->createInputPanelInterface(this); + + connect(shell, &InputPanelV1Interface::inputPanelSurfaceAdded, + this, &InputPanelV1Integration::createClient); +} + +void InputPanelV1Integration::createClient(InputPanelSurfaceV1Interface *shellSurface) +{ + emit clientCreated(new InputPanelV1Client(shellSurface)); +} + +} // namespace KWin diff --git a/inputpanelv1integration.h b/inputpanelv1integration.h new file mode 100644 index 0000000..fd9464b --- /dev/null +++ b/inputpanelv1integration.h @@ -0,0 +1,29 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "waylandshellintegration.h" + +namespace KWaylandServer +{ +class InputPanelSurfaceV1Interface; +} + +namespace KWin +{ + +class InputPanelV1Integration : public WaylandShellIntegration +{ + Q_OBJECT + +public: + explicit InputPanelV1Integration(QObject *parent = nullptr); + + void createClient(KWaylandServer::InputPanelSurfaceV1Interface *shellSurface); +}; + +} // namespace KWin diff --git a/internal_client.cpp b/internal_client.cpp new file mode 100644 index 0000000..3391de4 --- /dev/null +++ b/internal_client.cpp @@ -0,0 +1,563 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Martin Flöser + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "internal_client.h" +#include "decorations/decorationbridge.h" +#include "deleted.h" +#include "workspace.h" + +#include + +#include +#include + +Q_DECLARE_METATYPE(NET::WindowType) + +static const QByteArray s_skipClosePropertyName = QByteArrayLiteral("KWIN_SKIP_CLOSE_ANIMATION"); +static const QByteArray s_shadowEnabledPropertyName = QByteArrayLiteral("kwin_shadow_enabled"); + +namespace KWin +{ + +InternalClient::InternalClient(QWindow *window) + : m_internalWindow(window) + , m_windowId(window->winId()) + , m_internalWindowFlags(window->flags()) +{ + connect(m_internalWindow, &QWindow::xChanged, this, &InternalClient::updateInternalWindowGeometry); + connect(m_internalWindow, &QWindow::yChanged, this, &InternalClient::updateInternalWindowGeometry); + connect(m_internalWindow, &QWindow::widthChanged, this, &InternalClient::updateInternalWindowGeometry); + connect(m_internalWindow, &QWindow::heightChanged, this, &InternalClient::updateInternalWindowGeometry); + connect(m_internalWindow, &QWindow::windowTitleChanged, this, &InternalClient::setCaption); + connect(m_internalWindow, &QWindow::opacityChanged, this, &InternalClient::setOpacity); + connect(m_internalWindow, &QWindow::destroyed, this, &InternalClient::destroyClient); + + connect(this, &InternalClient::opacityChanged, this, &InternalClient::addRepaintFull); + + const QVariant windowType = m_internalWindow->property("kwin_windowType"); + if (!windowType.isNull()) { + m_windowType = windowType.value(); + } + + setCaption(m_internalWindow->title()); + setIcon(QIcon::fromTheme(QStringLiteral("kwin"))); + setOnAllDesktops(true); + setOpacity(m_internalWindow->opacity()); + setSkipCloseAnimation(m_internalWindow->property(s_skipClosePropertyName).toBool()); + + // Create scene window, effect window, and update server-side shadow. + setupCompositing(); + updateColorScheme(); + + blockGeometryUpdates(true); + commitGeometry(m_internalWindow->geometry()); + updateDecoration(true); + setFrameGeometry(clientRectToFrameRect(m_internalWindow->geometry())); + setGeometryRestore(frameGeometry()); + blockGeometryUpdates(false); + + m_internalWindow->installEventFilter(this); +} + +InternalClient::~InternalClient() +{ +} + +bool InternalClient::eventFilter(QObject *watched, QEvent *event) +{ + if (watched == m_internalWindow && event->type() == QEvent::DynamicPropertyChange) { + QDynamicPropertyChangeEvent *pe = static_cast(event); + if (pe->propertyName() == s_skipClosePropertyName) { + setSkipCloseAnimation(m_internalWindow->property(s_skipClosePropertyName).toBool()); + } + if (pe->propertyName() == s_shadowEnabledPropertyName) { + updateShadow(); + } + if (pe->propertyName() == "kwin_windowType") { + m_windowType = m_internalWindow->property("kwin_windowType").value(); + workspace()->updateClientArea(); + } + } + return false; +} + +QRect InternalClient::bufferGeometry() const +{ + return m_clientGeometry; +} + +QStringList InternalClient::activities() const +{ + return QStringList(); +} + +void InternalClient::blockActivityUpdates(bool b) +{ + Q_UNUSED(b) + + // Internal clients do not support activities. +} + +qreal InternalClient::bufferScale() const +{ + if (m_internalWindow) { + return m_internalWindow->devicePixelRatio(); + } + return 1; +} + +QString InternalClient::captionNormal() const +{ + return m_captionNormal; +} + +QString InternalClient::captionSuffix() const +{ + return m_captionSuffix; +} + +QPoint InternalClient::clientContentPos() const +{ + return -1 * clientPos(); +} + +QSize InternalClient::minSize() const +{ + return m_internalWindow->minimumSize(); +} + +QSize InternalClient::maxSize() const +{ + return m_internalWindow->maximumSize(); +} + +QRect InternalClient::transparentRect() const +{ + return QRect(); +} + +NET::WindowType InternalClient::windowType(bool direct, int supported_types) const +{ + Q_UNUSED(direct) + Q_UNUSED(supported_types) + return m_windowType; +} + +double InternalClient::opacity() const +{ + return m_opacity; +} + +void InternalClient::setOpacity(double opacity) +{ + if (m_opacity == opacity) { + return; + } + + const double oldOpacity = m_opacity; + m_opacity = opacity; + + emit opacityChanged(this, oldOpacity); +} + +void InternalClient::killWindow() +{ + // We don't kill our internal windows. +} + +bool InternalClient::isPopupWindow() const +{ + if (AbstractClient::isPopupWindow()) { + return true; + } + return m_internalWindowFlags.testFlag(Qt::Popup); +} + +QByteArray InternalClient::windowRole() const +{ + return QByteArray(); +} + +void InternalClient::closeWindow() +{ + if (m_internalWindow) { + m_internalWindow->hide(); + } +} + +bool InternalClient::isCloseable() const +{ + return true; +} + +bool InternalClient::isMovable() const +{ + return true; +} + +bool InternalClient::isMovableAcrossScreens() const +{ + return true; +} + +bool InternalClient::isResizable() const +{ + return true; +} + +bool InternalClient::isPlaceable() const +{ + return !m_internalWindowFlags.testFlag(Qt::BypassWindowManagerHint) && !m_internalWindowFlags.testFlag(Qt::Popup); +} + +bool InternalClient::noBorder() const +{ + return m_userNoBorder || m_internalWindowFlags.testFlag(Qt::FramelessWindowHint) || m_internalWindowFlags.testFlag(Qt::Popup); +} + +bool InternalClient::userCanSetNoBorder() const +{ + return !m_internalWindowFlags.testFlag(Qt::FramelessWindowHint) || m_internalWindowFlags.testFlag(Qt::Popup); +} + +bool InternalClient::wantsInput() const +{ + return false; +} + +bool InternalClient::isInternal() const +{ + return true; +} + +bool InternalClient::isLockScreen() const +{ + if (m_internalWindow) { + return m_internalWindow->property("org_kde_ksld_emergency").toBool(); + } + return false; +} + +bool InternalClient::isOutline() const +{ + if (m_internalWindow) { + return m_internalWindow->property("__kwin_outline").toBool(); + } + return false; +} + +quint32 InternalClient::windowId() const +{ + return m_windowId; +} + +bool InternalClient::isShown(bool shaded_is_shown) const +{ + Q_UNUSED(shaded_is_shown) + + return readyForPainting(); +} + +bool InternalClient::isHiddenInternal() const +{ + return false; +} + +void InternalClient::hideClient(bool hide) +{ + Q_UNUSED(hide) +} + +void InternalClient::resizeWithChecks(const QSize &size, ForceGeometry_t force) +{ + Q_UNUSED(force) + if (!m_internalWindow) { + return; + } + const QRect area = workspace()->clientArea(WorkArea, this); + setFrameGeometry(QRect{pos(), size.boundedTo(area.size())}, force); +} + +void InternalClient::setFrameGeometry(const QRect &rect, ForceGeometry_t force) +{ + if (areGeometryUpdatesBlocked()) { + m_frameGeometry = rect; + if (pendingGeometryUpdate() == PendingGeometryForced) { + // Maximum, nothing needed. + } else if (force == ForceGeometrySet) { + setPendingGeometryUpdate(PendingGeometryForced); + } else { + setPendingGeometryUpdate(PendingGeometryNormal); + } + return; + } + + if (pendingGeometryUpdate() != PendingGeometryNone) { + // Reset geometry to the one before blocking, so that we can compare properly. + m_frameGeometry = frameGeometryBeforeUpdateBlocking(); + } + + if (m_frameGeometry == rect) { + return; + } + + const QRect newClientGeometry = frameRectToClientRect(rect); + + if (clientSize() == newClientGeometry.size()) { + commitGeometry(rect); + } else { + requestGeometry(rect); + } +} + +AbstractClient *InternalClient::findModal(bool allow_itself) +{ + Q_UNUSED(allow_itself) + return nullptr; +} + +void InternalClient::setOnAllActivities(bool set) +{ + Q_UNUSED(set) + + // Internal clients do not support activities. +} + +bool InternalClient::takeFocus() +{ + return false; +} + +void InternalClient::setNoBorder(bool set) +{ + if (!userCanSetNoBorder()) { + return; + } + if (m_userNoBorder == set) { + return; + } + m_userNoBorder = set; + updateDecoration(true); +} + +void InternalClient::updateDecoration(bool check_workspace_pos, bool force) +{ + if (!force && isDecorated() == !noBorder()) { + return; + } + + const QRect oldFrameGeometry = frameGeometry(); + const QRect oldClientGeometry = oldFrameGeometry - frameMargins(); + + GeometryUpdatesBlocker blocker(this); + + if (force) { + destroyDecoration(); + } + + if (!noBorder()) { + createDecoration(oldClientGeometry); + } else { + destroyDecoration(); + } + + updateShadow(); + + if (check_workspace_pos) { + checkWorkspacePosition(oldFrameGeometry, -2, oldClientGeometry); + } +} + +void InternalClient::destroyClient() +{ + markAsZombie(); + if (isMoveResize()) { + leaveMoveResize(); + } + + Deleted *deleted = Deleted::create(this); + emit windowClosed(this, deleted); + + destroyDecoration(); + + workspace()->removeInternalClient(this); + + deleted->unrefWindow(); + m_internalWindow = nullptr; + + delete this; +} + +void InternalClient::present(const QSharedPointer fbo) +{ + Q_ASSERT(m_internalImage.isNull()); + + const QSize bufferSize = fbo->size() / bufferScale(); + + commitGeometry(QRect(pos(), clientSizeToFrameSize(bufferSize))); + markAsMapped(); + + if (m_internalFBO != fbo) { + discardWindowPixmap(); + m_internalFBO = fbo; + } + + setDepth(32); + addDamageFull(); + addRepaintFull(); +} + +void InternalClient::present(const QImage &image, const QRegion &damage) +{ + Q_ASSERT(m_internalFBO.isNull()); + + const QSize bufferSize = image.size() / bufferScale(); + + commitGeometry(QRect(pos(), clientSizeToFrameSize(bufferSize))); + markAsMapped(); + + if (m_internalImage.size() != image.size()) { + discardWindowPixmap(); + } + + m_internalImage = image; + + setDepth(32); + addDamage(damage); + addRepaint(damage.translated(borderLeft(), borderTop())); +} + +QWindow *InternalClient::internalWindow() const +{ + return m_internalWindow; +} + +bool InternalClient::acceptsFocus() const +{ + return false; +} + +bool InternalClient::belongsToSameApplication(const AbstractClient *other, SameApplicationChecks checks) const +{ + Q_UNUSED(checks) + + return qobject_cast(other) != nullptr; +} + +void InternalClient::doMove(int x, int y) +{ + Q_UNUSED(x) + Q_UNUSED(y) + + syncGeometryToInternalWindow(); +} + +void InternalClient::doResizeSync() +{ + requestGeometry(moveResizeGeometry()); +} + +void InternalClient::updateCaption() +{ + const QString oldSuffix = m_captionSuffix; + const auto shortcut = shortcutCaptionSuffix(); + m_captionSuffix = shortcut; + if ((!isSpecialWindow() || isToolbar()) && findClientWithSameCaption()) { + int i = 2; + do { + m_captionSuffix = shortcut + QLatin1String(" <") + QString::number(i) + QLatin1Char('>'); + i++; + } while (findClientWithSameCaption()); + } + if (m_captionSuffix != oldSuffix) { + emit captionChanged(); + } +} + +void InternalClient::requestGeometry(const QRect &rect) +{ + if (m_internalWindow) { + m_internalWindow->setGeometry(frameRectToClientRect(rect)); + } +} + +void InternalClient::commitGeometry(const QRect &rect) +{ + if (m_frameGeometry == rect && pendingGeometryUpdate() == PendingGeometryNone) { + return; + } + + // The client geometry and the buffer geometry are the same. + const QRect oldClientGeometry = m_clientGeometry; + const QRect oldFrameGeometry = m_frameGeometry; + + m_clientGeometry = frameRectToClientRect(rect); + m_frameGeometry = rect; + + addWorkspaceRepaint(visibleRect()); + updateGeometryBeforeUpdateBlocking(); + syncGeometryToInternalWindow(); + + if (oldClientGeometry != m_clientGeometry) { + emit bufferGeometryChanged(this, oldClientGeometry); + emit clientGeometryChanged(this, oldClientGeometry); + } + if (oldFrameGeometry != m_frameGeometry) { + emit frameGeometryChanged(this, oldFrameGeometry); + } + emit geometryShapeChanged(this, oldFrameGeometry); + + if (isResize()) { + performMoveResize(); + } +} + +void InternalClient::setCaption(const QString &caption) +{ + if (m_captionNormal == caption) { + return; + } + + m_captionNormal = caption; + + const QString oldCaptionSuffix = m_captionSuffix; + updateCaption(); + + if (m_captionSuffix == oldCaptionSuffix) { + emit captionChanged(); + } +} + +void InternalClient::markAsMapped() +{ + if (!ready_for_painting) { + setReadyForPainting(); + workspace()->addInternalClient(this); + } +} + +void InternalClient::syncGeometryToInternalWindow() +{ + if (m_internalWindow->geometry() == frameRectToClientRect(frameGeometry())) { + return; + } + + QTimer::singleShot(0, this, [this] { requestGeometry(frameGeometry()); }); +} + +void InternalClient::updateInternalWindowGeometry() +{ + if (isMoveResize()) { + return; + } + + commitGeometry(clientRectToFrameRect(m_internalWindow->geometry())); +} + +} diff --git a/internal_client.h b/internal_client.h new file mode 100644 index 0000000..7433c70 --- /dev/null +++ b/internal_client.h @@ -0,0 +1,99 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Martin Flöser + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "abstract_client.h" + +namespace KWin +{ + +class KWIN_EXPORT InternalClient : public AbstractClient +{ + Q_OBJECT + +public: + explicit InternalClient(QWindow *window); + ~InternalClient() override; + + bool eventFilter(QObject *watched, QEvent *event) override; + + QRect bufferGeometry() const override; + QStringList activities() const override; + void blockActivityUpdates(bool b = true) override; + qreal bufferScale() const override; + QString captionNormal() const override; + QString captionSuffix() const override; + QPoint clientContentPos() const override; + QSize minSize() const override; + QSize maxSize() const override; + QRect transparentRect() const override; + NET::WindowType windowType(bool direct = false, int supported_types = 0) const override; + double opacity() const override; + void setOpacity(double opacity) override; + void killWindow() override; + bool isPopupWindow() const override; + QByteArray windowRole() const override; + void closeWindow() override; + bool isCloseable() const override; + bool isMovable() const override; + bool isMovableAcrossScreens() const override; + bool isResizable() const override; + bool isPlaceable() const override; + bool noBorder() const override; + bool userCanSetNoBorder() const override; + bool wantsInput() const override; + bool isInternal() const override; + bool isLockScreen() const override; + bool isOutline() const override; + quint32 windowId() const override; + bool isShown(bool shaded_is_shown) const override; + bool isHiddenInternal() const override; + void hideClient(bool hide) override; + void resizeWithChecks(const QSize &size, ForceGeometry_t force = NormalGeometrySet) override; + void setFrameGeometry(const QRect &rect, ForceGeometry_t force = NormalGeometrySet) override; + AbstractClient *findModal(bool allow_itself = false) override; + void setOnAllActivities(bool set) override; + bool takeFocus() override; + void setNoBorder(bool set) override; + void updateDecoration(bool check_workspace_pos, bool force = false) override; + void destroyClient() override; + + void present(const QSharedPointer fbo); + void present(const QImage &image, const QRegion &damage); + QWindow *internalWindow() const; + +protected: + bool acceptsFocus() const override; + bool belongsToSameApplication(const AbstractClient *other, SameApplicationChecks checks) const override; + void doMove(int x, int y) override; + void doResizeSync() override; + void updateCaption() override; + +private: + void requestGeometry(const QRect &rect); + void commitGeometry(const QRect &rect); + void setCaption(const QString &caption); + void markAsMapped(); + void syncGeometryToInternalWindow(); + void updateInternalWindowGeometry(); + + QWindow *m_internalWindow = nullptr; + QString m_captionNormal; + QString m_captionSuffix; + double m_opacity = 1.0; + NET::WindowType m_windowType = NET::Normal; + quint32 m_windowId = 0; + Qt::WindowFlags m_internalWindowFlags = Qt::WindowFlags(); + bool m_userNoBorder = false; + + Q_DISABLE_COPY(InternalClient) +}; + +} diff --git a/kcmkwin/CMakeLists.txt b/kcmkwin/CMakeLists.txt new file mode 100644 index 0000000..c3de934 --- /dev/null +++ b/kcmkwin/CMakeLists.txt @@ -0,0 +1,15 @@ +remove_definitions(-DQT_NO_CAST_FROM_ASCII -DQT_STRICT_ITERATORS -DQT_NO_CAST_FROM_BYTEARRAY -DQT_NO_KEYWORDS) + +add_subdirectory(common) +add_subdirectory(kwincompositing) +add_subdirectory(kwinoptions) +add_subdirectory(kwindecoration) +add_subdirectory(kwinrules) +add_subdirectory(kwinscreenedges) +add_subdirectory(kwinscripts) +add_subdirectory(kwindesktop) +add_subdirectory(kwineffects) + +if (KWIN_BUILD_TABBOX) + add_subdirectory(kwintabbox) +endif() diff --git a/kcmkwin/common/CMakeLists.txt b/kcmkwin/common/CMakeLists.txt new file mode 100644 index 0000000..b41eab6 --- /dev/null +++ b/kcmkwin/common/CMakeLists.txt @@ -0,0 +1,32 @@ +# KI18N Translation Domain for this library +add_definitions(-DTRANSLATION_DOMAIN=\"kcmkwincommon\") + +include_directories(${KWin_SOURCE_DIR}/effects) + +set(kcmkwincommon_SRC + effectsmodel.cpp +) + +qt5_add_dbus_interface(kcmkwincommon_SRC + ${KWin_SOURCE_DIR}/org.kde.kwin.Effects.xml kwin_effects_interface +) + +add_library(kcmkwincommon SHARED ${kcmkwincommon_SRC}) + +target_link_libraries(kcmkwincommon + Qt5::Core + Qt5::DBus + KF5::CoreAddons + KF5::ConfigCore + KF5::I18n + KF5::Package + KF5::KCMUtils + kwin4_effect_builtins +) + +set_target_properties(kcmkwincommon PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR} +) + +install(TARGETS kcmkwincommon ${INSTALL_TARGETS_DEFAULT_ARGS} LIBRARY NAMELINK_SKIP) diff --git a/kcmkwin/common/Messages.sh b/kcmkwin/common/Messages.sh new file mode 100644 index 0000000..b4e10c6 --- /dev/null +++ b/kcmkwin/common/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name \*.cpp` -o $podir/kcmkwincommon.pot diff --git a/kcmkwin/common/effectsmodel.cpp b/kcmkwin/common/effectsmodel.cpp new file mode 100644 index 0000000..7f41653 --- /dev/null +++ b/kcmkwin/common/effectsmodel.cpp @@ -0,0 +1,671 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Antonis Tsiapaliokas + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "effectsmodel.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace KWin +{ + +static QString translatedCategory(const QString &category) +{ + static const QVector knownCategories = { + QStringLiteral("Accessibility"), + QStringLiteral("Appearance"), + QStringLiteral("Focus"), + QStringLiteral("Show Desktop Animation"), + QStringLiteral("Tools"), + QStringLiteral("Virtual Desktop Switching Animation"), + QStringLiteral("Window Management"), + QStringLiteral("Window Open/Close Animation") + }; + + static const QVector translatedCategories = { + i18nc("Category of Desktop Effects, used as section header", "Accessibility"), + i18nc("Category of Desktop Effects, used as section header", "Appearance"), + i18nc("Category of Desktop Effects, used as section header", "Focus"), + i18nc("Category of Desktop Effects, used as section header", "Show Desktop Animation"), + i18nc("Category of Desktop Effects, used as section header", "Tools"), + i18nc("Category of Desktop Effects, used as section header", "Virtual Desktop Switching Animation"), + i18nc("Category of Desktop Effects, used as section header", "Window Management"), + i18nc("Category of Desktop Effects, used as section header", "Window Open/Close Animation") + }; + + const int index = knownCategories.indexOf(category); + if (index == -1) { + qDebug() << "Unknown category '" << category << "' and thus not translated"; + return category; + } + + return translatedCategories[index]; +} + +static EffectsModel::Status effectStatus(bool enabled) +{ + return enabled ? EffectsModel::Status::Enabled : EffectsModel::Status::Disabled; +} + +EffectsModel::EffectsModel(QObject *parent) + : QAbstractItemModel(parent) +{ +} + +QHash EffectsModel::roleNames() const +{ + QHash roleNames; + roleNames[NameRole] = "NameRole"; + roleNames[DescriptionRole] = "DescriptionRole"; + roleNames[AuthorNameRole] = "AuthorNameRole"; + roleNames[AuthorEmailRole] = "AuthorEmailRole"; + roleNames[LicenseRole] = "LicenseRole"; + roleNames[VersionRole] = "VersionRole"; + roleNames[CategoryRole] = "CategoryRole"; + roleNames[ServiceNameRole] = "ServiceNameRole"; + roleNames[IconNameRole] = "IconNameRole"; + roleNames[StatusRole] = "StatusRole"; + roleNames[VideoRole] = "VideoRole"; + roleNames[WebsiteRole] = "WebsiteRole"; + roleNames[SupportedRole] = "SupportedRole"; + roleNames[ExclusiveRole] = "ExclusiveRole"; + roleNames[ConfigurableRole] = "ConfigurableRole"; + roleNames[ScriptedRole] = QByteArrayLiteral("ScriptedRole"); + roleNames[EnabledByDefaultRole] = "EnabledByDefaultRole"; + return roleNames; +} + +QModelIndex EffectsModel::index(int row, int column, const QModelIndex &parent) const +{ + if (parent.isValid() || column > 0 || column < 0 || row < 0 || row >= m_effects.count()) { + return {}; + } + + return createIndex(row, column); +} + +QModelIndex EffectsModel::parent(const QModelIndex &child) const +{ + Q_UNUSED(child) + return {}; +} + +int EffectsModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return 1; +} + +int EffectsModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + return m_effects.count(); +} + +QVariant EffectsModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return {}; + } + + const EffectData effect = m_effects.at(index.row()); + switch (role) { + case Qt::DisplayRole: + case NameRole: + return effect.name; + case DescriptionRole: + return effect.description; + case AuthorNameRole: + return effect.authorName; + case AuthorEmailRole: + return effect.authorEmail; + case LicenseRole: + return effect.license; + case VersionRole: + return effect.version; + case CategoryRole: + return effect.category; + case ServiceNameRole: + return effect.serviceName; + case IconNameRole: + return effect.iconName; + case StatusRole: + return static_cast(effect.status); + case VideoRole: + return effect.video; + case WebsiteRole: + return effect.website; + case SupportedRole: + return effect.supported; + case ExclusiveRole: + return effect.exclusiveGroup; + case InternalRole: + return effect.internal; + case ConfigurableRole: + return effect.configurable; + case ScriptedRole: + return effect.kind == Kind::Scripted; + case EnabledByDefaultRole: + return effect.enabledByDefault; + default: + return {}; + } +} + +bool EffectsModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!index.isValid()) { + return QAbstractItemModel::setData(index, value, role); + } + + if (role == StatusRole) { + // note: whenever the StatusRole is modified (even to the same value) the entry + // gets marked as changed and will get saved to the config file. This means the + // config file could get polluted + EffectData &data = m_effects[index.row()]; + data.status = Status(value.toInt()); + data.changed = data.status != data.originalStatus; + emit dataChanged(index, index); + + if (data.status == Status::Enabled && !data.exclusiveGroup.isEmpty()) { + // need to disable all other exclusive effects in the same category + for (int i = 0; i < m_effects.size(); ++i) { + if (i == index.row()) { + continue; + } + EffectData &otherData = m_effects[i]; + if (otherData.exclusiveGroup == data.exclusiveGroup) { + otherData.status = Status::Disabled; + otherData.changed = otherData.status != otherData.originalStatus; + emit dataChanged(this->index(i, 0), this->index(i, 0)); + } + } + } + + return true; + } + + return QAbstractItemModel::setData(index, value, role); +} + +void EffectsModel::loadBuiltInEffects(const KConfigGroup &kwinConfig, const KPluginInfo::List &configs) +{ + const auto builtins = BuiltInEffects::availableEffects(); + for (auto builtin : builtins) { + const BuiltInEffects::EffectData &data = BuiltInEffects::effectData(builtin); + EffectData effect; + effect.name = data.displayName; + effect.description = data.comment; + effect.authorName = i18n("KWin development team"); + effect.authorEmail = QString(); // not used at all + effect.license = QStringLiteral("GPL"); + effect.version = QStringLiteral(KWIN_VERSION_STRING); + effect.untranslatedCategory = data.category; + effect.category = translatedCategory(data.category); + effect.serviceName = data.name; + effect.iconName = QStringLiteral("preferences-system-windows"); + effect.enabledByDefault = data.enabled; + effect.enabledByDefaultFunction = (data.enabledFunction != nullptr); + const QString enabledKey = QStringLiteral("%1Enabled").arg(effect.serviceName); + if (kwinConfig.hasKey(enabledKey)) { + effect.status = effectStatus(kwinConfig.readEntry(effect.serviceName + "Enabled", effect.enabledByDefault)); + } else if (data.enabledFunction != nullptr) { + effect.status = Status::EnabledUndeterminded; + } else { + effect.status = effectStatus(effect.enabledByDefault); + } + effect.originalStatus = effect.status; + effect.video = data.video; + effect.website = QUrl(); + effect.supported = true; + effect.exclusiveGroup = data.exclusiveCategory; + effect.internal = data.internal; + effect.kind = Kind::BuiltIn; + + effect.configurable = std::any_of(configs.constBegin(), configs.constEnd(), + [data](const KPluginInfo &info) { + return info.property(QStringLiteral("X-KDE-ParentComponents")).toString() == data.name; + } + ); + + if (shouldStore(effect)) { + m_pendingEffects << effect; + } + } +} + +void EffectsModel::loadJavascriptEffects(const KConfigGroup &kwinConfig) +{ + const auto plugins = KPackage::PackageLoader::self()->listPackages( + QStringLiteral("KWin/Effect"), + QStringLiteral("kwin/effects") + ); + for (const KPluginMetaData &metaData : plugins) { + KPluginInfo plugin(metaData); + EffectData effect; + + effect.name = plugin.name(); + effect.description = plugin.comment(); + effect.authorName = plugin.author(); + effect.authorEmail = plugin.email(); + effect.license = plugin.license(); + effect.version = plugin.version(); + effect.untranslatedCategory = plugin.category(); + effect.category = translatedCategory(plugin.category()); + effect.serviceName = plugin.pluginName(); + effect.iconName = plugin.icon(); + effect.status = effectStatus(kwinConfig.readEntry(effect.serviceName + "Enabled", plugin.isPluginEnabledByDefault())); + effect.originalStatus = effect.status; + effect.enabledByDefault = plugin.isPluginEnabledByDefault(); + effect.enabledByDefaultFunction = false; + effect.video = plugin.property(QStringLiteral("X-KWin-Video-Url")).toUrl(); + effect.website = QUrl(plugin.website()); + effect.supported = true; + effect.exclusiveGroup = plugin.property(QStringLiteral("X-KWin-Exclusive-Category")).toString(); + effect.internal = plugin.property(QStringLiteral("X-KWin-Internal")).toBool(); + effect.kind = Kind::Scripted; + + const QString pluginKeyword = plugin.property(QStringLiteral("X-KDE-PluginKeyword")).toString(); + if (!pluginKeyword.isEmpty()) { + // scripted effects have their pluginName() as the keyword + effect.configurable = plugin.property(QStringLiteral("X-KDE-ParentComponents")).toString() == pluginKeyword; + } else { + effect.configurable = false; + } + + if (shouldStore(effect)) { + m_pendingEffects << effect; + } + } +} + +void EffectsModel::loadPluginEffects(const KConfigGroup &kwinConfig, const KPluginInfo::List &configs) +{ + const auto pluginEffects = KPluginLoader::findPlugins( + QStringLiteral("kwin/effects/plugins/"), + [](const KPluginMetaData &data) { + return data.serviceTypes().contains(QStringLiteral("KWin/Effect")); + } + ); + for (const KPluginMetaData &pluginEffect : pluginEffects) { + if (!pluginEffect.isValid()) { + continue; + } + EffectData effect; + effect.name = pluginEffect.name(); + effect.description = pluginEffect.description(); + effect.license = pluginEffect.license(); + effect.version = pluginEffect.version(); + effect.untranslatedCategory = pluginEffect.category(); + effect.category = translatedCategory(pluginEffect.category()); + effect.serviceName = pluginEffect.pluginId(); + effect.iconName = pluginEffect.iconName(); + effect.enabledByDefault = pluginEffect.isEnabledByDefault(); + effect.supported = true; + effect.enabledByDefaultFunction = false; + effect.internal = false; + effect.kind = Kind::Binary; + + for (int i = 0; i < pluginEffect.authors().count(); ++i) { + effect.authorName.append(pluginEffect.authors().at(i).name()); + effect.authorEmail.append(pluginEffect.authors().at(i).emailAddress()); + if (i+1 < pluginEffect.authors().count()) { + effect.authorName.append(", "); + effect.authorEmail.append(", "); + } + } + + if (pluginEffect.rawData().contains("org.kde.kwin.effect")) { + const QJsonObject d(pluginEffect.rawData().value("org.kde.kwin.effect").toObject()); + effect.exclusiveGroup = d.value("exclusiveGroup").toString(); + effect.video = QUrl::fromUserInput(d.value("video").toString()); + effect.enabledByDefaultFunction = d.value("enabledByDefaultMethod").toBool(); + } + + effect.website = QUrl(pluginEffect.website()); + + const QString enabledKey = QStringLiteral("%1Enabled").arg(effect.serviceName); + if (kwinConfig.hasKey(enabledKey)) { + effect.status = effectStatus(kwinConfig.readEntry(effect.serviceName + "Enabled", effect.enabledByDefault)); + } else if (effect.enabledByDefaultFunction) { + effect.status = Status::EnabledUndeterminded; + } else { + effect.status = effectStatus(effect.enabledByDefault); + } + + effect.originalStatus = effect.status; + + effect.configurable = std::any_of(configs.constBegin(), configs.constEnd(), + [pluginEffect](const KPluginInfo &info) { + return info.property(QStringLiteral("X-KDE-ParentComponents")).toString() == pluginEffect.pluginId(); + } + ); + + if (shouldStore(effect)) { + m_pendingEffects << effect; + } + } +} + +void EffectsModel::load(LoadOptions options) +{ + KConfigGroup kwinConfig(KSharedConfig::openConfig("kwinrc"), "Plugins"); + + m_pendingEffects.clear(); + const KPluginInfo::List configs = KPluginTrader::self()->query(QStringLiteral("kwin/effects/configs/")); + loadBuiltInEffects(kwinConfig, configs); + loadJavascriptEffects(kwinConfig); + loadPluginEffects(kwinConfig, configs); + + std::sort(m_pendingEffects.begin(), m_pendingEffects.end(), + [](const EffectData &a, const EffectData &b) { + if (a.category == b.category) { + if (a.exclusiveGroup == b.exclusiveGroup) { + return a.name < b.name; + } + return a.exclusiveGroup < b.exclusiveGroup; + } + return a.category < b.category; + } + ); + + auto commit = [this, options] { + if (options == LoadOptions::KeepDirty) { + for (const EffectData &oldEffect : m_effects) { + if (!oldEffect.changed) { + continue; + } + auto effectIt = std::find_if(m_pendingEffects.begin(), m_pendingEffects.end(), + [oldEffect](const EffectData &data) { + return data.serviceName == oldEffect.serviceName; + } + ); + if (effectIt == m_pendingEffects.end()) { + continue; + } + effectIt->status = oldEffect.status; + effectIt->changed = effectIt->status != effectIt->originalStatus; + } + } + + beginResetModel(); + m_effects = m_pendingEffects; + m_pendingEffects.clear(); + endResetModel(); + + emit loaded(); + }; + + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + + if (interface.isValid()) { + QStringList effectNames; + effectNames.reserve(m_pendingEffects.count()); + for (const EffectData &data : m_pendingEffects) { + effectNames.append(data.serviceName); + } + + const int serial = ++m_lastSerial; + + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(interface.areEffectsSupported(effectNames), this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, + [=](QDBusPendingCallWatcher *self) { + self->deleteLater(); + + if (m_lastSerial != serial) { + return; + } + + const QDBusPendingReply > reply = *self; + if (reply.isError()) { + commit(); + return; + } + + const QList supportedValues = reply.value(); + if (supportedValues.count() != effectNames.count()) { + return; + } + + for (int i = 0; i < effectNames.size(); ++i) { + const bool supported = supportedValues.at(i); + const QString effectName = effectNames.at(i); + + auto it = std::find_if(m_pendingEffects.begin(), m_pendingEffects.end(), + [effectName](const EffectData &data) { + return data.serviceName == effectName; + } + ); + if (it == m_pendingEffects.end()) { + continue; + } + + if ((*it).supported != supported) { + (*it).supported = supported; + } + } + + commit(); + } + ); + } else { + commit(); + } +} + +void EffectsModel::updateEffectStatus(const QModelIndex &rowIndex, Status effectState) +{ + setData(rowIndex, static_cast(effectState), StatusRole); +} + +void EffectsModel::save() +{ + KConfigGroup kwinConfig(KSharedConfig::openConfig("kwinrc"), "Plugins"); + + QVector dirtyEffects; + + for (EffectData &effect : m_effects) { + if (!effect.changed) { + continue; + } + + effect.changed = false; + effect.originalStatus = effect.status; + + const QString key = effect.serviceName + QStringLiteral("Enabled"); + const bool shouldEnable = (effect.status != Status::Disabled); + const bool restoreToDefault = effect.enabledByDefaultFunction + ? effect.status == Status::EnabledUndeterminded + : shouldEnable == effect.enabledByDefault; + if (restoreToDefault) { + kwinConfig.deleteEntry(key); + } else { + kwinConfig.writeEntry(key, shouldEnable); + } + + dirtyEffects.append(effect); + } + + if (dirtyEffects.isEmpty()) { + return; + } + + kwinConfig.sync(); + + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + + if (!interface.isValid()) { + return; + } + + for (const EffectData &effect : dirtyEffects) { + if (effect.status != Status::Disabled) { + interface.loadEffect(effect.serviceName); + } else { + interface.unloadEffect(effect.serviceName); + } + } +} + +void EffectsModel::defaults() +{ + for (int i = 0; i < m_effects.count(); ++i) { + const auto &effect = m_effects.at(i); + if (effect.enabledByDefaultFunction && effect.status != Status::EnabledUndeterminded) { + updateEffectStatus(index(i, 0), Status::EnabledUndeterminded); + } else if (static_cast(effect.status) != effect.enabledByDefault) { + updateEffectStatus(index(i, 0), effect.enabledByDefault ? Status::Enabled : Status::Disabled); + } + } +} + +bool EffectsModel::isDefaults() const +{ + return std::all_of(m_effects.constBegin(), m_effects.constEnd(), [](const EffectData &effect) { + if (effect.enabledByDefaultFunction && effect.status != Status::EnabledUndeterminded) { + return false; + } + if (static_cast(effect.status) != effect.enabledByDefault) { + return false; + } + return true; + }); +} + +bool EffectsModel::needsSave() const +{ + return std::any_of(m_effects.constBegin(), m_effects.constEnd(), + [](const EffectData &data) { + return data.changed; + } + ); +} + +QModelIndex EffectsModel::findByPluginId(const QString &pluginId) const +{ + auto it = std::find_if(m_effects.constBegin(), m_effects.constEnd(), + [pluginId](const EffectData &data) { + return data.serviceName == pluginId; + } + ); + if (it == m_effects.constEnd()) { + return {}; + } + return index(std::distance(m_effects.constBegin(), it), 0); +} + +static KCModule *findBinaryConfig(const QString &pluginId, QObject *parent) +{ + return KPluginTrader::createInstanceFromQuery( + QStringLiteral("kwin/effects/configs/"), + QString(), + QStringLiteral("'%1' in [X-KDE-ParentComponents]").arg(pluginId), + parent + ); +} + +static KCModule *findScriptedConfig(const QString &pluginId, QObject *parent) +{ + const auto offers = KPluginTrader::self()->query( + QStringLiteral("kwin/effects/configs/"), + QString(), + QStringLiteral("[X-KDE-Library] == 'kcm_kwin4_genericscripted'") + ); + + if (offers.isEmpty()) { + return nullptr; + } + + const KPluginInfo &generic = offers.first(); + KPluginLoader loader(generic.libraryPath()); + KPluginFactory *factory = loader.factory(); + if (!factory) { + return nullptr; + } + + return factory->create(pluginId, parent); +} + +void EffectsModel::requestConfigure(const QModelIndex &index, QWindow *transientParent) +{ + if (!index.isValid()) { + return; + } + + auto dialog = new QDialog(); + + KCModule *module = index.data(ScriptedRole).toBool() + ? findScriptedConfig(index.data(ServiceNameRole).toString(), dialog) + : findBinaryConfig(index.data(ServiceNameRole).toString(), dialog); + if (!module) { + delete dialog; + return; + } + + dialog->setWindowTitle(index.data(NameRole).toString()); + dialog->winId(); + dialog->windowHandle()->setTransientParent(transientParent); + + auto buttons = new QDialogButtonBox( + QDialogButtonBox::Ok | + QDialogButtonBox::Cancel | + QDialogButtonBox::RestoreDefaults, + dialog + ); + connect(buttons, &QDialogButtonBox::accepted, dialog, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, dialog, &QDialog::reject); + connect(buttons->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, + module, &KCModule::defaults); + connect(module, &KCModule::defaulted, this, [=](bool defaulted) { + buttons->button(QDialogButtonBox::RestoreDefaults)->setEnabled(!defaulted); + }); + + auto layout = new QVBoxLayout(dialog); + layout->addWidget(module); + layout->addWidget(buttons); + + connect(dialog, &QDialog::accepted, module, &KCModule::save); + + dialog->setModal(true); + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->show(); +} + +bool EffectsModel::shouldStore(const EffectData &data) const +{ + Q_UNUSED(data) + return true; +} + +} diff --git a/kcmkwin/common/effectsmodel.h b/kcmkwin/common/effectsmodel.h new file mode 100644 index 0000000..2a82232 --- /dev/null +++ b/kcmkwin/common/effectsmodel.h @@ -0,0 +1,264 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Antonis Tsiapaliokas + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include +#include + +#include +#include +#include +#include + +namespace KWin +{ + +class KWIN_EXPORT EffectsModel : public QAbstractItemModel +{ + Q_OBJECT + +public: + /** + * This enum type is used to specify data roles. + */ + enum AdditionalRoles { + /** + * The user-friendly name of the effect. + */ + NameRole = Qt::UserRole + 1, + /** + * The description of the effect. + */ + DescriptionRole, + /** + * The name of the effect's author. If there are several authors, they + * will be comma separated. + */ + AuthorNameRole, + /** + * The email of the effect's author. If there are several authors, the + * emails will be comma separated. + */ + AuthorEmailRole, + /** + * The license of the effect. + */ + LicenseRole, + /** + * The version of the effect. + */ + VersionRole, + /** + * The category of the effect. + */ + CategoryRole, + /** + * The service name(plugin name) of the effect. + */ + ServiceNameRole, + /** + * The icon name of the effect. + */ + IconNameRole, + /** + * Whether the effect is enabled or disabled. + */ + StatusRole, + /** + * Link to a video demonstration of the effect. + */ + VideoRole, + /** + * Link to the home page of the effect. + */ + WebsiteRole, + /** + * Whether the effect is supported. + */ + SupportedRole, + /** + * The exclusive group of the effect. + */ + ExclusiveRole, + /** + * Whether the effect is internal. + */ + InternalRole, + /** + * Whether the effect has a KCM. + */ + ConfigurableRole, + /** + * Whether this is a scripted effect. + */ + ScriptedRole, + /** + * Whether the effect is enabled by default. + */ + EnabledByDefaultRole + }; + + /** + * This enum type is used to specify the status of a given effect. + */ + enum class Status { + /** + * The effect is disabled. + */ + Disabled = Qt::Unchecked, + /** + * An enable function is used to determine whether the effect is enabled. + * For example, such function can be useful to disable the blur effect + * when running in a virtual machine. + */ + EnabledUndeterminded = Qt::PartiallyChecked, + /** + * The effect is enabled. + */ + Enabled = Qt::Checked + }; + + explicit EffectsModel(QObject *parent = nullptr); + + // Reimplemented from QAbstractItemModel. + QHash roleNames() const override; + QModelIndex index(int row, int column, const QModelIndex &parent = {}) const override; + QModelIndex parent(const QModelIndex &child) const override; + int rowCount(const QModelIndex &parent = {}) const override; + int columnCount(const QModelIndex &parent = {}) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + + /** + * Changes the status of a given effect. + * + * @param rowIndex An effect represented by the given index. + * @param effectState The new state. + * @note In order to actually apply the change, you have to call save(). + */ + void updateEffectStatus(const QModelIndex &rowIndex, Status effectState); + + /** + * This enum type is used to specify load options. + */ + enum class LoadOptions { + None, + /** + * Do not discard unsaved changes when reloading the model. + */ + KeepDirty + }; + + /** + * Loads effects. + * + * You have to call this method in order to populate the model. + */ + void load(LoadOptions options = LoadOptions::None); + + /** + * Saves status of each modified effect. + */ + void save(); + + /** + * Resets the status of each effect to the default state. + * + * @note In order to actually apply the change, you have to call save(). + */ + void defaults(); + + /** + * Whether the status of each effect is its default state. + */ + bool isDefaults() const; + + /** + * Whether the model has unsaved changes. + */ + bool needsSave() const; + + /** + * Finds an effect with the given plugin id. + */ + QModelIndex findByPluginId(const QString &pluginId) const; + + /** + * Shows a configuration dialog for a given effect. + * + * @param index An effect represented by the given index. + * @param transientParent The transient parent of the configuration dialog. + */ + void requestConfigure(const QModelIndex &index, QWindow *transientParent); + +Q_SIGNALS: + /** + * This signal is emitted when the model is loaded or reloaded. + * + * @see load + */ + void loaded(); + +protected: + enum class Kind { + BuiltIn, + Binary, + Scripted + }; + + struct EffectData { + QString name; + QString description; + QString authorName; + QString authorEmail; + QString license; + QString version; + QString untranslatedCategory; + QString category; + QString serviceName; + QString iconName; + Status status; + Status originalStatus; + bool enabledByDefault; + bool enabledByDefaultFunction; + QUrl video; + QUrl website; + bool supported; + QString exclusiveGroup; + bool internal; + bool configurable; + Kind kind; + bool changed = false; + }; + + /** + * Returns whether the given effect should be stored in the model. + * + * @param data The effect. + * @returns @c true if the effect should be stored, otherwise @c false. + */ + virtual bool shouldStore(const EffectData &data) const; + +private: + void loadBuiltInEffects(const KConfigGroup &kwinConfig, const KPluginInfo::List &configs); + void loadJavascriptEffects(const KConfigGroup &kwinConfig); + void loadPluginEffects(const KConfigGroup &kwinConfig, const KPluginInfo::List &configs); + + QVector m_effects; + QVector m_pendingEffects; + int m_lastSerial = -1; + + Q_DISABLE_COPY(EffectsModel) +}; + +} diff --git a/kcmkwin/kwincompositing/CMakeLists.txt b/kcmkwin/kwincompositing/CMakeLists.txt new file mode 100644 index 0000000..a701826 --- /dev/null +++ b/kcmkwin/kwincompositing/CMakeLists.txt @@ -0,0 +1,36 @@ +######################################################################### +# KI18N Translation Domain for this library +add_definitions(-DTRANSLATION_DOMAIN=\"kcmkwincompositing\") + +add_definitions(-DQT_NO_URL_CAST_FROM_STRING) + +remove_definitions(-DQT_NO_CAST_FROM_ASCII -DQT_STRICT_ITERATORS -DQT_NO_CAST_FROM_BYTEARRAY) + +################# configure checks and create the configured files ################# + +set(kwincompositing_SRC + main.cpp +) + +kconfig_add_kcfg_files(kwincompositing_SRC kwincompositing_setting.kcfgc GENERATE_MOC) + +qt5_add_dbus_interface(kwincompositing_SRC + ${KWin_SOURCE_DIR}/org.kde.kwin.Compositing.xml kwin_compositing_interface +) + +ki18n_wrap_ui(kwincompositing_SRC compositing.ui) + +add_library(kwincompositing MODULE ${kwincompositing_SRC}) + +target_link_libraries(kwincompositing + Qt5::DBus + Qt5::Widgets + + KF5::ConfigCore + KF5::CoreAddons + KF5::I18n + KF5::KCMUtils +) + +install(TARGETS kwincompositing DESTINATION ${PLUGIN_INSTALL_DIR}) +install(FILES kwincompositing.desktop DESTINATION ${SERVICES_INSTALL_DIR}) diff --git a/kcmkwin/kwincompositing/Messages.sh b/kcmkwin/kwincompositing/Messages.sh new file mode 100644 index 0000000..a034301 --- /dev/null +++ b/kcmkwin/kwincompositing/Messages.sh @@ -0,0 +1,3 @@ +#! /usr/bin/env bash +$EXTRACTRC `find . -name \*.ui` >> rc.cpp || exit 11 +$XGETTEXT *.cpp -o $podir/kcmkwincompositing.pot diff --git a/kcmkwin/kwincompositing/compositing.ui b/kcmkwin/kwincompositing/compositing.ui new file mode 100644 index 0000000..b73e46d --- /dev/null +++ b/kcmkwin/kwincompositing/compositing.ui @@ -0,0 +1,307 @@ + + + CompositingForm + + + + 0 + 0 + 526 + 395 + + + + + QFormLayout::AllNonFixedFieldsGrow + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + false + + + OpenGL compositing (the default) has crashed KWin in the past. +This was most likely due to a driver bug. +If you think that you have meanwhile upgraded to a stable driver, +you can reset this protection but be aware that this might result in an immediate crash! +Alternatively, you might want to use the XRender backend instead. + + + true + + + + + + + false + + + Scale method "Accurate" is not supported by all hardware and can cause performance regressions and rendering artifacts. + + + true + + + + + + + false + + + true + + + + + + + false + + + Keeping the window thumbnail always interferes with the minimized state of windows. This can result in windows not suspending their work when minimized. + + + true + + + + + + + + + Enable compositor on startup + + + + + + + Animation speed: + + + + + + + + 0 + 0 + + + + + + + 0 + + + 0 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + 1 + + + + + + + + + Very slow + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Instant + + + + + + + + + + + + Scale method: + + + + + + + + + + Crisp + + + + + Smooth (slower) + + + + + + + + + Crisp + + + + + Smooth + + + + + Accurate + + + + + + + + + + Qt::Horizontal + + + + + + + Rendering backend: + + + + + + + + + + Qt::Horizontal + + + + + + + Tearing prevention ("vsync"): + + + + + + + + Never + + + + + Automatic + + + + + Only when cheap + + + + + Full screen repaints + + + + + Re-use screen content + + + + + + + + Keep window thumbnails: + + + + + + + + Never + + + + + Only for Shown Windows + + + + + Always + + + + + + + + Applications can set a hint to block compositing when the window is open. + This brings performance improvements for e.g. games. + The setting can be overruled by window-specific rules. + + + Allow applications to block compositing + + + + + + + + KMessageWidget + QFrame +
kmessagewidget.h
+ 1 +
+
+ + +
diff --git a/kcmkwin/kwincompositing/kwincompositing.desktop b/kcmkwin/kwincompositing/kwincompositing.desktop new file mode 100644 index 0000000..f1f3b45 --- /dev/null +++ b/kcmkwin/kwincompositing/kwincompositing.desktop @@ -0,0 +1,138 @@ +[Desktop Entry] +Exec=kcmshell5 kwincompositing +Icon=preferences-desktop +Type=Service +X-KDE-ServiceTypes=KCModule +X-DocPath=https://userbase.kde.org/Desktop_Effects_Performance#Advanced_Desktop_Effects_Settings + +X-KDE-Library=kwincompositing +X-KDE-PluginKeyword=compositing +X-KDE-ParentApp=kcontrol + +X-KDE-System-Settings-Parent-Category=display +X-KDE-Weight=60 + +Name=Compositor +Name[ast]=Compositor +Name[az]=Effektlə təminatı +Name[bs]=Compositor +Name[ca]=Compositor +Name[ca@valencia]=Compositor +Name[cs]=Kompozitor +Name[da]=Compositor +Name[de]=Compositor +Name[el]=Συνθέτης +Name[en_GB]=Compositor +Name[es]=Compositor +Name[et]=Komposiitor +Name[eu]=Konposatzailea +Name[fi]=Koostin +Name[fr]=Compositeur +Name[gl]=Compositor +Name[hu]=Kompozitor +Name[ia]=Compositor +Name[id]=Kompositor +Name[it]=Compositore +Name[ja]=コンポジタ +Name[ko]=컴포지터 +Name[lt]=Kompozitorius +Name[nb]=Sammensetter +Name[nds]=Tosamensetten +Name[nl]=Compositor +Name[nn]=Samansetjar +Name[pa]=ਕੰਪੋਜੀਟਰ +Name[pl]=Kompozytor +Name[pt]=Compositor +Name[pt_BR]=Compositor +Name[ro]=Compozitor +Name[ru]=Обеспечение эффектов +Name[sk]=Kompozítor +Name[sl]=Upravljalnik skladnje +Name[sr]=Слагач +Name[sr@ijekavian]=Слагач +Name[sr@ijekavianlatin]=Slagač +Name[sr@latin]=Slagač +Name[sv]=Sammansättning +Name[tr]=Dizgici +Name[uk]=Засіб композиції +Name[vi]=Compositor +Name[x-test]=xxCompositorxx +Name[zh_CN]=混成器 +Name[zh_TW]=組合器 +Comment=Compositor Settings for Desktop Effects +Comment[az]=İş Masası üçün effekt tətbiq edilməsinin ayarlanması +Comment[bs]=Postavke Compositor-a za Desktop Efekte +Comment[ca]=Arranjament del compositor per als efectes de l'escriptori +Comment[ca@valencia]=Arranjament del compositor pels efectes d'escriptori +Comment[cs]=Nastavení kompozitoru pro efekty pracovní plochy +Comment[da]=Compositor-indstillinger til skrivebordseffekter +Comment[de]=Compositor-Einstellungen für Arbeitsflächen-Effekte +Comment[el]=Ρυθμίσεις συνθέτη για τα εφέ επιφάνειας εργασίας +Comment[en_GB]=Compositor Settings for Desktop Effects +Comment[es]=Configurar las preferencias del compositor para los efectos del escritorio +Comment[et]=Komposiitori seadistused töölauaefektide tarbeks +Comment[eu]=Konposatzailearen ezarpenak mahaigaineko efektuetarako +Comment[fi]=Koostimen asetukset työpöytätehosteita varten +Comment[fr]=Paramétrage du compositeur pour les effets de bureau +Comment[gl]=Configuración do compositor para os efectos de escritorio +Comment[hu]=A kompozitor beállításai az asztali effektusokhoz +Comment[ia]=Preferentias de compositor pro le effectos de scriptorio +Comment[id]=Pengaturan Kompositor untuk Efek Window +Comment[it]=Impostazioni del compositore per gli effetti del desktop +Comment[ja]=デスクトップ効果のためのコンポジタの設定 +Comment[ko]=데스크톱 효과에 사용되는 컴포지터 설정 +Comment[lt]=Darbalaukio efektų kompozitoriaus nuostatos +Comment[nb]=Sammensetter-innstillinger for skrivebordseffekter +Comment[nds]=Tosamensettoptschonen för de Schriefdischeffekten instellen +Comment[nl]=Instellingen van compositor configureren voor bureaubladeffecten +Comment[nn]=Samansetjarinnstillingar for skrivebordseffektar +Comment[pa]=ਡੈਸਕਟਾਪ ਪਰਭਾਵ ਲਈ ਕੰਪੋਜੀਟਰ ਸੈਟਿੰਗਾਂ +Comment[pl]=Ustawienia kompozytora dla efektów pulpitu +Comment[pt]=Configuração do Compositor para os Efeitos do Ecrã +Comment[pt_BR]=Definições do Compositor para os efeitos da área de trabalho +Comment[ro]=Configurări compozitor pentru efecte de birou +Comment[ru]=Настройка движка эффектов рабочего стола +Comment[sk]=Nastavenia kompozítora pre efekty plochy +Comment[sl]=Nastavitve upravljalnika skladnje za učinke namizja +Comment[sr]=Поставке слагача за ефекте површи +Comment[sr@ijekavian]=Поставке слагача за ефекте површи +Comment[sr@ijekavianlatin]=Postavke slagača za efekte površi +Comment[sr@latin]=Postavke slagača za efekte površi +Comment[sv]=Sammansättningsinställningar för skrivbordseffekter +Comment[tr]=Masaüstü Efektleri için Dizgici Ayarları +Comment[uk]=Параметри засобу композиції для ефектів стільниці +Comment[vi]=Thiết lập Compositor cho hiệu ứng màn hình +Comment[x-test]=xxCompositor Settings for Desktop Effectsxx +Comment[zh_CN]=桌面特效混成器设置 +Comment[zh_TW]=桌面效果使用的組合器設定 + +X-KDE-Keywords=kwin,window,manager,compositing,effect,3D effects,2D effects,OpenGL,XRender,video settings,graphical effects,desktop effects +X-KDE-Keywords[az]=pəncərə meneceri,effekt,3D effekt,2DEffekt,video ayarları,qrafik effektlər,iş masası effektləri,OpenGL,XRender,kwin,compositing +X-KDE-Keywords[ca]=kwin,finestra,gestor,composició,efecte,efectes 3D,efectes 2D,OpenGL,XRender,arranjament de vídeo,efectes gràfics,efectes de l'escriptori +X-KDE-Keywords[da]=kwin,vindue,håndtering,window,manager,compositing,effekt,3D effekter,2D effekter,OpenGL,XRender,video-indstillinger,grafiske effekter,skrivebordseffekter,desktop effects +X-KDE-Keywords[de]=kwin,Fenstermanager,Fensterverwaltung,Effekt,Fenster,Compositing,3D-Effekte,2D-Effekte,OpenGL,XRender,Video-Einstellungen,Grafikeffekte,Arbeitsflächeneffekte +X-KDE-Keywords[en_GB]=kwin,window,manager,compositing,effect,3D effects,2D effects,OpenGL,XRender,video settings,graphical effects,desktop effects +X-KDE-Keywords[es]=kwin,ventana,gestor,composición,efecto,efectos 3D,efectos 2D,OpenGL,XRender,preferencias de vídeo,efectos gráficos,efectos del escritorio +X-KDE-Keywords[et]=kwin,aken,haldur,komposiit,komposiitor,efekt,3D efektid,ruumilised efektid,2D efektid,OpenGL,XRender,videoseadistused,graafilised efektid,töölauaefektid +X-KDE-Keywords[eu]=kwin,leihoa,kudeatzailea,konposatzailea,efektua,3Dtako efektuak,2Dtako efektuak,OpenGL,XRender,bideo ezarpenak,efektu grafikoak,mahaigaineko efektuak +X-KDE-Keywords[fi]=kwin,window,manager,compositing,effect,3D effects,2D effects,OpenGL,XRender,video settings,graphical effects,desktop effects,ikkunointiohjelma,ikkunaohjelma,ikkunanhallinta,tehoste,3D-tehosteet,2D-tehosteet,videoasetukset,graafiset tehosteet,työpöytätehosteet +X-KDE-Keywords[fr]=Gestionnaire de fenêtres KWin, kwin, window, manager, compositeur, effet, effets 3D, effets 2D, OpenGL, XRender, paramétrage video, effets graphiques, effects du bureau +X-KDE-Keywords[ia]=kwin,window,manager,compositing,effect,3D effects,2D effects,OpenGL,XRender,video settings,graphical effects,desktop effects +X-KDE-Keywords[id]=kwin,window,pengelola,kompositing,efek,efek 3D,efek 2D,OpenGL,XRender,pengaturan video,efek grafis,efek desktop +X-KDE-Keywords[it]=kwin,finestra,gestore,composizione,effetto,effetti 3D,effetti 2D,OpenGL,XRender,impostazioni video,effetti grafici,effetti del desktop +X-KDE-Keywords[ko]=kwin,window,manager,compositing,effect,3D effects,2D effects,OpenGL,XRender,video settings,graphical effects,desktop effects,3D 효과,2D 효과,비디오 설정,그래픽 설정,그래픽 효과,데스크톱 효과 +X-KDE-Keywords[lt]=kwin,langas,langai,tvarkytuvė,tvarkytuve,komponavimas,kompozicionavimas,efektas,efektai,3D efektai,2D efektai,trimačiai efektai,trimaciai efektai,dvimačiai efektai,dvimaciai efektai,OpenGL,XRender,vaizdo nustatymai,vaizdo nuostatos,video nustatymai,video nuostatos,grafiniai efektai,grafikos efektai,darbalaukio efektai,darbastalio efektai +X-KDE-Keywords[nl]=kwin,window,manager,beheerder,compositing,effecten,3D-effecten,2D-effecten,OpenGL,XRender,video-instellingen,grafische effecten,bureaubladeffecten +X-KDE-Keywords[nn]=kwin,vindauge,vindaugshandsamar,samansetjing,effekt,3D-effektar,2D-effektar,OpenGL,XRender,videoinnstillingar,grafiske effektar,skrivebordseffektar +X-KDE-Keywords[pl]=kwin,okno,menadżer,menedżer,zarządca,kompozytor,efekt,efekt 3D,efekt 2D,OpenGL,XRender,ustawienia wideo,efekty graficzne,efekty pulpitu +X-KDE-Keywords[pt]=kwin,janela,gestor,composição,efeito,efeitos 3D,efeitos 2D,OpenGL,XRender,configuração do vídeo,efeitos gráficos,efeitos do ecrã +X-KDE-Keywords[pt_BR]=kwin,janela,gerenciador,composição,efeito,efeitos 3D,efeitos 2D,OpenGL,XRender,configurações de vídeo,efeitos gráficos,efeitos da área de trabalho +X-KDE-Keywords[ro]=kwin,fereastră,gestionar,compoziționare,efect,efecte 3D,OpenGL,XRender,configurări video,efecte grafice,efecte de birou +X-KDE-Keywords[ru]=kwin,window,manager,compositing,effect,3D effects,2D effects,OpenGL,XRender,video settings,graphical effects,desktop effects,композитинг,композитный диспетчер окон,эффекты рабочего стола,графические эффекты,рендеринг,параметры видео,настройка видео +X-KDE-Keywords[sk]=kwin,okno,správca,kompozícia,efekt,3D efekty,2D efekty,OpenGL,XRender,nastavenia videa,grafické efekty,efekty plochy +X-KDE-Keywords[sl]=kwin,okna,upravljalnik,skladnja,učinek,3D učinki,2D učinki,OpenGL,XRender,nastavitve videa,video,grafični učinki,hitrost animacije,učinki namizja +X-KDE-Keywords[sv]=kwin,fönster,hantering,sammansättning,effekt,3D effekter,2D effekter,OpenGL,XRender,videoinställningar,grafiska effekter,skrivbordseffekter +X-KDE-Keywords[uk]=kwin,window,manager,compositing,effect,3D effects,2D effects,OpenGL,XRender,video settings,graphical effects,desktop effects,вікно,керування,композитне,композиція,ефект,просторовий,ефекти,плоскі,параметри відео,графічні ефекти,ефекти стільниці +X-KDE-Keywords[x-test]=xxkwinxx,xxwindowxx,xxmanagerxx,xxcompositingxx,xxeffectxx,xx3D effectsxx,xx2D effectsxx,xxOpenGLxx,xxXRenderxx,xxvideo settingsxx,xxgraphical effectsxx,xxdesktop effectsxx +X-KDE-Keywords[zh_CN]=kwin,窗口,管理器,混成,特效,3D 特效,2D 特效,OpenGL,XRender,视频设置,图形特效,桌面特效 +X-KDE-Keywords[zh_TW]=kwin,window,manager,compositing,effect,3D effects,2D effects,OpenGL,XRender,video settings,graphical effects,desktop effects,桌面特效,圖形特效,視訊設定,特效,3D,2D,合成,視窗,管理器 diff --git a/kcmkwin/kwincompositing/kwincompositing_setting.kcfg b/kcmkwin/kwincompositing/kwincompositing_setting.kcfg new file mode 100644 index 0000000..162c82b --- /dev/null +++ b/kcmkwin/kwincompositing/kwincompositing_setting.kcfg @@ -0,0 +1,74 @@ + + + + + + + 1.0 + + + + + + + Shown + + + + + + + + + 2 + + + + Crisp + + + + + + + + true + + + + true + + + + AutoSwapStrategy + + + + + + + + + + + OpenGL + + + + + + + + false + + + + true + + + + + diff --git a/kcmkwin/kwincompositing/kwincompositing_setting.kcfgc b/kcmkwin/kwincompositing/kwincompositing_setting.kcfgc new file mode 100644 index 0000000..4b6ac59 --- /dev/null +++ b/kcmkwin/kwincompositing/kwincompositing_setting.kcfgc @@ -0,0 +1,5 @@ +File=kwincompositing_setting.kcfg +ClassName=KWinCompositingSetting +Mutators=true +DefaultValueGetters=true +ParentInConstructor=true diff --git a/kcmkwin/kwincompositing/main.cpp b/kcmkwin/kwincompositing/main.cpp new file mode 100644 index 0000000..8bbc0be --- /dev/null +++ b/kcmkwin/kwincompositing/main.cpp @@ -0,0 +1,294 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Antonis Tsiapaliokas + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + + +#include "ui_compositing.h" +#include + +#include +#include +#include + +#include +#include + +#include +#include + +#include "kwincompositing_setting.h" + +static bool isRunningPlasma() +{ + return qgetenv("XDG_CURRENT_DESKTOP") == "KDE"; +} + +class KWinCompositingKCM : public KCModule +{ + Q_OBJECT +public: + enum CompositingTypeIndex { + OPENGL31_INDEX = 0, + OPENGL20_INDEX, + XRENDER_INDEX + }; + + explicit KWinCompositingKCM(QWidget *parent = nullptr, const QVariantList &args = QVariantList()); + +public Q_SLOTS: + void load() override; + void save() override; + void defaults() override; + +private Q_SLOTS: + void onBackendChanged(); + void reenableGl(); + +private: + void init(); + void updateUnmanagedItemStatus(); + bool compositingRequired() const; + + Ui_CompositingForm m_form; + + OrgKdeKwinCompositingInterface *m_compositingInterface; + KWinCompositingSetting *m_settings; +}; + +static const QVector s_animationMultipliers = {8, 4, 2, 1, 0.5, 0.25, 0.125, 0}; + +bool KWinCompositingKCM::compositingRequired() const +{ + return m_compositingInterface->platformRequiresCompositing(); +} + +KWinCompositingKCM::KWinCompositingKCM(QWidget *parent, const QVariantList &args) + : KCModule(parent, args) + , m_compositingInterface(new OrgKdeKwinCompositingInterface(QStringLiteral("org.kde.KWin"), QStringLiteral("/Compositor"), QDBusConnection::sessionBus(), this)) + , m_settings(new KWinCompositingSetting(this)) +{ + m_form.setupUi(this); + addConfig(m_settings, this); + + m_form.glCrashedWarning->setIcon(QIcon::fromTheme(QStringLiteral("dialog-warning"))); + QAction *reenableGlAction = new QAction(i18n("Re-enable OpenGL detection"), this); + connect(reenableGlAction, &QAction::triggered, this, &KWinCompositingKCM::reenableGl); + connect(reenableGlAction, &QAction::triggered, m_form.glCrashedWarning, &KMessageWidget::animatedHide); + m_form.glCrashedWarning->addAction(reenableGlAction); + m_form.scaleWarning->setIcon(QIcon::fromTheme(QStringLiteral("dialog-warning"))); + m_form.tearingWarning->setIcon(QIcon::fromTheme(QStringLiteral("dialog-warning"))); + m_form.windowThumbnailWarning->setIcon(QIcon::fromTheme(QStringLiteral("dialog-warning"))); + + m_form.kcfg_Enabled->setVisible(!compositingRequired()); + m_form.kcfg_WindowsBlockCompositing->setVisible(!compositingRequired()); + + init(); +} + +void KWinCompositingKCM::reenableGl() +{ + m_settings->setOpenGLIsUnsafe(false); + m_settings->save(); +} + +void KWinCompositingKCM::init() +{ + auto currentIndexChangedSignal = static_cast(&QComboBox::currentIndexChanged); + + // animation speed + m_form.animationDurationFactor->setMaximum(s_animationMultipliers.size() - 1); + connect(m_form.animationDurationFactor, &QSlider::valueChanged, this, [this]() { + m_settings->setAnimationDurationFactor(s_animationMultipliers[m_form.animationDurationFactor->value()]); + updateUnmanagedItemStatus(); + }); + + if (isRunningPlasma()) { + m_form.animationSpeedLabel->hide(); + m_form.animationSpeedControls->hide(); + } + + // gl scale filter + connect(m_form.kcfg_glTextureFilter, currentIndexChangedSignal, this, + [this](int index) { + if (index == 2) { + m_form.scaleWarning->animatedShow(); + } else { + m_form.scaleWarning->animatedHide(); + } + } + ); + + // tearing prevention + connect(m_form.kcfg_glPreferBufferSwap, currentIndexChangedSignal, this, + [this](int index) { + if (index == 2) { + // only when cheap - tearing + m_form.tearingWarning->setText(i18n("\"Only when cheap\" only prevents tearing for full screen changes like a video.")); + m_form.tearingWarning->animatedShow(); + } else if (index == 3) { + // full screen repaints + m_form.tearingWarning->setText(i18n("\"Full screen repaints\" can cause performance problems.")); + m_form.tearingWarning->animatedShow(); + } else if (index == 4) { + // re-use screen content + m_form.tearingWarning->setText(i18n("\"Re-use screen content\" causes severe performance problems on MESA drivers.")); + m_form.tearingWarning->animatedShow(); + } else { + m_form.tearingWarning->animatedHide(); + } + } + ); + + // windowThumbnail + connect(m_form.kcfg_HiddenPreviews, currentIndexChangedSignal, this, + [this](int index) { + if (index == 2) { + m_form.windowThumbnailWarning->animatedShow(); + } else { + m_form.windowThumbnailWarning->animatedHide(); + } + } + ); + + // compositing type + m_form.backend->addItem(i18n("OpenGL 3.1"), CompositingTypeIndex::OPENGL31_INDEX); + m_form.backend->addItem(i18n("OpenGL 2.0"), CompositingTypeIndex::OPENGL20_INDEX); + m_form.backend->addItem(i18n("XRender"), CompositingTypeIndex::XRENDER_INDEX); + + connect(m_form.backend, currentIndexChangedSignal, this, &KWinCompositingKCM::onBackendChanged); + + if (m_settings->openGLIsUnsafe()) { + m_form.glCrashedWarning->animatedShow(); + } +} + +void KWinCompositingKCM::onBackendChanged() +{ + const int currentType = m_form.backend->currentData().toInt(); + + m_form.kcfg_glTextureFilter->setVisible(currentType != CompositingTypeIndex::XRENDER_INDEX); + m_form.kcfg_XRenderSmoothScale->setVisible(currentType == CompositingTypeIndex::XRENDER_INDEX); + + updateUnmanagedItemStatus(); +} + +void KWinCompositingKCM::updateUnmanagedItemStatus() +{ + int backend = KWinCompositingSetting::EnumBackend::OpenGL; + bool glCore = true; + const int currentType = m_form.backend->currentData().toInt(); + switch (currentType) { + case CompositingTypeIndex::OPENGL31_INDEX: + // default already set + break; + case CompositingTypeIndex::OPENGL20_INDEX: + glCore = false; + break; + case CompositingTypeIndex::XRENDER_INDEX: + backend = KWinCompositingSetting::EnumBackend::XRender; + glCore = false; + break; + } + const auto animationDuration = s_animationMultipliers[m_form.animationDurationFactor->value()]; + + const bool inPlasma = isRunningPlasma(); + + bool changed = glCore != m_settings->glCore(); + changed |= backend != m_settings->backend(); + if (!inPlasma) { + changed |= (animationDuration != m_settings->animationDurationFactor()); + } + unmanagedWidgetChangeState(changed); + + bool defaulted = glCore == m_settings->defaultGlCoreValue(); + defaulted &= backend == m_settings->defaultBackendValue(); + if (!inPlasma) { + defaulted &= animationDuration == m_settings->defaultAnimationDurationFactorValue(); + } + unmanagedWidgetDefaultState(defaulted); +} + +void KWinCompositingKCM::load() +{ + KCModule::load(); + + // unmanaged items + m_settings->findItem("AnimationDurationFactor")->readConfig(m_settings->config()); + const double multiplier = m_settings->animationDurationFactor(); + auto const it = std::lower_bound(s_animationMultipliers.begin(), s_animationMultipliers.end(), multiplier, std::greater()); + const int index = static_cast(std::distance(s_animationMultipliers.begin(), it)); + m_form.animationDurationFactor->setValue(index); + m_form.animationDurationFactor->setDisabled(m_settings->isAnimationDurationFactorImmutable()); + + m_settings->findItem("Backend")->readConfig(m_settings->config()); + m_settings->findItem("glCore")->readConfig(m_settings->config()); + + if (m_settings->backend() == KWinCompositingSetting::EnumBackend::OpenGL) { + if (m_settings->glCore()) { + m_form.backend->setCurrentIndex(CompositingTypeIndex::OPENGL31_INDEX); + } else { + m_form.backend->setCurrentIndex(CompositingTypeIndex::OPENGL20_INDEX); + } + } else { + m_form.backend->setCurrentIndex(CompositingTypeIndex::XRENDER_INDEX); + } + m_form.backend->setDisabled(m_settings->isBackendImmutable()); + + onBackendChanged(); +} + +void KWinCompositingKCM::defaults() +{ + KCModule::defaults(); + + // unmanaged widgets + m_form.backend->setCurrentIndex(CompositingTypeIndex::OPENGL20_INDEX); + // corresponds to 1.0 seconds in s_animationMultipliers + m_form.animationDurationFactor->setValue(3); +} + +void KWinCompositingKCM::save() +{ + int backend = KWinCompositingSetting::EnumBackend::OpenGL; + bool glCore = true; + const int currentType = m_form.backend->currentData().toInt(); + switch (currentType) { + case CompositingTypeIndex::OPENGL31_INDEX: + // default already set + break; + case CompositingTypeIndex::OPENGL20_INDEX: + backend = KWinCompositingSetting::EnumBackend::OpenGL; + glCore = false; + break; + case CompositingTypeIndex::XRENDER_INDEX: + backend = KWinCompositingSetting::EnumBackend::XRender; + glCore = false; + break; + } + m_settings->setBackend(backend); + m_settings->setGlCore(glCore); + + const auto animationDuration = s_animationMultipliers[m_form.animationDurationFactor->value()]; + m_settings->setAnimationDurationFactor(animationDuration); + m_settings->save(); + + KCModule::save(); + + // Send signal to all kwin instances + QDBusMessage message = QDBusMessage::createSignal(QStringLiteral("/Compositor"), + QStringLiteral("org.kde.kwin.Compositing"), + QStringLiteral("reinit")); + QDBusConnection::sessionBus().send(message); +} + +K_PLUGIN_FACTORY(KWinCompositingConfigFactory, + registerPlugin("compositing"); + ) + +#include "main.moc" diff --git a/kcmkwin/kwindecoration/CMakeLists.txt b/kcmkwin/kwindecoration/CMakeLists.txt new file mode 100644 index 0000000..5be0504 --- /dev/null +++ b/kcmkwin/kwindecoration/CMakeLists.txt @@ -0,0 +1,33 @@ +# KI18N Translation Domain for this library +add_definitions(-DTRANSLATION_DOMAIN=\"kcm_kwindecoration\") + +add_subdirectory(declarative-plugin) + +set(kcmkwindecoration_SRCS + declarative-plugin/buttonsmodel.cpp + decorationmodel.cpp + kcm.cpp + utils.cpp +) + +kconfig_add_kcfg_files(kcmkwindecoration_SRCS kwindecorationsettings.kcfgc GENERATE_MOC) + +add_library(kcm_kwindecoration MODULE ${kcmkwindecoration_SRCS}) + +target_link_libraries(kcm_kwindecoration + KDecoration2::KDecoration + KF5::I18n + KF5::NewStuff + KF5::QuickAddons + Qt5::Quick +) + +kcoreaddons_desktop_to_json(kcm_kwindecoration "kwindecoration.desktop" SERVICE_TYPES kcmodule.desktop) + +# This desktop file is installed only for retrocompatibility with sycoca +install(FILES kwindecorationsettings.kcfg DESTINATION ${KDE_INSTALL_KCFGDIR}) +install(FILES kwindecoration.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}) +install(FILES window-decorations.knsrc DESTINATION ${KDE_INSTALL_KNSRCDIR}) +install(TARGETS kcm_kwindecoration DESTINATION ${KDE_INSTALL_PLUGINDIR}/kcms) + +kpackage_install_package(package kcm_kwindecoration kcms) diff --git a/kcmkwin/kwindecoration/Messages.sh b/kcmkwin/kwindecoration/Messages.sh new file mode 100644 index 0000000..97ccd22 --- /dev/null +++ b/kcmkwin/kwindecoration/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name "*.cpp" -o -name "*.qml"` -o $podir/kcm_kwindecoration.pot diff --git a/kcmkwin/kwindecoration/declarative-plugin/CMakeLists.txt b/kcmkwin/kwindecoration/declarative-plugin/CMakeLists.txt new file mode 100644 index 0000000..1b0abc5 --- /dev/null +++ b/kcmkwin/kwindecoration/declarative-plugin/CMakeLists.txt @@ -0,0 +1,26 @@ +set(plugin_SRCS + previewbutton.cpp + previewbridge.cpp + previewclient.cpp + previewitem.cpp + previewsettings.cpp + plugin.cpp + buttonsmodel.cpp + ../../../decorations/decorationpalette.cpp + ../../../decorations/decorations_logging.cpp +) + +add_library(kdecorationprivatedeclarative SHARED ${plugin_SRCS}) +target_link_libraries(kdecorationprivatedeclarative + KDecoration2::KDecoration + KDecoration2::KDecoration2Private + Qt5::DBus + Qt5::Quick + KF5::CoreAddons + KF5::ConfigWidgets + KF5::I18n + KF5::Service +) + +install(TARGETS kdecorationprivatedeclarative DESTINATION ${QML_INSTALL_DIR}/org/kde/kwin/private/kdecoration ) +install(FILES qmldir DESTINATION ${QML_INSTALL_DIR}/org/kde/kwin/private/kdecoration ) diff --git a/kcmkwin/kwindecoration/declarative-plugin/buttonsmodel.cpp b/kcmkwin/kwindecoration/declarative-plugin/buttonsmodel.cpp new file mode 100644 index 0000000..666e977 --- /dev/null +++ b/kcmkwin/kwindecoration/declarative-plugin/buttonsmodel.cpp @@ -0,0 +1,187 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#include "buttonsmodel.h" + +#include + +#include + +namespace KDecoration2 +{ + +namespace Preview +{ + +ButtonsModel::ButtonsModel(const QVector< DecorationButtonType > &buttons, QObject *parent) + : QAbstractListModel(parent) + , m_buttons(buttons) +{ +} + +ButtonsModel::ButtonsModel(QObject* parent) + : ButtonsModel(QVector({ + DecorationButtonType::Menu, + DecorationButtonType::ApplicationMenu, + DecorationButtonType::OnAllDesktops, + DecorationButtonType::Minimize, + DecorationButtonType::Maximize, + DecorationButtonType::Close, + DecorationButtonType::ContextHelp, + DecorationButtonType::Shade, + DecorationButtonType::KeepBelow, + DecorationButtonType::KeepAbove + }), parent) +{ +} + +ButtonsModel::~ButtonsModel() = default; + +int ButtonsModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + return m_buttons.count(); +} + +static QString buttonToName(DecorationButtonType type) +{ + switch (type) { + case DecorationButtonType::Menu: + return i18n("Menu"); + case DecorationButtonType::ApplicationMenu: + return i18n("Application menu"); + case DecorationButtonType::OnAllDesktops: + return i18n("On all desktops"); + case DecorationButtonType::Minimize: + return i18n("Minimize"); + case DecorationButtonType::Maximize: + return i18n("Maximize"); + case DecorationButtonType::Close: + return i18n("Close"); + case DecorationButtonType::ContextHelp: + return i18n("Context help"); + case DecorationButtonType::Shade: + return i18n("Shade"); + case DecorationButtonType::KeepBelow: + return i18n("Keep below"); + case DecorationButtonType::KeepAbove: + return i18n("Keep above"); + default: + return QString(); + } +} + +QVariant ButtonsModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || + index.row() < 0 || + index.row() >= m_buttons.count() || + index.column() != 0) { + return QVariant(); + } + switch (role) { + case Qt::DisplayRole: + return buttonToName(m_buttons.at(index.row())); + case Qt::UserRole: + return QVariant::fromValue(int(m_buttons.at(index.row()))); + } + return QVariant(); +} + +QHash< int, QByteArray > ButtonsModel::roleNames() const +{ + QHash roles; + roles.insert(Qt::DisplayRole, QByteArrayLiteral("display")); + roles.insert(Qt::UserRole, QByteArrayLiteral("button")); + return roles; +} + +void ButtonsModel::remove(int row) +{ + if (row < 0 || row >= m_buttons.count()) { + return; + } + beginRemoveRows(QModelIndex(), row, row); + m_buttons.removeAt(row); + endRemoveRows(); +} + +void ButtonsModel::down(int index) +{ + if (m_buttons.count() < 2 || index == m_buttons.count() -1) { + return; + } + beginMoveRows(QModelIndex(), index, index, QModelIndex(), index + 2); + m_buttons.insert(index +1, m_buttons.takeAt(index)); + endMoveRows(); +} + +void ButtonsModel::up(int index) +{ + if (m_buttons.count() < 2 || index == 0) { + return; + } + beginMoveRows(QModelIndex(), index, index, QModelIndex(), index -1); + m_buttons.insert(index -1, m_buttons.takeAt(index)); + endMoveRows(); +} + +void ButtonsModel::add(DecorationButtonType type) +{ + beginInsertRows(QModelIndex(), m_buttons.count(), m_buttons.count()); + m_buttons.append(type); + endInsertRows(); +} + +void ButtonsModel::add(int index, int type) +{ + beginInsertRows(QModelIndex(), index, index); + m_buttons.insert(index, KDecoration2::DecorationButtonType(type)); + endInsertRows(); +} + +void ButtonsModel::move(int sourceIndex, int targetIndex) +{ + if (sourceIndex == qMax(0, targetIndex)) { + return; + } + + /* When moving an item down, the destination index needs to be incremented + by one, as explained in the documentation: + https://doc.qt.io/qt-5/qabstractitemmodel.html#beginMoveRows */ + if (targetIndex > sourceIndex) { + // Row will be moved down + beginMoveRows(QModelIndex(), sourceIndex, sourceIndex, QModelIndex(), targetIndex + 1); + } else { + beginMoveRows(QModelIndex(), sourceIndex, sourceIndex, QModelIndex(), qMax(0, targetIndex)); + } + + m_buttons.move(sourceIndex, qMax(0, targetIndex)); + endMoveRows(); +} + +void ButtonsModel::clear() +{ + beginResetModel(); + m_buttons.clear(); + endResetModel(); +} + +void ButtonsModel::replace(const QVector< DecorationButtonType > &buttons) +{ + if (buttons.isEmpty()) { + return; + } + + beginResetModel(); + m_buttons = buttons; + endResetModel(); +} + +} +} + diff --git a/kcmkwin/kwindecoration/declarative-plugin/buttonsmodel.h b/kcmkwin/kwindecoration/declarative-plugin/buttonsmodel.h new file mode 100644 index 0000000..efa5f9d --- /dev/null +++ b/kcmkwin/kwindecoration/declarative-plugin/buttonsmodel.h @@ -0,0 +1,52 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#ifndef KDECOARTIONS_PREVIEW_BUTTONS_MODEL_H +#define KDECOARTIONS_PREVIEW_BUTTONS_MODEL_H + +#include +#include + +namespace KDecoration2 +{ + +namespace Preview +{ +class PreviewBridge; + +class ButtonsModel : public QAbstractListModel +{ + Q_OBJECT +public: + explicit ButtonsModel(const QVector< DecorationButtonType > &buttons, QObject *parent = nullptr); + explicit ButtonsModel(QObject *parent = nullptr); + ~ButtonsModel() override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + QHash< int, QByteArray > roleNames() const override; + + QVector< DecorationButtonType > buttons() const { + return m_buttons; + } + + Q_INVOKABLE void clear(); + Q_INVOKABLE void remove(int index); + Q_INVOKABLE void up(int index); + Q_INVOKABLE void down(int index); + Q_INVOKABLE void move(int sourceIndex, int targetIndex); + + void replace(const QVector< DecorationButtonType > &buttons); + void add(DecorationButtonType type); + Q_INVOKABLE void add(int index, int type); + +private: + QVector< DecorationButtonType > m_buttons; +}; + +} +} + +#endif + diff --git a/kcmkwin/kwindecoration/declarative-plugin/plugin.cpp b/kcmkwin/kwindecoration/declarative-plugin/plugin.cpp new file mode 100644 index 0000000..9fb045c --- /dev/null +++ b/kcmkwin/kwindecoration/declarative-plugin/plugin.cpp @@ -0,0 +1,39 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#include "plugin.h" +#include "buttonsmodel.h" +#include "previewbutton.h" +#include "previewbridge.h" +#include "previewclient.h" +#include "previewitem.h" +#include "previewsettings.h" + +#include +#include + +namespace KDecoration2 +{ +namespace Preview +{ + +void Plugin::registerTypes(const char *uri) +{ + Q_ASSERT(QLatin1String(uri) == QLatin1String("org.kde.kwin.private.kdecoration")); + qmlRegisterType(uri, 1, 0, "Bridge"); + qmlRegisterType(uri, 1, 0, "Settings"); + qmlRegisterType(uri, 1, 0, "Decoration"); + qmlRegisterType(uri, 1, 0, "Button"); + qmlRegisterType(uri, 1, 0, "ButtonsModel"); + qmlRegisterAnonymousType(uri, 1); + qmlRegisterAnonymousType(uri, 1); + qmlRegisterAnonymousType(uri, 1); + qmlRegisterAnonymousType(uri, 1); +} + +} +} + + diff --git a/kcmkwin/kwindecoration/declarative-plugin/plugin.h b/kcmkwin/kwindecoration/declarative-plugin/plugin.h new file mode 100644 index 0000000..d62217a --- /dev/null +++ b/kcmkwin/kwindecoration/declarative-plugin/plugin.h @@ -0,0 +1,27 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#ifndef KDECOARTIONS_PREVIEW_PLUGIN_H +#define KDECOARTIONS_PREVIEW_PLUGIN_H + +#include + +namespace KDecoration2 +{ +namespace Preview +{ + +class Plugin : public QQmlExtensionPlugin +{ + Q_PLUGIN_METADATA(IID "org.kde.kdecoration2") + Q_OBJECT +public: + void registerTypes(const char *uri) override; +}; + +} +} + +#endif diff --git a/kcmkwin/kwindecoration/declarative-plugin/previewbridge.cpp b/kcmkwin/kwindecoration/declarative-plugin/previewbridge.cpp new file mode 100644 index 0000000..07afe34 --- /dev/null +++ b/kcmkwin/kwindecoration/declarative-plugin/previewbridge.cpp @@ -0,0 +1,245 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#include "previewbridge.h" +#include "previewclient.h" +#include "previewitem.h" +#include "previewsettings.h" + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace KDecoration2 +{ +namespace Preview +{ + +static const QString s_pluginName = QStringLiteral("org.kde.kdecoration2"); + +PreviewBridge::PreviewBridge(QObject *parent) + : DecorationBridge(parent) + , m_lastCreatedClient(nullptr) + , m_lastCreatedSettings(nullptr) + , m_valid(false) +{ + connect(this, &PreviewBridge::pluginChanged, this, &PreviewBridge::createFactory); +} + +PreviewBridge::~PreviewBridge() = default; + +std::unique_ptr PreviewBridge::createClient(DecoratedClient *client, Decoration *decoration) +{ + auto ptr = std::unique_ptr(new PreviewClient(client, decoration)); + m_lastCreatedClient = ptr.get(); + return ptr; +} + +void PreviewBridge::update(Decoration *decoration, const QRect &geometry) +{ + Q_UNUSED(geometry) + auto it = std::find_if(m_previewItems.constBegin(), m_previewItems.constEnd(), [decoration](PreviewItem *item) { + return item->decoration() == decoration; + }); + if (it != m_previewItems.constEnd()) { + (*it)->update(); + } +} + +std::unique_ptr PreviewBridge::settings(DecorationSettings *parent) +{ + auto ptr = std::unique_ptr(new PreviewSettings(parent)); + m_lastCreatedSettings = ptr.get(); + return ptr; +} + +void PreviewBridge::registerPreviewItem(PreviewItem *item) +{ + m_previewItems.append(item); +} + +void PreviewBridge::unregisterPreviewItem(PreviewItem *item) +{ + m_previewItems.removeAll(item); +} + +void PreviewBridge::setPlugin(const QString &plugin) +{ + if (m_plugin == plugin) { + return; + } + m_plugin = plugin; + emit pluginChanged(); +} + +QString PreviewBridge::theme() const +{ + return m_theme; +} + +void PreviewBridge::setTheme(const QString &theme) +{ + if (m_theme == theme) { + return; + } + m_theme = theme; + emit themeChanged(); +} + +QString PreviewBridge::plugin() const +{ + return m_plugin; +} + +void PreviewBridge::createFactory() +{ + m_factory.clear(); + + if (m_plugin.isNull()) { + setValid(false); + qWarning() << "Plugin not set"; + return; + } + + const auto offers = KPluginTrader::self()->query(s_pluginName, s_pluginName); + auto item = std::find_if(offers.constBegin(), offers.constEnd(), [this](const auto &plugin) { return plugin.pluginName() == m_plugin; }); + if (item != offers.constEnd()) { + KPluginLoader loader(item->libraryPath()); + m_factory = loader.factory(); + } + + setValid(!m_factory.isNull()); +} + +bool PreviewBridge::isValid() const +{ + return m_valid; +} + +void PreviewBridge::setValid(bool valid) +{ + if (m_valid == valid) { + return; + } + m_valid = valid; + emit validChanged(); +} + +Decoration *PreviewBridge::createDecoration(QObject *parent) +{ + if (!m_valid) { + return nullptr; + } + QVariantMap args({ {QStringLiteral("bridge"), QVariant::fromValue(this)} }); + if (!m_theme.isNull()) { + args.insert(QStringLiteral("theme"), m_theme); + } + return m_factory->create(parent, QVariantList({args})); +} + +DecorationButton *PreviewBridge::createButton(KDecoration2::Decoration *decoration, KDecoration2::DecorationButtonType type, QObject *parent) +{ + if (!m_valid) { + return nullptr; + } + return m_factory->create(QStringLiteral("button"), parent, QVariantList({QVariant::fromValue(type), QVariant::fromValue(decoration)})); +} + +void PreviewBridge::configure(QQuickItem *ctx) +{ + if (!m_valid) { + return; + } + //setup the UI + QDialog *dialog = new QDialog(); + dialog->setAttribute(Qt::WA_DeleteOnClose); + if (m_lastCreatedClient) { + dialog->setWindowTitle(m_lastCreatedClient->caption()); + } + + // create the KCModule through the plugintrader + QVariantMap args; + if (!m_theme.isNull()) { + args.insert(QStringLiteral("theme"), m_theme); + } + KCModule *kcm = m_factory->create(QStringLiteral("kcmodule"), dialog, QVariantList({args})); + if (!kcm) { + return; + } + + auto save = [this,kcm] { + kcm->save(); + if (m_lastCreatedSettings) { + emit m_lastCreatedSettings->decorationSettings()->reconfigured(); + } + // Send signal to all kwin instances + QDBusMessage message = QDBusMessage::createSignal(QStringLiteral("/KWin"), + QStringLiteral("org.kde.KWin"), + QStringLiteral("reloadConfig")); + QDBusConnection::sessionBus().send(message); + }; + connect(dialog, &QDialog::accepted, this, save); + + QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | + QDialogButtonBox::Cancel | + QDialogButtonBox::RestoreDefaults | + QDialogButtonBox::Reset, + dialog); + + QPushButton *reset = buttons->button(QDialogButtonBox::Reset); + reset->setEnabled(false); + // Here we connect our buttons with the dialog + connect(buttons, &QDialogButtonBox::accepted, dialog, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, dialog, &QDialog::reject); + connect(reset, &QPushButton::clicked, kcm, &KCModule::load); + auto changedSignal = static_cast(&KCModule::changed); + connect(kcm, changedSignal, reset, &QPushButton::setEnabled); + connect(buttons->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, kcm, &KCModule::defaults); + + QVBoxLayout *layout = new QVBoxLayout(dialog); + layout->addWidget(kcm); + layout->addWidget(buttons); + + if (ctx->window()) { + dialog->winId(); // so it creates windowHandle + dialog->windowHandle()->setTransientParent(QQuickRenderControl::renderWindowFor(ctx->window())); + dialog->setModal(true); + } + + dialog->show(); +} + +BridgeItem::BridgeItem(QObject *parent) + : QObject(parent) + , m_bridge(new PreviewBridge()) +{ + connect(m_bridge, &PreviewBridge::themeChanged, this, &BridgeItem::themeChanged); + connect(m_bridge, &PreviewBridge::pluginChanged, this, &BridgeItem::pluginChanged); + connect(m_bridge, &PreviewBridge::validChanged, this, &BridgeItem::validChanged); +} + +BridgeItem::~BridgeItem() +{ + m_bridge->deleteLater(); +} + +} +} diff --git a/kcmkwin/kwindecoration/declarative-plugin/previewbridge.h b/kcmkwin/kwindecoration/declarative-plugin/previewbridge.h new file mode 100644 index 0000000..8261f9e --- /dev/null +++ b/kcmkwin/kwindecoration/declarative-plugin/previewbridge.h @@ -0,0 +1,127 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#ifndef KDECOARTIONS_PREVIEW_BRIDGE_H +#define KDECOARTIONS_PREVIEW_BRIDGE_H + +#include +#include + +#include +#include + +class QQuickItem; + +class KPluginFactory; + +namespace KDecoration2 +{ +namespace Preview +{ + +class PreviewClient; +class PreviewItem; +class PreviewSettings; + +class PreviewBridge : public DecorationBridge +{ + Q_OBJECT + Q_PROPERTY(QString plugin READ plugin WRITE setPlugin NOTIFY pluginChanged) + Q_PROPERTY(QString theme READ theme WRITE setTheme NOTIFY themeChanged) + Q_PROPERTY(bool valid READ isValid NOTIFY validChanged) +public: + explicit PreviewBridge(QObject *parent = nullptr); + ~PreviewBridge() override; + std::unique_ptr createClient(DecoratedClient *client, Decoration *decoration) override; + void update(Decoration* decoration, const QRect& geometry) override; + std::unique_ptr settings(DecorationSettings *parent) override; + + PreviewClient *lastCreatedClient() { + return m_lastCreatedClient; + } + PreviewSettings *lastCreatedSettings() { + return m_lastCreatedSettings; + } + + void registerPreviewItem(PreviewItem *item); + void unregisterPreviewItem(PreviewItem *item); + + void setPlugin(const QString &plugin); + QString plugin() const; + void setTheme(const QString &theme); + QString theme() const; + bool isValid() const; + + KDecoration2::Decoration *createDecoration(QObject *parent = nullptr); + KDecoration2::DecorationButton *createButton(KDecoration2::Decoration *decoration, KDecoration2::DecorationButtonType type, QObject *parent = nullptr); + +public Q_SLOTS: + void configure(QQuickItem *ctx); + +Q_SIGNALS: + void pluginChanged(); + void themeChanged(); + void validChanged(); + +private: + void createFactory(); + void setValid(bool valid); + PreviewClient *m_lastCreatedClient; + PreviewSettings *m_lastCreatedSettings; + QList m_previewItems; + QString m_plugin; + QString m_theme; + QPointer m_factory; + bool m_valid; +}; + +class BridgeItem : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString plugin READ plugin WRITE setPlugin NOTIFY pluginChanged) + Q_PROPERTY(QString theme READ theme WRITE setTheme NOTIFY themeChanged) + Q_PROPERTY(bool valid READ isValid NOTIFY validChanged) + Q_PROPERTY(KDecoration2::Preview::PreviewBridge *bridge READ bridge CONSTANT) + +public: + explicit BridgeItem(QObject *parent = nullptr); + ~BridgeItem() override; + + void setPlugin(const QString &plugin) { + m_bridge->setPlugin(plugin); + } + QString plugin() const { + return m_bridge->plugin(); + } + void setTheme(const QString &theme) { + m_bridge->setTheme(theme); + } + QString theme() const { + return m_bridge->theme(); + } + bool isValid() const { + return m_bridge->isValid(); + } + + PreviewBridge *bridge() const { + return m_bridge; + } + +Q_SIGNALS: + void pluginChanged(); + void themeChanged(); + void validChanged(); + +private: + PreviewBridge *m_bridge; + +}; + +} +} + +Q_DECLARE_METATYPE(KDecoration2::Preview::PreviewBridge *) + +#endif diff --git a/kcmkwin/kwindecoration/declarative-plugin/previewbutton.cpp b/kcmkwin/kwindecoration/declarative-plugin/previewbutton.cpp new file mode 100644 index 0000000..ceaa424 --- /dev/null +++ b/kcmkwin/kwindecoration/declarative-plugin/previewbutton.cpp @@ -0,0 +1,135 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#include "previewbutton.h" +#include "previewbridge.h" +#include "previewclient.h" +#include "previewsettings.h" + +#include + +#include + +namespace KDecoration2 +{ + +namespace Preview +{ + +PreviewButtonItem::PreviewButtonItem(QQuickItem* parent) + : QQuickPaintedItem(parent) +{ +} + +PreviewButtonItem::~PreviewButtonItem() = default; + +void PreviewButtonItem::setType(int type) +{ + setType(KDecoration2::DecorationButtonType(type)); +} + +void PreviewButtonItem::setType(KDecoration2::DecorationButtonType type) +{ + if (m_type == type) { + return; + } + m_type = type; + emit typeChanged(); +} + +KDecoration2::DecorationButtonType PreviewButtonItem::type() const +{ + return m_type; +} + +PreviewBridge *PreviewButtonItem::bridge() const +{ + return m_bridge.data(); +} + +void PreviewButtonItem::setBridge(PreviewBridge *bridge) +{ + if (m_bridge == bridge) { + return; + } + m_bridge = bridge; + emit bridgeChanged(); +} + +Settings *PreviewButtonItem::settings() const +{ + return m_settings.data(); +} + +void PreviewButtonItem::setSettings(Settings *settings) +{ + if (m_settings == settings) { + return; + } + m_settings = settings; + emit settingsChanged(); +} + +int PreviewButtonItem::typeAsInt() const +{ + return int(m_type); +} + +void PreviewButtonItem::componentComplete() +{ + QQuickPaintedItem::componentComplete(); + createButton(); +} + +void PreviewButtonItem::createButton() +{ + if (m_type == KDecoration2::DecorationButtonType::Custom || m_decoration || !m_settings || !m_bridge) { + return; + } + m_decoration = m_bridge->createDecoration(this); + if (!m_decoration) { + return; + } + auto client = m_bridge->lastCreatedClient(); + client->setMinimizable(true); + client->setMaximizable(true); + client->setActive(false); + client->setProvidesContextHelp(true); + m_decoration->setSettings(m_settings->settings()); + m_decoration->init(); + m_button = m_bridge->createButton(m_decoration, m_type); + connect(this, &PreviewButtonItem::widthChanged, this, &PreviewButtonItem::syncGeometry); + connect(this, &PreviewButtonItem::heightChanged, this, &PreviewButtonItem::syncGeometry); + syncGeometry(); +} + +void PreviewButtonItem::syncGeometry() +{ + if (!m_button) { + return; + } + m_button->setGeometry(QRect(0, 0, width(), height())); +} + +void PreviewButtonItem::paint(QPainter *painter) +{ + if (!m_button) { + return; + } + QRect size { 0, 0, (int)width(), (int)height() }; + m_button->paint(painter, size); + painter->setCompositionMode(QPainter::CompositionMode_SourceAtop); + painter->fillRect(size, m_color); +} + +void PreviewButtonItem::setColor(const QColor &color) +{ + m_color = color; + m_color.setAlpha(127); + update(); +} + +} +} diff --git a/kcmkwin/kwindecoration/declarative-plugin/previewbutton.h b/kcmkwin/kwindecoration/declarative-plugin/previewbutton.h new file mode 100644 index 0000000..85d0a4d --- /dev/null +++ b/kcmkwin/kwindecoration/declarative-plugin/previewbutton.h @@ -0,0 +1,73 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#ifndef KDECOARTIONS_PREVIEW_BUTTON_ITEM_H +#define KDECOARTIONS_PREVIEW_BUTTON_ITEM_H + +#include +#include +#include +#include + +namespace KDecoration2 +{ +class Decoration; + +namespace Preview +{ +class PreviewBridge; +class Settings; + +class PreviewButtonItem : public QQuickPaintedItem +{ + Q_OBJECT + Q_PROPERTY(KDecoration2::Preview::PreviewBridge *bridge READ bridge WRITE setBridge NOTIFY bridgeChanged) + Q_PROPERTY(KDecoration2::Preview::Settings *settings READ settings WRITE setSettings NOTIFY settingsChanged) + Q_PROPERTY(int type READ typeAsInt WRITE setType NOTIFY typeChanged) + Q_PROPERTY(QColor color READ color WRITE setColor) + +public: + explicit PreviewButtonItem(QQuickItem *parent = nullptr); + ~PreviewButtonItem() override; + void paint(QPainter *painter) override; + + PreviewBridge *bridge() const; + void setBridge(PreviewBridge *bridge); + + Settings *settings() const; + void setSettings(Settings *settings); + + KDecoration2::DecorationButtonType type() const; + int typeAsInt() const; + void setType(KDecoration2::DecorationButtonType type); + void setType(int type); + + const QColor &color() const { return m_color; } + void setColor(const QColor &color); + +Q_SIGNALS: + void bridgeChanged(); + void typeChanged(); + void settingsChanged(); + +protected: + void componentComplete() override; + +private: + void createButton(); + void syncGeometry(); + QColor m_color; + QPointer m_bridge; + QPointer m_settings; + KDecoration2::Decoration *m_decoration = nullptr; + KDecoration2::DecorationButton *m_button = nullptr; + KDecoration2::DecorationButtonType m_type = KDecoration2::DecorationButtonType::Custom; + +}; + +} // Preview +} // KDecoration2 + +#endif diff --git a/kcmkwin/kwindecoration/declarative-plugin/previewclient.cpp b/kcmkwin/kwindecoration/declarative-plugin/previewclient.cpp new file mode 100644 index 0000000..9a0fb14 --- /dev/null +++ b/kcmkwin/kwindecoration/declarative-plugin/previewclient.cpp @@ -0,0 +1,456 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#include "previewclient.h" +#include +#include + +#include +#include +#include +#include + +namespace KDecoration2 +{ +namespace Preview +{ + +PreviewClient::PreviewClient(DecoratedClient *c, Decoration *decoration) + : QObject(decoration) + , ApplicationMenuEnabledDecoratedClientPrivate(c, decoration) + , m_icon(QIcon::fromTheme(QStringLiteral("start-here-kde"))) + , m_iconName(m_icon.name()) + , m_palette(QStringLiteral("kdeglobals")) + , m_active(true) + , m_closeable(true) + , m_keepBelow(false) + , m_keepAbove(false) + , m_maximizable(true) + , m_maximizedHorizontally(false) + , m_maximizedVertically(false) + , m_minimizable(true) + , m_modal(false) + , m_movable(true) + , m_resizable(true) + , m_shadeable(true) + , m_shaded(false) + , m_providesContextHelp(false) + , m_desktop(1) + , m_width(0) + , m_height(0) + , m_bordersTopEdge(false) + , m_bordersLeftEdge(false) + , m_bordersRightEdge(false) + , m_bordersBottomEdge(false) +{ + connect(this, &PreviewClient::captionChanged, c, &DecoratedClient::captionChanged); + connect(this, &PreviewClient::activeChanged, c, &DecoratedClient::activeChanged); + connect(this, &PreviewClient::closeableChanged, c, &DecoratedClient::closeableChanged); + connect(this, &PreviewClient::keepAboveChanged, c, &DecoratedClient::keepAboveChanged); + connect(this, &PreviewClient::keepBelowChanged, c, &DecoratedClient::keepBelowChanged); + connect(this, &PreviewClient::maximizableChanged, c, &DecoratedClient::maximizeableChanged); + connect(this, &PreviewClient::maximizedChanged, c, &DecoratedClient::maximizedChanged); + connect(this, &PreviewClient::maximizedVerticallyChanged, c, &DecoratedClient::maximizedVerticallyChanged); + connect(this, &PreviewClient::maximizedHorizontallyChanged, c, &DecoratedClient::maximizedHorizontallyChanged); + connect(this, &PreviewClient::minimizableChanged, c, &DecoratedClient::minimizeableChanged); + connect(this, &PreviewClient::movableChanged, c, &DecoratedClient::moveableChanged); + connect(this, &PreviewClient::onAllDesktopsChanged, c, &DecoratedClient::onAllDesktopsChanged); + connect(this, &PreviewClient::resizableChanged, c, &DecoratedClient::resizeableChanged); + connect(this, &PreviewClient::shadeableChanged, c, &DecoratedClient::shadeableChanged); + connect(this, &PreviewClient::shadedChanged, c, &DecoratedClient::shadedChanged); + connect(this, &PreviewClient::providesContextHelpChanged, c, &DecoratedClient::providesContextHelpChanged); + connect(this, &PreviewClient::onAllDesktopsChanged, c, &DecoratedClient::onAllDesktopsChanged); + connect(this, &PreviewClient::widthChanged, c, &DecoratedClient::widthChanged); + connect(this, &PreviewClient::heightChanged, c, &DecoratedClient::heightChanged); + connect(this, &PreviewClient::iconChanged, c, &DecoratedClient::iconChanged); + connect(this, &PreviewClient::paletteChanged, c, &DecoratedClient::paletteChanged); + connect(this, &PreviewClient::maximizedVerticallyChanged, this, + [this]() { + emit maximizedChanged(isMaximized()); + } + ); + connect(this, &PreviewClient::maximizedHorizontallyChanged, this, + [this]() { + emit maximizedChanged(isMaximized()); + } + ); + connect(this, &PreviewClient::iconNameChanged, this, + [this]() { + m_icon = QIcon::fromTheme(m_iconName); + emit iconChanged(m_icon); + } + ); + connect(this, &PreviewClient::desktopChanged, this, + [this]() { + emit onAllDesktopsChanged(isOnAllDesktops()); + } + ); + connect(&m_palette, &KWin::Decoration::DecorationPalette::changed, [this]() { + emit paletteChanged(m_palette.palette()); + }); + auto emitEdgesChanged = [this, c]() { + c->adjacentScreenEdgesChanged(adjacentScreenEdges()); + }; + connect(this, &PreviewClient::bordersTopEdgeChanged, this, emitEdgesChanged); + connect(this, &PreviewClient::bordersLeftEdgeChanged, this, emitEdgesChanged); + connect(this, &PreviewClient::bordersRightEdgeChanged, this, emitEdgesChanged); + connect(this, &PreviewClient::bordersBottomEdgeChanged, this, emitEdgesChanged); + auto emitSizeChanged = [c]() { + emit c->sizeChanged(c->size()); + }; + connect(this, &PreviewClient::widthChanged, this, emitSizeChanged); + connect(this, &PreviewClient::heightChanged, this, emitSizeChanged); + + qApp->installEventFilter(this); +} + +PreviewClient::~PreviewClient() = default; + +void PreviewClient::setIcon(const QIcon &pixmap) +{ + m_icon = pixmap; + emit iconChanged(m_icon); +} + +int PreviewClient::width() const +{ + return m_width; +} + +int PreviewClient::height() const +{ + return m_height; +} + +QSize PreviewClient::size() const +{ + return QSize(m_width, m_height); +} + +QString PreviewClient::caption() const +{ + return m_caption; +} + +WId PreviewClient::decorationId() const +{ + return 0; +} + +int PreviewClient::desktop() const +{ + return m_desktop; +} + +void PreviewClient::setDesktop(int desktop) +{ + if (desktop == 0) { + desktop = 1; + } + if (m_desktop == desktop) { + return; + } + m_desktop = desktop; + emit desktopChanged(m_desktop); +} + +QIcon PreviewClient::icon() const +{ + return m_icon; +} + +QString PreviewClient::iconName() const +{ + return m_iconName; +} + +bool PreviewClient::isActive() const +{ + return m_active; +} + +bool PreviewClient::isCloseable() const +{ + return m_closeable; +} + +bool PreviewClient::isKeepAbove() const +{ + return m_keepAbove; +} + +bool PreviewClient::isKeepBelow() const +{ + return m_keepBelow; +} + +bool PreviewClient::isMaximizeable() const +{ + return m_maximizable; +} + +bool PreviewClient::isMaximized() const +{ + return isMaximizedHorizontally() && isMaximizedVertically(); +} + +bool PreviewClient::isMaximizedHorizontally() const +{ + return m_maximizedHorizontally; +} + +bool PreviewClient::isMaximizedVertically() const +{ + return m_maximizedVertically; +} + +bool PreviewClient::isMinimizeable() const +{ + return m_minimizable; +} + +bool PreviewClient::isModal() const +{ + return m_modal; +} + +bool PreviewClient::isMoveable() const +{ + return m_movable; +} + +bool PreviewClient::isOnAllDesktops() const +{ + return desktop() == -1; +} + +bool PreviewClient::isResizeable() const +{ + return m_resizable; +} + +bool PreviewClient::isShadeable() const +{ + return m_shadeable; +} + +bool PreviewClient::isShaded() const +{ + return m_shaded; +} + +bool PreviewClient::providesContextHelp() const +{ + return m_providesContextHelp; +} + +WId PreviewClient::windowId() const +{ + return 0; +} + +QPalette PreviewClient::palette() const +{ + return m_palette.palette(); +} + +QColor PreviewClient::color(ColorGroup group, ColorRole role) const +{ + return m_palette.color(group, role); +} + +Qt::Edges PreviewClient::adjacentScreenEdges() const +{ + Qt::Edges edges; + if (m_bordersBottomEdge) { + edges |= Qt::BottomEdge; + } + if (m_bordersLeftEdge) { + edges |= Qt::LeftEdge; + } + if (m_bordersRightEdge) { + edges |= Qt::RightEdge; + } + if (m_bordersTopEdge) { + edges |= Qt::TopEdge; + } + return edges; +} + +bool PreviewClient::hasApplicationMenu() const +{ + return true; +} + +bool PreviewClient::isApplicationMenuActive() const +{ + return false; +} + +bool PreviewClient::bordersBottomEdge() const +{ + return m_bordersBottomEdge; +} + +bool PreviewClient::bordersLeftEdge() const +{ + return m_bordersLeftEdge; +} + +bool PreviewClient::bordersRightEdge() const +{ + return m_bordersRightEdge; +} + +bool PreviewClient::bordersTopEdge() const +{ + return m_bordersTopEdge; +} + +void PreviewClient::setBordersBottomEdge(bool enabled) +{ + if (m_bordersBottomEdge == enabled) { + return; + } + m_bordersBottomEdge = enabled; + emit bordersBottomEdgeChanged(enabled); +} + +void PreviewClient::setBordersLeftEdge(bool enabled) +{ + if (m_bordersLeftEdge == enabled) { + return; + } + m_bordersLeftEdge = enabled; + emit bordersLeftEdgeChanged(enabled); +} + +void PreviewClient::setBordersRightEdge(bool enabled) +{ + if (m_bordersRightEdge == enabled) { + return; + } + m_bordersRightEdge = enabled; + emit bordersRightEdgeChanged(enabled); +} + +void PreviewClient::setBordersTopEdge(bool enabled) +{ + if (m_bordersTopEdge == enabled) { + return; + } + m_bordersTopEdge = enabled; + emit bordersTopEdgeChanged(enabled); +} + +void PreviewClient::requestShowToolTip(const QString &text) +{ + Q_UNUSED(text); +} + +void PreviewClient::requestHideToolTip() +{ +} + +void PreviewClient::requestClose() +{ + emit closeRequested(); +} + +void PreviewClient::requestContextHelp() +{ +} + +void PreviewClient::requestToggleMaximization(Qt::MouseButtons buttons) +{ + if (buttons.testFlag(Qt::LeftButton)) { + const bool set = !isMaximized(); + setMaximizedHorizontally(set); + setMaximizedVertically(set); + } else if (buttons.testFlag(Qt::RightButton)) { + setMaximizedHorizontally(!isMaximizedHorizontally()); + } else if (buttons.testFlag(Qt::MiddleButton)) { + setMaximizedVertically(!isMaximizedVertically()); + } +} + +void PreviewClient::requestMinimize() +{ + emit minimizeRequested(); +} + +void PreviewClient::requestToggleKeepAbove() +{ + setKeepAbove(!isKeepAbove()); +} + +void PreviewClient::requestToggleKeepBelow() +{ + setKeepBelow(!isKeepBelow()); +} + +void PreviewClient::requestShowWindowMenu() +{ + emit showWindowMenuRequested(); +} + +void PreviewClient::requestShowApplicationMenu(const QRect &rect, int actionId) +{ + Q_UNUSED(rect); + Q_UNUSED(actionId); +} + +void PreviewClient::showApplicationMenu(int actionId) +{ + Q_UNUSED(actionId) +} + +void PreviewClient::requestToggleOnAllDesktops() +{ + setDesktop(isOnAllDesktops() ? 1 : -1); +} + +void PreviewClient::requestToggleShade() +{ + setShaded(!isShaded()); +} + +#define SETTER(type, name, variable) \ +void PreviewClient::name(type variable) \ +{ \ + if (m_##variable == variable) { \ + return; \ + } \ + m_##variable = variable; \ + emit variable##Changed(m_##variable); \ +} + +#define SETTER2(name, variable) SETTER(bool, name, variable) + +SETTER(const QString &, setCaption, caption) +SETTER(const QString &, setIconName, iconName) +SETTER(int, setWidth, width) +SETTER(int, setHeight, height) + +SETTER2(setActive, active) +SETTER2(setCloseable, closeable) +SETTER2(setMaximizable, maximizable) +SETTER2(setKeepBelow, keepBelow) +SETTER2(setKeepAbove, keepAbove) +SETTER2(setMaximizedHorizontally, maximizedHorizontally) +SETTER2(setMaximizedVertically, maximizedVertically) +SETTER2(setMinimizable, minimizable) +SETTER2(setModal, modal) +SETTER2(setMovable, movable) +SETTER2(setResizable, resizable) +SETTER2(setShadeable, shadeable) +SETTER2(setShaded, shaded) +SETTER2(setProvidesContextHelp, providesContextHelp) + +#undef SETTER2 +#undef SETTER + +} // namespace Preview +} // namespace KDecoration2 diff --git a/kcmkwin/kwindecoration/declarative-plugin/previewclient.h b/kcmkwin/kwindecoration/declarative-plugin/previewclient.h new file mode 100644 index 0000000..e515a9f --- /dev/null +++ b/kcmkwin/kwindecoration/declarative-plugin/previewclient.h @@ -0,0 +1,201 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#ifndef KDECOARTIONS_PREVIEW_CLIENT_H +#define KDECOARTIONS_PREVIEW_CLIENT_H + +#include "../../../decorations/decorationpalette.h" + +#include +#include +#include + +class QAbstractItemModel; + +namespace KDecoration2 +{ +namespace Preview +{ +class PreviewClient : public QObject, public ApplicationMenuEnabledDecoratedClientPrivate +{ + Q_OBJECT + Q_PROPERTY(KDecoration2::Decoration *decoration READ decoration CONSTANT) + Q_PROPERTY(QString caption READ caption WRITE setCaption NOTIFY captionChanged) + Q_PROPERTY(QIcon icon READ icon WRITE setIcon NOTIFY iconChanged) + Q_PROPERTY(QString iconName READ iconName WRITE setIconName NOTIFY iconNameChanged) + Q_PROPERTY(bool active READ isActive WRITE setActive NOTIFY activeChanged) + Q_PROPERTY(bool closeable READ isCloseable WRITE setCloseable NOTIFY closeableChanged) + Q_PROPERTY(bool keepAbove READ isKeepAbove WRITE setKeepAbove NOTIFY keepAboveChanged) + Q_PROPERTY(bool keepBelow READ isKeepBelow WRITE setKeepBelow NOTIFY keepBelowChanged) + Q_PROPERTY(bool maximizable READ isMaximizeable WRITE setMaximizable NOTIFY maximizableChanged) + Q_PROPERTY(bool maximized READ isMaximized NOTIFY maximizedChanged) + Q_PROPERTY(bool maximizedVertically READ isMaximizedVertically WRITE setMaximizedVertically NOTIFY maximizedVerticallyChanged) + Q_PROPERTY(bool maximizedHorizontally READ isMaximizedHorizontally WRITE setMaximizedHorizontally NOTIFY maximizedHorizontallyChanged) + Q_PROPERTY(bool minimizable READ isMinimizeable WRITE setMinimizable NOTIFY minimizableChanged) + Q_PROPERTY(bool modal READ isModal WRITE setModal NOTIFY modalChanged) + Q_PROPERTY(bool movable READ isMoveable WRITE setMovable NOTIFY movableChanged) + Q_PROPERTY(int desktop READ desktop WRITE setDesktop NOTIFY desktopChanged) + Q_PROPERTY(bool onAllDesktops READ isOnAllDesktops NOTIFY onAllDesktopsChanged) + Q_PROPERTY(bool resizable READ isResizeable WRITE setResizable NOTIFY resizableChanged) + Q_PROPERTY(bool shadeable READ isShadeable WRITE setShadeable NOTIFY shadeableChanged) + Q_PROPERTY(bool shaded READ isShaded WRITE setShaded NOTIFY shadedChanged) + Q_PROPERTY(bool providesContextHelp READ providesContextHelp WRITE setProvidesContextHelp NOTIFY providesContextHelpChanged) + Q_PROPERTY(int width READ width WRITE setWidth NOTIFY widthChanged) + Q_PROPERTY(int height READ height WRITE setHeight NOTIFY heightChanged) + Q_PROPERTY(bool bordersTopEdge READ bordersTopEdge WRITE setBordersTopEdge NOTIFY bordersTopEdgeChanged) + Q_PROPERTY(bool bordersLeftEdge READ bordersLeftEdge WRITE setBordersLeftEdge NOTIFY bordersLeftEdgeChanged) + Q_PROPERTY(bool bordersRightEdge READ bordersRightEdge WRITE setBordersRightEdge NOTIFY bordersRightEdgeChanged) + Q_PROPERTY(bool bordersBottomEdge READ bordersBottomEdge WRITE setBordersBottomEdge NOTIFY bordersBottomEdgeChanged) +public: + explicit PreviewClient(DecoratedClient *client, Decoration *decoration); + ~PreviewClient() override; + + QString caption() const override; + WId decorationId() const override; + WId windowId() const override; + int desktop() const override; + QIcon icon() const override; + bool isActive() const override; + bool isCloseable() const override; + bool isKeepAbove() const override; + bool isKeepBelow() const override; + bool isMaximizeable() const override; + bool isMaximized() const override; + bool isMaximizedVertically() const override; + bool isMaximizedHorizontally() const override; + bool isMinimizeable() const override; + bool isModal() const override; + bool isMoveable() const override; + bool isOnAllDesktops() const override; + bool isResizeable() const override; + bool isShadeable() const override; + bool isShaded() const override; + bool providesContextHelp() const override; + + int width() const override; + int height() const override; + QSize size() const override; + QPalette palette() const override; + QColor color(ColorGroup group, ColorRole role) const override; + Qt::Edges adjacentScreenEdges() const override; + + bool hasApplicationMenu() const override; + bool isApplicationMenuActive() const override; + + void requestShowToolTip(const QString &text) override; + void requestHideToolTip() override; + void requestClose() override; + void requestContextHelp() override; + void requestToggleMaximization(Qt::MouseButtons buttons) override; + void requestMinimize() override; + void requestToggleKeepAbove() override; + void requestToggleKeepBelow() override; + void requestToggleShade() override; + void requestShowWindowMenu() override; + void requestShowApplicationMenu(const QRect &rect, int actionId) override; + void requestToggleOnAllDesktops() override; + + void showApplicationMenu(int actionId) override; + + void setCaption(const QString &caption); + void setActive(bool active); + void setCloseable(bool closeable); + void setMaximizable(bool maximizable); + void setKeepBelow(bool keepBelow); + void setKeepAbove(bool keepAbove); + void setMaximizedHorizontally(bool maximized); + void setMaximizedVertically(bool maximized); + void setMinimizable(bool minimizable); + void setModal(bool modal); + void setMovable(bool movable); + void setResizable(bool resizable); + void setShadeable(bool shadeable); + void setShaded(bool shaded); + void setProvidesContextHelp(bool contextHelp); + void setDesktop(int desktop); + + void setWidth(int width); + void setHeight(int height); + + QString iconName() const; + void setIconName(const QString &icon); + void setIcon(const QIcon &icon); + + bool bordersTopEdge() const; + bool bordersLeftEdge() const; + bool bordersRightEdge() const; + bool bordersBottomEdge() const; + + void setBordersTopEdge(bool enabled); + void setBordersLeftEdge(bool enabled); + void setBordersRightEdge(bool enabled); + void setBordersBottomEdge(bool enabled); + +Q_SIGNALS: + void captionChanged(const QString &); + void iconChanged(const QIcon &); + void iconNameChanged(const QString &); + void activeChanged(bool); + void closeableChanged(bool); + void keepAboveChanged(bool); + void keepBelowChanged(bool); + void maximizableChanged(bool); + void maximizedChanged(bool); + void maximizedVerticallyChanged(bool); + void maximizedHorizontallyChanged(bool); + void minimizableChanged(bool); + void modalChanged(bool); + void movableChanged(bool); + void onAllDesktopsChanged(bool); + void resizableChanged(bool); + void shadeableChanged(bool); + void shadedChanged(bool); + void providesContextHelpChanged(bool); + void desktopChanged(int); + void widthChanged(int); + void heightChanged(int); + void paletteChanged(const QPalette&); + void bordersTopEdgeChanged(bool); + void bordersLeftEdgeChanged(bool); + void bordersRightEdgeChanged(bool); + void bordersBottomEdgeChanged(bool); + + void showWindowMenuRequested(); + void showApplicationMenuRequested(); + void minimizeRequested(); + void closeRequested(); + +private: + QString m_caption; + QIcon m_icon; + QString m_iconName; + KWin::Decoration::DecorationPalette m_palette; + bool m_active; + bool m_closeable; + bool m_keepBelow; + bool m_keepAbove; + bool m_maximizable; + bool m_maximizedHorizontally; + bool m_maximizedVertically; + bool m_minimizable; + bool m_modal; + bool m_movable; + bool m_resizable; + bool m_shadeable; + bool m_shaded; + bool m_providesContextHelp; + int m_desktop; + int m_width; + int m_height; + bool m_bordersTopEdge; + bool m_bordersLeftEdge; + bool m_bordersRightEdge; + bool m_bordersBottomEdge; +}; + +} // namespace Preview +} // namespace KDecoration2 + +#endif diff --git a/kcmkwin/kwindecoration/declarative-plugin/previewitem.cpp b/kcmkwin/kwindecoration/declarative-plugin/previewitem.cpp new file mode 100644 index 0000000..2043a0a --- /dev/null +++ b/kcmkwin/kwindecoration/declarative-plugin/previewitem.cpp @@ -0,0 +1,454 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#include "previewitem.h" +#include "previewbridge.h" +#include "previewsettings.h" +#include "previewclient.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +namespace KDecoration2 +{ +namespace Preview +{ + +PreviewItem::PreviewItem(QQuickItem *parent) + : QQuickPaintedItem(parent) + , m_decoration(nullptr) + , m_windowColor(QPalette().window().color()) +{ + setAcceptHoverEvents(true); + setFiltersChildMouseEvents(true); + setAcceptedMouseButtons(Qt::MouseButtons(~Qt::NoButton)); + connect(this, &PreviewItem::widthChanged, this, &PreviewItem::syncSize); + connect(this, &PreviewItem::heightChanged, this, &PreviewItem::syncSize); + connect(this, &PreviewItem::bridgeChanged, this, &PreviewItem::createDecoration); + connect(this, &PreviewItem::settingsChanged, this, &PreviewItem::createDecoration); +} + +PreviewItem::~PreviewItem() +{ + m_decoration->deleteLater(); + if (m_bridge){ + m_bridge->unregisterPreviewItem(this); + } +} + +void PreviewItem::componentComplete() +{ + QQuickPaintedItem::componentComplete(); + createDecoration(); + if (m_decoration) { + m_decoration->setSettings(m_settings->settings()); + m_decoration->init(); + syncSize(); + } +} + +void PreviewItem::createDecoration() +{ + if (m_bridge.isNull() || m_settings.isNull() || m_decoration) { + return; + } + Decoration *decoration = m_bridge->createDecoration(nullptr); + m_client = m_bridge->lastCreatedClient(); + setDecoration(decoration); +} + +Decoration *PreviewItem::decoration() const +{ + return m_decoration; +} + +void PreviewItem::setDecoration(Decoration *deco) +{ + if (!deco || m_decoration == deco) { + return; + } + + m_decoration = deco; + m_decoration->setProperty("visualParent", QVariant::fromValue(this)); + connect(m_decoration, &Decoration::bordersChanged, this, &PreviewItem::syncSize); + connect(m_decoration, &Decoration::shadowChanged, this, &PreviewItem::syncSize); + connect(m_decoration, &Decoration::shadowChanged, this, &PreviewItem::shadowChanged); + emit decorationChanged(m_decoration); +} + +QColor PreviewItem::windowColor() const +{ + return m_windowColor; +} + +void PreviewItem::setWindowColor(const QColor &color) +{ + if (m_windowColor == color) { + return; + } + m_windowColor = color; + emit windowColorChanged(m_windowColor); + update(); +} + +void PreviewItem::paint(QPainter *painter) +{ + if (!m_decoration) { + return; + } + int paddingLeft = 0; + int paddingTop = 0; + int paddingRight = 0; + int paddingBottom = 0; + paintShadow(painter, paddingLeft, paddingRight, paddingTop, paddingBottom); + m_decoration->paint(painter, QRect(0, 0, width(), height())); + if (m_drawBackground) { + painter->fillRect(m_decoration->borderLeft(), m_decoration->borderTop(), + width() - m_decoration->borderLeft() - m_decoration->borderRight() - paddingLeft - paddingRight, + height() - m_decoration->borderTop() - m_decoration->borderBottom() - paddingTop - paddingBottom, + m_windowColor); + } +} + +void PreviewItem::paintShadow(QPainter *painter, int &paddingLeft, int &paddingRight, int &paddingTop, int &paddingBottom) +{ + const auto &shadow = ((const Decoration*)(m_decoration))->shadow(); + if (!shadow) { + return; + } + + paddingLeft = shadow->paddingLeft(); + paddingTop = shadow->paddingTop(); + paddingRight = shadow->paddingRight(); + paddingBottom = shadow->paddingBottom(); + + const QImage shadowPixmap = shadow->shadow(); + if (shadowPixmap.isNull()) { + return; + } + + const QRect outerRect(-paddingLeft, -paddingTop, width(), height()); + const QRect shadowRect(shadowPixmap.rect()); + + const QSize topLeftSize(shadow->topLeftGeometry().size()); + QRect topLeftTarget( + QPoint(outerRect.x(), outerRect.y()), + topLeftSize); + + const QSize topRightSize(shadow->topRightGeometry().size()); + QRect topRightTarget( + QPoint(outerRect.x() + outerRect.width() - topRightSize.width(), + outerRect.y()), + topRightSize); + + const QSize bottomRightSize(shadow->bottomRightGeometry().size()); + QRect bottomRightTarget( + QPoint(outerRect.x() + outerRect.width() - bottomRightSize.width(), + outerRect.y() + outerRect.height() - bottomRightSize.height()), + bottomRightSize); + + const QSize bottomLeftSize(shadow->bottomLeftGeometry().size()); + QRect bottomLeftTarget( + QPoint(outerRect.x(), + outerRect.y() + outerRect.height() - bottomLeftSize.height()), + bottomLeftSize); + + // Re-distribute the corner tiles so no one of them is overlapping with others. + // By doing this, we assume that shadow's corner tiles are symmetric + // and it is OK to not draw top/right/bottom/left tile between corners. + // For example, let's say top-left and top-right tiles are overlapping. + // In that case, the right side of the top-left tile will be shifted to left, + // the left side of the top-right tile will shifted to right, and the top + // tile won't be rendered. + bool drawTop = true; + if (topLeftTarget.x() + topLeftTarget.width() >= topRightTarget.x()) { + const float halfOverlap = qAbs(topLeftTarget.x() + topLeftTarget.width() - topRightTarget.x()) / 2.0f; + topLeftTarget.setRight(topLeftTarget.right() - std::floor(halfOverlap)); + topRightTarget.setLeft(topRightTarget.left() + std::ceil(halfOverlap)); + drawTop = false; + } + + bool drawRight = true; + if (topRightTarget.y() + topRightTarget.height() >= bottomRightTarget.y()) { + const float halfOverlap = qAbs(topRightTarget.y() + topRightTarget.height() - bottomRightTarget.y()) / 2.0f; + topRightTarget.setBottom(topRightTarget.bottom() - std::floor(halfOverlap)); + bottomRightTarget.setTop(bottomRightTarget.top() + std::ceil(halfOverlap)); + drawRight = false; + } + + bool drawBottom = true; + if (bottomLeftTarget.x() + bottomLeftTarget.width() >= bottomRightTarget.x()) { + const float halfOverlap = qAbs(bottomLeftTarget.x() + bottomLeftTarget.width() - bottomRightTarget.x()) / 2.0f; + bottomLeftTarget.setRight(bottomLeftTarget.right() - std::floor(halfOverlap)); + bottomRightTarget.setLeft(bottomRightTarget.left() + std::ceil(halfOverlap)); + drawBottom = false; + } + + bool drawLeft = true; + if (topLeftTarget.y() + topLeftTarget.height() >= bottomLeftTarget.y()) { + const float halfOverlap = qAbs(topLeftTarget.y() + topLeftTarget.height() - bottomLeftTarget.y()) / 2.0f; + topLeftTarget.setBottom(topLeftTarget.bottom() - std::floor(halfOverlap)); + bottomLeftTarget.setTop(bottomLeftTarget.top() + std::ceil(halfOverlap)); + drawLeft = false; + } + + painter->translate(paddingLeft, paddingTop); + + painter->drawImage(topLeftTarget, shadowPixmap, + QRect(QPoint(0, 0), topLeftTarget.size())); + + painter->drawImage(topRightTarget, shadowPixmap, + QRect(QPoint(shadowRect.width() - topRightTarget.width(), 0), + topRightTarget.size())); + + painter->drawImage(bottomRightTarget, shadowPixmap, + QRect(QPoint(shadowRect.width() - bottomRightTarget.width(), + shadowRect.height() - bottomRightTarget.height()), + bottomRightTarget.size())); + + painter->drawImage(bottomLeftTarget, shadowPixmap, + QRect(QPoint(0, shadowRect.height() - bottomLeftTarget.height()), + bottomLeftTarget.size())); + + if (drawTop) { + QRect topTarget(topLeftTarget.x() + topLeftTarget.width(), + topLeftTarget.y(), + topRightTarget.x() - topLeftTarget.x() - topLeftTarget.width(), + topRightTarget.height()); + QRect topSource(shadow->topGeometry()); + topSource.setHeight(topTarget.height()); + topSource.moveTop(shadowRect.top()); + painter->drawImage(topTarget, shadowPixmap, topSource); + } + + if (drawRight) { + QRect rightTarget(topRightTarget.x(), + topRightTarget.y() + topRightTarget.height(), + topRightTarget.width(), + bottomRightTarget.y() - topRightTarget.y() - topRightTarget.height()); + QRect rightSource(shadow->rightGeometry()); + rightSource.setWidth(rightTarget.width()); + rightSource.moveRight(shadowRect.right()); + painter->drawImage(rightTarget, shadowPixmap, rightSource); + } + + if (drawBottom) { + QRect bottomTarget(bottomLeftTarget.x() + bottomLeftTarget.width(), + bottomLeftTarget.y(), + bottomRightTarget.x() - bottomLeftTarget.x() - bottomLeftTarget.width(), + bottomRightTarget.height()); + QRect bottomSource(shadow->bottomGeometry()); + bottomSource.setHeight(bottomTarget.height()); + bottomSource.moveBottom(shadowRect.bottom()); + painter->drawImage(bottomTarget, shadowPixmap, bottomSource); + } + + if (drawLeft) { + QRect leftTarget(topLeftTarget.x(), + topLeftTarget.y() + topLeftTarget.height(), + topLeftTarget.width(), + bottomLeftTarget.y() - topLeftTarget.y() - topLeftTarget.height()); + QRect leftSource(shadow->leftGeometry()); + leftSource.setWidth(leftTarget.width()); + leftSource.moveLeft(shadowRect.left()); + painter->drawImage(leftTarget, shadowPixmap, leftSource); + } +} + +void PreviewItem::mouseDoubleClickEvent(QMouseEvent *event) +{ + const auto &shadow = m_decoration->shadow(); + if (shadow) { + QMouseEvent e(event->type(), + event->localPos() - QPointF(shadow->paddingLeft(), shadow->paddingTop()), + event->button(), + event->buttons(), + event->modifiers()); + QCoreApplication::instance()->sendEvent(decoration(), &e); + } else { + QCoreApplication::instance()->sendEvent(decoration(), event); + } +} + +void PreviewItem::mousePressEvent(QMouseEvent *event) +{ + const auto &shadow = m_decoration->shadow(); + if (shadow) { + QMouseEvent e(event->type(), + event->localPos() - QPointF(shadow->paddingLeft(), shadow->paddingTop()), + event->button(), + event->buttons(), + event->modifiers()); + QCoreApplication::instance()->sendEvent(decoration(), &e); + } else { + QCoreApplication::instance()->sendEvent(decoration(), event); + } +} + +void PreviewItem::mouseReleaseEvent(QMouseEvent *event) +{ + const auto &shadow = m_decoration->shadow(); + if (shadow) { + QMouseEvent e(event->type(), + event->localPos() - QPointF(shadow->paddingLeft(), shadow->paddingTop()), + event->button(), + event->buttons(), + event->modifiers()); + QCoreApplication::instance()->sendEvent(decoration(), &e); + } else { + QCoreApplication::instance()->sendEvent(decoration(), event); + } +} + +void PreviewItem::mouseMoveEvent(QMouseEvent *event) +{ + const auto &shadow = m_decoration->shadow(); + if (shadow) { + QMouseEvent e(event->type(), + event->localPos() - QPointF(shadow->paddingLeft(), shadow->paddingTop()), + event->button(), + event->buttons(), + event->modifiers()); + QCoreApplication::instance()->sendEvent(decoration(), &e); + } else { + QCoreApplication::instance()->sendEvent(decoration(), event); + } +} + +void PreviewItem::hoverEnterEvent(QHoverEvent *event) +{ + const auto &shadow = m_decoration->shadow(); + if (shadow) { + QHoverEvent e(event->type(), + event->posF() - QPointF(shadow->paddingLeft(), shadow->paddingTop()), + event->oldPosF() - QPointF(shadow->paddingLeft(), shadow->paddingTop()), + event->modifiers()); + QCoreApplication::instance()->sendEvent(decoration(), &e); + } else { + QCoreApplication::instance()->sendEvent(decoration(), event); + } +} + +void PreviewItem::hoverLeaveEvent(QHoverEvent *event) +{ + const auto &shadow = m_decoration->shadow(); + if (shadow) { + QHoverEvent e(event->type(), + event->posF() - QPointF(shadow->paddingLeft(), shadow->paddingTop()), + event->oldPosF() - QPointF(shadow->paddingLeft(), shadow->paddingTop()), + event->modifiers()); + QCoreApplication::instance()->sendEvent(decoration(), &e); + } else { + QCoreApplication::instance()->sendEvent(decoration(), event); + } +} + +void PreviewItem::hoverMoveEvent(QHoverEvent *event) +{ + const auto &shadow = m_decoration->shadow(); + if (shadow) { + QHoverEvent e(event->type(), + event->posF() - QPointF(shadow->paddingLeft(), shadow->paddingTop()), + event->oldPosF() - QPointF(shadow->paddingLeft(), shadow->paddingTop()), + event->modifiers()); + QCoreApplication::instance()->sendEvent(decoration(), &e); + } else { + QCoreApplication::instance()->sendEvent(decoration(), event); + } +} + +bool PreviewItem::isDrawingBackground() const +{ + return m_drawBackground; +} + +void PreviewItem::setDrawingBackground(bool set) +{ + if (m_drawBackground == set) { + return; + } + m_drawBackground = set; + emit drawingBackgroundChanged(set); +} + +PreviewBridge *PreviewItem::bridge() const +{ + return m_bridge.data(); +} + +void PreviewItem::setBridge(PreviewBridge *bridge) +{ + if (m_bridge == bridge) { + return; + } + if (m_bridge) { + m_bridge->unregisterPreviewItem(this); + } + m_bridge = bridge; + if (m_bridge) { + m_bridge->registerPreviewItem(this); + } + emit bridgeChanged(); +} + +Settings *PreviewItem::settings() const +{ + return m_settings.data(); +} + +void PreviewItem::setSettings(Settings *settings) +{ + if (m_settings == settings) { + return; + } + m_settings = settings; + emit settingsChanged(); +} + +PreviewClient *PreviewItem::client() +{ + return m_client.data(); +} + +void PreviewItem::syncSize() +{ + if (!m_client) { + return; + } + int widthOffset = 0; + int heightOffset = 0; + auto shadow = m_decoration->shadow(); + if (shadow) { + widthOffset = shadow->paddingLeft() + shadow->paddingRight(); + heightOffset = shadow->paddingTop() + shadow->paddingBottom(); + } + m_client->setWidth(width() - m_decoration->borderLeft() - m_decoration->borderRight() - widthOffset); + m_client->setHeight(height() - m_decoration->borderTop() - m_decoration->borderBottom() - heightOffset); +} + +DecorationShadow *PreviewItem::shadow() const +{ + if (!m_decoration) { + return nullptr; + } + const auto &s = m_decoration->shadow(); + if (!s) { + return nullptr; + } + return s.data(); +} + +} +} diff --git a/kcmkwin/kwindecoration/declarative-plugin/previewitem.h b/kcmkwin/kwindecoration/declarative-plugin/previewitem.h new file mode 100644 index 0000000..8774b83 --- /dev/null +++ b/kcmkwin/kwindecoration/declarative-plugin/previewitem.h @@ -0,0 +1,90 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#ifndef KDECOARTIONS_PREVIEW_ITEM_H +#define KDECOARTIONS_PREVIEW_ITEM_H + +#include +#include + +namespace KDecoration2 +{ +class Decoration; +class DecorationShadow; +class DecorationSettings; + +namespace Preview +{ +class PreviewBridge; +class PreviewClient; +class Settings; + +class PreviewItem : public QQuickPaintedItem +{ + Q_OBJECT + Q_PROPERTY(KDecoration2::Decoration *decoration READ decoration NOTIFY decorationChanged) + Q_PROPERTY(KDecoration2::Preview::PreviewBridge *bridge READ bridge WRITE setBridge NOTIFY bridgeChanged) + Q_PROPERTY(KDecoration2::Preview::Settings *settings READ settings WRITE setSettings NOTIFY settingsChanged) + Q_PROPERTY(KDecoration2::Preview::PreviewClient *client READ client) + Q_PROPERTY(KDecoration2::DecorationShadow *shadow READ shadow NOTIFY shadowChanged) + Q_PROPERTY(QColor windowColor READ windowColor WRITE setWindowColor NOTIFY windowColorChanged) + Q_PROPERTY(bool drawBackground READ isDrawingBackground WRITE setDrawingBackground NOTIFY drawingBackgroundChanged) +public: + PreviewItem(QQuickItem *parent = nullptr); + ~PreviewItem() override; + void paint(QPainter *painter) override; + + KDecoration2::Decoration *decoration() const; + void setDecoration(KDecoration2::Decoration *deco); + + QColor windowColor() const; + void setWindowColor(const QColor &color); + + bool isDrawingBackground() const; + void setDrawingBackground(bool set); + + PreviewBridge *bridge() const; + void setBridge(PreviewBridge *bridge); + + Settings *settings() const; + void setSettings(Settings *settings); + + PreviewClient *client(); + DecorationShadow *shadow() const; + +Q_SIGNALS: + void decorationChanged(KDecoration2::Decoration *deco); + void windowColorChanged(const QColor &color); + void drawingBackgroundChanged(bool); + void bridgeChanged(); + void settingsChanged(); + void shadowChanged(); + +protected: + void mouseDoubleClickEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void hoverEnterEvent(QHoverEvent *event) override; + void hoverLeaveEvent(QHoverEvent *event) override; + void hoverMoveEvent(QHoverEvent *event) override; + void componentComplete() override; + +private: + void paintShadow(QPainter *painter, int &paddingLeft, int &paddingRight, int &paddingTop, int &paddingBottom); + void syncSize(); + void createDecoration(); + Decoration *m_decoration; + QColor m_windowColor; + bool m_drawBackground = true; + QPointer m_bridge; + QPointer m_settings; + QPointer m_client; +}; + +} // Preview +} // KDecoration2 + +#endif diff --git a/kcmkwin/kwindecoration/declarative-plugin/previewsettings.cpp b/kcmkwin/kwindecoration/declarative-plugin/previewsettings.cpp new file mode 100644 index 0000000..cc931aa --- /dev/null +++ b/kcmkwin/kwindecoration/declarative-plugin/previewsettings.cpp @@ -0,0 +1,265 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#include "previewsettings.h" +#include "previewbridge.h" +#include "buttonsmodel.h" + +#include + +#include + +namespace KDecoration2 +{ + +namespace Preview +{ + +BorderSizesModel::BorderSizesModel(QObject *parent) + : QAbstractListModel(parent) +{ +} + +BorderSizesModel::~BorderSizesModel() = default; + +QVariant BorderSizesModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= m_borders.count() || index.column() != 0) { + return QVariant(); + } + if (role != Qt::DisplayRole && role != Qt::UserRole) { + return QVariant(); + } + return QVariant::fromValue(m_borders.at(index.row())); +} + +int BorderSizesModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + return m_borders.count(); +} + +QHash< int, QByteArray > BorderSizesModel::roleNames() const +{ + QHash roles; + roles.insert(Qt::DisplayRole, QByteArrayLiteral("display")); + return roles; +} + +PreviewSettings::PreviewSettings(DecorationSettings *parent) + : QObject() + , DecorationSettingsPrivate(parent) + , m_alphaChannelSupported(true) + , m_onAllDesktopsAvailable(true) + , m_closeOnDoubleClick(false) + , m_leftButtons(new ButtonsModel(QVector({ + DecorationButtonType::Menu, + DecorationButtonType::ApplicationMenu, + DecorationButtonType::OnAllDesktops + }), this)) + , m_rightButtons(new ButtonsModel(QVector({ + DecorationButtonType::ContextHelp, + DecorationButtonType::Minimize, + DecorationButtonType::Maximize, + DecorationButtonType::Close + }), this)) + , m_availableButtons(new ButtonsModel(QVector({ + DecorationButtonType::Menu, + DecorationButtonType::ApplicationMenu, + DecorationButtonType::OnAllDesktops, + DecorationButtonType::Minimize, + DecorationButtonType::Maximize, + DecorationButtonType::Close, + DecorationButtonType::ContextHelp, + DecorationButtonType::Shade, + DecorationButtonType::KeepBelow, + DecorationButtonType::KeepAbove + }), this)) + , m_borderSizes(new BorderSizesModel(this)) + , m_borderSize(int(BorderSize::Normal)) + , m_font(QFontDatabase::systemFont(QFontDatabase::TitleFont)) +{ + connect(this, &PreviewSettings::alphaChannelSupportedChanged, parent, &DecorationSettings::alphaChannelSupportedChanged); + connect(this, &PreviewSettings::onAllDesktopsAvailableChanged, parent, &DecorationSettings::onAllDesktopsAvailableChanged); + connect(this, &PreviewSettings::closeOnDoubleClickOnMenuChanged, parent, &DecorationSettings::closeOnDoubleClickOnMenuChanged); + connect(this, &PreviewSettings::fontChanged, parent, &DecorationSettings::fontChanged); + auto updateLeft = [this, parent]() { + parent->decorationButtonsLeftChanged(decorationButtonsLeft()); + }; + auto updateRight = [this, parent]() { + parent->decorationButtonsRightChanged(decorationButtonsRight()); + }; + connect(m_leftButtons, &QAbstractItemModel::rowsRemoved, this, updateLeft); + connect(m_leftButtons, &QAbstractItemModel::rowsMoved, this, updateLeft); + connect(m_leftButtons, &QAbstractItemModel::rowsInserted, this, updateLeft); + connect(m_rightButtons, &QAbstractItemModel::rowsRemoved, this, updateRight); + connect(m_rightButtons, &QAbstractItemModel::rowsMoved, this, updateRight); + connect(m_rightButtons, &QAbstractItemModel::rowsInserted, this, updateRight); +} + +PreviewSettings::~PreviewSettings() = default; + +QAbstractItemModel *PreviewSettings::availableButtonsModel() const +{ + return m_availableButtons; +} + +QAbstractItemModel *PreviewSettings::leftButtonsModel() const +{ + return m_leftButtons; +} + +QAbstractItemModel *PreviewSettings::rightButtonsModel() const +{ + return m_rightButtons; +} + +bool PreviewSettings::isAlphaChannelSupported() const +{ + return m_alphaChannelSupported; +} + +bool PreviewSettings::isOnAllDesktopsAvailable() const +{ + return m_onAllDesktopsAvailable; +} + +void PreviewSettings::setAlphaChannelSupported(bool supported) +{ + if (m_alphaChannelSupported == supported) { + return; + } + m_alphaChannelSupported = supported; + emit alphaChannelSupportedChanged(m_alphaChannelSupported); +} + +void PreviewSettings::setOnAllDesktopsAvailable(bool available) +{ + if (m_onAllDesktopsAvailable == available) { + return; + } + m_onAllDesktopsAvailable = available; + emit onAllDesktopsAvailableChanged(m_onAllDesktopsAvailable); +} + +void PreviewSettings::setCloseOnDoubleClickOnMenu(bool enabled) +{ + if (m_closeOnDoubleClick == enabled) { + return; + } + m_closeOnDoubleClick = enabled; + emit closeOnDoubleClickOnMenuChanged(enabled); +} + +QVector< DecorationButtonType > PreviewSettings::decorationButtonsLeft() const +{ + return m_leftButtons->buttons(); +} + +QVector< DecorationButtonType > PreviewSettings::decorationButtonsRight() const +{ + return m_rightButtons->buttons(); +} + +void PreviewSettings::addButtonToLeft(int row) +{ + QModelIndex index = m_availableButtons->index(row); + if (!index.isValid()) { + return; + } + m_leftButtons->add(index.data(Qt::UserRole).value()); +} + +void PreviewSettings::addButtonToRight(int row) +{ + QModelIndex index = m_availableButtons->index(row); + if (!index.isValid()) { + return; + } + m_rightButtons->add(index.data(Qt::UserRole).value()); +} + +void PreviewSettings::setBorderSizesIndex(int index) +{ + if (m_borderSize == index) { + return; + } + m_borderSize = index; + emit borderSizesIndexChanged(index); + emit decorationSettings()->borderSizeChanged(borderSize()); +} + +BorderSize PreviewSettings::borderSize() const +{ + return m_borderSizes->index(m_borderSize).data(Qt::UserRole).value(); +} + +void PreviewSettings::setFont(const QFont &font) +{ + if (m_font == font) { + return; + } + m_font = font; + emit fontChanged(m_font); +} + +Settings::Settings(QObject *parent) + : QObject(parent) +{ + connect(this, &Settings::bridgeChanged, this, &Settings::createSettings); +} + +Settings::~Settings() = default; + +void Settings::setBridge(PreviewBridge *bridge) +{ + if (m_bridge == bridge) { + return; + } + m_bridge = bridge; + emit bridgeChanged(); +} + +PreviewBridge *Settings::bridge() const +{ + return m_bridge.data(); +} + +void Settings::createSettings() +{ + if (m_bridge.isNull()) { + m_settings.clear(); + } else { + m_settings = QSharedPointer::create(m_bridge.data()); + m_previewSettings = m_bridge->lastCreatedSettings(); + m_previewSettings->setBorderSizesIndex(m_borderSize); + connect(this, &Settings::borderSizesIndexChanged, m_previewSettings, &PreviewSettings::setBorderSizesIndex); + } + emit settingsChanged(); +} + +QSharedPointer Settings::settings() const +{ + return m_settings; +} + +DecorationSettings *Settings::settingsPointer() const +{ + return m_settings.data(); +} + +void Settings::setBorderSizesIndex(int index) +{ + if (m_borderSize == index) { + return; + } + m_borderSize = index; + emit borderSizesIndexChanged(m_borderSize); +} + +} +} diff --git a/kcmkwin/kwindecoration/declarative-plugin/previewsettings.h b/kcmkwin/kwindecoration/declarative-plugin/previewsettings.h new file mode 100644 index 0000000..301477d --- /dev/null +++ b/kcmkwin/kwindecoration/declarative-plugin/previewsettings.h @@ -0,0 +1,149 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#ifndef KDECOARTIONS_PREVIEW_SETTINGS_H +#define KDECOARTIONS_PREVIEW_SETTINGS_H + +#include +#include +#include +#include + +namespace KDecoration2 +{ + +namespace Preview +{ +class ButtonsModel; +class PreviewBridge; + +class BorderSizesModel : public QAbstractListModel +{ + Q_OBJECT +public: + explicit BorderSizesModel(QObject *parent = nullptr); + ~BorderSizesModel() override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + QHash< int, QByteArray > roleNames() const override; +private: + QList m_borders = QList({ + BorderSize::None, + BorderSize::NoSides, + BorderSize::Tiny, + BorderSize::Normal, + BorderSize::Large, + BorderSize::VeryLarge, + BorderSize::Huge, + BorderSize::VeryHuge, + BorderSize::Oversized + }); +}; + +class PreviewSettings : public QObject, public DecorationSettingsPrivate +{ + Q_OBJECT + Q_PROPERTY(bool onAllDesktopsAvailable READ isOnAllDesktopsAvailable WRITE setOnAllDesktopsAvailable NOTIFY onAllDesktopsAvailableChanged) + Q_PROPERTY(bool alphaChannelSupported READ isAlphaChannelSupported WRITE setAlphaChannelSupported NOTIFY alphaChannelSupportedChanged) + Q_PROPERTY(bool closeOnDoubleClickOnMenu READ isCloseOnDoubleClickOnMenu WRITE setCloseOnDoubleClickOnMenu NOTIFY closeOnDoubleClickOnMenuChanged) + Q_PROPERTY(QAbstractItemModel *leftButtonsModel READ leftButtonsModel CONSTANT) + Q_PROPERTY(QAbstractItemModel *rightButtonsModel READ rightButtonsModel CONSTANT) + Q_PROPERTY(QAbstractItemModel *availableButtonsModel READ availableButtonsModel CONSTANT) + Q_PROPERTY(QAbstractItemModel *borderSizesModel READ borderSizesModel CONSTANT) + Q_PROPERTY(int borderSizesIndex READ borderSizesIndex WRITE setBorderSizesIndex NOTIFY borderSizesIndexChanged) + Q_PROPERTY(QFont font READ font WRITE setFont NOTIFY fontChanged) +public: + explicit PreviewSettings(DecorationSettings *parent); + ~PreviewSettings() override; + bool isAlphaChannelSupported() const override; + bool isOnAllDesktopsAvailable() const override; + bool isCloseOnDoubleClickOnMenu() const override { + return m_closeOnDoubleClick; + } + BorderSize borderSize() const override; + + void setOnAllDesktopsAvailable(bool available); + void setAlphaChannelSupported(bool supported); + void setCloseOnDoubleClickOnMenu(bool enabled); + + QAbstractItemModel *leftButtonsModel() const; + QAbstractItemModel *rightButtonsModel() const; + QAbstractItemModel *availableButtonsModel() const; + QAbstractItemModel *borderSizesModel() const { + return m_borderSizes; + } + + QVector< DecorationButtonType > decorationButtonsLeft() const override; + QVector< DecorationButtonType > decorationButtonsRight() const override; + + Q_INVOKABLE void addButtonToLeft(int row); + Q_INVOKABLE void addButtonToRight(int row); + + int borderSizesIndex() const { + return m_borderSize; + } + void setBorderSizesIndex(int index); + + QFont font() const override { + return m_font; + } + void setFont(const QFont &font); + +Q_SIGNALS: + void onAllDesktopsAvailableChanged(bool); + void alphaChannelSupportedChanged(bool); + void closeOnDoubleClickOnMenuChanged(bool); + void borderSizesIndexChanged(int); + void fontChanged(const QFont &); + +private: + bool m_alphaChannelSupported; + bool m_onAllDesktopsAvailable; + bool m_closeOnDoubleClick; + ButtonsModel *m_leftButtons; + ButtonsModel *m_rightButtons; + ButtonsModel *m_availableButtons; + BorderSizesModel *m_borderSizes; + int m_borderSize; + QFont m_font; +}; + +class Settings : public QObject +{ + Q_OBJECT + Q_PROPERTY(KDecoration2::Preview::PreviewBridge *bridge READ bridge WRITE setBridge NOTIFY bridgeChanged) + Q_PROPERTY(KDecoration2::DecorationSettings *settings READ settingsPointer NOTIFY settingsChanged) + Q_PROPERTY(int borderSizesIndex READ borderSizesIndex WRITE setBorderSizesIndex NOTIFY borderSizesIndexChanged) +public: + explicit Settings(QObject *parent = nullptr); + ~Settings() override; + + PreviewBridge *bridge() const; + void setBridge(PreviewBridge *bridge); + + QSharedPointer settings() const; + DecorationSettings *settingsPointer() const; + int borderSizesIndex() const { + return m_borderSize; + } + void setBorderSizesIndex(int index); + +Q_SIGNALS: + void bridgeChanged(); + void settingsChanged(); + void borderSizesIndexChanged(int); + +private: + void createSettings(); + QPointer m_bridge; + QSharedPointer m_settings; + PreviewSettings *m_previewSettings = nullptr; + int m_borderSize = 3; +}; + +} +} + +#endif diff --git a/kcmkwin/kwindecoration/declarative-plugin/qmldir b/kcmkwin/kwindecoration/declarative-plugin/qmldir new file mode 100644 index 0000000..4277022 --- /dev/null +++ b/kcmkwin/kwindecoration/declarative-plugin/qmldir @@ -0,0 +1,2 @@ +module org.kde.kwin.private.kdecoration +plugin kdecorationprivatedeclarative diff --git a/kcmkwin/kwindecoration/decorationmodel.cpp b/kcmkwin/kwindecoration/decorationmodel.cpp new file mode 100644 index 0000000..783dbeb --- /dev/null +++ b/kcmkwin/kwindecoration/decorationmodel.cpp @@ -0,0 +1,192 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#include "decorationmodel.h" +// KDecoration2 +#include +#include +// KDE +#include +#include +#include +// Qt +#include + +namespace KDecoration2 +{ + +namespace Configuration +{ +static const QString s_pluginName = QStringLiteral("org.kde.kdecoration2"); + +DecorationsModel::DecorationsModel(QObject *parent) + : QAbstractListModel(parent) +{ +} + +DecorationsModel::~DecorationsModel() = default; + +int DecorationsModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + return m_plugins.size(); +} + +QVariant DecorationsModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.column() != 0 || index.row() < 0 || index.row() >= int(m_plugins.size())) { + return QVariant(); + } + const Data &d = m_plugins.at(index.row()); + switch (role) { + case Qt::DisplayRole: + return d.visibleName; + case PluginNameRole: + return d.pluginName; + case ThemeNameRole: + return d.themeName; + case ConfigurationRole: + return d.configuration; + case RecommendedBorderSizeRole: + return Utils::borderSizeToString(d.recommendedBorderSize); + } + return QVariant(); +} + +QHash< int, QByteArray > DecorationsModel::roleNames() const +{ + QHash roles({ + {Qt::DisplayRole, QByteArrayLiteral("display")}, + {PluginNameRole, QByteArrayLiteral("plugin")}, + {ThemeNameRole, QByteArrayLiteral("theme")}, + {ConfigurationRole, QByteArrayLiteral("configureable")}, + {RecommendedBorderSizeRole, QByteArrayLiteral("recommendedbordersize")} + }); + return roles; +} + +static bool isThemeEngine(const QVariantMap &decoSettingsMap) +{ + auto it = decoSettingsMap.find(QStringLiteral("themes")); + if (it == decoSettingsMap.end()) { + return false; + } + return it.value().toBool(); +} + +static bool isConfigureable(const QVariantMap &decoSettingsMap) +{ + auto it = decoSettingsMap.find(QStringLiteral("kcmodule")); + if (it == decoSettingsMap.end()) { + return false; + } + return it.value().toBool(); +} + +static KDecoration2::BorderSize recommendedBorderSize(const QVariantMap &decoSettingsMap) +{ + auto it = decoSettingsMap.find(QStringLiteral("recommendedBorderSize")); + if (it == decoSettingsMap.end()) { + return KDecoration2::BorderSize::Normal; + } + return Utils::stringToBorderSize(it.value().toString()); +} + +static QString themeListKeyword(const QVariantMap &decoSettingsMap) +{ + auto it = decoSettingsMap.find(QStringLiteral("themeListKeyword")); + if (it == decoSettingsMap.end()) { + return QString(); + } + return it.value().toString(); +} + +static QString findKNewStuff(const QVariantMap &decoSettingsMap) +{ + auto it = decoSettingsMap.find(QStringLiteral("KNewStuff")); + if (it == decoSettingsMap.end()) { + return QString(); + } + return it.value().toString(); +} + +void DecorationsModel::init() +{ + beginResetModel(); + m_plugins.clear(); + const auto plugins = KPluginTrader::self()->query(s_pluginName, s_pluginName); + for (const auto &info : plugins) { + KPluginLoader loader(info.libraryPath()); + KPluginFactory *factory = loader.factory(); + if (!factory) { + continue; + } + auto metadata = loader.metaData().value(QStringLiteral("MetaData")).toObject().value(s_pluginName); + Data data; + if (!metadata.isUndefined()) { + const auto decoSettingsMap = metadata.toObject().toVariantMap(); + const QString &kns = findKNewStuff(decoSettingsMap); + if (!kns.isEmpty() && !m_knsProviders.contains(kns)) { + m_knsProviders.append(kns); + } + if (isThemeEngine(decoSettingsMap)) { + const QString keyword = themeListKeyword(decoSettingsMap); + if (keyword.isNull()) { + // We cannot list the themes + continue; + } + QScopedPointer themeFinder(factory->create(keyword)); + if (themeFinder.isNull()) { + continue; + } + QVariant themes = themeFinder->property("themes"); + if (!themes.isValid()) { + continue; + } + const auto themesMap = themes.toMap(); + for (auto it = themesMap.begin(); it != themesMap.end(); ++it) { + Data d; + d.pluginName = info.pluginName(); + d.themeName = it.value().toString(); + d.visibleName = it.key(); + QMetaObject::invokeMethod(themeFinder.data(), "hasConfiguration", + Q_RETURN_ARG(bool, d.configuration), + Q_ARG(QString, d.themeName)); + m_plugins.emplace_back(std::move(d)); + } + + // it's a theme engine, we don't want to show this entry + continue; + } + data.configuration = isConfigureable(decoSettingsMap); + data.recommendedBorderSize = recommendedBorderSize(decoSettingsMap); + } + data.pluginName = info.pluginName(); + data.visibleName = info.name().isEmpty() ? info.pluginName() : info.name(); + data.themeName = data.visibleName; + + m_plugins.emplace_back(std::move(data)); + } + endResetModel(); +} + +QModelIndex DecorationsModel::findDecoration(const QString &pluginName, const QString &themeName) const +{ + auto it = std::find_if(m_plugins.cbegin(), m_plugins.cend(), + [pluginName, themeName](const Data &d) { + return d.pluginName == pluginName && d.themeName == themeName; + } + ); + if (it == m_plugins.cend()) { + return QModelIndex(); + } + const auto distance = std::distance(m_plugins.cbegin(), it); + return createIndex(distance, 0); +} + +} +} diff --git a/kcmkwin/kwindecoration/decorationmodel.h b/kcmkwin/kwindecoration/decorationmodel.h new file mode 100644 index 0000000..31843be --- /dev/null +++ b/kcmkwin/kwindecoration/decorationmodel.h @@ -0,0 +1,62 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#ifndef KDECORATION_DECORATION_MODEL_H +#define KDECORATION_DECORATION_MODEL_H + +#include "utils.h" + +#include + +namespace KDecoration2 +{ + +namespace Configuration +{ + +class DecorationsModel : public QAbstractListModel +{ + Q_OBJECT +public: + enum DecorationRole { + PluginNameRole = Qt::UserRole + 1, + ThemeNameRole, + ConfigurationRole, + RecommendedBorderSizeRole, + }; + +public: + explicit DecorationsModel(QObject *parent = nullptr); + ~DecorationsModel() override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QHash< int, QByteArray > roleNames() const override; + + QModelIndex findDecoration(const QString &pluginName, const QString &themeName = QString()) const; + + QStringList knsProviders() const { + return m_knsProviders; + } + +public Q_SLOTS: + void init(); + +private: + struct Data { + QString pluginName; + QString themeName; + QString visibleName; + bool configuration = false; + KDecoration2::BorderSize recommendedBorderSize = KDecoration2::BorderSize::Normal; + }; + std::vector m_plugins; + QStringList m_knsProviders; +}; + +} +} + +#endif diff --git a/kcmkwin/kwindecoration/kcm.cpp b/kcmkwin/kwindecoration/kcm.cpp new file mode 100644 index 0000000..9508792 --- /dev/null +++ b/kcmkwin/kwindecoration/kcm.cpp @@ -0,0 +1,260 @@ +/* + SPDX-FileCopyrightText: 2019 Valerio Pilo + SPDX-FileCopyrightText: 2019 Cyril Rossi + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "kcm.h" +#include "decorationmodel.h" +#include "declarative-plugin/buttonsmodel.h" +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include "kwindecorationsettings.h" + +K_PLUGIN_FACTORY_WITH_JSON(KCMKWinDecorationFactory, "kwindecoration.json", registerPlugin();) + +Q_DECLARE_METATYPE(KDecoration2::BorderSize) + + +namespace +{ +const KDecoration2::BorderSize s_defaultRecommendedBorderSize = KDecoration2::BorderSize::Normal; +} + +KCMKWinDecoration::KCMKWinDecoration(QObject *parent, const QVariantList &arguments) + : KQuickAddons::ManagedConfigModule(parent, arguments) + , m_themesModel(new KDecoration2::Configuration::DecorationsModel(this)) + , m_proxyThemesModel(new QSortFilterProxyModel(this)) + , m_leftButtonsModel(new KDecoration2::Preview::ButtonsModel(DecorationButtonsList(), this)) + , m_rightButtonsModel(new KDecoration2::Preview::ButtonsModel(DecorationButtonsList(), this)) + , m_availableButtonsModel(new KDecoration2::Preview::ButtonsModel(this)) + , m_settings(new KWinDecorationSettings(this)) +{ + auto about = new KAboutData(QStringLiteral("kcm_kwindecoration"), + i18n("Window Decorations"), + QStringLiteral("1.0"), + QString(), + KAboutLicense::GPL); + about->addAuthor(i18n("Valerio Pilo"), + i18n("Author"), + QStringLiteral("vpilo@coldshock.net")); + setAboutData(about); + setButtons(Apply | Default | Help); + qmlRegisterAnonymousType("org.kde.kwin.KWinDecoration", 1); + qmlRegisterAnonymousType("org.kde.kwin.KWinDecoration", 1); + qmlRegisterAnonymousType("org.kde.kwin.KWinDecoration", 1); + m_proxyThemesModel->setSourceModel(m_themesModel); + m_proxyThemesModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + m_proxyThemesModel->setSortCaseSensitivity(Qt::CaseInsensitive); + m_proxyThemesModel->sort(0); + + connect(m_settings, &KWinDecorationSettings::themeChanged, this, &KCMKWinDecoration::themeChanged); + connect(m_settings, &KWinDecorationSettings::borderSizeChanged, this, &KCMKWinDecoration::borderSizeChanged); + + connect(m_leftButtonsModel, &QAbstractItemModel::rowsInserted, this, &KCMKWinDecoration::onLeftButtonsChanged); + connect(m_leftButtonsModel, &QAbstractItemModel::rowsMoved, this, &KCMKWinDecoration::onLeftButtonsChanged); + connect(m_leftButtonsModel, &QAbstractItemModel::rowsRemoved, this, &KCMKWinDecoration::onLeftButtonsChanged); + connect(m_leftButtonsModel, &QAbstractItemModel::modelReset, this, &KCMKWinDecoration::onLeftButtonsChanged); + + connect(m_rightButtonsModel, &QAbstractItemModel::rowsInserted, this, &KCMKWinDecoration::onRightButtonsChanged); + connect(m_rightButtonsModel, &QAbstractItemModel::rowsMoved, this, &KCMKWinDecoration::onRightButtonsChanged); + connect(m_rightButtonsModel, &QAbstractItemModel::rowsRemoved, this, &KCMKWinDecoration::onRightButtonsChanged); + connect(m_rightButtonsModel, &QAbstractItemModel::modelReset, this, &KCMKWinDecoration::onRightButtonsChanged); + + connect(this, &KCMKWinDecoration::borderSizeChanged, this, &KCMKWinDecoration::settingsChanged); + + // Update the themes when the color scheme or a theme's settings change + QDBusConnection::sessionBus() + .connect(QString(), QStringLiteral("/KWin"), QStringLiteral("org.kde.KWin"), QStringLiteral("reloadConfig"), + this, SLOT(reloadKWinSettings())); + + QMetaObject::invokeMethod(m_themesModel, "init", Qt::QueuedConnection); +} + +KWinDecorationSettings *KCMKWinDecoration::settings() const +{ + return m_settings; +} + +void KCMKWinDecoration::reloadKWinSettings() +{ + QMetaObject::invokeMethod(m_themesModel, "init", Qt::QueuedConnection); +} + +void KCMKWinDecoration::getNewStuff(QQuickItem *context) +{ + if (!m_newStuffDialog) { + m_newStuffDialog = new KNS3::DownloadDialog(QStringLiteral("window-decorations.knsrc")); + m_newStuffDialog->setWindowTitle(i18n("Download New Window Decorations")); + m_newStuffDialog->setWindowModality(Qt::WindowModal); + connect(m_newStuffDialog, &KNS3::DownloadDialog::accepted, this, &KCMKWinDecoration::load); + } + + if (context && context->window()) { + m_newStuffDialog->winId(); // so it creates the windowHandle() + m_newStuffDialog->windowHandle()->setTransientParent(context->window()); + } + + connect(m_newStuffDialog, &QDialog::finished, this, &KCMKWinDecoration::reloadKWinSettings); + + m_newStuffDialog->show(); +} + +void KCMKWinDecoration::load() +{ + ManagedConfigModule::load(); + + m_leftButtonsModel->replace(Utils::buttonsFromString(m_settings->buttonsOnLeft())); + m_rightButtonsModel->replace(Utils::buttonsFromString(m_settings->buttonsOnRight())); + + setBorderSize(borderSizeIndexFromString(m_settings->borderSize())); + + emit themeChanged(); +} + +void KCMKWinDecoration::save() +{ + if (!m_settings->borderSizeAuto()) { + m_settings->setBorderSize(borderSizeIndexToString(m_borderSizeIndex)); + } else { + m_settings->setBorderSize(m_settings->defaultBorderSizeValue()); + } + + ManagedConfigModule::save(); + + // Send a signal to all kwin instances + QDBusMessage message = QDBusMessage::createSignal(QStringLiteral("/KWin"), + QStringLiteral("org.kde.KWin"), + QStringLiteral("reloadConfig")); + QDBusConnection::sessionBus().send(message); +} + +void KCMKWinDecoration::defaults() +{ + ManagedConfigModule::defaults(); + + setBorderSize(recommendedBorderSize()); + + m_leftButtonsModel->replace(Utils::buttonsFromString(m_settings->buttonsOnLeft())); + m_rightButtonsModel->replace(Utils::buttonsFromString(m_settings->buttonsOnRight())); +} + +void KCMKWinDecoration::onLeftButtonsChanged() +{ + m_settings->setButtonsOnLeft(Utils::buttonsToString(m_leftButtonsModel->buttons())); +} + +void KCMKWinDecoration::onRightButtonsChanged() +{ + m_settings->setButtonsOnRight(Utils::buttonsToString(m_rightButtonsModel->buttons())); +} + +QSortFilterProxyModel *KCMKWinDecoration::themesModel() const +{ + return m_proxyThemesModel; +} + +QAbstractListModel *KCMKWinDecoration::leftButtonsModel() +{ + return m_leftButtonsModel; +} + +QAbstractListModel *KCMKWinDecoration::rightButtonsModel() +{ + return m_rightButtonsModel; +} + +QAbstractListModel *KCMKWinDecoration::availableButtonsModel() const +{ + return m_availableButtonsModel; +} + +QStringList KCMKWinDecoration::borderSizesModel() const +{ + return Utils::getBorderSizeNames().values(); +} + +int KCMKWinDecoration::borderSize() const +{ + return m_borderSizeIndex; +} + +int KCMKWinDecoration::recommendedBorderSize() const +{ + typedef KDecoration2::Configuration::DecorationsModel::DecorationRole DecoRole; + const QModelIndex proxyIndex = m_proxyThemesModel->index(theme(), 0); + if (proxyIndex.isValid()) { + const QModelIndex index = m_proxyThemesModel->mapToSource(proxyIndex); + if (index.isValid()) { + QVariant ret = m_themesModel->data(index, DecoRole::RecommendedBorderSizeRole); + return Utils::getBorderSizeNames().keys().indexOf(Utils::stringToBorderSize(ret.toString())); + } + } + return Utils::getBorderSizeNames().keys().indexOf(s_defaultRecommendedBorderSize); +} + +int KCMKWinDecoration::theme() const +{ + return m_proxyThemesModel->mapFromSource(m_themesModel->findDecoration(m_settings->pluginName(), m_settings->theme())).row(); +} + +void KCMKWinDecoration::setBorderSize(int index) +{ + if (m_borderSizeIndex != index) { + m_borderSizeIndex = index; + emit borderSizeChanged(); + } +} + +void KCMKWinDecoration::setBorderSize(KDecoration2::BorderSize size) +{ + m_settings->setBorderSize(Utils::borderSizeToString(size)); +} + +void KCMKWinDecoration::setTheme(int index) +{ + QModelIndex dataIndex = m_proxyThemesModel->index(index, 0); + if (dataIndex.isValid()) { + m_settings->setTheme(m_proxyThemesModel->data(dataIndex, KDecoration2::Configuration::DecorationsModel::ThemeNameRole).toString()); + m_settings->setPluginName(m_proxyThemesModel->data(dataIndex, KDecoration2::Configuration::DecorationsModel::PluginNameRole).toString()); + emit themeChanged(); + } +} + +bool KCMKWinDecoration::isSaveNeeded() const +{ + return !m_settings->borderSizeAuto() && borderSizeIndexFromString(m_settings->borderSize()) != m_borderSizeIndex; +} + +bool KCMKWinDecoration::isDefaults() const +{ + return m_settings->borderSizeAuto() && recommendedBorderSize() == m_borderSizeIndex; +} + +int KCMKWinDecoration::borderSizeIndexFromString(const QString &size) const +{ + return Utils::getBorderSizeNames().keys().indexOf(Utils::stringToBorderSize(size)); +} + +QString KCMKWinDecoration::borderSizeIndexToString(int index) const +{ + return Utils::borderSizeToString(Utils::getBorderSizeNames().keys().at(index)); +} + +#include "kcm.moc" diff --git a/kcmkwin/kwindecoration/kcm.h b/kcmkwin/kwindecoration/kcm.h new file mode 100644 index 0000000..6249811 --- /dev/null +++ b/kcmkwin/kwindecoration/kcm.h @@ -0,0 +1,105 @@ +/* + SPDX-FileCopyrightText: 2019 Valerio Pilo + SPDX-FileCopyrightText: 2019 Cyril Rossi + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include "utils.h" + +#include + + +class QAbstractItemModel; +class QSortFilterProxyModel; +class QQuickItem; + +namespace KNS3 +{ +class DownloadDialog; +} + +namespace KDecoration2 +{ +enum class BorderSize; + +namespace Preview +{ +class ButtonsModel; +} +namespace Configuration +{ +class DecorationsModel; +} +} + +class KWinDecorationSettings; + +class KCMKWinDecoration : public KQuickAddons::ManagedConfigModule +{ + Q_OBJECT + Q_PROPERTY(KWinDecorationSettings *settings READ settings CONSTANT) + Q_PROPERTY(QSortFilterProxyModel *themesModel READ themesModel CONSTANT) + Q_PROPERTY(QStringList borderSizesModel READ borderSizesModel CONSTANT) + Q_PROPERTY(int borderSize READ borderSize WRITE setBorderSize NOTIFY borderSizeChanged) + Q_PROPERTY(int recommendedBorderSize READ recommendedBorderSize CONSTANT) + Q_PROPERTY(int theme READ theme WRITE setTheme NOTIFY themeChanged) + Q_PROPERTY(QAbstractListModel *leftButtonsModel READ leftButtonsModel NOTIFY buttonsChanged) + Q_PROPERTY(QAbstractListModel *rightButtonsModel READ rightButtonsModel NOTIFY buttonsChanged) + Q_PROPERTY(QAbstractListModel *availableButtonsModel READ availableButtonsModel CONSTANT) + +public: + KCMKWinDecoration(QObject *parent, const QVariantList &arguments); + + KWinDecorationSettings *settings() const; + QSortFilterProxyModel *themesModel() const; + QAbstractListModel *leftButtonsModel(); + QAbstractListModel *rightButtonsModel(); + QAbstractListModel *availableButtonsModel() const; + QStringList borderSizesModel() const; + int borderSize() const; + int recommendedBorderSize() const; + int theme() const; + + void setBorderSize(int index); + void setBorderSize(KDecoration2::BorderSize size); + void setTheme(int index); + + Q_INVOKABLE void getNewStuff(QQuickItem *context); + +Q_SIGNALS: + void themeChanged(); + void buttonsChanged(); + void borderSizeChanged(); + +public Q_SLOTS: + void load() override; + void save() override; + void defaults() override; + +private Q_SLOTS: + void onLeftButtonsChanged(); + void onRightButtonsChanged(); + void reloadKWinSettings(); + +private: + bool isSaveNeeded() const override; + bool isDefaults() const override; + + int borderSizeIndexFromString(const QString &size) const; + QString borderSizeIndexToString(int index) const; + + KDecoration2::Configuration::DecorationsModel *m_themesModel; + QSortFilterProxyModel *m_proxyThemesModel; + + KDecoration2::Preview::ButtonsModel *m_leftButtonsModel; + KDecoration2::Preview::ButtonsModel *m_rightButtonsModel; + KDecoration2::Preview::ButtonsModel *m_availableButtonsModel; + + QPointer m_newStuffDialog; + + int m_borderSizeIndex = -1; + KWinDecorationSettings *m_settings; +}; diff --git a/kcmkwin/kwindecoration/kwindecoration.desktop b/kcmkwin/kwindecoration/kwindecoration.desktop new file mode 100644 index 0000000..c8cdf21 --- /dev/null +++ b/kcmkwin/kwindecoration/kwindecoration.desktop @@ -0,0 +1,156 @@ +[Desktop Entry] +Icon=preferences-desktop-theme-windowdecorations +Exec=kcmshell5 kwindecoration + +Categories=Qt;KDE;X-KDE-settings-looknfeel; +Type=Service +X-KDE-ServiceTypes=KCModule +X-KDE-Library=kcm_kwindecoration +X-KDE-ParentApp=kcontrol +X-KDE-System-Settings-Parent-Category=applicationstyle +X-KDE-Weight=40 +X-DocPath=kcontrol/kwindecoration/index.html + +X-KDE-FormFactors=desktop,tablet + +Name=Window Decorations +Name[ar]=زخارف النوافذ +Name[az]=Pəncərə dekorasiyası +Name[bg]=Декорации на прозорците +Name[bs]=Dekoracije prozora +Name[ca]=Decoració de les finestres +Name[ca@valencia]=Decoració de les finestres +Name[cs]=Dekorace oken +Name[da]=Vinduesdekorationer +Name[de]=Fensterdekoration +Name[el]=Διακοσμήσεις παραθύρου +Name[en_GB]=Window Decorations +Name[es]=Decoraciones de las ventanas +Name[et]=Akna dekoratsioonid +Name[eu]=Leiho-apaindurak +Name[fi]=Ikkunakehykset +Name[fr]=Décorations de fenêtres +Name[ga]=Maisiúcháin Fhuinneog +Name[gl]=Decoración da xanela +Name[he]=מסגרת חלון +Name[hi]=विंडो सजावट +Name[hr]=Ukrasi prozora +Name[hu]=Ablakdekorációk +Name[ia]=Decorationes de fenestra +Name[id]=Dekorasi Window +Name[is]=Gluggaskreytingar +Name[it]=Decorazioni delle finestre +Name[ja]=ウィンドウの飾り +Name[kk]=Терезенің безендірулері +Name[km]=ការ​តុបតែង​បង្អួច +Name[kn]=ವಿಂಡೋ ಅಲಂಕಾರಗಳು +Name[ko]=창 장식 +Name[lt]=Langų dekoracijos +Name[lv]=Logu dekorācijas +Name[mr]=चौकट सजावट +Name[nb]=Vinduspynt +Name[nds]=Finstern opfladusen +Name[nl]=Vensterdecoraties +Name[nn]=Vindaugspynt +Name[pa]=ਵਿੰਡੋ ਸਜਾਵਟ +Name[pl]=Wygląd okien +Name[pt]=Decorações das Janelas +Name[pt_BR]=Decorações da janela +Name[ro]=Decorații fereastră +Name[ru]=Оформление окон +Name[si]=කවුළු සැරසිලි +Name[sk]=Dekorácie okien +Name[sl]=Okraski oken +Name[sr]=Декорације прозора +Name[sr@ijekavian]=Декорације прозора +Name[sr@ijekavianlatin]=Dekoracije prozora +Name[sr@latin]=Dekoracije prozora +Name[sv]=Fönsterdekorationer +Name[th]=ส่วนตกแต่งหน้าต่าง +Name[tr]=Pencere Dekorasyonları +Name[ug]=كۆزنەك بېزەكلىرى +Name[uk]=Обрамлення вікон +Name[wa]=Gåyotaedjes des fniesses +Name[x-test]=xxWindow Decorationsxx +Name[zh_CN]=窗口装饰 +Name[zh_TW]=視窗裝飾 + +Comment=Configure window titlebars and borders +Comment[az]=Pəncərə başlığı və çərçivəsini tənzimləmək +Comment[ca]=Configura la barra de títol i les vores de les finestres +Comment[ca@valencia]=Configura la barra de títol i les vores de les finestres +Comment[da]=Indstil vinduets titellinjer og kanter +Comment[de]=Titelleiste und Ränder von Fenstern einrichten +Comment[en_GB]=Configure window titlebars and borders +Comment[es]=Configurar barra de título y bordes de las ventanas +Comment[et]=Akende tiitliriba ja raami seadistamine +Comment[eu]=Konfiguratu leihoen titulu-barrak eta ertzak +Comment[fi]=Ikkunan otsikkopalkkien ja reunojen asetukset +Comment[fr]=Configure les barres de titre et les bordures de la fenêtre +Comment[gl]=Configurar as barras de título e os bordos das xanelas +Comment[ia]=Configura barras de titulo e margines +Comment[id]=Konfigurasikan bingkai dan bilah-judul window +Comment[it]=Configura la barra del titolo e i bordi delle finestre +Comment[ko]=창 제목 표시줄과 경계선 설정 +Comment[lt]=Konfigūruoti langų antraštės juostas ir rėmelius +Comment[nl]=Titelbalken en randen van venster configureren +Comment[nn]=Set opp tittellinjer og vindaugsrammer +Comment[pl]=Ustawienia pasków tytułów i obramowań okien +Comment[pt]=Configurar as barras de título e contornos das janelas +Comment[pt_BR]=Configure as barras de títulos e bordas da janela +Comment[ro]=Configurează barele de titlu și contururile ferestrelor +Comment[ru]=Настройка заголовка и границ окон +Comment[sk]=Nastaviť záhlavia a okraje okna +Comment[sl]=Nastavi naslovne vrstice in robove oken +Comment[sv]=Anpassa namnlister och kanter för fönster +Comment[uk]=Налаштовування смужок заголовків та рамок вікон +Comment[x-test]=xxConfigure window titlebars and bordersxx +Comment[zh_CN]=配置窗口标题栏和边框 +Comment[zh_TW]=設定視窗的標題列和邊框 + +X-KDE-Keywords=kwin,window,manager,border,style,theme,look,feel,layout,button,handle,edge,kwm,decoration +X-KDE-Keywords[az]=kwin, pəncərə,menecer,çərçivə,üslub,mövzu,görünüş,qat,düymə,kənar,kwm, dekorasiya +X-KDE-Keywords[bs]=kwin,prozor,upravitelj,granica,stil,tema,izgled,osjećati,izgled,dugme,držati,ivica,kwm,dekoracija +X-KDE-Keywords[ca]=kwin,finestra,gestor,vora,estil,tema,aspecte,comportament,disposició,botó,gestió,vora,kwm,decoració +X-KDE-Keywords[ca@valencia]=kwin,finestra,gestor,vora,estil,tema,aspecte,comportament,disposició,botó,gestió,vora,kwm,decoració +X-KDE-Keywords[cs]=kwin,okna,správce,okraj,styl,motiv,vzhled,pocit,rozvržení,tlačítko,madlo,okraj,kwm,dekorace +X-KDE-Keywords[da]=kwin,vindueshåndtering,window,manager,kant,stil,tema,udseende,layout,knap,håndtag,kant,kwm,dekoration +X-KDE-Keywords[de]=KWin,Kwm,Fenster,Manager,Rahmen,Design,Stile,Themes,Optik,Erscheinungsbild,Layout,Knöpfe,Ränder,Dekorationen +X-KDE-Keywords[el]=kwin,παράθυρο,διαχειριστής,περίγραμμα,στιλ,θέμα,εμφάνιση,αίσθηση,διάταξη,κουμπί,χειρισμός,άκρη,kwm,διακόσμηση +X-KDE-Keywords[en_GB]=kwin,window,manager,border,style,theme,look,feel,layout,button,handle,edge,kwm,decoration +X-KDE-Keywords[es]=kwin,ventana,gestor,borde,estilo,tema,aspecto,sensación,disposición,botón,asa,borde,kwm,decoración +X-KDE-Keywords[et]=kwin,aken,haldur,piire,stiil,teema,välimus,paigutus,nupp,pide,serv,kwm,dekoratsioon +X-KDE-Keywords[eu]=kwin,leiho,kudeatzaile,ertz,estilo,gai, itxura,izaera,diseinu,botoi,helduleku,kwm,dekorazio,apaindura,apainketa +X-KDE-Keywords[fi]=kwin,ikkuna,hallinta,ikkunointiohjelma,kehys,reunus,tyyli,teema,ulkoasu,toiminta,asettelu,painike,kahva,kulma,reuna,kwm,koriste +X-KDE-Keywords[fr]=kwin, fenêtre, gestionnaire, composition, bordure, style, thème, apparence, comportement, disposition, bouton, prise en main, bord, kwm, décoration +X-KDE-Keywords[ga]=kwin,fuinneog,bainisteoir,imlíne,stíl,téama,cuma,brath,leagan amach,cnaipe,hanla,ciumhais,kwm,maisiúchán +X-KDE-Keywords[gl]=kwin,xanela,xestor,estilo,tema,aparencia,comportamento,aspecto,disposición, botón,asa,bordo,kwm,decoración +X-KDE-Keywords[hu]=kwin,ablak,kezelő,szegély,stílus,téma,kinézet,megjelenés,elrendezés,gomb,kezel,szél,kwm,dekoráció +X-KDE-Keywords[ia]=kwin,fenestra,gerente,margine,stilo,thema,aspecto,sentir,disposition,button,maneator,bordo,kwm,decoration +X-KDE-Keywords[id]=kwin,window,pengelola,batas,gaya,tema,tampilan,rasa,tata letak,tombol,pegangan,bingkai,kwm,dekorasi +X-KDE-Keywords[it]=kwin,gestore,finestre,bordo,stile,tema,aspetto,disposizione,pulsante,gestore,kwm,decorazione +X-KDE-Keywords[kk]=kwin,window,manager,border,style,theme,look,feel,layout,button,handle,edge,kwm,decoration +X-KDE-Keywords[km]=kwin,window,manager,border,style,theme,look,feel,layout,button,handle,edge,kwm,decoration +X-KDE-Keywords[ko]=kwin,window,manager,border,style,theme,look,feel,layout,button,handle,edge,kwm,decoration,창,관리자,테두리,스타일,테마,단추,핸들,경계 +X-KDE-Keywords[lt]=kwin,langai,langas,langų,tvarkytuvė,tvarkytuve,rėmelis,remelis,stilius,apipavidalinimas,tema,išvaizda,isvaizda,turinys,išdėstymas,isdestymas,mygtukai,mygtukas,rankena,rankenėlė,rankenele,kraštas,krastas,kraštai,krastai,kwm,dekoracija,dekoracijos +X-KDE-Keywords[nb]=kwin,vindu,behandler,ramme,stil,tema,lås,utforming,knapp,håndtak,kant,kwm +X-KDE-Keywords[nds]=KWin,Finster,Pleger,Rahmen,Stil,Muster,Utsehn,Bedenen,Knoop,Greep,Kant,kwm,Dekoratschoon +X-KDE-Keywords[nl]=kwin,venster,beheerder,grens,stijl,thema,look,feel,indeling,knop,handel,rand,kwm,decoratie +X-KDE-Keywords[nn]=kwin,vindauge,handsamar,ramme,kantlinje,stil,tema,lås,utforming,knapp,handtak,kant,kwm,dekorasjon,pynt +X-KDE-Keywords[pl]=kwin,okno,menadżer,obramowanie,styl,motyw,wygląd,odczucie,układ,przycisk, uchwyt,krawędź,kwm,dekoracja +X-KDE-Keywords[pt]=kwin,gestor,janela,contorno,estilo,tema,aparência,comportamento,disposição,botão,pega,extremo,kwm,decoração +X-KDE-Keywords[pt_BR]=kwin,gerenciador,janela,borda,estilo,tema,aparência,comportamento,layout,botão,canto,extremo,kwm,decoração +X-KDE-Keywords[ro]=kwin,fereastră,gestionar,contur,stil,tematică,aspect,comportament,aranjament,mâner,muchie,margine,kwm,decorație +X-KDE-Keywords[ru]=kwin,window,manager,border,style,theme,look,feel,layout,button,handle,edge,kwm,decoration,окно,диспетчер,граница,стиль,тема,внешний вид,оформление,разметка,шаблон,кнопка,управление,край +X-KDE-Keywords[sk]=kwin,okno,správca,rám,štýl,téma,vzhľad,cítenie,rozloženie,tlačidlo,spracovanie,okraj,kwm,dekorácia +X-KDE-Keywords[sl]=kwin,okna,okenski upravljalnik,upravljalnik oken,rob,obroba,slog,tema,videz,obnašanje,občutek,razpored,gumbi,ročica,okraski,kwm +X-KDE-Keywords[sr]=kwin,window,manager,border,style,theme,look,feel,layout,button,handle,edge,kwm,decoration,К‑вин,прозор,менаџер,ивица,стила,тема,изглед,осећај,распоред,дугме,ручка,КВМ,декорација +X-KDE-Keywords[sr@ijekavian]=kwin,window,manager,border,style,theme,look,feel,layout,button,handle,edge,kwm,decoration,К‑вин,прозор,менаџер,ивица,стила,тема,изглед,осећај,распоред,дугме,ручка,КВМ,декорација +X-KDE-Keywords[sr@ijekavianlatin]=kwin,window,manager,border,style,theme,look,feel,layout,button,handle,edge,kwm,decoration,KWin,prozor,menadžer,ivica,stila,tema,izgled,osećaj,raspored,dugme,ručka,KWM,dekoracija +X-KDE-Keywords[sr@latin]=kwin,window,manager,border,style,theme,look,feel,layout,button,handle,edge,kwm,decoration,KWin,prozor,menadžer,ivica,stila,tema,izgled,osećaj,raspored,dugme,ručka,KWM,dekoracija +X-KDE-Keywords[sv]=kwin,fönster,hantering,kant,stil,tema,utseende,känsla,layout,knapp,grepp,kant,kwm,dekoration +X-KDE-Keywords[tr]=kwin,pencere,yönetici,kenarlık,biçim,tema,görünüm,şekil,düzen,düğme,kullanım,kenar,kwm,dekorasyon +X-KDE-Keywords[uk]=kwin,window,manager,border,style,theme,look,feel,layout,button,handle,edge,kwm,decoration,вікно,вікна,керування,менеджер,рамка,межа,стиль,тема,вигляд,поведінка,компонування,кнопка,елемент,край,декорації,обрамлення +X-KDE-Keywords[x-test]=xxkwinxx,xxwindowxx,xxmanagerxx,xxborderxx,xxstylexx,xxthemexx,xxlookxx,xxfeelxx,xxlayoutxx,xxbuttonxx,xxhandlexx,xxedgexx,xxkwmxx,xxdecorationxx +X-KDE-Keywords[zh_CN]=kwin,window,manager,border,style,theme,look,feel,layout,button,handle,edge,kwm,decoration,窗口,管理,边框,样式,主题,外怪,布局,按钮,边界,装饰 +X-KDE-Keywords[zh_TW]=kwin,window,manager,border,style,theme,look,feel,layout,button,handle,edge,kwm,decoration diff --git a/kcmkwin/kwindecoration/kwindecorationsettings.kcfg b/kcmkwin/kwindecoration/kwindecorationsettings.kcfg new file mode 100644 index 0000000..b13b7d7 --- /dev/null +++ b/kcmkwin/kwindecoration/kwindecorationsettings.kcfg @@ -0,0 +1,54 @@ + + + config-kwin.h + + + + + #if HAVE_BREEZE_DECO + const QString s_defaultPlugin { QStringLiteral(BREEZE_KDECORATION_PLUGIN_ID) }; + #else + const QString s_defaultPlugin { QStringLiteral("org.kde.kwin.aurorae") }; + #endif + + s_defaultPlugin + + + + #if HAVE_BREEZE_DECO + const QString s_defaultTheme { QStringLiteral("Breeze") }; + #else + const QString s_defaultTheme { QStringLiteral("kwin4_decoration_qml_plastik") }; + #endif + + s_defaultTheme + + + + Normal + + + + true + + + + false + + + + true + + + + MS + + + + HIAX + + + diff --git a/kcmkwin/kwindecoration/kwindecorationsettings.kcfgc b/kcmkwin/kwindecoration/kwindecorationsettings.kcfgc new file mode 100644 index 0000000..8207257 --- /dev/null +++ b/kcmkwin/kwindecoration/kwindecorationsettings.kcfgc @@ -0,0 +1,7 @@ +File=kwindecorationsettings.kcfg +ClassName=KWinDecorationSettings +Mutators=true +DefaultValueGetters=true +GenerateProperties=true +ParentInConstructor=true +Notifiers=buttonsOnLeft,buttonsOnRight,theme diff --git a/kcmkwin/kwindecoration/package/contents/ui/ButtonGroup.qml b/kcmkwin/kwindecoration/package/contents/ui/ButtonGroup.qml new file mode 100644 index 0000000..ae87767 --- /dev/null +++ b/kcmkwin/kwindecoration/package/contents/ui/ButtonGroup.qml @@ -0,0 +1,62 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +import QtQuick 2.7 +import org.kde.kwin.private.kdecoration 1.0 as KDecoration + +ListView { + id: view + property string key + property bool dragging: false + property int iconSize: units.iconSizes.small + orientation: ListView.Horizontal + interactive: false + spacing: units.smallSpacing + implicitHeight: iconSize + implicitWidth: count * (iconSize + units.smallSpacing) - Math.min(1, count) * units.smallSpacing + delegate: Item { + width: iconSize + height: iconSize + KDecoration.Button { + id: button + property int itemIndex: index + property var buttonsModel: parent.ListView.view.model + bridge: bridgeItem.bridge + settings: settingsItem + type: model["button"] + width: iconSize + height: iconSize + anchors.fill: Drag.active ? undefined : parent + Drag.keys: [ "decoButtonRemove", view.key ] + Drag.active: dragArea.drag.active + Drag.onActiveChanged: view.dragging = Drag.active + color: palette.windowText + } + MouseArea { + id: dragArea + cursorShape: Qt.SizeAllCursor + anchors.fill: parent + drag.target: button + onReleased: { + if (drag.target.Drag.target) { + drag.target.Drag.drop(); + } else { + drag.target.Drag.cancel(); + } + } + } + } + add: Transition { + NumberAnimation { property: "opacity"; from: 0; to: 1.0; duration: units.longDuration/2 } + NumberAnimation { property: "scale"; from: 0; to: 1.0; duration: units.longDuration/2 } + } + move: Transition { + NumberAnimation { property: "opacity"; from: 0; to: 1.0; duration: units.longDuration/2 } + NumberAnimation { property: "scale"; from: 0; to: 1.0; duration: units.longDuration/2 } + } + displaced: Transition { + NumberAnimation { properties: "x,y"; duration: units.longDuration; easing.type: Easing.OutBounce } + } +} diff --git a/kcmkwin/kwindecoration/package/contents/ui/Buttons.qml b/kcmkwin/kwindecoration/package/contents/ui/Buttons.qml new file mode 100644 index 0000000..2d1eeed --- /dev/null +++ b/kcmkwin/kwindecoration/package/contents/ui/Buttons.qml @@ -0,0 +1,236 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + SPDX-FileCopyrightText: 2019 Valerio Pilo + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +import QtQuick 2.7 +import QtQuick.Layouts 1.3 +import QtQuick.Controls 2.4 as Controls + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.kirigami 2.5 as Kirigami +import org.kde.kwin.private.kdecoration 1.0 as KDecoration + +ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + property int buttonIconSize: units.iconSizes.medium + property int titleBarSpacing: units.smallSpacing * 2 + readonly property bool draggingTitlebarButtons: leftButtonsView.dragging || rightButtonsView.dragging + readonly property bool hideDragHint: draggingTitlebarButtons || availableButtonsGrid.dragging + + KDecoration.Bridge { + id: bridgeItem + plugin: "org.kde.breeze" + } + KDecoration.Settings { + id: settingsItem + bridge: bridgeItem.bridge + } + + Rectangle { + Layout.fillWidth: true + color: palette.base + radius: units.smallSpacing + height: fakeWindow.height + Layout.margins: units.smallSpacing + + ColumnLayout { + id: fakeWindow + width: parent.width + + Rectangle { + id: titleBar + Layout.fillWidth: true + height: buttonPreviewRow.height + 2 * titleBarSpacing + radius: units.smallSpacing + gradient: Gradient { + GradientStop { position: 0.0; color: palette.midlight } + GradientStop { position: 1.0; color: palette.window } + } + + RowLayout { + id: buttonPreviewRow + anchors { + margins: titleBarSpacing + left: parent.left + right: parent.right + top: parent.top + } + + ButtonGroup { + id: leftButtonsView + iconSize: buttonIconSize + model: kcm.leftButtonsModel + key: "decoButtonLeft" + } + Controls.Label { + id: titleBarLabel + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + font.bold: true + text: i18n("Titlebar") + } + ButtonGroup { + id: rightButtonsView + iconSize: buttonIconSize + model: kcm.rightButtonsModel + key: "decoButtonRight" + } + } + DropArea { + id: titleBarDropArea + anchors { + fill: parent + margins: -titleBarSpacing + } + keys: [ "decoButtonAdd", "decoButtonRight", "decoButtonLeft" ] + onEntered: { + drag.accept(); + } + onDropped: { + var view = undefined; + var left = drag.x - (leftButtonsView.x + leftButtonsView.width); + var right = drag.x - rightButtonsView.x; + if (Math.abs(left) <= Math.abs(right)) { + view = leftButtonsView; + } else { + view = rightButtonsView; + } + if (!view) { + return; + } + var point = mapToItem(view, drag.x, drag.y); + var index = 0 + for(var childIndex = 0 ; childIndex < (view.count - 1) ; childIndex++) { + var child = view.contentItem.children[childIndex] + if (child.x > point.x) { + break + } + index = childIndex + 1 + } + if (drop.keys.indexOf("decoButtonAdd") !== -1) { + view.model.add(index, drag.source.type); + } else if (drop.keys.indexOf("decoButtonLeft") !== -1) { + if (view === leftButtonsView) { + // move in same view + if (index !== drag.source.itemIndex) { + drag.source.buttonsModel.move(drag.source.itemIndex, index); + } + } else { + // move to right view + view.model.add(index, drag.source.type); + drag.source.buttonsModel.remove(drag.source.itemIndex); + } + } else if (drop.keys.indexOf("decoButtonRight") !== -1) { + if (view === rightButtonsView) { + // move in same view + if (index !== drag.source.itemIndex) { + drag.source.buttonsModel.move(drag.source.itemIndex, index); + } + } else { + // move to left view + view.model.add(index, drag.source.type); + drag.source.buttonsModel.remove(drag.source.itemIndex); + } + } + } + } + } + GridView { + id: availableButtonsGrid + property bool dragging: false + Layout.fillWidth: true + Layout.minimumHeight: availableButtonsGrid.cellHeight * 2 + Layout.margins: units.largeSpacing + model: kcm.availableButtonsModel + interactive: false + delegate: Item { + id: availableDelegate + Layout.margins: units.largeSpacing + width: availableButtonsGrid.cellWidth + height: availableButtonsGrid.cellHeight + opacity: draggingTitlebarButtons ? 0.15 : 1.0 + Rectangle { + id: availableButtonFrame + anchors.horizontalCenter: parent.horizontalCenter + color: palette.window + radius: units.smallSpacing + width: buttonIconSize + units.largeSpacing + height: buttonIconSize + units.largeSpacing + + KDecoration.Button { + id: availableButton + anchors.centerIn: Drag.active ? undefined : availableButtonFrame + bridge: bridgeItem.bridge + settings: settingsItem + type: model["button"] + width: buttonIconSize + height: buttonIconSize + Drag.keys: [ "decoButtonAdd" ] + Drag.active: dragArea.drag.active + Drag.onActiveChanged: availableButtonsGrid.dragging = Drag.active + color: palette.windowText + } + MouseArea { + id: dragArea + anchors.fill: availableButton + drag.target: availableButton + cursorShape: Qt.SizeAllCursor + onReleased: { + if (availableButton.Drag.target) { + availableButton.Drag.drop(); + } else { + availableButton.Drag.cancel(); + } + } + } + } + Controls.Label { + id: iconLabel + text: model["display"] + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignTop + anchors.top: availableButtonFrame.bottom + anchors.topMargin: units.smallSpacing + anchors.left: parent.left + anchors.right: parent.right + elide: Text.ElideRight + wrapMode: Text.Wrap + height: 2 * implicitHeight + lineHeight + } + } + DropArea { + anchors.fill: parent + keys: [ "decoButtonRemove" ] + onEntered: { + drag.accept(); + } + onDropped: { + drag.source.buttonsModel.remove(drag.source.itemIndex); + } + Kirigami.Heading { + text: i18n("Drop button here to remove it") + font.weight: Font.Bold + level: 2 + anchors.centerIn: parent + opacity: draggingTitlebarButtons ? 1.0 : 0.0 + } + } + } + Text { + id: dragHint + readonly property real dragHintOpacitiy: enabled ? 1.0 : 0.3 + color: palette.windowText + opacity: hideDragHint ? 0.0 : dragHintOpacitiy + Layout.fillWidth: true + Layout.topMargin: titleBarSpacing + Layout.bottomMargin: titleBarSpacing + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.NoWrap + text: i18n("Drag buttons between here and the titlebar") + } + } + } +} diff --git a/kcmkwin/kwindecoration/package/contents/ui/Themes.qml b/kcmkwin/kwindecoration/package/contents/ui/Themes.qml new file mode 100644 index 0000000..cb87e98 --- /dev/null +++ b/kcmkwin/kwindecoration/package/contents/ui/Themes.qml @@ -0,0 +1,109 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + SPDX-FileCopyrightText: 2019 Valerio Pilo + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +import QtQuick 2.7 +import org.kde.kcm 1.1 as KCM +import org.kde.kirigami 2.2 as Kirigami +import org.kde.kwin.private.kdecoration 1.0 as KDecoration + +KCM.GridView { + function updateDecoration(item, marginTopLeft, marginBottomRight) { + var mainMargin = units.largeSpacing + var shd = item.shadow + item.anchors.leftMargin = mainMargin + marginTopLeft - (shd ? shd.paddingLeft : 0) + item.anchors.rightMargin = mainMargin + marginBottomRight - (shd ? shd.paddingRight : 0) + item.anchors.topMargin = mainMargin + marginTopLeft - (shd ? shd.paddingTop : 0) + item.anchors.bottomMargin = mainMargin + marginBottomRight - (shd ? shd.paddingBottom : 0) + } + + view.model: kcm.themesModel + view.currentIndex: kcm.theme + view.onContentHeightChanged: view.positionViewAtIndex(view.currentIndex, GridView.Visible) + + view.implicitCellWidth: Kirigami.Units.gridUnit * 18 + + view.delegate: KCM.GridDelegate { + id: delegate + text: model.display + + thumbnailAvailable: true + thumbnail: Rectangle { + anchors.fill: parent + color: palette.base + clip: true + + KDecoration.Bridge { + id: bridgeItem + plugin: model.plugin + theme: model.theme + } + KDecoration.Settings { + id: settingsItem + bridge: bridgeItem.bridge + Component.onCompleted: { + settingsItem.borderSizesIndex = kcm.borderSize + } + } + KDecoration.Decoration { + id: inactivePreview + bridge: bridgeItem.bridge + settings: settingsItem + anchors.fill: parent + onShadowChanged: updateDecoration(inactivePreview, 0, client.decoration.titleBar.height) + Component.onCompleted: { + client.active = false + client.caption = model.display + updateDecoration(inactivePreview, 0, client.decoration.titleBar.height) + } + } + KDecoration.Decoration { + id: activePreview + bridge: bridgeItem.bridge + settings: settingsItem + anchors.fill: parent + onShadowChanged: updateDecoration(activePreview, client.decoration.titleBar.height, 0) + Component.onCompleted: { + client.active = true + client.caption = model.display + updateDecoration(activePreview, client.decoration.titleBar.height, 0) + } + } + MouseArea { + anchors.fill: parent + onClicked: { + kcm.theme = index + view.currentIndex = index + } + } + Connections { + target: kcm + onBorderSizeChanged: settingsItem.borderSizesIndex = kcm.borderSize + } + } + actions: [ + Kirigami.Action { + iconName: "edit-entry" + tooltip: i18n("Edit %1 Theme", model.display) + enabled: model.configureable + onTriggered: { + kcm.theme = index + view.currentIndex = index + bridgeItem.bridge.configure(delegate) + } + } + ] + + onClicked: { + kcm.theme = index + view.currentIndex = index + } + } + Connections { + target: kcm + onThemeChanged: view.currentIndex = kcm.theme + } +} + diff --git a/kcmkwin/kwindecoration/package/contents/ui/main.qml b/kcmkwin/kwindecoration/package/contents/ui/main.qml new file mode 100644 index 0000000..49fc9f6 --- /dev/null +++ b/kcmkwin/kwindecoration/package/contents/ui/main.qml @@ -0,0 +1,155 @@ +/* + SPDX-FileCopyrightText: 2019 Valerio Pilo + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +import QtQuick 2.7 +import QtQuick.Layouts 1.3 +import QtQuick.Controls 2.4 as Controls +import org.kde.kcm 1.1 as KCM +import org.kde.kconfig 1.0 // for KAuthorized +import org.kde.kirigami 2.4 as Kirigami + +Kirigami.Page { + KCM.ConfigModule.quickHelp: i18n("This module lets you configure the window decorations.") + title: kcm.name + + SystemPalette { + id: palette + colorGroup: SystemPalette.Active + } + + // To match SimpleKCM's borders of Page + headerParent/footerParent (also specified in raw pixels) + leftPadding: Kirigami.Settings.isMobile ? 0 : 8 + topPadding: leftPadding + rightPadding: leftPadding + bottomPadding: leftPadding + + implicitWidth: Kirigami.Units.gridUnit * 48 + implicitHeight: Kirigami.Units.gridUnit * 33 + + // TODO: replace this TabBar-plus-Frame-in-a-ColumnLayout with whatever shakes + // out of https://bugs.kde.org/show_bug.cgi?id=394296 + ColumnLayout { + id: tabLayout + anchors.fill: parent + spacing: 0 + + Controls.TabBar { + id: tabBar + // Tab styles generally assume that they're touching the inner layout, + // not the frame, so we need to move the tab bar down a pixel and make + // sure it's drawn on top of the frame + z: 1 + Layout.bottomMargin: -1 + Layout.fillWidth: true + + Controls.TabButton { + text: i18nc("tab label", "Theme") + } + + Controls.TabButton { + text: i18nc("tab label", "Titlebar Buttons") + } + } + Controls.Frame { + Layout.fillWidth: true + Layout.fillHeight: true + + StackLayout { + anchors.fill: parent + + currentIndex: tabBar.currentIndex + + ColumnLayout { + Themes { + Layout.fillWidth: true + Layout.fillHeight: true + enabled: !kcm.settings.isImmutable("pluginName") && !kcm.settings.isImmutable("theme") + } + + RowLayout { + Controls.CheckBox { + id: borderSizeAutoCheckbox + // Let it elide but don't make it push the ComboBox away from it + Layout.fillWidth: true + Layout.maximumWidth: implicitWidth + text: i18nc("checkbox label", "Use theme's default window border size") + enabled: !kcm.settings.isImmutable("borderSizeAuto") + checked: kcm.settings.borderSizeAuto + onToggled: { + kcm.settings.borderSizeAuto = checked; + borderSizeComboBox.autoBorderUpdate() + } + } + Controls.ComboBox { + id: borderSizeComboBox + enabled: !borderSizeAutoCheckbox.checked && !kcm.settings.isImmutable("borderSize") + model: kcm.borderSizesModel + currentIndex: kcm.borderSize + onActivated: { + kcm.borderSize = currentIndex + } + function autoBorderUpdate() { + if (borderSizeAutoCheckbox.checked) { + kcm.borderSize = kcm.recommendedBorderSize + } + } + + Connections { + target: kcm + onThemeChanged: borderSizeComboBox.autoBorderUpdate() + } + } + Item { + Layout.fillWidth: true + } + Controls.Button { + text: i18nc("button text", "Get New Window Decorations...") + icon.name: "get-hot-new-stuff" + onClicked: kcm.getNewStuff(this) + visible: KAuthorized.authorize("ghns") + } + } + } + + ColumnLayout { + Buttons { + Layout.fillWidth: true + Layout.fillHeight: true + enabled: !kcm.settings.isImmutable("buttonsOnLeft") && !kcm.settings.isImmutable("buttonsOnRight") + } + + Controls.CheckBox { + id: closeOnDoubleClickOnMenuCheckBox + text: i18nc("checkbox label", "Close windows by double clicking the menu button") + enabled: !kcm.settings.isImmutable("closeOnDoubleClickOnMenu") + checked: kcm.settings.closeOnDoubleClickOnMenu + onToggled: { + kcm.settings.closeOnDoubleClickOnMenu = checked + infoLabel.visible = checked + } + } + + Kirigami.InlineMessage { + Layout.fillWidth: true + id: infoLabel + type: Kirigami.MessageType.Information + text: i18nc("popup tip", "Close by double clicking: Keep the window's Menu button pressed until it appears.") + showCloseButton: true + visible: false + } + + Controls.CheckBox { + id: showToolTipsCheckBox + text: i18nc("checkbox label", "Show titlebar button tooltips") + enabled: !kcm.settings.isImmutable("showToolTips") + checked: kcm.settings.showToolTips + onToggled: kcm.settings.showToolTips = checked + } + } + } + } + } +} diff --git a/kcmkwin/kwindecoration/package/metadata.desktop b/kcmkwin/kwindecoration/package/metadata.desktop new file mode 100644 index 0000000..e634a0f --- /dev/null +++ b/kcmkwin/kwindecoration/package/metadata.desktop @@ -0,0 +1,110 @@ +[Desktop Entry] +Icon=preferences-system-windows-action +Type=Service +Keywords= +X-KDE-ParentApp= +X-KDE-System-Settings-Parent-Category=applicationstyle +X-KDE-PluginInfo-Author=Valerio Pilo +X-KDE-PluginInfo-Email=vpilo@coldshock.net +X-KDE-PluginInfo-License=GPL-2.0+ +X-KDE-PluginInfo-Name=kcm_kwindecoration +X-KDE-PluginInfo-Version= +X-KDE-PluginInfo-Website=https://www.kde.org/plasma-desktop +X-KDE-ServiceTypes=Plasma/Generic +X-Plasma-API=declarativeappletscript +X-Plasma-MainScript=ui/main.qml +X-KDE-FormFactors=desktop,tablet + +Name=Window Decorations +Name[ar]=زخارف النوافذ +Name[az]=Pəncərə dekorasiyası +Name[bg]=Декорации на прозорците +Name[bs]=Dekoracije prozora +Name[ca]=Decoració de les finestres +Name[ca@valencia]=Decoració de les finestres +Name[cs]=Dekorace oken +Name[da]=Vinduesdekorationer +Name[de]=Fensterdekoration +Name[el]=Διακοσμήσεις παραθύρου +Name[en_GB]=Window Decorations +Name[es]=Decoraciones de las ventanas +Name[et]=Akna dekoratsioonid +Name[eu]=Leiho-apaindurak +Name[fi]=Ikkunakehykset +Name[fr]=Décorations de fenêtres +Name[ga]=Maisiúcháin Fhuinneog +Name[gl]=Decoración da xanela +Name[he]=מסגרת חלון +Name[hi]=विंडो सजावट +Name[hr]=Ukrasi prozora +Name[hu]=Ablakdekorációk +Name[ia]=Decorationes de fenestra +Name[id]=Dekorasi Window +Name[is]=Gluggaskreytingar +Name[it]=Decorazioni delle finestre +Name[ja]=ウィンドウの飾り +Name[kk]=Терезенің безендірулері +Name[km]=ការ​តុបតែង​បង្អួច +Name[kn]=ವಿಂಡೋ ಅಲಂಕಾರಗಳು +Name[ko]=창 장식 +Name[lt]=Langų dekoracijos +Name[lv]=Logu dekorācijas +Name[mr]=चौकट सजावट +Name[nb]=Vinduspynt +Name[nds]=Finstern opfladusen +Name[nl]=Vensterdecoraties +Name[nn]=Vindaugspynt +Name[pa]=ਵਿੰਡੋ ਸਜਾਵਟ +Name[pl]=Wygląd okien +Name[pt]=Decorações das Janelas +Name[pt_BR]=Decorações da janela +Name[ro]=Decorații fereastră +Name[ru]=Оформление окон +Name[si]=කවුළු සැරසිලි +Name[sk]=Dekorácie okien +Name[sl]=Okraski oken +Name[sr]=Декорације прозора +Name[sr@ijekavian]=Декорације прозора +Name[sr@ijekavianlatin]=Dekoracije prozora +Name[sr@latin]=Dekoracije prozora +Name[sv]=Fönsterdekorationer +Name[th]=ส่วนตกแต่งหน้าต่าง +Name[tr]=Pencere Dekorasyonları +Name[ug]=كۆزنەك بېزەكلىرى +Name[uk]=Обрамлення вікон +Name[wa]=Gåyotaedjes des fniesses +Name[x-test]=xxWindow Decorationsxx +Name[zh_CN]=窗口装饰 +Name[zh_TW]=視窗裝飾 +Comment=Configure window titlebars and borders +Comment[az]=Pəncərə başlığı və çərçivəsini tənzimləmək +Comment[ca]=Configura la barra de títol i les vores de les finestres +Comment[ca@valencia]=Configura la barra de títol i les vores de les finestres +Comment[da]=Indstil vinduets titellinjer og kanter +Comment[de]=Titelleiste und Ränder von Fenstern einrichten +Comment[en_GB]=Configure window titlebars and borders +Comment[es]=Configurar barra de título y bordes de las ventanas +Comment[et]=Akende tiitliriba ja raami seadistamine +Comment[eu]=Konfiguratu leihoen titulu-barrak eta ertzak +Comment[fi]=Ikkunan otsikkopalkkien ja reunojen asetukset +Comment[fr]=Configure les barres de titre et les bordures de la fenêtre +Comment[gl]=Configurar as barras de título e os bordos das xanelas +Comment[ia]=Configura barras de titulo e margines +Comment[id]=Konfigurasikan bingkai dan bilah-judul window +Comment[it]=Configura la barra del titolo e i bordi delle finestre +Comment[ko]=창 제목 표시줄과 경계선 설정 +Comment[lt]=Konfigūruoti langų antraštės juostas ir rėmelius +Comment[nl]=Titelbalken en randen van venster configureren +Comment[nn]=Set opp tittellinjer og vindaugsrammer +Comment[pl]=Ustawienia pasków tytułów i obramowań okien +Comment[pt]=Configurar as barras de título e contornos das janelas +Comment[pt_BR]=Configure as barras de títulos e bordas da janela +Comment[ro]=Configurează barele de titlu și contururile ferestrelor +Comment[ru]=Настройка заголовка и границ окон +Comment[sk]=Nastaviť záhlavia a okraje okna +Comment[sl]=Nastavi naslovne vrstice in robove oken +Comment[sv]=Anpassa namnlister och kanter för fönster +Comment[uk]=Налаштовування смужок заголовків та рамок вікон +Comment[x-test]=xxConfigure window titlebars and bordersxx +Comment[zh_CN]=配置窗口标题栏和边框 +Comment[zh_TW]=設定視窗的標題列和邊框 diff --git a/kcmkwin/kwindecoration/utils.cpp b/kcmkwin/kwindecoration/utils.cpp new file mode 100644 index 0000000..05dd79f --- /dev/null +++ b/kcmkwin/kwindecoration/utils.cpp @@ -0,0 +1,109 @@ +/* + SPDX-FileCopyrightText: 2019 Valerio Pilo + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "utils.h" + +#include +#include + +namespace +{ +const QMap s_borderSizes { + { QStringLiteral("None"), KDecoration2::BorderSize::None }, + { QStringLiteral("NoSides"), KDecoration2::BorderSize::NoSides }, + { QStringLiteral("Tiny"), KDecoration2::BorderSize::Tiny }, + { QStringLiteral("Normal"), KDecoration2::BorderSize::Normal }, + { QStringLiteral("Large"), KDecoration2::BorderSize::Large }, + { QStringLiteral("VeryLarge"), KDecoration2::BorderSize::VeryLarge }, + { QStringLiteral("Huge"), KDecoration2::BorderSize::Huge }, + { QStringLiteral("VeryHuge"), KDecoration2::BorderSize::VeryHuge }, + { QStringLiteral("Oversized"), KDecoration2::BorderSize::Oversized } +}; +const QMap s_borderSizeNames { + { KDecoration2::BorderSize::None, i18n("No Borders") }, + { KDecoration2::BorderSize::NoSides, i18n("No Side Borders") }, + { KDecoration2::BorderSize::Tiny, i18n("Tiny") }, + { KDecoration2::BorderSize::Normal, i18n("Normal") }, + { KDecoration2::BorderSize::Large, i18n("Large") }, + { KDecoration2::BorderSize::VeryLarge, i18n("Very Large") }, + { KDecoration2::BorderSize::Huge, i18n("Huge") }, + { KDecoration2::BorderSize::VeryHuge, i18n("Very Huge") }, + { KDecoration2::BorderSize::Oversized, i18n("Oversized") } +}; + +const QHash s_buttonNames { + {KDecoration2::DecorationButtonType::Menu, QChar('M') }, + {KDecoration2::DecorationButtonType::ApplicationMenu, QChar('N') }, + {KDecoration2::DecorationButtonType::OnAllDesktops, QChar('S') }, + {KDecoration2::DecorationButtonType::ContextHelp, QChar('H') }, + {KDecoration2::DecorationButtonType::Minimize, QChar('I') }, + {KDecoration2::DecorationButtonType::Maximize, QChar('A') }, + {KDecoration2::DecorationButtonType::Close, QChar('X') }, + {KDecoration2::DecorationButtonType::KeepAbove, QChar('F') }, + {KDecoration2::DecorationButtonType::KeepBelow, QChar('B') }, + {KDecoration2::DecorationButtonType::Shade, QChar('L') } +}; +} + + +namespace Utils +{ + +QString buttonsToString(const DecorationButtonsList &buttons) +{ + auto buttonToString = [](KDecoration2::DecorationButtonType button) -> QChar { + const auto it = s_buttonNames.constFind(button); + if (it != s_buttonNames.constEnd()) { + return it.value(); + } + return QChar(); + }; + QString ret; + for (auto button : buttons) { + ret.append(buttonToString(button)); + } + return ret; +} + +DecorationButtonsList buttonsFromString(const QString &buttons) +{ + DecorationButtonsList ret; + for (auto it = buttons.begin(); it != buttons.end(); ++it) { + for (auto it2 = s_buttonNames.constBegin(); it2 != s_buttonNames.constEnd(); ++it2) { + if (it2.value() == (*it)) { + ret << it2.key(); + } + } + } + return ret; +} + +DecorationButtonsList readDecorationButtons(const KConfigGroup &config, const QString &key, const DecorationButtonsList &defaultValue) +{ + return buttonsFromString(config.readEntry(key, buttonsToString(defaultValue))); +} + +KDecoration2::BorderSize stringToBorderSize(const QString &name) +{ + auto it = s_borderSizes.constFind(name); + if (it == s_borderSizes.constEnd()) { + // non sense values are interpreted just like normal + return KDecoration2::BorderSize::Normal; + } + return it.value(); +} + +QString borderSizeToString(KDecoration2::BorderSize size) +{ + return s_borderSizes.key(size); +} + +const QMap &getBorderSizeNames() +{ + return s_borderSizeNames; +} + +} // namespace Utils diff --git a/kcmkwin/kwindecoration/utils.h b/kcmkwin/kwindecoration/utils.h new file mode 100644 index 0000000..de467ad --- /dev/null +++ b/kcmkwin/kwindecoration/utils.h @@ -0,0 +1,29 @@ +/* + SPDX-FileCopyrightText: 2019 Valerio Pilo + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include +#include + +#include + + +using DecorationButtonsList = QVector; + +namespace Utils +{ + +QString buttonsToString(const DecorationButtonsList &buttons); +DecorationButtonsList buttonsFromString(const QString &buttons); +DecorationButtonsList readDecorationButtons(const KConfigGroup &config, const QString &key, const DecorationButtonsList &defaultValue); + +KDecoration2::BorderSize stringToBorderSize(const QString &name); +QString borderSizeToString(KDecoration2::BorderSize size); + +const QMap &getBorderSizeNames(); + +} diff --git a/kcmkwin/kwindecoration/window-decorations.knsrc b/kcmkwin/kwindecoration/window-decorations.knsrc new file mode 100644 index 0000000..95b3163 --- /dev/null +++ b/kcmkwin/kwindecoration/window-decorations.knsrc @@ -0,0 +1,67 @@ +[KNewStuff3] +Name=Window Decorations +Name[ar]=زخارف النوافذ +Name[az]=Pəncərə dekorasiyası +Name[bg]=Декорации на прозорците +Name[bs]=Dekoracije prozora +Name[ca]=Decoració de les finestres +Name[ca@valencia]=Decoració de les finestres +Name[cs]=Dekorace oken +Name[da]=Vinduesdekorationer +Name[de]=Fensterdekoration +Name[el]=Διακοσμήσεις παραθύρου +Name[en_GB]=Window Decorations +Name[es]=Decoraciones de las ventanas +Name[et]=Akna dekoratsioonid +Name[eu]=Leiho-apaindurak +Name[fi]=Ikkunakehykset +Name[fr]=Décorations de fenêtres +Name[ga]=Maisiúcháin Fhuinneog +Name[gl]=Decoración da xanela +Name[he]=מסגרת חלון +Name[hi]=विंडो सजावट +Name[hr]=Ukrasi prozora +Name[hu]=Ablakdekorációk +Name[ia]=Decorationes de fenestra +Name[id]=Dekorasi Window +Name[is]=Gluggaskreytingar +Name[it]=Decorazioni delle finestre +Name[ja]=ウィンドウの飾り +Name[kk]=Терезенің безендірулері +Name[km]=ការ​តុបតែង​បង្អួច +Name[kn]=ವಿಂಡೋ ಅಲಂಕಾರಗಳು +Name[ko]=창 장식 +Name[lt]=Langų dekoracijos +Name[lv]=Logu dekorācijas +Name[mr]=चौकट सजावट +Name[nb]=Vinduspynt +Name[nds]=Finstern opfladusen +Name[nl]=Vensterdecoraties +Name[nn]=Vindaugspynt +Name[pa]=ਵਿੰਡੋ ਸਜਾਵਟ +Name[pl]=Wygląd okien +Name[pt]=Decorações das Janelas +Name[pt_BR]=Decorações da janela +Name[ro]=Decorații fereastră +Name[ru]=Оформление окон +Name[si]=කවුළු සැරසිලි +Name[sk]=Dekorácie okien +Name[sl]=Okraski oken +Name[sr]=Декорације прозора +Name[sr@ijekavian]=Декорације прозора +Name[sr@ijekavianlatin]=Dekoracije prozora +Name[sr@latin]=Dekoracije prozora +Name[sv]=Fönsterdekorationer +Name[th]=ส่วนตกแต่งหน้าต่าง +Name[tr]=Pencere Dekorasyonları +Name[ug]=كۆزنەك بېزەكلىرى +Name[uk]=Обрамлення вікон +Name[wa]=Gåyotaedjes des fniesses +Name[x-test]=xxWindow Decorationsxx +Name[zh_CN]=窗口装饰 +Name[zh_TW]=視窗裝飾 + +ProvidersUrl=https://autoconfig.kde.org/ocs/providers.xml +Categories=Window Decoration Aurorae +TargetDir=aurorae/themes +Uncompress=archive diff --git a/kcmkwin/kwindesktop/CMakeLists.txt b/kcmkwin/kwindesktop/CMakeLists.txt new file mode 100644 index 0000000..2e36e53 --- /dev/null +++ b/kcmkwin/kwindesktop/CMakeLists.txt @@ -0,0 +1,38 @@ +include(ECMQMLModules) +ecm_find_qmlmodule(org.kde.plasma.core 2.0) + +# KI18N Translation Domain for this library. +add_definitions(-DTRANSLATION_DOMAIN=\"kcm_kwin_virtualdesktops\") + +########### next target ############### + +set(kcm_kwin_virtualdesktops_PART_SRCS + ../../virtualdesktopsdbustypes.cpp + animationsmodel.cpp + desktopsmodel.cpp + virtualdesktops.cpp +) + +kconfig_add_kcfg_files(kcm_kwin_virtualdesktops_PART_SRCS virtualdesktopssettings.kcfgc GENERATE_MOC) + +add_library(kcm_kwin_virtualdesktops MODULE ${kcm_kwin_virtualdesktops_PART_SRCS}) + +target_link_libraries(kcm_kwin_virtualdesktops + Qt5::DBus + + KF5::I18n + KF5::KCMUtils + KF5::QuickAddons + KF5::XmlGui + + kcmkwincommon +) + +kcoreaddons_desktop_to_json(kcm_kwin_virtualdesktops "kcm_kwin_virtualdesktops.desktop") + +########### install files ############### + +install(FILES virtualdesktopssettings.kcfg DESTINATION ${KDE_INSTALL_KCFGDIR}) +install(TARGETS kcm_kwin_virtualdesktops DESTINATION ${KDE_INSTALL_PLUGINDIR}/kcms) +install(FILES kcm_kwin_virtualdesktops.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}) +kpackage_install_package(package kcm_kwin_virtualdesktops kcms) diff --git a/kcmkwin/kwindesktop/Messages.sh b/kcmkwin/kwindesktop/Messages.sh new file mode 100644 index 0000000..b1a90c6 --- /dev/null +++ b/kcmkwin/kwindesktop/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name \*.cpp -o -name \*.qml` -o $podir/kcm_kwin_virtualdesktops.pot diff --git a/kcmkwin/kwindesktop/animationsmodel.cpp b/kcmkwin/kwindesktop/animationsmodel.cpp new file mode 100644 index 0000000..480be5a --- /dev/null +++ b/kcmkwin/kwindesktop/animationsmodel.cpp @@ -0,0 +1,154 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "animationsmodel.h" + +namespace KWin +{ + +AnimationsModel::AnimationsModel(QObject *parent) + : EffectsModel(parent) +{ + connect(this, &EffectsModel::loaded, this, + [this] { + setEnabled(modelCurrentEnabled()); + setCurrentIndex(modelCurrentIndex()); + } + ); + connect(this, &AnimationsModel::currentIndexChanged, this, + [this] { + const QModelIndex index_ = index(m_currentIndex, 0); + if (!index_.isValid()) { + return; + } + const bool configurable = index_.data(ConfigurableRole).toBool(); + if (configurable != m_currentConfigurable) { + m_currentConfigurable = configurable; + emit currentConfigurableChanged(); + } + } + ); +} + +bool AnimationsModel::enabled() const +{ + return m_enabled; +} + +void AnimationsModel::setEnabled(bool enabled) +{ + if (m_enabled != enabled) { + m_enabled = enabled; + emit enabledChanged(); + } +} + +int AnimationsModel::currentIndex() const +{ + return m_currentIndex; +} + +void AnimationsModel::setCurrentIndex(int index) +{ + if (m_currentIndex != index) { + m_currentIndex = index; + emit currentIndexChanged(); + } +} + +bool AnimationsModel::currentConfigurable() const +{ + return m_currentConfigurable; +} + +bool AnimationsModel::shouldStore(const EffectData &data) const +{ + return data.untranslatedCategory.contains( + QStringLiteral("Virtual Desktop Switching Animation"), Qt::CaseInsensitive); +} + +EffectsModel::Status AnimationsModel::status(int row) const +{ + return Status(data(index(row, 0), static_cast(StatusRole)).toInt()); +} + +bool AnimationsModel::modelCurrentEnabled() const +{ + for (int i = 0; i < rowCount(); ++i) { + if (status(i) != Status::Disabled) { + return true; + } + } + + return false; +} + +int AnimationsModel::modelCurrentIndex() const +{ + for (int i = 0; i < rowCount(); ++i) { + if (status(i) != Status::Disabled) { + return i; + } + } + + return 0; +} + +void AnimationsModel::load() +{ + EffectsModel::load(); +} + +void AnimationsModel::save() +{ + for (int i = 0; i < rowCount(); ++i) { + const auto status = (m_enabled && i == m_currentIndex) + ? EffectsModel::Status::Enabled + : EffectsModel::Status::Disabled; + updateEffectStatus(index(i, 0), status); + } + + EffectsModel::save(); +} + +void AnimationsModel::defaults() +{ + EffectsModel::defaults(); + setEnabled(modelCurrentEnabled()); + setCurrentIndex(modelCurrentIndex()); +} + +bool AnimationsModel::isDefaults() const +{ + // effect at m_currentIndex index may not be the current saved selected effect + const bool enabledByDefault = index(m_currentIndex, 0).data(EnabledByDefaultRole).toBool(); + return enabledByDefault; +} + +bool AnimationsModel::needsSave() const +{ + KConfigGroup kwinConfig(KSharedConfig::openConfig("kwinrc"), "Plugins"); + + for (int i = 0; i < rowCount(); ++i) { + const QModelIndex index_ = index(i, 0); + const bool enabledConfig = kwinConfig.readEntry( + index_.data(ServiceNameRole).toString() + QLatin1String("Enabled"), + index_.data(EnabledByDefaultRole).toBool() + ); + const bool enabled = (m_enabled && i == m_currentIndex); + + if (enabled != enabledConfig) { + return true; + } + } + + return false; +} + +} diff --git a/kcmkwin/kwindesktop/animationsmodel.h b/kcmkwin/kwindesktop/animationsmodel.h new file mode 100644 index 0000000..4ede4bf --- /dev/null +++ b/kcmkwin/kwindesktop/animationsmodel.h @@ -0,0 +1,61 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effectsmodel.h" + +namespace KWin +{ + +class AnimationsModel : public EffectsModel +{ + Q_OBJECT + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged) + Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) + Q_PROPERTY(bool currentConfigurable READ currentConfigurable NOTIFY currentConfigurableChanged) + +public: + explicit AnimationsModel(QObject *parent = nullptr); + + bool enabled() const; + void setEnabled(bool enabled); + + int currentIndex() const; + void setCurrentIndex(int index); + + bool currentConfigurable() const; + + void load(); + void save(); + void defaults(); + bool isDefaults() const; + bool needsSave() const; + +Q_SIGNALS: + void enabledChanged(); + void currentIndexChanged(); + void currentConfigurableChanged(); + +protected: + bool shouldStore(const EffectData &data) const override; + +private: + Status status(int row) const; + bool modelCurrentEnabled() const; + int modelCurrentIndex() const; + + bool m_enabled = false; + int m_currentIndex = -1; + bool m_currentConfigurable = false; + + Q_DISABLE_COPY(AnimationsModel) +}; + +} diff --git a/kcmkwin/kwindesktop/desktopsmodel.cpp b/kcmkwin/kwindesktop/desktopsmodel.cpp new file mode 100644 index 0000000..da1d4d9 --- /dev/null +++ b/kcmkwin/kwindesktop/desktopsmodel.cpp @@ -0,0 +1,665 @@ +/* + SPDX-FileCopyrightText: 2018 Eike Hein + SPDX-FileCopyrightText: 2018 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "desktopsmodel.h" + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace KWin +{ + +static const QString s_serviceName(QStringLiteral("org.kde.KWin")); +static const QString s_virtualDesktopsInterface(QStringLiteral("org.kde.KWin.VirtualDesktopManager")); +static const QString s_virtDesktopsPath(QStringLiteral("/VirtualDesktopManager")); +static const QString s_fdoPropertiesInterface(QStringLiteral("org.freedesktop.DBus.Properties")); + +DesktopsModel::DesktopsModel(QObject *parent) + : QAbstractListModel(parent) + , m_userModified(false) + , m_serverModified(false) + , m_serverSideRows(-1) + , m_rows(-1) + , m_synchronizing(false) +{ + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + + m_serviceWatcher = new QDBusServiceWatcher(s_serviceName, + QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange); + + QObject::connect(m_serviceWatcher, &QDBusServiceWatcher::serviceRegistered, + this, [this]() { reset(); }); + + QObject::connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, + this, [this]() { + QDBusConnection::sessionBus().disconnect( + s_serviceName, + s_virtDesktopsPath, + s_virtualDesktopsInterface, + QStringLiteral("desktopCreated"), + this, + SLOT(desktopCreated(QString,KWin::DBusDesktopDataStruct))); + + QDBusConnection::sessionBus().disconnect( + s_serviceName, + s_virtDesktopsPath, + s_virtualDesktopsInterface, + QStringLiteral("desktopRemoved"), + this, + SLOT(desktopRemoved(QString))); + + QDBusConnection::sessionBus().disconnect( + s_serviceName, + s_virtDesktopsPath, + s_virtualDesktopsInterface, + QStringLiteral("desktopDataChanged"), + this, + SLOT(desktopDataChanged(QString,KWin::DBusDesktopDataStruct))); + + + QDBusConnection::sessionBus().disconnect( + s_serviceName, + s_virtDesktopsPath, + s_virtualDesktopsInterface, + QStringLiteral("rowsChanged"), + this, + SLOT(desktopRowsChanged(uint))); + } + ); + + reset(); +} + +DesktopsModel::~DesktopsModel() +{ +} + +QHash DesktopsModel::roleNames() const +{ + QHash roles = QAbstractItemModel::roleNames(); + + QMetaEnum e = metaObject()->enumerator(metaObject()->indexOfEnumerator("AdditionalRoles")); + + for (int i = 0; i < e.keyCount(); ++i) { + roles.insert(e.value(i), e.key(i)); + } + + return roles; +} + +QVariant DesktopsModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() > (m_desktops.count() - 1)) { + return QVariant(); + } + + if (role == Qt::DisplayRole) { + return m_names.value(m_desktops.at(index.row())); + } else if (role == Id) { + return m_desktops.at(index.row()); + } else if (role == DesktopRow) { + const int rows = std::max(m_rows, 1); + const int perRow = std::ceil((qreal)m_desktops.count() / (qreal)rows); + + return (index.row() / perRow) + 1; + + } + + return QVariant(); +} + +int DesktopsModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + + return m_desktops.count(); +} + +bool DesktopsModel::ready() const +{ + return !m_desktops.isEmpty(); +} + +QString DesktopsModel::error() const +{ + return m_error; +} + +bool DesktopsModel::userModified() const +{ + return m_userModified; +} + +bool DesktopsModel::serverModified() const +{ + return m_serverModified; +} + +int DesktopsModel::rows() const +{ + return m_rows; +} + +void DesktopsModel::setRows(int rows) +{ + if (!ready()) { + return; + } + + if (m_rows != rows) { + m_rows = rows; + + emit rowsChanged(); + emit dataChanged(index(0, 0), index(m_desktops.count() - 1, 0), QVector{DesktopRow}); + + updateModifiedState(); + } +} + +void DesktopsModel::createDesktop(const QString &name) +{ + if (!ready()) { + return; + } + + beginInsertRows(QModelIndex(), m_desktops.count(), m_desktops.count()); + + const QString &dummyId = QUuid::createUuid().toString(QUuid::WithoutBraces); + + m_desktops.append(dummyId); + m_names[dummyId] = name; + + endInsertRows(); + + updateModifiedState(); +} + +void DesktopsModel::removeDesktop(const QString &id) +{ + if (!ready() || !m_desktops.contains(id)) { + return; + } + + const int desktopIndex = m_desktops.indexOf(id); + + beginRemoveRows(QModelIndex(), desktopIndex, desktopIndex); + + m_desktops.removeAt(desktopIndex); + m_names.remove(id); + + endRemoveRows(); + + updateModifiedState(); +} + +void DesktopsModel::setDesktopName(const QString &id, const QString &name) +{ + if (!ready() || !m_desktops.contains(id)) { + return; + } + + m_names[id] = name; + + const QModelIndex &idx = index(m_desktops.indexOf(id), 0); + + dataChanged(idx, idx, QVector{Qt::DisplayRole}); + + updateModifiedState(); +} + +void DesktopsModel::syncWithServer() +{ + m_synchronizing = true; + + auto callFinished = [this](QDBusPendingCallWatcher *call) { + QDBusPendingReply reply = *call; + + if (reply.isError()) { + handleCallError(); + } + + call->deleteLater(); + }; + + if (m_desktops.count() > m_serverSideDesktops.count()) { + auto call = QDBusMessage::createMethodCall( + s_serviceName, + s_virtDesktopsPath, + s_virtualDesktopsInterface, + QStringLiteral("createDesktop")); + + const int newIndex = m_serverSideDesktops.count(); + + call.setArguments({(uint)newIndex, m_names.value(m_desktops.at(newIndex))}); + + QDBusPendingCall pending = QDBusConnection::sessionBus().asyncCall(call); + + const auto *watcher = new QDBusPendingCallWatcher(pending, this); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, callFinished); + + return; // The change-handling slot will call syncWithServer() again, + // until everything is in sync. + } + + if (m_desktops.count() < m_serverSideDesktops.count()) { + QStringListIterator i(m_serverSideDesktops); + + i.toBack(); + + while (i.hasPrevious()) { + const QString &previous = i.previous(); + + if (!m_desktops.contains(previous)) { + auto call = QDBusMessage::createMethodCall( + s_serviceName, + s_virtDesktopsPath, + s_virtualDesktopsInterface, + QStringLiteral("removeDesktop")); + + call.setArguments({previous}); + + QDBusPendingCall pending = QDBusConnection::sessionBus().asyncCall(call); + + const QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pending, this); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, callFinished); + + return; // The change-handling slot will call syncWithServer() again, + // until everything is in sync. + } + } + } + + // Sync ids. Replace dummy ids in the process. + for (int i = 0; i < m_serverSideDesktops.count(); ++i) { + const QString oldId = m_desktops.at(i); + const QString &newId = m_serverSideDesktops.at(i); + m_desktops[i] = newId; + m_names[newId] = m_names.take(oldId); + } + + emit dataChanged(index(0, 0), index(rowCount() - 1, 0), QVector{Qt::DisplayRole}); + + // Sync names. + if (m_names != m_serverSideNames) { + QHashIterator i(m_names); + + while (i.hasNext()) { + i.next(); + + if (i.value() != m_serverSideNames.value(i.key())) { + auto call = QDBusMessage::createMethodCall( + s_serviceName, + s_virtDesktopsPath, + s_virtualDesktopsInterface, + QStringLiteral("setDesktopName")); + + call.setArguments({i.key(), i.value()}); + + QDBusPendingCall pending = QDBusConnection::sessionBus().asyncCall(call); + + const QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pending, this); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, callFinished); + + break; + } + } + + return; // The change-handling slot will call syncWithServer() again, + // until everything is in sync.. + } + + // Sync rows. + if (m_rows != m_serverSideRows) { + auto call = QDBusMessage::createMethodCall( + s_serviceName, + s_virtDesktopsPath, + s_fdoPropertiesInterface, + QStringLiteral("Set")); + + call.setArguments({s_virtualDesktopsInterface, + QStringLiteral("rows"), QVariant::fromValue(QDBusVariant(QVariant((uint)m_rows)))}); + + QDBusPendingCall pending = QDBusConnection::sessionBus().asyncCall(call); + + const QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pending, this); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, callFinished); + } +} + +void DesktopsModel::reset() +{ + m_synchronizing = false; // Sanity. + + auto getAllAndConnectCall = QDBusMessage::createMethodCall( + s_serviceName, + s_virtDesktopsPath, + s_fdoPropertiesInterface, + QStringLiteral("GetAll")); + + getAllAndConnectCall.setArguments({s_virtualDesktopsInterface}); + + QDBusConnection::sessionBus().callWithCallback( + getAllAndConnectCall, + this, + SLOT(getAllAndConnect(QDBusMessage)), + SLOT(handleCallError())); +} + +bool DesktopsModel::needsSave() const +{ + return m_userModified; +} + +bool DesktopsModel::isDefaults() const +{ + return m_rows == 2 && m_desktops.count() == 1; +} + +void DesktopsModel::defaults() +{ + beginResetModel(); + // default is 1 desktop with 2 rows + // see kwin/virtualdesktops.cpp VirtualDesktopGrid::VirtualDesktopGrid + while (m_desktops.count() > 1) { + const auto desktop = m_desktops.takeLast(); + m_names.remove(desktop); + } + m_rows = 2; + + endResetModel(); + + m_userModified = true; + updateModifiedState(); +} + +void DesktopsModel::load() +{ + beginResetModel(); + m_desktops = m_serverSideDesktops; + m_names = m_serverSideNames; + m_rows = m_serverSideRows; + endResetModel(); + + m_userModified = true; + updateModifiedState(); +} + +void DesktopsModel::getAllAndConnect(const QDBusMessage &msg) +{ + const QVariantMap &data = qdbus_cast(msg.arguments().at(0).value()); + + const KWin::DBusDesktopDataVector &desktops = qdbus_cast( + data.value(QStringLiteral("desktops")).value() + ); + + const int newServerSideRows = data.value(QStringLiteral("rows")).toUInt(); + QStringList newServerSideDesktops; + QHash newServerSideNames; + + for (const KWin::DBusDesktopDataStruct &d : desktops) { + newServerSideDesktops.append(d.id); + newServerSideNames[d.id] = d.name; + } + + // If the server-side state changed during a KWin restart, and the + // user had made notifications, the model should notify about the + // change. + if (m_serverSideDesktops != newServerSideDesktops + || m_serverSideNames != newServerSideNames + || m_serverSideRows != newServerSideRows) { + if (!m_serverSideDesktops.isEmpty() || m_userModified) { + m_serverModified = true; + emit serverModifiedChanged(); + } + + m_serverSideDesktops = newServerSideDesktops; + m_serverSideNames = newServerSideNames; + m_serverSideRows = newServerSideRows; + } + + // For the case KWin restarts while the KCM was open: If the user had + // made no modifications, just reset to the server data. E.g. perhaps + // the user intentionally nuked the KWin config while it was down, so + // we should follow. + if (!m_userModified || m_desktops.empty()) { + beginResetModel(); + m_desktops = m_serverSideDesktops; + m_names = m_serverSideNames; + m_rows = m_serverSideRows; + endResetModel(); + } + + emit readyChanged(); + + auto handleConnectionError = [this]() { + m_error = i18n("There was an error connecting to the compositor."); + emit errorChanged(); + }; + + bool connected = QDBusConnection::sessionBus().connect( + s_serviceName, + s_virtDesktopsPath, + s_virtualDesktopsInterface, + QStringLiteral("desktopCreated"), + this, + SLOT(desktopCreated(QString,KWin::DBusDesktopDataStruct))); + + if (!connected) { + handleConnectionError(); + + return; + } + + connected = QDBusConnection::sessionBus().connect( + s_serviceName, + s_virtDesktopsPath, + s_virtualDesktopsInterface, + QStringLiteral("desktopRemoved"), + this, + SLOT(desktopRemoved(QString))); + + if (!connected) { + handleConnectionError(); + + return; + } + + connected = QDBusConnection::sessionBus().connect( + s_serviceName, + s_virtDesktopsPath, + s_virtualDesktopsInterface, + QStringLiteral("desktopDataChanged"), + this, + SLOT(desktopDataChanged(QString,KWin::DBusDesktopDataStruct))); + + if (!connected) { + handleConnectionError(); + + return; + } + + connected = QDBusConnection::sessionBus().connect( + s_serviceName, + s_virtDesktopsPath, + s_virtualDesktopsInterface, + QStringLiteral("rowsChanged"), + this, + SLOT(desktopRowsChanged(uint))); + + if (!connected) { + handleConnectionError(); + + return; + } +} + +void DesktopsModel::desktopCreated(const QString &id, const KWin::DBusDesktopDataStruct &data) +{ + m_serverSideDesktops.insert(data.position, id); + m_serverSideNames[data.id] = data.name; + + // If the user didn't make any changes, we can just stay in sync. + if (!m_userModified) { + beginInsertRows(QModelIndex(), data.position, data.position); + + m_desktops = m_serverSideDesktops; + m_names = m_serverSideNames; + + endInsertRows(); + } else { + // Remove dummy data. + const QString dummyId = m_desktops.at(data.position); + m_desktops[data.position] = id; + m_names.remove(dummyId); + m_names[id] = data.name; + const QModelIndex &idx = index(data.position, 0); + emit dataChanged(idx, idx, QVector{Id}); + + updateModifiedState(/* server */ true); + } +} + +void DesktopsModel::desktopRemoved(const QString &id) +{ + const int desktopIndex = m_serverSideDesktops.indexOf(id); + + m_serverSideDesktops.removeAt(desktopIndex); + m_serverSideNames.remove(id); + + // If the user didn't make any changes, we can just stay in sync. + if (!m_userModified) { + beginRemoveRows(QModelIndex(), desktopIndex, desktopIndex); + + m_desktops = m_serverSideDesktops; + m_names = m_serverSideNames; + + endRemoveRows(); + } else { + updateModifiedState(/* server */ true); + } +} + +void DesktopsModel::desktopDataChanged(const QString &id, const KWin::DBusDesktopDataStruct &data) +{ + const int desktopIndex = m_serverSideDesktops.indexOf(id); + + m_serverSideDesktops[desktopIndex] = id; + m_serverSideNames[id] = data.name; + + // If the user didn't make any changes, we can just stay in sync. + if (!m_userModified) { + m_desktops = m_serverSideDesktops; + m_names = m_serverSideNames; + + const QModelIndex &idx = index(desktopIndex, 0); + + dataChanged(idx, idx, QVector{Qt::DisplayRole}); + } else { + updateModifiedState(/* server */ true); + } +} + +void DesktopsModel::desktopRowsChanged(uint rows) +{ + // Unfortunately we sometimes get this signal from the server with an unchanged value. + if ((int)rows == m_serverSideRows) { + return; + } + + m_serverSideRows = rows; + + // If the user didn't make any changes, we can just stay in sync. + if (!m_userModified) { + m_rows = m_serverSideRows; + + emit rowsChanged(); + emit dataChanged(index(0, 0), index(m_desktops.count() - 1, 0), QVector{DesktopRow}); + } else { + updateModifiedState(/* server */ true); + } +} + +void DesktopsModel::updateModifiedState(bool server) +{ + // Count is the same but contents are not: The user may have + // removed and created new desktops in the UI, but there were + // no changes to send to the server because number and names + // have remained the same. In that case we can just clean + // that up here. + if (m_desktops.count() == m_serverSideDesktops.count() + && m_desktops != m_serverSideDesktops) { + + for (int i = 0; i < m_serverSideDesktops.count(); ++i) { + const QString oldId = m_desktops.at(i); + const QString &newId = m_serverSideDesktops.at(i); + m_desktops[i] = newId; + m_names[newId] = m_names.take(oldId); + } + + emit dataChanged(index(0, 0), index(rowCount() - 1, 0), QVector{Qt::DisplayRole}); + } + + if (m_desktops == m_serverSideDesktops + && m_names == m_serverSideNames + && m_rows == m_serverSideRows) { + + m_userModified = false; + emit userModifiedChanged(); + + m_serverModified = false; + emit serverModifiedChanged(); + + m_synchronizing = false; + } else { + if (m_synchronizing) { + m_serverModified = false; + emit serverModifiedChanged(); + + syncWithServer(); + } else if (server) { + m_serverModified = true; + emit serverModifiedChanged(); + } else { + m_userModified = true; + emit userModifiedChanged(); + } + } +} + +void DesktopsModel::handleCallError() +{ + if (m_synchronizing) { + m_synchronizing = false; + + m_serverModified = false; + emit serverModifiedChanged(); + + m_error = i18n("There was an error saving the settings to the compositor."); + emit errorChanged(); + } else { + m_error = i18n("There was an error requesting information from the compositor."); + emit errorChanged(); + } +} + +} diff --git a/kcmkwin/kwindesktop/desktopsmodel.h b/kcmkwin/kwindesktop/desktopsmodel.h new file mode 100644 index 0000000..95ee0fa --- /dev/null +++ b/kcmkwin/kwindesktop/desktopsmodel.h @@ -0,0 +1,123 @@ +/* + SPDX-FileCopyrightText: 2018 Eike Hein + SPDX-FileCopyrightText: 2018 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef DESKTOPSMODEL_H +#define DESKTOPSMODEL_H + +#include + +#include "../virtualdesktopsdbustypes.h" + +class QDBusArgument; +class QDBusMessage; +class QDBusServiceWatcher; + + +namespace KWin +{ + +/** + * @short An item model around KWin's D-Bus API for virtual desktops. + * + * The model initially gets the state from KWin and populates. + * + * As long as the user makes no changes, KWin-side changes are directly + * exposed in the model. + * + * If the user makes changes (see the `userModified` property), it stops + * exposing KWin-side changes live, but it keeps track of the KWin-side + * changes, so it can figure out and apply the delta when `syncWithServer` + * is called. + * + * When KWin-side changes happen while the model is user-modified, the + * model signals this via the `serverModified` property. A call to + * `syncWithServer` will overwrite the KWin-side changes. + * + * After synchronization, the model tracks Kwin-side changes again, + * until the user makes further changes. + * + * @author Eike Hein + */ + +class DesktopsModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(bool ready READ ready NOTIFY readyChanged) + Q_PROPERTY(QString error READ error NOTIFY errorChanged) + Q_PROPERTY(bool userModified READ userModified NOTIFY userModifiedChanged) + Q_PROPERTY(bool serverModified READ serverModified NOTIFY serverModifiedChanged) + Q_PROPERTY(int rows READ rows WRITE setRows NOTIFY rowsChanged) + +public: + enum AdditionalRoles { + Id = Qt::UserRole + 1, + DesktopRow + }; + Q_ENUM(AdditionalRoles) + + explicit DesktopsModel(QObject *parent = nullptr); + ~DesktopsModel() override; + + QHash roleNames() const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex &parent = {}) const override; + + bool ready() const; + QString error() const; + + bool userModified() const; + bool serverModified() const; + + int rows() const; + void setRows(int rows); + + Q_INVOKABLE void createDesktop(const QString &name); + Q_INVOKABLE void removeDesktop(const QString &id); + Q_INVOKABLE void setDesktopName(const QString &id, const QString &name); + + Q_INVOKABLE void syncWithServer(); + + bool needsSave() const; + void load(); + void defaults(); + bool isDefaults() const; + +Q_SIGNALS: + void readyChanged() const; + void errorChanged() const; + void userModifiedChanged() const; + void serverModifiedChanged() const; + void rowsChanged() const; + +protected Q_SLOTS: + void reset(); + void getAllAndConnect(const QDBusMessage &msg); + void desktopCreated(const QString &id, const KWin::DBusDesktopDataStruct &data); + void desktopRemoved(const QString &id); + void desktopDataChanged(const QString &id, const KWin::DBusDesktopDataStruct &data); + void desktopRowsChanged(uint rows); + void updateModifiedState(bool server = false); + void handleCallError(); + +private: + QDBusServiceWatcher *m_serviceWatcher; + QString m_error; + bool m_userModified; + bool m_serverModified; + QStringList m_serverSideDesktops; + QHash m_serverSideNames; + int m_serverSideRows; + QStringList m_desktops; + QHash m_names; + int m_rows; + bool m_synchronizing; +}; + +} + +#endif diff --git a/kcmkwin/kwindesktop/kcm_kwin_virtualdesktops.desktop b/kcmkwin/kwindesktop/kcm_kwin_virtualdesktops.desktop new file mode 100644 index 0000000..80c94f9 --- /dev/null +++ b/kcmkwin/kwindesktop/kcm_kwin_virtualdesktops.desktop @@ -0,0 +1,157 @@ +[Desktop Entry] +Exec=kcmshell5 kcm_kwin_virtualdesktops +Icon=preferences-desktop-virtual +Type=Service +X-KDE-ServiceTypes=KCModule +X-DocPath=kcontrol/kwin_virtualdesktops/index.html + +X-KDE-Library=kcm_kwin_virtualdesktops +X-KDE-ParentApp=kcontrol +X-KDE-FormFactors=desktop + +X-KDE-System-Settings-Parent-Category=desktopbehavior +X-KDE-Weight=60 + +Name=Virtual Desktops +Name[ar]=أسطح المكتب الافتراضية +Name[ast]=Escritorios virtuales +Name[az]=Virtual İş Masaları +Name[bg]=Виртуални работни плотове +Name[bs]=Virtuelne površi +Name[ca]=Escriptoris virtuals +Name[ca@valencia]=Escriptoris virtuals +Name[cs]=Virtuální plochy +Name[da]=Virtuelle skriveborde +Name[de]=Virtuelle Arbeitsflächen +Name[el]=Εικονικές επιφάνειες εργασίες +Name[en_GB]=Virtual Desktops +Name[es]=Escritorios virtuales +Name[et]=Virtuaalsed töölauad +Name[eu]=Alegiazko mahaigaina +Name[fi]=Virtuaalityöpöydät +Name[fr]=Bureaux virtuels +Name[ga]=Deasca Fíorúla +Name[gl]=Escritorios virtuais +Name[gu]=વર્ચ્યુઅલ ડેસ્કટોપો +Name[he]=שולחנות עבודה וירטואליים +Name[hi]=आभासी डेस्कटॉप +Name[hr]=Virtualne radne površine +Name[hu]=Virtuális asztalok +Name[ia]=Scriptorios virtual +Name[id]=Desktop Virtual +Name[is]=Sýndarskjáborð +Name[it]=Desktop virtuali +Name[ja]=仮想デスクトップ +Name[kk]=Виртуалды Үстелдер +Name[km]=ផ្ទៃតុ​និម្មិត +Name[kn]=ವಾಸ್ತವಪ್ರಾಯ ಗಣಕತೆರೆಗಳು +Name[ko]=가상 바탕 화면 +Name[lt]=Virtualūs darbalaukiai +Name[lv]=Virtuālās darbvirsmas +Name[mr]=आभासी डेस्कटॉप +Name[nb]=Virtuelle skrivebord +Name[nds]=Mehr Schriefdischen +Name[nl]=Virtuele bureaubladen +Name[nn]=Virtuelle skrivebord +Name[pa]=ਵਰਚੁਅਲ ਡੈਸਕਟਾਪ +Name[pl]=Pulpity wirtualne +Name[pt]=Ecrãs Virtuais +Name[pt_BR]=Áreas de trabalho virtuais +Name[ro]=Birouri virtuale +Name[ru]=Рабочие столы +Name[si]=අත්ථ්‍ය වැඩතල +Name[sk]=Virtuálne pracovné plochy +Name[sl]=Navidezna namizja +Name[sr]=Виртуелне површи +Name[sr@ijekavian]=Виртуелне површи +Name[sr@ijekavianlatin]=Virtuelne površi +Name[sr@latin]=Virtuelne površi +Name[sv]=Virtuella skrivbord +Name[th]=พื้นที่ทำงานเสมือน +Name[tr]=Sanal Masaüstleri +Name[ug]=مەۋھۇم ئۈستەلئۈستى +Name[uk]=Віртуальні стільниці +Name[wa]=Forveyous scribannes +Name[x-test]=xxVirtual Desktopsxx +Name[zh_CN]=虚拟桌面 +Name[zh_TW]=虛擬桌面 + +Comment=Configure navigation, number and layout of virtual desktops +Comment[az]=Virtual İş Masasının nömrəsi, qatı və istiqamətinin tənzimlənməsi +Comment[ca]=Configura la navegació, el nombre i la disposició dels escriptoris virtuals +Comment[da]=Indstil navigation, antal og layout af virtuelle skriveborde +Comment[de]=Navigation, Anzahl und Layout virtueller Arbeitsflächen einrichten +Comment[en_GB]=Configure navigation, number and layout of virtual desktops +Comment[es]=Configurar la navegación, número y disposición de los escritorios virtuales +Comment[et]=Virtuaalsete töölaudade vahel liikumise, nende arvu ja paigutuse seadistamine +Comment[eu]=Konfiguratu nabigatzea, alegiazko mahaigainen kopurua eta antolamendua +Comment[fi]=Aseta virtuaalityöpöytien määrä, asettelu ja niiden välillä siirtyminen +Comment[fr]=Configurer la navigation, le nombre et la disposition des bureaux virtuels +Comment[gl]=Configurar a navegación, cantidade e disposición dos escritorios virtuais +Comment[ia]=Configura navigation, numero e disposition de scriptorios virtual +Comment[id]=Konfigurasikan navigasi, nomor dan tataletak desktop virtual +Comment[it]=Configura navigazione, numero e disposizione dei desktop virtuali +Comment[ko]=가상 바탕 화면 탐색, 개수, 레이아웃 설정 +Comment[lt]=Konfigūruoti virtualių darbalaukių naršymą, skaičių ir išdėstymą +Comment[nl]=Navigatie, aantal en indeling van virtuele bureaubladen configureren +Comment[nn]=Set opp navigering, nummer og vising av virtuelle skrivebord +Comment[pl]=Ustawienia poruszania się, liczby oraz układu wirtualnych klawiatur +Comment[pt]=Configura a navegação, número e disposição dos ecrãs virtuais +Comment[pt_BR]=Configura a navegação, quantidade e layout das áreas de trabalho virtuais +Comment[ro]=Configurează navigarea, numărul și aranjamentul birourilor virtuale +Comment[ru]=Число, расположение и способ переключения рабочих столов +Comment[sk]=Nastaviť navigáciu, počet a rozloženie virtuálnych plôch +Comment[sl]=Nastavi krmarjenje, število in razporeditev navideznih namizij +Comment[sv]=Anpassa navigering, antal och layout av virtuella skrivbord +Comment[uk]=Налаштовування навігації, кількості та компонування віртуальних стільниць +Comment[x-test]=xxConfigure navigation, number and layout of virtual desktopsxx +Comment[zh_CN]=配置虚拟桌面的导航,数目和布局 +Comment[zh_TW]=設定虛擬桌面的導覽、數字與佈局 +X-KDE-Keywords=desktop,desktops,number,virtual desktop,multiple desktops,pager,pager widget,pager applet,pager settings +X-KDE-Keywords[az]=iş masası,iş masaları,sayı,nömrəsi,virtual iş masası,bir neçə iş masası,səhifə,səhifələmə vidjeti,səhifələmə əlavəsi,səhifələmə ayarları +X-KDE-Keywords[bs]=pozadina,pozadine,broj,virtuelna pozadina,višestruka pozadina,pejdžer,dodatak pejdžeru,aplet pejdžer,pejdžer postavke +X-KDE-Keywords[ca]=escriptori,escriptoris,nombre,escriptori virtual,múltiples escriptoris,paginador,giny paginador,miniaplicació de paginació,arranjament del paginador +X-KDE-Keywords[ca@valencia]=escriptori,escriptoris,nombre,escriptori virtual,escriptoris múltiples,paginador,estri paginador,miniaplicació de paginació,arranjament de paginador +X-KDE-Keywords[da]=skrivebord,skriveborde,desktop,desktops,virtuelt skrivebord,flere skriveborde,spaces,pager,skrivebordsvælger,pager widget,pager applet +X-KDE-Keywords[de]=Arbeitsfläche,Arbeitsflächen,Desktop,Anzahl,Virtuelle Arbeitsfläche,Mehrere Arbeitsflächen,Arbeitsflächenumschalter,Arbeitsflächenumschalter-Bedienelement,Arbeitsflächenumschalter-Miniprogramm,Arbeitsflächenumschalter-Einstellungen +X-KDE-Keywords[el]=επιφάνεια εργασίας,επιφάνειες εργασίας,αριθμός,εικονική επιφάνεια εργασίας,πολλαπλές επιφάνειες εργασίας,χαρτί,γραφικό συστατικό χαρτιού,μικροεφαρμογή χαρτιού,ρυθμίσεις χαρτιού +X-KDE-Keywords[en_GB]=desktop,desktops,number,virtual desktop,multiple desktops,pager,pager widget,pager applet,pager settings +X-KDE-Keywords[es]=escritorio,escritorios,número,escritorio virtual,múltiples escritorios,paginador,control de paginación,miniaplicación del paginador,preferencias del paginador +X-KDE-Keywords[et]=töölaud,töölauad,arv,virtuaalne töölaud,mitu töölauda,töölauavahetaja,töölaudade vahetaja,töölauavahetaja aplett,töölauavahetaja vidin,töölauavahetaja seadistused +X-KDE-Keywords[eu]=mahaigain,mahaigainak,kopuru,mahaigain birtuala,alegiazko mahaigaina,hainbat mahaigain,bilagailu,bilagailuaren trepeta,bilagailuaren miniaplikazioa,bilagailuaren ezarpenak +X-KDE-Keywords[fi]=työpöytä,työpöydät,lukumäärä,virtuaalityöpöytä,monta työpöytää,sivutin,sivutinsovelma,sivuttimen asetukset +X-KDE-Keywords[fr]=bureau, bureaux, numéro, bureau virtuel, bureaux multiples, gestionnaire de bureau, composant graphique du gestionnaire de bureau, paramètres du gestionnaire de bureaux +X-KDE-Keywords[gl]=escritorio,escritorios,número,escritorio virtual,escritorios múltiplos,paxinador, trebello paxinador, miniaplicativo paxinador,configuración do paxinador +X-KDE-Keywords[hu]=asztal,asztalok,szám,virtuális asztal,több asztal,papír,papír felületi elem,papír kisalkalmazás,papírbeállítások +X-KDE-Keywords[ia]=scriptorio,scriptorios,numero,scriptorio virtual,scriptorio multiple,pager, widget de pager, applet de pager, preferentias de pager +X-KDE-Keywords[id]=desktop,desktop,jumlah,desktop virtual,banyak desktop,halaman,widget halaman,applet halaman,pengaturan halaman +X-KDE-Keywords[it]=desktop,numero,desktop virtuali,desktop multipli,cambiadesktop,oggetto cambiadesktop,applet cambiadesktop,impostazioni del cambiadesktop +X-KDE-Keywords[kk]=desktop,desktops,number,virtual desktop,multiple desktops,pager,pager widget,pager applet,pager settings +X-KDE-Keywords[km]=desktop,desktops,number,virtual desktop,multiple desktops,pager,pager widget,pager applet,pager settings +X-KDE-Keywords[ko]=desktop,desktops,number,virtual desktop,multiple desktops,pager,pager widget,pager applet,pager settings,데스크톱,가상 데스크톱,다중 데스크톱,바탕 화면,가상 바탕 화면,다중 바탕 화면 +X-KDE-Keywords[lt]=darbalaukis,darbastalis,darbalaukiai,darbastaliai,skaičius,numeris,numeriai,skaičiai,skaicius,skaiciai,virtualus darbalaukis,virtualūs darbalaukiai,virtualus darbalaukiai,keli darbalaukiai,keletas darbalaukių,keletas darbalaukiu,puslapiuotojas,perjungiklis,perjungiklio valdiklis,perjungiklio programėlė,perjungiklio programele,perjungiklio nuostatos,perjungiklio nustatymai +X-KDE-Keywords[nb]=skrivebord,antall,virtuelt skrivebord,flere skrivebord,veksler,vekslerelement,veksler-miniprogram,vekslerinnstillinger +X-KDE-Keywords[nds]=Schriefdisch,Schriefdischen,virtuell,mehr,Schriefdisch-Översicht,instellen +X-KDE-Keywords[nl]=bureaublad,bureaubladen,aantal,virtueel bureaublad,meervoudige bureaubladen,pager,pager-widget,pager-applet,pagerinstellingen +X-KDE-Keywords[nn]=skrivebord,mengd,tal,virtuelt skrivebord,fleire skrivebord,vekslar,vekslarelement,vekslarelement,vekslerinnstillinger,vekslaroppsett +X-KDE-Keywords[pa]=ਡੈਸਕਟਾਪ,ਗਿਣਤੀ,ਨੰਬਰ,ਅੰਕ,ਵਰਚੁਅਲ ਡੈਸਕਟਾਪ,ਕਈ ਡੈਸਕਟਾਪ,ਪੇਜ਼ਰ,ਪੇਜ਼ਰ ਵਿਜੈਟ,ਪੇਜ਼ਰ ਐਪਲਿਟ,ਪੇਜ਼ਰ ਸੈਟਿੰਗਾਂ +X-KDE-Keywords[pl]=pulpit,pulpity,liczba,pulpity wirtualne,wiele pulpitów +X-KDE-Keywords[pt]=ecrã,ecrãs,número,ecrã virtual,múltiplos ecrãs,paginador,elemento paginador,'applet' do paginador,configuração do paginador +X-KDE-Keywords[pt_BR]=área de trabalho,áreas de trabalho,desktop,desktops,número,área de trabalho virtual,múltiplas áreas de trabalho,paginador,elemento paginador,miniaplicativo do paginador,configurações do paginador +X-KDE-Keywords[ro]=birou,birouri,număr,birou virtual,birouri multiple,paginator,control paginator,miniaplicație paginator,configurare paginator,paginare +X-KDE-Keywords[ru]=desktop,desktops,number,virtual desktop,multiple desktops,pager,pager widget,pager applet,pager settings,рабочий стол,рабочие столы,число,виртуальный рабочий стол,несколько рабочих столов,переключатель,переключение,виджет переключения,аплет переключения,параметры переключения,настройки переключения +X-KDE-Keywords[sk]=plocha,plochy,číslo,virtuálna plocha,viac plôch,pager,widget pagera,applet pagera,nastavenia pagera +X-KDE-Keywords[sl]=namizje,namizja,število namizij,navidezna namizja,več namizij,pozivnik +X-KDE-Keywords[sr]=desktop,desktops,number,virtual desktop,multiple desktops,pager,pager widget,pager applet,pager settings,површ,број,виртуелна површ,више површи,листач,виџет листача,аплет листача,поставке листача +X-KDE-Keywords[sr@ijekavian]=desktop,desktops,number,virtual desktop,multiple desktops,pager,pager widget,pager applet,pager settings,површ,број,виртуелна површ,више површи,листач,виџет листача,аплет листача,поставке листача +X-KDE-Keywords[sr@ijekavianlatin]=desktop,desktops,number,virtual desktop,multiple desktops,pager,pager widget,pager applet,pager settings,površ,broj,virtuelna površ,više površi,listač,vidžet listača,aplet listača,postavke listača +X-KDE-Keywords[sr@latin]=desktop,desktops,number,virtual desktop,multiple desktops,pager,pager widget,pager applet,pager settings,površ,broj,virtuelna površ,više površi,listač,vidžet listača,aplet listača,postavke listača +X-KDE-Keywords[sv]=skrivbord,antal,virtuellt skrivbord,flera skrivbord,skrivbordsvisning,visningskomponent,visningsminiprogram,visningsinställningar +X-KDE-Keywords[tr]=masaüstü,masaüstleri,sayı,sanal masaüstü,çoklu masaüstü,sayfalayıcı,sayfalayıcı gereci,sayfalayıcı gereci,sayfalayıcı ayarları +X-KDE-Keywords[uk]=desktop,desktops,number,virtual desktop,multiple desktops,pager,pager widget,pager applet,pager settings,стільниця,стільниці,кількість,віртуальна стільниця,перемикач,пейджер,віджет перемикача,віджет пейджера,аплет перемикання,аплет перемикача,параметри перемикання,параметри перемикача +X-KDE-Keywords[x-test]=xxdesktopxx,xxdesktopsxx,xxnumberxx,xxvirtual desktopxx,xxmultiple desktopsxx,xxpagerxx,xxpager widgetxx,xxpager appletxx,xxpager settingsxx +X-KDE-Keywords[zh_CN]=desktop,desktops,number,virtual desktop,multiple desktops,pager,pager widget,pager applet,pager settings,桌面,虚拟桌面,多桌面,分页,分页器,分页器组件,分页器设置 +X-KDE-Keywords[zh_TW]=desktop,desktops,number,virtual desktop,multiple desktops,pager,pager widget,pager applet,pager settings + + +Categories=Qt;KDE;X-KDE-settings-translations; diff --git a/kcmkwin/kwindesktop/package/contents/ui/main.qml b/kcmkwin/kwindesktop/package/contents/ui/main.qml new file mode 100644 index 0000000..d18ac3c --- /dev/null +++ b/kcmkwin/kwindesktop/package/contents/ui/main.qml @@ -0,0 +1,264 @@ +/* + SPDX-FileCopyrightText: 2018 Eike Hein + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.5 +import QtQuick.Controls 2.5 as QQC2 +import QtQuick.Layouts 1.1 + +import org.kde.kcm 1.2 +import org.kde.kirigami 2.10 as Kirigami +import org.kde.plasma.core 2.1 as PlasmaCore + +ScrollViewKCM { + id: root + + ConfigModule.quickHelp: i18n("This module lets you configure the navigation, number and layout of virtual desktops.") + + Connections { + target: kcm.desktopsModel + + onReadyChanged: { + rowsSpinBox.value = kcm.desktopsModel.rows; + } + + onRowsChanged: { + rowsSpinBox.value = kcm.desktopsModel.rows; + } + } + implicitWidth: Kirigami.Units.gridUnit * 35 + implicitHeight: Kirigami.Units.gridUnit * 30 + + Component { + id: desktopsListItemComponent + + Kirigami.SwipeListItem { + id: listItem + + contentItem: RowLayout { + QQC2.TextField { + id: nameField + + background: null + leftPadding: Kirigami.Units.largeSpacing + topPadding: 0 + bottomPadding: 0 + + Layout.fillWidth: true + + Layout.alignment: Qt.AlignVCenter + + text: model.display + + readOnly: true + + onEditingFinished: { + readOnly = true; + Qt.callLater(kcm.desktopsModel.setDesktopName, model.Id, text); + } + } + } + + actions: [ + Kirigami.Action { + enabled: !model.IsMissing + iconName: "edit-rename" + tooltip: i18nc("@info:tooltip", "Rename") + onTriggered: { + nameField.readOnly = false; + nameField.selectAll(); + nameField.forceActiveFocus(); + } + }, + Kirigami.Action { + enabled: !model.IsMissing + iconName: "edit-delete-remove" + tooltip: i18nc("@info:tooltip", "Remove") + onTriggered: kcm.desktopsModel.removeDesktop(model.Id) + }] + } + } + + header: ColumnLayout { + id: messagesLayout + + spacing: Kirigami.Units.largeSpacing + + Kirigami.InlineMessage { + Layout.fillWidth: true + + type: Kirigami.MessageType.Error + + text: kcm.desktopsModel.error + + visible: kcm.desktopsModel.error != "" + } + + Kirigami.InlineMessage { + Layout.fillWidth: true + + type: Kirigami.MessageType.Information + + text: i18n("Virtual desktops have been changed outside this settings application. Saving now will overwrite the changes.") + + visible: kcm.desktopsModel.serverModified + } + } + + view: ListView { + id: desktopsList + + model: kcm.desktopsModel.ready ? kcm.desktopsModel : null + + section.property: "DesktopRow" + section.delegate: Kirigami.ListSectionHeader { + width: desktopsList.width + label: i18n("Row %1", section) + } + + delegate: Kirigami.DelegateRecycler { + width: desktopsList.width + + sourceComponent: desktopsListItemComponent + } + } + + footer: ColumnLayout { + RowLayout { + QQC2.Button { + text: i18nc("@action:button", "Add") + icon.name: "list-add" + + onClicked: kcm.desktopsModel.createDesktop(i18n("New Desktop")) + } + + Item { // Spacer + Layout.fillWidth: true + } + + QQC2.SpinBox { + id: rowsSpinBox + + from: 1 + to: 20 + editable: true + + textFromValue: function(value, locale) { return i18np("1 Row", "%1 Rows", value)} + valueFromText: function(text, locale) { return parseInt(text, 10); } + + onValueModified: kcm.desktopsModel.rows = value + } + } + + Kirigami.FormLayout { + + QQC2.CheckBox { + id: navWraps + + Kirigami.FormData.label: i18n("Options:") + + text: i18n("Navigation wraps around") + enabled: !kcm.virtualDesktopsSettings.isImmutable("rollOverDesktops") + checked: kcm.virtualDesktopsSettings.rollOverDesktops + onToggled: kcm.virtualDesktopsSettings.rollOverDesktops = checked + } + + RowLayout { + Layout.fillWidth: true + + QQC2.CheckBox { + id: animationEnabled + // Let it elide but don't make it push the ComboBox away from it + Layout.fillWidth: true + Layout.maximumWidth: implicitWidth + + text: i18n("Show animation when switching:") + + checked: kcm.animationsModel.enabled + + onToggled: kcm.animationsModel.enabled = checked + } + + QQC2.ComboBox { + enabled: animationEnabled.checked + + model: kcm.animationsModel + textRole: "NameRole" + currentIndex: kcm.animationsModel.currentIndex + onActivated: kcm.animationsModel.currentIndex = currentIndex + } + + QQC2.Button { + enabled: animationEnabled.checked && kcm.animationsModel.currentConfigurable + + icon.name: "configure" + + onClicked: kcm.configureAnimation() + } + + QQC2.Button { + enabled: animationEnabled.checked + + icon.name: "dialog-information" + + onClicked: kcm.showAboutAnimation() + } + + Item { + Layout.fillWidth: true + } + } + + RowLayout { + Layout.fillWidth: true + + QQC2.CheckBox { + id: osdEnabled + + text: i18n("Show on-screen display when switching:") + + enabled: !kcm.virtualDesktopsSettings.isImmutable("desktopChangeOsdEnabled") + + checked: kcm.virtualDesktopsSettings.desktopChangeOsdEnabled + + onToggled: kcm.virtualDesktopsSettings.desktopChangeOsdEnabled = checked + } + + QQC2.SpinBox { + id: osdDuration + + enabled: osdEnabled.checked && !kcm.virtualDesktopsSettings.isImmutable("popupHideDelay") + + from: 0 + to: 10000 + stepSize: 100 + + textFromValue: function(value, locale) { return i18n("%1 ms", value)} + + value: kcm.virtualDesktopsSettings.popupHideDelay + + onValueModified: kcm.virtualDesktopsSettings.popupHideDelay = value + } + } + + RowLayout { + Layout.fillWidth: true + + Item { + width: units.largeSpacing + } + + QQC2.CheckBox { + id: osdTextOnly + enabled: osdEnabled.checked && !kcm.virtualDesktopsSettings.isImmutable("textOnly") + text: i18n("Show desktop layout indicators") + checked: !kcm.virtualDesktopsSettings.textOnly + onToggled: kcm.virtualDesktopsSettings.textOnly = !checked + } + } + } + } +} + diff --git a/kcmkwin/kwindesktop/package/metadata.desktop b/kcmkwin/kwindesktop/package/metadata.desktop new file mode 100644 index 0000000..d57a2f4 --- /dev/null +++ b/kcmkwin/kwindesktop/package/metadata.desktop @@ -0,0 +1,106 @@ +[Desktop Entry] +Name=Virtual Desktops +Name[ar]=أسطح المكتب الافتراضية +Name[ast]=Escritorios virtuales +Name[az]=Virtual İş Masaları +Name[bg]=Виртуални работни плотове +Name[bs]=Virtuelne površi +Name[ca]=Escriptoris virtuals +Name[ca@valencia]=Escriptoris virtuals +Name[cs]=Virtuální plochy +Name[da]=Virtuelle skriveborde +Name[de]=Virtuelle Arbeitsflächen +Name[el]=Εικονικές επιφάνειες εργασίες +Name[en_GB]=Virtual Desktops +Name[es]=Escritorios virtuales +Name[et]=Virtuaalsed töölauad +Name[eu]=Alegiazko mahaigaina +Name[fi]=Virtuaalityöpöydät +Name[fr]=Bureaux virtuels +Name[ga]=Deasca Fíorúla +Name[gl]=Escritorios virtuais +Name[gu]=વર્ચ્યુઅલ ડેસ્કટોપો +Name[he]=שולחנות עבודה וירטואליים +Name[hi]=आभासी डेस्कटॉप +Name[hr]=Virtualne radne površine +Name[hu]=Virtuális asztalok +Name[ia]=Scriptorios virtual +Name[id]=Desktop Virtual +Name[is]=Sýndarskjáborð +Name[it]=Desktop virtuali +Name[ja]=仮想デスクトップ +Name[kk]=Виртуалды Үстелдер +Name[km]=ផ្ទៃតុ​និម្មិត +Name[kn]=ವಾಸ್ತವಪ್ರಾಯ ಗಣಕತೆರೆಗಳು +Name[ko]=가상 바탕 화면 +Name[lt]=Virtualūs darbalaukiai +Name[lv]=Virtuālās darbvirsmas +Name[mr]=आभासी डेस्कटॉप +Name[nb]=Virtuelle skrivebord +Name[nds]=Mehr Schriefdischen +Name[nl]=Virtuele bureaubladen +Name[nn]=Virtuelle skrivebord +Name[pa]=ਵਰਚੁਅਲ ਡੈਸਕਟਾਪ +Name[pl]=Pulpity wirtualne +Name[pt]=Ecrãs Virtuais +Name[pt_BR]=Áreas de trabalho virtuais +Name[ro]=Birouri virtuale +Name[ru]=Рабочие столы +Name[si]=අත්ථ්‍ය වැඩතල +Name[sk]=Virtuálne pracovné plochy +Name[sl]=Navidezna namizja +Name[sr]=Виртуелне површи +Name[sr@ijekavian]=Виртуелне површи +Name[sr@ijekavianlatin]=Virtuelne površi +Name[sr@latin]=Virtuelne površi +Name[sv]=Virtuella skrivbord +Name[th]=พื้นที่ทำงานเสมือน +Name[tr]=Sanal Masaüstleri +Name[ug]=مەۋھۇم ئۈستەلئۈستى +Name[uk]=Віртуальні стільниці +Name[wa]=Forveyous scribannes +Name[x-test]=xxVirtual Desktopsxx +Name[zh_CN]=虚拟桌面 +Name[zh_TW]=虛擬桌面 + +Comment=Configure navigation, number and layout of virtual desktops +Comment[az]=Virtual İş Masasının nömrəsi, qatı və istiqamətinin tənzimlənməsi +Comment[ca]=Configura la navegació, el nombre i la disposició dels escriptoris virtuals +Comment[da]=Indstil navigation, antal og layout af virtuelle skriveborde +Comment[de]=Navigation, Anzahl und Layout virtueller Arbeitsflächen einrichten +Comment[en_GB]=Configure navigation, number and layout of virtual desktops +Comment[es]=Configurar la navegación, número y disposición de los escritorios virtuales +Comment[et]=Virtuaalsete töölaudade vahel liikumise, nende arvu ja paigutuse seadistamine +Comment[eu]=Konfiguratu nabigatzea, alegiazko mahaigainen kopurua eta antolamendua +Comment[fi]=Aseta virtuaalityöpöytien määrä, asettelu ja niiden välillä siirtyminen +Comment[fr]=Configurer la navigation, le nombre et la disposition des bureaux virtuels +Comment[gl]=Configurar a navegación, cantidade e disposición dos escritorios virtuais +Comment[ia]=Configura navigation, numero e disposition de scriptorios virtual +Comment[id]=Konfigurasikan navigasi, nomor dan tataletak desktop virtual +Comment[it]=Configura navigazione, numero e disposizione dei desktop virtuali +Comment[ko]=가상 바탕 화면 탐색, 개수, 레이아웃 설정 +Comment[lt]=Konfigūruoti virtualių darbalaukių naršymą, skaičių ir išdėstymą +Comment[nl]=Navigatie, aantal en indeling van virtuele bureaubladen configureren +Comment[nn]=Set opp navigering, nummer og vising av virtuelle skrivebord +Comment[pl]=Ustawienia poruszania się, liczby oraz układu wirtualnych klawiatur +Comment[pt]=Configura a navegação, número e disposição dos ecrãs virtuais +Comment[pt_BR]=Configura a navegação, quantidade e layout das áreas de trabalho virtuais +Comment[ro]=Configurează navigarea, numărul și aranjamentul birourilor virtuale +Comment[ru]=Число, расположение и способ переключения рабочих столов +Comment[sk]=Nastaviť navigáciu, počet a rozloženie virtuálnych plôch +Comment[sl]=Nastavi krmarjenje, število in razporeditev navideznih namizij +Comment[sv]=Anpassa navigering, antal och layout av virtuella skrivbord +Comment[uk]=Налаштовування навігації, кількості та компонування віртуальних стільниць +Comment[x-test]=xxConfigure navigation, number and layout of virtual desktopsxx +Comment[zh_CN]=配置虚拟桌面的导航,数目和布局 +Comment[zh_TW]=設定虛擬桌面的導覽、數字與佈局 + +Icon=preferences-desktop +Type=Service +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-Name=kcm_kwin_virtualdesktops +X-KDE-ServiceTypes=Plasma/Generic +X-Plasma-API=declarativeappletscript +X-KDE-FormFactors=desktop + +X-Plasma-MainScript=ui/main.qml diff --git a/kcmkwin/kwindesktop/virtualdesktops.cpp b/kcmkwin/kwindesktop/virtualdesktops.cpp new file mode 100644 index 0000000..f9fa57f --- /dev/null +++ b/kcmkwin/kwindesktop/virtualdesktops.cpp @@ -0,0 +1,164 @@ +/* + SPDX-FileCopyrightText: 2018 Eike Hein + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "virtualdesktops.h" +#include "animationsmodel.h" +#include "desktopsmodel.h" +#include "virtualdesktopssettings.h" + +#include +#include +#include +#include + +K_PLUGIN_FACTORY_WITH_JSON(VirtualDesktopsFactory, "kcm_kwin_virtualdesktops.json", registerPlugin();) + +namespace KWin +{ + +VirtualDesktops::VirtualDesktops(QObject *parent, const QVariantList &args) + : KQuickAddons::ManagedConfigModule(parent, args) + , m_settings(new VirtualDesktopsSettings(this)) + , m_desktopsModel(new KWin::DesktopsModel(this)) + , m_animationsModel(new AnimationsModel(this)) +{ + KAboutData *about = new KAboutData(QStringLiteral("kcm_kwin_virtualdesktops"), + i18n("Virtual Desktops"), + QStringLiteral("2.0"), QString(), KAboutLicense::GPL); + setAboutData(about); + + qmlRegisterType(); + + setButtons(Apply | Default); + + QObject::connect(m_desktopsModel, &KWin::DesktopsModel::userModifiedChanged, + this, &VirtualDesktops::settingsChanged); + connect(m_animationsModel, &AnimationsModel::enabledChanged, + this, &VirtualDesktops::settingsChanged); + connect(m_animationsModel, &AnimationsModel::currentIndexChanged, + this, &VirtualDesktops::settingsChanged); +} + +VirtualDesktops::~VirtualDesktops() +{ +} + +QAbstractItemModel *VirtualDesktops::desktopsModel() const +{ + return m_desktopsModel; +} + +QAbstractItemModel *VirtualDesktops::animationsModel() const +{ + return m_animationsModel; +} + +VirtualDesktopsSettings *VirtualDesktops::virtualDesktopsSettings() const +{ + return m_settings; +} + +void VirtualDesktops::load() +{ + ManagedConfigModule::load(); + + m_desktopsModel->load(); + m_animationsModel->load(); +} + +void VirtualDesktops::save() +{ + ManagedConfigModule::save(); + + m_desktopsModel->syncWithServer(); + m_animationsModel->save(); + + QDBusMessage message = QDBusMessage::createSignal(QStringLiteral("/KWin"), + QStringLiteral("org.kde.KWin"), QStringLiteral("reloadConfig")); + QDBusConnection::sessionBus().send(message); +} + +void VirtualDesktops::defaults() +{ + ManagedConfigModule::defaults(); + + m_desktopsModel->defaults(); + m_animationsModel->defaults(); +} + +bool VirtualDesktops::isDefaults() const +{ + return m_animationsModel->isDefaults() && m_desktopsModel->isDefaults(); +} + +void VirtualDesktops::configureAnimation() +{ + const QModelIndex index = m_animationsModel->index(m_animationsModel->currentIndex(), 0); + if (!index.isValid()) { + return; + } + + m_animationsModel->requestConfigure(index, nullptr); +} + +void VirtualDesktops::showAboutAnimation() +{ + const QModelIndex index = m_animationsModel->index(m_animationsModel->currentIndex(), 0); + if (!index.isValid()) { + return; + } + + const QString name = index.data(AnimationsModel::NameRole).toString(); + const QString comment = index.data(AnimationsModel::DescriptionRole).toString(); + const QString author = index.data(AnimationsModel::AuthorNameRole).toString(); + const QString email = index.data(AnimationsModel::AuthorEmailRole).toString(); + const QString website = index.data(AnimationsModel::WebsiteRole).toString(); + const QString version = index.data(AnimationsModel::VersionRole).toString(); + const QString license = index.data(AnimationsModel::LicenseRole).toString(); + const QString icon = index.data(AnimationsModel::IconNameRole).toString(); + + const KAboutLicense::LicenseKey licenseType = KAboutLicense::byKeyword(license).key(); + + KAboutData aboutData( + name, // Plugin name + name, // Display name + version, // Version + comment, // Short description + licenseType, // License + QString(), // Copyright statement + QString(), // Other text + website.toLatin1() // Home page + ); + aboutData.setProgramLogo(icon); + + const QStringList authors = author.split(','); + const QStringList emails = email.split(','); + + if (authors.count() == emails.count()) { + int i = 0; + for (const QString &author : authors) { + if (!author.isEmpty()) { + aboutData.addAuthor(i18n(author.toUtf8()), QString(), emails[i]); + } + i++; + } + } + + QPointer aboutPlugin = new KAboutApplicationDialog(aboutData); + aboutPlugin->exec(); + + delete aboutPlugin; +} + +bool VirtualDesktops::isSaveNeeded() const +{ + return m_animationsModel->needsSave() || m_desktopsModel->needsSave(); +} + +} + +#include "virtualdesktops.moc" diff --git a/kcmkwin/kwindesktop/virtualdesktops.h b/kcmkwin/kwindesktop/virtualdesktops.h new file mode 100644 index 0000000..3cfef52 --- /dev/null +++ b/kcmkwin/kwindesktop/virtualdesktops.h @@ -0,0 +1,59 @@ +/* + SPDX-FileCopyrightText: 2018 Eike Hein + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef VIRTUALDESKTOPS_H +#define VIRTUALDESKTOPS_H + +#include +#include + +class VirtualDesktopsSettings; + +namespace KWin +{ + +class AnimationsModel; +class DesktopsModel; + +class VirtualDesktops : public KQuickAddons::ManagedConfigModule +{ + Q_OBJECT + + Q_PROPERTY(QAbstractItemModel* desktopsModel READ desktopsModel CONSTANT) + Q_PROPERTY(QAbstractItemModel *animationsModel READ animationsModel CONSTANT) + Q_PROPERTY(VirtualDesktopsSettings *virtualDesktopsSettings READ virtualDesktopsSettings CONSTANT) + +public: + explicit VirtualDesktops(QObject *parent = nullptr, const QVariantList &list = QVariantList()); + ~VirtualDesktops() override; + + QAbstractItemModel *desktopsModel() const; + + QAbstractItemModel *animationsModel() const; + + VirtualDesktopsSettings *virtualDesktopsSettings() const; + + bool isDefaults() const override; + bool isSaveNeeded() const override; + +public Q_SLOTS: + void load() override; + void save() override; + void defaults() override; + + void configureAnimation(); + void showAboutAnimation(); + +private: + VirtualDesktopsSettings *m_settings; + DesktopsModel *m_desktopsModel; + AnimationsModel *m_animationsModel; +}; + +} + +#endif diff --git a/kcmkwin/kwindesktop/virtualdesktopssettings.kcfg b/kcmkwin/kwindesktop/virtualdesktopssettings.kcfg new file mode 100644 index 0000000..7db898a --- /dev/null +++ b/kcmkwin/kwindesktop/virtualdesktopssettings.kcfg @@ -0,0 +1,29 @@ + + + + + + + true + + + + + + false + + + + + + 1000 + + + + false + + + diff --git a/kcmkwin/kwindesktop/virtualdesktopssettings.kcfgc b/kcmkwin/kwindesktop/virtualdesktopssettings.kcfgc new file mode 100644 index 0000000..469abde --- /dev/null +++ b/kcmkwin/kwindesktop/virtualdesktopssettings.kcfgc @@ -0,0 +1,6 @@ +File=virtualdesktopssettings.kcfg +ClassName=VirtualDesktopsSettings +Mutators=true +DefaultValueGetters=true +GenerateProperties=true +ParentInConstructor=true diff --git a/kcmkwin/kwineffects/CMakeLists.txt b/kcmkwin/kwineffects/CMakeLists.txt new file mode 100644 index 0000000..4f533b9 --- /dev/null +++ b/kcmkwin/kwineffects/CMakeLists.txt @@ -0,0 +1,35 @@ +include(ECMQMLModules) +ecm_find_qmlmodule(org.kde.plasma.core 2.0) + +# KI18N Translation Domain for this library. +add_definitions(-DTRANSLATION_DOMAIN=\"kcm_kwin_effects\") + +########### next target ############### + +set(kcm_kwin_effects_PART_SRCS + kcm.cpp + effectsfilterproxymodel.cpp +) + +add_library(kcm_kwin_effects MODULE ${kcm_kwin_effects_PART_SRCS}) + +target_link_libraries(kcm_kwin_effects + Qt5::DBus + + KF5::I18n + KF5::KCMUtils + KF5::NewStuff + KF5::QuickAddons + KF5::XmlGui + + kcmkwincommon +) + +kcoreaddons_desktop_to_json(kcm_kwin_effects "kcm_kwin_effects.desktop") + +########### install files ############### + +install(TARGETS kcm_kwin_effects DESTINATION ${KDE_INSTALL_PLUGINDIR}/kcms) +install(FILES kcm_kwin_effects.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}) +install(FILES kwineffect.knsrc DESTINATION ${KDE_INSTALL_KNSRCDIR}) +kpackage_install_package(package kcm_kwin_effects kcms) diff --git a/kcmkwin/kwineffects/Messages.sh b/kcmkwin/kwineffects/Messages.sh new file mode 100644 index 0000000..6ff6bf8 --- /dev/null +++ b/kcmkwin/kwineffects/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name \*.cpp -o -name \*.qml` -o $podir/kcm_kwin_effects.pot diff --git a/kcmkwin/kwineffects/effectsfilterproxymodel.cpp b/kcmkwin/kwineffects/effectsfilterproxymodel.cpp new file mode 100644 index 0000000..6b212e3 --- /dev/null +++ b/kcmkwin/kwineffects/effectsfilterproxymodel.cpp @@ -0,0 +1,93 @@ +/* + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "effectsfilterproxymodel.h" + +#include "effectsmodel.h" + +namespace KWin +{ + +EffectsFilterProxyModel::EffectsFilterProxyModel(QObject *parent) + : QSortFilterProxyModel(parent) +{ +} + +EffectsFilterProxyModel::~EffectsFilterProxyModel() +{ +} + +QString EffectsFilterProxyModel::query() const +{ + return m_query; +} + +void EffectsFilterProxyModel::setQuery(const QString &query) +{ + if (m_query != query) { + m_query = query; + emit queryChanged(); + invalidateFilter(); + } +} + +bool EffectsFilterProxyModel::excludeInternal() const +{ + return m_excludeInternal; +} + +void EffectsFilterProxyModel::setExcludeInternal(bool exclude) +{ + if (m_excludeInternal != exclude) { + m_excludeInternal = exclude; + emit excludeInternalChanged(); + invalidateFilter(); + } +} + +bool EffectsFilterProxyModel::excludeUnsupported() const +{ + return m_excludeUnsupported; +} + +void EffectsFilterProxyModel::setExcludeUnsupported(bool exclude) +{ + if (m_excludeUnsupported != exclude) { + m_excludeUnsupported = exclude; + emit excludeUnsupportedChanged(); + invalidateFilter(); + } +} + +bool EffectsFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + const QModelIndex idx = sourceModel()->index(sourceRow, 0, sourceParent); + + if (!m_query.isEmpty()) { + const bool matches = idx.data(EffectsModel::NameRole).toString().contains(m_query, Qt::CaseInsensitive) || + idx.data(EffectsModel::DescriptionRole).toString().contains(m_query, Qt::CaseInsensitive) || + idx.data(EffectsModel::CategoryRole).toString().contains(m_query, Qt::CaseInsensitive); + if (!matches) { + return false; + } + } + + if (m_excludeInternal) { + if (idx.data(EffectsModel::InternalRole).toBool()) { + return false; + } + } + + if (m_excludeUnsupported) { + if (!idx.data(EffectsModel::SupportedRole).toBool()) { + return false; + } + } + + return true; +} + +} // namespace KWin diff --git a/kcmkwin/kwineffects/effectsfilterproxymodel.h b/kcmkwin/kwineffects/effectsfilterproxymodel.h new file mode 100644 index 0000000..16af9b7 --- /dev/null +++ b/kcmkwin/kwineffects/effectsfilterproxymodel.h @@ -0,0 +1,51 @@ +/* + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +namespace KWin +{ + +class EffectsFilterProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + Q_PROPERTY(QAbstractItemModel *sourceModel READ sourceModel WRITE setSourceModel) + Q_PROPERTY(QString query READ query WRITE setQuery NOTIFY queryChanged) + Q_PROPERTY(bool excludeInternal READ excludeInternal WRITE setExcludeInternal NOTIFY excludeInternalChanged) + Q_PROPERTY(bool excludeUnsupported READ excludeUnsupported WRITE setExcludeUnsupported NOTIFY excludeUnsupportedChanged) + +public: + explicit EffectsFilterProxyModel(QObject *parent = nullptr); + ~EffectsFilterProxyModel() override; + + QString query() const; + void setQuery(const QString &query); + + bool excludeInternal() const; + void setExcludeInternal(bool exclude); + + bool excludeUnsupported() const; + void setExcludeUnsupported(bool exclude); + +Q_SIGNALS: + void queryChanged(); + void excludeInternalChanged(); + void excludeUnsupportedChanged(); + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; + +private: + QString m_query; + bool m_excludeInternal = true; + bool m_excludeUnsupported = true; + + Q_DISABLE_COPY(EffectsFilterProxyModel) +}; + +} // namespace KWin diff --git a/kcmkwin/kwineffects/kcm.cpp b/kcmkwin/kwineffects/kcm.cpp new file mode 100644 index 0000000..8d53ea7 --- /dev/null +++ b/kcmkwin/kwineffects/kcm.cpp @@ -0,0 +1,113 @@ +/* + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kcm.h" +#include "effectsfilterproxymodel.h" +#include "effectsmodel.h" + +#include +#include +#include + +#include +#include + +K_PLUGIN_FACTORY_WITH_JSON(DesktopEffectsKCMFactory, + "kcm_kwin_effects.json", + registerPlugin();) + +namespace KWin +{ + +DesktopEffectsKCM::DesktopEffectsKCM(QObject *parent, const QVariantList &args) + : KQuickAddons::ConfigModule(parent, args) + , m_model(new EffectsModel(this)) +{ + qmlRegisterType("org.kde.private.kcms.kwin.effects", 1, 0, "EffectsFilterProxyModel"); + + auto about = new KAboutData( + QStringLiteral("kcm_kwin_effects"), + i18n("Desktop Effects"), + QStringLiteral("2.0"), + QString(), + KAboutLicense::GPL + ); + about->addAuthor(i18n("Vlad Zahorodnii"), QString(), QStringLiteral("vlad.zahorodnii@kde.org")); + setAboutData(about); + + setButtons(Apply | Default); + + connect(m_model, &EffectsModel::dataChanged, this, &DesktopEffectsKCM::updateNeedsSave); + connect(m_model, &EffectsModel::loaded, this, &DesktopEffectsKCM::updateNeedsSave); +} + +DesktopEffectsKCM::~DesktopEffectsKCM() +{ +} + +QAbstractItemModel *DesktopEffectsKCM::effectsModel() const +{ + return m_model; +} + +void DesktopEffectsKCM::load() +{ + m_model->load(); + setNeedsSave(false); +} + +void DesktopEffectsKCM::save() +{ + m_model->save(); + setNeedsSave(false); +} + +void DesktopEffectsKCM::defaults() +{ + m_model->defaults(); + updateNeedsSave(); +} + +void DesktopEffectsKCM::openGHNS(QQuickItem *context) +{ + QPointer dialog = new KNS3::DownloadDialog(QStringLiteral("kwineffect.knsrc")); + dialog->setWindowTitle(i18n("Download New Desktop Effects")); + dialog->winId(); + + if (context && context->window()) { + dialog->windowHandle()->setTransientParent(context->window()); + } + + if (dialog->exec() == QDialog::Accepted) { + if (!dialog->changedEntries().isEmpty()) { + m_model->load(EffectsModel::LoadOptions::KeepDirty); + } + } + + delete dialog; +} + +void DesktopEffectsKCM::configure(const QString &pluginId, QQuickItem *context) +{ + const QModelIndex index = m_model->findByPluginId(pluginId); + + QWindow *transientParent = nullptr; + if (context && context->window()) { + transientParent = context->window(); + } + + m_model->requestConfigure(index, transientParent); +} + +void DesktopEffectsKCM::updateNeedsSave() +{ + setNeedsSave(m_model->needsSave()); + setRepresentsDefaults(m_model->isDefaults()); +} + +} // namespace KWin + +#include "kcm.moc" diff --git a/kcmkwin/kwineffects/kcm.h b/kcmkwin/kwineffects/kcm.h new file mode 100644 index 0000000..ed19ea3 --- /dev/null +++ b/kcmkwin/kwineffects/kcm.h @@ -0,0 +1,47 @@ +/* + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include +#include + +namespace KWin +{ + +class EffectsModel; + +class DesktopEffectsKCM : public KQuickAddons::ConfigModule +{ + Q_OBJECT + Q_PROPERTY(QAbstractItemModel *effectsModel READ effectsModel CONSTANT) + +public: + explicit DesktopEffectsKCM(QObject *parent = nullptr, const QVariantList &list = {}); + ~DesktopEffectsKCM() override; + + QAbstractItemModel *effectsModel() const; + +public Q_SLOTS: + void load() override; + void save() override; + void defaults() override; + + void openGHNS(QQuickItem *context); + void configure(const QString &pluginId, QQuickItem *context); + +private Q_SLOTS: + void updateNeedsSave(); + +private: + EffectsModel *m_model; + + Q_DISABLE_COPY(DesktopEffectsKCM) +}; + +} // namespace KWin diff --git a/kcmkwin/kwineffects/kcm_kwin_effects.desktop b/kcmkwin/kwineffects/kcm_kwin_effects.desktop new file mode 100644 index 0000000..b6d94b8 --- /dev/null +++ b/kcmkwin/kwineffects/kcm_kwin_effects.desktop @@ -0,0 +1,129 @@ +[Desktop Entry] +Exec=kcmshell5 kcm_kwin_effects +Icon=preferences-desktop-effects +Type=Service +X-KDE-ServiceTypes=KCModule +X-DocPath=kcontrol/kwineffects/index.html + +X-KDE-Library=kcm_kwin_effects +X-KDE-PluginKeyword=effects +X-KDE-ParentApp=kcontrol + +X-KDE-System-Settings-Parent-Category=desktopbehavior +X-KDE-Weight=50 + +X-KDE-FormFactors=desktop,tablet + +Name=Desktop Effects +Name[az]=İş Masası effektləri +Name[bs]=Efekti površi +Name[ca]=Efectes de l'escriptori +Name[ca@valencia]=Efectes d'escriptori +Name[cs]=Efekty na ploše +Name[da]=Skrivebordseffekter +Name[de]=Arbeitsflächen-Effekte +Name[el]=Εφέ επιφάνειας εργασίας +Name[en_GB]=Desktop Effects +Name[es]=Efectos del escritorio +Name[et]=Töölauaefektid +Name[eu]=Mahaigaineko efektuak +Name[fi]=Työpöytätehosteet +Name[fr]=Effets de bureau +Name[gl]=Efectos do escritorio +Name[he]=הנפשות שולחן עבודה +Name[hu]=Asztali effektusok +Name[ia]=Effectos de scriptorio +Name[id]=Efek Desktop +Name[it]=Effetti del desktop +Name[ja]=デスクトップ効果 +Name[ko]=데스크톱 효과 +Name[lt]=Darbalaukio efektai +Name[nb]=Skrivebordseffekter +Name[nds]=Schriefdischeffekten +Name[nl]=Bureaubladeffecten +Name[nn]=Skrivebords­effektar +Name[pa]=ਡੈਸਕਟਾਪ ਪਰਭਾਵ +Name[pl]=Efekty pulpitu +Name[pt]=Efeitos do Ecrã +Name[pt_BR]=Efeitos da área de trabalho +Name[ro]=Efecte de birou +Name[ru]=Эффекты +Name[se]=Čállinbeavdeeffeavttat +Name[sk]=Efekty plochy +Name[sl]=Učinki namizja +Name[sr]=Ефекти површи +Name[sr@ijekavian]=Ефекти површи +Name[sr@ijekavianlatin]=Efekti površi +Name[sr@latin]=Efekti površi +Name[sv]=Skrivbordseffekter +Name[tg]=Таъсирҳои мизи корӣ +Name[tr]=Masaüstü Efektleri +Name[uk]=Ефекти стільниці +Name[x-test]=xxDesktop Effectsxx +Name[zh_CN]=桌面特效 +Name[zh_TW]=桌面效果 +Comment=Configure compositor settings for desktop effects +Comment[az]=İş Masası effektlrəri tətbiqinin tənzimlənməsi +Comment[ca]=Configura l'arranjament del compositor per als efectes de l'escriptori +Comment[cs]=Nastavení kompozitoru pro efekty pracovní plochy +Comment[da]=Compositor-indstillinger til skrivebordseffekter +Comment[de]=Compositor-Einstellungen für Arbeitsflächen-Effekte einrichten +Comment[en_GB]=Configure compositor settings for desktop effects +Comment[es]=Configurar las preferencias del compositor para los efectos del escritorio +Comment[et]=Komposiitori seadistamine töölauaefektide tarbeks +Comment[eu]=Konfiguratu konposatzailearen ezarpenak mahaigaineko efektuetarako +Comment[fi]=Koostimen asetukset työpöytätehosteille +Comment[fr]=Configurer les paramètres du compositeur pour les effets de bureau +Comment[gl]=Configurar o compositor para os efectos de escritorio +Comment[ia]=Configura preferentias de compositor pro le effectos de scriptorio +Comment[id]=Konfigurasikan pengaturan kompositor untuk efek desktop +Comment[it]=Configura impostazioni del compositore per gli effetti del desktop +Comment[ko]=데스크톱 효과에 사용되는 컴포지터 설정 +Comment[lt]=Konfigūruoti darbalaukio efektų kompozitoriaus nuostatas +Comment[nl]=Instellingen van compositor configureren voor bureaubladeffecten +Comment[nn]=Samansetjarinnstillingar for skrivebordseffektar +Comment[pl]=Ustawienia kompozytora dla efektów pulpitu +Comment[pt]=Configuração do compositor para os efeitos do ecrã +Comment[pt_BR]=Defina as configurações do compositor para os efeitos da área de trabalho +Comment[ro]=Configurează opțiunile compozitorului pentru efecte de birou +Comment[ru]=Настройка движка эффектов рабочего стола +Comment[sk]=Nastavenia kompozítora pre efekty plochy +Comment[sl]=Nastavitve upravljalnika skladnje za učinke namizja +Comment[sv]=Anpassa sammansättningsinställningar för skrivbordseffekter +Comment[uk]=Налаштовування параметрів засобу композиції для ефектів стільниці +Comment[x-test]=xxConfigure compositor settings for desktop effectsxx +Comment[zh_CN]=配置桌面特效混成器设置 +Comment[zh_TW]=設定桌面特效的合成器設定 + +X-KDE-Keywords=kwin,window,manager,effect,3D effects,2D effects,graphical effects,desktop effects,animations,various animations,window management effects,window switching effect,desktop switching effect,animations,desktop animations,drivers,driver settings,rendering,render,invert effect,looking glass effect,magnifier effect,snap helper effect,track mouse effect,zoom effect,blur effect,fade effect,fade desktop effect,fall apart effect,glide effect,highlight window effect,login effect,logout effect,magic lamp effect,minimize animation effect,mouse mark effect,scale effect,screenshot effect,sheet effect,slide effect,sliding popups effect,thumbnail aside effect,translucency,translucency effect,transparency,window geometry effect,wobbly windows effect,startup feedback effect,dialog parent effect,dim inactive effect,dim screen effect,slide back effect,eye candy,candy,show FPS effect,show paint effect,cover switch effect,desktop cube effect,desktop cube animation effect,desktop grid effect,flip switch effect,present windows effect,resize window effect,background contrast effect +X-KDE-Keywords[az]=kwin, pəncərə,menecer,effekt,3D effektlər,2D effektlər,qrafik effektlər,iş masası effektləri,animasiyalar,dəyişkən animasiyalar,pəncərə effektləri idarəsi,pəncərə dəyişdirmə effektləri,iş masası dəyişdirmə effektləri,animasiyalar,iş masası animasiyaları,sürücülər,sürücü ayarları,təsvirin işlənməsi,əks effektlər,linza effekti,lupa effekti,qopma effekti,siçanı izləmə effekti,miqyaslama effekti,yayğınlaşma effekti,solma effekti,iş masasının solması effekti,səpələnmə effekti,süzülmə effekti,vurğulama effekti,pəncərə işıqlandırma effekti,giriş effekti,çıxış effekti,sehirli lampa,animasiyalı yığılma,siçanı işarələmək,tor,ekran şəkli,vərəq,sürüşdürmə,sürüşən pəncərələr,yan miniatürləri,şəffaflıq,pəncərə həndəsəsi,titrək pəncərə,dialoq pəncərəsi,tutqunlaşma,pəncərə tutqunlaşması,ekran tutqunlaşması,geriyə sürüşmə,saniyədə kadrlar,ekranda şəkil çəkmə,örtmə,iş masası kubu animasiyası,kitab vərəqləri,pəncərə təqdimatı,pəncərə ölçüsünün dəyişdirilməsi,arxa fon kontrastı +X-KDE-Keywords[ca]=kwin,finestra,gestor,efecte,efectes 3D,efectes 2D,efectes gràfics,efectes d'escriptori,animacions,animacions diverses,efectes en la gestió de les finestres,efecte en el canvi de finestra,efecte en el canvi d'escriptori,animacions,animacions a l'escriptori,controladors,configuració dels controladors,renderització,render,efecte d'inversió,efecte d'aspecte de vidre,efecte de lupa,efecte ajudant del desplaçament,efecte de seguiment del ratolí,efecte de zoom,efecte de difuminat,efecte d'esvaïment,efecte d'esvaïment de l'escriptori,efecte de trencament,efecte de lliscament,efecte de ressaltat de la finestra,efecte en l'inici de la sessió,efecte en sortir de la sessió,efecte de làmpada màgica,efecte d'animació de la minimització,efecte de marca del ratolí,efecte d'apropament,efecte de captura de la pantalla,efecte de full,efecte de diapositiva,efecte de missatges emergents lliscants,efecte de miniatures laterals,translucidesa,efecte de translucidesa,transparència,efecte de geometria de la finestra,efecte de finestres sacsejades,efecte de confirmació d'engegada,efecte de diàleg principal,efecte d'enfosquiment en estar inactiu,efecte d'enfosquiment de la pantalla,efecte de diapositiva prèvia,decoració,efecte per a mostrar els FPS,efecte de mostrar el pintat,efecte de canvi de coberta,efecte de cub de l'escriptori,efecte d'animació del cub de l'escriptori,efecte de quadrícula de l'escriptori,efecte de canvi en roda,efecte de presentació de les finestres,efecte de redimensionat de la finestra,efecte de contrast del fons +X-KDE-Keywords[ca@valencia]=kwin,finestra,gestor,efecte,efectes 3D,efectes 2D,efectes gràfics,efectes d'escriptori,animacions,animacions diverses,efectes en la gestió de les finestres,efecte en el canvi de finestra,efecte en el canvi d'escriptori,animacions,animacions en l'escriptori,controladors,configuració dels controladors,renderització,render,efecte d'inversió,efecte d'aspecte de vidre,efecte de lupa,efecte ajudant del desplaçament,efecte de seguiment del ratolí,efecte de zoom,efecte de difuminat,efecte de fosa,efecte de fosa de l'escriptori,efecte de trencament,efecte de lliscament,efecte de ressaltat de la finestra,efecte en l'inici de la sessió,efecte en eixir de la sessió,efecte de làmpada màgica,efecte d'animació de la minimització,efecte de marca del ratolí,efecte d'apropament,efecte de captura de pantalla,efecte de full,efecte de diapositiva,efecte de missatges emergents lliscants,efecte de miniatures laterals,translucidesa,efecte de translucidesa,transparència,efecte de geometria de la finestra,efecte de finestres sacsejades,efecte de retroacció d'inici,efecte de diàleg principal,efecte d'enfosquiment en estar inactiu,efecte d'enfosquiment de la pantalla,efecte de diapositiva prèvia,decoració,efecte per a mostrar els FPS,efecte per a mostrar les zones pintades,efecte de canvi de coberta,efecte de cub de l'escriptori,efecte d'animació del cub de l'escriptori,efecte de graella de l'escriptori,efecte de canvi en roda,efecte de presentació de les finestres,efecte de redimensionat de la finestra,efecte de contrast del fons +X-KDE-Keywords[da]=kwin,vindue,vindueshåndtering,effekter,3D-effekter,2D-effekter,grafiske effekter,skrivebordseffekter,animationer,diverse animationer,vindueshåndteringseffekter,effekt til skift af vinduer,effekt til skrivebordsskift,skrivebordsanimationer,drivere,driverindstillinger,rendering,render,invertereffect,kikkerteffekt,forstørrelsesglaseffekt,hægtehjælpereffekt,følg musen-effekt,zoomeffect,sløreffekt,fade-effect,svæve-effect,fremhæv vindue-effekt,login-effekt,log ud-effekt,magisk lampe-effekt,minimer-effekt,musemærke-effekt,skalerind-effekt,skærmbillede-effekt,glide-effekt,glidende pop-op-effekt,gennemsigtighed,transparens,ugennemsigtighed,vinduesgeometri-effekt,wobbly,blævrende vinduer,eye candy,øjeguf,vis FPS-effekt,cube,terning,gitter,baggrundskontrast-effekt +X-KDE-Keywords[de]=KWin,Fenster,Verwaltung,Effekt,2D-Effekte,3D-Effekte,Grafische Effekte,Desktopeffekte,Arbeitsflächeneffekte,Animation,Fensterverwaltungs-Effekte,Fensterwechsel-Effekte,Desktop-Wechsel,Arbeitsflächenwechsel,Desktop-Animation,Arbeitsflächen-Animation,Treiber,Treibereinstellung,Rendering,Rendern,Invertierungseffekt,Bildschirmlupeneffekt,Vergrößerungseffekt,Einrasteffekt,Maus folgen,Zoomeffekt,Dashboard,Überblendungseffekt,Gleiteneffekt,Fensterhervorhebungs-Effekt,Anmeldungseffekt,Abmeldungseffekt,Animierter Minimierungseffekt,Mausmarkierungseffekt,Skalierungseffekt,Bildschirmeffekt,Blatteffekt,Folieneffekt,Vorschaueffekt,Durchsichtigkeit,Durchsichtigkeitseffekt,Fenstergeometrieffekt,Effekt Wabernde Fenster,Programmstartanzeigeneffekt,Inaktiveffekt,Bildschirmabdunkelungseffekt,FPS-Effekt,Zeichnungsbereicheffekt, 3D-Fenstergalerieeffekt,Desktopgittereffekt,3D-Fensterstapelumschalteffekt,Fensteranzeigeeffekt,Fenstergrößenänderungseffekt,Hintergrundkonstrasteffekt +X-KDE-Keywords[en_GB]=kwin,window,manager,effect,3D effects,2D effects,graphical effects,desktop effects,animations,various animations,window management effects,window switching effect,desktop switching effect,animations,desktop animations,drivers,driver settings,rendering,render,invert effect,looking glass effect,magnifier effect,snap helper effect,track mouse effect,zoom effect,blur effect,fade effect,fade desktop effect,fall apart effect,glide effect,highlight window effect,login effect,logout effect,magic lamp effect,minimize animation effect,mouse mark effect,scale effect,screenshot effect,sheet effect,slide effect,sliding popups effect,thumbnail aside effect,translucency,translucency effect,transparency,window geometry effect,wobbly windows effect,startup feedback effect,dialog parent effect,dim inactive effect,dim screen effect,slide back effect,eye candy,candy,show FPS effect,show paint effect,cover switch effect,desktop cube effect,desktop cube animation effect,desktop grid effect,flip switch effect,present windows effect,resize window effect,background contrast effect +X-KDE-Keywords[es]=kwin,ventana,gestor,efecto,efectos 3D,efectos 2D,efectos gráficos,efectos del escritorio,animaciones,animaciones diversas,efectos de la gestión de ventanas,efecto de cambio de ventana,efecto de cambio de escritorio,animaciones,animaciones del escritorio,controladores,preferencias del controlador,renderización,renderizar,efecto de inversión,efecto de espejo,efecto de lupa,efecto auxiliar de instantánea,efecto de seguimiento del ratón,efecto de ampliación,efecto borroso,efecto de desvanecimiento,efecto de desvanecimiento del escritorio,efecto de romper en pedazos,efecto de planeo,efecto de resaltar ventanas,efecto de inicio de sesión,efecto de final de sesión,efecto de lámpara mágica,efecto de animación al minimizar,efecto de marcas del ratón,efecto de escalado,efecto de captura de pantalla,efecto de hoja,efecto de deslizamiento,efecto de ventanas emergentes deslizantes,efecto de miniaturas laterales,transparencia,efecto de transparencia,efecto de geometría de las ventanas, efecto de ventanas gelatinosas,efecto de notificación de inicio,efecto de padre de la ventana,efecto de oscurecer inactiva,efecto de oscurecer la pantalla,efecto atrás,efectos atractivos,efecto de mostrar FPS,efecto de mostrar pintura,efecto de selección de ventana en modo carátula,efecto de cubo del escritorio,efecto de animación del cubo del escritorio,efecto de rejilla del escritorio,efecto de selección de ventana en modo cascada,efecto de presentación de ventanas,efecto de cambiar tamaño de las ventanas,efecto de contraste del fondo +X-KDE-Keywords[et]=kwin,aken,hakdur,efekt,3D efektid,ruumilised efektid,2D efektid,graafikaefektid,töölauaefektid,animatsioonid,eri animatsioonid,aknahaldusefektid,akna lülitamise efekt,töölaua lülitamise efekt,animatsioonid,töölauaanimatsioonid,draiverid,draiveri seadistused,renderdamine,renderdus,inverteerimisefekt,pikksilmaefekt,suurendusklaasiefekt,tõmbe abistaja efekt,hiire jälgimise efekt,suurendusefekt,vidinavaate efekt,plahvatuseefekt,hääbumisefekt,töölaua kadumise efekt,lagunemise efekt,liuglemisefekt,akna esiletõstmise efekt,sisselogimisefekt,väljalogimisefekt,maagilise laterna efekt,minimeerimisanimatsiooni efekt,hiirega tähistamise efekt,skaleerimisefekt,ekraanipildi efekt,leheefekt,slaidiefekt,liuglevate hüpikakende efekt,tegumiriba pisipiltide efekt,kõrvalasuvate pisipiltide efekt,läbipaistvus,läbipaistvuseefekt,akende geomeetria efekt,vonklevate akende efekt,käivitamise tagasiside efekt,dialoogi eellase efekt,tuhmi mitteaktiivse efekt,tuhmi ekraani efekt,tagasiliugumise efekt,silmarõõm,FPS-i näitamise efekt,joonistamise näitamise efekt,kastina lülitamise efekt,vaiplülitamise efekt,töölauakuubiku efekt, töölauakuubiku animatsiooni efekt,töölauavõrgustiku efekt,pööramisega lülitamise efekt,kontuuriefekt,aktiivsete akende efekt, akende suuruse muutmise efekt +X-KDE-Keywords[eu]=kwin,leihoa,kudeatzailea,efektua,3D efektuak,2D efektuak,efektu grafikoak,mahaigaineko efektuak,animazioak,hainbat animazio,leiho kudeaketaren efektuak,leiho-aldatze efektuak,mahaigaina aldatzeko efektua,animazioak,mahaigaineko animazioak,gidariak,gidarien ezarpenak,errendatze,errendatu,alderantzikatu efektua,pantaila-lupa efektua,lupa efektua,atxikitze laguntzaile efektua,jarraitu saguari efektua,zoom efektua,lausotze efektua,desagertze efektua,mahaigain-desagertze efektua,puskatze efektua,irristatze efektua,leiho-nabarmentze efektua,saio-haste efektua,saio-ixte efektua,lanpara magiko efektua,minimizatze-animazio efektua, sagu-marka efektua,eskalatze efektua,pantaila-argazki efektua,orrialde efektua,diapositiba efektua,diapositiba gainerakor efektua,koadro txikiak alboan efektua,zeharrargitasuna,zeharrargitasun efektua,gardentasuna,leiho-geometria efektua,leiho dardartsuak efektua,abio berrelikadura efektua,guraso elkarrizketa-koadro efektua,itzaldu ez-aktiboa efektua,itzaldu pantaila efektua,diapositibak atzera efektua,ikusteko atsegina,atsegina,FPS erakuste efektua,erakutsi pintura efektua,azal aldaketa efektua,kubo-mahaigain efektua,kubo-mahaigain animazio efektua,mahaigain-sareta efektua,perspektiban pilatutako aldatze efektua,aurkeztu leihoak efektua,leihoaren neurri-aldatze efektua,atzealdeko kontraste efektua +X-KDE-Keywords[fi]=kwin,ikkuna,hallinta,tehoste,3D-tehosteet,2D-tehosteet,graafiset tehosteet,työpöytätehosteet,animoinnit,eri animoinnit,ikkunanhallintatehosteet,ikkunanvaihtotehoste,työpöydänvaihtotehoste,animoinnit,työpöytäanimoinnit,ajurit,ajuriasetukset,hahmonnus,hahmonna,käänteistehoste,peilitehoste,suurennuslasitehoste,kiinnitysavustajatehoste,hiiren jäljitystehoste,suurennustehoste,sumennustehoste,häivytystehoste,työpöydän häivytystehoste,hajotustehoste,liukutehoste,ikkunan korostustehoste,kirjautumistehoste,uloskirjautumistehoste,taikalampputehoste,pienennyksen animointitehoste,hiiren jälki -tehoste,skaalaustehoste,ruudunkaappaustehoste,sheet effect,liukutehoste,liukuvat ponnahdusikkunat -tehoste,esikatselukuva reunalla -tehoste,läpikuultavuus,läpikuultavuustehoste,läpinäkyvyys,ikkunageometriatehoste,heiluvat ikkunat -tehoste,käynnistyspalautetehoste,kyselyikkunan isäntäikkuna -tehoste,himmennä passiivinen -tehoste,himmennä näyttö -tehoste,liuku taakse -tehoste,silmäkarkki,karkki,näytä FPS -tehoste,näytä näytönpiirto -tehoste,kansivaihtotehoste,työpöytäkuutiotehoste,työpöytäkuutioanimointitehoste,työpöytäruudukkotehoste,kääntövaihtotehoste,näytä ikkunat -tehoste,ikkunan koonmuuttamistehoste,taustakontrastitehoste +X-KDE-Keywords[fr]=kwin, fenêtre, gestionnaire, effet, effets 3D, effets 2D, effets graphiques, effets de bureau, animations, animations variés, effets de gestion des fenêtres, effets de changement de fenêtre, effets de changement de bureau, animations, animation du bureau, pilotes, paramètres du pilote, rendu, rendre, effet d'inversion, effet de verre, effet de loupe, effet d'aide au positionnement, effet de repérage de la souris, effet de zoom, effet de flou, effet de fondu, effet de fondu du bureau, effet d'effondrement, effet de glissement, effet de mise en valeur de la fenêtre, effet de connexion, effet de déconnexion, effet de lampe magique, effet de minimisation de l'application, effet de marque de la souris, effet de gradation, effet de capture d'écran, effet de feuille, effet de glisse, effet d'annotations glissantes, effet vignettes sur le coté, translucidité, effet de translucidité, transparence, effet de géométrie de la fenêtre, effet de fenêtre en gélatine, effet du témoin de démarrage, effet de dialogue parent, effet d'obscurcissement de fenêtre inactive, effet d'obscurcissement du bureau, effet de glissement en arrière, confort visuel, beauté, effet d'affichage du FPS, effet d'affichage des zones peintes, effet de défilement circulaire, effet de bureaux en cube, effet d'animation de cube de bureaux, effet de bureaux en grille, effet d'empilement en perspective, effet de présentation des fenêtres, effet de redimensionnement des fenêtres, effet de contraste du bureau +X-KDE-Keywords[gl]=kwin,window,xanela,manager,xestor,effect,efecto,3D effects,efectos 3D,2D effects,efectos 2D,configuración de vídeo,graphical effects,efectos gráficos,efectos visuais,desktop effects,efectos de escritorio,animations,animacións,various animations,animacións diversas,varias animacións,animacións variadas,window management effects,efectos de xestión de xanelas,window switching effect,efecto de cambio de xanela,desktop switching effect,efecto de cambio de escritorio,animations,animacións,desktop animations,animacións de escritorio,animacións dos escritorios,animacións do escritorio,drivers,controlador,controladores,driver settings,configuración dos controladores,configuración do controlador,rendering,renderización,renderizado,renderizamento,render,renderizar,invert effect,inverter un efecto,inverter efecto,reverter un efecto,reverter efecto,looking glass effect,efecto de lupa,efecto lupa,magnifier effect,snap helper effect,track mouse effect,efecto de seguir o rato,efecto de seguimento do rato,zoom effect,efecto de ampliación,blur effect,efecto borroso,fade effect,efecto de esvaer,fade desktop effect,efecto de esvaer o escritorio,fall apart effect,efecto de destrución,glide effect,efecto de brillo,highlight window effect,efecto de realzar a xanela,efecto de resaltar a xanela,efecto de salientar a xanela,efecto de salientar a xanela,login effect,efecto de acceder,logout effect,efecto de saír,magic lamp effect,efecto de lámpada máxica,minimize animation effect,efecto de minimizar,mouse mark effect,efecto de marca co rato,scale in effect,efecto de achegar,efecto de cambia de escala,screenshot effect,efecto de captura,sheet effect,efecto de folla,slide effect,efecto de dispositiva,sliding popups effect,thumbnail aside effect,translucency,transparencia,translucidez,translucency effect,efecto de translucidez,efecto de transparencia,transparency,transparencia,window geometry effect,efecto de xeometría da xanela,efecto de xeometría das xanelas,wobbly windows effect,efecto de xanelas a tremer,startup feedback effect,dialog parent effect,efecto do pai do diálogo,dim inactive effect,dim screen effect,efecto de escurecer,slide back effect,eye candy,candy,show FPS effect,efecto de mostrar os FPS,show paint effect,cover switch effect,desktop cube effect,efecto de cubo de escritorio,efecto do cubo de escritorio,efecto de cubo do escritorio,desktop cube animation effect,desktop grid effect,efecto de grade de escritorios,efecto de grade de escritorios,flip switch effect,efecto de interruptor,efecto de contorno,present windows effect,resize window effect,efecto de cambio de tamaño das xanelas,efecto de contraste de fondo +X-KDE-Keywords[hu]=kwin,ablak,kezelő,hatás,3D hatás,2D hatás,grafikai hatások,asztali hatások,animációk,különféle animációk,ablakkezelő hatások,ablakváltó hatások,asztalváltó hatások,animációk,asztali animációk,meghajtók,meghajtó beállítások,leképezés,renderelés,fordított hatás,tükörhatás,nagyító hatás,elkapás segítő hatás,egérkövetés hatás,nagyítás hatás,elmosás,elhalványulás hatás,asztal elhalványulása hatás,széteső hatás,csúszás hatás,ablak kiemelése hatás,belépés hatás,kilépés hatás,varázslámpa hatás,minimalizálás animáció hatás,egérjelölés hatás,méretezés hatás,képernyőkép hatás,munkalap hatás,dia hatás,csúszó felugrók hatás,,bélyegképek félre hatás, áttetszőség,áttetszőség hatás,átlátszóság,ablak geometria hatás,ingó ablak hatás,indulási visszajelzés hatás,párbeszédablak szülő hatás,dim inaktív hatás,dim kijelző hatás,dia vissza hatás,látványelem,édesség,FPS megjelenítése hatás,festék megjelenése hatás,eltakarás váltás hatás,asztal kocka hatás,asztal kockaanimáció hatás,asztal rács hatás,tükrözésváltás hatás,jelenlegi ablakok hatás,ablak átméretezése hatás,háttérkontraszt hatás +X-KDE-Keywords[ia]=kwin,window,manager,effect,3D effects,2D effects,graphical effects,desktop effects,animations,various animations,window management effects,window switching effect,desktop switching effect,animations,desktop animations,drivers,driver settings,rendering,render,invert effect,looking glass effect,magnifier effect,snap helper effect,track mouse effect,zoom effect,blur effect,fade effect,fade desktop effect,fall apart effect,glide effect,highlight window effect,login effect,logout effect,magic lamp effect,minimize animation effect,mouse mark effect,scale effect,screenshot effect,sheet effect,slide effect,sliding popups effect,thumbnail aside effect,translucency,translucency effect,transparency,window geometry effect,wobbly windows effect,startup feedback effect,dialog parent effect,dim inactive effect,dim screen effect,slide back effect,eye candy,candy,show FPS effect,show paint effect,cover switch effect,desktop cube effect,desktop cube animation effect,desktop grid effect,flip switch effect,present windows effect,resize window effect,background contrast effect +X-KDE-Keywords[id]=kwin,window,pengelola,efek,efek 3D,efek 2D,efek grafik,efek desktop,animasi,beragam animasi,efek pengelola window,efek beralih window,efek beralih desktop,animasi,animasi desktop,driver,pengaturan driver,rendering,render,efek kebalikan,efek seperti gelas,efek kaca pembesar,efek penunjang jepret,efek lacak mouse,efek pembesaran,efek buram,efek lesap,efek desktop lesap,efek hancur,efek petak,efek window sorot,efek login,efek logout,efek lampu ajaib,efek animasi minimalkan,efek tanda mouse,efek skala,efek cuplikan layar,efek lembar,efek geser,efek sembul geser,efek gambar-mini disamping,translusensi,efek translusensi,transparan,efek geometri window,efek window goyang,efek feedback pemulaian,efek induk dialog,efek layar suram,efek geser mundur,eye candy,candy,efek tampilkan FPS,efek tampilkan lukisan,efek alih kotak,efek alih sampul,efek kubus desktop,efek animasi kubus desktop,efek kisi desktop,efek alih lipat,efek window hadir,efek ubah ukuran window,efek kontras latarbelakang +X-KDE-Keywords[it]=kwin,finestra,gestore,effetto,effetti 3D,effetti 2D,effetti grafici,effetti del desktop,animazioni,animazioni varie, effetti del gestore delle finestre,effetto dello scambiafinestre,effetto dello scambiatore di desktop,animazioni,animazioni del desktop,driver,impostazioni driver,rendering,render,effetto invertito,effetto vetro,effetto lente,effetto snap helper,effetto traccia mouse,effetto ingrandimento, effetto sfocatura,effetto dissolvenza,effetto dissolvenza desktop,effetto caduta,effetto planatura,effetto evidenziazione finestra,effetto schermata di accesso, effetto disconnessione,effetto lampada magica,effetto animazione di minimizzazione,effetto marcatura mouse, effetto scalatura,effetto schermata,effetto foglio,effetto diapositiva,effetto scivolamento,effetto miniature nella barra delle applicazioni,effetto miniatura su un lato,translucenza,effetto translucenza, trasparenza,effetto geometria finestra,effetto finestre tremolanti,effetto segnale di avvio,effetto finestra madre,effetto oscura finestra inattiva,effetto oscura schermo,effetto scivola all'indietro,gradevole,effetto gradevole,effetto mostra FPS,effetto ridisegno,effetto scambio cubi,effetto scambio copertina,effetto cubi del desktop,effetto animazione cubi del desktop,effetto griglia desktop,effetto scambiatore con inversione,effetto riquadro,effetto finestra presente,effetto ridimensionamento finestra,effetto contrasto dello sfondo +X-KDE-Keywords[ko]=kwin,window,manager,effect,3D effects,2D effects,graphical effects,desktop effects,animations,various animations,window management effects,window switching effect,desktop switching effect,animations,desktop animations,drivers,driver settings,rendering,render,invert effect,looking glass effect,magnifier effect,snap helper effect,track mouse effect,zoom effect,blur effect,dashboard effect,explosion effect,fade effect,fade desktop effect,fall apart effect,glide effect,highlight window effect,login effect,logout effect,magic lamp effect,minimize animation effect,mouse mark effect,scale in effect,screenshot effect,sheet effect,slide effect,sliding popups effect,taskbar thumbnails effect,thumbnail aside effect,translucency,translucency effect,transparency,window geometry effect,wobbly windows effect,startup feedback effect,dialog parent effect,dim inactive effect,dim screen effect,slide back effect,eye candy,candy,show FPS effect,show paint effect,box switch effect,cover switch effect,desktop cube effect,desktop cube animation effect,desktop grid effect,flip switch effect,outline effect,present windows effect,resize window effect,창,관리자,효과,3D 효과,2D 효과,그래픽 효과,데스크톱 효과,애니메이션,창 관리자,창 관리자 효과,데스크톱 전환,데스크톱 전환 효과,드라이버,드라이버 설정,렌더링,렌더링 설정,애니메이션 속도,투명 유리 효과,확대 축소 효과,흐림 효과,대시보드 효과,페이드 효과,로그인 효과,창 강조 효과,글라이드 효과,로그아웃 효과,램프 효과,시트 효과,최소화 효과,최대화 효과,팝업 효과 +X-KDE-Keywords[lt]=kwin,langas,langai,langų,langu,tvarkytuvė,tvarkytuve,efektas,efektai,3D efektai,2D efektai,trimačiai efektai,trimaciai efektai,dvimačiai efektai,dvimaciai efektai,grafiniai efektai,grafikos efektai,darbalaukio efektai,darbastalio efektai,animacijos,įvairios animacijos,ivairios animacijos,langų tvarkymo efektai,langu tvarkymo efektai,langų valdymo efektai,langu valdymo efektai,langų perjungimo efektai,langu perjungimo efektai,langų perjungiklio efektai,langu perjungiklio efektai,darbalaukio perjungimo efektai,darbastalio perjungimo efektai,darbalaukio perjungiklio efektai,darbastalio perjungiklio efektai,animacijos,darbalaukio animacijos,darbastalio animacijos,tvarkyklės,tvarkykles,tvarkyklė,tvarkykle,tvarkyklės nustatymai,tvarkykles nustatymai,tvarkyklės nuostatos,tvarkykles nuostatos,vaizdavimas,atvaizdavimas,invertavimo efektas,invertavimas,didinamojo stiklo efektas,didinamasis stiklas,didintuvo efektas,didintuvas,pritraukimo pagelbiklio efektas,traukimo pagelbiklio efektas,pritraukimo pagelbiklis,traukimo pagelbiklis,sekti pelę,sekti pele,pelės sekimas,peles sekimas,didinimo efektas,didinimas,suliejimo efektas,suliejimas,išblukimo efektas,išblukimas,isblukimo efektas,isblukimas,atsiradimo efektas,atsiradimas,išnykimo efektas,isnykimo efektas,išnykimas,isnykimas,darbalaukio išblukimo efektas,darbalaukio isblukimo efektas,darbastalio išblukimo efektas,darbastalio isblukimo efektas,subyrėjimo efektas,subyrejimo efektas,subyrėjimas,subyrejimas,byrėjimas,byrejimas,sklandymo efektas,sklandymas,lango paryškinimo efektas,lango paryskinimo efektas,langų paryškinimo efektas,langu paryskinimo efektas,lango paryškinimas,lango paryskinimas,langų paryškinimas,langu paryskinimas,prisijungimo efektas,atsijungimo efektas,magiškos lempos efektas,magiskos lempos efektas,magiška lempa,magiska lempa,suskleidimo animacijos efektas,sumažinimo animacijos efektas,sumazinimo animacijos efektas,suskleidimas,sumažinimas,sumazinimas,žymėjimo pele efektas,zymejimo pele efektas,žymėjimas pele,zymejimas pele,mastelio keitimo efektas,mastelio keitimas,ekrano kopijos efektas,ekrano kopija,ekranvaizdžio efektas,ekranvaizdzio efektas,lapo efektas,lapas,slydimo efektas,slydimas,slidimas,slystančių iškylančiųjų langų efektas,slystanciu iskylanciuju langu efektas,slystantys iškylantieji langai,slystantys iskylantieji langai,miniatiūros šone efektas,miniatiuros sone efektas,dalinis permatomumas,dalinio permatomumo efektas,permatomumas,skaidrumas,lango geometrijos efektas,lango geometrija,svirduliuojančių langų efektas,svirduliuojanciu langu efektas,svirduliuojantys langai,paleidimo grįžtamojo ryšio efektas,paleidimo griztamojo rysio efektas,paleidimo grįžtamasis ryšys,paleidimo griztamasis rysys,paleisties grįžtamasis ryšys,paleisties griztamasis rysys,dialogo viršesnio efektas,dialogo virsesnio efektas,dialogo viršesnis,dialogo virsesnis,pasyviųjų pritemdymo efektas,pasyviuju pritemdymo efektas,pasyviųjų pritemdymas,pasyviuju pritemdymas,pasyvių pritemdymo efektas,pasyviu pritemdymo efektas,pasyvių pritemdymas,pasyviu pritemdymas,ekrano pritemdymo efektas,ekrano pritemdymas,slydimo atgal efektas,slydimas atgal,grožybės,grozybes,gražu,grazu,FPS rodymo efektas,FPS rodymas,kadr./sek.,kadr/sek,kadr./s,kadr/s,kadr./sek. rodymas,piešimo rodymo efektas,piesimo rodymo efektas,piešimo rodymas,piesimo rodymas,viršelių perjungiklio efektas,viršelio perjungiklio efektas,virseliu perjungiklio efektas,viršelių perjungiklis,virseliu perjungiklis,višelių perjungimas,virseliu perjungimas,darbalaukio kubo efektas,darbastalio kubo efektas,darbalaukio kubas,darbastalio kubas,darbalaukio kubo animacijos efektas,darbastalio kubo animacijos efektas,darbalaukio kubo animacija,darbastalio kubo animacija,darbalaukio tinklelio efektas,darbastalio tinklelio efektas,darbalaukio tinklelis,darbastalio tinklelis,kartotekos pavidalo langų perjungiklio efektas,kartotekos pavidalo langu perjungiklio efektas,kartotekos pavidalo langų perjungiklis,kartotekos pavidalo langu perjungiklis,langų pateikimo efektas,langu pateikimo efektas,langų pateikimas,langu pateikimas,langų dydžio keitimo efektas,lango dydžio keitimo efektas,langu dydzio keitimo efektas,lango dydzio keitimo efektas,fono kontrasto efektas,fono kontrastas +X-KDE-Keywords[nl]=kwin,venster,beheerder,effect,3D effecten,2D effecten,grafische effecten,bureaubladeffecten,animaties,verrschillende animaties,vensterbeheereffecten,vensteromschakeleffect,bureaublad-omschakeleffect,animaties,bureaubladanimaties,stuurprogramma's,stuurprogramma-instellingen,rendering,render,inversieeffect,vergrootglaseffect,vergrotingseffect,snaphelpereffect,trackmuiseffect,zoomeffect,vervagingseffect,uitvaageffect,uitvaagbureaubladeffect,uiteenvaleffect,glijeffect,vensteraccentueringseffect,aanmeldeffect,afmeldeffect,magische lampeffect,animatie-effect minimaliseren,muismarkeringseffect,schaaleffect,schermafdrukeffect,bladeneffect,dia-effect,glijdende pop-upseffect,miniatuur-opzijeffect,doorzichtigheid,doorzichtigheidseffect,transparantie,vensterafmetingeneffect,wiebelende vensterseffect,opstartterugkoppeleffect,dialoogoudereffect,dim bij inactiviteitseffect,dim het schermeffect,schuif terugeffect,oogstrelend,snoepgoed,FPS toneneffect,verf toneneffect,vak deksel schakelaareffect,bureaublad kubuseffect,bureaublad kubus animatie-effect,bureaubladrastereffect,omschakeleffect,huidig venstereffect, wijzig grootte van venstereffect, achtergrondcontrasteffect +X-KDE-Keywords[nn]=kwin,vindauge,handsamar,vindaugshandsamar,effekt,3D-effektar,2D-effektar,grafiske effektar,skrivebordeffektar,animasjonar,ymse animasjonar,vindaugsbyteeffektar,effektar ved skrivebordbyte,animasjonar,skrivebordsanimasjonar,drivarar,drivarinnstillingar,oppteikning,oppteiknar,inverteringseffekt,spegeleffekt,lupeeffekt,gripehjelpareffekt,musmerkeeffekt,forstørringseffekt,zoomeffekt,sløringseffekt,uklar-effekt,kontrollpulteffekt,uttoningseffekt,skrivebordtoningseffekt,gå-i-knas-effekt,glidareffekt,framhev vindauge-effekt,innloggingseffekt,utloggingseffekt,magisk lampe-effekt,animasjonseffekt ved vindaugsminimering,musmerkeeffekt,skaleringseffekt,skjermdumpeffekt,ark-effekt,lysbileteffekt,glidande sprettopp-effekt,effekt for minibilete på sida,gjennomsiktseffekt,gjennomsikt,vindaugsgeometri-effekt,vaklande vindauge-effekt,effekt for oppstartsmelding,effekt for forelderdialog,effekt for mørk inaktiv,effekt for mørk skjerm,gli tilbake-effekt,augesnop,vis FPS-effekt,vis målingseffekt,omslagsbyteeffekt,skrivebordskube-effekt,effekt for animert skrivebordskube,effekt for skrivebordrutenett,effekt for flipp-byte,presenter vindauge-effekt,vindaugsskaleringseffekt,bakgrunnskontrast-effekt +X-KDE-Keywords[pl]=kwin,okno,menadżer,efekt,efekty 3D,efekty 2D,efekty graficzne,efekty pulpitu,animacje,różne animacje,efekty zarządzania oknami,efekty przełączania okien,efekty przełączania pulpitów,animacje,animacje pulpitu,sterowniki,ustawienia sterowników,renderowania, efekt odwrócenia,szkło powiększające,efekt powiększenia,efekt pomocnika przyciągania, efekt śledzenia myszy,efekt przybliżenia,rozmycie,tablica,efekt eksplozji,efekt zanikania,efekt zanikania pulpitu,efekt rozpadania,efekt slajdu,efekt podświetlania okna, efekt logowania,efekt wylogowywania,efekt magicznej lampy,efekt animacji minimalizacji, efekt znacznika myszy,efekt skalowania,efekt zrzutu ekranu,efekt arkusza,efekt slajdu,efekt wysuwających się elementów wyskakujących,efekt prześwitywania,przezroczystość,efekt geometrii okna,efekt chwiejnych okien,efekt odczuć przy starcie,efekt okna rodzica,efekt przyciemniania nieaktywnych,efekt przyciemniania ekranu,efekt przesuwania do tył,efekt pokazania ilości klatek na sekundę +X-KDE-Keywords[pt]=kwin,janela,gestor,composição,efeito,efeitos 3D,efeitos 2D,OpenGL,XRender,configuração do vídeo,efeitos gráficos,efeitos do ecrã,animações,animações diversas,efeitos de gestão das janelas,efeito de mudança de janelas,efeito de mudança de ecrãs,animações,velocidade da animação,animações do ecrã,controladores,configuração dos controladores,desenho,efeito de inversão,efeito de lupa,efeito de lente,efeito de ajuda no ajuste, efeito de seguimento do rato,efeito de ampliação,efeito de borrão,efeito de quadro,efeito de explosão,efeito de desvanecimento,efeito de desvanecimento do ecrã,efeito de destruição,efeito de deslizamento,efeito de realce da janela,efeito na autenticação,efeito do encerramento,efeito de lâmpada mágica,efeito de animação na minimização,efeito de marcação com rato,efeito de escala,efeito de captura do ecrã,efeito de folha,efeito de mensagens deslizantes,efeito de miniaturas na barra de tarefas,efeito de miniaturas laterais,efeito de janelas a tremer,efeito do arranque inicial,efeito da janela-mãe,efeito de escurecimento de janelas inactivas,efeito de deslize para trás,efeitos visuais,beleza,efeito de apresentação das IPS,efeito de pintura,efeito de mudança em caixa,efeito de mudança de capas, efeito de animação do cubo do ecrã,efeito de grelha do ecrã,efeito de mudança por viragem,efeito de destaque,efeito de apresentação das janelas,efeito de dimensionamento das janelas,efeito de contraste do fundo +X-KDE-Keywords[pt_BR]=kwin,janela,gerenciador,efeito,efeitos 3D,efeitos 2D,efeitos gráficos,efeitos da área de trabalho,animações,animações diversas,efeitos do gerenciamento de janelas,efeito de mudança de janelas,efeito de mudança de área de trabalho,animações da área de trabalho,drivers,configuração dos drivers,desenho,renderização,efeito de inversão,efeito de lupa,efeito de lente,efeito de ajuda no ajuste,efeito de rastreamento do mouse,efeito de ampliação,efeito de borrão,efeito de escurecimento,efeito de escurecimento da área de trabalho,efeito de destruição,efeito de deslizamento,efeito de realce da janela,efeito na autenticação,efeito de encerramento de sessão,efeito de lâmpada mágica,efeito de animação na minimização,efeito de marcação com mouse,efeito em escala,efeito de captura de tela,efeito de folha,efeito de slide,efeito de mensagens deslizantes,efeito de miniaturas na barra de tarefas,transparência,efeito de transparência,efeito de geometria de janelas,efeito de janelas trêmulas,efeito do inicialização,efeito da janela-mãe,efeito de escurecimento de janelas inativas,efeito de deslize para trás,efeitos visuais,beleza,efeito de apresentação de FPS,efeito de pintura,efeito de mudança de capas,efeito de animação do cubo da área de trabalho,efeito de grade de áreas de trabalho virtuais,efeito de mudança em pilha,efeito de apresentação das janelas,efeito de dimensionamento das janelas,efeito de contraste de fundo +X-KDE-Keywords[ru]=kwin,window,manager,effect,3D effects,2D effects,graphical effects,desktop effects,animations,various animations,window management effects,window switching effect,desktop switching effect,animations,desktop animations,drivers,driver settings,rendering,render,invert effect,looking glass effect,magnifier effect,snap helper effect,track mouse effect,zoom effect,blur effect,fade effect,fade desktop effect,fall apart effect,glide effect,highlight window effect,login effect,logout effect,magic lamp effect,minimize animation effect,mouse mark effect,scale effect,screenshot effect,sheet effect,slide effect,sliding popups effect,thumbnail aside effect,translucency,translucency effect,transparency,window geometry effect,wobbly windows effect,startup feedback effect,dialog parent effect,dim inactive effect,dim screen effect,slide back effect,eye candy,candy,show FPS effect,show paint effect,cover switch effect,desktop cube effect,desktop cube animation effect,desktop grid effect,flip switch effect,present windows effect,resize window effect,background contrast effect,эффекты рабочего стола,графические эффекты,рендеринг,параметры видео,настройка видео,трёхмерные эффекты,двумерные эффекты,эффекты управления окнами,эффекты диспетчера окон,анимация куба рабочих столов,плавная смена рабочих столов,прокрутка,анимация всплывающих окон,анимация появления окна,вход в систему,завершение сеанса,завершение работы,колышущиеся окна,болтающиеся окна,колыхание окон,край экрана,лист,анимация распахивания,анимация максимизации,максимизация окна,распахивание окна,миниатюры окон сбоку,миниатюры окон на краю экрана,полупрозрачность окон,размытие фона,размывание фона,распад закрывающихся окон,растворение закрывающихся окон,рисование на экране,рисование мышью на экране,скольжение окон,анимация сворачивания,сворачивание окон,волшебная лампа,график производительности,производительность эффектов,подсветка отрисовки,подсветка рендеринга,подсветка обновляемых частей экрана,анимация щелчка мышью,инверсия цветов,инвертирование цветов,поиск курсора мыши,разметка экрана,сетка на экране,линейки на экране,экранная разметка,экранная сетка,линза,искажение линзой,лупа,увеличение области экрана,масштаб рабочего стола,изменение масштаба рабочего стола,управление окнами,специальные возможности,инструменты,внешний вид,анимация переключения рабочих столов,все окна,просмотр всех окон,все рабочие столы,просмотр всех рабочих столов,изменение размера окна,масштабирование текстуры окна,куб с рабочими столами,перелистывание окон,управление фокусом,затемнение неактивных окон,затемнение основного окна,затемнение под диалогом,затемнение экрана при административной задаче,соскальзывание окон при смене фокуса,контрастность фона +X-KDE-Keywords[sk]=kwin, okno, manažér, kompozícia, efekt, 3D efekty, 2D efekty, OpenGL, XRender,nastavenia obrazu, grafické efekty, desktop efekty, animácie, rôzneanimácie, efekty správa okien, okno prepínanie efektov, stolnýspínacie efekt, animácie, animácie rýchlosť, stolný animácie, ovládače,nastavenie ovládača, renderovanie, poskytnúť, invertný skutočnosti zrkadlá účinok,lupa efekt, snap pomocník efekt, trať myš efekt, zoom efekt, rozmazaniuúčinok, prístrojová doska efekt, výbuch efekt, fade efekt, fade stolný efekt,rozpadnúť účinok, zostupovej efekt, zvýraznenie okno efekt, efekt prihlásenie, odhlásenieúčinok, čarovnú lampu účinok, minimálny efekt animácie, myši značky efekt, mierkav skutočnosti, screenshot efekt, list efekt, snímka efekt, posuvné vyskakovacie okná účinok,miniatúry na hlavnom paneli efekt, náhľad stranou efekt, priesvitnosť, translucencieúčinok, transparentnosť, okno geometrie efekt, vratkú okná efekt, uvedenie do prevádzkyspätná väzba, dialóg rodič efekt, matný efekt neaktívny, stlmiť obrazovku efekt,posunutím zadnej efekt, pastva pre oči, cukrík zobraziť FPS efekt, zobrazovať farby efekt, boxprepínač efekt, kryt prepínače účinok, desktop, desktop cube efekt kocky animácieúčinok, Desktop Grid efekt, flip switch efekt, obrys účinok, súčasné okná účinok, zmena veľkosti okna efekt, kontrast pozadie efekt +X-KDE-Keywords[sl]=kwin,upravljalnik oken,učinek,učinki 3d,učinki 2d,grafični učinki,namizni učinki,animacije,upravljanje z okni,preklapljanje oken,preklapljanje namizij,namizne animacije,gonilniki,izrisovanje,upodabljanje,obrni,povečevalno steklo,pripenjalni pomagalnik,sledenje miški,približanje,zabriši,eksplozija,pojemanje,pojemanje namizja,razpad,drsenje,poudari okno,učinek prijave,učinek odjave,čarobna svetilka,animacija skrčenja,risanje,animirano pojavljanje,zaslonska slika,list,drsenje,drseča pojavna okna,sličice za opravilno vrstico,sličica ob strani,prosojnost,prozornost,geometrija okna,majava okna,odziv zagona,nadrejeno pogovorno okno,potemni nedejavno,potemni zaslon,zdrs v ozadje,vidni bonbončki,pokaži sličice na sekundo,izrisovanje,preklapljanje - škatla,preklapljanje - ovitki,kocka z namizji,animacija kocka z namizji,mreža namizij,preklapljanje - sklad,oris,predstavi okna,spreminjanje velikosti okna +X-KDE-Keywords[sv]=kwin,fönster,hanterare,effekt,3D-effekter,grafiska effekter,skrivbordseffekter,animeringar,diverse animeringar,fönsterhanteringseffekter,fönsterbyteseffekt,skrivbordsbyteseffekt,skrivbordsanimeringar,drivrutiner,drivrutininställningar,återgivning,återge,inverteringseffekt,förstoringsglaseffekt,förstoringseffekt,låshjälpeffekt,musföljningseffekt,zoomeffekt,suddighetseffekt,explosionseffekt,borttoningseffekt,skrivbordsborttoningseffekt,sönderfallseffekt,glidningseffekt,fönstermarkeringseffekt,inloggningseffekt,utloggningseffekt,magisk lampeffekt,minimeringsanimeringseffekt,musmarkeringseffekt,inskalningseffekt,skärmbildseffekt,bladeffekt,skjuteffekt,glidande ruteffekt,miniatyrbilder i aktivitetsfältet,miniatyrbild vid sidan om,genomskinlighet,genomskinlighetseffekt,fönstergeometrieffekt,ostadiga fönstereffekt,startgensvarseffekt,dialogrutors ägareffekt,dämpa inaktiva effekt,dämpa skärmen effekt,glid tillbaka effekt,ögongodis,godis.visa ramar/s effekt, visa uppritningseffekt,byte med ruta effekt,skrivbordskubeffekt,animeringseffekt för skrivbordskub,skrivbordsrutnätseffekt,blädderbyteseffekt,befintliga fönstereffekt,ändra fönsterstorlekseffekt,bakgrundskontrasteffekt +X-KDE-Keywords[uk]=kwin,window,manager,effect,3D effects,2D effects,graphical effects,desktop effects,animations,various animations,window management effects,window switching effect,desktop switching effect,animations,desktop animations,drivers,driver settings,rendering,render,invert effect,looking glass effect,magnifier effect,snap helper effect,track mouse effect,zoom effect,blur effect,fade effect,fade desktop effect,fall apart effect,glide effect,highlight window effect,login effect,logout effect,magic lamp effect,minimize animation effect,mouse mark effect,scale in effect,screenshot effect,sheet effect,slide effect,sliding popups effect,taskbar thumbnails effect,thumbnail aside effect,translucency,translucency effect,transparency,window geometry effect,wobbly windows effect,startup feedback effect,dialog parent effect,dim inactive effect,dim screen effect,slide back effect,eye candy,candy,show FPS effect,show paint effect,box switch effect,cover switch effect,desktop cube effect,desktop cube animation effect,desktop grid effect,flip switch effect,outline effect,present windows effect,resize window effect,вікно,керування вікнами,менеджер вікон,ефект,просторовий,плоский,параметри відео,графічні ефекти,анімації,анімація,перемикання вікон,драйвери,параметри драйверів,показ,відтворення,інвертування,інверсія,збільшувальне скло,збільшення,прилипання,шлейф за вказівником,шлейф,масштабування,масштаб,зміна розмірів,розмивання,панель,згасання,поява,ковзання,підсвічування,підсвічування вікон,вихід,магічна лампа,чарівна лампа,джин,аркуші,стос,знімок екрана,мініатюри панелі задач,мініатюри,прозорість,ефект прозорості,желе,желейні вікна,супровід запуску,стрибунець,притлумлення,сірість,прикраси,показ частоти,малювання,обкладинки,стрибання,контур,поточні вікна,зміна розмірів +X-KDE-Keywords[x-test]=xxkwinxx,xxwindowxx,xxmanagerxx,xxeffectxx,xx3D effectsxx,xx2D effectsxx,xxgraphical effectsxx,xxdesktop effectsxx,xxanimationsxx,xxvarious animationsxx,xxwindow management effectsxx,xxwindow switching effectxx,xxdesktop switching effectxx,xxanimationsxx,xxdesktop animationsxx,xxdriversxx,xxdriver settingsxx,xxrenderingxx,xxrenderxx,xxinvert effectxx,xxlooking glass effectxx,xxmagnifier effectxx,xxsnap helper effectxx,xxtrack mouse effectxx,xxzoom effectxx,xxblur effectxx,xxfade effectxx,xxfade desktop effectxx,xxfall apart effectxx,xxglide effectxx,xxhighlight window effectxx,xxlogin effectxx,xxlogout effectxx,xxmagic lamp effectxx,xxminimize animation effectxx,xxmouse mark effectxx,xxscale effectxx,xxscreenshot effectxx,xxsheet effectxx,xxslide effectxx,xxsliding popups effectxx,xxthumbnail aside effectxx,xxtranslucencyxx,xxtranslucency effectxx,xxtransparencyxx,xxwindow geometry effectxx,xxwobbly windows effectxx,xxstartup feedback effectxx,xxdialog parent effectxx,xxdim inactive effectxx,xxdim screen effectxx,xxslide back effectxx,xxeye candyxx,xxcandyxx,xxshow FPS effectxx,xxshow paint effectxx,xxcover switch effectxx,xxdesktop cube effectxx,xxdesktop cube animation effectxx,xxdesktop grid effectxx,xxflip switch effectxx,xxpresent windows effectxx,xxresize window effectxx,xxbackground contrast effectxx +X-KDE-Keywords[zh_CN]=kwin,window,manager,effect,3D effects,2D effects,graphical effects,desktop effects,animations,various animations,window management effects,window switching effect,desktop switching effect,animations,desktop animations,drivers,driver settings,rendering,render,invert effect,looking glass effect,magnifier effect,snap helper effect,track mouse effect,zoom effect,blur effect,explosion effect,fade effect,fade desktop effect,fall apart effect,glide effect,highlight window kwin,window,manager,effect,3D effects,2D effects,graphical effects,desktop effects,animations,various animations,window management effects,window switching effect,desktop switching effect,animations,desktop animations,drivers,driver settings,rendering,render,invert effect,looking glass effect,magnifier effect,snap helper effect,track mouse effect,zoom effect,blur effect,fade effect,fade desktop effect,fall apart effect,glide effect,highlight window effect,login effect,logout effect,magic lamp effect,minimize animation effect,mouse mark effect,scale effect,screenshot effect,sheet effect,slide effect,sliding popups effect,thumbnail aside effect,translucency,translucency effect,transparency,window geometry effect,wobbly windows effect,startup feedback effect,dialog parent effect,dim inactive effect,dim screen effect,slide back effect,eye candy,candy,show FPS effect,show paint effect,cover switch effect,desktop cube effect,desktop cube animation effect,desktop grid effect,flip switch effect,present windows effect,resize window effect,background contrast effect,窗口,管理,特效,3D 特效,2D 特效,图形特效,桌面特效,动画,窗口管理特效,窗口切换特效,桌面切换特效,桌面动画,驱动,去掉设置,渲染,反色,放大镜特效,鼠标跟踪,缩放特效,模糊特效,渐变特效,破碎特效,滑动特效,高亮窗口,登录特效,注销特效,神灯特效,最小化动画,鼠标标记,截屏,飘落,滑动弹出窗口,缩略图置边,透明特效,摇摆窗口,启动反馈特效,黯淡对话框特效,黯淡屏幕特效,视觉效果,显示 FPS 特效,显示绘制区域特效,封面切换特效,桌面立方体特效,桌面立方体动画特效,翻转切换特效,展示窗口特效,调整窗口大小特效,背景对比特效 +X-KDE-Keywords[zh_TW]=kwin,window,manager,effect,3D effects,2D effects,graphical effects,desktop effects,animations,various animations,window management effects,window switching effect,desktop switching effect,animations,desktop animations,drivers,driver settings,rendering,render,invert effect,looking glass effect,magnifier effect,snap helper effect,track mouse effect,zoom effect,blur effect,fade effect,fade desktop effect,fall apart effect,glide effect,highlight window effect,login effect,logout effect,magic lamp effect,minimize animation effect,mouse mark effect,scale effect,screenshot effect,sheet effect,slide effect,sliding popups effect,thumbnail aside effect,translucency,translucency effect,transparency,window geometry effect,wobbly windows effect,startup feedback effect,dialog parent effect,dim inactive effect,dim screen effect,slide back effect,eye candy,candy,show FPS effect,show paint effect,cover switch effect,desktop cube effect,desktop cube animation effect,desktop grid effect,flip switch effect,present windows effect,resize window effect,background contrast effect diff --git a/kcmkwin/kwineffects/kwineffect.knsrc b/kcmkwin/kwineffects/kwineffect.knsrc new file mode 100644 index 0000000..75d0eac --- /dev/null +++ b/kcmkwin/kwineffects/kwineffect.knsrc @@ -0,0 +1,49 @@ +[KNewStuff3] +Name=Window Manager Effects +Name[az]=Pəncərə meneceri effektləri +Name[ca]=Efectes del gestor de finestres +Name[ca@valencia]=Efectes del gestor de finestres +Name[cs]=Efekty správce oken +Name[da]=Vindueshåndteringseffekter +Name[de]=Fensterverwaltungs-Effekte +Name[el]=Εφέ διαχειριστή παραθύρων +Name[en_GB]=Window Manager Effects +Name[es]=Efectos del gestor de ventanas +Name[et]=Aknahalduri efektid +Name[eu]=Leiho kudeatzailearen efektua +Name[fi]=Ikkunointiohjelman tehosteet +Name[fr]=Effets du gestionnaire de fenêtres +Name[gl]=Efectos do xestor de xanelas +Name[he]=אפקטי מנהל חלונות +Name[hu]=Ablakkezelő-effektusok +Name[ia]=Gerente de effectos de fenestra +Name[id]=Efek Pengelola Window +Name[it]=Effetti del gestore delle finestre +Name[ko]=창 관리자 효과 +Name[lt]=Langų tvarkytuvės efektai +Name[nl]=Effecten van vensterbeheerder +Name[nn]=Effektar for vindaugshandsamar +Name[pa]=ਵਿੰਡੋ ਮੈਨੇਜਰ ਪਰਭਾਵ +Name[pl]=Efekty zarządzania oknami +Name[pt]=Efeitos do Gestor de Janelas +Name[pt_BR]=Efeitos do gerenciador de janelas +Name[ro]=Efectele gestionarului de ferestre +Name[ru]=Эффекты диспетчера окон KWin +Name[sk]=Efekty správcu okien +Name[sl]=Učinki upravljalnika oken +Name[sr]=Ефекти менаџера прозора +Name[sr@ijekavian]=Ефекти менаџера прозора +Name[sr@ijekavianlatin]=Efekti menadžera prozora +Name[sr@latin]=Efekti menadžera prozora +Name[sv]=Fönsterhanteringseffekter +Name[tr]=Pencere Yöneticisi Efektleri +Name[uk]=Ефекти засобу керування вікнами +Name[x-test]=xxWindow Manager Effectsxx +Name[zh_CN]=窗口管理器效果 +Name[zh_TW]=視窗管理員效果 + +ProvidersUrl=https://download.kde.org/ocs/providers.xml +Categories=KWin Effects +StandardResource=tmp +Uncompress=kpackage +KPackageType=KWin/Effect diff --git a/kcmkwin/kwineffects/package/contents/ui/Effect.qml b/kcmkwin/kwineffects/package/contents/ui/Effect.qml new file mode 100644 index 0000000..cd8f294 --- /dev/null +++ b/kcmkwin/kwineffects/package/contents/ui/Effect.qml @@ -0,0 +1,117 @@ +/* + SPDX-FileCopyrightText: 2013 Antonis Tsiapaliokas + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later + +*/ + +import QtQuick 2.5 +import QtQuick.Controls 2.5 as QQC2 +import QtQuick.Layouts 1.1 + +import org.kde.kirigami 2.5 as Kirigami + +Kirigami.SwipeListItem { + id: listItem + hoverEnabled: true + onClicked: { + view.currentIndex = index; + } + contentItem: RowLayout { + id: row + QQC2.RadioButton { + property bool _exclusive: model.ExclusiveRole != "" + property bool _toggled: false + + checked: model.StatusRole + visible: _exclusive + QQC2.ButtonGroup.group: _exclusive ? effectsList.findButtonGroup(model.ExclusiveRole) : null + + onToggled: { + model.StatusRole = checked ? Qt.Checked : Qt.Unchecked; + _toggled = true; + } + onClicked: { + // Uncheck the radio button if it's clicked. + if (checked && !_toggled) { + model.StatusRole = Qt.Unchecked; + } + _toggled = false; + } + } + + QQC2.CheckBox { + checkState: model.StatusRole + visible: model.ExclusiveRole == "" + + onToggled: model.StatusRole = checkState + } + + ColumnLayout { + Layout.topMargin: Kirigami.Units.smallSpacing + Layout.bottomMargin: Kirigami.Units.smallSpacing + spacing: 0 + + Kirigami.Heading { + Layout.fillWidth: true + + level: 5 + text: model.NameRole + wrapMode: Text.Wrap + } + + QQC2.Label { + Layout.fillWidth: true + + text: model.DescriptionRole + opacity: listItem.hovered ? 0.8 : 0.6 + wrapMode: Text.Wrap + } + + QQC2.Label { + id: aboutItem + + Layout.fillWidth: true + + text: i18n("Author: %1\nLicense: %2", model.AuthorNameRole, model.LicenseRole) + opacity: listItem.hovered ? 0.8 : 0.6 + visible: view.currentIndex === index + wrapMode: Text.Wrap + } + + Loader { + id: videoItem + + active: false + source: "Video.qml" + visible: false + + function showHide() { + if (!videoItem.active) { + videoItem.active = true; + videoItem.visible = true; + } else { + videoItem.active = false; + videoItem.visible = false; + } + } + } + } + } + actions: [ + Kirigami.Action { + visible: model.VideoRole.toString() !== "" + icon.name: "videoclip-amarok" + tooltip: i18nc("@info:tooltip", "Show/Hide Video") + onTriggered: videoItem.showHide() + }, + Kirigami.Action { + visible: model.ConfigurableRole + enabled: model.StatusRole != Qt.Unchecked + icon.name: "configure" + tooltip: i18nc("@info:tooltip", "Configure...") + onTriggered: kcm.configure(model.ServiceNameRole, this) + } + ] +} diff --git a/kcmkwin/kwineffects/package/contents/ui/Video.qml b/kcmkwin/kwineffects/package/contents/ui/Video.qml new file mode 100644 index 0000000..f089bd6 --- /dev/null +++ b/kcmkwin/kwineffects/package/contents/ui/Video.qml @@ -0,0 +1,43 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Antonis Tsiapaliokas + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.5 +import QtQuick.Controls 2.5 as QQC2 +import QtQuick.Layouts 1.1 +import QtMultimedia 5.0 as Multimedia + +Multimedia.Video { + id: videoItem + autoPlay: true + source: model.VideoRole + width: 400 + height: 400 + QQC2.BusyIndicator { + anchors.centerIn: parent + visible: videoItem.status == Multimedia.MediaPlayer.Loading + running: true + } + QQC2.Button { + id: replayButton + visible: false + anchors.centerIn: parent + icon.name: "media-playback-start" + onClicked: { + replayButton.visible = false; + videoItem.play(); + } + Connections { + target: videoItem + onStopped: { + replayButton.visible = true + } + } + } +} diff --git a/kcmkwin/kwineffects/package/contents/ui/main.qml b/kcmkwin/kwineffects/package/contents/ui/main.qml new file mode 100644 index 0000000..e46ae5e --- /dev/null +++ b/kcmkwin/kwineffects/package/contents/ui/main.qml @@ -0,0 +1,131 @@ +/* + SPDX-FileCopyrightText: 2013 Antonis Tsiapaliokas + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.5 +import QtQuick.Controls 2.5 as QQC2 +import QtQuick.Layouts 1.1 + +import org.kde.kcm 1.2 +import org.kde.kconfig 1.0 +import org.kde.kirigami 2.10 as Kirigami +import org.kde.private.kcms.kwin.effects 1.0 as Private + +ScrollViewKCM { + ConfigModule.quickHelp: i18n("This module lets you configure desktop effects.") + + header: ColumnLayout { + QQC2.Label { + Layout.fillWidth: true + + elide: Text.ElideRight + text: i18n("Hint: To find out or configure how to activate an effect, look at the effect's settings.") + } + + RowLayout { + Kirigami.SearchField { + id: searchField + + Layout.fillWidth: true + } + + QQC2.ToolButton { + id: filterButton + + icon.name: "view-filter" + + checkable: true + checked: menu.opened + onClicked: menu.popup(filterButton, filterButton.width - menu.width, filterButton.height) + + QQC2.ToolTip { + text: i18n("Configure Filter") + } + } + + QQC2.Menu { + id: menu + + modal: true + + QQC2.MenuItem { + checkable: true + checked: searchModel.excludeUnsupported + text: i18n("Exclude unsupported effects") + + onToggled: searchModel.excludeUnsupported = checked + } + + QQC2.MenuItem { + checkable: true + checked: searchModel.excludeInternal + text: i18n("Exclude internal effects") + + onToggled: searchModel.excludeInternal = checked + } + } + } + } + + view: ListView { + id: effectsList + + property var _buttonGroups: [] + + clip: true + + model: Private.EffectsFilterProxyModel { + id: searchModel + + query: searchField.text + sourceModel: kcm.effectsModel + } + + delegate: Effect { + width: effectsList.width + } + + section.property: "CategoryRole" + section.delegate: Kirigami.ListSectionHeader { + width: effectsList.width + text: section + } + + function findButtonGroup(name) { + for (let item of effectsList._buttonGroups) { + if (item.name == name) { + return item.group; + } + } + + let group = Qt.createQmlObject( + 'import QtQuick 2.5;' + + 'import QtQuick.Controls 2.5;' + + 'ButtonGroup {}', + effectsList, + "dynamicButtonGroup" + effectsList._buttonGroups.length + ); + + effectsList._buttonGroups.push({ name, group }); + + return group; + } + } + + footer: ColumnLayout { + RowLayout { + Layout.alignment: Qt.AlignRight + + QQC2.Button { + icon.name: "get-hot-new-stuff" + text: i18n("Get New Desktop Effects...") + visible: KAuthorized.authorize("ghns") + + onClicked: kcm.openGHNS(this) + } + } + } +} diff --git a/kcmkwin/kwineffects/package/metadata.desktop b/kcmkwin/kwineffects/package/metadata.desktop new file mode 100644 index 0000000..b7556d9 --- /dev/null +++ b/kcmkwin/kwineffects/package/metadata.desktop @@ -0,0 +1,107 @@ +[Desktop Entry] +Name=Desktop Effects +Name[az]=İş Masası effektləri +Name[bs]=Efekti površi +Name[ca]=Efectes de l'escriptori +Name[ca@valencia]=Efectes d'escriptori +Name[cs]=Efekty na ploše +Name[da]=Skrivebordseffekter +Name[de]=Arbeitsflächen-Effekte +Name[el]=Εφέ επιφάνειας εργασίας +Name[en_GB]=Desktop Effects +Name[es]=Efectos del escritorio +Name[et]=Töölauaefektid +Name[eu]=Mahaigaineko efektuak +Name[fi]=Työpöytätehosteet +Name[fr]=Effets de bureau +Name[gl]=Efectos do escritorio +Name[he]=הנפשות שולחן עבודה +Name[hu]=Asztali effektusok +Name[ia]=Effectos de scriptorio +Name[id]=Efek Desktop +Name[it]=Effetti del desktop +Name[ja]=デスクトップ効果 +Name[ko]=데스크톱 효과 +Name[lt]=Darbalaukio efektai +Name[nb]=Skrivebordseffekter +Name[nds]=Schriefdischeffekten +Name[nl]=Bureaubladeffecten +Name[nn]=Skrivebords­effektar +Name[pa]=ਡੈਸਕਟਾਪ ਪਰਭਾਵ +Name[pl]=Efekty pulpitu +Name[pt]=Efeitos do Ecrã +Name[pt_BR]=Efeitos da área de trabalho +Name[ro]=Efecte de birou +Name[ru]=Эффекты +Name[se]=Čállinbeavdeeffeavttat +Name[sk]=Efekty plochy +Name[sl]=Učinki namizja +Name[sr]=Ефекти површи +Name[sr@ijekavian]=Ефекти површи +Name[sr@ijekavianlatin]=Efekti površi +Name[sr@latin]=Efekti površi +Name[sv]=Skrivbordseffekter +Name[tg]=Таъсирҳои мизи корӣ +Name[tr]=Masaüstü Efektleri +Name[uk]=Ефекти стільниці +Name[x-test]=xxDesktop Effectsxx +Name[zh_CN]=桌面特效 +Name[zh_TW]=桌面效果 + +Comment=Desktop Effects +Comment[az]=İş Masası effektləri +Comment[bs]=Efekti površi +Comment[ca]=Efectes de l'escriptori +Comment[ca@valencia]=Efectes d'escriptori +Comment[cs]=Efekty na ploše +Comment[da]=Skrivebordseffekter +Comment[de]=Arbeitsflächen-Effekte +Comment[el]=Εφέ επιφάνειας εργασίας +Comment[en_GB]=Desktop Effects +Comment[es]=Efectos del escritorio +Comment[et]=Töölauaefektid +Comment[eu]=Mahaigaineko efektuak +Comment[fi]=Työpöytätehosteet +Comment[fr]=Effets de bureau +Comment[gl]=Efectos do escritorio +Comment[he]=הנפשות שולחן עבודה +Comment[hu]=Asztali effektusok +Comment[ia]=Effectos de scriptorio +Comment[id]=Efek Desktop +Comment[it]=Effetti del desktop +Comment[ja]=デスクトップ効果 +Comment[ko]=데스크톱 효과 +Comment[lt]=Darbalaukio efektai +Comment[nb]=Skrivebordseffekter +Comment[nds]=Schriefdischeffekten +Comment[nl]=Bureaubladeffecten +Comment[nn]=Skrivebords­effektar +Comment[pa]=ਡੈਸਕਟਾਪ ਪਰਭਾਵ +Comment[pl]=Efekty pulpitu +Comment[pt]=Efeitos do Ecrã +Comment[pt_BR]=Efeitos da área de trabalho +Comment[ro]=Efecte de birou +Comment[ru]=Настройка эффектов рабочего стола +Comment[sk]=Efekty plochy +Comment[sl]=Učinki namizja +Comment[sr]=Ефекти површи +Comment[sr@ijekavian]=Ефекти површи +Comment[sr@ijekavianlatin]=Efekti površi +Comment[sr@latin]=Efekti površi +Comment[sv]=Skrivbordseffekter +Comment[tg]=Таъсирҳои мизи корӣ +Comment[tr]=Masaüstü Efektleri +Comment[uk]=Ефекти стільниці +Comment[x-test]=xxDesktop Effectsxx +Comment[zh_CN]=桌面特效 +Comment[zh_TW]=桌面效果 + +Icon=preferences-desktop-effects +Type=Service +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-Name=kcm_kwin_effects +X-KDE-ServiceTypes=Plasma/Generic +X-Plasma-API=declarativeappletscript +X-KDE-FormFactors=desktop,tablet + +X-Plasma-MainScript=ui/main.qml diff --git a/kcmkwin/kwinoptions/AUTHORS b/kcmkwin/kwinoptions/AUTHORS new file mode 100644 index 0000000..745e9ee --- /dev/null +++ b/kcmkwin/kwinoptions/AUTHORS @@ -0,0 +1,12 @@ +Please use https://bugs.kde.org to report bugs. +The following authors may have retired by the time you read this :-) + +KWM Configuration Module: + + Pat Dowler (dowler@pt1B1106.FSH.UVic.CA) + + Bernd Wuebben + +Conversion to kcontrol applet: + + Matthias Hoelzer (hoelzer@physik.uni-wuerzburg.de) diff --git a/kcmkwin/kwinoptions/CMakeLists.txt b/kcmkwin/kwinoptions/CMakeLists.txt new file mode 100644 index 0000000..8c45d4f --- /dev/null +++ b/kcmkwin/kwinoptions/CMakeLists.txt @@ -0,0 +1,39 @@ +########### next target ############### +# KI18N Translation Domain for this library +add_definitions(-DTRANSLATION_DOMAIN=\"kcmkwm\") + +set(kcm_kwinoptions_PART_SRCS + ${KWin_SOURCE_DIR}/effects/effect_builtins.cpp + main.cpp + mouse.cpp + windows.cpp +) + +ki18n_wrap_ui(kcm_kwinoptions_PART_SRCS + actions.ui + advanced.ui + focus.ui + mouse.ui + moving.ui +) + +kconfig_add_kcfg_files(kcm_kwinoptions_PART_SRCS kwinoptions_settings.kcfgc GENERATE_MOC) +kconfig_add_kcfg_files(kcm_kwinoptions_PART_SRCS kwinoptions_kdeglobals_settings.kcfgc GENERATE_MOC) + +qt5_add_dbus_interface(kcm_kwinoptions_PART_SRCS ${KWin_SOURCE_DIR}/org.kde.kwin.Effects.xml kwin_effects_interface) +add_library(kcm_kwinoptions MODULE ${kcm_kwinoptions_PART_SRCS}) +target_link_libraries(kcm_kwinoptions kwin Qt5::DBus KF5::Completion KF5::I18n KF5::ConfigWidgets KF5::Service KF5::WindowSystem) +install(TARGETS kcm_kwinoptions DESTINATION ${PLUGIN_INSTALL_DIR}) + +########### install files ############### + +install( + FILES + kwinactions.desktop + kwinadvanced.desktop + kwinfocus.desktop + kwinmoving.desktop + kwinoptions.desktop + DESTINATION + ${SERVICES_INSTALL_DIR} +) diff --git a/kcmkwin/kwinoptions/ChangeLog b/kcmkwin/kwinoptions/ChangeLog new file mode 100644 index 0000000..0b92386 --- /dev/null +++ b/kcmkwin/kwinoptions/ChangeLog @@ -0,0 +1,51 @@ +1999-03-06 Mario Weilguni + + * changes for Qt 2.0 + +1998-11-29 Alex Zepeda + + * pics/Makefile.am, pics/mini/Makefile.am: Install icons from their + "proper" subdirectories. + +1998-11-20 Cristian Tibirna + + * advanced.[cpp,h]: fixed bugs. Mostly a disgusting one: + no lists saving for the special options (Decor, Focus a.o.) + +1998-11-09 Cristian Tibirna + + * advanced.[cpp,h] : new tab for some of the last of the + kwm's options which remained out of the GUI config: + CtrlTab, TraverseAll, AltTabeMode, Button3Grab and + the filter lists for decorations, focus, stickyness, + session management ignore ( I kinda disklike the solution + I got for the latest) + +1998-11-06 Cristian Tibirna + + * titlebar.[cpp,h] : added title alignment config + +1998-10-23 Cristian Tibirna + + * titlebar.cpp: completed what Matthias started (took out + useless checks) + * widows.cpp: make autoRaise toggling clearer + +1998-10-22 Matthias Ettrich + + * titlebar.cpp: less options on titlebar doubleclick + +1998-10-21 Cristian Tibirna + + * desktop.[cpp,h]: now with consistent layout use + resizeEvent() deleted + +1998-10-19 Cristian Tibirna + + * windows.[cpp,h]: now with consistent layout use + resizeEvent() deleted + +1998-10-18 Cristian Tibirna + + * titlebar.[cpp,h]: fixed the (in)activetitleebar pixmap selection + 1998-10-21 (still buggy, don't quite understand why) diff --git a/kcmkwin/kwinoptions/Messages.sh b/kcmkwin/kwinoptions/Messages.sh new file mode 100644 index 0000000..e0a423e --- /dev/null +++ b/kcmkwin/kwinoptions/Messages.sh @@ -0,0 +1,3 @@ +#! /usr/bin/env bash +$EXTRACTRC `find . -name \*.ui` >> rc.cpp || exit 11 +$XGETTEXT *.cpp -o $podir/kcmkwm.pot diff --git a/kcmkwin/kwinoptions/actions.ui b/kcmkwin/kwinoptions/actions.ui new file mode 100644 index 0000000..7614c51 --- /dev/null +++ b/kcmkwin/kwinoptions/actions.ui @@ -0,0 +1,539 @@ + + + KWinActionsConfigForm + + + + 0 + 0 + 600 + 500 + + + + + + + Inactive Inner Window Actions + + + + Qt::AlignHCenter|Qt::AlignTop + + + + + &Left click: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_CommandWindow1 + + + + + + + In this row you can customize left click behavior when clicking into an inactive inner window ('inner' means: not titlebar, not frame). + + + + Activate, raise and pass click + + + + + Activate and pass click + + + + + Activate + + + + + Activate and raise + + + + + + + + &Middle click: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_CommandWindow2 + + + + + + + In this row you can customize middle click behavior when clicking into an inactive inner window ('inner' means: not titlebar, not frame). + + + + Activate, raise and pass click + + + + + Activate and pass click + + + + + Activate + + + + + Activate and raise + + + + + + + + &Right click: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_CommandWindow3 + + + + + + + In this row you can customize right click behavior when clicking into an inactive inner window ('inner' means: not titlebar, not frame). + + + + Activate, raise and pass click + + + + + Activate and pass click + + + + + Activate + + + + + Activate and raise + + + + + + + + Mouse &wheel: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_CommandWindowWheel + + + + + + + In this row you can customize behavior when scrolling into an inactive inner window ('inner' means: not titlebar, not frame). + + + + Scroll + + + + + Activate and scroll + + + + + Activate, raise and scroll + + + + + + + + + + + Inner Window, Titlebar and Frame Actions + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + Mo&difier key: + + + kcfg_CommandAllKey + + + + + + + Here you select whether holding the Meta key or Alt key will allow you to perform the following actions. + + + + Meta + + + + + Alt + + + + + + + + + + + 0 + 0 + + + + + 24 + 0 + + + + + + + + Qt::AlignCenter + + + + + + + + + L&eft click: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_CommandAll1 + + + + + + + In this row you can customize left click behavior when clicking into the titlebar or the frame. + + + + Move + + + + + Activate, raise and move + + + + + Toggle raise and lower + + + + + Resize + + + + + Raise + + + + + Lower + + + + + Minimize + + + + + Decrease opacity + + + + + Increase opacity + + + + + Do nothing + + + + + + + + Middle &click: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_CommandAll2 + + + + + + + In this row you can customize middle click behavior when clicking into the titlebar or the frame. + + + + Move + + + + + Activate, raise and move + + + + + Toggle raise and lower + + + + + Resize + + + + + Raise + + + + + Lower + + + + + Minimize + + + + + Decrease opacity + + + + + Increase opacity + + + + + Do nothing + + + + + + + + Right clic&k: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_CommandAll3 + + + + + + + In this row you can customize right click behavior when clicking into the titlebar or the frame. + + + + Move + + + + + Activate, raise and move + + + + + Toggle raise and lower + + + + + Resize + + + + + Raise + + + + + Lower + + + + + Minimize + + + + + Decrease opacity + + + + + Increase opacity + + + + + Do nothing + + + + + + + + Mo&use wheel: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_CommandAllWheel + + + + + + + Here you can customize KDE's behavior when scrolling with the mouse wheel in a window while pressing the modifier key. + + + + Raise/lower + + + + + Shade/unshade + + + + + Maximize/restore + + + + + Keep above/below + + + + + Move to previous/next desktop + + + + + Change opacity + + + + + Do nothing + + + + + + + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + + KComboBox + QComboBox +
kcombobox.h
+
+
+ + +
diff --git a/kcmkwin/kwinoptions/advanced.ui b/kcmkwin/kwinoptions/advanced.ui new file mode 100644 index 0000000..85b6f49 --- /dev/null +++ b/kcmkwin/kwinoptions/advanced.ui @@ -0,0 +1,156 @@ + + + KWinAdvancedConfigForm + + + + 0 + 0 + 600 + 500 + + + + + Qt::AlignHCenter|Qt::AlignTop + + + + + Window &unshading: + + + kcfg_ShadeHover + + + + + + + + + <html><head/><body><p>If this option is enabled, a shaded window will unshade automatically when the mouse pointer has been over the titlebar for some time.</p></body></html> + + + On titlebar hover after: + + + + + + + Sets the time in milliseconds before the window unshades when the mouse pointer goes over the shaded window. + + + ms + + + 0 + + + 3000 + + + 100 + + + 250 + + + + + + + + + Window &placement: + + + kcfg_Placement + + + + + + + <html><head/><body><p>The placement policy determines where a new window will appear on the desktop.</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-style:italic;">Smart</span> will try to achieve a minimum overlap of windows</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-style:italic;">Maximizing</span> will try to maximize every window to fill the whole screen. It might be useful to selectively affect placement of some windows using the window-specific settings.</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-style:italic;">Cascade</span> will cascade the windows</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-style:italic;">Random</span> will use a random position</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-style:italic;">Centered</span> will place the window centered</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-style:italic;">Zero-cornered</span> will place the window in the top-left corner</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-style:italic;">Under mouse</span> will place the window under the pointer</li></ul></body></html> + + + + Minimal Overlapping + + + + + Maximized + + + + + Cascaded + + + + + Random + + + + + Centered + + + + + In Top-Left Corner + + + + + Under mouse + + + + + + + + When turned on, KDE apps which are able to remember the positions of their windows are allowed to do so. This will override the window placement mode defined above. + + + Allow KDE apps to remember the positions of their own windows + + + + + + + &Special windows: + + + kcfg_HideUtilityWindowsForInactive + + + + + + + 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. + + + Hide utility windows for inactive applications + + + + + + + + KComboBox + QComboBox +
kcombobox.h
+
+
+ + +
diff --git a/kcmkwin/kwinoptions/focus.ui b/kcmkwin/kwinoptions/focus.ui new file mode 100644 index 0000000..8bf07ed --- /dev/null +++ b/kcmkwin/kwinoptions/focus.ui @@ -0,0 +1,298 @@ + + + KWinFocusConfigForm + + + + 0 + 0 + 600 + 500 + + + + + + + Qt::AlignHCenter|Qt::AlignTop + + + + + Window &activation policy: + + + windowFocusPolicy + + + + + + + With this option you can specify how and when windows will be focused. + + + + Click to focus + + + + + Click to focus (mouse precedence) + + + + + Focus follows mouse + + + + + Focus follows mouse (mouse precedence) + + + + + Focus under mouse + + + + + Focus strictly under mouse + + + + + + + + &Delay focus by: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_DelayFocusInterval + + + + + + + This is the delay after which the window the mouse pointer is over will automatically receive focus. + + + ms + + + 0 + + + 3000 + + + 100 + + + + + + + Focus &stealing prevention: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_FocusStealingPreventionLevel + + + + + + + <html><head/><body><p>This option specifies how much KWin will try to prevent unwanted focus stealing caused by unexpected activation of new windows. (Note: This feature does not work with the <span style=" font-style:italic;">Focus under mouse</span> or <span style=" font-style:italic;">Focus strictly under mouse</span> focus policies.) </p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-style:italic;">None:</span> Prevention is turned off and new windows always become activated.</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-style:italic;">Low:</span> Prevention is enabled; when some window does not have support for the underlying mechanism and KWin cannot reliably decide whether to activate the window or not, it will be activated. This setting may have both worse and better results than the medium level, depending on the applications.</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-style:italic;">Medium:</span> Prevention is enabled.</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-style:italic;">High:</span> New windows get activated only if no window is currently active or if they belong to the currently active application. This setting is probably not really usable when not using mouse focus policy.</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-style:italic;">Extreme:</span> All windows must be explicitly activated by the user.</li></ul><p>Windows that are prevented from stealing focus are marked as demanding attention, which by default means their taskbar entry will be highlighted. This can be changed in the Notifications control module.</p></body></html> + + + + None + + + + + Low + + + + + Medium + + + + + High + + + + + Extreme + + + + + + + + Raising windows: + + + + + + + When this option is enabled, the active window will be brought to the front when you click somewhere into the window contents. To change it for inactive windows, you need to change the settings in the Actions tab. + + + &Click raises active window + + + + + + + + + When this option is enabled, a window in the background will automatically come to the front when the mouse pointer has been over it for some time. + + + &Raise on hover, delayed by: + + + + + + + This is the delay after which the window that the mouse pointer is over will automatically come to the front. + + + ms + + + 0 + + + 3000 + + + 100 + + + + + + + + + Multiscreen behavior: + + + + + + + When this option is enabled, the active Xinerama screen (where new windows appear, for example) is the screen containing the mouse pointer. When disabled, the active Xinerama screen is the screen containing the focused window. By default this option is disabled for Click to focus and enabled for other focus policies. + + + Active screen follows &mouse + + + + + + + When this option is enabled, focus operations are limited only to the active Xinerama screen + + + &Separate screen focus + + + + + + + + 280 + 0 + + + + Window activation policy description + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + KComboBox + QComboBox +
kcombobox.h
+
+
+ + + + kcfg_AutoRaise + toggled(bool) + kcfg_AutoRaiseInterval + setEnabled(bool) + + + 338 + 189 + + + 485 + 189 + + + + + kcfg_AutoRaise + toggled(bool) + kcfg_ClickRaise + setDisabled(bool) + + + 338 + 189 + + + 333 + 155 + + + + +
diff --git a/kcmkwin/kwinoptions/kwinactions.desktop b/kcmkwin/kwinoptions/kwinactions.desktop new file mode 100644 index 0000000..183b5e8 --- /dev/null +++ b/kcmkwin/kwinoptions/kwinactions.desktop @@ -0,0 +1,122 @@ +[Desktop Entry] +Icon=preferences-system-windows-action +Type=Service +X-KDE-ServiceTypes=KCModule +Exec=kcmshell5 kwinactions +X-DocPath=kcontrol/windowbehaviour/index.html#titlebar-actions +Icon=preferences-system-windows-actions + +X-KDE-Library=kcm_kwinoptions +X-KDE-PluginKeyword=kwinactions + +Name=Window Actions +Name[az]=Pəncərə fəaliyyətləri +Name[ca]=Accions de la finestra +Name[cs]=Činnosti oken +Name[da]=Vindueshandlinger +Name[de]=Fensteraktionen +Name[en_GB]=Window Actions +Name[es]=Acciones de ventanas +Name[et]=Akna toimingud +Name[eu]=Leihoetako ekintzak +Name[fi]=Ikkunan toiminnot +Name[fr]=Actions de la fenêtre +Name[gl]=Accións da xanela +Name[ia]=Actiones de fenestra +Name[id]=Aksi Window +Name[it]=Azioni delle finestre +Name[ko]=창 동작 +Name[lt]=Langų veiksmai +Name[nl]=Vensteracties +Name[nn]=Handlingar for vindauge +Name[pl]=Zachowanie okien +Name[pt]=Acções das Janelas +Name[pt_BR]=Ações da janela +Name[ro]=Acțiuni fereastră +Name[ru]=Действия с окнами +Name[sk]=Akcie okna +Name[sl]=Dejavnosti oken +Name[sv]=Fönsteråtgärder +Name[uk]=Дії з вікнами +Name[x-test]=xxWindow Actionsxx +Name[zh_CN]=窗口动作 +Name[zh_TW]=視窗動作 + +Comment=Configure mouse actions for windows and titlebars +Comment[az]=Pəncərələr və başlıqlar üçün siçan fəaliyyətlərinin tənzimlənməsi +Comment[ca]=Configura les accions del ratolí per a les finestres i les barres de títol +Comment[cs]=Nastavení činností myši na oknech a pruhu titulku +Comment[da]=Indstil mushandlinger for vinduer og titellinjer +Comment[de]=Mausaktionen für Fenster und Titelleisten einrichten +Comment[en_GB]=Configure mouse actions for windows and titlebars +Comment[es]=Configurar las acciones del ratón para las ventanas y las barras de títulos +Comment[et]=Hiiretoimingute seadistamine akendes ja tiitliribadel +Comment[eu]=Konfiguratu saguaren ekintzak leihoekin eta titulu-barrekin +Comment[fi]=Ikkunoiden ja otsikkopalkkien hiiritoimintojen asetukset +Comment[fr]=Configurer les actions de la souris pour les fenêtres et barres de titres +Comment[gl]=Configurar as accións do rato para xanelas e barras de título +Comment[ia]=Configura actiones de mus per fenestras e barras de titulo +Comment[id]=Konfigurasikan aksi mouse untuk window dan bilah-judul +Comment[it]=Configura azioni del mouse per finestre e barre del titolo +Comment[ko]=창과 제목 표시줄 마우스 동작 설정 +Comment[lt]=Konfigūruoti pelės veiksmus langams ir antraštės juostoms +Comment[nl]=Muisacties voor vensters en titelbalken configureren +Comment[nn]=Set opp musehandlingar for vindauge og tittellinjer +Comment[pl]=Ustawienia działań myszy dla okien i pasków tytułu +Comment[pt]=Configurar as acções do rato para as janelas e barras do título +Comment[pt_BR]=Configura as ações do mouse para as janelas e barras de títulos +Comment[ro]=Configurează acțiunile mausului pentru ferestre și bare de titlu +Comment[ru]=Настройка действий для окон и их заголовков +Comment[sk]=Nastavenie akcií myši pre okná a záhlavia +Comment[sl]=Nastavi aktivnosti miške za okna in naslovne vrstice +Comment[sv]=Anpassa musåtgärder för fönster och namnlister +Comment[uk]=Налаштовування керування мишею для вікон та смужок заголовків +Comment[x-test]=xxConfigure mouse actions for windows and titlebarsxx +Comment[zh_CN]=配置窗口和标题栏的鼠标动作 +Comment[zh_TW]=設定視窗及標題列的滑鼠動作 + +X-KDE-Keywords=shade,maximise,maximize,minimize,minimise,lower,operations menu,titlebar,resize +X-KDE-Keywords[az]=kölgə,genişlənmə,genişləndirmə,yığılma,zəif,əməliyyat menyusu,başlıq,ölçü dəyişmə +X-KDE-Keywords[bs]=sjena,povećali,povećala,smanjiti,umanjiti,sniziti,izbornik operacija,naslovnica,promjena veličine +X-KDE-Keywords[ca]=ombra,maximitza,maximitza,minimitza,minimitza,abaixa,menú d'operacions,barra de títol,redimensiona +X-KDE-Keywords[ca@valencia]=ombra,maximitza,maximitza,minimitza,minimitza,abaixa,menú d'operacions,barra de títol,redimensiona +X-KDE-Keywords[da]=skyg,maksimer,minimer,nedre,operationsmenu,titellinje,ændr størrelse +X-KDE-Keywords[de]=Fenstermenü,Fensterheber,Maximieren,Minimieren,Nach oben/unten,Titelleiste,Größe ändern +X-KDE-Keywords[el]=σκιά,μεγιστοποίηση,μεγιστοποίηση,ελαχιστοποίηση,ελαχιστοποίηση, χαμηλότερα,μενού λειτουργιών,γραμμή τίτλου,αλλαγή μεγέθους +X-KDE-Keywords[en_GB]=shade,maximise,maximize,minimize,minimise,lower,operations menu,titlebar,resize +X-KDE-Keywords[es]=sombra,maximizar,maximizar,minimizar,minimizar,inferior,menú de operaciones,barra de título,cambio de tamaño +X-KDE-Keywords[et]=varjamine,peitmine,maksimeerimine,minimeerimine,allakerimine,üleskerimine,menüü,tiitliriba,suuruse muutmine +X-KDE-Keywords[eu]=bildu,maximizatu,ikonotu,jaitsi,eragiketa-menua,titulu-barra,tamainaz aldatu +X-KDE-Keywords[fi]=varjosta,rullaa,suurenna,pienennä,laske,toimintovalikko,otsikkopalkki,muuta kokoa +X-KDE-Keywords[fr]=ombre, maximiser, maximise, minimiser, minimise, menu des opérations, barre de titre, redimensionner +X-KDE-Keywords[ga]=scáth,scáthaigh,uasmhéadaigh,íosmhéadaigh,íoslaghdaigh,laghdaigh,roghchlár oibríochta,barra teidil,athraigh méid +X-KDE-Keywords[gl]=sombra,sombrear,maximizar,minimizar,recoller,menú de operacións, barra de título, redimensionar +X-KDE-Keywords[hu]=árnyék,maximalizálás,maximalizálás,minimalizálás,minimalizálás,alacsonyabb,műveletek menü,címsáv,átméretezés +X-KDE-Keywords[ia]=tinta,maximisa,maximisa,minimisa,minimisa,plus basse,menu de operationes,barra de titulo, redimensionar +X-KDE-Keywords[id]=bayangan,maksimalkan,maksimalkan,minimalkan,minimalkan,ke bawah,menu operasi,titlebar,ubah ukuran +X-KDE-Keywords[it]=ombra,massimizza,minimizza,abbassa,menu operazioni,barra del titolo,ridimensiona +X-KDE-Keywords[kk]=shade,maximise,maximize,minimize,minimise,lower,operations menu,titlebar,resize +X-KDE-Keywords[km]=shade,maximise,maximize,minimize,minimise,lower,operations menu,titlebar,resize +X-KDE-Keywords[ko]=shade,maximise,maximize,minimize,minimise,lower,operations menu,titlebar,resize,최대화,최소화,제목 표시줄,크기 조정 +X-KDE-Keywords[lt]=pridengti,išskleisti,isskleisti,išdidinti,isdidinti,suskleisti,sumažinti,sumazinti,nuleisti į antrą planą,nuleisti i antra plana,operacijų meniu,operaciju meniu,antraštės juosta,antrastes juosta,keisti dydį,keisti dydi +X-KDE-Keywords[nb]=rull,maksimer,minimer,senk,handlinger,meny,tittellinje,endre størrelse +X-KDE-Keywords[nds]=Inrullen,maximeren,minimeren,na achtern,Akschonenmenü,Titelbalken,Finsternmenü,Grött ännern +X-KDE-Keywords[nl]=verdonkeren,maximaliseren,minimaliseren,naar onderen,bedieningsmenu,titelbalk,grootte wijzigen +X-KDE-Keywords[nn]=rull,fald saman,fald ut,samanfalding,maksimer,minimer,senk,handlingar,meny,tittellinje,storleiksendring +X-KDE-Keywords[pl]=zwiń,maksymalizuj,minimalizuj,obniż,operacje na menu,pasek tytułu,zmień rozmiar +X-KDE-Keywords[pt]=enrolar,maximizar,minimizar,baixar,menu de operações,barra de título,dimensionar +X-KDE-Keywords[pt_BR]=enrolar,maximizar,minimizar,baixar,menu de operações,barra de título,redimensionar +X-KDE-Keywords[ro]=strânge,maximizează,minimizează,coboară,meniu operații,bară de titlu,redimensionează +X-KDE-Keywords[ru]=shade,maximise,maximize,minimize,minimise,lower,operations menu,titlebar,resize,свернуть,распахнуть,убрать вниз,меню операций,меню действий,заголовок окна,заголовок,изменить размер +X-KDE-Keywords[sk]=tieň,maximalizácia,maximalizovanie,minimalizácia,minimalizovanie,nižší,ponuka operácií,záhlavie,zmeniť veľkosť +X-KDE-Keywords[sl]=zvij,povečaj,razpni,pomanjšaj,skrči,dvigni,spusti,naslovna vrstica,spremeni velikost,okenski meni,meni okna +X-KDE-Keywords[sr]=shade,maximise,maximize,minimize,minimise,lower,operations menu,titlebar,resize,сенка,максимизуј,минимизуј,спусти,мени радњи,насловна трака,промени величину +X-KDE-Keywords[sr@ijekavian]=shade,maximise,maximize,minimize,minimise,lower,operations menu,titlebar,resize,сенка,максимизуј,минимизуј,спусти,мени радњи,насловна трака,промени величину +X-KDE-Keywords[sr@ijekavianlatin]=shade,maximise,maximize,minimize,minimise,lower,operations menu,titlebar,resize,senka,maksimizuj,minimizuj,spusti,meni radnji,naslovna traka,promeni veličinu +X-KDE-Keywords[sr@latin]=shade,maximise,maximize,minimize,minimise,lower,operations menu,titlebar,resize,senka,maksimizuj,minimizuj,spusti,meni radnji,naslovna traka,promeni veličinu +X-KDE-Keywords[sv]=skugga,maximera,minimera,åtgärdsmeny,namnlist,ändra storlek +X-KDE-Keywords[tr]=geri yükle, gölgele,büyüt,küçült,aşağı al,işlemler menüsü,başlık çubuğu,yeniden boyutlandır +X-KDE-Keywords[uk]=shade,maximise,maximize,minimize,minimise,lower,operations menu,titlebar,resize,тінь,максимізувати,розгорнути,згорнути,нижче,меню дій,заголовок,смужка заголовка,розмір,розміри,зміна розмірів +X-KDE-Keywords[x-test]=xxshadexx,xxmaximisexx,xxmaximizexx,xxminimizexx,xxminimisexx,xxlowerxx,xxoperations menuxx,xxtitlebarxx,xxresizexx +X-KDE-Keywords[zh_CN]=shade,maximise,maximize,minimize,minimise,lower,operations menu,titlebar,resize,阴影,最大化,最小化,降低,动作,菜单,标题栏,更改大小 +X-KDE-Keywords[zh_TW]=shade,maximise,maximize,minimize,minimise,lower,operations menu,titlebar,resize diff --git a/kcmkwin/kwinoptions/kwinadvanced.desktop b/kcmkwin/kwinoptions/kwinadvanced.desktop new file mode 100644 index 0000000..e8ef759 --- /dev/null +++ b/kcmkwin/kwinoptions/kwinadvanced.desktop @@ -0,0 +1,106 @@ +[Desktop Entry] +Icon=preferences-system-windows-actions +Type=Service +X-KDE-ServiceTypes=KCModule +Exec=kcmshell5 kwinadvanced +X-DocPath=kcontrol/windowbehaviour/index.html#action-advanced + +X-KDE-Library=kcm_kwinoptions +X-KDE-PluginKeyword=kwinadvanced + +Name=Advanced Window Behavior +Name[az]=Əlavə pəncərə davranışları +Name[ca]=Comportament avançat de les finestres +Name[cs]=Pokročilé chování oken +Name[da]=Avanceret vinduesopførsel +Name[de]=Erweitertes Fensterverhalten +Name[en_GB]=Advanced Window Behaviour +Name[es]=Comportamiento avanzado de las ventanas +Name[et]=Täpsem akende käitumine +Name[eu]=Leihoaren portaera aurreratua +Name[fi]=Ikkunatoiminnan lisäasetukset +Name[fr]=Comportement avancé des fenêtres +Name[gl]=Comportamento avanzado das xanelas +Name[ia]=Comportamento de fenestra avantiate +Name[id]=Perilaku Window Tingkat-Lanjut +Name[it]=Comportamento avanzato delle finestre +Name[ko]=고급 창 행동 +Name[lt]=Išplėstinė langų elgsena +Name[nl]=Geavanceerd venstergedrag +Name[nn]=Avansert vindaugsåtferd +Name[pl]=Zaawansowane zachowania okien +Name[pt]=Comportamento Avançado das Janelas +Name[pt_BR]=Comportamento avançado das janelas +Name[ro]=Comportament avansat al ferestrelor +Name[ru]=Расширенное поведение окон +Name[sk]=Pokročilé správanie okien +Name[sl]=Napredno obnašanje oken +Name[sv]=Avancerat fönsterbeteende +Name[uk]=Додаткові параметри вікон +Name[x-test]=xxAdvanced Window Behaviorxx +Name[zh_CN]=高级窗口行为 +Name[zh_TW]=進階視窗行為 + +Comment=Configure advanced window management options +Comment[az]=Əlavə pəncərə idarəedilməsi seçimlərini tənzimləmək +Comment[ca]=Configura les opcions avançades per a la gestió de les finestres +Comment[cs]=Nastavení pokročilých voleb správce oken +Comment[da]=Indstil avancerede vindueshåndteringsegenskaber +Comment[de]=Einstellungen für erweiterte Fensterverwaltung einrichten +Comment[en_GB]=Configure advanced window management options +Comment[es]=Configurar las opciones avanzadas de la gestión de ventanas +Comment[et]=Muude aknahalduse valikute seadistamine +Comment[eu]=Konfiguratu leiho kudeaketa aukera aurreratuak +Comment[fi]=Ikkunanhallinnan lisäasetukset +Comment[fr]=Configurer les options de gestion avancée des fenêtres +Comment[gl]=Configurar as funcionalidades avanzadas da xestión de xanelas +Comment[ia]=Configura optiones avantiate de gestion de fenestra +Comment[id]=Konfigurasikan opsi pengelolaan window tingkat-lanjut +Comment[it]=Configura opzioni avanzate della gestione delle finestre +Comment[ko]=고급 창 관리자 기능 설정 +Comment[lt]=Konfigūruoti išplėstines langų tvarkymo parinktis +Comment[nl]=Geavanceerde vensterbeheermogelijkheden configureren +Comment[nn]=Set opp avanserte vindaugshandsamarinnstillingar +Comment[pl]=Zaawansowane ustawienia zarządzania oknami +Comment[pt]=Configurar as funcionalidades de gestão de janelas avançadas +Comment[pt_BR]=Configure as opções avançadas de gerenciamento de janelas +Comment[ro]=Configurează opțiuni avansate de gestiune a ferestrelor +Comment[ru]=Настройка дополнительных возможностей управления окнами +Comment[sk]=Nastaviť pokročilé možnosti správy okien +Comment[sl]=Nastavi napredne zmožnosti upravljanja oken +Comment[sv]=Anpassa avancerade fönsterhanteringsalternativ +Comment[uk]=Налаштовування додаткових параметрів керування вікнами +Comment[x-test]=xxConfigure advanced window management optionsxx +Comment[zh_CN]=配置高级窗口管理选项 +Comment[zh_TW]=設定進階視窗管理選項 + +X-KDE-Keywords=unshade,unshading,shade,shading,border,hover,active borders,tiling,tabs,tabbing,window,window tabbing,window grouping,window tiling,placement,window placement,placement of windows,window advanced behavior +X-KDE-Keywords[az]=kölgələmə,kölgələnmə,kölgə,qaralma,çərçivə,örtük,aktiv çərçivələr,mozaika,vərəqlər,vərəqləmə,pəncərə,pəncərə vərəqlənməsi,pəncərə qrupları,pəncərə mozaikası,yerləşmə,pəncərə yerləşməsi,pəncərənin yerləşdirilməsi,əlavə pəncərə davranışları +X-KDE-Keywords[ca]=desplega,desplegament,ombra,vora,passar per sobre,vores actives,mosaic,pestanyes,pestanyes de finestra,finestra,agrupació de les finestres,mosaic de les finestres,col·locació,col·locació de les finestres,comportament avançat de les finestres +X-KDE-Keywords[da]=kant,hover,aktive kanter,tiling,faneblade,vinduesfaneblade,gruppering af vinduer,vinduesplacering,placering,placering af vinduer,avanceret vinduesopførsel +X-KDE-Keywords[de]=Fensterheber,Rand,Überfahren,Aktive Ränder,Kacheln,Unterfenster,Fenstergruppierung,Fensterkachelung,Fensteranordnung,Erweitertes Fensterverhalten +X-KDE-Keywords[en_GB]=unshade,unshading,shade,shading,border,hover,active borders,tiling,tabs,tabbing,window,window tabbing,window grouping,window tiling,placement,window placement,placement of windows,window advanced behaviour +X-KDE-Keywords[es]=desplegar,extender,recoger,plegar,borde,pasada,bordes activos,mosaico,pestañas,páginas en pestañas,ventana,pestañas de páginas,agrupación de ventanas,ventanas en mosaico,posicionamiento,posicionamiento de ventanas,comportamiento avanzado de las ventanas +X-KDE-Keywords[et]=varjamine,piire,kohalviibimine,aktiivsed piirded,paanimine,kaardid,aknad kaartidena,akende rühmitamine,akende paanimine,akende paigutus,akende täpne käitumine,aken,paigutus +X-KDE-Keywords[eu]=zabaldu,zabaltzea,bildu,biltzea,gainetik pasatzea,ertz aktiboak,lauzatze,fitxak,leihoa,leihoen fitxak,leihoak lauza moduan,leihoen kokapena,leihoen jokabide aurreratua +X-KDE-Keywords[fi]=rullauksen avaus,rullauksen avaaminen,rullaus,rullaaminen,kehys,leijunta,aktiiviset reunat,asettelu,välilehdet,ikkunavälilehdet,ikkunoiden ryhmittely,ikkunoiden asettelu,sijoittaminen,sijoittelu,ikkunoiden sijoittaminen,ikkunoiden sijoittelu,ikkunoiden lisäasetukset +X-KDE-Keywords[fr]=non ombré, dé-nuancé, nuancé, ombré, bordure, survol, bords actifs, mosaïque, onglets, changement d'onglet, fenêtre, changement de fenêtres, regroupement de fenêtres, mosaïque de fenêtres, placement de fenêtres, positionnement de fenêtre, comportement avancé des fenêtres +X-KDE-Keywords[gl]=dessombrar,dessombramento,sombra,bordo,beira,pasar,bordos activos,beiras activas,xanela,separadores,agrupar xanelas, situación das xanelas,colocación,colocación das xanelas,comportamento avanzado das xanelas +X-KDE-Keywords[ia]=unshade,unshading,shade,shading,border,hover,active borders,tiling,tabs,tabbing,window,window tabbing,window grouping,window tiling,placement,window placement,placement of windows,window advanced behavior +X-KDE-Keywords[id]=tak berbayang,tak bayang,berbayang,batas,melayang,bingkai aktif,pengubinan,tab,pengetaban,window,pengetaban window,pengelompokan window,pengubinan window,penempatan,penempatan window,penempatan si window,perilaku tingkat-lanjut window +X-KDE-Keywords[it]=arrotola,srotola,ombra,ombreggiatura,bordo,sovrapponi,bordi attivi,affiancamento,schede,navigazione a schede,finestre,finestre a schede,raggruppamento finestre,affiancamento finestre,posizionamento,posizionamento finestre,posizionamento delle finestre,comportamento avanzato delle finestre +X-KDE-Keywords[ko]=unshade,unshading,shade,shading,border,hover,active borders,tiling,tabs,tabbing,window,window tabbing,window grouping,window tiling,placement,window placement,placement of windows,window advanced behavior,그림자,경계선,호버,지나다니기,타일,탭,창 탭,창 그룹,창 타일,창 위치,말아 올리기,풀어 내리기,배치,창 +X-KDE-Keywords[lt]=atidengti,atidengimas,pridengti,pridengimas,rėmelis,remelis,remas,rėmas,pelės užvedimas,peles uzvedimas,užvesti pelę,uzvesti pele,kortelės,korteles,langas,langai,langų kortelės,langu korteles,langų grupavimas,langu grupavimas,langų išklojimas,langu isklojimas,išdėstymas,isdestymas,padėjimas,padejimas,langų išdėstymas,langu isdestymas,langų padėjimas,langu padejimas,išplėstinė langų elgsena,isplestine langu elgsena,išplėstinė lango elgsena,isplestine lango elgsena,išplėstinis langų elgesys,isplestinis langu elgesys,išplėstinis lango elgesys,isplestinis lango elgesys +X-KDE-Keywords[nl]=verhelderen,verheldering,schaduw,verduistering,rand,boven zweven,actieve randen,schuin achter elkaar,tabbladen,met tabbladen werken,venster,vensterwisseling,verstergroepering,vensters schuin achter elkaar,plaatsing,vensterplaatsing,plaatsing van vensters,geavanceerd gedrag van vensters +X-KDE-Keywords[nn]=opprulling,kant,sveva,aktive kantar,flislegging,faner,vindaugsfaner,vindaugsgruppering,vindaugsflislegging,vindaugsplassering,plassering,plassering av vindauge,avansert vindaugsåtferd +X-KDE-Keywords[pl]=rozwijanie,zwijanie,obramowanie,unoszenie,aktywne obramowania,kafelkowanie,karty,tworzenie kart,okno, umieszczanie okien w kartach,grupowanie okien,kafelkowanie okien,umieszczanie,umieszczanie okien, zaawansowane zachowania okien +X-KDE-Keywords[pt]=sombra,contorno,passagem,contornos activos,lado-a-lado,páginas,páginas da janela,agrupamento de janelas,janelas lado-a-lado,colocação das janelas,comportamento avançado das janelas +X-KDE-Keywords[pt_BR]=enrolar,desenrolar,desenrolando,sombra,contorno,passagem,contornos ativos, lado a lado,janela,páginas,páginas da janela,agrupamento de janelas,janelas lado a lado,colocação das janelas,colocação,comportamento avançado das janelas +X-KDE-Keywords[ru]=unshade,unshading,shade,shading,border,hover,active borders,tiling,tabs,tabbing,window,window tabbing,window grouping,window tiling,placement,window placement,placement of windows,window advanced behavior,затенение,сворачивание в заголовок,разворачивание из заголовка,граница,наведение,активные границы,мозаика,вкладки,окна во вкладках,группировка окон,мозаичный режим,окно,расположение окон,расширенное поведение окон +X-KDE-Keywords[sk]=tieňovanie,okraj,prechod,aktívne okraje,dlaždicovanie,karty,kartovanie okien, zoskupovanie okien,dlaždicovanie okien,umiestnenie okna,poloha okien,pokročilé správanie okien +X-KDE-Keywords[sl]=osvetljevanje,senčenje,zvijanje,rob,obroba,robovi,obrobe,prehod,lebdenje,tlakovanje,zavihki,združevanje oken,tlakovanje oken,postavljanje oken,postavitev oken,napredno obnašanje oken +X-KDE-Keywords[sv]=skugga,skuggning,kanter,hålla musen över,aktiva kanter,sida vid sida,flikar,fönster,fönsterflikar,fönstergruppering,fönster sida vid sida,placering,fönsterplacering,placering av fönster,avancerat fönsterbeteende +X-KDE-Keywords[uk]=unshade,unshading,shade,shading,border,hover,active borders,tiling,tabs,tabbing,window tabbing,window grouping,window tiling,placement?window placement,placement of windows,window advanced behavior,прибирання тіні,тінь,тіні,границі,межі,краї,активні краї,плитка,тайлінґ,вкладки,мозаїка,вікно з вкладками,групування вікон,розташування,розташування вікон, додаткові ефекти поведінки +X-KDE-Keywords[x-test]=xxunshadexx,xxunshadingxx,xxshadexx,xxshadingxx,xxborderxx,xxhoverxx,xxactive bordersxx,xxtilingxx,xxtabsxx,xxtabbingxx,xxwindowxx,xxwindow tabbingxx,xxwindow groupingxx,xxwindow tilingxx,xxplacementxx,xxwindow placementxx,xxplacement of windowsxx,xxwindow advanced behaviorxx +X-KDE-Keywords[zh_CN]=阴影,投影,边框,边界,悬停,激活边界,平铺,标签,窗口,窗口标签,窗口分组,平铺窗口,窗口位置,窗口高级行为 +X-KDE-Keywords[zh_TW]=unshade,unshading,shade,shading,border,hover,active borders,tiling,tabs,tabbing,window,window tabbing,window grouping,window tiling,placement,window placement,placement of windows,window advanced behavior diff --git a/kcmkwin/kwinoptions/kwinfocus.desktop b/kcmkwin/kwinoptions/kwinfocus.desktop new file mode 100644 index 0000000..c2dc7ae --- /dev/null +++ b/kcmkwin/kwinoptions/kwinfocus.desktop @@ -0,0 +1,107 @@ +[Desktop Entry] +Icon=preferences-system-windows +Type=Service +X-KDE-ServiceTypes=KCModule +Exec=kcmshell5 kwinfocus +X-DocPath=kcontrol/windowbehaviour/index.html#action-focus + +X-KDE-Library=kcm_kwinoptions +X-KDE-PluginKeyword=kwinfocus + +Name=Window Focus Behavior +Name[az]=Pəncərə fokuslama davranışları +Name[ca]=Comportament del focus de les finestres +Name[cs]=Chování při zaměření na okno +Name[da]=Opførsel af vinduesfokus +Name[en_GB]=Window Focus Behaviour +Name[es]=Comportamiento del foco de las ventanas +Name[et]=Akende fookuse käitumine +Name[eu]=Leiho fokuaren portaera +Name[fi]=Ikkunoiden kohdistuskäytäntö +Name[fr]=Comportement de focus des fenêtres +Name[gl]=Comportamento do foco das xanelas +Name[ia]=Comportamento de foco de fenestra +Name[id]=Perilaku Fokus Window +Name[it]=Comportamento del fuoco delle finestre +Name[ko]=창 초점 동작 +Name[lt]=Langų fokusavimo elgsena +Name[nl]=Focusgedrag venster +Name[nn]=Fokus­åtferd for vindauge +Name[pl]=Zachowanie uaktywnienia okien +Name[pt]=Comportamento do Foco da Janela +Name[pt_BR]=Comportamento do foco da janela +Name[ro]=Comportament focalizare ferestre +Name[ru]=Фокус окон +Name[sk]=Správanie zamerania okien +Name[sl]=Obnašanje oken pri osredotočanju +Name[sv]=Fönsterfokusbeteende +Name[uk]=Параметри фокусування вікон +Name[vi]=Ứng xử nhắm của cửa sổ +Name[x-test]=xxWindow Focus Behaviorxx +Name[zh_CN]=窗口焦点行为 +Name[zh_TW]=視窗焦點行為 + +Comment=Configure window activation policy +Comment[az]=Pəncərə aktivliyi qaydalarını tənzimləmək +Comment[ca]=Configura la política d'activació de les finestres +Comment[cs]=Nastavení pravidel aktivování oken +Comment[da]=Indstil vinduers aktiveringspolitik +Comment[en_GB]=Configure window activation policy +Comment[es]=Configurar la política de activación de ventanas +Comment[et]=Akende aktiveerimise reeglite seadistamine +Comment[eu]=Konfiguratu leihoa aktibatzeko gidalerroak +Comment[fi]=Ikkunoiden aktivointikäytännön asetukset +Comment[fr]=Configurer la politique d'activation des fenêtres +Comment[gl]=Configurar a política de activación de xanelas +Comment[ia]=Configura le politica de activation de fenestra +Comment[id]=Konfigurasikan kebijakan pengaktifan window +Comment[it]=Configura criteri di attivazione delle finestre +Comment[ko]=창 활성화 정책 설정 +Comment[lt]=Konfigūruoti langų aktyvavimo politiką +Comment[nl]=Vensteractiveringsbeleid configureren +Comment[nn]=Set opp praksisen for vindaugsaktivering +Comment[pl]=Ustawienia uaktywniania okien +Comment[pt]=Configurar a política de activação da janela +Comment[pt_BR]=Configure a política de ativação de janela +Comment[ro]=Configurează politica de activare a ferestrelor +Comment[ru]=Настройка активации окон +Comment[sk]=Nastavenie spôsobu aktivácie okien +Comment[sl]=Nastavi politiko aktiviranja okna +Comment[sv]=Anpassa policy för fönsteraktivering +Comment[uk]=Налаштовування правил активації вікон +Comment[vi]=Cấu hình chính sách kích hoạt cửa sổ +Comment[x-test]=xxConfigure window activation policyxx +Comment[zh_CN]=配置窗口激活策略 +Comment[zh_TW]=設定視窗啟用策略 + +X-KDE-Keywords=focus,auto raise,raise,click raise,keyboard,CDE,alt-tab,all desktop,focus follows mouse,focus prevention,focus stealing,focus policy,window focus behavior,window screen behavior +X-KDE-Keywords[az]=fokus,qaldırmaq,avtomatik qaldırma,klaviatura,CDE,bütün iş masaları,fokususn siçanı izləməsi,fokusun əngəllənməsi,fokus oğurlanması,fokus qaydaları,pəncərənin fokuslanması davranışları,pəncərə ekran davranışları +X-KDE-Keywords[ca]=focus,elevació automàtica,elevació,elevació en clic,teclat,CDE,alt-tab,tots els escriptoris,focus segueix el ratolí,prevenció del focus,robatori del focus,política del focus,comportament del focus de la finestra,comportament en pantalla de la finestra +X-KDE-Keywords[da]=fokus,autohæv,hæv,klikhæv,tastatur,CDE,alt-tab,alle skriveborde,fokus følger mus,fokusforhindring,stjæler fokus,fokuspolitik,opførsel for vinduesfokus +X-KDE-Keywords[de]=Fokus,Aktivierung,Automatisch nach vorne,Auf Klick nach vorne,Tastatur,CDE,Alt-Tab,Alle Arbeitsflächen,Aktivierung bei Mauskontakt,Vorbeugung gegen unerwünschte Aktivierung,Aktivierungsregel,Aktivierungsverhalten des Fensters,Fensterverhalten +X-KDE-Keywords[en_GB]=focus,auto raise,raise,click raise,keyboard,CDE,alt-tab,all desktop,focus follows mouse,focus prevention,focus stealing,focus policy,window focus behaviour,window screen behaviour +X-KDE-Keywords[es]=foco,auto levantar,levantar,clic para levantar,teclado,CDE,alt-tab,todo el escritorio,el foco sigue al ratón,prevención de foco,robo del foco,política del foco,comportamiento del foco de las ventanas,comportamiento de la pantalla de ventanas +X-KDE-Keywords[et]=fookus,asetus,automaatne esiletoomine,klõpsuga esiletoomine,klaviatuur,CDE,alt-tab,kõik töölauad,fookus järgib hiirt,fookuse vältimine,fookuse röövimine,fookuse reegel,akna fookuse käitumine +X-KDE-Keywords[eu]=fokua,automatikoki igo,igo,egin klik igotzeko,teklatu,CDE,alt-tab,mahaigain guztiak,fokuak saguari jarraitzen dio,foku-prebentzioa,foku-lapurreta,fokuaren gidalerro,leihoen fokuaren portaera,leihoen pantailen portaera +X-KDE-Keywords[fi]=kohdistus,sijoitus,automaattinen nosto,automaattinen nostaminen,nosta,nosta napsauttamalla,näppäimistö,alt-sarkain,kaikki työpöydät,kohdistus seuraa hiirtä,kohdistuksen esto,kohdistuksen varastaminen,kohdistustapa,ikkunoiden kohdistuksen toiminta,ikkunoiden näyttötoiminta +X-KDE-Keywords[fr]=focus, agrandissement automatique, agrandissement, clic d'agrandissement, clavier, CDE, alt-tab, tous les bureaux, focus suivi par la souris, prise de focus, politique de focus, comportement du focus des fenêtres, comportement des fenêtres +X-KDE-Keywords[gl]=foco,erguer automaticamente,erguer,erguer ao premer,teclado,CDE,alt-tab,todo o escritorio,foco que segue o rato,prevención do foco,roubar o foco,política de foco,comportamento de foco de xanela,comportamento de pantalla de xanela +X-KDE-Keywords[ia]=focus,auto raise,raise,click raise,keyboard,CDE,alt-tab,all desktop,focus follows mouse,focus prevention,focus stealing,focus policy,window focus behavior,window screen behavior +X-KDE-Keywords[id]=fokus,auto naik,naikkan,klik naikkan,keyboard,papan ketik,CDE,alt-tab,semua desktop,fokus mengikuti mouse,pencegahan fokus,pencurian fokus,kebijakan fokus,perilaku fokus window,perilaku layar window +X-KDE-Keywords[it]=fuoco,avanzamento automatico,avanzamento,avanzamento con clic,tastiera,CDE,alt-tab,tutti i desktop,il fuoco segue il mouse,impedisci il fuoco,mantieni il fuoco,regole fuoco,comportamento fuoco finestra, comportamento schermo finestra +X-KDE-Keywords[ko]=focus,auto raise,raise,click raise,keyboard,CDE,alt-tab,all desktop,focus follows mouse,focus prevention,focus stealing,focus policy,window focus behavior,window screen behavior,초점,키보드,모든 데스크톱,초점,초점 훔치기,초점 훔치기 방지,초점 정책,창 초점 행동,창 화면 행동 +X-KDE-Keywords[lt]=fokusavimas,fokusas,automatinis iškėlimas,automatinis iskelimas,automatiškai iškelti į pirmą planą,automatiskai iskelti i pirma plana,iškelti į pirmą planą,iskelti i pirma plana,pakelti,pakėlimas,iškėlimas spustelėjus,iskelimas spustelejus,iškėlimas spustelėjant,iskelimas spustelejant,klaviatūra,klaviatura,CDE,alt-tab,visi darbalaukiai,visi darbastaliai,fokusas seka pelę,fokusas seka pele,fokusas seka paskui pelę,fokusas seka paskui pele,fokusavimas seka pelę,fokusavimas seka pele,fokusavimas seka paskui pelę,fokusavimas seka paskui pele,fokuso prevencija,fokusavimo prevencija,fokuso vogimas,fokusavimo vogimas,fokuso politika,fokusavimo politika,langų fokusavimo elgsena,langų fokuso elgsena,langų fokuso elgesys,lango fokuso elgsena,lango fokuso elgesys,langų fokusavimo elgesys,langų ekrano elgsena,langu ekrano elgsena,langų ekrano elgesys,langu ekrano elgesys,lango ekrano elgsena,lango ekrano elgesys +X-KDE-Keywords[nl]=focus,automatisch omhoog komen,omhoog komen,omhoog komen bij klikken,toetsenbord,CDE,alt-tab,alle bureaubladen,focus volgt muis,voorkomen van focus,focus stelen,focusbeleid,focusgedrag in venster,gedrag van vensterscherm +X-KDE-Keywords[nn]=fokus,autohev,hev,klikk-og-hev,tastatur,CDE,alt-tab,alle skrivebord,fokus følgjer mus,fokushindring,fokussteling,fokuspraksis,fokusåtferd for vindauge,vindaugsåtferd på skjerm +X-KDE-Keywords[pl]=uaktywnienie,auto wznoszenie,wznoszenie,wznoszenie na kliknięcie,klawiatura,CDE,alt-tab,wszystkie pulpity +X-KDE-Keywords[pt]=foco,colocação,elevação automática,elevar,elevar ao carregar,teclado,CDE,alt-tab,todos os ecrãs,foco segue o rato,prevenção do foco,roubo do foco,política de foco,comportamento do foco da janela,comportamento da janela no ecrã +X-KDE-Keywords[pt_BR]=foco,elevação automática,elevar,elevar ao clicar,teclado,CDE,alt-tab,todas as áreas de trabalho,foco segue o mouse,prevenção do foco,captura do foco,política de foco,comportamento do foco da janela,comportamento da janela na tela +X-KDE-Keywords[ru]=focus,auto raise,raise,click raise,keyboard,CDE,alt-tab,all desktop,focus follows mouse,focus prevention,focus stealing,focus policy,window focus behavior,window screen behavior,фокус,активация окон,автоматически,поднятие,поднимать,клавиатура,весь рабочий стол,фокус следует за мышью,фокус под мышью,похищение фокуса,предотвращение перехвата фокуса,поведение фокуса,окон,поведение экрана +X-KDE-Keywords[sk]=zameranie,umiestnenie,automatické zdvihnutie,zdvihnutie,klik na zdvihnutie,klávesnica,CDE,alt-tab, všetky plochy,zameranie nasleduje mys,predchádzanie zameraniu,kradnutie zamerania,politika zamerania, správania zamerania okien,správania okien obrazovky +X-KDE-Keywords[sl]=fokus,žarišče,postavitev,postavljanje,samodejni dvig,samodejno dvigovanje,dvig,dvigovanje,dvig na klik,tipkovnica,cde,celotno namizje,fokus sledi miški,žarišče sledi miški,preprečevanje fokusa,preprečevanje žarišča,kraja fokusa,kraja žarišča,pravila osredotočanja,pravila za osredotočanje,obnašanje pri osredotočanju oken,obnašanje pri postavljanju oken v žarišče,obnašanje oken +X-KDE-Keywords[sv]=fokus,fönsterbeteende,animering,höj,höj automatiskt,höj med klick,CDE,alt-tab,alla skrivbord,fokus följer musen,förhindra fokus,stjäla fokus,fokusprincip,fönsterfokusbeteende,fönsterskärmbeteende +X-KDE-Keywords[uk]=focus,auto raise,raise,click raise,keyboard,CDE,alt-tab,all desktop,focus follows mouse,focus prevention,focus stealing,focus policy,window focus behavior,window screen behavior,фокус,фокусування,автопідняття,підняття,клацання,клавіатура,альт-таб,всі стільниці,фокус за мишею,запобігання,перехід фокуса,правила фокусування,поведінка вікон +X-KDE-Keywords[vi]=focus,auto raise,raise,click raise,keyboard,CDE,alt-tab,all desktop,focus follows mouse,focus prevention,focus stealing,focus policy,window focus behavior,window screen behavior,nhắm,nâng tự động,nâng,bấm nâng,bàn phím,tất cả bàn làm việc,nhắm đi theo chuột,ngăn chặn nhắm,lấy nhắm,chính sách nhắm,ứng xử nhắm của cửa sổ,ứng xử màn hình cửa sổ +X-KDE-Keywords[x-test]=xxfocusxx,xxauto raisexx,xxraisexx,xxclick raisexx,xxkeyboardxx,xxCDExx,xxalt-tabxx,xxall desktopxx,xxfocus follows mousexx,xxfocus preventionxx,xxfocus stealingxx,xxfocus policyxx,xxwindow focus behaviorxx,xxwindow screen behaviorxx +X-KDE-Keywords[zh_CN]=focus,auto raise,raise,click raise,keyboard,CDE,alt-tab,all desktop,focus follows mouse,focus prevention,focus stealing,focus policy,window focus behavior,window screen behavior,焦点,聚焦,自动升起,升起,点击升起,键盘,全部桌面,焦点跟随鼠标,偷取焦点,焦点策略,窗口焦点行为,窗口屏幕行为 +X-KDE-Keywords[zh_TW]=focus,auto raise,raise,click raise,keyboard,CDE,alt-tab,all desktop,focus follows mouse,focus prevention,focus stealing,focus policy,window focus behavior,window screen behavior diff --git a/kcmkwin/kwinoptions/kwinmoving.desktop b/kcmkwin/kwinoptions/kwinmoving.desktop new file mode 100644 index 0000000..6f6fa1f --- /dev/null +++ b/kcmkwin/kwinoptions/kwinmoving.desktop @@ -0,0 +1,119 @@ +[Desktop Entry] +Icon=preferences-system-windows-movement +Type=Service +X-KDE-ServiceTypes=KCModule +Exec=kcmshell5 kwinmoving +X-DocPath=kcontrol/windowbehaviour/index.html#action-moving +Icon=preferences-system-windows-move + +X-KDE-Library=kcm_kwinoptions +X-KDE-PluginKeyword=kwinmoving + +Name=Window Movement +Name[az]=Pəncərə yerdəyişdirilməsi +Name[ca]=Moviment de les finestres +Name[cs]=Pohyb oken +Name[da]=Flytning af vinduer +Name[en_GB]=Window Movement +Name[es]=Movimiento de las ventanas +Name[et]=Akna liigutamine +Name[eu]=Leihoaren mugimendua +Name[fi]=Ikkunoiden siirtäminen +Name[fr]=Déplacement des fenêtres +Name[gl]=Movemento das xanelas +Name[ia]=Movimento de fenestra +Name[id]=Pemindahan Window +Name[it]=Spostamento delle finestre +Name[ko]=창 이동 +Name[lt]=Langų perkėlimas +Name[nl]=Verplaatsen van vensters +Name[nn]=Vidaugs­flytting +Name[pl]=Przesuwanie okien +Name[pt]=Movimentação das Janelas +Name[pt_BR]=Movimento da janela +Name[ro]=Mutare ferestre +Name[ru]=Перемещение окон +Name[sk]=Presuny okien +Name[sl]=Premikanje oken +Name[sv]=Fönsterförflyttning +Name[uk]=Пересування вікон +Name[x-test]=xxWindow Movementxx +Name[zh_CN]=窗口移动 +Name[zh_TW]=視窗移動 + +Comment=Configure window movement options +Comment[az]=Pəncərə yerdəyişdirilməsinin tənzimlənməsi +Comment[ca]=Configura les opcions de moviment de les finestres +Comment[cs]=Nastavit volby pohybu oken +Comment[da]=Indstil flytning af vinduer +Comment[en_GB]=Configure window movement options +Comment[es]=Configurar las opciones del movimiento de las ventanas +Comment[et]=Akende liigutamise seadistamine +Comment[eu]=Konfiguratu leiho mugimenduaren aukerak +Comment[fi]=Ikkunoiden siirtämisen asetukset +Comment[fr]=Configurer les options de déplacement des fenêtres +Comment[gl]=Configurar o movemento das xanelas +Comment[ia]=Configura optiones de movimento de fenestra +Comment[id]=Konfigurasikan opsi pemindahan window +Comment[it]=Configura opzioni di spostamento delle finestre +Comment[ko]=창 이동 옵션 설정 +Comment[lt]=Konfigūruoti langų perkėlimo parinktis +Comment[nl]=Opties voor vensterverplaatsing configureren +Comment[nn]=Set opp vindaugsflytting +Comment[pl]=Ustawienia opcji przesuwania okien +Comment[pt]=Configurar as opções de movimentação das janelas +Comment[pt_BR]=Configure as opções de movimento da janela +Comment[ro]=Configurează opțiuni de mutare a ferestrelor +Comment[ru]=Настройка поведения при перемещении окон +Comment[sk]=Nastavenie spôsobu presunu okien +Comment[sl]=Nastavi možnosti premikanja okna +Comment[sv]=Anpassa alternativ för fönsterförflyttning +Comment[uk]=Налаштовування параметрів пересування вікон +Comment[x-test]=xxConfigure window movement optionsxx +Comment[zh_CN]=配置窗口移动选项 +Comment[zh_TW]=設定視窗移動選項 + +X-KDE-Keywords=moving,smart,cascade,maximize,maximise,snap zone,snap,border +X-KDE-Keywords[az]=köçürmə,yerdəyişmə,ağıllı,kaskad,genişləndirmə,qopma zonası,çərçivə +X-KDE-Keywords[bs]=pomjeranje,pametno,kaskada,povećali,povećalo,namjestiti zonu,namjestiti,granica +X-KDE-Keywords[ca]=moviment,intel·ligent,cascada,maximització,zona d'ajust,ajust,vora +X-KDE-Keywords[ca@valencia]=moviment,intel·ligent,cascada,maximització,zona d'ajust,ajust,vora +X-KDE-Keywords[da]=flytning,smart,kaskade,maksimer,hægtzone,hægt,kant +X-KDE-Keywords[de]=Verschieben,Gestaffelt,Maximieren,Minimieren,Einrastzone,Ränder +X-KDE-Keywords[el]=κίνηση,έξυπνη,διαδοχική,μεγιστοποίηση,ελαχιστοποίηση,snap zone,snap,περίγραμμα +X-KDE-Keywords[en_GB]=moving,smart,cascade,maximize,maximise,snap zone,snap,border +X-KDE-Keywords[es]=movimiento,inteligente,cascada,maximizar,maximizar,zona de instantánea,instantánea,borde +X-KDE-Keywords[et]=liigutamine,nutikas,kaskaad,maksimeerimine,haardetsoon,haaramine,piire +X-KDE-Keywords[eu]=lekuz aldatzea,adimendun,kaskada,maximizatu,atxikitze-eremu,atxikitu,ertz +X-KDE-Keywords[fi]=siirtäminen,älykäs,porrastus,suurentaminen,tarttuminen,kiinnitysalue,tartunta,kiinnitys,kiinnittyminen,reuna +X-KDE-Keywords[fr]=déplacement, intelligent, cascade, maximiser, maximise, zone de rupture, rupture, bordure +X-KDE-Keywords[gl]=mover,intelixente,solapar,fervenza,maximizar,minimizar,zona de adherencia, adherencia,bordo,beira,bordo +X-KDE-Keywords[hu]=mozgatás,intelligens,lépcsőzetes,maximalizálás,maximalizálás,vonzási távolság,szegély +X-KDE-Keywords[ia]=movente,intelligente,cascada,maximisa,maximisa,zona de ruptura,ruptura,margine +X-KDE-Keywords[id]=pemindahan,cerdas,kaskade,maksimalkan,maksimalkan,zona jepret,jepret,bingkai +X-KDE-Keywords[it]=spostamento,intelligente,cascata,massimizza,zona di aggancio,agganciamento,bordo +X-KDE-Keywords[kk]=moving,smart,cascade,maximize,maximise,snap zone,snap,border +X-KDE-Keywords[km]=moving,smart,cascade,maximize,maximise,snap zone,snap,border +X-KDE-Keywords[ko]=moving,smart,cascade,maximize,maximise,snap zone,snap,border,이동,스마트,계단식,최대화,경계선 +X-KDE-Keywords[lt]=perkėlimas,perkelti,perkelimas,išmanus,ismanus,mažiausias persidengimas,maziausias persidengimas,mažiausio persidengimo,maziausio persidengimo,pakopomis,kaskada,išskleisti,isskleisti,isdidinti,išdidinti,pritraukimo zona,traukimo zona,pritraukimas,traukimas,rėmelis,remelis,rėmas,remas +X-KDE-Keywords[nb]=flytting,smart,kaskade,maksimer,gripesone,gripe,kant +X-KDE-Keywords[nds]=Bewegen,klook,överenanner,maximeren,Andockrebeet,andocken,Rahmen,Kant +X-KDE-Keywords[nl]=verplaatsen,smart,cascade,maximaliseren,zone vastzetten,vastzetten,grens +X-KDE-Keywords[nn]=flytting,smart,kaskade,maksimering,gripesone,gripa,kant +X-KDE-Keywords[pl]=przesuwanie,elegancki,kaskada,maksymalizuj,obszar przyciągania,przyciągaj,obramowanie +X-KDE-Keywords[pt]=movimento,inteligente,cascata,maximizar,ajuste à zona,ajuste,contorno +X-KDE-Keywords[pt_BR]=movimento,movimentação,inteligente,cascata,maximizar,ajuste à área,ajuste,borda +X-KDE-Keywords[ro]=mutare,inteligent,cascadă,maximizează,minimizează,zonă magnetică,magnet,contur +X-KDE-Keywords[ru]=moving,smart,cascade,maximize,maximise,snap zone,snap,border,перемещение,каскад,распахнуть,свернуть,захват,привязка,граница +X-KDE-Keywords[sk]=presun,smart,kaskáda,maximalizácia,miinmalizácia,oblasť prichytenia,prichytenie,rám +X-KDE-Keywords[sl]=premikanje,pametno premikanje,kaskada,povečaj,razpni,območje pripenjanja,pripenjanje,rob,robovi,obroba,obrobe +X-KDE-Keywords[sr]=moving,smart,cascade,maximize,maximise,snap zone,snap,border,померање,паметно,наслагано,максимизуј,зона лепљења,лепљење,ивица +X-KDE-Keywords[sr@ijekavian]=moving,smart,cascade,maximize,maximise,snap zone,snap,border,померање,паметно,наслагано,максимизуј,зона лепљења,лепљење,ивица +X-KDE-Keywords[sr@ijekavianlatin]=moving,smart,cascade,maximize,maximise,snap zone,snap,border,pomeranje,pametno,naslagano,maksimizuj,zona lepljenja,lepljenje,ivica +X-KDE-Keywords[sr@latin]=moving,smart,cascade,maximize,maximise,snap zone,snap,border,pomeranje,pametno,naslagano,maksimizuj,zona lepljenja,lepljenje,ivica +X-KDE-Keywords[sv]=flytta,smart,kaskad,maximera,låszon,lås,kanter +X-KDE-Keywords[tr]=taşıma,akıllı,döşeme,büyütme,en büyük,kopma alanı,kopma,kenarlık +X-KDE-Keywords[uk]=moving,smart,cascade,maximize,maximise,snap zone,snap,border,пересування,кмітливе,каскадом,максимізувати,розгорнути,прилипання,зона прилипання,межа +X-KDE-Keywords[x-test]=xxmovingxx,xxsmartxx,xxcascadexx,xxmaximizexx,xxmaximisexx,xxsnap zonexx,xxsnapxx,xxborderxx +X-KDE-Keywords[zh_CN]=moving,smart,cascade,maximize,maximise,snap zone,snap,border,移动,智能,最大化,级联,吸附区,吸附,边框 +X-KDE-Keywords[zh_TW]=moving,smart,cascade,maximize,maximise,snap zone,snap,border diff --git a/kcmkwin/kwinoptions/kwinoptions.desktop b/kcmkwin/kwinoptions/kwinoptions.desktop new file mode 100644 index 0000000..37fe665 --- /dev/null +++ b/kcmkwin/kwinoptions/kwinoptions.desktop @@ -0,0 +1,181 @@ +[Desktop Entry] +Exec=kcmshell5 kwinoptions +Icon=preferences-system-windows-actions +Type=Service +X-KDE-ServiceTypes=KCModule +X-DocPath=kcontrol/windowbehaviour/index.html + +X-KDE-Library=kcm_kwinoptions +X-KDE-PluginKeyword=kwinoptions +X-KDE-ParentApp=kcontrol + +X-KDE-System-Settings-Parent-Category=windowmanagement +X-KDE-Weight=40 + +Name=Window Behavior +Name[af]=Venstergedrag +Name[ar]=سلوك النوافذ +Name[az]=Pəncərə Davranışı +Name[be]=Паводзіны вокнаў +Name[be@latin]=Pavodziny akna +Name[bg]=Поведение на прозорците +Name[bn]=উইণ্ডো আচরণ +Name[bn_IN]=উইন্ডোর আচরণ +Name[br]=Emzalc'h ar prenester +Name[bs]=Ponašanje prozora +Name[ca]=Comportament de les finestres +Name[ca@valencia]=Comportament de les finestres +Name[cs]=Chování oken +Name[csb]=Ùchòwanié òkna +Name[cy]=Ymddygiad Ffenestri +Name[da]=Vinduesopførsel +Name[de]=Fensterverhalten +Name[el]=Συμπεριφορά παραθύρων +Name[en_GB]=Window Behaviour +Name[eo]=Fenestrokonduto +Name[es]=Comportamiento de las ventanas +Name[et]=Akende käitumine +Name[eu]=Leihoaren portaera +Name[fa]=رفتار پنجره +Name[fi]=Ikkunoiden toiminta +Name[fr]=Comportement des fenêtres +Name[fy]=Finstergedrach +Name[ga]=Oibriú na bhFuinneog +Name[gl]=Comportamento das xanelas +Name[gu]=વિન્ડો વર્તણૂક +Name[he]=התנהגות חלונות +Name[hi]=विंडो व्यवहार +Name[hne]=विंडो व्यवहार +Name[hr]=Ponašanje prozora +Name[hu]=Ablakműveletek +Name[ia]=Comportamento de fenestra +Name[id]=Perilaku Window +Name[is]=Hegðun glugga +Name[it]=Comportamento delle finestre +Name[ja]=ウィンドウの挙動 +Name[ka]=ფანჯრის ქცევა +Name[kk]=Терезе қасиеттері +Name[km]=ឥរិយាបថ​បង្អួច +Name[kn]=ಕಿಟಕಿ ವರ್ತನೆ +Name[ko]=창 동작 +Name[ku]=Helwesta Paceyan +Name[lt]=Langų elgsena +Name[lv]=Logu izturēšanās +Name[mai]=विंडो व्यवहार +Name[mk]=Однесување на прозорци +Name[ml]=ജാലകത്തിന്റെ വിശേഷത +Name[mr]=चौकट वर्तन +Name[nb]=Vindusoppførsel +Name[nds]=Finsterbedregen +Name[ne]=सञ्झ्याल व्यवहार +Name[nl]=Venstergedrag +Name[nn]=Vindaugs­åtferd +Name[pa]=ਵਿੰਡੋ ਰਵੱਈਆ +Name[pl]=Zachowania okien +Name[pt]=Comportamento das Janelas +Name[pt_BR]=Comportamento das janelas +Name[ro]=Comportament ferestre +Name[ru]=Поведение окон +Name[se]=Láseláhtten +Name[si]=කවුළු හැසිරීම +Name[sk]=Správanie okien +Name[sl]=Obnašanje oken +Name[sr]=Понашање прозора +Name[sr@ijekavian]=Понашање прозора +Name[sr@ijekavianlatin]=Ponašanje prozora +Name[sr@latin]=Ponašanje prozora +Name[sv]=Fönsterbeteende +Name[ta]=சாளர நடத்தை +Name[te]=విండో ప్రవర్తన +Name[th]=พฤติกรรมของหน้าต่าง +Name[tr]=Pencere Davranışı +Name[ug]=كۆزنەكنىڭ ئىش-ھەرىكەتلىرى +Name[uk]=Поведінка вікон +Name[uz]=Oynaning xususiyatlari +Name[uz@cyrillic]=Ойнанинг хусусиятлари +Name[vi]=Ứng xử của Cửa sổ +Name[wa]=Dujhance des fniesses +Name[xh]=Ukuziphatha kwe Window +Name[x-test]=xxWindow Behaviorxx +Name[zh_CN]=窗口行为 +Name[zh_TW]=視窗行為 + +Comment=Configure window actions and behavior +Comment[az]=Pəncərə fəaliyyətlərini və davranışını tənzimləmək +Comment[ca]=Configura les accions i comportament de les finestres +Comment[cs]=Nastavte činností a chování oken +Comment[da]=Indstil vindueshandlinger og -opførsel +Comment[de]=Fenster-Aktionen und -verhalten einrichten +Comment[en_GB]=Configure window actions and behaviour +Comment[es]=Configurar las acciones y el comportamiento de las ventanas +Comment[et]=Akende toimingute ja käitumise seadistamine +Comment[eu]=Konfiguratu leihoaren ekintzak eta jokabidea +Comment[fi]=Ikkunatoimintojen asetukset +Comment[fr]=Configurer les actions et le comportement des fenêtres +Comment[gl]=Configurar o comportamento e as accións das xanelas +Comment[ia]=Cnfigura comportamento e actiones de fenestra +Comment[id]=Konfigurasikan perilaku dan aksi window +Comment[it]=Configura azioni e comportamento delle finestre +Comment[ko]=창 동작과 행동 설정 +Comment[lt]=Konfigūruoti langų veiksmus ir elgseną +Comment[nl]=Vensteracties en gedrag configureren +Comment[nn]=Set opp utsjånad og åtferd for vindauge +Comment[pl]=Ustawienia działań i zachowań okien +Comment[pt]=Configurar as acções e comportamento das janelas +Comment[pt_BR]=Configure as ações e comportamento das janelas +Comment[ro]=Configurează acțiunile și comportamentul ferestrelor +Comment[ru]=Настройка поведения окон +Comment[sk]=Nastaviť akcie a správanie okien +Comment[sl]=Nastavi dejanja in obnašanje oken +Comment[sv]=Anpassa fönsteråtgärder och beteende +Comment[uk]=Налаштовування реакції та поведінки вікон +Comment[x-test]=xxConfigure window actions and behaviorxx +Comment[zh_CN]=配置窗口操作和行为 +Comment[zh_TW]=設定視窗動作及行為 + +X-KDE-Keywords=focus,placement,window behavior,window actions,animation,raise,auto raise,windows,frame,titlebar,doubleclick +X-KDE-Keywords[az]=fokus,yerləşmə,pəncərə davranışı,pəncərə fəaliyyəti,qaldırmaq,avtomatik qaldırma,pəncərə,çərçivə,başlıq,ikili klik +X-KDE-Keywords[bs]=focus,placement,window behavior,window actions,animation,raise,auto raise,windows,frame,titlebar,doubleclick,fokus,pozicioniranje,ponašanje prozora,akcije prozora,animacija,podizanje,okvir,naslovna traka +X-KDE-Keywords[ca]=focus,emplaçament,comportament de la finestra,accions de la finestra,animació,elevació,elevació automàtica,finestres,marc,barra de títol,doble clic +X-KDE-Keywords[ca@valencia]=focus,emplaçament,comportament de la finestra,accions de la finestra,animació,elevació,elevació automàtica,finestres,marc,barra de títol,clic doble +X-KDE-Keywords[da]=fokus,placering,vinduesopførsel,vindueshandlinger,animation,hæv,autohæv,vinduesramme,titelbjælke,dobbeltklik +X-KDE-Keywords[de]=Aktivierung,Platzierung,Fensterverhalten,Fensteraktionen,Animation,Nach vorn/hinten, Fenster,Rahmen,Umrandung,Titelleiste,Doppelklick +X-KDE-Keywords[el]=εστίαση,τοποθέτηση,συμπεριφορά παραθύρου,κίνηση εικόνας,αύξηση,αυτόματη αύξηση,παράθυρα,πλαίσιο,γραμμή τίτλου,διπλό κλικ +X-KDE-Keywords[en_GB]=focus,placement,window behaviour,window actions,animation,raise,auto raise,windows,frame,titlebar,doubleclick +X-KDE-Keywords[es]=foco,posicionamiento,comportamiento de las ventanas,acciones de las ventanas,animación,elevación,autoelevación,ventanas,marco,barra de título,doble clic +X-KDE-Keywords[et]=fookus,asetus,paigutus,akende käitumine,aknatoimingud,animeerimine,animatsioon,esiletoomine,automaatne esiletoomine,aknad,raam,tiitliriba,topeltklõps +X-KDE-Keywords[eu]=foku,kokaleku,leihoen portaera,leiho-ekintzak,animazio,igo,automatikoki igo,leihoak,marko,titulu-barra,klik bikoitz +X-KDE-Keywords[fi]=kohdistus,sijoittelu,sijoitus,ikkunoiden toiminta,ikkunoiden toiminnot,animaatio,nosta,automaattinen nosto,ikkunat,kehys,otsikkopalkki,kaksoisnapsautus,tuplanapsautus,kaksoisklikkaus,tuplaklikkaus +X-KDE-Keywords[fr]=focus, placement, comportement de la fenêtre, actions sur les fenêtres, animation, agrandissement, agrandissement automatique, fenêtres, cadre, barre de titre, double clic +X-KDE-Keywords[gl]=foco,posicionamento,comportamento das xanelas,accións das xanelas, animación,xanelas,moldura,barra de título,marco +X-KDE-Keywords[hu]=fókusz,elhelyezés,ablakműködés,ablakműveletek,animáció,felemelés,automatikus felemelés,ablakok,keret,címsor,dupla kattintás +X-KDE-Keywords[ia]=focus,placiamento,comportamento de fenestra,actiones de fenestra,animation,altiar,auto altiar,fenestras,quadro,barra de titulo,duple click +X-KDE-Keywords[id]=fokus,penempatan,perilaku window,aksi window,animasi,naikkan,naikkan otomatis,window,bingkai,titlebar,klik ganda +X-KDE-Keywords[it]=fuoco,posizionamento,comportamento della finestra,azioni delle finestre,animazione,sollevamento,sollevamento automatico,finestre,riquadro,barra del titolo,doppio clic +X-KDE-Keywords[ko]=focus,placement,window behavior,animation,raise,auto raise,windows,frame,titlebar,doubleclick,초점,위치,창 행동,애니메이션,올리기,창,프레임,제목 표시줄 +X-KDE-Keywords[lt]=fokusas,fokusavimas,išdėstymas,isdestymas,padėjimas,padejimas,lango elgsena,langų elgsena,langu elgsena,lango elgesys,langų elgsesys,langu elgesys,lango veiksmai,langų veiksmai,langu veiksmai,animacija,animacijos,iškelti į pirmą planą,iskelti i pirma plana,iškėlimas į pirmą planą,iskelimas i pirma plana,automatiškai iškelti į pirmą planą,automatiskai iskelti i pirma plana,automatinis iškėlimas į pirmą planą,automatinis iskelimas i pirma plana,langas,langai,rėmelis,remelis,rėmas,remas,antraštės juosta,antrastes juosta,pavadinimo juosta,dvikartis spustelėjimas,dvikartis spustelejimas,dvigubas spustelėjimas,dvigubas spustelejimas +X-KDE-Keywords[nb]=fokus,plassering,vindusoppførsel,vindushandlinger,animering,hev,autohev,vinduer,ramme,tittellinje,dobbeltklikk +X-KDE-Keywords[nds]=Fokus,Platzeren,Finsterbedregen,Finsterakschonen,Animeren,na vörn,automaatsch,Finstern,Rahmen,Titelbalken,Dubbelklick +X-KDE-Keywords[nl]=focus,plaatsing,venstegedrag,vensteracties,animatie,omhoog,automatisch omhoog,vensters,frame,titelbalk,dubbelklik +X-KDE-Keywords[nn]=fokus,plassering,vindaugsåtferd,vindaugshandlingar,animering,hev,autohev,vindauge,ramme,tittellinje,dobbeltklikk +X-KDE-Keywords[pl]=uaktywnienie,umieszczenie,zachowanie okna,działania okien,animacja,wzniesienie,auto-wzniesienie, okna,ramka,pasek tytułu,podwójne kliknięcie +X-KDE-Keywords[pt]=foco,colocação,comportamento da janela,acções das janelas,animação,elevar,elevar automaticamente,janelas,contorno,barra de título,duplo-click +X-KDE-Keywords[pt_BR]=foco,colocação,comportamento da janela,ações da janela,animação,elevar,elevar automaticamente,janelas,contorno,barra de título,clique duplo +X-KDE-Keywords[ro]=focalizare,amplasare,comportament ferestre,acțiuni ferestre,animație,ridică,ridicare automată,ferestre,cadru,bară de titlu,dublu clic +X-KDE-Keywords[ru]=focus,placement,window behavior,window actions,animation,raise,auto raise,windows,frame,titlebar,doubleclick,фокус,местоположение,поведение окон,анимация,увеличение,автоувеличение,окна,рамка,заголовок,двойной щелчок,действия над окнами +X-KDE-Keywords[sk]=zameranie,umiestnenie,správanie okien,animácia,zdvihnúť,automaticky zdvihnúť,okná,rám,záhlavie,dvojklik +X-KDE-Keywords[sl]=fokus,žarišče,postavitev,postavljanje,obnašanje oken,dejanja oken,animacija,dvig,samodejni dvig,okna,okvir,naslovna vrstica,dvojni klik,dvoklik +X-KDE-Keywords[sr]=focus,placement,window behavior,window actions,animation,raise,auto raise,windows,frame,titlebar,doubleclick,фокус,постављење,понашање прозора,радње над прозорима,анимација,подигни,аутоматско подизање,прозор,оквир,насловна трака,двоклик +X-KDE-Keywords[sr@ijekavian]=focus,placement,window behavior,window actions,animation,raise,auto raise,windows,frame,titlebar,doubleclick,фокус,постављење,понашање прозора,радње над прозорима,анимација,подигни,аутоматско подизање,прозор,оквир,насловна трака,двоклик +X-KDE-Keywords[sr@ijekavianlatin]=focus,placement,window behavior,window actions,animation,raise,auto raise,windows,frame,titlebar,doubleclick,fokus,postavljenje,ponašanje prozora,radnje nad prozorima,animacija,podigni,automatsko podizanje,prozor,okvir,naslovna traka,dvoklik +X-KDE-Keywords[sr@latin]=focus,placement,window behavior,window actions,animation,raise,auto raise,windows,frame,titlebar,doubleclick,fokus,postavljenje,ponašanje prozora,radnje nad prozorima,animacija,podigni,automatsko podizanje,prozor,okvir,naslovna traka,dvoklik +X-KDE-Keywords[sv]=fokus,placering,fönsterbeteende,animering,höj,höj automatiskt,fönster,ram,namnlist,dubbelklick +X-KDE-Keywords[tr]=odak,yerleşim,pencere davranışı,pencere eylemleri,canlandırma,yükselt,otomatik yükselt,pencereler,çerçeve,başlık çubuğu,çift tıklama +X-KDE-Keywords[uk]=focus,placement,window behavior,window actions,animation,raise,auto raise,windows,frame,titlebar,doubleclick,фокус,розташування,місце,вікно,поведінка,поведінка вікон,дії,реакція,дії з вікнами,реакція вікон,анімація,підняти,підняття,автоматична,автоматично,рамка,заголовок,смужка заголовка,клацання,подвійне +X-KDE-Keywords[vi]=focus,placement,window behavior,window actions,animation,raise,auto raise,windows,frame,titlebar,doubleclick,nhắm,xếp chỗ,ứng xử của cửa sổ,hành động của cửa sổ,hiệu ứng động,nâng,nâng tự động,cửa sổ,khung,thanh tiêu đề,bấm đúp +X-KDE-Keywords[x-test]=xxfocusxx,xxplacementxx,xxwindow behaviorxx,xxwindow actionsxx,xxanimationxx,xxraisexx,xxauto raisexx,xxwindowsxx,xxframexx,xxtitlebarxx,xxdoubleclickxx +X-KDE-Keywords[zh_CN]=focus,placement,window behavior,window actions,animation,raise,auto raise,windows,frame,titlebar,doubleclick,焦点,位置,窗口行为,窗口动作,动画,升起,自动升起,窗口,边框,标题栏,双击 +X-KDE-Keywords[zh_TW]=focus,placement,window behavior,window actions,animation,raise,auto raise,windows,frame,titlebar,doubleclick + +Categories=Qt;KDE;X-KDE-settings-looknfeel; + diff --git a/kcmkwin/kwinoptions/kwinoptions_kdeglobals_settings.kcfg b/kcmkwin/kwinoptions/kwinoptions_kdeglobals_settings.kcfg new file mode 100644 index 0000000..2a1c486 --- /dev/null +++ b/kcmkwin/kwinoptions/kwinoptions_kdeglobals_settings.kcfg @@ -0,0 +1,15 @@ + + + + + + + + true + + + + diff --git a/kcmkwin/kwinoptions/kwinoptions_kdeglobals_settings.kcfgc b/kcmkwin/kwinoptions/kwinoptions_kdeglobals_settings.kcfgc new file mode 100644 index 0000000..b2e1ccb --- /dev/null +++ b/kcmkwin/kwinoptions/kwinoptions_kdeglobals_settings.kcfgc @@ -0,0 +1,6 @@ +File=kwinoptions_kdeglobals_settings.kcfg +ClassName=KWinOptionsKDEGlobalsSettings +IncludeFiles=options.h +Mutators=true +DefaultValueGetters=true +ParentInConstructor=true diff --git a/kcmkwin/kwinoptions/kwinoptions_settings.kcfg b/kcmkwin/kwinoptions/kwinoptions_settings.kcfg new file mode 100644 index 0000000..b5402a5 --- /dev/null +++ b/kcmkwin/kwinoptions/kwinoptions_settings.kcfg @@ -0,0 +1,368 @@ + + + + + + + false + + + + 10 + 0 + 100 + + + + 10 + 0 + 100 + + + + 0 + 0 + 100 + + + + false + + + + false + + + + 250 + 0 + + + + + + + + + + + + + 0 + + + + true + + + + Maximize + + + + + + + + + + + + + + + Maximize + + + + + + + + + MaximizeVerticalOnly + + + + + + + + + MaximizeHorizontalOnly + + + + + + + + + ClickToFocus + + + + + + + + + + false + + + + 750 + 0 + + + + 300 + 0 + + + + false + + + + true + + + + false + + + + focusPolicy() != KWin::Options::ClickToFocus + + + + 1 + 0 + 4 + + + + + + + + Raise + + + + + + + + + + + + + + Nothing + + + + + + + + + + + + + + OperationsMenu + + + + + + + + + + + + + + Nothing + + + + + + + + + + + + + ActivateAndRaise + + + + + + + + + + + + + + + + + Nothing + + + + + + + + + + + + + + + + + OperationsMenu + + + + + + + + + + + + + + + + + ActivateRaisePassClick + + + + + + + + + + ActivatePassClick + + + + + + + + + + ActivatePassClick + + + + + + + + + + Scroll + + + + + + + + + Meta + + + + + + + + Move + + + + + + + + + + + + + + + + ToggleRaiseAndLower + + + + + + + + + + + + + + + + Resize + + + + + + + + + + + + + + + + Nothing + + + + + + + + + + + + + diff --git a/kcmkwin/kwinoptions/kwinoptions_settings.kcfgc b/kcmkwin/kwinoptions/kwinoptions_settings.kcfgc new file mode 100644 index 0000000..dc559ec --- /dev/null +++ b/kcmkwin/kwinoptions/kwinoptions_settings.kcfgc @@ -0,0 +1,6 @@ +File=kwinoptions_settings.kcfg +ClassName=KWinOptionsSettings +IncludeFiles=options.h +Mutators=true +DefaultValueGetters=true +ParentInConstructor=true diff --git a/kcmkwin/kwinoptions/main.cpp b/kcmkwin/kwinoptions/main.cpp new file mode 100644 index 0000000..633d560 --- /dev/null +++ b/kcmkwin/kwinoptions/main.cpp @@ -0,0 +1,235 @@ +/* + SPDX-FileCopyrightText: 2001 Waldo Bastian + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "main.h" + +#include +//Added by qt3to4: +#include + +#include + +#include +#include +#include +#include +#include + +#include "mouse.h" +#include "windows.h" +#include "kwinoptions_settings.h" +#include "kwinoptions_kdeglobals_settings.h" + +K_PLUGIN_FACTORY_DECLARATION(KWinOptionsFactory) + +class KFocusConfigStandalone : public KFocusConfig +{ + Q_OBJECT +public: + KFocusConfigStandalone(QWidget* parent, const QVariantList &) + : KFocusConfig(true, nullptr, parent) + { + initialize(new KWinOptionsSettings(this)); + } +}; + +class KMovingConfigStandalone : public KMovingConfig +{ + Q_OBJECT +public: + KMovingConfigStandalone(QWidget* parent, const QVariantList &) + : KMovingConfig(true, nullptr, parent) + { + initialize(new KWinOptionsSettings(this)); + } +}; + +class KAdvancedConfigStandalone : public KAdvancedConfig +{ + Q_OBJECT +public: + KAdvancedConfigStandalone(QWidget* parent, const QVariantList &) + : KAdvancedConfig(true, nullptr, nullptr, parent) + { + initialize(new KWinOptionsSettings(this), new KWinOptionsKDEGlobalsSettings(this)); + } +}; + +KWinOptions::KWinOptions(QWidget *parent, const QVariantList &) + : KCModule(parent) +{ + mSettings = new KWinOptionsSettings(this); + + QVBoxLayout *layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + tab = new QTabWidget(this); + layout->addWidget(tab); + + mFocus = new KFocusConfig(false, mSettings, this); + mFocus->setObjectName(QLatin1String("KWin Focus Config")); + tab->addTab(mFocus, i18n("&Focus")); + connect(mFocus, qOverload(&KCModule::changed), this, qOverload(&KCModule::changed)); + connect(mFocus, qOverload(&KCModule::defaulted), this, qOverload(&KCModule::defaulted)); + connect(this, &KCModule::defaultsIndicatorsVisibleChanged, mFocus, &KCModule::setDefaultsIndicatorsVisible); + + // Need to relay unmanagedWidgetDefaultState and unmanagedWidgetChangeState to wrapping KCModule + connect(mFocus, &KFocusConfig::unmanagedWidgetDefaulted, this, &KWinOptions::unmanagedWidgetDefaultState); + connect(mFocus, &KFocusConfig::unmanagedWidgetStateChanged, this, &KWinOptions::unmanagedWidgetChangeState); + + mTitleBarActions = new KTitleBarActionsConfig(false, mSettings, this); + mTitleBarActions->setObjectName(QLatin1String("KWin TitleBar Actions")); + tab->addTab(mTitleBarActions, i18n("Titlebar A&ctions")); + connect(mTitleBarActions, qOverload(&KCModule::changed), this, qOverload(&KCModule::changed)); + connect(mTitleBarActions, qOverload(&KCModule::defaulted), this, qOverload(&KCModule::defaulted)); + connect(this, &KCModule::defaultsIndicatorsVisibleChanged, mTitleBarActions, &KCModule::setDefaultsIndicatorsVisible); + + mWindowActions = new KWindowActionsConfig(false, mSettings, this); + mWindowActions->setObjectName(QLatin1String("KWin Window Actions")); + tab->addTab(mWindowActions, i18n("W&indow Actions")); + connect(mWindowActions, qOverload(&KCModule::changed), this, qOverload(&KCModule::changed)); + connect(mWindowActions, qOverload(&KCModule::defaulted), this, qOverload(&KCModule::defaulted)); + connect(this, &KCModule::defaultsIndicatorsVisibleChanged, mWindowActions, &KCModule::setDefaultsIndicatorsVisible); + + mMoving = new KMovingConfig(false, mSettings, this); + mMoving->setObjectName(QLatin1String("KWin Moving")); + tab->addTab(mMoving, i18n("Mo&vement")); + connect(mMoving, qOverload(&KCModule::changed), this, qOverload(&KCModule::changed)); + connect(mMoving, qOverload(&KCModule::defaulted), this, qOverload(&KCModule::defaulted)); + connect(this, &KCModule::defaultsIndicatorsVisibleChanged, mMoving, &KCModule::setDefaultsIndicatorsVisible); + + mAdvanced = new KAdvancedConfig(false, mSettings, new KWinOptionsKDEGlobalsSettings(this), this); + mAdvanced->setObjectName(QLatin1String("KWin Advanced")); + tab->addTab(mAdvanced, i18n("Adva&nced")); + connect(mAdvanced, qOverload(&KCModule::changed), this, qOverload(&KCModule::changed)); + connect(mAdvanced, qOverload(&KCModule::defaulted), this, qOverload(&KCModule::defaulted)); + connect(this, &KCModule::defaultsIndicatorsVisibleChanged, mAdvanced, &KCModule::setDefaultsIndicatorsVisible); + KAboutData *about = + new KAboutData(QStringLiteral("kcmkwinoptions"), i18n("Window Behavior Configuration Module"), + QString(), QString(), KAboutLicense::GPL, + i18n("(c) 1997 - 2002 KWin and KControl Authors")); + + about->addAuthor(i18n("Matthias Ettrich"), QString(), "ettrich@kde.org"); + about->addAuthor(i18n("Waldo Bastian"), QString(), "bastian@kde.org"); + about->addAuthor(i18n("Cristian Tibirna"), QString(), "tibirna@kde.org"); + about->addAuthor(i18n("Matthias Kalle Dalheimer"), QString(), "kalle@kde.org"); + about->addAuthor(i18n("Daniel Molkentin"), QString(), "molkentin@kde.org"); + about->addAuthor(i18n("Wynn Wilkes"), QString(), "wynnw@caldera.com"); + about->addAuthor(i18n("Pat Dowler"), QString(), "dowler@pt1B1106.FSH.UVic.CA"); + about->addAuthor(i18n("Bernd Wuebben"), QString(), "wuebben@kde.org"); + about->addAuthor(i18n("Matthias Hoelzer-Kluepfel"), QString(), "hoelzer@kde.org"); + setAboutData(about); +} + +void KWinOptions::load() +{ + mTitleBarActions->load(); + mWindowActions->load(); + mMoving->load(); + mAdvanced->load(); + // mFocus is last because it may send unmanagedWidgetStateChanged + // that need to have the final word + mFocus->load(); +} + +void KWinOptions::save() +{ + mFocus->save(); + mTitleBarActions->save(); + mWindowActions->save(); + mMoving->save(); + mAdvanced->save(); + + emit KCModule::changed(false); + + // Send signal to all kwin instances + QDBusMessage message = + QDBusMessage::createSignal("/KWin", "org.kde.KWin", "reloadConfig"); + QDBusConnection::sessionBus().send(message); +} + + +void KWinOptions::defaults() +{ + mTitleBarActions->defaults(); + mWindowActions->defaults(); + mMoving->defaults(); + mAdvanced->defaults(); + // mFocus is last because it may send unmanagedWidgetDefaulted + // that need to have the final word + mFocus->defaults(); +} + +QString KWinOptions::quickHelp() const +{ + return i18n("

Window Behavior

Here you can customize the way windows behave when being" + " moved, resized or clicked on. You can also specify a focus policy as well as a placement" + " policy for new windows.

" + "

Please note that this configuration will not take effect if you do not use" + " KWin as your window manager. If you do use a different window manager, please refer to its documentation" + " for how to customize window behavior.

"); +} + +KActionsOptions::KActionsOptions(QWidget *parent, const QVariantList &) + : KCModule(parent) +{ + mSettings = new KWinOptionsSettings(this); + + QVBoxLayout *layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + tab = new QTabWidget(this); + layout->addWidget(tab); + + mTitleBarActions = new KTitleBarActionsConfig(false, mSettings, this); + mTitleBarActions->setObjectName(QLatin1String("KWin TitleBar Actions")); + tab->addTab(mTitleBarActions, i18n("&Titlebar Actions")); + connect(mTitleBarActions, qOverload(&KCModule::changed), this, qOverload(&KCModule::changed)); + connect(mTitleBarActions, qOverload(&KCModule::defaulted), this, qOverload(&KCModule::defaulted)); + + mWindowActions = new KWindowActionsConfig(false, mSettings, this); + mWindowActions->setObjectName(QLatin1String("KWin Window Actions")); + tab->addTab(mWindowActions, i18n("Window Actio&ns")); + connect(mWindowActions, qOverload(&KCModule::changed), this, qOverload(&KCModule::changed)); + connect(mWindowActions, qOverload(&KCModule::defaulted), this, qOverload(&KCModule::defaulted)); +} + +void KActionsOptions::load() +{ + mTitleBarActions->load(); + mWindowActions->load(); +} + +void KActionsOptions::save() +{ + mTitleBarActions->save(); + mWindowActions->save(); + + emit KCModule::changed(false); + // Send signal to all kwin instances + QDBusMessage message = + QDBusMessage::createSignal("/KWin", "org.kde.KWin", "reloadConfig"); + QDBusConnection::sessionBus().send(message); +} + +void KActionsOptions::defaults() +{ + mTitleBarActions->defaults(); + mWindowActions->defaults(); +} + +void KActionsOptions::moduleChanged(bool state) +{ + emit KCModule::changed(state); +} + +K_PLUGIN_FACTORY_DEFINITION(KWinOptionsFactory, + registerPlugin("kwinactions"); + registerPlugin("kwinfocus"); + registerPlugin("kwinmoving"); + registerPlugin("kwinadvanced"); + registerPlugin("kwinoptions"); + ) + +#include "main.moc" diff --git a/kcmkwin/kwinoptions/main.h b/kcmkwin/kwinoptions/main.h new file mode 100644 index 0000000..c366512 --- /dev/null +++ b/kcmkwin/kwinoptions/main.h @@ -0,0 +1,79 @@ +/* + main.h + + SPDX-FileCopyrightText: 2001 Waldo Bastian + + Requires the Qt widget libraries, available at no cost at + https://www.qt.io + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + + +#ifndef __MAIN_H__ +#define __MAIN_H__ + +#include +#include + +class KWinOptionsSettings; +class KWinOptionsKDEGlobalsSettings; +class KFocusConfig; +class KTitleBarActionsConfig; +class KWindowActionsConfig; +class KAdvancedConfig; +class KMovingConfig; + +class KWinOptions : public KCModule +{ + Q_OBJECT + +public: + + KWinOptions(QWidget *parent, const QVariantList &args); + + void load() override; + void save() override; + void defaults() override; + QString quickHelp() const override; + +private: + + QTabWidget *tab; + + KFocusConfig *mFocus; + KTitleBarActionsConfig *mTitleBarActions; + KWindowActionsConfig *mWindowActions; + KMovingConfig *mMoving; + KAdvancedConfig *mAdvanced; + + KWinOptionsSettings *mSettings; +}; + +class KActionsOptions : public KCModule +{ + Q_OBJECT + +public: + + KActionsOptions(QWidget *parent, const QVariantList &args); + + void load() override; + void save() override; + void defaults() override; + +protected Q_SLOTS: + + void moduleChanged(bool state); + +private: + + QTabWidget *tab; + + KTitleBarActionsConfig *mTitleBarActions; + KWindowActionsConfig *mWindowActions; + + KWinOptionsSettings *mSettings; +}; + +#endif diff --git a/kcmkwin/kwinoptions/mouse.cpp b/kcmkwin/kwinoptions/mouse.cpp new file mode 100644 index 0000000..9ef2ac8 --- /dev/null +++ b/kcmkwin/kwinoptions/mouse.cpp @@ -0,0 +1,106 @@ +/* + SPDX-FileCopyrightText: 1998 Matthias Ettrich + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "mouse.h" + +#include +#include + +#include + +#include "kwinoptions_settings.h" + +KWinMouseConfigForm::KWinMouseConfigForm(QWidget *parent) + : QWidget(parent) +{ + setupUi(parent); +} + +KWinActionsConfigForm::KWinActionsConfigForm(QWidget *parent) + : QWidget(parent) +{ + setupUi(parent); +} + +KTitleBarActionsConfig::KTitleBarActionsConfig(bool _standAlone, KWinOptionsSettings *settings, QWidget *parent) + : KCModule(parent), standAlone(_standAlone) + , m_ui(new KWinMouseConfigForm(this)) +{ + if (settings) { + initialize(settings); + } +} + +void KTitleBarActionsConfig::initialize(KWinOptionsSettings *settings) +{ + m_settings = settings; + addConfig(m_settings, this); + load(); +} + +void KTitleBarActionsConfig::showEvent(QShowEvent *ev) +{ + if (!standAlone) { + // Workaround KCModule::showEvent() calling load(), see bug 163817 + QWidget::showEvent(ev); + return; + } + KCModule::showEvent(ev); +} + +void KTitleBarActionsConfig::changeEvent(QEvent *ev) +{ + ev->accept(); +} + +void KTitleBarActionsConfig::save() +{ + KCModule::save(); + + if (standAlone) { + // Send signal to all kwin instances + QDBusMessage message = + QDBusMessage::createSignal("/KWin", "org.kde.KWin", "reloadConfig"); + QDBusConnection::sessionBus().send(message); + } +} + +KWindowActionsConfig::KWindowActionsConfig(bool _standAlone, KWinOptionsSettings *settings, QWidget *parent) + : KCModule(parent), standAlone(_standAlone) + , m_ui(new KWinActionsConfigForm(this)) +{ + if (settings) { + initialize(settings); + } +} + +void KWindowActionsConfig::initialize(KWinOptionsSettings *settings) +{ + m_settings = settings; + addConfig(m_settings, this); + load(); +} + +void KWindowActionsConfig::showEvent(QShowEvent *ev) +{ + if (!standAlone) { + QWidget::showEvent(ev); + return; + } + KCModule::showEvent(ev); +} + +void KWindowActionsConfig::save() +{ + KCModule::save(); + + if (standAlone) { + // Send signal to all kwin instances + QDBusMessage message = + QDBusMessage::createSignal("/KWin", "org.kde.KWin", "reloadConfig"); + QDBusConnection::sessionBus().send(message); + } +} diff --git a/kcmkwin/kwinoptions/mouse.h b/kcmkwin/kwinoptions/mouse.h new file mode 100644 index 0000000..b8c70dc --- /dev/null +++ b/kcmkwin/kwinoptions/mouse.h @@ -0,0 +1,83 @@ +/* + mouse.h + + SPDX-FileCopyrightText: 1998 Matthias Ettrich + + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef __KKWMMOUSECONFIG_H__ +#define __KKWMMOUSECONFIG_H__ + +class KConfig; + +#include +#include + +#include "ui_actions.h" +#include "ui_mouse.h" + +class KWinOptionsSettings; + +class KWinMouseConfigForm : public QWidget, public Ui::KWinMouseConfigForm +{ + Q_OBJECT + +public: + explicit KWinMouseConfigForm(QWidget* parent); +}; + +class KWinActionsConfigForm : public QWidget, public Ui::KWinActionsConfigForm +{ + Q_OBJECT + +public: + explicit KWinActionsConfigForm(QWidget* parent); +}; + +class KTitleBarActionsConfig : public KCModule +{ + Q_OBJECT + +public: + + KTitleBarActionsConfig(bool _standAlone, KWinOptionsSettings *settings, QWidget *parent); + + void save() override; + +protected: + void initialize(KWinOptionsSettings *settings); + void showEvent(QShowEvent *ev) override; + void changeEvent(QEvent *ev) override; + +private: + bool standAlone; + + KWinMouseConfigForm *m_ui; + KWinOptionsSettings *m_settings; +}; + +class KWindowActionsConfig : public KCModule +{ + Q_OBJECT + +public: + + KWindowActionsConfig(bool _standAlone, KWinOptionsSettings *settings, QWidget *parent); + + void save() override; + +protected: + void initialize(KWinOptionsSettings *settings); + void showEvent(QShowEvent *ev) override; + +private: + bool standAlone; + + KWinActionsConfigForm *m_ui; + KWinOptionsSettings *m_settings; +}; + +#endif + diff --git a/kcmkwin/kwinoptions/mouse.ui b/kcmkwin/kwinoptions/mouse.ui new file mode 100644 index 0000000..6fed1c1 --- /dev/null +++ b/kcmkwin/kwinoptions/mouse.ui @@ -0,0 +1,740 @@ + + + KWinMouseConfigForm + + + + 0 + 0 + 600 + 500 + + + + + + + Titlebar Actions + + + + Qt::AlignHCenter|Qt::AlignTop + + + + + &Double-click: + + + kcfg_TitlebarDoubleClickCommand + + + + + + + Behavior on <em>double</em> click into the titlebar. + + + + Maximize + + + + + Vertically maximize + + + + + Horizontally maximize + + + + + Minimize + + + + + Shade + + + + + Lower + + + + + Close + + + + + Show on all desktops + + + + + Do nothing + + + + + + + + Mouse &wheel: + + + kcfg_CommandTitlebarWheel + + + + + + + Behavior on <em>mouse wheel</em> scroll over the titlebar. + + + + Raise/lower + + + + + Shade/unshade + + + + + Maximize/restore + + + + + Keep above/below + + + + + Move to previous/next desktop + + + + + Change opacity + + + + + Do nothing + + + + + + + + + + + Titlebar and Frame Actions + + + Qt::AlignCenter + + + + + + Qt::Horizontal + + + + 0 + 0 + + + + + + + + + + Active + + + Qt::AlignCenter + + + + + + + &Left click: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_CommandActiveTitlebar1 + + + + + + + Inactive + + + Qt::AlignCenter + + + + + + + &Middle click: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_CommandActiveTitlebar2 + + + + + + + &Right click: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_CommandActiveTitlebar3 + + + + + + + + 0 + 0 + + + + Behavior on <em>left</em> click into the titlebar or frame of an <em>active</em> window. + + + + Raise + + + + + Lower + + + + + Toggle raise and lower + + + + + Minimize + + + + + Shade + + + + + Close + + + + + Show actions menu + + + + + Do nothing + + + + + + + + Behavior on <em>left</em> click into the titlebar or frame of an <em>inactive</em> window. + + + + Activate and raise + + + + + Activate and lower + + + + + Activate + + + + + Raise + + + + + Lower + + + + + Toggle raise and lower + + + + + Minimize + + + + + Shade + + + + + Close + + + + + Show actions menu + + + + + Do nothing + + + + + + + + + 0 + 0 + + + + Behavior on <em>left</em> click into the titlebar or frame of an <em>active</em> window. + + + + Raise + + + + + Lower + + + + + Toggle raise and lower + + + + + Minimize + + + + + Shade + + + + + Close + + + + + Show actions menu + + + + + Do nothing + + + + + + + + Behavior on <em>left</em> click into the titlebar or frame of an <em>inactive</em> window. + + + + Activate and raise + + + + + Activate and lower + + + + + Activate + + + + + Raise + + + + + Lower + + + + + Toggle raise and lower + + + + + Minimize + + + + + Shade + + + + + Close + + + + + Show actions menu + + + + + Do nothing + + + + + + + + + 0 + 0 + + + + Behavior on <em>left</em> click into the titlebar or frame of an <em>active</em> window. + + + + Raise + + + + + Lower + + + + + Toggle raise and lower + + + + + Minimize + + + + + Shade + + + + + Close + + + + + Show actions menu + + + + + Do nothing + + + + + + + + Behavior on <em>left</em> click into the titlebar or frame of an <em>inactive</em> window. + + + + Activate and raise + + + + + Activate and lower + + + + + Activate + + + + + Raise + + + + + Lower + + + + + Toggle raise and lower + + + + + Minimize + + + + + Shade + + + + + Close + + + + + Show actions menu + + + + + Do nothing + + + + + + + + + + Qt::Horizontal + + + + 0 + 0 + + + + + + + + + + + Maximize Button Actions + + + + Qt::AlignHCenter|Qt::AlignTop + + + + + Behavior on <em>left</em> click onto the maximize button. + + + L&eft click: + + + kcfg_MaximizeButtonLeftClickCommand + + + + + + + Behavior on <em>left</em> click onto the maximize button. + + + + Maximize + + + + + Vertically maximize + + + + + Horizontally maximize + + + + + + + + Behavior on <em>middle</em> click onto the maximize button. + + + Middle c&lick: + + + kcfg_MaximizeButtonMiddleClickCommand + + + + + + + Behavior on <em>middle</em> click onto the maximize button. + + + + Maximize + + + + + Vertically maximize + + + + + Horizontally maximize + + + + + + + + Behavior on <em>right</em> click onto the maximize button. + + + Right clic&k: + + + kcfg_MaximizeButtonRightClickCommand + + + + + + + Behavior on <em>right</em> click onto the maximize button. + + + + Maximize + + + + + Vertically maximize + + + + + Horizontally maximize + + + + + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + + KComboBox + QComboBox +
kcombobox.h
+
+
+ + kcfg_TitlebarDoubleClickCommand + kcfg_CommandTitlebarWheel + kcfg_CommandActiveTitlebar1 + kcfg_CommandInactiveTitlebar1 + kcfg_CommandActiveTitlebar2 + kcfg_CommandInactiveTitlebar2 + kcfg_CommandActiveTitlebar3 + kcfg_CommandInactiveTitlebar3 + kcfg_MaximizeButtonLeftClickCommand + kcfg_MaximizeButtonMiddleClickCommand + kcfg_MaximizeButtonRightClickCommand + + + +
diff --git a/kcmkwin/kwinoptions/moving.ui b/kcmkwin/kwinoptions/moving.ui new file mode 100644 index 0000000..69019b3 --- /dev/null +++ b/kcmkwin/kwinoptions/moving.ui @@ -0,0 +1,170 @@ + + + KWinMovingConfigForm + + + + 0 + 0 + 600 + 500 + + + + + Qt::AlignHCenter|Qt::AlignTop + + + + + Window &geometry: + + + kcfg_GeometryTip + + + + + + + Enable this option if you want a window's geometry to be displayed while it is being moved or resized. The window position relative to the top-left corner of the screen is displayed together with its size. + + + Display when moving or resizing + + + + + + + Screen &edge snap zone: + + + kcfg_BorderSnapZone + + + + + + + Here you can set the snap zone for screen edges, i.e. the 'strength' of the magnetic field which will make windows snap to the border when moved near it. + + + None + + + px + + + 0 + + + 100 + + + 10 + + + + + + + Here you can set the snap zone for windows, i.e. the 'strength' of the magnetic field which will make windows snap to each other when they are moved near another window. + + + None + + + px + + + 0 + + + 100 + + + 10 + + + + + + + Here you can set the snap zone for the screen center, i.e. the 'strength' of the magnetic field which will make windows snap to the center of the screen when moved near it. + + + None + + + px + + + 0 + + + 100 + + + + + + + Here you can set that windows will be only snapped if you try to overlap them, i.e. they will not be snapped if the windows comes only near another window or border. + + + Only when overlapping + + + + + + + &Window snap zone: + + + kcfg_WindowSnapZone + + + + + + + &Center snap zone: + + + kcfg_CenterSnapZone + + + + + + + &Snap windows: + + + kcfg_SnapOnlyWhenOverlapping + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 2 + + + + + + + + + diff --git a/kcmkwin/kwinoptions/windows.cpp b/kcmkwin/kwinoptions/windows.cpp new file mode 100644 index 0000000..c804003 --- /dev/null +++ b/kcmkwin/kwinoptions/windows.cpp @@ -0,0 +1,323 @@ +/* + windows.cpp + + SPDX-FileCopyrightText: 1997 Patrick Dowler + SPDX-FileCopyrightText: 2001 Waldo Bastian + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "windows.h" +#include "kwinoptions_settings.h" +#include +#include + +#include "kwinoptions_settings.h" +#include "kwinoptions_kdeglobals_settings.h" +#include + +#define CLICK_TO_FOCUS 0 +#define CLICK_TO_FOCUS_MOUSE_PRECEDENT 1 +#define FOCUS_FOLLOWS_MOUSE 2 +#define FOCUS_FOLLOWS_MOUSE_PRECEDENT 3 +#define FOCUS_UNDER_MOUSE 4 +#define FOCUS_STRICTLY_UNDER_MOUSE 5 + +KWinFocusConfigForm::KWinFocusConfigForm(QWidget* parent) + : QWidget(parent) +{ + setupUi(parent); +} + +KFocusConfig::KFocusConfig(bool _standAlone, KWinOptionsSettings *settings, QWidget * parent) + : KCModule(parent), standAlone(_standAlone) + , m_ui(new KWinFocusConfigForm(this)) +{ + if (settings) { + initialize(settings); + } +} + +void KFocusConfig::initialize(KWinOptionsSettings *settings) +{ + m_settings = settings; + addConfig(m_settings, this); + + connect(m_ui->windowFocusPolicy, qOverload(&QComboBox::currentIndexChanged), this, &KFocusConfig::focusPolicyChanged); + + connect(qApp, &QGuiApplication::screenAdded, this, &KFocusConfig::updateMultiScreen); + connect(qApp, &QGuiApplication::screenRemoved, this, &KFocusConfig::updateMultiScreen); + updateMultiScreen(); + + load(); +} + +void KFocusConfig::updateMultiScreen() +{ + m_ui->multiscreenBehaviorLabel->setVisible(QApplication::screens().count() > 1); + m_ui->kcfg_ActiveMouseScreen->setVisible(QApplication::screens().count() > 1); + m_ui->kcfg_SeparateScreenFocus->setVisible(QApplication::screens().count() > 1); +} + +void KFocusConfig::focusPolicyChanged() +{ + int selectedFocusPolicy = 0; + bool selectedNextFocusPrefersMouseItem = false; + const bool loadedNextFocusPrefersMouseItem = m_settings->nextFocusPrefersMouse(); + + int focusPolicy = m_ui->windowFocusPolicy->currentIndex(); + switch (focusPolicy) { + case CLICK_TO_FOCUS: + m_ui->windowFocusPolicyDescriptionLabel->setText(i18n("Click to focus: A window becomes active when you click into it. This behavior is common on other operating systems and likely what you want.")); + selectedFocusPolicy = KWinOptionsSettings::EnumFocusPolicy::ClickToFocus; + break; + case CLICK_TO_FOCUS_MOUSE_PRECEDENT: + m_ui->windowFocusPolicyDescriptionLabel->setText(i18n("Click to focus (mouse precedence): Mostly the same as Click to focus. If an active window has to be chosen by the system (eg. because the currently active one was closed) the window under the mouse is the preferred candidate. Unusual, but possible variant of Click to focus.")); + selectedFocusPolicy = KWinOptionsSettings::EnumFocusPolicy::ClickToFocus; + selectedNextFocusPrefersMouseItem = true; + break; + case FOCUS_FOLLOWS_MOUSE: + m_ui->windowFocusPolicyDescriptionLabel->setText(i18n("Focus follows mouse: Moving the mouse onto a window will activate it. Eg. windows randomly appearing under the mouse will not gain the focus. Focus stealing prevention takes place as usual. Think as Click to focus just without having to actually click.")); + selectedFocusPolicy = KWinOptionsSettings::EnumFocusPolicy::FocusFollowsMouse; + break; + case FOCUS_FOLLOWS_MOUSE_PRECEDENT: + m_ui->windowFocusPolicyDescriptionLabel->setText(i18n("This is mostly the same as Focus follows mouse. If an active window has to be chosen by the system (eg. because the currently active one was closed) the window under the mouse is the preferred candidate. Choose this, if you want a hover controlled focus.")); + selectedFocusPolicy = KWinOptionsSettings::EnumFocusPolicy::FocusFollowsMouse; + selectedNextFocusPrefersMouseItem = true; + break; + case FOCUS_UNDER_MOUSE: + m_ui->windowFocusPolicyDescriptionLabel->setText(i18n("Focus under mouse: The focus always remains on the window under the mouse.
Warning: Focus stealing prevention and the tabbox ('Alt+Tab') contradict the activation policy and will not work. You very likely want to use Focus follows mouse (mouse precedence) instead!")); + selectedFocusPolicy = KWinOptionsSettings::EnumFocusPolicy::FocusUnderMouse; + break; + case FOCUS_STRICTLY_UNDER_MOUSE: + m_ui->windowFocusPolicyDescriptionLabel->setText(i18n("Focus strictly under mouse: The focus is always on the window under the mouse (in doubt nowhere) very much like the focus behavior in an unmanaged legacy X11 environment.
Warning: Focus stealing prevention and the tabbox ('Alt+Tab') contradict the activation policy and will not work. You very likely want to use Focus follows mouse (mouse precedence) instead!")); + selectedFocusPolicy = KWinOptionsSettings::EnumFocusPolicy::FocusStrictlyUnderMouse; + break; + } + + const bool changed = m_settings->focusPolicy() != selectedFocusPolicy || loadedNextFocusPrefersMouseItem != selectedNextFocusPrefersMouseItem; + unmanagedWidgetChangeState(changed); + emit unmanagedWidgetStateChanged(changed); + + const bool isDefault = focusPolicy == CLICK_TO_FOCUS; + unmanagedWidgetDefaultState(isDefault); + emit unmanagedWidgetDefaulted(isDefault); + + // the auto raise related widgets are: autoRaise + m_ui->kcfg_AutoRaise->setEnabled(focusPolicy != CLICK_TO_FOCUS && focusPolicy != CLICK_TO_FOCUS_MOUSE_PRECEDENT); + + m_ui->kcfg_FocusStealingPreventionLevel->setDisabled(focusPolicy == FOCUS_UNDER_MOUSE || focusPolicy == FOCUS_STRICTLY_UNDER_MOUSE); + + // the delayed focus related widgets are: delayFocus + m_ui->delayFocusOnLabel->setEnabled(focusPolicy != CLICK_TO_FOCUS); + m_ui->kcfg_DelayFocusInterval->setEnabled(focusPolicy != CLICK_TO_FOCUS); + + // on by default for non click to focus policies + if (m_settings->activeMouseScreen() == m_settings->defaultActiveMouseScreenValue()) { + m_ui->kcfg_ActiveMouseScreen->setChecked(focusPolicy != CLICK_TO_FOCUS && focusPolicy != CLICK_TO_FOCUS_MOUSE_PRECEDENT); + } +} + +void KFocusConfig::showEvent(QShowEvent *ev) +{ + if (!standAlone) { + QWidget::showEvent(ev); + return; + } + KCModule::showEvent(ev); +} + +void KFocusConfig::load(void) +{ + KCModule::load(); + + const bool loadedNextFocusPrefersMouseItem = m_settings->nextFocusPrefersMouse(); + + int focusPolicy = m_settings->focusPolicy(); + + switch (focusPolicy) { + // the ClickToFocus and FocusFollowsMouse have special values when + // NextFocusPrefersMouse is true + case KWinOptionsSettings::EnumFocusPolicy::ClickToFocus: + m_ui->windowFocusPolicy->setCurrentIndex(CLICK_TO_FOCUS + loadedNextFocusPrefersMouseItem); + break; + case KWinOptionsSettings::EnumFocusPolicy::FocusFollowsMouse: + m_ui->windowFocusPolicy->setCurrentIndex(FOCUS_FOLLOWS_MOUSE + loadedNextFocusPrefersMouseItem); + break; + default: + // +2 to ignore the two special values + m_ui->windowFocusPolicy->setCurrentIndex(focusPolicy + 2); + break; + } +} + +void KFocusConfig::save(void) +{ + KCModule::save(); + + int idxFocusPolicy = m_ui->windowFocusPolicy->currentIndex(); + switch (idxFocusPolicy) { + case CLICK_TO_FOCUS: + case CLICK_TO_FOCUS_MOUSE_PRECEDENT: + m_settings->setFocusPolicy(KWinOptionsSettings::EnumFocusPolicy::ClickToFocus); + break; + case FOCUS_FOLLOWS_MOUSE: + case FOCUS_FOLLOWS_MOUSE_PRECEDENT: + // the ClickToFocus and FocusFollowsMouse have special values when + // NextFocusPrefersMouse is true + m_settings->setFocusPolicy(KWinOptionsSettings::EnumFocusPolicy::FocusFollowsMouse); + break; + case FOCUS_UNDER_MOUSE: + m_settings->setFocusPolicy(KWinOptionsSettings::EnumFocusPolicy::FocusUnderMouse); + break; + case FOCUS_STRICTLY_UNDER_MOUSE: + m_settings->setFocusPolicy(KWinOptionsSettings::EnumFocusPolicy::FocusStrictlyUnderMouse); + break; + } + m_settings->setNextFocusPrefersMouse(idxFocusPolicy == CLICK_TO_FOCUS_MOUSE_PRECEDENT || idxFocusPolicy == FOCUS_FOLLOWS_MOUSE_PRECEDENT); + + m_settings->save(); + + if (standAlone) { + // Send signal to all kwin instances + QDBusMessage message = + QDBusMessage::createSignal("/KWin", "org.kde.KWin", "reloadConfig"); + QDBusConnection::sessionBus().send(message); + } + emit KCModule::changed(false); +} + +void KFocusConfig::defaults() +{ + KCModule::defaults(); + m_ui->windowFocusPolicy->setCurrentIndex(CLICK_TO_FOCUS); +} + +KWinAdvancedConfigForm::KWinAdvancedConfigForm(QWidget* parent) + : QWidget(parent) +{ + setupUi(parent); +} + +KAdvancedConfig::KAdvancedConfig(bool _standAlone, KWinOptionsSettings *settings, KWinOptionsKDEGlobalsSettings *globalSettings, QWidget *parent) + : KCModule(parent), standAlone(_standAlone) + , m_ui(new KWinAdvancedConfigForm(this)) +{ + if (settings && globalSettings) { + initialize(settings, globalSettings); + } +} + +void KAdvancedConfig::initialize(KWinOptionsSettings *settings, KWinOptionsKDEGlobalsSettings *globalSettings) +{ + m_settings = settings; + addConfig(m_settings, this); + addConfig(globalSettings, this); + + m_ui->kcfg_Placement->setItemData(KWinOptionsSettings::PlacementChoices::Smart, "Smart"); + m_ui->kcfg_Placement->setItemData(KWinOptionsSettings::PlacementChoices::Maximizing, "Maximizing"); + m_ui->kcfg_Placement->setItemData(KWinOptionsSettings::PlacementChoices::Cascade, "Cascade"); + m_ui->kcfg_Placement->setItemData(KWinOptionsSettings::PlacementChoices::Random, "Random"); + m_ui->kcfg_Placement->setItemData(KWinOptionsSettings::PlacementChoices::Centered, "Centered"); + m_ui->kcfg_Placement->setItemData(KWinOptionsSettings::PlacementChoices::ZeroCornered, "ZeroCornered"); + m_ui->kcfg_Placement->setItemData(KWinOptionsSettings::PlacementChoices::UnderMouse, "UnderMouse"); + + // Don't show the option to prevent KDE apps from remembering their window + // positions on Wayland because it doesn't work on Wayland and the feature + // will eventually be implemented in a different way there. + // This option lives in the kdeglobals file because it is consumed by + // kxmlgui. + m_ui->kcfg_AllowKDEAppsToRememberWindowPositions->setVisible(KWindowSystem::isPlatformX11()); + + load(); +} + +void KAdvancedConfig::showEvent(QShowEvent *ev) +{ + if (!standAlone) { + QWidget::showEvent(ev); + return; + } + KCModule::showEvent(ev); +} + +void KAdvancedConfig::save(void) +{ + KCModule::save(); + + if (standAlone) { + // Send signal to all kwin instances + QDBusMessage message = + QDBusMessage::createSignal("/KWin", "org.kde.KWin", "reloadConfig"); + QDBusConnection::sessionBus().send(message); + + } +} + +KWinMovingConfigForm::KWinMovingConfigForm(QWidget* parent) + : QWidget(parent) +{ + setupUi(parent); +} + +KMovingConfig::KMovingConfig(bool _standAlone, KWinOptionsSettings *settings, QWidget *parent) + : KCModule(parent), standAlone(_standAlone) + , m_ui(new KWinMovingConfigForm(this)) +{ + if (settings) { + initialize(settings); + } +} + +void KMovingConfig::initialize(KWinOptionsSettings *settings) +{ + m_settings = settings; + addConfig(m_settings, this); + load(); +} + +void KMovingConfig::showEvent(QShowEvent *ev) +{ + if (!standAlone) { + QWidget::showEvent(ev); + return; + } + KCModule::showEvent(ev); +} + +void KMovingConfig::save(void) +{ + KCModule::save(); + + if (standAlone) { + // Send signal to all kwin instances + QDBusMessage message = + QDBusMessage::createSignal("/KWin", "org.kde.KWin", "reloadConfig"); + QDBusConnection::sessionBus().send(message); + } + // and reconfigure the effect + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + if (m_settings->geometryTip()) { + interface.loadEffect(KWin::BuiltInEffects::nameForEffect(KWin::BuiltInEffect::WindowGeometry)); + } else { + interface.unloadEffect(KWin::BuiltInEffects::nameForEffect(KWin::BuiltInEffect::WindowGeometry)); + } +} diff --git a/kcmkwin/kwinoptions/windows.h b/kcmkwin/kwinoptions/windows.h new file mode 100644 index 0000000..9f325ff --- /dev/null +++ b/kcmkwin/kwinoptions/windows.h @@ -0,0 +1,126 @@ +/* + windows.h + + SPDX-FileCopyrightText: 1997 Patrick Dowler + SPDX-FileCopyrightText: 2001 Waldo Bastian + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KKWMWINDOWS_H +#define KKWMWINDOWS_H + +#include +#include + +#include "ui_advanced.h" +#include "ui_focus.h" +#include "ui_moving.h" + +class QRadioButton; +class QCheckBox; +class QPushButton; +class KComboBox; +class QGroupBox; +class QLabel; +class QSlider; +class QGroupBox; +class QSpinBox; + +class KColorButton; + +class KWinOptionsSettings; +class KWinOptionsKDEGlobalsSettings; + +class KWinFocusConfigForm : public QWidget, public Ui::KWinFocusConfigForm +{ + Q_OBJECT + +public: + explicit KWinFocusConfigForm(QWidget* parent); +}; + +class KWinMovingConfigForm : public QWidget, public Ui::KWinMovingConfigForm +{ + Q_OBJECT + +public: + explicit KWinMovingConfigForm(QWidget* parent); +}; + +class KWinAdvancedConfigForm : public QWidget, public Ui::KWinAdvancedConfigForm +{ + Q_OBJECT + +public: + explicit KWinAdvancedConfigForm(QWidget* parent); +}; + +class KFocusConfig : public KCModule +{ + Q_OBJECT +public: + KFocusConfig(bool _standAlone, KWinOptionsSettings *settings, QWidget *parent); + + void load() override; + void save() override; + void defaults() override; + +Q_SIGNALS: + void unmanagedWidgetDefaulted(bool defaulted); + void unmanagedWidgetStateChanged(bool changed); + +protected: + void initialize(KWinOptionsSettings *settings); + void showEvent(QShowEvent *ev) override; + +private Q_SLOTS: + void focusPolicyChanged(); + void updateMultiScreen(); + +private: + + bool standAlone; + + KWinFocusConfigForm *m_ui; + KWinOptionsSettings *m_settings; +}; + +class KMovingConfig : public KCModule +{ + Q_OBJECT +public: + KMovingConfig(bool _standAlone, KWinOptionsSettings *settings, QWidget *parent); + + void save() override; + +protected: + void initialize(KWinOptionsSettings *settings); + void showEvent(QShowEvent *ev) override; + +private: + KWinOptionsSettings *m_settings; + bool standAlone; + KWinMovingConfigForm *m_ui; +}; + +class KAdvancedConfig : public KCModule +{ + Q_OBJECT +public: + KAdvancedConfig(bool _standAlone, KWinOptionsSettings *settings, KWinOptionsKDEGlobalsSettings *globalSettings, QWidget *parent); + + void save() override; + +protected: + void initialize(KWinOptionsSettings *settings, KWinOptionsKDEGlobalsSettings *globalSettings); + void showEvent(QShowEvent *ev) override; + +private: + + bool standAlone; + KWinAdvancedConfigForm *m_ui; + KWinOptionsSettings *m_settings; +}; + +#endif // KKWMWINDOWS_H diff --git a/kcmkwin/kwinrules/CMakeLists.txt b/kcmkwin/kwinrules/CMakeLists.txt new file mode 100644 index 0000000..dc6e935 --- /dev/null +++ b/kcmkwin/kwinrules/CMakeLists.txt @@ -0,0 +1,57 @@ +# KI18N Translation Domain for this library +add_definitions(-DTRANSLATION_DOMAIN=\"kcm_kwinrules\") +add_definitions(-DKCMRULES) + +include_directories(../../) + +set(kwinrules_SRCS + ../../rulebooksettings.cpp + ../../cursor.cpp + ../../plugins/platforms/x11/standalone/x11cursor.cpp + ../../rules.cpp + ../../placement.cpp + ../../utils.cpp + ../../virtualdesktopsdbustypes.cpp + kwinsrc.cpp + optionsmodel.cpp + ruleitem.cpp + rulesmodel.cpp +) + +kconfig_add_kcfg_files(kwinrules_SRCS ../../rulesettings.kcfgc) +kconfig_add_kcfg_files(kwinrules_SRCS ../../rulebooksettingsbase.kcfgc) + +add_library(KWinRulesObjects STATIC ${kwinrules_SRCS}) + +set(kwin_kcm_rules_XCB_LIBS + XCB::CURSOR + XCB::XCB + XCB::XFIXES +) + +set(kcm_libs + Qt5::Quick + Qt5::QuickWidgets + + KF5::I18n + KF5::QuickAddons + KF5::WindowSystem + KF5::XmlGui +) + +if (KWIN_BUILD_ACTIVITIES) + set(kcm_libs ${kcm_libs} KF5::Activities) +endif() +target_link_libraries(KWinRulesObjects ${kcm_libs} ${kwin_kcm_rules_XCB_LIBS}) + +add_executable(kwin_rules_dialog main.cpp rulesdialog.cpp) +target_link_libraries(kwin_rules_dialog KWinRulesObjects) +install(TARGETS kwin_rules_dialog DESTINATION ${LIBEXEC_INSTALL_DIR}) + +add_library(kcm_kwinrules MODULE kcmrules.cpp rulebookmodel.cpp) +target_link_libraries(kcm_kwinrules KWinRulesObjects) +kcoreaddons_desktop_to_json(kcm_kwinrules "kcm_kwinrules.desktop" SERVICE_TYPES kcmodule.desktop) + +install(TARGETS kcm_kwinrules DESTINATION ${PLUGIN_INSTALL_DIR}/kcms) +install(FILES kcm_kwinrules.desktop DESTINATION ${SERVICES_INSTALL_DIR}) +kpackage_install_package(package kcm_kwinrules kcms) diff --git a/kcmkwin/kwinrules/Messages.sh b/kcmkwin/kwinrules/Messages.sh new file mode 100644 index 0000000..cbb5e58 --- /dev/null +++ b/kcmkwin/kwinrules/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name \*.cpp -o -name \*.h -o -name \*.qml` -o $podir/kcm_kwinrules.pot diff --git a/kcmkwin/kwinrules/kcm_kwinrules.desktop b/kcmkwin/kwinrules/kcm_kwinrules.desktop new file mode 100644 index 0000000..5c597c4 --- /dev/null +++ b/kcmkwin/kwinrules/kcm_kwinrules.desktop @@ -0,0 +1,169 @@ +[Desktop Entry] +Exec=kcmshell5 kwinrules +Icon=preferences-system-windows-actions +Categories=Qt;KDE;X-KDE-settings-looknfeel; +Type=Service + +X-KDE-ServiceTypes=KCModule +X-KDE-Library=kcm_kwinrules +X-KDE-ParentApp=kcontrol +X-KDE-System-Settings-Parent-Category=windowmanagement +X-KDE-Weight=120 + +X-KDE-FormFactors=desktop,tablet + +Name=Window Rules +Name[ar]=قواعد النوافذ +Name[az]=Pəncərə qaydaları +Name[bg]=Правила за прозорци +Name[bs]=Pravila prozora +Name[ca]=Regles de les finestres +Name[ca@valencia]=Regles de les finestres +Name[cs]=Pravidla oken +Name[da]=Vinduesregler +Name[de]=Fensterregeln +Name[el]=Κανόνες παραθύρου +Name[en_GB]=Window Rules +Name[es]=Reglas de las ventanas +Name[et]=Akna reeglid +Name[eu]=Leihoaren arauak +Name[fi]=Ikkunasäännöt +Name[fr]=Règles de la fenêtre +Name[ga]=Rialacha Fuinneog +Name[gl]=Regras da xanela +Name[gu]=વિન્ડો નિયમો +Name[he]=כללי חלון +Name[hi]=विंडो निय +Name[hr]=Pravila prozora +Name[hu]=Ablakszabályok +Name[ia]=Regulas de fenestra +Name[id]=Peraturan Window +Name[is]=Gluggahegðunarreglur +Name[it]=Regole delle finestre +Name[ja]=ウィンドウルール +Name[kk]=Терезе тәртібі +Name[km]=ក្បួន​បង្អួច +Name[kn]=ವಿಂಡೋ ನಿಯಮಗಳು +Name[ko]=창 규칙 +Name[lt]=Langų taisyklės +Name[lv]=Loga noteikumi +Name[mr]=चौकट नियम +Name[nb]=Vindusregler +Name[nds]=Finsterbedregen +Name[nl]=Vensterregels +Name[nn]=Vindaugsreglar +Name[pa]=ਵਿੰਡੋ ਨਿਯਮ +Name[pl]=Zasady okien +Name[pt]=Regras das Janelas +Name[pt_BR]=Regras das janelas +Name[ro]=Reguli ferestre +Name[ru]=Особые параметры окон +Name[si]=කවුළු නීති +Name[sk]=Pravidlá okien +Name[sl]=Pravila za okna +Name[sr]=Правила прозора +Name[sr@ijekavian]=Правила прозора +Name[sr@ijekavianlatin]=Pravila prozora +Name[sr@latin]=Pravila prozora +Name[sv]=Fönsterregler +Name[th]=กฎต่าง ๆ ของหน้าต่าง +Name[tr]=Pencere Kuralları +Name[ug]=كۆزنەك بەلگىلىمىسى +Name[uk]=Правила вікон +Name[wa]=Rîles des finiesses +Name[x-test]=xxWindow Rulesxx +Name[zh_CN]=窗口规则 +Name[zh_TW]=視窗規則 + +Comment=Individual Window Behavior +Comment[az]=İndividual pəncərə davranışları +Comment[bs]=Ponašanje pojedinog prozora +Comment[ca]=Comportament individual de les finestres +Comment[ca@valencia]=Comportament individual de les finestres +Comment[cs]=Chování individuálních oken +Comment[da]=Opførsel af enkeltvinduer +Comment[de]=Individuelles Fensterverhalten +Comment[el]=Συμπεριφορά ανεξάρτητου παραθύρου +Comment[en_GB]=Individual Window Behaviour +Comment[es]=Comportamiento de las ventanas individuales +Comment[et]=Konkreetse akna käitumine +Comment[eu]=Leihoen banakako portaera +Comment[fi]=Yksittäisten ikkunoiden toiminta +Comment[fr]=Comportement individuel des fenêtres +Comment[gl]=Comportamento individual das xanelas +Comment[he]=התנהגות חלונות ספציפים +Comment[hu]=Egyéni ablakműveletek +Comment[ia]=Comportamento de fenestra individual +Comment[id]=Perilaku Window Individu +Comment[it]=Comportamento della singola finestra +Comment[ja]=個別のウィンドウの挙動 +Comment[ko]=개별 창 동작 +Comment[lt]=Individuali langų elgsena +Comment[nb]=Oppførsel for individuelle vinduer +Comment[nds]=Bedregen vun enkelte Finstern +Comment[nl]=Individueel venstergedrag +Comment[nn]=Åtferd for einskildvindauge +Comment[pa]=ਵੱਖ-ਵੱਖ ਵਿੰਡੋ ਰਵੱਈਆ +Comment[pl]=Wyjątkowe okna +Comment[pt]=Comportamento das Janelas Individuais +Comment[pt_BR]=Comportamento das janelas individuais +Comment[ro]=Comportament al ferestrelor individuale +Comment[ru]=Особые параметры конкретных окон +Comment[sk]=Individuálne správanie okien +Comment[sl]=Obnašanje posameznih oken +Comment[sr]=Понашање појединачних прозора +Comment[sr@ijekavian]=Понашање појединачних прозора +Comment[sr@ijekavianlatin]=Ponašanje pojedinačnih prozora +Comment[sr@latin]=Ponašanje pojedinačnih prozora +Comment[sv]=Individuellt fönsterbeteende +Comment[tr]=Bireysel Pencere Davranışı +Comment[uk]=Поведінка окремих вікон +Comment[x-test]=xxIndividual Window Behaviorxx +Comment[zh_CN]=个别窗口行为 +Comment[zh_TW]=個別視窗行為 + +X-KDE-Keywords=size,position,state,window behavior,windows,specific,workarounds,remember,rules +X-KDE-Keywords[az]=ölçü,mövqe,bölgə,pəncərə davranışı,pəncərələr,xüsusi,iş sahəsi,xatırlama,rollar +X-KDE-Keywords[bs]=veličina,pozicija,grad,reagiranje prozora,prozori,specifičan,workarounds,sjećati se,pravila +X-KDE-Keywords[ca]=mida,posició,estat,comportament de la finestra,finestres,específic,solucions alternatives,recorda,regles +X-KDE-Keywords[ca@valencia]=mida,posició,estat,comportament de la finestra,finestres,específic,solucions alternatives,recorda,regles +X-KDE-Keywords[da]=størrelse,position,tilstand,vinduesopførsel,vinduer,specifikt,workarounds,husk,regler +X-KDE-Keywords[de]=Größe,Position,Status,Fensterverhalten,Fenster,Regeln +X-KDE-Keywords[el]=μέγεθος,θέση,κατάσταση,συμπεριφορά παραθύρου,παράθυρα,ειδική,εναλλακτικές,απομνημόνευση,κανόνες +X-KDE-Keywords[en_GB]=size,position,state,window behaviour,windows,specific,workarounds,remember,rules +X-KDE-Keywords[es]=tamaño,posición,estado,comportamiento de las ventanas,ventanas,específicos,soluciones,recordatorio,reglas +X-KDE-Keywords[et]=suurus,asukoht,olek,akende käitumine,aknad,meeldejätmine,reeglid +X-KDE-Keywords[eu]=tamaina,posizio,egoera,leihoaren portaera,leihoak,zehatz,konponbide,gogorarazpen,arau +X-KDE-Keywords[fi]=koko,sijainti,tila,ikkunoiden toiminta,ikkunat,erikoisasetukset,ikkunakohtaiset,korjaukset,muista,muistaminen,säännöt +X-KDE-Keywords[fr]=taille, position, état, comportement de la fenêtre, fenêtres, spécifique, contournements, rappel, règles +X-KDE-Keywords[ga]=méid,ionad,staid,oibriú na bhfuinneog,fuinneoga,sainiúil,réitigh seiftithe,meabhraigh,rialacha +X-KDE-Keywords[gl]=tamaño,posición,estado,comportamento da xanela,xanelas,específico,regra +X-KDE-Keywords[hu]=méret,elhelyezkedés,állapot,ablakműködés,ablakok,specifikus,kerülő megoldások,megjegyzés,szabályok +X-KDE-Keywords[ia]=grandor,position,stato,comportamento de fenestra,fenestras,specific,workarounds,memora,regulas +X-KDE-Keywords[id]=ukuran,posisi,kondisi,perilaku window,window,spesifik,sekeliling,ingat,peraturan +X-KDE-Keywords[it]=dimensione,posizione,stato,comportamento della finestra,finestre,specifico,espedienti,ricorda,regole +X-KDE-Keywords[kk]=size,position,state,window behavior,windows,specific,workarounds,remember,rules +X-KDE-Keywords[km]=size,position,state,window behavior,windows,specific,workarounds,remember,rules +X-KDE-Keywords[ko]=size,position,state,window behavior,windows,specific,workarounds,remember,rules,크기,위치,창 행동,창,창 지정,규칙 +X-KDE-Keywords[lt]=dydis,pozicija,vieta,būsena,busena,lango elgsena,langų elgsena,langu elgsena,langas,langai,apėjimas,apejimas,apėjimai,apejimai,specifiniai,tam tikri,tam tikros,tam tikrų,tam tikru,įsiminti,isiminti,atsiminti,prisiminti,taisyklės,taisykles +X-KDE-Keywords[nb]=størrelse,plassering,vindusoppførsel,vindu,bestemt,løsninger,husk,regler +X-KDE-Keywords[nds]=Grött,Positschoon,Tostand,Finsterbedregen,Finstern,besünner,Ümto,wohren,Regeln +X-KDE-Keywords[nl]=grootte,positie,status,venstergedrag,vensters,specifiek,er omheen gewerkt,herinneren,regels +X-KDE-Keywords[nn]=storleik,plassering,tilstand,vindaugsåtferd,vindauge,einskild,løysingar,unntak,hugs,reglar +X-KDE-Keywords[pl]=rozmiar,pozycja,stan,zachowanie okna,okna,specyficzne,obejścia,zapamiętaj,reguły +X-KDE-Keywords[pt]=tamanho,posição,estado,comportamento da janela,janelas,específico,alternativas,recordar,regras +X-KDE-Keywords[pt_BR]=tamanho,posição,estado,comportamento da janela,janelas,específico,alternativas,lembrar,regras +X-KDE-Keywords[ro]=dimensiune,poziție,stare,comportament ferestre,ferestre,specific,ține minte,reguli +X-KDE-Keywords[ru]=size,position,state,window behavior,windows,specific,workarounds,remember,rules,размер,позиция,состояние,поведение окон,окна,специальный,специальные возможности,запомнить,правила +X-KDE-Keywords[sk]=veľkosť,poloha,stav,správanie okien,okná,špecifický,workaroundy,pamätať,pravidlá +X-KDE-Keywords[sl]=velikost,položaj,stanje,obnašanje oken,okna,določeno,popravki,pomnjenje,pravila +X-KDE-Keywords[sr]=size,position,state,window behavior,windows,specific,workarounds,remember,rules,величина,положај,стање,понашање прозора,прозор,заобилазак,запамти,правила +X-KDE-Keywords[sr@ijekavian]=size,position,state,window behavior,windows,specific,workarounds,remember,rules,величина,положај,стање,понашање прозора,прозор,заобилазак,запамти,правила +X-KDE-Keywords[sr@ijekavianlatin]=size,position,state,window behavior,windows,specific,workarounds,remember,rules,veličina,položaj,stanje,ponašanje prozora,prozor,zaobilazak,zapamti,pravila +X-KDE-Keywords[sr@latin]=size,position,state,window behavior,windows,specific,workarounds,remember,rules,veličina,položaj,stanje,ponašanje prozora,prozor,zaobilazak,zapamti,pravila +X-KDE-Keywords[sv]=storlek,position,tillstånd,fönsterbeteende,fönster,specifik,kom ihåg,regler +X-KDE-Keywords[tr]=boyut,konum,durum,pencere davranışı,pencere,özel,etrafından dolanmalar,anımsa,kurallar +X-KDE-Keywords[uk]=size,position,state,window behavior,windows,specific,workarounds,remember,rules,розмір,розташування,місце,стан,поведінка,вікно,вікна,поведінка вікон,окрема,специфічна,окремо,запам’ятати,пам’ять,правило,правила +X-KDE-Keywords[x-test]=xxsizexx,xxpositionxx,xxstatexx,xxwindow behaviorxx,xxwindowsxx,xxspecificxx,xxworkaroundsxx,xxrememberxx,xxrulesxx +X-KDE-Keywords[zh_CN]=size,position,state,window behavior,windows,specific,workarounds,remember,rules,大小,位置,窗口行为,特定,记住,规则 +X-KDE-Keywords[zh_TW]=size,position,state,window behavior,windows,specific,workarounds,remember,rules diff --git a/kcmkwin/kwinrules/kcmrules.cpp b/kcmkwin/kwinrules/kcmrules.cpp new file mode 100644 index 0000000..9e6cd71 --- /dev/null +++ b/kcmkwin/kwinrules/kcmrules.cpp @@ -0,0 +1,240 @@ +/* + SPDX-FileCopyrightText: 2004 Lubos Lunak + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "kcmrules.h" + +#include +#include + +#include +#include +#include +#include + + +namespace KWin +{ + +KCMKWinRules::KCMKWinRules(QObject *parent, const QVariantList &arguments) + : KQuickAddons::ConfigModule(parent, arguments) + , m_ruleBookModel(new RuleBookModel(this)) + , m_rulesModel(new RulesModel(this)) +{ + auto about = new KAboutData(QStringLiteral("kcm_kwinrules"), + i18n("Window Rules"), + QStringLiteral("1.0"), + QString(), + KAboutLicense::GPL); + about->addAuthor(i18n("Ismael Asensio"), + i18n("Author"), + QStringLiteral("isma.af@gmail.com")); + setAboutData(about); + + setQuickHelp(i18n("

Window-specific Settings

Here you can customize window settings specifically only" + " for some windows.

" + "

Please note that this configuration will not take effect if you do not use" + " KWin as your window manager. If you do use a different window manager, please refer to its documentation" + " for how to customize window behavior.

")); + + connect(m_rulesModel, &RulesModel::descriptionChanged, this, [this]{ + if (m_editIndex.isValid()) { + m_ruleBookModel->setDescriptionAt(m_editIndex.row(), m_rulesModel->description()); + } + } ); + connect(m_rulesModel, &RulesModel::dataChanged, this, &KCMKWinRules::updateNeedsSave); + connect(m_ruleBookModel, &RulesModel::dataChanged, this, &KCMKWinRules::updateNeedsSave); +} + +void KCMKWinRules::load() +{ + m_ruleBookModel->load(); + + m_editIndex = QModelIndex(); + emit editIndexChanged(); + + setNeedsSave(false); +} + +void KCMKWinRules::save() +{ + saveCurrentRule(); + + m_ruleBookModel->save(); + + // Notify kwin to reload configuration + QDBusMessage message = QDBusMessage::createSignal("/KWin", "org.kde.KWin", "reloadConfig"); + QDBusConnection::sessionBus().send(message); +} + +void KCMKWinRules::updateNeedsSave() +{ + setNeedsSave(true); + emit needsSaveChanged(); +} + +void KCMKWinRules::saveCurrentRule() +{ + if (m_editIndex.isValid() && needsSave()) { + m_ruleBookModel->setRuleAt(m_editIndex.row(), m_rulesModel->exportToRules()); + } +} + +int KCMKWinRules::editIndex() const +{ + if (!m_editIndex.isValid()) { + return -1; + } + return m_editIndex.row(); +} + + +void KCMKWinRules::setRuleDescription(int index, const QString &description) +{ + if (index < 0 || index >= m_ruleBookModel->rowCount()) { + return; + } + + if (m_editIndex.row() == index) { + m_rulesModel->setDescription(description); + return; + } + m_ruleBookModel->setDescriptionAt(index, description); + + updateNeedsSave(); +} + + +void KCMKWinRules::editRule(int index) +{ + if (index < 0 || index >= m_ruleBookModel->rowCount()) { + return; + } + saveCurrentRule(); + + m_editIndex = m_ruleBookModel->index(index); + emit editIndexChanged(); + + m_rulesModel->importFromRules(m_ruleBookModel->ruleAt(m_editIndex.row())); + + // Set the active page to rules editor (0:RulesList, 1:RulesEditor) + setCurrentIndex(1); +} + +void KCMKWinRules::createRule() +{ + const int newIndex = m_ruleBookModel->rowCount(); + m_ruleBookModel->insertRow(newIndex); + + updateNeedsSave(); + + editRule(newIndex); +} + +void KCMKWinRules::removeRule(int index) +{ + if (index < 0 || index >= m_ruleBookModel->rowCount()) { + return; + } + + m_ruleBookModel->removeRow(index); + + emit editIndexChanged(); + updateNeedsSave(); +} + +void KCMKWinRules::moveRule(int sourceIndex, int destIndex) +{ + const int lastIndex = m_ruleBookModel->rowCount() - 1; + if (sourceIndex == destIndex + || (sourceIndex < 0 || sourceIndex > lastIndex) + || (destIndex < 0 || destIndex > lastIndex)) { + return; + } + + m_ruleBookModel->moveRow(QModelIndex(), sourceIndex, QModelIndex(), destIndex); + + emit editIndexChanged(); + updateNeedsSave(); +} + +void KCMKWinRules::exportToFile(const QUrl &path, const QList &indexes) +{ + if (indexes.isEmpty()) { + return; + } + saveCurrentRule(); + + const auto config = KSharedConfig::openConfig(path.toLocalFile(), KConfig::SimpleConfig); + + for (const QString &groupName : config->groupList()) { + config->deleteGroup(groupName); + } + + for (int index : indexes) { + if (index < 0 || index > m_ruleBookModel->rowCount()) { + continue; + } + const Rules *rule = m_ruleBookModel->ruleAt(index); + RuleSettings settings(config, rule->description); + settings.setDefaults(); + rule->write(&settings); + settings.save(); + } +} + +void KCMKWinRules::importFromFile(const QUrl &path) +{ + const auto config = KSharedConfig::openConfig(path.toLocalFile(), KConfig::SimpleConfig); + const QStringList groups = config->groupList(); + if (groups.isEmpty()) { + return; + } + + for (const QString &groupName : groups) { + RuleSettings settings(config, groupName); + + const bool remove = settings.deleteRule(); + const QString importDescription = settings.description(); + if (importDescription.isEmpty()) { + continue; + } + + // Try to find a rule with the same description to replace + int newIndex = -2; + for (int index = 0; index < m_ruleBookModel->rowCount(); index++) { + if (m_ruleBookModel->descriptionAt(index) == importDescription) { + newIndex = index; + break; + } + } + + if (remove) { + m_ruleBookModel->removeRow(newIndex); + continue; + } + + if (newIndex < 0) { + newIndex = m_ruleBookModel->rowCount(); + m_ruleBookModel->insertRow(newIndex); + } + + m_ruleBookModel->setRuleAt(newIndex, new Rules(&settings)); + + // Reset rule editor if the current rule changed when importing + if (m_editIndex.row() == newIndex) { + m_rulesModel->importFromRules(m_ruleBookModel->ruleAt(m_editIndex.row())); + } + } + + updateNeedsSave(); +} + +K_PLUGIN_CLASS_WITH_JSON(KCMKWinRules, "kcm_kwinrules.json"); + +} // namespace + +#include "kcmrules.moc" diff --git a/kcmkwin/kwinrules/kcmrules.h b/kcmkwin/kwinrules/kcmrules.h new file mode 100644 index 0000000..d7a1ae2 --- /dev/null +++ b/kcmkwin/kwinrules/kcmrules.h @@ -0,0 +1,61 @@ +/* + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include +#include "rulebookmodel.h" +#include "rulesmodel.h" + +#include + + +namespace KWin +{ + +class KCMKWinRules : public KQuickAddons::ConfigModule +{ + Q_OBJECT + + Q_PROPERTY(RuleBookModel *ruleBookModel MEMBER m_ruleBookModel CONSTANT) + Q_PROPERTY(RulesModel *rulesModel MEMBER m_rulesModel CONSTANT) + Q_PROPERTY(int editIndex READ editIndex NOTIFY editIndexChanged) + +public: + explicit KCMKWinRules(QObject *parent, const QVariantList &arguments); + + Q_INVOKABLE void setRuleDescription(int index, const QString &description); + Q_INVOKABLE void editRule(int index); + + Q_INVOKABLE void createRule(); + Q_INVOKABLE void removeRule(int index); + Q_INVOKABLE void moveRule(int sourceIndex, int destIndex); + + Q_INVOKABLE void exportToFile(const QUrl &path, const QList &indexes); + Q_INVOKABLE void importFromFile(const QUrl &path); + +public slots: + void load() override; + void save() override; + +signals: + void editIndexChanged(); + +private slots: + void updateNeedsSave(); + +private: + int editIndex() const; + void saveCurrentRule(); + +private: + RuleBookModel *m_ruleBookModel; + RulesModel* m_rulesModel; + + QPersistentModelIndex m_editIndex; +}; + +} // namespace diff --git a/kcmkwin/kwinrules/kwinsrc.cpp b/kcmkwin/kwinrules/kwinsrc.cpp new file mode 100644 index 0000000..166a891 --- /dev/null +++ b/kcmkwin/kwinrules/kwinsrc.cpp @@ -0,0 +1,33 @@ +/* + SPDX-FileCopyrightText: 2004 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +KWin::InputRedirection *KWin::InputRedirection::s_self = nullptr; + +Qt::MouseButtons KWin::InputRedirection::qtButtonStates() const +{ + return Qt::NoButton; +} + +Qt::KeyboardModifiers KWin::InputRedirection::keyboardModifiers() const +{ + return Qt::NoModifier; +} + +void KWin::InputRedirection::warpPointer(const QPointF&) +{ +} + +bool KWin::InputRedirection::supportsPointerWarping() const +{ + return false; +} + +QPointF KWin::InputRedirection::globalPointer() const +{ + return QPointF(); +} diff --git a/kcmkwin/kwinrules/main.cpp b/kcmkwin/kwinrules/main.cpp new file mode 100644 index 0000000..75bea63 --- /dev/null +++ b/kcmkwin/kwinrules/main.cpp @@ -0,0 +1,241 @@ +/* + SPDX-FileCopyrightText: 2004 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include +#include +#include +#include + +#include "rulebooksettings.h" +#include "rulesdialog.h" +#include "../../rules.h" +#include + +#include +#include +#include +#include +#include + +Q_DECLARE_METATYPE(NET::WindowType) + +namespace KWin +{ + +static Rules *findRule(const QVector &rules, const QVariantMap &data, bool whole_app) +{ + QByteArray wmclass_class = data.value("resourceClass").toByteArray().toLower(); + QByteArray wmclass_name = data.value("resourceName").toByteArray().toLower(); + QByteArray role = data.value("role").toByteArray().toLower(); + NET::WindowType type = data.value("type").value(); + QString title = data.value("caption").toString(); + QByteArray machine = data.value("clientMachine").toByteArray(); + Rules* best_match = nullptr; + int match_quality = 0; + for (const auto rule : rules) { + // try to find an exact match, i.e. not a generic rule + int quality = 0; + bool generic = true; + if (rule->wmclassmatch != Rules::ExactMatch) + continue; // too generic + if (!rule->matchWMClass(wmclass_class, wmclass_name)) + continue; + // from now on, it matches the app - now try to match for a specific window + if (rule->wmclasscomplete) { + quality += 1; + generic = false; // this can be considered specific enough (old X apps) + } + if (!whole_app) { + if (rule->windowrolematch != Rules::UnimportantMatch) { + quality += rule->windowrolematch == Rules::ExactMatch ? 5 : 1; + generic = false; + } + if (rule->titlematch != Rules::UnimportantMatch) { + quality += rule->titlematch == Rules::ExactMatch ? 3 : 1; + generic = false; + } + if (rule->types != NET::AllTypesMask) { + int bits = 0; + for (unsigned int bit = 1; + bit < 1U << 31; + bit <<= 1) + if (rule->types & bit) + ++bits; + if (bits == 1) + quality += 2; + } + if (generic) // ignore generic rules, use only the ones that are for this window + continue; + } else { + if (rule->types == NET::AllTypesMask) + quality += 2; + } + if (!rule->matchType(type) + || !rule->matchRole(role) + || !rule->matchTitle(title) + || !rule->matchClientMachine(machine, data.value("localhost").toBool())) + continue; + if (quality > match_quality) { + best_match = rule; + match_quality = quality; + } + } + if (best_match != nullptr) + return best_match; + Rules* ret = new Rules; + if (whole_app) { + ret->description = i18n("Application settings for %1", QString::fromLatin1(wmclass_class)); + // TODO maybe exclude some types? If yes, then also exclude them above + // when searching. + ret->types = NET::AllTypesMask; + ret->titlematch = Rules::UnimportantMatch; + ret->clientmachine = machine; // set, but make unimportant + ret->clientmachinematch = Rules::UnimportantMatch; + ret->windowrolematch = Rules::UnimportantMatch; + if (wmclass_name == wmclass_class) { + ret->wmclasscomplete = false; + ret->wmclass = wmclass_class; + ret->wmclassmatch = Rules::ExactMatch; + } else { + // WM_CLASS components differ - perhaps the app got -name argument + ret->wmclasscomplete = true; + ret->wmclass = wmclass_name + ' ' + wmclass_class; + ret->wmclassmatch = Rules::ExactMatch; + } + return ret; + } + ret->description = i18n("Window settings for %1", QString::fromLatin1(wmclass_class)); + if (type == NET::Unknown) + ret->types = NET::NormalMask; + else + ret->types = NET::WindowTypeMask( 1 << type); // convert type to its mask + ret->title = title; // set, but make unimportant + ret->titlematch = Rules::UnimportantMatch; + ret->clientmachine = machine; // set, but make unimportant + ret->clientmachinematch = Rules::UnimportantMatch; + if (!role.isEmpty() + && role != "unknown" && role != "unnamed") { // Qt sets this if not specified + ret->windowrole = role; + ret->windowrolematch = Rules::ExactMatch; + if (wmclass_name == wmclass_class) { + ret->wmclasscomplete = false; + ret->wmclass = wmclass_class; + ret->wmclassmatch = Rules::ExactMatch; + } else { + // WM_CLASS components differ - perhaps the app got -name argument + ret->wmclasscomplete = true; + ret->wmclass = wmclass_name + ' ' + wmclass_class; + ret->wmclassmatch = Rules::ExactMatch; + } + } else { // no role set + if (wmclass_name != wmclass_class) { + ret->wmclasscomplete = true; + ret->wmclass = wmclass_name + ' ' + wmclass_class; + ret->wmclassmatch = Rules::ExactMatch; + } else { + // This is a window that has no role set, and both components of WM_CLASS + // match (possibly only differing in case), which most likely means either + // the application doesn't give a damn about distinguishing its various + // windows, or it's an app that uses role for that, but this window + // lacks it for some reason. Use non-complete WM_CLASS matching, also + // include window title in the matching, and pray it causes many more positive + // matches than negative matches. + ret->titlematch = Rules::ExactMatch; + ret->wmclasscomplete = false; + ret->wmclass = wmclass_class; + ret->wmclassmatch = Rules::ExactMatch; + } + } + return ret; +} + +static void edit(const QVariantMap &data, bool whole_app) +{ + RuleBookSettings settings(KConfig::NoGlobals); + QVector rules = settings.rules(); + Rules *orig_rule = findRule(rules, data, whole_app); + RulesDialog dlg; + if (whole_app) + dlg.setWindowTitle(i18nc("Window caption for the application wide rules dialog", "Edit Application-Specific Settings")); + // dlg.edit() creates new Rules instance if edited + Rules* edited_rule = dlg.edit(orig_rule, data, true); + if (edited_rule == nullptr || edited_rule->isEmpty()) { + rules.removeAll(orig_rule); + delete orig_rule; + if (orig_rule != edited_rule) + delete edited_rule; + } else if (edited_rule != orig_rule) { + int pos = rules.indexOf(orig_rule); + if (pos != -1) + rules[ pos ] = edited_rule; + else + rules.prepend(edited_rule); + delete orig_rule; + } + settings.setRules(rules); + settings.save(); + // Send signal to all kwin instances + QDBusMessage message = + QDBusMessage::createSignal("/KWin", "org.kde.KWin", "reloadConfig"); + QDBusConnection::sessionBus().send(message); + qApp->quit(); +} + +} // namespace + +int main(int argc, char* argv[]) +{ + QApplication app(argc, argv); + + KLocalizedString::setApplicationDomain("kcm_kwinrules"); + + app.setAttribute(Qt::AA_UseHighDpiPixmaps, true); + app.setApplicationDisplayName(i18n("KWin")); + app.setApplicationName("kwin_rules_dialog"); + app.setApplicationVersion("1.0"); + bool whole_app = false; + QUuid uuid; + { + QCommandLineParser parser; + parser.setApplicationDescription(i18n("KWin helper utility")); + parser.addOption(QCommandLineOption("uuid", i18n("KWin id of the window for special window settings."), "uuid")); + parser.addOption(QCommandLineOption("whole-app", i18n("Whether the settings should affect all windows of the application."))); + parser.process(app); + + uuid = QUuid::fromString(parser.value("uuid")); + whole_app = parser.isSet("whole-app"); + } + + + if (uuid.isNull()) { + printf("%s\n", qPrintable(i18n("This helper utility is not supposed to be called directly."))); + return 1; + } + QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.KWin"), + QStringLiteral("/KWin"), + QStringLiteral("org.kde.KWin"), + QStringLiteral("getWindowInfo")); + message.setArguments({uuid.toString()}); + QDBusPendingReply async = QDBusConnection::sessionBus().asyncCall(message); + + QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(async, &app); + QObject::connect(callWatcher, &QDBusPendingCallWatcher::finished, &app, + [&whole_app] (QDBusPendingCallWatcher *self) { + QDBusPendingReply reply = *self; + self->deleteLater(); + if (!reply.isValid() || reply.value().isEmpty()) { + qApp->quit(); + return; + } + KWin::edit(reply.value(), whole_app); + } + ); + + + + return app.exec(); +} diff --git a/kcmkwin/kwinrules/optionsmodel.cpp b/kcmkwin/kwinrules/optionsmodel.cpp new file mode 100644 index 0000000..f183601 --- /dev/null +++ b/kcmkwin/kwinrules/optionsmodel.cpp @@ -0,0 +1,196 @@ +/* + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "optionsmodel.h" + +#include + + +namespace KWin +{ + +QHash OptionsModel::roleNames() const +{ + return { + {Qt::DisplayRole, QByteArrayLiteral("display")}, + {Qt::DecorationRole, QByteArrayLiteral("decoration")}, + {Qt::ToolTipRole, QByteArrayLiteral("tooltip")}, + {Qt::UserRole, QByteArrayLiteral("value")}, + {Qt::UserRole + 1, QByteArrayLiteral("iconName")}, + }; +} + +int OptionsModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + return m_data.size(); +} + +QVariant OptionsModel::data(const QModelIndex &index, int role) const +{ + if (!checkIndex(index, CheckIndexOption::IndexIsValid | CheckIndexOption::ParentIsInvalid)) { + return QVariant(); + } + + const Data data = m_data.at(index.row()); + + switch (role) { + case Qt::DisplayRole: + return data.text; + case Qt::UserRole: + return data.value; + case Qt::DecorationRole: + return data.icon; + case Qt::UserRole + 1: + return data.icon.name(); + case Qt::ToolTipRole: + return data.description; + } + return QVariant(); +} + +int OptionsModel::selectedIndex() const +{ + return m_index; +} + +int OptionsModel::indexOf(QVariant value) const +{ + for (int index = 0; index < m_data.count(); index++) { + if (m_data.at(index).value == value) { + return index; + } + } + return -1; +} + +QString OptionsModel::textOfValue(QVariant value) const +{ + int index = indexOf(value); + if (index < 0 || index >= m_data.count()) { + return QString(); + } + return m_data.at(index).text; +} + +QVariant OptionsModel::value() const +{ + if (m_data.isEmpty()) { + return QVariant(); + } + return m_data.at(m_index).value; +} + +void OptionsModel::setValue(QVariant value) +{ + if (this->value() == value) { + return; + } + int index = indexOf(value); + if (index >= 0 && index != m_index) { + m_index = index; + emit selectedIndexChanged(index); + } +} + +void OptionsModel::resetValue() +{ + m_index = 0; + emit selectedIndexChanged(m_index); +} + +void OptionsModel::updateModelData(const QList &data) { + beginResetModel(); + m_data = data; + endResetModel(); +} + + +RulePolicy::Type RulePolicy::type() const +{ + return m_type; +} + +int RulePolicy::value() const +{ + if (m_type == RulePolicy::NoPolicy) { + return Rules::Apply; // To simplify external checks when rule has no policy + } + return OptionsModel::value().toInt(); +} + +QString RulePolicy::policyKey(const QString &key) const +{ + switch (m_type) { + case NoPolicy: + return QString(); + case StringMatch: + return QStringLiteral("%1match").arg(key); + case SetRule: + case ForceRule: + return QStringLiteral("%1rule").arg(key); + } + + return QString(); +} + +QList RulePolicy::policyOptions(RulePolicy::Type type) +{ + static const auto stringMatchOptions = QList { + {Rules::UnimportantMatch, i18n("Unimportant")}, + {Rules::ExactMatch, i18n("Exact Match")}, + {Rules::SubstringMatch, i18n("Substring Match")}, + {Rules::RegExpMatch, i18n("Regular Expression")} + }; + + static const auto setRuleOptions = QList { + {Rules::DontAffect, + i18n("Do Not Affect"), + i18n("The window property will not be affected and therefore the default handling for it will be used." + "\nSpecifying this will block more generic window settings from taking effect.")}, + {Rules::Apply, + i18n("Apply Initially"), + i18n("The window property will be only set to the given value after the window is created." + "\nNo further changes will be affected.")}, + {Rules::Remember, + i18n("Remember"), + i18n("The value of the window property will be remembered and, every time the window" + " is created, the last remembered value will be applied.")}, + {Rules::Force, + i18n("Force"), + i18n("The window property will be always forced to the given value.")}, + {Rules::ApplyNow, + i18n("Apply Now"), + i18n("The window property will be set to the given value immediately and will not be affected later" + "\n(this action will be deleted afterwards).")}, + {Rules::ForceTemporarily, + i18n("Force Temporarily"), + i18n("The window property will be forced to the given value until it is hidden" + "\n(this action will be deleted after the window is hidden).")} + }; + + static auto forceRuleOptions = QList { + setRuleOptions.at(0), // Rules::DontAffect + setRuleOptions.at(3), // Rules::Force + setRuleOptions.at(5), // Rules::ForceTemporarily + }; + + switch (type) { + case NoPolicy: + return {}; + case StringMatch: + return stringMatchOptions; + case SetRule: + return setRuleOptions; + case ForceRule: + return forceRuleOptions; + } + return {}; +} + +} //namespace diff --git a/kcmkwin/kwinrules/optionsmodel.h b/kcmkwin/kwinrules/optionsmodel.h new file mode 100644 index 0000000..8609def --- /dev/null +++ b/kcmkwin/kwinrules/optionsmodel.h @@ -0,0 +1,101 @@ +/* + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#ifndef KWIN_OPTIONS_MODEL_H +#define KWIN_OPTIONS_MODEL_H + +#include + +#include +#include +#include + + +namespace KWin { + +class OptionsModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(int selectedIndex READ selectedIndex NOTIFY selectedIndexChanged) + +public: + struct Data { + Data(const QVariant &value, const QString &text, const QIcon &icon = {}, const QString &description = {}) + : value(value) + , text(text) + , icon(icon) + , description(description) + {} + Data(const QVariant &value, const QString &text, const QString &description) + : value(value) + , text(text) + , description(description) + {} + + QVariant value; + QString text; + QIcon icon; + QString description; + }; + +public: + OptionsModel() : QAbstractListModel(), m_data(), m_index(0) {}; + OptionsModel(const QList &data) : QAbstractListModel(), m_data(data), m_index(0) {}; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QHash roleNames() const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + QVariant value() const; + void setValue(QVariant value); + void resetValue(); + + void updateModelData(const QList &data); + + Q_INVOKABLE int indexOf(QVariant value) const; + Q_INVOKABLE QString textOfValue(QVariant value) const; + int selectedIndex() const; + +signals: + void selectedIndexChanged(int index); + +public: + QList m_data; + +protected: + int m_index = 0; +}; + +class RulePolicy : public OptionsModel +{ +public: + enum Type { + NoPolicy, + StringMatch, + SetRule, + ForceRule + }; + +public: + RulePolicy(Type type) + : OptionsModel(policyOptions(type)) + , m_type(type) + {}; + + Type type() const; + int value() const; + QString policyKey(const QString &key) const; + +private: + static QList policyOptions(RulePolicy::Type type); + +private: + Type m_type; +}; + +} //namespace + +#endif //KWIN_OPTIONS_MODEL_H diff --git a/kcmkwin/kwinrules/package/contents/ui/FileDialogLoader.qml b/kcmkwin/kwinrules/package/contents/ui/FileDialogLoader.qml new file mode 100644 index 0000000..7a9c8f1 --- /dev/null +++ b/kcmkwin/kwinrules/package/contents/ui/FileDialogLoader.qml @@ -0,0 +1,45 @@ +/* + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.14 +import QtQuick.Dialogs 1.0 as QtDialogs + +Loader { + id: root + active: false + + property string title : i18n("Select File"); + property string lastFolder : "" + property bool isSaveDialog : false + + signal fileSelected(string path) + + sourceComponent: QtDialogs.FileDialog { + id: fileDialog + + title: root.title + selectExisting: !root.isSaveDialog + folder: root.lastFolder || shortcuts.home + nameFilters: [ i18n("KWin Rules (*.kwinrule)") ] + defaultSuffix: "*.kwinrule" + + Component.onCompleted: { + open(); + } + + onAccepted: { + root.lastFolder = folder; + if (fileUrl != "") { + root.fileSelected(fileUrl); + } + root.active = false; + } + + onRejected: { + root.active = false; + } + } +} diff --git a/kcmkwin/kwinrules/package/contents/ui/OptionsComboBox.qml b/kcmkwin/kwinrules/package/contents/ui/OptionsComboBox.qml new file mode 100644 index 0000000..4445ec9 --- /dev/null +++ b/kcmkwin/kwinrules/package/contents/ui/OptionsComboBox.qml @@ -0,0 +1,94 @@ +/* + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.14 +import QtQuick.Layouts 1.14 +import QtQuick.Controls 2.14 as QQC2 + +import org.kde.kirigami 2.10 as Kirigami + + +QQC2.ComboBox { + id: optionsCombo + + textRole: "display" + valueRole: "value" + + property bool multipleChoice: false + property int selectionMask: 0 + + currentIndex: multipleChoice ? -1 : model.selectedIndex + + displayText: { + if (!multipleChoice) { + return currentText; + } + var selectionCount = selectionMask.toString(2).replace(/0/g, '').length; + switch (selectionCount) { + case 0: + return i18n("None selected"); + case 1: + var selectedValue = selectionMask.toString(2).length - 1; + return model.textOfValue(selectedValue); + case count: + return i18n("All selected"); + } + return i18np("%1 selected", "%1 selected", selectionCount); + } + + delegate: QQC2.ItemDelegate { + highlighted: optionsCombo.highlightedIndex == index + width: parent.width + + contentItem: RowLayout { + QQC2.CheckBox { + id: itemSelection + visible: multipleChoice + checked: (selectionMask & (1 << value)) + onToggled: { + selectionMask = (selectionMask & ~(1 << value)) | (checked << value); + activated(index); + } + } + Kirigami.Icon { + source: model.decoration + Layout.preferredHeight: Kirigami.Units.iconSizes.small + Layout.preferredWidth: Kirigami.Units.iconSizes.small + } + QQC2.Label { + text: model.display + color: highlighted ? Kirigami.Theme.highlightedTextColor : Kirigami.Theme.textColor + Layout.fillWidth: true + horizontalAlignment: Text.AlignLeft + } + } + + MouseArea { + anchors.fill: contentItem + enabled: multipleChoice + onClicked: { + itemSelection.toggle(); + itemSelection.toggled(); + } + } + + QQC2.ToolTip { + text: model.tooltip + visible: hovered && (model.tooltip.length > 0) + } + + Component.onCompleted: { + //FIXME: work around bug https://bugs.kde.org/show_bug.cgi?id=403153 + optionsCombo.popup.width = Math.max(implicitWidth, optionsCombo.width, optionsCombo.popup.width); + } + + onActiveFocusChanged: { + if (!activeFocus) { + popup.close(); + } + } + } +} diff --git a/kcmkwin/kwinrules/package/contents/ui/RuleItemDelegate.qml b/kcmkwin/kwinrules/package/contents/ui/RuleItemDelegate.qml new file mode 100644 index 0000000..9f0ba1c --- /dev/null +++ b/kcmkwin/kwinrules/package/contents/ui/RuleItemDelegate.qml @@ -0,0 +1,98 @@ +/* + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.14 +import QtQuick.Layouts 1.14 +import QtQuick.Controls 2.14 as QQC2 +import org.kde.kirigami 2.10 as Kirigami + +Kirigami.AbstractListItem { + id: ruleDelegate + + property bool ruleEnabled: model.enabled + + Kirigami.Theme.colorSet: Kirigami.Theme.View + + width: ListView.view.width + highlighted: false + hoverEnabled: false + + RowLayout { + + Kirigami.Icon { + id: itemIcon + source: model.icon + Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium + Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium + Layout.rightMargin: Kirigami.Units.smallSpacing + Layout.alignment: Qt.AlignVCenter + } + + QQC2.Label { + text: model.name + horizontalAlignment: Text.AlignLeft + elide: Text.ElideRight + Layout.preferredWidth: 10 * Kirigami.Units.gridUnit + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + } + + RowLayout { + // This layout keeps the width constant between delegates, independent of items visibility + Layout.fillWidth: true + Layout.preferredWidth: 20 * Kirigami.Units.gridUnit + Layout.minimumWidth: 13 * Kirigami.Units.gridUnit + + OptionsComboBox { + id: policyCombo + Layout.preferredWidth: 50 // 50% + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + flat: true + + visible: count > 0 + enabled: ruleEnabled + + model: policyModel + onActivated: { + policy = currentValue; + } + } + + ValueEditor { + id: valueEditor + Layout.preferredWidth: 50 // 50% + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + + enabled: model.enabled + + ruleValue: model.value + ruleOptions: model.options + controlType: model.type + + onValueEdited: (value) => { + model.value = value; + } + } + + QQC2.ToolButton { + id: itemEnabled + icon.name: "edit-delete" + visible: model.selectable + Layout.alignment: Qt.AlignVCenter + onClicked: { + model.enabled = false; + } + } + } + + QQC2.ToolTip { + text: model.description + visible: hovered && (text.length > 0) + } + } +} diff --git a/kcmkwin/kwinrules/package/contents/ui/RulesEditor.qml b/kcmkwin/kwinrules/package/contents/ui/RulesEditor.qml new file mode 100644 index 0000000..74bac1f --- /dev/null +++ b/kcmkwin/kwinrules/package/contents/ui/RulesEditor.qml @@ -0,0 +1,270 @@ +/* + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.14 +import QtQuick.Layouts 1.14 +import QtQuick.Controls 2.14 as QQC2 +import org.kde.kirigami 2.12 as Kirigami +import org.kde.kcm 1.2 +import org.kde.kitemmodels 1.0 +import org.kde.kcms.kwinrules 1.0 + + +ScrollViewKCM { + id: rulesEditor + + property var rulesModel: kcm.rulesModel + + title: rulesModel.description + + view: ListView { + id: rulesView + clip: true + + model: enabledRulesModel + delegate: RuleItemDelegate {} + section { + property: "section" + delegate: Kirigami.ListSectionHeader { label: section } + } + + Kirigami.PlaceholderMessage { + id: hintArea + visible: rulesView.count <= 4 + anchors { + // We need to center on the free space below contentItem, not the full ListView. + // Setting both top and bottom anchors (or using anchors.fill) stretches the component + // and distorts the spacing between its internal items. + // This is fine as long as we have a single item here. + horizontalCenter: parent.horizontalCenter + top: parent.contentItem.bottom + bottom: parent.bottom + } + width: parent.width - (units.largeSpacing * 4) + helpfulAction: QQC2.Action { + text: i18n("Add Properties...") + icon.name: "list-add-symbolic" + onTriggered: { + propertySheet.open(); + } + } + } + } + + // FIXME: InlineMessage.qml:241:13: QML Label: Binding loop detected for property "verticalAlignment" + header: Kirigami.InlineMessage { + Layout.fillWidth: true + Layout.fillHeight: true + text: rulesModel.warningMessage + visible: text != "" + } + + footer: RowLayout { + QQC2.Button { + text: checked ? i18n("Close") : i18n("Add Properties...") + icon.name: checked ? "dialog-close" : "list-add-symbolic" + checkable: true + checked: propertySheet.sheetOpen + visible: !hintArea.visible || checked + onToggled: { + propertySheet.sheetOpen = checked; + } + } + Item { + Layout.fillWidth: true + } + QQC2.Button { + text: i18n("Detect Window Properties") + icon.name: "edit-find" + onClicked: { + overlayModel.onlySuggestions = true; + rulesModel.detectWindowProperties(delaySpin.value); + } + } + QQC2.SpinBox { + id: delaySpin + Layout.preferredWidth: Kirigami.Units.gridUnit * 8 + from: 0 + to: 30 + textFromValue: (value, locale) => { + return (value == 0) ? i18n("Instantly") + : i18np("After %1 second", "After %1 seconds", value) + } + } + + } + + Connections { + target: rulesModel + onSuggestionsChanged: { + propertySheet.sheetOpen = true; + } + } + + Kirigami.OverlaySheet { + id: propertySheet + + parent: view + + header: Kirigami.Heading { + text: i18n("Select properties") + } + footer: Kirigami.SearchField { + id: searchField + horizontalAlignment: Text.AlignLeft + } + + ListView { + id: overlayView + model: overlayModel + Layout.preferredWidth: Kirigami.Units.gridUnit * 28 + + section { + property: "section" + delegate: Kirigami.ListSectionHeader { label: section } + } + + delegate: Kirigami.AbstractListItem { + id: propertyDelegate + highlighted: false + width: ListView.view.width + + RowLayout { + Kirigami.Icon { + source: model.icon + Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium + Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium + Layout.alignment: Qt.AlignVCenter + } + QQC2.Label { + id: itemNameLabel + text: model.name + horizontalAlignment: Qt.AlignLeft + Layout.preferredWidth: implicitWidth + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + QQC2.ToolTip { + text: model.description + visible: hovered && (model.description.length > 0) + } + } + QQC2.Label { + id: suggestedLabel + text: formatValue(model.suggested, model.type, model.options) + horizontalAlignment: Text.AlignRight + elide: Text.ElideRight + opacity: 0.7 + Layout.maximumWidth: propertyDelegate.width - itemNameLabel.implicitWidth - Kirigami.Units.gridUnit * 6 + Layout.alignment: Qt.AlignVCenter + QQC2.ToolTip { + text: suggestedLabel.text + visible: hovered && suggestedLabel.truncated + } + } + QQC2.ToolButton { + icon.name: (model.enabled) ? "dialog-ok-apply" : "list-add-symbolic" + opacity: propertyDelegate.hovered ? 1 : 0 + onClicked: propertyDelegate.clicked() + Layout.preferredWidth: implicitWidth + Layout.leftMargin: -Kirigami.Units.smallSpacing + Layout.rightMargin: -Kirigami.Units.smallSpacing + Layout.alignment: Qt.AlignVCenter + } + } + + onClicked: { + model.enabled = true; + if (model.suggested != null) { + model.value = model.suggested; + model.suggested = null; + } + } + } + } + + onSheetOpenChanged: { + searchField.text = ""; + if (sheetOpen) { + overlayModel.ready = true; + searchField.forceActiveFocus(); + } else { + overlayModel.onlySuggestions = false; + } + } + } + + function formatValue(value, type, options) { + if (value == null) { + return ""; + } + switch (type) { + case RuleItem.Boolean: + return value ? i18n("Yes") : i18n("No"); + case RuleItem.Percentage: + return i18n("%1 %", value); + case RuleItem.Point: + return i18nc("Coordinates (x, y)", "(%1, %2)", value.x, value.y); + case RuleItem.Size: + return i18nc("Size (width, height)", "(%1, %2)", value.width, value.height); + case RuleItem.Option: + return options.textOfValue(value); + case RuleItem.NetTypes: + var selectedValue = value.toString(2).length - 1; + return options.textOfValue(selectedValue); + } + return value; + } + + KSortFilterProxyModel { + id: enabledRulesModel + sourceModel: rulesModel + filterRowCallback: (source_row, source_parent) => { + var index = sourceModel.index(source_row, 0, source_parent); + return sourceModel.data(index, RulesModel.EnabledRole); + } + } + + KSortFilterProxyModel { + id: overlayModel + sourceModel: rulesModel + + property bool onlySuggestions: false + onOnlySuggestionsChanged: { + invalidateFilter(); + } + + // Delay the model filtering until `ready` is set + // FIXME: Workaround https://bugs.kde.org/show_bug.cgi?id=422289 + property bool ready: false + onReadyChanged: { + invalidateFilter(); + } + + filterString: searchField.text.trim().toLowerCase() + filterRowCallback: (source_row, source_parent) => { + if (!ready) { + return false; + } + + var index = sourceModel.index(source_row, 0, source_parent); + + var hasSuggestion = sourceModel.data(index, RulesModel.SuggestedValueRole) != null; + var isOptional = sourceModel.data(index, RulesModel.SelectableRole); + var isEnabled = sourceModel.data(index, RulesModel.EnabledRole); + + var showItem = hasSuggestion || (!onlySuggestions && isOptional && !isEnabled); + + if (!showItem) { + return false; + } + if (filterString.length > 0) { + return sourceModel.data(index, RulesModel.NameRole).toLowerCase().includes(filterString) + } + return true; + } + } + +} diff --git a/kcmkwin/kwinrules/package/contents/ui/RulesList.qml b/kcmkwin/kwinrules/package/contents/ui/RulesList.qml new file mode 100644 index 0000000..d2c5d41 --- /dev/null +++ b/kcmkwin/kwinrules/package/contents/ui/RulesList.qml @@ -0,0 +1,243 @@ +/* + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.14 +import QtQuick.Layouts 1.14 +import QtQuick.Controls 2.14 as QQC2 +import QtQml.Models 2.14 +import org.kde.kcm 1.2 +import org.kde.kirigami 2.12 as Kirigami + +ScrollViewKCM { + id: rulesListKCM + + // FIXME: ScrollViewKCM.qml:73:13: QML Control: Binding loop detected for property "implicitHeight" + implicitWidth: Kirigami.Units.gridUnit * 35 + implicitHeight: Kirigami.Units.gridUnit * 25 + + ConfigModule.columnWidth: Kirigami.Units.gridUnit * 23 + ConfigModule.buttons: ConfigModule.Apply + + property var selectedIndexes: [] + + // Manage KCM pages + Connections { + target: kcm + onEditIndexChanged: { + if (kcm.editIndex < 0) { + // If no rule is being edited, hide RulesEdidor page + kcm.pop(); + } else if (kcm.depth < 2) { + // Add the RulesEditor page if it wasn't already + kcm.push("RulesEditor.qml"); + } + } + } + + view: ListView { + id: ruleBookView + clip: true + + model: kcm.ruleBookModel + currentIndex: kcm.editIndex + delegate: Kirigami.DelegateRecycler { + width: ruleBookView.width + sourceComponent: ruleBookDelegate + } + + highlightMoveDuration: Kirigami.Units.longDuration + + displaced: Transition { + NumberAnimation { properties: "y"; duration: Kirigami.Units.longDuration } + } + + Kirigami.PlaceholderMessage { + visible: ruleBookView.count === 0 + anchors.centerIn: parent + width: parent.width - (units.largeSpacing * 4) + text: i18n("No rules for specific windows are currently set"); + } + } + + header: Kirigami.InlineMessage { + id: exportInfo + icon.source: "document-export" + showCloseButton: true + text: i18n("Select the rules to export") + actions: [ + Kirigami.Action { + iconName: "object-select-symbolic" + text: checked ? i18n("Unselect All") : i18n("Select All") + checkable: true + checked: selectedIndexes.length === ruleBookView.count + onToggled: { + if (checked) { + selectedIndexes = [...Array(ruleBookView.count).keys()] + } else { + selectedIndexes = []; + } + } + } + , + Kirigami.Action { + iconName: "document-save" + text: i18n("Save Rules") + enabled: selectedIndexes.length > 0 + onTriggered: { + exportDialog.active = true; + } + } + ] + } + + footer: RowLayout { + QQC2.Button { + text: i18n("Add New...") + icon.name: "list-add-symbolic" + enabled: !exportInfo.visible + onClicked: { + kcm.createRule(); + } + } + Item { + Layout.fillWidth: true + } + QQC2.Button { + text: i18n("Import...") + icon.name: "document-import" + enabled: !exportInfo.visible + onClicked: { + importDialog.active = true; + } + } + QQC2.Button { + text: checked ? i18n("Cancel Export") : i18n("Export...") + icon.name: exportInfo.visible ? "dialog-cancel" : "document-export" + enabled: ruleBookView.count > 0 + checkable: true + checked: exportInfo.visible + onToggled: { + selectedIndexes = [] + exportInfo.visible = checked; + } + } + } + + Component { + id: ruleBookDelegate + Kirigami.SwipeListItem { + id: ruleBookItem + + RowLayout { + //FIXME: If not used within DelegateRecycler, item goes on top of the first item when clicked + Kirigami.ListItemDragHandle { + visible: !exportInfo.visible + listItem: ruleBookItem + listView: ruleBookView + onMoveRequested: { + kcm.moveRule(oldIndex, newIndex); + } + } + + QQC2.TextField { + id: descriptionField + Layout.minimumWidth: Kirigami.Units.gridUnit * 2 + Layout.fillWidth: true + background: Item {} + horizontalAlignment: Text.AlignLeft + text: model && model.display + onEditingFinished: { + kcm.setRuleDescription(index, text); + } + Keys.onPressed: { + switch (event.key) { + case Qt.Key_Escape: + // On key reset to model data before losing focus + text = model.display; + case Qt.Key_Enter: + case Qt.Key_Return: + case Qt.Key_Tab: + ruleBookItem.forceActiveFocus(); + event.accepted = true; + break; + } + } + + MouseArea { + anchors.fill: parent + enabled: exportInfo.visible + cursorShape: enabled ? Qt.PointingHandCursor : Qt.IBeamCursor + onClicked: { + itemSelectionCheck.toggle(); + itemSelectionCheck.toggled(); + } + } + } + + QQC2.CheckBox { + id: itemSelectionCheck + visible: exportInfo.visible + checked: selectedIndexes.includes(index) + onToggled: { + var position = selectedIndexes.indexOf(index); + if (checked) { + if (position < 0) { selectedIndexes.push(index); } + } else { + if (position >= 0) { selectedIndexes.splice(position, 1); } + } + selectedIndexesChanged(); + } + } + } + + actions: [ + Kirigami.Action { + text: i18n("Edit") + iconName: "edit-entry" + visible: !exportInfo.visible + onTriggered: { + kcm.editRule(index); + } + } + , + Kirigami.Action { + text: i18n("Delete") + iconName: "entry-delete" + visible: !exportInfo.visible + onTriggered: { + kcm.removeRule(index); + } + } + ] + } + } + + FileDialogLoader { + id: importDialog + title: i18n("Import Rules") + isSaveDialog: false + onLastFolderChanged: { + exportDialog.lastFolder = lastFolder; + } + onFileSelected: { + kcm.importFromFile(path); + } + } + + FileDialogLoader { + id: exportDialog + title: i18n("Export Rules") + isSaveDialog: true + onLastFolderChanged: { + importDialog.lastFolder = lastFolder; + } + onFileSelected: { + selectedIndexes.sort(); + kcm.exportToFile(path, selectedIndexes); + exportInfo.visible = false; + } + } +} diff --git a/kcmkwin/kwinrules/package/contents/ui/ValueEditor.qml b/kcmkwin/kwinrules/package/contents/ui/ValueEditor.qml new file mode 100644 index 0000000..100ff40 --- /dev/null +++ b/kcmkwin/kwinrules/package/contents/ui/ValueEditor.qml @@ -0,0 +1,189 @@ +/* + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.14 +import QtQuick.Layouts 1.14 +import QtQuick.Controls 2.14 as QQC2 + +import org.kde.kirigami 2.10 as Kirigami +import org.kde.kquickcontrols 2.0 as KQC +import org.kde.kcms.kwinrules 1.0 + + +Loader { + id: valueEditor + focus: true + + property var ruleValue + property var ruleOptions + property int controlType + + signal valueEdited(var value) + + sourceComponent: { + switch (controlType) { + case RuleItem.Boolean: return booleanEditor + case RuleItem.String: return stringEditor + case RuleItem.Integer: return integerEditor + case RuleItem.Option: return optionEditor + case RuleItem.NetTypes: return netTypesEditor + case RuleItem.Percentage: return percentageEditor + case RuleItem.Point: return coordinateEditor + case RuleItem.Size: return coordinateEditor + case RuleItem.Shortcut: return shortcutEditor + default: return emptyEditor + } + } + + Component { + id: emptyEditor + Item {} + } + + Component { + id: booleanEditor + RowLayout { + Item { + Layout.fillWidth: true + } + QQC2.RadioButton { + text: i18n("Yes") + checked: ruleValue + Layout.margins: Kirigami.Units.smallSpacing + onToggled: valueEditor.valueEdited(checked) + } + QQC2.RadioButton { + text: i18n("No") + checked: !ruleValue + Layout.margins: Kirigami.Units.smallSpacing + onToggled: valueEditor.valueEdited(!checked) + } + } + } + + Component { + id: stringEditor + QQC2.TextField { + property bool isTextEdited: false + text: ruleValue + horizontalAlignment: Text.AlignLeft + onTextEdited: { isTextEdited = true; } + onEditingFinished: { + if (isTextEdited) { valueEditor.valueEdited(text); } + isTextEdited = false; + } + } + } + + Component { + id: integerEditor + QQC2.SpinBox { + editable: true + value: ruleValue + onValueModified: valueEditor.valueEdited(value) + } + } + + Component { + id: optionEditor + OptionsComboBox { + flat: true + model: ruleOptions + onActivated: (index) => { + valueEditor.valueEdited(currentValue); + } + } + } + + Component { + id: netTypesEditor + OptionsComboBox { + flat: true + model: ruleOptions + multipleChoice: true + // Filter the provided value with the options mask + selectionMask: ruleValue & optionsMask + onActivated: { + valueEditor.valueEdited(selectionMask); + } + } + } + + Component { + id: percentageEditor + RowLayout { + QQC2.Slider { + id: slider + Layout.fillWidth: true + from: 0 + to: 100 + value: ruleValue + onMoved: valueEditor.valueEdited(Math.round(slider.value)) + } + QQC2.Label { + text: i18n("%1 %", Math.round(slider.value)) + horizontalAlignment: Qt.AlignRight + Layout.minimumWidth: maxPercentage.width + Kirigami.Units.smallSpacing + Layout.margins: Kirigami.Units.smallSpacing + } + TextMetrics { + id: maxPercentage + text: i18n("%1 %", 100) + } + } + } + + Component { + id: coordinateEditor + RowLayout { + id: coordItem + spacing: Kirigami.Units.smallSpacing + + readonly property bool isSize: controlType == RuleItem.Size + readonly property var coord: (isSize) ? Qt.size(coordX.value, coordY.value) + : Qt.point(coordX.value, coordY.value) + onCoordChanged: valueEditor.valueEdited(coord) + + QQC2.SpinBox { + id: coordX + editable: true + Layout.preferredWidth: 50 // 50% + Layout.fillWidth: true + from: (isSize) ? 0 : -32767 + to: 32767 + value: (isSize) ? ruleValue.width : ruleValue.x + } + QQC2.Label { + id: coordSeparator + Layout.preferredWidth: implicitWidth + text: i18nc("(x, y) coordinates separator in size/position","x") + horizontalAlignment: Text.AlignHCenter + } + QQC2.SpinBox { + id: coordY + editable: true + from: coordX.from + to: coordX.to + Layout.preferredWidth: 50 // 50% + Layout.fillWidth: true + value: (isSize) ? ruleValue.height : ruleValue.y + } + } + } + + Component { + id: shortcutEditor + RowLayout { + Item { + Layout.fillWidth: true + } + KQC.KeySequenceItem { + keySequence: ruleValue + onCaptureFinished: valueEditor.valueEdited(keySequence) + } + } + } +} diff --git a/kcmkwin/kwinrules/package/metadata.desktop b/kcmkwin/kwinrules/package/metadata.desktop new file mode 100644 index 0000000..17c6aed --- /dev/null +++ b/kcmkwin/kwinrules/package/metadata.desktop @@ -0,0 +1,125 @@ +[Desktop Entry] +Icon=preferences-system-windows-actions +Type=Service +Keywords= +X-KDE-ParentApp= +X-KDE-System-Settings-Parent-Category=applicationstyle +X-KDE-PluginInfo-Author=Ismael Asensio +X-KDE-PluginInfo-Email=isma.af@gmail.com +X-KDE-PluginInfo-License=GPL-2.0+ +X-KDE-PluginInfo-Name=kcm_kwinrules +X-KDE-PluginInfo-Version= +X-KDE-PluginInfo-Website=https://www.kde.org/plasma-desktop +X-KDE-ServiceTypes=Plasma/Generic +X-Plasma-API=declarativeappletscript +X-Plasma-MainScript=ui/RulesList.qml +X-KDE-FormFactors=desktop,tablet + +Name=Window Rules +Name[ar]=قواعد النوافذ +Name[az]=Pəncərə qaydaları +Name[bg]=Правила за прозорци +Name[bs]=Pravila prozora +Name[ca]=Regles de les finestres +Name[ca@valencia]=Regles de les finestres +Name[cs]=Pravidla oken +Name[da]=Vinduesregler +Name[de]=Fensterregeln +Name[el]=Κανόνες παραθύρου +Name[en_GB]=Window Rules +Name[es]=Reglas de las ventanas +Name[et]=Akna reeglid +Name[eu]=Leihoaren arauak +Name[fi]=Ikkunasäännöt +Name[fr]=Règles de la fenêtre +Name[ga]=Rialacha Fuinneog +Name[gl]=Regras da xanela +Name[gu]=વિન્ડો નિયમો +Name[he]=כללי חלון +Name[hi]=विंडो निय +Name[hr]=Pravila prozora +Name[hu]=Ablakszabályok +Name[ia]=Regulas de fenestra +Name[id]=Peraturan Window +Name[is]=Gluggahegðunarreglur +Name[it]=Regole delle finestre +Name[ja]=ウィンドウルール +Name[kk]=Терезе тәртібі +Name[km]=ក្បួន​បង្អួច +Name[kn]=ವಿಂಡೋ ನಿಯಮಗಳು +Name[ko]=ì°½ 규칙 +Name[lt]=Langų taisyklės +Name[lv]=Loga noteikumi +Name[mr]=चौकट नियम +Name[nb]=Vindusregler +Name[nds]=Finsterbedregen +Name[nl]=Vensterregels +Name[nn]=Vindaugsreglar +Name[pa]=ਵਿੰਡੋ ਨਿਯਮ +Name[pl]=Zasady okien +Name[pt]=Regras das Janelas +Name[pt_BR]=Regras das janelas +Name[ro]=Reguli ferestre +Name[ru]=Особые параметры окон +Name[si]=කවුළු නීති +Name[sk]=Pravidlá okien +Name[sl]=Pravila za okna +Name[sr]=Правила прозора +Name[sr@ijekavian]=Правила прозора +Name[sr@ijekavianlatin]=Pravila prozora +Name[sr@latin]=Pravila prozora +Name[sv]=Fönsterregler +Name[th]=กฎต่าง ๆ ของหน้าต่าง +Name[tr]=Pencere Kuralları +Name[ug]=كۆزنەك بەلگىلىمىسى +Name[uk]=Правила вікон +Name[wa]=Rîles des finiesses +Name[x-test]=xxWindow Rulesxx +Name[zh_CN]=窗口规则 +Name[zh_TW]=視窗規則 +Comment=Individual Window Behavior +Comment[az]=İndividual pəncərə davranışları +Comment[bs]=PonaÅ¡anje pojedinog prozora +Comment[ca]=Comportament individual de les finestres +Comment[ca@valencia]=Comportament individual de les finestres +Comment[cs]=Chování individuálních oken +Comment[da]=Opførsel af enkeltvinduer +Comment[de]=Individuelles Fensterverhalten +Comment[el]=Συμπεριφορά ανεξάρτητου παραθύρου +Comment[en_GB]=Individual Window Behaviour +Comment[es]=Comportamiento de las ventanas individuales +Comment[et]=Konkreetse akna käitumine +Comment[eu]=Leihoen banakako portaera +Comment[fi]=Yksittäisten ikkunoiden toiminta +Comment[fr]=Comportement individuel des fenêtres +Comment[gl]=Comportamento individual das xanelas +Comment[he]=התנהגות חלונות ספציפים +Comment[hu]=Egyéni ablakműveletek +Comment[ia]=Comportamento de fenestra individual +Comment[id]=Perilaku Window Individu +Comment[it]=Comportamento della singola finestra +Comment[ja]=個別のウィンドウの挙動 +Comment[ko]=개별 ì°½ 동작 +Comment[lt]=Individuali langų elgsena +Comment[nb]=Oppførsel for individuelle vinduer +Comment[nds]=Bedregen vun enkelte Finstern +Comment[nl]=Individueel venstergedrag +Comment[nn]=Åtferd for einskildvindauge +Comment[pa]=ਵੱਖ-ਵੱਖ ਵਿੰਡੋ ਰਵੱਈਆ +Comment[pl]=Wyjątkowe okna +Comment[pt]=Comportamento das Janelas Individuais +Comment[pt_BR]=Comportamento das janelas individuais +Comment[ro]=Comportament al ferestrelor individuale +Comment[ru]=Особые параметры конкретных окон +Comment[sk]=Individuálne správanie okien +Comment[sl]=ObnaÅ¡anje posameznih oken +Comment[sr]=Понашање појединачних прозора +Comment[sr@ijekavian]=Понашање појединачних прозора +Comment[sr@ijekavianlatin]=PonaÅ¡anje pojedinačnih prozora +Comment[sr@latin]=PonaÅ¡anje pojedinačnih prozora +Comment[sv]=Individuellt fönsterbeteende +Comment[tr]=Bireysel Pencere Davranışı +Comment[uk]=Поведінка окремих вікон +Comment[x-test]=xxIndividual Window Behaviorxx +Comment[zh_CN]=个别窗口行为 +Comment[zh_TW]=個別視窗行為 diff --git a/kcmkwin/kwinrules/rulebookmodel.cpp b/kcmkwin/kwinrules/rulebookmodel.cpp new file mode 100644 index 0000000..167a7d2 --- /dev/null +++ b/kcmkwin/kwinrules/rulebookmodel.cpp @@ -0,0 +1,173 @@ +/* + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "rulebookmodel.h" + + +namespace KWin +{ + +RuleBookModel::RuleBookModel(QObject *parent) + : QAbstractListModel(parent) + , m_ruleBook(new RuleBookSettings(this)) +{ +} + +RuleBookModel::~RuleBookModel() +{ + qDeleteAll(m_rules); +} + +int RuleBookModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_rules.count(); +} + +QVariant RuleBookModel::data(const QModelIndex &index, int role) const +{ + if (!checkIndex(index, CheckIndexOption::IndexIsValid | CheckIndexOption::ParentIsInvalid)) { + return QVariant(); + } + + switch (role) { + case Qt::DisplayRole: + return descriptionAt(index.row()); + default: + return QVariant(); + } +} + +bool RuleBookModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!checkIndex(index, CheckIndexOption::IndexIsValid | CheckIndexOption::ParentIsInvalid)) { + return false; + } + + switch (role) { + case Qt::DisplayRole: + setDescriptionAt(index.row(), value.toString()); + return true; + default: + return false; + } +} + +bool RuleBookModel::insertRows(int row, int count, const QModelIndex &parent) +{ + if (row < 0 || row > rowCount() || parent.isValid()) { + return false; + } + beginInsertRows(parent, row, row + count - 1); + + for (int i = 0; i < count; i++) { + Rules *newRule = new Rules(); + // HACK: Improve integration with RuleSettings and use directly its defaults + newRule->wmclassmatch = Rules::ExactMatch; + m_rules.insert(row + i, newRule); + } + + m_ruleBook->setCount(m_rules.count()); + + endInsertRows(); + return true; +} + +bool RuleBookModel::removeRows(int row, int count, const QModelIndex &parent) +{ + if (row < 0 || row > rowCount() || parent.isValid()) { + return false; + } + beginRemoveRows(parent, row, row + count - 1); + + for (int i = 0; i < count; i++) { + delete m_rules.at(row + i); + } + m_rules.remove(row, count); + m_ruleBook->setCount(m_rules.count()); + + endRemoveRows(); + return true; +} + +bool RuleBookModel::moveRows(const QModelIndex &sourceParent, int sourceRow, int count, + const QModelIndex &destinationParent, int destinationChild) +{ + if (sourceParent != destinationParent || sourceParent != QModelIndex()){ + return false; + } + + const bool isMoveDown = destinationChild > sourceRow; + // QAbstractItemModel::beginMoveRows(): when moving rows down in the same parent, + // the rows will be placed before the destinationChild index. + if (!beginMoveRows(sourceParent, sourceRow, sourceRow + count - 1, + destinationParent, isMoveDown ? destinationChild + 1 : destinationChild)) { + return false; + } + + for (int i = 0; i < count; i++) { + m_rules.insert(destinationChild + i, + m_rules.takeAt(isMoveDown ? sourceRow : sourceRow + i)); + } + + endMoveRows(); + return true; +} + + +QString RuleBookModel::descriptionAt(int row) const +{ + Q_ASSERT (row >= 0 && row < m_rules.count()); + return m_rules.at(row)->description; +} + +Rules *RuleBookModel::ruleAt(int row) const +{ + Q_ASSERT (row >= 0 && row < m_rules.count()); + return m_rules.at(row); +} + +void RuleBookModel::setDescriptionAt(int row, const QString &description) +{ + Q_ASSERT (row >= 0 && row < m_rules.count()); + if (description == m_rules.at(row)->description) { + return; + } + + m_rules.at(row)->description = description; + + emit dataChanged(index(row), index(row), QVector{Qt::DisplayRole}); +} + +void RuleBookModel::setRuleAt(int row, Rules *rule) +{ + Q_ASSERT (row >= 0 && row < m_rules.count()); + + delete m_rules.at(row); + m_rules[row] = rule; + + emit dataChanged(index(row), index(row), QVector{Qt::DisplayRole}); +} + + +void RuleBookModel::load() +{ + beginResetModel(); + + m_ruleBook->load(); + qDeleteAll(m_rules); + m_rules = m_ruleBook->rules(); + + endResetModel(); +} + +void RuleBookModel::save() +{ + m_ruleBook->setRules(m_rules); + m_ruleBook->save(); +} + +} // namespace diff --git a/kcmkwin/kwinrules/rulebookmodel.h b/kcmkwin/kwinrules/rulebookmodel.h new file mode 100644 index 0000000..aaf3937 --- /dev/null +++ b/kcmkwin/kwinrules/rulebookmodel.h @@ -0,0 +1,50 @@ +/* + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include "rulebooksettings.h" +#include + +#include + + +namespace KWin +{ + +class RuleBookModel : public QAbstractListModel +{ + Q_OBJECT + +public: + explicit RuleBookModel(QObject *parent = nullptr); + ~RuleBookModel(); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + + bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) override; + bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override; + bool moveRows(const QModelIndex &sourceParent, int sourceRow, int count, + const QModelIndex &destinationParent, int destinationChild) override; + + QString descriptionAt(int row) const; + void setDescriptionAt(int row, const QString &description); + + Rules *ruleAt(int row) const; + void setRuleAt(int row, Rules *rule); + + void load(); + void save(); + +private: + RuleBookSettings *m_ruleBook; + QVector m_rules; +}; + +} // namespace diff --git a/kcmkwin/kwinrules/ruleitem.cpp b/kcmkwin/kwinrules/ruleitem.cpp new file mode 100644 index 0000000..12fa9b4 --- /dev/null +++ b/kcmkwin/kwinrules/ruleitem.cpp @@ -0,0 +1,226 @@ +/* + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "ruleitem.h" + + +namespace KWin +{ + +RuleItem::RuleItem(const QString &key, + const RulePolicy::Type policyType, + const RuleItem::Type type, + const QString &name, + const QString §ion, + const QIcon &icon, + const QString &description) + : m_key(key) + , m_type(type) + , m_name(name) + , m_section(section) + , m_icon(icon) + , m_description(description) + , m_flags(NoFlags) + , m_enabled(false) + , m_policy(new RulePolicy(policyType)) + , m_options(nullptr) + , m_optionsMask(0U - 1) +{ + reset(); +} + +RuleItem::~RuleItem() +{ + delete m_policy; + delete m_options; +} + +void RuleItem::reset() +{ + m_enabled = hasFlag(AlwaysEnabled) | hasFlag(StartEnabled); + m_value = typedValue(QVariant()); + m_suggestedValue = QVariant(); + m_policy->resetValue(); + if (m_options) { + m_options->resetValue(); + } +} + +QString RuleItem::key() const +{ + return m_key; +} + +QString RuleItem::name() const +{ + return m_name; +} + +QString RuleItem::section() const +{ + return m_section; +} + +QString RuleItem::iconName() const +{ + return m_icon.name(); +} + +QIcon RuleItem::icon() const +{ + return m_icon; +} + +QString RuleItem::description() const +{ + return m_description; +} + +bool RuleItem::isEnabled() const +{ + return m_enabled; +} + +void RuleItem::setEnabled(bool enabled) +{ + m_enabled = (enabled && !hasFlag(SuggestionOnly)) || hasFlag(AlwaysEnabled); +} + +bool RuleItem::hasFlag(RuleItem::Flags flag) const +{ + return m_flags.testFlag(flag); +} + +void RuleItem::setFlag(RuleItem::Flags flag, bool active) +{ + m_flags.setFlag(flag, active); +} + +RuleItem::Type RuleItem::type() const +{ + return m_type; +} + +QVariant RuleItem::value() const +{ + if (m_options && m_type == Option) { + return m_options->value(); + } + return m_value; +} + +void RuleItem::setValue(QVariant value) +{ + if (m_options && m_type == Option) { + m_options->setValue(value); + } + m_value = typedValue(value); +} + +QVariant RuleItem::suggestedValue() const +{ + return m_suggestedValue; +} + +void RuleItem::setSuggestedValue(QVariant value, bool forceValue) +{ + if (forceValue) { + setValue(value); + } + m_suggestedValue = value.isNull() ? QVariant() : typedValue(value); +} + +QVariant RuleItem::options() const +{ + if (!m_options) { + return QVariant(); + } + return QVariant::fromValue(m_options); +} + +void RuleItem::setOptionsData(const QList &data) +{ + if (!m_options) { + if (m_type != Option && m_type != NetTypes) { + return; + } + m_options = new OptionsModel(); + } + m_options->updateModelData(data); + m_options->setValue(m_value); + + if (m_type == NetTypes) { + m_optionsMask = 0; + for (const OptionsModel::Data &dataItem : data) { + m_optionsMask += 1 << dataItem.value.toUInt(); + } + } +} + +uint RuleItem::optionsMask() const +{ + return m_optionsMask; +} + +int RuleItem::policy() const +{ + return m_policy->value(); +} + +void RuleItem::setPolicy(int policy) +{ + m_policy->setValue(policy); +} + +RulePolicy::Type RuleItem::policyType() const +{ + return m_policy->type(); +} + +QVariant RuleItem::policyModel() const +{ + return QVariant::fromValue(m_policy); +} + +QString RuleItem::policyKey() const +{ + return m_policy->policyKey(m_key); +} + +QVariant RuleItem::typedValue(const QVariant &value) const +{ + switch (type()) { + case Undefined: + case Option: + return value; + case Boolean: + return value.toBool(); + case Integer: + case Percentage: + return value.toInt(); + case NetTypes: { + const uint typesMask = value.toUInt() & optionsMask(); // filter by the allowed mask in the model + if (typesMask == 0 || typesMask == optionsMask()) { // if no types or all of them are selected + return 0U - 1; // return an all active mask (NET:AllTypesMask) + } + return typesMask; + } + case Point: { + const QPoint point = value.toPoint(); + return (point == invalidPoint) ? QPoint(0, 0) : point; + } + case Size: + return value.toSize(); + case String: + return value.toString().trimmed(); + case Shortcut: + return value.toString(); + } + return value; +} + +} //namespace + diff --git a/kcmkwin/kwinrules/ruleitem.h b/kcmkwin/kwinrules/ruleitem.h new file mode 100644 index 0000000..a907c02 --- /dev/null +++ b/kcmkwin/kwinrules/ruleitem.h @@ -0,0 +1,115 @@ +/* + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#ifndef KWIN_RULEITEM_H +#define KWIN_RULEITEM_H + +#include "optionsmodel.h" + +#include +#include + + +namespace KWin +{ + +class RuleItem : public QObject +{ + Q_OBJECT + +public: + enum Type { + Undefined, + Boolean, + String, + Integer, + Option, + NetTypes, + Percentage, + Point, + Size, + Shortcut + }; + Q_ENUM(Type) + + enum Flags { + NoFlags = 0, + AlwaysEnabled = 1u << 0, + StartEnabled = 1u << 1, + AffectsWarning = 1u << 2, + AffectsDescription = 1u << 3, + SuggestionOnly = 1u << 4, + AllFlags = 0b11111 + }; + +public: + RuleItem() {}; + RuleItem(const QString &key, + const RulePolicy::Type policyType, + const Type type, + const QString &name, + const QString §ion, + const QIcon &icon = QIcon::fromTheme("window"), + const QString &description = QString("") + ); + ~RuleItem(); + + QString key() const; + QString name() const; + QString section() const; + QIcon icon() const; + QString iconName() const; + QString description() const; + + bool isEnabled() const; + void setEnabled(bool enabled); + + bool hasFlag(RuleItem::Flags flag) const; + void setFlag(RuleItem::Flags flag, bool active=true); + + Type type() const; + QVariant value() const; + void setValue(QVariant value); + QVariant suggestedValue() const; + void setSuggestedValue(QVariant value, bool forceValue = false); + + QVariant options() const; + void setOptionsData(const QList &data); + uint optionsMask() const; + + RulePolicy::Type policyType() const; + int policy() const; // int belongs to anonymous enum in Rules:: + void setPolicy(int policy); // int belongs to anonymous enum in Rules:: + QVariant policyModel() const; + QString policyKey() const; + + void reset(); + +private: + QVariant typedValue(const QVariant &value) const; + +private: + QString m_key; + RuleItem::Type m_type; + QString m_name; + QString m_section; + QIcon m_icon; + QString m_description; + QFlags m_flags; + + bool m_enabled; + + QVariant m_value; + QVariant m_suggestedValue; + + RulePolicy *m_policy; + OptionsModel *m_options; + uint m_optionsMask; +}; + +} //namespace + +#endif //KWIN_RULEITEM_H diff --git a/kcmkwin/kwinrules/rulesdialog.cpp b/kcmkwin/kwinrules/rulesdialog.cpp new file mode 100644 index 0000000..370ff67 --- /dev/null +++ b/kcmkwin/kwinrules/rulesdialog.cpp @@ -0,0 +1,74 @@ +/* + SPDX-FileCopyrightText: 2004 Lubos Lunak + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "rulesdialog.h" + +#include +#include +#include +#include +#include + +#include + + +namespace KWin +{ + +RulesDialog::RulesDialog(QWidget* parent, const char* name) + : QDialog(parent) + , m_rulesModel(new RulesModel(this)) +{ + setObjectName(name); + setModal(true); + setWindowTitle(i18n("Edit Window-Specific Settings")); + setWindowIcon(QIcon::fromTheme("preferences-system-windows-actions")); + setLayout(new QVBoxLayout); + + // Init RuleEditor QML QuickView + QQuickView *quickView = new QQuickView(); + quickView->setSource(QUrl::fromLocalFile(QStandardPaths::locate( + QStandardPaths::GenericDataLocation, + QStringLiteral("kpackage/kcms/kcm_kwinrules/contents/ui/RulesEditor.qml")))); + quickView->setResizeMode(QQuickView::SizeRootObjectToView); + quickView->rootObject()->setProperty("rulesModel", QVariant::fromValue(m_rulesModel)); + + m_quickWidget = QWidget::createWindowContainer(quickView, this); + m_quickWidget->setMinimumSize(QSize(650, 575)); + layout()->addWidget(m_quickWidget); + + QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + connect(buttons, SIGNAL(accepted()), SLOT(accept())); + connect(buttons, SIGNAL(rejected()), SLOT(reject())); + layout()->addWidget(buttons); +} + +// window is set only for Alt+F3/Window-specific settings, because the dialog +// is then related to one specific window +Rules* RulesDialog::edit(Rules* r, const QVariantMap& info, bool show_hints) +{ + Q_UNUSED(show_hints); + + m_rules = r; + + m_rulesModel->importFromRules(m_rules); + if (!info.isEmpty()) { + m_rulesModel->setWindowProperties(info, true); + } + + exec(); + + return m_rules; +} + +void RulesDialog::accept() +{ + m_rules = m_rulesModel->exportToRules(); + QDialog::accept(); +} + +} diff --git a/kcmkwin/kwinrules/rulesdialog.h b/kcmkwin/kwinrules/rulesdialog.h new file mode 100644 index 0000000..78c4447 --- /dev/null +++ b/kcmkwin/kwinrules/rulesdialog.h @@ -0,0 +1,41 @@ +/* + SPDX-FileCopyrightText: 2004 Lubos Lunak + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_RULESDIALOG_H +#define KWIN_RULESDIALOG_H + +#include "rulesmodel.h" +#include "../../rules.h" + +#include + +namespace KWin +{ + +class Rules; + +class RulesDialog : public QDialog +{ + Q_OBJECT + +public: + explicit RulesDialog(QWidget* parent = nullptr, const char* name = nullptr); + + Rules* edit(Rules* r, const QVariantMap& info, bool show_hints); + +protected: + void accept() override; + +private: + RulesModel* m_rulesModel; + QWidget *m_quickWidget; + Rules* m_rules; +}; + +} // namespace + +#endif // KWIN_RULESDIALOG_H diff --git a/kcmkwin/kwinrules/rulesmodel.cpp b/kcmkwin/kwinrules/rulesmodel.cpp new file mode 100644 index 0000000..9ae8c80 --- /dev/null +++ b/kcmkwin/kwinrules/rulesmodel.cpp @@ -0,0 +1,877 @@ +/* + SPDX-FileCopyrightText: 2004 Lubos Lunak + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "rulesmodel.h" +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + + +namespace KWin +{ + +RulesModel::RulesModel(QObject *parent) + : QAbstractListModel(parent) +{ + qmlRegisterUncreatableType("org.kde.kcms.kwinrules", 1, 0, "RuleItem", + QStringLiteral("Do not create objects of type RuleItem")); + qmlRegisterUncreatableType("org.kde.kcms.kwinrules", 1, 0, "RulesModel", + QStringLiteral("Do not create objects of type RulesModel")); + + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + + populateRuleList(); +} + +RulesModel::~RulesModel() +{ +} + +QHash< int, QByteArray > RulesModel::roleNames() const +{ + return { + {KeyRole, QByteArrayLiteral("key")}, + {NameRole, QByteArrayLiteral("name")}, + {IconRole, QByteArrayLiteral("icon")}, + {IconNameRole, QByteArrayLiteral("iconName")}, + {SectionRole, QByteArrayLiteral("section")}, + {DescriptionRole, QByteArrayLiteral("description")}, + {EnabledRole, QByteArrayLiteral("enabled")}, + {SelectableRole, QByteArrayLiteral("selectable")}, + {ValueRole, QByteArrayLiteral("value")}, + {TypeRole, QByteArrayLiteral("type")}, + {PolicyRole, QByteArrayLiteral("policy")}, + {PolicyModelRole, QByteArrayLiteral("policyModel")}, + {OptionsModelRole, QByteArrayLiteral("options")}, + {OptionsMaskRole, QByteArrayLiteral("optionsMask")}, + {SuggestedValueRole, QByteArrayLiteral("suggested")}, + }; +} + +int RulesModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + return m_ruleList.size(); +} + +QVariant RulesModel::data(const QModelIndex &index, int role) const +{ + if (!checkIndex(index, CheckIndexOption::IndexIsValid | CheckIndexOption::ParentIsInvalid)) { + return QVariant(); + } + + const RuleItem *rule = m_ruleList.at(index.row()); + + switch (role) { + case KeyRole: + return rule->key(); + case NameRole: + return rule->name(); + case IconRole: + return rule->icon(); + case IconNameRole: + return rule->iconName(); + case DescriptionRole: + return rule->description(); + case SectionRole: + return rule->section(); + case EnabledRole: + return rule->isEnabled(); + case SelectableRole: + return !rule->hasFlag(RuleItem::AlwaysEnabled) && !rule->hasFlag(RuleItem::SuggestionOnly); + case ValueRole: + return rule->value(); + case TypeRole: + return rule->type(); + case PolicyRole: + return rule->policy(); + case PolicyModelRole: + return rule->policyModel(); + case OptionsModelRole: + return rule->options(); + case OptionsMaskRole: + return rule->optionsMask(); + case SuggestedValueRole: + return rule->suggestedValue(); + } + return QVariant(); +} + +bool RulesModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!checkIndex(index, CheckIndexOption::IndexIsValid | CheckIndexOption::ParentIsInvalid)) { + return false; + } + + RuleItem *rule = m_ruleList.at(index.row()); + + switch (role) { + case EnabledRole: + if (value.toBool() == rule->isEnabled()) { + return true; + } + rule->setEnabled(value.toBool()); + break; + case ValueRole: + if (rule->hasFlag(RuleItem::SuggestionOnly)) { + processSuggestion(rule->key(), value); + } + if (value == rule->value()) { + return true; + } + rule->setValue(value); + break; + case PolicyRole: + if (value.toInt() == rule->policy()) { + return true; + } + rule->setPolicy(value.toInt()); + break; + case SuggestedValueRole: + if (value == rule->suggestedValue()) { + return true; + } + rule->setSuggestedValue(value); + break; + default: + return false; + } + + emit dataChanged(index, index, QVector{role}); + + if (rule->hasFlag(RuleItem::AffectsDescription)) { + emit descriptionChanged(); + } + if (rule->hasFlag(RuleItem::AffectsWarning)) { + emit warningMessageChanged(); + } + + return true; +} + +QModelIndex RulesModel::indexOf(const QString& key) const +{ + const QModelIndexList indexes = match(index(0), RulesModel::KeyRole, key, 1, Qt::MatchFixedString); + if (indexes.isEmpty()) { + return QModelIndex(); + } + return indexes.at(0); +} + +RuleItem *RulesModel::addRule(RuleItem *rule) +{ + m_ruleList << rule; + m_rules.insert(rule->key(), rule); + + return rule; +} + +bool RulesModel::hasRule(const QString& key) const +{ + return m_rules.contains(key); +} + + +RuleItem *RulesModel::ruleItem(const QString& key) const +{ + return m_rules.value(key); +} + +QString RulesModel::description() const +{ + const QString desc = m_rules["description"]->value().toString(); + if (!desc.isEmpty()) { + return desc; + } + return defaultDescription(); +} + +void RulesModel::setDescription(const QString &description) +{ + setData(indexOf("description"), description, RulesModel::ValueRole); +} + +QString RulesModel::defaultDescription() const +{ + const QString wmclass = m_rules["wmclass"]->value().toString(); + const QString title = m_rules["title"]->isEnabled() ? m_rules["title"]->value().toString() : QString(); + + if (!title.isEmpty()) { + return i18n("Window settings for %1", title); + } + if (!wmclass.isEmpty()) { + return i18n("Settings for %1", wmclass); + } + + return i18n("New window settings"); +} + +void RulesModel::processSuggestion(const QString &key, const QVariant &value) +{ + if (key == QLatin1String("wmclasshelper")) { + setData(indexOf("wmclass"), value, RulesModel::ValueRole); + setData(indexOf("wmclasscomplete"), true, RulesModel::ValueRole); + } +} + +QString RulesModel::warningMessage() const +{ + if (wmclassWarning()) { + return i18n("You have specified the window class as unimportant.\n" + "This means the settings will possibly apply to windows from all applications." + " If you really want to create a generic setting, it is recommended" + " you at least limit the window types to avoid special window types."); + } + + return QString(); +} + +bool RulesModel::wmclassWarning() const +{ + const bool no_wmclass = !m_rules["wmclass"]->isEnabled() + || m_rules["wmclass"]->policy() == Rules::UnimportantMatch; + const bool alltypes = !m_rules["types"]->isEnabled() + || (m_rules["types"]->value() == 0) + || (m_rules["types"]->value() == NET::AllTypesMask) + || ((m_rules["types"]->value().toInt() | (1 << NET::Override)) == 0x3FF); + + return (no_wmclass && alltypes); +} + + +void RulesModel::readFromSettings(RuleSettings *settings) +{ + beginResetModel(); + + for (RuleItem *rule : qAsConst(m_ruleList)) { + const KConfigSkeletonItem *configItem = settings->findItem(rule->key()); + const KConfigSkeletonItem *configPolicyItem = settings->findItem(rule->policyKey()); + + rule->reset(); + + if (!configItem) { + continue; + } + + const bool isEnabled = configPolicyItem ? configPolicyItem->property() != Rules::Unused + : !configItem->property().toString().isEmpty(); + rule->setEnabled(isEnabled); + + const QVariant value = configItem->property(); + rule->setValue(value); + + if (configPolicyItem) { + const int policy = configPolicyItem->property().toInt(); + rule->setPolicy(policy); + } + } + + endResetModel(); + + emit descriptionChanged(); + emit warningMessageChanged(); +} + +void RulesModel::writeToSettings(RuleSettings *settings) const +{ + const QString description = m_rules["description"]->value().toString(); + if (description.isEmpty()) { + m_rules["description"]->setValue(defaultDescription()); + } + + for (const RuleItem *rule : qAsConst(m_ruleList)) { + KConfigSkeletonItem *configItem = settings->findItem(rule->key()); + KConfigSkeletonItem *configPolicyItem = settings->findItem(rule->policyKey()); + + if (!configItem) { + continue; + } + + if (rule->isEnabled()) { + configItem->setProperty(rule->value()); + if (configPolicyItem) { + configPolicyItem->setProperty(rule->policy()); + } + } else { + if (configPolicyItem) { + configPolicyItem->setProperty(Rules::Unused); + } else { + // Rules without policy gets deactivated by an empty string + configItem->setProperty(QString()); + } + } + } +} + +void RulesModel::importFromRules(Rules* rules) +{ + QTemporaryFile tempFile; + if (!tempFile.open()) { + return; + } + const auto cfg = KSharedConfig::openConfig(tempFile.fileName(), KConfig::SimpleConfig); + RuleSettings *settings = new RuleSettings(cfg, QStringLiteral("tempSettings")); + + settings->setDefaults(); + if (rules) { + rules->write(settings); + } + readFromSettings(settings); + + delete(settings); +} + +Rules *RulesModel::exportToRules() const +{ + QTemporaryFile tempFile; + if (!tempFile.open()) { + return nullptr; + } + const auto cfg = KSharedConfig::openConfig(tempFile.fileName(), KConfig::SimpleConfig); + + RuleSettings *settings = new RuleSettings(cfg, QStringLiteral("tempSettings")); + + writeToSettings(settings); + Rules *rules = new Rules(settings); + + delete(settings); + return rules; +} + + +void RulesModel::populateRuleList() +{ + qDeleteAll(m_ruleList); + m_ruleList.clear(); + + //Rule description + auto description = addRule(new RuleItem(QLatin1String("description"), + RulePolicy::NoPolicy, RuleItem::String, + i18n("Description"), i18n("Window matching"), + QIcon::fromTheme("entry-edit"))); + description->setFlag(RuleItem::AlwaysEnabled); + description->setFlag(RuleItem::AffectsDescription); + + // Window matching + auto wmclass = addRule(new RuleItem(QLatin1String("wmclass"), + RulePolicy::StringMatch, RuleItem::String, + i18n("Window class (application)"), i18n("Window matching"), + QIcon::fromTheme("window"))); + wmclass->setFlag(RuleItem::AlwaysEnabled); + wmclass->setFlag(RuleItem::AffectsDescription); + wmclass->setFlag(RuleItem::AffectsWarning); + + auto wmclasscomplete = addRule(new RuleItem(QLatin1String("wmclasscomplete"), + RulePolicy::NoPolicy, RuleItem::Boolean, + i18n("Match whole window class"), i18n("Window matching"), + QIcon::fromTheme("window"))); + wmclasscomplete->setFlag(RuleItem::AlwaysEnabled); + + // Helper item to store the detected whole window class when detecting properties + auto wmclasshelper = addRule(new RuleItem(QLatin1String("wmclasshelper"), + RulePolicy::NoPolicy, RuleItem::String, + i18n("Whole window class"), i18n("Window matching"), + QIcon::fromTheme("window"))); + wmclasshelper->setFlag(RuleItem::SuggestionOnly); + + auto types = addRule(new RuleItem(QLatin1String("types"), + RulePolicy::NoPolicy, RuleItem::NetTypes, + i18n("Window types"), i18n("Window matching"), + QIcon::fromTheme("window-duplicate"))); + types->setOptionsData(windowTypesModelData()); + types->setFlag(RuleItem::AlwaysEnabled); + types->setFlag(RuleItem::AffectsWarning); + + addRule(new RuleItem(QLatin1String("windowrole"), + RulePolicy::StringMatch, RuleItem::String, + i18n("Window role"), i18n("Window matching"), + QIcon::fromTheme("dialog-object-properties"))); + + auto title = addRule(new RuleItem(QLatin1String("title"), + RulePolicy::StringMatch, RuleItem::String, + i18n("Window title"), i18n("Window matching"), + QIcon::fromTheme("edit-comment"))); + title->setFlag(RuleItem::AffectsDescription); + + addRule(new RuleItem(QLatin1String("clientmachine"), + RulePolicy::StringMatch, RuleItem::String, + i18n("Machine (hostname)"), i18n("Window matching"), + QIcon::fromTheme("computer"))); + + // Size & Position + addRule(new RuleItem(QLatin1String("position"), + RulePolicy::SetRule, RuleItem::Point, + i18n("Position"), i18n("Size & Position"), + QIcon::fromTheme("transform-move"))); + + addRule(new RuleItem(QLatin1String("size"), + RulePolicy::SetRule, RuleItem::Size, + i18n("Size"), i18n("Size & Position"), + QIcon::fromTheme("image-resize-symbolic"))); + + addRule(new RuleItem(QLatin1String("maximizehoriz"), + RulePolicy::SetRule, RuleItem::Boolean, + i18n("Maximized horizontally"), i18n("Size & Position"), + QIcon::fromTheme("resizecol"))); + + addRule(new RuleItem(QLatin1String("maximizevert"), + RulePolicy::SetRule, RuleItem::Boolean, + i18n("Maximized vertically"), i18n("Size & Position"), + QIcon::fromTheme("resizerow"))); + + auto desktop = addRule(new RuleItem(QLatin1String("desktop"), + RulePolicy::SetRule, RuleItem::Option, + i18n("Virtual Desktop"), i18n("Size & Position"), + QIcon::fromTheme("virtual-desktops"))); + desktop->setOptionsData(virtualDesktopsModelData()); + + connect(this, &RulesModel::virtualDesktopsUpdated, + this, [this] { m_rules["desktop"]->setOptionsData(virtualDesktopsModelData()); }); + updateVirtualDesktops(); + +#ifdef KWIN_BUILD_ACTIVITIES + m_activities = new KActivities::Consumer(this); + + auto activity = addRule(new RuleItem(QLatin1String("activity"), + RulePolicy::SetRule, RuleItem::Option, + i18n("Activity"), i18n("Size & Position"), + QIcon::fromTheme("activities"))); + activity->setOptionsData(activitiesModelData()); + + // Activites consumer may update the available activities later + connect(m_activities, &KActivities::Consumer::activitiesChanged, + this, [this] { m_rules["activity"]->setOptionsData(activitiesModelData()); }); + connect(m_activities, &KActivities::Consumer::serviceStatusChanged, + this, [this] { m_rules["activity"]->setOptionsData(activitiesModelData()); }); + +#endif + + addRule(new RuleItem(QLatin1String("screen"), + RulePolicy::SetRule, RuleItem::Integer, + i18n("Screen"), i18n("Size & Position"), + QIcon::fromTheme("osd-shutd-screen"))); + + addRule(new RuleItem(QLatin1String("fullscreen"), + RulePolicy::SetRule, RuleItem::Boolean, + i18n("Fullscreen"), i18n("Size & Position"), + QIcon::fromTheme("view-fullscreen"))); + + addRule(new RuleItem(QLatin1String("minimize"), + RulePolicy::SetRule, RuleItem::Boolean, + i18n("Minimized"), i18n("Size & Position"), + QIcon::fromTheme("window-minimize"))); + + addRule(new RuleItem(QLatin1String("shade"), + RulePolicy::SetRule, RuleItem::Boolean, + i18n("Shaded"), i18n("Size & Position"), + QIcon::fromTheme("window-shade"))); + + auto placement = addRule(new RuleItem(QLatin1String("placement"), + RulePolicy::ForceRule, RuleItem::Option, + i18n("Initial placement"), i18n("Size & Position"), + QIcon::fromTheme("region"))); + placement->setOptionsData(placementModelData()); + + addRule(new RuleItem(QLatin1String("ignoregeometry"), + RulePolicy::SetRule, RuleItem::Boolean, + i18n("Ignore requested geometry"), i18n("Size & Position"), + QIcon::fromTheme("view-time-schedule-baselined-remove"), + i18n("Windows can ask to appear in a certain position.\n" + "By default this overrides the placement strategy\n" + "what might be nasty if the client abuses the feature\n" + "to unconditionally popup in the middle of your screen."))); + + addRule(new RuleItem(QLatin1String("minsize"), + RulePolicy::ForceRule, RuleItem::Size, + i18n("Minimum Size"), i18n("Size & Position"), + QIcon::fromTheme("image-resize-symbolic"))); + + addRule(new RuleItem(QLatin1String("maxsize"), + RulePolicy::ForceRule, RuleItem::Size, + i18n("Maximum Size"), i18n("Size & Position"), + QIcon::fromTheme("image-resize-symbolic"))); + + addRule(new RuleItem(QLatin1String("strictgeometry"), + RulePolicy::ForceRule, RuleItem::Boolean, + i18n("Obey geometry restrictions"), i18n("Size & Position"), + QIcon::fromTheme("transform-crop-and-resize"), + i18n("Eg. terminals or video players can ask to keep a certain aspect ratio\n" + "or only grow by values larger than one\n" + "(eg. by the dimensions of one character).\n" + "This may be pointless and the restriction prevents arbitrary dimensions\n" + "like your complete screen area."))); + + // Arrangement & Access + addRule(new RuleItem(QLatin1String("above"), + RulePolicy::SetRule, RuleItem::Boolean, + i18n("Keep above"), i18n("Arrangement & Access"), + QIcon::fromTheme("window-keep-above"))); + + addRule(new RuleItem(QLatin1String("below"), + RulePolicy::SetRule, RuleItem::Boolean, + i18n("Keep below"), i18n("Arrangement & Access"), + QIcon::fromTheme("window-keep-below"))); + + addRule(new RuleItem(QLatin1String("skiptaskbar"), + RulePolicy::SetRule, RuleItem::Boolean, + i18n("Skip taskbar"), i18n("Arrangement & Access"), + QIcon::fromTheme("kt-show-statusbar"), + i18n("Window shall (not) appear in the taskbar."))); + + addRule(new RuleItem(QLatin1String("skippager"), + RulePolicy::SetRule, RuleItem::Boolean, + i18n("Skip pager"), i18n("Arrangement & Access"), + QIcon::fromTheme("org.kde.plasma.pager"), + i18n("Window shall (not) appear in the manager for virtual desktops"))); + + addRule(new RuleItem(QLatin1String("skipswitcher"), + RulePolicy::SetRule, RuleItem::Boolean, + i18n("Skip switcher"), i18n("Arrangement & Access"), + QIcon::fromTheme("preferences-system-windows-effect-flipswitch"), + i18n("Window shall (not) appear in the Alt+Tab list"))); + + addRule(new RuleItem(QLatin1String("shortcut"), + RulePolicy::SetRule, RuleItem::Shortcut, + i18n("Shortcut"), i18n("Arrangement & Access"), + QIcon::fromTheme("configure-shortcuts"))); + + // Appearance & Fixes + addRule(new RuleItem(QLatin1String("noborder"), + RulePolicy::SetRule, RuleItem::Boolean, + i18n("No titlebar and frame"), i18n("Appearance & Fixes"), + QIcon::fromTheme("dialog-cancel"))); + + auto decocolor = addRule(new RuleItem(QLatin1String("decocolor"), + RulePolicy::ForceRule, RuleItem::Option, + i18n("Titlebar color scheme"), i18n("Appearance & Fixes"), + QIcon::fromTheme("preferences-desktop-theme"))); + decocolor->setOptionsData(colorSchemesModelData()); + + addRule(new RuleItem(QLatin1String("opacityactive"), + RulePolicy::ForceRule, RuleItem::Percentage, + i18n("Active opacity"), i18n("Appearance & Fixes"), + QIcon::fromTheme("edit-opacity"))); + + addRule(new RuleItem(QLatin1String("opacityinactive"), + RulePolicy::ForceRule, RuleItem::Percentage, + i18n("Inactive opacity"), i18n("Appearance & Fixes"), + QIcon::fromTheme("edit-opacity"))); + + auto fsplevel = addRule(new RuleItem(QLatin1String("fsplevel"), + RulePolicy::ForceRule, RuleItem::Option, + i18n("Focus stealing prevention"), i18n("Appearance & Fixes"), + QIcon::fromTheme("preferences-system-windows-effect-glide"), + i18n("KWin tries to prevent windows from taking the focus\n" + "(\"activate\") while you're working in another window,\n" + "but this may sometimes fail or superact.\n" + "\"None\" will unconditionally allow this window to get the focus while\n" + "\"Extreme\" will completely prevent it from taking the focus."))); + fsplevel->setOptionsData(focusModelData()); + + auto fpplevel = addRule(new RuleItem(QLatin1String("fpplevel"), + RulePolicy::ForceRule, RuleItem::Option, + i18n("Focus protection"), i18n("Appearance & Fixes"), + QIcon::fromTheme("preferences-system-windows-effect-minimize"), + i18n("This controls the focus protection of the currently active window.\n" + "None will always give the focus away,\n" + "Extreme will keep it.\n" + "Otherwise it's interleaved with the stealing prevention\n" + "assigned to the window that wants the focus."))); + fpplevel->setOptionsData(focusModelData()); + + addRule(new RuleItem(QLatin1String("acceptfocus"), + RulePolicy::ForceRule, RuleItem::Boolean, + i18n("Accept focus"), i18n("Appearance & Fixes"), + QIcon::fromTheme("preferences-desktop-cursors"), + i18n("Windows may prevent to get the focus (activate) when being clicked.\n" + "On the other hand you might wish to prevent a window\n" + "from getting focused on a mouse click."))); + + addRule(new RuleItem(QLatin1String("disableglobalshortcuts"), + RulePolicy::ForceRule, RuleItem::Boolean, + i18n("Ignore global shortcuts"), i18n("Appearance & Fixes"), + QIcon::fromTheme("input-keyboard-virtual-off"), + i18n("When used, a window will receive\n" + "all keyboard inputs while it is active, including Alt+Tab etc.\n" + "This is especially interesting for emulators or virtual machines.\n" + "\n" + "Be warned:\n" + "you won't be able to Alt+Tab out of the window\n" + "nor use any other global shortcut (such as Alt+F2 to show KRunner)\n" + "while it's active!"))); + + addRule(new RuleItem(QLatin1String("closeable"), + RulePolicy::ForceRule, RuleItem::Boolean, + i18n("Closeable"), i18n("Appearance & Fixes"), + QIcon::fromTheme("dialog-close"))); + + auto type = addRule(new RuleItem(QLatin1String("type"), + RulePolicy::ForceRule, RuleItem::Option, + i18n("Set window type"), i18n("Appearance & Fixes"), + QIcon::fromTheme("window-duplicate"))); + type->setOptionsData(windowTypesModelData()); + + addRule(new RuleItem(QLatin1String("desktopfile"), + RulePolicy::SetRule, RuleItem::String, + i18n("Desktop file name"), i18n("Appearance & Fixes"), + QIcon::fromTheme("application-x-desktop"))); + + addRule(new RuleItem(QLatin1String("blockcompositing"), + RulePolicy::ForceRule, RuleItem::Boolean, + i18n("Block compositing"), i18n("Appearance & Fixes"), + QIcon::fromTheme("composite-track-on"))); +} + + +const QHash RulesModel::x11PropertyHash() +{ + static const auto propertyToRule = QHash { + { "caption", "title" }, + { "role", "windowrole" }, + { "clientMachine", "clientmachine" }, + { "x11DesktopNumber", "desktop" }, + { "maximizeHorizontal", "maximizehoriz" }, + { "maximizeVertical", "maximizevert" }, + { "minimized", "minimize" }, + { "shaded", "shade" }, + { "fullscreen", "fullscreen" }, + { "keepAbove", "above" }, + { "keepBelow", "below" }, + { "noBorder", "noborder" }, + { "skipTaskbar", "skiptaskbar" }, + { "skipPager", "skippager" }, + { "skipSwitcher", "skipswitcher" }, + { "type", "type" }, + { "desktopFile", "desktopfile" } + }; + return propertyToRule; +}; + +void RulesModel::setWindowProperties(const QVariantMap &info, bool forceValue) +{ + // Properties that cannot be directly applied via x11PropertyHash + const QPoint position = QPoint(info.value("x").toInt(), info.value("y").toInt()); + const QSize size = QSize(info.value("width").toInt(), info.value("height").toInt()); + + m_rules["position"]->setSuggestedValue(position, forceValue); + m_rules["size"]->setSuggestedValue(size, forceValue); + m_rules["minsize"]->setSuggestedValue(size, forceValue); + m_rules["maxsize"]->setSuggestedValue(size, forceValue); + + NET::WindowType window_type = static_cast(info.value("type", 0).toInt()); + if (window_type == NET::Unknown) { + window_type = NET::Normal; + } + m_rules["types"]->setSuggestedValue(1 << window_type); + + const QString wmsimpleclass = info.value("resourceClass").toString(); + const QString wmcompleteclass = QStringLiteral("%1 %2").arg(info.value("resourceName").toString(), + info.value("resourceClass").toString()); + const bool isComplete = m_rules.value("wmclasscomplete")->value().toBool(); + + m_rules["wmclass"]->setSuggestedValue(wmsimpleclass); + m_rules["wmclasshelper"]->setSuggestedValue(wmcompleteclass); + + if (forceValue) { + m_rules["wmclass"]->setValue(isComplete ? wmcompleteclass : wmsimpleclass); + } + + const auto ruleForProperty = x11PropertyHash(); + for (QString &property : info.keys()) { + if (!ruleForProperty.contains(property)) { + continue; + } + const QString ruleKey = ruleForProperty.value(property, QString()); + Q_ASSERT(hasRule(ruleKey)); + + m_rules[ruleKey]->setSuggestedValue(info.value(property), forceValue); + } + + emit dataChanged(index(0), index(rowCount()-1), {RulesModel::SuggestedValueRole}); + if (!forceValue) { + emit suggestionsChanged(); + } +} + + +QList RulesModel::windowTypesModelData() const +{ + static const auto modelData = QList { + //TODO: Find/create better icons + { NET::Normal, i18n("Normal Window") , QIcon::fromTheme("window") }, + { NET::Dialog, i18n("Dialog Window") , QIcon::fromTheme("window-duplicate") }, + { NET::Utility, i18n("Utility Window") , QIcon::fromTheme("dialog-object-properties") }, + { NET::Dock, i18n("Dock (panel)") , QIcon::fromTheme("list-remove") }, + { NET::Toolbar, i18n("Toolbar") , QIcon::fromTheme("tools") }, + { NET::Menu, i18n("Torn-Off Menu") , QIcon::fromTheme("overflow-menu-left") }, + { NET::Splash, i18n("Splash Screen") , QIcon::fromTheme("embosstool") }, + { NET::Desktop, i18n("Desktop") , QIcon::fromTheme("desktop") }, + // { NET::Override, i18n("Unmanaged Window") }, deprecated + { NET::TopMenu, i18n("Standalone Menubar"), QIcon::fromTheme("open-menu-symbolic") } + }; + return modelData; +} + +QList RulesModel::virtualDesktopsModelData() const +{ + QList modelData; + for (const DBusDesktopDataStruct &desktop : m_virtualDesktops) { + modelData << OptionsModel::Data{ + desktop.position + 1, // "desktop" setting uses the desktop position (int) starting at 1 + QString::number(desktop.position + 1).rightJustified(2) + QStringLiteral(": ") + desktop.name, + QIcon::fromTheme("virtual-desktops") + }; + } + modelData << OptionsModel::Data{ NET::OnAllDesktops, i18n("All Desktops"), QIcon::fromTheme("window-pin") }; + return modelData; +} + + +QList RulesModel::activitiesModelData() const +{ +#ifdef KWIN_BUILD_ACTIVITIES + QList modelData; + + // NULL_ID from kactivities/src/lib/core/consumer.cpp + modelData << OptionsModel::Data{ + QString::fromLatin1("00000000-0000-0000-0000-000000000000"), + i18n("All Activities"), + QIcon::fromTheme("activities") + }; + + const auto activities = m_activities->activities(KActivities::Info::Running); + if (m_activities->serviceStatus() == KActivities::Consumer::Running) { + for (const QString &activityId : activities) { + const KActivities::Info info(activityId); + modelData << OptionsModel::Data{ activityId, info.name(), QIcon::fromTheme(info.icon()) }; + } + } + + return modelData; +#else + return {}; +#endif +} + +QList RulesModel::placementModelData() const +{ + static const auto modelData = QList { + { Placement::Default, i18n("Default") }, + { Placement::NoPlacement, i18n("No Placement") }, + { Placement::Smart, i18n("Minimal Overlapping") }, + { Placement::Maximizing, i18n("Maximized") }, + { Placement::Cascade, i18n("Cascaded") }, + { Placement::Centered, i18n("Centered") }, + { Placement::Random, i18n("Random") }, + { Placement::ZeroCornered, i18n("In Top-Left Corner") }, + { Placement::UnderMouse, i18n("Under Mouse") }, + { Placement::OnMainWindow, i18n("On Main Window") } + }; + return modelData; +} + +QList RulesModel::focusModelData() const +{ + static const auto modelData = QList { + { 0, i18n("None") }, + { 1, i18n("Low") }, + { 2, i18n("Normal") }, + { 3, i18n("High") }, + { 4, i18n("Extreme") } + }; + return modelData; +} + +QList RulesModel::colorSchemesModelData() const +{ + QList modelData; + + KColorSchemeManager schemes; + QAbstractItemModel *schemesModel = schemes.model(); + + // Skip row 0, which is Default scheme + for (int r = 1; r < schemesModel->rowCount(); r++) { + const QModelIndex index = schemesModel->index(r, 0); + modelData << OptionsModel::Data{ + QFileInfo(index.data(Qt::UserRole).toString()).baseName(), + index.data(Qt::DisplayRole).toString(), + index.data(Qt::DecorationRole).value() + }; + } + + return modelData; +} + +void RulesModel::detectWindowProperties(int secs) +{ + QTimer::singleShot(secs*1000, this, &RulesModel::selectX11Window); +} + +void RulesModel::selectX11Window() +{ + QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.KWin"), + QStringLiteral("/KWin"), + QStringLiteral("org.kde.KWin"), + QStringLiteral("queryWindowInfo")); + + QDBusPendingReply async = QDBusConnection::sessionBus().asyncCall(message); + + QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(async, this); + connect(callWatcher, &QDBusPendingCallWatcher::finished, this, + [this](QDBusPendingCallWatcher *self) { + QDBusPendingReply reply = *self; + self->deleteLater(); + if (!reply.isValid()) { + return; + } + const QVariantMap windowInfo = reply.value(); + setWindowProperties(windowInfo); + } + ); +} + +void RulesModel::updateVirtualDesktops() +{ + QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.KWin"), + QStringLiteral("/VirtualDesktopManager"), + QStringLiteral("org.freedesktop.DBus.Properties"), + QStringLiteral("Get")); + message.setArguments(QVariantList{ + QStringLiteral("org.kde.KWin.VirtualDesktopManager"), + QStringLiteral("desktops") + }); + + QDBusPendingReply async = QDBusConnection::sessionBus().asyncCall(message); + + QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(async, this); + connect(callWatcher, &QDBusPendingCallWatcher::finished, this, + [this](QDBusPendingCallWatcher *self) { + QDBusPendingReply reply = *self; + self->deleteLater(); + if (!reply.isValid()) { + return; + } + m_virtualDesktops = qdbus_cast(reply.value()); + emit virtualDesktopsUpdated(); + } + ); +} + + +} //namespace diff --git a/kcmkwin/kwinrules/rulesmodel.h b/kcmkwin/kwinrules/rulesmodel.h new file mode 100644 index 0000000..8e441ad --- /dev/null +++ b/kcmkwin/kwinrules/rulesmodel.h @@ -0,0 +1,121 @@ +/* + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#ifndef KWIN_RULES_MODEL_H +#define KWIN_RULES_MODEL_H + +#include "ruleitem.h" +#include +#include +#include + +#include +#include +#include + +#ifdef KWIN_BUILD_ACTIVITIES +#include +#endif + + +namespace KWin +{ + +class RulesModel : public QAbstractListModel +{ + Q_OBJECT + + Q_PROPERTY(QString description READ description WRITE setDescription NOTIFY descriptionChanged) + Q_PROPERTY(QString warningMessage READ warningMessage NOTIFY warningMessageChanged) + +public: + enum RulesRole { + NameRole = Qt::DisplayRole, + DescriptionRole = Qt::ToolTipRole, + IconRole = Qt::DecorationRole, + IconNameRole = Qt::UserRole + 1, + KeyRole, + SectionRole, + EnabledRole, + SelectableRole, + ValueRole, + TypeRole, + PolicyRole, + PolicyModelRole, + OptionsModelRole, + OptionsMaskRole, + SuggestedValueRole + }; + Q_ENUM(RulesRole) + +public: + explicit RulesModel(QObject *parent = nullptr); + ~RulesModel(); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QHash roleNames() const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + bool setData(const QModelIndex & index, const QVariant & value, int role) override; + + QModelIndex indexOf(const QString &key) const; + bool hasRule(const QString &key) const; + RuleItem *ruleItem(const QString &key) const; + + void readFromSettings(RuleSettings *settings); + void writeToSettings(RuleSettings *settings) const; + + void importFromRules(Rules *rules); + Rules *exportToRules() const; + + void setWindowProperties(const QVariantMap &info, bool forceValue = false); + + QString description() const; + void setDescription(const QString &description); + QString warningMessage() const; + + +public slots: + void detectWindowProperties(int secs); + +signals: + void descriptionChanged(); + void warningMessageChanged(); + void suggestionsChanged(); + + void virtualDesktopsUpdated(); + +private: + void populateRuleList(); + bool wmclassWarning() const; + RuleItem *addRule(RuleItem *rule); + QString defaultDescription() const; + void processSuggestion(const QString &key, const QVariant &value); + + static const QHash x11PropertyHash(); + void updateVirtualDesktops(); + + QList windowTypesModelData() const; + QList virtualDesktopsModelData() const; + QList activitiesModelData() const; + QList placementModelData() const; + QList focusModelData() const; + QList colorSchemesModelData() const; + +private slots: + void selectX11Window(); + +private: + QList m_ruleList; + QHash m_rules; + DBusDesktopDataVector m_virtualDesktops; +#ifdef KWIN_BUILD_ACTIVITIES + KActivities::Consumer *m_activities; +#endif +}; + +} + +#endif diff --git a/kcmkwin/kwinscreenedges/CMakeLists.txt b/kcmkwin/kwinscreenedges/CMakeLists.txt new file mode 100644 index 0000000..69a7ed0 --- /dev/null +++ b/kcmkwin/kwinscreenedges/CMakeLists.txt @@ -0,0 +1,39 @@ +# KI18N Translation Domain for this library +add_definitions(-DTRANSLATION_DOMAIN=\"kcmkwinscreenedges\") + +include_directories(${KWin_SOURCE_DIR}/effects) +set(kcm_screenedges_SRCS + monitor.cpp + screenpreviewwidget.cpp + kwinscreenedge.cpp + kwinscreenedgeconfigform.cpp +) +qt5_add_dbus_interface(kcm_screenedges_SRCS ${KWin_SOURCE_DIR}/org.kde.kwin.Effects.xml kwin_effects_interface) + +set(kcm_kwinscreenedges_PART_SRCS main.cpp ${kcm_screenedges_SRCS}) +ki18n_wrap_ui(kcm_kwinscreenedges_PART_SRCS main.ui) +kconfig_add_kcfg_files(kcm_kwinscreenedges_PART_SRCS kwinscreenedgesettings.kcfgc kwinscreenedgescriptsettings.kcfgc) +add_library(kcm_kwinscreenedges MODULE ${kcm_kwinscreenedges_PART_SRCS}) +set(kcm_screenedges_LIBS + Qt5::DBus + + KF5::Completion + KF5::ConfigCore + KF5::ConfigWidgets + KF5::I18n + KF5::Package + KF5::Plasma + KF5::Service + + kwin4_effect_builtins +) +target_link_libraries(kcm_kwinscreenedges ${X11_LIBRARIES} ${kcm_screenedges_LIBS}) + +set(kcm_kwintouchscreenedges_PART_SRCS touch.cpp kwintouchscreenedgeconfigform.cpp ${kcm_screenedges_SRCS}) +ki18n_wrap_ui(kcm_kwintouchscreenedges_PART_SRCS main.ui touch.ui) +kconfig_add_kcfg_files(kcm_kwintouchscreenedges_PART_SRCS kwintouchscreensettings.kcfgc kwintouchscreenscriptsettings.kcfgc) +add_library(kcm_kwintouchscreen MODULE ${kcm_kwintouchscreenedges_PART_SRCS}) +target_link_libraries(kcm_kwintouchscreen ${X11_LIBRARIES} ${kcm_screenedges_LIBS}) + +install(TARGETS kcm_kwinscreenedges kcm_kwintouchscreen DESTINATION ${PLUGIN_INSTALL_DIR}) +install(FILES kwinscreenedges.desktop kwintouchscreen.desktop DESTINATION ${SERVICES_INSTALL_DIR}) diff --git a/kcmkwin/kwinscreenedges/Messages.sh b/kcmkwin/kwinscreenedges/Messages.sh new file mode 100644 index 0000000..2ab0771 --- /dev/null +++ b/kcmkwin/kwinscreenedges/Messages.sh @@ -0,0 +1,4 @@ +#! /usr/bin/env bash +$EXTRACTRC *.ui >> rc.cpp || exit 11 +$XGETTEXT *.cpp -o $podir/kcmkwinscreenedges.pot +rm -f rc.cpp diff --git a/kcmkwin/kwinscreenedges/kwinscreenedge.cpp b/kcmkwin/kwinscreenedges/kwinscreenedge.cpp new file mode 100644 index 0000000..e71e907 --- /dev/null +++ b/kcmkwin/kwinscreenedges/kwinscreenedge.cpp @@ -0,0 +1,222 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2020 Cyril Rossi + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwinscreenedge.h" + +#include "monitor.h" + +namespace KWin +{ + +KWinScreenEdge::KWinScreenEdge(QWidget *parent) + : QWidget(parent) +{ + QMetaObject::invokeMethod(this, "createConnection", Qt::QueuedConnection); +} + +KWinScreenEdge::~KWinScreenEdge() +{ +} + +void KWinScreenEdge::monitorHideEdge(ElectricBorder border, bool hidden) +{ + const int edge = KWinScreenEdge::electricBorderToMonitorEdge(border); + monitor()->setEdgeHidden(edge, hidden); +} + +void KWinScreenEdge::monitorEnableEdge(ElectricBorder border, bool enabled) +{ + const int edge = KWinScreenEdge::electricBorderToMonitorEdge(border); + monitor()->setEdgeEnabled(edge, enabled); +} + +void KWinScreenEdge::monitorAddItem(const QString &item) +{ + for (int i = 0; i < 8; i++) { + monitor()->addEdgeItem(i, item); + } +} + +void KWinScreenEdge::monitorItemSetEnabled(int index, bool enabled) +{ + for (int i = 0; i < 8; i++) { + monitor()->setEdgeItemEnabled(i, index, enabled); + } +} + +void KWinScreenEdge::monitorChangeEdge(const QList &borderList, int index) +{ + for (int border : borderList) { + monitorChangeEdge(static_cast(border), index); + } +} + +void KWinScreenEdge::monitorChangeEdge(ElectricBorder border, int index) +{ + if (ELECTRIC_COUNT == border || ElectricNone == border) { + return; + } + m_reference[border] = index; + monitor()->selectEdgeItem(KWinScreenEdge::electricBorderToMonitorEdge(border), index); +} + +QList KWinScreenEdge::monitorCheckEffectHasEdge(int index) const +{ + QList list; + if (monitor()->selectedEdgeItem(Monitor::Top) == index) { + list.append(ElectricTop); + } + if (monitor()->selectedEdgeItem(Monitor::TopRight) == index) { + list.append(ElectricTopRight); + } + if (monitor()->selectedEdgeItem(Monitor::Right) == index) { + list.append(ElectricRight); + } + if (monitor()->selectedEdgeItem(Monitor::BottomRight) == index) { + list.append(ElectricBottomRight); + } + if (monitor()->selectedEdgeItem(Monitor::Bottom) == index) { + list.append(ElectricBottom); + } + if (monitor()->selectedEdgeItem(Monitor::BottomLeft) == index) { + list.append(ElectricBottomLeft); + } + if (monitor()->selectedEdgeItem(Monitor::Left) == index) { + list.append(ElectricLeft); + } + if (monitor()->selectedEdgeItem(Monitor::TopLeft) == index) { + list.append(ElectricTopLeft); + } + + if (list.isEmpty()) { + list.append(ElectricNone); + } + return list; +} + +int KWinScreenEdge::selectedEdgeItem(ElectricBorder border) const +{ + return monitor()->selectedEdgeItem(KWinScreenEdge::electricBorderToMonitorEdge(border)); +} + +void KWinScreenEdge::monitorChangeDefaultEdge(ElectricBorder border, int index) +{ + if (ELECTRIC_COUNT == border || ElectricNone == border) { + return; + } + m_default[border] = index; +} + +void KWinScreenEdge::monitorChangeDefaultEdge(const QList &borderList, int index) +{ + for (int border : borderList) { + monitorChangeDefaultEdge(static_cast(border), index); + } +} + +void KWinScreenEdge::reload() +{ + for (auto it = m_reference.cbegin(); it != m_reference.cend(); ++it) { + monitor()->selectEdgeItem(KWinScreenEdge::electricBorderToMonitorEdge(it.key()), it.value()); + } + onChanged(); +} + +void KWinScreenEdge::setDefaults() +{ + for (auto it = m_default.cbegin(); it != m_default.cend(); ++it) { + monitor()->selectEdgeItem(KWinScreenEdge::electricBorderToMonitorEdge(it.key()), it.value()); + } + onChanged(); +} + +int KWinScreenEdge::electricBorderToMonitorEdge(ElectricBorder border) +{ + switch(border) { + case ElectricTop: + return Monitor::Top; + case ElectricTopRight: + return Monitor::TopRight; + case ElectricRight: + return Monitor::Right; + case ElectricBottomRight: + return Monitor::BottomRight; + case ElectricBottom: + return Monitor::Bottom; + case ElectricBottomLeft: + return Monitor::BottomLeft; + case ElectricLeft: + return Monitor::Left; + case ElectricTopLeft: + return Monitor::TopLeft; + default: // ELECTRIC_COUNT and ElectricNone + return Monitor::None; + } +} + +ElectricBorder KWinScreenEdge::monitorEdgeToElectricBorder(int edge) +{ + const Monitor::Edges monitorEdge = static_cast(edge); + switch (monitorEdge) { + case Monitor::Left: + return ElectricLeft; + case Monitor::Right: + return ElectricRight; + case Monitor::Top: + return ElectricTop; + case Monitor::Bottom: + return ElectricBottom; + case Monitor::TopLeft: + return ElectricTopLeft; + case Monitor::TopRight: + return ElectricTopRight; + case Monitor::BottomLeft: + return ElectricBottomLeft; + case Monitor::BottomRight: + return ElectricBottomRight; + default: + return ElectricNone; + } +} + +void KWinScreenEdge::onChanged() +{ + bool needSave = isSaveNeeded(); + for (auto it = m_reference.cbegin(); it != m_reference.cend(); ++it) { + needSave |= it.value() != monitor()->selectedEdgeItem(KWinScreenEdge::electricBorderToMonitorEdge(it.key())); + } + emit saveNeededChanged(needSave); + + bool defaults = isDefault(); + for (auto it = m_default.cbegin(); it != m_default.cend(); ++it) { + defaults &= it.value() == monitor()->selectedEdgeItem(KWinScreenEdge::electricBorderToMonitorEdge(it.key())); + } + emit defaultChanged(defaults); +} + +void KWinScreenEdge::createConnection() +{ + connect(monitor(), + &Monitor::changed, + this, + &KWinScreenEdge::onChanged); +} + +bool KWinScreenEdge::isSaveNeeded() const +{ + return false; +} + +bool KWinScreenEdge::isDefault() const +{ + return true; +} + +} // namespace diff --git a/kcmkwin/kwinscreenedges/kwinscreenedge.h b/kcmkwin/kwinscreenedges/kwinscreenedge.h new file mode 100644 index 0000000..48bd73c --- /dev/null +++ b/kcmkwin/kwinscreenedges/kwinscreenedge.h @@ -0,0 +1,75 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2020 Cyril Rossi + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef __KWINSCREENEDGE_H__ +#define __KWINSCREENEDGE_H__ + +#include + +#include "kwinglobals.h" + +namespace KWin +{ + +class Monitor; + +class KWinScreenEdge : public QWidget +{ + Q_OBJECT + +public: + explicit KWinScreenEdge(QWidget *parent = nullptr); + ~KWinScreenEdge() override; + + void monitorHideEdge(ElectricBorder border, bool hidden); + void monitorEnableEdge(ElectricBorder border, bool enabled); + + void monitorAddItem(const QString &item); + void monitorItemSetEnabled(int index, bool enabled); + + QList monitorCheckEffectHasEdge(int index) const; + int selectedEdgeItem(ElectricBorder border) const; + + void monitorChangeEdge(ElectricBorder border, int index); + void monitorChangeEdge(const QList &borderList, int index); + + void monitorChangeDefaultEdge(ElectricBorder border, int index); + void monitorChangeDefaultEdge(const QList &borderList, int index); + + // revert to reference settings and assess for saveNeeded and default changed + virtual void reload(); + // reset to default settings and assess for saveNeeded and default changed + virtual void setDefaults(); + +private Q_SLOTS: + void onChanged(); + void createConnection(); + +Q_SIGNALS: + void saveNeededChanged(bool isNeeded); + void defaultChanged(bool isDefault); + +private: + virtual Monitor *monitor() const = 0; + virtual bool isSaveNeeded() const; + virtual bool isDefault() const; + + // internal use, return Monitor::None if border equals ELECTRIC_COUNT or ElectricNone + static int electricBorderToMonitorEdge(ElectricBorder border); + static ElectricBorder monitorEdgeToElectricBorder(int edge); + +private: + QHash m_reference; // reference settings + QHash m_default; // default settings +}; + +} // namespace + +#endif diff --git a/kcmkwin/kwinscreenedges/kwinscreenedgeconfigform.cpp b/kcmkwin/kwinscreenedges/kwinscreenedgeconfigform.cpp new file mode 100644 index 0000000..86e8a04 --- /dev/null +++ b/kcmkwin/kwinscreenedges/kwinscreenedgeconfigform.cpp @@ -0,0 +1,104 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2020 Cyril Rossi + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwinscreenedgeconfigform.h" +#include "ui_main.h" + +namespace KWin +{ + +KWinScreenEdgesConfigForm::KWinScreenEdgesConfigForm(QWidget *parent) + : KWinScreenEdge(parent) + , ui(new Ui::KWinScreenEdgesConfigUI) +{ + ui->setupUi(this); + + connect(ui->kcfg_ElectricBorderDelay, SIGNAL(valueChanged(int)), this, SLOT(sanitizeCooldown())); + + // Visual feedback of action group conflicts + connect(ui->kcfg_ElectricBorders, SIGNAL(currentIndexChanged(int)), this, SLOT(groupChanged())); + connect(ui->kcfg_ElectricBorderMaximize, SIGNAL(stateChanged(int)), this, SLOT(groupChanged())); + connect(ui->kcfg_ElectricBorderTiling, SIGNAL(stateChanged(int)), this, SLOT(groupChanged())); + + connect(ui->electricBorderCornerRatioSpin, SIGNAL(valueChanged(int)), this, SLOT(onChanged())); +} + +KWinScreenEdgesConfigForm::~KWinScreenEdgesConfigForm() +{ + delete ui; +} + +void KWinScreenEdgesConfigForm::setElectricBorderCornerRatio(double value) +{ + m_referenceCornerRatio = value; + ui->electricBorderCornerRatioSpin->setValue(m_referenceCornerRatio * 100.); +} + +void KWinScreenEdgesConfigForm::setDefaultElectricBorderCornerRatio(double value) +{ + m_defaultCornerRatio = value; +} + +double KWinScreenEdgesConfigForm::electricBorderCornerRatio() const +{ + return ui->electricBorderCornerRatioSpin->value() / 100.; +} + +void KWinScreenEdgesConfigForm::setElectricBorderCornerRatioEnabled(bool enable) +{ + return ui->electricBorderCornerRatioSpin->setEnabled(enable); +} + +void KWinScreenEdgesConfigForm::reload() +{ + ui->electricBorderCornerRatioSpin->setValue(m_referenceCornerRatio * 100.); + KWinScreenEdge::reload(); +} + +void KWinScreenEdgesConfigForm::setDefaults() +{ + ui->electricBorderCornerRatioSpin->setValue(m_defaultCornerRatio * 100.); + KWinScreenEdge::setDefaults(); +} + +Monitor *KWinScreenEdgesConfigForm::monitor() const +{ + return ui->monitor; +} + +bool KWinScreenEdgesConfigForm::isSaveNeeded() const +{ + return m_referenceCornerRatio != electricBorderCornerRatio(); +} + +bool KWinScreenEdgesConfigForm::isDefault() const +{ + return m_defaultCornerRatio == electricBorderCornerRatio(); +} + +void KWinScreenEdgesConfigForm::sanitizeCooldown() +{ + ui->kcfg_ElectricBorderCooldown->setMinimum(ui->kcfg_ElectricBorderDelay->value() + 50); +} + +void KWinScreenEdgesConfigForm::groupChanged() +{ + // Monitor conflicts + bool hide = false; + if (ui->kcfg_ElectricBorders->currentIndex() == 2) { + hide = true; + } + monitorHideEdge(ElectricTop, hide); + monitorHideEdge(ElectricRight, hide); + monitorHideEdge(ElectricBottom, hide); + monitorHideEdge(ElectricLeft, hide); +} + +} // namespace diff --git a/kcmkwin/kwinscreenedges/kwinscreenedgeconfigform.h b/kcmkwin/kwinscreenedges/kwinscreenedgeconfigform.h new file mode 100644 index 0000000..bac5636 --- /dev/null +++ b/kcmkwin/kwinscreenedges/kwinscreenedgeconfigform.h @@ -0,0 +1,63 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2020 Cyril Rossi + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef __KWINSCREENEDGECONFIGFORM_H__ +#define __KWINSCREENEDGECONFIGFORM_H__ + +#include "kwinscreenedge.h" + +namespace Ui +{ +class KWinScreenEdgesConfigUI; +} + +namespace KWin +{ + +class KWinScreenEdgesConfigForm : public KWinScreenEdge +{ + Q_OBJECT + +public: + KWinScreenEdgesConfigForm(QWidget *parent = nullptr); + ~KWinScreenEdgesConfigForm() override; + + // value is between 0. and 1. + void setElectricBorderCornerRatio(double value); + void setDefaultElectricBorderCornerRatio(double value); + + // return value between 0. and 1. + double electricBorderCornerRatio() const; + + void setElectricBorderCornerRatioEnabled(bool enable); + + void reload() override; + void setDefaults() override; + +protected: + Monitor *monitor() const override; + bool isSaveNeeded() const override; + bool isDefault() const override; + +private Q_SLOTS: + void sanitizeCooldown(); + void groupChanged(); + +private: + // electricBorderCornerRatio value between 0. and 1. + double m_referenceCornerRatio = 0.; + double m_defaultCornerRatio = 0.; + + Ui::KWinScreenEdgesConfigUI *ui; +}; + +} // namespace + +#endif diff --git a/kcmkwin/kwinscreenedges/kwinscreenedges.desktop b/kcmkwin/kwinscreenedges/kwinscreenedges.desktop new file mode 100644 index 0000000..122d6c5 --- /dev/null +++ b/kcmkwin/kwinscreenedges/kwinscreenedges.desktop @@ -0,0 +1,157 @@ +[Desktop Entry] +Exec=kcmshell5 kwinscreenedges +Icon=preferences-desktop-gestures-screenedges +Type=Service +X-KDE-ServiceTypes=KCModule +X-DocPath=kcontrol/kwinscreenedges/index.html + +X-KDE-Library=kcm_kwinscreenedges +X-KDE-ParentApp=kcontrol + +X-KDE-System-Settings-Parent-Category=desktopbehavior +X-KDE-Weight=50 + +Name=Screen Edges +Name[ar]=حواف الشاشة +Name[az]=Pəncərə kənarları +Name[bg]=Краища на екрана +Name[bs]=Ivice ekrana +Name[ca]=Vores de la pantalla +Name[ca@valencia]=Vores de la pantalla +Name[cs]=Hrany obrazovky +Name[csb]=Nórtë ekranu +Name[da]=Skærmkanter +Name[de]=Bildschirmränder +Name[el]=Άκρα οθόνης +Name[en_GB]=Screen Edges +Name[eo]=Ekrananguloj +Name[es]=Bordes de pantalla +Name[et]=Ekraani servad +Name[eu]=Pantailaren ertzak +Name[fi]=Näytön reunat +Name[fr]=Bords de l'écran +Name[fy]=Skerm rânen +Name[ga]=Ciumhaiseanna Scáileáin +Name[gl]=Bordos da pantalla +Name[gu]=સ્ક્રિનનાં ખૂણાઓ +Name[he]=קצוות מסך +Name[hi]=स्क्रीन सीमाएँ +Name[hr]=Rubovi ekrana +Name[hu]=Képernyőszélek +Name[ia]=Margines de schermo +Name[id]=Tepian Layar +Name[is]=Skjájaðrar +Name[it]=Lati dello schermo +Name[ja]=スクリーンエッジ +Name[kk]=Экран жиектері +Name[km]=គែម​អេក្រង់​ +Name[kn]=ತೆರೆ ಅಂಚುಗಳು +Name[ko]=화면 경계 +Name[lt]=Ekrano kraÅ¡tai +Name[lv]=Ekrāna malas +Name[mai]=स्क्रीन किनार +Name[mk]=Рабови на екранот +Name[ml]=സ്ക്രീന്‍ അതിരുകള്‍ +Name[mr]=स्क्रीनच्या कडा +Name[nb]=Skjermkanter +Name[nds]=Schirmkanten +Name[nl]=Schermranden +Name[nn]=Skjerm­kantar +Name[pa]=ਸਕਰੀਨ ਬਾਹੀਆਂ +Name[pl]=Krawędzie ekranu +Name[pt]=Extremos do Ecrã +Name[pt_BR]=Contornos da tela +Name[ro]=Marginile ecranului +Name[ru]=Края экрана +Name[si]=තිර මුළු +Name[sk]=Okraje obrazovky +Name[sl]=Robovi zaslona +Name[sr]=Ивице екрана +Name[sr@ijekavian]=Ивице екрана +Name[sr@ijekavianlatin]=Ivice ekrana +Name[sr@latin]=Ivice ekrana +Name[sv]=Skärmkanter +Name[tg]=Канорҳои экран +Name[th]=ขอบจอ +Name[tr]=Ekran Kenarları +Name[ug]=ئېكران گىرۋەكلىرى +Name[uk]=Краї екрана +Name[wa]=Boirds del waitroûle +Name[x-test]=xxScreen Edgesxx +Name[zh_CN]=屏幕边缘 +Name[zh_TW]=螢幕邊緣 + +Comment=Configure active screen corners and edges +Comment[az]=Aktiv ekran künclərini və kənarlarını tənzimləmək +Comment[ca]=Configura les cantonades i vores actives de la pantalla +Comment[da]=Indstil aktive skærmhjørner- og kanter +Comment[de]=Aktive Bildschirmränder und -ecken einrichten +Comment[en_GB]=Configure active screen corners and edges +Comment[es]=Configurar los bordes y las esquinas de pantalla activos +Comment[et]=Aktiivsete ekraani nurkade ja servade seadistamine +Comment[eu]=Konfiguratu pantailaren izkina eta ertz aktiboak +Comment[fi]=Aseta näytön aktiiviset reunat ja kulmat +Comment[fr]=Configurer les bords et les coins d'écran actifs +Comment[gl]=Configurar os bordos e esquinas activos da pantalla +Comment[hu]=Aktív képernyősarkok és szélek beállítása +Comment[ia]=Configura margines e angulos de schermo active +Comment[id]=Konfigurasikan tepian dan sudut layar aktif +Comment[it]=Configura angoli e bordi attivi dello schermo +Comment[ko]=활성 화면 경계와 꼭짓점 설정 +Comment[lt]=KonfigÅ«ruoti aktyvaus ekrano kampus ir kraÅ¡tus +Comment[nl]=Actieve schermhoeken en -randen configureren +Comment[nn]=Set opp aktive skjermhjørne og skjermkantar +Comment[pl]=Ustawienia czułych narożników i krawędzi ekranu +Comment[pt]=Configurar os cantos e extremos do ecrã activo +Comment[pt_BR]=Configura os cantos e bordas da tela ativa +Comment[ro]=Configurează colțurile și marginile active ale ecranului +Comment[ru]=Настройка действий для краёв и углов экрана +Comment[sk]=Nastavenie aktívnych okrajov obrazovky a hrán +Comment[sl]=Nastavi dejavne kote in robove okna +Comment[sv]=Anpassa aktiva skärmhörn och kanter +Comment[uk]=Налаштовування активних кутів і країв екрана +Comment[x-test]=xxConfigure active screen corners and edgesxx +Comment[zh_CN]=配置活动屏幕的边角和边缘 +Comment[zh_TW]=設定作用中螢幕角落與邊緣 + +X-KDE-Keywords=kwin,window,manager,effect,corner,edge,border,action,switch,desktop,kwin screen edges,desktop edges,screen edges,maximize windows,tile windows,side of screen,screen behavior,switch desktop,virtual desktop,screen corners +X-KDE-Keywords[az]=pəncərə,menecer,efekt,künc,kənar,sərhəd,fəaliyyət,əməl,keçiş,kwin ekran kənarları,iş masası kənarlar,ekran kənarları,pəncərənin genişlənməsi,ekranın yanları,mozaik pəncərələr,ekran davranışları,iş masasını dəyişmə,virtual iş masalarıekran küncləri +X-KDE-Keywords[bs]=kwin,window,manager,effect,corner,edge,border,action,switch,desktop,kwin screen edges,desktop edges,screen edges,maximize windows,tile windows,side of screen,screen behavior,switch desktop,virtual desktop,screen corners, prozor, menadćer, efekt, uigao, ivica, akcija, prebacivanje, desktop ivice, ivice ekrana, maksimitiranje prozora, ponaÅ¡anje ekrana, virtualni desktop +X-KDE-Keywords[ca]=kwin,finestra,gestor,efecte,cantonada,vora,acció,canvi,escriptori,vores de la pantalla del kwin,vores de l'escriptori,vores de la pantalla,maximitza les finestres,finestres en mosaic,costat de la pantalla,comportament de la pantalla,canvi d'escriptori,escriptori virtual,cantonades de la pantalla +X-KDE-Keywords[ca@valencia]=kwin,finestra,gestor,efecte,cantó,vora,acció,canvi,escriptori,vores de pantalla de kwin,vores d'escriptori,vores de pantalla,maximitza finestres,mosaic de les finestres,costat de pantalla,comportament de pantalla,canvi d'escriptori,escriptori virtual,cantons de la pantalla +X-KDE-Keywords[da]=kwin,vindue,hÃ¥ndtering,manager,effekt,hjørne,kant,handling,skift,skrivebord,kwin skærmkanter,skrivebordskanter,skærmkanter,maksimer vinduer,tile windows,fliser,felter,siden af skærmen,skærmens opførsel,skift skrivebord,virtuelle skriveborde,skærmhjørner,hjørner +X-KDE-Keywords[de]=KWin,Fenster,Verwaltung,Effekt,Kante,Rand,Aktion,Wechseln,Desktop,Arbeitsfläche,KWin Bildschirmkanten,Desktopkanten,Bildschirmkanten,Fenster maximieren,Fenster kacheln,Bildschirmseite,Bildschirmverhalten,Desktop wechseln,Arbeitsfläche wechseln,Virtueller Desktop,Virtuelle Arbeitsfläche,Bildschirmecken +X-KDE-Keywords[el]=kwin,παράθυρο,διαχειριστής,εφέ,άκρη,περίγραμμα,ενέργεια,εναλλαγή,επιφάνεια εργασίας,άκρες οθόνης kwin,άκρες επιφάνειας εργασίας,άκρες οθόνης,μεγιστοποίηση παραθύρων,παράθεση παραθύρων,πλευρά οθόνης,συμπεριφορά οθόνης,εναλλαγή επιφάνειας εργασίας,εικονική επιφάνεια εργασίας,γωνίες οθόνης +X-KDE-Keywords[en_GB]=kwin,window,manager,effect,corner,edge,border,action,switch,desktop,kwin screen edges,desktop edges,screen edges,maximise windows,tile windows,side of screen,screen behaviour,switch desktop,virtual desktop,screen corners +X-KDE-Keywords[es]=kwin,ventana,gestor,efecto,esquina,borde,acción,cambiar,escritorio,bordes de pantalla de kwin,bordes del escritorio,bordes de la pantalla,maximizar ventanas,ventanas en mosaico,lado de la pantalla,comportamiento de la pantalla,cambiar escritorio,escritorio virtual,esquinas de la pantalla +X-KDE-Keywords[et]=kwin,aken,haldur,efekt.nurk,serv,piire,toiming,lülitamine,töölaud,kwini ekraani servad,töölaua servad,ekraani servad,akende maksimeerimine,akende paanimine,ekraani äär,ekraani käitumine,töölaua lülitamine,virtuaalne töölaud,ekraani nurgad +X-KDE-Keywords[eu]=kwin,leiho,kudeatzaile,efektu,izkin,ertz,ekintza,aldatu,mahaigain,kwin pantailaren ertzak,mahaigainaren ertzak,pantailen ertzak,maximizatu leihoak,lauzatu leihoak,leihoaren alboa,pantailaren portaera,aldatu mahaigaina,mahaigain birtuala,alegiazko mahaigaina,pantailaren izkinak +X-KDE-Keywords[fi]=kwin,window,manager,effect,corner,edge,border,action,switch,desktop,kwin screen edges,desktop edges,screen edges,maximize windows,tile windows,side of screen,screen behavior,switch desktop,virtual desktop,screen corners,ikkunamanageri,ikkunointiohjelma,tehoste,kulma,reuna,vaihda,vaihto,työpöytä,näytön reunat,työpöydän reunat,suurenna ikkuna,kasaa ikkunat,näytön toiminta,vaihda työpöytää,virtuaalityöpöytä,näytön reunat +X-KDE-Keywords[fr]=kwin, fenêtre, gestionnaire, effet, bord, coin, bordure, action, bascule, bureau, bords de l'écran kwin, bords du bureau, bords de l'écran, maximisation des fenêtres, mosaïque de fenêtres, cotés de l'écran, comportement de l'écran, changement de bureau, bureaux virtuels, coins de l'écran +X-KDE-Keywords[gl]=kwin,xanela,xestor,efecto,esquina,beira,bordo,bordo,acción,trocar,escritorio,bordo do escritorio,maximizar xanelas,escritorio virtual,esquinas da pantalla +X-KDE-Keywords[hu]=kwin,ablak,kezelő,effektus,sarok,szél,szegély,művelet,váltás,asztal,kwin képernyőszél,asztalszél,képernyőszél,ablakok maximalizálása,ablakcím,képernyőoldal,képernyő működése,asztalváltás,virtuális asztal,képernyősarkok +X-KDE-Keywords[ia]=kwin,fenestra,gerente,effecto,bordo,margine,action,commuta,scriptorio,bordos de schermo de kwin,bordos de scriptorio,bordos de scriptorio,maximisa fenestras,tegula fenestras,parte de schermo, comportamento de schermo,commuta scriptorio,scriptorio virtual,angulos de schermo +X-KDE-Keywords[id]=kwin,window,pengelola,efek,sudut,,tepi,bingkai,aksi,alihkan,desktop,tepian layar kwin,tepian desktop,tepian layar,maksimalkan window,ubinkan window,sisi layar,perilaku layar,alihkan desktop,virtualkan desktop,sudut layar +X-KDE-Keywords[it]=kwin,finestra,gestore,effetto,angolo,bordo,azione,scambiatore,desktop,bordi schermo kwin,bordi desktop,bordi schermo,massimizza finestre,affianca finestre,lato dello schermo,comportamento schermo,scambia desktop,desktop virtuale,angoli dello schermo +X-KDE-Keywords[ko]=kwin,window,manager,effect,corner,edge,border,action,switch,desktop,kwin screen edges,desktop edges,screen edges,maximize windows,tile windows,side of screen,screen behavior,switch desktop,virtual desktop,screen corners,ì°½,관리자,효과,경계,경계선,동작,액션,전환,kwin 화면 경계,화면 경계,ì°½ 최대화,최대화,바둑판식 배열,화면 행동,데스크톱 전환,가상 데스크톱,화면 모서리,꼭짓점,바탕 화면 전환,가상 바탕 화면 +X-KDE-Keywords[lt]=kwin,langas,langų,lango,langu,tvarkytuvė,tvarkytuve,efektas,efektai,kampas,kampai,kraÅ¡tas,krastas,veiksmas,veiksmai,perjungti,perjungiklis,darbalaukis,darbalaukio,kwin ekrano kraÅ¡tai,kwin ekrano kraÅ¡tas,kwin ekrano krastas,kwin ekrano krastai,darbalaukio kraÅ¡tai,darbalaukio kraÅ¡tas,darbalaukio krastas,darbalaukio krastai,ekrano kraÅ¡tai,ekrano krastai,ekrano kraÅ¡tas,ekrano krastas,iÅ¡skleisti langus,isskleisti langus,langų iÅ¡skleidimas,langu isskleidimas,lango iÅ¡skleidimas,lango isskleidimas,iÅ¡skleisti langą,isskleisti langa,iÅ¡kloti langą,iÅ¡kloti langus,iskloti langa,iskloti langus,ekrano Å¡onas,ekrano sonas,ekrano pusė,ekrano puse,ekrano Å¡onai,ekrano sonai,ekrano elgsena,ekrano elgesys,perjungti darbalaukį,perjungti darbalauki,darbalaukio perjungimas,virtualus darbalaukis,virtualÅ«s darbalaukiai,virtualus darbalaukiai,ekrano kampas,ekrano kampai +X-KDE-Keywords[nb]=kwin,vindu,behandler,effekt,kant,hjørne,ramme,handling,bytte,skrivebord,kwin skjermkanter,skrivebordkanter,skjermkanter,maksimere vinduer,flislegge vinduer,skjermside,skjermoppførsel,bytte skrivebord,virtuelt skrivebord,skjermhjørner +X-KDE-Keywords[nds]=KWin,Finster,Pleger,Effekt,Hörn,Kant,Rahmen,Akschoon,wesseln,Schriefdisch,maximeren,kacheln,Schirmkant,Schirmbedregen,Schirmhuuk +X-KDE-Keywords[nl]=kwin,venster,beheerder,effect,hoek,kant,rand,actie,omschakelen,bureaublad,schermranden van kwin,bureaubladkanten,schermkanten,vensters maximaliseren,venster schuin achter elkaar,zijkant van het scherm,schermgedrag,bureaublad omschakelen,virtueel bureaublad,schermhoeken +X-KDE-Keywords[nn]=kwin,vindauge,handsamar,effekt,kant,hjørne,ramme,handling,byte,skrivebord,kwin skjermkantar,skrivebordskantar,skjermkantar,maksimera vindauge,flisleggja vindauge,skjermside,skjermÃ¥tferd,byte skrivebord,virtuelt skrivebord,skjermhjørne +X-KDE-Keywords[pl]=kwin,okno,menadżer,efekt,narożnik,krawędź,obramowanie,działanie,przełącz,pulpit, krawędzie ekranu kwin,krawędzie pulpitu,krawędzie ekranu,maksymalizacja okien, kafelkowanie okien,strona ekranu,zachowanie ekranu,przełączanie pulpitu,wirtualny pulpit, krawędzie ekranu +X-KDE-Keywords[pt]=kwin,janela,gestor,efeito,extremo,contorno,acção,mudar,ecrã,extremos do ecrã no kwin,extremos do ecrã,maximizar as janelas,janelas lado-a-lado,lado do ecrã,comportamento do ecrã,mudar de ecrã,ecrã virtual,cantos do ecrã +X-KDE-Keywords[pt_BR]=kwin,janela,gerenciador,efeito,canto,contorno,borda,ação,mudar,área de trabalho,cantos da área de trabalho no kwin,cantos da área de trabalho,maximizar as janelas,janelas lado a lado,lado da tela,comportamento da tela,mudar de área de trabalho virtual,desktop,cantos da tela +X-KDE-Keywords[ru]=kwin,window,manager,effect,corner,edge,border,action,switch,desktop,kwin screen edges,desktop edges,screen edges,maximize windows,tile windows,side of screen,screen behavior,switch desktop,virtual desktop,screen corners,окно,окон,диспетчер,эффект,край,граница,действие,переключить,рабочий стол,края экрана kwin,края экрана,края рабочего стола,распахнуть окна,мозаика окон,край экрана,поведение экрана,переключить рабочий стол,виртуальный рабочий стол,углы рабочего стола,углы экрана +X-KDE-Keywords[sk]=kwin,okno, správca,efekt,okraj,hrana,akcia,prepnúť,plocha,okraje obrazovky kwin, efekty plochy,okraje obrazovky,maximalizovaÅ¥ okná,dlaždicové okná,strana obrazovky, správanie obrazovky,prepnúť plochu,virtuálna plocha,okraje obrazovky +X-KDE-Keywords[sl]=kwin,okno,upravljalnik oken,upravljanje oken,učinki,kot,rob,obroba,dejanje,preklopi,preklapljanje,robovi zaslona,robovi namizja,razpni okna,povečaj okna,tlakuj okna,rob zaslona,obnaÅ¡anje zaslona,preklopi namizje,preklapljanje namizij,navidezno namizje,koti zaslona +X-KDE-Keywords[sr]=kwin,window,manager,effect,corner,edge,border,action,switch,desktop,kwin screen edges,desktop edges,screen edges,maximize windows,tile windows,side of screen,screen behavior,switch desktop,virtual desktop,screen corners,К‑вин,прозор,менаџер,ефекат,угао,ивица,радња,пребаци,површ,ивице екрана,ивице површи,максимизовање прозора,поплочавање прозора,странице прозора,понашање прозора,мењање површи,виртуелна површ,углови екрана +X-KDE-Keywords[sr@ijekavian]=kwin,window,manager,effect,corner,edge,border,action,switch,desktop,kwin screen edges,desktop edges,screen edges,maximize windows,tile windows,side of screen,screen behavior,switch desktop,virtual desktop,screen corners,К‑вин,прозор,менаџер,ефекат,угао,ивица,радња,пребаци,површ,ивице екрана,ивице површи,максимизовање прозора,поплочавање прозора,странице прозора,понашање прозора,мењање површи,виртуелна површ,углови екрана +X-KDE-Keywords[sr@ijekavianlatin]=kwin,window,manager,effect,corner,edge,border,action,switch,desktop,kwin screen edges,desktop edges,screen edges,maximize windows,tile windows,side of screen,screen behavior,switch desktop,virtual desktop,screen corners,KWin,prozor,menadžer,efekat,ugao,ivica,radnja,prebaci,povrÅ¡,ivice ekrana,ivice povrÅ¡i,maksimizovanje prozora,popločavanje prozora,stranice prozora,ponaÅ¡anje prozora,menjanje povrÅ¡i,virtuelna povrÅ¡,uglovi ekrana +X-KDE-Keywords[sr@latin]=kwin,window,manager,effect,corner,edge,border,action,switch,desktop,kwin screen edges,desktop edges,screen edges,maximize windows,tile windows,side of screen,screen behavior,switch desktop,virtual desktop,screen corners,KWin,prozor,menadžer,efekat,ugao,ivica,radnja,prebaci,povrÅ¡,ivice ekrana,ivice povrÅ¡i,maksimizovanje prozora,popločavanje prozora,stranice prozora,ponaÅ¡anje prozora,menjanje povrÅ¡i,virtuelna povrÅ¡,uglovi ekrana +X-KDE-Keywords[sv]=kwin,fönster,hanterare,effekt,kant,gräns,Ã¥tgärd,byta,skrivbord,kwin skärmkanter,skrivbordskanter,maximera fönster,lägg fönster sida vid sida,skärmsidan, skärmbeteende,skrivbordsbyte,virtuellt skrivbord,skärmhörn +X-KDE-Keywords[tr]=kwin,pencere,yönetici,efekt,kenar,kenarlık,eylem,seç,masaüstü,kwin ekran kenarlıkları,kenarlıklar,masaüstü kenarları,ekran kenarları,pencereleri büyüt,pencereleri döşe,ekranın kenarı,ekran davranışı,masaüstünü seç,sanal masaüstü,ekran köşeleri +X-KDE-Keywords[uk]=kwin,window,manager,effect,corner,edge,border,action,switch,desktop,kwin screen edges,desktop edges,screen edges,maximize windows,tile windows,side of screen,screen behavior,switch desktop,virtual desktop,screen corners,вікно,керування,край,кут,межа,сторона,бік,дія,перемикання,стільниця,краї екрана,максимізація,мозаїка,плитка,край екрана,поведінка екрана,перемикання стільниць,віртуальна стільниця +X-KDE-Keywords[x-test]=xxkwinxx,xxwindowxx,xxmanagerxx,xxeffectxx,xxcornerxx,xxedgexx,xxborderxx,xxactionxx,xxswitchxx,xxdesktopxx,xxkwin screen edgesxx,xxdesktop edgesxx,xxscreen edgesxx,xxmaximize windowsxx,xxtile windowsxx,xxside of screenxx,xxscreen behaviorxx,xxswitch desktopxx,xxvirtual desktopxx,xxscreen cornersxx +X-KDE-Keywords[zh_CN]=kwin,window,manager,effect,corner,edge,border,action,switch,desktop,kwin screen edges,desktop edges,screen edges,maximize windows,tile windows,side of screen,screen behavior,switch desktop,virtual desktop,screen corners,窗口,管理,特效,角,边缘,动作,切换,桌面,kwin 屏幕边缘,屏幕边缘,桌面边缘,最大化窗口,平铺窗口,屏幕行为,桌面切换,虚拟桌面,屏幕角落 +X-KDE-Keywords[zh_TW]= kwin,window,manager,effect,edge,border,action,switch,desktop,kwin screen edges,desktop edges,screen edges,maximize windows,tile windows,side of screen,screen behavior,switch desktop,virtual desktop,screen corners diff --git a/kcmkwin/kwinscreenedges/kwinscreenedgescriptsettings.kcfg b/kcmkwin/kwinscreenedges/kwinscreenedgescriptsettings.kcfg new file mode 100644 index 0000000..dd5f8ff --- /dev/null +++ b/kcmkwin/kwinscreenedges/kwinscreenedgescriptsettings.kcfg @@ -0,0 +1,14 @@ + + + + + + + + ElectricNone + + + diff --git a/kcmkwin/kwinscreenedges/kwinscreenedgescriptsettings.kcfgc b/kcmkwin/kwinscreenedges/kwinscreenedgescriptsettings.kcfgc new file mode 100644 index 0000000..6b43ae0 --- /dev/null +++ b/kcmkwin/kwinscreenedges/kwinscreenedgescriptsettings.kcfgc @@ -0,0 +1,7 @@ +File=kwinscreenedgescriptsettings.kcfg +NameSpace=KWin +ClassName=KWinScreenEdgeScriptSettings +IncludeFiles=kwinglobals.h +Mutators=true +DefaultValueGetters=true +ParentInConstructor=true diff --git a/kcmkwin/kwinscreenedges/kwinscreenedgesettings.kcfg b/kcmkwin/kwinscreenedges/kwinscreenedgesettings.kcfg new file mode 100644 index 0000000..79d2320 --- /dev/null +++ b/kcmkwin/kwinscreenedges/kwinscreenedgesettings.kcfg @@ -0,0 +1,88 @@ + + + + + + 0 + + + 150 + + + 350 + + + true + + + true + + + 0.25 + + + + + None + + + None + + + None + + + None + + + None + + + None + + + None + + + PresentWindowsAll + + + + + ElectricTopLeft + + + ElectricNone + + + ElectricNone + + + + + ElectricNone + + + + + ElectricNone + + + ElectricNone + + + ElectricNone + + + + + ElectricLeft + + + ElectricNone + + + diff --git a/kcmkwin/kwinscreenedges/kwinscreenedgesettings.kcfgc b/kcmkwin/kwinscreenedges/kwinscreenedgesettings.kcfgc new file mode 100644 index 0000000..23dee5b --- /dev/null +++ b/kcmkwin/kwinscreenedges/kwinscreenedgesettings.kcfgc @@ -0,0 +1,7 @@ +File=kwinscreenedgesettings.kcfg +NameSpace=KWin +ClassName=KWinScreenEdgeSettings +IncludeFiles=kwinglobals.h +Mutators=true +DefaultValueGetters=true +ParentInConstructor=true diff --git a/kcmkwin/kwinscreenedges/kwintouchscreen.desktop b/kcmkwin/kwinscreenedges/kwintouchscreen.desktop new file mode 100644 index 0000000..efae5a0 --- /dev/null +++ b/kcmkwin/kwinscreenedges/kwintouchscreen.desktop @@ -0,0 +1,127 @@ +[Desktop Entry] +Exec=kcmshell5 kwintouchscreen +Icon=preferences-desktop-gestures-touch +Type=Service +X-KDE-ServiceTypes=KCModule + +X-KDE-Library=kcm_kwintouchscreen +X-KDE-ParentApp=kcontrol + +X-KDE-System-Settings-Parent-Category=desktopbehavior +X-KDE-Weight=50 + +Name=Touch Screen +Name[ast]=Pantalla táutil +Name[az]=Toxunma ekranı +Name[ca]=Pantalla tàctil +Name[ca@valencia]=Pantalla tàctil +Name[cs]=Dotyková obrazovka +Name[da]=Touchskærm +Name[de]=Touchscreen +Name[el]=Οθόνη αφής +Name[en_GB]=Touch Screen +Name[es]=Pantalla táctil +Name[et]=Puuteekraan +Name[eu]=Ukimen-pantaila +Name[fi]=Kosketusnäyttö +Name[fr]=Écran tactile +Name[gl]=Pantalla táctil +Name[he]=מסך מגע +Name[hu]=Érintőképernyő +Name[ia]=Schermo a tacto +Name[id]=Layar Sentuh +Name[it]=Schermo a sfioramento +Name[ko]=터치 스크린 +Name[lt]=Jutiklinis ekranas +Name[nl]=Aanraakscherm +Name[nn]=Trykkskjerm +Name[pa]=ਟੱਚ ਸਕਰੀਨ +Name[pl]=Ekran dotykowy +Name[pt]=Ecrã Táctil +Name[pt_BR]=Touch Screen +Name[ro]=Ecran tactil +Name[ru]=Сенсорный экран +Name[sk]=Dotyková obrazovka +Name[sl]=Zaslon na dotik +Name[sr]=Додирник +Name[sr@ijekavian]=Додирник +Name[sr@ijekavianlatin]=Dodirnik +Name[sr@latin]=Dodirnik +Name[sv]=Pekskärm +Name[tr]=Dokunmatik Ekran +Name[uk]=Сенсорна панель +Name[x-test]=xxTouch Screenxx +Name[zh_CN]=触摸屏 +Name[zh_TW]=觸控螢幕 +Comment=Configure touch screen swipe gestures +Comment[az]=Toxunma ekranı jestlərini ayarlamaq +Comment[ca]=Configura els gestos de lliscament en la pantalla tàctil +Comment[da]=Indstil strygegestusser til touchskærme +Comment[de]=Wischgesten für Touchscreens einrichten +Comment[en_GB]=Configure touch screen swipe gestures +Comment[es]=Configurar los gestos de deslizamiento en pantalla táctil +Comment[et]=Puuteekraani žestide seadistamine +Comment[eu]=Konfiguratu ukimen-pantailako kolpe arinen keinuak +Comment[fi]=Aseta kosketusnäytön pyyhkäisyeleet +Comment[fr]=Configurer les mouvements sur l'écran tactile +Comment[gl]=Configurar os xestos de pantalla táctil +Comment[hu]=Érintőképernyő-gesztusok beállítása +Comment[ia]=Configura gestos e glissar de schermo tactil +Comment[id]=Konfigurasikan gestur usapan layar sentuh +Comment[it]=Configura gesti dello schermo a sfioramento +Comment[ko]=터치 스크린 밀기 제스처 설정 +Comment[lt]=KonfigÅ«ruoti jutiklinio ekrano perbraukimų gestus +Comment[nl]=Veeggebaren voor aanraakscherm configureren +Comment[nn]=Set opp fingerrørsler pÃ¥ trykkskjerm +Comment[pl]=Ustawienia gestów na ekranie dotykowym +Comment[pt]=Configurar os gestos para deslizar o ecrã táctil +Comment[pt_BR]=Configura os gestos na tela sensível ao toque +Comment[ro]=Configurează gesturi de tragere pe ecran tactil +Comment[ru]=Действия при проведении по сенсорному экрану +Comment[sk]=NastaviÅ¥ Å¥ahacie gestá dotykovej obrazovky +Comment[sl]=Nastavi kretnje vlečenja za zaslon na dotik +Comment[sv]=Anpassa draggester för pekskärm +Comment[uk]=Налаштовування жестів на сенсорній панелі +Comment[x-test]=xxConfigure touch screen swipe gesturesxx +Comment[zh_CN]=配置触摸屏滑动手势 +Comment[zh_TW]=設定觸控螢幕滑動手勢 + +X-KDE-Keywords=kwin,window,manager,effect,edge,border,action,switch,desktop,desktop edges,screen edges,side of screen,screen behavior,touch screen +X-KDE-Keywords[az]=pəncərə,menecer,effekt,sərhəd,kənar,fəaliyyət,dəyişmə,keçiş,iş masası,iş masası kənarları,ekran kənarları,ekran davranışları,toxunma ekranı +X-KDE-Keywords[ca]=kwin,finestra,gestor,efecte,vora,frontera,acció,canvi,escriptori,vores de l'escriptori,vores de la pantalla,costat de la pantalla,comportament de la pantalla,pantalla tàctil +X-KDE-Keywords[ca@valencia]=kwin,finestra,gestor,efecte,vora,borde,acció,canvi,escriptori,vores d'escriptori,vores de pantalla,costat de pantalla,comportament de la pantalla,pantalla tàctil +X-KDE-Keywords[da]=kwin,vindue,hÃ¥ndtering,manager,effekt,hjørne,kant,handling,skift,skrivebord,kwin skærmkanter,skrivebordskanter,skærmkanter,maksimer vinduer,tile windows,fliser,felter,siden af skærmen,skærmens opførsel,touch,skift skrivebord,virtuelle skriveborde,skærmhjørner,hjørner +X-KDE-Keywords[de]=KWin,Fenster,Verwaltung,Effekt,Kante,Rand,Aktion,Wechseln,Desktop,Arbeitsfläche,Bildschirmkanten,Desktopkanten,Bildschirmseite,Bildschirmverhalten,Touchscreen +X-KDE-Keywords[el]=kwin,παράθυρο,διαχειριστής,εφέ,άκρη,περίγραμμα,ενέργεια,εναλλαγή,επιφάνεια εργασίας,άκρες επιφάνειας εργασίας,άκρες οθόνης,πλευρά οθόνης,συμπεριφορά οθόνης, οθόνη αφής +X-KDE-Keywords[en_GB]=kwin,window,manager,effect,edge,border,action,switch,desktop,desktop edges,screen edges,side of screen,screen behaviour,touch screen +X-KDE-Keywords[es]=kwin,ventana,gestor,efecto,esquina,borde,acción,cambiar,escritorio,bordes del escritorio,bordes de la pantalla,lado de la pantalla,comportamiento de la pantalla,pantalla táctil +X-KDE-Keywords[et]=kwin,aken,haldur,efekt,nurk,serv,piire,toiming,lülitamine,töölaud,töölaua servad,ekraani servad,ekraani äär,ekraani käitumine,puuteekraan +X-KDE-Keywords[eu]=kwin,leiho,kudeatzaile,efektu,izkin,ertz,ekintza,aldatu,mahaigain,mahaigainaren ertzak,pantailen ertzak,pantailaren aldea,pantailaren portaera,ukipen-pantaila +X-KDE-Keywords[fi]=kwin,ikkuna,hallinta,tehoste,kulma,laita,reuna,toiminto,vaihda,työpöytä,työpöydän reunat,näytön reunat,näytön laita,näytön käyttäytyminen,kosketusnäyttö +X-KDE-Keywords[fr]=kwin, fenêtre, gestionnaire, effet, bord, bordure, action, bascule, bureau, bords du bureau, bords de l'écran, côté de l'écran, comportement de l'écran, écran tactile +X-KDE-Keywords[gl]=kwin,window,xanela,manager,xestor,effect,efecto,edge,beira,bordo,contorno,esquina,border,action,acción,switch,cambiar,conmutar,trocar,desktop,escritorio,desktop edges,screen edges,pantalla,side of screen,screen behavior,comportamento,touch screen,táctil +X-KDE-Keywords[hu]=kwin,ablak,kezelő,effektus,szél,szegély,művelet,váltás,asztal,asztalszél,képernyőszél,képernyőoldal,képernyő működése,érintőképernyő +X-KDE-Keywords[ia]=kwin,window,manager,effect,edge,border,action,switch,desktop,desktop edges,screen edges,side of screen,screen behavior,touch screen +X-KDE-Keywords[id]=kwin,window,pengelola,efek,tepi,bingkai,aksi,alih,desktop,tepian desktop,tepian layar,sisi layar,perilaku layar,layar sentuh +X-KDE-Keywords[it]=kwin,finestra,gestore,effetto,angolo,bordo,azione,scambiatore,desktop,bordi desktop,bordi schermo,lato dello schermo,comportamento schermo,schermo a sfioramento +X-KDE-Keywords[ko]=kwin,window,manager,effect,edge,border,action,switch,desktop,desktop edges,screen edges,side of screen,screen behavior,touch screen,ì°½,관리자,효과,경계,경계선,동작,액션,데스크톱,화면 경계,경계,터치,터치 스크린,터치스크린,바탕 화면 +X-KDE-Keywords[lt]=kwin,lango,langų,langu,tvarkytuvė,tvarkytuve,efektas,efektai,kraÅ¡tas,krastas,rėmelis,remelis,rėmas,remas,veiksmas,veiksmai,perjungti,perjugimo,perjungimas,darbalaukis,darbalaukio,darbalaukio kraÅ¡tai,darbalaukio kraÅ¡tas,darbalaukio krastai,darbalaukio krastas,ekrano kraÅ¡tai,ekrano krastai,ekrano kraÅ¡tas,ekrano krastas,ekrano Å¡onas,ekrano sonas,ekrano pusė,ekrano puse,ekrano elgsena,ekrano elgsesys,jutiklinis ekranas,jutiminis ekranas,jautrusis ekranas,touchscreen,touch screen +X-KDE-Keywords[nl]=kwin,venster,beheerder,effect,kant,rand,actie,omschakelen,bureaublad,bureaubladkanten,schermkanten,zijkant van het scherm,schermgedrag,aanraakscherm +X-KDE-Keywords[nn]=kwin,vindauge,handsamar,effekt,kant,ramme,handling,byte,skrivebord,skrivebordkantar,skjermkantar,skjermside,skjermÃ¥tferd,trykkskjerm +X-KDE-Keywords[pl]=kwin,okno,menadżer,efekt,krawędź,obramowanie,działanie,przełącz,pulpit,krawędzie pulpitu,krawędzie ekranu,strona ekranu,zachowanie ekranu,ekran dotykowy +X-KDE-Keywords[pt]=kwin,janela,gestor,efeito,extremo,contorno,acção,mudar,ecrã,extremos do ecrã no kwin,extremos do ecrã,maximizar as janelas,janelas lado-a-lado,lado do ecrã,comportamento do ecrã,ecrã táctil +X-KDE-Keywords[pt_BR]=kwin,janela,gerenciador,efeito,canto,contorno,borda,ação,mudar,área de trabalho,cantos da área de trabalho, desktop,lado da tela,comportamento da tela,touch screen +X-KDE-Keywords[ro]=kwin,fereastră,gestionar,efect,margine,muchie,latură,acțiune,schimbă,birou,marginile biroului,marginile ecranului,marginea ecranului,comportament ecran,ecran tactil +X-KDE-Keywords[ru]=kwin,window,manager,effect,corner,edge,border,action,switch,desktop,kwin screen edges,desktop edges,screen edges,maximize windows,tile windows,side of screen,screen behavior,switch desktop,virtual desktop,screen corners,окно,окон,диспетчер,эффект,край,граница,действие,переключить,рабочий стол,края экрана kwin,края экрана,края рабочего стола,распахнуть окна,мозаика окон,край экрана,поведение экрана,переключить рабочий стол,виртуальный рабочий стол,углы рабочего стола,углы экрана,тачскрин,сенсорный экран,touch screen +X-KDE-Keywords[sk]=Kwin, okná, manažér, efekt, okraj, hranice, akcie, prepínač, desktop, okraje plochy,Okraje obrazovky, bočná strana obrazovky, správanie obrazovky, dotyková obrazovka +X-KDE-Keywords[sl]=kwin,okno,upravljalnik oken,upravljanje oken,učinki,rob,obroba,dejanje,preklopi,preklapljanje,robovi namizja,robovi zaslona,rob zaslona,obnaÅ¡anje zaslona,zaslon na dotik +X-KDE-Keywords[sr]=kwin,window,manager,effect,edge,border,action,switch,desktop,desktop edges,screen edges,side of screen,screen behavior,touch screen,К‑вин,прозор,менаџер,ефекат,ивица,радња,пребаци,површ,ивице екрана,ивице површи,странице прозора,понашање прозора,додирник +X-KDE-Keywords[sr@ijekavian]=kwin,window,manager,effect,edge,border,action,switch,desktop,desktop edges,screen edges,side of screen,screen behavior,touch screen,К‑вин,прозор,менаџер,ефекат,ивица,радња,пребаци,површ,ивице екрана,ивице површи,странице прозора,понашање прозора,додирник +X-KDE-Keywords[sr@ijekavianlatin]=kwin,window,manager,effect,edge,border,action,switch,desktop,desktop edges,screen edges,side of screen,screen behavior,touch screen,KWin,prozor,menadžer,efekat,ivica,radnja,prebaci,povrÅ¡,ivice ekrana,ivice povrÅ¡i,stranice prozora,ponaÅ¡anje prozora,dodirnik +X-KDE-Keywords[sr@latin]=kwin,window,manager,effect,edge,border,action,switch,desktop,desktop edges,screen edges,side of screen,screen behavior,touch screen,KWin,prozor,menadžer,efekat,ivica,radnja,prebaci,povrÅ¡,ivice ekrana,ivice povrÅ¡i,stranice prozora,ponaÅ¡anje prozora,dodirnik +X-KDE-Keywords[sv]=kwin,fönster,hanterare,effekt,kant,gräns,Ã¥tgärd,byta,skrivbord,skrivbordskanter,skärmkanter,skärmsidan,skärmbeteende,pekskärm +X-KDE-Keywords[tr]=kwin,pencere,yönetici,efekt,kenar,sınır,eylem,geçiş,masaüstü,masaüstü kenarları,ekran kenarları,ekranın yan tarafı,ekran davranışı,dokunmatik ekran +X-KDE-Keywords[uk]=kwin,window,manager,effect,corner,edge,border,action,switch,desktop,desktop edges,screen edges,side of screen,screen behavior,touch screen,вікно,керування,край,кут,межа,сторона,бік,дія,перемикання,стільниця,плитка,край екрана,поведінка екрана,перемикання стільниць,віртуальна стільниця,сенсорна панель +X-KDE-Keywords[x-test]=xxkwinxx,xxwindowxx,xxmanagerxx,xxeffectxx,xxedgexx,xxborderxx,xxactionxx,xxswitchxx,xxdesktopxx,xxdesktop edgesxx,xxscreen edgesxx,xxside of screenxx,xxscreen behaviorxx,xxtouch screenxx +X-KDE-Keywords[zh_CN]=kwin,window,manager,effect,corner,edge,border,action,switch,desktop,kwin screen edges,desktop edges,screen edges,maximize windows,tile windows,side of screen,screen behavior,switch desktop,virtual desktop,screen corners,窗口,管理,特效,角,边缘,动作,切换,桌面,kwin 屏幕边缘,屏幕边缘,桌面边缘,最大化窗口,平铺窗口,屏幕行为,桌面切换,虚拟桌面,屏幕角落 +X-KDE-Keywords[zh_TW]=kwin,window,manager,effect,edge,border,action,switch,desktop,desktop edges,screen edges,side of screen,screen behavior,touch screen diff --git a/kcmkwin/kwinscreenedges/kwintouchscreenedgeconfigform.cpp b/kcmkwin/kwinscreenedges/kwintouchscreenedgeconfigform.cpp new file mode 100644 index 0000000..ac8e1f9 --- /dev/null +++ b/kcmkwin/kwinscreenedges/kwintouchscreenedgeconfigform.cpp @@ -0,0 +1,34 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2020 Cyril Rossi + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwintouchscreenedgeconfigform.h" +#include "ui_touch.h" + +namespace KWin +{ + +KWinTouchScreenEdgeConfigForm::KWinTouchScreenEdgeConfigForm(QWidget *parent) + : KWinScreenEdge(parent) + , ui(new Ui::KWinTouchScreenConfigUi) +{ + ui->setupUi(this); +} + +KWinTouchScreenEdgeConfigForm::~KWinTouchScreenEdgeConfigForm() +{ + delete ui; +} + +Monitor *KWinTouchScreenEdgeConfigForm::monitor() const +{ + return ui->monitor; +} + +} // namespace diff --git a/kcmkwin/kwinscreenedges/kwintouchscreenedgeconfigform.h b/kcmkwin/kwinscreenedges/kwintouchscreenedgeconfigform.h new file mode 100644 index 0000000..ff7824a --- /dev/null +++ b/kcmkwin/kwinscreenedges/kwintouchscreenedgeconfigform.h @@ -0,0 +1,41 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2020 Cyril Rossi + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef __KWINTOUCHSCREENEDGECONFIGFORM_H__ +#define __KWINTOUCHSCREENEDGECONFIGFORM_H__ + +#include "kwinscreenedge.h" + +namespace Ui +{ +class KWinTouchScreenConfigUi; +} + +namespace KWin +{ + +class KWinTouchScreenEdgeConfigForm : public KWinScreenEdge +{ + Q_OBJECT + +public: + KWinTouchScreenEdgeConfigForm(QWidget *parent = nullptr); + ~KWinTouchScreenEdgeConfigForm() override; + +protected: + Monitor *monitor() const override; + +private: + Ui::KWinTouchScreenConfigUi *ui; +}; + +} // namespace + +#endif diff --git a/kcmkwin/kwinscreenedges/kwintouchscreenscriptsettings.kcfg b/kcmkwin/kwinscreenedges/kwintouchscreenscriptsettings.kcfg new file mode 100644 index 0000000..e9e42a5 --- /dev/null +++ b/kcmkwin/kwinscreenedges/kwintouchscreenscriptsettings.kcfg @@ -0,0 +1,14 @@ + + + + + + + + ElectricNone + + + diff --git a/kcmkwin/kwinscreenedges/kwintouchscreenscriptsettings.kcfgc b/kcmkwin/kwinscreenedges/kwintouchscreenscriptsettings.kcfgc new file mode 100644 index 0000000..640d8f5 --- /dev/null +++ b/kcmkwin/kwinscreenedges/kwintouchscreenscriptsettings.kcfgc @@ -0,0 +1,7 @@ +File=kwintouchscreenscriptsettings.kcfg +NameSpace=KWin +ClassName=KWinTouchScreenScriptSettings +IncludeFiles=kwinglobals.h +Mutators=true +DefaultValueGetters=true +ParentInConstructor=true diff --git a/kcmkwin/kwinscreenedges/kwintouchscreensettings.kcfg b/kcmkwin/kwinscreenedges/kwintouchscreensettings.kcfg new file mode 100644 index 0000000..49a253e --- /dev/null +++ b/kcmkwin/kwinscreenedges/kwintouchscreensettings.kcfg @@ -0,0 +1,56 @@ + + + + + + None + + + None + + + None + + + None + + + + + ElectricNone + + + ElectricNone + + + ElectricNone + + + + + ElectricNone + + + + + ElectricNone + + + ElectricNone + + + ElectricNone + + + + + ElectricLeft + + + ElectricNone + + + diff --git a/kcmkwin/kwinscreenedges/kwintouchscreensettings.kcfgc b/kcmkwin/kwinscreenedges/kwintouchscreensettings.kcfgc new file mode 100644 index 0000000..0a857f0 --- /dev/null +++ b/kcmkwin/kwinscreenedges/kwintouchscreensettings.kcfgc @@ -0,0 +1,7 @@ +File=kwintouchscreensettings.kcfg +NameSpace=KWin +ClassName=KWinTouchScreenSettings +IncludeFiles=kwinglobals.h +Mutators=true +DefaultValueGetters=true +ParentInConstructor=true diff --git a/kcmkwin/kwinscreenedges/main.cpp b/kcmkwin/kwinscreenedges/main.cpp new file mode 100644 index 0000000..4df7808 --- /dev/null +++ b/kcmkwin/kwinscreenedges/main.cpp @@ -0,0 +1,363 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008 Martin Gräßlin + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2020 Cyril Rossi + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "main.h" +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "kwinscreenedgeconfigform.h" +#include "kwinscreenedgesettings.h" +#include "kwinscreenedgescriptsettings.h" + +K_PLUGIN_FACTORY(KWinScreenEdgesConfigFactory, registerPlugin();) + +namespace KWin +{ + +KWinScreenEdgesConfig::KWinScreenEdgesConfig(QWidget *parent, const QVariantList &args) + : KCModule(parent, args) + , m_form(new KWinScreenEdgesConfigForm(this)) + , m_config(KSharedConfig::openConfig("kwinrc")) + , m_settings(new KWinScreenEdgeSettings(this)) +{ + QVBoxLayout *layout = new QVBoxLayout(this); + layout->addWidget(m_form); + + addConfig(m_settings, m_form); + + monitorInit(); + + connect(m_form, &KWinScreenEdgesConfigForm::saveNeededChanged, this, &KWinScreenEdgesConfig::unmanagedWidgetChangeState); + connect(m_form, &KWinScreenEdgesConfigForm::defaultChanged, this, &KWinScreenEdgesConfig::unmanagedWidgetDefaultState); +} + +KWinScreenEdgesConfig::~KWinScreenEdgesConfig() +{ +} + +void KWinScreenEdgesConfig::load() +{ + KCModule::load(); + m_settings->load(); + for (KWinScreenEdgeScriptSettings *setting : qAsConst(m_scriptSettings)) { + setting->load(); + } + + monitorLoadSettings(); + monitorLoadDefaultSettings(); + m_form->setElectricBorderCornerRatio(m_settings->electricBorderCornerRatio()); + m_form->setDefaultElectricBorderCornerRatio(m_settings->defaultElectricBorderCornerRatioValue()); + m_form->reload(); +} + +void KWinScreenEdgesConfig::save() +{ + monitorSaveSettings(); + m_settings->setElectricBorderCornerRatio(m_form->electricBorderCornerRatio()); + m_settings->save(); + for (KWinScreenEdgeScriptSettings *setting : qAsConst(m_scriptSettings)) { + setting->save(); + } + + // Reload saved settings to ScreenEdge UI + monitorLoadSettings(); + m_form->setElectricBorderCornerRatio(m_settings->electricBorderCornerRatio()); + m_form->reload(); + + // Reload KWin. + QDBusMessage message = QDBusMessage::createSignal("/KWin", "org.kde.KWin", "reloadConfig"); + QDBusConnection::sessionBus().send(message); + // and reconfigure the effects + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(BuiltInEffects::nameForEffect(BuiltInEffect::PresentWindows)); + interface.reconfigureEffect(BuiltInEffects::nameForEffect(BuiltInEffect::DesktopGrid)); + interface.reconfigureEffect(BuiltInEffects::nameForEffect(BuiltInEffect::Cube)); + + KCModule::save(); +} + +void KWinScreenEdgesConfig::defaults() +{ + m_form->setDefaults(); + + KCModule::defaults(); +} + +void KWinScreenEdgesConfig::showEvent(QShowEvent *e) +{ + KCModule::showEvent(e); + + monitorShowEvent(); +} + +// Copied from kcmkwin/kwincompositing/main.cpp +bool KWinScreenEdgesConfig::effectEnabled(const BuiltInEffect &effect, const KConfigGroup &cfg) const +{ + return cfg.readEntry(BuiltInEffects::nameForEffect(effect) + "Enabled", BuiltInEffects::enabledByDefault(effect)); +} + +//----------------------------------------------------------------------------- +// Monitor + +void KWinScreenEdgesConfig::monitorInit() +{ + m_form->monitorAddItem(i18n("No Action")); + m_form->monitorAddItem(i18n("Show Desktop")); + m_form->monitorAddItem(i18n("Lock Screen")); + m_form->monitorAddItem(i18n("Show KRunner")); + m_form->monitorAddItem(i18n("Activity Manager")); + m_form->monitorAddItem(i18n("Application Launcher")); + + // Add the effects + const QString presentWindowsName = BuiltInEffects::effectData(BuiltInEffect::PresentWindows).displayName; + m_form->monitorAddItem(i18n("%1 - All Desktops", presentWindowsName)); + m_form->monitorAddItem(i18n("%1 - Current Desktop", presentWindowsName)); + m_form->monitorAddItem(i18n("%1 - Current Application", presentWindowsName)); + m_form->monitorAddItem(BuiltInEffects::effectData(BuiltInEffect::DesktopGrid).displayName); + const QString cubeName = BuiltInEffects::effectData(BuiltInEffect::Cube).displayName; + m_form->monitorAddItem(i18n("%1 - Cube", cubeName)); + m_form->monitorAddItem(i18n("%1 - Cylinder", cubeName)); + m_form->monitorAddItem(i18n("%1 - Sphere", cubeName)); + + m_form->monitorAddItem(i18n("Toggle window switching")); + m_form->monitorAddItem(i18n("Toggle alternative window switching")); + + const QString scriptFolder = QStringLiteral("kwin/scripts/"); + const auto scripts = KPackage::PackageLoader::self()->listPackages(QStringLiteral("KWin/Script"), scriptFolder); + + KConfigGroup config(m_config, "Plugins"); + for (const KPluginMetaData &script: scripts) { + if (script.value(QStringLiteral("X-KWin-Border-Activate")) != QLatin1String("true")) { + continue; + } + + if (!config.readEntry(script.pluginId() + QStringLiteral("Enabled"), script.isEnabledByDefault())) { + continue; + } + m_scripts << script.pluginId(); + m_form->monitorAddItem(script.name()); + m_scriptSettings[script.pluginId()] = new KWinScreenEdgeScriptSettings(script.pluginId(), this); + } + + monitorShowEvent(); +} + +void KWinScreenEdgesConfig::monitorLoadSettings() +{ + // Load ElectricBorderActions + m_form->monitorChangeEdge(ElectricTop, KWinScreenEdgesConfig::electricBorderActionFromString(m_settings->top())); + m_form->monitorChangeEdge(ElectricTopRight, KWinScreenEdgesConfig::electricBorderActionFromString(m_settings->topRight())); + m_form->monitorChangeEdge(ElectricRight, KWinScreenEdgesConfig::electricBorderActionFromString(m_settings->right())); + m_form->monitorChangeEdge(ElectricBottomRight, KWinScreenEdgesConfig::electricBorderActionFromString(m_settings->bottomRight())); + m_form->monitorChangeEdge(ElectricBottom, KWinScreenEdgesConfig::electricBorderActionFromString(m_settings->bottom())); + m_form->monitorChangeEdge(ElectricBottomLeft, KWinScreenEdgesConfig::electricBorderActionFromString(m_settings->bottomLeft())); + m_form->monitorChangeEdge(ElectricLeft, KWinScreenEdgesConfig::electricBorderActionFromString(m_settings->left())); + m_form->monitorChangeEdge(ElectricTopLeft, KWinScreenEdgesConfig::electricBorderActionFromString(m_settings->topLeft())); + + // Load effect-specific actions: + + // PresentWindows BorderActivateAll + m_form->monitorChangeEdge(m_settings->borderActivateAll(), PresentWindowsAll); + + // PresentWindows BorderActivate + m_form->monitorChangeEdge(m_settings->borderActivatePresentWindows(), PresentWindowsCurrent); + + // PresentWindows BorderActivateClass + m_form->monitorChangeEdge(m_settings->borderActivateClass(), PresentWindowsClass); + + // Desktop Grid + m_form->monitorChangeEdge(m_settings->borderActivateDesktopGrid(), DesktopGrid); + + // Desktop Cube + m_form->monitorChangeEdge(m_settings->borderActivateCube(), Cube); + m_form->monitorChangeEdge(m_settings->borderActivateCylinder(), Cylinder); + m_form->monitorChangeEdge(m_settings->borderActivateSphere(), Sphere); + + // TabBox + m_form->monitorChangeEdge(m_settings->borderActivateTabBox(), TabBox); + // Alternative TabBox + m_form->monitorChangeEdge(m_settings->borderAlternativeActivate(), TabBoxAlternative); + + // Scripts + for (int i = 0; i < m_scripts.size(); i++) { + int index = EffectCount + i; + m_form->monitorChangeEdge(m_scriptSettings[m_scripts[i]]->borderActivate(), index); + } +} + +void KWinScreenEdgesConfig::monitorLoadDefaultSettings() +{ + // Load ElectricBorderActions + m_form->monitorChangeDefaultEdge(ElectricTop, KWinScreenEdgesConfig::electricBorderActionFromString(m_settings->defaultTopValue())); + m_form->monitorChangeDefaultEdge(ElectricTopRight, KWinScreenEdgesConfig::electricBorderActionFromString(m_settings->defaultTopRightValue())); + m_form->monitorChangeDefaultEdge(ElectricRight, KWinScreenEdgesConfig::electricBorderActionFromString(m_settings->defaultRightValue())); + m_form->monitorChangeDefaultEdge(ElectricBottomRight, KWinScreenEdgesConfig::electricBorderActionFromString(m_settings->defaultBottomRightValue())); + m_form->monitorChangeDefaultEdge(ElectricBottom, KWinScreenEdgesConfig::electricBorderActionFromString(m_settings->defaultBottomValue())); + m_form->monitorChangeDefaultEdge(ElectricBottomLeft, KWinScreenEdgesConfig::electricBorderActionFromString(m_settings->defaultBottomLeftValue())); + m_form->monitorChangeDefaultEdge(ElectricLeft, KWinScreenEdgesConfig::electricBorderActionFromString(m_settings->defaultLeftValue())); + m_form->monitorChangeDefaultEdge(ElectricTopLeft, KWinScreenEdgesConfig::electricBorderActionFromString(m_settings->defaultTopLeftValue())); + + // Load effect-specific actions: + + // PresentWindows BorderActivateAll + m_form->monitorChangeDefaultEdge(m_settings->defaultBorderActivateAllValue(), PresentWindowsAll); + + // PresentWindows BorderActivate + m_form->monitorChangeDefaultEdge(m_settings->defaultBorderActivatePresentWindowsValue(), PresentWindowsCurrent); + + // PresentWindows BorderActivateClass + m_form->monitorChangeDefaultEdge(m_settings->defaultBorderActivateClassValue(), PresentWindowsClass); + + // Desktop Grid + m_form->monitorChangeDefaultEdge(m_settings->defaultBorderActivateDesktopGridValue(), DesktopGrid); + + // Desktop Cube + m_form->monitorChangeDefaultEdge(m_settings->defaultBorderActivateCubeValue(), Cube); + m_form->monitorChangeDefaultEdge(m_settings->defaultBorderActivateCylinderValue(), Cylinder); + m_form->monitorChangeDefaultEdge(m_settings->defaultBorderActivateSphereValue(), Sphere); + + // TabBox + m_form->monitorChangeDefaultEdge(m_settings->defaultBorderActivateTabBoxValue(), TabBox); + // Alternative TabBox + m_form->monitorChangeDefaultEdge(m_settings->defaultBorderAlternativeActivateValue(), TabBoxAlternative); +} + +void KWinScreenEdgesConfig::monitorSaveSettings() +{ + // Save ElectricBorderActions + m_settings->setTop(KWinScreenEdgesConfig::electricBorderActionToString(m_form->selectedEdgeItem(ElectricTop))); + m_settings->setTopRight(KWinScreenEdgesConfig::electricBorderActionToString(m_form->selectedEdgeItem(ElectricTopRight))); + m_settings->setRight(KWinScreenEdgesConfig::electricBorderActionToString(m_form->selectedEdgeItem(ElectricRight))); + m_settings->setBottomRight(KWinScreenEdgesConfig::electricBorderActionToString(m_form->selectedEdgeItem(ElectricBottomRight))); + m_settings->setBottom(KWinScreenEdgesConfig::electricBorderActionToString(m_form->selectedEdgeItem(ElectricBottom))); + m_settings->setBottomLeft(KWinScreenEdgesConfig::electricBorderActionToString(m_form->selectedEdgeItem(ElectricBottomLeft))); + m_settings->setLeft(KWinScreenEdgesConfig::electricBorderActionToString(m_form->selectedEdgeItem(ElectricLeft))); + m_settings->setTopLeft(KWinScreenEdgesConfig::electricBorderActionToString(m_form->selectedEdgeItem(ElectricTopLeft))); + + // Save effect-specific actions: + + // Present Windows + m_settings->setBorderActivateAll(m_form->monitorCheckEffectHasEdge(PresentWindowsAll)); + m_settings->setBorderActivatePresentWindows(m_form->monitorCheckEffectHasEdge(PresentWindowsCurrent)); + m_settings->setBorderActivateClass(m_form->monitorCheckEffectHasEdge(PresentWindowsClass)); + + // Desktop Grid + m_settings->setBorderActivateDesktopGrid(m_form->monitorCheckEffectHasEdge(DesktopGrid)); + + // Desktop Cube + m_settings->setBorderActivateCube(m_form->monitorCheckEffectHasEdge(Cube)); + m_settings->setBorderActivateCylinder(m_form->monitorCheckEffectHasEdge(Cylinder)); + m_settings->setBorderActivateSphere(m_form->monitorCheckEffectHasEdge(Sphere)); + + // TabBox + m_settings->setBorderActivateTabBox(m_form->monitorCheckEffectHasEdge(TabBox)); + m_settings->setBorderAlternativeActivate(m_form->monitorCheckEffectHasEdge(TabBoxAlternative)); + + // Scripts + for (int i = 0; i < m_scripts.size(); i++) { + int index = EffectCount + i; + m_scriptSettings[m_scripts[i]]->setBorderActivate(m_form->monitorCheckEffectHasEdge(index)); + } +} + +void KWinScreenEdgesConfig::monitorShowEvent() +{ + // Check if they are enabled + KConfigGroup config(m_config, "Plugins"); + + // Present Windows + bool enabled = effectEnabled(BuiltInEffect::PresentWindows, config); + m_form->monitorItemSetEnabled(PresentWindowsCurrent, enabled); + m_form->monitorItemSetEnabled(PresentWindowsAll, enabled); + + // Desktop Grid + enabled = effectEnabled(BuiltInEffect::DesktopGrid, config); + m_form->monitorItemSetEnabled(DesktopGrid, enabled); + + // Desktop Cube + enabled = effectEnabled(BuiltInEffect::Cube, config); + m_form->monitorItemSetEnabled(Cube, enabled); + m_form->monitorItemSetEnabled(Cylinder, enabled); + m_form->monitorItemSetEnabled(Sphere, enabled); + // tabbox, depends on reasonable focus policy. + KConfigGroup config2(m_config, "Windows"); + QString focusPolicy = config2.readEntry("FocusPolicy", QString()); + bool reasonable = focusPolicy != "FocusStrictlyUnderMouse" && focusPolicy != "FocusUnderMouse"; + m_form->monitorItemSetEnabled(TabBox, reasonable); + m_form->monitorItemSetEnabled(TabBoxAlternative, reasonable); + + // Disable Edge if ElectricBorders group entries are immutable + m_form->monitorEnableEdge(ElectricTop, !m_settings->isTopImmutable()); + m_form->monitorEnableEdge(ElectricTopRight, !m_settings->isTopRightImmutable()); + m_form->monitorEnableEdge(ElectricRight, !m_settings->isRightImmutable()); + m_form->monitorEnableEdge(ElectricBottomRight, !m_settings->isBottomRightImmutable()); + m_form->monitorEnableEdge(ElectricBottom, !m_settings->isBottomImmutable()); + m_form->monitorEnableEdge(ElectricBottomLeft, !m_settings->isBottomLeftImmutable()); + m_form->monitorEnableEdge(ElectricLeft, !m_settings->isLeftImmutable()); + m_form->monitorEnableEdge(ElectricTopLeft, !m_settings->isTopLeftImmutable()); + + // Disable ElectricBorderCornerRatio if entry is immutable + m_form->setElectricBorderCornerRatioEnabled(!m_settings->isElectricBorderCornerRatioImmutable()); +} + +ElectricBorderAction KWinScreenEdgesConfig::electricBorderActionFromString(const QString &string) +{ + QString lowerName = string.toLower(); + if (lowerName == QStringLiteral("showdesktop")) { + return ElectricActionShowDesktop; + } + if (lowerName == QStringLiteral("lockscreen")) { + return ElectricActionLockScreen; + } + if (lowerName == QStringLiteral("krunner")) { + return ElectricActionKRunner; + } + if (lowerName == QStringLiteral("activitymanager")) { + return ElectricActionActivityManager; + } + if (lowerName == QStringLiteral("applicationlauncher")) { + return ElectricActionApplicationLauncher; + } + return ElectricActionNone; +} + +QString KWinScreenEdgesConfig::electricBorderActionToString(int action) +{ + switch (action) { + case 1: + return QStringLiteral("ShowDesktop"); + case 2: + return QStringLiteral("LockScreen"); + case 3: + return QStringLiteral("KRunner"); + case 4: + return QStringLiteral("ActivityManager"); + case 5: + return QStringLiteral("ApplicationLauncher"); + default: + return QStringLiteral("None"); + } +} + +} // namespace + +#include "main.moc" diff --git a/kcmkwin/kwinscreenedges/main.h b/kcmkwin/kwinscreenedges/main.h new file mode 100644 index 0000000..ea71a4e --- /dev/null +++ b/kcmkwin/kwinscreenedges/main.h @@ -0,0 +1,78 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2020 Cyril Rossi + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef __MAIN_H__ +#define __MAIN_H__ + +#include +#include + +#include "kwinglobals.h" + +class QShowEvent; + +namespace KWin +{ +class KWinScreenEdgesConfigForm; +class KWinScreenEdgeSettings; +class KWinScreenEdgeScriptSettings; +enum class BuiltInEffect; + +class KWinScreenEdgesConfig : public KCModule +{ + Q_OBJECT + +public: + explicit KWinScreenEdgesConfig(QWidget *parent, const QVariantList &args); + ~KWinScreenEdgesConfig() override; + +public Q_SLOTS: + void save() override; + void load() override; + void defaults() override; + +protected: + void showEvent(QShowEvent *e) override; + +private: + KWinScreenEdgesConfigForm *m_form; + KSharedConfigPtr m_config; + QStringList m_scripts; //list of script IDs ordered in the list they are presented in the menu + QHash m_scriptSettings; + KWinScreenEdgeSettings *m_settings; + + enum EffectActions { + PresentWindowsAll = ELECTRIC_ACTION_COUNT, // Start at the end of built in actions + PresentWindowsCurrent, + PresentWindowsClass, + DesktopGrid, + Cube, + Cylinder, + Sphere, + TabBox, + TabBoxAlternative, + EffectCount + }; + + bool effectEnabled(const BuiltInEffect &effect, const KConfigGroup &cfg) const; + + void monitorInit(); + void monitorLoadSettings(); + void monitorLoadDefaultSettings(); + void monitorSaveSettings(); + void monitorShowEvent(); + + static ElectricBorderAction electricBorderActionFromString(const QString &string); + static QString electricBorderActionToString(int action); +}; + +} // namespace + +#endif diff --git a/kcmkwin/kwinscreenedges/main.ui b/kcmkwin/kwinscreenedges/main.ui new file mode 100644 index 0000000..3a08189 --- /dev/null +++ b/kcmkwin/kwinscreenedges/main.ui @@ -0,0 +1,331 @@ + + + KWinScreenEdgesConfigUI + + + + 0 + 0 + 500 + 525 + + + + + 500 + 525 + + + + + + + You can trigger an action by pushing the mouse cursor against the corresponding screen edge or corner. + + + true + + + + + + + Qt::Vertical + + + QSizePolicy::Minimum + + + + 20 + 20 + + + + + + + + + 200 + 200 + + + + Qt::StrongFocus + + + + + + + Qt::AlignHCenter|Qt::AlignTop + + + + + &Maximize: + + + kcfg_ElectricBorderMaximize + + + + + + + Windows dragged to top edge + + + + + + + &Tile: + + + kcfg_ElectricBorderTiling + + + + + + + Windows dragged to left or right edge + + + + + + + Trigger &quarter tiling in: + + + electricBorderCornerRatioSpin + + + + + + + + + false + + + % + + + Outer + + + 1 + + + 49 + + + + + + + false + + + of the screen + + + + + + + + + Change desktop when the mouse cursor is pushed against the edge of the screen + + + &Switch desktop on edge: + + + kcfg_ElectricBorders + + + + + + + + Disabled + + + + + Only When Moving Windows + + + + + Always Enabled + + + + + + + + Amount of time required for the mouse cursor to be pushed against the edge of the screen before the action is triggered + + + Activation &delay: + + + kcfg_ElectricBorderDelay + + + + + + + ms + + + 1000 + + + 50 + + + 0 + + + + + + + true + + + Amount of time required after triggering an action until the next trigger can occur + + + &Reactivation delay: + + + kcfg_ElectricBorderCooldown + + + + + + + true + + + ms + + + 1000 + + + 50 + + + 0 + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 4 + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + + + + + KComboBox + QComboBox +
kcombobox.h
+
+ + KWin::Monitor + QWidget +
monitor.h
+ 1 +
+
+ + + + kcfg_ElectricBorderTiling + toggled(bool) + label_1 + setEnabled(bool) + + + 20 + 20 + + + 20 + 20 + + + + + kcfg_ElectricBorderTiling + toggled(bool) + electricBorderCornerRatioSpin + setEnabled(bool) + + + 20 + 20 + + + 20 + 20 + + + + + kcfg_ElectricBorderTiling + toggled(bool) + electricBorderCornerRatioLabel + setEnabled(bool) + + + 20 + 20 + + + 20 + 20 + + + + +
diff --git a/kcmkwin/kwinscreenedges/monitor.cpp b/kcmkwin/kwinscreenedges/monitor.cpp new file mode 100644 index 0000000..c12ca2b --- /dev/null +++ b/kcmkwin/kwinscreenedges/monitor.cpp @@ -0,0 +1,313 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008 Lubos Lunak + SPDX-FileCopyrightText: 2009 Lucas Murray + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "monitor.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace KWin +{ + +static QWindow *windowFromWidget(const QWidget *widget) +{ + QWindow *windowHandle = widget->windowHandle(); + if (windowHandle) { + return windowHandle; + } + + const QWidget *nativeParent = widget->nativeParentWidget(); + if (nativeParent) { + return nativeParent->windowHandle(); + } + + return nullptr; +} + +static QScreen *screenFromWidget(const QWidget *widget) +{ + const QWindow *windowHandle = windowFromWidget(widget); + if (windowHandle && windowHandle->screen()) { + return windowHandle->screen(); + } + + return QGuiApplication::primaryScreen(); +} + +Monitor::Monitor(QWidget* parent) + : ScreenPreviewWidget(parent) +{ + QRect avail = screenFromWidget(this)->geometry(); + setRatio((qreal)avail.width() / (qreal)avail.height()); + for (int i = 0; + i < 8; + ++i) + popups[ i ] = new QMenu(this); + scene = new QGraphicsScene(this); + view = new QGraphicsView(scene, this); + view->setBackgroundBrush(Qt::black); + view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + view->setFocusPolicy(Qt::NoFocus); + view->setFrameShape(QFrame::NoFrame); + for (int i = 0; + i < 8; + ++i) { + items[ i ] = new Corner(this); + scene->addItem(items[ i ]); + hidden[ i ] = false; + grp[ i ] = new QActionGroup(this); + } + checkSize(); +} + +void Monitor::clear() +{ + for (int i = 0; + i < 8; + ++i) { + popups[ i ]->clear(); + setEdge(i, false); + setEdgeHidden(i, false); + delete grp[ i ]; + grp[ i ] = new QActionGroup(this); + } +} + +void Monitor::resizeEvent(QResizeEvent* e) +{ + ScreenPreviewWidget::resizeEvent(e); + checkSize(); +} + +void Monitor::checkSize() +{ + QRect contentsRect = previewRect(); + //int w = 151; + //int h = 115; + view->setGeometry(contentsRect); + scene->setSceneRect(QRect(QPoint(0, 0), contentsRect.size())); + int x2 = (contentsRect.width() - 20) / 2; + int x3 = contentsRect.width() - 20; + int y2 = (contentsRect.height() - 20) / 2; + int y3 = contentsRect.height() - 20; + items[ 0 ]->setRect(0, y2, 20, 20); + items[ 1 ]->setRect(x3, y2, 20, 20); + items[ 2 ]->setRect(x2, 0, 20, 20); + items[ 3 ]->setRect(x2, y3, 20, 20); + items[ 4 ]->setRect(0, 0, 20, 20); + items[ 5 ]->setRect(x3, 0, 20, 20); + items[ 6 ]->setRect(0, y3, 20, 20); + items[ 7 ]->setRect(x3, y3, 20, 20); +} + +void Monitor::setEdge(int edge, bool set) +{ + items[ edge ]->setActive(set); +} + +bool Monitor::edge(int edge) const +{ + return items[ edge ]->brush() == Qt::green; +} + +void Monitor::setEdgeEnabled(int edge, bool enabled) +{ + for (QAction *action : qAsConst(popup_actions[edge])) { + action->setEnabled(enabled); + } +} + +void Monitor::setEdgeHidden(int edge, bool set) +{ + hidden[ edge ] = set; + if (set) + items[ edge ]->hide(); + else + items[ edge ]->show(); +} + +bool Monitor::edgeHidden(int edge) const +{ + return hidden[ edge ]; +} + +void Monitor::addEdgeItem(int edge, const QString& item) +{ + QAction* act = popups[ edge ]->addAction(item); + act->setCheckable(true); + popup_actions[ edge ].append(act); + grp[ edge ]->addAction(act); + if (popup_actions[ edge ].count() == 1) { + act->setChecked(true); + items[ edge ]->setToolTip(item); + } + setEdge(edge, !popup_actions[ edge ][ 0 ]->isChecked()); +} + +void Monitor::setEdgeItemEnabled(int edge, int index, bool enabled) +{ + popup_actions[ edge ][ index ]->setEnabled(enabled); +} + +bool Monitor::edgeItemEnabled(int edge, int index) const +{ + return popup_actions[ edge ][ index ]->isEnabled(); +} + +void Monitor::selectEdgeItem(int edge, int index) +{ + popup_actions[ edge ][ index ]->setChecked(true); + setEdge(edge, !popup_actions[ edge ][ 0 ]->isChecked()); + QString actionText = popup_actions[ edge ][ index ]->text(); + // remove accelerators added by KAcceleratorManager + actionText = KLocalizedString::removeAcceleratorMarker(actionText); + items[ edge ]->setToolTip(actionText); +} + +int Monitor::selectedEdgeItem(int edge) const +{ + foreach (QAction * act, popup_actions[ edge ]) + if (act->isChecked()) + return popup_actions[ edge ].indexOf(act); + abort(); +} + +void Monitor::popup(Corner* c, QPoint pos) +{ + for (int i = 0; + i < 8; + ++i) { + if (items[ i ] == c) { + if (popup_actions[ i ].count() == 0) + return; + if (QAction* a = popups[ i ]->exec(pos)) { + selectEdgeItem(i, popup_actions[ i ].indexOf(a)); + emit changed(); + emit edgeSelectionChanged(i, popup_actions[ i ].indexOf(a)); + c->setToolTip(KLocalizedString::removeAcceleratorMarker(a->text())); + } + return; + } + } + abort(); +} + +void Monitor::flip(Corner* c, QPoint pos) +{ + for (int i = 0; + i < 8; + ++i) { + if (items[ i ] == c) { + if (popup_actions[ i ].count() == 0) + setEdge(i, !edge(i)); + else + popup(c, pos); + return; + } + } + abort(); +} + +Monitor::Corner::Corner(Monitor* m) + : monitor(m), + m_active(false), + m_hover(false) +{ + button = new Plasma::FrameSvg(); + button->setImagePath("widgets/button"); + setAcceptHoverEvents(true); +} + +Monitor::Corner::~Corner() +{ + delete button; +} + +void Monitor::Corner::contextMenuEvent(QGraphicsSceneContextMenuEvent* e) +{ + monitor->popup(this, e->screenPos()); +} + +void Monitor::Corner::mousePressEvent(QGraphicsSceneMouseEvent* e) +{ + monitor->flip(this, e->screenPos()); +} + +void Monitor::Corner::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) +{ + Q_UNUSED(option) + Q_UNUSED(widget) + + if (m_hover) { + button->setElementPrefix("normal"); + + qreal left, top, right, bottom; + button->getMargins(left, top, right, bottom); + + button->setElementPrefix("active"); + qreal activeLeft, activeTop, activeRight, activeBottom; + button->getMargins(activeLeft, activeTop, activeRight, activeBottom); + + QRectF activeRect = QRectF(QPointF(0, 0), rect().size()); + activeRect.adjust(left - activeLeft, top - activeTop, + -(right - activeRight), -(bottom - activeBottom)); + button->setElementPrefix("active"); + button->resizeFrame(activeRect.size()); + button->paintFrame(painter, rect().topLeft() + activeRect.topLeft()); + } else { + button->setElementPrefix(m_active ? "pressed" : "normal"); + button->resizeFrame(rect().size()); + button->paintFrame(painter, rect().topLeft()); + } + + if (m_active) { + QPainterPath roundedRect; + painter->setRenderHint(QPainter::Antialiasing); + roundedRect.addRoundedRect(rect().adjusted(5, 5, -5, -5), 2, 2); + painter->fillPath(roundedRect, QApplication::palette().text()); + } +} + +void Monitor::Corner::hoverEnterEvent(QGraphicsSceneHoverEvent * e) +{ + Q_UNUSED(e); + m_hover = true; + update(); +} + +void Monitor::Corner::hoverLeaveEvent(QGraphicsSceneHoverEvent * e) +{ + Q_UNUSED(e); + m_hover = false; + update(); +} + +void Monitor::Corner::setActive(bool active) +{ + m_active = active; + update(); +} + +bool Monitor::Corner::active() const +{ + return m_active; +} +} // namespace + diff --git a/kcmkwin/kwinscreenedges/monitor.h b/kcmkwin/kwinscreenedges/monitor.h new file mode 100644 index 0000000..a62334a --- /dev/null +++ b/kcmkwin/kwinscreenedges/monitor.h @@ -0,0 +1,104 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008 Lubos Lunak + SPDX-FileCopyrightText: 2009 Lucas Murray + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef CCSM_MONITOR_H +#define CCSM_MONITOR_H + +#include "screenpreviewwidget.h" + +#include +#include +#include + +class QAction; +class QGraphicsView; +class QGraphicsScene; +class QMenu; + +namespace Plasma +{ +class FrameSvg; +} + +namespace KWin +{ + +class Monitor + : public ScreenPreviewWidget +{ + Q_OBJECT +public: + explicit Monitor(QWidget* parent); + void setEdge(int edge, bool set); + bool edge(int edge) const; + void setEdgeEnabled(int edge, bool enabled); + void setEdgeHidden(int edge, bool set); + bool edgeHidden(int edge) const; + void clear(); + void addEdgeItem(int edge, const QString& item); + void setEdgeItemEnabled(int edge, int index, bool enabled); + bool edgeItemEnabled(int edge, int index) const; + void selectEdgeItem(int edge, int index); + int selectedEdgeItem(int edge) const; + + enum Edges { + Left, + Right, + Top, + Bottom, + TopLeft, + TopRight, + BottomLeft, + BottomRight, + None + }; +Q_SIGNALS: + void changed(); + void edgeSelectionChanged(int edge, int index); +protected: + void resizeEvent(QResizeEvent* e) override; +private: + class Corner; + void popup(Corner* c, QPoint pos); + void flip(Corner* c, QPoint pos); + void checkSize(); + QGraphicsView* view; + QGraphicsScene* scene; + Corner* items[ 8 ]; + bool hidden[ 8 ]; + QMenu* popups[ 8 ]; + QVector< QAction* > popup_actions[ 8 ]; + QActionGroup* grp[ 8 ]; +}; + +class Monitor::Corner + : public QGraphicsRectItem +{ +public: + Corner(Monitor* m); + ~Corner() override; + void setActive(bool active); + bool active() const; +protected: + void contextMenuEvent(QGraphicsSceneContextMenuEvent* e) override; + void mousePressEvent(QGraphicsSceneMouseEvent* e) override; + void hoverEnterEvent(QGraphicsSceneHoverEvent * e) override; + void hoverLeaveEvent(QGraphicsSceneHoverEvent * e) override; + void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget = nullptr) override; +private: + Monitor* monitor; + Plasma::FrameSvg *button; + bool m_active; + bool m_hover; +}; + +} // namespace + +#endif diff --git a/kcmkwin/kwinscreenedges/screenpreviewwidget.cpp b/kcmkwin/kwinscreenedges/screenpreviewwidget.cpp new file mode 100644 index 0000000..b506318 --- /dev/null +++ b/kcmkwin/kwinscreenedges/screenpreviewwidget.cpp @@ -0,0 +1,149 @@ +/* + SPDX-FileCopyrightText: 2009 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "screenpreviewwidget.h" + +#include +#include +#include + +#include +#include + +#include +#include + + +class ScreenPreviewWidgetPrivate +{ +public: + ScreenPreviewWidgetPrivate(ScreenPreviewWidget *screen) + : q(screen), + ratio(1) + {} + + ~ScreenPreviewWidgetPrivate() + {} + + void updateRect(const QRectF& rect) + { + q->update(rect.toRect()); + } + + void updateScreenGraphics() + { + int bottomElements = screenGraphics->elementSize("base").height() + screenGraphics->marginSize(Plasma::Types::BottomMargin); + QRect bounds(QPoint(0,0), QSize(q->size().width(), q->height() - bottomElements)); + + QSize monitorSize(q->size().width(), q->size().width()/ratio); + monitorSize.scale(bounds.size(), Qt::KeepAspectRatio); + + if (monitorSize.isEmpty()) { + return; + } + + monitorRect = QRect(QPoint(0,0), monitorSize); + monitorRect.moveCenter(bounds.center()); + + screenGraphics->resizeFrame(monitorRect.size()); + + previewRect = screenGraphics->contentsRect().toRect(); + previewRect.moveCenter(bounds.center()); + } + + ScreenPreviewWidget *q; + Plasma::FrameSvg *screenGraphics; + QPixmap preview; + QRect monitorRect; + qreal ratio; + QRect previewRect; +}; + +ScreenPreviewWidget::ScreenPreviewWidget(QWidget *parent) + : QWidget(parent), + d(new ScreenPreviewWidgetPrivate(this)) +{ + d->screenGraphics = new Plasma::FrameSvg(this); + d->screenGraphics->setImagePath("widgets/monitor"); + d->updateScreenGraphics(); +} + +ScreenPreviewWidget::~ScreenPreviewWidget() +{ + delete d; +} + +void ScreenPreviewWidget::setPreview(const QPixmap &preview) +{ + d->preview = preview; + + update(); +} + +const QPixmap ScreenPreviewWidget::preview() const +{ + return d->preview; +} + +void ScreenPreviewWidget::setRatio(const qreal ratio) +{ + d->ratio = ratio; + d->updateScreenGraphics(); +} + +qreal ScreenPreviewWidget::ratio() const +{ + return d->ratio; +} + +QRect ScreenPreviewWidget::previewRect() const +{ + return d->previewRect; +} + +void ScreenPreviewWidget::resizeEvent(QResizeEvent *e) +{ + Q_UNUSED(e) + d->updateScreenGraphics(); +} + +void ScreenPreviewWidget::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event) + if (d->monitorRect.size().isEmpty()) { + return; + } + + QPainter painter(this); + QPoint standPosition(d->monitorRect.center().x() - d->screenGraphics->elementSize("base").width()/2, d->previewRect.bottom()); + + d->screenGraphics->paint(&painter, QRect(standPosition, d->screenGraphics->elementSize("base")), "base"); + d->screenGraphics->paintFrame(&painter, d->monitorRect.topLeft()); + + painter.save(); + if (!d->preview.isNull()) { + painter.setRenderHint(QPainter::SmoothPixmapTransform); + painter.drawPixmap(d->previewRect, d->preview, d->preview.rect()); + } + painter.restore(); + + d->screenGraphics->paint(&painter, d->previewRect, "glass"); +} + +void ScreenPreviewWidget::dropEvent(QDropEvent *e) +{ + if (!e->mimeData()->hasUrls()) + return; + + QList uris(KUrlMimeData::urlsFromMimeData(e->mimeData())); + if (!uris.isEmpty()) { + // TODO: Download remote file + if (uris.first().isLocalFile()) + emit imageDropped(uris.first().path()); + } +} + +#include "moc_screenpreviewwidget.cpp" diff --git a/kcmkwin/kwinscreenedges/screenpreviewwidget.h b/kcmkwin/kwinscreenedges/screenpreviewwidget.h new file mode 100644 index 0000000..a8ae965 --- /dev/null +++ b/kcmkwin/kwinscreenedges/screenpreviewwidget.h @@ -0,0 +1,44 @@ +/* + SPDX-FileCopyrightText: 2009 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef SCREENPREVIEWWIDGET_H +#define SCREENPREVIEWWIDGET_H + +#include + +class ScreenPreviewWidgetPrivate; + +class ScreenPreviewWidget : public QWidget +{ + Q_OBJECT + +public: + ScreenPreviewWidget(QWidget *parent); + ~ScreenPreviewWidget() override; + + void setPreview(const QPixmap &preview); + const QPixmap preview() const; + void setRatio(const qreal ratio); + qreal ratio() const; + + QRect previewRect() const; + +protected: + void resizeEvent(QResizeEvent *event) override; + void paintEvent(QPaintEvent *event) override; + void dropEvent(QDropEvent *event) override; + +Q_SIGNALS: + void imageDropped(const QString &); + +private: + ScreenPreviewWidgetPrivate *const d; + + Q_PRIVATE_SLOT(d, void updateRect(const QRectF& rect)) +}; + + +#endif diff --git a/kcmkwin/kwinscreenedges/touch.cpp b/kcmkwin/kwinscreenedges/touch.cpp new file mode 100644 index 0000000..785540f --- /dev/null +++ b/kcmkwin/kwinscreenedges/touch.cpp @@ -0,0 +1,340 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008 Martin Gräßlin + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2020 Cyril Rossi + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "touch.h" +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "kwintouchscreenedgeconfigform.h" +#include "kwintouchscreensettings.h" +#include "kwintouchscreenscriptsettings.h" + +K_PLUGIN_FACTORY(KWinScreenEdgesConfigFactory, registerPlugin();) + +namespace KWin +{ + +KWinScreenEdgesConfig::KWinScreenEdgesConfig(QWidget *parent, const QVariantList &args) + : KCModule(parent, args) + , m_form(new KWinTouchScreenEdgeConfigForm(this)) + , m_config(KSharedConfig::openConfig("kwinrc")) + , m_settings(new KWinTouchScreenSettings(this)) +{ + QVBoxLayout* layout = new QVBoxLayout(this); + layout->addWidget(m_form); + + monitorInit(); + + connect(m_form, &KWinTouchScreenEdgeConfigForm::saveNeededChanged, this, &KWinScreenEdgesConfig::unmanagedWidgetChangeState); + connect(m_form, &KWinTouchScreenEdgeConfigForm::defaultChanged, this, &KWinScreenEdgesConfig::unmanagedWidgetDefaultState); +} + +KWinScreenEdgesConfig::~KWinScreenEdgesConfig() +{ +} + +void KWinScreenEdgesConfig::load() +{ + KCModule::load(); + m_settings->load(); + for (KWinTouchScreenScriptSettings *setting : qAsConst(m_scriptSettings)) { + setting->load(); + } + + monitorLoadSettings(); + monitorLoadDefaultSettings(); + m_form->reload(); +} + +void KWinScreenEdgesConfig::save() +{ + monitorSaveSettings(); + m_settings->save(); + for (KWinTouchScreenScriptSettings *setting : qAsConst(m_scriptSettings)) { + setting->save(); + } + + // Reload saved settings to ScreenEdge UI + monitorLoadSettings(); + m_form->reload(); + + // Reload KWin. + QDBusMessage message = QDBusMessage::createSignal("/KWin", "org.kde.KWin", "reloadConfig"); + QDBusConnection::sessionBus().send(message); + // and reconfigure the effects + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(BuiltInEffects::nameForEffect(BuiltInEffect::PresentWindows)); + interface.reconfigureEffect(BuiltInEffects::nameForEffect(BuiltInEffect::DesktopGrid)); + interface.reconfigureEffect(BuiltInEffects::nameForEffect(BuiltInEffect::Cube)); + + KCModule::save(); +} + +void KWinScreenEdgesConfig::defaults() +{ + m_form->setDefaults(); + + KCModule::defaults(); +} + +void KWinScreenEdgesConfig::showEvent(QShowEvent* e) +{ + KCModule::showEvent(e); + + monitorShowEvent(); +} + +// Copied from kcmkwin/kwincompositing/main.cpp +bool KWinScreenEdgesConfig::effectEnabled(const BuiltInEffect& effect, const KConfigGroup& cfg) const +{ + return cfg.readEntry(BuiltInEffects::nameForEffect(effect) + "Enabled", BuiltInEffects::enabledByDefault(effect)); +} + +//----------------------------------------------------------------------------- +// Monitor + +void KWinScreenEdgesConfig::monitorInit() +{ + m_form->monitorHideEdge(ElectricTopLeft, true); + m_form->monitorHideEdge(ElectricTopRight, true); + m_form->monitorHideEdge(ElectricBottomRight, true); + m_form->monitorHideEdge(ElectricBottomLeft, true); + + m_form->monitorAddItem(i18n("No Action")); + m_form->monitorAddItem(i18n("Show Desktop")); + m_form->monitorAddItem(i18n("Lock Screen")); + m_form->monitorAddItem(i18n("Show KRunner")); + m_form->monitorAddItem(i18n("Activity Manager")); + m_form->monitorAddItem(i18n("Application Launcher")); + + // Add the effects + const QString presentWindowsName = BuiltInEffects::effectData(BuiltInEffect::PresentWindows).displayName; + m_form->monitorAddItem(i18n("%1 - All Desktops", presentWindowsName)); + m_form->monitorAddItem(i18n("%1 - Current Desktop", presentWindowsName)); + m_form->monitorAddItem(i18n("%1 - Current Application", presentWindowsName)); + m_form->monitorAddItem(BuiltInEffects::effectData(BuiltInEffect::DesktopGrid).displayName); + const QString cubeName = BuiltInEffects::effectData(BuiltInEffect::Cube).displayName; + m_form->monitorAddItem(i18n("%1 - Cube", cubeName)); + m_form->monitorAddItem(i18n("%1 - Cylinder", cubeName)); + m_form->monitorAddItem(i18n("%1 - Sphere", cubeName)); + + m_form->monitorAddItem(i18n("Toggle window switching")); + m_form->monitorAddItem(i18n("Toggle alternative window switching")); + + const QString scriptFolder = QStringLiteral("kwin/scripts/"); + const auto scripts = KPackage::PackageLoader::self()->listPackages(QStringLiteral("KWin/Script"), scriptFolder); + + KConfigGroup config(m_config, "Plugins"); + for (const KPluginMetaData &script: scripts) { + if (script.value(QStringLiteral("X-KWin-Border-Activate")) != QLatin1String("true")) { + continue; + } + + if (!config.readEntry(script.pluginId() + QStringLiteral("Enabled"), script.isEnabledByDefault())) { + continue; + } + m_scripts << script.pluginId(); + m_form->monitorAddItem(script.name()); + m_scriptSettings[script.pluginId()] = new KWinTouchScreenScriptSettings(script.pluginId(), this); + } + + monitorShowEvent(); +} + +void KWinScreenEdgesConfig::monitorLoadSettings() +{ + // Load ElectricBorderActions + m_form->monitorChangeEdge(ElectricTop, KWinScreenEdgesConfig::electricBorderActionFromString(m_settings->top())); + m_form->monitorChangeEdge(ElectricRight, KWinScreenEdgesConfig::electricBorderActionFromString(m_settings->right())); + m_form->monitorChangeEdge(ElectricBottom, KWinScreenEdgesConfig::electricBorderActionFromString(m_settings->bottom())); + m_form->monitorChangeEdge(ElectricLeft, KWinScreenEdgesConfig::electricBorderActionFromString(m_settings->left())); + + // Load effect-specific actions: + + // Present Windows BorderActivateAll + m_form->monitorChangeEdge(m_settings->touchBorderActivateAll(), PresentWindowsAll); + // PresentWindows BorderActivate + m_form->monitorChangeEdge(m_settings->touchBorderActivatePresentWindows(), PresentWindowsCurrent); + // PresentWindows BorderActivateClass + m_form->monitorChangeEdge(m_settings->touchBorderActivateClass(), PresentWindowsClass); + + // Desktop Grid BorderActivate + m_form->monitorChangeEdge(m_settings->touchBorderActivateDesktopGrid(), DesktopGrid); + + // Desktop Cube BorderActivate + m_form->monitorChangeEdge(m_settings->touchBorderActivateCube(), Cube); + // Desktop Cube BorderActivateCylinder + m_form->monitorChangeEdge(m_settings->touchBorderActivateCylinder(), Cylinder); + // Desktop Cube BorderActivateSphere + m_form->monitorChangeEdge(m_settings->touchBorderActivateSphere(), Sphere); + + // TabBox BorderActivate + m_form->monitorChangeEdge(m_settings->touchBorderActivateTabBox(), TabBox); + // Alternative TabBox + m_form->monitorChangeEdge(m_settings->touchBorderAlternativeActivate(), TabBoxAlternative); + + // Scripts + for (int i=0; i < m_scripts.size(); i++) { + int index = EffectCount + i; + m_form->monitorChangeEdge(m_scriptSettings[m_scripts[i]]->touchBorderActivate(), index); + } +} + +void KWinScreenEdgesConfig::monitorLoadDefaultSettings() +{ + m_form->monitorChangeDefaultEdge(ElectricTop, KWinScreenEdgesConfig::electricBorderActionFromString(m_settings->defaultTopValue())); + m_form->monitorChangeDefaultEdge(ElectricRight, KWinScreenEdgesConfig::electricBorderActionFromString(m_settings->defaultRightValue())); + m_form->monitorChangeDefaultEdge(ElectricBottom, KWinScreenEdgesConfig::electricBorderActionFromString(m_settings->defaultBottomValue())); + m_form->monitorChangeDefaultEdge(ElectricLeft, KWinScreenEdgesConfig::electricBorderActionFromString(m_settings->defaultLeftValue())); + + // Present Windows BorderActivateAll + m_form->monitorChangeDefaultEdge(m_settings->defaultTouchBorderActivateAllValue(), PresentWindowsAll); + // PresentWindows BorderActivate + m_form->monitorChangeDefaultEdge(m_settings->defaultTouchBorderActivatePresentWindowsValue(), PresentWindowsCurrent); + // PresentWindows BorderActivateClass + m_form->monitorChangeDefaultEdge(m_settings->defaultTouchBorderActivateClassValue(), PresentWindowsClass); + + // Desktop Grid BorderActivate + m_form->monitorChangeDefaultEdge(m_settings->defaultTouchBorderActivateDesktopGridValue(), DesktopGrid); + + // Desktop Cube BorderActivate + m_form->monitorChangeDefaultEdge(m_settings->defaultTouchBorderActivateCubeValue(), Cube); + // Desktop Cube BorderActivateCylinder + m_form->monitorChangeDefaultEdge(m_settings->defaultTouchBorderActivateCylinderValue(), Cylinder); + // Desktop Cube BorderActivateSphere + m_form->monitorChangeDefaultEdge(m_settings->defaultTouchBorderActivateSphereValue(), Sphere); + + // TabBox BorderActivate + m_form->monitorChangeDefaultEdge(m_settings->defaultTouchBorderActivateTabBoxValue(), TabBox); + // Alternative TabBox + m_form->monitorChangeDefaultEdge(m_settings->defaultTouchBorderAlternativeActivateValue(), TabBoxAlternative); +} + +void KWinScreenEdgesConfig::monitorSaveSettings() +{ + // Save ElectricBorderActions + m_settings->setTop(KWinScreenEdgesConfig::electricBorderActionToString(m_form->selectedEdgeItem(ElectricTop))); + m_settings->setRight(KWinScreenEdgesConfig::electricBorderActionToString(m_form->selectedEdgeItem(ElectricRight))); + m_settings->setBottom(KWinScreenEdgesConfig::electricBorderActionToString(m_form->selectedEdgeItem(ElectricBottom))); + m_settings->setLeft(KWinScreenEdgesConfig::electricBorderActionToString(m_form->selectedEdgeItem(ElectricLeft))); + + // Save effect-specific actions: + + // Present Windows + m_settings->setTouchBorderActivateAll(m_form->monitorCheckEffectHasEdge(PresentWindowsAll)); + m_settings->setTouchBorderActivatePresentWindows(m_form->monitorCheckEffectHasEdge(PresentWindowsCurrent)); + m_settings->setTouchBorderActivateClass(m_form->monitorCheckEffectHasEdge(PresentWindowsClass)); + + // Desktop Grid + m_settings->setTouchBorderActivateDesktopGrid(m_form->monitorCheckEffectHasEdge(DesktopGrid)); + + // Desktop Cube + m_settings->setTouchBorderActivateCube(m_form->monitorCheckEffectHasEdge(Cube)); + m_settings->setTouchBorderActivateCylinder(m_form->monitorCheckEffectHasEdge(Cylinder)); + m_settings->setTouchBorderActivateSphere(m_form->monitorCheckEffectHasEdge(Sphere)); + + // TabBox + m_settings->setTouchBorderActivateTabBox(m_form->monitorCheckEffectHasEdge(TabBox)); + m_settings->setTouchBorderAlternativeActivate(m_form->monitorCheckEffectHasEdge(TabBoxAlternative)); + + // Scripts + for (int i = 0; i < m_scripts.size(); i++) { + int index = EffectCount + i; + m_scriptSettings[m_scripts[i]]->setTouchBorderActivate(m_form->monitorCheckEffectHasEdge(index)); + } +} + +void KWinScreenEdgesConfig::monitorShowEvent() +{ + // Check if they are enabled + KConfigGroup config(m_config, "Plugins"); + + // Present Windows + bool enabled = effectEnabled(BuiltInEffect::PresentWindows, config); + m_form->monitorItemSetEnabled(PresentWindowsCurrent, enabled); + m_form->monitorItemSetEnabled(PresentWindowsAll, enabled); + + // Desktop Grid + enabled = effectEnabled(BuiltInEffect::DesktopGrid, config); + m_form->monitorItemSetEnabled(DesktopGrid, enabled); + + // Desktop Cube + enabled = effectEnabled(BuiltInEffect::Cube, config); + m_form->monitorItemSetEnabled(Cube, enabled); + m_form->monitorItemSetEnabled(Cylinder, enabled); + m_form->monitorItemSetEnabled(Sphere, enabled); + // tabbox, depends on reasonable focus policy. + KConfigGroup config2(m_config, "Windows"); + QString focusPolicy = config2.readEntry("FocusPolicy", QString()); + bool reasonable = focusPolicy != "FocusStrictlyUnderMouse" && focusPolicy != "FocusUnderMouse"; + m_form->monitorItemSetEnabled(TabBox, reasonable); + m_form->monitorItemSetEnabled(TabBoxAlternative, reasonable); + + // Disable Edge if TouchEdges group entries are immutable + m_form->monitorEnableEdge(ElectricTop, !m_settings->isTopImmutable()); + m_form->monitorEnableEdge(ElectricRight, !m_settings->isRightImmutable()); + m_form->monitorEnableEdge(ElectricBottom, !m_settings->isBottomImmutable()); + m_form->monitorEnableEdge(ElectricLeft, !m_settings->isLeftImmutable()); +} + +ElectricBorderAction KWinScreenEdgesConfig::electricBorderActionFromString(const QString &string) +{ + QString lowerName = string.toLower(); + if (lowerName == QStringLiteral("showdesktop")) { + return ElectricActionShowDesktop; + } + if (lowerName == QStringLiteral("lockscreen")) { + return ElectricActionLockScreen; + } + if (lowerName == QStringLiteral("krunner")) { + return ElectricActionKRunner; + } + if (lowerName == QStringLiteral("activitymanager")) { + return ElectricActionActivityManager; + } + if (lowerName == QStringLiteral("applicationlauncher")) { + return ElectricActionApplicationLauncher; + } + return ElectricActionNone; +} + +QString KWinScreenEdgesConfig::electricBorderActionToString(int action) +{ + switch (action) { + case 1: + return QStringLiteral("ShowDesktop"); + case 2: + return QStringLiteral("LockScreen"); + case 3: + return QStringLiteral("KRunner"); + case 4: + return QStringLiteral("ActivityManager"); + case 5: + return QStringLiteral("ApplicationLauncher"); + default: + return QStringLiteral("None"); + } +} + +} // namespace + +#include "touch.moc" diff --git a/kcmkwin/kwinscreenedges/touch.h b/kcmkwin/kwinscreenedges/touch.h new file mode 100644 index 0000000..72bfb9d --- /dev/null +++ b/kcmkwin/kwinscreenedges/touch.h @@ -0,0 +1,78 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2020 Cyril Rossi + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef __TOUCH_H__ +#define __TOUCH_H__ + +#include +#include + +#include "kwinglobals.h" + +class QShowEvent; + +namespace KWin +{ +class KWinTouchScreenEdgeConfigForm; +class KWinTouchScreenSettings; +class KWinTouchScreenScriptSettings; +enum class BuiltInEffect; + +class KWinScreenEdgesConfig : public KCModule +{ + Q_OBJECT + +public: + explicit KWinScreenEdgesConfig(QWidget *parent, const QVariantList &args); + ~KWinScreenEdgesConfig() override; + +public Q_SLOTS: + void save() override; + void load() override; + void defaults() override; + +protected: + void showEvent(QShowEvent *e) override; + +private: + KWinTouchScreenEdgeConfigForm *m_form; + KSharedConfigPtr m_config; + QStringList m_scripts; //list of script IDs ordered in the list they are presented in the menu + QHash m_scriptSettings; + KWinTouchScreenSettings *m_settings; + + enum EffectActions { + PresentWindowsAll = ELECTRIC_ACTION_COUNT, // Start at the end of built in actions + PresentWindowsCurrent, + PresentWindowsClass, + DesktopGrid, + Cube, + Cylinder, + Sphere, + TabBox, + TabBoxAlternative, + EffectCount + }; + + bool effectEnabled(const BuiltInEffect &effect, const KConfigGroup &cfg) const; + + void monitorInit(); + void monitorLoadSettings(); + void monitorLoadDefaultSettings(); + void monitorSaveSettings(); + void monitorShowEvent(); + + static ElectricBorderAction electricBorderActionFromString(const QString &string); + static QString electricBorderActionToString(int action); +}; + +} // namespace + +#endif diff --git a/kcmkwin/kwinscreenedges/touch.ui b/kcmkwin/kwinscreenedges/touch.ui new file mode 100644 index 0000000..b30233b --- /dev/null +++ b/kcmkwin/kwinscreenedges/touch.ui @@ -0,0 +1,72 @@ + + + KWinTouchScreenConfigUi + + + + 0 + 0 + 500 + 500 + + + + + + + You can trigger an action by swiping from the screen edge towards the center of the screen. + + + true + + + + + + + Qt::Vertical + + + QSizePolicy::Minimum + + + + 20 + 20 + + + + + + + + + 200 + 200 + + + + Qt::StrongFocus + + + + + + + Qt::Vertical + + + + + + + + KWin::Monitor + QWidget +
monitor.h
+ 1 +
+
+ + +
diff --git a/kcmkwin/kwinscripts/CMakeLists.txt b/kcmkwin/kwinscripts/CMakeLists.txt new file mode 100644 index 0000000..12966bd --- /dev/null +++ b/kcmkwin/kwinscripts/CMakeLists.txt @@ -0,0 +1,28 @@ +# KI18N Translation Domain for this library +add_definitions(-DTRANSLATION_DOMAIN=\"kcm-kwin-scripts\") + +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/version.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/version.h) +include_directories(${CMAKE_CURRENT_BINARY_DIR}) + +set(kcm_SRCS + main.cpp + module.cpp +) + +ki18n_wrap_ui(kcm_SRCS module.ui) + +add_library(kcm_kwin_scripts MODULE ${kcm_SRCS}) + +target_link_libraries(kcm_kwin_scripts + Qt5::DBus + + KF5::I18n + KF5::KCMUtils + KF5::KIOCore + KF5::NewStuff + KF5::Package +) + +install(TARGETS kcm_kwin_scripts DESTINATION ${PLUGIN_INSTALL_DIR}) +install(FILES kwinscripts.desktop DESTINATION ${SERVICES_INSTALL_DIR}) +install(FILES kwinscripts.knsrc DESTINATION ${KDE_INSTALL_KNSRCDIR}) diff --git a/kcmkwin/kwinscripts/Messages.sh b/kcmkwin/kwinscripts/Messages.sh new file mode 100755 index 0000000..567ab31 --- /dev/null +++ b/kcmkwin/kwinscripts/Messages.sh @@ -0,0 +1,4 @@ +#!bin/sh +$EXTRACTRC `find . -name \*.rc -o -name \*.ui -o -name \*.kcfg` >> rc.cpp +$XGETTEXT `find . -name \*.cc -o -name \*.cpp -o -name \*.h` -o $podir/kcm-kwin-scripts.pot +rm -f rc.cpp diff --git a/kcmkwin/kwinscripts/kwinscripts.desktop b/kcmkwin/kwinscripts/kwinscripts.desktop new file mode 100644 index 0000000..5cb30f7 --- /dev/null +++ b/kcmkwin/kwinscripts/kwinscripts.desktop @@ -0,0 +1,166 @@ +[Desktop Entry] +Exec=kcmshell5 kwinscripts +Icon=preferences-system-windows-actions +Type=Service +X-KDE-ServiceTypes=KCModule + +X-KDE-Library=kcm_kwin_scripts +X-KDE-PluginKeyword=kwin-scripts +X-KDE-ParentApp=kcontrol + +X-KDE-Keywords=kwin script +X-KDE-Keywords[az]=kwin skripti +X-KDE-Keywords[bs]=kwin skripta +X-KDE-Keywords[ca]=script del KWin +X-KDE-Keywords[ca@valencia]=script de kwin +X-KDE-Keywords[cs]=skript KWinu +X-KDE-Keywords[da]=kwin-script +X-KDE-Keywords[de]=KWin-Skript +X-KDE-Keywords[el]=σενάριο kwin +X-KDE-Keywords[en_GB]=kwin script +X-KDE-Keywords[es]=guion de kwin +X-KDE-Keywords[et]=kwini skript +X-KDE-Keywords[eu]=KWin scripta +X-KDE-Keywords[fi]=kwin-skripti +X-KDE-Keywords[fr]=Script de KWin +X-KDE-Keywords[ga]=Script kwin +X-KDE-Keywords[gl]=script do kwin +X-KDE-Keywords[he]=תוספים של KWin +X-KDE-Keywords[hu]=kwin szkript +X-KDE-Keywords[ia]=Script de kwin +X-KDE-Keywords[id]=skrip kwin +X-KDE-Keywords[it]=Script kwin +X-KDE-Keywords[ja]=kwin スクリプト +X-KDE-Keywords[kk]=kwin скрипті +X-KDE-Keywords[km]=ស្គ្រីប kwin +X-KDE-Keywords[ko]=kwin 스크립트 +X-KDE-Keywords[lt]=kwin scenarijus +X-KDE-Keywords[mr]=के-विन स्क्रिप्ट +X-KDE-Keywords[nb]=kwin-skript +X-KDE-Keywords[nds]=KWin-Skript +X-KDE-Keywords[nl]=kwin-script +X-KDE-Keywords[nn]=kwin-skript +X-KDE-Keywords[pa]=kwin ਸਕ੍ਰਿਪਟ +X-KDE-Keywords[pl]=skrypt kwin +X-KDE-Keywords[pt]=programa do kwin +X-KDE-Keywords[pt_BR]=Script do kwin +X-KDE-Keywords[ro]=script kwin +X-KDE-Keywords[ru]=kwin script,сценарий kwin,скрипт kwin +X-KDE-Keywords[sk]=kwin skript +X-KDE-Keywords[sl]=skript kwin +X-KDE-Keywords[sr]=К‑винова скрипта +X-KDE-Keywords[sr@ijekavian]=К‑винова скрипта +X-KDE-Keywords[sr@ijekavianlatin]=KWinova skripta +X-KDE-Keywords[sr@latin]=KWinova skripta +X-KDE-Keywords[sv]=Kwin-skript +X-KDE-Keywords[tr]=kwin betiği +X-KDE-Keywords[uk]=скрипт,kwin +X-KDE-Keywords[x-test]=xxkwin scriptxx +X-KDE-Keywords[zh_CN]=kwin 脚本 +X-KDE-Keywords[zh_TW]=KWin 文稿 +X-KDE-System-Settings-Parent-Category=windowmanagement +X-KDE-Weight=70 + +Name=KWin Scripts +Name[az]=KWin Skriptləri +Name[bs]=KWin skripte +Name[ca]=Scripts del KWin +Name[ca@valencia]=Scripts de KWin +Name[cs]=Skripty KWinu +Name[da]=KWin-scripts +Name[de]=KWin-Skripte +Name[el]=Σενάρια KWin +Name[en_GB]=KWin Scripts +Name[es]=Guiones de KWin +Name[et]=KWini skriptid +Name[eu]=KWin scriptak +Name[fi]=KWin-skriptit +Name[fr]=Scripts de KWin +Name[ga]=Scripteanna KWin +Name[gl]=Scripts de KWin +Name[he]=תוספים של KWin +Name[hu]=KWin szkriptek +Name[ia]=Scripts de KWin +Name[id]=Skrip KWin +Name[it]=Script kwin +Name[ja]=KWin Scripts +Name[kk]=KWin скрипттері +Name[km]=ស្គ្រីប KWin +Name[ko]=KWin 스크립트 +Name[lt]=KWin scenarijai +Name[mr]=के-विन स्क्रिप्ट्स +Name[nb]=KWin-skripter +Name[nds]=KWin-Skripten +Name[nl]=KWin-scripts +Name[nn]=KWin-skript +Name[pa]=KWin ਸਕ੍ਰਿਪਟ +Name[pl]=Skrypty KWin +Name[pt]=Programas do KWin +Name[pt_BR]=Scripts do KWin +Name[ro]=Scripturi KWin +Name[ru]=Сценарии KWin +Name[sk]=KWin skripty +Name[sl]=Skripti KWin +Name[sr]=К‑винове скрипте +Name[sr@ijekavian]=К‑винове скрипте +Name[sr@ijekavianlatin]=KWinove skripte +Name[sr@latin]=KWinove skripte +Name[sv]=Kwin-skript +Name[tr]=KWin Betikleri +Name[uk]=Скрипти KWin +Name[vi]=Tập lệnh KWin +Name[x-test]=xxKWin Scriptsxx +Name[zh_CN]=KWin 脚本 +Name[zh_TW]=KWin 文稿 +Comment=Manage KWin scripts +Comment[az]=KWin skriptləri meneceri +Comment[bs]=Podesi KWin skripte +Comment[ca]=Gestiona els scripts del KWin +Comment[ca@valencia]=Gestiona els scripts de KWin +Comment[cs]=Spravovat skripty KWinu +Comment[da]=Håndtér KWin-scripts +Comment[de]=KWin-Skripte verwalten +Comment[el]=Διαχείριση σεναρίων KWin +Comment[en_GB]=Manage KWin scripts +Comment[es]=Gestionar guiones de KWin +Comment[et]=KWini skriptide haldamine +Comment[eu]=Kudeatu KWin scriptak +Comment[fi]=KWin-skriptien hallinta +Comment[fr]=Gérer les scripts de KWin +Comment[ga]=Bainistigh scripteanna KWin +Comment[gl]=Xestiona os scripts de KWin +Comment[he]=נהל תוספים של KWin +Comment[hu]=KWin szkriptek kezelése +Comment[ia]=Gere scriptos de KWin +Comment[id]=Kelola skrip KWin +Comment[it]=Gestione script kwin +Comment[kk]=KWin скрипттерін басқару +Comment[km]=គ្រប់គ្រង​ស្គ្រីប KWin +Comment[ko]=KWin 스크립트 관리 +Comment[lt]=Tvarkyti KWin scenarijus +Comment[mr]=के-विन स्क्रिप्ट्स व्यवस्थापीत करा +Comment[nb]=Behandle KWin-skripter +Comment[nds]=KWin-Skripten plegen +Comment[nl]=KWin-scripts beheren +Comment[nn]=Handsam KWin-skript +Comment[pa]=ਕੇਵਿਨ ਸਕ੍ਰਿਪਟ ਪਰਬੰਧ +Comment[pl]=Zarządzanie skryptami KWin +Comment[pt]=Gerir os programas do KWin +Comment[pt_BR]=Gerencia os scripts do KWin +Comment[ro]=Gestionare scripturi KWin +Comment[ru]=Управление сценариями KWin +Comment[sk]=Spravovať KWin skripty +Comment[sl]=Upravljajte s skripti KWin +Comment[sr]=Управљајте К‑винових скриптама +Comment[sr@ijekavian]=Управљајте К‑винових скриптама +Comment[sr@ijekavianlatin]=Upravljajte KWinovih skriptama +Comment[sr@latin]=Upravljajte KWinovih skriptama +Comment[sv]=Hantera Kwin-skript +Comment[tr]=KWin betiklerini yönet +Comment[uk]=Керування скриптами KWin +Comment[vi]=Quản lí tập lệnh KWin +Comment[x-test]=xxManage KWin scriptsxx +Comment[zh_CN]=管理 KWin 脚本 +Comment[zh_TW]=管理 KWin 文稿 + +Categories=KDE;X-KDE-settings-system; diff --git a/kcmkwin/kwinscripts/kwinscripts.knsrc b/kcmkwin/kwinscripts/kwinscripts.knsrc new file mode 100644 index 0000000..9508d6e --- /dev/null +++ b/kcmkwin/kwinscripts/kwinscripts.knsrc @@ -0,0 +1,48 @@ +[KNewStuff3] +Name=Window Manager Scripts +Name[az]=Pəncərə Meneceri skriptləri +Name[ca]=Scripts del gestor de finestres +Name[ca@valencia]=Scripts del gestor de finestres +Name[cs]=Skripty správce oken +Name[da]=Vindueshåndteringsscripts +Name[de]=Fensterverwaltungs-Skripte +Name[el]=Σενάρια διαχειριστή παραθύρων +Name[en_GB]=Window Manager Scripts +Name[es]=Guiones del gestor de ventanas +Name[et]=Aknahalduri skriptid +Name[eu]=Leiho kudeatzailearen scriptak +Name[fi]=Ikkunointiohjelman skriptit +Name[fr]=Scripts du gestionnaire de fenêtres +Name[gl]=Scripts do xestor de xanelas +Name[hu]=Ablakkezelő szkriptek +Name[ia]=Gerente de scripts de fenestra +Name[id]=Skrip Pengelola Window +Name[it]=Script del gestore delle finestre +Name[ko]=창 관리자 스크립트 +Name[lt]=Langų tvarkytuvės scenarijai +Name[nl]=Scripts van vensterbeheerder +Name[nn]=Skript for vindaugshandsamar +Name[pa]=ਵਿੰਡੋ ਮੈਨੇਜਰ ਸਕ੍ਰਿਪਟਾਂ +Name[pl]=Skrypty zarządzania oknami +Name[pt]=Programas do Gestor de Janelas +Name[pt_BR]=Scripts do gerenciador de janelas +Name[ro]=Scripturi pentru gestionar de ferestre +Name[ru]=Сценарии для диспетчера окон KWin +Name[sk]=Skripty správcu okien +Name[sl]=Skripti upravljalnika oken +Name[sr]=Скрипте менаџера прозора +Name[sr@ijekavian]=Скрипте менаџера прозора +Name[sr@ijekavianlatin]=Skripte menadžera prozora +Name[sr@latin]=Skripte menadžera prozora +Name[sv]=Fönsterhanteringsskript +Name[tr]=Pencere Yöneticisi Betikleri +Name[uk]=Скрипти засобу керування вікнами +Name[x-test]=xxWindow Manager Scriptsxx +Name[zh_CN]=窗口管理器脚本 +Name[zh_TW]=視窗管理員指令稿 + +ProvidersUrl=https://download.kde.org/ocs/providers.xml +Categories=KWin Scripts +StandardResource=tmp +Uncompress=kpackage +KPackageType=KWin/Script diff --git a/kcmkwin/kwinscripts/main.cpp b/kcmkwin/kwinscripts/main.cpp new file mode 100644 index 0000000..5389b5a --- /dev/null +++ b/kcmkwin/kwinscripts/main.cpp @@ -0,0 +1,14 @@ +/* + SPDX-FileCopyrightText: 2011 Tamas Krutki + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +#include "module.h" + +K_PLUGIN_FACTORY(KcmKWinScriptsFactory, + registerPlugin("kwin-scripts");) + +#include "main.moc" diff --git a/kcmkwin/kwinscripts/module.cpp b/kcmkwin/kwinscripts/module.cpp new file mode 100644 index 0000000..7917de8 --- /dev/null +++ b/kcmkwin/kwinscripts/module.cpp @@ -0,0 +1,178 @@ +/* + SPDX-FileCopyrightText: 2011 Tamas Krutki + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "module.h" +#include "ui_module.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "version.h" + +Module::Module(QWidget *parent, const QVariantList &args) : + KCModule(parent, args), + ui(new Ui::Module), + m_kwinConfig(KSharedConfig::openConfig("kwinrc")) +{ + KAboutData *about = new KAboutData("kwin-scripts", + i18n("KWin Scripts"), + global_s_versionStringFull, + i18n("Configure KWin scripts"), + KAboutLicense::GPL_V2); + + about->addAuthor(i18n("Tamás Krutki")); + setAboutData(about); + + ui->setupUi(this); + + ui->messageWidget->hide(); + + ui->ghnsButton->setConfigFile(QStringLiteral("kwinscripts.knsrc")); + connect(ui->ghnsButton, &KNS3::Button::dialogFinished, this, [this](const KNS3::Entry::List &changedEntries) { + if (!changedEntries.isEmpty()) { + ui->scriptSelector->clearPlugins(); + updateListViewContents(); + } + }); + + connect(ui->scriptSelector, &KPluginSelector::changed, this, qOverload(&KCModule::changed)); + connect(ui->scriptSelector, &KPluginSelector::defaulted, this, qOverload(&KCModule::defaulted)); + connect(ui->importScriptButton, &QPushButton::clicked, this, &Module::importScript); + + ui->scriptSelector->setAdditionalButtonHandler([this](const KPluginInfo &info) { + QPushButton *button = new QPushButton(ui->scriptSelector); + button->setIcon(QIcon::fromTheme(QStringLiteral("delete"))); + button->setEnabled(QFileInfo(info.entryPath()).isWritable()); + connect(button, &QPushButton::clicked, this, [this, info](){ + using namespace KPackage; + PackageStructure *structure = PackageLoader::self()->loadPackageStructure(QStringLiteral("KWin/Script")); + Package package(structure); + // We can get the package root from the entry path + QDir root = QFileInfo(info.entryPath()).dir(); + root.cdUp(); + KJob *uninstallJob = Package(structure).uninstall(info.pluginName(), root.absolutePath()); + connect(uninstallJob, &KJob::result, this, [this, uninstallJob](){ + ui->scriptSelector->clearPlugins(); + updateListViewContents(); + // If the uninstallation is successful the entry will be immediately removed + if (!uninstallJob->errorString().isEmpty()) { + ui->messageWidget->setText(i18n("Error when uninstalling KWin Script: %1", uninstallJob->errorString())); + ui->messageWidget->setMessageType(KMessageWidget::Error); + ui->messageWidget->animatedShow(); + } + }); + }); + return button; + }); + + updateListViewContents(); +} + +Module::~Module() +{ + delete ui; +} + +void Module::importScript() +{ + ui->messageWidget->animatedHide(); + + QString path = QFileDialog::getOpenFileName(nullptr, i18n("Import KWin Script"), QDir::homePath(), + i18n("*.kwinscript|KWin scripts (*.kwinscript)")); + + if (path.isNull()) { + return; + } + + using namespace KPackage; + PackageStructure *structure = PackageLoader::self()->loadPackageStructure(QStringLiteral("KWin/Script")); + Package package(structure); + + KJob *installJob = package.update(path); + installJob->setProperty("packagePath", path); // so we can retrieve it later for showing the script's name + connect(installJob, &KJob::result, this, &Module::importScriptInstallFinished); +} + +void Module::importScriptInstallFinished(KJob *job) +{ + // if the applet is already installed, just add it to the containment + if (job->error() != KJob::NoError) { + ui->messageWidget->setText(i18nc("Placeholder is error message returned from the install service", "Cannot import selected script.\n%1", job->errorString())); + ui->messageWidget->setMessageType(KMessageWidget::Error); + ui->messageWidget->animatedShow(); + return; + } + + using namespace KPackage; + + // so we can show the name of the package we just imported + PackageStructure *structure = PackageLoader::self()->loadPackageStructure(QStringLiteral("KWin/Script")); + Package package(structure); + package.setPath(job->property("packagePath").toString()); + Q_ASSERT(package.isValid()); + + ui->messageWidget->setText(i18nc("Placeholder is name of the script that was imported", "The script \"%1\" was successfully imported.", package.metadata().name())); + ui->messageWidget->setMessageType(KMessageWidget::Information); + ui->messageWidget->animatedShow(); + + updateListViewContents(); + + emit changed(true); +} + +void Module::updateListViewContents() +{ + auto filter = [](const KPluginMetaData &md) { + return md.isValid() && !md.rawData().value("X-KWin-Exclude-Listing").toBool(); + }; + + const QString scriptFolder = QStringLiteral("kwin/scripts/"); + const auto scripts = KPackage::PackageLoader::self()->findPackages(QStringLiteral("KWin/Script"), scriptFolder, filter); + + QList scriptinfos = KPluginInfo::fromMetaData(scripts.toVector()); + + ui->scriptSelector->addPlugins(scriptinfos, KPluginSelector::ReadConfigFile, QString(), QString(), m_kwinConfig); +} + +void Module::defaults() +{ + ui->scriptSelector->defaults(); +} + +void Module::load() +{ + updateListViewContents(); + ui->scriptSelector->load(); + + emit changed(false); +} + +void Module::save() +{ + ui->scriptSelector->save(); + m_kwinConfig->sync(); + QDBusMessage message = QDBusMessage::createMethodCall("org.kde.KWin", "/Scripting", "org.kde.kwin.Scripting", "start"); + QDBusConnection::sessionBus().asyncCall(message); + + emit changed(false); +} diff --git a/kcmkwin/kwinscripts/module.h b/kcmkwin/kwinscripts/module.h new file mode 100644 index 0000000..27e9968 --- /dev/null +++ b/kcmkwin/kwinscripts/module.h @@ -0,0 +1,61 @@ +/* + SPDX-FileCopyrightText: 2011 Tamas Krutki + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef MODULE_H +#define MODULE_H + +#include +#include + +namespace Ui +{ +class Module; +} + +class KJob; + +class Module : public KCModule +{ + Q_OBJECT +public: + /** + * Constructor. + * + * @param parent Parent widget of the module + * @param args Arguments for the module + */ + explicit Module(QWidget *parent, const QVariantList &args = QVariantList()); + + /** + * Destructor. + */ + ~Module() override; + void load() override; + void save() override; + void defaults() override; + +protected Q_SLOTS: + + /** + * Called when the import script button is clicked. + */ + void importScript(); + + void importScriptInstallFinished(KJob *job); + +private: + /** + * UI + */ + Ui::Module *ui; + /** + * Updates the contents of the list view. + */ + void updateListViewContents(); + KSharedConfigPtr m_kwinConfig; +}; + +#endif // MODULE_H diff --git a/kcmkwin/kwinscripts/module.ui b/kcmkwin/kwinscripts/module.ui new file mode 100644 index 0000000..6a4215b --- /dev/null +++ b/kcmkwin/kwinscripts/module.ui @@ -0,0 +1,96 @@ + + + Module + + + + 0 + 0 + 484 + 300 + + + + KWin script configuration + + + + + + + + + + 0 + 0 + + + + Qt::WheelFocus + + + + + + + + + Qt::Horizontal + + + + + + + + 80 + 40 + + + + + + + + Install from File... + + + + + + + + + + + + Get New Scripts... + + + + + + + + + + KPluginSelector + QWidget +
kpluginselector.h
+ 1 +
+ + KNS3::Button + QPushButton +
KNS3/Button
+
+ + KMessageWidget + QFrame +
kmessagewidget.h
+ 1 +
+
+ + +
diff --git a/kcmkwin/kwinscripts/version.h.cmake b/kcmkwin/kwinscripts/version.h.cmake new file mode 100644 index 0000000..935fc96 --- /dev/null +++ b/kcmkwin/kwinscripts/version.h.cmake @@ -0,0 +1,7 @@ +#ifndef VERSION_H +#define VERSION_H + +static const char global_s_versionString[] = "${VERSION_STRING}"; +static const char global_s_versionStringFull[] = "${VERSION_STRING_FULL}"; + +#endif // VERSION_H diff --git a/kcmkwin/kwintabbox/CMakeLists.txt b/kcmkwin/kwintabbox/CMakeLists.txt new file mode 100644 index 0000000..6a7fc35 --- /dev/null +++ b/kcmkwin/kwintabbox/CMakeLists.txt @@ -0,0 +1,43 @@ +# KI18N Translation Domain for this library +add_definitions(-DTRANSLATION_DOMAIN=\"kcm_kwintabbox\") + +include_directories(${KWin_SOURCE_DIR}/effects ${KWin_SOURCE_DIR}/tabbox ${KWin_SOURCE_DIR}) + +########### next target ############### + +set(kcm_kwintabbox_PART_SRCS + ${KWin_SOURCE_DIR}/tabbox/tabboxconfig.cpp + layoutpreview.cpp + main.cpp + thumbnailitem.cpp + kwintabboxconfigform.cpp +) + +ki18n_wrap_ui(kcm_kwintabbox_PART_SRCS main.ui) +qt5_add_dbus_interface(kcm_kwintabbox_PART_SRCS ${KWin_SOURCE_DIR}/org.kde.kwin.Effects.xml kwin_effects_interface) + +kconfig_add_kcfg_files(kcm_kwintabbox_PART_SRCS kwintabboxsettings.kcfgc kwinswitcheffectsettings.kcfgc kwinpluginssettings.kcfgc) +add_library(kcm_kwintabbox MODULE ${kcm_kwintabbox_PART_SRCS}) + +target_link_libraries(kcm_kwintabbox + Qt5::Quick + + KF5::Completion + KF5::GlobalAccel + KF5::I18n + KF5::KCMUtils + KF5::NewStuff + KF5::Package + KF5::Service + + XCB::XCB + + kwin4_effect_builtins +) + +install(TARGETS kcm_kwintabbox DESTINATION ${PLUGIN_INSTALL_DIR} ) + +########### install files ############### +install(FILES kwintabbox.desktop DESTINATION ${SERVICES_INSTALL_DIR}) +install(FILES thumbnails/konqueror.png thumbnails/kmail.png thumbnails/systemsettings.png thumbnails/dolphin.png DESTINATION ${DATA_INSTALL_DIR}/kwin/kcm_kwintabbox) +install(FILES kwinswitcher.knsrc DESTINATION ${KDE_INSTALL_KNSRCDIR}) diff --git a/kcmkwin/kwintabbox/Messages.sh b/kcmkwin/kwintabbox/Messages.sh new file mode 100644 index 0000000..b4dd7b3 --- /dev/null +++ b/kcmkwin/kwintabbox/Messages.sh @@ -0,0 +1,4 @@ +#! /usr/bin/env bash +$EXTRACTRC *.ui >> rc.cpp || exit 11 +$XGETTEXT *.cpp -o $podir/kcm_kwintabbox.pot +rm -f rc.cpp diff --git a/kcmkwin/kwintabbox/kwinpluginssettings.kcfg b/kcmkwin/kwintabbox/kwinpluginssettings.kcfg new file mode 100644 index 0000000..26415ef --- /dev/null +++ b/kcmkwin/kwintabbox/kwinpluginssettings.kcfg @@ -0,0 +1,18 @@ + + + + + + BuiltInEffects::enabledByDefault(BuiltInEffect::CoverSwitch) + + + BuiltInEffects::enabledByDefault(BuiltInEffect::FlipSwitch) + + + false + + + diff --git a/kcmkwin/kwintabbox/kwinpluginssettings.kcfgc b/kcmkwin/kwintabbox/kwinpluginssettings.kcfgc new file mode 100644 index 0000000..93564cd --- /dev/null +++ b/kcmkwin/kwintabbox/kwinpluginssettings.kcfgc @@ -0,0 +1,7 @@ +File=kwinpluginssettings.kcfg +NameSpace=KWin::TabBox +ClassName=PluginsSettings +IncludeFiles=effect_builtins.h +Mutators=true +DefaultValueGetters=true +ParentInConstructor=true diff --git a/kcmkwin/kwintabbox/kwinswitcheffectsettings.kcfg b/kcmkwin/kwintabbox/kwinswitcheffectsettings.kcfg new file mode 100644 index 0000000..ac7450b --- /dev/null +++ b/kcmkwin/kwintabbox/kwinswitcheffectsettings.kcfg @@ -0,0 +1,17 @@ + + + + + + + + false + + + false + + + diff --git a/kcmkwin/kwintabbox/kwinswitcheffectsettings.kcfgc b/kcmkwin/kwintabbox/kwinswitcheffectsettings.kcfgc new file mode 100644 index 0000000..3ee2962 --- /dev/null +++ b/kcmkwin/kwintabbox/kwinswitcheffectsettings.kcfgc @@ -0,0 +1,6 @@ +File=kwinswitcheffectsettings.kcfg +NameSpace=KWin::TabBox +ClassName=SwitchEffectSettings +Mutators=true +DefaultValueGetters=true +ParentInConstructor=true diff --git a/kcmkwin/kwintabbox/kwinswitcher.knsrc b/kcmkwin/kwintabbox/kwinswitcher.knsrc new file mode 100644 index 0000000..6edad0a --- /dev/null +++ b/kcmkwin/kwintabbox/kwinswitcher.knsrc @@ -0,0 +1,49 @@ +[KNewStuff3] +Name=Window Manager Switching Layouts +Name[az]=Pəncrərə meneceri üçün pəncərə dəyişdirici sxemi +Name[ca]=Disposicions del commutador del gestor de finestres +Name[ca@valencia]=Disposicions del commutador del gestor de finestres +Name[cs]=Přepínání rozvržení správce oken +Name[da]=Skifterlayouts til vindueshåndtering +Name[de]=Wechsel-Layout für Fensterverwaltung +Name[el]=Διατάξεις εναλλαγής διαχειριστή παραθύρων +Name[en_GB]=Window Manager Switching Layouts +Name[es]=Esquemas de cambio del gestor de ventanas +Name[et]=Aknahalduri lülitamise paigutused +Name[eu]=Leiho kudeatzailearen aldatzeko-antolamenduak +Name[fi]=Ikkunointiohjelman vaihdon asettelut +Name[fr]=Changement de disposition du gestionnaire de fenêtres +Name[gl]=Disposicións de cambio do xestor de xanelas +Name[he]=מחליף פריסות של מנהל החלונות +Name[hu]=Ablakkezelő-váltó elrendezések +Name[ia]=Disposition de commutator de gerente de fenestra +Name[id]=Tataletak Pengalihan Pengelola Window +Name[it]=Disposizione scambiafinestre del gestore delle finestre +Name[ko]=창 관리자 전환기 레이아웃 +Name[lt]=Langų tvarkytuvės perjungimo išdėstymai +Name[nl]=Omschakelende indelingen van vensterbeheerder +Name[nn]=Veksleoppsett for vindaugshandsamar +Name[pa]=ਵਿੰਡੋ ਮੈਨੇਜਰ ਸਵਿੱਚਰ ਲੇਆਉਟ +Name[pl]=Układ przełączania zarządzana oknami +Name[pt]=Disposições da Mudança de Janelas do KWin +Name[pt_BR]=Layouts de mudança do gerenciador de janelas +Name[ro]=Aranjamente de schimbare pentru gestionar de ferestre +Name[ru]=Оформления переключателя окон для KWin +Name[sk]=Prepínanie rozložení správcu okien +Name[sl]=Razporedi preklapljanja upravljalnika oken +Name[sr]=Распореди пребацивања менаџера прозора +Name[sr@ijekavian]=Распореди пребацивања менаџера прозора +Name[sr@ijekavianlatin]=Rasporedi prebacivanja menadžera prozora +Name[sr@latin]=Rasporedi prebacivanja menadžera prozora +Name[sv]=Fönsterbyteslayouter +Name[tr]=Pencere Yöneticisi Geçiş Düzenleri +Name[uk]=Компонування засобу перемикання вікон +Name[x-test]=xxWindow Manager Switching Layoutsxx +Name[zh_CN]=窗口切换器布局 +Name[zh_TW]=視窗切換器佈局 + +ProvidersUrl=https://download.kde.org/ocs/providers.xml +Categories=KWin Switching Layouts +StandardResource=tmp +Uncompress=kpackage +KPackageType=KWin/WindowSwitcher diff --git a/kcmkwin/kwintabbox/kwintabbox.desktop b/kcmkwin/kwintabbox/kwintabbox.desktop new file mode 100644 index 0000000..4864907 --- /dev/null +++ b/kcmkwin/kwintabbox/kwintabbox.desktop @@ -0,0 +1,163 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=KCModule +Icon=preferences-system-tabbox +Exec=kcmshell5 kwintabbox +X-DocPath=kcontrol/kwintabbox/index.html +X-KDE-Library=kcm_kwintabbox +X-KDE-ParentApp=kcontrol + +X-KDE-System-Settings-Parent-Category=windowmanagement +X-KDE-Weight=60 + +Name=Task Switcher +Name[ar]=مبدّل المهام +Name[az]=Tapşırıq dəyişdirici +Name[bg]=Превключване на задачи +Name[bs]=Prebacivanje zadataka +Name[ca]=Commutador de tasques +Name[ca@valencia]=Commutador de tasques +Name[cs]=Přepínač úloh +Name[da]=Opgaveskifter +Name[de]=Anwendungsumschalter +Name[el]=Εναλλαγή εργασιών +Name[en_GB]=Task Switcher +Name[es]=Cambiador de tareas +Name[et]=Ülesannete vahetaja +Name[eu]=Ataza-aldarazlea +Name[fi]=Ikkunanvalitsin +Name[fr]=Changeur de tâches +Name[ga]=Malartóir Tascanna +Name[gl]=Selector de tarefa +Name[he]=מחליף חלונות +Name[hi]=कार्य बदल +Name[hr]=Prebacivač zadataka +Name[hu]=Feladatváltó +Name[ia]=Commutator de carga +Name[id]=Pengalih Tugas +Name[is]=Verkefnaskiptir +Name[it]=Scambiafinestre +Name[ja]=タスクスイッチャー +Name[kk]=Тапсырма ауыстырғышы +Name[km]=កម្មវិធី​ប្ដូរ​ភារកិច្ច​ +Name[kn]=ಕಾರ್ಯ ಬದಲಾವಣೆಗಾರ +Name[ko]=작업 전환기 +Name[lt]=Užduočių perjungiklis +Name[lv]=Uzdevumu pārslēdzējs +Name[mr]=कार्य बदलणारा +Name[nb]=Oppgavebytter +Name[nds]=Twischen Opgaven wesseln +Name[nl]=Taakschakelaar +Name[nn]=Oppgåvevekslar +Name[pa]=ਟਾਸਕ ਸਵਿੱਚਰ +Name[pl]=Przełączanie zadań +Name[pt]=Mudança de Tarefas +Name[pt_BR]=Mudança de tarefas +Name[ro]=Comutator de sarcini +Name[ru]=Переключение окон +Name[si]=කාර්ය මාරුකරනය +Name[sk]=Prepínač úloh +Name[sl]=Preklop med opravili +Name[sr]=Пребацивање задатака +Name[sr@ijekavian]=Пребацивање задатака +Name[sr@ijekavianlatin]=Prebacivanje zadataka +Name[sr@latin]=Prebacivanje zadataka +Name[sv]=Aktivitetsbyte +Name[th]=ตัวสลับงาน +Name[tr]=Görev Seçici +Name[ug]=ۋەزىپە ئالماشتۇرغۇچ +Name[uk]=Перемикання задач +Name[wa]=Passaedje d' ene bouye a l' ôte +Name[x-test]=xxTask Switcherxx +Name[zh_CN]=任务切换器 +Name[zh_TW]=工作切換器 +Comment=Navigation Through Windows +Comment[az]=Pəncərələrdə səyahət +Comment[bs]=Kretanje kroz prozore +Comment[ca]=Navegació per les finestres +Comment[ca@valencia]=Navegació per les finestres +Comment[cs]=Navigace skrz okna +Comment[da]=Navigation igennem vinduer +Comment[de]=Zwischen Fenstern wechseln +Comment[el]=Περιήγηση στα παράθυρα +Comment[en_GB]=Navigation Through Windows +Comment[es]=Navegación a través de las ventanas +Comment[et]=Akende vahel liikumine +Comment[eu]=Leihoen artean nabigatu +Comment[fi]=Ikkunoiden välillä siirtyminen +Comment[fr]=Navigation dans les fenêtres +Comment[gl]=Navegación polas xanelas +Comment[he]=ניווט בין חלונות +Comment[hu]=Navigálás az ablakok közt +Comment[ia]=Navigation per fenestras +Comment[id]=Navigasi Melalui Window +Comment[it]=Navigazione tra le finestre +Comment[ko]=창간 탐색 +Comment[lt]=Naršymas per langus +Comment[nb]=Navigasjon gjennom vinduer +Comment[nds]=Finstern dörgahn +Comment[nl]=Navigatie door vensters +Comment[nn]=Bla gjennom vindauge +Comment[pa]=ਵਿੰਡੋਆਂ 'ਚ ਏਧਰ ਓਧਰ ਜਾਓ +Comment[pl]=Przełączanie pomiędzy oknami +Comment[pt]=Navegação pelas Janelas +Comment[pt_BR]=Navegação pelas janelas +Comment[ro]=Navigare printre ferestre +Comment[ru]=Настройка переключателя окон +Comment[sk]=Navigácia cez okná +Comment[sl]=Krmarjenje med okni +Comment[sr]=Кретање између прозора +Comment[sr@ijekavian]=Кретање између прозора +Comment[sr@ijekavianlatin]=Kretanje između prozora +Comment[sr@latin]=Kretanje između prozora +Comment[sv]=Navigering via fönster +Comment[tr]=Pencereler Arası Gezinti +Comment[uk]=Навігація вікнами +Comment[vi]=Điều hướng qua các cửa sổ +Comment[x-test]=xxNavigation Through Windowsxx +Comment[zh_CN]=遍历窗口 +Comment[zh_TW]=透過視窗導覽 +X-KDE-Keywords=window,windows,switcher,window switcher,switching,window switching,alttab,alt-tab,alt+tab,alt tab +X-KDE-Keywords[az]=pəncərə,pəncərələr,dəyişdirici,pəncərə dəyişdiricisi,pəncərə dəyişməsi, +X-KDE-Keywords[bs]=prozor,prozori,razmijenjivati,razimjenjivati prozor,gašenje prozora,alttab,alt-tab,alt+tab,alt tab +X-KDE-Keywords[ca]=finestra,finestres,commutador,commutador de finestres,commutació,commutació de finestres,alttab,alt-tab,alt+tab,alt tab +X-KDE-Keywords[ca@valencia]=finestra,finestres,commutador,commutador de finestres,commutació,commutació de finestres,alttab,alt-tab,alt+tab,alt tab +X-KDE-Keywords[da]=vindue,vinduer,skifter,vinduesskifter,skift,alttab,alt-tab,alt+tab,alt tab +X-KDE-Keywords[de]=fenster,umschalter,fensterumschalter,alttab,alt-tab,alt+tab,alt tab +X-KDE-Keywords[el]=παράθυρο,παράθυρα,εναλλάκτης,εναλλάκτης παραθύρων,εναλλαγή,εναλλαγή παραθύρων,alttab,alt-tab,alt+tab,alt tab +X-KDE-Keywords[en_GB]=window,windows,switcher,window switcher,switching,window switching,alttab,alt-tab,alt+tab,alt tab +X-KDE-Keywords[es]=ventana,ventanas,conmutador,conmutador de ventanas,conmutación,conmutación de ventanas,alttab,alt-tab,alt+tab,alt tab +X-KDE-Keywords[et]=aken,aknad,lülitaja,akende vahetaja,vahetamine,lülitamine,akende lülitamine,alttab,alt-tab,alt+tab,alt tab +X-KDE-Keywords[eu]=leiho,leihoak,kommutadore,aldatzaile,leiho-kommutadore,leiho-aldatzaile,aldatzea,leihoz aldatzea,alttab,alt-tab,alt+tab,alt tab +X-KDE-Keywords[fi]=ikkuna,ikkunat,vaihtaja,vaihto,valitsin,ikkunan vaihtaja,tehtävänvalitsin,ikkunanvalitsin,vaihtaminen,ikkunan vaihtaminen,ikkunan vaihto,alttab,alt-tab,alt+tab,alt tab,altsarkain,alt-sarkain,alt+sarkain,alt sarkain +X-KDE-Keywords[fr]=fenêtre, fenêtres, basculeur, changeur de fenêtre, basculer, changement de fenêtre, alttab, alt-tab, alt+tab, alt tab +X-KDE-Keywords[gl]=xanela,xanelas,alternador,cambiar,trocar de xanela,alttab,alt-tab,alt+tab,alt tab +X-KDE-Keywords[hu]=ablak,ablakok,váltó,ablakváltó,váltás,ablakváltás,alttab,alt-tab,alt+tab,alt tab +X-KDE-Keywords[ia]=fenestra,fenestras,commutator,commutator de fenestra,commutar,commutar fenestra,alttab,alt-tab,alt+tab,alt tab +X-KDE-Keywords[id]=window,window,pengalih,pengalih window,pengalihan,pengalihan window,alttab,alt-tab,alt+tab,alt tab +X-KDE-Keywords[it]=finestra,finestre,scambiatore,scambiafinestre,scambio,scambio finestre,alttab,alt-tab,alt+tab,alt tab +X-KDE-Keywords[kk]=window,windows,switcher,window switcher,switching,window switching,alttab,alt-tab,alt+tab,alt tab +X-KDE-Keywords[km]=window,windows,switcher,window switcher,switching,window switching,alttab,alt-tab,alt+tab,alt tab +X-KDE-Keywords[ko]=window,windows,switcher,window switcher,switching,window switching,alttab,alt-tab,alt+tab,alt tab,창,창 전환,창 전환기,전환 +X-KDE-Keywords[lt]=lango,langų,langas,langai,perjungimas,perjungiklis,perjungti,langų perjungiklis,langų perjungimas,langu,alttab,alt-tab,alt+tab,alt tab +X-KDE-Keywords[nb]=vindu,vinduer,bytter,vindusbytter,bytte,vindusbytte,alttab,alt-tab,alt+tab,alt tab +X-KDE-Keywords[nds]=Finster,Finstern,wesseln,alttab,alt-tab,alt+tab,alt tab +X-KDE-Keywords[nl]=venster,vensters,schakelaar,vensterwisselaar,wisseling,vensterwisseling,alttab,alt-tab,alt+tab,alt tab +X-KDE-Keywords[nn]=vindauge,vindauge,byte,vindaugsbytar,byte,vindaugsbytar,alttab,alt-tab,alt+tab,alt tab +X-KDE-Keywords[pl]=okno,okna,przełączanie,przełączanie okien,alttab,alt-tab,alt+tab,alt tab +X-KDE-Keywords[pt]=janela,janelas,selector,selector de janelas,mudar,mudança de janela,alttab,alt-tab,alt+tab,alt tab +X-KDE-Keywords[pt_BR]=janela,janelas,seletor,seletor de janelas,mudar,mudança de janela,alttab,alt-tab,alt+tab,alt tab +X-KDE-Keywords[ro]=fereastră,ferestre,comutator,comutator ferestre,schimbare,schimbare ferestre,alttab,alt-tab,alt+tab,alt tab +X-KDE-Keywords[ru]=window,windows,switcher,window switcher,switching,window switching,alttab,alt-tab,alt+tab,alt tab,окно,окна,переключатель,переключатель окон,переключение,переключение окон +X-KDE-Keywords[sk]=okno,okná,prepínač,prepínanie okien,prepínanie,prepínač okien,alttab,alt-tab,alt+tab,alt tab +X-KDE-Keywords[sl]=okno,okna,preklapljanje,preklapljanje med okni,alttab,alt-tab,alt+tab,alt tab +X-KDE-Keywords[sr]=window,windows,switcher,window switcher,switching,window switching,alttab,alt-tab,alt+tab,alt tab,прозор,мењач,мењач прозора,мењање,пребацивање,AltTab,Alt-Tab,Alt+Tab,Alt Tab +X-KDE-Keywords[sr@ijekavian]=window,windows,switcher,window switcher,switching,window switching,alttab,alt-tab,alt+tab,alt tab,прозор,мењач,мењач прозора,мењање,пребацивање,AltTab,Alt-Tab,Alt+Tab,Alt Tab +X-KDE-Keywords[sr@ijekavianlatin]=window,windows,switcher,window switcher,switching,window switching,alttab,alt-tab,alt+tab,alt tab,prozor,menjač,menjač prozora,menjanje,prebacivanje,AltTab,Alt-Tab,Alt+Tab,Alt Tab +X-KDE-Keywords[sr@latin]=window,windows,switcher,window switcher,switching,window switching,alttab,alt-tab,alt+tab,alt tab,prozor,menjač,menjač prozora,menjanje,prebacivanje,AltTab,Alt-Tab,Alt+Tab,Alt Tab +X-KDE-Keywords[sv]=fönster,byte,fönsterbytare,byta,fönsterbyte,alttabulator,alt-tabulator,alt+tabulator,alt tabulator +X-KDE-Keywords[tr]=pencere,pencereler,değiştirici,pencere seçici,değiştirme,pencere seçimi,alttab,alt-tab,alt+tab,alt tab +X-KDE-Keywords[uk]=window,windows,switcher,window switcher,switching,window switching,alttab,alt-tab,alt+tab,alt tab,вікно,вікна,перемикач,перемикач вікон,перемикання,перемикання вікон,альттаб,альт-таб,альт+таб +X-KDE-Keywords[x-test]=xxwindowxx,xxwindowsxx,xxswitcherxx,xxwindow switcherxx,xxswitchingxx,xxwindow switchingxx,xxalttabxx,xxalt-tabxx,xxalt+tabxx,xxalt tabxx +X-KDE-Keywords[zh_CN]=window,windows,switcher,window switcher,switching,window switching,alttab,alt-tab,alt+tab,alt tab,窗口,窗口切换器,切换器, +X-KDE-Keywords[zh_TW]=window,windows,switcher,window switcher,switching,window switching,alttab,alt-tab,alt+tab,alt tab diff --git a/kcmkwin/kwintabbox/kwintabboxconfigform.cpp b/kcmkwin/kwintabbox/kwintabboxconfigform.cpp new file mode 100644 index 0000000..3840fde --- /dev/null +++ b/kcmkwin/kwintabbox/kwintabboxconfigform.cpp @@ -0,0 +1,396 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Martin Gräßlin + SPDX-FileCopyrightText: 2020 Cyril Rossi + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwintabboxconfigform.h" +#include "ui_main.h" + +#include + +#include +#include +#include + + +namespace KWin +{ + +using namespace TabBox; + +KWinTabBoxConfigForm::KWinTabBoxConfigForm(TabboxType type, QWidget *parent) + : QWidget(parent) + , m_type(type) + , ui(new Ui::KWinTabBoxConfigForm) +{ + ui->setupUi(this); + + ui->effectConfigButton->setIcon(QIcon::fromTheme(QStringLiteral("view-preview"))); + + if (QApplication::screens().count() < 2) { + ui->filterScreens->hide(); + ui->screenFilter->hide(); + } + + connect(ui->effectConfigButton, &QPushButton::clicked, this, &KWinTabBoxConfigForm::effectConfigButtonClicked); + + connect(ui->kcfg_ShowTabBox, SIGNAL(clicked(bool)), SLOT(tabBoxToggled(bool))); + + connect(ui->filterScreens, SIGNAL(clicked(bool)), SLOT(onFilterScreen())); + connect(ui->currentScreen, SIGNAL(clicked(bool)), SLOT(onFilterScreen())); + connect(ui->otherScreens, SIGNAL(clicked(bool)), SLOT(onFilterScreen())); + + connect(ui->filterDesktops, SIGNAL(clicked(bool)), SLOT(onFilterDesktop())); + connect(ui->currentDesktop, SIGNAL(clicked(bool)), SLOT(onFilterDesktop())); + connect(ui->otherDesktops, SIGNAL(clicked(bool)), SLOT(onFilterDesktop())); + + connect(ui->filterActivities, SIGNAL(clicked(bool)), SLOT(onFilterActivites())); + connect(ui->currentActivity, SIGNAL(clicked(bool)), SLOT(onFilterActivites())); + connect(ui->otherActivities, SIGNAL(clicked(bool)), SLOT(onFilterActivites())); + + connect(ui->filterMinimization, SIGNAL(clicked(bool)), SLOT(onFilterMinimization())); + connect(ui->visibleWindows, SIGNAL(clicked(bool)), SLOT(onFilterMinimization())); + connect(ui->hiddenWindows, SIGNAL(clicked(bool)), SLOT(onFilterMinimization())); + + connect(ui->oneAppWindow, SIGNAL(clicked(bool)), SLOT(onApplicationMode())); + connect(ui->showDesktop, SIGNAL(clicked(bool)), SLOT(onShowDesktopMode())); + + connect(ui->switchingModeCombo, SIGNAL(currentIndexChanged(int)), SLOT(onSwitchingMode())); + connect(ui->effectCombo, SIGNAL(currentIndexChanged(int)), SLOT(onEffectCombo())); + + auto addShortcut = [this](const char *name, KKeySequenceWidget *widget, const QKeySequence &sequence = QKeySequence()) { + QAction *a = m_actionCollection->addAction(name); + a->setProperty("isConfigurationAction", true); + widget->setProperty("shortcutAction", name); + a->setText(i18n(name)); + KGlobalAccel::self()->setShortcut(a, QList() << sequence); + connect(widget, SIGNAL(keySequenceChanged(QKeySequence)), this, SLOT(shortcutChanged(QKeySequence))); + }; + + // Shortcut config. The shortcut belongs to the component "kwin"! + m_actionCollection = new KActionCollection(this, QStringLiteral("kwin")); + m_actionCollection->setComponentDisplayName(i18n("KWin")); + m_actionCollection->setConfigGroup("Navigation"); + m_actionCollection->setConfigGlobal(true); + + if (TabboxType::Main == m_type) { + addShortcut("Walk Through Windows", ui->scAll, Qt::ALT + Qt::Key_Tab); + addShortcut("Walk Through Windows (Reverse)", ui->scAllReverse, Qt::ALT + Qt::SHIFT + Qt::Key_Backtab); + addShortcut("Walk Through Windows of Current Application", ui->scCurrent, Qt::ALT + Qt::Key_QuoteLeft); + addShortcut("Walk Through Windows of Current Application (Reverse)", ui->scCurrentReverse, Qt::ALT + Qt::Key_AsciiTilde); + } else if (TabboxType::Alternative == m_type) { + addShortcut("Walk Through Windows Alternative", ui->scAll); + addShortcut("Walk Through Windows Alternative (Reverse)", ui->scAllReverse); + addShortcut("Walk Through Windows of Current Application Alternative", ui->scCurrent); + addShortcut("Walk Through Windows of Current Application Alternative (Reverse)", ui->scCurrentReverse); + } +} + +KWinTabBoxConfigForm::~KWinTabBoxConfigForm() +{ + delete ui; +} + +bool KWinTabBoxConfigForm::highlightWindows() const +{ + return ui->kcfg_HighlightWindows->isChecked(); +} + +bool KWinTabBoxConfigForm::showTabBox() const +{ + return ui->kcfg_ShowTabBox->isChecked(); +} + +int KWinTabBoxConfigForm::filterScreen() const +{ + if (ui->filterScreens->isChecked()) { + return ui->currentScreen->isChecked() ? TabBoxConfig::OnlyCurrentScreenClients : TabBoxConfig::ExcludeCurrentScreenClients; + } else { + return TabBoxConfig::IgnoreMultiScreen; + } +} + +int KWinTabBoxConfigForm::filterDesktop() const +{ + if (ui->filterDesktops->isChecked()) { + return ui->currentDesktop->isChecked() ? TabBoxConfig::OnlyCurrentDesktopClients : TabBoxConfig::ExcludeCurrentDesktopClients; + } else { + return TabBoxConfig::AllDesktopsClients; + } +} + +int KWinTabBoxConfigForm::filterActivities() const +{ + if (ui->filterActivities->isChecked()) { + return ui->currentActivity->isChecked() ? TabBoxConfig::OnlyCurrentActivityClients : TabBoxConfig::ExcludeCurrentActivityClients; + } else { + return TabBoxConfig::AllActivitiesClients; + } +} + +int KWinTabBoxConfigForm::filterMinimization() const +{ + if (ui->filterMinimization->isChecked()) { + return ui->visibleWindows->isChecked() ? TabBoxConfig::ExcludeMinimizedClients : TabBoxConfig::OnlyMinimizedClients; + } else { + return TabBoxConfig::IgnoreMinimizedStatus; + } +} + +int KWinTabBoxConfigForm::applicationMode() const +{ + return ui->oneAppWindow->isChecked() ? TabBoxConfig::OneWindowPerApplication : TabBoxConfig::AllWindowsAllApplications; +} + +int KWinTabBoxConfigForm::showDesktopMode() const +{ + return ui->showDesktop->isChecked() ? TabBoxConfig::ShowDesktopClient : TabBoxConfig::DoNotShowDesktopClient; +} + +int KWinTabBoxConfigForm::switchingMode() const +{ + return ui->switchingModeCombo->currentIndex(); +} + +QString KWinTabBoxConfigForm::layoutName() const +{ + return ui->effectCombo->currentData().toString(); +} + +void KWinTabBoxConfigForm::setFilterScreen(TabBox::TabBoxConfig::ClientMultiScreenMode mode) +{ + ui->filterScreens->setChecked(mode != TabBoxConfig::IgnoreMultiScreen); + ui->currentScreen->setChecked(mode == TabBoxConfig::OnlyCurrentScreenClients); + ui->otherScreens->setChecked(mode == TabBoxConfig::ExcludeCurrentScreenClients); +} + +void KWinTabBoxConfigForm::setFilterDesktop(TabBox::TabBoxConfig::ClientDesktopMode mode) +{ + ui->filterDesktops->setChecked(mode != TabBoxConfig::AllDesktopsClients); + ui->currentDesktop->setChecked(mode == TabBoxConfig::OnlyCurrentDesktopClients); + ui->otherDesktops->setChecked(mode == TabBoxConfig::ExcludeCurrentDesktopClients); +} + +void KWinTabBoxConfigForm::setFilterActivities(TabBox::TabBoxConfig::ClientActivitiesMode mode) +{ + ui->filterActivities->setChecked(mode != TabBoxConfig::AllActivitiesClients); + ui->currentActivity->setChecked(mode == TabBoxConfig::OnlyCurrentActivityClients); + ui->otherActivities->setChecked(mode == TabBoxConfig::ExcludeCurrentActivityClients); +} + +void KWinTabBoxConfigForm::setFilterMinimization(TabBox::TabBoxConfig::ClientMinimizedMode mode) +{ + ui->filterMinimization->setChecked(mode != TabBoxConfig::IgnoreMinimizedStatus); + ui->visibleWindows->setChecked(mode == TabBoxConfig::ExcludeMinimizedClients); + ui->hiddenWindows->setChecked(mode == TabBoxConfig::OnlyMinimizedClients); +} + +void KWinTabBoxConfigForm::setApplicationMode(TabBox::TabBoxConfig::ClientApplicationsMode mode) +{ + ui->oneAppWindow->setChecked(mode == TabBoxConfig::OneWindowPerApplication); +} + +void KWinTabBoxConfigForm::setShowDesktopMode(TabBox::TabBoxConfig::ShowDesktopMode mode) +{ + ui->showDesktop->setChecked(mode == TabBoxConfig::ShowDesktopClient); +} + +void KWinTabBoxConfigForm::setSwitchingModeChanged(TabBox::TabBoxConfig::ClientSwitchingMode mode) +{ + ui->switchingModeCombo->setCurrentIndex(mode); +} + +void KWinTabBoxConfigForm::setLayoutName(const QString &layoutName) +{ + ui->effectCombo->setCurrentIndex(ui->effectCombo->findData(layoutName)); +} + +void KWinTabBoxConfigForm::setEffectComboModel(QStandardItemModel *model) +{ + int index = ui->effectCombo->currentIndex(); + QVariant data = ui->effectCombo->itemData(index); + + ui->effectCombo->setModel(model); + + if (data.isValid()) { + ui->effectCombo->setCurrentIndex(ui->effectCombo->findData(data)); + } else if (index != -1) { + ui->effectCombo->setCurrentIndex(index); + } +} + +QVariant KWinTabBoxConfigForm::effectComboCurrentData(int role) const +{ + return ui->effectCombo->currentData(role); +} + +void KWinTabBoxConfigForm::loadShortcuts() +{ + auto loadShortcut = [this](KKeySequenceWidget *widget) { + QString actionName = widget->property("shortcutAction").toString(); + qDebug() << "load shortcut for " << actionName; + if (QAction *action = m_actionCollection->action(actionName)) { + auto shortcuts = KGlobalAccel::self()->shortcut(action); + if (!shortcuts.isEmpty()) { + widget->setKeySequence(shortcuts.first()); + } + } + }; + + loadShortcut(ui->scAll); + loadShortcut(ui->scAllReverse); + loadShortcut(ui->scCurrent); + loadShortcut(ui->scCurrentReverse); +} + +void KWinTabBoxConfigForm::resetShortcuts() +{ + QString action; + auto resetShortcut = [this](KKeySequenceWidget *widget, const QKeySequence &sequence = QKeySequence()) { + const QString action = widget->property("shortcutAction").toString(); + QAction *a = m_actionCollection->action(action); + KGlobalAccel::self()->setShortcut(a, QList() << sequence, KGlobalAccel::NoAutoloading); + }; + if (TabboxType::Main == m_type) { + resetShortcut(ui->scAll, Qt::ALT + Qt::Key_Tab); + resetShortcut(ui->scAllReverse, Qt::ALT + Qt::SHIFT + Qt::Key_Backtab); + resetShortcut(ui->scCurrent, Qt::ALT + Qt::Key_QuoteLeft); + resetShortcut(ui->scCurrentReverse, Qt::ALT + Qt::Key_AsciiTilde); + } else if (TabboxType::Alternative == m_type) { + resetShortcut(ui->scAll); + resetShortcut(ui->scAllReverse); + resetShortcut(ui->scCurrent); + resetShortcut(ui->scCurrentReverse); + } + m_actionCollection->writeSettings(); +} + +void KWinTabBoxConfigForm::setHighlightWindowsEnabled(bool enabled) +{ + m_isHighlightWindowsEnabled = enabled; + ui->kcfg_HighlightWindows->setEnabled(m_isHighlightWindowsEnabled); +} + +void KWinTabBoxConfigForm::setFilterScreenEnabled(bool enabled) +{ + ui->filterScreens->setEnabled(enabled); + ui->currentScreen->setEnabled(enabled); + ui->otherScreens->setEnabled(enabled); +} + +void KWinTabBoxConfigForm::setFilterDesktopEnabled(bool enabled) +{ + ui->filterDesktops->setEnabled(enabled); + ui->currentDesktop->setEnabled(enabled); + ui->otherDesktops->setEnabled(enabled); +} + +void KWinTabBoxConfigForm::setFilterActivitiesEnabled(bool enabled) +{ + ui->filterActivities->setEnabled(enabled); + ui->currentActivity->setEnabled(enabled); + ui->otherActivities->setEnabled(enabled); +} + +void KWinTabBoxConfigForm::setFilterMinimizationEnabled(bool enabled) +{ + ui->filterMinimization->setEnabled(enabled); + ui->visibleWindows->setEnabled(enabled); + ui->hiddenWindows->setEnabled(enabled); +} + +void KWinTabBoxConfigForm::setApplicationModeEnabled(bool enabled) +{ + ui->oneAppWindow->setEnabled(enabled); +} + +void KWinTabBoxConfigForm::setShowDesktopModeEnabled(bool enabled) +{ + ui->showDesktop->setEnabled(enabled); +} + +void KWinTabBoxConfigForm::setSwitchingModeEnabled(bool enabled) +{ + ui->switchingModeCombo->setEnabled(enabled); +} + +void KWinTabBoxConfigForm::setLayoutNameEnabled(bool enabled) +{ + ui->effectCombo->setEnabled(enabled); +} + +void KWinTabBoxConfigForm::tabBoxToggled(bool on) +{ + // Highlight Windows options is availabled if no TabBox effect is selected + // or if Tabbox is not builtin effet. + on = !on || ui->effectCombo->currentData(AddonEffect).toBool(); + ui->kcfg_HighlightWindows->setEnabled(on && m_isHighlightWindowsEnabled); +} + +void KWinTabBoxConfigForm::onFilterScreen() +{ + emit filterScreenChanged(filterScreen()); +} + +void KWinTabBoxConfigForm::onFilterDesktop() +{ + emit filterDesktopChanged(filterDesktop()); +} + +void KWinTabBoxConfigForm::onFilterActivites() +{ + emit filterActivitiesChanged(filterActivities()); +} + +void KWinTabBoxConfigForm::onFilterMinimization() +{ + emit filterMinimizationChanged(filterMinimization()); +} + +void KWin::KWinTabBoxConfigForm::onApplicationMode() +{ + emit applicationModeChanged(applicationMode()); +} + +void KWinTabBoxConfigForm::onShowDesktopMode() +{ + emit showDesktopModeChanged(showDesktopMode()); +} + +void KWinTabBoxConfigForm::onSwitchingMode() +{ + emit switchingModeChanged(switchingMode()); +} + +void KWinTabBoxConfigForm::onEffectCombo() +{ + const bool isAddonEffect = ui->effectCombo->currentData(AddonEffect).toBool(); + ui->effectConfigButton->setIcon(QIcon::fromTheme(isAddonEffect ? "view-preview" : "configure")); + if (!ui->kcfg_ShowTabBox->isChecked()) { + return; + } + ui->kcfg_HighlightWindows->setEnabled(isAddonEffect && m_isHighlightWindowsEnabled); + + emit layoutNameChanged(layoutName()); +} + +void KWinTabBoxConfigForm::shortcutChanged(const QKeySequence &seq) +{ + QString action; + if (sender()) { + action = sender()->property("shortcutAction").toString(); + } + if (action.isEmpty()) { + return; + } + QAction *a = m_actionCollection->action(action); + KGlobalAccel::self()->setShortcut(a, QList() << seq, KGlobalAccel::NoAutoloading); + m_actionCollection->writeSettings(); +} + +} // namespace diff --git a/kcmkwin/kwintabbox/kwintabboxconfigform.h b/kcmkwin/kwintabbox/kwintabboxconfigform.h new file mode 100644 index 0000000..47b6ef0 --- /dev/null +++ b/kcmkwin/kwintabbox/kwintabboxconfigform.h @@ -0,0 +1,122 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Martin Gräßlin + SPDX-FileCopyrightText: 2020 Cyril Rossi + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef __KWINTABBOXCONFIGFORM_H__ +#define __KWINTABBOXCONFIGFORM_H__ + +#include +#include + +#include "tabboxconfig.h" + +class KShortcutsEditor; +class KActionCollection; + +namespace Ui +{ +class KWinTabBoxConfigForm; +} + +namespace KWin +{ + +class KWinTabBoxConfigForm : public QWidget +{ + Q_OBJECT + +public: + enum class TabboxType + { + Main, + Alternative, + }; + + + enum EffectComboRole + { + LayoutPath = Qt::UserRole + 1, + AddonEffect, // i.e not builtin effects + }; + + explicit KWinTabBoxConfigForm(TabboxType type, QWidget *parent = nullptr); + ~KWinTabBoxConfigForm() override; + + bool highlightWindows() const; + bool showTabBox() const; + int filterScreen() const; + int filterDesktop() const; + int filterActivities() const; + int filterMinimization() const; + int applicationMode() const; + int showDesktopMode() const; + int switchingMode() const; + QString layoutName() const; + + void setFilterScreen(TabBox::TabBoxConfig::ClientMultiScreenMode mode); + void setFilterDesktop(TabBox::TabBoxConfig::ClientDesktopMode mode); + void setFilterActivities(TabBox::TabBoxConfig::ClientActivitiesMode mode); + void setFilterMinimization(TabBox::TabBoxConfig::ClientMinimizedMode mode); + void setApplicationMode(TabBox::TabBoxConfig::ClientApplicationsMode mode); + void setShowDesktopMode(TabBox::TabBoxConfig::ShowDesktopMode mode); + void setSwitchingModeChanged(TabBox::TabBoxConfig::ClientSwitchingMode mode); + void setLayoutName(const QString &layoutName); + + // EffectCombo Data Model + void setEffectComboModel(QStandardItemModel *model); + QVariant effectComboCurrentData(int role = Qt::UserRole) const; + + void loadShortcuts(); + void resetShortcuts(); + + void setHighlightWindowsEnabled(bool enabled); + void setFilterScreenEnabled(bool enabled); + void setFilterDesktopEnabled(bool enabled); + void setFilterActivitiesEnabled(bool enabled); + void setFilterMinimizationEnabled(bool enabled); + void setApplicationModeEnabled(bool enabled); + void setShowDesktopModeEnabled(bool enabled); + void setSwitchingModeEnabled(bool enabled); + void setLayoutNameEnabled(bool enabled); + +Q_SIGNALS: + void filterScreenChanged(int value); + void filterDesktopChanged(int value); + void filterActivitiesChanged(int value); + void filterMinimizationChanged(int value); + void applicationModeChanged(int value); + void showDesktopModeChanged(int value); + void switchingModeChanged(int value); + void layoutNameChanged(const QString &layoutName); + void effectConfigButtonClicked(); + +private Q_SLOTS: + void tabBoxToggled(bool on); + void onFilterScreen(); + void onFilterDesktop(); + void onFilterActivites(); + void onFilterMinimization(); + void onApplicationMode(); + void onShowDesktopMode(); + void onSwitchingMode(); + void onEffectCombo(); + void shortcutChanged(const QKeySequence &seq); + +private: + KActionCollection *m_actionCollection = nullptr; + KShortcutsEditor *m_editor = nullptr; + + bool m_isHighlightWindowsEnabled = true; + TabboxType m_type; + Ui::KWinTabBoxConfigForm *ui; +}; + +} // namespace + +#endif diff --git a/kcmkwin/kwintabbox/kwintabboxsettings.kcfg b/kcmkwin/kwintabbox/kwintabboxsettings.kcfg new file mode 100644 index 0000000..01cf73b --- /dev/null +++ b/kcmkwin/kwintabbox/kwintabboxsettings.kcfg @@ -0,0 +1,41 @@ + + + + + + + + TabBoxConfig::defaultDesktopMode() + + + TabBoxConfig::defaultActivitiesMode() + + + TabBoxConfig::defaultApplicationsMode() + + + TabBoxConfig::defaultMinimizedMode() + + + TabBoxConfig::defaultShowDesktopMode() + + + TabBoxConfig::defaultMultiScreenMode() + + + TabBoxConfig::defaultSwitchingMode() + + + TabBoxConfig::defaultLayoutName() + + + TabBoxConfig::defaultShowTabBox() + + + TabBoxConfig::defaultHighlightWindow() + + + diff --git a/kcmkwin/kwintabbox/kwintabboxsettings.kcfgc b/kcmkwin/kwintabbox/kwintabboxsettings.kcfgc new file mode 100644 index 0000000..2f570c9 --- /dev/null +++ b/kcmkwin/kwintabbox/kwintabboxsettings.kcfgc @@ -0,0 +1,7 @@ +File=kwintabboxsettings.kcfg +NameSpace=KWin::TabBox +ClassName=TabBoxSettings +IncludeFiles=\"tabboxconfig.h\" +Mutators=true +DefaultValueGetters=true +ParentInConstructor=true diff --git a/kcmkwin/kwintabbox/layoutpreview.cpp b/kcmkwin/kwintabbox/layoutpreview.cpp new file mode 100644 index 0000000..2fd7468 --- /dev/null +++ b/kcmkwin/kwintabbox/layoutpreview.cpp @@ -0,0 +1,254 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009, 2011 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +// own +#include "layoutpreview.h" +#include "thumbnailitem.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace KWin +{ +namespace TabBox +{ + +LayoutPreview::LayoutPreview(const QString &path, QObject *parent) + : QObject(parent) + , m_item(nullptr) +{ + QQmlEngine *engine = new QQmlEngine(this); + QQmlComponent *component = new QQmlComponent(engine, this); + qmlRegisterType("org.kde.kwin", 2, 0, "ThumbnailItem"); + qmlRegisterType("org.kde.kwin", 2, 0, "Switcher"); + qmlRegisterType(); + component->loadUrl(QUrl::fromLocalFile(path)); + if (component->isError()) { + qDebug() << component->errorString(); + } + QObject *item = component->create(); + auto findSwitcher = [item]() -> SwitcherItem* { + if (!item) { + return nullptr; + } + if (SwitcherItem *i = qobject_cast(item)) { + return i; + } else if (QQuickWindow *w = qobject_cast(item)) { + return w->contentItem()->findChild(); + } + return item->findChild(); + }; + if (SwitcherItem *switcher = findSwitcher()) { + m_item = switcher; + switcher->setVisible(true); + } + auto findWindow = [item]() -> QQuickWindow* { + if (!item) { + return nullptr; + } + if (QQuickWindow *w = qobject_cast(item)) { + return w; + } + return item->findChild(); + }; + if (QQuickWindow *w = findWindow()) { + w->setKeyboardGrabEnabled(true); + w->setMouseGrabEnabled(true); + w->installEventFilter(this); + } +} + +LayoutPreview::~LayoutPreview() +{ +} + +bool LayoutPreview::eventFilter(QObject *object, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Escape || + keyEvent->key() == Qt::Key_Return || + keyEvent->key() == Qt::Key_Enter || + keyEvent->key() == Qt::Key_Space) { + object->deleteLater(); + deleteLater(); + } + if (m_item && keyEvent->key() == Qt::Key_Tab) { + m_item->incrementIndex(); + } + if (m_item && keyEvent->key() == Qt::Key_Backtab) { + m_item->decrementIndex(); + } + } else if (event->type() == QEvent::MouseButtonPress) { + if (QWindow *w = qobject_cast(object)) { + if (!w->geometry().contains(static_cast(event)->globalPos())) { + object->deleteLater(); + deleteLater(); + } + } + } + return QObject::eventFilter(object, event); +} + +ExampleClientModel::ExampleClientModel (QObject* parent) + : QAbstractListModel (parent) +{ + init(); +} + +ExampleClientModel::~ExampleClientModel() +{ +} + +void ExampleClientModel::init() +{ + if (const auto s = KApplicationTrader::preferredService(QStringLiteral("inode/directory"))) { + m_services << s; + m_fileManager = s; + } + if (const auto s = KApplicationTrader::preferredService(QStringLiteral("text/html"))) { + m_services << s; + m_browser = s; + } + if (const auto s = KApplicationTrader::preferredService(QStringLiteral("message/rfc822"))) { + m_services << s; + m_email = s; + } + if (const auto s = KService::serviceByDesktopName(QStringLiteral("kdesystemsettings"))) { + m_services << s; + m_systemSettings = s; + } +} + +QVariant ExampleClientModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + switch (role) { + case Qt::DisplayRole: + case CaptionRole: + return m_services.at(index.row())->name(); + case MinimizedRole: + return false; + case DesktopNameRole: + return i18nc("An example Desktop Name", "Desktop 1"); + case IconRole: + return m_services.at(index.row())->icon(); + case WindowIdRole: + const auto s = m_services.at(index.row()); + if (s == m_browser) { + return WindowThumbnailItem::Konqueror; + } else if (s == m_email) { + return WindowThumbnailItem::KMail; + } else if (s == m_systemSettings) { + return WindowThumbnailItem::Systemsettings; + } else if (s == m_fileManager) { + return WindowThumbnailItem::Dolphin; + } + return 0; + } + return QVariant(); +} + +QString ExampleClientModel::longestCaption() const +{ + QString caption; + for (const auto &item : m_services) { + const QString name = item->name(); + if (name.size() > caption.size()) { + caption = name; + } + } + return caption; +} + +int ExampleClientModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_services.size(); +} + +QHash ExampleClientModel::roleNames() const +{ + return { + { CaptionRole, QByteArrayLiteral("caption") }, + { MinimizedRole, QByteArrayLiteral("minimized") }, + { DesktopNameRole, QByteArrayLiteral("desktopName") }, + { IconRole, QByteArrayLiteral("icon") }, + { WindowIdRole, QByteArrayLiteral("windowId") }, + }; +} + +SwitcherItem::SwitcherItem(QObject *parent) + : QObject(parent) + , m_model(new ExampleClientModel(this)) + , m_item(nullptr) + , m_currentIndex(0) + , m_visible(false) +{ +} + +SwitcherItem::~SwitcherItem() +{ +} + +void SwitcherItem::setVisible(bool visible) +{ + if (m_visible == visible) { + return; + } + m_visible = visible; + emit visibleChanged(); +} + +void SwitcherItem::setItem(QObject *item) +{ + m_item = item; + emit itemChanged(); +} + +void SwitcherItem::setCurrentIndex(int index) +{ + if (m_currentIndex == index) { + return; + } + m_currentIndex = index; + emit currentIndexChanged(m_currentIndex); +} + +QRect SwitcherItem::screenGeometry() const +{ + const QScreen *primaryScreen = qApp->primaryScreen(); + return primaryScreen->geometry(); +} + +void SwitcherItem::incrementIndex() +{ + setCurrentIndex((m_currentIndex + 1) % m_model->rowCount()); +} + +void SwitcherItem::decrementIndex() +{ + int index = m_currentIndex -1; + if (index < 0) { + index = m_model->rowCount() -1; + } + setCurrentIndex(index); +} + +} // namespace KWin +} // namespace TabBox + diff --git a/kcmkwin/kwintabbox/layoutpreview.h b/kcmkwin/kwintabbox/layoutpreview.h new file mode 100644 index 0000000..7960b25 --- /dev/null +++ b/kcmkwin/kwintabbox/layoutpreview.h @@ -0,0 +1,142 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009, 2011 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_TABBOX_LAYOUTPREVIEW_H +#define KWIN_TABBOX_LAYOUTPREVIEW_H + +#include +#include +#include +#include + +namespace KWin +{ + +namespace TabBox +{ + +class SwitcherItem; + +class LayoutPreview : public QObject +{ + Q_OBJECT +public: + explicit LayoutPreview(const QString &path, QObject *parent = nullptr); + ~LayoutPreview() override; + + bool eventFilter(QObject *object, QEvent *event) override; +private: + SwitcherItem *m_item; +}; + +class ExampleClientModel : public QAbstractListModel +{ + Q_OBJECT +public: + enum { + CaptionRole = Qt::UserRole + 1, + MinimizedRole, + DesktopNameRole, + IconRole, + WindowIdRole + }; + + explicit ExampleClientModel(QObject *parent = nullptr); + ~ExampleClientModel() override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QHash roleNames() const override; + Q_INVOKABLE QString longestCaption() const; + +private: + void init(); + QList m_services; + KService::Ptr m_fileManager; + KService::Ptr m_browser; + KService::Ptr m_email; + KService::Ptr m_systemSettings; +}; + + +class SwitcherItem : public QObject +{ + Q_OBJECT + Q_PROPERTY(QAbstractItemModel *model READ model NOTIFY modelChanged) + Q_PROPERTY(QRect screenGeometry READ screenGeometry NOTIFY screenGeometryChanged) + Q_PROPERTY(bool visible READ isVisible NOTIFY visibleChanged) + Q_PROPERTY(bool allDesktops READ isAllDesktops NOTIFY allDesktopsChanged) + Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) + + /** + * The main QML item that will be displayed in the Dialog + */ + Q_PROPERTY(QObject *item READ item WRITE setItem NOTIFY itemChanged) + + Q_CLASSINFO("DefaultProperty", "item") +public: + SwitcherItem(QObject *parent = nullptr); + ~SwitcherItem() override; + + QAbstractItemModel *model() const; + QRect screenGeometry() const; + bool isVisible() const; + bool isAllDesktops() const; + int currentIndex() const; + void setCurrentIndex(int index); + QObject *item() const; + void setItem(QObject *item); + + void setVisible(bool visible); + void incrementIndex(); + void decrementIndex(); + +Q_SIGNALS: + void visibleChanged(); + void currentIndexChanged(int index); + void modelChanged(); + void allDesktopsChanged(); + void screenGeometryChanged(); + void itemChanged(); + +private: + QAbstractItemModel *m_model; + QObject *m_item; + int m_currentIndex; + bool m_visible; +}; + +inline QAbstractItemModel *SwitcherItem::model() const +{ + return m_model; +} + +inline bool SwitcherItem::isVisible() const +{ + return m_visible; +} + +inline bool SwitcherItem::isAllDesktops() const +{ + return true; +} + +inline int SwitcherItem::currentIndex() const +{ + return m_currentIndex; +} + +inline QObject *SwitcherItem::item() const +{ + return m_item; +} + +} // namespace TabBox +} // namespace KWin + +#endif // KWIN_TABBOX_LAYOUTPREVIEW_H diff --git a/kcmkwin/kwintabbox/main.cpp b/kcmkwin/kwintabbox/main.cpp new file mode 100644 index 0000000..73cd8d7 --- /dev/null +++ b/kcmkwin/kwintabbox/main.cpp @@ -0,0 +1,478 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Martin Gräßlin + SPDX-FileCopyrightText: 2020 Cyril Rossi + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "main.h" +#include +#include + +// Qt +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// KDE +#include +#include +#include +#include +#include +#include +#include +// Plasma +#include +#include + +// own +#include "kwintabboxconfigform.h" +#include "layoutpreview.h" +#include "kwintabboxsettings.h" +#include "kwinswitcheffectsettings.h" +#include "kwinpluginssettings.h" + +K_PLUGIN_FACTORY(KWinTabBoxConfigFactory, registerPlugin();) + +namespace KWin +{ + +using namespace TabBox; + + +KWinTabBoxConfig::KWinTabBoxConfig(QWidget* parent, const QVariantList& args) + : KCModule(parent, args) + , m_config(KSharedConfig::openConfig("kwinrc")) + , m_tabBoxConfig(new TabBoxSettings(QStringLiteral("TabBox"), this)) + , m_tabBoxAlternativeConfig(new TabBoxSettings(QStringLiteral("TabBoxAlternative"), this)) + , m_coverSwitchConfig(new SwitchEffectSettings(QStringLiteral("Effect-CoverSwitch"), this)) + , m_flipSwitchConfig(new SwitchEffectSettings(QStringLiteral("Effect-FlipSwitch"), this)) + , m_pluginsConfig(new PluginsSettings(this)) +{ + QTabWidget* tabWidget = new QTabWidget(this); + m_primaryTabBoxUi = new KWinTabBoxConfigForm(KWinTabBoxConfigForm::TabboxType::Main, tabWidget); + m_alternativeTabBoxUi = new KWinTabBoxConfigForm(KWinTabBoxConfigForm::TabboxType::Alternative, tabWidget); + tabWidget->addTab(m_primaryTabBoxUi, i18n("Main")); + tabWidget->addTab(m_alternativeTabBoxUi, i18n("Alternative")); + + QPushButton* ghnsButton = new QPushButton(QIcon::fromTheme(QStringLiteral("get-hot-new-stuff")), i18n("Get New Task Switchers...")); + connect(ghnsButton, SIGNAL(clicked(bool)), SLOT(slotGHNS())); + + QHBoxLayout* buttonBar = new QHBoxLayout(); + QSpacerItem* buttonBarSpacer = new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum); + buttonBar->addItem(buttonBarSpacer); + buttonBar->addWidget(ghnsButton); + + QVBoxLayout* layout = new QVBoxLayout(this); + KTitleWidget* infoLabel = new KTitleWidget(tabWidget); + infoLabel->setText(i18n("Focus policy settings limit the functionality of navigating through windows."), + KTitleWidget::InfoMessage); + infoLabel->setIcon(KTitleWidget::InfoMessage, KTitleWidget::ImageLeft); + layout->addWidget(infoLabel,0); + layout->addWidget(tabWidget,1); + layout->addLayout(buttonBar); + setLayout(layout); + + addConfig(m_tabBoxConfig, m_primaryTabBoxUi); + addConfig(m_tabBoxAlternativeConfig, m_alternativeTabBoxUi); + + createConnections(m_primaryTabBoxUi); + createConnections(m_alternativeTabBoxUi); + + initLayoutLists(); + + // check focus policy - we don't offer configs for unreasonable focus policies + KConfigGroup config(m_config, "Windows"); + QString policy = config.readEntry("FocusPolicy", "ClickToFocus"); + if ((policy == "FocusUnderMouse") || (policy == "FocusStrictlyUnderMouse")) { + tabWidget->setEnabled(false); + infoLabel->show(); + } else { + infoLabel->hide(); + } + + setEnabledUi(m_primaryTabBoxUi, m_tabBoxConfig); + setEnabledUi(m_alternativeTabBoxUi, m_tabBoxAlternativeConfig); +} + +KWinTabBoxConfig::~KWinTabBoxConfig() +{ +} + +static QList availableLnFPackages() +{ + QList packages; + QStringList paths; + const QStringList dataPaths = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); + + for (const QString &path : dataPaths) { + QDir dir(path + QLatin1String("/plasma/look-and-feel")); + paths << dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot); + } + + const auto &p = paths; + for (const QString &path : p) { + KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/LookAndFeel")); + pkg.setPath(path); + pkg.setFallbackPackage(KPackage::Package()); + if (!pkg.filePath("defaults").isEmpty()) { + KSharedConfigPtr conf = KSharedConfig::openConfig(pkg.filePath("defaults")); + KConfigGroup cg = KConfigGroup(conf, "kwinrc"); + cg = KConfigGroup(&cg, "WindowSwitcher"); + if (!cg.readEntry("LayoutName", QString()).isEmpty()) { + packages << pkg; + } + } + } + + return packages; +} + +void KWinTabBoxConfig::initLayoutLists() +{ + // search the effect names + m_coverSwitch = BuiltInEffects::effectData(BuiltInEffect::CoverSwitch).name; + m_flipSwitch = BuiltInEffects::effectData(BuiltInEffect::FlipSwitch).name; + + QList offers = KPackage::PackageLoader::self()->listPackages("KWin/WindowSwitcher"); + QStringList layoutNames, layoutPlugins, layoutPaths; + + const auto lnfPackages = availableLnFPackages(); + for (const auto &package : lnfPackages) { + const auto &metaData = package.metadata(); + layoutNames << metaData.name(); + layoutPlugins << metaData.pluginId(); + layoutPaths << package.filePath("windowswitcher", QStringLiteral("WindowSwitcher.qml")); + } + + for (const auto &offer : offers) { + const QString pluginName = offer.pluginId(); + if (offer.value("X-Plasma-API") != "declarativeappletscript") { + continue; + } + //we don't have a proper servicetype + if (offer.value("X-KWin-Exclude-Listing") == QStringLiteral("true")) { + continue; + } + const QString scriptName = offer.value("X-Plasma-MainScript"); + const QString scriptFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, + QLatin1String("kwin/tabbox/") + pluginName + QLatin1String("/contents/") + + scriptName); + if (scriptFile.isNull()) { + continue; + } + + layoutNames << offer.name(); + layoutPlugins << pluginName; + layoutPaths << scriptFile; + } + + + KWinTabBoxConfigForm *ui[2] = { m_primaryTabBoxUi, m_alternativeTabBoxUi }; + for (int i=0; i<2; ++i) { + QStandardItemModel *model = new QStandardItemModel; + + QStandardItem *coverItem = new QStandardItem(BuiltInEffects::effectData(BuiltInEffect::CoverSwitch).displayName); + coverItem->setData(m_coverSwitch, Qt::UserRole); + coverItem->setData(false, KWinTabBoxConfigForm::AddonEffect); + model->appendRow(coverItem); + + QStandardItem *flipItem = new QStandardItem(BuiltInEffects::effectData(BuiltInEffect::FlipSwitch).displayName); + flipItem->setData(m_flipSwitch, Qt::UserRole); + flipItem->setData(false, KWinTabBoxConfigForm::AddonEffect); + model->appendRow(flipItem); + + for (int j = 0; j < layoutNames.count(); ++j) { + QStandardItem *item = new QStandardItem(layoutNames[j]); + item->setData(layoutPlugins[j], Qt::UserRole); + item->setData(layoutPaths[j], KWinTabBoxConfigForm::LayoutPath); + item->setData(true, KWinTabBoxConfigForm::AddonEffect); + model->appendRow(item); + } + model->sort(0); + ui[i]->setEffectComboModel(model); + } +} + +void KWinTabBoxConfig::setEnabledUi(KWinTabBoxConfigForm *form, const TabBoxSettings *config) +{ + form->setHighlightWindowsEnabled(!config->isHighlightWindowsImmutable()); + form->setFilterScreenEnabled(!config->isMultiScreenModeImmutable()); + form->setFilterDesktopEnabled(!config->isDesktopModeImmutable()); + form->setFilterActivitiesEnabled(!config->isActivitiesModeImmutable()); + form->setFilterMinimizationEnabled(!config->isMinimizedModeImmutable()); + form->setApplicationModeEnabled(!config->isApplicationsModeImmutable()); + form->setShowDesktopModeEnabled(!config->isShowDesktopModeImmutable()); + form->setSwitchingModeEnabled(!config->isSwitchingModeImmutable()); + form->setLayoutNameEnabled(!config->isLayoutNameImmutable()); +} + +void KWinTabBoxConfig::createConnections(KWinTabBoxConfigForm *form) +{ + connect(form, SIGNAL(effectConfigButtonClicked()), this, SLOT(configureEffectClicked())); + + connect(form, SIGNAL(filterScreenChanged(int)), this, SLOT(updateUnmanagedState())); + connect(form, SIGNAL(filterDesktopChanged(int)), this, SLOT(updateUnmanagedState())); + connect(form, SIGNAL(filterActivitiesChanged(int)), this, SLOT(updateUnmanagedState())); + connect(form, SIGNAL(filterMinimizationChanged(int)), this, SLOT(updateUnmanagedState())); + connect(form, SIGNAL(applicationModeChanged(int)), this, SLOT(updateUnmanagedState())); + connect(form, SIGNAL(showDesktopModeChanged(int)), this, SLOT(updateUnmanagedState())); + connect(form, SIGNAL(switchingModeChanged(int)), this, SLOT(updateUnmanagedState())); + connect(form, SIGNAL(layoutNameChanged(QString)), this, SLOT(updateUnmanagedState())); +} + +void KWinTabBoxConfig::updateUnmanagedState() +{ + bool isNeedSave = false; + isNeedSave |= updateUnmanagedIsNeedSave(m_primaryTabBoxUi, m_tabBoxConfig); + isNeedSave |= updateUnmanagedIsNeedSave(m_alternativeTabBoxUi, m_tabBoxAlternativeConfig); + + unmanagedWidgetChangeState(isNeedSave); + + bool isDefault = true; + isDefault &= updateUnmanagedIsDefault(m_primaryTabBoxUi, m_tabBoxConfig); + isDefault &= updateUnmanagedIsDefault(m_alternativeTabBoxUi, m_tabBoxAlternativeConfig); + + unmanagedWidgetDefaultState(isDefault); +} + +bool KWinTabBoxConfig::updateUnmanagedIsNeedSave(const KWinTabBoxConfigForm *form, const TabBoxSettings *config) +{ + bool isNeedSave = false; + isNeedSave |= form->filterScreen() != config->multiScreenMode(); + isNeedSave |= form->filterDesktop() != config->desktopMode(); + isNeedSave |= form->filterActivities() != config->activitiesMode(); + isNeedSave |= form->filterMinimization() != config->minimizedMode(); + isNeedSave |= form->applicationMode() != config->applicationsMode(); + isNeedSave |= form->showDesktopMode() != config->showDesktopMode(); + isNeedSave |= form->switchingMode() != config->switchingMode(); + isNeedSave |= form->layoutName() != config->layoutName(); + + return isNeedSave; +} + +bool KWinTabBoxConfig::updateUnmanagedIsDefault(const KWinTabBoxConfigForm *form, const TabBoxSettings *config) +{ + bool isDefault = true; + isDefault &= form->filterScreen() == config->defaultMultiScreenModeValue(); + isDefault &= form->filterDesktop() == config->defaultDesktopModeValue(); + isDefault &= form->filterActivities() == config->defaultActivitiesModeValue(); + isDefault &= form->filterMinimization() == config->defaultMinimizedModeValue(); + isDefault &= form->applicationMode() == config->defaultApplicationsModeValue(); + isDefault &= form->showDesktopMode() == config->defaultShowDesktopModeValue(); + isDefault &= form->switchingMode() == config->defaultSwitchingModeValue(); + isDefault &= form->layoutName() == config->defaultLayoutNameValue(); + + return isDefault; +} + +void KWinTabBoxConfig::load() +{ + KCModule::load(); + + m_tabBoxConfig->load(); + m_tabBoxAlternativeConfig->load(); + + updateUiFromConfig(m_primaryTabBoxUi, m_tabBoxConfig); + updateUiFromConfig(m_alternativeTabBoxUi , m_tabBoxAlternativeConfig); + + m_coverSwitchConfig->load(); + m_flipSwitchConfig->load(); + + m_pluginsConfig->load(); + + if (m_pluginsConfig->coverswitchEnabled()) { + if (m_coverSwitchConfig->tabBox()) { + m_primaryTabBoxUi->setLayoutName(m_coverSwitch); + } + if (m_coverSwitchConfig->tabBoxAlternative()) { + m_alternativeTabBoxUi->setLayoutName(m_coverSwitch); + } + } + if (m_pluginsConfig->flipswitchEnabled()) { + if (m_flipSwitchConfig->tabBox()) { + m_primaryTabBoxUi->setLayoutName(m_flipSwitch); + } + if (m_flipSwitchConfig->tabBoxAlternative()) { + m_alternativeTabBoxUi->setLayoutName(m_flipSwitch); + } + } + + m_primaryTabBoxUi->loadShortcuts(); + m_alternativeTabBoxUi->loadShortcuts(); + + updateUnmanagedState(); +} + +void KWinTabBoxConfig::save() +{ + // effects + const bool highlightWindows = m_primaryTabBoxUi->highlightWindows() || m_alternativeTabBoxUi->highlightWindows(); + const bool coverSwitch = m_primaryTabBoxUi->showTabBox() + && m_primaryTabBoxUi->effectComboCurrentData().toString() == m_coverSwitch; + const bool flipSwitch = m_primaryTabBoxUi->showTabBox() + && m_primaryTabBoxUi->effectComboCurrentData().toString() == m_flipSwitch; + const bool coverSwitchAlternative = m_alternativeTabBoxUi->showTabBox() + && m_alternativeTabBoxUi->effectComboCurrentData().toString() == m_coverSwitch; + const bool flipSwitchAlternative = m_alternativeTabBoxUi->showTabBox() + && m_alternativeTabBoxUi->effectComboCurrentData().toString() == m_flipSwitch; + + // activate effects if not active + if (coverSwitch || coverSwitchAlternative) { + m_pluginsConfig->setCoverswitchEnabled(true); + } + if (flipSwitch || flipSwitchAlternative) { + m_pluginsConfig->setFlipswitchEnabled(true); + } + if (highlightWindows) { + m_pluginsConfig->setHighlightwindowEnabled(true); + } + m_pluginsConfig->save(); + + m_coverSwitchConfig->setTabBox(coverSwitch); + m_coverSwitchConfig->setTabBoxAlternative(coverSwitchAlternative); + m_coverSwitchConfig->save(); + + m_flipSwitchConfig->setTabBox(flipSwitch); + m_flipSwitchConfig->setTabBoxAlternative(flipSwitchAlternative); + m_flipSwitchConfig->save(); + + updateConfigFromUi(m_primaryTabBoxUi, m_tabBoxConfig); + updateConfigFromUi(m_alternativeTabBoxUi, m_tabBoxAlternativeConfig); + + m_tabBoxConfig->save(); + m_tabBoxAlternativeConfig->save(); + + KCModule::save(); + updateUnmanagedState(); + + // Reload KWin. + QDBusMessage message = QDBusMessage::createSignal("/KWin", "org.kde.KWin", "reloadConfig"); + QDBusConnection::sessionBus().send(message); + // and reconfigure the effects + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(BuiltInEffects::nameForEffect(BuiltInEffect::CoverSwitch)); + interface.reconfigureEffect(BuiltInEffects::nameForEffect(BuiltInEffect::FlipSwitch)); +} + +void KWinTabBoxConfig::defaults() +{ + m_coverSwitchConfig->setDefaults(); + m_flipSwitchConfig->setDefaults(); + + updateUiFromDefaultConfig(m_primaryTabBoxUi, m_tabBoxConfig); + updateUiFromDefaultConfig(m_alternativeTabBoxUi, m_tabBoxAlternativeConfig); + + m_primaryTabBoxUi->resetShortcuts(); + m_alternativeTabBoxUi->resetShortcuts(); + + KCModule::defaults(); + updateUnmanagedState(); +} + +void KWinTabBoxConfig::updateUiFromConfig(KWinTabBoxConfigForm *form, const KWin::TabBox::TabBoxSettings *config) +{ + form->setFilterScreen(static_cast(config->multiScreenMode())); + form->setFilterDesktop(static_cast(config->desktopMode())); + form->setFilterActivities(static_cast(config->activitiesMode())); + form->setFilterMinimization(static_cast(config->minimizedMode())); + form->setApplicationMode(static_cast(config->applicationsMode())); + form->setShowDesktopMode(static_cast(config->showDesktopMode())); + form->setSwitchingModeChanged(static_cast(config->switchingMode())); + form->setLayoutName(config->layoutName()); +} + +void KWinTabBoxConfig::updateConfigFromUi(const KWinTabBoxConfigForm *form, TabBoxSettings *config) +{ + config->setMultiScreenMode(form->filterScreen()); + config->setDesktopMode(form->filterDesktop()); + config->setActivitiesMode(form->filterActivities()); + config->setMinimizedMode(form->filterMinimization()); + config->setApplicationsMode(form->applicationMode()); + config->setShowDesktopMode(form->showDesktopMode()); + config->setSwitchingMode(form->switchingMode()); + config->setLayoutName(form->layoutName()); +} + +void KWinTabBoxConfig::updateUiFromDefaultConfig(KWinTabBoxConfigForm *form, const KWin::TabBox::TabBoxSettings *config) +{ + form->setFilterScreen(static_cast(config->defaultMultiScreenModeValue())); + form->setFilterDesktop(static_cast(config->defaultDesktopModeValue())); + form->setFilterActivities(static_cast(config->defaultActivitiesModeValue())); + form->setFilterMinimization(static_cast(config->defaultMinimizedModeValue())); + form->setApplicationMode(static_cast(config->defaultApplicationsModeValue())); + form->setShowDesktopMode(static_cast(config->defaultShowDesktopModeValue())); + form->setSwitchingModeChanged(static_cast(config->defaultSwitchingModeValue())); + form->setLayoutName(config->defaultLayoutNameValue()); +} + +void KWinTabBoxConfig::configureEffectClicked() +{ + auto form = qobject_cast(sender()); + Q_ASSERT(form); + + if (form->effectComboCurrentData(KWinTabBoxConfigForm::AddonEffect).toBool()) { + // Show the preview for addon effect + new LayoutPreview(form->effectComboCurrentData(KWinTabBoxConfigForm::LayoutPath).toString(), this); + } else { + // For builtin effect, display a configuration dialog + QPointer configDialog = new QDialog(this); + configDialog->setLayout(new QVBoxLayout); + configDialog->setWindowTitle(form->effectComboCurrentData(Qt::DisplayRole).toString()); + QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel|QDialogButtonBox::RestoreDefaults, configDialog); + connect(buttonBox, SIGNAL(accepted()), configDialog, SLOT(accept())); + connect(buttonBox, SIGNAL(rejected()), configDialog, SLOT(reject())); + + const QString name = form->effectComboCurrentData().toString(); + KCModule *kcm = KPluginTrader::createInstanceFromQuery(QStringLiteral("kwin/effects/configs/"), QString(), + QStringLiteral("'%1' in [X-KDE-ParentComponents]").arg(name), + configDialog); + if (!kcm) { + delete configDialog; + return; + } + + connect(buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, kcm, &KCModule::defaults); + + QWidget *showWidget = new QWidget(configDialog); + QVBoxLayout *layout = new QVBoxLayout; + showWidget->setLayout(layout); + layout->addWidget(kcm); + configDialog->layout()->addWidget(showWidget); + configDialog->layout()->addWidget(buttonBox); + + if (configDialog->exec() == QDialog::Accepted) { + kcm->save(); + } else { + kcm->load(); + } + delete configDialog; + } +} + +void KWinTabBoxConfig::slotGHNS() +{ + QPointer downloadDialog = new KNS3::DownloadDialog("kwinswitcher.knsrc", this); + if (downloadDialog->exec() == QDialog::Accepted) { + if (!downloadDialog->changedEntries().isEmpty()) { + initLayoutLists(); + } + } + delete downloadDialog; +} + +} // namespace + +#include "main.moc" diff --git a/kcmkwin/kwintabbox/main.h b/kcmkwin/kwintabbox/main.h new file mode 100644 index 0000000..99d5f5c --- /dev/null +++ b/kcmkwin/kwintabbox/main.h @@ -0,0 +1,76 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Martin Gräßlin + SPDX-FileCopyrightText: 2020 Cyril Rossi + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef __MAIN_H__ +#define __MAIN_H__ + +#include +#include +#include "tabboxconfig.h" + +namespace KWin +{ +class KWinTabBoxConfigForm; +enum class BuiltInEffect; +namespace TabBox +{ +class TabBoxSettings; +class SwitchEffectSettings; +class PluginsSettings; +} + + +class KWinTabBoxConfig : public KCModule +{ + Q_OBJECT + +public: + explicit KWinTabBoxConfig(QWidget* parent, const QVariantList& args); + ~KWinTabBoxConfig() override; + +public Q_SLOTS: + void save() override; + void load() override; + void defaults() override; + +private Q_SLOTS: + void updateUnmanagedState(); + void configureEffectClicked(); + void slotGHNS(); + +private: + void updateUiFromConfig(KWinTabBoxConfigForm *form, const TabBox::TabBoxSettings *config); + void updateConfigFromUi(const KWinTabBoxConfigForm *form, TabBox::TabBoxSettings *config); + void updateUiFromDefaultConfig(KWinTabBoxConfigForm *form, const TabBox::TabBoxSettings *config); + void initLayoutLists(); + void setEnabledUi(KWinTabBoxConfigForm *form, const TabBox::TabBoxSettings *config); + void createConnections(KWinTabBoxConfigForm *form); + bool updateUnmanagedIsNeedSave(const KWinTabBoxConfigForm *form, const TabBox::TabBoxSettings *config); + bool updateUnmanagedIsDefault(const KWinTabBoxConfigForm *form, const TabBox::TabBoxSettings *config); + +private: + KWinTabBoxConfigForm *m_primaryTabBoxUi = nullptr; + KWinTabBoxConfigForm *m_alternativeTabBoxUi = nullptr; + KSharedConfigPtr m_config; + + TabBox::TabBoxSettings *m_tabBoxConfig; + TabBox::TabBoxSettings *m_tabBoxAlternativeConfig; + TabBox::SwitchEffectSettings *m_coverSwitchConfig; + TabBox::SwitchEffectSettings *m_flipSwitchConfig; + TabBox::PluginsSettings *m_pluginsConfig; + + // Builtin effects' names + QString m_coverSwitch; + QString m_flipSwitch; +}; + +} // namespace + +#endif diff --git a/kcmkwin/kwintabbox/main.ui b/kcmkwin/kwintabbox/main.ui new file mode 100644 index 0000000..e702e35 --- /dev/null +++ b/kcmkwin/kwintabbox/main.ui @@ -0,0 +1,699 @@ + + + KWinTabBoxConfigForm + + + + 0 + 0 + 658 + 418 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Content + + + true + + + + + + Include "Show Desktop" icon + + + + + + + + 0 + 0 + + + + + Recently used + + + + + Stacking order + + + + + + + + Only one window per application + + + false + + + + + + + Sort order: + + + switchingModeCombo + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Filter windows by + + + true + + + + + + Virtual desktops + + + false + + + + + + + false + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 24 + 20 + + + + + + + + Current desktop + + + + + + + All other desktops + + + + + + + + + + Activities + + + false + + + + + + + false + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 24 + 20 + + + + + + + + Current activity + + + + + + + All other activities + + + + + + + + + + Screens + + + false + + + + + + + false + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 24 + 20 + + + + + + + + Current screen + + + + + + + All other screens + + + + + + + + + + Minimization + + + false + + + + + + + false + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 24 + 20 + + + + + + + + Visible windows + + + + + + + Hidden windows + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Shortcuts + + + true + + + + + + Forward + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Horizontal + + + + + + + + 75 + true + + + + All windows + + + Qt::AlignCenter + + + + + + + Reverse + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Forward + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Reverse + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + + + 75 + true + + + + Current application + + + Qt::AlignCenter + + + + + + + + + + + + + + + + Visualization + + + true + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + The effect to replace the list window when desktop effects are active. + + + + + + + + 0 + 0 + + + + + + + + + + + + + + true + + + + + + + The currently selected window will be highlighted by fading out all other windows. This option requires desktop effects to be active. + + + Show selected window + + + + + + + + + + Qt::Vertical + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + KKeySequenceWidget + QWidget +
kkeysequencewidget.h
+
+ + KComboBox + QComboBox +
kcombobox.h
+
+
+ + kcfg_HighlightWindows + kcfg_ShowTabBox + effectCombo + effectConfigButton + switchingModeCombo + showDesktop + oneAppWindow + filterDesktops + currentDesktop + otherDesktops + filterActivities + currentActivity + otherActivities + filterScreens + currentScreen + otherScreens + filterMinimization + visibleWindows + hiddenWindows + + + + + filterDesktops + toggled(bool) + desktopFilter + setEnabled(bool) + + + 541 + 172 + + + 701 + 197 + + + + + filterActivities + toggled(bool) + activityFilter + setEnabled(bool) + + + 543 + 222 + + + 701 + 247 + + + + + filterScreens + toggled(bool) + screenFilter + setEnabled(bool) + + + 555 + 272 + + + 701 + 297 + + + + + filterMinimization + toggled(bool) + minimizationFilter + setEnabled(bool) + + + 558 + 322 + + + 701 + 347 + + + + + kcfg_ShowTabBox + toggled(bool) + widget_6 + setEnabled(bool) + + + 164 + 125 + + + 230 + 108 + + + + +
diff --git a/kcmkwin/kwintabbox/thumbnailitem.cpp b/kcmkwin/kwintabbox/thumbnailitem.cpp new file mode 100644 index 0000000..ea2f8f9 --- /dev/null +++ b/kcmkwin/kwintabbox/thumbnailitem.cpp @@ -0,0 +1,208 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2011, 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "thumbnailitem.h" +// Qt +#include +#include +#include + +namespace KWin +{ + +BrightnessSaturationShader::BrightnessSaturationShader() + : QSGMaterialShader() + , m_id_matrix(0) + , m_id_opacity(0) + , m_id_saturation(0) + , m_id_brightness(0) +{ +} + +const char *BrightnessSaturationShader::vertexShader() const +{ + return + "attribute highp vec4 vertex; \n" + "attribute highp vec2 texCoord; \n" + "uniform highp mat4 u_matrix; \n" + "varying highp vec2 v_coord; \n" + "void main() { \n" + " v_coord = texCoord; \n" + " gl_Position = u_matrix * vertex; \n" + "}"; +} + +const char *BrightnessSaturationShader::fragmentShader() const +{ + return + "uniform sampler2D qt_Texture; \n" + "uniform lowp float u_opacity; \n" + "uniform highp float u_saturation; \n" + "uniform highp float u_brightness; \n" + "varying highp vec2 v_coord; \n" + "void main() { \n" + " lowp vec4 tex = texture2D(qt_Texture, v_coord); \n" + " if (u_saturation != 1.0) { \n" + " tex.rgb = mix(vec3(dot( vec3( 0.30, 0.59, 0.11 ), tex.rgb )), tex.rgb, u_saturation); \n" + " } \n" + " tex.rgb = tex.rgb * u_brightness; \n" + " gl_FragColor = tex * u_opacity; \n" + "}"; +} + +const char* const *BrightnessSaturationShader::attributeNames() const +{ + static char const *const names[] = { "vertex", "texCoord", nullptr }; + return names; +} + +void BrightnessSaturationShader::updateState(const QSGMaterialShader::RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial) +{ + Q_ASSERT(program()->isLinked()); + if (state.isMatrixDirty()) + program()->setUniformValue(m_id_matrix, state.combinedMatrix()); + if (state.isOpacityDirty()) + program()->setUniformValue(m_id_opacity, state.opacity()); + + auto *tx = static_cast(newMaterial); + auto *oldTx = static_cast(oldMaterial); + QSGTexture *t = tx->texture(); + t->setFiltering(QSGTexture::Linear); + + if (!oldTx || oldTx->texture()->textureId() != t->textureId()) + t->bind(); + else + t->updateBindOptions(); + + program()->setUniformValue(m_id_saturation, static_cast(tx->saturation)); + program()->setUniformValue(m_id_brightness, static_cast(tx->brightness)); +} + +void BrightnessSaturationShader::initialize() +{ + QSGMaterialShader::initialize(); + m_id_matrix = program()->uniformLocation("u_matrix"); + m_id_opacity = program()->uniformLocation("u_opacity"); + m_id_saturation = program()->uniformLocation("u_saturation"); + m_id_brightness = program()->uniformLocation("u_brightness"); +} + +WindowThumbnailItem::WindowThumbnailItem(QQuickItem* parent) + : QQuickItem(parent) + , m_wId(0) + , m_image() + , m_clipToItem(nullptr) + , m_brightness(1.0) + , m_saturation(1.0) +{ + setFlag(ItemHasContents); +} + +WindowThumbnailItem::~WindowThumbnailItem() +{ +} + +void WindowThumbnailItem::setWId(qulonglong wId) +{ + m_wId = wId; + emit wIdChanged(wId); + findImage(); +} + +void WindowThumbnailItem::setClipTo(QQuickItem *clip) +{ + if (m_clipToItem == clip) { + return; + } + m_clipToItem = clip; + emit clipToChanged(); +} + +void WindowThumbnailItem::findImage() +{ + QString imagePath; + switch (m_wId) { + case Konqueror: + imagePath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, "kwin/kcm_kwintabbox/konqueror.png"); + break; + case Systemsettings: + imagePath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, "kwin/kcm_kwintabbox/systemsettings.png"); + break; + case KMail: + imagePath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, "kwin/kcm_kwintabbox/kmail.png"); + break; + case Dolphin: + imagePath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, "kwin/kcm_kwintabbox/dolphin.png"); + break; + default: + // ignore + break; + } + if (imagePath.isNull()) { + m_image = QImage(); + } else { + m_image = QImage(imagePath); + } +} + +QSGNode *WindowThumbnailItem::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *updatePaintNodeData) +{ + Q_UNUSED(updatePaintNodeData) + QSGGeometryNode *node = static_cast(oldNode); + if (!node) { + node = new QSGGeometryNode(); + auto *material = new BrightnessSaturationMaterial; + material->setFlag(QSGMaterial::Blending); + material->setTexture(window()->createTextureFromImage(m_image)); + node->setMaterial(material); + QSGGeometry *geometry = new QSGGeometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 4); + node->setGeometry(geometry); + } + auto *material = static_cast(node->material()); + const QSize size(material->texture()->textureSize().scaled(boundingRect().size().toSize(), Qt::KeepAspectRatio)); + const qreal x = boundingRect().x() + (boundingRect().width() - size.width())/2; + const qreal y = boundingRect().y() + (boundingRect().height() - size.height())/2; + QSGGeometry::updateTexturedRectGeometry(node->geometry(), QRectF(QPointF(x, y), size), QRectF(0.0, 0.0, 1.0, 1.0)); + material->brightness = m_brightness; + material->saturation = m_saturation; + node->markDirty(QSGNode::DirtyGeometry | QSGNode::DirtyMaterial); + return node; +} + +qreal WindowThumbnailItem::brightness() const +{ + return m_brightness; +} + +qreal WindowThumbnailItem::saturation() const +{ + return m_saturation; +} + +void WindowThumbnailItem::setBrightness(qreal brightness) +{ + if (m_brightness == brightness) { + return; + } + m_brightness = brightness; + update(); + emit brightnessChanged(); +} + +void WindowThumbnailItem::setSaturation(qreal saturation) +{ + if (m_saturation == saturation) { + return; + } + m_saturation = saturation; + update(); + emit saturationChanged(); +} + +} // namespace KWin diff --git a/kcmkwin/kwintabbox/thumbnailitem.h b/kcmkwin/kwintabbox/thumbnailitem.h new file mode 100644 index 0000000..4e6fa0c --- /dev/null +++ b/kcmkwin/kwintabbox/thumbnailitem.h @@ -0,0 +1,97 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2011, 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_THUMBNAILITEM_H +#define KWIN_THUMBNAILITEM_H + +#include +#include +#include + +namespace KWin +{ + +class BrightnessSaturationShader : public QSGMaterialShader +{ +public: + BrightnessSaturationShader(); + const char* vertexShader() const override; + const char* fragmentShader() const override; + const char*const* attributeNames() const override; + void updateState(const RenderState& state, QSGMaterial* newMaterial, QSGMaterial* oldMaterial) override; + void initialize() override; +private: + int m_id_matrix; + int m_id_opacity; + int m_id_saturation; + int m_id_brightness; +}; + +class BrightnessSaturationMaterial : public QSGTextureMaterial +{ +public: + QSGMaterialShader* createShader() const override { + return new BrightnessSaturationShader; + } + QSGMaterialType *type() const override { + static QSGMaterialType type; + return &type; + } + qreal brightness; + qreal saturation; +}; + +class WindowThumbnailItem : public QQuickItem +{ + Q_OBJECT + Q_PROPERTY(qulonglong wId READ wId WRITE setWId NOTIFY wIdChanged SCRIPTABLE true) + Q_PROPERTY(QQuickItem *clipTo READ clipTo WRITE setClipTo NOTIFY clipToChanged) + Q_PROPERTY(qreal brightness READ brightness WRITE setBrightness NOTIFY brightnessChanged) + Q_PROPERTY(qreal saturation READ saturation WRITE setSaturation NOTIFY saturationChanged) +public: + explicit WindowThumbnailItem(QQuickItem *parent = nullptr); + ~WindowThumbnailItem() override; + + qulonglong wId() const { + return m_wId; + } + QQuickItem *clipTo() const { + return m_clipToItem; + } + qreal brightness() const; + qreal saturation() const; + void setWId(qulonglong wId); + void setClipTo(QQuickItem *clip); + void setBrightness(qreal brightness); + void setSaturation(qreal saturation); + QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *updatePaintNodeData) override; + + enum Thumbnail { + Konqueror = 1, + KMail, + Systemsettings, + Dolphin + }; +Q_SIGNALS: + void wIdChanged(qulonglong wid); + void clipToChanged(); + void brightnessChanged(); + void saturationChanged(); +private: + void findImage(); + qulonglong m_wId; + QImage m_image; + QQuickItem *m_clipToItem; + qreal m_brightness; + qreal m_saturation; +}; + +} // KWin + +#endif // KWIN_THUMBNAILITEM_H diff --git a/kcmkwin/kwintabbox/thumbnails/dolphin.png b/kcmkwin/kwintabbox/thumbnails/dolphin.png new file mode 100644 index 0000000000000000000000000000000000000000..d79d83f40f927af94e57735864f393aec29dd29b GIT binary patch literal 26991 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

GmO^EfdyE$?(s= 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 z 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 akQ4j{6CecUKVW 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 +{ + chomp; + s/placement=NoPlacement/placement=0/; + s/placement=Default/placement=1/; + s/placement=Random/placement=3/; + s/placement=Smart/placement=4/; + s/placement=Cascade/placement=5/; + s/placement=Centered/placement=6/; + s/placement=ZeroCornered/placement=7/; + s/placement=UnderMouse/placement=8/; + s/placement=OnMainWindow/placement=9/; + s/placement=Maximizing/placement=10/; + print "$_\n"; +} diff --git a/kconf_update/kwinrules.upd b/kconf_update/kwinrules.upd new file mode 100644 index 0000000..0b30010 --- /dev/null +++ b/kconf_update/kwinrules.upd @@ -0,0 +1,7 @@ +Version=5 + +# Replace `placement` string policy to its enum value equivalent +Id=replace-placement-string-to-enum +File=kwinrulesrc +Script=kwinrules-5.19-placement.pl,perl + diff --git a/keyboard_input.cpp b/keyboard_input.cpp new file mode 100644 index 0000000..10f4729 --- /dev/null +++ b/keyboard_input.cpp @@ -0,0 +1,246 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013, 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "keyboard_input.h" +#include "input_event.h" +#include "input_event_spy.h" +#include "keyboard_layout.h" +#include "keyboard_repeat.h" +#include "abstract_client.h" +#include "modifier_only_shortcuts.h" +#include "utils.h" +#include "screenlockerwatcher.h" +#include "toplevel.h" +#include "wayland_server.h" +#include "workspace.h" +// KWayland +#include +#include +//screenlocker +#include +// Frameworks +#include +// Qt +#include + +namespace KWin +{ + +KeyboardInputRedirection::KeyboardInputRedirection(InputRedirection *parent) + : QObject(parent) + , m_input(parent) + , m_xkb(new Xkb(parent)) +{ + connect(m_xkb.data(), &Xkb::ledsChanged, this, &KeyboardInputRedirection::ledsChanged); + if (waylandServer()) { + m_xkb->setSeat(waylandServer()->seat()); + } +} + +KeyboardInputRedirection::~KeyboardInputRedirection() = default; + +class KeyStateChangedSpy : public InputEventSpy +{ +public: + KeyStateChangedSpy(InputRedirection *input) + : m_input(input) + { + } + + void keyEvent(KeyEvent *event) override + { + if (event->isAutoRepeat()) { + return; + } + emit m_input->keyStateChanged(event->nativeScanCode(), event->type() == QEvent::KeyPress ? InputRedirection::KeyboardKeyPressed : InputRedirection::KeyboardKeyReleased); + } + +private: + InputRedirection *m_input; +}; + +class ModifiersChangedSpy : public InputEventSpy +{ +public: + ModifiersChangedSpy(InputRedirection *input) + : m_input(input) + , m_modifiers() + { + } + + void keyEvent(KeyEvent *event) override + { + if (event->isAutoRepeat()) { + return; + } + updateModifiers(event->modifiers()); + } + + void updateModifiers(Qt::KeyboardModifiers mods) + { + if (mods == m_modifiers) { + return; + } + emit m_input->keyboardModifiersChanged(mods, m_modifiers); + m_modifiers = mods; + } + +private: + InputRedirection *m_input; + Qt::KeyboardModifiers m_modifiers; +}; + +void KeyboardInputRedirection::init() +{ + Q_ASSERT(!m_inited); + m_inited = true; + const auto config = kwinApp()->kxkbConfig(); + m_xkb->setNumLockConfig(InputConfig::self()->inputConfig()); + m_xkb->setConfig(config); + + m_input->installInputEventSpy(new KeyStateChangedSpy(m_input)); + m_modifiersChangedSpy = new ModifiersChangedSpy(m_input); + m_input->installInputEventSpy(m_modifiersChangedSpy); + m_keyboardLayout = new KeyboardLayout(m_xkb.data()); + m_keyboardLayout->setConfig(config); + m_keyboardLayout->init(); + m_input->installInputEventSpy(m_keyboardLayout); + + if (waylandServer()->hasGlobalShortcutSupport()) { + m_input->installInputEventSpy(new ModifierOnlyShortcuts); + } + + KeyboardRepeat *keyRepeatSpy = new KeyboardRepeat(m_xkb.data()); + connect(keyRepeatSpy, &KeyboardRepeat::keyRepeat, this, + std::bind(&KeyboardInputRedirection::processKey, this, std::placeholders::_1, InputRedirection::KeyboardKeyAutoRepeat, std::placeholders::_2, nullptr)); + m_input->installInputEventSpy(keyRepeatSpy); + + connect(workspace(), &QObject::destroyed, this, [this] { m_inited = false; }); + connect(waylandServer(), &QObject::destroyed, this, [this] { m_inited = false; }); + connect(workspace(), &Workspace::clientActivated, this, + [this] { + disconnect(m_activeClientSurfaceChangedConnection); + if (auto c = workspace()->activeClient()) { + m_activeClientSurfaceChangedConnection = connect(c, &Toplevel::surfaceChanged, this, &KeyboardInputRedirection::update); + } else { + m_activeClientSurfaceChangedConnection = QMetaObject::Connection(); + } + update(); + } + ); + if (waylandServer()->hasScreenLockerIntegration()) { + connect(ScreenLocker::KSldApp::self(), &ScreenLocker::KSldApp::lockStateChanged, this, &KeyboardInputRedirection::update); + } +} + +void KeyboardInputRedirection::update() +{ + if (!m_inited) { + return; + } + auto seat = waylandServer()->seat(); + // TODO: this needs better integration + Toplevel *found = nullptr; + if (waylandServer()->isScreenLocked()) { + const QList &stacking = Workspace::self()->stackingOrder(); + if (!stacking.isEmpty()) { + auto it = stacking.end(); + do { + --it; + Toplevel *t = (*it); + if (t->isDeleted()) { + // a deleted window doesn't get mouse events + continue; + } + if (!t->isLockScreen()) { + continue; + } + if (!t->readyForPainting()) { + continue; + } + found = t; + break; + } while (it != stacking.begin()); + } + } else if (!input()->isSelectingWindow()) { + found = workspace()->activeClient(); + } + if (found && found->surface()) { + if (found->surface() != seat->focusedKeyboardSurface()) { + seat->setFocusedKeyboardSurface(found->surface()); + } + } else { + seat->setFocusedKeyboardSurface(nullptr); + } +} + +void KeyboardInputRedirection::processKey(uint32_t key, InputRedirection::KeyboardKeyState state, uint32_t time, LibInput::Device *device) +{ + QEvent::Type type; + bool autoRepeat = false; + switch (state) { + case InputRedirection::KeyboardKeyAutoRepeat: + autoRepeat = true; + // fall through + case InputRedirection::KeyboardKeyPressed: + type = QEvent::KeyPress; + break; + case InputRedirection::KeyboardKeyReleased: + type = QEvent::KeyRelease; + break; + default: + Q_UNREACHABLE(); + } + + if (!autoRepeat) { + m_xkb->updateKey(key, state); + } + + const xkb_keysym_t keySym = m_xkb->currentKeysym(); + KeyEvent event(type, + m_xkb->toQtKey(keySym), + m_xkb->modifiers(), + key, + keySym, + m_xkb->toString(keySym), + autoRepeat, + time, + device); + event.setModifiersRelevantForGlobalShortcuts(m_xkb->modifiersRelevantForGlobalShortcuts()); + + m_input->processSpies(std::bind(&InputEventSpy::keyEvent, std::placeholders::_1, &event)); + if (!m_inited) { + return; + } + m_input->processFilters(std::bind(&InputEventFilter::keyEvent, std::placeholders::_1, &event)); + + m_xkb->forwardModifiers(); +} + +void KeyboardInputRedirection::processModifiers(uint32_t modsDepressed, uint32_t modsLatched, uint32_t modsLocked, uint32_t group) +{ + if (!m_inited) { + return; + } + // TODO: send to proper Client and also send when active Client changes + m_xkb->updateModifiers(modsDepressed, modsLatched, modsLocked, group); + m_modifiersChangedSpy->updateModifiers(modifiers()); + m_keyboardLayout->checkLayoutChange(); +} + +void KeyboardInputRedirection::processKeymapChange(int fd, uint32_t size) +{ + if (!m_inited) { + return; + } + // TODO: should we pass the keymap to our Clients? Or only to the currently active one and update + m_xkb->installKeymap(fd, size); + m_keyboardLayout->resetLayout(); +} + +} diff --git a/keyboard_input.h b/keyboard_input.h new file mode 100644 index 0000000..cbc26e9 --- /dev/null +++ b/keyboard_input.h @@ -0,0 +1,93 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013, 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_KEYBOARD_INPUT_H +#define KWIN_KEYBOARD_INPUT_H + +#include "input.h" +#include "xkb.h" + +#include +#include +#include + +#include + +class QWindow; +struct xkb_context; +struct xkb_keymap; +struct xkb_state; +struct xkb_compose_table; +struct xkb_compose_state; +typedef uint32_t xkb_mod_index_t; +typedef uint32_t xkb_led_index_t; +typedef uint32_t xkb_keysym_t; +typedef uint32_t xkb_layout_index_t; + +namespace KWin +{ + +class InputRedirection; +class KeyboardLayout; +class ModifiersChangedSpy; +class Toplevel; + +namespace LibInput +{ +class Device; +} + +class KWIN_EXPORT KeyboardInputRedirection : public QObject +{ + Q_OBJECT +public: + explicit KeyboardInputRedirection(InputRedirection *parent); + ~KeyboardInputRedirection() override; + + void init(); + + void update(); + + /** + * @internal + */ + void processKey(uint32_t key, InputRedirection::KeyboardKeyState state, uint32_t time, LibInput::Device *device = nullptr); + /** + * @internal + */ + void processModifiers(uint32_t modsDepressed, uint32_t modsLatched, uint32_t modsLocked, uint32_t group); + /** + * @internal + */ + void processKeymapChange(int fd, uint32_t size); + + Xkb *xkb() const { + return m_xkb.data(); + } + Qt::KeyboardModifiers modifiers() const { + return m_xkb->modifiers(); + } + Qt::KeyboardModifiers modifiersRelevantForGlobalShortcuts() const { + return m_xkb->modifiersRelevantForGlobalShortcuts(); + } + +Q_SIGNALS: + void ledsChanged(KWin::Xkb::LEDs); + +private: + InputRedirection *m_input; + bool m_inited = false; + QScopedPointer m_xkb; + QMetaObject::Connection m_activeClientSurfaceChangedConnection; + ModifiersChangedSpy *m_modifiersChangedSpy = nullptr; + KeyboardLayout *m_keyboardLayout = nullptr; +}; + +} + +#endif diff --git a/keyboard_layout.cpp b/keyboard_layout.cpp new file mode 100644 index 0000000..530c18b --- /dev/null +++ b/keyboard_layout.cpp @@ -0,0 +1,332 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016, 2017 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "keyboard_layout.h" +#include "keyboard_layout_switching.h" +#include "keyboard_input.h" +#include "input_event.h" +#include "main.h" +#include "platform.h" +#include "utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace KWin +{ + +KeyboardLayout::KeyboardLayout(Xkb *xkb) + : QObject() + , m_xkb(xkb) + , m_notifierItem(nullptr) +{ +} + +KeyboardLayout::~KeyboardLayout() = default; + +static QString translatedLayout(const QString &layout) +{ + return i18nd("xkeyboard-config", layout.toUtf8().constData()); +} + +void KeyboardLayout::init() +{ + QAction *switchKeyboardAction = new QAction(this); + switchKeyboardAction->setObjectName(QStringLiteral("Switch to Next Keyboard Layout")); + switchKeyboardAction->setProperty("componentName", QStringLiteral("KDE Keyboard Layout Switcher")); + const QKeySequence sequence = QKeySequence(Qt::ALT+Qt::CTRL+Qt::Key_K); + KGlobalAccel::self()->setDefaultShortcut(switchKeyboardAction, QList({sequence})); + KGlobalAccel::self()->setShortcut(switchKeyboardAction, QList({sequence})); + kwinApp()->platform()->setupActionForGlobalAccel(switchKeyboardAction); + connect(switchKeyboardAction, &QAction::triggered, this, &KeyboardLayout::switchToNextLayout); + + QDBusConnection::sessionBus().connect(QString(), + QStringLiteral("/Layouts"), + QStringLiteral("org.kde.keyboard"), + QStringLiteral("reloadConfig"), + this, + SLOT(reconfigure())); + + reconfigure(); +} + +void KeyboardLayout::initDBusInterface() +{ + if (m_xkb->numberOfLayouts() <= 1) { + if (m_dbusInterface) { + m_dbusInterface->deleteLater(); + m_dbusInterface = nullptr; + } + return; + } + if (m_dbusInterface) { + return; + } + m_dbusInterface = new KeyboardLayoutDBusInterface(m_xkb, this); + connect(this, &KeyboardLayout::layoutChanged, m_dbusInterface, + [this] { + emit m_dbusInterface->currentLayoutChanged(m_xkb->layoutName()); + } + ); + // TODO: the signal might be emitted even if the list didn't change + connect(this, &KeyboardLayout::layoutsReconfigured, m_dbusInterface, &KeyboardLayoutDBusInterface::layoutListChanged); +} + +void KeyboardLayout::initNotifierItem() +{ + bool showNotifier = true; + bool showSingle = false; + if (m_config) { + const auto config = m_config->group(QStringLiteral("Layout")); + showNotifier = config.readEntry("ShowLayoutIndicator", true); + showSingle = config.readEntry("ShowSingle", false); + } + const bool shouldShow = showNotifier && (showSingle || m_xkb->numberOfLayouts() > 1); + if (shouldShow) { + if (m_notifierItem) { + return; + } + } else { + delete m_notifierItem; + m_notifierItem = nullptr; + return; + } + + m_notifierItem = new KStatusNotifierItem(this); + m_notifierItem->setCategory(KStatusNotifierItem::Hardware); + m_notifierItem->setStatus(KStatusNotifierItem::Passive); + m_notifierItem->setToolTipTitle(i18nc("tooltip title", "Keyboard Layout")); + m_notifierItem->setTitle(i18nc("tooltip title", "Keyboard Layout")); + m_notifierItem->setToolTipIconByName(QStringLiteral("input-keyboard")); + m_notifierItem->setStandardActionsEnabled(false); + + // TODO: proper icon + m_notifierItem->setIconByName(QStringLiteral("input-keyboard")); + + connect(m_notifierItem, &KStatusNotifierItem::activateRequested, this, &KeyboardLayout::switchToNextLayout); + connect(m_notifierItem, &KStatusNotifierItem::scrollRequested, this, + [this] (int delta, Qt::Orientation orientation) { + if (orientation == Qt::Horizontal) { + return; + } + if (delta > 0) { + switchToNextLayout(); + } else { + switchToPreviousLayout(); + } + } + ); +} + +void KeyboardLayout::switchToNextLayout() +{ + m_xkb->switchToNextLayout(); + checkLayoutChange(); +} + +void KeyboardLayout::switchToPreviousLayout() +{ + m_xkb->switchToPreviousLayout(); + checkLayoutChange(); +} + +void KeyboardLayout::switchToLayout(xkb_layout_index_t index) +{ + m_xkb->switchToLayout(index); + checkLayoutChange(); +} + +void KeyboardLayout::reconfigure() +{ + if (m_config) { + m_config->reparseConfiguration(); + const KConfigGroup layoutGroup = m_config->group("Layout"); + const QString policyKey = layoutGroup.readEntry("SwitchMode", QStringLiteral("Global")); + m_xkb->reconfigure(); + if (!m_policy || m_policy->name() != policyKey) { + delete m_policy; + m_policy = KeyboardLayoutSwitching::Policy::create(m_xkb, this, layoutGroup, policyKey); + } + } else { + m_xkb->reconfigure(); + } + resetLayout(); +} + +void KeyboardLayout::resetLayout() +{ + m_layout = m_xkb->currentLayout(); + initNotifierItem(); + updateNotifier(); + reinitNotifierMenu(); + loadShortcuts(); + + initDBusInterface(); + emit layoutsReconfigured(); +} + +void KeyboardLayout::loadShortcuts() +{ + qDeleteAll(m_layoutShortcuts); + m_layoutShortcuts.clear(); + const auto layouts = m_xkb->layoutNames(); + const QString componentName = QStringLiteral("KDE Keyboard Layout Switcher"); + for (auto it = layouts.begin(); it != layouts.end(); it++) { + // layout name is translated in the action name in keyboard kcm! + const QString action = QStringLiteral("Switch keyboard layout to %1").arg(translatedLayout(it.value())); + const auto shortcuts = KGlobalAccel::self()->globalShortcut(componentName, action); + if (shortcuts.isEmpty()) { + continue; + } + QAction *a = new QAction(this); + a->setObjectName(action); + a->setProperty("componentName", componentName); + connect(a, &QAction::triggered, this, + std::bind(&KeyboardLayout::switchToLayout, this, it.key())); + KGlobalAccel::self()->setShortcut(a, shortcuts, KGlobalAccel::Autoloading); + m_layoutShortcuts << a; + } +} + +void KeyboardLayout::keyEvent(KeyEvent *event) +{ + if (!event->isAutoRepeat()) { + checkLayoutChange(); + } +} + +void KeyboardLayout::checkLayoutChange() +{ + const auto layout = m_xkb->currentLayout(); + if (m_layout == layout) { + return; + } + m_layout = layout; + notifyLayoutChange(); + updateNotifier(); + emit layoutChanged(); +} + +void KeyboardLayout::notifyLayoutChange() +{ + // notify OSD service about the new layout + QDBusMessage msg = QDBusMessage::createMethodCall( + QStringLiteral("org.kde.plasmashell"), + QStringLiteral("/org/kde/osdService"), + QStringLiteral("org.kde.osdService"), + QStringLiteral("kbdLayoutChanged")); + + msg << translatedLayout(m_xkb->layoutName()); + + QDBusConnection::sessionBus().asyncCall(msg); +} + +void KeyboardLayout::updateNotifier() +{ + if (!m_notifierItem) { + return; + } + m_notifierItem->setToolTipSubTitle(translatedLayout(m_xkb->layoutName())); + // TODO: update icon +} + +void KeyboardLayout::reinitNotifierMenu() +{ + if (!m_notifierItem) { + return; + } + const auto layouts = m_xkb->layoutNames(); + + QMenu *menu = new QMenu; + for (auto it = layouts.begin(); it != layouts.end(); it++) { + menu->addAction(translatedLayout(it.value()), std::bind(&KeyboardLayout::switchToLayout, this, it.key())); + } + + menu->addSeparator(); + menu->addAction(QIcon::fromTheme(QStringLiteral("configure")), i18n("Configure Layouts..."), this, + [this] { + // TODO: introduce helper function to start kcmshell5 + QProcess *p = new Process(this); + p->setArguments(QStringList{QStringLiteral("--args=--tab=layouts"), QStringLiteral("kcm_keyboard")}); + p->setProcessEnvironment(kwinApp()->processStartupEnvironment()); + p->setProgram(QStringLiteral("kcmshell5")); + connect(p, static_cast(&QProcess::finished), p, &QProcess::deleteLater); + connect(p, &QProcess::errorOccurred, this, [](QProcess::ProcessError e) { + if (e == QProcess::FailedToStart) { + qCDebug(KWIN_CORE) << "Failed to start kcmshell5"; + } + }); + p->start(); + } + ); + + m_notifierItem->setContextMenu(menu); +} + +static const QString s_keyboardService = QStringLiteral("org.kde.keyboard"); +static const QString s_keyboardObject = QStringLiteral("/Layouts"); + +KeyboardLayoutDBusInterface::KeyboardLayoutDBusInterface(Xkb *xkb, KeyboardLayout *parent) + : QObject(parent) + , m_xkb(xkb) + , m_keyboardLayout(parent) +{ + QDBusConnection::sessionBus().registerService(s_keyboardService); + QDBusConnection::sessionBus().registerObject(s_keyboardObject, this, QDBusConnection::ExportAllSlots | QDBusConnection::ExportAllSignals); +} + +KeyboardLayoutDBusInterface::~KeyboardLayoutDBusInterface() +{ + QDBusConnection::sessionBus().unregisterService(s_keyboardService); +} + +bool KeyboardLayoutDBusInterface::setLayout(const QString &layout) +{ + const auto layouts = m_xkb->layoutNames(); + auto it = layouts.begin(); + for (; it !=layouts.end(); it++) { + if (it.value() == layout) { + break; + } + } + if (it == layouts.end()) { + return false; + } + m_xkb->switchToLayout(it.key()); + m_keyboardLayout->checkLayoutChange(); + return true; +} + +QString KeyboardLayoutDBusInterface::getCurrentLayout() +{ + return m_xkb->layoutName(); +} + +QStringList KeyboardLayoutDBusInterface::getLayoutsList() +{ + const auto layouts = m_xkb->layoutNames(); + QStringList ret; + for (auto it = layouts.begin(); it != layouts.end(); it++) { + ret << it.value(); + } + return ret; +} + +QString KeyboardLayoutDBusInterface::getLayoutDisplayName(const QString &layout) +{ + return translatedLayout(layout); +} + +} diff --git a/keyboard_layout.h b/keyboard_layout.h new file mode 100644 index 0000000..f44cc92 --- /dev/null +++ b/keyboard_layout.h @@ -0,0 +1,102 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016, 2017 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_KEYBOARD_LAYOUT_H +#define KWIN_KEYBOARD_LAYOUT_H + +#include "input_event_spy.h" +#include +#include + +#include +typedef uint32_t xkb_layout_index_t; + +class KStatusNotifierItem; +class QAction; + +namespace KWin +{ +class Xkb; +class KeyboardLayoutDBusInterface; + +namespace KeyboardLayoutSwitching +{ +class Policy; +} + +class KeyboardLayout : public QObject, public InputEventSpy +{ + Q_OBJECT +public: + explicit KeyboardLayout(Xkb *xkb); + ~KeyboardLayout() override; + + void setConfig(KSharedConfigPtr config) { + m_config = config; + } + + void init(); + + void checkLayoutChange(); + void resetLayout(); + void updateNotifier(); + + void keyEvent(KeyEvent *event) override; + +Q_SIGNALS: + void layoutChanged(); + void layoutsReconfigured(); + +private Q_SLOTS: + void reconfigure(); + +private: + void initDBusInterface(); + void notifyLayoutChange(); + void initNotifierItem(); + void switchToNextLayout(); + void switchToPreviousLayout(); + void switchToLayout(xkb_layout_index_t index); + void reinitNotifierMenu(); + void loadShortcuts(); + Xkb *m_xkb; + xkb_layout_index_t m_layout = 0; + KStatusNotifierItem *m_notifierItem; + KSharedConfigPtr m_config; + QVector m_layoutShortcuts; + KeyboardLayoutDBusInterface *m_dbusInterface = nullptr; + KeyboardLayoutSwitching::Policy *m_policy = nullptr; +}; + +class KeyboardLayoutDBusInterface : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.KeyboardLayouts") + +public: + explicit KeyboardLayoutDBusInterface(Xkb *xkb, KeyboardLayout *parent); + ~KeyboardLayoutDBusInterface() override; + +public Q_SLOTS: + bool setLayout(const QString &layout); + QString getCurrentLayout(); + QStringList getLayoutsList(); + QString getLayoutDisplayName(const QString &layout); + +Q_SIGNALS: + void currentLayoutChanged(QString layout); + void layoutListChanged(); + +private: + Xkb *m_xkb; + KeyboardLayout *m_keyboardLayout; +}; + +} + +#endif diff --git a/keyboard_layout_switching.cpp b/keyboard_layout_switching.cpp new file mode 100644 index 0000000..49bbc72 --- /dev/null +++ b/keyboard_layout_switching.cpp @@ -0,0 +1,371 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "keyboard_layout_switching.h" +#include "keyboard_layout.h" +#include "abstract_client.h" +#include "deleted.h" +#include "virtualdesktops.h" +#include "workspace.h" +#include "xkb.h" + +namespace KWin +{ + +namespace KeyboardLayoutSwitching +{ + +Policy::Policy(Xkb *xkb, KeyboardLayout *layout, const KConfigGroup &config) + : QObject(layout) + , m_config(config) + , m_xkb(xkb) + , m_layout(layout) +{ + connect(m_layout, &KeyboardLayout::layoutsReconfigured, this, &Policy::clearCache); + connect(m_layout, &KeyboardLayout::layoutChanged, this, &Policy::layoutChanged); +} + +Policy::~Policy() = default; + +void Policy::setLayout(quint32 layout) +{ + const quint32 previousLayout = m_xkb->currentLayout(); + m_xkb->switchToLayout(layout); + if (previousLayout != m_xkb->currentLayout()) { + m_layout->updateNotifier(); + } +} + +quint32 Policy::layout() const +{ + return m_xkb->currentLayout(); +} + +Policy *Policy::create(Xkb *xkb, KeyboardLayout *layout, const KConfigGroup &config, const QString &policy) +{ + if (policy.toLower() == QStringLiteral("desktop")) { + return new VirtualDesktopPolicy(xkb, layout, config); + } + if (policy.toLower() == QStringLiteral("window")) { + return new WindowPolicy(xkb, layout); + } + if (policy.toLower() == QStringLiteral("winclass")) { + return new ApplicationPolicy(xkb, layout, config); + } + return new GlobalPolicy(xkb, layout, config); +} + +const char Policy::defaultLayoutEntryKeyPrefix[] = "LayoutDefault"; +const QString Policy::defaultLayoutEntryKey() const +{ + return QLatin1String(defaultLayoutEntryKeyPrefix) % name() % QLatin1Char('_'); +} + +void Policy::clearLayouts() +{ + const QStringList layoutEntryList = m_config.keyList().filter(defaultLayoutEntryKeyPrefix); + for (const auto &layoutEntry : layoutEntryList) { + m_config.deleteEntry(layoutEntry); + } +} + +const QString GlobalPolicy::defaultLayoutEntryKey() const +{ + return QLatin1String(defaultLayoutEntryKeyPrefix) % name(); +} + +GlobalPolicy::GlobalPolicy(Xkb *xkb, KeyboardLayout *_layout, const KConfigGroup &config) + : Policy(xkb, _layout, config) +{ + connect(workspace()->sessionManager(), &SessionManager::prepareSessionSaveRequested, this, + [this] (const QString &name) { + Q_UNUSED(name) + clearLayouts(); + if (layout()) { + m_config.writeEntry(defaultLayoutEntryKey(), layout()); + } + } + ); + + connect(workspace()->sessionManager(), &SessionManager::loadSessionRequested, this, + [this, xkb] (const QString &name) { + Q_UNUSED(name) + if (xkb->numberOfLayouts() > 1) { + setLayout(m_config.readEntry(defaultLayoutEntryKey(), 0)); + } + } + ); +} + +GlobalPolicy::~GlobalPolicy() = default; + +VirtualDesktopPolicy::VirtualDesktopPolicy(Xkb *xkb, KeyboardLayout *layout, const KConfigGroup &config) + : Policy(xkb, layout, config) +{ + connect(VirtualDesktopManager::self(), &VirtualDesktopManager::currentChanged, + this, &VirtualDesktopPolicy::desktopChanged); + + connect(workspace()->sessionManager(), &SessionManager::prepareSessionSaveRequested, this, + [this] (const QString &name) { + Q_UNUSED(name) + clearLayouts(); + + for (auto i = m_layouts.constBegin(); i != m_layouts.constEnd(); ++i) { + if (const uint layout = *i) { + m_config.writeEntry( + defaultLayoutEntryKey() % + QLatin1String( QByteArray::number(i.key()->x11DesktopNumber()) ), + layout); + } + } + } + ); + + connect(workspace()->sessionManager(), &SessionManager::loadSessionRequested, this, + [this, xkb] (const QString &name) { + Q_UNUSED(name) + if (xkb->numberOfLayouts() > 1) { + for (KWin::VirtualDesktop* const desktop : VirtualDesktopManager::self()->desktops()) { + const uint layout = m_config.readEntry( + defaultLayoutEntryKey() % + QLatin1String( QByteArray::number(desktop->x11DesktopNumber()) ), + 0u); + if (layout) { + m_layouts.insert(desktop, layout); + connect(desktop, &VirtualDesktop::aboutToBeDestroyed, this, + [this, desktop] { + m_layouts.remove(desktop); + } + ); + } + } + desktopChanged(); + } + } + ); +} + +VirtualDesktopPolicy::~VirtualDesktopPolicy() = default; + +void VirtualDesktopPolicy::clearCache() +{ + m_layouts.clear(); +} + +namespace { +template +quint32 getLayout(const T &layouts, const U &reference) +{ + auto it = layouts.constFind(reference); + if (it == layouts.constEnd()) { + return 0; + } else { + return it.value(); + } +} +} + +void VirtualDesktopPolicy::desktopChanged() +{ + auto d = VirtualDesktopManager::self()->currentDesktop(); + if (!d) { + return; + } + setLayout(getLayout(m_layouts, d)); +} + +void VirtualDesktopPolicy::layoutChanged() +{ + auto d = VirtualDesktopManager::self()->currentDesktop(); + if (!d) { + return; + } + auto it = m_layouts.find(d); + const auto l = layout(); + if (it == m_layouts.end()) { + m_layouts.insert(d, l); + connect(d, &VirtualDesktop::aboutToBeDestroyed, this, + [this, d] { + m_layouts.remove(d); + } + ); + } else { + if (it.value() == l) { + return; + } + it.value() = l; + } +} + +WindowPolicy::WindowPolicy(KWin::Xkb* xkb, KWin::KeyboardLayout* layout) + : Policy(xkb, layout) +{ + connect(workspace(), &Workspace::clientActivated, this, + [this] (AbstractClient *c) { + if (!c) { + return; + } + // ignore some special types + if (c->isDesktop() || c->isDock()) { + return; + } + setLayout(getLayout(m_layouts, c)); + } + ); +} + +WindowPolicy::~WindowPolicy() +{ +} + +void WindowPolicy::clearCache() +{ + m_layouts.clear(); +} + +void WindowPolicy::layoutChanged() +{ + auto c = workspace()->activeClient(); + if (!c) { + return; + } + // ignore some special types + if (c->isDesktop() || c->isDock()) { + return; + } + + auto it = m_layouts.find(c); + const auto l = layout(); + if (it == m_layouts.end()) { + m_layouts.insert(c, l); + connect(c, &AbstractClient::windowClosed, this, + [this, c] { + m_layouts.remove(c); + } + ); + } else { + if (it.value() == l) { + return; + } + it.value() = l; + } +} + +ApplicationPolicy::ApplicationPolicy(KWin::Xkb* xkb, KWin::KeyboardLayout* layout, const KConfigGroup &config) + : Policy(xkb, layout, config) +{ + connect(workspace(), &Workspace::clientActivated, this, &ApplicationPolicy::clientActivated); + + connect(workspace()->sessionManager(), &SessionManager::prepareSessionSaveRequested, this, + [this] (const QString &name) { + Q_UNUSED(name) + clearLayouts(); + + for (auto i = m_layouts.constBegin(); i != m_layouts.constEnd(); ++i) { + if (const uint layout = *i) { + const QByteArray desktopFileName = i.key()->desktopFileName(); + if (!desktopFileName.isEmpty()) { + m_config.writeEntry( + defaultLayoutEntryKey() % QLatin1String(desktopFileName), + layout); + } + } + } + } + ); + + connect(workspace()->sessionManager(), &SessionManager::loadSessionRequested, this, + [this, xkb] (const QString &name) { + Q_UNUSED(name) + if (xkb->numberOfLayouts() > 1) { + const QString keyPrefix = defaultLayoutEntryKey(); + const QStringList keyList = m_config.keyList().filter(keyPrefix); + for (const QString& key : keyList) { + m_layoutsRestored.insert( + key.midRef(keyPrefix.size()).toLatin1(), + m_config.readEntry(key, 0)); + } + } + m_layoutsRestored.squeeze(); + } + ); +} + +ApplicationPolicy::~ApplicationPolicy() +{ +} + +void ApplicationPolicy::clientActivated(AbstractClient *c) +{ + if (!c) { + return; + } + // ignore some special types + if (c->isDesktop() || c->isDock()) { + return; + } + auto it = m_layouts.constFind(c); + if(it != m_layouts.constEnd()) { + setLayout(it.value()); + return; + }; + for (it = m_layouts.constBegin(); it != m_layouts.constEnd(); it++) { + if (AbstractClient::belongToSameApplication(c, it.key())) { + setLayout(it.value()); + layoutChanged(); + return; + } + } + setLayout( m_layoutsRestored.take(c->desktopFileName()) ); + if (layout()) { + layoutChanged(); + } +} + +void ApplicationPolicy::clearCache() +{ + m_layouts.clear(); +} + +void ApplicationPolicy::layoutChanged() +{ + auto c = workspace()->activeClient(); + if (!c) { + return; + } + // ignore some special types + if (c->isDesktop() || c->isDock()) { + return; + } + + auto it = m_layouts.find(c); + const auto l = layout(); + if (it == m_layouts.end()) { + m_layouts.insert(c, l); + connect(c, &AbstractClient::windowClosed, this, + [this, c] { + m_layouts.remove(c); + } + ); + } else { + if (it.value() == l) { + return; + } + it.value() = l; + } + // update all layouts for the application + for (it = m_layouts.begin(); it != m_layouts.end(); it++) { + if (!AbstractClient::belongToSameApplication(it.key(), c)) { + continue; + } + it.value() = l; + } +} + +} +} diff --git a/keyboard_layout_switching.h b/keyboard_layout_switching.h new file mode 100644 index 0000000..dc243fc --- /dev/null +++ b/keyboard_layout_switching.h @@ -0,0 +1,138 @@ +/* + 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 +*/ +#ifndef KWIN_KEYBOARD_LAYOUT_SWITCHING_H +#define KWIN_KEYBOARD_LAYOUT_SWITCHING_H + +#include +#include +#include + +namespace KWin +{ + +class AbstractClient; +class KeyboardLayout; +class Xkb; +class VirtualDesktop; + +namespace KeyboardLayoutSwitching +{ + +class Policy : public QObject +{ + Q_OBJECT +public: + ~Policy() override; + + virtual QString name() const = 0; + + static Policy *create(Xkb *xkb, KeyboardLayout *layout, const KConfigGroup &config, const QString &policy); + +protected: + explicit Policy(Xkb *xkb, KeyboardLayout *layout, const KConfigGroup &config = KConfigGroup()); + virtual void clearCache() = 0; + virtual void layoutChanged() = 0; + + void setLayout(quint32 layout); + quint32 layout() const; + + KConfigGroup m_config; + virtual const QString defaultLayoutEntryKey() const; + void clearLayouts(); + + static const char defaultLayoutEntryKeyPrefix[]; + +private: + Xkb *m_xkb; + KeyboardLayout *m_layout; +}; + +class GlobalPolicy : public Policy +{ + Q_OBJECT +public: + explicit GlobalPolicy(Xkb *xkb, KeyboardLayout *layout, const KConfigGroup &config); + ~GlobalPolicy() override; + + QString name() const override { + return QStringLiteral("Global"); + } + +protected: + void clearCache() override {} + void layoutChanged() override {} + +private: + const QString defaultLayoutEntryKey() const override; +}; + +class VirtualDesktopPolicy : public Policy +{ + Q_OBJECT +public: + explicit VirtualDesktopPolicy(Xkb *xkb, KeyboardLayout *layout, const KConfigGroup &config); + ~VirtualDesktopPolicy() override; + + QString name() const override { + return QStringLiteral("Desktop"); + } + +protected: + void clearCache() override; + void layoutChanged() override; + +private: + void desktopChanged(); + QHash m_layouts; +}; + +class WindowPolicy : public Policy +{ + Q_OBJECT +public: + explicit WindowPolicy(Xkb *xkb, KeyboardLayout *layout); + ~WindowPolicy() override; + + QString name() const override { + return QStringLiteral("Window"); + } + +protected: + void clearCache() override; + void layoutChanged() override; + +private: + QHash m_layouts; +}; + +class ApplicationPolicy : public Policy +{ + Q_OBJECT +public: + explicit ApplicationPolicy(Xkb *xkb, KeyboardLayout *layout, const KConfigGroup &config); + ~ApplicationPolicy() override; + + QString name() const override { + return QStringLiteral("WinClass"); + } + +protected: + void clearCache() override; + void layoutChanged() override; + +private: + void clientActivated(AbstractClient *c); + QHash m_layouts; + QHash m_layoutsRestored; +}; + +} +} + +#endif diff --git a/keyboard_repeat.cpp b/keyboard_repeat.cpp new file mode 100644 index 0000000..b620882 --- /dev/null +++ b/keyboard_repeat.cpp @@ -0,0 +1,62 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016, 2017 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "keyboard_repeat.h" +#include "keyboard_input.h" +#include "input_event.h" +#include "wayland_server.h" + +#include + +#include + +namespace KWin +{ + +KeyboardRepeat::KeyboardRepeat(Xkb *xkb) + : QObject() + , m_timer(new QTimer(this)) + , m_xkb(xkb) +{ + connect(m_timer, &QTimer::timeout, this, &KeyboardRepeat::handleKeyRepeat); +} + +KeyboardRepeat::~KeyboardRepeat() = default; + +void KeyboardRepeat::handleKeyRepeat() +{ + // TODO: don't depend on WaylandServer + if (waylandServer()->seat()->keyRepeatRate() != 0) { + m_timer->setInterval(1000 / waylandServer()->seat()->keyRepeatRate()); + } + // TODO: better time + emit keyRepeat(m_key, m_time); +} + +void KeyboardRepeat::keyEvent(KeyEvent *event) +{ + if (event->isAutoRepeat()) { + return; + } + const quint32 key = event->nativeScanCode(); + if (event->type() == QEvent::KeyPress) { + // TODO: don't get these values from WaylandServer + if (m_xkb->shouldKeyRepeat(key) && waylandServer()->seat()->keyRepeatDelay() != 0) { + m_timer->setInterval(waylandServer()->seat()->keyRepeatDelay()); + m_key = key; + m_time = event->timestamp(); + m_timer->start(); + } + } else if (event->type() == QEvent::KeyRelease) { + if (key == m_key) { + m_timer->stop(); + } + } +} + +} diff --git a/keyboard_repeat.h b/keyboard_repeat.h new file mode 100644 index 0000000..faad0de --- /dev/null +++ b/keyboard_repeat.h @@ -0,0 +1,45 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016, 2017 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_KEYBOARD_REPEAT +#define KWIN_KEYBOARD_REPEAT + +#include "input_event_spy.h" + +#include + +class QTimer; + +namespace KWin +{ +class Xkb; + +class KeyboardRepeat : public QObject, public InputEventSpy +{ + Q_OBJECT +public: + explicit KeyboardRepeat(Xkb *xkb); + ~KeyboardRepeat() override; + + void keyEvent(KeyEvent *event) override; + +Q_SIGNALS: + void keyRepeat(quint32 key, quint32 time); + +private: + void handleKeyRepeat(); + QTimer *m_timer; + Xkb *m_xkb; + quint32 m_time; + quint32 m_key = 0; +}; + + +} + +#endif diff --git a/killwindow.cpp b/killwindow.cpp new file mode 100644 index 0000000..3217e56 --- /dev/null +++ b/killwindow.cpp @@ -0,0 +1,50 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "killwindow.h" +#include "abstract_client.h" +#include "main.h" +#include "platform.h" +#include "osd.h" +#include "unmanaged.h" + +#include + +namespace KWin +{ + +KillWindow::KillWindow() +{ +} + +KillWindow::~KillWindow() +{ +} + +void KillWindow::start() +{ + OSD::show(i18n("Select window to force close with left click or enter.\nEscape or right click to cancel."), + QStringLiteral("window-close")); + kwinApp()->platform()->startInteractiveWindowSelection( + [] (KWin::Toplevel *t) { + OSD::hide(); + if (!t) { + return; + } + if (AbstractClient *c = qobject_cast(t)) { + c->killWindow(); + } else if (Unmanaged *u = qobject_cast(t)) { + xcb_kill_client(connection(), u->window()); + } + }, QByteArrayLiteral("pirate") + ); +} + +} // namespace diff --git a/killwindow.h b/killwindow.h new file mode 100644 index 0000000..7da1a94 --- /dev/null +++ b/killwindow.h @@ -0,0 +1,30 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_KILLWINDOW_H +#define KWIN_KILLWINDOW_H + +namespace KWin +{ + +class KillWindow +{ +public: + + KillWindow(); + ~KillWindow(); + + void start(); +}; + +} // namespace + +#endif diff --git a/kwin.kcfg b/kwin.kcfg new file mode 100644 index 0000000..0b59606 --- /dev/null +++ b/kwin.kcfg @@ -0,0 +1,317 @@ + + + + + + Nothing + + + Meta + + + Nothing + + + Raise + + + Nothing + + + Operations menu + + + Activate and raise + + + Nothing + + + Operations menu + + + Activate, raise and pass click + + + Activate and pass click + + + Activate and pass click + + + Scroll + + + Move + + + Toggle raise and lower + + + Resize + + + + + None + + + None + + + None + + + None + + + None + + + None + + + None + + + None + + + + + false + + + false + + + + + + + + + Options::ClickToFocus + + + false + + + false + + + focusPolicy() != Options::ClickToFocus + + + true + + + 1 + 0 + 4 + + + + + + + + + + + + + + + + Placement::Smart + + + false + + + 750 + + + 300 + + + false + + + 250 + + + true + + + 10 + + + 10 + + + 0 + + + false + + + 0 + + + 150 + + + 350 + + + 1 + + + true + + + true + + + 0.25 + 0.0 + 1.0 + + + Maximize + + + Maximize + + + Maximize (vertical only) + + + Maximize (horizontal only) + + + 5000 + + + true + + + false + + + false + + + true + + + false + + + + + 60 + + + 0 + + + 6144 + + + OpenGL + + + true + + + 2 + -1 + 2 + + + true + + + false + + + false + + + 5 + 4 + 6 + + + glx + + + true + + + + + true + + + 90 + + + 1 + + + 1 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + true + + + true + + + true + + + thumbnails + + + + + 1 + 0 + + + + + + + + + XwaylandCrashPolicy::Restart + + + 3 + + + diff --git a/kwin.notifyrc b/kwin.notifyrc new file mode 100644 index 0000000..07421fd --- /dev/null +++ b/kwin.notifyrc @@ -0,0 +1,339 @@ +[Global] +IconName=kwin +Comment=KWin Window Manager +Comment[ar]=مدير النوافذ كوين +Comment[az]=KWin pəncərə Meneceri +Comment[bg]=Мениджър на прозорци KWin +Comment[bs]=Menadžer prozora K‑vin +Comment[ca]=Gestor de finestres KWin +Comment[ca@valencia]=Gestor de finestres KWin +Comment[cs]=Správce oken KWin +Comment[da]=KWin vindueshÃ¥ndtering +Comment[de]=KWin-Fensterverwaltung +Comment[el]=Διαχειριστής παραθύρων Kwin +Comment[en_GB]=KWin Window Manager +Comment[es]=Gestor de ventanas KWin +Comment[et]=Kwini aknahaldur +Comment[eu]=KWin leiho-kudeatzailea +Comment[fi]=KWin-ikkunointiohjelma +Comment[fr]=Gestionnaire de fenêtres KWin +Comment[ga]=Bainisteoir Fuinneog KWin +Comment[gl]=Xestor de xanelas KWin +Comment[gu]=KWin વિન્ડો સંચાલક +Comment[he]=מנהל החלונות KWin +Comment[hi]=केविन विंडो प्रबंधक +Comment[hr]=Upravitelj prozora KWin +Comment[hu]=KWin ablakkezelő +Comment[ia]=Gerente de fenestra KWin +Comment[id]=Pengelola Window KWin +Comment[is]=KWin gluggastjóri +Comment[it]=Gestore delle finestre KWin +Comment[ja]=KWin ウィンドウマネージャ +Comment[kk]=KWin терезе менеджері +Comment[km]=កម្មវិធី​គ្រប់គ្រង​បង្អួច KWin +Comment[kn]=ಕೆವಿನ್(KWin) ವಿಂಡೋ ವ್ಯವಸ್ಥಾಪಕ +Comment[ko]=KWin ì°½ 관리자 +Comment[lt]=KWin langų tvarkytuvė +Comment[lv]=KWin logu pārvaldnieks +Comment[mr]=के-विन चौकट व्यवस्थापक +Comment[nb]=KWin vindusbehandler +Comment[nds]=KWin-Finsterpleger +Comment[nl]=KWin vensterbeheerder +Comment[nn]=KWin vindaugshandsamar +Comment[pa]=KWin ਵਿੰਡੋ ਮੈਨੇਜਰ +Comment[pl]=Zarządzanie oknami KWin +Comment[pt]=Gestor de Janelas KWin +Comment[pt_BR]=Gerenciador de janelas KWin +Comment[ro]=Gestionar de ferestre KWin +Comment[ru]=Диспетчер окон KWin +Comment[si]=KWin කවුළු කළමනාකරු +Comment[sk]=Správca okien KWin +Comment[sl]=Upravljalnik oken KWin +Comment[sr]=Менаџер прозора К‑вин +Comment[sr@ijekavian]=Менаџер прозора К‑вин +Comment[sr@ijekavianlatin]=Menadžer prozora KWin +Comment[sr@latin]=Menadžer prozora KWin +Comment[sv]=Kwin fönsterhanterare +Comment[th]=ตัวจัดการหน้าต่าง KWin +Comment[tr]=KWin Pencere Yöneticisi +Comment[ug]=KWin كۆزنەك باشقۇرغۇچ +Comment[uk]=Керування вікнами KWin +Comment[vi]=Trình quản lí cá»­a sổ KWin +Comment[wa]=Manaedjeu des fniesses KWin +Comment[x-test]=xxKWin Window Managerxx +Comment[zh_CN]=KWin 窗口管理器 +Comment[zh_TW]=KWin 視窗管理員 + +[Event/compositingsuspendeddbus] +Name=Compositing has been suspended +Name[ar]=عُلِّق التركيب +Name[az]=Effektlər dayandırıldı +Name[bg]=Ефектите са временно спрени +Name[bs]=Slaganje je suspendovano +Name[ca]=S'ha suspès la composició +Name[ca@valencia]=S'ha suspès la composició +Name[cs]=Kompozice byla pozastavena +Name[da]=Compositing er blevet suspenderet +Name[de]=Compositing ist ausgesetzt worden +Name[el]=Η σύνθεση εικόνας τέθηκε σε αναστολή +Name[en_GB]=Compositing has been suspended +Name[eo]=Kunmetado prokrastiĝis +Name[es]=Se ha suspendido la composición +Name[et]=Komposiit on peatatud +Name[eu]=Konposizioa eten egin da +Name[fi]=Koostaminen on keskeytetty +Name[fr]=La composition a été suspendue +Name[fy]=Kompositing is ûnderbrútsen +Name[ga]=Cuireadh comhshuí ar fionraí +Name[gl]=Suspendeuse a composición +Name[gu]=કોમ્પોઝિટીંગ બંધ કરવામાં આવ્યું છે +Name[he]=השזירה הושהתה +Name[hr]=MijeÅ¡anje je pauzirano +Name[hu]=A kompozit mód felfüggesztve +Name[ia]=Composition ha essite suspendite +Name[id]=Pengomposisian telah disuspensi +Name[is]=Gerð skjásamsetningar hefur verið hætt í bili +Name[it]=La composizione è stata sospesa +Name[ja]=コンポジティングが一時停止されました +Name[kk]=Құрастыру аялдатылды +Name[km]=ការ​តែង​ត្រូវ​បានផ្អាក +Name[kn]=ಮಿಶ್ರಗೊಳಿಕೆಯನ್ನು ತಡೆಹಿಡಿಯಲಾಗಿದೆ +Name[ko]=컴포지팅 중지됨 +Name[lt]=Komponavimas pristabdytas +Name[lv]=Kompozitēšana ir apturēta +Name[mai]=कंपोजिटिंग निलंबित कएल गेल अछि +Name[ml]=കോമ്പോസിറ്റിങ്ങ് താല്‍കാലികമായി നിര്‍ത്തിയിരിക്കുന്നു +Name[mr]=कंपोझिटींग अकार्यक्षम करण्यात आले आहे +Name[nb]=Sammensetting er blitt suspendert +Name[nds]=Dat Tosamensetten wöör utmaakt +Name[nl]=Compositing is uitgesteld +Name[nn]=Samansetjinga er stoppa +Name[pa]=ਕੰਪੋਜੀਟ ਕਰਨ ਨੂੰ ਨੂੰ ਸਸਪੈਂਡ ਕੀਤਾ ਗਿਆ +Name[pl]=Wstrzymano kompozycje +Name[pt]=A composição foi suspensa +Name[pt_BR]=A composição foi suspensa +Name[ro]=Compoziționarea a fost suspendată +Name[ru]=Графические эффекты были отключены +Name[si]=රචනය අත්හිටුවිය +Name[sk]=Kompozícia bola pozastavená +Name[sl]=Skladnja 3D je bila prestavljena v pripravljenost +Name[sr]=Слагање је суспендовано +Name[sr@ijekavian]=Слагање је суспендовано +Name[sr@ijekavianlatin]=Slaganje je suspendovano +Name[sr@latin]=Slaganje je suspendovano +Name[sv]=Sammansättning har stoppats +Name[th]=การทำคอมโพสิตถูกหยุดชั่วคราว +Name[tr]=Birleşiklik askıya alındı +Name[ug]=ئارىلاش مەشغۇلاتى توختىتىلدى +Name[uk]=Композитний показ було тимчасово вимкнено +Name[wa]=Li môde compôzite a stî djoké +Name[x-test]=xxCompositing has been suspendedxx +Name[zh_CN]=混成已被中断 +Name[zh_TW]=組合效能已被暫停 +Comment=Another application has requested to suspend compositing. +Comment[ar]=تطبيق أخر طلب تعليق التركيب +Comment[az]=Hansısa tətbiq qrafik effektləri söndürməyi tələb edti. +Comment[bg]=Друго приложение е поискало временно спиране на ефектите. +Comment[bs]=Drugi program je zatražio da se slaganje suspenduje. +Comment[ca]=Una altra aplicació ha sol·licitat de suspendre la composició. +Comment[ca@valencia]=Una altra aplicació ha sol·licitat suspendre la composició. +Comment[cs]=Jiná aplikace si vyžádala pozastavení kompozice. +Comment[da]=Et andet program har anmodet om suspendering af compositing. +Comment[de]=Eine andere Anwendung hat das Aussetzen von Compositing erbeten. +Comment[el]=Κάποια εφαρμογή αιτήθηκε την αναστολή της σύνθεσης εικόνας. +Comment[en_GB]=Another application has requested to suspend compositing. +Comment[es]=Otra aplicación ha solicitado suspender la composición. +Comment[et]=Mingi muu rakendus on nõudnud komposiidi peatamist. +Comment[eu]=Beste aplikazio batek konposizioa eteteko eskatu du. +Comment[fi]=Toinen sovellus vaati keskeyttämään koostamisen. +Comment[fr]=Une autre application a demandé la suspension de la composition. +Comment[fy]=In oare applikaasje hat frege om compositing út te stellen. +Comment[ga]=Tá feidhmchlár eile ag iarraidh comhshuí a chur ar fionraí. +Comment[gl]=Outra aplicación pediu que a suspensión da composición. +Comment[he]=יישום אחר ביקש להשהות את השזירה. +Comment[hr]=Neka aplikacija je dala zahtjev za paziranjem mijeÅ¡anja. +Comment[hu]=Egy másik alkalmazás a kompozit mód felfüggesztését kérte. +Comment[ia]=Altere application ha requirite de suspender le composition. +Comment[id]=Aplikasi lain telah meminta untuk mensuspensi komposit. +Comment[is]=Annað forrit hefur beðið um að skjásamsetningu verði hætt. +Comment[it]=Un'altra applicazione ha richiesto di sospendere la composizione. +Comment[ja]=他のアプリケーションがコンポジティングの一時停止を要求しました。 +Comment[kk]=Басқа қолданбаның талабымен құрастыру аялдатылды. +Comment[km]=កម្មវិធី​ផ្សេង​បានស្នើ​ឲ្យ​ផ្អាក​ការ​តែង ។ +Comment[kn]=ಮಿಶ್ರಗೊಳಿಕೆಯನ್ನು ತಡೆಹಿಡಿಯುವಂತೆ ಬೇರೊಂದು ಅನ್ವಯವು ಮನವಿ ಸಲ್ಲಿಸಿದೆ. +Comment[ko]=다른 프로그램이 컴포지팅을 꺼 달라고 요청했습니다. +Comment[lt]=Kita programa paprašė pristabdyti komponavimą. +Comment[lv]=Kāda programma pieprasÄ«ja apturēt kompozitēšanu. +Comment[ml]=കമ്പോസിറ്റിംഗ് നിര്‍ത്തിവെയ്ക്കാന്‍ വേറൊരു പ്രയോഗം ആവശ്യപ്പെട്ടിട്ടുണ്ട് +Comment[mr]=कंपोझिटींग अकार्यक्षम करण्याची विनंती वेगळ्या अनुप्रयोगाने केलेली आहे. +Comment[nb]=Et annet program har bedt om at sammensetting skal suspenderes. +Comment[nds]=En anner Programm will dat Tosamensetten utsetten. +Comment[nl]=Een andere applicatie heeft verzocht compositing uit te stellen. +Comment[nn]=Eit anna program har spurt om stopping av samansetjinga. +Comment[pa]=ਹੋਰ ਐਪਲੀਕੇਸ਼ਨ ਕੰਪੋਜੀਸ਼ਨ ਨੂੰ ਸਸਪੈਂਡ ਕਰਨ ਦੀ ਮੰਗ ਕਰ ਚੁੱਕੀ ਹੈ। +Comment[pl]=Kolejny program zażądał wyłączenia kompozycji. +Comment[pt]=Outra aplicação pediu para suspender a composição. +Comment[pt_BR]=Outro aplicativo requisitou suspender a composição. +Comment[ro]=Altă aplicație a cerut suspendarea compoziționării. +Comment[ru]=Одно из приложений отключило графические эффекты +Comment[si]=වෙනත් යෙදුමක් මගින් රචනය අත්හිටුවීමට ඉල්ලා ඇත. +Comment[sk]=Iná aplikácia si vyžiadala pozastavenie kompozície. +Comment[sl]=Drug program je zahteval prestavitev skladnje 3D v pripravljenost. +Comment[sr]=Други програм је затражио да се слагање суспендује. +Comment[sr@ijekavian]=Други програм је затражио да се слагање суспендује. +Comment[sr@ijekavianlatin]=Drugi program je zatražio da se slaganje suspenduje. +Comment[sr@latin]=Drugi program je zatražio da se slaganje suspenduje. +Comment[sv]=Ett annat program har begärt att stoppa sammansättning. +Comment[th]=โปรแกรมอื่นบางตัวได้ร้องขอทำการพักการทำงานของการทำคอมโพสิต +Comment[tr]=Başka bir uygulama birleşikliğin askıya alınmasını istedi. +Comment[ug]=باشقا بىر پروگرامما ئارىلاش مەشغۇلاتىنى توختىتىشنى تەلەپ قىلدى. +Comment[uk]=Ще одна програма надіслала запит на вимикання композитного режиму. +Comment[wa]=Èn ôte programe a dmandé d' djoker l' môde compôzite. +Comment[x-test]=xxAnother application has requested to suspend compositing.xx +Comment[zh_CN]=另一个应用程序已经请求中断混成操作。 +Comment[zh_TW]=另一個應用程式要求暫停組合效能。 +Action=Popup + +[Event/graphicsreset] +Name=Graphics Reset +Name[az]=Qrafikanın sıfırlanması +Name[bs]=Reset grafike +Name[ca]=Reinici dels gràfics +Name[ca@valencia]=Reinici dels gràfics +Name[cs]=Resetovat grafiku +Name[da]=Grafiknulstilling +Name[de]=Grafik-Reset +Name[el]=Επαναφορά γραφικών +Name[en_GB]=Graphics Reset +Name[es]=Reinicio gráfico +Name[et]=Graafika lähtestamine +Name[eu]=Grafikoak berrezarri +Name[fi]=Grafiikan nollaus +Name[fr]=Réinitialisation graphique +Name[gl]=Reinicio dos gráficos +Name[he]=איפוס גרפיקה +Name[hu]=Grafikai visszaállítás +Name[ia]=Reinitia Graphic +Name[id]=Set-ulang Grafik +Name[it]=Azzeramento grafica +Name[kk]=Графиканы ысыру +Name[ko]=그래픽 초기화 +Name[lt]=Grafikos atstatymas +Name[nb]=Grafikk tilbakestilt +Name[nds]=Grafik-Torüchsetten +Name[nl]=Grafische reset +Name[nn]=Grafikk tilbakestilt +Name[pa]=ਗਰਾਫਿਕਸ ਮੁੜ-ਸੈੱਟ +Name[pl]=Ponowny rozruch grafiki +Name[pt]=Reinício Gráfico +Name[pt_BR]=Reinício gráfico +Name[ro]=Reinițializare grafică +Name[ru]=Сброс графики +Name[sk]=Grafické vynulovanie +Name[sl]=Ponastavitev grafike +Name[sr]=Ресетовање графике +Name[sr@ijekavian]=Ресетовање графике +Name[sr@ijekavianlatin]=Resetovanje grafike +Name[sr@latin]=Resetovanje grafike +Name[sv]=GrafikÃ¥terställning +Name[tr]=Grafik Sıfırlama +Name[uk]=Скидання графіки +Name[x-test]=xxGraphics Resetxx +Name[zh_CN]=图形重置 +Name[zh_TW]=圖形重置 +Comment=A graphics reset event occurred +Comment[az]=Qrafik sistemdə qəza baş verdi +Comment[bs]=Grafički reset događaj se desio +Comment[ca]=Ha ocorregut un esdeveniment de reinici dels gràfics +Comment[ca@valencia]=Ha ocorregut un esdeveniment de reinici dels gràfics +Comment[cs]=Nastala událost resetování grafiky +Comment[da]=En grafiknulstillingshændelse fandt sted +Comment[de]=Ein Zurücksetzen der Grafik ist aufgetreten +Comment[el]=Συνέβη μια επαναφορά των γραφικών +Comment[en_GB]=A graphics reset event occurred +Comment[es]=Ha ocurrido un evento de reinicio gráfico +Comment[et]=Toimus graafika lähtestamise sündmus +Comment[eu]=Grafikoak berrezartzeko gertaera bat jazo da +Comment[fi]=Sattui grafiikan nollaustapahtuma +Comment[fr]=Un évènement de réinitialisation graphique est intervenu +Comment[gl]=Aconteceu un evento de reinicio de gráficos +Comment[he]=הגרפיקה אופסה +Comment[hu]=Egy grafikai visszaállítás esemény történt +Comment[ia]=Il necessita un evento de reinitiar le graphic +Comment[id]=Sebuah peristiwa set ulang grafik yang terjadi +Comment[it]=Si è verificato un evento di azzeramento della grafica +Comment[kk]=Графиканы ысыру оқиғасы болды +Comment[ko]=그래픽 초기화 이벤트가 발생함 +Comment[lt]=Ä®vyko grafikos atstatymo įvykis +Comment[nb]=Det har foregÃ¥tt en grafikk-tilbakestilling +Comment[nds]=Dat geev en Grafik-Torüchsett-Begeefnis +Comment[nl]=Een gebeurtenis van een grafische reset deed zich voor +Comment[nn]=Det har skjedd ei grafikktilbakestilling +Comment[pl]=Nastąpiło zdarzenie ponownego rozruchu systemu graficznego +Comment[pt]=Ocorreu um evento de reinício gráfico +Comment[pt_BR]=Ocorreu um evento de reinício gráfico +Comment[ro]=A intervenit un eveniment de reinițializare a graficii +Comment[ru]=Произошёл сброс графической системы +Comment[sk]=Nastala chyba grafického vynulovania +Comment[sl]=PriÅ¡lo je do dogodka ponastavitve grafike +Comment[sr]=Дошло је до ресетовања графике +Comment[sr@ijekavian]=Дошло је до ресетовања графике +Comment[sr@ijekavianlatin]=DoÅ¡lo je do resetovanja grafike +Comment[sr@latin]=DoÅ¡lo je do resetovanja grafike +Comment[sv]=En grafikÃ¥terställningshändelse inträffade +Comment[tr]=Bir grafik sıfırlama olayı oluştu +Comment[uk]=Сталася подія відновлення початкового стану графіки +Comment[x-test]=xxA graphics reset event occurredxx +Comment[zh_CN]=发生了图形重置事件 +Comment[zh_TW]=發生了圖形重置事件 +Action=Popup + +[Event/xwaylandcrash] +Name=Xwayland Crash +Name[az]=Xwayland Qəzası +Name[ca]=Fallada de l'Xwayland +Name[en_GB]=Xwayland Crash +Name[es]=Fallo de Xwayland +Name[et]=Xwaylandi krahh +Name[eu]=Xwayland kraskatzea +Name[fi]=XWayland kaatui +Name[fr]=Plantage « Xwayland » +Name[ia]=Xwayland Crash +Name[it]=Chiusura inattesa di Xwayland +Name[ko]=Xwayland 충돌 +Name[nl]=Xwayland-crash +Name[nn]=Xwayland-krasj +Name[pl]=Usterka Xwayland +Name[pt]=Estoiro do Xwayland +Name[pt_BR]=Falha no Xwayland +Name[ro]=Prăbușire Xwayland +Name[sk]=Pád Xwayland-u +Name[sl]=Sesutje Xwayland +Name[sv]=Krasch av Xwayland +Name[uk]=Аварія Xwayland +Name[x-test]=xxXwayland Crashxx +Comment=Xwayland has crashed +Comment[az]=Xwayland qəzaya uğradı +Comment[ca]=L'Xwayland ha fallat +Comment[cs]=Xwayland spadnul +Comment[en_GB]=Xwayland has crashed +Comment[es]=Xwayland ha fallado +Comment[et]=Xwayland lõpetas krahhiga +Comment[eu]=Xwayland kraskatu egin da +Comment[fi]=XWayland on kaatunut +Comment[fr]=Plantage de « Xwayland » +Comment[ia]=Xwayland ha fracassate +Comment[it]=Xwayland è stato terminato in modo inatteso +Comment[ko]=Xwayland가 충돌함 +Comment[nl]=Xwayland is gecrasht +Comment[nn]=Xwayland krasja +Comment[pl]=Xwayland napotkał usterkę +Comment[pt]=O Xwayland estoirou +Comment[pt_BR]=O Xwayland falhou +Comment[ro]=Xwayland s-a prăbușit +Comment[sk]=Xwayland spadol +Comment[sl]=Xwayland se je sesul +Comment[sv]=Xwayland har kraschat +Comment[uk]=Xwayland завершив роботу в аварійному режимі +Comment[x-test]=xxXwayland has crashedxx +Action=Popup diff --git a/kwinbindings.cpp b/kwinbindings.cpp new file mode 100644 index 0000000..21395dd --- /dev/null +++ b/kwinbindings.cpp @@ -0,0 +1,157 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +// This file is #included from within: +// Workspace::initShortcuts() +// { + +// Some shortcuts have Tarzan-speech like names, they need extra +// normal human descriptions with DEF2() the others can use DEF() +// new DEF3 allows to pass data to the action, replacing the %1 argument in the name + +#define DEF2( name, descr, key, fnSlot ) \ + initShortcut(QStringLiteral(name), i18n(descr), key, &Workspace::fnSlot); + +#define DEF( name, key, fnSlot ) \ + initShortcut(QStringLiteral(name), i18n(name), key, &Workspace::fnSlot); + +#define DEF3( name, key, fnSlot, value ) \ + initShortcut(QStringLiteral(name).arg(value), i18n(name, value), key, &Workspace::fnSlot, value); + +#define DEF4( name, descr, key, functor ) \ + initShortcut(QStringLiteral(name), i18n(descr), key, functor); + +#define DEF5( name, key, functor, value ) \ + initShortcut(QStringLiteral(name).arg(value), i18n(name, value), key, functor, value); + +#define DEF6( name, key, target, fnSlot ) \ + initShortcut(QStringLiteral(name), i18n(name), key, target, &fnSlot); + + +DEF(I18N_NOOP("Window Operations Menu"), + Qt::ALT + Qt::Key_F3, slotWindowOperations); +DEF2("Window Close", I18N_NOOP("Close Window"), + Qt::ALT + Qt::Key_F4, slotWindowClose); +DEF2("Window Maximize", I18N_NOOP("Maximize Window"), + Qt::META + Qt::Key_PageUp, slotWindowMaximize); +DEF2("Window Maximize Vertical", I18N_NOOP("Maximize Window Vertically"), + 0, slotWindowMaximizeVertical); +DEF2("Window Maximize Horizontal", I18N_NOOP("Maximize Window Horizontally"), + 0, slotWindowMaximizeHorizontal); +DEF2("Window Minimize", I18N_NOOP("Minimize Window"), + Qt::META + Qt::Key_PageDown, slotWindowMinimize); +DEF2("Window Shade", I18N_NOOP("Shade Window"), + 0, slotWindowShade); +DEF2("Window Move", I18N_NOOP("Move Window"), + 0, slotWindowMove); +DEF2("Window Resize", I18N_NOOP("Resize Window"), + 0, slotWindowResize); +DEF2("Window Raise", I18N_NOOP("Raise Window"), + 0, slotWindowRaise); +DEF2("Window Lower", I18N_NOOP("Lower Window"), + 0, slotWindowLower); +DEF(I18N_NOOP("Toggle Window Raise/Lower"), + 0, slotWindowRaiseOrLower); +DEF2("Window Fullscreen", I18N_NOOP("Make Window Fullscreen"), + 0, slotWindowFullScreen); +DEF2("Window No Border", I18N_NOOP("Hide Window Border"), + 0, slotWindowNoBorder); +DEF2("Window Above Other Windows", I18N_NOOP("Keep Window Above Others"), + 0, slotWindowAbove); +DEF2("Window Below Other Windows", I18N_NOOP("Keep Window Below Others"), + 0, slotWindowBelow); +DEF(I18N_NOOP("Activate Window Demanding Attention"), + Qt::CTRL + Qt::ALT + Qt::Key_A, slotActivateAttentionWindow); +DEF(I18N_NOOP("Setup Window Shortcut"), + 0, slotSetupWindowShortcut); +DEF2("Window Pack Right", I18N_NOOP("Pack Window to the Right"), + 0, slotWindowPackRight); +DEF2("Window Pack Left", I18N_NOOP("Pack Window to the Left"), + 0, slotWindowPackLeft); +DEF2("Window Pack Up", I18N_NOOP("Pack Window Up"), + 0, slotWindowPackUp); +DEF2("Window Pack Down", I18N_NOOP("Pack Window Down"), + 0, slotWindowPackDown); +DEF2("Window Grow Horizontal", I18N_NOOP("Pack Grow Window Horizontally"), + 0, slotWindowGrowHorizontal); +DEF2("Window Grow Vertical", I18N_NOOP("Pack Grow Window Vertically"), + 0, slotWindowGrowVertical); +DEF2("Window Shrink Horizontal", I18N_NOOP("Pack Shrink Window Horizontally"), + 0, slotWindowShrinkHorizontal); +DEF2("Window Shrink Vertical", I18N_NOOP("Pack Shrink Window Vertically"), + 0, slotWindowShrinkVertical); +DEF4("Window Quick Tile Left", I18N_NOOP("Quick Tile Window to the Left"), + Qt::META + Qt::Key_Left, std::bind(&Workspace::quickTileWindow, this, QuickTileFlag::Left)); +DEF4("Window Quick Tile Right", I18N_NOOP("Quick Tile Window to the Right"), + Qt::META + Qt::Key_Right, std::bind(&Workspace::quickTileWindow, this, QuickTileFlag::Right)); +DEF4("Window Quick Tile Top", I18N_NOOP("Quick Tile Window to the Top"), + Qt::META + Qt::Key_Up, std::bind(&Workspace::quickTileWindow, this, QuickTileFlag::Top)); +DEF4("Window Quick Tile Bottom", I18N_NOOP("Quick Tile Window to the Bottom"), + Qt::META + Qt::Key_Down, std::bind(&Workspace::quickTileWindow, this, QuickTileFlag::Bottom)); +DEF4("Window Quick Tile Top Left", I18N_NOOP("Quick Tile Window to the Top Left"), + 0, std::bind(&Workspace::quickTileWindow, this, QuickTileFlag::Top | QuickTileFlag::Left)); +DEF4("Window Quick Tile Bottom Left", I18N_NOOP("Quick Tile Window to the Bottom Left"), + 0, std::bind(&Workspace::quickTileWindow, this, QuickTileFlag::Bottom | QuickTileFlag::Left)); +DEF4("Window Quick Tile Top Right", I18N_NOOP("Quick Tile Window to the Top Right"), + 0, std::bind(&Workspace::quickTileWindow, this, QuickTileFlag::Top | QuickTileFlag::Right)); +DEF4("Window Quick Tile Bottom Right", I18N_NOOP("Quick Tile Window to the Bottom Right"), + 0, std::bind(&Workspace::quickTileWindow, this, QuickTileFlag::Bottom | QuickTileFlag::Right)); +DEF4("Switch Window Up", I18N_NOOP("Switch to Window Above"), + Qt::META + Qt::ALT + Qt::Key_Up, std::bind(static_cast(&Workspace::switchWindow), this, DirectionNorth)); +DEF4("Switch Window Down", I18N_NOOP("Switch to Window Below"), + Qt::META + Qt::ALT + Qt::Key_Down, std::bind(static_cast(&Workspace::switchWindow), this, DirectionSouth)); +DEF4("Switch Window Right", I18N_NOOP("Switch to Window to the Right"), + Qt::META + Qt::ALT + Qt::Key_Right, std::bind(static_cast(&Workspace::switchWindow), this, DirectionEast)); +DEF4("Switch Window Left", I18N_NOOP("Switch to Window to the Left"), + Qt::META + Qt::ALT + Qt::Key_Left, std::bind(static_cast(&Workspace::switchWindow), this, DirectionWest)); +DEF2("Increase Opacity", I18N_NOOP("Increase Opacity of Active Window by 5 %"), + 0, slotIncreaseWindowOpacity); +DEF2("Decrease Opacity", I18N_NOOP("Decrease Opacity of Active Window by 5 %"), + 0, slotLowerWindowOpacity); + +DEF2("Window On All Desktops", I18N_NOOP("Keep Window on All Desktops"), + 0, slotWindowOnAllDesktops); + +for (int i = 1; i < 21; ++i) { + DEF5(I18N_NOOP("Window to Desktop %1"), 0, std::bind(&Workspace::slotWindowToDesktop, this, i), i); +} +DEF(I18N_NOOP("Window to Next Desktop"), 0, slotWindowToNextDesktop); +DEF(I18N_NOOP("Window to Previous Desktop"), 0, slotWindowToPreviousDesktop); +DEF(I18N_NOOP("Window One Desktop to the Right"), 0, slotWindowToDesktopRight); +DEF(I18N_NOOP("Window One Desktop to the Left"), 0, slotWindowToDesktopLeft); +DEF(I18N_NOOP("Window One Desktop Up"), 0, slotWindowToDesktopUp); +DEF(I18N_NOOP("Window One Desktop Down"), 0, slotWindowToDesktopDown); + +for (int i = 0; i < 8; ++i) { + DEF3(I18N_NOOP("Window to Screen %1"), 0, slotWindowToScreen, i); +} +DEF(I18N_NOOP("Window to Next Screen"), 0, slotWindowToNextScreen); +DEF(I18N_NOOP("Window to Previous Screen"), 0, slotWindowToPrevScreen); +DEF(I18N_NOOP("Show Desktop"), Qt::META + Qt::Key_D, slotToggleShowDesktop); + +for (int i = 0; i < 8; ++i) { + DEF3(I18N_NOOP("Switch to Screen %1"), 0, slotSwitchToScreen, i); +} + +DEF(I18N_NOOP("Switch to Next Screen"), 0, slotSwitchToNextScreen); +DEF(I18N_NOOP("Switch to Previous Screen"), 0, slotSwitchToPrevScreen); + +DEF(I18N_NOOP("Kill Window"), Qt::CTRL + Qt::ALT + Qt::Key_Escape, slotKillWindow); +DEF6(I18N_NOOP("Suspend Compositing"), Qt::SHIFT + Qt::ALT + Qt::Key_F12, Compositor::self(), Compositor::toggleCompositing); +DEF6(I18N_NOOP("Invert Screen Colors"), 0, kwinApp()->platform(), Platform::invertScreen); + +#undef DEF +#undef DEF2 +#undef DEF3 +#undef DEF4 +#undef DEF5 +#undef DEF6 + +// } diff --git a/layers.cpp b/layers.cpp new file mode 100644 index 0000000..e6c3826 --- /dev/null +++ b/layers.cpp @@ -0,0 +1,847 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +// SELI zmenit doc + +/* + + This file contains things relevant to stacking order and layers. + + Design: + + Normal unconstrained stacking order, as requested by the user (by clicking + on windows to raise them, etc.), is in Workspace::unconstrained_stacking_order. + That list shouldn't be used at all, except for building + Workspace::stacking_order. The building is done + in Workspace::constrainedStackingOrder(). Only Workspace::stackingOrder() should + be used to get the stacking order, because it also checks the stacking order + is up to date. + All clients are also stored in Workspace::clients (except for isDesktop() clients, + as those are very special, and are stored in Workspace::desktops), in the order + the clients were created. + + Every window has one layer assigned in which it is. There are 7 layers, + from bottom : DesktopLayer, BelowLayer, NormalLayer, DockLayer, AboveLayer, NotificationLayer, + ActiveLayer, CriticalNotificationLayer, and OnScreenDisplayLayer (see also NETWM sect.7.10.). + The layer a window is in depends on the window type, and on other things like whether the window + is active. We extend the layers provided in NETWM by the NotificationLayer, OnScreenDisplayLayer, + and CriticalNotificationLayer. + The NoficationLayer contains notification windows which are kept above all windows except the active + fullscreen window. The CriticalNotificationLayer contains notification windows which are important + enough to keep them even above fullscreen windows. The OnScreenDisplayLayer is used for eg. volume + and brightness change feedback and is kept above all windows since it provides immediate response + to a user action. + + NET::Splash clients belong to the Normal layer. NET::TopMenu clients + belong to Dock layer. Clients that are both NET::Dock and NET::KeepBelow + are in the Normal layer in order to keep the 'allow window to cover + the panel' Kicker setting to work as intended (this may look like a slight + spec violation, but a) I have no better idea, b) the spec allows adjusting + the stacking order if the WM thinks it's a good idea . We put all + NET::KeepAbove above all Docks too, even though the spec suggests putting + them in the same layer. + + Most transients are in the same layer as their mainwindow, + see Workspace::constrainedStackingOrder(), they may also be in higher layers, but + they should never be below their mainwindow. + + When some client attribute changes (above/below flag, transiency...), + Workspace::updateClientLayer() should be called in order to make + sure it's moved to the appropriate layer QList if needed. + + Currently the things that affect client in which layer a client + belongs: KeepAbove/Keep Below flags, window type, fullscreen + state and whether the client is active, mainclient (transiency). + + Make sure updateStackingOrder() is called in order to make + Workspace::stackingOrder() up to date and propagated to the world. + Using Workspace::blockStackingUpdates() (or the StackingUpdatesBlocker + helper class) it's possible to temporarily disable updates + and the stacking order will be updated once after it's allowed again. + +*/ + +#include "utils.h" +#include "x11client.h" +#include "focuschain.h" +#include "netinfo.h" +#include "workspace.h" +#include "tabbox.h" +#include "group.h" +#include "rules.h" +#include "screens.h" +#include "unmanaged.h" +#include "deleted.h" +#include "effects.h" +#include "composite.h" +#include "screenedge.h" +#include "wayland_server.h" +#include "internal_client.h" + +#include + +namespace KWin +{ + +//******************************* +// Workspace +//******************************* + +void Workspace::updateClientLayer(AbstractClient* c) +{ + if (c) + c->updateLayer(); +} + +void Workspace::updateStackingOrder(bool propagate_new_clients) +{ + if (block_stacking_updates > 0) { + if (propagate_new_clients) + blocked_propagating_new_clients = true; + return; + } + QList new_stacking_order = constrainedStackingOrder(); + bool changed = (force_restacking || new_stacking_order != stacking_order); + force_restacking = false; + stacking_order = new_stacking_order; + if (changed || propagate_new_clients) { + propagateClients(propagate_new_clients); + markXStackingOrderAsDirty(); + emit stackingOrderChanged(); + if (m_compositor) { + m_compositor->addRepaintFull(); + } + + if (active_client) + active_client->updateMouseGrab(); + } +} + +/** + * Some fullscreen effects have to raise the screenedge on top of an input window, thus all windows + * this function puts them back where they belong for regular use and is some cheap variant of + * the regular propagateClients function in that it completely ignores managed clients and everything + * else and also does not update the NETWM property. + * Called from Effects::destroyInputWindow so far. + */ +void Workspace::stackScreenEdgesUnderOverrideRedirect() +{ + if (!rootInfo()) { + return; + } + Xcb::restackWindows(QVector() << rootInfo()->supportWindow() << ScreenEdges::self()->windows()); +} + +/** + * Propagates the managed clients to the world. + * Called ONLY from updateStackingOrder(). + */ +void Workspace::propagateClients(bool propagate_new_clients) +{ + if (!rootInfo()) { + return; + } + // restack the windows according to the stacking order + // supportWindow > electric borders > clients > hidden clients + QVector newWindowStack; + + // Stack all windows under the support window. The support window is + // not used for anything (besides the NETWM property), and it's not shown, + // but it was lowered after kwin startup. Stacking all clients below + // it ensures that no client will be ever shown above override-redirect + // windows (e.g. popups). + newWindowStack << rootInfo()->supportWindow(); + + newWindowStack << ScreenEdges::self()->windows(); + + newWindowStack << manual_overlays; + + newWindowStack.reserve(newWindowStack.size() + 2*stacking_order.size()); // *2 for inputWindow + + for (int i = stacking_order.size() - 1; i >= 0; --i) { + X11Client *client = qobject_cast(stacking_order.at(i)); + if (!client || client->hiddenPreview()) { + continue; + } + + if (client->inputId()) + // Stack the input window above the frame + newWindowStack << client->inputId(); + + newWindowStack << client->frameId(); + } + + // when having hidden previews, stack hidden windows below everything else + // (as far as pure X stacking order is concerned), in order to avoid having + // these windows that should be unmapped to interfere with other windows + for (int i = stacking_order.size() - 1; i >= 0; --i) { + X11Client *client = qobject_cast(stacking_order.at(i)); + if (!client || !client->hiddenPreview()) + continue; + newWindowStack << client->frameId(); + } + // TODO isn't it too inefficient to restack always all clients? + // TODO don't restack not visible windows? + Q_ASSERT(newWindowStack.at(0) == rootInfo()->supportWindow()); + Xcb::restackWindows(newWindowStack); + + int pos = 0; + xcb_window_t *cl(nullptr); + if (propagate_new_clients) { + cl = new xcb_window_t[ manual_overlays.count() + clients.count()]; + for (const auto win : manual_overlays) { + cl[pos++] = win; + } + for (auto it = clients.constBegin(); it != clients.constEnd(); ++it) + cl[pos++] = (*it)->window(); + rootInfo()->setClientList(cl, pos); + delete [] cl; + } + + cl = new xcb_window_t[ manual_overlays.count() + stacking_order.count()]; + pos = 0; + for (auto it = stacking_order.constBegin(); it != stacking_order.constEnd(); ++it) { + X11Client *client = qobject_cast(*it); + if (client) { + cl[pos++] = client->window(); + } + } + for (const auto win : manual_overlays) { + cl[pos++] = win; + } + rootInfo()->setClientListStacking(cl, pos); + delete [] cl; +} + +/** + * Returns topmost visible client. Windows on the dock, the desktop + * or of any other special kind are excluded. Also if the window + * doesn't accept focus it's excluded. + */ +// TODO misleading name for this method, too many slightly different ways to use it +AbstractClient* Workspace::topClientOnDesktop(int desktop, int screen, bool unconstrained, bool only_normal) const +{ +// TODO Q_ASSERT( block_stacking_updates == 0 ); + QList list; + if (!unconstrained) + list = stacking_order; + else + list = unconstrained_stacking_order; + for (int i = list.size() - 1; + i >= 0; + --i) { + AbstractClient *c = qobject_cast(list.at(i)); + if (!c) { + continue; + } + if (c->isOnDesktop(desktop) && c->isShown(false) && c->isOnCurrentActivity()) { + if (screen != -1 && c->screen() != screen) + continue; + if (!only_normal) + return c; + if (c->wantsTabFocus() && !c->isSpecialWindow()) + return c; + } + } + return nullptr; +} + +AbstractClient* Workspace::findDesktop(bool topmost, int desktop) const +{ +// TODO Q_ASSERT( block_stacking_updates == 0 ); + if (topmost) { + for (int i = stacking_order.size() - 1; i >= 0; i--) { + AbstractClient *c = qobject_cast(stacking_order.at(i)); + if (c && c->isOnDesktop(desktop) && c->isDesktop() + && c->isShown(true)) + return c; + } + } else { // bottom-most + foreach (Toplevel * c, stacking_order) { + AbstractClient *client = qobject_cast(c); + if (client && c->isOnDesktop(desktop) && c->isDesktop() + && client->isShown(true)) + return client; + } + } + return nullptr; +} + +void Workspace::raiseOrLowerClient(AbstractClient *c) +{ + if (!c) return; + AbstractClient* topmost = nullptr; +// TODO Q_ASSERT( block_stacking_updates == 0 ); + if (most_recently_raised && stacking_order.contains(most_recently_raised) && + most_recently_raised->isShown(true) && c->isOnCurrentDesktop()) + topmost = most_recently_raised; + else + topmost = topClientOnDesktop(c->isOnAllDesktops() ? VirtualDesktopManager::self()->current() : c->desktop(), + options->isSeparateScreenFocus() ? c->screen() : -1); + + if (c == topmost) + lowerClient(c); + else + raiseClient(c); +} + + +void Workspace::lowerClient(AbstractClient* c, bool nogroup) +{ + if (!c) + return; + + c->cancelAutoRaise(); + + StackingUpdatesBlocker blocker(this); + + unconstrained_stacking_order.removeAll(c); + unconstrained_stacking_order.prepend(c); + if (!nogroup && c->isTransient()) { + // lower also all windows in the group, in their reversed stacking order + QList wins; + if (auto group = c->group()) { + wins = ensureStackingOrder(group->members()); + } + for (int i = wins.size() - 1; + i >= 0; + --i) { + if (wins[ i ] != c) + lowerClient(wins[ i ], true); + } + } + + if (c == most_recently_raised) + most_recently_raised = nullptr; +} + +void Workspace::lowerClientWithinApplication(AbstractClient* c) +{ + if (!c) + return; + + c->cancelAutoRaise(); + + StackingUpdatesBlocker blocker(this); + + unconstrained_stacking_order.removeAll(c); + bool lowered = false; + // first try to put it below the bottom-most window of the application + for (auto it = unconstrained_stacking_order.begin(); + it != unconstrained_stacking_order.end(); + ++it) { + AbstractClient *client = qobject_cast(*it); + if (!client) { + continue; + } + if (AbstractClient::belongToSameApplication(client, c)) { + unconstrained_stacking_order.insert(it, c); + lowered = true; + break; + } + } + if (!lowered) + unconstrained_stacking_order.prepend(c); + // ignore mainwindows +} + +void Workspace::raiseClient(AbstractClient* c, bool nogroup) +{ + if (!c) + return; + + c->cancelAutoRaise(); + + StackingUpdatesBlocker blocker(this); + + if (!nogroup && c->isTransient()) { + QList transients; + AbstractClient *transient_parent = c; + while ((transient_parent = transient_parent->transientFor())) + transients << transient_parent; + foreach (transient_parent, transients) + raiseClient(transient_parent, true); + } + + unconstrained_stacking_order.removeAll(c); + unconstrained_stacking_order.append(c); + + if (!c->isSpecialWindow()) { + most_recently_raised = c; + } +} + +void Workspace::raiseClientWithinApplication(AbstractClient* c) +{ + if (!c) + return; + + c->cancelAutoRaise(); + + StackingUpdatesBlocker blocker(this); + // ignore mainwindows + + // first try to put it above the top-most window of the application + for (int i = unconstrained_stacking_order.size() - 1; i > -1 ; --i) { + AbstractClient *other = qobject_cast(unconstrained_stacking_order.at(i)); + if (!other) { + continue; + } + if (other == c) // don't lower it just because it asked to be raised + return; + if (AbstractClient::belongToSameApplication(other, c)) { + unconstrained_stacking_order.removeAll(c); + unconstrained_stacking_order.insert(unconstrained_stacking_order.indexOf(other) + 1, c); // insert after the found one + break; + } + } +} + +void Workspace::raiseClientRequest(KWin::AbstractClient *c, NET::RequestSource src, xcb_timestamp_t timestamp) +{ + if (src == NET::FromTool || allowFullClientRaising(c, timestamp)) + raiseClient(c); + else { + raiseClientWithinApplication(c); + c->demandAttention(); + } +} + +void Workspace::lowerClientRequest(KWin::X11Client *c, NET::RequestSource src, xcb_timestamp_t /*timestamp*/) +{ + // If the client has support for all this focus stealing prevention stuff, + // do only lowering within the application, as that's the more logical + // variant of lowering when application requests it. + // No demanding of attention here of course. + if (src == NET::FromTool || !c->hasUserTimeSupport()) + lowerClient(c); + else + lowerClientWithinApplication(c); +} + +void Workspace::lowerClientRequest(KWin::AbstractClient *c) +{ + lowerClientWithinApplication(c); +} + +void Workspace::restack(AbstractClient* c, AbstractClient* under, bool force) +{ + Q_ASSERT(unconstrained_stacking_order.contains(under)); + if (!force && !AbstractClient::belongToSameApplication(under, c)) { + // put in the stacking order below _all_ windows belonging to the active application + for (int i = 0; i < unconstrained_stacking_order.size(); ++i) { + AbstractClient *other = qobject_cast(unconstrained_stacking_order.at(i)); + if (other && other->layer() == c->layer() && AbstractClient::belongToSameApplication(under, other)) { + under = (c == other) ? nullptr : other; + break; + } + } + } + if (under) { + unconstrained_stacking_order.removeAll(c); + unconstrained_stacking_order.insert(unconstrained_stacking_order.indexOf(under), c); + } + + Q_ASSERT(unconstrained_stacking_order.contains(c)); + FocusChain::self()->moveAfterClient(c, under); + updateStackingOrder(); +} + +void Workspace::restackClientUnderActive(AbstractClient* c) +{ + if (!active_client || active_client == c || active_client->layer() != c->layer()) { + raiseClient(c); + return; + } + restack(c, active_client); +} + +void Workspace::restoreSessionStackingOrder(X11Client *c) +{ + if (c->sessionStackingOrder() < 0) + return; + StackingUpdatesBlocker blocker(this); + unconstrained_stacking_order.removeAll(c); + for (auto it = unconstrained_stacking_order.begin(); // from bottom + it != unconstrained_stacking_order.end(); + ++it) { + X11Client *current = qobject_cast(*it); + if (!current) { + continue; + } + if (current->sessionStackingOrder() > c->sessionStackingOrder()) { + unconstrained_stacking_order.insert(it, c); + return; + } + } + unconstrained_stacking_order.append(c); +} + +/** + * Returns a stacking order based upon \a list that fulfills certain contained. + */ +QList Workspace::constrainedStackingOrder() +{ + QList layer[ NumLayers ]; + + // build the order from layers + QVector< QMultiMap > minimum_layer(screens()->count()); + for (auto it = unconstrained_stacking_order.constBegin(), + end = unconstrained_stacking_order.constEnd(); it != end; ++it) { + Layer l = (*it)->layer(); + + const int screen = (*it)->screen(); + X11Client *c = qobject_cast(*it); + QMap< Group*, Layer >::iterator mLayer = minimum_layer[screen].find(c ? c->group() : nullptr); + if (mLayer != minimum_layer[screen].end()) { + // If a window is raised above some other window in the same window group + // which is in the ActiveLayer (i.e. it's fulscreened), make sure it stays + // above that window (see #95731). + if (*mLayer == ActiveLayer && (l > BelowLayer)) + l = ActiveLayer; + *mLayer = l; + } else if (c) { + minimum_layer[screen].insert(c->group(), l); + } + layer[ l ].append(*it); + } + QList stacking; + for (int lay = FirstLayer; lay < NumLayers; ++lay) { + stacking += layer[lay]; + } + // now keep transients above their mainwindows + // TODO this could(?) use some optimization + for (int i = stacking.size() - 1; i >= 0;) { + // Index of the main window for the current transient window. + int i2 = -1; + + // If the current transient has "child" transients, we'd like to restart + // construction of the constrained stacking order from the position where + // the current transient will be moved. + bool hasTransients = false; + + // Find topmost client this one is transient for. + if (auto *client = qobject_cast(stacking[i])) { + if (!client->isTransient()) { + --i; + continue; + } + for (i2 = stacking.size() - 1; i2 >= 0; --i2) { + auto *c2 = qobject_cast(stacking[i2]); + if (!c2) { + continue; + } + if (c2 == client) { + i2 = -1; // Don't reorder, already on top of its main window. + break; + } + if (c2->hasTransient(client, true) + && keepTransientAbove(c2, client)) { + break; + } + } + + hasTransients = !client->transients().isEmpty(); + + // If the current transient doesn't have any "alive" transients, check + // whether it has deleted transients that have to be raised. + const bool searchForDeletedTransients = !hasTransients + && !deletedList().isEmpty(); + if (searchForDeletedTransients) { + for (int j = i + 1; j < stacking.count(); ++j) { + auto *deleted = qobject_cast(stacking[j]); + if (!deleted) { + continue; + } + if (deleted->wasTransientFor(client)) { + hasTransients = true; + break; + } + } + } + } else if (auto *deleted = qobject_cast(stacking[i])) { + if (!deleted->wasTransient()) { + --i; + continue; + } + for (i2 = stacking.size() - 1; i2 >= 0; --i2) { + Toplevel *c2 = stacking[i2]; + if (c2 == deleted) { + i2 = -1; // Don't reorder, already on top of its main window. + break; + } + if (deleted->wasTransientFor(c2) + && keepDeletedTransientAbove(c2, deleted)) { + break; + } + } + hasTransients = !deleted->transients().isEmpty(); + } + + if (i2 == -1) { + --i; + continue; + } + + Toplevel *current = stacking[i]; + + stacking.removeAt(i); + --i; // move onto the next item (for next for () iteration) + --i2; // adjust index of the mainwindow after the remove above + if (hasTransients) { // this one now can be possibly above its transients, + i = i2; // so go again higher in the stack order and possibly move those transients again + } + ++i2; // insert after (on top of) the mainwindow, it's ok if it2 is now stacking.end() + stacking.insert(i2, current); + } + return stacking; +} + +void Workspace::blockStackingUpdates(bool block) +{ + if (block) { + if (block_stacking_updates == 0) + blocked_propagating_new_clients = false; + ++block_stacking_updates; + } else // !block + if (--block_stacking_updates == 0) { + updateStackingOrder(blocked_propagating_new_clients); + if (effects) + static_cast(effects)->checkInputWindowStacking(); + } +} + +namespace { +template +QList ensureStackingOrderInList(const QList &stackingOrder, const QList &list) +{ + static_assert(std::is_base_of::value, + "U must be derived from T"); +// TODO Q_ASSERT( block_stacking_updates == 0 ); + if (list.count() < 2) + return list; + // TODO is this worth optimizing? + QList result = list; + for (auto it = stackingOrder.begin(); + it != stackingOrder.end(); + ++it) { + T *c = qobject_cast(*it); + if (!c) { + continue; + } + if (result.removeAll(c) != 0) + result.append(c); + } + return result; +} +} + +// Ensure list is in stacking order +QList Workspace::ensureStackingOrder(const QList &list) const +{ + return ensureStackingOrderInList(stacking_order, list); +} + +QList Workspace::ensureStackingOrder(const QList &list) const +{ + return ensureStackingOrderInList(stacking_order, list); +} + +// check whether a transient should be actually kept above its mainwindow +// there may be some special cases where this rule shouldn't be enfored +bool Workspace::keepTransientAbove(const AbstractClient* mainwindow, const AbstractClient* transient) +{ + // #93832 - don't keep splashscreens above dialogs + if (transient->isSplash() && mainwindow->isDialog()) + return false; + // This is rather a hack for #76026. Don't keep non-modal dialogs above + // the mainwindow, but only if they're group transient (since only such dialogs + // have taskbar entry in Kicker). A proper way of doing this (both kwin and kicker) + // needs to be found. + if (transient->isDialog() && !transient->isModal() && transient->groupTransient()) + return false; + // #63223 - don't keep transients above docks, because the dock is kept high, + // and e.g. dialogs for them would be too high too + // ignore this if the transient has a placement hint which indicates it should go above it's parent + if (mainwindow->isDock() && !transient->hasTransientPlacementHint()) + return false; + return true; +} + +bool Workspace::keepDeletedTransientAbove(const Toplevel *mainWindow, const Deleted *transient) const +{ + // #93832 - Don't keep splashscreens above dialogs. + if (transient->isSplash() && mainWindow->isDialog()) { + return false; + } + + if (transient->wasX11Client()) { + // If a group transient was active, we should keep it above no matter + // what, because at the time when the transient was closed, it was above + // the main window. + if (transient->wasGroupTransient() && transient->wasActive()) { + return true; + } + + // This is rather a hack for #76026. Don't keep non-modal dialogs above + // the mainwindow, but only if they're group transient (since only such + // dialogs have taskbar entry in Kicker). A proper way of doing this + // (both kwin and kicker) needs to be found. + if (transient->wasGroupTransient() && transient->isDialog() + && !transient->isModal()) { + return false; + } + + // #63223 - Don't keep transients above docks, because the dock is kept + // high, and e.g. dialogs for them would be too high too. + if (mainWindow->isDock()) { + return false; + } + } + + return true; +} + +// Returns all windows in their stacking order on the root window. +QList Workspace::xStackingOrder() const +{ + if (m_xStackingDirty) { + const_cast(this)->updateXStackingOrder(); + } + return x_stacking; +} + +void Workspace::updateXStackingOrder() +{ + x_stacking.clear(); + std::unique_ptr tree{std::move(m_xStackingQueryTree)}; + // use our own stacking order, not the X one, as they may differ + foreach (Toplevel * c, stacking_order) + x_stacking.append(c); + + if (tree && !tree->isNull()) { + xcb_window_t *windows = tree->children(); + const auto count = tree->data()->children_len; + int foundUnmanagedCount = m_unmanaged.count(); + for (unsigned int i = 0; + i < count; + ++i) { + for (auto it = m_unmanaged.constBegin(); it != m_unmanaged.constEnd(); ++it) { + Unmanaged *u = *it; + if (u->window() == windows[i]) { + x_stacking.append(u); + foundUnmanagedCount--; + break; + } + } + if (foundUnmanagedCount == 0) { + break; + } + } + } + + for (InternalClient *client : workspace()->internalClients()) { + if (client->isShown(false)) { + x_stacking.append(client); + } + } + + m_xStackingDirty = false; +} + +//******************************* +// Client +//******************************* + +void X11Client::restackWindow(xcb_window_t above, int detail, NET::RequestSource src, xcb_timestamp_t timestamp, bool send_event) +{ + X11Client *other = nullptr; + if (detail == XCB_STACK_MODE_OPPOSITE) { + other = workspace()->findClient(Predicate::WindowMatch, above); + if (!other) { + workspace()->raiseOrLowerClient(this); + return; + } + auto it = workspace()->stackingOrder().constBegin(), + end = workspace()->stackingOrder().constEnd(); + while (it != end) { + if (*it == this) { + detail = XCB_STACK_MODE_ABOVE; + break; + } else if (*it == other) { + detail = XCB_STACK_MODE_BELOW; + break; + } + ++it; + } + } + else if (detail == XCB_STACK_MODE_TOP_IF) { + other = workspace()->findClient(Predicate::WindowMatch, above); + if (other && other->frameGeometry().intersects(frameGeometry())) + workspace()->raiseClientRequest(this, src, timestamp); + return; + } + else if (detail == XCB_STACK_MODE_BOTTOM_IF) { + other = workspace()->findClient(Predicate::WindowMatch, above); + if (other && other->frameGeometry().intersects(frameGeometry())) + workspace()->lowerClientRequest(this, src, timestamp); + return; + } + + if (!other) + other = workspace()->findClient(Predicate::WindowMatch, above); + + if (other && detail == XCB_STACK_MODE_ABOVE) { + auto it = workspace()->stackingOrder().constEnd(), + begin = workspace()->stackingOrder().constBegin(); + while (--it != begin) { + + if (*it == other) { // the other one is top on stack + it = begin; // invalidate + src = NET::FromTool; // force + break; + } + X11Client *c = qobject_cast(*it); + + if (!c || !( (*it)->isNormalWindow() && c->isShown(true) && + (*it)->isOnCurrentDesktop() && (*it)->isOnCurrentActivity() && (*it)->isOnScreen(screen()) )) + continue; // irrelevant clients + + if (*(it - 1) == other) + break; // "it" is the one above the target one, stack below "it" + } + + if (it != begin && (*(it - 1) == other)) + other = qobject_cast(*it); + else + other = nullptr; + } + + if (other) + workspace()->restack(this, other); + else if (detail == XCB_STACK_MODE_BELOW) + workspace()->lowerClientRequest(this, src, timestamp); + else if (detail == XCB_STACK_MODE_ABOVE) + workspace()->raiseClientRequest(this, src, timestamp); + + if (send_event) + sendSyntheticConfigureNotify(); +} + +bool X11Client::belongsToDesktop() const +{ + foreach (const X11Client *c, group()->members()) { + if (c->isDesktop()) + return true; + } + return false; +} + +} // namespace diff --git a/layershellv1client.cpp b/layershellv1client.cpp new file mode 100644 index 0000000..556e44a --- /dev/null +++ b/layershellv1client.cpp @@ -0,0 +1,276 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "layershellv1client.h" +#include "abstract_output.h" +#include "layershellv1integration.h" +#include "deleted.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include +#include + +using namespace KWaylandServer; + +namespace KWin +{ + +static NET::WindowType scopeToType(const QString &scope) +{ + static const QHash scopeToType { + { QStringLiteral("desktop"), NET::Desktop }, + { QStringLiteral("dock"), NET::Dock }, + { QStringLiteral("crititical-notification"), NET::CriticalNotification }, + { QStringLiteral("notification"), NET::Notification }, + { QStringLiteral("tooltip"), NET::Tooltip }, + { QStringLiteral("on-screen-display"), NET::OnScreenDisplay }, + { QStringLiteral("dialog"), NET::Dialog }, + { QStringLiteral("splash"), NET::Splash }, + { QStringLiteral("utility"), NET::Utility }, + }; + return scopeToType.value(scope.toLower(), NET::Normal); +} + +LayerShellV1Client::LayerShellV1Client(LayerSurfaceV1Interface *shellSurface, + AbstractOutput *output, + LayerShellV1Integration *integration) + : WaylandClient(shellSurface->surface()) + , m_output(output) + , m_integration(integration) + , m_shellSurface(shellSurface) + , m_windowType(scopeToType(shellSurface->scope())) +{ + setSkipSwitcher(!isDesktop()); + setSkipPager(true); + setSkipTaskbar(true); + setSizeSyncMode(SyncMode::Async); + setPositionSyncMode(SyncMode::Sync); + + connect(shellSurface, &LayerSurfaceV1Interface::aboutToBeDestroyed, + this, &LayerShellV1Client::destroyClient); + connect(shellSurface->surface(), &SurfaceInterface::aboutToBeDestroyed, + this, &LayerShellV1Client::destroyClient); + + connect(output, &AbstractOutput::geometryChanged, + this, &LayerShellV1Client::scheduleRearrange); + connect(output, &AbstractOutput::destroyed, + this, &LayerShellV1Client::handleOutputDestroyed); + + connect(shellSurface->surface(), &SurfaceInterface::sizeChanged, + this, &LayerShellV1Client::handleSizeChanged); + connect(shellSurface->surface(), &SurfaceInterface::unmapped, + this, &LayerShellV1Client::handleUnmapped); + connect(shellSurface->surface(), &SurfaceInterface::committed, + this, &LayerShellV1Client::handleCommitted); + + connect(shellSurface, &LayerSurfaceV1Interface::desiredSizeChanged, + this, &LayerShellV1Client::scheduleRearrange); + connect(shellSurface, &LayerSurfaceV1Interface::layerChanged, + this, &LayerShellV1Client::scheduleRearrange); + connect(shellSurface, &LayerSurfaceV1Interface::marginsChanged, + this, &LayerShellV1Client::scheduleRearrange); + connect(shellSurface, &LayerSurfaceV1Interface::anchorChanged, + this, &LayerShellV1Client::scheduleRearrange); + connect(shellSurface, &LayerSurfaceV1Interface::exclusiveZoneChanged, + this, &LayerShellV1Client::scheduleRearrange); + connect(shellSurface, &LayerSurfaceV1Interface::acceptsFocusChanged, + this, &LayerShellV1Client::handleAcceptsFocusChanged); +} + +LayerSurfaceV1Interface *LayerShellV1Client::shellSurface() const +{ + return m_shellSurface; +} + +AbstractOutput *LayerShellV1Client::output() const +{ + return m_output; +} + +void LayerShellV1Client::scheduleRearrange() +{ + m_integration->scheduleRearrange(); +} + +NET::WindowType LayerShellV1Client::windowType(bool, int) const +{ + return m_windowType; +} + +bool LayerShellV1Client::isPlaceable() const +{ + return false; +} + +bool LayerShellV1Client::isCloseable() const +{ + return true; +} + +bool LayerShellV1Client::isMovable() const +{ + return false; +} + +bool LayerShellV1Client::isMovableAcrossScreens() const +{ + return false; +} + +bool LayerShellV1Client::isResizable() const +{ + return false; +} + +bool LayerShellV1Client::isInitialPositionSet() const +{ + return true; +} + +bool LayerShellV1Client::takeFocus() +{ + setActive(true); + return true; +} + +bool LayerShellV1Client::wantsInput() const +{ + return acceptsFocus() && readyForPainting(); +} + +StrutRect LayerShellV1Client::strutRect(StrutArea area) const +{ + switch (area) { + case StrutAreaLeft: + if (m_shellSurface->exclusiveEdge() == Qt::LeftEdge) { + return StrutRect(x(), y(), m_shellSurface->exclusiveZone(), height(), StrutAreaLeft); + } + return StrutRect(); + case StrutAreaRight: + if (m_shellSurface->exclusiveEdge() == Qt::RightEdge) { + return StrutRect(x() + width() - m_shellSurface->exclusiveZone(), y(), + m_shellSurface->exclusiveZone(), height(), StrutAreaRight); + } + return StrutRect(); + case StrutAreaTop: + if (m_shellSurface->exclusiveEdge() == Qt::TopEdge) { + return StrutRect(x(), y(), width(), m_shellSurface->exclusiveZone(), StrutAreaTop); + } + return StrutRect(); + case StrutAreaBottom: + if (m_shellSurface->exclusiveEdge() == Qt::BottomEdge) { + return StrutRect(x(), y() + height() - m_shellSurface->exclusiveZone(), + width(), m_shellSurface->exclusiveZone(), StrutAreaBottom); + } + return StrutRect(); + default: + return StrutRect(); + } +} + +bool LayerShellV1Client::hasStrut() const +{ + return m_shellSurface->exclusiveZone() > 0; +} + +void LayerShellV1Client::destroyClient() +{ + markAsZombie(); + cleanTabBox(); + Deleted *deleted = Deleted::create(this); + emit windowClosed(this, deleted); + StackingUpdatesBlocker blocker(workspace()); + cleanGrouping(); + waylandServer()->removeClient(this); + deleted->unrefWindow(); + scheduleRearrange(); + delete this; +} + +void LayerShellV1Client::closeWindow() +{ + m_shellSurface->sendClosed(); +} + +Layer LayerShellV1Client::belongsToLayer() const +{ + if (!isNormalWindow()) { + return WaylandClient::belongsToLayer(); + } + switch (m_shellSurface->layer()) { + case LayerSurfaceV1Interface::BackgroundLayer: + return DesktopLayer; + case LayerSurfaceV1Interface::BottomLayer: + return BelowLayer; + case LayerSurfaceV1Interface::TopLayer: + return AboveLayer; + case LayerSurfaceV1Interface::OverlayLayer: + return UnmanagedLayer; + default: + Q_UNREACHABLE(); + } +} + +bool LayerShellV1Client::acceptsFocus() const +{ + return m_shellSurface->acceptsFocus(); +} + +void LayerShellV1Client::addDamage(const QRegion ®ion) +{ + addRepaint(region); + WaylandClient::addDamage(region); +} + +void LayerShellV1Client::requestGeometry(const QRect &rect) +{ + WaylandClient::requestGeometry(rect); + m_shellSurface->sendConfigure(rect.size()); +} + +void LayerShellV1Client::handleSizeChanged() +{ + updateGeometry(QRect(pos(), clientSizeToFrameSize(surface()->size()))); + scheduleRearrange(); +} + +void LayerShellV1Client::handleUnmapped() +{ + m_integration->recreateClient(shellSurface()); +} + +void LayerShellV1Client::handleCommitted() +{ + if (surface()->buffer()) { + updateDepth(); + setReadyForPainting(); + } +} + +void LayerShellV1Client::handleAcceptsFocusChanged() +{ + switch (m_shellSurface->layer()) { + case LayerSurfaceV1Interface::TopLayer: + case LayerSurfaceV1Interface::OverlayLayer: + if (wantsInput()) { + workspace()->activateClient(this); + } + break; + case LayerSurfaceV1Interface::BackgroundLayer: + case LayerSurfaceV1Interface::BottomLayer: + break; + } +} + +void LayerShellV1Client::handleOutputDestroyed() +{ + closeWindow(); + destroyClient(); +} + +} // namespace KWin diff --git a/layershellv1client.h b/layershellv1client.h new file mode 100644 index 0000000..c59cc01 --- /dev/null +++ b/layershellv1client.h @@ -0,0 +1,68 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "waylandclient.h" + +namespace KWaylandServer +{ +class LayerSurfaceV1Interface; +} + +namespace KWin +{ + +class AbstractOutput; +class LayerShellV1Integration; + +class LayerShellV1Client : public WaylandClient +{ + Q_OBJECT + +public: + explicit LayerShellV1Client(KWaylandServer::LayerSurfaceV1Interface *shellSurface, + AbstractOutput *output, + LayerShellV1Integration *integration); + + KWaylandServer::LayerSurfaceV1Interface *shellSurface() const; + AbstractOutput *output() const; + + NET::WindowType windowType(bool direct = false, int supported_types = 0) const override; + bool isPlaceable() const override; + bool isCloseable() const override; + bool isMovable() const override; + bool isMovableAcrossScreens() const override; + bool isResizable() const override; + bool isInitialPositionSet() const override; + bool takeFocus() override; + bool wantsInput() const override; + StrutRect strutRect(StrutArea area) const override; + bool hasStrut() const override; + void destroyClient() override; + void closeWindow() override; + +protected: + Layer belongsToLayer() const override; + bool acceptsFocus() const override; + void requestGeometry(const QRect &rect) override; + void addDamage(const QRegion ®ion) override; + +private: + void handleSizeChanged(); + void handleUnmapped(); + void handleCommitted(); + void handleAcceptsFocusChanged(); + void handleOutputDestroyed(); + void scheduleRearrange(); + + AbstractOutput *m_output; + LayerShellV1Integration *m_integration; + KWaylandServer::LayerSurfaceV1Interface *m_shellSurface; + NET::WindowType m_windowType; +}; + +} // namespace KWin diff --git a/layershellv1integration.cpp b/layershellv1integration.cpp new file mode 100644 index 0000000..ebf5b70 --- /dev/null +++ b/layershellv1integration.cpp @@ -0,0 +1,216 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "layershellv1integration.h" +#include "abstract_wayland_output.h" +#include "layershellv1client.h" +#include "platform.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include + +#include + +using namespace KWaylandServer; + +namespace KWin +{ + +static const Qt::Edges AnchorHorizontal = Qt::LeftEdge | Qt::RightEdge; +static const Qt::Edges AnchorVertical = Qt::TopEdge | Qt::BottomEdge; + +LayerShellV1Integration::LayerShellV1Integration(QObject *parent) + : WaylandShellIntegration(parent) +{ + LayerShellV1Interface *shell = waylandServer()->display()->createLayerShellV1(this); + connect(shell, &KWaylandServer::LayerShellV1Interface::surfaceCreated, + this, &LayerShellV1Integration::createClient); + + m_rearrangeTimer = new QTimer(this); + m_rearrangeTimer->setSingleShot(true); + connect(m_rearrangeTimer, &QTimer::timeout, this, &LayerShellV1Integration::rearrange); +} + +void LayerShellV1Integration::createClient(LayerSurfaceV1Interface *shellSurface) +{ + AbstractOutput *output = waylandServer()->findOutput(shellSurface->output()); + if (!output) { + output = kwinApp()->platform()->findOutput(screens()->current()); + } + if (!output) { + qCWarning(KWIN_CORE) << "Could not find any suitable output for a layer surface"; + shellSurface->sendClosed(); + return; + } + + emit clientCreated(new LayerShellV1Client(shellSurface, output, this)); +} + +void LayerShellV1Integration::recreateClient(LayerSurfaceV1Interface *shellSurface) +{ + destroyClient(shellSurface); + createClient(shellSurface); +} + +void LayerShellV1Integration::destroyClient(LayerSurfaceV1Interface *shellSurface) +{ + const QList clients = waylandServer()->clients(); + for (AbstractClient *client : clients) { + LayerShellV1Client *layerShellClient = qobject_cast(client); + if (layerShellClient && layerShellClient->shellSurface() == shellSurface) { + layerShellClient->destroyClient(); + break; + } + } +} + +static void adjustWorkArea(const LayerSurfaceV1Interface *shellSurface, QRect *workArea) +{ + switch (shellSurface->exclusiveEdge()) { + case Qt::LeftEdge: + workArea->adjust(shellSurface->leftMargin() + shellSurface->exclusiveZone(), 0, 0, 0); + break; + case Qt::RightEdge: + workArea->adjust(0, 0, -shellSurface->rightMargin() - shellSurface->exclusiveZone(), 0); + break; + case Qt::TopEdge: + workArea->adjust(0, shellSurface->topMargin() + shellSurface->exclusiveZone(), 0, 0); + break; + case Qt::BottomEdge: + workArea->adjust(0, 0, 0, -shellSurface->bottomMargin() - shellSurface->exclusiveZone()); + break; + } +} + +static void rearrangeLayer(const QList &clients, QRect *workArea, + LayerSurfaceV1Interface::Layer layer, bool exclusive) +{ + for (LayerShellV1Client *client : clients) { + LayerSurfaceV1Interface *shellSurface = client->shellSurface(); + + if (shellSurface->layer() != layer) { + continue; + } + if (exclusive != (shellSurface->exclusiveZone() > 0)) { + continue; + } + + QRect bounds; + if (shellSurface->exclusiveZone() == -1) { + bounds = workspace()->clientArea(ScreenArea, client); + } else { + bounds = *workArea; + } + + QRect geometry(QPoint(0, 0), shellSurface->desiredSize()); + + if ((shellSurface->anchor() & AnchorHorizontal) && geometry.width() == 0) { + geometry.setLeft(bounds.left()); + geometry.setWidth(bounds.width()); + } else if (shellSurface->anchor() & Qt::LeftEdge) { + geometry.moveLeft(bounds.left()); + } else if (shellSurface->anchor() & Qt::RightEdge) { + geometry.moveRight(bounds.right()); + } else { + geometry.moveLeft(bounds.left() + (bounds.width() - geometry.width()) / 2); + } + + if ((shellSurface->anchor() & AnchorVertical) && geometry.height() == 0) { + geometry.setTop(bounds.top()); + geometry.setHeight(bounds.height()); + } else if (shellSurface->anchor() & Qt::TopEdge) { + geometry.moveTop(bounds.top()); + } else if (shellSurface->anchor() & Qt::BottomEdge) { + geometry.moveBottom(bounds.bottom()); + } else { + geometry.moveTop(bounds.top() + (bounds.height() - geometry.height()) / 2); + } + + if ((shellSurface->anchor() & AnchorHorizontal) == AnchorHorizontal) { + geometry.adjust(shellSurface->leftMargin(), 0, -shellSurface->rightMargin(), 0); + } else if (shellSurface->anchor() & Qt::LeftEdge) { + geometry.translate(shellSurface->leftMargin(), 0); + } else if (shellSurface->anchor() & Qt::RightEdge) { + geometry.translate(-shellSurface->rightMargin(), 0); + } + + if ((shellSurface->anchor() & AnchorVertical) == AnchorVertical) { + geometry.adjust(0, shellSurface->topMargin(), 0, -shellSurface->bottomMargin()); + } else if (shellSurface->anchor() & Qt::TopEdge) { + geometry.translate(0, shellSurface->topMargin()); + } else if (shellSurface->anchor() & Qt::BottomEdge) { + geometry.translate(0, -shellSurface->bottomMargin()); + } + + if (geometry.isValid()) { + client->setFrameGeometry(geometry); + } else { + qCWarning(KWIN_CORE) << "Closing a layer shell client due to invalid geometry"; + client->closeWindow(); + continue; + } + + if (exclusive && shellSurface->exclusiveZone() > 0) { + adjustWorkArea(shellSurface, workArea); + } + } +} + +static QList clientsForOutput(AbstractOutput *output) +{ + QList result; + const QList clients = waylandServer()->clients(); + for (AbstractClient *client : clients) { + LayerShellV1Client *layerShellClient = qobject_cast(client); + if (!layerShellClient || layerShellClient->output() != output) { + continue; + } + if (layerShellClient->shellSurface()->isCommitted()) { + result.append(layerShellClient); + } + } + return result; +} + +static void rearrangeOutput(AbstractOutput *output) +{ + const QList clients = clientsForOutput(output); + if (!clients.isEmpty()) { + QRect workArea = output->geometry(); + + rearrangeLayer(clients, &workArea, LayerSurfaceV1Interface::OverlayLayer, true); + rearrangeLayer(clients, &workArea, LayerSurfaceV1Interface::TopLayer, true); + rearrangeLayer(clients, &workArea, LayerSurfaceV1Interface::BottomLayer, true); + rearrangeLayer(clients, &workArea, LayerSurfaceV1Interface::BackgroundLayer, true); + + rearrangeLayer(clients, &workArea, LayerSurfaceV1Interface::OverlayLayer, false); + rearrangeLayer(clients, &workArea, LayerSurfaceV1Interface::TopLayer, false); + rearrangeLayer(clients, &workArea, LayerSurfaceV1Interface::BottomLayer, false); + rearrangeLayer(clients, &workArea, LayerSurfaceV1Interface::BackgroundLayer, false); + } +} + +void LayerShellV1Integration::rearrange() +{ + m_rearrangeTimer->stop(); + + const QVector outputs = kwinApp()->platform()->outputs(); + for (AbstractOutput *output : outputs) { + rearrangeOutput(output); + } + + workspace()->updateClientArea(); +} + +void LayerShellV1Integration::scheduleRearrange() +{ + m_rearrangeTimer->start(); +} + +} // namespace KWin diff --git a/layershellv1integration.h b/layershellv1integration.h new file mode 100644 index 0000000..0ccf4bb --- /dev/null +++ b/layershellv1integration.h @@ -0,0 +1,37 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "waylandshellintegration.h" + +namespace KWaylandServer +{ +class LayerSurfaceV1Interface; +} + +namespace KWin +{ + +class LayerShellV1Integration : public WaylandShellIntegration +{ + Q_OBJECT + +public: + explicit LayerShellV1Integration(QObject *parent = nullptr); + + void rearrange(); + void scheduleRearrange(); + + void createClient(KWaylandServer::LayerSurfaceV1Interface *shellSurface); + void recreateClient(KWaylandServer::LayerSurfaceV1Interface *shellSurface); + void destroyClient(KWaylandServer::LayerSurfaceV1Interface *shellSurface); + +private: + QTimer *m_rearrangeTimer; +}; + +} // namespace KWin diff --git a/libinput/connection.cpp b/libinput/connection.cpp new file mode 100644 index 0000000..d66d361 --- /dev/null +++ b/libinput/connection.cpp @@ -0,0 +1,815 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "connection.h" +#include "context.h" +#include "device.h" +#include "events.h" + +// TODO: Make it compile also in testing environment +#ifndef KWIN_BUILD_TESTING +#include "../abstract_wayland_output.h" +#include "../main.h" +#include "../platform.h" +#include "../screens.h" +#endif + +#include "../logind.h" +#include "../udev.h" +#include "libinput_logging.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace KWin +{ +namespace LibInput +{ + +class ConnectionAdaptor : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.KWin.InputDeviceManager") + Q_PROPERTY(QStringList devicesSysNames READ devicesSysNames CONSTANT) + +private: + Connection *m_con; + +public: + ConnectionAdaptor(Connection *con) + : m_con(con) + { + connect(con, &Connection::deviceAddedSysName, this, &ConnectionAdaptor::deviceAdded, Qt::QueuedConnection); + connect(con, &Connection::deviceRemovedSysName, this, &ConnectionAdaptor::deviceRemoved, Qt::QueuedConnection); + + QDBusConnection::sessionBus().registerObject(QStringLiteral("/org/kde/KWin/InputDevice"), + QStringLiteral("org.kde.KWin.InputDeviceManager"), + this, + QDBusConnection::ExportAllProperties | QDBusConnection::ExportAllSignals + ); + } + + ~ConnectionAdaptor() override { + QDBusConnection::sessionBus().unregisterObject(QStringLiteral("/org/kde/KWin/InputDeviceManager")); + } + + QStringList devicesSysNames() { + // TODO: is this allowed? directly calling function of object in another thread!? + // otherwise use signal-slot mechanism + return m_con->devicesSysNames(); + } + +Q_SIGNALS: + void deviceAdded(QString sysName); + void deviceRemoved(QString sysName); + +}; + +Connection *Connection::s_self = nullptr; +QPointer Connection::s_thread; + +static ConnectionAdaptor *s_adaptor = nullptr; +static Context *s_context = nullptr; + +static quint32 toLibinputLEDS(Xkb::LEDs leds) +{ + quint32 libinputLeds = 0; + if (leds.testFlag(Xkb::LED::NumLock)) { + libinputLeds = libinputLeds | LIBINPUT_LED_NUM_LOCK; + } + if (leds.testFlag(Xkb::LED::CapsLock)) { + libinputLeds = libinputLeds | LIBINPUT_LED_CAPS_LOCK; + } + if (leds.testFlag(Xkb::LED::ScrollLock)) { + libinputLeds = libinputLeds | LIBINPUT_LED_SCROLL_LOCK; + } + return libinputLeds; +} + +Connection::Connection(QObject *parent) + : Connection(nullptr, parent) +{ + // only here to fix build, using will crash, BUG 343529 +} + +void Connection::createThread() +{ + if (s_thread) { + return; + } + s_thread = new QThread(); + s_thread->setObjectName(QStringLiteral("libinput-connection")); + s_thread->start(); +} + +Connection *Connection::create(QObject *parent) +{ + Q_ASSERT(!s_self); + static Udev s_udev; + if (!s_udev.isValid()) { + qCWarning(KWIN_LIBINPUT) << "Failed to initialize udev"; + return nullptr; + } + if (!s_context) { + s_context = new Context(s_udev); + if (!s_context->isValid()) { + qCWarning(KWIN_LIBINPUT) << "Failed to create context from udev"; + delete s_context; + s_context = nullptr; + return nullptr; + } + if (!s_context->assignSeat(LogindIntegration::self()->seat().toUtf8().constData())) { + qCWarning(KWIN_LIBINPUT) << "Failed to assign seat" << LogindIntegration::self()->seat(); + delete s_context; + s_context = nullptr; + return nullptr; + } + } + Connection::createThread(); + s_self = new Connection(s_context); + s_self->moveToThread(s_thread); + QObject::connect(s_thread, &QThread::finished, s_self, &QObject::deleteLater); + QObject::connect(s_thread, &QThread::finished, s_thread, &QObject::deleteLater); + QObject::connect(parent, &QObject::destroyed, s_thread, &QThread::quit); + if (!s_adaptor) { + s_adaptor = new ConnectionAdaptor(s_self); + } + + return s_self; +} + + +Connection::Connection(Context *input, QObject *parent) + : QObject(parent) + , m_input(input) + , m_notifier(nullptr) + , m_mutex(QMutex::Recursive) + , m_leds() +{ + Q_ASSERT(m_input); + // need to connect to KGlobalSettings as the mouse KCM does not emit a dedicated signal + QDBusConnection::sessionBus().connect(QString(), QStringLiteral("/KGlobalSettings"), QStringLiteral("org.kde.KGlobalSettings"), + QStringLiteral("notifyChange"), this, SLOT(slotKGlobalSettingsNotifyChange(int,int))); +} + +Connection::~Connection() +{ + delete s_adaptor; + s_adaptor = nullptr; + s_self = nullptr; + delete s_context; + s_context = nullptr; +} + +void Connection::setup() +{ + QMetaObject::invokeMethod(this, "doSetup", Qt::QueuedConnection); +} + +void Connection::doSetup() +{ + connect(s_self, &Connection::deviceAdded, s_self, [](Device* device) { + emit s_self->deviceAddedSysName(device->sysName()); + }); + connect(s_self, &Connection::deviceRemoved, s_self, [](Device* device) { + emit s_self->deviceRemovedSysName(device->sysName()); + }); + + Q_ASSERT(!m_notifier); + m_notifier = new QSocketNotifier(m_input->fileDescriptor(), QSocketNotifier::Read, this); + connect(m_notifier, &QSocketNotifier::activated, this, &Connection::handleEvent); + + LogindIntegration *logind = LogindIntegration::self(); + connect(logind, &LogindIntegration::sessionActiveChanged, this, + [this](bool active) { + if (active) { + if (!m_input->isSuspended()) { + return; + } + m_input->resume(); + wasSuspended = true; + } else { + deactivate(); + } + } + ); + handleEvent(); +} + +void Connection::deactivate() +{ + if (m_input->isSuspended()) { + return; + } + m_keyboardBeforeSuspend = hasKeyboard(); + m_alphaNumericKeyboardBeforeSuspend = hasAlphaNumericKeyboard(); + m_pointerBeforeSuspend = hasPointer(); + m_touchBeforeSuspend = hasTouch(); + m_tabletModeSwitchBeforeSuspend = hasTabletModeSwitch(); + m_input->suspend(); + handleEvent(); +} + +void Connection::handleEvent() +{ + QMutexLocker locker(&m_mutex); + const bool wasEmpty = m_eventQueue.isEmpty(); + do { + m_input->dispatch(); + Event *event = m_input->event(); + if (!event) { + break; + } + m_eventQueue << event; + } while (true); + if (wasEmpty && !m_eventQueue.isEmpty()) { + emit eventsRead(); + } +} + +#ifndef KWIN_BUILD_TESTING +QPointF devicePointToGlobalPosition(const QPointF &devicePos, const AbstractWaylandOutput *output) +{ + using Transform = AbstractWaylandOutput::Transform; + + QPointF pos = devicePos; + // TODO: Do we need to handle the flipped cases differently? + switch (output->transform()) { + case Transform::Normal: + case Transform::Flipped: + break; + case Transform::Rotated90: + case Transform::Flipped90: + pos = QPointF(output->modeSize().height() - devicePos.y(), devicePos.x()); + break; + case Transform::Rotated180: + case Transform::Flipped180: + pos = QPointF(output->modeSize().width() - devicePos.x(), + output->modeSize().height() - devicePos.y()); + break; + case Transform::Rotated270: + case Transform::Flipped270: + pos = QPointF(devicePos.y(), output->modeSize().width() - devicePos.x()); + break; + default: + Q_UNREACHABLE(); + } + return output->geometry().topLeft() + pos / output->scale(); +} +#endif + +void Connection::processEvents() +{ + QMutexLocker locker(&m_mutex); + while (!m_eventQueue.isEmpty()) { + QScopedPointer event(m_eventQueue.takeFirst()); + switch (event->type()) { + case LIBINPUT_EVENT_DEVICE_ADDED: { + auto device = new Device(event->nativeDevice()); + device->moveToThread(s_thread); + m_devices << device; + if (device->isKeyboard()) { + m_keyboard++; + if (device->isAlphaNumericKeyboard()) { + m_alphaNumericKeyboard++; + if (m_alphaNumericKeyboard == 1) { + emit hasAlphaNumericKeyboardChanged(true); + } + } + if (m_keyboard == 1) { + emit hasKeyboardChanged(true); + } + } + if (device->isPointer()) { + m_pointer++; + if (m_pointer == 1) { + emit hasPointerChanged(true); + } + } + if (device->isTouch()) { + m_touch++; + if (m_touch == 1) { + emit hasTouchChanged(true); + } + } + if (device->isTabletModeSwitch()) { + m_tabletModeSwitch++; + if (m_tabletModeSwitch == 1) { + emit hasTabletModeSwitchChanged(true); + } + } + applyDeviceConfig(device); + applyScreenToDevice(device); + + // enable possible leds + libinput_device_led_update(device->device(), static_cast(toLibinputLEDS(m_leds))); + + emit deviceAdded(device); + break; + } + case LIBINPUT_EVENT_DEVICE_REMOVED: { + auto it = std::find_if(m_devices.begin(), m_devices.end(), [&event] (Device *d) { return event->device() == d; } ); + if (it == m_devices.end()) { + // we don't know this device + break; + } + auto device = *it; + m_devices.erase(it); + emit deviceRemoved(device); + + if (device->isKeyboard()) { + m_keyboard--; + if (device->isAlphaNumericKeyboard()) { + m_alphaNumericKeyboard--; + if (m_alphaNumericKeyboard == 0) { + emit hasAlphaNumericKeyboardChanged(false); + } + } + if (m_keyboard == 0) { + emit hasKeyboardChanged(false); + } + } + if (device->isPointer()) { + m_pointer--; + if (m_pointer == 0) { + emit hasPointerChanged(false); + } + } + if (device->isTouch()) { + m_touch--; + if (m_touch == 0) { + emit hasTouchChanged(false); + } + } + if (device->isTabletModeSwitch()) { + m_tabletModeSwitch--; + if (m_tabletModeSwitch == 0) { + emit hasTabletModeSwitchChanged(false); + } + } + device->deleteLater(); + break; + } + case LIBINPUT_EVENT_KEYBOARD_KEY: { + KeyEvent *ke = static_cast(event.data()); + emit keyChanged(ke->key(), ke->state(), ke->time(), ke->device()); + break; + } + case LIBINPUT_EVENT_POINTER_AXIS: { + PointerEvent *pe = static_cast(event.data()); + const auto axes = pe->axis(); + for (const InputRedirection::PointerAxis &axis : axes) { + emit pointerAxisChanged(axis, pe->axisValue(axis), pe->discreteAxisValue(axis), + pe->axisSource(), pe->time(), pe->device()); + } + break; + } + case LIBINPUT_EVENT_POINTER_BUTTON: { + PointerEvent *pe = static_cast(event.data()); + emit pointerButtonChanged(pe->button(), pe->buttonState(), pe->time(), pe->device()); + break; + } + case LIBINPUT_EVENT_POINTER_MOTION: { + PointerEvent *pe = static_cast(event.data()); + auto delta = pe->delta(); + auto deltaNonAccel = pe->deltaUnaccelerated(); + quint32 latestTime = pe->time(); + quint64 latestTimeUsec = pe->timeMicroseconds(); + auto it = m_eventQueue.begin(); + while (it != m_eventQueue.end()) { + if ((*it)->type() == LIBINPUT_EVENT_POINTER_MOTION) { + QScopedPointer p(static_cast(*it)); + delta += p->delta(); + deltaNonAccel += p->deltaUnaccelerated(); + latestTime = p->time(); + latestTimeUsec = p->timeMicroseconds(); + it = m_eventQueue.erase(it); + } else { + break; + } + } + emit pointerMotion(delta, deltaNonAccel, latestTime, latestTimeUsec, pe->device()); + break; + } + case LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE: { + PointerEvent *pe = static_cast(event.data()); + emit pointerMotionAbsolute(pe->absolutePos(), pe->absolutePos(m_size), pe->time(), pe->device()); + break; + } + case LIBINPUT_EVENT_TOUCH_DOWN: { +#ifndef KWIN_BUILD_TESTING + TouchEvent *te = static_cast(event.data()); + const auto *output = static_cast( + kwinApp()->platform()->enabledOutputs()[te->device()->screenId()]); + const QPointF globalPos = + devicePointToGlobalPosition(te->absolutePos(output->modeSize()), + output); + emit touchDown(te->id(), globalPos, te->time(), te->device()); + break; +#endif + } + case LIBINPUT_EVENT_TOUCH_UP: { + TouchEvent *te = static_cast(event.data()); + emit touchUp(te->id(), te->time(), te->device()); + break; + } + case LIBINPUT_EVENT_TOUCH_MOTION: { +#ifndef KWIN_BUILD_TESTING + TouchEvent *te = static_cast(event.data()); + const auto *output = static_cast( + kwinApp()->platform()->enabledOutputs()[te->device()->screenId()]); + const QPointF globalPos = + devicePointToGlobalPosition(te->absolutePos(output->modeSize()), + output); + emit touchMotion(te->id(), globalPos, te->time(), te->device()); + break; +#endif + } + case LIBINPUT_EVENT_TOUCH_CANCEL: { + emit touchCanceled(event->device()); + break; + } + case LIBINPUT_EVENT_TOUCH_FRAME: { + emit touchFrame(event->device()); + break; + } + case LIBINPUT_EVENT_GESTURE_PINCH_BEGIN: { + PinchGestureEvent *pe = static_cast(event.data()); + emit pinchGestureBegin(pe->fingerCount(), pe->time(), pe->device()); + break; + } + case LIBINPUT_EVENT_GESTURE_PINCH_UPDATE: { + PinchGestureEvent *pe = static_cast(event.data()); + emit pinchGestureUpdate(pe->scale(), pe->angleDelta(), pe->delta(), pe->time(), pe->device()); + break; + } + case LIBINPUT_EVENT_GESTURE_PINCH_END: { + PinchGestureEvent *pe = static_cast(event.data()); + if (pe->isCancelled()) { + emit pinchGestureCancelled(pe->time(), pe->device()); + } else { + emit pinchGestureEnd(pe->time(), pe->device()); + } + break; + } + case LIBINPUT_EVENT_GESTURE_SWIPE_BEGIN: { + SwipeGestureEvent *se = static_cast(event.data()); + emit swipeGestureBegin(se->fingerCount(), se->time(), se->device()); + break; + } + case LIBINPUT_EVENT_GESTURE_SWIPE_UPDATE: { + SwipeGestureEvent *se = static_cast(event.data()); + emit swipeGestureUpdate(se->delta(), se->time(), se->device()); + break; + } + case LIBINPUT_EVENT_GESTURE_SWIPE_END: { + SwipeGestureEvent *se = static_cast(event.data()); + if (se->isCancelled()) { + emit swipeGestureCancelled(se->time(), se->device()); + } else { + emit swipeGestureEnd(se->time(), se->device()); + } + break; + } + case LIBINPUT_EVENT_SWITCH_TOGGLE: { + SwitchEvent *se = static_cast(event.data()); + switch (se->state()) { + case SwitchEvent::State::Off: + emit switchToggledOff(se->time(), se->timeMicroseconds(), se->device()); + break; + case SwitchEvent::State::On: + emit switchToggledOn(se->time(), se->timeMicroseconds(), se->device()); + break; + default: + Q_UNREACHABLE(); + } + break; + } + case LIBINPUT_EVENT_TABLET_TOOL_AXIS: + case LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY: + case LIBINPUT_EVENT_TABLET_TOOL_TIP: { + auto *tte = static_cast(event.data()); + + KWin::InputRedirection::TabletEventType tabletEventType; + switch (event->type()) { + case LIBINPUT_EVENT_TABLET_TOOL_AXIS: + tabletEventType = KWin::InputRedirection::Axis; + break; + case LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY: + tabletEventType = KWin::InputRedirection::Proximity; + break; + case LIBINPUT_EVENT_TABLET_TOOL_TIP: + default: + tabletEventType = KWin::InputRedirection::Tip; + break; + } + auto serial = libinput_tablet_tool_get_serial(tte->tool()); + auto toolId = libinput_tablet_tool_get_tool_id(tte->tool()); + auto type = libinput_tablet_tool_get_type(tte->tool()); + InputRedirection::TabletToolType toolType; + switch (type) { + case LIBINPUT_TABLET_TOOL_TYPE_PEN: + toolType = InputRedirection::Pen; + break; + case LIBINPUT_TABLET_TOOL_TYPE_ERASER: + toolType = InputRedirection::Eraser; + break; + case LIBINPUT_TABLET_TOOL_TYPE_BRUSH: + toolType = InputRedirection::Brush; + break; + case LIBINPUT_TABLET_TOOL_TYPE_PENCIL: + toolType = InputRedirection::Pencil; + break; + case LIBINPUT_TABLET_TOOL_TYPE_AIRBRUSH: + toolType = InputRedirection::Airbrush; + break; + case LIBINPUT_TABLET_TOOL_TYPE_MOUSE: + toolType = InputRedirection::Mouse; + break; + case LIBINPUT_TABLET_TOOL_TYPE_LENS: + toolType = InputRedirection::Lens; + break; +#ifdef LIBINPUT_HAS_TOTEM + case LIBINPUT_TABLET_TOOL_TYPE_TOTEM: + toolType = InputRedirection::Totem; + break; +#endif + } + QVector capabilities; + if (libinput_tablet_tool_has_pressure(tte->tool())) { + capabilities << InputRedirection::Pressure; + } + if (libinput_tablet_tool_has_distance(tte->tool())) { + capabilities << InputRedirection::Distance; + } + if (libinput_tablet_tool_has_rotation(tte->tool())) { + capabilities << InputRedirection::Rotation; + } + if (libinput_tablet_tool_has_tilt(tte->tool())) { + capabilities << InputRedirection::Tilt; + } + if (libinput_tablet_tool_has_slider(tte->tool())) { + capabilities << InputRedirection::Slider; + } + if (libinput_tablet_tool_has_wheel(tte->tool())) { + capabilities << InputRedirection::Wheel; + } + +#ifndef KWIN_BUILD_TESTING + const auto *output = static_cast( + kwinApp()->platform()->enabledOutputs()[tte->device()->screenId()]); + const QPointF globalPos = + devicePointToGlobalPosition(tte->transformedPosition(output->modeSize()), + output); +#else + const QPointF globalPos; +#endif + emit tabletToolEvent(tabletEventType, + globalPos, tte->pressure(), + tte->xTilt(), tte->yTilt(), tte->rotation(), + tte->isTipDown(), tte->isNearby(), serial, + toolId, toolType, capabilities, tte->time(), + event->device()); + break; + } + case LIBINPUT_EVENT_TABLET_TOOL_BUTTON: { + auto *tabletEvent = static_cast(event.data()); + emit tabletToolButtonEvent(tabletEvent->buttonId(), + tabletEvent->isButtonPressed()); + break; + } + case LIBINPUT_EVENT_TABLET_PAD_BUTTON: { + auto *tabletEvent = static_cast(event.data()); + emit tabletPadButtonEvent(tabletEvent->buttonId(), + tabletEvent->isButtonPressed()); + break; + } + case LIBINPUT_EVENT_TABLET_PAD_RING: { + auto *tabletEvent = static_cast(event.data()); + emit tabletPadRingEvent(tabletEvent->number(), + tabletEvent->position(), + tabletEvent->source() == + LIBINPUT_TABLET_PAD_RING_SOURCE_FINGER); + break; + } + case LIBINPUT_EVENT_TABLET_PAD_STRIP: { + auto *tabletEvent = static_cast(event.data()); + emit tabletPadStripEvent(tabletEvent->number(), + tabletEvent->position(), + tabletEvent->source() == + LIBINPUT_TABLET_PAD_STRIP_SOURCE_FINGER); + break; + } + default: + // nothing + break; + } + } + if (wasSuspended) { + if (m_keyboardBeforeSuspend && !m_keyboard) { + emit hasKeyboardChanged(false); + } + if (m_alphaNumericKeyboardBeforeSuspend && !m_alphaNumericKeyboard) { + emit hasAlphaNumericKeyboardChanged(false); + } + if (m_pointerBeforeSuspend && !m_pointer) { + emit hasPointerChanged(false); + } + if (m_touchBeforeSuspend && !m_touch) { + emit hasTouchChanged(false); + } + if (m_tabletModeSwitchBeforeSuspend && !m_tabletModeSwitch) { + emit hasTabletModeSwitchChanged(false); + } + wasSuspended = false; + } +} + +void Connection::setScreenSize(const QSize &size) +{ + m_size = size; +} + +void Connection::updateScreens() +{ + QMutexLocker locker(&m_mutex); + for (auto device: qAsConst(m_devices)) { + applyScreenToDevice(device); + } +} + + +void Connection::applyScreenToDevice(Device *device) +{ +#ifndef KWIN_BUILD_TESTING + QMutexLocker locker(&m_mutex); + if (!device->isTouch()) { + return; + } + int id = -1; + // let's try to find a screen for it + if (screens()->count() == 1) { + id = 0; + } + if (id == -1 && !device->outputName().isEmpty()) { + // we have an output name, try to find a screen with matching name + for (int i = 0; i < screens()->count(); i++) { + if (screens()->name(i) == device->outputName()) { + id = i; + break; + } + } + } + if (id == -1) { + // do we have an internal screen? + int internalId = -1; + for (int i = 0; i < screens()->count(); i++) { + if (screens()->isInternal(i)) { + internalId = i; + break; + } + } + auto testScreenMatches = [device] (int id) { + const auto &size = device->size(); + const auto &screenSize = screens()->physicalSize(id); + return std::round(size.width()) == std::round(screenSize.width()) + && std::round(size.height()) == std::round(screenSize.height()); + }; + if (internalId != -1 && testScreenMatches(internalId)) { + id = internalId; + } + // let's compare all screens for size + for (int i = 0; i < screens()->count(); i++) { + if (testScreenMatches(i)) { + id = i; + break; + } + } + if (id == -1) { + // still not found + if (internalId != -1) { + // we have an internal id, so let's use that + id = internalId; + } else { + // just take first screen, we have no clue + id = 0; + } + } + } + device->setScreenId(id); + + // TODO: this is currently non-functional even on DRM. Needs orientation() override there. + device->setOrientation(screens()->orientation(id)); +#else + Q_UNUSED(device) +#endif +} + +bool Connection::isSuspended() const +{ + if (!s_context) { + return false; + } + return s_context->isSuspended(); +} + +void Connection::applyDeviceConfig(Device *device) +{ + // pass configuration to Device + device->setConfig(m_config->group("Libinput").group(QString::number(device->vendor())).group(QString::number(device->product())).group(device->name())); + device->loadConfiguration(); +} + +void Connection::slotKGlobalSettingsNotifyChange(int type, int arg) +{ + if (type == 3 /**SettingsChanged**/ && arg == 0 /** SETTINGS_MOUSE */) { + m_config->reparseConfiguration(); + for (auto it = m_devices.constBegin(), end = m_devices.constEnd(); it != end; ++it) { + if ((*it)->isPointer()) { + applyDeviceConfig(*it); + } + } + } +} + +void Connection::toggleTouchpads() +{ + bool changed = false; + m_touchpadsEnabled = !m_touchpadsEnabled; + for (auto it = m_devices.constBegin(); it != m_devices.constEnd(); ++it) { + auto device = *it; + if (!device->isTouchpad()) { + continue; + } + const bool old = device->isEnabled(); + device->setEnabled(m_touchpadsEnabled); + if (old != device->isEnabled()) { + changed = true; + } + } + if (changed) { + // send OSD message + QDBusMessage msg = QDBusMessage::createMethodCall( + QStringLiteral("org.kde.plasmashell"), + QStringLiteral("/org/kde/osdService"), + QStringLiteral("org.kde.osdService"), + QStringLiteral("touchpadEnabledChanged") + ); + msg.setArguments({m_touchpadsEnabled}); + QDBusConnection::sessionBus().asyncCall(msg); + } +} + +void Connection::enableTouchpads() +{ + if (m_touchpadsEnabled) { + return; + } + toggleTouchpads(); +} + +void Connection::disableTouchpads() +{ + if (!m_touchpadsEnabled) { + return; + } + toggleTouchpads(); +} + +void Connection::updateLEDs(Xkb::LEDs leds) +{ + if (m_leds == leds) { + return; + } + m_leds = leds; + // update on devices + const libinput_led l = static_cast(toLibinputLEDS(leds)); + for (auto it = m_devices.constBegin(), end = m_devices.constEnd(); it != end; ++it) { + libinput_device_led_update((*it)->device(), l); + } +} + +QStringList Connection::devicesSysNames() const { + QStringList sl; + foreach (Device *d, m_devices) { + sl.append(d->sysName()); + } + return sl; +} + +} +} + +#include "connection.moc" diff --git a/libinput/connection.h b/libinput/connection.h new file mode 100644 index 0000000..06505a4 --- /dev/null +++ b/libinput/connection.h @@ -0,0 +1,175 @@ +/* + 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 +*/ +#ifndef KWIN_LIBINPUT_CONNECTION_H +#define KWIN_LIBINPUT_CONNECTION_H + +#include "../input.h" +#include "../keyboard_input.h" +#include + +#include +#include +#include +#include +#include +#include + +class QSocketNotifier; +class QThread; + +namespace KWin +{ +namespace LibInput +{ + +class Event; +class Device; +class Context; + +class KWIN_EXPORT Connection : public QObject +{ + Q_OBJECT + +public: + ~Connection() override; + + void setInputConfig(const KSharedConfigPtr &config) { + m_config = config; + } + + void setup(); + /** + * Sets the screen @p size. This is needed for mapping absolute pointer events to + * the screen data. + */ + void setScreenSize(const QSize &size); + + void updateScreens(); + + bool hasKeyboard() const { + return m_keyboard > 0; + } + bool hasAlphaNumericKeyboard() const { + return m_alphaNumericKeyboard > 0; + } + bool hasTouch() const { + return m_touch > 0; + } + bool hasPointer() const { + return m_pointer > 0; + } + bool hasTabletModeSwitch() const { + return m_tabletModeSwitch > 0; + } + + bool isSuspended() const; + + void deactivate(); + + void processEvents(); + + void toggleTouchpads(); + void enableTouchpads(); + void disableTouchpads(); + + QVector devices() const { + return m_devices; + } + + QStringList devicesSysNames() const; + + void updateLEDs(KWin::Xkb::LEDs leds); + + static void createThread(); + +Q_SIGNALS: + void keyChanged(quint32 key, KWin::InputRedirection::KeyboardKeyState, quint32 time, KWin::LibInput::Device *device); + void pointerButtonChanged(quint32 button, KWin::InputRedirection::PointerButtonState state, quint32 time, KWin::LibInput::Device *device); + void pointerMotionAbsolute(QPointF orig, QPointF screen, quint32 time, KWin::LibInput::Device *device); + void pointerMotion(const QSizeF &delta, const QSizeF &deltaNonAccelerated, quint32 time, quint64 timeMicroseconds, KWin::LibInput::Device *device); + void pointerAxisChanged(KWin::InputRedirection::PointerAxis axis, qreal delta, qint32 discreteDelta, + KWin::InputRedirection::PointerAxisSource source, quint32 time, KWin::LibInput::Device *device); + void touchFrame(KWin::LibInput::Device *device); + void touchCanceled(KWin::LibInput::Device *device); + void touchDown(qint32 id, const QPointF &absolutePos, quint32 time, KWin::LibInput::Device *device); + void touchUp(qint32 id, quint32 time, KWin::LibInput::Device *device); + void touchMotion(qint32 id, const QPointF &absolutePos, quint32 time, KWin::LibInput::Device *device); + void hasKeyboardChanged(bool); + void hasAlphaNumericKeyboardChanged(bool); + void hasPointerChanged(bool); + void hasTouchChanged(bool); + void hasTabletModeSwitchChanged(bool); + void deviceAdded(KWin::LibInput::Device *); + void deviceRemoved(KWin::LibInput::Device *); + void deviceAddedSysName(QString); + void deviceRemovedSysName(QString); + void swipeGestureBegin(int fingerCount, quint32 time, KWin::LibInput::Device *device); + void swipeGestureUpdate(const QSizeF &delta, quint32 time, KWin::LibInput::Device *device); + void swipeGestureEnd(quint32 time, KWin::LibInput::Device *device); + void swipeGestureCancelled(quint32 time, KWin::LibInput::Device *device); + void pinchGestureBegin(int fingerCount, quint32 time, KWin::LibInput::Device *device); + void pinchGestureUpdate(qreal scale, qreal angleDelta, const QSizeF &delta, quint32 time, KWin::LibInput::Device *device); + void pinchGestureEnd(quint32 time, KWin::LibInput::Device *device); + void pinchGestureCancelled(quint32 time, KWin::LibInput::Device *device); + void switchToggledOn(quint32 time, quint64 timeMicroseconds, KWin::LibInput::Device *device); + void switchToggledOff(quint32 time, quint64 timeMicroseconds, KWin::LibInput::Device *device); + + void tabletToolEvent(KWin::InputRedirection::TabletEventType type, const QPointF &pos, + qreal pressure, int xTilt, int yTilt, qreal rotation, bool tipDown, + bool tipNear, quint64 serialId, quint64 toolId, + InputRedirection::TabletToolType toolType, + const QVector &capabilities, + quint32 time, + LibInput::Device *device); + void tabletToolButtonEvent(uint button, bool isPressed); + + void tabletPadButtonEvent(uint button, bool isPressed); + void tabletPadStripEvent(int number, int position, bool isFinger); + void tabletPadRingEvent(int number, int position, bool isFinger); + + void eventsRead(); + +private Q_SLOTS: + void doSetup(); + void slotKGlobalSettingsNotifyChange(int type, int arg); + +private: + Connection(Context *input, QObject *parent = nullptr); + void handleEvent(); + void applyDeviceConfig(Device *device); + void applyScreenToDevice(Device *device); + Context *m_input; + QSocketNotifier *m_notifier; + QSize m_size; + int m_keyboard = 0; + int m_alphaNumericKeyboard = 0; + int m_pointer = 0; + int m_touch = 0; + int m_tabletModeSwitch = 0; + bool m_keyboardBeforeSuspend = false; + bool m_alphaNumericKeyboardBeforeSuspend = false; + bool m_pointerBeforeSuspend = false; + bool m_touchBeforeSuspend = false; + bool m_tabletModeSwitchBeforeSuspend = false; + QMutex m_mutex; + QVector m_eventQueue; + bool wasSuspended = false; + QVector m_devices; + KSharedConfigPtr m_config; + bool m_touchpadsEnabled = true; + Xkb::LEDs m_leds; + + KWIN_SINGLETON(Connection) + static QPointer s_thread; +}; + +} +} + +#endif diff --git a/libinput/context.cpp b/libinput/context.cpp new file mode 100644 index 0000000..00aa984 --- /dev/null +++ b/libinput/context.cpp @@ -0,0 +1,174 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "context.h" +#include "events.h" +#include "libinput_logging.h" +#include "../logind.h" +#include "../udev.h" + +#include +#include + +#include + +namespace KWin +{ +namespace LibInput +{ + +static void libinputLogHandler(libinput *libinput, libinput_log_priority priority, const char *format, va_list args) +{ + Q_UNUSED(libinput) + char buf[1024]; + if (std::vsnprintf(buf, 1023, format, args) <= 0) { + return; + } + switch (priority) { + case LIBINPUT_LOG_PRIORITY_DEBUG: + qCDebug(KWIN_LIBINPUT) << "Libinput:" << buf; + break; + case LIBINPUT_LOG_PRIORITY_INFO: + qCInfo(KWIN_LIBINPUT) << "Libinput:" << buf; + break; + case LIBINPUT_LOG_PRIORITY_ERROR: + default: + qCCritical(KWIN_LIBINPUT) << "Libinput:" << buf; + break; + } +} + +Context::Context(const Udev &udev) + : m_libinput(libinput_udev_create_context(&Context::s_interface, this, udev)) + , m_suspended(false) +{ + libinput_log_set_priority(m_libinput, LIBINPUT_LOG_PRIORITY_DEBUG); + libinput_log_set_handler(m_libinput, &libinputLogHandler); +} + +Context::~Context() +{ + if (m_libinput) { + libinput_unref(m_libinput); + } +} + +bool Context::assignSeat(const char *seat) +{ + if (!isValid()) { + return false; + } + return libinput_udev_assign_seat(m_libinput, seat) == 0; +} + +int Context::fileDescriptor() +{ + if (!isValid()) { + return -1; + } + return libinput_get_fd(m_libinput); +} + +void Context::dispatch() +{ + libinput_dispatch(m_libinput); +} + +const struct libinput_interface Context::s_interface = { + Context::openRestrictedCallback, + Context::closeRestrictedCallBack +}; + +int Context::openRestrictedCallback(const char *path, int flags, void *user_data) +{ + return ((Context*)user_data)->openRestricted(path, flags); +} + +void Context::closeRestrictedCallBack(int fd, void *user_data) +{ + ((Context*)user_data)->closeRestricted(fd); +} + +int Context::openRestricted(const char *path, int flags) +{ + LogindIntegration *logind = LogindIntegration::self(); + Q_ASSERT(logind); + int fd = logind->takeDevice(path); + if (fd < 0) { + // failed + return fd; + } + // adjust flags - based on Weston (logind-util.c) + int fl = fcntl(fd, F_GETFL); + auto errorHandling = [fd, this]() { + close(fd); + closeRestricted(fd); + }; + if (fl < 0) { + errorHandling(); + return -1; + } + + if (flags & O_NONBLOCK) { + fl |= O_NONBLOCK; + } + + if (fcntl(fd, F_SETFL, fl) < 0) { + errorHandling(); + return -1; + } + + fl = fcntl(fd, F_GETFD); + if (fl < 0) { + errorHandling(); + return -1; + } + + if (!(flags & O_CLOEXEC)) { + fl &= ~FD_CLOEXEC; + } + + if (fcntl(fd, F_SETFD, fl) < 0) { + errorHandling(); + return -1; + } + return fd; +} + +void Context::closeRestricted(int fd) +{ + LogindIntegration *logind = LogindIntegration::self(); + Q_ASSERT(logind); + logind->releaseDevice(fd); +} + +Event *Context::event() +{ + return Event::create(libinput_get_event(m_libinput)); +} + +void Context::suspend() +{ + if (m_suspended) { + return; + } + libinput_suspend(m_libinput); + m_suspended = true; +} + +void Context::resume() +{ + if (!m_suspended) { + return; + } + libinput_resume(m_libinput); + m_suspended = false; +} + +} +} diff --git a/libinput/context.h b/libinput/context.h new file mode 100644 index 0000000..d05e8b4 --- /dev/null +++ b/libinput/context.h @@ -0,0 +1,69 @@ +/* + 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 +*/ +#ifndef KWIN_LIBINPUT_CONTEXT_H +#define KWIN_LIBINPUT_CONTEXT_H + +#include + +namespace KWin +{ + +class Udev; + +namespace LibInput +{ + +class Event; + +class Context +{ +public: + Context(const Udev &udev); + ~Context(); + bool assignSeat(const char *seat); + bool isValid() const { + return m_libinput != nullptr; + } + bool isSuspended() const { + return m_suspended; + } + + int fileDescriptor(); + void dispatch(); + void suspend(); + void resume(); + + operator libinput*() { + return m_libinput; + } + operator libinput*() const { + return m_libinput; + } + + /** + * Gets the next event, if there is no new event @c null is returned. + * The caller takes ownership of the returned pointer. + */ + Event *event(); + + static int openRestrictedCallback(const char *path, int flags, void *user_data); + static void closeRestrictedCallBack(int fd, void *user_data); + static const struct libinput_interface s_interface; + +private: + int openRestricted(const char *path, int flags); + void closeRestricted(int fd); + struct libinput *m_libinput; + bool m_suspended; +}; + +} +} + +#endif diff --git a/libinput/device.cpp b/libinput/device.cpp new file mode 100644 index 0000000..6687ff5 --- /dev/null +++ b/libinput/device.cpp @@ -0,0 +1,541 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "device.h" + +#include + +#include + +#include + +namespace KWin +{ +namespace LibInput +{ + +static bool checkAlphaNumericKeyboard(libinput_device *device) +{ + for (uint i = KEY_1; i <= KEY_0; i++) { + if (libinput_device_keyboard_has_key(device, i) == 0) { + return false; + } + } + for (uint i = KEY_Q; i <= KEY_P; i++) { + if (libinput_device_keyboard_has_key(device, i) == 0) { + return false; + } + } + for (uint i = KEY_A; i <= KEY_L; i++) { + if (libinput_device_keyboard_has_key(device, i) == 0) { + return false; + } + } + for (uint i = KEY_Z; i <= KEY_M; i++) { + if (libinput_device_keyboard_has_key(device, i) == 0) { + return false; + } + } + return true; +} + +QVector Device::s_devices; + +Device *Device::getDevice(libinput_device *native) +{ + auto it = std::find_if(s_devices.constBegin(), s_devices.constEnd(), + [native] (const Device *d) { + return d->device() == native; + } + ); + if (it != s_devices.constEnd()) { + return *it; + } + return nullptr; +} + +enum class ConfigKey { + Enabled, + LeftHanded, + DisableWhileTyping, + PointerAcceleration, + PointerAccelerationProfile, + TapToClick, + LmrTapButtonMap, + TapAndDrag, + TapDragLock, + MiddleButtonEmulation, + NaturalScroll, + ScrollMethod, + ScrollButton, + ClickMethod, + ScrollFactor +}; + +struct ConfigData { + explicit ConfigData(QByteArray _key, void (Device::*_setter)(bool), bool (Device::*_defaultValue)() const = nullptr) + : key(_key) + { booleanSetter.setter = _setter; booleanSetter.defaultValue = _defaultValue; } + + explicit ConfigData(QByteArray _key, void (Device::*_setter)(quint32), quint32 (Device::*_defaultValue)() const = nullptr) + : key(_key) + { quint32Setter.setter = _setter; quint32Setter.defaultValue = _defaultValue; } + + explicit ConfigData(QByteArray _key, void (Device::*_setter)(QString), QString (Device::*_defaultValue)() const = nullptr) + : key(_key) + { stringSetter.setter = _setter; stringSetter.defaultValue = _defaultValue; } + + explicit ConfigData(QByteArray _key, void (Device::*_setter)(qreal), qreal (Device::*_defaultValue)() const = nullptr) + : key(_key) + { qrealSetter.setter = _setter; qrealSetter.defaultValue = _defaultValue; } + + QByteArray key; + + struct { + void (Device::*setter)(bool) = nullptr; + bool (Device::*defaultValue)() const; + } booleanSetter; + + struct { + void (Device::*setter)(quint32) = nullptr; + quint32 (Device::*defaultValue)() const; + } quint32Setter; + struct { + void (Device::*setter)(QString) = nullptr; + QString (Device::*defaultValue)() const; + } stringSetter; + struct { + void (Device::*setter)(qreal) = nullptr; + qreal (Device::*defaultValue)() const; + } qrealSetter; +}; + +static const QMap s_configData { + {ConfigKey::Enabled, ConfigData(QByteArrayLiteral("Enabled"), &Device::setEnabled)}, + {ConfigKey::LeftHanded, ConfigData(QByteArrayLiteral("LeftHanded"), &Device::setLeftHanded, &Device::leftHandedEnabledByDefault)}, + {ConfigKey::DisableWhileTyping, ConfigData(QByteArrayLiteral("DisableWhileTyping"), &Device::setDisableWhileTyping, &Device::disableWhileTypingEnabledByDefault)}, + {ConfigKey::PointerAcceleration, ConfigData(QByteArrayLiteral("PointerAcceleration"), &Device::setPointerAccelerationFromString, &Device::defaultPointerAccelerationToString)}, + {ConfigKey::PointerAccelerationProfile, ConfigData(QByteArrayLiteral("PointerAccelerationProfile"), &Device::setPointerAccelerationProfileFromInt, &Device::defaultPointerAccelerationProfileToInt)}, + {ConfigKey::TapToClick, ConfigData(QByteArrayLiteral("TapToClick"), &Device::setTapToClick, &Device::tapToClickEnabledByDefault)}, + {ConfigKey::TapAndDrag, ConfigData(QByteArrayLiteral("TapAndDrag"), &Device::setTapAndDrag, &Device::tapAndDragEnabledByDefault)}, + {ConfigKey::TapDragLock, ConfigData(QByteArrayLiteral("TapDragLock"), &Device::setTapDragLock, &Device::tapDragLockEnabledByDefault)}, + {ConfigKey::MiddleButtonEmulation, ConfigData(QByteArrayLiteral("MiddleButtonEmulation"), &Device::setMiddleEmulation, &Device::middleEmulationEnabledByDefault)}, + {ConfigKey::LmrTapButtonMap, ConfigData(QByteArrayLiteral("LmrTapButtonMap"), &Device::setLmrTapButtonMap, &Device::lmrTapButtonMapEnabledByDefault)}, + {ConfigKey::NaturalScroll, ConfigData(QByteArrayLiteral("NaturalScroll"), &Device::setNaturalScroll, &Device::naturalScrollEnabledByDefault)}, + {ConfigKey::ScrollMethod, ConfigData(QByteArrayLiteral("ScrollMethod"), &Device::activateScrollMethodFromInt, &Device::defaultScrollMethodToInt)}, + {ConfigKey::ScrollButton, ConfigData(QByteArrayLiteral("ScrollButton"), &Device::setScrollButton, &Device::defaultScrollButton)}, + {ConfigKey::ClickMethod, ConfigData(QByteArrayLiteral("ClickMethod"), &Device::setClickMethodFromInt, &Device::defaultClickMethodToInt)}, + {ConfigKey::ScrollFactor, ConfigData(QByteArrayLiteral("ScrollFactor"), &Device::setScrollFactor, &Device::scrollFactorDefault)} +}; + +namespace { +QMatrix4x4 defaultCalibrationMatrix(libinput_device *device) +{ + float matrix[6]; + const int ret = libinput_device_config_calibration_get_default_matrix(device, matrix); + if (ret == 0) { + return QMatrix4x4(); + } + return QMatrix4x4{ + matrix[0], matrix[1], matrix[2], 0.0f, + matrix[3], matrix[4], matrix[5], 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f + }; +} +} + +Device::Device(libinput_device *device, QObject *parent) + : QObject(parent) + , m_device(device) + , m_keyboard(libinput_device_has_capability(m_device, LIBINPUT_DEVICE_CAP_KEYBOARD)) + , m_pointer(libinput_device_has_capability(m_device, LIBINPUT_DEVICE_CAP_POINTER)) + , m_touch(libinput_device_has_capability(m_device, LIBINPUT_DEVICE_CAP_TOUCH)) + , m_tabletTool(libinput_device_has_capability(m_device, LIBINPUT_DEVICE_CAP_TABLET_TOOL)) + , m_tabletPad(libinput_device_has_capability(m_device, LIBINPUT_DEVICE_CAP_TABLET_PAD)) + , m_supportsGesture(libinput_device_has_capability(m_device, LIBINPUT_DEVICE_CAP_GESTURE)) + , m_switch(libinput_device_has_capability(m_device, LIBINPUT_DEVICE_CAP_SWITCH)) + , m_lidSwitch(m_switch ? libinput_device_switch_has_switch(m_device, LIBINPUT_SWITCH_LID) : false) + , m_tabletSwitch(m_switch ? libinput_device_switch_has_switch(m_device, LIBINPUT_SWITCH_TABLET_MODE) : false) + , m_name(QString::fromLocal8Bit(libinput_device_get_name(m_device))) + , m_sysName(QString::fromLocal8Bit(libinput_device_get_sysname(m_device))) + , m_outputName(QString::fromLocal8Bit(libinput_device_get_output_name(m_device))) + , m_product(libinput_device_get_id_product(m_device)) + , m_vendor(libinput_device_get_id_vendor(m_device)) + , m_tapFingerCount(libinput_device_config_tap_get_finger_count(m_device)) + , m_defaultTapButtonMap(libinput_device_config_tap_get_default_button_map(m_device)) + , m_tapButtonMap(libinput_device_config_tap_get_button_map(m_device)) + , m_tapToClickEnabledByDefault(libinput_device_config_tap_get_default_enabled(m_device) == LIBINPUT_CONFIG_TAP_ENABLED) + , m_tapToClick(libinput_device_config_tap_get_enabled(m_device)) + , m_tapAndDragEnabledByDefault(libinput_device_config_tap_get_default_drag_enabled(m_device)) + , m_tapAndDrag(libinput_device_config_tap_get_drag_enabled(m_device)) + , m_tapDragLockEnabledByDefault(libinput_device_config_tap_get_default_drag_lock_enabled(m_device)) + , m_tapDragLock(libinput_device_config_tap_get_drag_lock_enabled(m_device)) + , m_supportsDisableWhileTyping(libinput_device_config_dwt_is_available(m_device)) + , m_supportsPointerAcceleration(libinput_device_config_accel_is_available(m_device)) + , m_supportsLeftHanded(libinput_device_config_left_handed_is_available(m_device)) + , m_supportsCalibrationMatrix(libinput_device_config_calibration_has_matrix(m_device)) + , m_supportsDisableEvents(libinput_device_config_send_events_get_modes(m_device) & LIBINPUT_CONFIG_SEND_EVENTS_DISABLED) + , m_supportsDisableEventsOnExternalMouse(libinput_device_config_send_events_get_modes(m_device) & LIBINPUT_CONFIG_SEND_EVENTS_DISABLED_ON_EXTERNAL_MOUSE) + , m_supportsMiddleEmulation(libinput_device_config_middle_emulation_is_available(m_device)) + , m_supportsNaturalScroll(libinput_device_config_scroll_has_natural_scroll(m_device)) + , m_supportedScrollMethods(libinput_device_config_scroll_get_methods(m_device)) + , m_leftHandedEnabledByDefault(libinput_device_config_left_handed_get_default(m_device)) + , m_middleEmulationEnabledByDefault(libinput_device_config_middle_emulation_get_default_enabled(m_device) == LIBINPUT_CONFIG_MIDDLE_EMULATION_ENABLED) + , m_naturalScrollEnabledByDefault(libinput_device_config_scroll_get_default_natural_scroll_enabled(m_device)) + , m_defaultScrollMethod(libinput_device_config_scroll_get_default_method(m_device)) + , m_defaultScrollButton(libinput_device_config_scroll_get_default_button(m_device)) + , m_disableWhileTypingEnabledByDefault(libinput_device_config_dwt_get_default_enabled(m_device)) + , m_disableWhileTyping(m_supportsDisableWhileTyping ? libinput_device_config_dwt_get_enabled(m_device) == LIBINPUT_CONFIG_DWT_ENABLED : false) + , m_middleEmulation(libinput_device_config_middle_emulation_get_enabled(m_device) == LIBINPUT_CONFIG_MIDDLE_EMULATION_ENABLED) + , m_leftHanded(m_supportsLeftHanded ? libinput_device_config_left_handed_get(m_device) : false) + , m_naturalScroll(m_supportsNaturalScroll ? libinput_device_config_scroll_get_natural_scroll_enabled(m_device) : false) + , m_scrollMethod(libinput_device_config_scroll_get_method(m_device)) + , m_scrollButton(libinput_device_config_scroll_get_button(m_device)) + , m_defaultPointerAcceleration(libinput_device_config_accel_get_default_speed(m_device)) + , m_pointerAcceleration(libinput_device_config_accel_get_speed(m_device)) + , m_scrollFactor(scrollFactorDefault()) + , m_supportedPointerAccelerationProfiles(libinput_device_config_accel_get_profiles(m_device)) + , m_defaultPointerAccelerationProfile(libinput_device_config_accel_get_default_profile(m_device)) + , m_pointerAccelerationProfile(libinput_device_config_accel_get_profile(m_device)) + , m_enabled(m_supportsDisableEvents ? libinput_device_config_send_events_get_mode(m_device) == LIBINPUT_CONFIG_SEND_EVENTS_ENABLED : true) + , m_config() + , m_defaultCalibrationMatrix(m_supportsCalibrationMatrix ? defaultCalibrationMatrix(m_device) : QMatrix4x4{}) + , m_supportedClickMethods(libinput_device_config_click_get_methods(m_device)) + , m_defaultClickMethod(libinput_device_config_click_get_default_method(m_device)) + , m_clickMethod(libinput_device_config_click_get_method(m_device)) +{ + libinput_device_ref(m_device); + + qreal width = 0; + qreal height = 0; + if (libinput_device_get_size(m_device, &width, &height) == 0) { + m_size = QSizeF(width, height); + } + if (m_pointer) { + if (libinput_device_pointer_has_button(m_device, BTN_LEFT) == 1) { + m_supportedButtons |= Qt::LeftButton; + } + if (libinput_device_pointer_has_button(m_device, BTN_MIDDLE) == 1) { + m_supportedButtons |= Qt::MiddleButton; + } + if (libinput_device_pointer_has_button(m_device, BTN_RIGHT) == 1) { + m_supportedButtons |= Qt::RightButton; + } + if (libinput_device_pointer_has_button(m_device, BTN_SIDE) == 1) { + m_supportedButtons |= Qt::ExtraButton1; + } + if (libinput_device_pointer_has_button(m_device, BTN_EXTRA) == 1) { + m_supportedButtons |= Qt::ExtraButton2; + } + if (libinput_device_pointer_has_button(m_device, BTN_BACK) == 1) { + m_supportedButtons |= Qt::BackButton; + } + if (libinput_device_pointer_has_button(m_device, BTN_FORWARD) == 1) { + m_supportedButtons |= Qt::ForwardButton; + } + if (libinput_device_pointer_has_button(m_device, BTN_TASK) == 1) { + m_supportedButtons |= Qt::TaskButton; + } + } + if (m_keyboard) { + m_alphaNumericKeyboard = checkAlphaNumericKeyboard(m_device); + } + + s_devices << this; + QDBusConnection::sessionBus().registerObject(QStringLiteral("/org/kde/KWin/InputDevice/") + m_sysName, + QStringLiteral("org.kde.KWin.InputDevice"), + this, + QDBusConnection::ExportAllProperties + ); +} + +Device::~Device() +{ + s_devices.removeOne(this); + QDBusConnection::sessionBus().unregisterObject(QStringLiteral("/org/kde/KWin/InputDevice/") + m_sysName); + libinput_device_unref(m_device); +} + +template +void Device::writeEntry(const ConfigKey &key, const T &value) +{ + if (!m_config.isValid()) { + return; + } + if (m_loading) { + return; + } + auto it = s_configData.find(key); + Q_ASSERT(it != s_configData.end()); + m_config.writeEntry(it.value().key.constData(), value); + m_config.sync(); +} + +template +void Device::readEntry(const QByteArray &key, const Setter &s, const T &defaultValue) +{ + if (!s.setter) { + return; + } + + (this->*(s.setter))(m_config.readEntry(key.constData(), s.defaultValue ? (this->*(s.defaultValue))() : defaultValue)); +} + +void Device::loadConfiguration() +{ + if (!m_config.isValid()) { + return; + } + m_loading = true; + for (auto it = s_configData.begin(), end = s_configData.end(); it != end; ++it) { + const auto key = it.value().key; + if (!m_config.hasKey(key.constData())) { + continue; + } + readEntry(key, it.value().booleanSetter, true); + readEntry(key, it.value().quint32Setter, 0); + readEntry(key, it.value().stringSetter, ""); + readEntry(key, it.value().qrealSetter, 1.0); + }; + + m_loading = false; +} + +void Device::setPointerAcceleration(qreal acceleration) +{ + if (!m_supportsPointerAcceleration) { + return; + } + acceleration = qBound(-1.0, acceleration, 1.0); + if (libinput_device_config_accel_set_speed(m_device, acceleration) == LIBINPUT_CONFIG_STATUS_SUCCESS) { + if (m_pointerAcceleration != acceleration) { + m_pointerAcceleration = acceleration; + emit pointerAccelerationChanged(); + writeEntry(ConfigKey::PointerAcceleration, QString::number(acceleration, 'f', 3)); + } + } +} + +void Device::setScrollButton(quint32 button) +{ + if (!(m_supportedScrollMethods & LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN)) { + return; + } + if (libinput_device_config_scroll_set_button(m_device, button) == LIBINPUT_CONFIG_STATUS_SUCCESS) { + if (m_scrollButton != button) { + m_scrollButton = button; + writeEntry(ConfigKey::ScrollButton, m_scrollButton); + emit scrollButtonChanged(); + } + } +} + +void Device::setPointerAccelerationProfile(bool set, enum libinput_config_accel_profile profile) +{ + if (!(m_supportedPointerAccelerationProfiles & profile)) { + return; + } + if (!set) { + profile = (profile == LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT) ? LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE : LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT; + if (!(m_supportedPointerAccelerationProfiles & profile)) { + return; + } + } + + if (libinput_device_config_accel_set_profile(m_device, profile) == LIBINPUT_CONFIG_STATUS_SUCCESS) { + if (m_pointerAccelerationProfile != profile) { + m_pointerAccelerationProfile = profile; + emit pointerAccelerationProfileChanged(); + writeEntry(ConfigKey::PointerAccelerationProfile, (quint32) profile); + } + } +} + +void Device::setClickMethod(bool set, enum libinput_config_click_method method) +{ + if (!(m_supportedClickMethods & method)) { + return; + } + if (!set) { + method = (method == LIBINPUT_CONFIG_CLICK_METHOD_BUTTON_AREAS) ? LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER : LIBINPUT_CONFIG_CLICK_METHOD_BUTTON_AREAS; + if (!(m_supportedClickMethods & method)) { + return; + } + } + + if (libinput_device_config_click_set_method(m_device, method) == LIBINPUT_CONFIG_STATUS_SUCCESS) { + if (m_clickMethod != method) { + m_clickMethod = method; + emit clickMethodChanged(); + writeEntry(ConfigKey::ClickMethod, (quint32) method); + } + } +} + +void Device::setScrollMethod(bool set, enum libinput_config_scroll_method method) +{ + if (!(m_supportedScrollMethods & method)) { + return; + } + + bool isCurrent = m_scrollMethod == method; + if (!set) { + if (isCurrent) { + method = LIBINPUT_CONFIG_SCROLL_NO_SCROLL; + isCurrent = false; + } else { + return; + } + } + + if (libinput_device_config_scroll_set_method(m_device, method) == LIBINPUT_CONFIG_STATUS_SUCCESS) { + if (!isCurrent) { + m_scrollMethod = method; + emit scrollMethodChanged(); + writeEntry(ConfigKey::ScrollMethod, (quint32) method); + } + } +} + +void Device::setLmrTapButtonMap(bool set) +{ + enum libinput_config_tap_button_map map = set ? LIBINPUT_CONFIG_TAP_MAP_LMR : LIBINPUT_CONFIG_TAP_MAP_LRM; + + if (m_tapFingerCount < 2) { + return; + } + if (!set) { + map = LIBINPUT_CONFIG_TAP_MAP_LRM; + } + + if (libinput_device_config_tap_set_button_map(m_device, map) == LIBINPUT_CONFIG_STATUS_SUCCESS) { + if (m_tapButtonMap != map) { + m_tapButtonMap = map; + writeEntry(ConfigKey::LmrTapButtonMap, set); + emit tapButtonMapChanged(); + } + } +} + +int Device::stripsCount() const +{ + return libinput_device_tablet_pad_get_num_strips(m_device); +} + +int Device::ringsCount() const +{ + return libinput_device_tablet_pad_get_num_rings(m_device); +} + +#define CONFIG(method, condition, function, variable, key) \ +void Device::method(bool set) \ +{ \ + if (condition) { \ + return; \ + } \ + if (libinput_device_config_##function(m_device, set) == LIBINPUT_CONFIG_STATUS_SUCCESS) { \ + if (m_##variable != set) { \ + m_##variable = set; \ + writeEntry(ConfigKey::key, m_##variable); \ + emit variable##Changed(); \ + }\ + } \ +} + +CONFIG(setLeftHanded, !m_supportsLeftHanded, left_handed_set, leftHanded, LeftHanded) +CONFIG(setNaturalScroll, !m_supportsNaturalScroll, scroll_set_natural_scroll_enabled, naturalScroll, NaturalScroll) + +#undef CONFIG + +#define CONFIG(method, condition, function, enum, variable, key) \ +void Device::method(bool set) \ +{ \ + if (condition) { \ + return; \ + } \ + if (libinput_device_config_##function(m_device, set ? LIBINPUT_CONFIG_##enum##_ENABLED : LIBINPUT_CONFIG_##enum##_DISABLED) == LIBINPUT_CONFIG_STATUS_SUCCESS) { \ + if (m_##variable != set) { \ + m_##variable = set; \ + writeEntry(ConfigKey::key, m_##variable); \ + emit variable##Changed(); \ + }\ + } \ +} + +CONFIG(setEnabled, !m_supportsDisableEvents, send_events_set_mode, SEND_EVENTS, enabled, Enabled) +CONFIG(setDisableWhileTyping, !m_supportsDisableWhileTyping, dwt_set_enabled, DWT, disableWhileTyping, DisableWhileTyping) +CONFIG(setTapToClick, m_tapFingerCount == 0, tap_set_enabled, TAP, tapToClick, TapToClick) +CONFIG(setTapAndDrag, false, tap_set_drag_enabled, DRAG, tapAndDrag, TapAndDrag) +CONFIG(setTapDragLock, false, tap_set_drag_lock_enabled, DRAG_LOCK, tapDragLock, TapDragLock) +CONFIG(setMiddleEmulation, m_supportsMiddleEmulation == false, middle_emulation_set_enabled, MIDDLE_EMULATION, middleEmulation, MiddleButtonEmulation) + +#undef CONFIG + +void Device::setScrollFactor(qreal factor) +{ + if (m_scrollFactor != factor) { + m_scrollFactor = factor; + writeEntry(ConfigKey::ScrollFactor, m_scrollFactor); + emit scrollFactorChanged(); + } +} + +void Device::setOrientation(Qt::ScreenOrientation orientation) +{ + if (!m_supportsCalibrationMatrix) { + return; + } + // 90 deg cw: + static const QMatrix4x4 portraitMatrix{ + 0.0f, -1.0f, 1.0f, 0.0f, + 1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f + }; + // 180 deg cw: + static const QMatrix4x4 invertedLandscapeMatrix{ + -1.0f, 0.0f, 1.0f, 0.0f, + 0.0f, -1.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f + }; + // 270 deg cw + static const QMatrix4x4 invertedPortraitMatrix{ + 0.0f, 1.0f, 0.0f, 0.0f, + -1.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f + }; + QMatrix4x4 matrix; + switch (orientation) { + case Qt::PortraitOrientation: + matrix = portraitMatrix; + break; + case Qt::InvertedLandscapeOrientation: + matrix = invertedLandscapeMatrix; + break; + case Qt::InvertedPortraitOrientation: + matrix = invertedPortraitMatrix; + break; + case Qt::PrimaryOrientation: + case Qt::LandscapeOrientation: + default: + break; + } + const auto combined = m_defaultCalibrationMatrix * matrix; + const auto columnOrder = combined.constData(); + float m[6] = { + columnOrder[0], columnOrder[4], columnOrder[8], + columnOrder[1], columnOrder[5], columnOrder[9] + }; + libinput_device_config_calibration_set_matrix(m_device, m); +} + +} +} diff --git a/libinput/device.h b/libinput/device.h new file mode 100644 index 0000000..ec1d147 --- /dev/null +++ b/libinput/device.h @@ -0,0 +1,586 @@ +/* + 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 KWIN_LIBINPUT_DEVICE_H +#define KWIN_LIBINPUT_DEVICE_H + +#include + +#include + +#include +#include +#include +#include +#include "kwin_export.h" + +struct libinput_device; + +namespace KWin +{ +namespace LibInput +{ +enum class ConfigKey; + +class KWIN_EXPORT Device : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.KWin.InputDevice") + // + // general + Q_PROPERTY(bool keyboard READ isKeyboard CONSTANT) + Q_PROPERTY(bool alphaNumericKeyboard READ isAlphaNumericKeyboard CONSTANT) + Q_PROPERTY(bool pointer READ isPointer CONSTANT) + Q_PROPERTY(bool touchpad READ isTouchpad CONSTANT) + Q_PROPERTY(bool touch READ isTouch CONSTANT) + Q_PROPERTY(bool tabletTool READ isTabletTool CONSTANT) + Q_PROPERTY(bool tabletPad READ isTabletPad CONSTANT) + Q_PROPERTY(bool gestureSupport READ supportsGesture CONSTANT) + Q_PROPERTY(QString name READ name CONSTANT) + Q_PROPERTY(QString sysName READ sysName CONSTANT) + Q_PROPERTY(QString outputName READ outputName CONSTANT) + Q_PROPERTY(QSizeF size READ size CONSTANT) + Q_PROPERTY(quint32 product READ product CONSTANT) + Q_PROPERTY(quint32 vendor READ vendor CONSTANT) + Q_PROPERTY(bool supportsDisableEvents READ supportsDisableEvents CONSTANT) + Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled NOTIFY enabledChanged) + // + // advanced + Q_PROPERTY(int supportedButtons READ supportedButtons CONSTANT) + Q_PROPERTY(bool supportsCalibrationMatrix READ supportsCalibrationMatrix CONSTANT) + + Q_PROPERTY(bool supportsLeftHanded READ supportsLeftHanded CONSTANT) + Q_PROPERTY(bool leftHandedEnabledByDefault READ leftHandedEnabledByDefault CONSTANT) + Q_PROPERTY(bool leftHanded READ isLeftHanded WRITE setLeftHanded NOTIFY leftHandedChanged) + + Q_PROPERTY(bool supportsDisableEventsOnExternalMouse READ supportsDisableEventsOnExternalMouse CONSTANT) + + Q_PROPERTY(bool supportsDisableWhileTyping READ supportsDisableWhileTyping CONSTANT) + Q_PROPERTY(bool disableWhileTypingEnabledByDefault READ disableWhileTypingEnabledByDefault CONSTANT) + Q_PROPERTY(bool disableWhileTyping READ isDisableWhileTyping WRITE setDisableWhileTyping NOTIFY disableWhileTypingChanged) + // + // acceleration speed and profile + Q_PROPERTY(bool supportsPointerAcceleration READ supportsPointerAcceleration CONSTANT) + Q_PROPERTY(qreal defaultPointerAcceleration READ defaultPointerAcceleration CONSTANT) + Q_PROPERTY(qreal pointerAcceleration READ pointerAcceleration WRITE setPointerAcceleration NOTIFY pointerAccelerationChanged) + + Q_PROPERTY(bool supportsPointerAccelerationProfileFlat READ supportsPointerAccelerationProfileFlat CONSTANT) + Q_PROPERTY(bool defaultPointerAccelerationProfileFlat READ defaultPointerAccelerationProfileFlat CONSTANT) + Q_PROPERTY(bool pointerAccelerationProfileFlat READ pointerAccelerationProfileFlat WRITE setPointerAccelerationProfileFlat NOTIFY pointerAccelerationProfileChanged) + + Q_PROPERTY(bool supportsPointerAccelerationProfileAdaptive READ supportsPointerAccelerationProfileAdaptive CONSTANT) + Q_PROPERTY(bool defaultPointerAccelerationProfileAdaptive READ defaultPointerAccelerationProfileAdaptive CONSTANT) + Q_PROPERTY(bool pointerAccelerationProfileAdaptive READ pointerAccelerationProfileAdaptive WRITE setPointerAccelerationProfileAdaptive NOTIFY pointerAccelerationProfileChanged) + // + // tapping + Q_PROPERTY(int tapFingerCount READ tapFingerCount CONSTANT) + Q_PROPERTY(bool tapToClickEnabledByDefault READ tapToClickEnabledByDefault CONSTANT) + Q_PROPERTY(bool tapToClick READ isTapToClick WRITE setTapToClick NOTIFY tapToClickChanged) + + Q_PROPERTY(bool supportsLmrTapButtonMap READ supportsLmrTapButtonMap CONSTANT) + Q_PROPERTY(bool lmrTapButtonMapEnabledByDefault READ lmrTapButtonMapEnabledByDefault CONSTANT) + Q_PROPERTY(bool lmrTapButtonMap READ lmrTapButtonMap WRITE setLmrTapButtonMap NOTIFY tapButtonMapChanged) + + Q_PROPERTY(bool tapAndDragEnabledByDefault READ tapAndDragEnabledByDefault CONSTANT) + Q_PROPERTY(bool tapAndDrag READ isTapAndDrag WRITE setTapAndDrag NOTIFY tapAndDragChanged) + Q_PROPERTY(bool tapDragLockEnabledByDefault READ tapDragLockEnabledByDefault CONSTANT) + Q_PROPERTY(bool tapDragLock READ isTapDragLock WRITE setTapDragLock NOTIFY tapDragLockChanged) + + Q_PROPERTY(bool supportsMiddleEmulation READ supportsMiddleEmulation CONSTANT) + Q_PROPERTY(bool middleEmulationEnabledByDefault READ middleEmulationEnabledByDefault CONSTANT) + Q_PROPERTY(bool middleEmulation READ isMiddleEmulation WRITE setMiddleEmulation NOTIFY middleEmulationChanged) + // + // scrolling + Q_PROPERTY(bool supportsNaturalScroll READ supportsNaturalScroll CONSTANT) + Q_PROPERTY(bool naturalScrollEnabledByDefault READ naturalScrollEnabledByDefault CONSTANT) + Q_PROPERTY(bool naturalScroll READ isNaturalScroll WRITE setNaturalScroll NOTIFY naturalScrollChanged) + + Q_PROPERTY(bool supportsScrollTwoFinger READ supportsScrollTwoFinger CONSTANT) + Q_PROPERTY(bool scrollTwoFingerEnabledByDefault READ scrollTwoFingerEnabledByDefault CONSTANT) + Q_PROPERTY(bool scrollTwoFinger READ isScrollTwoFinger WRITE setScrollTwoFinger NOTIFY scrollMethodChanged) + + Q_PROPERTY(bool supportsScrollEdge READ supportsScrollEdge CONSTANT) + Q_PROPERTY(bool scrollEdgeEnabledByDefault READ scrollEdgeEnabledByDefault CONSTANT) + Q_PROPERTY(bool scrollEdge READ isScrollEdge WRITE setScrollEdge NOTIFY scrollMethodChanged) + + Q_PROPERTY(bool supportsScrollOnButtonDown READ supportsScrollOnButtonDown CONSTANT) + Q_PROPERTY(bool scrollOnButtonDownEnabledByDefault READ scrollOnButtonDownEnabledByDefault CONSTANT) + Q_PROPERTY(quint32 defaultScrollButton READ defaultScrollButton CONSTANT) + Q_PROPERTY(bool scrollOnButtonDown READ isScrollOnButtonDown WRITE setScrollOnButtonDown NOTIFY scrollMethodChanged) + Q_PROPERTY(quint32 scrollButton READ scrollButton WRITE setScrollButton NOTIFY scrollButtonChanged) + + Q_PROPERTY(qreal scrollFactor READ scrollFactor WRITE setScrollFactor NOTIFY scrollFactorChanged) + + // switches + Q_PROPERTY(bool switchDevice READ isSwitch CONSTANT) + Q_PROPERTY(bool lidSwitch READ isLidSwitch CONSTANT) + Q_PROPERTY(bool tabletModeSwitch READ isTabletModeSwitch CONSTANT) + + // Click Methods + Q_PROPERTY(bool supportsClickMethodAreas READ supportsClickMethodAreas CONSTANT) + Q_PROPERTY(bool defaultClickMethodAreas READ defaultClickMethodAreas CONSTANT) + Q_PROPERTY(bool clickMethodAreas READ isClickMethodAreas WRITE setClickMethodAreas NOTIFY clickMethodChanged) + + Q_PROPERTY(bool supportsClickMethodClickfinger READ supportsClickMethodClickfinger CONSTANT) + Q_PROPERTY(bool defaultClickMethodClickfinger READ defaultClickMethodClickfinger CONSTANT) + Q_PROPERTY(bool clickMethodClickfinger READ isClickMethodClickfinger WRITE setClickMethodClickfinger NOTIFY clickMethodChanged) + +public: + explicit Device(libinput_device *device, QObject *parent = nullptr); + ~Device() override; + + bool isKeyboard() const { + return m_keyboard; + } + bool isAlphaNumericKeyboard() const { + return m_alphaNumericKeyboard; + } + bool isPointer() const { + return m_pointer; + } + bool isTouchpad() const{ + return m_pointer && + // ignore all combined devices. E.g. a touchpad on a keyboard we don't want to toggle + // as that would result in the keyboard going off as well + !(m_keyboard || m_touch || m_tabletPad || m_tabletTool) && + // is this a touch pad? We don't really know, let's do some assumptions + (m_tapFingerCount > 0 || m_supportsDisableWhileTyping || m_supportsDisableEventsOnExternalMouse); + } + bool isTouch() const { + return m_touch; + } + bool isTabletTool() const { + return m_tabletTool; + } + bool isTabletPad() const { + return m_tabletPad; + } + bool supportsGesture() const { + return m_supportsGesture; + } + QString name() const { + return m_name; + } + QString sysName() const { + return m_sysName; + } + QString outputName() const { + return m_outputName; + } + QSizeF size() const { + return m_size; + } + quint32 product() const { + return m_product; + } + quint32 vendor() const { + return m_vendor; + } + Qt::MouseButtons supportedButtons() const { + return m_supportedButtons; + } + int tapFingerCount() const { + return m_tapFingerCount; + } + bool tapToClickEnabledByDefault() const { + return m_tapToClickEnabledByDefault; + } + bool isTapToClick() const { + return m_tapToClick; + } + /** + * Set the Device to tap to click if @p set is @c true. + */ + void setTapToClick(bool set); + bool tapAndDragEnabledByDefault() const { + return m_tapAndDragEnabledByDefault; + } + bool isTapAndDrag() const { + return m_tapAndDrag; + } + void setTapAndDrag(bool set); + bool tapDragLockEnabledByDefault() const { + return m_tapDragLockEnabledByDefault; + } + bool isTapDragLock() const { + return m_tapDragLock; + } + void setTapDragLock(bool set); + bool supportsDisableWhileTyping() const { + return m_supportsDisableWhileTyping; + } + bool disableWhileTypingEnabledByDefault() const { + return m_disableWhileTypingEnabledByDefault; + } + bool supportsPointerAcceleration() const { + return m_supportsPointerAcceleration; + } + bool supportsLeftHanded() const { + return m_supportsLeftHanded; + } + bool supportsCalibrationMatrix() const { + return m_supportsCalibrationMatrix; + } + bool supportsDisableEvents() const { + return m_supportsDisableEvents; + } + bool supportsDisableEventsOnExternalMouse() const { + return m_supportsDisableEventsOnExternalMouse; + } + bool supportsMiddleEmulation() const { + return m_supportsMiddleEmulation; + } + bool supportsNaturalScroll() const { + return m_supportsNaturalScroll; + } + bool supportsScrollTwoFinger() const { + return (m_supportedScrollMethods & LIBINPUT_CONFIG_SCROLL_2FG); + } + bool supportsScrollEdge() const { + return (m_supportedScrollMethods & LIBINPUT_CONFIG_SCROLL_EDGE); + } + bool supportsScrollOnButtonDown() const { + return (m_supportedScrollMethods & LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN); + } + bool leftHandedEnabledByDefault() const { + return m_leftHandedEnabledByDefault; + } + bool middleEmulationEnabledByDefault() const { + return m_middleEmulationEnabledByDefault; + } + bool naturalScrollEnabledByDefault() const { + return m_naturalScrollEnabledByDefault; + } + enum libinput_config_scroll_method defaultScrollMethod() const { + return m_defaultScrollMethod; + } + quint32 defaultScrollMethodToInt() const { + return (quint32) m_defaultScrollMethod; + } + bool scrollTwoFingerEnabledByDefault() const { + return m_defaultScrollMethod == LIBINPUT_CONFIG_SCROLL_2FG; + } + bool scrollEdgeEnabledByDefault() const { + return m_defaultScrollMethod == LIBINPUT_CONFIG_SCROLL_EDGE; + } + bool scrollOnButtonDownEnabledByDefault() const { + return m_defaultScrollMethod == LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN; + } + bool supportsLmrTapButtonMap() const { + return m_tapFingerCount > 1; + } + bool lmrTapButtonMapEnabledByDefault() const { + return m_defaultTapButtonMap == LIBINPUT_CONFIG_TAP_MAP_LMR; + } + + void setLmrTapButtonMap(bool set); + bool lmrTapButtonMap() const { + return m_tapButtonMap & LIBINPUT_CONFIG_TAP_MAP_LMR; + } + + quint32 defaultScrollButton() const { + return m_defaultScrollButton; + } + bool isMiddleEmulation() const { + return m_middleEmulation; + } + void setMiddleEmulation(bool set); + bool isNaturalScroll() const { + return m_naturalScroll; + } + void setNaturalScroll(bool set); + void setScrollMethod(bool set, enum libinput_config_scroll_method method); + bool isScrollTwoFinger() const { + return m_scrollMethod & LIBINPUT_CONFIG_SCROLL_2FG; + } + void setScrollTwoFinger(bool set) { + setScrollMethod(set, LIBINPUT_CONFIG_SCROLL_2FG); + } + bool isScrollEdge() const { + return m_scrollMethod & LIBINPUT_CONFIG_SCROLL_EDGE; + } + void setScrollEdge(bool set) { + setScrollMethod(set, LIBINPUT_CONFIG_SCROLL_EDGE); + } + bool isScrollOnButtonDown() const { + return m_scrollMethod & LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN; + } + void setScrollOnButtonDown(bool set) { + setScrollMethod(set, LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN); + } + void activateScrollMethodFromInt(quint32 method) { + setScrollMethod(true, (libinput_config_scroll_method) method); + } + quint32 scrollButton() const { + return m_scrollButton; + } + void setScrollButton(quint32 button); + + qreal scrollFactorDefault() const { + return 1.0; + } + qreal scrollFactor() const { + return m_scrollFactor; + } + void setScrollFactor(qreal factor); + + void setDisableWhileTyping(bool set); + bool isDisableWhileTyping() const { + return m_disableWhileTyping; + } + bool isLeftHanded() const { + return m_leftHanded; + } + /** + * Sets the Device to left handed mode if @p set is @c true. + * If @p set is @c false the device is set to right handed mode + */ + void setLeftHanded(bool set); + + qreal defaultPointerAcceleration() const { + return m_defaultPointerAcceleration; + } + qreal pointerAcceleration() const { + return m_pointerAcceleration; + } + /** + * @param acceleration mapped to range [-1,1] with -1 being the slowest, 1 being the fastest supported acceleration. + */ + void setPointerAcceleration(qreal acceleration); + void setPointerAccelerationFromString(QString acceleration) { + setPointerAcceleration(acceleration.toDouble()); + } + QString defaultPointerAccelerationToString() const { + return QString::number(m_pointerAcceleration, 'f', 3); + } + bool supportsPointerAccelerationProfileFlat() const { + return (m_supportedPointerAccelerationProfiles & LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT); + } + bool supportsPointerAccelerationProfileAdaptive() const { + return (m_supportedPointerAccelerationProfiles & LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE); + } + bool defaultPointerAccelerationProfileFlat() const { + return (m_defaultPointerAccelerationProfile & LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT); + } + bool defaultPointerAccelerationProfileAdaptive() const { + return (m_defaultPointerAccelerationProfile & LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE); + } + bool pointerAccelerationProfileFlat() const { + return (m_pointerAccelerationProfile & LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT); + } + bool pointerAccelerationProfileAdaptive() const { + return (m_pointerAccelerationProfile & LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE); + } + void setPointerAccelerationProfile(bool set, enum libinput_config_accel_profile profile); + void setPointerAccelerationProfileFlat(bool set) { + setPointerAccelerationProfile(set, LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT); + } + void setPointerAccelerationProfileAdaptive(bool set) { + setPointerAccelerationProfile(set, LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE); + } + void setPointerAccelerationProfileFromInt(quint32 profile) { + setPointerAccelerationProfile(true, (libinput_config_accel_profile) profile); + } + quint32 defaultPointerAccelerationProfileToInt() const { + return (quint32) m_defaultPointerAccelerationProfile; + } + bool supportsClickMethodAreas() const { + return (m_supportedClickMethods & LIBINPUT_CONFIG_CLICK_METHOD_BUTTON_AREAS); + } + bool defaultClickMethodAreas() const { + return (m_defaultClickMethod == LIBINPUT_CONFIG_CLICK_METHOD_BUTTON_AREAS); + } + bool isClickMethodAreas() const { + return (m_clickMethod == LIBINPUT_CONFIG_CLICK_METHOD_BUTTON_AREAS); + } + bool supportsClickMethodClickfinger() const { + return (m_supportedClickMethods & LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER); + } + bool defaultClickMethodClickfinger() const { + return (m_defaultClickMethod == LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER); + } + bool isClickMethodClickfinger() const { + return (m_clickMethod == LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER); + } + void setClickMethod(bool set, enum libinput_config_click_method method); + void setClickMethodAreas(bool set) { + setClickMethod(set, LIBINPUT_CONFIG_CLICK_METHOD_BUTTON_AREAS); + } + void setClickMethodClickfinger(bool set) { + setClickMethod(set, LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER); + } + void setClickMethodFromInt(quint32 method) { + setClickMethod(true, (libinput_config_click_method) method); + } + quint32 defaultClickMethodToInt() const { + return (quint32) m_defaultClickMethod; + } + + bool isEnabled() const { + return m_enabled; + } + void setEnabled(bool enabled); + + libinput_device *device() const { + return m_device; + } + + /** + * Sets the @p config to load the Device configuration from and to store each + * successful Device configuration. + */ + void setConfig(const KConfigGroup &config) { + m_config = config; + } + + /** + * The id of the screen in KWin identifiers. Set from KWin through setScreenId. + */ + int screenId() const { + return m_screenId; + } + + /** + * Sets the KWin screen id for the device + */ + void setScreenId(int screenId) { + m_screenId = screenId; + } + + void setOrientation(Qt::ScreenOrientation orientation); + + /** + * Loads the configuration and applies it to the Device + */ + void loadConfiguration(); + + bool isSwitch() const { + return m_switch; + } + + bool isLidSwitch() const { + return m_lidSwitch; + } + + bool isTabletModeSwitch() const { + return m_tabletSwitch; + } + + int stripsCount() const; + int ringsCount() const; + + /** + * All created Devices + */ + static QVector devices() { + return s_devices; + } + /** + * Gets the Device for @p native. @c null if there is no Device for @p native. + */ + static Device *getDevice(libinput_device *native); + +Q_SIGNALS: + void tapButtonMapChanged(); + void leftHandedChanged(); + void disableWhileTypingChanged(); + void pointerAccelerationChanged(); + void pointerAccelerationProfileChanged(); + void enabledChanged(); + void tapToClickChanged(); + void tapAndDragChanged(); + void tapDragLockChanged(); + void middleEmulationChanged(); + void naturalScrollChanged(); + void scrollMethodChanged(); + void scrollButtonChanged(); + void scrollFactorChanged(); + void clickMethodChanged(); + +private: + template + void writeEntry(const ConfigKey &key, const T &value); + template + void readEntry(const QByteArray &key, const Setter &s, const T &defaultValue = T()); + libinput_device *m_device; + bool m_keyboard; + bool m_alphaNumericKeyboard = false; + bool m_pointer; + bool m_touch; + bool m_tabletTool; + bool m_tabletPad; + bool m_supportsGesture; + bool m_switch = false; + bool m_lidSwitch = false; + bool m_tabletSwitch = false; + QString m_name; + QString m_sysName; + QString m_outputName; + QSizeF m_size; + quint32 m_product; + quint32 m_vendor; + Qt::MouseButtons m_supportedButtons = Qt::NoButton; + int m_tapFingerCount; + enum libinput_config_tap_button_map m_defaultTapButtonMap; + enum libinput_config_tap_button_map m_tapButtonMap; + bool m_tapToClickEnabledByDefault; + bool m_tapToClick; + bool m_tapAndDragEnabledByDefault; + bool m_tapAndDrag; + bool m_tapDragLockEnabledByDefault; + bool m_tapDragLock; + bool m_supportsDisableWhileTyping; + bool m_supportsPointerAcceleration; + bool m_supportsLeftHanded; + bool m_supportsCalibrationMatrix; + bool m_supportsDisableEvents; + bool m_supportsDisableEventsOnExternalMouse; + bool m_supportsMiddleEmulation; + bool m_supportsNaturalScroll; + quint32 m_supportedScrollMethods; + bool m_supportsScrollEdge; + bool m_supportsScrollOnButtonDown; + bool m_leftHandedEnabledByDefault; + bool m_middleEmulationEnabledByDefault; + bool m_naturalScrollEnabledByDefault; + enum libinput_config_scroll_method m_defaultScrollMethod; + quint32 m_defaultScrollButton; + bool m_disableWhileTypingEnabledByDefault; + bool m_disableWhileTyping; + bool m_middleEmulation; + bool m_leftHanded; + bool m_naturalScroll; + enum libinput_config_scroll_method m_scrollMethod; + quint32 m_scrollButton; + qreal m_defaultPointerAcceleration; + qreal m_pointerAcceleration; + qreal m_scrollFactor; + quint32 m_supportedPointerAccelerationProfiles; + enum libinput_config_accel_profile m_defaultPointerAccelerationProfile; + enum libinput_config_accel_profile m_pointerAccelerationProfile; + bool m_enabled; + + KConfigGroup m_config; + bool m_loading = false; + + int m_screenId = 0; + Qt::ScreenOrientation m_orientation = Qt::PrimaryOrientation; + QMatrix4x4 m_defaultCalibrationMatrix; + quint32 m_supportedClickMethods; + enum libinput_config_click_method m_defaultClickMethod; + enum libinput_config_click_method m_clickMethod; + + static QVector s_devices; +}; + +} +} + +Q_DECLARE_METATYPE(KWin::LibInput::Device*) + +#endif diff --git a/libinput/events.cpp b/libinput/events.cpp new file mode 100644 index 0000000..899fc2c --- /dev/null +++ b/libinput/events.cpp @@ -0,0 +1,386 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "events.h" +#include "device.h" + +#include + +namespace KWin +{ +namespace LibInput +{ + +Event *Event::create(libinput_event *event) +{ + if (!event) { + return nullptr; + } + const auto t = libinput_event_get_type(event); + // TODO: add touch events + // TODO: add device notify events + switch (t) { + case LIBINPUT_EVENT_KEYBOARD_KEY: + return new KeyEvent(event); + case LIBINPUT_EVENT_POINTER_AXIS: + case LIBINPUT_EVENT_POINTER_BUTTON: + case LIBINPUT_EVENT_POINTER_MOTION: + case LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE: + return new PointerEvent(event, t); + case LIBINPUT_EVENT_TOUCH_DOWN: + case LIBINPUT_EVENT_TOUCH_UP: + case LIBINPUT_EVENT_TOUCH_MOTION: + case LIBINPUT_EVENT_TOUCH_CANCEL: + case LIBINPUT_EVENT_TOUCH_FRAME: + return new TouchEvent(event, t); + case LIBINPUT_EVENT_GESTURE_SWIPE_BEGIN: + case LIBINPUT_EVENT_GESTURE_SWIPE_UPDATE: + case LIBINPUT_EVENT_GESTURE_SWIPE_END: + return new SwipeGestureEvent(event, t); + case LIBINPUT_EVENT_GESTURE_PINCH_BEGIN: + case LIBINPUT_EVENT_GESTURE_PINCH_UPDATE: + case LIBINPUT_EVENT_GESTURE_PINCH_END: + return new PinchGestureEvent(event, t); + case LIBINPUT_EVENT_TABLET_TOOL_AXIS: + case LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY: + case LIBINPUT_EVENT_TABLET_TOOL_TIP: + return new TabletToolEvent(event, t); + case LIBINPUT_EVENT_TABLET_TOOL_BUTTON: + return new TabletToolButtonEvent(event, t); + case LIBINPUT_EVENT_TABLET_PAD_RING: + return new TabletPadRingEvent(event, t); + case LIBINPUT_EVENT_TABLET_PAD_STRIP: + return new TabletPadStripEvent(event, t); + case LIBINPUT_EVENT_TABLET_PAD_BUTTON: + return new TabletPadButtonEvent(event, t); + case LIBINPUT_EVENT_SWITCH_TOGGLE: + return new SwitchEvent(event, t); + default: + return new Event(event, t); + } +} + +Event::Event(libinput_event *event, libinput_event_type type) + : m_event(event) + , m_type(type) + , m_device(nullptr) +{ +} + +Event::~Event() +{ + libinput_event_destroy(m_event); +} + +Device *Event::device() const +{ + if (!m_device) { + m_device = Device::getDevice(libinput_event_get_device(m_event)); + } + return m_device; +} + +libinput_device *Event::nativeDevice() const +{ + if (m_device) { + return m_device->device(); + } + return libinput_event_get_device(m_event); +} + +KeyEvent::KeyEvent(libinput_event *event) + : Event(event, LIBINPUT_EVENT_KEYBOARD_KEY) + , m_keyboardEvent(libinput_event_get_keyboard_event(event)) +{ +} + +KeyEvent::~KeyEvent() = default; + +uint32_t KeyEvent::key() const +{ + return libinput_event_keyboard_get_key(m_keyboardEvent); +} + +InputRedirection::KeyboardKeyState KeyEvent::state() const +{ + switch (libinput_event_keyboard_get_key_state(m_keyboardEvent)) { + case LIBINPUT_KEY_STATE_PRESSED: + return InputRedirection::KeyboardKeyPressed; + case LIBINPUT_KEY_STATE_RELEASED: + return InputRedirection::KeyboardKeyReleased; + } + abort(); +} + +uint32_t KeyEvent::time() const +{ + return libinput_event_keyboard_get_time(m_keyboardEvent); +} + +PointerEvent::PointerEvent(libinput_event *event, libinput_event_type type) + : Event(event, type) + , m_pointerEvent(libinput_event_get_pointer_event(event)) +{ +} + +PointerEvent::~PointerEvent() = default; + +QPointF PointerEvent::absolutePos() const +{ + Q_ASSERT(type() == LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE); + return QPointF(libinput_event_pointer_get_absolute_x(m_pointerEvent), + libinput_event_pointer_get_absolute_y(m_pointerEvent)); +} + +QPointF PointerEvent::absolutePos(const QSize &size) const +{ + Q_ASSERT(type() == LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE); + return QPointF(libinput_event_pointer_get_absolute_x_transformed(m_pointerEvent, size.width()), + libinput_event_pointer_get_absolute_y_transformed(m_pointerEvent, size.height())); +} + +QSizeF PointerEvent::delta() const +{ + Q_ASSERT(type() == LIBINPUT_EVENT_POINTER_MOTION); + return QSizeF(libinput_event_pointer_get_dx(m_pointerEvent), libinput_event_pointer_get_dy(m_pointerEvent)); +} + +QSizeF PointerEvent::deltaUnaccelerated() const +{ + Q_ASSERT(type() == LIBINPUT_EVENT_POINTER_MOTION); + return QSizeF(libinput_event_pointer_get_dx_unaccelerated(m_pointerEvent), libinput_event_pointer_get_dy_unaccelerated(m_pointerEvent)); +} + +uint32_t PointerEvent::time() const +{ + return libinput_event_pointer_get_time(m_pointerEvent); +} + +quint64 PointerEvent::timeMicroseconds() const +{ + return libinput_event_pointer_get_time_usec(m_pointerEvent); +} + +uint32_t PointerEvent::button() const +{ + Q_ASSERT(type() == LIBINPUT_EVENT_POINTER_BUTTON); + return libinput_event_pointer_get_button(m_pointerEvent); +} + +InputRedirection::PointerButtonState PointerEvent::buttonState() const +{ + Q_ASSERT(type() == LIBINPUT_EVENT_POINTER_BUTTON); + switch (libinput_event_pointer_get_button_state(m_pointerEvent)) { + case LIBINPUT_BUTTON_STATE_PRESSED: + return InputRedirection::PointerButtonPressed; + case LIBINPUT_BUTTON_STATE_RELEASED: + return InputRedirection::PointerButtonReleased; + } + abort(); +} + +QVector PointerEvent::axis() const +{ + Q_ASSERT(type() == LIBINPUT_EVENT_POINTER_AXIS); + QVector a; + if (libinput_event_pointer_has_axis(m_pointerEvent, LIBINPUT_POINTER_AXIS_SCROLL_HORIZONTAL)) { + a << InputRedirection::PointerAxisHorizontal; + } + if (libinput_event_pointer_has_axis(m_pointerEvent, LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL)) { + a << InputRedirection::PointerAxisVertical; + } + return a; +} + +qreal PointerEvent::axisValue(InputRedirection::PointerAxis axis) const +{ + Q_ASSERT(type() == LIBINPUT_EVENT_POINTER_AXIS); + const libinput_pointer_axis a = axis == InputRedirection::PointerAxisHorizontal + ? LIBINPUT_POINTER_AXIS_SCROLL_HORIZONTAL + : LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL; + return libinput_event_pointer_get_axis_value(m_pointerEvent, a) * device()->scrollFactor(); +} + +qint32 PointerEvent::discreteAxisValue(InputRedirection::PointerAxis axis) const +{ + Q_ASSERT(type() == LIBINPUT_EVENT_POINTER_AXIS); + const libinput_pointer_axis a = (axis == InputRedirection::PointerAxisHorizontal) + ? LIBINPUT_POINTER_AXIS_SCROLL_HORIZONTAL + : LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL; + return libinput_event_pointer_get_axis_value_discrete(m_pointerEvent, a) * device()->scrollFactor(); +} + +InputRedirection::PointerAxisSource PointerEvent::axisSource() const +{ + Q_ASSERT(type() == LIBINPUT_EVENT_POINTER_AXIS); + switch (libinput_event_pointer_get_axis_source(m_pointerEvent)) { + case LIBINPUT_POINTER_AXIS_SOURCE_WHEEL: + return InputRedirection::PointerAxisSourceWheel; + case LIBINPUT_POINTER_AXIS_SOURCE_FINGER: + return InputRedirection::PointerAxisSourceFinger; + case LIBINPUT_POINTER_AXIS_SOURCE_CONTINUOUS: + return InputRedirection::PointerAxisSourceContinuous; + case LIBINPUT_POINTER_AXIS_SOURCE_WHEEL_TILT: + return InputRedirection::PointerAxisSourceWheelTilt; + default: + return InputRedirection::PointerAxisSourceUnknown; + } +} + +TouchEvent::TouchEvent(libinput_event *event, libinput_event_type type) + : Event(event, type) + , m_touchEvent(libinput_event_get_touch_event(event)) +{ +} + +TouchEvent::~TouchEvent() = default; + +quint32 TouchEvent::time() const +{ + return libinput_event_touch_get_time(m_touchEvent); +} + +QPointF TouchEvent::absolutePos() const +{ + Q_ASSERT(type() == LIBINPUT_EVENT_TOUCH_DOWN || type() == LIBINPUT_EVENT_TOUCH_MOTION); + return QPointF(libinput_event_touch_get_x(m_touchEvent), + libinput_event_touch_get_y(m_touchEvent)); +} + +QPointF TouchEvent::absolutePos(const QSize &size) const +{ + Q_ASSERT(type() == LIBINPUT_EVENT_TOUCH_DOWN || type() == LIBINPUT_EVENT_TOUCH_MOTION); + return QPointF(libinput_event_touch_get_x_transformed(m_touchEvent, size.width()), + libinput_event_touch_get_y_transformed(m_touchEvent, size.height())); +} + +qint32 TouchEvent::id() const +{ + Q_ASSERT(type() != LIBINPUT_EVENT_TOUCH_CANCEL && type() != LIBINPUT_EVENT_TOUCH_FRAME); + + const qint32 slot = libinput_event_touch_get_slot(m_touchEvent); + + return slot == -1 ? 0 : slot; +} + +GestureEvent::GestureEvent(libinput_event *event, libinput_event_type type) + : Event(event, type) + , m_gestureEvent(libinput_event_get_gesture_event(event)) +{ +} + +GestureEvent::~GestureEvent() = default; + +quint32 GestureEvent::time() const +{ + return libinput_event_gesture_get_time(m_gestureEvent); +} + +int GestureEvent::fingerCount() const +{ + return libinput_event_gesture_get_finger_count(m_gestureEvent); +} + +QSizeF GestureEvent::delta() const +{ + return QSizeF(libinput_event_gesture_get_dx(m_gestureEvent), + libinput_event_gesture_get_dy(m_gestureEvent)); +} + +bool GestureEvent::isCancelled() const +{ + return libinput_event_gesture_get_cancelled(m_gestureEvent) != 0; +} + +PinchGestureEvent::PinchGestureEvent(libinput_event *event, libinput_event_type type) + : GestureEvent(event, type) +{ +} + +PinchGestureEvent::~PinchGestureEvent() = default; + +qreal PinchGestureEvent::scale() const +{ + return libinput_event_gesture_get_scale(m_gestureEvent); +} + +qreal PinchGestureEvent::angleDelta() const +{ + return libinput_event_gesture_get_angle_delta(m_gestureEvent); +} + +SwipeGestureEvent::SwipeGestureEvent(libinput_event *event, libinput_event_type type) + : GestureEvent(event, type) +{ +} + +SwipeGestureEvent::~SwipeGestureEvent() = default; + +SwitchEvent::SwitchEvent(libinput_event *event, libinput_event_type type) + : Event(event, type) + , m_switchEvent(libinput_event_get_switch_event(event)) +{ +} + +SwitchEvent::~SwitchEvent() = default; + +SwitchEvent::State SwitchEvent::state() const +{ + switch (libinput_event_switch_get_switch_state(m_switchEvent)) + { + case LIBINPUT_SWITCH_STATE_OFF: + return State::Off; + case LIBINPUT_SWITCH_STATE_ON: + return State::On; + default: + Q_UNREACHABLE(); + } + return State::Off; +} + +quint32 SwitchEvent::time() const +{ + return libinput_event_switch_get_time(m_switchEvent); +} + +quint64 SwitchEvent::timeMicroseconds() const +{ + return libinput_event_switch_get_time_usec(m_switchEvent); +} + +TabletToolEvent::TabletToolEvent(libinput_event *event, libinput_event_type type) + : Event(event, type) + , m_tabletToolEvent(libinput_event_get_tablet_tool_event(event)) +{ +} + +TabletToolButtonEvent::TabletToolButtonEvent(libinput_event *event, libinput_event_type type) + : Event(event, type) + , m_tabletToolEvent(libinput_event_get_tablet_tool_event(event)) +{ +} + +TabletPadButtonEvent::TabletPadButtonEvent(libinput_event *event, libinput_event_type type) + : Event(event, type) + , m_tabletPadEvent(libinput_event_get_tablet_pad_event(event)) +{ +} + +TabletPadStripEvent::TabletPadStripEvent(libinput_event *event, libinput_event_type type) + : Event(event, type) + , m_tabletPadEvent(libinput_event_get_tablet_pad_event(event)) +{ +} + +TabletPadRingEvent::TabletPadRingEvent(libinput_event *event, libinput_event_type type) + : Event(event, type) + , m_tabletPadEvent(libinput_event_get_tablet_pad_event(event)) +{ +} +} +} diff --git a/libinput/events.h b/libinput/events.h new file mode 100644 index 0000000..a5b3b27 --- /dev/null +++ b/libinput/events.h @@ -0,0 +1,367 @@ +/* + 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 +*/ +#ifndef KWIN_LIBINPUT_EVENTS_H +#define KWIN_LIBINPUT_EVENTS_H + +#include "../input.h" + +#include + +namespace KWin +{ +namespace LibInput +{ + +class Device; + +class Event +{ +public: + virtual ~Event(); + + libinput_event_type type() const; + Device *device() const; + libinput_device *nativeDevice() const; + + operator libinput_event*() { + return m_event; + } + operator libinput_event*() const { + return m_event; + } + + static Event *create(libinput_event *event); + +protected: + Event(libinput_event *event, libinput_event_type type); + +private: + libinput_event *m_event; + libinput_event_type m_type; + mutable Device *m_device; +}; + +class KeyEvent : public Event +{ +public: + KeyEvent(libinput_event *event); + ~KeyEvent() override; + + uint32_t key() const; + InputRedirection::KeyboardKeyState state() const; + uint32_t time() const; + + operator libinput_event_keyboard*() { + return m_keyboardEvent; + } + operator libinput_event_keyboard*() const { + return m_keyboardEvent; + } + +private: + libinput_event_keyboard *m_keyboardEvent; +}; + +class PointerEvent : public Event +{ +public: + PointerEvent(libinput_event* event, libinput_event_type type); + ~PointerEvent() override; + + QPointF absolutePos() const; + QPointF absolutePos(const QSize &size) const; + QSizeF delta() const; + QSizeF deltaUnaccelerated() const; + uint32_t button() const; + InputRedirection::PointerButtonState buttonState() const; + uint32_t time() const; + quint64 timeMicroseconds() const; + QVector axis() const; + qreal axisValue(InputRedirection::PointerAxis a) const; + qint32 discreteAxisValue(InputRedirection::PointerAxis axis) const; + InputRedirection::PointerAxisSource axisSource() const; + + operator libinput_event_pointer*() { + return m_pointerEvent; + } + operator libinput_event_pointer*() const { + return m_pointerEvent; + } + +private: + libinput_event_pointer *m_pointerEvent; +}; + +class TouchEvent : public Event +{ +public: + TouchEvent(libinput_event *event, libinput_event_type type); + ~TouchEvent() override; + + quint32 time() const; + QPointF absolutePos() const; + QPointF absolutePos(const QSize &size) const; + qint32 id() const; + + operator libinput_event_touch*() { + return m_touchEvent; + } + operator libinput_event_touch*() const { + return m_touchEvent; + } + +private: + libinput_event_touch *m_touchEvent; +}; + +class GestureEvent : public Event +{ +public: + ~GestureEvent() override; + + quint32 time() const; + int fingerCount() const; + + QSizeF delta() const; + + bool isCancelled() const; + + operator libinput_event_gesture*() { + return m_gestureEvent; + } + operator libinput_event_gesture*() const { + return m_gestureEvent; + } + +protected: + GestureEvent(libinput_event *event, libinput_event_type type); + libinput_event_gesture *m_gestureEvent; +}; + +class PinchGestureEvent : public GestureEvent +{ +public: + PinchGestureEvent(libinput_event *event, libinput_event_type type); + ~PinchGestureEvent() override; + + qreal scale() const; + qreal angleDelta() const; +}; + +class SwipeGestureEvent : public GestureEvent +{ +public: + SwipeGestureEvent(libinput_event *event, libinput_event_type type); + ~SwipeGestureEvent() override; +}; + +class SwitchEvent : public Event +{ +public: + SwitchEvent(libinput_event *event, libinput_event_type type); + ~SwitchEvent() override; + + enum class State { + Off, + On + }; + State state() const; + + quint32 time() const; + quint64 timeMicroseconds() const; + +private: + libinput_event_switch *m_switchEvent; +}; + +class TabletToolEvent : public Event +{ +public: + TabletToolEvent(libinput_event *event, libinput_event_type type); + + uint32_t time() const + { + return libinput_event_tablet_tool_get_time(m_tabletToolEvent); + } + bool xHasChanged() const { + return libinput_event_tablet_tool_x_has_changed(m_tabletToolEvent); + } + bool yHasChanged() const { + return libinput_event_tablet_tool_y_has_changed(m_tabletToolEvent); + } + bool pressureHasChanged() const { + return libinput_event_tablet_tool_pressure_has_changed(m_tabletToolEvent); + } + bool distanceHasChanged() const { + return libinput_event_tablet_tool_distance_has_changed(m_tabletToolEvent); + } + bool tiltXHasChanged() const { + return libinput_event_tablet_tool_tilt_x_has_changed(m_tabletToolEvent); + } + bool tiltYHasChanged() const { + return libinput_event_tablet_tool_tilt_y_has_changed(m_tabletToolEvent); + } + bool rotationHasChanged() const { + return libinput_event_tablet_tool_rotation_has_changed(m_tabletToolEvent); + } + bool sliderHasChanged() const { + return libinput_event_tablet_tool_slider_has_changed(m_tabletToolEvent); + } + + // uncomment when depending on libinput 1.14 or when implementing totems + // bool sizeMajorHasChanged() const { return + // libinput_event_tablet_tool_size_major_has_changed(m_tabletToolEvent); } bool + // sizeMinorHasChanged() const { return + // libinput_event_tablet_tool_size_minor_has_changed(m_tabletToolEvent); } + bool wheelHasChanged() const { + return libinput_event_tablet_tool_wheel_has_changed(m_tabletToolEvent); + } + QPointF position() const { + return {libinput_event_tablet_tool_get_x(m_tabletToolEvent), + libinput_event_tablet_tool_get_y(m_tabletToolEvent)}; + } + QPointF delta() const { + return {libinput_event_tablet_tool_get_dx(m_tabletToolEvent), + libinput_event_tablet_tool_get_dy(m_tabletToolEvent)}; + } + qreal pressure() const { + return libinput_event_tablet_tool_get_pressure(m_tabletToolEvent); + } + qreal distance() const { + return libinput_event_tablet_tool_get_distance(m_tabletToolEvent); + } + int xTilt() const { + return libinput_event_tablet_tool_get_tilt_x(m_tabletToolEvent); + } + int yTilt() const { + return libinput_event_tablet_tool_get_tilt_y(m_tabletToolEvent); + } + qreal rotation() const { + return libinput_event_tablet_tool_get_rotation(m_tabletToolEvent); + } + qreal sliderPosition() const { + return libinput_event_tablet_tool_get_slider_position(m_tabletToolEvent); + } + // Uncomment when depending on libinput 1.14 or when implementing totems + // qreal sizeMajor() const { return + // libinput_event_tablet_tool_get_size_major(m_tabletToolEvent); } + // qreal sizeMinor() const { + // return libinput_event_tablet_tool_get_size_minor(m_tabletToolEvent); } + qreal wheelDelta() const { + return libinput_event_tablet_tool_get_wheel_delta(m_tabletToolEvent); + } + int wheelDeltaDiscrete() const { + return libinput_event_tablet_tool_get_wheel_delta_discrete(m_tabletToolEvent); + } + + bool isTipDown() const { + const auto state = libinput_event_tablet_tool_get_tip_state(m_tabletToolEvent); + return state == LIBINPUT_TABLET_TOOL_TIP_DOWN; + } + bool isNearby() const { + const auto state = libinput_event_tablet_tool_get_proximity_state(m_tabletToolEvent); + return state == LIBINPUT_TABLET_TOOL_PROXIMITY_STATE_IN; + } + + QPointF transformedPosition(const QSize &size) const { + return {libinput_event_tablet_tool_get_x_transformed(m_tabletToolEvent, size.width()), + libinput_event_tablet_tool_get_y_transformed(m_tabletToolEvent, size.height())}; + } + + struct libinput_tablet_tool *tool() { + return libinput_event_tablet_tool_get_tool(m_tabletToolEvent); + } + +private: + libinput_event_tablet_tool *m_tabletToolEvent; +}; + +class TabletToolButtonEvent : public Event +{ +public: + TabletToolButtonEvent(libinput_event *event, libinput_event_type type); + + uint buttonId() const { + return libinput_event_tablet_tool_get_button(m_tabletToolEvent); + } + + bool isButtonPressed() const { + const auto state = libinput_event_tablet_tool_get_button_state(m_tabletToolEvent); + return state == LIBINPUT_BUTTON_STATE_PRESSED; + } + +private: + libinput_event_tablet_tool *m_tabletToolEvent; +}; + +class TabletPadRingEvent : public Event +{ +public: + TabletPadRingEvent(libinput_event *event, libinput_event_type type); + + int position() const { + return libinput_event_tablet_pad_get_ring_position(m_tabletPadEvent); + } + int number() const { + return libinput_event_tablet_pad_get_ring_number(m_tabletPadEvent); + } + libinput_tablet_pad_ring_axis_source source() const { + return libinput_event_tablet_pad_get_ring_source(m_tabletPadEvent); + } + +private: + libinput_event_tablet_pad *m_tabletPadEvent; +}; + +class TabletPadStripEvent : public Event +{ +public: + TabletPadStripEvent(libinput_event *event, libinput_event_type type); + + int position() const { + return libinput_event_tablet_pad_get_strip_position(m_tabletPadEvent); + } + int number() const { + return libinput_event_tablet_pad_get_strip_number(m_tabletPadEvent); + } + libinput_tablet_pad_strip_axis_source source() const { + return libinput_event_tablet_pad_get_strip_source(m_tabletPadEvent); + } + +private: + libinput_event_tablet_pad *m_tabletPadEvent; +}; + +class TabletPadButtonEvent : public Event +{ +public: + TabletPadButtonEvent(libinput_event *event, libinput_event_type type); + + uint buttonId() const { + return libinput_event_tablet_pad_get_button_number(m_tabletPadEvent); + } + bool isButtonPressed() const { + const auto state = libinput_event_tablet_pad_get_button_state(m_tabletPadEvent); + return state == LIBINPUT_BUTTON_STATE_PRESSED; + } + +private: + libinput_event_tablet_pad *m_tabletPadEvent; +}; + +inline +libinput_event_type Event::type() const +{ + return m_type; +} + +} +} + +#endif diff --git a/libinput/libinput_logging.cpp b/libinput/libinput_logging.cpp new file mode 100644 index 0000000..bc76abd --- /dev/null +++ b/libinput/libinput_logging.cpp @@ -0,0 +1,10 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "libinput_logging.h" +Q_LOGGING_CATEGORY(KWIN_LIBINPUT, "kwin_libinput", QtCriticalMsg) diff --git a/libinput/libinput_logging.h b/libinput/libinput_logging.h new file mode 100644 index 0000000..36851c0 --- /dev/null +++ b/libinput/libinput_logging.h @@ -0,0 +1,15 @@ +/* + 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_LIBINPUT_LOGGING_H +#define KWIN_LIBINPUT_LOGGING_H +#include +#include +Q_DECLARE_LOGGING_CATEGORY(KWIN_LIBINPUT) + +#endif diff --git a/libkwineffects/CMakeLists.txt b/libkwineffects/CMakeLists.txt new file mode 100644 index 0000000..5f9ac31 --- /dev/null +++ b/libkwineffects/CMakeLists.txt @@ -0,0 +1,128 @@ +########### next target ############### +include(ECMSetupVersion) + +ecm_setup_version(${PROJECT_VERSION} + VARIABLE_PREFIX KWINEFFECTS + VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/kwineffects_version.h" + PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/KWinEffectsConfigVersion.cmake" + SOVERSION 12 +) + +### xrenderutils lib ### +set(kwin_XRENDERUTILS_SRCS + kwinxrenderutils.cpp + logging.cpp +) + +add_library(kwinxrenderutils SHARED ${kwin_XRENDERUTILS_SRCS}) +generate_export_header(kwinxrenderutils EXPORT_FILE_NAME kwinxrenderutils_export.h) +target_link_libraries(kwinxrenderutils + PUBLIC + Qt5::Core + Qt5::Gui + + Plasma::KWaylandServer + + XCB::RENDER + XCB::XCB + XCB::XFIXES +) + +set_target_properties(kwinxrenderutils PROPERTIES + VERSION ${KWINEFFECTS_VERSION_STRING} + SOVERSION ${KWINEFFECTS_SOVERSION} +) +set_target_properties(kwinxrenderutils PROPERTIES OUTPUT_NAME ${KWIN_NAME}xrenderutils) + +install(TARGETS kwinxrenderutils EXPORT kdeworkspaceLibraryTargets ${INSTALL_TARGETS_DEFAULT_ARGS}) + +### effects lib ### +set(kwin_EFFECTSLIB_SRCS + anidata.cpp + kwinanimationeffect.cpp + kwineffectquickview.cpp + kwineffects.cpp + logging.cpp +) + +set(kwineffects_QT_LIBS + Qt5::DBus + Qt5::Widgets + Qt5::Quick +) + +set(kwineffects_KDE_LIBS + KF5::ConfigCore + KF5::CoreAddons + KF5::WindowSystem + KF5::Declarative +) + +set(kwineffects_XCB_LIBS + XCB::XCB +) + +add_library(kwineffects SHARED ${kwin_EFFECTSLIB_SRCS}) +generate_export_header(kwineffects EXPORT_FILE_NAME kwineffects_export.h) +target_link_libraries(kwineffects + PUBLIC + ${kwineffects_QT_LIBS} + ${kwineffects_KDE_LIBS} + ${kwineffects_XCB_LIBS} + kwinglutils +) +if (KWIN_HAVE_XRENDER_COMPOSITING) + target_link_libraries(kwineffects PRIVATE kwinxrenderutils XCB::XFIXES) +endif() +set_target_properties(kwineffects PROPERTIES + VERSION ${KWINEFFECTS_VERSION_STRING} + SOVERSION ${KWINEFFECTS_SOVERSION} +) +set_target_properties(kwineffects PROPERTIES OUTPUT_NAME ${KWIN_NAME}effects) + +install(TARGETS kwineffects EXPORT kdeworkspaceLibraryTargets ${INSTALL_TARGETS_DEFAULT_ARGS}) + +# kwingl(es)utils library +set(kwin_GLUTILSLIB_SRCS + kwinglplatform.cpp + kwingltexture.cpp + kwinglutils.cpp + kwinglutils_funcs.cpp + kwineglimagetexture.cpp + logging.cpp +) + +macro(KWIN4_ADD_GLUTILS_BACKEND name glinclude) + include_directories(${glinclude}) + add_library(${name} SHARED ${kwin_GLUTILSLIB_SRCS}) + generate_export_header(${name} BASE_NAME kwinglutils EXPORT_FILE_NAME kwinglutils_export.h) + target_link_libraries(${name} PUBLIC XCB::XCB KF5::CoreAddons KF5::ConfigCore KF5::WindowSystem) + set_target_properties(${name} PROPERTIES + VERSION ${KWINEFFECTS_VERSION_STRING} + SOVERSION ${KWINEFFECTS_SOVERSION} + ) + target_link_libraries(${name} PUBLIC ${ARGN}) + + install(TARGETS ${name} EXPORT kdeworkspaceLibraryTargets ${INSTALL_TARGETS_DEFAULT_ARGS}) +endmacro() + +kwin4_add_glutils_backend(kwinglutils ${epoxy_INCLUDE_DIR} ${epoxy_LIBRARY}) +set_target_properties(kwinglutils PROPERTIES OUTPUT_NAME ${KWIN_NAME}glutils) + +target_link_libraries(kwinglutils PUBLIC ${epoxy_LIBRARY}) + +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/kwinconfig.h + ${CMAKE_CURRENT_BINARY_DIR}/kwineffects_export.h + ${CMAKE_CURRENT_BINARY_DIR}/kwinglutils_export.h + ${CMAKE_CURRENT_BINARY_DIR}/kwinxrenderutils_export.h + kwinanimationeffect.h + kwineffectquickview.h + kwineffects.h + kwinglobals.h + kwinglplatform.h + kwingltexture.h + kwinglutils.h + kwinglutils_funcs.h + kwinxrenderutils.h + DESTINATION ${INCLUDE_INSTALL_DIR} COMPONENT Devel) diff --git a/libkwineffects/Mainpage.dox b/libkwineffects/Mainpage.dox new file mode 100644 index 0000000..696ecb3 --- /dev/null +++ b/libkwineffects/Mainpage.dox @@ -0,0 +1,22 @@ +/** @mainpage KWin Effects Library + +

+@ref kwineffects is a library for implementing window transition effect +plugins for KWin. + +@authors +Lubos Lunak \
+Rivo Laks \
+Lucas Murray \
+Fredrik Höglund \
+Martin Gräßlin \ + +@maintainers +Martin Gräßlin \ + +@licences +libkwineffects: @gpl + +*/ + +// DOXYGEN_SET_PROJECT_NAME = KWin Effects Library diff --git a/libkwineffects/Messages.sh b/libkwineffects/Messages.sh new file mode 100644 index 0000000..48a4e63 --- /dev/null +++ b/libkwineffects/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name \*.cpp` -o $podir/libkwineffects.pot diff --git a/libkwineffects/anidata.cpp b/libkwineffects/anidata.cpp new file mode 100644 index 0000000..d2ee9d8 --- /dev/null +++ b/libkwineffects/anidata.cpp @@ -0,0 +1,125 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2011 Thomas Lübking + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "anidata_p.h" + +#include "logging_p.h" + +namespace KWin +{ + +QDebug operator<<(QDebug dbg, const KWin::AniData &a) +{ + dbg.nospace() << a.debugInfo(); + return dbg.space(); +} + +FullScreenEffectLock::FullScreenEffectLock(Effect *effect) +{ + effects->setActiveFullScreenEffect(effect); +} + +FullScreenEffectLock::~FullScreenEffectLock() +{ + effects->setActiveFullScreenEffect(nullptr); +} + +KeepAliveLock::KeepAliveLock(EffectWindow *w) + : m_window(w) +{ + m_window->refWindow(); +} + +KeepAliveLock::~KeepAliveLock() +{ + m_window->unrefWindow(); +} + +PreviousWindowPixmapLock::PreviousWindowPixmapLock(EffectWindow *w) + : m_window(w) +{ + m_window->referencePreviousWindowPixmap(); +} + +PreviousWindowPixmapLock::~PreviousWindowPixmapLock() +{ + m_window->unreferencePreviousWindowPixmap(); + + // Add synthetic repaint to prevent glitches after cross-fading + // translucent windows. + effects->addRepaint(m_window->expandedGeometry()); +} + +AniData::AniData() + : attribute(AnimationEffect::Opacity) + , customCurve(0) // Linear + , meta(0) + , startTime(0) + , waitAtSource(false) + , keepAlive(true) +{ +} + +AniData::AniData(AnimationEffect::Attribute a, int meta_, const FPx2 &to_, + int delay, const FPx2 &from_, bool waitAtSource_, + FullScreenEffectLockPtr fullScreenEffectLock_, bool keepAlive, + PreviousWindowPixmapLockPtr previousWindowPixmapLock_) + : attribute(a) + , from(from_) + , to(to_) + , meta(meta_) + , startTime(AnimationEffect::clock() + delay) + , fullScreenEffectLock(std::move(fullScreenEffectLock_)) + , waitAtSource(waitAtSource_) + , keepAlive(keepAlive) + , previousWindowPixmapLock(std::move(previousWindowPixmapLock_)) +{ +} + +bool AniData::isActive() const +{ + if (!timeLine.done()) { + return true; + } + + if (timeLine.direction() == TimeLine::Backward) { + return !(terminationFlags & AnimationEffect::TerminateAtSource); + } + + return !(terminationFlags & AnimationEffect::TerminateAtTarget); +} + +static QString attributeString(KWin::AnimationEffect::Attribute attribute) +{ + switch (attribute) { + case KWin::AnimationEffect::Opacity: return QStringLiteral("Opacity"); + case KWin::AnimationEffect::Brightness: return QStringLiteral("Brightness"); + case KWin::AnimationEffect::Saturation: return QStringLiteral("Saturation"); + case KWin::AnimationEffect::Scale: return QStringLiteral("Scale"); + case KWin::AnimationEffect::Translation: return QStringLiteral("Translation"); + case KWin::AnimationEffect::Rotation: return QStringLiteral("Rotation"); + case KWin::AnimationEffect::Position: return QStringLiteral("Position"); + case KWin::AnimationEffect::Size: return QStringLiteral("Size"); + case KWin::AnimationEffect::Clip: return QStringLiteral("Clip"); + default: return QStringLiteral(" "); + } +} + +QString AniData::debugInfo() const +{ + return QLatin1String("Animation: ") + attributeString(attribute) + + QLatin1String("\n From: ") + from.toString() + + QLatin1String("\n To: ") + to.toString() + + QLatin1String("\n Started: ") + QString::number(AnimationEffect::clock() - startTime) + QLatin1String("ms ago\n") + + QLatin1String( " Duration: ") + QString::number(timeLine.duration().count()) + QLatin1String("ms\n") + + QLatin1String( " Passed: ") + QString::number(timeLine.elapsed().count()) + QLatin1String("ms\n"); +} + +} // namespace KWin diff --git a/libkwineffects/anidata_p.h b/libkwineffects/anidata_p.h new file mode 100644 index 0000000..c1bedaf --- /dev/null +++ b/libkwineffects/anidata_p.h @@ -0,0 +1,97 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2011 Thomas Lübking + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef ANIDATA_H +#define ANIDATA_H + +#include "kwinanimationeffect.h" + +#include + +namespace KWin { + +/** + * Wraps effects->setActiveFullScreenEffect for the duration of it's lifespan + */ +class FullScreenEffectLock +{ +public: + FullScreenEffectLock(Effect *effect); + ~FullScreenEffectLock(); +private: + Q_DISABLE_COPY(FullScreenEffectLock) +}; +typedef QSharedPointer FullScreenEffectLockPtr; + +/** + * Keeps windows alive during animation after they got closed + */ +class KeepAliveLock +{ +public: + KeepAliveLock(EffectWindow *w); + ~KeepAliveLock(); + +private: + EffectWindow *m_window; + Q_DISABLE_COPY(KeepAliveLock) +}; +typedef QSharedPointer KeepAliveLockPtr; + +/** + * References the previous window pixmap to prevent discarding. + */ +class PreviousWindowPixmapLock +{ +public: + PreviousWindowPixmapLock(EffectWindow *w); + ~PreviousWindowPixmapLock(); + +private: + EffectWindow *m_window; + Q_DISABLE_COPY(PreviousWindowPixmapLock) +}; +typedef QSharedPointer PreviousWindowPixmapLockPtr; + +class KWINEFFECTS_EXPORT AniData { +public: + AniData(); + AniData(AnimationEffect::Attribute a, int meta, const FPx2 &to, + int delay, const FPx2 &from, bool waitAtSource, + FullScreenEffectLockPtr =FullScreenEffectLockPtr(), + bool keepAlive = true, PreviousWindowPixmapLockPtr previousWindowPixmapLock = {}); + + bool isActive() const; + + inline bool isOneDimensional() const { + return from[0] == from[1] && to[0] == to[1]; + } + + quint64 id{0}; + QString debugInfo() const; + AnimationEffect::Attribute attribute; + int customCurve; + FPx2 from, to; + TimeLine timeLine; + uint meta; + qint64 startTime; + QSharedPointer fullScreenEffectLock; + bool waitAtSource; + bool keepAlive; + KeepAliveLockPtr keepAliveLock; + PreviousWindowPixmapLockPtr previousWindowPixmapLock; + AnimationEffect::TerminationFlags terminationFlags; +}; + +} // namespace + +QDebug operator<<(QDebug dbg, const KWin::AniData &a); + +#endif // ANIDATA_H diff --git a/libkwineffects/kwinanimationeffect.cpp b/libkwineffects/kwinanimationeffect.cpp new file mode 100644 index 0000000..eb287aa --- /dev/null +++ b/libkwineffects/kwinanimationeffect.cpp @@ -0,0 +1,1046 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2011 Thomas Lübking + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwinanimationeffect.h" +#include "anidata_p.h" + +#include +#include +#include +#include + +namespace KWin +{ + +QDebug operator<<(QDebug dbg, const KWin::FPx2 &fpx2) +{ + dbg.nospace() << fpx2[0] << "," << fpx2[1] << QString(fpx2.isValid() ? QStringLiteral(" (valid)") : QStringLiteral(" (invalid)")); + return dbg.space(); +} + +QElapsedTimer AnimationEffect::s_clock; + +class AnimationEffectPrivate { +public: + AnimationEffectPrivate() + { + m_animated = m_damageDirty = m_animationsTouched = m_isInitialized = false; + m_justEndedAnimation = 0; + } + AnimationEffect::AniMap m_animations; + static quint64 m_animCounter; + quint64 m_justEndedAnimation; // protect against cancel + QWeakPointer m_fullScreenEffectLock; + bool m_animated, m_damageDirty, m_needSceneRepaint, m_animationsTouched, m_isInitialized; +}; + +quint64 AnimationEffectPrivate::m_animCounter = 0; + +AnimationEffect::AnimationEffect() : d_ptr(new AnimationEffectPrivate()) +{ + Q_D(AnimationEffect); + d->m_animated = false; + if (!s_clock.isValid()) + s_clock.start(); + /* this is the same as the QTimer::singleShot(0, SLOT(init())) kludge + * defering the init and esp. the connection to the windowClosed slot */ + QMetaObject::invokeMethod( this, "init", Qt::QueuedConnection ); +} + +AnimationEffect::~AnimationEffect() +{ + delete d_ptr; +} + +void AnimationEffect::init() +{ + Q_D(AnimationEffect); + if (d->m_isInitialized) + return; // not more than once, please + d->m_isInitialized = true; + /* by connecting the signal from a slot AFTER the inheriting class constructor had the chance to + * connect it we can provide auto-referencing of animated and closed windows, since at the time + * our slot will be called, the slot of the subclass has been (SIGNAL/SLOT connections are FIFO) + * and has pot. started an animation so we have the window in our hash :) */ + connect(effects, &EffectsHandler::windowClosed, this, &AnimationEffect::_windowClosed); + connect(effects, &EffectsHandler::windowDeleted, this, &AnimationEffect::_windowDeleted); +} + +bool AnimationEffect::isActive() const +{ + Q_D(const AnimationEffect); + return !d->m_animations.isEmpty() && !effects->isScreenLocked(); +} + + +#define RELATIVE_XY(_FIELD_) const bool relative[2] = { static_cast(metaData(Relative##_FIELD_##X, meta)), \ + static_cast(metaData(Relative##_FIELD_##Y, meta)) } + +void AnimationEffect::validate(Attribute a, uint &meta, FPx2 *from, FPx2 *to, const EffectWindow *w) const +{ + if (a < NonFloatBase) { + if (a == Scale) { + QRect area = effects->clientArea(ScreenArea , w); + if (from && from->isValid()) { + RELATIVE_XY(Source); + from->set(relative[0] ? (*from)[0] * area.width() / w->width() : (*from)[0], + relative[1] ? (*from)[1] * area.height() / w->height() : (*from)[1]); + } + if (to && to->isValid()) { + RELATIVE_XY(Target); + to->set(relative[0] ? (*to)[0] * area.width() / w->width() : (*to)[0], + relative[1] ? (*to)[1] * area.height() / w->height() : (*to)[1] ); + } + } else if (a == Rotation) { + if (from && !from->isValid()) { + setMetaData(SourceAnchor, metaData(TargetAnchor, meta), meta); + from->set(0.0,0.0); + } + if (to && !to->isValid()) { + setMetaData(TargetAnchor, metaData(SourceAnchor, meta), meta); + to->set(0.0,0.0); + } + } + if (from && !from->isValid()) + from->set(1.0,1.0); + if (to && !to->isValid()) + to->set(1.0,1.0); + + + } else if (a == Position) { + QRect area = effects->clientArea(ScreenArea , w); + QPoint pt = w->geometry().bottomRight(); // cannot be < 0 ;-) + if (from) { + if (from->isValid()) { + RELATIVE_XY(Source); + from->set(relative[0] ? area.x() + (*from)[0] * area.width() : (*from)[0], + relative[1] ? area.y() + (*from)[1] * area.height() : (*from)[1]); + } else { + from->set(pt.x(), pt.y()); + setMetaData(SourceAnchor, AnimationEffect::Bottom|AnimationEffect::Right, meta); + } + } + + if (to) { + if (to->isValid()) { + RELATIVE_XY(Target); + to->set(relative[0] ? area.x() + (*to)[0] * area.width() : (*to)[0], + relative[1] ? area.y() + (*to)[1] * area.height() : (*to)[1]); + } else { + to->set(pt.x(), pt.y()); + setMetaData( TargetAnchor, AnimationEffect::Bottom|AnimationEffect::Right, meta ); + } + } + + + } else if (a == Size) { + QRect area = effects->clientArea(ScreenArea , w); + if (from) { + if (from->isValid()) { + RELATIVE_XY(Source); + from->set(relative[0] ? (*from)[0] * area.width() : (*from)[0], + relative[1] ? (*from)[1] * area.height() : (*from)[1]); + } else { + from->set(w->width(), w->height()); + } + } + + if (to) { + if (to->isValid()) { + RELATIVE_XY(Target); + to->set(relative[0] ? (*to)[0] * area.width() : (*to)[0], + relative[1] ? (*to)[1] * area.height() : (*to)[1]); + } else { + to->set(w->width(), w->height()); + } + } + + } else if (a == Translation) { + QRect area = w->rect(); + if (from) { + if (from->isValid()) { + RELATIVE_XY(Source); + from->set(relative[0] ? (*from)[0] * area.width() : (*from)[0], + relative[1] ? (*from)[1] * area.height() : (*from)[1]); + } else { + from->set(0.0, 0.0); + } + } + + if (to) { + if (to->isValid()) { + RELATIVE_XY(Target); + to->set(relative[0] ? (*to)[0] * area.width() : (*to)[0], + relative[1] ? (*to)[1] * area.height() : (*to)[1]); + } else { + to->set(0.0, 0.0); + } + } + + } else if (a == Clip) { + if (from && !from->isValid()) { + from->set(1.0,1.0); + setMetaData(SourceAnchor, metaData(TargetAnchor, meta), meta); + } + if (to && !to->isValid()) { + to->set(1.0,1.0); + setMetaData(TargetAnchor, metaData(SourceAnchor, meta), meta); + } + + } else if (a == CrossFadePrevious) { + if (from && !from->isValid()) { + from->set(0.0); + } + if (to && !to->isValid()) { + to->set(1.0); + } + } +} + +quint64 AnimationEffect::p_animate( EffectWindow *w, Attribute a, uint meta, int ms, FPx2 to, const QEasingCurve &curve, int delay, FPx2 from, bool keepAtTarget, bool fullScreenEffect, bool keepAlive) +{ + const bool waitAtSource = from.isValid(); + validate(a, meta, &from, &to, w); + + Q_D(AnimationEffect); + if (!d->m_isInitialized) + init(); // needs to ensure the window gets removed if deleted in the same event cycle + if (d->m_animations.isEmpty()) { + connect(effects, &EffectsHandler::windowGeometryShapeChanged, + this, &AnimationEffect::_expandedGeometryChanged); + connect(effects, &EffectsHandler::windowStepUserMovedResized, + this, &AnimationEffect::_expandedGeometryChanged); + connect(effects, &EffectsHandler::windowPaddingChanged, + this, &AnimationEffect::_expandedGeometryChanged); + } + AniMap::iterator it = d->m_animations.find(w); + if (it == d->m_animations.end()) + it = d->m_animations.insert(w, QPair, QRect>(QList(), QRect())); + + FullScreenEffectLockPtr fullscreen; + if (fullScreenEffect) { + if (d->m_fullScreenEffectLock.isNull()) { + fullscreen = FullScreenEffectLockPtr::create(this); + d->m_fullScreenEffectLock = fullscreen.toWeakRef(); + } else { + fullscreen = d->m_fullScreenEffectLock.toStrongRef(); + } + } + + PreviousWindowPixmapLockPtr previousPixmap; + if (a == CrossFadePrevious) { + previousPixmap = PreviousWindowPixmapLockPtr::create(w); + } + + it->first.append(AniData( + a, // Attribute + meta, // Metadata + to, // Target + delay, // Delay + from, // Source + waitAtSource, // Whether the animation should be kept at source + fullscreen, // Full screen effect lock + keepAlive, // Keep alive flag + previousPixmap // Previous window pixmap lock + )); + + const quint64 ret_id = ++d->m_animCounter; + AniData &animation = it->first.last(); + animation.id = ret_id; + + animation.timeLine.setDirection(TimeLine::Forward); + animation.timeLine.setDuration(std::chrono::milliseconds(ms)); + animation.timeLine.setEasingCurve(curve); + animation.timeLine.setSourceRedirectMode(TimeLine::RedirectMode::Strict); + animation.timeLine.setTargetRedirectMode(TimeLine::RedirectMode::Relaxed); + + animation.terminationFlags = TerminateAtSource; + if (!keepAtTarget) { + animation.terminationFlags |= TerminateAtTarget; + } + + it->second = QRect(); + + d->m_animationsTouched = true; + + if (delay > 0) { + QTimer::singleShot(delay, this, &AnimationEffect::triggerRepaint); + const QSize &s = effects->virtualScreenSize(); + if (waitAtSource) + w->addLayerRepaint(0, 0, s.width(), s.height()); + } + else { + triggerRepaint(); + } + return ret_id; +} + +bool AnimationEffect::retarget(quint64 animationId, FPx2 newTarget, int newRemainingTime) +{ + Q_D(AnimationEffect); + if (animationId == d->m_justEndedAnimation) + return false; // this is just ending, do not try to retarget it + for (AniMap::iterator entry = d->m_animations.begin(), + mapEnd = d->m_animations.end(); entry != mapEnd; ++entry) { + for (QList::iterator anim = entry->first.begin(), + animEnd = entry->first.end(); anim != animEnd; ++anim) { + if (anim->id == animationId) { + anim->from.set(interpolated(*anim, 0), interpolated(*anim, 1)); + validate(anim->attribute, anim->meta, nullptr, &newTarget, entry.key()); + anim->to.set(newTarget[0], newTarget[1]); + + anim->timeLine.setDirection(TimeLine::Forward); + anim->timeLine.setDuration(std::chrono::milliseconds(newRemainingTime)); + anim->timeLine.reset(); + + return true; + } + } + } + return false; // no animation found +} + +bool AnimationEffect::redirect(quint64 animationId, Direction direction, TerminationFlags terminationFlags) +{ + Q_D(AnimationEffect); + + if (animationId == d->m_justEndedAnimation) { + return false; + } + + for (auto entryIt = d->m_animations.begin(); entryIt != d->m_animations.end(); ++entryIt) { + auto animIt = std::find_if(entryIt->first.begin(), entryIt->first.end(), + [animationId] (AniData &anim) { + return anim.id == animationId; + } + ); + if (animIt == entryIt->first.end()) { + continue; + } + + switch (direction) { + case Backward: + animIt->timeLine.setDirection(TimeLine::Backward); + break; + + case Forward: + animIt->timeLine.setDirection(TimeLine::Forward); + break; + } + + animIt->terminationFlags = terminationFlags & ~TerminateAtTarget; + + return true; + } + + return false; +} + +bool AnimationEffect::complete(quint64 animationId) +{ + Q_D(AnimationEffect); + + if (animationId == d->m_justEndedAnimation) { + return false; + } + + for (auto entryIt = d->m_animations.begin(); entryIt != d->m_animations.end(); ++entryIt) { + auto animIt = std::find_if(entryIt->first.begin(), entryIt->first.end(), + [animationId] (AniData &anim) { + return anim.id == animationId; + } + ); + if (animIt == entryIt->first.end()) { + continue; + } + + animIt->timeLine.setElapsed(animIt->timeLine.duration()); + + return true; + } + + return false; +} + +bool AnimationEffect::cancel(quint64 animationId) +{ + Q_D(AnimationEffect); + if (animationId == d->m_justEndedAnimation) + return true; // this is just ending, do not try to cancel it but fake success + for (AniMap::iterator entry = d->m_animations.begin(), mapEnd = d->m_animations.end(); entry != mapEnd; ++entry) { + for (QList::iterator anim = entry->first.begin(), animEnd = entry->first.end(); anim != animEnd; ++anim) { + if (anim->id == animationId) { + entry->first.erase(anim); // remove the animation + if (entry->first.isEmpty()) { // no other animations on the window, release it. + d->m_animations.erase(entry); + } + if (d->m_animations.isEmpty()) + disconnectGeometryChanges(); + d->m_animationsTouched = true; // could be called from animationEnded + return true; + } + } + } + return false; +} + +void AnimationEffect::prePaintScreen( ScreenPrePaintData& data, int time ) +{ + Q_D(AnimationEffect); + if (d->m_animations.isEmpty()) { + effects->prePaintScreen(data, time); + return; + } + + d->m_animationsTouched = false; + AniMap::iterator entry = d->m_animations.begin(), mapEnd = d->m_animations.end(); + d->m_animated = false; +// short int transformed = 0; + while (entry != mapEnd) { + bool invalidateLayerRect = false; + QList::iterator anim = entry->first.begin(), animEnd = entry->first.end(); + int animCounter = 0; + while (anim != animEnd) { + if (anim->startTime > clock()) { + if (!anim->waitAtSource) { + ++anim; + ++animCounter; + continue; + } + } else { + anim->timeLine.update(std::chrono::milliseconds(time)); + } + + if (anim->isActive()) { +// if (anim->attribute != Brightness && anim->attribute != Saturation && anim->attribute != Opacity) +// transformed = true; + d->m_animated = true; + ++anim; + ++animCounter; + } else { + EffectWindow *oldW = entry.key(); + d->m_justEndedAnimation = anim->id; + animationEnded(oldW, anim->attribute, anim->meta); + d->m_justEndedAnimation = 0; + // NOTICE animationEnded is an external call and might have called "::animate" + // as a result our iterators could now point random junk on the heap + // so we've to restore the former states, ie. find our window list and animation + if (d->m_animationsTouched) { + d->m_animationsTouched = false; + entry = d->m_animations.begin(), mapEnd = d->m_animations.end(); + while (entry.key() != oldW && entry != mapEnd) + ++entry; + Q_ASSERT(entry != mapEnd); // usercode should not delete animations from animationEnded (not even possible atm.) + anim = entry->first.begin(), animEnd = entry->first.end(); + Q_ASSERT(animCounter < entry->first.count()); + for (int i = 0; i < animCounter; ++i) + ++anim; + } + anim = entry->first.erase(anim); + invalidateLayerRect = d->m_damageDirty = true; + animEnd = entry->first.end(); + } + } + if (entry->first.isEmpty()) { + data.paint |= entry->second; +// d->m_damageDirty = true; // TODO likely no longer required + entry = d->m_animations.erase(entry); + mapEnd = d->m_animations.end(); + } else { + if (invalidateLayerRect) + *const_cast(&(entry->second)) = QRect(); // invalidate + ++entry; + } + } + + // janitorial... + if (d->m_animations.isEmpty()) { + disconnectGeometryChanges(); + } + + effects->prePaintScreen(data, time); +} + +static int xCoord(const QRect &r, int flag) { + if (flag & AnimationEffect::Left) + return r.x(); + else if (flag & AnimationEffect::Right) + return r.right(); + else + return r.x() + r.width()/2; +} + +static int yCoord(const QRect &r, int flag) { + if (flag & AnimationEffect::Top) + return r.y(); + else if (flag & AnimationEffect::Bottom) + return r.bottom(); + else + return r.y() + r.height()/2; +} + +QRect AnimationEffect::clipRect(const QRect &geo, const AniData &anim) const +{ + QRect clip = geo; + FPx2 ratio = anim.from + progress(anim) * (anim.to - anim.from); + if (anim.from[0] < 1.0 || anim.to[0] < 1.0) { + clip.setWidth(clip.width() * ratio[0]); + } + if (anim.from[1] < 1.0 || anim.to[1] < 1.0) { + clip.setHeight(clip.height() * ratio[1]); + } + const QRect center = geo.adjusted(clip.width()/2, clip.height()/2, + -(clip.width()+1)/2, -(clip.height()+1)/2 ); + const int x[2] = { xCoord(center, metaData(SourceAnchor, anim.meta)), + xCoord(center, metaData(TargetAnchor, anim.meta)) }; + const int y[2] = { yCoord(center, metaData(SourceAnchor, anim.meta)), + yCoord(center, metaData(TargetAnchor, anim.meta)) }; + const QPoint d(x[0] + ratio[0]*(x[1]-x[0]), y[0] + ratio[1]*(y[1]-y[0])); + clip.moveTopLeft(QPoint(d.x() - clip.width()/2, d.y() - clip.height()/2)); + return clip; +} + +void AnimationEffect::clipWindow(const EffectWindow *w, const AniData &anim, WindowQuadList &quads) const +{ + return; + const QRect geo = w->expandedGeometry(); + QRect clip = AnimationEffect::clipRect(geo, anim); + WindowQuadList filtered; + if (clip.left() != geo.left()) { + quads = quads.splitAtX(clip.left()); + foreach (const WindowQuad &quad, quads) { + if (quad.right() >= clip.left()) + filtered << quad; + } + quads = filtered; + filtered.clear(); + } + if (clip.right() != geo.right()) { + quads = quads.splitAtX(clip.left()); + foreach (const WindowQuad &quad, quads) { + if (quad.right() <= clip.right()) + filtered << quad; + } + quads = filtered; + filtered.clear(); + } + if (clip.top() != geo.top()) { + quads = quads.splitAtY(clip.top()); + foreach (const WindowQuad &quad, quads) { + if (quad.top() >= clip.top()) + filtered << quad; + } + quads = filtered; + filtered.clear(); + } + if (clip.bottom() != geo.bottom()) { + quads = quads.splitAtY(clip.bottom()); + foreach (const WindowQuad &quad, quads) { + if (quad.bottom() <= clip.bottom()) + filtered << quad; + } + quads = filtered; + } +} + +void AnimationEffect::disconnectGeometryChanges() +{ + disconnect(effects, &EffectsHandler::windowGeometryShapeChanged, + this, &AnimationEffect::_expandedGeometryChanged); + disconnect(effects, &EffectsHandler::windowStepUserMovedResized, + this, &AnimationEffect::_expandedGeometryChanged); + disconnect(effects, &EffectsHandler::windowPaddingChanged, + this, &AnimationEffect::_expandedGeometryChanged); +} + + +void AnimationEffect::prePaintWindow( EffectWindow* w, WindowPrePaintData& data, int time ) +{ + Q_D(AnimationEffect); + if ( d->m_animated ) { + AniMap::const_iterator entry = d->m_animations.constFind( w ); + if ( entry != d->m_animations.constEnd() ) { + bool isUsed = false; + bool paintDeleted = false; + for (QList::const_iterator anim = entry->first.constBegin(); anim != entry->first.constEnd(); ++anim) { + if (anim->startTime > clock() && !anim->waitAtSource) + continue; + + isUsed = true; + if (anim->attribute == Opacity || anim->attribute == CrossFadePrevious) + data.setTranslucent(); + else if (!(anim->attribute == Brightness || anim->attribute == Saturation)) { + data.setTransformed(); + if (anim->attribute == Clip) + clipWindow(w, *anim, data.quads); + } + + paintDeleted |= anim->keepAlive; + } + if ( isUsed ) { + if ( w->isMinimized() ) + w->enablePainting( EffectWindow::PAINT_DISABLED_BY_MINIMIZE ); + else if ( w->isDeleted() && paintDeleted ) + w->enablePainting( EffectWindow::PAINT_DISABLED_BY_DELETE ); + else if ( !w->isOnCurrentDesktop() ) + w->enablePainting( EffectWindow::PAINT_DISABLED_BY_DESKTOP ); +// if( !w->isPaintingEnabled() && !effects->activeFullScreenEffect() ) +// effects->addLayerRepaint(w->expandedGeometry()); + } + } + } + effects->prePaintWindow( w, data, time ); +} + +static inline float geometryCompensation(int flags, float v) +{ + if (flags & (AnimationEffect::Left|AnimationEffect::Top)) + return 0.0; // no compensation required + if (flags & (AnimationEffect::Right|AnimationEffect::Bottom)) + return 1.0 - v; // full compensation + return 0.5 * (1.0 - v); // half compensation +} + +void AnimationEffect::paintWindow( EffectWindow* w, int mask, QRegion region, WindowPaintData& data ) +{ + Q_D(AnimationEffect); + if ( d->m_animated ) { + AniMap::const_iterator entry = d->m_animations.constFind( w ); + if ( entry != d->m_animations.constEnd() ) { + for ( QList::const_iterator anim = entry->first.constBegin(); anim != entry->first.constEnd(); ++anim ) { + + if (anim->startTime > clock() && !anim->waitAtSource) + continue; + + switch (anim->attribute) { + case Opacity: + data.multiplyOpacity(interpolated(*anim)); break; + case Brightness: + data.multiplyBrightness(interpolated(*anim)); break; + case Saturation: + data.multiplySaturation(interpolated(*anim)); break; + case Scale: { + const QSize sz = w->geometry().size(); + float f1(1.0), f2(0.0); + if (anim->from[0] >= 0.0 && anim->to[0] >= 0.0) { // scale x + f1 = interpolated(*anim, 0); + f2 = geometryCompensation( anim->meta & AnimationEffect::Horizontal, f1 ); + data.translate(f2 * sz.width()); + data.setXScale(data.xScale() * f1); + } + if (anim->from[1] >= 0.0 && anim->to[1] >= 0.0) { // scale y + if (!anim->isOneDimensional()) { + f1 = interpolated(*anim, 1); + f2 = geometryCompensation( anim->meta & AnimationEffect::Vertical, f1 ); + } + else if ( ((anim->meta & AnimationEffect::Vertical)>>1) != (anim->meta & AnimationEffect::Horizontal) ) + f2 = geometryCompensation( anim->meta & AnimationEffect::Vertical, f1 ); + data.translate(0.0, f2 * sz.height()); + data.setYScale(data.yScale() * f1); + } + break; + } + case Clip: + region = clipRect(w->expandedGeometry(), *anim); + break; + case Translation: + data += QPointF(interpolated(*anim, 0), interpolated(*anim, 1)); + break; + case Size: { + FPx2 dest = anim->from + progress(*anim) * (anim->to - anim->from); + const QSize sz = w->geometry().size(); + float f; + if (anim->from[0] >= 0.0 && anim->to[0] >= 0.0) { // resize x + f = dest[0]/sz.width(); + data.translate(geometryCompensation( anim->meta & AnimationEffect::Horizontal, f ) * sz.width()); + data.setXScale(data.xScale() * f); + } + if (anim->from[1] >= 0.0 && anim->to[1] >= 0.0) { // resize y + f = dest[1]/sz.height(); + data.translate(0.0, geometryCompensation( anim->meta & AnimationEffect::Vertical, f ) * sz.height()); + data.setYScale(data.yScale() * f); + } + break; + } + case Position: { + const QRect geo = w->geometry(); + const float prgrs = progress(*anim); + if ( anim->from[0] >= 0.0 && anim->to[0] >= 0.0 ) { + float dest = interpolated(*anim, 0); + const int x[2] = { xCoord(geo, metaData(SourceAnchor, anim->meta)), + xCoord(geo, metaData(TargetAnchor, anim->meta)) }; + data.translate(dest - (x[0] + prgrs*(x[1] - x[0]))); + } + if ( anim->from[1] >= 0.0 && anim->to[1] >= 0.0 ) { + float dest = interpolated(*anim, 1); + const int y[2] = { yCoord(geo, metaData(SourceAnchor, anim->meta)), + yCoord(geo, metaData(TargetAnchor, anim->meta)) }; + data.translate(0.0, dest - (y[0] + prgrs*(y[1] - y[0]))); + } + break; + } + case Rotation: { + data.setRotationAxis((Qt::Axis)metaData(Axis, anim->meta)); + const float prgrs = progress(*anim); + data.setRotationAngle(anim->from[0] + prgrs*(anim->to[0] - anim->from[0])); + + const QRect geo = w->rect(); + const uint sAnchor = metaData(SourceAnchor, anim->meta), + tAnchor = metaData(TargetAnchor, anim->meta); + QPointF pt(xCoord(geo, sAnchor), yCoord(geo, sAnchor)); + + if (tAnchor != sAnchor) { + QPointF pt2(xCoord(geo, tAnchor), yCoord(geo, tAnchor)); + pt += static_cast(prgrs)*(pt2 - pt); + } + data.setRotationOrigin(QVector3D(pt)); + break; + } + case Generic: + genericAnimation(w, data, progress(*anim), anim->meta); + break; + case CrossFadePrevious: + data.setCrossFadeProgress(progress(*anim)); + break; + default: + break; + } + } + } + } + effects->paintWindow( w, mask, region, data ); +} + +void AnimationEffect::postPaintScreen() +{ + Q_D(AnimationEffect); + if ( d->m_animated ) { + if (d->m_damageDirty) + updateLayerRepaints(); + if (d->m_needSceneRepaint) { + effects->addRepaintFull(); + } else { + AniMap::const_iterator it = d->m_animations.constBegin(), end = d->m_animations.constEnd(); + for (; it != end; ++it) { + bool addRepaint = false; + QList::const_iterator anim = it->first.constBegin(); + for (; anim != it->first.constEnd(); ++anim) { + if (anim->startTime > clock()) + continue; + if (!anim->timeLine.done()) { + addRepaint = true; + break; + } + } + if (addRepaint) { + it.key()->addLayerRepaint(it->second); + } + } + } + } + effects->postPaintScreen(); +} + +float AnimationEffect::interpolated( const AniData &a, int i ) const +{ + if (a.startTime > clock()) + return a.from[i]; + if (!a.timeLine.done()) + return a.from[i] + a.timeLine.value() * (a.to[i] - a.from[i]); + return a.to[i]; // we're done and "waiting" at the target value +} + +float AnimationEffect::progress( const AniData &a ) const +{ + return a.startTime < clock() ? a.timeLine.value() : 0.0; +} + + +// TODO - get this out of the header - the functionpointer usage of QEasingCurve somehow sucks ;-) +// qreal AnimationEffect::qecGaussian(qreal progress) // exp(-5*(2*x-1)^2) +// { +// progress = 2*progress - 1; +// progress *= -5*progress; +// return qExp(progress); +// } + +int AnimationEffect::metaData( MetaType type, uint meta ) +{ + switch (type) { + case SourceAnchor: + return ((meta>>5) & 0x1f); + case TargetAnchor: + return (meta& 0x1f); + case RelativeSourceX: + case RelativeSourceY: + case RelativeTargetX: + case RelativeTargetY: { + const int shift = 10 + type - RelativeSourceX; + return ((meta>>shift) & 1); + } + case Axis: + return ((meta>>10) & 3); + default: + return 0; + } +} + +void AnimationEffect::setMetaData( MetaType type, uint value, uint &meta ) +{ + switch (type) { + case SourceAnchor: + meta &= ~(0x1f<<5); + meta |= ((value & 0x1f)<<5); + break; + case TargetAnchor: + meta &= ~(0x1f); + meta |= (value & 0x1f); + break; + case RelativeSourceX: + case RelativeSourceY: + case RelativeTargetX: + case RelativeTargetY: { + const int shift = 10 + type - RelativeSourceX; + if (value) + meta |= (1<m_animations.constBegin(), mapEnd = d->m_animations.constEnd(); entry != mapEnd; ++entry) + *const_cast(&(entry->second)) = QRect(); + updateLayerRepaints(); + if (d->m_needSceneRepaint) { + effects->addRepaintFull(); + } else { + AniMap::const_iterator it = d->m_animations.constBegin(), end = d->m_animations.constEnd(); + for (; it != end; ++it) { + it.key()->addLayerRepaint(it->second); + } + } +} + +static float fixOvershoot(float f, const AniData &d, short int dir, float s = 1.1) +{ + switch(d.timeLine.easingCurve().type()) { + case QEasingCurve::InOutElastic: + case QEasingCurve::InOutBack: + return f * s; + case QEasingCurve::InElastic: + case QEasingCurve::OutInElastic: + case QEasingCurve::OutBack: + return (dir&2) ? f * s : f; + case QEasingCurve::OutElastic: + case QEasingCurve::InBack: + return (dir&1) ? f * s : f; + default: + return f; + } +} + +void AnimationEffect::updateLayerRepaints() +{ + Q_D(AnimationEffect); + d->m_needSceneRepaint = false; + for (AniMap::const_iterator entry = d->m_animations.constBegin(), mapEnd = d->m_animations.constEnd(); entry != mapEnd; ++entry) { + if (!entry->second.isNull()) + continue; + float f[2] = {1.0, 1.0}; + float t[2] = {0.0, 0.0}; + bool createRegion = false; + QList rects; + QRect *layerRect = const_cast(&(entry->second)); + for (QList::const_iterator anim = entry->first.constBegin(), animEnd = entry->first.constEnd(); anim != animEnd; ++anim) { + if (anim->startTime > clock()) + continue; + switch (anim->attribute) { + case Opacity: + case Brightness: + case Saturation: + case CrossFadePrevious: + createRegion = true; + break; + case Rotation: + createRegion = false; + *layerRect = QRect(QPoint(0, 0), effects->virtualScreenSize()); + goto region_creation; // sic! no need to do anything else + case Generic: + d->m_needSceneRepaint = true; // we don't know whether this will change visual stacking order + return; // sic! no need to do anything else + case Translation: + case Position: { + createRegion = true; + QRect r(entry.key()->geometry()); + int x[2] = {0,0}; + int y[2] = {0,0}; + if (anim->attribute == Translation) { + x[0] = anim->from[0]; + x[1] = anim->to[0]; + y[0] = anim->from[1]; + y[1] = anim->to[1]; + } else { + if ( anim->from[0] >= 0.0 && anim->to[0] >= 0.0 ) { + x[0] = anim->from[0] - xCoord(r, metaData(SourceAnchor, anim->meta)); + x[1] = anim->to[0] - xCoord(r, metaData(TargetAnchor, anim->meta)); + } + if ( anim->from[1] >= 0.0 && anim->to[1] >= 0.0 ) { + y[0] = anim->from[1] - yCoord(r, metaData(SourceAnchor, anim->meta)); + y[1] = anim->to[1] - yCoord(r, metaData(TargetAnchor, anim->meta)); + } + } + r = entry.key()->expandedGeometry(); + rects << r.translated(x[0], y[0]) << r.translated(x[1], y[1]); + break; + } + case Clip: + createRegion = true; + break; + case Size: + case Scale: { + createRegion = true; + const QSize sz = entry.key()->geometry().size(); + float fx = qMax(fixOvershoot(anim->from[0], *anim, 1), fixOvershoot(anim->to[0], *anim, 2)); +// float fx = qMax(interpolated(*anim,0), anim->to[0]); + if (fx >= 0.0) { + if (anim->attribute == Size) + fx /= sz.width(); + f[0] *= fx; + t[0] += geometryCompensation( anim->meta & AnimationEffect::Horizontal, fx ) * sz.width(); + } +// float fy = qMax(interpolated(*anim,1), anim->to[1]); + float fy = qMax(fixOvershoot(anim->from[1], *anim, 1), fixOvershoot(anim->to[1], *anim, 2)); + if (fy >= 0.0) { + if (anim->attribute == Size) + fy /= sz.height(); + if (!anim->isOneDimensional()) { + f[1] *= fy; + t[1] += geometryCompensation( anim->meta & AnimationEffect::Vertical, fy ) * sz.height(); + } else if ( ((anim->meta & AnimationEffect::Vertical)>>1) != (anim->meta & AnimationEffect::Horizontal) ) { + f[1] *= fx; + t[1] += geometryCompensation( anim->meta & AnimationEffect::Vertical, fx ) * sz.height(); + } + } + break; + } + } + } +region_creation: + if (createRegion) { + const QRect geo = entry.key()->expandedGeometry(); + if (rects.isEmpty()) + rects << geo; + QList::const_iterator r, rEnd = rects.constEnd(); + for ( r = rects.constBegin(); r != rEnd; ++r) { // transform + const_cast(&(*r))->setSize(QSize(qRound(r->width()*f[0]), qRound(r->height()*f[1]))); + const_cast(&(*r))->translate(t[0], t[1]); // "const_cast" - don't do that at home, kids ;-) + } + QRect rect = rects.at(0); + if (rects.count() > 1) { + for ( r = rects.constBegin() + 1; r != rEnd; ++r) // unite + rect |= *r; + const int dx = 110*(rect.width() - geo.width())/100 + 1 - rect.width() + geo.width(); + const int dy = 110*(rect.height() - geo.height())/100 + 1 - rect.height() + geo.height(); + rect.adjust(-dx,-dy,dx,dy); // fix pot. overshoot + } + *layerRect = rect; + } + } + d->m_damageDirty = false; +} + +void AnimationEffect::_expandedGeometryChanged(KWin::EffectWindow *w, const QRect &old) +{ + Q_UNUSED(old) + Q_D(AnimationEffect); + AniMap::const_iterator entry = d->m_animations.constFind(w); + if (entry != d->m_animations.constEnd()) { + *const_cast(&(entry->second)) = QRect(); + updateLayerRepaints(); + if (!entry->second.isNull()) // actually got updated, ie. is in use - ensure it get's a repaint + w->addLayerRepaint(entry->second); + } +} + +void AnimationEffect::_windowClosed( EffectWindow* w ) +{ + Q_D(AnimationEffect); + + auto it = d->m_animations.find(w); + if (it == d->m_animations.end()) { + return; + } + + KeepAliveLockPtr keepAliveLock; + + QList &animations = (*it).first; + for (auto animationIt = animations.begin(); + animationIt != animations.end(); + ++animationIt) { + if (!(*animationIt).keepAlive) { + continue; + } + + if (keepAliveLock.isNull()) { + keepAliveLock = KeepAliveLockPtr::create(w); + } + + (*animationIt).keepAliveLock = keepAliveLock; + } +} + +void AnimationEffect::_windowDeleted( EffectWindow* w ) +{ + Q_D(AnimationEffect); + d->m_animations.remove( w ); +} + + +QString AnimationEffect::debug(const QString &/*parameter*/) const +{ + Q_D(const AnimationEffect); + QString dbg; + if (d->m_animations.isEmpty()) + dbg = QStringLiteral("No window is animated"); + else { + AniMap::const_iterator entry = d->m_animations.constBegin(), mapEnd = d->m_animations.constEnd(); + for (; entry != mapEnd; ++entry) { + QString caption = entry.key()->isDeleted() ? QStringLiteral("[Deleted]") : entry.key()->caption(); + if (caption.isEmpty()) + caption = QStringLiteral("[Untitled]"); + dbg += QLatin1String("Animating window: ") + caption + QLatin1Char('\n'); + QList::const_iterator anim = entry->first.constBegin(), animEnd = entry->first.constEnd(); + for (; anim != animEnd; ++anim) + dbg += anim->debugInfo(); + } + } + return dbg; +} + +AnimationEffect::AniMap AnimationEffect::state() const +{ + Q_D(const AnimationEffect); + return d->m_animations; +} + +} // namespace KWin + +#include "moc_kwinanimationeffect.cpp" diff --git a/libkwineffects/kwinanimationeffect.h b/libkwineffects/kwinanimationeffect.h new file mode 100644 index 0000000..94af2fc --- /dev/null +++ b/libkwineffects/kwinanimationeffect.h @@ -0,0 +1,408 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2011 Thomas Lübking + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef ANIMATION_EFFECT_H +#define ANIMATION_EFFECT_H + +#include +#include +#include +#include +#include + +namespace KWin +{ + +class KWINEFFECTS_EXPORT FPx2 { +public: + FPx2() { f[0] = f[1] = 0.0; valid = false; } + explicit FPx2(float v) { f[0] = f[1] = v; valid = true; } + FPx2(float v1, float v2) { f[0] = v1; f[1] = v2; valid = true; } + FPx2(const FPx2 &other) { f[0] = other.f[0]; f[1] = other.f[1]; valid = other.valid; } + explicit FPx2(const QPoint &other) { f[0] = other.x(); f[1] = other.y(); valid = true; } + explicit FPx2(const QPointF &other) { f[0] = other.x(); f[1] = other.y(); valid = true; } + explicit FPx2(const QSize &other) { f[0] = other.width(); f[1] = other.height(); valid = true; } + explicit FPx2(const QSizeF &other) { f[0] = other.width(); f[1] = other.height(); valid = true; } + inline void invalidate() { valid = false; } + inline bool isValid() const { return valid; } + inline float operator[](int n) const { return f[n]; } + inline QString toString() const { + QString ret; + if (valid) + ret = QString::number(f[0]) + QLatin1Char(',') + QString::number(f[1]); + else + ret = QString(); + return ret; + } + + inline FPx2 &operator=(const FPx2 &other) + { f[0] = other.f[0]; f[1] = other.f[1]; valid = other.valid; return *this; } + inline FPx2 &operator+=(const FPx2 &other) + { f[0] += other[0]; f[1] += other[1]; return *this; } + inline FPx2 &operator-=(const FPx2 &other) + { f[0] -= other[0]; f[1] -= other[1]; return *this; } + inline FPx2 &operator*=(float fl) + { f[0] *= fl; f[1] *= fl; return *this; } + inline FPx2 &operator/=(float fl) + { f[0] /= fl; f[1] /= fl; return *this; } + + friend inline bool operator==(const FPx2 &f1, const FPx2 &f2) + { return f1[0] == f2[0] && f1[1] == f2[1]; } + friend inline bool operator!=(const FPx2 &f1, const FPx2 &f2) + { return f1[0] != f2[0] || f1[1] != f2[1]; } + friend inline const FPx2 operator+(const FPx2 &f1, const FPx2 &f2) + { return FPx2( f1[0] + f2[0], f1[1] + f2[1] ); } + friend inline const FPx2 operator-(const FPx2 &f1, const FPx2 &f2) + { return FPx2( f1[0] - f2[0], f1[1] - f2[1] ); } + friend inline const FPx2 operator*(const FPx2 &f, float fl) + { return FPx2( f[0] * fl, f[1] * fl ); } + friend inline const FPx2 operator*(float fl, const FPx2 &f) + { return FPx2( f[0] * fl, f[1] *fl ); } + friend inline const FPx2 operator-(const FPx2 &f) + { return FPx2( -f[0], -f[1] ); } + friend inline const FPx2 operator/(const FPx2 &f, float fl) + { return FPx2( f[0] / fl, f[1] / fl ); } + + inline void set(float v) { f[0] = v; valid = true; } + inline void set(float v1, float v2) { f[0] = v1; f[1] = v2; valid = true; } + +private: + float f[2]; + bool valid; +}; + +class AniData; +class AnimationEffectPrivate; + +/** + * Base class for animation effects. + * + * AnimationEffect serves as a base class for animation effects. It makes easier + * implementing animated transitions, without having to worry about low-level + * specific stuff, e.g. referencing and unreferencing deleted windows, scheduling + * repaints for the next frame, etc. + * + * Each animation animates one specific attribute, e.g. size, position, scale, etc. + * You can provide your own implementation of the Generic attribute if none of the + * standard attributes(e.g. size, position, etc) satisfy your requirements. + * + * @since 4.8 + */ +class KWINEFFECTS_EXPORT AnimationEffect : public Effect +{ + Q_OBJECT + +public: + enum Anchor { Left = 1<<0, Top = 1<<1, Right = 1<<2, Bottom = 1<<3, + Horizontal = Left|Right, Vertical = Top|Bottom, Mouse = 1<<4 }; + Q_ENUM(Anchor) + + enum Attribute { + Opacity = 0, Brightness, Saturation, Scale, Rotation, + Position, Size, Translation, Clip, Generic, CrossFadePrevious, + NonFloatBase = Position + }; + Q_ENUM(Attribute) + + enum MetaType { SourceAnchor, TargetAnchor, + RelativeSourceX, RelativeSourceY, RelativeTargetX, RelativeTargetY, Axis }; + Q_ENUM(MetaType) + + /** + * This enum type is used to specify the direction of the animation. + * + * @since 5.15 + */ + enum Direction { + Forward, ///< The animation goes from source to target. + Backward ///< The animation goes from target to source. + }; + Q_ENUM(Direction) + + /** + * This enum type is used to specify when the animation should be terminated. + * + * @since 5.15 + */ + enum TerminationFlag { + /** + * Don't terminate the animation when it reaches source or target position. + */ + DontTerminate = 0x00, + /** + * Terminate the animation when it reaches the source position. An animation + * can reach the source position if its direction was changed to go backward + * (from target to source). + */ + TerminateAtSource = 0x01, + /** + * Terminate the animation when it reaches the target position. If this flag + * is not set, then the animation will be persistent. + */ + TerminateAtTarget = 0x02 + }; + Q_FLAGS(TerminationFlag) + Q_DECLARE_FLAGS(TerminationFlags, TerminationFlag) + + /** + * Constructs AnimationEffect. + * + * Whenever you intend to connect to the EffectsHandler::windowClosed() signal, + * do so when reimplementing the constructor. Do not add private slots named + * _windowClosed or _windowDeleted! The AnimationEffect connects them right after + * the construction. + * + * If you shadow the _windowDeleted slot (it doesn't matter that it's a private + * slot), this will lead to segfaults. + * + * If you shadow _windowClosed or connect your slot to EffectsHandler::windowClosed() + * after _windowClosed was connected, animations for closing windows will fail. + */ + AnimationEffect(); + ~AnimationEffect() override; + + bool isActive() const override; + + /** + * Gets stored metadata. + * + * Metadata can be used to store some extra information, for example rotation axis, + * etc. The first 24 bits are reserved for the AnimationEffect class, you can use + * the last 8 bits for custom hints. In case when you transform a Generic attribute, + * all 32 bits are yours and you can use them as you want and read them in your + * genericAnimation() implementation. + * + * @param type The type of the metadata. + * @param meta Where the metadata is stored. + * @returns Stored metadata. + * @since 4.8 + */ + static int metaData(MetaType type, uint meta ); + + /** + * Sets metadata. + * + * @param type The type of the metadata. + * @param value The data to be stored. + * @param meta Where the metadata will be stored. + * @since 4.8 + */ + static void setMetaData(MetaType type, uint value, uint &meta ); + + // Reimplemented from KWin::Effect. + QString debug(const QString ¶meter) const override; + void prePaintScreen( ScreenPrePaintData& data, int time ) override; + void prePaintWindow( EffectWindow* w, WindowPrePaintData& data, int time ) override; + void paintWindow( EffectWindow* w, int mask, QRegion region, WindowPaintData& data ) override; + void postPaintScreen() override; + + /** + * Gaussian (bumper) animation curve for QEasingCurve. + * + * @since 4.8 + */ + static qreal qecGaussian(qreal progress) + { + progress = 2*progress - 1; + progress *= -5*progress; + return qExp(progress); + } + + /** + * @since 4.8 + */ + static inline qint64 clock() { + return s_clock.elapsed(); + } + +protected: + /** + * Starts an animated transition of any supported attribute. + * + * @param w The animated window. + * @param a The animated attribute. + * @param meta Basically a wildcard to carry various extra information, e.g. + * the anchor, relativity or rotation axis. You will probably use it when + * performing Generic animations. + * @param ms How long the transition will last. + * @param to The target value. FPx2 is an agnostic two component float type + * (like QPointF or QSizeF, but without requiring to be either and supporting + * an invalid state). + * @param curve How the animation progresses, e.g. Linear progresses constantly + * while Exponential start slow and becomes very fast in the end. + * @param delay When the animation will start compared to "now" (the window will + * remain at the "from" position until then). + * @param from The starting value, the default is invalid, ie. the attribute for + * the window is not transformed in the beginning. + * @param fullScreen Sets this effect as the active full screen effect for the + * duration of the animation. + * @param keepAlive Whether closed windows should be kept alive during animation. + * @returns An ID that you can use to cancel a running animation. + * @since 4.8 + */ + quint64 animate( EffectWindow *w, Attribute a, uint meta, int ms, const FPx2 &to, const QEasingCurve &curve = QEasingCurve(), int delay = 0, const FPx2 &from = FPx2(), bool fullScreen = false, bool keepAlive = true) + { return p_animate(w, a, meta, ms, to, curve, delay, from, false, fullScreen, keepAlive); } + + /** + * Starts a persistent animated transition of any supported attribute. + * + * This method is equal to animate() with one important difference: + * the target value for the attribute is kept until you call cancel(). + * + * @param w The animated window. + * @param a The animated attribute. + * @param meta Basically a wildcard to carry various extra information, e.g. + * the anchor, relativity or rotation axis. You will probably use it when + * performing Generic animations. + * @param ms How long the transition will last. + * @param to The target value. FPx2 is an agnostic two component float type + * (like QPointF or QSizeF, but without requiring to be either and supporting + * an invalid state). + * @param curve How the animation progresses, e.g. Linear progresses constantly + * while Exponential start slow and becomes very fast in the end. + * @param delay When the animation will start compared to "now" (the window will + * remain at the "from" position until then). + * @param from The starting value, the default is invalid, ie. the attribute for + * the window is not transformed in the beginning. + * @param fullScreen Sets this effect as the active full screen effect for the + * duration of the animation. + * @param keepAlive Whether closed windows should be kept alive during animation. + * @returns An ID that you need to use to cancel this manipulation. + * @since 4.11 + */ + quint64 set( EffectWindow *w, Attribute a, uint meta, int ms, const FPx2 &to, const QEasingCurve &curve = QEasingCurve(), int delay = 0, const FPx2 &from = FPx2(), bool fullScreen = false, bool keepAlive = true) + { return p_animate(w, a, meta, ms, to, curve, delay, from, true, fullScreen, keepAlive); } + + /** + * Changes the target (but not type or curve) of a running animation. + * + * Please use cancel() to cancel an animation rather than altering it. + * + * @param animationId The id of the animation to be retargetted. + * @param newTarget The new target. + * @param newRemainingTime The new duration of the transition. By default (-1), + * the remaining time remains unchanged. + * @returns @c true if the animation was retargetted successfully, @c false otherwise. + * @note You can NOT retarget an animation that just has just ended! + * @since 5.6 + */ + bool retarget(quint64 animationId, FPx2 newTarget, int newRemainingTime = -1); + + /** + * Changes the direction of the animation. + * + * @param animationId The id of the animation. + * @param direction The new direction of the animation. + * @param terminationFlags Whether the animation should be terminated when it + * reaches the source position after its direction was changed to go backward. + * Currently, TerminationFlag::TerminateAtTarget has no effect. + * @returns @c true if the direction of the animation was changed successfully, + * otherwise @c false. + * @since 5.15 + */ + bool redirect(quint64 animationId, + Direction direction, + TerminationFlags terminationFlags = TerminateAtSource); + + /** + * Fast-forwards the animation to the target position. + * + * @param animationId The id of the animation. + * @returns @c true if the animation was fast-forwarded successfully, otherwise + * @c false. + * @since 5.15 + */ + bool complete(quint64 animationId); + + /** + * Called whenever an animation ends. + * + * You can reimplement this method to keep a constant transformation for the window + * (i.e. keep it at some opacity or position) or to start another animation. + * + * @param w The animated window. + * @param a The animated attribute. + * @param meta Originally supplied metadata to animate() or set(). + * @since 4.8 + */ + virtual void animationEnded(EffectWindow *w, Attribute a, uint meta) + {Q_UNUSED(w); Q_UNUSED(a); Q_UNUSED(meta);} + + /** + * Cancels a running animation. + * + * @param animationId The id of the animation. + * @returns @c true if the animation was found (and canceled), @c false otherwise. + * @note There is NO animated reset of the original value. You'll have to provide + * that with a second animation. + * @note This will eventually release a Deleted window as well. + * @note If you intend to run another animation on the (Deleted) window, you have + * to do that before cancelling the old animation (to keep the window around). + * @since 4.11 + */ + bool cancel(quint64 animationId); + + /** + * Called whenever animation that transforms Generic attribute needs to be painted. + * + * You should reimplement this method if you transform Generic attribute. @p meta + * can be used to support more than one additional animations. + * + * @param w The animated window. + * @param data The paint data. + * @param progress Current progress value. + * @param meta The metadata. + * @since 4.8 + */ + virtual void genericAnimation( EffectWindow *w, WindowPaintData &data, float progress, uint meta ) + {Q_UNUSED(w); Q_UNUSED(data); Q_UNUSED(progress); Q_UNUSED(meta);} + + /** + * @internal + */ + typedef QMap, QRect> > AniMap; + + /** + * @internal + */ + AniMap state() const; + +private: + quint64 p_animate(EffectWindow *w, Attribute a, uint meta, int ms, FPx2 to, const QEasingCurve &curve, int delay, FPx2 from, bool keepAtTarget, bool fullScreenEffect, bool keepAlive); + QRect clipRect(const QRect &windowRect, const AniData&) const; + void clipWindow(const EffectWindow *, const AniData &, WindowQuadList &) const; + float interpolated( const AniData&, int i = 0 ) const; + float progress( const AniData& ) const; + void disconnectGeometryChanges(); + void updateLayerRepaints(); + void validate(Attribute a, uint &meta, FPx2 *from, FPx2 *to, const EffectWindow *w) const; + +private Q_SLOTS: + void init(); + void triggerRepaint(); + void _windowClosed( KWin::EffectWindow* w ); + void _windowDeleted( KWin::EffectWindow* w ); + void _expandedGeometryChanged(KWin::EffectWindow *w, const QRect &old); + +private: + static QElapsedTimer s_clock; + AnimationEffectPrivate * const d_ptr; + Q_DECLARE_PRIVATE(AnimationEffect) + Q_DISABLE_COPY(AnimationEffect) +}; + +} // namespace + +QDebug operator<<(QDebug dbg, const KWin::FPx2 &fpx2); + +Q_DECLARE_METATYPE(KWin::FPx2) +Q_DECLARE_OPERATORS_FOR_FLAGS(KWin::AnimationEffect::TerminationFlags) + +#endif // ANIMATION_EFFECT_H diff --git a/libkwineffects/kwinconfig.h.cmake b/libkwineffects/kwinconfig.h.cmake new file mode 100644 index 0000000..4718cf0 --- /dev/null +++ b/libkwineffects/kwinconfig.h.cmake @@ -0,0 +1,26 @@ +/* + + This file includes config #define's for KWin's libraries + that are installed. Installed files and files using them + should be using these instead of their own. + +*/ + +#ifndef KWINCONFIG_H +#define KWINCONFIG_H + +/* + + These should be primarily used to detect what kind of compositing + support is available. + +*/ + +/* KWIN_HAVE_XRENDER_COMPOSITING - whether XRender-based compositing support is available */ +#cmakedefine KWIN_HAVE_XRENDER_COMPOSITING + +#cmakedefine01 HAVE_EPOXY_GLX + +#cmakedefine01 HAVE_DL_LIBRARY + +#endif diff --git a/libkwineffects/kwineffectquickview.cpp b/libkwineffects/kwineffectquickview.cpp new file mode 100644 index 0000000..b2554cf --- /dev/null +++ b/libkwineffects/kwineffectquickview.cpp @@ -0,0 +1,380 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 David Edmundson + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwineffectquickview.h" + +#include "kwinglutils.h" +#include "kwineffects.h" +#include "logging_p.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +namespace KWin +{ + +static std::unique_ptr s_shareContext; + +class Q_DECL_HIDDEN EffectQuickView::Private +{ +public: + QQuickWindow *m_view; + QQuickRenderControl *m_renderControl; + QScopedPointer m_glcontext; + QScopedPointer m_offscreenSurface; + QScopedPointer m_fbo; + + QImage m_image; + QScopedPointer m_textureExport; + // if we should capture a QImage after rendering into our BO. + // Used for either software QtQuick rendering and nonGL kwin rendering + bool m_useBlit = false; + bool m_visible = true; + + void releaseResources(); +}; + +class Q_DECL_HIDDEN EffectQuickScene::Private +{ +public: + KDeclarative::QmlObjectSharedEngine *qmlObject = nullptr; +}; + +EffectQuickView::EffectQuickView(QObject *parent) + : EffectQuickView(parent, effects->isOpenGLCompositing() ? ExportMode::Texture : ExportMode::Image) +{ +} + +EffectQuickView::EffectQuickView(QObject *parent, ExportMode exportMode) + : QObject(parent) + , d(new EffectQuickView::Private) +{ + d->m_renderControl = new QQuickRenderControl(this); + + d->m_view = new QQuickWindow(d->m_renderControl); + d->m_view->setFlags(Qt::FramelessWindowHint); + d->m_view->setColor(Qt::transparent); + + if (exportMode == ExportMode::Image) { + d->m_useBlit = true; + } + + const bool usingGl = d->m_view->rendererInterface()->graphicsApi() == QSGRendererInterface::OpenGL; + + if (!usingGl) { + qCDebug(LIBKWINEFFECTS) << "QtQuick Software rendering mode detected"; + d->m_useBlit = true; + d->m_renderControl->initialize(nullptr); + } else { + QSurfaceFormat format; + format.setOption(QSurfaceFormat::ResetNotification); + format.setDepthBufferSize(16); + format.setStencilBufferSize(8); + + d->m_glcontext.reset(new QOpenGLContext); + d->m_glcontext->setShareContext(s_shareContext.get()); + d->m_glcontext->setFormat(format); + d->m_glcontext->create(); + + // and the offscreen surface + d->m_offscreenSurface.reset(new QOffscreenSurface); + d->m_offscreenSurface->setFormat(d->m_glcontext->format()); + d->m_offscreenSurface->create(); + + d->m_glcontext->makeCurrent(d->m_offscreenSurface.data()); + d->m_renderControl->initialize(d->m_glcontext.data()); + d->m_glcontext->doneCurrent(); + + if (!d->m_glcontext->shareContext()) { + qCDebug(LIBKWINEFFECTS) << "Failed to create a shared context, falling back to raster rendering"; + + qCDebug(LIBKWINEFFECTS) << "Extra debug:"; + qCDebug(LIBKWINEFFECTS) << "our context:" << d->m_glcontext.data(); + qCDebug(LIBKWINEFFECTS) << "share context:" << s_shareContext.get(); + + // still render via GL, but blit for presentation + d->m_useBlit = true; + } + } + + auto updateSize = [this]() { contentItem()->setSize(d->m_view->size()); }; + updateSize(); + connect(d->m_view, &QWindow::widthChanged, this, updateSize); + connect(d->m_view, &QWindow::heightChanged, this, updateSize); + + QTimer *t = new QTimer(this); + t->setSingleShot(true); + t->setInterval(10); + + connect(t, &QTimer::timeout, this, &EffectQuickView::update); + connect(d->m_renderControl, &QQuickRenderControl::renderRequested, t, [t]() { t->start(); }); + connect(d->m_renderControl, &QQuickRenderControl::sceneChanged, t, [t]() { t->start(); }); +} + +EffectQuickView::~EffectQuickView() +{ + if (d->m_glcontext) { + d->m_glcontext->makeCurrent(d->m_offscreenSurface.data()); + d->m_renderControl->invalidate(); + d->m_glcontext->doneCurrent(); + } +} + +void EffectQuickView::update() +{ + if (!d->m_visible) { + return; + } + if (d->m_view->size().isEmpty()) { + return; + } + + bool usingGl = d->m_glcontext; + + if (usingGl) { + if (!d->m_glcontext->makeCurrent(d->m_offscreenSurface.data())) { + // probably a context loss event, kwin is about to reset all the effects anyway + return; + } + + if (d->m_fbo.isNull() || d->m_fbo->size() != d->m_view->size()) { + d->m_textureExport.reset(nullptr); + d->m_fbo.reset(new QOpenGLFramebufferObject(d->m_view->size(), QOpenGLFramebufferObject::CombinedDepthStencil)); + if (!d->m_fbo->isValid()) { + d->m_fbo.reset(); + d->m_glcontext->doneCurrent(); + return; + } + } + d->m_view->setRenderTarget(d->m_fbo.data()); + } + + d->m_renderControl->polishItems(); + d->m_renderControl->sync(); + + d->m_renderControl->render(); + if (usingGl) { + d->m_view->resetOpenGLState(); + } + + if (d->m_useBlit) { + d->m_image = d->m_renderControl->grab(); + } + + if (usingGl) { + QOpenGLFramebufferObject::bindDefault(); + d->m_glcontext->doneCurrent(); + } + emit repaintNeeded(); +} + +void EffectQuickView::forwardMouseEvent(QEvent *e) +{ + if (!d->m_visible) { + return; + } + switch (e->type()) { + case QEvent::MouseMove: + case QEvent::MouseButtonPress: + case QEvent::MouseButtonRelease: + case QEvent::MouseButtonDblClick: + { + QMouseEvent *me = static_cast(e); + const QPoint widgetPos = d->m_view->mapFromGlobal(me->pos()); + QMouseEvent cloneEvent(me->type(), widgetPos, me->pos(), me->button(), me->buttons(), me->modifiers()); + QCoreApplication::sendEvent(d->m_view, &cloneEvent); + e->setAccepted(cloneEvent.isAccepted()); + return; + } + case QEvent::HoverEnter: + case QEvent::HoverLeave: + case QEvent::HoverMove: + { + QHoverEvent *he = static_cast(e); + const QPointF widgetPos = d->m_view->mapFromGlobal(he->pos()); + const QPointF oldWidgetPos = d->m_view->mapFromGlobal(he->oldPos()); + QHoverEvent cloneEvent(he->type(), widgetPos, oldWidgetPos, he->modifiers()); + QCoreApplication::sendEvent(d->m_view, &cloneEvent); + e->setAccepted(cloneEvent.isAccepted()); + return; + } + case QEvent::Wheel: + { + QWheelEvent *we = static_cast(e); + const QPointF widgetPos = d->m_view->mapFromGlobal(we->pos()); + QWheelEvent cloneEvent(widgetPos, we->globalPosF(), we->pixelDelta(), we->angleDelta(), we->buttons(), + we->modifiers(), we->phase(), we->inverted()); + QCoreApplication::sendEvent(d->m_view, &cloneEvent); + e->setAccepted(cloneEvent.isAccepted()); + return; + } + default: + return; + } +} + +void EffectQuickView::forwardKeyEvent(QKeyEvent *keyEvent) +{ + if (!d->m_visible) { + return; + } + QCoreApplication::sendEvent(d->m_view, keyEvent); +} + +void EffectQuickView::setShareContext(std::unique_ptr context) +{ + s_shareContext = std::move(context); +} + +QRect EffectQuickView::geometry() const +{ + return d->m_view->geometry(); +} + +QQuickItem *EffectQuickView::contentItem() const +{ + return d->m_view->contentItem(); +} + +void EffectQuickView::setVisible(bool visible) +{ + if (d->m_visible == visible) { + return; + } + d->m_visible = visible; + + if (visible){ + d->m_renderControl->renderRequested(); + } else { + // deferred to not change GL context + QTimer::singleShot(0, this, [this]() { + d->releaseResources(); + }); + } +} + +bool EffectQuickView::isVisible() const +{ + return d->m_visible; +} + +void EffectQuickView::show() +{ + setVisible(true); +} + +void EffectQuickView::hide() +{ + setVisible(false); +} + +GLTexture *EffectQuickView::bufferAsTexture() +{ + if (d->m_useBlit) { + if (d->m_image.isNull()) { + return nullptr; + } + d->m_textureExport.reset(new GLTexture(d->m_image)); + } else { + if (!d->m_fbo) { + return nullptr; + } + if (!d->m_textureExport) { + d->m_textureExport.reset(new GLTexture(d->m_fbo->texture(), d->m_fbo->format().internalTextureFormat(), d->m_fbo->size())); + } + } + return d->m_textureExport.data(); +} + +QImage EffectQuickView::bufferAsImage() const +{ + return d->m_image; +} + +QSize EffectQuickView::size() const +{ + return d->m_view->geometry().size(); +} + +void EffectQuickView::setGeometry(const QRect &rect) +{ + const QRect oldGeometry = d->m_view->geometry(); + d->m_view->setGeometry(rect); + emit geometryChanged(oldGeometry, rect); +} + +void EffectQuickView::Private::releaseResources() +{ + if (m_glcontext) { + m_glcontext->makeCurrent(m_offscreenSurface.data()); + m_view->releaseResources(); + m_glcontext->doneCurrent(); + } else { + m_view->releaseResources(); + } +} + +EffectQuickScene::EffectQuickScene(QObject *parent) + : EffectQuickView(parent) + , d(new EffectQuickScene::Private) +{ + d->qmlObject = new KDeclarative::QmlObjectSharedEngine(this); +} + +EffectQuickScene::EffectQuickScene(QObject *parent, EffectQuickView::ExportMode exportMode) + : EffectQuickView(parent, exportMode) + , d(new EffectQuickScene::Private) +{ + d->qmlObject = new KDeclarative::QmlObjectSharedEngine(this); +} + +EffectQuickScene::~EffectQuickScene() +{ +} + +void EffectQuickScene::setSource(const QUrl &source) +{ + d->qmlObject->setSource(source); + + QQuickItem *item = rootItem(); + if (!item) { + qCDebug(LIBKWINEFFECTS) << "Could not load effect quick view" << source; + return; + } + item->setParentItem(contentItem()); + + auto updateSize = [item, this]() { item->setSize(contentItem()->size()); }; + updateSize(); + connect(contentItem(), &QQuickItem::widthChanged, item, updateSize); + connect(contentItem(), &QQuickItem::heightChanged, item, updateSize); +} + +QQmlContext *EffectQuickScene::rootContext() const +{ + return d->qmlObject->rootContext(); +} + +QQuickItem *EffectQuickScene::rootItem() const +{ + return qobject_cast(d->qmlObject->rootObject()); +} + +} // namespace KWin diff --git a/libkwineffects/kwineffectquickview.h b/libkwineffects/kwineffectquickview.h new file mode 100644 index 0000000..8a6e7b4 --- /dev/null +++ b/libkwineffects/kwineffectquickview.h @@ -0,0 +1,168 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 David Edmundson + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +#include + +#include "kwineffects.h" + +#include + +class QKeyEvent; +class QMouseEvent; +class QOpenGLContext; + +class QMouseEvent; +class QKeyEvent; + +class QQmlContext; +class QQuickItem; + +namespace KWin +{ +class GLTexture; + +class EffectQuickView; + +/** + * @brief The KwinQuickView class provides a convenient API for exporting + * QtQuick scenes as buffers that can be composited in any other fashion. + * + * Contents can be fetched as a GL Texture or as a QImage + * If data is to be fetched as an image, it should be specified upfront as + * blitting is performed when we update our FBO to keep kwin's render loop + * as fast as possible. + */ +class KWINEFFECTS_EXPORT EffectQuickView : public QObject +{ + Q_OBJECT + +public: + static void setShareContext(std::unique_ptr context); + + enum class ExportMode { + /** The contents will be available as a texture in the shared contexts. Image will be blank*/ + Texture, + /** The contents will be blit during the update into a QImage buffer. */ + Image + }; + + /** + * Construct a new KWinQuickView + * Export mode will be determined by the current effectsHandler + */ + EffectQuickView(QObject *parent); + + /** + * Construct a new KWinQuickView explicitly stating an export mode + */ + EffectQuickView(QObject *parent, ExportMode exportMode); + + /** + * Note that this may change the current GL Context + */ + ~EffectQuickView(); + + QSize size() const; + + /** + * The geometry of the current view + * This may be out of sync with the current buffer size if an update is pending + */ + void setGeometry(const QRect &rect); + QRect geometry() const; + + /** + * Render the current scene graph into the FBO. + * This is typically done automatically when the scene changes + * albeit deffered by a timer + * + * It can be manually invoked to update the contents immediately. + * Note this will change the GL context + */ + void update(); + + /** The invisble root item of the window*/ + QQuickItem *contentItem() const; + + /** + * @brief Marks the window as visible/invisible + * This can be used to release resources used by the window + * The default is true. + */ + void setVisible(bool visible); + bool isVisible() const; + + void show(); + void hide(); + + /** + * Returns the current output of the scene graph + * @note The render context must valid at the time of calling + */ + GLTexture *bufferAsTexture(); + + /** + * Returns the current output of the scene graph + */ + QImage bufferAsImage() const; + + /** + * Inject any mouse event into the QQuickWindow. + * Local co-ordinates are transformed + * If it is handled the event will be accepted + */ + void forwardMouseEvent(QEvent *mouseEvent); + /** + * Inject a key event into the window. + * If it is handled the event will be accepted + */ + void forwardKeyEvent(QKeyEvent *keyEvent); + +Q_SIGNALS: + /** + * The frame buffer has changed, contents need re-rendering on screen + */ + void repaintNeeded(); + void geometryChanged(const QRect &oldGeometry, const QRect &newGeometry); + +private: + class Private; + QScopedPointer d; +}; + +/** + * The KWinQuickScene class extends KWinQuickView + * adding QML support. This will represent a context + * powered by an engine + */ +class KWINEFFECTS_EXPORT EffectQuickScene : public EffectQuickView +{ +public: + EffectQuickScene(QObject *parent); + EffectQuickScene(QObject *parent, ExportMode exportMode); + ~EffectQuickScene(); + + QQmlContext *rootContext() const; + /** top level item in the given source*/ + QQuickItem *rootItem() const; + + void setSource(const QUrl &source); + +private: + class Private; + QScopedPointer d; +}; + +} diff --git a/libkwineffects/kwineffects.cpp b/libkwineffects/kwineffects.cpp new file mode 100644 index 0000000..d1c3e2d --- /dev/null +++ b/libkwineffects/kwineffects.cpp @@ -0,0 +1,1930 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwineffects.h" + +#include "config-kwin.h" +#ifdef KWIN_HAVE_XRENDER_COMPOSITING +#include "kwinxrenderutils.h" +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#ifdef KWIN_HAVE_XRENDER_COMPOSITING +#include +#endif + +#if defined(__SSE2__) +# include +#endif + + +namespace KWin +{ + +void WindowPrePaintData::setTranslucent() +{ + mask |= Effect::PAINT_WINDOW_TRANSLUCENT; + mask &= ~Effect::PAINT_WINDOW_OPAQUE; + clip = QRegion(); // cannot clip, will be transparent +} + +void WindowPrePaintData::setTransformed() +{ + mask |= Effect::PAINT_WINDOW_TRANSFORMED; +} + +class PaintDataPrivate { +public: + QGraphicsScale scale; + QVector3D translation; + QGraphicsRotation rotation; +}; + +PaintData::PaintData() + : d(new PaintDataPrivate()) +{ +} + +PaintData::~PaintData() +{ + delete d; +} + +qreal PaintData::xScale() const +{ + return d->scale.xScale(); +} + +qreal PaintData::yScale() const +{ + return d->scale.yScale(); +} + +qreal PaintData::zScale() const +{ + return d->scale.zScale(); +} + +void PaintData::setScale(const QVector2D &scale) +{ + d->scale.setXScale(scale.x()); + d->scale.setYScale(scale.y()); +} + +void PaintData::setScale(const QVector3D &scale) +{ + d->scale.setXScale(scale.x()); + d->scale.setYScale(scale.y()); + d->scale.setZScale(scale.z()); +} + +void PaintData::setXScale(qreal scale) +{ + d->scale.setXScale(scale); +} + +void PaintData::setYScale(qreal scale) +{ + d->scale.setYScale(scale); +} + +void PaintData::setZScale(qreal scale) +{ + d->scale.setZScale(scale); +} + +const QGraphicsScale &PaintData::scale() const +{ + return d->scale; +} + +void PaintData::setXTranslation(qreal translate) +{ + d->translation.setX(translate); +} + +void PaintData::setYTranslation(qreal translate) +{ + d->translation.setY(translate); +} + +void PaintData::setZTranslation(qreal translate) +{ + d->translation.setZ(translate); +} + +void PaintData::translate(qreal x, qreal y, qreal z) +{ + translate(QVector3D(x, y, z)); +} + +void PaintData::translate(const QVector3D &t) +{ + d->translation += t; +} + +qreal PaintData::xTranslation() const +{ + return d->translation.x(); +} + +qreal PaintData::yTranslation() const +{ + return d->translation.y(); +} + +qreal PaintData::zTranslation() const +{ + return d->translation.z(); +} + +const QVector3D &PaintData::translation() const +{ + return d->translation; +} + +qreal PaintData::rotationAngle() const +{ + return d->rotation.angle(); +} + +QVector3D PaintData::rotationAxis() const +{ + return d->rotation.axis(); +} + +QVector3D PaintData::rotationOrigin() const +{ + return d->rotation.origin(); +} + +void PaintData::setRotationAngle(qreal angle) +{ + d->rotation.setAngle(angle); +} + +void PaintData::setRotationAxis(Qt::Axis axis) +{ + d->rotation.setAxis(axis); +} + +void PaintData::setRotationAxis(const QVector3D &axis) +{ + d->rotation.setAxis(axis); +} + +void PaintData::setRotationOrigin(const QVector3D &origin) +{ + d->rotation.setOrigin(origin); +} + +class WindowPaintDataPrivate { +public: + qreal opacity; + qreal saturation; + qreal brightness; + int screen; + qreal crossFadeProgress; + QMatrix4x4 pMatrix; + QMatrix4x4 mvMatrix; + QMatrix4x4 screenProjectionMatrix; +}; + +WindowPaintData::WindowPaintData(EffectWindow *w) + : WindowPaintData(w, QMatrix4x4()) +{ +} + +WindowPaintData::WindowPaintData(EffectWindow* w, const QMatrix4x4 &screenProjectionMatrix) + : PaintData() + , shader(nullptr) + , d(new WindowPaintDataPrivate()) +{ + d->screenProjectionMatrix = screenProjectionMatrix; + quads = w->buildQuads(); + setOpacity(w->opacity()); + setSaturation(1.0); + setBrightness(1.0); + setScreen(0); + setCrossFadeProgress(1.0); +} + +WindowPaintData::WindowPaintData(const WindowPaintData &other) + : PaintData() + , quads(other.quads) + , shader(other.shader) + , d(new WindowPaintDataPrivate()) +{ + setXScale(other.xScale()); + setYScale(other.yScale()); + setZScale(other.zScale()); + translate(other.translation()); + setRotationOrigin(other.rotationOrigin()); + setRotationAxis(other.rotationAxis()); + setRotationAngle(other.rotationAngle()); + setOpacity(other.opacity()); + setSaturation(other.saturation()); + setBrightness(other.brightness()); + setScreen(other.screen()); + setCrossFadeProgress(other.crossFadeProgress()); + setProjectionMatrix(other.projectionMatrix()); + setModelViewMatrix(other.modelViewMatrix()); + d->screenProjectionMatrix = other.d->screenProjectionMatrix; +} + +WindowPaintData::~WindowPaintData() +{ + delete d; +} + +qreal WindowPaintData::opacity() const +{ + return d->opacity; +} + +qreal WindowPaintData::saturation() const +{ + return d->saturation; +} + +qreal WindowPaintData::brightness() const +{ + return d->brightness; +} + +int WindowPaintData::screen() const +{ + return d->screen; +} + +void WindowPaintData::setOpacity(qreal opacity) +{ + d->opacity = opacity; +} + +void WindowPaintData::setSaturation(qreal saturation) const +{ + d->saturation = saturation; +} + +void WindowPaintData::setBrightness(qreal brightness) +{ + d->brightness = brightness; +} + +void WindowPaintData::setScreen(int screen) const +{ + d->screen = screen; +} + +qreal WindowPaintData::crossFadeProgress() const +{ + return d->crossFadeProgress; +} + +void WindowPaintData::setCrossFadeProgress(qreal factor) +{ + d->crossFadeProgress = qBound(qreal(0.0), factor, qreal(1.0)); +} + +qreal WindowPaintData::multiplyOpacity(qreal factor) +{ + d->opacity *= factor; + return d->opacity; +} + +qreal WindowPaintData::multiplySaturation(qreal factor) +{ + d->saturation *= factor; + return d->saturation; +} + +qreal WindowPaintData::multiplyBrightness(qreal factor) +{ + d->brightness *= factor; + return d->brightness; +} + +void WindowPaintData::setProjectionMatrix(const QMatrix4x4 &matrix) +{ + d->pMatrix = matrix; +} + +QMatrix4x4 WindowPaintData::projectionMatrix() const +{ + return d->pMatrix; +} + +QMatrix4x4 &WindowPaintData::rprojectionMatrix() +{ + return d->pMatrix; +} + +void WindowPaintData::setModelViewMatrix(const QMatrix4x4 &matrix) +{ + d->mvMatrix = matrix; +} + +QMatrix4x4 WindowPaintData::modelViewMatrix() const +{ + return d->mvMatrix; +} + +QMatrix4x4 &WindowPaintData::rmodelViewMatrix() +{ + return d->mvMatrix; +} + +WindowPaintData &WindowPaintData::operator*=(qreal scale) +{ + this->setXScale(this->xScale() * scale); + this->setYScale(this->yScale() * scale); + this->setZScale(this->zScale() * scale); + return *this; +} + +WindowPaintData &WindowPaintData::operator*=(const QVector2D &scale) +{ + this->setXScale(this->xScale() * scale.x()); + this->setYScale(this->yScale() * scale.y()); + return *this; +} + +WindowPaintData &WindowPaintData::operator*=(const QVector3D &scale) +{ + this->setXScale(this->xScale() * scale.x()); + this->setYScale(this->yScale() * scale.y()); + this->setZScale(this->zScale() * scale.z()); + return *this; +} + +WindowPaintData &WindowPaintData::operator+=(const QPointF &translation) +{ + return this->operator+=(QVector3D(translation)); +} + +WindowPaintData &WindowPaintData::operator+=(const QPoint &translation) +{ + return this->operator+=(QVector3D(translation)); +} + +WindowPaintData &WindowPaintData::operator+=(const QVector2D &translation) +{ + return this->operator+=(QVector3D(translation)); +} + +WindowPaintData &WindowPaintData::operator+=(const QVector3D &translation) +{ + translate(translation); + return *this; +} + +QMatrix4x4 WindowPaintData::screenProjectionMatrix() const +{ + return d->screenProjectionMatrix; +} + +class ScreenPaintData::Private +{ +public: + QMatrix4x4 projectionMatrix; + QRect outputGeometry; + qreal screenScale; +}; + +ScreenPaintData::ScreenPaintData() + : PaintData() + , d(new Private()) +{ +} + +ScreenPaintData::ScreenPaintData(const QMatrix4x4 &projectionMatrix, const QRect &outputGeometry, const qreal screenScale) + : PaintData() + , d(new Private()) +{ + d->projectionMatrix = projectionMatrix; + d->outputGeometry = outputGeometry; + d->screenScale = screenScale; +} + +ScreenPaintData::~ScreenPaintData() = default; + +ScreenPaintData::ScreenPaintData(const ScreenPaintData &other) + : PaintData() + , d(new Private()) +{ + translate(other.translation()); + setXScale(other.xScale()); + setYScale(other.yScale()); + setZScale(other.zScale()); + setRotationOrigin(other.rotationOrigin()); + setRotationAxis(other.rotationAxis()); + setRotationAngle(other.rotationAngle()); + d->projectionMatrix = other.d->projectionMatrix; + d->outputGeometry = other.d->outputGeometry; +} + +ScreenPaintData &ScreenPaintData::operator=(const ScreenPaintData &rhs) +{ + setXScale(rhs.xScale()); + setYScale(rhs.yScale()); + setZScale(rhs.zScale()); + setXTranslation(rhs.xTranslation()); + setYTranslation(rhs.yTranslation()); + setZTranslation(rhs.zTranslation()); + setRotationOrigin(rhs.rotationOrigin()); + setRotationAxis(rhs.rotationAxis()); + setRotationAngle(rhs.rotationAngle()); + d->projectionMatrix = rhs.d->projectionMatrix; + d->outputGeometry = rhs.d->outputGeometry; + return *this; +} + +ScreenPaintData &ScreenPaintData::operator*=(qreal scale) +{ + setXScale(this->xScale() * scale); + setYScale(this->yScale() * scale); + setZScale(this->zScale() * scale); + return *this; +} + +ScreenPaintData &ScreenPaintData::operator*=(const QVector2D &scale) +{ + setXScale(this->xScale() * scale.x()); + setYScale(this->yScale() * scale.y()); + return *this; +} + +ScreenPaintData &ScreenPaintData::operator*=(const QVector3D &scale) +{ + setXScale(this->xScale() * scale.x()); + setYScale(this->yScale() * scale.y()); + setZScale(this->zScale() * scale.z()); + return *this; +} + +ScreenPaintData &ScreenPaintData::operator+=(const QPointF &translation) +{ + return this->operator+=(QVector3D(translation)); +} + +ScreenPaintData &ScreenPaintData::operator+=(const QPoint &translation) +{ + return this->operator+=(QVector3D(translation)); +} + +ScreenPaintData &ScreenPaintData::operator+=(const QVector2D &translation) +{ + return this->operator+=(QVector3D(translation)); +} + +ScreenPaintData &ScreenPaintData::operator+=(const QVector3D &translation) +{ + translate(translation); + return *this; +} + +QMatrix4x4 ScreenPaintData::projectionMatrix() const +{ + return d->projectionMatrix; +} + +QRect ScreenPaintData::outputGeometry() const +{ + return d->outputGeometry; +} + +qreal ScreenPaintData::screenScale() const +{ + return d->screenScale; +} + +//**************************************** +// Effect +//**************************************** + +Effect::Effect() +{ +} + +Effect::~Effect() +{ +} + +void Effect::reconfigure(ReconfigureFlags) +{ +} + +void* Effect::proxy() +{ + return nullptr; +} + +void Effect::windowInputMouseEvent(QEvent*) +{ +} + +void Effect::grabbedKeyboardEvent(QKeyEvent*) +{ +} + +bool Effect::borderActivated(ElectricBorder) +{ + return false; +} + +void Effect::prePaintScreen(ScreenPrePaintData& data, int time) +{ + effects->prePaintScreen(data, time); +} + +void Effect::paintScreen(int mask, const QRegion ®ion, ScreenPaintData& data) +{ + effects->paintScreen(mask, region, data); +} + +void Effect::postPaintScreen() +{ + effects->postPaintScreen(); +} + +void Effect::prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time) +{ + effects->prePaintWindow(w, data, time); +} + +void Effect::paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data) +{ + effects->paintWindow(w, mask, region, data); +} + +void Effect::postPaintWindow(EffectWindow* w) +{ + effects->postPaintWindow(w); +} + +void Effect::paintEffectFrame(KWin::EffectFrame* frame, const QRegion ®ion, double opacity, double frameOpacity) +{ + effects->paintEffectFrame(frame, region, opacity, frameOpacity); +} + +bool Effect::provides(Feature) +{ + return false; +} + +bool Effect::isActive() const +{ + return true; +} + +QString Effect::debug(const QString &) const +{ + return QString(); +} + +void Effect::drawWindow(EffectWindow* w, int mask, const QRegion ®ion, WindowPaintData& data) +{ + effects->drawWindow(w, mask, region, data); +} + +void Effect::buildQuads(EffectWindow* w, WindowQuadList& quadList) +{ + effects->buildQuads(w, quadList); +} + +void Effect::setPositionTransformations(WindowPaintData& data, QRect& region, EffectWindow* w, + const QRect& r, Qt::AspectRatioMode aspect) +{ + QSize size = w->size(); + size.scale(r.size(), aspect); + data.setXScale(size.width() / double(w->width())); + data.setYScale(size.height() / double(w->height())); + int width = int(w->width() * data.xScale()); + int height = int(w->height() * data.yScale()); + int x = r.x() + (r.width() - width) / 2; + int y = r.y() + (r.height() - height) / 2; + region = QRect(x, y, width, height); + data.setXTranslation(x - w->x()); + data.setYTranslation(y - w->y()); +} + +QPoint Effect::cursorPos() +{ + return effects->cursorPos(); +} + +double Effect::animationTime(const KConfigGroup& cfg, const QString& key, int defaultTime) +{ + int time = cfg.readEntry(key, 0); + return time != 0 ? time : qMax(defaultTime * effects->animationTimeFactor(), 1.); +} + +double Effect::animationTime(int defaultTime) +{ + // at least 1ms, otherwise 0ms times can break some things + return qMax(defaultTime * effects->animationTimeFactor(), 1.); +} + +int Effect::requestedEffectChainPosition() const +{ + return 0; +} + +xcb_connection_t *Effect::xcbConnection() const +{ + return effects->xcbConnection(); +} + +xcb_window_t Effect::x11RootWindow() const +{ + return effects->x11RootWindow(); +} + +bool Effect::touchDown(qint32 id, const QPointF &pos, quint32 time) +{ + Q_UNUSED(id) + Q_UNUSED(pos) + Q_UNUSED(time) + return false; +} + +bool Effect::touchMotion(qint32 id, const QPointF &pos, quint32 time) +{ + Q_UNUSED(id) + Q_UNUSED(pos) + Q_UNUSED(time) + return false; +} + +bool Effect::touchUp(qint32 id, quint32 time) +{ + Q_UNUSED(id) + Q_UNUSED(time) + return false; +} + +bool Effect::perform(Feature feature, const QVariantList &arguments) +{ + Q_UNUSED(feature) + Q_UNUSED(arguments) + return false; +} + +//**************************************** +// EffectFactory +//**************************************** +EffectPluginFactory::EffectPluginFactory() +{ +} + +EffectPluginFactory::~EffectPluginFactory() +{ +} + +bool EffectPluginFactory::enabledByDefault() const +{ + return true; +} + +bool EffectPluginFactory::isSupported() const +{ + return true; +} + +//**************************************** +// EffectsHandler +//**************************************** + +EffectsHandler::EffectsHandler(CompositingType type) + : compositing_type(type) +{ + if (compositing_type == NoCompositing) + return; + KWin::effects = this; +} + +EffectsHandler::~EffectsHandler() +{ + // All effects should already be unloaded by Impl dtor + Q_ASSERT(loaded_effects.count() == 0); + KWin::effects = nullptr; +} + +CompositingType EffectsHandler::compositingType() const +{ + return compositing_type; +} + +bool EffectsHandler::isOpenGLCompositing() const +{ + return compositing_type & OpenGLCompositing; +} + +EffectsHandler* effects = nullptr; + + +//**************************************** +// EffectWindow +//**************************************** + +class Q_DECL_HIDDEN EffectWindow::Private +{ +public: + Private(EffectWindow *q); + + EffectWindow *q; +}; + +EffectWindow::Private::Private(EffectWindow *q) + : q(q) +{ +} + +EffectWindow::EffectWindow(QObject *parent) + : QObject(parent) + , d(new Private(this)) +{ +} + +EffectWindow::~EffectWindow() +{ +} + +bool EffectWindow::isOnActivity(const QString &activity) const +{ + const QStringList _activities = activities(); + return _activities.isEmpty() || _activities.contains(activity); +} + +bool EffectWindow::isOnAllActivities() const +{ + return activities().isEmpty(); +} + +void EffectWindow::setMinimized(bool min) +{ + if (min) { + minimize(); + } else { + unminimize(); + } +} + +bool EffectWindow::isOnCurrentActivity() const +{ + return isOnActivity(effects->currentActivity()); +} + +bool EffectWindow::isOnCurrentDesktop() const +{ + return isOnDesktop(effects->currentDesktop()); +} + +bool EffectWindow::isOnDesktop(int d) const +{ + const QVector ds = desktops(); + return ds.isEmpty() || ds.contains(d); +} + +bool EffectWindow::isOnAllDesktops() const +{ + return desktops().isEmpty(); +} + +bool EffectWindow::hasDecoration() const +{ + return contentsRect() != QRect(0, 0, width(), height()); +} + +bool EffectWindow::isVisible() const +{ + return !isMinimized() + && isOnCurrentDesktop() + && isOnCurrentActivity(); +} + +//**************************************** +// EffectWindowGroup +//**************************************** + +EffectWindowGroup::~EffectWindowGroup() +{ +} + +/*************************************************************** + WindowQuad +***************************************************************/ + +WindowQuad WindowQuad::makeSubQuad(double x1, double y1, double x2, double y2) const +{ + Q_ASSERT(x1 < x2 && y1 < y2 && x1 >= left() && x2 <= right() && y1 >= top() && y2 <= bottom()); +#if !defined(QT_NO_DEBUG) + if (isTransformed()) + qFatal("Splitting quads is allowed only in pre-paint calls!"); +#endif + WindowQuad ret(*this); + // vertices are clockwise starting from topleft + ret.verts[ 0 ].px = x1; + ret.verts[ 3 ].px = x1; + ret.verts[ 1 ].px = x2; + ret.verts[ 2 ].px = x2; + ret.verts[ 0 ].py = y1; + ret.verts[ 1 ].py = y1; + ret.verts[ 2 ].py = y2; + ret.verts[ 3 ].py = y2; + // original x/y are supposed to be the same, no transforming is done here + ret.verts[ 0 ].ox = x1; + ret.verts[ 3 ].ox = x1; + ret.verts[ 1 ].ox = x2; + ret.verts[ 2 ].ox = x2; + ret.verts[ 0 ].oy = y1; + ret.verts[ 1 ].oy = y1; + ret.verts[ 2 ].oy = y2; + ret.verts[ 3 ].oy = y2; + + const double my_u0 = verts[0].tx; + const double my_u1 = verts[2].tx; + const double my_v0 = verts[0].ty; + const double my_v1 = verts[2].ty; + + const double width = right() - left(); + const double height = bottom() - top(); + + const double texWidth = my_u1 - my_u0; + const double texHeight = my_v1 - my_v0; + + if (!uvAxisSwapped()) { + const double u0 = (x1 - left()) / width * texWidth + my_u0; + const double u1 = (x2 - left()) / width * texWidth + my_u0; + const double v0 = (y1 - top()) / height * texHeight + my_v0; + const double v1 = (y2 - top()) / height * texHeight + my_v0; + + ret.verts[0].tx = u0; + ret.verts[3].tx = u0; + ret.verts[1].tx = u1; + ret.verts[2].tx = u1; + ret.verts[0].ty = v0; + ret.verts[1].ty = v0; + ret.verts[2].ty = v1; + ret.verts[3].ty = v1; + } else { + const double u0 = (y1 - top()) / height * texWidth + my_u0; + const double u1 = (y2 - top()) / height * texWidth + my_u0; + const double v0 = (x1 - left()) / width * texHeight + my_v0; + const double v1 = (x2 - left()) / width * texHeight + my_v0; + + ret.verts[0].tx = u0; + ret.verts[1].tx = u0; + ret.verts[2].tx = u1; + ret.verts[3].tx = u1; + ret.verts[0].ty = v0; + ret.verts[3].ty = v0; + ret.verts[1].ty = v1; + ret.verts[2].ty = v1; + } + + ret.setUVAxisSwapped(uvAxisSwapped()); + + return ret; +} + +bool WindowQuad::smoothNeeded() const +{ + // smoothing is needed if the width or height of the quad does not match the original size + double width = verts[ 1 ].ox - verts[ 0 ].ox; + double height = verts[ 2 ].oy - verts[ 1 ].oy; + return(verts[ 1 ].px - verts[ 0 ].px != width || verts[ 2 ].px - verts[ 3 ].px != width + || verts[ 2 ].py - verts[ 1 ].py != height || verts[ 3 ].py - verts[ 0 ].py != height); +} + +/*************************************************************** + WindowQuadList +***************************************************************/ + +WindowQuadList WindowQuadList::splitAtX(double x) const +{ + WindowQuadList ret; + ret.reserve(count()); + for (const WindowQuad & quad : *this) { +#if !defined(QT_NO_DEBUG) + if (quad.isTransformed()) + qFatal("Splitting quads is allowed only in pre-paint calls!"); +#endif + bool wholeleft = true; + bool wholeright = true; + for (int i = 0; + i < 4; + ++i) { + if (quad[ i ].x() < x) + wholeright = false; + if (quad[ i ].x() > x) + wholeleft = false; + } + if (wholeleft || wholeright) { // is whole in one split part + ret.append(quad); + continue; + } + if (quad.top() == quad.bottom() || quad.left() == quad.right()) { // quad has no size + ret.append(quad); + continue; + } + ret.append(quad.makeSubQuad(quad.left(), quad.top(), x, quad.bottom())); + ret.append(quad.makeSubQuad(x, quad.top(), quad.right(), quad.bottom())); + } + return ret; +} + +WindowQuadList WindowQuadList::splitAtY(double y) const +{ + WindowQuadList ret; + ret.reserve(count()); + for (const WindowQuad & quad : *this) { +#if !defined(QT_NO_DEBUG) + if (quad.isTransformed()) + qFatal("Splitting quads is allowed only in pre-paint calls!"); +#endif + bool wholetop = true; + bool wholebottom = true; + for (int i = 0; + i < 4; + ++i) { + if (quad[ i ].y() < y) + wholebottom = false; + if (quad[ i ].y() > y) + wholetop = false; + } + if (wholetop || wholebottom) { // is whole in one split part + ret.append(quad); + continue; + } + if (quad.top() == quad.bottom() || quad.left() == quad.right()) { // quad has no size + ret.append(quad); + continue; + } + ret.append(quad.makeSubQuad(quad.left(), quad.top(), quad.right(), y)); + ret.append(quad.makeSubQuad(quad.left(), y, quad.right(), quad.bottom())); + } + return ret; +} + +WindowQuadList WindowQuadList::makeGrid(int maxQuadSize) const +{ + if (empty()) + return *this; + + // Find the bounding rectangle + double left = first().left(); + double right = first().right(); + double top = first().top(); + double bottom = first().bottom(); + + foreach (const WindowQuad &quad, *this) { +#if !defined(QT_NO_DEBUG) + if (quad.isTransformed()) + qFatal("Splitting quads is allowed only in pre-paint calls!"); +#endif + left = qMin(left, quad.left()); + right = qMax(right, quad.right()); + top = qMin(top, quad.top()); + bottom = qMax(bottom, quad.bottom()); + } + + WindowQuadList ret; + + for (const WindowQuad &quad : *this) { + const double quadLeft = quad.left(); + const double quadRight = quad.right(); + const double quadTop = quad.top(); + const double quadBottom = quad.bottom(); + + // sanity check, see BUG 390953 + if (quadLeft == quadRight || quadTop == quadBottom) { + ret.append(quad); + continue; + } + + // Compute the top-left corner of the first intersecting grid cell + const double xBegin = left + qFloor((quadLeft - left) / maxQuadSize) * maxQuadSize; + const double yBegin = top + qFloor((quadTop - top) / maxQuadSize) * maxQuadSize; + + // Loop over all intersecting cells and add sub-quads + for (double y = yBegin; y < quadBottom; y += maxQuadSize) { + const double y0 = qMax(y, quadTop); + const double y1 = qMin(quadBottom, y + maxQuadSize); + + for (double x = xBegin; x < quadRight; x += maxQuadSize) { + const double x0 = qMax(x, quadLeft); + const double x1 = qMin(quadRight, x + maxQuadSize); + + ret.append(quad.makeSubQuad(x0, y0, x1, y1)); + } + } + } + + return ret; +} + +WindowQuadList WindowQuadList::makeRegularGrid(int xSubdivisions, int ySubdivisions) const +{ + if (empty()) + return *this; + + // Find the bounding rectangle + double left = first().left(); + double right = first().right(); + double top = first().top(); + double bottom = first().bottom(); + + for (const WindowQuad &quad : *this) { +#if !defined(QT_NO_DEBUG) + if (quad.isTransformed()) + qFatal("Splitting quads is allowed only in pre-paint calls!"); +#endif + left = qMin(left, quad.left()); + right = qMax(right, quad.right()); + top = qMin(top, quad.top()); + bottom = qMax(bottom, quad.bottom()); + } + + double xIncrement = (right - left) / xSubdivisions; + double yIncrement = (bottom - top) / ySubdivisions; + + WindowQuadList ret; + + for (const WindowQuad &quad : *this) { + const double quadLeft = quad.left(); + const double quadRight = quad.right(); + const double quadTop = quad.top(); + const double quadBottom = quad.bottom(); + + // sanity check, see BUG 390953 + if (quadLeft == quadRight || quadTop == quadBottom) { + ret.append(quad); + continue; + } + + // Compute the top-left corner of the first intersecting grid cell + const double xBegin = left + qFloor((quadLeft - left) / xIncrement) * xIncrement; + const double yBegin = top + qFloor((quadTop - top) / yIncrement) * yIncrement; + + // Loop over all intersecting cells and add sub-quads + for (double y = yBegin; y < quadBottom; y += yIncrement) { + const double y0 = qMax(y, quadTop); + const double y1 = qMin(quadBottom, y + yIncrement); + + for (double x = xBegin; x < quadRight; x += xIncrement) { + const double x0 = qMax(x, quadLeft); + const double x1 = qMin(quadRight, x + xIncrement); + + ret.append(quad.makeSubQuad(x0, y0, x1, y1)); + } + } + } + + return ret; +} + +#ifndef GL_TRIANGLES +# define GL_TRIANGLES 0x0004 +#endif + +#ifndef GL_QUADS +# define GL_QUADS 0x0007 +#endif + +void WindowQuadList::makeInterleavedArrays(unsigned int type, GLVertex2D *vertices, const QMatrix4x4 &textureMatrix) const +{ + // Since we know that the texture matrix just scales and translates + // we can use this information to optimize the transformation + const QVector2D coeff(textureMatrix(0, 0), textureMatrix(1, 1)); + const QVector2D offset(textureMatrix(0, 3), textureMatrix(1, 3)); + + GLVertex2D *vertex = vertices; + + Q_ASSERT(type == GL_QUADS || type == GL_TRIANGLES); + + switch (type) + { + case GL_QUADS: +#if defined(__SSE2__) + if (!(intptr_t(vertex) & 0xf)) { + for (const WindowQuad &quad : *this) { + alignas(16) GLVertex2D v[4]; + + for (int j = 0; j < 4; j++) { + const WindowVertex &wv = quad[j]; + + v[j].position = QVector2D(wv.x(), wv.y()); + v[j].texcoord = QVector2D(wv.u(), wv.v()) * coeff + offset; + } + + const __m128i *srcP = reinterpret_cast(&v); + __m128i *dstP = reinterpret_cast<__m128i *>(vertex); + + _mm_stream_si128(&dstP[0], _mm_load_si128(&srcP[0])); // Top-left + _mm_stream_si128(&dstP[1], _mm_load_si128(&srcP[1])); // Top-right + _mm_stream_si128(&dstP[2], _mm_load_si128(&srcP[2])); // Bottom-right + _mm_stream_si128(&dstP[3], _mm_load_si128(&srcP[3])); // Bottom-left + + vertex += 4; + } + } else +#endif // __SSE2__ + { + for (const WindowQuad &quad : *this) { + for (int j = 0; j < 4; j++) { + const WindowVertex &wv = quad[j]; + + GLVertex2D v; + v.position = QVector2D(wv.x(), wv.y()); + v.texcoord = QVector2D(wv.u(), wv.v()) * coeff + offset; + + *(vertex++) = v; + } + } + } + break; + + case GL_TRIANGLES: +#if defined(__SSE2__) + if (!(intptr_t(vertex) & 0xf)) { + for (const WindowQuad &quad : *this) { + alignas(16) GLVertex2D v[4]; + + for (int j = 0; j < 4; j++) { + const WindowVertex &wv = quad[j]; + + v[j].position = QVector2D(wv.x(), wv.y()); + v[j].texcoord = QVector2D(wv.u(), wv.v()) * coeff + offset; + } + + const __m128i *srcP = reinterpret_cast(&v); + __m128i *dstP = reinterpret_cast<__m128i *>(vertex); + + __m128i src[4]; + src[0] = _mm_load_si128(&srcP[0]); // Top-left + src[1] = _mm_load_si128(&srcP[1]); // Top-right + src[2] = _mm_load_si128(&srcP[2]); // Bottom-right + src[3] = _mm_load_si128(&srcP[3]); // Bottom-left + + // First triangle + _mm_stream_si128(&dstP[0], src[1]); // Top-right + _mm_stream_si128(&dstP[1], src[0]); // Top-left + _mm_stream_si128(&dstP[2], src[3]); // Bottom-left + + // Second triangle + _mm_stream_si128(&dstP[3], src[3]); // Bottom-left + _mm_stream_si128(&dstP[4], src[2]); // Bottom-right + _mm_stream_si128(&dstP[5], src[1]); // Top-right + + vertex += 6; + } + } else +#endif // __SSE2__ + { + for (const WindowQuad &quad : *this) { + GLVertex2D v[4]; // Four unique vertices / quad + + for (int j = 0; j < 4; j++) { + const WindowVertex &wv = quad[j]; + + v[j].position = QVector2D(wv.x(), wv.y()); + v[j].texcoord = QVector2D(wv.u(), wv.v()) * coeff + offset; + } + + // First triangle + *(vertex++) = v[1]; // Top-right + *(vertex++) = v[0]; // Top-left + *(vertex++) = v[3]; // Bottom-left + + // Second triangle + *(vertex++) = v[3]; // Bottom-left + *(vertex++) = v[2]; // Bottom-right + *(vertex++) = v[1]; // Top-right + } + } + break; + + default: + break; + } +} + +void WindowQuadList::makeArrays(float **vertices, float **texcoords, const QSizeF &size, bool yInverted) const +{ + *vertices = new float[count() * 6 * 2]; + *texcoords = new float[count() * 6 * 2]; + + float *vpos = *vertices; + float *tpos = *texcoords; + + // Note: The positions in a WindowQuad are stored in clockwise order + const int index[] = { 1, 0, 3, 3, 2, 1 }; + + for (const WindowQuad &quad : *this) { + for (int j = 0; j < 6; j++) { + const WindowVertex &wv = quad[index[j]]; + + *vpos++ = wv.x(); + *vpos++ = wv.y(); + + *tpos++ = wv.u() / size.width(); + *tpos++ = yInverted ? (wv.v() / size.height()) : (1.0 - wv.v() / size.height()); + } + } +} + +WindowQuadList WindowQuadList::select(WindowQuadType type) const +{ + foreach (const WindowQuad & q, *this) { + if (q.type() != type) { // something else than ones to select, make a copy and filter + WindowQuadList ret; + foreach (const WindowQuad & q, *this) { + if (q.type() == type) + ret.append(q); + } + return ret; + } + } + return *this; // nothing to filter out +} + +WindowQuadList WindowQuadList::filterOut(WindowQuadType type) const +{ + for (const WindowQuad & q : *this) { + if (q.type() == type) { // something to filter out, make a copy and filter + WindowQuadList ret; + foreach (const WindowQuad & q, *this) { + if (q.type() != type) + ret.append(q); + } + return ret; + } + } + return *this; // nothing to filter out +} + +bool WindowQuadList::smoothNeeded() const +{ + return std::any_of(constBegin(), constEnd(), [] (const WindowQuad & q) { return q.smoothNeeded(); }); +} + +bool WindowQuadList::isTransformed() const +{ + return std::any_of(constBegin(), constEnd(), [] (const WindowQuad & q) { return q.isTransformed(); }); +} + +/*************************************************************** + PaintClipper +***************************************************************/ + +QStack< QRegion >* PaintClipper::areas = nullptr; + +PaintClipper::PaintClipper(const QRegion& allowed_area) + : area(allowed_area) +{ + push(area); +} + +PaintClipper::~PaintClipper() +{ + pop(area); +} + +void PaintClipper::push(const QRegion& allowed_area) +{ + if (allowed_area == infiniteRegion()) // don't push these + return; + if (areas == nullptr) + areas = new QStack< QRegion >; + areas->push(allowed_area); +} + +void PaintClipper::pop(const QRegion& allowed_area) +{ + if (allowed_area == infiniteRegion()) + return; + Q_ASSERT(areas != nullptr); + Q_ASSERT(areas->top() == allowed_area); + areas->pop(); + if (areas->isEmpty()) { + delete areas; + areas = nullptr; + } +} + +bool PaintClipper::clip() +{ + return areas != nullptr; +} + +QRegion PaintClipper::paintArea() +{ + Q_ASSERT(areas != nullptr); // can be called only with clip() == true + const QSize &s = effects->virtualScreenSize(); + QRegion ret(0, 0, s.width(), s.height()); + for (const QRegion & r : qAsConst(*areas)) { + ret &= r; + } + return ret; +} + +struct PaintClipper::Iterator::Data { + Data() : index(0) {} + int index; + QRegion region; +}; + +PaintClipper::Iterator::Iterator() + : data(new Data) +{ + if (clip() && effects->isOpenGLCompositing()) { + data->region = paintArea(); + data->index = -1; + next(); // move to the first one + } +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + if (clip() && effects->compositingType() == XRenderCompositing) { + XFixesRegion region(paintArea()); + xcb_xfixes_set_picture_clip_region(connection(), effects->xrenderBufferPicture(), region, 0, 0); + } +#endif +} + +PaintClipper::Iterator::~Iterator() +{ +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + if (clip() && effects->compositingType() == XRenderCompositing) + xcb_xfixes_set_picture_clip_region(connection(), effects->xrenderBufferPicture(), XCB_XFIXES_REGION_NONE, 0, 0); +#endif + delete data; +} + +bool PaintClipper::Iterator::isDone() +{ + if (!clip()) + return data->index == 1; // run once + if (effects->isOpenGLCompositing()) + return data->index >= data->region.rectCount(); // run once per each area +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + if (effects->compositingType() == XRenderCompositing) + return data->index == 1; // run once +#endif + abort(); +} + +void PaintClipper::Iterator::next() +{ + data->index++; +} + +QRect PaintClipper::Iterator::boundingRect() const +{ + if (!clip()) + return infiniteRegion(); + if (effects->isOpenGLCompositing()) + return *(data->region.begin() + data->index); +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + if (effects->compositingType() == XRenderCompositing) + return data->region.boundingRect(); +#endif + abort(); + return infiniteRegion(); +} + +/*************************************************************** + Motion1D +***************************************************************/ + +Motion1D::Motion1D(double initial, double strength, double smoothness) + : Motion(initial, strength, smoothness) +{ +} + +Motion1D::Motion1D(const Motion1D &other) + : Motion(other) +{ +} + +Motion1D::~Motion1D() +{ +} + +/*************************************************************** + Motion2D +***************************************************************/ + +Motion2D::Motion2D(QPointF initial, double strength, double smoothness) + : Motion(initial, strength, smoothness) +{ +} + +Motion2D::Motion2D(const Motion2D &other) + : Motion(other) +{ +} + +Motion2D::~Motion2D() +{ +} + +/*************************************************************** + WindowMotionManager +***************************************************************/ + +WindowMotionManager::WindowMotionManager(bool useGlobalAnimationModifier) + : m_useGlobalAnimationModifier(useGlobalAnimationModifier) + +{ + // TODO: Allow developer to modify motion attributes +} // TODO: What happens when the window moves by an external force? + +WindowMotionManager::~WindowMotionManager() +{ +} + +void WindowMotionManager::manage(EffectWindow *w) +{ + if (m_managedWindows.contains(w)) + return; + + double strength = 0.08; + double smoothness = 4.0; + if (m_useGlobalAnimationModifier && effects->animationTimeFactor()) { + // If the factor is == 0 then we just skip the calculation completely + strength = 0.08 / effects->animationTimeFactor(); + smoothness = effects->animationTimeFactor() * 4.0; + } + + WindowMotion &motion = m_managedWindows[ w ]; + motion.translation.setStrength(strength); + motion.translation.setSmoothness(smoothness); + motion.scale.setStrength(strength * 1.33); + motion.scale.setSmoothness(smoothness / 2.0); + + motion.translation.setValue(w->pos()); + motion.scale.setValue(QPointF(1.0, 1.0)); +} + +void WindowMotionManager::unmanage(EffectWindow *w) +{ + m_movingWindowsSet.remove(w); + m_managedWindows.remove(w); +} + +void WindowMotionManager::unmanageAll() +{ + m_managedWindows.clear(); + m_movingWindowsSet.clear(); +} + +void WindowMotionManager::calculate(int time) +{ + if (!effects->animationTimeFactor()) { + // Just skip it completely if the user wants no animation + m_movingWindowsSet.clear(); + QHash::iterator it = m_managedWindows.begin(); + for (; it != m_managedWindows.end(); ++it) { + WindowMotion *motion = &it.value(); + motion->translation.finish(); + motion->scale.finish(); + } + } + + QHash::iterator it = m_managedWindows.begin(); + for (; it != m_managedWindows.end(); ++it) { + WindowMotion *motion = &it.value(); + int stopped = 0; + + // TODO: What happens when distance() == 0 but we are still moving fast? + // TODO: Motion needs to be calculated from the window's center + + Motion2D *trans = &motion->translation; + if (trans->distance().isNull()) + ++stopped; + else { + // Still moving + trans->calculate(time); + const short fx = trans->target().x() <= trans->startValue().x() ? -1 : 1; + const short fy = trans->target().y() <= trans->startValue().y() ? -1 : 1; + if (trans->distance().x()*fx/0.5 < 1.0 && trans->velocity().x()*fx/0.2 < 1.0 && + trans->distance().y()*fy/0.5 < 1.0 && trans->velocity().y()*fy/0.2 < 1.0) { + // Hide tiny oscillations + motion->translation.finish(); + ++stopped; + } + } + + Motion2D *scale = &motion->scale; + if (scale->distance().isNull()) + ++stopped; + else { + // Still scaling + scale->calculate(time); + const short fx = scale->target().x() < 1.0 ? -1 : 1; + const short fy = scale->target().y() < 1.0 ? -1 : 1; + if (scale->distance().x()*fx/0.001 < 1.0 && scale->velocity().x()*fx/0.05 < 1.0 && + scale->distance().y()*fy/0.001 < 1.0 && scale->velocity().y()*fy/0.05 < 1.0) { + // Hide tiny oscillations + motion->scale.finish(); + ++stopped; + } + } + + // We just finished this window's motion + if (stopped == 2) + m_movingWindowsSet.remove(it.key()); + } +} + +void WindowMotionManager::reset() +{ + QHash::iterator it = m_managedWindows.begin(); + for (; it != m_managedWindows.end(); ++it) { + WindowMotion *motion = &it.value(); + EffectWindow *window = it.key(); + motion->translation.setTarget(window->pos()); + motion->translation.finish(); + motion->scale.setTarget(QPointF(1.0, 1.0)); + motion->scale.finish(); + } +} + +void WindowMotionManager::reset(EffectWindow *w) +{ + QHash::iterator it = m_managedWindows.find(w); + if (it == m_managedWindows.end()) + return; + + WindowMotion *motion = &it.value(); + motion->translation.setTarget(w->pos()); + motion->translation.finish(); + motion->scale.setTarget(QPointF(1.0, 1.0)); + motion->scale.finish(); +} + +void WindowMotionManager::apply(EffectWindow *w, WindowPaintData &data) +{ + QHash::iterator it = m_managedWindows.find(w); + if (it == m_managedWindows.end()) + return; + + // TODO: Take into account existing scale so that we can work with multiple managers (E.g. Present windows + grid) + WindowMotion *motion = &it.value(); + data += (motion->translation.value() - QPointF(w->x(), w->y())); + data *= QVector2D(motion->scale.value()); +} + +void WindowMotionManager::moveWindow(EffectWindow *w, QPoint target, double scale, double yScale) +{ + QHash::iterator it = m_managedWindows.find(w); + if (it == m_managedWindows.end()) + abort(); // Notify the effect author that they did something wrong + + WindowMotion *motion = &it.value(); + + if (yScale == 0.0) + yScale = scale; + QPointF scalePoint(scale, yScale); + + if (motion->translation.value() == target && motion->scale.value() == scalePoint) + return; // Window already at that position + + motion->translation.setTarget(target); + motion->scale.setTarget(scalePoint); + + m_movingWindowsSet << w; +} + +QRectF WindowMotionManager::transformedGeometry(EffectWindow *w) const +{ + QHash::const_iterator it = m_managedWindows.constFind(w); + if (it == m_managedWindows.end()) + return w->geometry(); + + const WindowMotion *motion = &it.value(); + QRectF geometry(w->geometry()); + + // TODO: Take into account existing scale so that we can work with multiple managers (E.g. Present windows + grid) + geometry.moveTo(motion->translation.value()); + geometry.setWidth(geometry.width() * motion->scale.value().x()); + geometry.setHeight(geometry.height() * motion->scale.value().y()); + + return geometry; +} + +void WindowMotionManager::setTransformedGeometry(EffectWindow *w, const QRectF &geometry) +{ + QHash::iterator it = m_managedWindows.find(w); + if (it == m_managedWindows.end()) + return; + WindowMotion *motion = &it.value(); + motion->translation.setValue(geometry.topLeft()); + motion->scale.setValue(QPointF(geometry.width() / qreal(w->width()), geometry.height() / qreal(w->height()))); +} + +QRectF WindowMotionManager::targetGeometry(EffectWindow *w) const +{ + QHash::const_iterator it = m_managedWindows.constFind(w); + if (it == m_managedWindows.end()) + return w->geometry(); + + const WindowMotion *motion = &it.value(); + QRectF geometry(w->geometry()); + + // TODO: Take into account existing scale so that we can work with multiple managers (E.g. Present windows + grid) + geometry.moveTo(motion->translation.target()); + geometry.setWidth(geometry.width() * motion->scale.target().x()); + geometry.setHeight(geometry.height() * motion->scale.target().y()); + + return geometry; +} + +EffectWindow* WindowMotionManager::windowAtPoint(QPoint point, bool useStackingOrder) const +{ + Q_UNUSED(useStackingOrder); + // TODO: Stacking order uses EffectsHandler::stackingOrder() then filters by m_managedWindows + QHash< EffectWindow*, WindowMotion >::ConstIterator it = m_managedWindows.constBegin(); + while (it != m_managedWindows.constEnd()) { + if (transformedGeometry(it.key()).contains(point)) + return it.key(); + ++it; + } + + return nullptr; +} + +/*************************************************************** + EffectFramePrivate +***************************************************************/ +class EffectFramePrivate +{ +public: + EffectFramePrivate(); + ~EffectFramePrivate(); + + bool crossFading; + qreal crossFadeProgress; + QMatrix4x4 screenProjectionMatrix; +}; + +EffectFramePrivate::EffectFramePrivate() + : crossFading(false) + , crossFadeProgress(1.0) +{ +} + +EffectFramePrivate::~EffectFramePrivate() +{ +} + +/*************************************************************** + EffectFrame +***************************************************************/ +EffectFrame::EffectFrame() + : d(new EffectFramePrivate) +{ +} + +EffectFrame::~EffectFrame() +{ + delete d; +} + +qreal EffectFrame::crossFadeProgress() const +{ + return d->crossFadeProgress; +} + +void EffectFrame::setCrossFadeProgress(qreal progress) +{ + d->crossFadeProgress = progress; +} + +bool EffectFrame::isCrossFade() const +{ + return d->crossFading; +} + +void EffectFrame::enableCrossFade(bool enable) +{ + d->crossFading = enable; +} + +QMatrix4x4 EffectFrame::screenProjectionMatrix() const +{ + return d->screenProjectionMatrix; +} + +void EffectFrame::setScreenProjectionMatrix(const QMatrix4x4 &spm) +{ + d->screenProjectionMatrix = spm; +} + +/*************************************************************** + TimeLine +***************************************************************/ + +class Q_DECL_HIDDEN TimeLine::Data : public QSharedData +{ +public: + std::chrono::milliseconds duration; + Direction direction; + QEasingCurve easingCurve; + + std::chrono::milliseconds elapsed = std::chrono::milliseconds::zero(); + bool done = false; + RedirectMode sourceRedirectMode = RedirectMode::Relaxed; + RedirectMode targetRedirectMode = RedirectMode::Strict; +}; + +TimeLine::TimeLine(std::chrono::milliseconds duration, Direction direction) + : d(new Data) +{ + Q_ASSERT(duration > std::chrono::milliseconds::zero()); + d->duration = duration; + d->direction = direction; +} + +TimeLine::TimeLine(const TimeLine &other) + : d(other.d) +{ +} + +TimeLine::~TimeLine() = default; + +qreal TimeLine::progress() const +{ + return static_cast(d->elapsed.count()) / d->duration.count(); +} + +qreal TimeLine::value() const +{ + const qreal t = progress(); + return d->easingCurve.valueForProgress( + d->direction == Backward ? 1.0 - t : t); +} + +void TimeLine::update(std::chrono::milliseconds delta) +{ + Q_ASSERT(delta >= std::chrono::milliseconds::zero()); + if (d->done) { + return; + } + d->elapsed += delta; + if (d->elapsed >= d->duration) { + d->done = true; + d->elapsed = d->duration; + } +} + +std::chrono::milliseconds TimeLine::elapsed() const +{ + return d->elapsed; +} + +void TimeLine::setElapsed(std::chrono::milliseconds elapsed) +{ + Q_ASSERT(elapsed >= std::chrono::milliseconds::zero()); + if (elapsed == d->elapsed) { + return; + } + reset(); + update(elapsed); +} + +std::chrono::milliseconds TimeLine::duration() const +{ + return d->duration; +} + +void TimeLine::setDuration(std::chrono::milliseconds duration) +{ + Q_ASSERT(duration > std::chrono::milliseconds::zero()); + if (duration == d->duration) { + return; + } + d->elapsed = std::chrono::milliseconds(qRound(progress() * duration.count())); + d->duration = duration; + if (d->elapsed == d->duration) { + d->done = true; + } +} + +TimeLine::Direction TimeLine::direction() const +{ + return d->direction; +} + +void TimeLine::setDirection(TimeLine::Direction direction) +{ + if (d->direction == direction) { + return; + } + + d->direction = direction; + + if (d->elapsed > std::chrono::milliseconds::zero() + || d->sourceRedirectMode == RedirectMode::Strict) { + d->elapsed = d->duration - d->elapsed; + } + + if (d->done && d->targetRedirectMode == RedirectMode::Relaxed) { + d->done = false; + } + + if (d->elapsed >= d->duration) { + d->done = true; + } +} + +void TimeLine::toggleDirection() +{ + setDirection(d->direction == Forward ? Backward : Forward); +} + +QEasingCurve TimeLine::easingCurve() const +{ + return d->easingCurve; +} + +void TimeLine::setEasingCurve(const QEasingCurve &easingCurve) +{ + d->easingCurve = easingCurve; +} + +void TimeLine::setEasingCurve(QEasingCurve::Type type) +{ + d->easingCurve.setType(type); +} + +bool TimeLine::running() const +{ + return d->elapsed != std::chrono::milliseconds::zero() + && d->elapsed != d->duration; +} + +bool TimeLine::done() const +{ + return d->done; +} + +void TimeLine::reset() +{ + d->elapsed = std::chrono::milliseconds::zero(); + d->done = false; +} + +TimeLine::RedirectMode TimeLine::sourceRedirectMode() const +{ + return d->sourceRedirectMode; +} + +void TimeLine::setSourceRedirectMode(RedirectMode mode) +{ + d->sourceRedirectMode = mode; +} + +TimeLine::RedirectMode TimeLine::targetRedirectMode() const +{ + return d->targetRedirectMode; +} + +void TimeLine::setTargetRedirectMode(RedirectMode mode) +{ + d->targetRedirectMode = mode; +} + +TimeLine &TimeLine::operator=(const TimeLine &other) +{ + d = other.d; + return *this; +} + +} // namespace + +#include "moc_kwinglobals.cpp" diff --git a/libkwineffects/kwineffects.h b/libkwineffects/kwineffects.h new file mode 100644 index 0000000..5bf770e --- /dev/null +++ b/libkwineffects/kwineffects.h @@ -0,0 +1,4014 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2010, 2011 Martin Gräßlin + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWINEFFECTS_H +#define KWINEFFECTS_H + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include +#include + +class KConfigGroup; +class QFont; +class QGraphicsScale; +class QKeyEvent; +class QMatrix4x4; +class QAction; + +/** + * Logging category to be used inside the KWin effects. + * Do not use in this library. + */ +Q_DECLARE_LOGGING_CATEGORY(KWINEFFECTS) + +namespace KWaylandServer { + class SurfaceInterface; + class Display; +} + +namespace KWin +{ + +class PaintDataPrivate; +class WindowPaintDataPrivate; + +class EffectWindow; +class EffectWindowGroup; +class EffectFrame; +class EffectFramePrivate; +class EffectQuickView; +class Effect; +class WindowQuad; +class GLShader; +class XRenderPicture; +class WindowQuadList; +class WindowPrePaintData; +class WindowPaintData; +class ScreenPrePaintData; +class ScreenPaintData; + +typedef QPair< QString, Effect* > EffectPair; +typedef QList< KWin::EffectWindow* > EffectWindowList; + + +/** @defgroup kwineffects KWin effects library + * KWin effects library contains necessary classes for creating new KWin + * compositing effects. + * + * @section creating Creating new effects + * This example will demonstrate the basics of creating an effect. We'll use + * CoolEffect as the class name, cooleffect as internal name and + * "Cool Effect" as user-visible name of the effect. + * + * This example doesn't demonstrate how to write the effect's code. For that, + * see the documentation of the Effect class. + * + * @subsection creating-class CoolEffect class + * First you need to create CoolEffect class which has to be a subclass of + * @ref KWin::Effect. In that class you can reimplement various virtual + * methods to control how and where the windows are drawn. + * + * @subsection creating-macro KWIN_EFFECT_FACTORY macro + * This library provides a specialized KPluginFactory subclass and macros to + * create a sub class. This subclass of KPluginFactory has to be used, otherwise + * KWin won't load the plugin. Use the @ref KWIN_EFFECT_FACTORY macro to create the + * plugin factory. + * + * @subsection creating-buildsystem Buildsystem + * To build the effect, you can use the KWIN_ADD_EFFECT() cmake macro which + * can be found in effects/CMakeLists.txt file in KWin's source. First + * argument of the macro is the name of the library that will contain + * your effect. Although not strictly required, it is usually a good idea to + * use the same name as your effect's internal name there. Following arguments + * to the macro are the files containing your effect's source. If our effect's + * source is in cooleffect.cpp, we'd use following: + * @code + * KWIN_ADD_EFFECT(cooleffect cooleffect.cpp) + * @endcode + * + * This macro takes care of compiling your effect. You'll also need to install + * your effect's .desktop file, so the example CMakeLists.txt file would be + * as follows: + * @code + * KWIN_ADD_EFFECT(cooleffect cooleffect.cpp) + * install( FILES cooleffect.desktop DESTINATION ${SERVICES_INSTALL_DIR}/kwin ) + * @endcode + * + * @subsection creating-desktop Effect's .desktop file + * You will also need to create .desktop file to set name, description, icon + * and other properties of your effect. Important fields of the .desktop file + * are: + * @li Name User-visible name of your effect + * @li Icon Name of the icon of the effect + * @li Comment Short description of the effect + * @li Type must be "Service" + * @li X-KDE-ServiceTypes must be "KWin/Effect" + * @li X-KDE-PluginInfo-Name effect's internal name as passed to the KWIN_EFFECT macro plus "kwin4_effect_" prefix + * @li X-KDE-PluginInfo-Category effect's category. Should be one of Appearance, Accessibility, Window Management, Demos, Tests, Misc + * @li X-KDE-PluginInfo-EnabledByDefault whether the effect should be enabled by default (use sparingly). Default is false + * @li X-KDE-Library name of the library containing the effect. This is the first argument passed to the KWIN_ADD_EFFECT macro in cmake file plus "kwin4_effect_" prefix. + * + * Example cooleffect.desktop file follows: + * @code +[Desktop Entry] +Name=Cool Effect +Comment=The coolest effect you've ever seen +Icon=preferences-system-windows-effect-cooleffect + +Type=Service +X-KDE-ServiceTypes=KWin/Effect +X-KDE-PluginInfo-Author=My Name +X-KDE-PluginInfo-Email=my@email.here +X-KDE-PluginInfo-Name=kwin4_effect_cooleffect +X-KDE-PluginInfo-Category=Misc +X-KDE-Library=kwin4_effect_cooleffect + * @endcode + * + * + * @section accessing Accessing windows and workspace + * Effects can gain access to the properties of windows and workspace via + * EffectWindow and EffectsHandler classes. + * + * There is one global EffectsHandler object which you can access using the + * @ref effects pointer. + * For each window, there is an EffectWindow object which can be used to read + * window properties such as position and also to change them. + * + * For more information about this, see the documentation of the corresponding + * classes. + * + * @{ + */ + +#define KWIN_EFFECT_API_MAKE_VERSION( major, minor ) (( major ) << 8 | ( minor )) +#define KWIN_EFFECT_API_VERSION_MAJOR 0 +#define KWIN_EFFECT_API_VERSION_MINOR 231 +#define KWIN_EFFECT_API_VERSION KWIN_EFFECT_API_MAKE_VERSION( \ + KWIN_EFFECT_API_VERSION_MAJOR, KWIN_EFFECT_API_VERSION_MINOR ) + +enum WindowQuadType { + WindowQuadError, // for the stupid default ctor + WindowQuadContents, + WindowQuadDecoration, + // Shadow Quad types + WindowQuadShadow, // OpenGL only. The other shadow types are only used by Xrender + WindowQuadShadowTop, + WindowQuadShadowTopRight, + WindowQuadShadowRight, + WindowQuadShadowBottomRight, + WindowQuadShadowBottom, + WindowQuadShadowBottomLeft, + WindowQuadShadowLeft, + WindowQuadShadowTopLeft, + EFFECT_QUAD_TYPE_START = 100 ///< @internal +}; + +/** + * EffectWindow::setData() and EffectWindow::data() global roles. + * All values between 0 and 999 are reserved for global roles. + */ +enum DataRole { + // Grab roles are used to force all other animations to ignore the window. + // The value of the data is set to the Effect's `this` value. + WindowAddedGrabRole = 1, + WindowClosedGrabRole, + WindowMinimizedGrabRole, + WindowUnminimizedGrabRole, + WindowForceBlurRole, ///< For fullscreen effects to enforce blurring of windows, + WindowBlurBehindRole, ///< For single windows to blur behind + WindowForceBackgroundContrastRole, ///< For fullscreen effects to enforce the background contrast, + WindowBackgroundContrastRole, ///< For single windows to enable Background contrast + LanczosCacheRole +}; + +/** + * Style types used by @ref EffectFrame. + * @since 4.6 + */ +enum EffectFrameStyle { + EffectFrameNone, ///< Displays no frame around the contents. + EffectFrameUnstyled, ///< Displays a basic box around the contents. + EffectFrameStyled ///< Displays a Plasma-styled frame around the contents. +}; + +/** + * Infinite region (i.e. a special region type saying that everything needs to be painted). + */ +KWINEFFECTS_EXPORT inline +QRect infiniteRegion() +{ + // INT_MIN / 2 because width/height is used (INT_MIN+INT_MAX==-1) + return QRect(INT_MIN / 2, INT_MIN / 2, INT_MAX, INT_MAX); +} + +/** + * @short Base class for all KWin effects + * + * This is the base class for all effects. By reimplementing virtual methods + * of this class, you can customize how the windows are painted. + * + * The virtual methods are used for painting and need to be implemented for + * custom painting. + * + * In order to react to state changes (e.g. a window gets closed) the effect + * should provide slots for the signals emitted by the EffectsHandler. + * + * @section Chaining + * Most methods of this class are called in chain style. This means that when + * effects A and B area active then first e.g. A::paintWindow() is called and + * then from within that method B::paintWindow() is called (although + * indirectly). To achieve this, you need to make sure to call corresponding + * method in EffectsHandler class from each such method (using @ref effects + * pointer): + * @code + * void MyEffect::postPaintScreen() + * { + * // Do your own processing here + * ... + * // Call corresponding EffectsHandler method + * effects->postPaintScreen(); + * } + * @endcode + * + * @section Effectsptr Effects pointer + * @ref effects pointer points to the global EffectsHandler object that you can + * use to interact with the windows. + * + * @section painting Painting stages + * Painting of windows is done in three stages: + * @li First, the prepaint pass.
+ * Here you can specify how the windows will be painted, e.g. that they will + * be translucent and transformed. + * @li Second, the paint pass.
+ * Here the actual painting takes place. You can change attributes such as + * opacity of windows as well as apply transformations to them. You can also + * paint something onto the screen yourself. + * @li Finally, the postpaint pass.
+ * Here you can mark windows, part of windows or even the entire screen for + * repainting to create animations. + * + * For each stage there are *Screen() and *Window() methods. The window method + * is called for every window which the screen method is usually called just + * once. + * + * @section OpenGL + * Effects can use OpenGL if EffectsHandler::isOpenGLCompositing() returns @c true. + * The OpenGL context may not always be current when code inside the effect is + * executed. The framework ensures that the OpenGL context is current when the Effect + * gets created, destroyed or reconfigured and during the painting stages. All virtual + * methods which have the OpenGL context current are documented. + * + * If OpenGL code is going to be executed outside the painting stages, e.g. in reaction + * to a global shortcut, it is the task of the Effect to make the OpenGL context current: + * @code + * effects->makeOpenGLContextCurrent(); + * @endcode + * + * There is in general no need to call the matching doneCurrent method. + */ +class KWINEFFECTS_EXPORT Effect : public QObject +{ + Q_OBJECT +public: + /** Flags controlling how painting is done. */ + // TODO: is that ok here? + enum { + /** + * Window (or at least part of it) will be painted opaque. + */ + PAINT_WINDOW_OPAQUE = 1 << 0, + /** + * Window (or at least part of it) will be painted translucent. + */ + PAINT_WINDOW_TRANSLUCENT = 1 << 1, + /** + * Window will be painted with transformed geometry. + */ + PAINT_WINDOW_TRANSFORMED = 1 << 2, + /** + * Paint only a region of the screen (can be optimized, cannot + * be used together with TRANSFORMED flags). + */ + PAINT_SCREEN_REGION = 1 << 3, + /** + * The whole screen will be painted with transformed geometry. + * Forces the entire screen to be painted. + */ + PAINT_SCREEN_TRANSFORMED = 1 << 4, + /** + * At least one window will be painted with transformed geometry. + * Forces the entire screen to be painted. + */ + PAINT_SCREEN_WITH_TRANSFORMED_WINDOWS = 1 << 5, + /** + * Clear whole background as the very first step, without optimizing it + */ + PAINT_SCREEN_BACKGROUND_FIRST = 1 << 6, + // PAINT_DECORATION_ONLY = 1 << 7 has been deprecated + /** + * Window will be painted with a lanczos filter. + */ + PAINT_WINDOW_LANCZOS = 1 << 8 + // PAINT_SCREEN_WITH_TRANSFORMED_WINDOWS_WITHOUT_FULL_REPAINTS = 1 << 9 has been removed + }; + + enum Feature { + Nothing = 0, + Resize, + GeometryTip, + Outline, /**< @deprecated */ + ScreenInversion, + Blur, + Contrast, + HighlightWindows + }; + + /** + * Constructs new Effect object. + * + * In OpenGL based compositing, the frameworks ensures that the context is current + * when the Effect is constructed. + */ + Effect(); + /** + * Destructs the Effect object. + * + * In OpenGL based compositing, the frameworks ensures that the context is current + * when the Effect is destroyed. + */ + ~Effect() override; + + /** + * Flags describing which parts of configuration have changed. + */ + enum ReconfigureFlag { + ReconfigureAll = 1 << 0 /// Everything needs to be reconfigured. + }; + Q_DECLARE_FLAGS(ReconfigureFlags, ReconfigureFlag) + + /** + * Called when configuration changes (either the effect's or KWin's global). + * + * In OpenGL based compositing, the frameworks ensures that the context is current + * when the Effect is reconfigured. If this method is called from within the Effect it is + * required to ensure that the context is current if the implementation does OpenGL calls. + */ + virtual void reconfigure(ReconfigureFlags flags); + + /** + * Called when another effect requests the proxy for this effect. + */ + virtual void* proxy(); + + /** + * Called before starting to paint the screen. + * In this method you can: + * @li set whether the windows or the entire screen will be transformed + * @li change the region of the screen that will be painted + * @li do various housekeeping tasks such as initing your effect's variables + for the upcoming paint pass or updating animation's progress + * + * In OpenGL based compositing, the frameworks ensures that the context is current + * when this method is invoked. + */ + virtual void prePaintScreen(ScreenPrePaintData& data, int time); + /** + * In this method you can: + * @li paint something on top of the windows (by painting after calling + * effects->paintScreen()) + * @li paint multiple desktops and/or multiple copies of the same desktop + * by calling effects->paintScreen() multiple times + * + * In OpenGL based compositing, the frameworks ensures that the context is current + * when this method is invoked. + */ + virtual void paintScreen(int mask, const QRegion ®ion, ScreenPaintData& data); + /** + * Called after all the painting has been finished. + * In this method you can: + * @li schedule next repaint in case of animations + * You shouldn't paint anything here. + * + * In OpenGL based compositing, the frameworks ensures that the context is current + * when this method is invoked. + */ + virtual void postPaintScreen(); + + /** + * Called for every window before the actual paint pass + * In this method you can: + * @li enable or disable painting of the window (e.g. enable paiting of minimized window) + * @li set window to be painted with translucency + * @li set window to be transformed + * @li request the window to be divided into multiple parts + * + * In OpenGL based compositing, the frameworks ensures that the context is current + * when this method is invoked. + */ + virtual void prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time); + /** + * This is the main method for painting windows. + * In this method you can: + * @li do various transformations + * @li change opacity of the window + * @li change brightness and/or saturation, if it's supported + * + * In OpenGL based compositing, the frameworks ensures that the context is current + * when this method is invoked. + */ + virtual void paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data); + /** + * Called for every window after all painting has been finished. + * In this method you can: + * @li schedule next repaint for individual window(s) in case of animations + * You shouldn't paint anything here. + * + * In OpenGL based compositing, the frameworks ensures that the context is current + * when this method is invoked. + */ + virtual void postPaintWindow(EffectWindow* w); + + /** + * This method is called directly before painting an @ref EffectFrame. + * You can implement this method if you need to bind a shader or perform + * other operations before the frame is rendered. + * @param frame The EffectFrame which will be rendered + * @param region Region to restrict painting to + * @param opacity Opacity of text/icon + * @param frameOpacity Opacity of background + * @since 4.6 + * + * In OpenGL based compositing, the frameworks ensures that the context is current + * when this method is invoked. + */ + virtual void paintEffectFrame(EffectFrame* frame, const QRegion ®ion, double opacity, double frameOpacity); + + /** + * Called on Transparent resizes. + * return true if your effect substitutes questioned feature + */ + virtual bool provides(Feature); + + /** + * Performs the @p feature with the @p arguments. + * + * This allows to have specific protocols between KWin core and an Effect. + * + * The method is supposed to return @c true if it performed the features, + * @c false otherwise. + * + * The default implementation returns @c false. + * @since 5.8 + */ + virtual bool perform(Feature feature, const QVariantList &arguments); + + /** + * Can be called to draw multiple copies (e.g. thumbnails) of a window. + * You can change window's opacity/brightness/etc here, but you can't + * do any transformations. + * + * In OpenGL based compositing, the frameworks ensures that the context is current + * when this method is invoked. + */ + virtual void drawWindow(EffectWindow* w, int mask, const QRegion ®ion, WindowPaintData& data); + + /** + * Define new window quads so that they can be transformed by other effects. + * It's up to the effect to keep track of them. + */ + virtual void buildQuads(EffectWindow* w, WindowQuadList& quadList); + + virtual void windowInputMouseEvent(QEvent* e); + virtual void grabbedKeyboardEvent(QKeyEvent* e); + + /** + * Overwrite this method to indicate whether your effect will be doing something in + * the next frame to be rendered. If the method returns @c false the effect will be + * excluded from the chained methods in the next rendered frame. + * + * This method is called always directly before the paint loop begins. So it is totally + * fine to e.g. react on a window event, issue a repaint to trigger an animation and + * change a flag to indicate that this method returns @c true. + * + * As the method is called each frame, you should not perform complex calculations. + * Best use just a boolean flag. + * + * The default implementation of this method returns @c true. + * @since 4.8 + */ + virtual bool isActive() const; + + /** + * Reimplement this method to provide online debugging. + * This could be as trivial as printing specific detail information about the effect state + * but could also be used to move the effect in and out of a special debug modes, clear bogus + * data, etc. + * Notice that the functions is const by intent! Whenever you alter the state of the object + * due to random user input, you should do so with greatest care, hence const_cast<> your + * object - signalling "let me alone, i know what i'm doing" + * @param parameter A freeform string user input for your effect to interpret. + * @since 4.11 + */ + virtual QString debug(const QString ¶meter) const; + + /** + * Reimplement this method to indicate where in the Effect chain the Effect should be placed. + * + * A low number indicates early chain position, thus before other Effects got called, a high + * number indicates a late position. The returned number should be in the interval [0, 100]. + * The default value is 0. + * + * In KWin4 this information was provided in the Effect's desktop file as property + * X-KDE-Ordering. In the case of Scripted Effects this property is still used. + * + * @since 5.0 + */ + virtual int requestedEffectChainPosition() const; + + + /** + * A touch point was pressed. + * + * If the effect wants to exclusively use the touch event it should return @c true. + * If @c false is returned the touch event is passed to further effects. + * + * In general an Effect should only return @c true if it is the exclusive effect getting + * input events. E.g. has grabbed mouse events. + * + * Default implementation returns @c false. + * + * @param id The unique id of the touch point + * @param pos The position of the touch point in global coordinates + * @param time Timestamp + * + * @see touchMotion + * @see touchUp + * @since 5.8 + */ + virtual bool touchDown(qint32 id, const QPointF &pos, quint32 time); + /** + * A touch point moved. + * + * If the effect wants to exclusively use the touch event it should return @c true. + * If @c false is returned the touch event is passed to further effects. + * + * In general an Effect should only return @c true if it is the exclusive effect getting + * input events. E.g. has grabbed mouse events. + * + * Default implementation returns @c false. + * + * @param id The unique id of the touch point + * @param pos The position of the touch point in global coordinates + * @param time Timestamp + * + * @see touchDown + * @see touchUp + * @since 5.8 + */ + virtual bool touchMotion(qint32 id, const QPointF &pos, quint32 time); + /** + * A touch point was released. + * + * If the effect wants to exclusively use the touch event it should return @c true. + * If @c false is returned the touch event is passed to further effects. + * + * In general an Effect should only return @c true if it is the exclusive effect getting + * input events. E.g. has grabbed mouse events. + * + * Default implementation returns @c false. + * + * @param id The unique id of the touch point + * @param time Timestamp + * + * @see touchDown + * @see touchMotion + * @since 5.8 + */ + virtual bool touchUp(qint32 id, quint32 time); + + static QPoint cursorPos(); + + /** + * Read animation time from the configuration and possibly adjust using animationTimeFactor(). + * The configuration value in the effect should also have special value 'default' (set using + * QSpinBox::setSpecialValueText()) with the value 0. This special value is adjusted + * using the global animation speed, otherwise the exact time configured is returned. + * @param cfg configuration group to read value from + * @param key configuration key to read value from + * @param defaultTime default animation time in milliseconds + */ + // return type is intentionally double so that one can divide using it without losing data + static double animationTime(const KConfigGroup& cfg, const QString& key, int defaultTime); + /** + * @overload Use this variant if the animation time is hardcoded and not configurable + * in the effect itself. + */ + static double animationTime(int defaultTime); + /** + * @overload Use this variant if animation time is provided through a KConfigXT generated class + * having a property called "duration". + */ + template + int animationTime(int defaultDuration); + /** + * Linearly interpolates between @p x and @p y. + * + * Returns @p x when @p a = 0; returns @p y when @p a = 1. + */ + static double interpolate(double x, double y, double a) { + return x * (1 - a) + y * a; + } + /** Helper to set WindowPaintData and QRegion to necessary transformations so that + * a following drawWindow() would put the window at the requested geometry (useful for thumbnails) + */ + static void setPositionTransformations(WindowPaintData& data, QRect& region, EffectWindow* w, + const QRect& r, Qt::AspectRatioMode aspect); + +public Q_SLOTS: + virtual bool borderActivated(ElectricBorder border); + +protected: + xcb_connection_t *xcbConnection() const; + xcb_window_t x11RootWindow() const; + + /** + * An implementing class can call this with it's kconfig compiled singleton class. + * This method will perform the instance on the class. + * @since 5.9 + */ + template + void initConfig(); +}; + + +/** + * Prefer the KWIN_EFFECT_FACTORY macros. + */ +class KWINEFFECTS_EXPORT EffectPluginFactory : public KPluginFactory +{ + Q_OBJECT +public: + EffectPluginFactory(); + ~EffectPluginFactory() override; + /** + * Returns whether the Effect is supported. + * + * An Effect can implement this method to determine at runtime whether the Effect is supported. + * + * If the current compositing backend is not supported it should return @c false. + * + * This method is optional, by default @c true is returned. + */ + virtual bool isSupported() const; + /** + * Returns whether the Effect should get enabled by default. + * + * This function provides a way for an effect to override the default at runtime, + * e.g. based on the capabilities of the hardware. + * + * This method is optional; the effect doesn't have to provide it. + * + * Note that this function is only called if the supported() function returns true, + * and if X-KDE-PluginInfo-EnabledByDefault is set to true in the .desktop file. + * + * This method is optional, by default @c true is returned. + */ + virtual bool enabledByDefault() const; + /** + * This method returns the created Effect. + */ + virtual KWin::Effect *createEffect() const = 0; +}; + +/** + * Defines an EffectPluginFactory sub class with customized isSupported and enabledByDefault methods. + * + * If the Effect to be created does not need the isSupported or enabledByDefault methods prefer + * the simplified KWIN_EFFECT_FACTORY, KWIN_EFFECT_FACTORY_SUPPORTED or KWIN_EFFECT_FACTORY_ENABLED + * macros which create an EffectPluginFactory with a useable default value. + * + * The macro also adds a useable K_EXPORT_PLUGIN_VERSION to the definition. KWin will not load + * any Effect with a non-matching plugin version. This API is not providing binary compatibility + * and thus the effect plugin must be compiled against the same kwineffects library version as + * KWin. + * + * @param factoryName The name to be used for the EffectPluginFactory + * @param className The class name of the Effect sub class which is to be created by the factory + * @param jsonFile Name of the json file to be compiled into the plugin as metadata + * @param supported Source code to go into the isSupported() method, must return a boolean + * @param enabled Source code to go into the enabledByDefault() method, must return a boolean + */ +#define KWIN_EFFECT_FACTORY_SUPPORTED_ENABLED( factoryName, className, jsonFile, supported, enabled ) \ + class factoryName : public KWin::EffectPluginFactory \ + { \ + Q_OBJECT \ + Q_PLUGIN_METADATA(IID KPluginFactory_iid FILE jsonFile) \ + Q_INTERFACES(KPluginFactory) \ + public: \ + explicit factoryName() {} \ + ~factoryName() {} \ + bool isSupported() const override { \ + supported \ + } \ + bool enabledByDefault() const override { \ + enabled \ + } \ + KWin::Effect *createEffect() const override { \ + return new className(); \ + } \ + }; \ + K_EXPORT_PLUGIN_VERSION(quint32(KWIN_EFFECT_API_VERSION)) + +#define KWIN_EFFECT_FACTORY_ENABLED( factoryName, className, jsonFile, enabled ) \ + KWIN_EFFECT_FACTORY_SUPPORTED_ENABLED( factoryName, className, jsonFile, return true;, enabled ) + +#define KWIN_EFFECT_FACTORY_SUPPORTED( factoryName, className, jsonFile, supported ) \ + KWIN_EFFECT_FACTORY_SUPPORTED_ENABLED( factoryName, className, jsonFile, supported, return true; ) + +#define KWIN_EFFECT_FACTORY( factoryName, className, jsonFile ) \ + KWIN_EFFECT_FACTORY_SUPPORTED_ENABLED( factoryName, className, jsonFile, return true;, return true; ) + + + +/** + * @short Manager class that handles all the effects. + * + * This class creates Effect objects and calls it's appropriate methods. + * + * Effect objects can call methods of this class to interact with the + * workspace, e.g. to activate or move a specific window, change current + * desktop or create a special input window to receive mouse and keyboard + * events. + */ +class KWINEFFECTS_EXPORT EffectsHandler : public QObject +{ + Q_OBJECT + Q_PROPERTY(int currentDesktop READ currentDesktop WRITE setCurrentDesktop NOTIFY desktopChanged) + Q_PROPERTY(QString currentActivity READ currentActivity NOTIFY currentActivityChanged) + Q_PROPERTY(KWin::EffectWindow *activeWindow READ activeWindow WRITE activateWindow NOTIFY windowActivated) + Q_PROPERTY(QSize desktopGridSize READ desktopGridSize) + Q_PROPERTY(int desktopGridWidth READ desktopGridWidth) + Q_PROPERTY(int desktopGridHeight READ desktopGridHeight) + Q_PROPERTY(int workspaceWidth READ workspaceWidth) + Q_PROPERTY(int workspaceHeight READ workspaceHeight) + /** + * The number of desktops currently used. Minimum number of desktops is 1, maximum 20. + */ + Q_PROPERTY(int desktops READ numberOfDesktops WRITE setNumberOfDesktops NOTIFY numberDesktopsChanged) + Q_PROPERTY(bool optionRollOverDesktops READ optionRollOverDesktops) + Q_PROPERTY(int activeScreen READ activeScreen) + Q_PROPERTY(int numScreens READ numScreens NOTIFY numberScreensChanged) + /** + * Factor by which animation speed in the effect should be modified (multiplied). + * If configurable in the effect itself, the option should have also 'default' + * animation speed. The actual value should be determined using animationTime(). + * Note: The factor can be also 0, so make sure your code can cope with 0ms time + * if used manually. + */ + Q_PROPERTY(qreal animationTimeFactor READ animationTimeFactor) + Q_PROPERTY(QList< KWin::EffectWindow* > stackingOrder READ stackingOrder) + /** + * Whether window decorations use the alpha channel. + */ + Q_PROPERTY(bool decorationsHaveAlpha READ decorationsHaveAlpha) + /** + * Whether the window decorations support blurring behind the decoration. + */ + Q_PROPERTY(bool decorationSupportsBlurBehind READ decorationSupportsBlurBehind) + Q_PROPERTY(CompositingType compositingType READ compositingType CONSTANT) + Q_PROPERTY(QPoint cursorPos READ cursorPos) + Q_PROPERTY(QSize virtualScreenSize READ virtualScreenSize NOTIFY virtualScreenSizeChanged) + Q_PROPERTY(QRect virtualScreenGeometry READ virtualScreenGeometry NOTIFY virtualScreenGeometryChanged) + Q_PROPERTY(bool hasActiveFullScreenEffect READ hasActiveFullScreenEffect NOTIFY hasActiveFullScreenEffectChanged) + + /** + * The status of the session i.e if the user is logging out + * @since 5.18 + */ + Q_PROPERTY(KWin::SessionState sessionState READ sessionState NOTIFY sessionStateChanged) + + friend class Effect; +public: + explicit EffectsHandler(CompositingType type); + ~EffectsHandler() override; + // for use by effects + virtual void prePaintScreen(ScreenPrePaintData& data, int time) = 0; + virtual void paintScreen(int mask, const QRegion ®ion, ScreenPaintData& data) = 0; + virtual void postPaintScreen() = 0; + virtual void prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time) = 0; + virtual void paintWindow(EffectWindow* w, int mask, const QRegion ®ion, WindowPaintData& data) = 0; + virtual void postPaintWindow(EffectWindow* w) = 0; + virtual void paintEffectFrame(EffectFrame* frame, const QRegion ®ion, double opacity, double frameOpacity) = 0; + virtual void drawWindow(EffectWindow* w, int mask, const QRegion ®ion, WindowPaintData& data) = 0; + virtual void buildQuads(EffectWindow* w, WindowQuadList& quadList) = 0; + virtual QVariant kwinOption(KWinOption kwopt) = 0; + /** + * Sets the cursor while the mouse is intercepted. + * @see startMouseInterception + * @since 4.11 + */ + virtual void defineCursor(Qt::CursorShape shape) = 0; + virtual QPoint cursorPos() const = 0; + virtual bool grabKeyboard(Effect* effect) = 0; + virtual void ungrabKeyboard() = 0; + /** + * Ensures that all mouse events are sent to the @p effect. + * No window will get the mouse events. Only fullscreen effects providing a custom user interface should + * be using this method. The input events are delivered to Effect::windowInputMouseEvent. + * + * @note This method does not perform an X11 mouse grab. On X11 a fullscreen input window is raised above + * all other windows, but no grab is performed. + * + * @param effect The effect + * @param shape Sets the cursor to be used while the mouse is intercepted + * @see stopMouseInterception + * @see Effect::windowInputMouseEvent + * @since 4.11 + */ + virtual void startMouseInterception(Effect *effect, Qt::CursorShape shape) = 0; + /** + * Releases the hold mouse interception for @p effect + * @see startMouseInterception + * @since 4.11 + */ + virtual void stopMouseInterception(Effect *effect) = 0; + + /** + * @brief Registers a global shortcut with the provided @p action. + * + * @param shortcut The global shortcut which should trigger the action + * @param action The action which gets triggered when the shortcut matches + */ + virtual void registerGlobalShortcut(const QKeySequence &shortcut, QAction *action) = 0; + /** + * @brief Registers a global pointer shortcut with the provided @p action. + * + * @param modifiers The keyboard modifiers which need to be holded + * @param pointerButtons The pointer buttons which need to be pressed + * @param action The action which gets triggered when the shortcut matches + */ + virtual void registerPointerShortcut(Qt::KeyboardModifiers modifiers, Qt::MouseButton pointerButtons, QAction *action) = 0; + /** + * @brief Registers a global axis shortcut with the provided @p action. + * + * @param modifiers The keyboard modifiers which need to be holded + * @param axis The direction in which the axis needs to be moved + * @param action The action which gets triggered when the shortcut matches + */ + virtual void registerAxisShortcut(Qt::KeyboardModifiers modifiers, PointerAxisDirection axis, QAction *action) = 0; + + /** + * @brief Registers a global touchpad swipe gesture shortcut with the provided @p action. + * + * @param direction The direction for the swipe + * @param action The action which gets triggered when the gesture triggers + * @since 5.10 + */ + virtual void registerTouchpadSwipeShortcut(SwipeDirection direction, QAction *action) = 0; + + /** + * Retrieve the proxy class for an effect if it has one. Will return NULL if + * the effect isn't loaded or doesn't have a proxy class. + */ + virtual void* getProxy(QString name) = 0; + + // Mouse polling + virtual void startMousePolling() = 0; + virtual void stopMousePolling() = 0; + + virtual void reserveElectricBorder(ElectricBorder border, Effect *effect) = 0; + virtual void unreserveElectricBorder(ElectricBorder border, Effect *effect) = 0; + + /** + * Registers the given @p action for the given @p border to be activated through + * a touch swipe gesture. + * + * If the @p border gets triggered through a touch swipe gesture the QAction::triggered + * signal gets invoked. + * + * To unregister the touch screen action either delete the @p action or + * invoke unregisterTouchBorder. + * + * @see unregisterTouchBorder + * @since 5.10 + */ + virtual void registerTouchBorder(ElectricBorder border, QAction *action) = 0; + /** + * Unregisters the given @p action for the given touch @p border. + * + * @see registerTouchBorder + * @since 5.10 + */ + virtual void unregisterTouchBorder(ElectricBorder border, QAction *action) = 0; + + // functions that allow controlling windows/desktop + virtual void activateWindow(KWin::EffectWindow* c) = 0; + virtual KWin::EffectWindow* activeWindow() const = 0 ; + Q_SCRIPTABLE virtual void moveWindow(KWin::EffectWindow* w, const QPoint& pos, bool snap = false, double snapAdjust = 1.0) = 0; + + /** + * Moves the window to the specific desktop + * Setting desktop to NET::OnAllDesktops will set the window on all desktops + */ + Q_SCRIPTABLE virtual void windowToDesktop(KWin::EffectWindow* w, int desktop) = 0; + + /** + * Moves a window to the given desktops + * On X11, the window will end up on the last window in the list + * Setting this to an empty list will set the window on all desktops + * + * @arg desktopIds a list of desktops the window should be placed on. NET::OnAllDesktops is not a valid desktop X11Id + */ + Q_SCRIPTABLE virtual void windowToDesktops(KWin::EffectWindow* w, const QVector &desktopIds) = 0; + + Q_SCRIPTABLE virtual void windowToScreen(KWin::EffectWindow* w, int screen) = 0; + virtual void setShowingDesktop(bool showing) = 0; + + // Activities + /** + * @returns The ID of the current activity. + */ + virtual QString currentActivity() const = 0; + // Desktops + /** + * @returns The ID of the current desktop. + */ + virtual int currentDesktop() const = 0; + /** + * @returns Total number of desktops currently in existence. + */ + virtual int numberOfDesktops() const = 0; + /** + * Set the current desktop to @a desktop. + */ + virtual void setCurrentDesktop(int desktop) = 0; + /** + * Sets the total number of desktops to @a desktops. + */ + virtual void setNumberOfDesktops(int desktops) = 0; + /** + * @returns The size of desktop layout in grid units. + */ + virtual QSize desktopGridSize() const = 0; + /** + * @returns The width of desktop layout in grid units. + */ + virtual int desktopGridWidth() const = 0; + /** + * @returns The height of desktop layout in grid units. + */ + virtual int desktopGridHeight() const = 0; + /** + * @returns The width of desktop layout in pixels. + */ + virtual int workspaceWidth() const = 0; + /** + * @returns The height of desktop layout in pixels. + */ + virtual int workspaceHeight() const = 0; + /** + * @returns The ID of the desktop at the point @a coords or 0 if no desktop exists at that + * point. @a coords is to be in grid units. + */ + virtual int desktopAtCoords(QPoint coords) const = 0; + /** + * @returns The coords of desktop @a id in grid units. + */ + virtual QPoint desktopGridCoords(int id) const = 0; + /** + * @returns The coords of the top-left corner of desktop @a id in pixels. + */ + virtual QPoint desktopCoords(int id) const = 0; + /** + * @returns The ID of the desktop above desktop @a id. Wraps around to the bottom of + * the layout if @a wrap is set. If @a id is not set use the current one. + */ + Q_SCRIPTABLE virtual int desktopAbove(int desktop = 0, bool wrap = true) const = 0; + /** + * @returns The ID of the desktop to the right of desktop @a id. Wraps around to the + * left of the layout if @a wrap is set. If @a id is not set use the current one. + */ + Q_SCRIPTABLE virtual int desktopToRight(int desktop = 0, bool wrap = true) const = 0; + /** + * @returns The ID of the desktop below desktop @a id. Wraps around to the top of the + * layout if @a wrap is set. If @a id is not set use the current one. + */ + Q_SCRIPTABLE virtual int desktopBelow(int desktop = 0, bool wrap = true) const = 0; + /** + * @returns The ID of the desktop to the left of desktop @a id. Wraps around to the + * right of the layout if @a wrap is set. If @a id is not set use the current one. + */ + Q_SCRIPTABLE virtual int desktopToLeft(int desktop = 0, bool wrap = true) const = 0; + Q_SCRIPTABLE virtual QString desktopName(int desktop) const = 0; + virtual bool optionRollOverDesktops() const = 0; + + virtual int activeScreen() const = 0; // Xinerama + virtual int numScreens() const = 0; // Xinerama + Q_SCRIPTABLE virtual int screenNumber(const QPoint& pos) const = 0; // Xinerama + virtual QRect clientArea(clientAreaOption, int screen, int desktop) const = 0; + virtual QRect clientArea(clientAreaOption, const EffectWindow* c) const = 0; + virtual QRect clientArea(clientAreaOption, const QPoint& p, int desktop) const = 0; + + /** + * The bounding size of all screens combined. Overlapping areas + * are not counted multiple times. + * + * @see virtualScreenGeometry() + * @see virtualScreenSizeChanged() + * @since 5.0 + */ + virtual QSize virtualScreenSize() const = 0; + /** + * The bounding geometry of all outputs combined. Always starts at (0,0) and has + * virtualScreenSize as it's size. + * + * @see virtualScreenSize() + * @see virtualScreenGeometryChanged() + * @since 5.0 + */ + virtual QRect virtualScreenGeometry() const = 0; + /** + * Factor by which animation speed in the effect should be modified (multiplied). + * If configurable in the effect itself, the option should have also 'default' + * animation speed. The actual value should be determined using animationTime(). + * Note: The factor can be also 0, so make sure your code can cope with 0ms time + * if used manually. + */ + virtual double animationTimeFactor() const = 0; + virtual WindowQuadType newWindowQuadType() = 0; + + Q_SCRIPTABLE virtual KWin::EffectWindow* findWindow(WId id) const = 0; + Q_SCRIPTABLE virtual KWin::EffectWindow* findWindow(KWaylandServer::SurfaceInterface *surf) const = 0; + /** + * Finds the EffectWindow for the internal window @p w. + * If there is no such window @c null is returned. + * + * On Wayland this returns the internal window. On X11 it returns an Unamanged with the + * window id matching that of the provided window @p w. + * + * @since 5.16 + */ + Q_SCRIPTABLE virtual KWin::EffectWindow *findWindow(QWindow *w) const = 0; + /** + * Finds the EffectWindow for the Toplevel with KWin internal @p id. + * If there is no such window @c null is returned. + * + * @since 5.16 + */ + Q_SCRIPTABLE virtual KWin::EffectWindow *findWindow(const QUuid &id) const = 0; + virtual EffectWindowList stackingOrder() const = 0; + // window will be temporarily painted as if being at the top of the stack + Q_SCRIPTABLE virtual void setElevatedWindow(KWin::EffectWindow* w, bool set) = 0; + + virtual void setTabBoxWindow(EffectWindow*) = 0; + virtual void setTabBoxDesktop(int) = 0; + virtual EffectWindowList currentTabBoxWindowList() const = 0; + virtual void refTabBox() = 0; + virtual void unrefTabBox() = 0; + virtual void closeTabBox() = 0; + virtual QList< int > currentTabBoxDesktopList() const = 0; + virtual int currentTabBoxDesktop() const = 0; + virtual EffectWindow* currentTabBoxWindow() const = 0; + + virtual void setActiveFullScreenEffect(Effect* e) = 0; + virtual Effect* activeFullScreenEffect() const = 0; + + /** + * Schedules the entire workspace to be repainted next time. + * If you call it during painting (including prepaint) then it does not + * affect the current painting. + */ + Q_SCRIPTABLE virtual void addRepaintFull() = 0; + Q_SCRIPTABLE virtual void addRepaint(const QRect& r) = 0; + Q_SCRIPTABLE virtual void addRepaint(const QRegion& r) = 0; + Q_SCRIPTABLE virtual void addRepaint(int x, int y, int w, int h) = 0; + + CompositingType compositingType() const; + /** + * @brief Whether the Compositor is OpenGL based (either GL 1 or 2). + * + * @return bool @c true in case of OpenGL based Compositor, @c false otherwise + */ + bool isOpenGLCompositing() const; + virtual unsigned long xrenderBufferPicture() = 0; + /** + * @brief Provides access to the QPainter which is rendering to the back buffer. + * + * Only relevant for CompositingType QPainterCompositing. For all other compositing types + * @c null is returned. + * + * @return QPainter* The Scene's QPainter or @c null. + */ + virtual QPainter *scenePainter() = 0; + virtual void reconfigure() = 0; + + virtual QByteArray readRootProperty(long atom, long type, int format) const = 0; + /** + * @brief Announces support for the feature with the given name. If no other Effect + * has announced support for this feature yet, an X11 property will be installed on + * the root window. + * + * The Effect will be notified for events through the signal propertyNotify(). + * + * To remove the support again use removeSupportProperty. When an Effect is + * destroyed it is automatically taken care of removing the support. It is not + * required to call removeSupportProperty in the Effect's cleanup handling. + * + * @param propertyName The name of the property to announce support for + * @param effect The effect which announces support + * @return xcb_atom_t The created X11 atom + * @see removeSupportProperty + * @since 4.11 + */ + virtual xcb_atom_t announceSupportProperty(const QByteArray &propertyName, Effect *effect) = 0; + /** + * @brief Removes support for the feature with the given name. If there is no other Effect left + * which has announced support for the given property, the property will be removed from the + * root window. + * + * In case the Effect had not registered support, calling this function does not change anything. + * + * @param propertyName The name of the property to remove support for + * @param effect The effect which had registered the property. + * @see announceSupportProperty + * @since 4.11 + */ + virtual void removeSupportProperty(const QByteArray &propertyName, Effect *effect) = 0; + + /** + * Returns @a true if the active window decoration has shadow API hooks. + */ + virtual bool hasDecorationShadows() const = 0; + + /** + * Returns @a true if the window decorations use the alpha channel, and @a false otherwise. + * @since 4.5 + */ + virtual bool decorationsHaveAlpha() const = 0; + + /** + * Returns @a true if the window decorations support blurring behind the decoration, and @a false otherwise + * @since 4.6 + */ + virtual bool decorationSupportsBlurBehind() const = 0; + + /** + * Creates a new frame object. If the frame does not have a static size + * then it will be located at @a position with @a alignment. A + * non-static frame will automatically adjust its size to fit the contents. + * @returns A new @ref EffectFrame. It is the responsibility of the caller to delete the + * EffectFrame. + * @since 4.6 + */ + virtual EffectFrame* effectFrame(EffectFrameStyle style, bool staticSize = true, + const QPoint& position = QPoint(-1, -1), Qt::Alignment alignment = Qt::AlignCenter) const = 0; + + /** + * Allows an effect to trigger a reload of itself. + * This can be used by an effect which needs to be reloaded when screen geometry changes. + * It is possible that the effect cannot be loaded again as it's supported method does no longer + * hold. + * @param effect The effect to reload + * @since 4.8 + */ + virtual void reloadEffect(Effect *effect) = 0; + + /** + * Whether the screen is currently considered as locked. + * Note for technical reasons this is not always possible to detect. The screen will only + * be considered as locked if the screen locking process implements the + * org.freedesktop.ScreenSaver interface. + * + * @returns @c true if the screen is currently locked, @c false otherwise + * @see screenLockingChanged + * @since 4.11 + */ + virtual bool isScreenLocked() const = 0; + + /** + * @brief Makes the OpenGL compositing context current. + * + * If the compositing backend is not using OpenGL, this method returns @c false. + * + * @return bool @c true if the context became current, @c false otherwise. + */ + virtual bool makeOpenGLContextCurrent() = 0; + /** + * @brief Makes a null OpenGL context current resulting in no context + * being current. + * + * If the compositing backend is not OpenGL based, this method is a noop. + * + * There is normally no reason for an Effect to call this method. + */ + virtual void doneOpenGLContextCurrent() = 0; + + virtual xcb_connection_t *xcbConnection() const = 0; + virtual xcb_window_t x11RootWindow() const = 0; + + /** + * Interface to the Wayland display: this is relevant only + * on Wayland, on X11 it will be nullptr + * @since 5.5 + */ + virtual KWaylandServer::Display *waylandDisplay() const = 0; + + /** + * Whether animations are supported by the Scene. + * If this method returns @c false Effects are supposed to not + * animate transitions. + * + * @returns Whether the Scene can drive animations + * @since 5.8 + */ + virtual bool animationsSupported() const = 0; + + /** + * The current cursor image of the Platform. + * @see cursorPos + * @since 5.9 + */ + virtual PlatformCursorImage cursorImage() const = 0; + + /** + * The cursor image should be hidden. + * @see showCursor + * @since 5.9 + */ + virtual void hideCursor() = 0; + + /** + * The cursor image should be shown again after having been hidden. + * @see hideCursor + * @since 5.9 + */ + virtual void showCursor() = 0; + + /** + * Starts an interactive window selection process. + * + * Once the user selected a window the @p callback is invoked with the selected EffectWindow as + * argument. In case the user cancels the interactive window selection or selecting a window is currently + * not possible (e.g. screen locked) the @p callback is invoked with a @c nullptr argument. + * + * During the interactive window selection the cursor is turned into a crosshair cursor. + * + * @param callback The function to invoke once the interactive window selection ends + * @since 5.9 + */ + virtual void startInteractiveWindowSelection(std::function callback) = 0; + + /** + * Starts an interactive position selection process. + * + * Once the user selected a position on the screen the @p callback is invoked with + * the selected point as argument. In case the user cancels the interactive position selection + * or selecting a position is currently not possible (e.g. screen locked) the @p callback + * is invoked with a point at @c -1 as x and y argument. + * + * During the interactive window selection the cursor is turned into a crosshair cursor. + * + * @param callback The function to invoke once the interactive position selection ends + * @since 5.9 + */ + virtual void startInteractivePositionSelection(std::function callback) = 0; + + /** + * Shows an on-screen-message. To hide it again use hideOnScreenMessage. + * + * @param message The message to show + * @param iconName The optional themed icon name + * @see hideOnScreenMessage + * @since 5.9 + */ + virtual void showOnScreenMessage(const QString &message, const QString &iconName = QString()) = 0; + + /** + * Flags for how to hide a shown on-screen-message + * @see hideOnScreenMessage + * @since 5.9 + */ + enum class OnScreenMessageHideFlag { + /** + * The on-screen-message should skip the close window animation. + * @see EffectWindow::skipsCloseAnimation + */ + SkipsCloseAnimation = 1 + }; + Q_DECLARE_FLAGS(OnScreenMessageHideFlags, OnScreenMessageHideFlag) + /** + * Hides a previously shown on-screen-message again. + * @param flags The flags for how to hide the message + * @see showOnScreenMessage + * @since 5.9 + */ + virtual void hideOnScreenMessage(OnScreenMessageHideFlags flags = OnScreenMessageHideFlags()) = 0; + + /* + * @returns The configuration used by the EffectsHandler. + * @since 5.10 + */ + virtual KSharedConfigPtr config() const = 0; + + /** + * @returns The global input configuration (kcminputrc) + * @since 5.10 + */ + virtual KSharedConfigPtr inputConfig() const = 0; + + /** + * Returns if activeFullScreenEffect is set + */ + virtual bool hasActiveFullScreenEffect() const = 0; + + /** + * Render the supplied EffectQuickView onto the scene + * It can be called at any point during the scene rendering + * @since 5.18 + */ + virtual void renderEffectQuickView(EffectQuickView *effectQuickView) const = 0; + + /** + * The status of the session i.e if the user is logging out + * @since 5.18 + */ + virtual SessionState sessionState() const = 0; +Q_SIGNALS: + /** + * Signal emitted when the current desktop changed. + * @param oldDesktop The previously current desktop + * @param newDesktop The new current desktop + * @param with The window which is taken over to the new desktop, can be NULL + * @since 4.9 + */ + void desktopChanged(int oldDesktop, int newDesktop, KWin::EffectWindow *with); + /** + * @since 4.7 + * @deprecated + */ + void desktopChanged(int oldDesktop, int newDesktop); + /** + * Signal emitted when a window moved to another desktop + * NOTICE that this does NOT imply that the desktop has changed + * The @param window which is moved to the new desktop + * @param oldDesktop The previous desktop of the window + * @param newDesktop The new desktop of the window + * @since 4.11.4 + */ + void desktopPresenceChanged(KWin::EffectWindow *window, int oldDesktop, int newDesktop); + /** + * Signal emitted when the number of currently existing desktops is changed. + * @param old The previous number of desktops in used. + * @see EffectsHandler::numberOfDesktops. + * @since 4.7 + */ + void numberDesktopsChanged(uint old); + /** + * Signal emitted when the number of screens changed. + * @since 5.0 + */ + void numberScreensChanged(); + /** + * Signal emitted when the desktop showing ("dashboard") state changed + * The desktop is risen to the keepAbove layer, you may want to elevate + * windows or such. + * @since 5.3 + */ + void showingDesktopChanged(bool); + /** + * Signal emitted when a new window has been added to the Workspace. + * @param w The added window + * @since 4.7 + */ + void windowAdded(KWin::EffectWindow *w); + /** + * Signal emitted when a window is being removed from the Workspace. + * An effect which wants to animate the window closing should connect + * to this signal and reference the window by using + * refWindow + * @param w The window which is being closed + * @since 4.7 + */ + void windowClosed(KWin::EffectWindow *w); + /** + * Signal emitted when a window get's activated. + * @param w The new active window, or @c NULL if there is no active window. + * @since 4.7 + */ + void windowActivated(KWin::EffectWindow *w); + /** + * Signal emitted when a window is deleted. + * This means that a closed window is not referenced any more. + * An effect bookkeeping the closed windows should connect to this + * signal to clean up the internal references. + * @param w The window which is going to be deleted. + * @see EffectWindow::refWindow + * @see EffectWindow::unrefWindow + * @see windowClosed + * @since 4.7 + */ + void windowDeleted(KWin::EffectWindow *w); + /** + * Signal emitted when a user begins a window move or resize operation. + * To figure out whether the user resizes or moves the window use + * isUserMove or isUserResize. + * Whenever the geometry is updated the signal @ref windowStepUserMovedResized + * is emitted with the current geometry. + * The move/resize operation ends with the signal @ref windowFinishUserMovedResized. + * Only one window can be moved/resized by the user at the same time! + * @param w The window which is being moved/resized + * @see windowStepUserMovedResized + * @see windowFinishUserMovedResized + * @see EffectWindow::isUserMove + * @see EffectWindow::isUserResize + * @since 4.7 + */ + void windowStartUserMovedResized(KWin::EffectWindow *w); + /** + * Signal emitted during a move/resize operation when the user changed the geometry. + * Please note: KWin supports two operation modes. In one mode all changes are applied + * instantly. This means the window's geometry matches the passed in @p geometry. In the + * other mode the geometry is changed after the user ended the move/resize mode. + * The @p geometry differs from the window's geometry. Also the window's pixmap still has + * the same size as before. Depending what the effect wants to do it would be recommended + * to scale/translate the window. + * @param w The window which is being moved/resized + * @param geometry The geometry of the window in the current move/resize step. + * @see windowStartUserMovedResized + * @see windowFinishUserMovedResized + * @see EffectWindow::isUserMove + * @see EffectWindow::isUserResize + * @since 4.7 + */ + void windowStepUserMovedResized(KWin::EffectWindow *w, const QRect &geometry); + /** + * Signal emitted when the user finishes move/resize of window @p w. + * @param w The window which has been moved/resized + * @see windowStartUserMovedResized + * @see windowFinishUserMovedResized + * @since 4.7 + */ + void windowFinishUserMovedResized(KWin::EffectWindow *w); + /** + * Signal emitted when the maximized state of the window @p w changed. + * A window can be in one of four states: + * @li restored: both @p horizontal and @p vertical are @c false + * @li horizontally maximized: @p horizontal is @c true and @p vertical is @c false + * @li vertically maximized: @p horizontal is @c false and @p vertical is @c true + * @li completely maximized: both @p horizontal and @p vertical are @c true + * @param w The window whose maximized state changed + * @param horizontal If @c true maximized horizontally + * @param vertical If @c true maximized vertically + * @since 4.7 + */ + void windowMaximizedStateChanged(KWin::EffectWindow *w, bool horizontal, bool vertical); + /** + * Signal emitted when the geometry or shape of a window changed. + * This is caused if the window changes geometry without user interaction. + * E.g. the decoration is changed. This is in opposite to windowUserMovedResized + * which is caused by direct user interaction. + * @param w The window whose geometry changed + * @param old The previous geometry + * @see windowUserMovedResized + * @since 4.7 + */ + void windowGeometryShapeChanged(KWin::EffectWindow *w, const QRect &old); + /** + * This signal is emitted when the frame geometry of a window changed. + * @param window The window whose geometry changed + * @param oldGeometry The previous geometry + * @since 5.19 + */ + void windowFrameGeometryChanged(KWin::EffectWindow *window, const QRect &oldGeometry); + /** + * Signal emitted when the padding of a window changed. (eg. shadow size) + * @param w The window whose geometry changed + * @param old The previous expandedGeometry() + * @since 4.9 + */ + void windowPaddingChanged(KWin::EffectWindow *w, const QRect &old); + /** + * Signal emitted when the windows opacity is changed. + * @param w The window whose opacity level is changed. + * @param oldOpacity The previous opacity level + * @param newOpacity The new opacity level + * @since 4.7 + */ + void windowOpacityChanged(KWin::EffectWindow *w, qreal oldOpacity, qreal newOpacity); + /** + * Signal emitted when a window got minimized. + * @param w The window which was minimized + * @since 4.7 + */ + void windowMinimized(KWin::EffectWindow *w); + /** + * Signal emitted when a window got unminimized. + * @param w The window which was unminimized + * @since 4.7 + */ + void windowUnminimized(KWin::EffectWindow *w); + /** + * Signal emitted when a window either becomes modal (ie. blocking for its main client) or looses that state. + * @param w The window which was unminimized + * @since 4.11 + */ + void windowModalityChanged(KWin::EffectWindow *w); + /** + * Signal emitted when a window either became unresponsive (eg. app froze or crashed) + * or respoonsive + * @param w The window that became (un)responsive + * @param unresponsive Whether the window is responsive or unresponsive + * @since 5.10 + */ + void windowUnresponsiveChanged(KWin::EffectWindow *w, bool unresponsive); + /** + * Signal emitted when an area of a window is scheduled for repainting. + * Use this signal in an effect if another area needs to be synced as well. + * @param w The window which is scheduled for repainting + * @param r Always empty. + * @since 4.7 + */ + void windowDamaged(KWin::EffectWindow *w, const QRect &r); + /** + * Signal emitted when a tabbox is added. + * An effect who wants to replace the tabbox with itself should use refTabBox. + * @param mode The TabBoxMode. + * @see refTabBox + * @see tabBoxClosed + * @see tabBoxUpdated + * @see tabBoxKeyEvent + * @since 4.7 + */ + void tabBoxAdded(int mode); + /** + * Signal emitted when the TabBox was closed by KWin core. + * An effect which referenced the TabBox should use unrefTabBox to unref again. + * @see unrefTabBox + * @see tabBoxAdded + * @since 4.7 + */ + void tabBoxClosed(); + /** + * Signal emitted when the selected TabBox window changed or the TabBox List changed. + * An effect should only response to this signal if it referenced the TabBox with refTabBox. + * @see refTabBox + * @see currentTabBoxWindowList + * @see currentTabBoxDesktopList + * @see currentTabBoxWindow + * @see currentTabBoxDesktop + * @since 4.7 + */ + void tabBoxUpdated(); + /** + * Signal emitted when a key event, which is not handled by TabBox directly is, happens while + * TabBox is active. An effect might use the key event to e.g. change the selected window. + * An effect should only response to this signal if it referenced the TabBox with refTabBox. + * @param event The key event not handled by TabBox directly + * @see refTabBox + * @since 4.7 + */ + void tabBoxKeyEvent(QKeyEvent* event); + void currentTabAboutToChange(KWin::EffectWindow* from, KWin::EffectWindow* to); + void tabAdded(KWin::EffectWindow* from, KWin::EffectWindow* to); // from merged with to + void tabRemoved(KWin::EffectWindow* c, KWin::EffectWindow* group); // c removed from group + /** + * Signal emitted when mouse changed. + * If an effect needs to get updated mouse positions, it needs to first call startMousePolling. + * For a fullscreen effect it is better to use an input window and react on windowInputMouseEvent. + * @param pos The new mouse position + * @param oldpos The previously mouse position + * @param buttons The pressed mouse buttons + * @param oldbuttons The previously pressed mouse buttons + * @param modifiers Pressed keyboard modifiers + * @param oldmodifiers Previously pressed keyboard modifiers. + * @see startMousePolling + * @since 4.7 + */ + void mouseChanged(const QPoint& pos, const QPoint& oldpos, + Qt::MouseButtons buttons, Qt::MouseButtons oldbuttons, + Qt::KeyboardModifiers modifiers, Qt::KeyboardModifiers oldmodifiers); + /** + * Signal emitted when the cursor shape changed. + * You'll likely want to query the current cursor as reaction: xcb_xfixes_get_cursor_image_unchecked + * Connection to this signal is tracked, so if you don't need it anymore, disconnect from it to stop cursor event filtering + */ + void cursorShapeChanged(); + /** + * Receives events registered for using registerPropertyType. + * Use readProperty() to get the property data. + * Note that the property may be already set on the window, so doing the same + * processing from windowAdded() (e.g. simply calling propertyNotify() from it) + * is usually needed. + * @param w The window whose property changed, is @c null if it is a root window property + * @param atom The property + * @since 4.7 + */ + void propertyNotify(KWin::EffectWindow* w, long atom); + + /** + * Signal emitted after the screen geometry changed (e.g. add of a monitor). + * Effects using displayWidth()/displayHeight() to cache information should + * react on this signal and update the caches. + * @param size The new screen size + * @since 4.8 + */ + void screenGeometryChanged(const QSize &size); + + /** + * This signal is emitted when the global + * activity is changed + * @param id id of the new current activity + * @since 4.9 + */ + void currentActivityChanged(const QString &id); + /** + * This signal is emitted when a new activity is added + * @param id id of the new activity + * @since 4.9 + */ + void activityAdded(const QString &id); + /** + * This signal is emitted when the activity + * is removed + * @param id id of the removed activity + * @since 4.9 + */ + void activityRemoved(const QString &id); + /** + * This signal is emitted when the screen got locked or unlocked. + * @param locked @c true if the screen is now locked, @c false if it is now unlocked + * @since 4.11 + */ + void screenLockingChanged(bool locked); + + /** + * This signal is emitted just before the screen locker tries to grab keys and lock the screen + * Effects should release any grabs immediately + * @since 5.17 + */ + void screenAboutToLock(); + + /** + * This signels is emitted when ever the stacking order is change, ie. a window is risen + * or lowered + * @since 4.10 + */ + void stackingOrderChanged(); + /** + * This signal is emitted when the user starts to approach the @p border with the mouse. + * The @p factor describes how far away the mouse is in a relative mean. The values are in + * [0.0, 1.0] with 0.0 being emitted when first entered and on leaving. The value 1.0 means that + * the @p border is reached with the mouse. So the values are well suited for animations. + * The signal is always emitted when the mouse cursor position changes. + * @param border The screen edge which is being approached + * @param factor Value in range [0.0,1.0] to describe how close the mouse is to the border + * @param geometry The geometry of the edge which is being approached + * @since 4.11 + */ + void screenEdgeApproaching(ElectricBorder border, qreal factor, const QRect &geometry); + /** + * Emitted whenever the virtualScreenSize changes. + * @see virtualScreenSize() + * @since 5.0 + */ + void virtualScreenSizeChanged(); + /** + * Emitted whenever the virtualScreenGeometry changes. + * @see virtualScreenGeometry() + * @since 5.0 + */ + void virtualScreenGeometryChanged(); + + /** + * The window @p w gets shown again. The window was previously + * initially shown with windowAdded and hidden with windowHidden. + * + * @see windowHidden + * @see windowAdded + * @since 5.8 + */ + void windowShown(KWin::EffectWindow *w); + + /** + * The window @p w got hidden but not yet closed. + * This can happen when a window is still being used and is supposed to be shown again + * with windowShown. On X11 an example is autohiding panels. On Wayland every + * window first goes through the window hidden state and might get shown again, or might + * get closed the normal way. + * + * @see windowShown + * @see windowClosed + * @since 5.8 + */ + void windowHidden(KWin::EffectWindow *w); + + /** + * This signal gets emitted when the data on EffectWindow @p w for @p role changed. + * + * An Effect can connect to this signal to read the new value and react on it. + * E.g. an Effect which does not operate on windows grabbed by another Effect wants + * to cancel the already scheduled animation if another Effect adds a grab. + * + * @param w The EffectWindow for which the data changed + * @param role The data role which changed + * @see EffectWindow::setData + * @see EffectWindow::data + * @since 5.8.4 + */ + void windowDataChanged(KWin::EffectWindow *w, int role); + + /** + * The xcb connection changed, either a new xcbConnection got created or the existing one + * got destroyed. + * Effects can use this to refetch the properties they want to set. + * + * When the xcbConnection changes also the x11RootWindow becomes invalid. + * @see xcbConnection + * @see x11RootWindow + * @since 5.11 + */ + void xcbConnectionChanged(); + + /** + * This signal is emitted when active fullscreen effect changed. + * + * @see activeFullScreenEffect + * @see setActiveFullScreenEffect + * @since 5.14 + */ + void activeFullScreenEffectChanged(); + + /** + * This signal is emitted when active fullscreen effect changed to being + * set or unset + * + * @see activeFullScreenEffect + * @see setActiveFullScreenEffect + * @since 5.15 + */ + void hasActiveFullScreenEffectChanged(); + + /** + * This signal is emitted when the keep above state of @p w was changed. + * + * @param w The window whose the keep above state was changed. + * @since 5.15 + */ + void windowKeepAboveChanged(KWin::EffectWindow *w); + + /** + * This signal is emitted when the keep below state of @p was changed. + * + * @param w The window whose the keep below state was changed. + * @since 5.15 + */ + void windowKeepBelowChanged(KWin::EffectWindow *w); + + /** + * This signal is emitted when the full screen state of @p w was changed. + * + * @param w The window whose the full screen state was changed. + * @since 5.15 + */ + void windowFullScreenChanged(KWin::EffectWindow *w); + + /** + * This signal is emitted when the session state was changed + * @since 5.18 + */ + void sessionStateChanged(); + +protected: + QVector< EffectPair > loaded_effects; + //QHash< QString, EffectFactory* > effect_factories; + CompositingType compositing_type; +}; + + +/** + * @short Representation of a window used by/for Effect classes. + * + * The purpose is to hide internal data and also to serve as a single + * representation for the case when Client/Unmanaged becomes Deleted. + */ +class KWINEFFECTS_EXPORT EffectWindow : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool alpha READ hasAlpha CONSTANT) + Q_PROPERTY(QRect geometry READ geometry) + Q_PROPERTY(QRect expandedGeometry READ expandedGeometry) + Q_PROPERTY(int height READ height) + Q_PROPERTY(qreal opacity READ opacity) + Q_PROPERTY(QPoint pos READ pos) + Q_PROPERTY(int screen READ screen) + Q_PROPERTY(QSize size READ size) + Q_PROPERTY(int width READ width) + Q_PROPERTY(int x READ x) + Q_PROPERTY(int y READ y) + Q_PROPERTY(int desktop READ desktop) + Q_PROPERTY(bool onAllDesktops READ isOnAllDesktops) + Q_PROPERTY(bool onCurrentDesktop READ isOnCurrentDesktop) + Q_PROPERTY(QRect rect READ rect) + Q_PROPERTY(QString windowClass READ windowClass) + Q_PROPERTY(QString windowRole READ windowRole) + /** + * Returns whether the window is a desktop background window (the one with wallpaper). + * See _NET_WM_WINDOW_TYPE_DESKTOP at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + Q_PROPERTY(bool desktopWindow READ isDesktop) + /** + * Returns whether the window is a dock (i.e. a panel). + * See _NET_WM_WINDOW_TYPE_DOCK at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + Q_PROPERTY(bool dock READ isDock) + /** + * Returns whether the window is a standalone (detached) toolbar window. + * See _NET_WM_WINDOW_TYPE_TOOLBAR at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + Q_PROPERTY(bool toolbar READ isToolbar) + /** + * Returns whether the window is a torn-off menu. + * See _NET_WM_WINDOW_TYPE_MENU at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + Q_PROPERTY(bool menu READ isMenu) + /** + * Returns whether the window is a "normal" window, i.e. an application or any other window + * for which none of the specialized window types fit. + * See _NET_WM_WINDOW_TYPE_NORMAL at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + Q_PROPERTY(bool normalWindow READ isNormalWindow) + /** + * Returns whether the window is a dialog window. + * See _NET_WM_WINDOW_TYPE_DIALOG at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + Q_PROPERTY(bool dialog READ isDialog) + /** + * Returns whether the window is a splashscreen. Note that many (especially older) applications + * do not support marking their splash windows with this type. + * See _NET_WM_WINDOW_TYPE_SPLASH at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + Q_PROPERTY(bool splash READ isSplash) + /** + * Returns whether the window is a utility window, such as a tool window. + * See _NET_WM_WINDOW_TYPE_UTILITY at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + Q_PROPERTY(bool utility READ isUtility) + /** + * Returns whether the window is a dropdown menu (i.e. a popup directly or indirectly open + * from the applications menubar). + * See _NET_WM_WINDOW_TYPE_DROPDOWN_MENU at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + Q_PROPERTY(bool dropdownMenu READ isDropdownMenu) + /** + * Returns whether the window is a popup menu (that is not a torn-off or dropdown menu). + * See _NET_WM_WINDOW_TYPE_POPUP_MENU at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + Q_PROPERTY(bool popupMenu READ isPopupMenu) + /** + * Returns whether the window is a tooltip. + * See _NET_WM_WINDOW_TYPE_TOOLTIP at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + Q_PROPERTY(bool tooltip READ isTooltip) + /** + * Returns whether the window is a window with a notification. + * See _NET_WM_WINDOW_TYPE_NOTIFICATION at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + Q_PROPERTY(bool notification READ isNotification) + /** + * Returns whether the window is a window with a critical notification. + * using the non-standard _KDE_NET_WM_WINDOW_TYPE_CRITICAL_NOTIFICATION + */ + Q_PROPERTY(bool criticalNotification READ isCriticalNotification) + /** + * Returns whether the window is an on screen display window + * using the non-standard _KDE_NET_WM_WINDOW_TYPE_ON_SCREEN_DISPLAY + */ + Q_PROPERTY(bool onScreenDisplay READ isOnScreenDisplay) + /** + * Returns whether the window is a combobox popup. + * See _NET_WM_WINDOW_TYPE_COMBO at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + Q_PROPERTY(bool comboBox READ isComboBox) + /** + * Returns whether the window is a Drag&Drop icon. + * See _NET_WM_WINDOW_TYPE_DND at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + Q_PROPERTY(bool dndIcon READ isDNDIcon) + /** + * Returns the NETWM window type + * See https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + Q_PROPERTY(int windowType READ windowType) + /** + * Whether this EffectWindow is managed by KWin (it has control over its placement and other + * aspects, as opposed to override-redirect windows that are entirely handled by the application). + */ + Q_PROPERTY(bool managed READ isManaged) + /** + * Whether this EffectWindow represents an already deleted window and only kept for the compositor for animations. + */ + Q_PROPERTY(bool deleted READ isDeleted) + /** + * Whether the window has an own shape + */ + Q_PROPERTY(bool shaped READ hasOwnShape) + /** + * The Window's shape + */ + Q_PROPERTY(QRegion shape READ shape) + /** + * The Caption of the window. Read from WM_NAME property together with a suffix for hostname and shortcut. + */ + Q_PROPERTY(QString caption READ caption) + /** + * Whether the window is set to be kept above other windows. + */ + Q_PROPERTY(bool keepAbove READ keepAbove) + /** + * Whether the window is set to be kept below other windows. + */ + Q_PROPERTY(bool keepBelow READ keepBelow) + /** + * Whether the window is minimized. + */ + Q_PROPERTY(bool minimized READ isMinimized WRITE setMinimized) + /** + * Whether the window represents a modal window. + */ + Q_PROPERTY(bool modal READ isModal) + /** + * Whether the window is moveable. Even if it is not moveable, it might be possible to move + * it to another screen. + * @see moveableAcrossScreens + */ + Q_PROPERTY(bool moveable READ isMovable) + /** + * Whether the window can be moved to another screen. + * @see moveable + */ + Q_PROPERTY(bool moveableAcrossScreens READ isMovableAcrossScreens) + /** + * By how much the window wishes to grow/shrink at least. Usually QSize(1,1). + * MAY BE DISOBEYED BY THE WM! It's only for information, do NOT rely on it at all. + */ + Q_PROPERTY(QSize basicUnit READ basicUnit) + /** + * Whether the window is currently being moved by the user. + */ + Q_PROPERTY(bool move READ isUserMove) + /** + * Whether the window is currently being resized by the user. + */ + Q_PROPERTY(bool resize READ isUserResize) + /** + * The optional geometry representing the minimized Client in e.g a taskbar. + * See _NET_WM_ICON_GEOMETRY at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + Q_PROPERTY(QRect iconGeometry READ iconGeometry) + /** + * Returns whether the window is any of special windows types (desktop, dock, splash, ...), + * i.e. window types that usually don't have a window frame and the user does not use window + * management (moving, raising,...) on them. + */ + Q_PROPERTY(bool specialWindow READ isSpecialWindow) + Q_PROPERTY(QIcon icon READ icon) + /** + * Whether the window should be excluded from window switching effects. + */ + Q_PROPERTY(bool skipSwitcher READ isSkipSwitcher) + /** + * Geometry of the actual window contents inside the whole (including decorations) window. + */ + Q_PROPERTY(QRect contentsRect READ contentsRect) + /** + * Geometry of the transparent rect in the decoration. + * May be different from contentsRect if the decoration is extended into the client area. + */ + Q_PROPERTY(QRect decorationInnerRect READ decorationInnerRect) + Q_PROPERTY(bool hasDecoration READ hasDecoration) + Q_PROPERTY(QStringList activities READ activities) + Q_PROPERTY(bool onCurrentActivity READ isOnCurrentActivity) + Q_PROPERTY(bool onAllActivities READ isOnAllActivities) + /** + * Whether the decoration currently uses an alpha channel. + * @since 4.10 + */ + Q_PROPERTY(bool decorationHasAlpha READ decorationHasAlpha) + /** + * Whether the window is currently visible to the user, that is: + *

    + *
  • Not minimized
  • + *
  • On current desktop
  • + *
  • On current activity
  • + *
+ * @since 4.11 + */ + Q_PROPERTY(bool visible READ isVisible) + /** + * Whether the window does not want to be animated on window close. + * In case this property is @c true it is not useful to start an animation on window close. + * The window will not be visible, but the animation hooks are executed. + * @since 5.0 + */ + Q_PROPERTY(bool skipsCloseAnimation READ skipsCloseAnimation) + + /** + * Interface to the corresponding wayland surface. + * relevant only in Wayland, on X11 it will be nullptr + */ + Q_PROPERTY(KWaylandServer::SurfaceInterface *surface READ surface) + + /** + * Whether the window is fullscreen. + * @since 5.6 + */ + Q_PROPERTY(bool fullScreen READ isFullScreen) + + /** + * Whether this client is unresponsive. + * + * When an application failed to react on a ping request in time, it is + * considered unresponsive. This usually indicates that the application froze or crashed. + * + * @since 5.10 + */ + Q_PROPERTY(bool unresponsive READ isUnresponsive) + + /** + * Whether this is a Wayland client. + * @since 5.15 + */ + Q_PROPERTY(bool waylandClient READ isWaylandClient CONSTANT) + + /** + * Whether this is an X11 client. + * @since 5.15 + */ + Q_PROPERTY(bool x11Client READ isX11Client CONSTANT) + + /** + * Whether the window is a popup. + * + * A popup is a window that can be used to implement tooltips, combo box popups, + * popup menus and other similar user interface concepts. + * + * @since 5.15 + */ + Q_PROPERTY(bool popupWindow READ isPopupWindow CONSTANT) + + /** + * KWin internal window. Specific to Wayland platform. + * + * If the EffectWindow does not reference an internal window, this property is @c null. + * @since 5.16 + */ + Q_PROPERTY(QWindow *internalWindow READ internalWindow CONSTANT) + + /** + * Whether this EffectWindow represents the outline. + * + * When compositing is turned on, the outline is an actual window. + * + * @since 5.16 + */ + Q_PROPERTY(bool outline READ isOutline CONSTANT) + + /** + * The PID of the application this window belongs to. + * + * @since 5.18 + */ + Q_PROPERTY(pid_t pid READ pid CONSTANT) + +public: + /** Flags explaining why painting should be disabled */ + enum { + /** Window will not be painted */ + PAINT_DISABLED = 1 << 0, + /** Window will not be painted because it is deleted */ + PAINT_DISABLED_BY_DELETE = 1 << 1, + /** Window will not be painted because of which desktop it's on */ + PAINT_DISABLED_BY_DESKTOP = 1 << 2, + /** Window will not be painted because it is minimized */ + PAINT_DISABLED_BY_MINIMIZE = 1 << 3, + /** Deprecated, tab groups have been removed: Window will not be painted because it is not the active window in a client group */ + PAINT_DISABLED_BY_TAB_GROUP = 1 << 4, + /** Window will not be painted because it's not on the current activity */ + PAINT_DISABLED_BY_ACTIVITY = 1 << 5 + }; + + explicit EffectWindow(QObject *parent = nullptr); + ~EffectWindow() override; + + virtual void enablePainting(int reason) = 0; + virtual void disablePainting(int reason) = 0; + virtual bool isPaintingEnabled() = 0; + Q_SCRIPTABLE virtual void addRepaint(const QRect &r) = 0; + Q_SCRIPTABLE virtual void addRepaint(int x, int y, int w, int h) = 0; + Q_SCRIPTABLE virtual void addRepaintFull() = 0; + Q_SCRIPTABLE virtual void addLayerRepaint(const QRect &r) = 0; + Q_SCRIPTABLE virtual void addLayerRepaint(int x, int y, int w, int h) = 0; + + virtual void refWindow() = 0; + virtual void unrefWindow() = 0; + + virtual bool isDeleted() const = 0; + + virtual bool isMinimized() const = 0; + virtual double opacity() const = 0; + virtual bool hasAlpha() const = 0; + + bool isOnCurrentActivity() const; + Q_SCRIPTABLE bool isOnActivity(const QString &id) const; + bool isOnAllActivities() const; + virtual QStringList activities() const = 0; + + Q_SCRIPTABLE bool isOnDesktop(int d) const; + bool isOnCurrentDesktop() const; + bool isOnAllDesktops() const; + /** + * The desktop this window is in. This makes sense only on X11 + * where desktops are mutually exclusive, on Wayland it's the last + * desktop the window has been added to. + * use desktops() instead. + * @see desktops() + * @deprecated + */ +#ifndef KWIN_NO_DEPRECATED + virtual int KWIN_DEPRECATED desktop() const = 0; // prefer isOnXXX() +#endif + /** + * All the desktops by number that the window is in. On X11 this list will always have + * a length of 1, on Wayland can be any subset. + * If the list is empty it means the window is on all desktops + */ + virtual QVector desktops() const = 0; + + virtual int x() const = 0; + virtual int y() const = 0; + virtual int width() const = 0; + virtual int height() const = 0; + /** + * By how much the window wishes to grow/shrink at least. Usually QSize(1,1). + * MAY BE DISOBEYED BY THE WM! It's only for information, do NOT rely on it at all. + */ + virtual QSize basicUnit() const = 0; + /** + * @deprecated Use frameGeometry() instead. + */ + virtual QRect geometry() const = 0; + /** + * Returns the geometry of the window excluding server-side and client-side + * drop-shadows. + * + * @since 5.18 + */ + virtual QRect frameGeometry() const = 0; + /** + * Returns the geometry of the pixmap or buffer attached to this window. + * + * For X11 clients, this method returns server-side geometry of the Toplevel. + * + * For Wayland clients, this method returns rectangle that the main surface + * occupies on the screen, in global screen coordinates. + * + * @since 5.18 + */ + virtual QRect bufferGeometry() const = 0; + /** + * Geometry of the window including decoration and potentially shadows. + * May be different from geometry() if the window has a shadow. + * @since 4.9 + */ + virtual QRect expandedGeometry() const = 0; + virtual QRegion shape() const = 0; + virtual int screen() const = 0; + /** @internal Do not use */ + virtual bool hasOwnShape() const = 0; // only for shadow effect, for now + virtual QPoint pos() const = 0; + virtual QSize size() const = 0; + virtual QRect rect() const = 0; + virtual bool isMovable() const = 0; + virtual bool isMovableAcrossScreens() const = 0; + virtual bool isUserMove() const = 0; + virtual bool isUserResize() const = 0; + virtual QRect iconGeometry() const = 0; + + /** + * Geometry of the actual window contents inside the whole (including decorations) window. + */ + virtual QRect contentsRect() const = 0; + /** + * Geometry of the transparent rect in the decoration. + * May be different from contentsRect() if the decoration is extended into the client area. + * @since 4.5 + */ + virtual QRect decorationInnerRect() const = 0; + bool hasDecoration() const; + virtual bool decorationHasAlpha() const = 0; + virtual QByteArray readProperty(long atom, long type, int format) const = 0; + virtual void deleteProperty(long atom) const = 0; + + virtual QString caption() const = 0; + virtual QIcon icon() const = 0; + virtual QString windowClass() const = 0; + virtual QString windowRole() const = 0; + virtual const EffectWindowGroup* group() const = 0; + + /** + * Returns whether the window is a desktop background window (the one with wallpaper). + * See _NET_WM_WINDOW_TYPE_DESKTOP at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + virtual bool isDesktop() const = 0; + /** + * Returns whether the window is a dock (i.e. a panel). + * See _NET_WM_WINDOW_TYPE_DOCK at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + virtual bool isDock() const = 0; + /** + * Returns whether the window is a standalone (detached) toolbar window. + * See _NET_WM_WINDOW_TYPE_TOOLBAR at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + virtual bool isToolbar() const = 0; + /** + * Returns whether the window is a torn-off menu. + * See _NET_WM_WINDOW_TYPE_MENU at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + virtual bool isMenu() const = 0; + /** + * Returns whether the window is a "normal" window, i.e. an application or any other window + * for which none of the specialized window types fit. + * See _NET_WM_WINDOW_TYPE_NORMAL at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + virtual bool isNormalWindow() const = 0; // normal as in 'NET::Normal or NET::Unknown non-transient' + /** + * Returns whether the window is any of special windows types (desktop, dock, splash, ...), + * i.e. window types that usually don't have a window frame and the user does not use window + * management (moving, raising,...) on them. + */ + virtual bool isSpecialWindow() const = 0; + /** + * Returns whether the window is a dialog window. + * See _NET_WM_WINDOW_TYPE_DIALOG at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + virtual bool isDialog() const = 0; + /** + * Returns whether the window is a splashscreen. Note that many (especially older) applications + * do not support marking their splash windows with this type. + * See _NET_WM_WINDOW_TYPE_SPLASH at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + virtual bool isSplash() const = 0; + /** + * Returns whether the window is a utility window, such as a tool window. + * See _NET_WM_WINDOW_TYPE_UTILITY at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + virtual bool isUtility() const = 0; + /** + * Returns whether the window is a dropdown menu (i.e. a popup directly or indirectly open + * from the applications menubar). + * See _NET_WM_WINDOW_TYPE_DROPDOWN_MENU at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + virtual bool isDropdownMenu() const = 0; + /** + * Returns whether the window is a popup menu (that is not a torn-off or dropdown menu). + * See _NET_WM_WINDOW_TYPE_POPUP_MENU at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + virtual bool isPopupMenu() const = 0; // a context popup, not dropdown, not torn-off + /** + * Returns whether the window is a tooltip. + * See _NET_WM_WINDOW_TYPE_TOOLTIP at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + virtual bool isTooltip() const = 0; + /** + * Returns whether the window is a window with a notification. + * See _NET_WM_WINDOW_TYPE_NOTIFICATION at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + virtual bool isNotification() const = 0; + /** + * Returns whether the window is a window with a critical notification. + * using the non-standard _KDE_NET_WM_WINDOW_TYPE_CRITICAL_NOTIFICATION + */ + virtual bool isCriticalNotification() const = 0; + /** + * Returns whether the window is an on screen display window + * using the non-standard _KDE_NET_WM_WINDOW_TYPE_ON_SCREEN_DISPLAY + */ + virtual bool isOnScreenDisplay() const = 0; + /** + * Returns whether the window is a combobox popup. + * See _NET_WM_WINDOW_TYPE_COMBO at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + virtual bool isComboBox() const = 0; + /** + * Returns whether the window is a Drag&Drop icon. + * See _NET_WM_WINDOW_TYPE_DND at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + virtual bool isDNDIcon() const = 0; + /** + * Returns the NETWM window type + * See https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + virtual NET::WindowType windowType() const = 0; + /** + * Returns whether the window is managed by KWin (it has control over its placement and other + * aspects, as opposed to override-redirect windows that are entirely handled by the application). + */ + virtual bool isManaged() const = 0; // whether it's managed or override-redirect + /** + * Returns whether or not the window can accept keyboard focus. + */ + virtual bool acceptsFocus() const = 0; + /** + * Returns whether or not the window is kept above all other windows. + */ + virtual bool keepAbove() const = 0; + /** + * Returns whether the window is kept below all other windows. + */ + virtual bool keepBelow() const = 0; + + virtual bool isModal() const = 0; + Q_SCRIPTABLE virtual KWin::EffectWindow* findModal() = 0; + Q_SCRIPTABLE virtual KWin::EffectWindow* transientFor() = 0; + Q_SCRIPTABLE virtual QList mainWindows() const = 0; + + /** + * Returns whether the window should be excluded from window switching effects. + * @since 4.5 + */ + virtual bool isSkipSwitcher() const = 0; + + /** + * Returns the unmodified window quad list. Can also be used to force rebuilding. + */ + virtual WindowQuadList buildQuads(bool force = false) const = 0; + + void setMinimized(bool minimize); + virtual void minimize() = 0; + virtual void unminimize() = 0; + Q_SCRIPTABLE virtual void closeWindow() = 0; + + /// deprecated + virtual bool isCurrentTab() const = 0; + + /** + * @since 4.11 + */ + bool isVisible() const; + + /** + * @since 5.0 + */ + virtual bool skipsCloseAnimation() const = 0; + + /** + * @since 5.5 + */ + virtual KWaylandServer::SurfaceInterface *surface() const = 0; + + /** + * @since 5.6 + */ + virtual bool isFullScreen() const = 0; + + /** + * @since 5.10 + */ + virtual bool isUnresponsive() const = 0; + + /** + * @since 5.15 + */ + virtual bool isWaylandClient() const = 0; + + /** + * @since 5.15 + */ + virtual bool isX11Client() const = 0; + + /** + * @since 5.15 + */ + virtual bool isPopupWindow() const = 0; + + /** + * @since 5.16 + */ + virtual QWindow *internalWindow() const = 0; + + /** + * @since 5.16 + */ + virtual bool isOutline() const = 0; + + /** + * @since 5.18 + */ + virtual pid_t pid() const = 0; + + /** + * Can be used to by effects to store arbitrary data in the EffectWindow. + * + * Invoking this method will emit the signal EffectsHandler::windowDataChanged. + * @see EffectsHandler::windowDataChanged + */ + Q_SCRIPTABLE virtual void setData(int role, const QVariant &data) = 0; + Q_SCRIPTABLE virtual QVariant data(int role) const = 0; + + /** + * @brief References the previous window pixmap to prevent discarding. + * + * This method allows to reference the previous window pixmap in case that a window changed + * its size, which requires a new window pixmap. By referencing the previous (and then outdated) + * window pixmap an effect can for example cross fade the current window pixmap with the previous + * one. This allows for smoother transitions for window geometry changes. + * + * If an effect calls this method on a window it also needs to call unreferencePreviousWindowPixmap + * once it does no longer need the previous window pixmap. + * + * Note: the window pixmap is not kept forever even when referenced. If the geometry changes again, so that + * a new window pixmap is created, the previous window pixmap will be exchanged with the current one. This + * means it's still possible to have rendering glitches. An effect is supposed to track for itself the changes + * to the window's geometry and decide how the transition should continue in such a situation. + * + * @see unreferencePreviousWindowPixmap + * @since 4.11 + */ + virtual void referencePreviousWindowPixmap() = 0; + /** + * @brief Unreferences the previous window pixmap. Only relevant after referencePreviousWindowPixmap had + * been called. + * + * @see referencePreviousWindowPixmap + * @since 4.11 + */ + virtual void unreferencePreviousWindowPixmap() = 0; + +private: + class Private; + QScopedPointer d; +}; + +class KWINEFFECTS_EXPORT EffectWindowGroup +{ +public: + virtual ~EffectWindowGroup(); + virtual EffectWindowList members() const = 0; +}; + + +struct GLVertex2D +{ + QVector2D position; + QVector2D texcoord; +}; + +struct GLVertex3D +{ + QVector3D position; + QVector2D texcoord; +}; + + +/** + * @short Vertex class + * + * A vertex is one position in a window. WindowQuad consists of four WindowVertex objects + * and represents one part of a window. + */ +class KWINEFFECTS_EXPORT WindowVertex +{ +public: + WindowVertex(); + WindowVertex(const QPointF &position, const QPointF &textureCoordinate); + WindowVertex(double x, double y, double tx, double ty); + + double x() const { return px; } + double y() const { return py; } + double u() const { return tx; } + double v() const { return ty; } + double originalX() const { return ox; } + double originalY() const { return oy; } + double textureX() const { return tx; } + double textureY() const { return ty; } + void move(double x, double y); + void setX(double x); + void setY(double y); + +private: + friend class WindowQuad; + friend class WindowQuadList; + double px, py; // position + double ox, oy; // origional position + double tx, ty; // texture coords +}; + +/** + * @short Class representing one area of a window. + * + * WindowQuads consists of four WindowVertex objects and represents one part of a window. + */ +// NOTE: This class expects the (original) vertices to be in the clockwise order starting from topleft. +class KWINEFFECTS_EXPORT WindowQuad +{ +public: + explicit WindowQuad(WindowQuadType type, int id = -1); + WindowQuad makeSubQuad(double x1, double y1, double x2, double y2) const; + WindowVertex& operator[](int index); + const WindowVertex& operator[](int index) const; + WindowQuadType type() const; + void setUVAxisSwapped(bool value) { uvSwapped = value; } + bool uvAxisSwapped() const { return uvSwapped; } + int id() const; + bool decoration() const; + bool effect() const; + double left() const; + double right() const; + double top() const; + double bottom() const; + double originalLeft() const; + double originalRight() const; + double originalTop() const; + double originalBottom() const; + bool smoothNeeded() const; + bool isTransformed() const; +private: + friend class WindowQuadList; + WindowVertex verts[ 4 ]; + WindowQuadType quadType; // 0 - contents, 1 - decoration + bool uvSwapped; + int quadID; +}; + +class KWINEFFECTS_EXPORT WindowQuadList + : public QVector +{ +public: + WindowQuadList splitAtX(double x) const; + WindowQuadList splitAtY(double y) const; + WindowQuadList makeGrid(int maxquadsize) const; + WindowQuadList makeRegularGrid(int xSubdivisions, int ySubdivisions) const; + WindowQuadList select(WindowQuadType type) const; + WindowQuadList filterOut(WindowQuadType type) const; + bool smoothNeeded() const; + void makeInterleavedArrays(unsigned int type, GLVertex2D *vertices, const QMatrix4x4 &matrix) const; + void makeArrays(float** vertices, float** texcoords, const QSizeF &size, bool yInverted) const; + bool isTransformed() const; +}; + +class KWINEFFECTS_EXPORT WindowPrePaintData +{ +public: + int mask; + /** + * Region that will be painted, in screen coordinates. + */ + QRegion paint; + /** + * The clip region will be subtracted from paint region of following windows. + * I.e. window will definitely cover it's clip region + */ + QRegion clip; + WindowQuadList quads; + /** + * Simple helper that sets data to say the window will be painted as non-opaque. + * Takes also care of changing the regions. + */ + void setTranslucent(); + /** + * Helper to mark that this window will be transformed + */ + void setTransformed(); +}; + +class KWINEFFECTS_EXPORT PaintData +{ +public: + virtual ~PaintData(); + /** + * @returns scale factor in X direction. + * @since 4.10 + */ + qreal xScale() const; + /** + * @returns scale factor in Y direction. + * @since 4.10 + */ + qreal yScale() const; + /** + * @returns scale factor in Z direction. + * @since 4.10 + */ + qreal zScale() const; + /** + * Sets the scale factor in X direction to @p scale + * @param scale The scale factor in X direction + * @since 4.10 + */ + void setXScale(qreal scale); + /** + * Sets the scale factor in Y direction to @p scale + * @param scale The scale factor in Y direction + * @since 4.10 + */ + void setYScale(qreal scale); + /** + * Sets the scale factor in Z direction to @p scale + * @param scale The scale factor in Z direction + * @since 4.10 + */ + void setZScale(qreal scale); + /** + * Sets the scale factor in X and Y direction. + * @param scale The scale factor for X and Y direction + * @since 4.10 + */ + void setScale(const QVector2D &scale); + /** + * Sets the scale factor in X, Y and Z direction + * @param scale The scale factor for X, Y and Z direction + * @since 4.10 + */ + void setScale(const QVector3D &scale); + const QGraphicsScale &scale() const; + const QVector3D &translation() const; + /** + * @returns the translation in X direction. + * @since 4.10 + */ + qreal xTranslation() const; + /** + * @returns the translation in Y direction. + * @since 4.10 + */ + qreal yTranslation() const; + /** + * @returns the translation in Z direction. + * @since 4.10 + */ + qreal zTranslation() const; + /** + * Sets the translation in X direction to @p translate. + * @since 4.10 + */ + void setXTranslation(qreal translate); + /** + * Sets the translation in Y direction to @p translate. + * @since 4.10 + */ + void setYTranslation(qreal translate); + /** + * Sets the translation in Z direction to @p translate. + * @since 4.10 + */ + void setZTranslation(qreal translate); + /** + * Performs a translation by adding the values component wise. + * @param x Translation in X direction + * @param y Translation in Y direction + * @param z Translation in Z direction + * @since 4.10 + */ + void translate(qreal x, qreal y = 0.0, qreal z = 0.0); + /** + * Performs a translation by adding the values component wise. + * Overloaded method for convenience. + * @param translate The translation + * @since 4.10 + */ + void translate(const QVector3D &translate); + + /** + * Sets the rotation angle. + * @param angle The new rotation angle. + * @since 4.10 + * @see rotationAngle() + */ + void setRotationAngle(qreal angle); + /** + * Returns the rotation angle. + * Initially 0.0. + * @returns The current rotation angle. + * @since 4.10 + * @see setRotationAngle + */ + qreal rotationAngle() const; + /** + * Sets the rotation origin. + * @param origin The new rotation origin. + * @since 4.10 + * @see rotationOrigin() + */ + void setRotationOrigin(const QVector3D &origin); + /** + * Returns the rotation origin. That is the point in space which is fixed during the rotation. + * Initially this is 0/0/0. + * @returns The rotation's origin + * @since 4.10 + * @see setRotationOrigin() + */ + QVector3D rotationOrigin() const; + /** + * Sets the rotation axis. + * Set a component to 1.0 to rotate around this axis and to 0.0 to disable rotation around the + * axis. + * @param axis A vector holding information on which axis to rotate + * @since 4.10 + * @see rotationAxis() + */ + void setRotationAxis(const QVector3D &axis); + /** + * Sets the rotation axis. + * Overloaded method for convenience. + * @param axis The axis around which should be rotated. + * @since 4.10 + * @see rotationAxis() + */ + void setRotationAxis(Qt::Axis axis); + /** + * The current rotation axis. + * By default the rotation is (0/0/1) which means a rotation around the z axis. + * @returns The current rotation axis. + * @since 4.10 + * @see setRotationAxis + */ + QVector3D rotationAxis() const; + +protected: + PaintData(); + PaintData(const PaintData &other); + +private: + PaintDataPrivate * const d; +}; + +class KWINEFFECTS_EXPORT WindowPaintData : public PaintData +{ +public: + explicit WindowPaintData(EffectWindow* w); + explicit WindowPaintData(EffectWindow* w, const QMatrix4x4 &screenProjectionMatrix); + WindowPaintData(const WindowPaintData &other); + ~WindowPaintData() override; + /** + * Scales the window by @p scale factor. + * Multiplies all three components by the given factor. + * @since 4.10 + */ + WindowPaintData& operator*=(qreal scale); + /** + * Scales the window by @p scale factor. + * Performs a component wise multiplication on x and y components. + * @since 4.10 + */ + WindowPaintData& operator*=(const QVector2D &scale); + /** + * Scales the window by @p scale factor. + * Performs a component wise multiplication. + * @since 4.10 + */ + WindowPaintData& operator*=(const QVector3D &scale); + /** + * Translates the window by the given @p translation and returns a reference to the ScreenPaintData. + * @since 4.10 + */ + WindowPaintData& operator+=(const QPointF &translation); + /** + * Translates the window by the given @p translation and returns a reference to the ScreenPaintData. + * Overloaded method for convenience. + * @since 4.10 + */ + WindowPaintData& operator+=(const QPoint &translation); + /** + * Translates the window by the given @p translation and returns a reference to the ScreenPaintData. + * Overloaded method for convenience. + * @since 4.10 + */ + WindowPaintData& operator+=(const QVector2D &translation); + /** + * Translates the window by the given @p translation and returns a reference to the ScreenPaintData. + * Overloaded method for convenience. + * @since 4.10 + */ + WindowPaintData& operator+=(const QVector3D &translation); + /** + * Window opacity, in range 0 = transparent to 1 = fully opaque + * @see setOpacity + * @since 4.10 + */ + qreal opacity() const; + /** + * Sets the window opacity to the new @p opacity. + * If you want to modify the existing opacity level consider using multiplyOpacity. + * @param opacity The new opacity level + * @since 4.10 + */ + void setOpacity(qreal opacity); + /** + * Multiplies the current opacity with the @p factor. + * @param factor Factor with which the opacity should be multiplied + * @return New opacity level + * @since 4.10 + */ + qreal multiplyOpacity(qreal factor); + /** + * Saturation of the window, in range [0; 1] + * 1 means that the window is unchanged, 0 means that it's completely + * unsaturated (greyscale). 0.5 would make the colors less intense, + * but not completely grey + * Use EffectsHandler::saturationSupported() to find out whether saturation + * is supported by the system, otherwise this value has no effect. + * @return The current saturation + * @see setSaturation() + * @since 4.10 + */ + qreal saturation() const; + /** + * Sets the window saturation level to @p saturation. + * If you want to modify the existing saturation level consider using multiplySaturation. + * @param saturation The new saturation level + * @since 4.10 + */ + void setSaturation(qreal saturation) const; + /** + * Multiplies the current saturation with @p factor. + * @param factor with which the saturation should be multiplied + * @return New saturation level + * @since 4.10 + */ + qreal multiplySaturation(qreal factor); + /** + * Brightness of the window, in range [0; 1] + * 1 means that the window is unchanged, 0 means that it's completely + * black. 0.5 would make it 50% darker than usual + */ + qreal brightness() const; + /** + * Sets the window brightness level to @p brightness. + * If you want to modify the existing brightness level consider using multiplyBrightness. + * @param brightness The new brightness level + */ + void setBrightness(qreal brightness); + /** + * Multiplies the current brightness level with @p factor. + * @param factor with which the brightness should be multiplied. + * @return New brightness level + * @since 4.10 + */ + qreal multiplyBrightness(qreal factor); + /** + * The screen number for which the painting should be done. + * This affects color correction (different screens may need different + * color correction lookup tables because they have different ICC profiles). + * @return screen for which painting should be done + */ + int screen() const; + /** + * @param screen New screen number + * A value less than 0 will indicate that a default profile should be done. + */ + void setScreen(int screen) const; + /** + * @brief Sets the cross fading @p factor to fade over with previously sized window. + * If @c 1.0 only the current window is used, if @c 0.0 only the previous window is used. + * + * By default only the current window is used. This factor can only make any visual difference + * if the previous window get referenced. + * + * @param factor The cross fade factor between @c 0.0 (previous window) and @c 1.0 (current window) + * @see crossFadeProgress + */ + void setCrossFadeProgress(qreal factor); + /** + * @see setCrossFadeProgress + */ + qreal crossFadeProgress() const; + + /** + * Sets the projection matrix that will be used when painting the window. + * + * The default projection matrix can be overridden by setting this matrix + * to a non-identity matrix. + */ + void setProjectionMatrix(const QMatrix4x4 &matrix); + + /** + * Returns the current projection matrix. + * + * The default value for this matrix is the identity matrix. + */ + QMatrix4x4 projectionMatrix() const; + + /** + * Returns a reference to the projection matrix. + */ + QMatrix4x4 &rprojectionMatrix(); + + /** + * Sets the model-view matrix that will be used when painting the window. + * + * The default model-view matrix can be overridden by setting this matrix + * to a non-identity matrix. + */ + void setModelViewMatrix(const QMatrix4x4 &matrix); + + /** + * Returns the current model-view matrix. + * + * The default value for this matrix is the identity matrix. + */ + QMatrix4x4 modelViewMatrix() const; + + /** + * Returns a reference to the model-view matrix. + */ + QMatrix4x4 &rmodelViewMatrix(); + + /** + * Returns The projection matrix as used by the current screen painting pass + * including screen transformations. + * + * @since 5.6 + */ + QMatrix4x4 screenProjectionMatrix() const; + + WindowQuadList quads; + + /** + * Shader to be used for rendering, if any. + */ + GLShader* shader; + +private: + WindowPaintDataPrivate * const d; +}; + +class KWINEFFECTS_EXPORT ScreenPaintData : public PaintData +{ +public: + ScreenPaintData(); + ScreenPaintData(const QMatrix4x4 &projectionMatrix, const QRect &outputGeometry = QRect(), const qreal screenScale = 1.0); + ScreenPaintData(const ScreenPaintData &other); + ~ScreenPaintData() override; + /** + * Scales the screen by @p scale factor. + * Multiplies all three components by the given factor. + * @since 4.10 + */ + ScreenPaintData& operator*=(qreal scale); + /** + * Scales the screen by @p scale factor. + * Performs a component wise multiplication on x and y components. + * @since 4.10 + */ + ScreenPaintData& operator*=(const QVector2D &scale); + /** + * Scales the screen by @p scale factor. + * Performs a component wise multiplication. + * @since 4.10 + */ + ScreenPaintData& operator*=(const QVector3D &scale); + /** + * Translates the screen by the given @p translation and returns a reference to the ScreenPaintData. + * @since 4.10 + */ + ScreenPaintData& operator+=(const QPointF &translation); + /** + * Translates the screen by the given @p translation and returns a reference to the ScreenPaintData. + * Overloaded method for convenience. + * @since 4.10 + */ + ScreenPaintData& operator+=(const QPoint &translation); + /** + * Translates the screen by the given @p translation and returns a reference to the ScreenPaintData. + * Overloaded method for convenience. + * @since 4.10 + */ + ScreenPaintData& operator+=(const QVector2D &translation); + /** + * Translates the screen by the given @p translation and returns a reference to the ScreenPaintData. + * Overloaded method for convenience. + * @since 4.10 + */ + ScreenPaintData& operator+=(const QVector3D &translation); + ScreenPaintData& operator=(const ScreenPaintData &rhs); + + /** + * The projection matrix used by the scene for the current rendering pass. + * On non-OpenGL compositors it's set to Identity matrix. + * @since 5.6 + */ + QMatrix4x4 projectionMatrix() const; + + /** + * The geometry of the currently rendered output. + * Only set for per-output rendering (e.g. Wayland). + * + * This geometry can be used as a hint about the native window the OpenGL context + * is bound. OpenGL calls need to be translated to this geometry. + * @since 5.9 + */ + QRect outputGeometry() const; + + /** + * The scale factor for the output + * + * @since 5.19 + */ + qreal screenScale() const; +private: + class Private; + QScopedPointer d; +}; + +class KWINEFFECTS_EXPORT ScreenPrePaintData +{ +public: + int mask; + QRegion paint; +}; + +/** + * @short Helper class for restricting painting area only to allowed area. + * + * This helper class helps specifying areas that should be painted, clipping + * out the rest. The simplest usage is creating an object on the stack + * and giving it the area that is allowed to be painted to. When the object + * is destroyed, the restriction will be removed. + * Note that all painting code must use paintArea() to actually perform the clipping. + */ +class KWINEFFECTS_EXPORT PaintClipper +{ +public: + /** + * Calls push(). + */ + explicit PaintClipper(const QRegion& allowed_area); + /** + * Calls pop(). + */ + ~PaintClipper(); + /** + * Allows painting only in the given area. When areas have been already + * specified, painting is allowed only in the intersection of all areas. + */ + static void push(const QRegion& allowed_area); + /** + * Removes the given area. It must match the top item in the stack. + */ + static void pop(const QRegion& allowed_area); + /** + * Returns true if any clipping should be performed. + */ + static bool clip(); + /** + * If clip() returns true, this function gives the resulting area in which + * painting is allowed. It is usually simpler to use the helper Iterator class. + */ + static QRegion paintArea(); + /** + * Helper class to perform the clipped painting. The usage is: + * @code + * for ( PaintClipper::Iterator iterator; + * !iterator.isDone(); + * iterator.next()) + * { // do the painting, possibly use iterator.boundingRect() + * } + * @endcode + */ + class KWINEFFECTS_EXPORT Iterator + { + public: + Iterator(); + ~Iterator(); + bool isDone(); + void next(); + QRect boundingRect() const; + private: + struct Data; + Data* data; + }; +private: + QRegion area; + static QStack< QRegion >* areas; +}; + +/** + * @internal + */ +template +class KWINEFFECTS_EXPORT Motion +{ +public: + /** + * Creates a new motion object. "Strength" is the amount of + * acceleration that is applied to the object when the target + * changes and "smoothness" relates to how fast the object + * can change its direction and speed. + */ + explicit Motion(T initial, double strength, double smoothness); + /** + * Creates an exact copy of another motion object, including + * position, target and velocity. + */ + Motion(const Motion &other); + ~Motion(); + + inline T value() const { + return m_value; + } + inline void setValue(const T value) { + m_value = value; + } + inline T target() const { + return m_target; + } + inline void setTarget(const T target) { + m_start = m_value; + m_target = target; + } + inline T velocity() const { + return m_velocity; + } + inline void setVelocity(const T velocity) { + m_velocity = velocity; + } + + inline double strength() const { + return m_strength; + } + inline void setStrength(const double strength) { + m_strength = strength; + } + inline double smoothness() const { + return m_smoothness; + } + inline void setSmoothness(const double smoothness) { + m_smoothness = smoothness; + } + inline T startValue() { + return m_start; + } + + /** + * The distance between the current position and the target. + */ + inline T distance() const { + return m_target - m_value; + } + + /** + * Calculates the new position if not at the target. Called + * once per frame only. + */ + void calculate(const int msec); + /** + * Place the object on top of the target immediately, + * bypassing all movement calculation. + */ + void finish(); + +private: + T m_value; + T m_start; + T m_target; + T m_velocity; + double m_strength; + double m_smoothness; +}; + +/** + * @short A single 1D motion dynamics object. + * + * This class represents a single object that can be moved around a + * 1D space. Although it can be used directly by itself it is + * recommended to use a motion manager instead. + */ +class KWINEFFECTS_EXPORT Motion1D : public Motion +{ +public: + explicit Motion1D(double initial = 0.0, double strength = 0.08, double smoothness = 4.0); + Motion1D(const Motion1D &other); + ~Motion1D(); +}; + +/** + * @short A single 2D motion dynamics object. + * + * This class represents a single object that can be moved around a + * 2D space. Although it can be used directly by itself it is + * recommended to use a motion manager instead. + */ +class KWINEFFECTS_EXPORT Motion2D : public Motion +{ +public: + explicit Motion2D(QPointF initial = QPointF(), double strength = 0.08, double smoothness = 4.0); + Motion2D(const Motion2D &other); + ~Motion2D(); +}; + +/** + * @short Helper class for motion dynamics in KWin effects. + * + * This motion manager class is intended to help KWin effect authors + * move windows across the screen smoothly and naturally. Once + * windows are registered by the manager the effect can issue move + * commands with the moveWindow() methods. The position of any + * managed window can be determined in realtime by the + * transformedGeometry() method. As the manager knows if any windows + * are moving at any given time it can also be used as a notifier as + * to see whether the effect is active or not. + */ +class KWINEFFECTS_EXPORT WindowMotionManager +{ +public: + /** + * Creates a new window manager object. + */ + explicit WindowMotionManager(bool useGlobalAnimationModifier = true); + ~WindowMotionManager(); + + /** + * Register a window for managing. + */ + void manage(EffectWindow *w); + /** + * Register a list of windows for managing. + */ + inline void manage(const EffectWindowList &list) { + for (int i = 0; i < list.size(); i++) + manage(list.at(i)); + } + /** + * Deregister a window. All transformations applied to the + * window will be permanently removed and cannot be recovered. + */ + void unmanage(EffectWindow *w); + /** + * Deregister all windows, returning the manager to its + * originally initiated state. + */ + void unmanageAll(); + /** + * Determine the new positions for windows that have not + * reached their target. Called once per frame, usually in + * prePaintScreen(). Remember to set the + * Effect::PAINT_SCREEN_WITH_TRANSFORMED_WINDOWS flag. + */ + void calculate(int time); + /** + * Modify a registered window's paint data to make it appear + * at its real location on the screen. Usually called in + * paintWindow(). Remember to flag the window as having been + * transformed in prePaintWindow() by calling + * WindowPrePaintData::setTransformed() + */ + void apply(EffectWindow *w, WindowPaintData &data); + /** + * Set all motion targets and values back to where the + * windows were before transformations. The same as + * unmanaging then remanaging all windows. + */ + void reset(); + /** + * Resets the motion target and current value of a single + * window. + */ + void reset(EffectWindow *w); + + /** + * Ask the manager to move the window to the target position + * with the specified scale. If `yScale` is not provided or + * set to 0.0, `scale` will be used as the scale in the + * vertical direction as well as in the horizontal direction. + */ + void moveWindow(EffectWindow *w, QPoint target, double scale = 1.0, double yScale = 0.0); + /** + * This is an overloaded method, provided for convenience. + * + * Ask the manager to move the window to the target rectangle. + * Automatically determines scale. + */ + inline void moveWindow(EffectWindow *w, QRect target) { + // TODO: Scale might be slightly different in the comparison due to rounding + moveWindow(w, target.topLeft(), + target.width() / double(w->width()), target.height() / double(w->height())); + } + + /** + * Retrieve the current tranformed geometry of a registered + * window. + */ + QRectF transformedGeometry(EffectWindow *w) const; + /** + * Sets the current transformed geometry of a registered window to the given geometry. + * @see transformedGeometry + * @since 4.5 + */ + void setTransformedGeometry(EffectWindow *w, const QRectF &geometry); + /** + * Retrieve the current target geometry of a registered + * window. + */ + QRectF targetGeometry(EffectWindow *w) const; + /** + * Return the window that has its transformed geometry under + * the specified point. It is recommended to use the stacking + * order as it's what the user sees, but it is slightly + * slower to process. + */ + EffectWindow* windowAtPoint(QPoint point, bool useStackingOrder = true) const; + + /** + * Return a list of all currently registered windows. + */ + inline EffectWindowList managedWindows() const { + return m_managedWindows.keys(); + } + /** + * Returns whether or not a specified window is being managed + * by this manager object. + */ + inline bool isManaging(EffectWindow *w) const { + return m_managedWindows.contains(w); + } + /** + * Returns whether or not this manager object is actually + * managing any windows or not. + */ + inline bool managingWindows() const { + return !m_managedWindows.empty(); + } + /** + * Returns whether all windows have reached their targets yet + * or not. Can be used to see if an effect should be + * processed and displayed or not. + */ + inline bool areWindowsMoving() const { + return !m_movingWindowsSet.isEmpty(); + } + /** + * Returns whether a window has reached its targets yet + * or not. + */ + inline bool isWindowMoving(EffectWindow *w) const { + return m_movingWindowsSet.contains(w); + } + +private: + bool m_useGlobalAnimationModifier; + struct WindowMotion { + // TODO: Rotation, etc? + Motion2D translation; // Absolute position + Motion2D scale; // xScale and yScale + }; + QHash m_managedWindows; + QSet m_movingWindowsSet; +}; + +/** + * @short Helper class for displaying text and icons in frames. + * + * Paints text and/or and icon with an optional frame around them. The + * available frames includes one that follows the default Plasma theme and + * another that doesn't. + * It is recommended to use this class whenever displaying text. + */ +class KWINEFFECTS_EXPORT EffectFrame +{ +public: + EffectFrame(); + virtual ~EffectFrame(); + + /** + * Delete any existing textures to free up graphics memory. They will + * be automatically recreated the next time they are required. + */ + virtual void free() = 0; + + /** + * Render the frame. + */ + virtual void render(const QRegion ®ion = infiniteRegion(), double opacity = 1.0, double frameOpacity = 1.0) = 0; + + virtual void setPosition(const QPoint& point) = 0; + /** + * Set the text alignment for static frames and the position alignment + * for non-static. + */ + virtual void setAlignment(Qt::Alignment alignment) = 0; + virtual Qt::Alignment alignment() const = 0; + virtual void setGeometry(const QRect& geometry, bool force = false) = 0; + virtual const QRect& geometry() const = 0; + + virtual void setText(const QString& text) = 0; + virtual const QString& text() const = 0; + virtual void setFont(const QFont& font) = 0; + virtual const QFont& font() const = 0; + /** + * Set the icon that will appear on the left-hand size of the frame. + */ + virtual void setIcon(const QIcon& icon) = 0; + virtual const QIcon& icon() const = 0; + virtual void setIconSize(const QSize& size) = 0; + virtual const QSize& iconSize() const = 0; + + /** + * Sets the geometry of a selection. + * To remove the selection set a null rect. + * @param selection The geometry of the selection in screen coordinates. + */ + virtual void setSelection(const QRect& selection) = 0; + + /** + * @param shader The GLShader for rendering. + */ + virtual void setShader(GLShader* shader) = 0; + /** + * @returns The GLShader used for rendering or null if none. + */ + virtual GLShader* shader() const = 0; + + /** + * @returns The style of this EffectFrame. + */ + virtual EffectFrameStyle style() const = 0; + + /** + * If @p enable is @c true cross fading between icons and text is enabled + * By default disabled. Use setCrossFadeProgress to cross fade. + * Cross Fading is currently only available if OpenGL is used. + * @param enable @c true enables cross fading, @c false disables it again + * @see isCrossFade + * @see setCrossFadeProgress + * @since 4.6 + */ + void enableCrossFade(bool enable); + /** + * @returns @c true if cross fading is enabled, @c false otherwise + * @see enableCrossFade + * @since 4.6 + */ + bool isCrossFade() const; + /** + * Sets the current progress for cross fading the last used icon/text + * with current icon/text to @p progress. + * A value of 0.0 means completely old icon/text, a value of 1.0 means + * completely current icon/text. + * Default value is 1.0. You have to enable cross fade before using it. + * Cross Fading is currently only available if OpenGL is used. + * @see enableCrossFade + * @see isCrossFade + * @see crossFadeProgress + * @since 4.6 + */ + void setCrossFadeProgress(qreal progress); + /** + * @returns The current progress for cross fading + * @see setCrossFadeProgress + * @see enableCrossFade + * @see isCrossFade + * @since 4.6 + */ + qreal crossFadeProgress() const; + + /** + * Returns The projection matrix as used by the current screen painting pass + * including screen transformations. + * + * This matrix is only valid during a rendering pass started by render. + * + * @since 5.6 + * @see render + * @see EffectsHandler::paintEffectFrame + * @see Effect::paintEffectFrame + */ + QMatrix4x4 screenProjectionMatrix() const; + +protected: + void setScreenProjectionMatrix(const QMatrix4x4 &projection); + +private: + EffectFramePrivate* const d; +}; + +/** + * The TimeLine class is a helper for controlling animations. + */ +class KWINEFFECTS_EXPORT TimeLine +{ +public: + /** + * Direction of the timeline. + * + * When the direction of the timeline is Forward, the progress + * value will go from 0.0 to 1.0. + * + * When the direction of the timeline is Backward, the progress + * value will go from 1.0 to 0.0. + */ + enum Direction { + Forward, + Backward + }; + + /** + * Constructs a new instance of TimeLine. + * + * @param duration Duration of the timeline, in milliseconds + * @param direction Direction of the timeline + * @since 5.14 + */ + explicit TimeLine(std::chrono::milliseconds duration = std::chrono::milliseconds(1000), + Direction direction = Forward); + TimeLine(const TimeLine &other); + ~TimeLine(); + + /** + * Returns the current value of the timeline. + * + * @since 5.14 + */ + qreal value() const; + + /** + * Updates the progress of the timeline. + * + * @note The delta value should be a non-negative number, i.e. it + * should be greater or equal to 0. + * + * @param delta The number milliseconds passed since last frame + * @since 5.14 + */ + void update(std::chrono::milliseconds delta); + + /** + * Returns the number of elapsed milliseconds. + * + * @see setElapsed + * @since 5.14 + */ + std::chrono::milliseconds elapsed() const; + + /** + * Sets the number of elapsed milliseconds. + * + * This method overwrites previous value of elapsed milliseconds. + * If the new value of elapsed milliseconds is greater or equal + * to duration of the timeline, the timeline will be finished, i.e. + * proceeding TimeLine::done method calls will return @c true. + * Please don't use it. Instead, use TimeLine::update. + * + * @note The new number of elapsed milliseconds should be a non-negative + * number, i.e. it should be greater or equal to 0. + * + * @param elapsed The new number of elapsed milliseconds + * @see elapsed + * @since 5.14 + */ + void setElapsed(std::chrono::milliseconds elapsed); + + /** + * Returns the duration of the timeline. + * + * @returns Duration of the timeline, in milliseconds + * @see setDuration + * @since 5.14 + */ + std::chrono::milliseconds duration() const; + + /** + * Sets the duration of the timeline. + * + * In addition to setting new value of duration, the timeline will + * try to retarget the number of elapsed milliseconds to match + * as close as possible old progress value. If the new duration + * is much smaller than old duration, there is a big chance that + * the timeline will be finished after setting new duration. + * + * @note The new duration should be a positive number, i.e. it + * should be greater or equal to 1. + * + * @param duration The new duration of the timeline, in milliseconds + * @see duration + * @since 5.14 + */ + void setDuration(std::chrono::milliseconds duration); + + /** + * Returns the direction of the timeline. + * + * @returns Direction of the timeline(TimeLine::Forward or TimeLine::Backward) + * @see setDirection + * @see toggleDirection + * @since 5.14 + */ + Direction direction() const; + + /** + * Sets the direction of the timeline. + * + * @param direction The new direction of the timeline + * @see direction + * @see toggleDirection + * @since 5.14 + */ + void setDirection(Direction direction); + + /** + * Toggles the direction of the timeline. + * + * If the direction of the timeline was TimeLine::Forward, it becomes + * TimeLine::Backward, and vice verca. + * + * @see direction + * @see setDirection + * @since 5.14 + */ + void toggleDirection(); + + /** + * Returns the easing curve of the timeline. + * + * @see setEasingCurve + * @since 5.14 + */ + QEasingCurve easingCurve() const; + + /** + * Sets new easing curve. + * + * @param easingCurve An easing curve to be set + * @see easingCurve + * @since 5.14 + */ + void setEasingCurve(const QEasingCurve &easingCurve); + + /** + * Sets new easing curve by providing its type. + * + * @param type Type of the easing curve(e.g. QEasingCurve::InCubic, etc) + * @see easingCurve + * @since 5.14 + */ + void setEasingCurve(QEasingCurve::Type type); + + /** + * Returns whether the timeline is currently in progress. + * + * @see done + * @since 5.14 + */ + bool running() const; + + /** + * Returns whether the timeline is finished. + * + * @see reset + * @since 5.14 + */ + bool done() const; + + /** + * Resets the timeline to initial state. + * + * @since 5.14 + */ + void reset(); + + enum class RedirectMode { + Strict, + Relaxed + }; + + /** + * Returns the redirect mode for the source position. + * + * The redirect mode controls behavior of the timeline when its direction is + * changed at the source position, e.g. what should we do when the timeline + * initially goes forward and we change its direction to go backward. + * + * In the strict mode, the timeline will stop. + * + * In the relaxed mode, the timeline will go in the new direction. For example, + * if the timeline goes forward(from 0 to 1), then with the new direction it + * will go backward(from 1 to 0). + * + * The default is RedirectMode::Relaxed. + * + * @see targetRedirectMode + * @since 5.15 + */ + RedirectMode sourceRedirectMode() const; + + /** + * Sets the redirect mode for the source position. + * + * @param mode The new mode. + * @since 5.15 + */ + void setSourceRedirectMode(RedirectMode mode); + + /** + * Returns the redirect mode for the target position. + * + * The redirect mode controls behavior of the timeline when its direction is + * changed at the target position. + * + * In the strict mode, subsequent update calls won't have any effect on the + * current value of the timeline. + * + * In the relaxed mode, the timeline will go in the new direction. + * + * The default is RedirectMode::Strict. + * + * @see sourceRedirectMode + * @since 5.15 + */ + RedirectMode targetRedirectMode() const; + + /** + * Sets the redirect mode for the target position. + * + * @param mode The new mode. + * @since 5.15 + */ + void setTargetRedirectMode(RedirectMode mode); + + TimeLine &operator=(const TimeLine &other); + +private: + qreal progress() const; + +private: + class Data; + QSharedDataPointer d; +}; + +/** + * Pointer to the global EffectsHandler object. + */ +extern KWINEFFECTS_EXPORT EffectsHandler* effects; + +/*************************************************************** + WindowVertex +***************************************************************/ + +inline +WindowVertex::WindowVertex() + : px(0), py(0), ox(0), oy(0), tx(0), ty(0) +{ +} + +inline +WindowVertex::WindowVertex(double _x, double _y, double _tx, double _ty) + : px(_x), py(_y), ox(_x), oy(_y), tx(_tx), ty(_ty) +{ +} + + +inline +WindowVertex::WindowVertex(const QPointF &position, const QPointF &texturePosition) + : px(position.x()), py(position.y()), ox(position.x()), oy(position.y()), tx(texturePosition.x()), ty(texturePosition.y()) +{ +} + +inline +void WindowVertex::move(double x, double y) +{ + px = x; + py = y; +} + +inline +void WindowVertex::setX(double x) +{ + px = x; +} + +inline +void WindowVertex::setY(double y) +{ + py = y; +} + +/*************************************************************** + WindowQuad +***************************************************************/ + +inline +WindowQuad::WindowQuad(WindowQuadType t, int id) + : quadType(t) + , uvSwapped(false) + , quadID(id) +{ +} + +inline +WindowVertex& WindowQuad::operator[](int index) +{ + Q_ASSERT(index >= 0 && index < 4); + return verts[ index ]; +} + +inline +const WindowVertex& WindowQuad::operator[](int index) const +{ + Q_ASSERT(index >= 0 && index < 4); + return verts[ index ]; +} + +inline +WindowQuadType WindowQuad::type() const +{ + Q_ASSERT(quadType != WindowQuadError); + return quadType; +} + +inline +int WindowQuad::id() const +{ + return quadID; +} + +inline +bool WindowQuad::decoration() const +{ + Q_ASSERT(quadType != WindowQuadError); + return quadType == WindowQuadDecoration; +} + +inline +bool WindowQuad::effect() const +{ + Q_ASSERT(quadType != WindowQuadError); + return quadType >= EFFECT_QUAD_TYPE_START; +} + +inline +bool WindowQuad::isTransformed() const +{ + return !(verts[ 0 ].px == verts[ 0 ].ox && verts[ 0 ].py == verts[ 0 ].oy + && verts[ 1 ].px == verts[ 1 ].ox && verts[ 1 ].py == verts[ 1 ].oy + && verts[ 2 ].px == verts[ 2 ].ox && verts[ 2 ].py == verts[ 2 ].oy + && verts[ 3 ].px == verts[ 3 ].ox && verts[ 3 ].py == verts[ 3 ].oy); +} + +inline +double WindowQuad::left() const +{ + return qMin(verts[ 0 ].px, qMin(verts[ 1 ].px, qMin(verts[ 2 ].px, verts[ 3 ].px))); +} + +inline +double WindowQuad::right() const +{ + return qMax(verts[ 0 ].px, qMax(verts[ 1 ].px, qMax(verts[ 2 ].px, verts[ 3 ].px))); +} + +inline +double WindowQuad::top() const +{ + return qMin(verts[ 0 ].py, qMin(verts[ 1 ].py, qMin(verts[ 2 ].py, verts[ 3 ].py))); +} + +inline +double WindowQuad::bottom() const +{ + return qMax(verts[ 0 ].py, qMax(verts[ 1 ].py, qMax(verts[ 2 ].py, verts[ 3 ].py))); +} + +inline +double WindowQuad::originalLeft() const +{ + return verts[ 0 ].ox; +} + +inline +double WindowQuad::originalRight() const +{ + return verts[ 2 ].ox; +} + +inline +double WindowQuad::originalTop() const +{ + return verts[ 0 ].oy; +} + +inline +double WindowQuad::originalBottom() const +{ + return verts[ 2 ].oy; +} + +/*************************************************************** + Motion +***************************************************************/ + +template +Motion::Motion(T initial, double strength, double smoothness) + : m_value(initial) + , m_start(initial) + , m_target(initial) + , m_velocity() + , m_strength(strength) + , m_smoothness(smoothness) +{ +} + +template +Motion::Motion(const Motion &other) + : m_value(other.value()) + , m_start(other.target()) + , m_target(other.target()) + , m_velocity(other.velocity()) + , m_strength(other.strength()) + , m_smoothness(other.smoothness()) +{ +} + +template +Motion::~Motion() +{ +} + +template +void Motion::calculate(const int msec) +{ + if (m_value == m_target && m_velocity == T()) // At target and not moving + return; + + // Poor man's time independent calculation + int steps = qMax(1, msec / 5); + for (int i = 0; i < steps; i++) { + T diff = m_target - m_value; + T strength = diff * m_strength; + m_velocity = (m_smoothness * m_velocity + strength) / (m_smoothness + 1.0); + m_value += m_velocity; + } +} + +template +void Motion::finish() +{ + m_value = m_target; + m_velocity = T(); +} + +/*************************************************************** + Effect +***************************************************************/ +template +int Effect::animationTime(int defaultDuration) +{ + return animationTime(T::duration() != 0 ? T::duration() : defaultDuration); +} + +template +void Effect::initConfig() +{ + T::instance(effects->config()); +} + +} // namespace +Q_DECLARE_METATYPE(KWin::EffectWindow*) +Q_DECLARE_METATYPE(QList) +Q_DECLARE_METATYPE(KWin::TimeLine) +Q_DECLARE_METATYPE(KWin::TimeLine::Direction) + +/** @} */ + +#endif // KWINEFFECTS_H diff --git a/libkwineffects/kwineglimagetexture.cpp b/libkwineffects/kwineglimagetexture.cpp new file mode 100644 index 0000000..9400b69 --- /dev/null +++ b/libkwineffects/kwineglimagetexture.cpp @@ -0,0 +1,36 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwineglimagetexture.h" + +#include +#include + +namespace KWin +{ + +EGLImageTexture::EGLImageTexture(EGLDisplay display, EGLImage image, int internalFormat, const QSize &size) + : GLTexture(internalFormat, size, 1, true) + , m_image(image) + , m_display(display) +{ + if (m_image == EGL_NO_IMAGE_KHR) { + return; + } + + bind(); + glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, m_image); +} + +EGLImageTexture::~EGLImageTexture() +{ + eglDestroyImageKHR(m_display, m_image); +} + +} // namespace KWin diff --git a/libkwineffects/kwineglimagetexture.h b/libkwineffects/kwineglimagetexture.h new file mode 100644 index 0000000..2367c11 --- /dev/null +++ b/libkwineffects/kwineglimagetexture.h @@ -0,0 +1,34 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +typedef void *EGLImageKHR; +typedef void *EGLDisplay; +typedef void *EGLClientBuffer; + +namespace KWin +{ + +class KWINGLUTILS_EXPORT EGLImageTexture : public GLTexture +{ +public: + EGLImageTexture(EGLDisplay display, EGLImageKHR image, int internalFormat, const QSize &size); + ~EGLImageTexture() override; + +private: + EGLImageKHR m_image; + EGLDisplay m_display; +}; + +} + diff --git a/libkwineffects/kwinglobals.h b/libkwineffects/kwinglobals.h new file mode 100644 index 0000000..8eece41 --- /dev/null +++ b/libkwineffects/kwinglobals.h @@ -0,0 +1,217 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_LIB_KWINGLOBALS_H +#define KWIN_LIB_KWINGLOBALS_H + +#include +#include +#include +#include + +#include + +#include + +#include + +#define KWIN_QT5_PORTING 0 + +namespace KWin +{ +KWIN_EXPORT Q_NAMESPACE + +enum CompositingType { + NoCompositing = 0, + /** + * Used as a flag whether OpenGL based compositing is used. + * The flag is or-ed to the enum values of the specific OpenGL types. + * The actual Compositors use the or @c OpenGL2Compositing + * flags. If you need to know whether OpenGL is used, either and the flag or + * use EffectsHandler::isOpenGLCompositing(). + */ + OpenGLCompositing = 1, + XRenderCompositing = 1<<1, + QPainterCompositing = 1<< 2, + OpenGL2Compositing = 1<<3 | OpenGLCompositing +}; + +enum OpenGLPlatformInterface { + NoOpenGLPlatformInterface = 0, + GlxPlatformInterface, + EglPlatformInterface +}; + +enum clientAreaOption { + PlacementArea, // geometry where a window will be initially placed after being mapped + MovementArea, // ??? window movement snapping area? ignore struts + MaximizeArea, // geometry to which a window will be maximized + MaximizeFullArea, // like MaximizeArea, but ignore struts - used e.g. for topmenu + FullScreenArea, // area for fullscreen windows + // these below don't depend on xinerama settings + WorkArea, // whole workarea (all screens together) + FullArea, // whole area (all screens together), ignore struts + ScreenArea // one whole screen, ignore struts +}; + +enum ElectricBorder { + ElectricTop, + ElectricTopRight, + ElectricRight, + ElectricBottomRight, + ElectricBottom, + ElectricBottomLeft, + ElectricLeft, + ElectricTopLeft, + ELECTRIC_COUNT, + ElectricNone +}; + +// TODO: Hardcoding is bad, need to add some way of registering global actions to these. +// When designing the new system we must keep in mind that we have conditional actions +// such as "only when moving windows" desktop switching that the current global action +// system doesn't support. +enum ElectricBorderAction { + ElectricActionNone, // No special action, not set, desktop switch or an effect + ElectricActionShowDesktop, // Show desktop or restore + ElectricActionLockScreen, // Lock screen + ElectricActionKRunner, // Open KRunner + ElectricActionActivityManager, // Activity Manager + ElectricActionApplicationLauncher, // Application Launcher + ELECTRIC_ACTION_COUNT +}; + +// DesktopMode and WindowsMode are based on the order in which the desktop +// or window were viewed. +// DesktopListMode lists them in the order created. +enum TabBoxMode { + TabBoxDesktopMode, // Focus chain of desktops + TabBoxDesktopListMode, // Static desktop order + TabBoxWindowsMode, // Primary window switching mode + TabBoxWindowsAlternativeMode, // Secondary window switching mode + TabBoxCurrentAppWindowsMode, // Same as primary window switching mode but only for windows of current application + TabBoxCurrentAppWindowsAlternativeMode // Same as secondary switching mode but only for windows of current application +}; + +enum KWinOption { + CloseButtonCorner, + SwitchDesktopOnScreenEdge, + SwitchDesktopOnScreenEdgeMovingWindows +}; + +/** + * @brief The direction in which a pointer axis is moved. + */ +enum PointerAxisDirection { + PointerAxisUp, + PointerAxisDown, + PointerAxisLeft, + PointerAxisRight +}; + +/** + * @brief Directions for swipe gestures + * @since 5.10 + */ +enum class SwipeDirection { + Invalid, + Down, + Left, + Up, + Right +}; + +/** + * Represents the state of the session running outside kwin + * Under Plasma this is managed by ksmserver + */ +enum class SessionState { + Normal, + Saving, + Quitting +}; +Q_ENUM_NS(SessionState) + +inline +KWIN_EXPORT xcb_connection_t *connection() +{ + return reinterpret_cast(qApp->property("x11Connection").value()); +} + +inline +KWIN_EXPORT xcb_window_t rootWindow() +{ + return qApp->property("x11RootWindow").value(); +} + +inline +KWIN_EXPORT xcb_timestamp_t xTime() +{ + return qApp->property("x11Time").value(); +} + +/** + * Short wrapper for a cursor image provided by the Platform. + * @since 5.9 + */ +class PlatformCursorImage { +public: + explicit PlatformCursorImage() + : m_image() + , m_hotSpot() + { + } + explicit PlatformCursorImage(const QImage &image, const QPoint &hotSpot) + : m_image(image) + , m_hotSpot(hotSpot) + { + } + virtual ~PlatformCursorImage() = default; + + bool isNull() const { + return m_image.isNull(); + } + QImage image() const { + return m_image; + } + QPoint hotSpot() const { + return m_hotSpot; + } + +private: + QImage m_image; + QPoint m_hotSpot; +}; + +} // namespace + +#define KWIN_SINGLETON_VARIABLE(ClassName, variableName) \ +public: \ + static ClassName *create(QObject *parent = nullptr);\ + static ClassName *self() { return variableName; }\ +protected: \ + explicit ClassName(QObject *parent = nullptr); \ +private: \ + static ClassName *variableName; + +#define KWIN_SINGLETON(ClassName) KWIN_SINGLETON_VARIABLE(ClassName, s_self) + +#define KWIN_SINGLETON_FACTORY_VARIABLE_FACTORED(ClassName, FactoredClassName, variableName) \ +ClassName *ClassName::variableName = nullptr; \ +ClassName *ClassName::create(QObject *parent) \ +{ \ + Q_ASSERT(!variableName); \ + variableName = new FactoredClassName(parent); \ + return variableName; \ +} +#define KWIN_SINGLETON_FACTORY_VARIABLE(ClassName, variableName) KWIN_SINGLETON_FACTORY_VARIABLE_FACTORED(ClassName, ClassName, variableName) +#define KWIN_SINGLETON_FACTORY_FACTORED(ClassName, FactoredClassName) KWIN_SINGLETON_FACTORY_VARIABLE_FACTORED(ClassName, FactoredClassName, s_self) +#define KWIN_SINGLETON_FACTORY(ClassName) KWIN_SINGLETON_FACTORY_VARIABLE(ClassName, s_self) + +#endif diff --git a/libkwineffects/kwinglplatform.cpp b/libkwineffects/kwinglplatform.cpp new file mode 100644 index 0000000..56a97f0 --- /dev/null +++ b/libkwineffects/kwinglplatform.cpp @@ -0,0 +1,1264 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2010 Fredrik Höglund + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwinglplatform.h" +// include kwinglutils_funcs.h to avoid the redeclaration issues +// between qopengl.h and epoxy/gl.h +#include "kwinglutils_funcs.h" +#include + +#include +#include +#include +#include + +#include + +#include +#include +#include + +namespace KWin +{ + +GLPlatform *GLPlatform::s_platform = nullptr; + +static qint64 parseVersionString(const QByteArray &version) +{ + // Skip any leading non digit + int start = 0; + while (start < version.length() && !QChar::fromLatin1(version[start]).isDigit()) + start++; + + // Strip any non digit, non '.' characters from the end + int end = start; + while (end < version.length() && (version[end] == '.' || QChar::fromLatin1(version[end]).isDigit())) + end++; + + const QByteArray result = version.mid(start, end-start); + const QList tokens = result.split('.'); + const qint64 major = tokens.at(0).toInt(); + const qint64 minor = tokens.count() > 1 ? tokens.at(1).toInt() : 0; + const qint64 patch = tokens.count() > 2 ? tokens.at(2).toInt() : 0; + + return kVersionNumber(major, minor, patch); +} + +static qint64 getXServerVersion() +{ + qint64 major, minor, patch; + major = 0; + minor = 0; + patch = 0; + + if (xcb_connection_t *c = connection()) { + auto setup = xcb_get_setup(c); + const QByteArray vendorName(xcb_setup_vendor(setup), xcb_setup_vendor_length(setup)); + if (vendorName.contains("X.Org")) { + const int release = setup->release_number; + major = (release / 10000000); + minor = (release / 100000) % 100; + patch = (release / 1000) % 100; + } + } + + return kVersionNumber(major, minor, patch); +} + +static qint64 getKernelVersion() +{ + struct utsname name; + uname(&name); + + if (qstrcmp(name.sysname, "Linux") == 0) + return parseVersionString(name.release); + + return 0; +} + +// Extracts the portion of a string that matches a regular expression +static QString extract(const QString &text, const QString &pattern) +{ + const QRegularExpression regexp(pattern); + const QRegularExpressionMatch match = regexp.match(text); + if (!match.hasMatch()) + return QString(); + return match.captured(); +} + +static ChipClass detectRadeonClass(const QByteArray &chipset) +{ + if (chipset.isEmpty()) + return UnknownRadeon; + + if (chipset.contains("R100") || + chipset.contains("RV100") || + chipset.contains("RS100")) + return R100; + + if (chipset.contains("RV200") || + chipset.contains("RS200") || + chipset.contains("R200") || + chipset.contains("RV250") || + chipset.contains("RS300") || + chipset.contains("RV280")) + return R200; + + if (chipset.contains("R300") || + chipset.contains("R350") || + chipset.contains("R360") || + chipset.contains("RV350") || + chipset.contains("RV370") || + chipset.contains("RV380")) + return R300; + + if (chipset.contains("R420") || + chipset.contains("R423") || + chipset.contains("R430") || + chipset.contains("R480") || + chipset.contains("R481") || + chipset.contains("RV410") || + chipset.contains("RS400") || + chipset.contains("RC410") || + chipset.contains("RS480") || + chipset.contains("RS482") || + chipset.contains("RS600") || + chipset.contains("RS690") || + chipset.contains("RS740")) + return R400; + + if (chipset.contains("RV515") || + chipset.contains("R520") || + chipset.contains("RV530") || + chipset.contains("R580") || + chipset.contains("RV560") || + chipset.contains("RV570")) + return R500; + + if (chipset.contains("R600") || + chipset.contains("RV610") || + chipset.contains("RV630") || + chipset.contains("RV670") || + chipset.contains("RV620") || + chipset.contains("RV635") || + chipset.contains("RS780") || + chipset.contains("RS880")) + return R600; + + if (chipset.contains("R700") || + chipset.contains("RV770") || + chipset.contains("RV730") || + chipset.contains("RV710") || + chipset.contains("RV740")) + return R700; + + if (chipset.contains("EVERGREEN") || // Not an actual chipset, but returned by R600G in 7.9 + chipset.contains("CEDAR") || + chipset.contains("REDWOOD") || + chipset.contains("JUNIPER") || + chipset.contains("CYPRESS") || + chipset.contains("HEMLOCK") || + chipset.contains("PALM")) + return Evergreen; + + if (chipset.contains("SUMO") || + chipset.contains("SUMO2") || + chipset.contains("BARTS") || + chipset.contains("TURKS") || + chipset.contains("CAICOS") || + chipset.contains("CAYMAN")) + return NorthernIslands; + + if (chipset.contains("TAHITI") || + chipset.contains("PITCAIRN") || + chipset.contains("VERDE") || + chipset.contains("OLAND") || + chipset.contains("HAINAN")) { + return SouthernIslands; + } + + if (chipset.contains("BONAIRE") || + chipset.contains("KAVERI") || + chipset.contains("KABINI") || + chipset.contains("HAWAII") || + chipset.contains("MULLINS")) { + return SeaIslands; + } + + if (chipset.contains("TONGA") || + chipset.contains("TOPAZ") || + chipset.contains("FIJI") || + chipset.contains("CARRIZO") || + chipset.contains("STONEY")) { + return VolcanicIslands; + } + + if (chipset.contains("POLARIS10") || + chipset.contains("POLARIS11") || + chipset.contains("POLARIS12") || + chipset.contains("VEGAM")) { + return ArcticIslands; + } + + if (chipset.contains("VEGA10") || + chipset.contains("VEGA12") || + chipset.contains("VEGA20") || + chipset.contains("RAVEN") || + chipset.contains("RAVEN2") || + chipset.contains("RENOIR") || + chipset.contains("ARCTURUS")) { + return Vega; + } + + if (chipset.contains("NAVI10") || + chipset.contains("NAVI12") || + chipset.contains("NAVI14")) { + return Navi; + } + + const QString chipset16 = QString::fromLatin1(chipset); + QString name = extract(chipset16, QStringLiteral("HD [0-9]{4}")); // HD followed by a space and 4 digits + if (!name.isEmpty()) { + const int id = name.rightRef(4).toInt(); + if (id == 6250 || id == 6310) // Palm + return Evergreen; + + if (id >= 6000 && id < 7000) + return NorthernIslands; // HD 6xxx + + if (id >= 5000 && id < 6000) + return Evergreen; // HD 5xxx + + if (id >= 4000 && id < 5000) + return R700; // HD 4xxx + + if (id >= 2000 && id < 4000) // HD 2xxx/3xxx + return R600; + + return UnknownRadeon; + } + + name = extract(chipset16, QStringLiteral("X[0-9]{3,4}")); // X followed by 3-4 digits + if (!name.isEmpty()) { + const int id = name.midRef(1, -1).toInt(); + + // X1xxx + if (id >= 1300) + return R500; + + // X7xx, X8xx, X12xx, 2100 + if ((id >= 700 && id < 1000) || id >= 1200) + return R400; + + // X200, X3xx, X5xx, X6xx, X10xx, X11xx + if ((id >= 300 && id < 700) || (id >= 1000 && id < 1200)) + return R300; + + return UnknownRadeon; + } + + name = extract(chipset16, QStringLiteral("\\b[0-9]{4}\\b")); // A group of 4 digits + if (!name.isEmpty()) { + const int id = name.toInt(); + + // 7xxx + if (id >= 7000 && id < 8000) + return R100; + + // 8xxx, 9xxx + if (id >= 8000 && id < 9500) + return R200; + + // 9xxx + if (id >= 9500) + return R300; + + if (id == 2100) + return R400; + } + + return UnknownRadeon; +} + +static ChipClass detectNVidiaClass(const QString &chipset) +{ + QString name = extract(chipset, QStringLiteral("\\bNV[0-9,A-F]{2}\\b")); // NV followed by two hexadecimal digits + if (!name.isEmpty()) { + const int id = chipset.midRef(2, -1).toInt(nullptr, 16); // Strip the 'NV' from the id + + switch(id & 0xf0) { + case 0x00: + case 0x10: + return NV10; + + case 0x20: + return NV20; + + case 0x30: + return NV30; + + case 0x40: + case 0x60: + return NV40; + + case 0x50: + case 0x80: + case 0x90: + case 0xA0: + return G80; + + default: + return UnknownNVidia; + } + } + + if (chipset.contains(QLatin1String("GeForce2")) || chipset.contains(QLatin1String("GeForce 256"))) + return NV10; + + if (chipset.contains(QLatin1String("GeForce3"))) + return NV20; + + if (chipset.contains(QLatin1String("GeForce4"))) { + if (chipset.contains(QLatin1String("MX 420")) || + chipset.contains(QLatin1String("MX 440")) || // including MX 440SE + chipset.contains(QLatin1String("MX 460")) || + chipset.contains(QLatin1String("MX 4000")) || + chipset.contains(QLatin1String("PCX 4300"))) + return NV10; + + return NV20; + } + + // GeForce 5,6,7,8,9 + name = extract(chipset, QStringLiteral("GeForce (FX |PCX |Go )?\\d{4}(M|\\b)")).trimmed(); + if (!name.isEmpty()) { + if (!name[name.length() - 1].isDigit()) + name.chop(1); + + const int id = name.rightRef(4).toInt(); + if (id < 6000) + return NV30; + + if (id >= 6000 && id < 8000) + return NV40; + + if (id >= 8000) + return G80; + + return UnknownNVidia; + } + + // GeForce 100/200/300/400/500 + name = extract(chipset, QStringLiteral("GeForce (G |GT |GTX |GTS )?\\d{3}(M|\\b)")).trimmed(); + if (!name.isEmpty()) { + if (!name[name.length() - 1].isDigit()) + name.chop(1); + + const int id = name.rightRef(3).toInt(); + if (id >= 100 && id < 600) { + if (id >= 400) + return GF100; + + return G80; + } + return UnknownNVidia; + } + + return UnknownNVidia; +} +static inline ChipClass detectNVidiaClass(const QByteArray &chipset) +{ + return detectNVidiaClass(QString::fromLatin1(chipset)); +} + +static ChipClass detectIntelClass(const QByteArray &chipset) +{ + // see mesa repository: src/mesa/drivers/dri/intel/intel_context.c + // GL 1.3, DX8? SM ? + if (chipset.contains("845G") || + chipset.contains("830M") || + chipset.contains("852GM/855GM") || + chipset.contains("865G")) + return I8XX; + + // GL 1.4, DX 9.0, SM 2.0 + if (chipset.contains("915G") || + chipset.contains("E7221G") || + chipset.contains("915GM") || + chipset.contains("945G") || // DX 9.0c + chipset.contains("945GM") || + chipset.contains("945GME") || + chipset.contains("Q33") || // GL1.5 + chipset.contains("Q35") || + chipset.contains("G33") || + chipset.contains("965Q") || // GMA 3000, but apparently considered gen 4 by the driver + chipset.contains("946GZ") || // GMA 3000, but apparently considered gen 4 by the driver + chipset.contains("IGD")) + return I915; + + // GL 2.0, DX 9.0c, SM 3.0 + if (chipset.contains("965G") || + chipset.contains("G45/G43") || // SM 4.0 + chipset.contains("965GM") || // GL 2.1 + chipset.contains("965GME/GLE") || + chipset.contains("GM45") || + chipset.contains("Q45/Q43") || + chipset.contains("G41") || + chipset.contains("B43") || + chipset.contains("Ironlake")) + return I965; + + // GL 3.1, CL 1.1, DX 10.1 + if (chipset.contains("Sandybridge")) { + return SandyBridge; + } + + // GL4.0, CL1.1, DX11, SM 5.0 + if (chipset.contains("Ivybridge")) { + return IvyBridge; + } + + // GL4.0, CL1.2, DX11.1, SM 5.0 + if (chipset.contains("Haswell")) { + return Haswell; + } + + return UnknownIntel; +} + +static ChipClass detectQualcommClass(const QByteArray &chipClass) +{ + if (!chipClass.contains("Adreno")) { + return UnknownChipClass; + } + const auto parts = chipClass.split(' '); + if (parts.count() < 3) { + return UnknownAdreno; + } + bool ok = false; + const int value = parts.at(2).toInt(&ok); + if (ok) { + if (value >= 100 && value < 200) { + return Adreno1XX; + } + if (value >= 200 && value < 300) { + return Adreno2XX; + } + if (value >= 300 && value < 400) { + return Adreno3XX; + } + if (value >= 400 && value < 500) { + return Adreno4XX; + } + if (value >= 500 && value < 600) { + return Adreno5XX; + } + } + return UnknownAdreno; +} + +QString GLPlatform::versionToString(qint64 version) +{ + return QString::fromLatin1(versionToString8(version)); +} +QByteArray GLPlatform::versionToString8(qint64 version) +{ + int major = (version >> 32); + int minor = (version >> 16) & 0xffff; + int patch = version & 0xffff; + + QByteArray string = QByteArray::number(major) + '.' + QByteArray::number(minor); + if (patch != 0) + string += '.' + QByteArray::number(patch); + + return string; +} + +QString GLPlatform::driverToString(Driver driver) +{ + return QString::fromLatin1(driverToString8(driver)); +} +QByteArray GLPlatform::driverToString8(Driver driver) +{ + switch(driver) { + case Driver_R100: + return QByteArrayLiteral("Radeon"); + case Driver_R200: + return QByteArrayLiteral("R200"); + case Driver_R300C: + return QByteArrayLiteral("R300C"); + case Driver_R300G: + return QByteArrayLiteral("R300G"); + case Driver_R600C: + return QByteArrayLiteral("R600C"); + case Driver_R600G: + return QByteArrayLiteral("R600G"); + case Driver_RadeonSI: + return QByteArrayLiteral("RadeonSI"); + case Driver_Nouveau: + return QByteArrayLiteral("Nouveau"); + case Driver_Intel: + return QByteArrayLiteral("Intel"); + case Driver_NVidia: + return QByteArrayLiteral("NVIDIA"); + case Driver_Catalyst: + return QByteArrayLiteral("Catalyst"); + case Driver_Swrast: + return QByteArrayLiteral("Software rasterizer"); + case Driver_Softpipe: + return QByteArrayLiteral("softpipe"); + case Driver_Llvmpipe: + return QByteArrayLiteral("LLVMpipe"); + case Driver_VirtualBox: + return QByteArrayLiteral("VirtualBox (Chromium)"); + case Driver_VMware: + return QByteArrayLiteral("VMware (SVGA3D)"); + case Driver_Qualcomm: + return QByteArrayLiteral("Qualcomm"); + case Driver_Virgl: + return QByteArrayLiteral("Virgl (virtio-gpu, Qemu/KVM guest)"); + + default: + return QByteArrayLiteral("Unknown"); + } +} + +QString GLPlatform::chipClassToString(ChipClass chipClass) +{ + return QString::fromLatin1(chipClassToString8(chipClass)); +} +QByteArray GLPlatform::chipClassToString8(ChipClass chipClass) +{ + switch(chipClass) { + case R100: + return QByteArrayLiteral("R100"); + case R200: + return QByteArrayLiteral("R200"); + case R300: + return QByteArrayLiteral("R300"); + case R400: + return QByteArrayLiteral("R400"); + case R500: + return QByteArrayLiteral("R500"); + case R600: + return QByteArrayLiteral("R600"); + case R700: + return QByteArrayLiteral("R700"); + case Evergreen: + return QByteArrayLiteral("EVERGREEN"); + case NorthernIslands: + return QByteArrayLiteral("Northern Islands"); + case SouthernIslands: + return QByteArrayLiteral("Southern Islands"); + case SeaIslands: + return QByteArrayLiteral("Sea Islands"); + case VolcanicIslands: + return QByteArrayLiteral("Volcanic Islands"); + case ArcticIslands: + return QByteArrayLiteral("Arctic Islands"); + case Vega: + return QByteArrayLiteral("Vega"); + case Navi: + return QByteArrayLiteral("Navi"); + + case NV10: + return QByteArrayLiteral("NV10"); + case NV20: + return QByteArrayLiteral("NV20"); + case NV30: + return QByteArrayLiteral("NV30"); + case NV40: + return QByteArrayLiteral("NV40/G70"); + case G80: + return QByteArrayLiteral("G80/G90"); + case GF100: + return QByteArrayLiteral("GF100"); + + case I8XX: + return QByteArrayLiteral("i830/i835"); + case I915: + return QByteArrayLiteral("i915/i945"); + case I965: + return QByteArrayLiteral("i965"); + case SandyBridge: + return QByteArrayLiteral("SandyBridge"); + case IvyBridge: + return QByteArrayLiteral("IvyBridge"); + case Haswell: + return QByteArrayLiteral("Haswell"); + + case Adreno1XX: + return QByteArrayLiteral("Adreno 1xx series"); + case Adreno2XX: + return QByteArrayLiteral("Adreno 2xx series"); + case Adreno3XX: + return QByteArrayLiteral("Adreno 3xx series"); + case Adreno4XX: + return QByteArrayLiteral("Adreno 4xx series"); + case Adreno5XX: + return QByteArrayLiteral("Adreno 5xx series"); + + default: + return QByteArrayLiteral("Unknown"); + } +} + + + +// ------- + + + +GLPlatform::GLPlatform() + : m_driver(Driver_Unknown), + m_chipClass(UnknownChipClass), + m_recommendedCompositor(XRenderCompositing), + m_glVersion(0), + m_glslVersion(0), + m_mesaVersion(0), + m_driverVersion(0), + m_galliumVersion(0), + m_serverVersion(0), + m_kernelVersion(0), + m_looseBinding(false), + m_supportsGLSL(false), + m_limitedGLSL(false), + m_textureNPOT(false), + m_limitedNPOT(false), + m_virtualMachine(false), + m_preferBufferSubData(false), + m_platformInterface(NoOpenGLPlatformInterface), + m_gles(false) +{ +} + +GLPlatform::~GLPlatform() +{ +} + +void GLPlatform::detect(OpenGLPlatformInterface platformInterface) +{ + m_platformInterface = platformInterface; + + m_vendor = (const char*)glGetString(GL_VENDOR); + m_renderer = (const char*)glGetString(GL_RENDERER); + m_version = (const char*)glGetString(GL_VERSION); + + // Parse the OpenGL version + const QList versionTokens = m_version.split(' '); + if (versionTokens.count() > 0) { + const QByteArray version = QByteArray(m_version); + m_glVersion = parseVersionString(version); + if (platformInterface == EglPlatformInterface) { + // only EGL can have OpenGLES, GLX is OpenGL only + if (version.startsWith("OpenGL ES")) { + // from GLES 2: "Returns a version or release number of the form OpenGLES." + // from GLES 3: "Returns a version or release number." and "The version number uses one of these forms: major_number.minor_number major_number.minor_number.release_number" + m_gles = true; + } + } + } + + if (!isGLES() && m_glVersion >= kVersionNumber(3, 0)) { + int count; + glGetIntegerv(GL_NUM_EXTENSIONS, &count); + + for (int i = 0; i < count; i++) { + const char *name = (const char *) glGetStringi(GL_EXTENSIONS, i); + m_extensions.insert(name); + } + } else { + const QByteArray extensions = (const char *) glGetString(GL_EXTENSIONS); + QList extensionsList = extensions.split(' '); + m_extensions = {extensionsList.constBegin(), extensionsList.constEnd()}; + } + + // Parse the Mesa version + const int mesaIndex = versionTokens.indexOf("Mesa"); + if (mesaIndex != -1) { + const QByteArray &version = versionTokens.at(mesaIndex + 1); + m_mesaVersion = parseVersionString(version); + } + + if (isGLES()) { + m_supportsGLSL = true; + m_textureNPOT = true; + } else { + m_supportsGLSL = m_extensions.contains("GL_ARB_shader_objects") && + m_extensions.contains("GL_ARB_fragment_shader") && + m_extensions.contains("GL_ARB_vertex_shader"); + + m_textureNPOT = m_extensions.contains("GL_ARB_texture_non_power_of_two"); + } + + m_serverVersion = getXServerVersion(); + m_kernelVersion = getKernelVersion(); + + m_glslVersion = 0; + m_glsl_version.clear(); + + if (m_supportsGLSL) { + // Parse the GLSL version + m_glsl_version = (const char*)glGetString(GL_SHADING_LANGUAGE_VERSION); + m_glslVersion = parseVersionString(m_glsl_version); + } + + m_chipset = QByteArrayLiteral("Unknown"); + m_preferBufferSubData = false; + + + // Mesa classic drivers + // ==================================================== + + // Radeon + if (m_renderer.startsWith("Mesa DRI R")) { + // Sample renderer string: Mesa DRI R600 (RV740 94B3) 20090101 x86/MMX/SSE2 TCL DRI2 + const QList tokens = m_renderer.split(' '); + const QByteArray &chipClass = tokens.at(2); + m_chipset = tokens.at(3).mid(1, -1); // Strip the leading '(' + + if (chipClass == "R100") + // Vendor: Tungsten Graphics, Inc. + m_driver = Driver_R100; + + else if (chipClass == "R200") + // Vendor: Tungsten Graphics, Inc. + m_driver = Driver_R200; + + else if (chipClass == "R300") + // Vendor: DRI R300 Project + m_driver = Driver_R300C; + + else if (chipClass == "R600") + // Vendor: Advanced Micro Devices, Inc. + m_driver = Driver_R600C; + + m_chipClass = detectRadeonClass(m_chipset); + } + + // Intel + else if (m_renderer.contains("Intel")) { + // Vendor: Tungsten Graphics, Inc. + // Sample renderer string: Mesa DRI Mobile Intel® GM45 Express Chipset GEM 20100328 2010Q1 + + QByteArray chipset; + if (m_renderer.startsWith("Intel(R) Integrated Graphics Device")) + chipset = "IGD"; + else + chipset = m_renderer; + + m_driver = Driver_Intel; + m_chipClass = detectIntelClass(chipset); + } + + // Properietary drivers + // ==================================================== + else if (m_vendor == "ATI Technologies Inc.") { + m_chipClass = detectRadeonClass(m_renderer); + m_driver = Driver_Catalyst; + + if (versionTokens.count() > 1 && versionTokens.at(2)[0] == '(') + m_driverVersion = parseVersionString(versionTokens.at(1)); + else if (versionTokens.count() > 0) + m_driverVersion = parseVersionString(versionTokens.at(0)); + else + m_driverVersion = 0; + } + + else if (m_vendor == "NVIDIA Corporation") { + m_chipClass = detectNVidiaClass(m_renderer); + m_driver = Driver_NVidia; + + int index = versionTokens.indexOf("NVIDIA"); + if (versionTokens.count() > index) + m_driverVersion = parseVersionString(versionTokens.at(index + 1)); + else + m_driverVersion = 0; + } + + else if (m_vendor == "Qualcomm") { + m_driver = Driver_Qualcomm; + m_chipClass = detectQualcommClass(m_renderer); + } + + else if (m_renderer == "Software Rasterizer") { + m_driver = Driver_Swrast; + } + + // Virtual Hardware + // ==================================================== + else if (m_vendor == "Humper" && m_renderer == "Chromium") { + // Virtual Box + m_driver = Driver_VirtualBox; + + const int index = versionTokens.indexOf("Chromium"); + if (versionTokens.count() > index) + m_driverVersion = parseVersionString(versionTokens.at(index + 1)); + else + m_driverVersion = 0; + } + + // Gallium drivers + // ==================================================== + else { + const QList tokens = m_renderer.split(' '); + if (m_renderer.contains("Gallium")) { + // Sample renderer string: Gallium 0.4 on AMD RV740 + m_galliumVersion = parseVersionString(tokens.at(1)); + m_chipset = (tokens.at(3) == "AMD" || tokens.at(3) == "ATI") ? + tokens.at(4) : tokens.at(3); + } + else { + // The renderer string does not contain "Gallium" anymore. + m_chipset = tokens.at(0); + // We don't know the actual version anymore, but it's at least 0.4. + m_galliumVersion = kVersionNumber(0, 4, 0); + } + + // R300G + if (m_vendor == QByteArrayLiteral("X.Org R300 Project")) { + m_chipClass = detectRadeonClass(m_chipset); + m_driver = Driver_R300G; + } + + // R600G + else if (m_vendor == "X.Org" && + (m_renderer.contains("R6") || + m_renderer.contains("R7") || + m_renderer.contains("RV6") || + m_renderer.contains("RV7") || + m_renderer.contains("RS780") || + m_renderer.contains("RS880") || + m_renderer.contains("CEDAR") || + m_renderer.contains("REDWOOD") || + m_renderer.contains("JUNIPER") || + m_renderer.contains("CYPRESS") || + m_renderer.contains("HEMLOCK") || + m_renderer.contains("PALM") || + m_renderer.contains("EVERGREEN") || + m_renderer.contains("SUMO") || + m_renderer.contains("SUMO2") || + m_renderer.contains("BARTS") || + m_renderer.contains("TURKS") || + m_renderer.contains("CAICOS") || + m_renderer.contains("CAYMAN"))) { + m_chipClass = detectRadeonClass(m_chipset); + m_driver = Driver_R600G; + } + + // RadeonSI + else if (m_vendor == "X.Org" && + (m_renderer.contains("TAHITI") || + m_renderer.contains("PITCAIRN") || + m_renderer.contains("VERDE") || + m_renderer.contains("OLAND") || + m_renderer.contains("HAINAN") || + m_renderer.contains("BONAIRE") || + m_renderer.contains("KAVERI") || + m_renderer.contains("KABINI") || + m_renderer.contains("HAWAII") || + m_renderer.contains("MULLINS") || + m_renderer.contains("TOPAZ") || + m_renderer.contains("TONGA") || + m_renderer.contains("FIJI") || + m_renderer.contains("CARRIZO") || + m_renderer.contains("STONEY") || + m_renderer.contains("POLARIS10") || + m_renderer.contains("POLARIS11") || + m_renderer.contains("POLARIS12") || + m_renderer.contains("VEGAM") || + m_renderer.contains("VEGA10") || + m_renderer.contains("VEGA12") || + m_renderer.contains("VEGA20") || + m_renderer.contains("RAVEN") || + m_renderer.contains("RAVEN2") || + m_renderer.contains("RENOIR") || + m_renderer.contains("ARCTURUS") || + m_renderer.contains("NAVI10") || + m_renderer.contains("NAVI12") || + m_renderer.contains("NAVI14"))) { + m_chipClass = detectRadeonClass(m_renderer); + m_driver = Driver_RadeonSI; + } + + // Nouveau + else if (m_vendor == "nouveau") { + m_chipClass = detectNVidiaClass(m_chipset); + m_driver = Driver_Nouveau; + } + + // softpipe + else if (m_chipset == "softpipe") { + m_driver = Driver_Softpipe; + } + + // llvmpipe + else if (m_chipset == "llvmpipe") { + m_driver = Driver_Llvmpipe; + } + + // SVGA3D + else if (m_vendor == "VMware, Inc." && m_chipset.contains("SVGA3D")) { + m_driver = Driver_VMware; + } + + // virgl + else if (m_renderer == "virgl") { + m_driver = Driver_Virgl; + } + } + + // Driver/GPU specific features + // ==================================================== + if (isRadeon()) { + // R200 technically has a programmable pipeline, but since it's SM 1.4, + // it's too limited to to be of any practical value to us. + if (m_chipClass < R300) + m_supportsGLSL = false; + + m_limitedGLSL = false; + m_limitedNPOT = false; + + if (m_chipClass < R600) { + if (driver() == Driver_Catalyst) + m_textureNPOT = m_limitedNPOT = false; // Software fallback + else if (driver() == Driver_R300G) + m_limitedNPOT = m_textureNPOT; + + m_limitedGLSL = m_supportsGLSL; + } + + if (m_chipClass < R300) { + // fallback to XRender for R100 and R200 + m_recommendedCompositor = XRenderCompositing; + } else if (m_chipClass < R600) { + // XRender due to NPOT limitations not supported by KWin's shaders + m_recommendedCompositor = XRenderCompositing; + } else { + m_recommendedCompositor = OpenGL2Compositing; + } + + if (driver() == Driver_R600G || + (driver() == Driver_R600C && m_renderer.contains("DRI2"))) { + m_looseBinding = true; + } + } + + if (isNvidia()) { + if (m_driver == Driver_NVidia && m_chipClass < NV40) + m_supportsGLSL = false; // High likelihood of software emulation + + if (m_driver == Driver_NVidia) { + m_looseBinding = true; + m_preferBufferSubData = true; + } + + if (m_chipClass < NV40) { + m_recommendedCompositor = XRenderCompositing; + } else { + m_recommendedCompositor = OpenGL2Compositing; + } + + m_limitedNPOT = m_textureNPOT && m_chipClass < NV40; + m_limitedGLSL = m_supportsGLSL && m_chipClass < G80; + } + + if (isIntel()) { + if (m_chipClass < I915) + m_supportsGLSL = false; + + m_limitedGLSL = m_supportsGLSL && m_chipClass < I965; + // see https://bugs.freedesktop.org/show_bug.cgi?id=80349#c1 + m_looseBinding = false; + + if (m_chipClass < I915) { + m_recommendedCompositor = XRenderCompositing; + } else { + m_recommendedCompositor = OpenGL2Compositing; + } + } + + if (isMesaDriver() && platformInterface == EglPlatformInterface) { + // According to the reference implementation in + // mesa/demos/src/egl/opengles1/texture_from_pixmap + // the mesa egl implementation does not require a strict binding (so far). + m_looseBinding = true; + } + + if (isSoftwareEmulation()) { + if (m_driver < Driver_Llvmpipe) { + // we recommend XRender + m_recommendedCompositor = XRenderCompositing; + // Software emulation does not provide GLSL + m_limitedGLSL = m_supportsGLSL = false; + } else { + // llvmpipe does support GLSL + m_recommendedCompositor = OpenGL2Compositing; + m_limitedGLSL = false; + m_supportsGLSL = true; + } + } + + if (m_driver == Driver_Qualcomm) { + if (m_chipClass == Adreno1XX) { + m_recommendedCompositor = NoCompositing; + } else { + // all other drivers support at least GLES 2 + m_recommendedCompositor = OpenGL2Compositing; + } + } + + if (m_chipClass == UnknownChipClass && m_driver == Driver_Unknown) { + // we don't know the hardware. Let's be optimistic and assume OpenGL compatible hardware + m_recommendedCompositor = OpenGL2Compositing; + m_supportsGLSL = true; + } + + if (isVirtualBox()) { + m_virtualMachine = true; + m_recommendedCompositor = OpenGL2Compositing; + } + + if (isVMware()) { + m_virtualMachine = true; + m_recommendedCompositor = OpenGL2Compositing; + } + + if (m_driver == Driver_Virgl) { + m_virtualMachine = true; + m_recommendedCompositor = OpenGL2Compositing; + } + + // and force back to shader supported on gles, we wouldn't have got a context if not supported + if (isGLES()) { + m_supportsGLSL = true; + m_limitedGLSL = false; + } +} + +static void print(const QByteArray &label, const QByteArray &setting) +{ + std::cout << std::setw(40) << std::left + << label.data() << setting.data() << std::endl; +} + +void GLPlatform::printResults() const +{ + print(QByteArrayLiteral("OpenGL vendor string:"), m_vendor); + print(QByteArrayLiteral("OpenGL renderer string:"), m_renderer); + print(QByteArrayLiteral("OpenGL version string:"), m_version); + + if (m_supportsGLSL) + print(QByteArrayLiteral("OpenGL shading language version string:"), m_glsl_version); + + print(QByteArrayLiteral("Driver:"), driverToString8(m_driver)); + if (!isMesaDriver()) + print(QByteArrayLiteral("Driver version:"), versionToString8(m_driverVersion)); + + print(QByteArrayLiteral("GPU class:"), chipClassToString8(m_chipClass)); + + print(QByteArrayLiteral("OpenGL version:"), versionToString8(m_glVersion)); + + if (m_supportsGLSL) + print(QByteArrayLiteral("GLSL version:"), versionToString8(m_glslVersion)); + + if (isMesaDriver()) + print(QByteArrayLiteral("Mesa version:"), versionToString8(mesaVersion())); + //if (galliumVersion() > 0) + // print("Gallium version:", versionToString(m_galliumVersion)); + if (serverVersion() > 0) + print(QByteArrayLiteral("X server version:"), versionToString8(m_serverVersion)); + if (kernelVersion() > 0) + print(QByteArrayLiteral("Linux kernel version:"), versionToString8(m_kernelVersion)); + + print(QByteArrayLiteral("Requires strict binding:"), !m_looseBinding ? QByteArrayLiteral("yes") : QByteArrayLiteral("no")); + print(QByteArrayLiteral("GLSL shaders:"), m_supportsGLSL ? (m_limitedGLSL ? QByteArrayLiteral("limited") : QByteArrayLiteral("yes")) : QByteArrayLiteral("no")); + print(QByteArrayLiteral("Texture NPOT support:"), m_textureNPOT ? (m_limitedNPOT ? QByteArrayLiteral("limited") : QByteArrayLiteral("yes")) : QByteArrayLiteral("no")); + print(QByteArrayLiteral("Virtual Machine:"), m_virtualMachine ? QByteArrayLiteral("yes") : QByteArrayLiteral("no")); +} + +bool GLPlatform::supports(GLFeature feature) const +{ + switch(feature) { + case LooseBinding: + return m_looseBinding; + + case GLSL: + return m_supportsGLSL; + + case LimitedGLSL: + return m_limitedGLSL; + + case TextureNPOT: + return m_textureNPOT; + + case LimitedNPOT: + return m_limitedNPOT; + + default: + return false; + } +} + +qint64 GLPlatform::glVersion() const +{ + return m_glVersion; +} + +qint64 GLPlatform::glslVersion() const +{ + return m_glslVersion; +} + +qint64 GLPlatform::mesaVersion() const +{ + return m_mesaVersion; +} + +qint64 GLPlatform::galliumVersion() const +{ + return m_galliumVersion; +} + +qint64 GLPlatform::serverVersion() const +{ + return m_serverVersion; +} + +qint64 GLPlatform::kernelVersion() const +{ + return m_kernelVersion; +} + +qint64 GLPlatform::driverVersion() const +{ + if (isMesaDriver()) + return mesaVersion(); + + return m_driverVersion; +} + +Driver GLPlatform::driver() const +{ + return m_driver; +} + +ChipClass GLPlatform::chipClass() const +{ + return m_chipClass; +} + +bool GLPlatform::isMesaDriver() const +{ + return mesaVersion() > 0; +} + +bool GLPlatform::isGalliumDriver() const +{ + return galliumVersion() > 0; +} + +bool GLPlatform::isRadeon() const +{ + return m_chipClass >= R100 && m_chipClass <= UnknownRadeon; +} + +bool GLPlatform::isNvidia() const +{ + return m_chipClass >= NV10 && m_chipClass <= UnknownNVidia; +} + +bool GLPlatform::isIntel() const +{ + return m_chipClass >= I8XX && m_chipClass <= UnknownIntel; +} + +bool GLPlatform::isVirtualBox() const +{ + return m_driver == Driver_VirtualBox; +} + +bool GLPlatform::isVMware() const +{ + return m_driver == Driver_VMware; +} + +bool GLPlatform::isVirgl() const +{ + return m_driver == Driver_Virgl; +} + +bool GLPlatform::isSoftwareEmulation() const +{ + return m_driver == Driver_Softpipe || m_driver == Driver_Swrast || m_driver == Driver_Llvmpipe; +} + +bool GLPlatform::isAdreno() const +{ + return m_chipClass >= Adreno1XX && m_chipClass <= UnknownAdreno; +} + +const QByteArray &GLPlatform::glRendererString() const +{ + return m_renderer; +} + +const QByteArray &GLPlatform::glVendorString() const +{ + return m_vendor; +} + +const QByteArray &GLPlatform::glVersionString() const +{ + return m_version; +} + +const QByteArray &GLPlatform::glShadingLanguageVersionString() const +{ + return m_glsl_version; +} + +bool GLPlatform::isLooseBinding() const +{ + return m_looseBinding; +} + +bool GLPlatform::isVirtualMachine() const +{ + return m_virtualMachine; +} + +CompositingType GLPlatform::recommendedCompositor() const +{ + return m_recommendedCompositor; +} + +bool GLPlatform::preferBufferSubData() const +{ + return m_preferBufferSubData; +} + +OpenGLPlatformInterface GLPlatform::platformInterface() const +{ + return m_platformInterface; +} + +bool GLPlatform::isGLES() const +{ + return m_gles; +} + +void GLPlatform::cleanup() +{ + delete s_platform; + s_platform = nullptr; +} + +} // namespace KWin + diff --git a/libkwineffects/kwinglplatform.h b/libkwineffects/kwinglplatform.h new file mode 100644 index 0000000..21dad1f --- /dev/null +++ b/libkwineffects/kwinglplatform.h @@ -0,0 +1,436 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2010 Fredrik Höglund + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_GLPLATFORM_H +#define KWIN_GLPLATFORM_H + +#include +#include + +#include +#include + +namespace KWin +{ +// forward declare method +void cleanupGL(); + +inline qint64 kVersionNumber(qint64 major, qint64 minor, qint64 patch = 0) +{ + return ((major & 0xffff) << 32) | ((minor & 0xffff) << 16) | (patch & 0xffff); +} + +enum GLFeature { + /** + * Set when a texture bound to a pixmap uses the same storage as the pixmap, + * and thus doesn't need to be rebound when the contents of the pixmap + * has changed. + */ + LooseBinding, + + /** + * Set if the driver supports the following extensions: + * - GL_ARB_shader_objects + * - GL_ARB_fragment_shader + * - GL_ARB_vertex_shader + * - GL_ARB_shading_language_100 + */ + GLSL, + + /** + * If set, assume the following: + * - No flow control or branches + * - No loops, unless the loops have a fixed iteration count and can be unrolled + * - No functions, unless they can be inlined + * - No indirect indexing of arrays + * - No support for gl_ClipVertex or gl_FrontFacing + * - No texture fetches in vertex shaders + * - Max 32 texture fetches in fragment shaders + * - Max 4 texture indirections + */ + LimitedGLSL, + + /** + * Set when the driver supports GL_ARB_texture_non_power_of_two. + */ + TextureNPOT, + + /** + * If set, the driver supports GL_ARB_texture_non_power_of_two with the + * GL_ARB_texture_rectangle limitations. + * + * This means no support for mipmap filters, and that only the following + * wrap modes are supported: + * - GL_CLAMP + * - GL_CLAMP_TO_EDGE + * - GL_CLAMP_TO_BORDER + */ + LimitedNPOT +}; + +enum Driver { + Driver_R100, // Technically "Radeon" + Driver_R200, + Driver_R300C, + Driver_R300G, + Driver_R600C, + Driver_R600G, + Driver_Nouveau, + Driver_Intel, + Driver_NVidia, + Driver_Catalyst, + Driver_Swrast, + Driver_Softpipe, + Driver_Llvmpipe, + Driver_VirtualBox, + Driver_VMware, + Driver_Qualcomm, + Driver_RadeonSI, + Driver_Virgl, + Driver_Unknown +}; + +enum ChipClass { + // Radeon + R100 = 0, // GL1.3 DX7 2000 + R200, // GL1.4 DX8.1 SM 1.4 2001 + R300, // GL2.0 DX9 SM 2.0 2002 + R400, // GL2.0 DX9b SM 2.0b 2004 + R500, // GL2.0 DX9c SM 3.0 2005 + R600, // GL3.3 DX10 SM 4.0 2006 + R700, // GL3.3 DX10.1 SM 4.1 2008 + Evergreen, // GL4.0 CL1.0 DX11 SM 5.0 2009 + NorthernIslands, // GL4.0 CL1.1 DX11 SM 5.0 2010 + SouthernIslands, // GL4.5 CL1.2 DX11.1 SM 5.1 2012 + SeaIslands, // GL4.5 CL2.0 DX12 SM 6.0 2013 + VolcanicIslands, // GL4.5 CL2.0 DX12 SM 6.0 2015 + ArcticIslands, // GL4.5 CL2.0 DX12 SM 6.0 2016 + Vega, // GL4.6 CL2.0 DX12 SM 6.0 2017 + Navi, // GL4.6 CL2.0 DX12.1 SM 6.4 2019 + UnknownRadeon = 999, + + // NVIDIA + NV10 = 1000, // GL1.2 DX7 1999 + NV20, // GL1.3 DX8 SM 1.1 2001 + NV30, // GL1.5 DX9a SM 2.0 2003 + NV40, // GL2.1 DX9c SM 3.0 2004 + G80, // GL3.3 DX10 SM 4.0 2006 + GF100, // GL4.1 CL1.1 DX11 SM 5.0 2010 + UnknownNVidia = 1999, + + // Intel + I8XX = 2000, // GL1.3 DX7 2001 + I915, // GL1.4/1.5 DX9/DX9c SM 2.0 2004 + I965, // GL2.0/2.1 DX9/DX10 SM 3.0/4.0 2006 + SandyBridge, // GL3.1 CL1.1 DX10.1 SM 4.0 2010 + IvyBridge, // GL4.0 CL1.1 DX11 SM 5.0 2012 + Haswell, // GL4.0 CL1.2 DX11.1 SM 5.0 2013 + UnknownIntel = 2999, + + // Qualcomm Adreno + // from https://en.wikipedia.org/wiki/Adreno + Adreno1XX = 3000, // GLES1.1 + Adreno2XX, // GLES2.0 DX9c + Adreno3XX, // GLES3.0 CL1.1 DX11.1 + Adreno4XX, // GLES3.1 CL1.2 DX11.2 + Adreno5XX, // GLES3.1 CL2.0 DX11.2 + UnknownAdreno = 3999, + + UnknownChipClass = 99999 +}; + + +class KWINGLUTILS_EXPORT GLPlatform +{ +public: + ~GLPlatform(); + + /** + * Runs the detection code using the current OpenGL context. + */ + void detect(OpenGLPlatformInterface platformInterface); + + /** + * Prints the results of the detection code. + */ + void printResults() const; + + /** + * Returns a pointer to the GLPlatform instance. + */ + static GLPlatform *instance(); + + /** + * Returns true if the driver support the given feature, and false otherwise. + */ + bool supports(GLFeature feature) const; + + /** + * Returns the OpenGL version. + */ + qint64 glVersion() const; + + /** + * Returns the GLSL version if the driver supports GLSL, and 0 otherwise. + */ + qint64 glslVersion() const; + + /** + * Returns the Mesa version if the driver is a Mesa driver, and 0 otherwise. + */ + qint64 mesaVersion() const; + + /** + * Returns the Gallium version if the driver is a Gallium driver, and 0 otherwise. + */ + qint64 galliumVersion() const; + + /** + * Returns the X server version. + * + * Note that the version number changed from 7.2 to 1.3 in the first release + * following the doupling of the X server from the katamari. + * + * For non X.org servers, this method returns 0. + */ + qint64 serverVersion() const; + + /** + * Returns the Linux kernel version. + * + * If the kernel is not a Linux kernel, this method returns 0. + */ + qint64 kernelVersion() const; + + /** + * Returns the driver version. + * + * For Mesa drivers, this is the same as the Mesa version number. + */ + qint64 driverVersion() const; + + /** + * Returns the driver. + */ + Driver driver() const; + + /** + * Returns the chip class. + */ + ChipClass chipClass() const; + + /** + * Returns true if the driver is a Mesa driver, and false otherwise. + */ + bool isMesaDriver() const; + + /** + * Returns true if the driver is a Gallium driver, and false otherwise. + */ + bool isGalliumDriver() const; + + /** + * Returns true if the GPU is a Radeon GPU, and false otherwise. + */ + bool isRadeon() const; + + /** + * Returns true if the GPU is an NVIDIA GPU, and false otherwise. + */ + bool isNvidia() const; + + /** + * Returns true if the GPU is an Intel GPU, and false otherwise. + */ + bool isIntel() const; + + /** + * @returns @c true if the "GPU" is a VirtualBox GPU, and @c false otherwise. + * @since 4.10 + */ + bool isVirtualBox() const; + + /** + * @returns @c true if the "GPU" is a VMWare GPU, and @c false otherwise. + * @since 4.10 + */ + bool isVMware() const; + + /** + * @returns @c true if OpenGL is emulated in software. + * @since 4.7 + */ + bool isSoftwareEmulation() const; + + /** + * @returns @c true if the driver is known to be from a virtual machine. + * @since 4.10 + */ + bool isVirtualMachine() const; + + /** + * @returns @c true if the GPU is a Qualcomm Adreno GPU, and false otherwise + * @since 5.8 + */ + bool isAdreno() const; + + /** + * @returns @c true if the "GPU" is a virtio-gpu (Qemu/KVM) + * @since 5.18 + **/ + bool isVirgl() const; + + /** + * @returns the GL_VERSION string as provided by the driver. + * @since 4.9 + */ + const QByteArray &glVersionString() const; + /** + * @returns the GL_RENDERER string as provided by the driver. + * @since 4.9 + */ + const QByteArray &glRendererString() const; + /** + * @returns the GL_VENDOR string as provided by the driver. + * @since 4.9 + */ + const QByteArray &glVendorString() const; + /** + * @returns the GL_SHADING_LANGUAGE_VERSION string as provided by the driver. + * If the driver does not support the OpenGL Shading Language a null bytearray is returned. + * @since 4.9 + */ + const QByteArray &glShadingLanguageVersionString() const; + /** + * @returns Whether the driver supports loose texture binding. + * @since 4.9 + */ + bool isLooseBinding() const; + /** + * @returns Whether OpenGL ES is used + */ + bool isGLES() const; + + /** + * @returns The CompositingType recommended by the driver. + * @since 4.10 + */ + CompositingType recommendedCompositor() const; + + /** + * Returns true if glMapBufferRange() is likely to perform worse than glBufferSubData() + * when updating an unused range of a buffer object, and false otherwise. + * + * @since 4.11 + */ + bool preferBufferSubData() const; + + /** + * @returns The OpenGLPlatformInterface currently used + * @since 5.0 + */ + OpenGLPlatformInterface platformInterface() const; + + /** + * @returns a human readable form of the @p version as a QString. + * @since 4.9 + * @see glVersion + * @see glslVersion + * @see driverVersion + * @see mesaVersion + * @see galliumVersion + * @see kernelVersion + * @see serverVersion + */ + static QString versionToString(qint64 version); + /** + * @returns a human readable form of the @p version as a QByteArray. + * @since 5.5 + * @see glVersion + * @see glslVersion + * @see driverVersion + * @see mesaVersion + * @see galliumVersion + * @see kernelVersion + * @see serverVersion + */ + static QByteArray versionToString8(qint64 version); + + /** + * @returns a human readable form for the @p driver as a QString. + * @since 4.9 + * @see driver + */ + static QString driverToString(Driver driver); + /** + * @returns a human readable form for the @p driver as a QByteArray. + * @since 5.5 + * @see driver + */ + static QByteArray driverToString8(Driver driver); + + /** + * @returns a human readable form for the @p chipClass as a QString. + * @since 4.9 + * @see chipClass + */ + static QString chipClassToString(ChipClass chipClass); + /** + * @returns a human readable form for the @p chipClass as a QByteArray. + * @since 5.5 + * @see chipClass + */ + static QByteArray chipClassToString8(ChipClass chipClass); + +private: + GLPlatform(); + friend void KWin::cleanupGL(); + static void cleanup(); + +private: + QByteArray m_renderer; + QByteArray m_vendor; + QByteArray m_version; + QByteArray m_glsl_version; + QByteArray m_chipset; + QSet m_extensions; + Driver m_driver; + ChipClass m_chipClass; + CompositingType m_recommendedCompositor; + qint64 m_glVersion; + qint64 m_glslVersion; + qint64 m_mesaVersion; + qint64 m_driverVersion; + qint64 m_galliumVersion; + qint64 m_serverVersion; + qint64 m_kernelVersion; + bool m_looseBinding: 1; + bool m_supportsGLSL: 1; + bool m_limitedGLSL: 1; + bool m_textureNPOT: 1; + bool m_limitedNPOT: 1; + bool m_virtualMachine: 1; + bool m_preferBufferSubData: 1; + OpenGLPlatformInterface m_platformInterface; + bool m_gles: 1; + static GLPlatform *s_platform; +}; + +inline GLPlatform *GLPlatform::instance() +{ + if (!s_platform) + s_platform = new GLPlatform; + + return s_platform; +} + +} // namespace KWin + +#endif // KWIN_GLPLATFORM_H + diff --git a/libkwineffects/kwingltexture.cpp b/libkwineffects/kwingltexture.cpp new file mode 100644 index 0000000..195980e --- /dev/null +++ b/libkwineffects/kwingltexture.cpp @@ -0,0 +1,682 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006-2007 Rivo Laks + SPDX-FileCopyrightText: 2010, 2011 Martin Gräßlin + SPDX-FileCopyrightText: 2012 Philipp Knechtges + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwinconfig.h" // KWIN_HAVE_OPENGL + +#include "kwinglplatform.h" +#include "kwinglutils_funcs.h" +#include "kwinglutils.h" + +#include "kwingltexture_p.h" + +#include +#include +#include +#include +#include +#include + +namespace KWin +{ + +//**************************************** +// GLTexture +//**************************************** + +bool GLTexturePrivate::s_supportsFramebufferObjects = false; +bool GLTexturePrivate::s_supportsARGB32 = false; +bool GLTexturePrivate::s_supportsUnpack = false; +bool GLTexturePrivate::s_supportsTextureStorage = false; +bool GLTexturePrivate::s_supportsTextureSwizzle = false; +bool GLTexturePrivate::s_supportsTextureFormatRG = false; +uint GLTexturePrivate::s_textureObjectCounter = 0; +uint GLTexturePrivate::s_fbo = 0; + + +GLTexture::GLTexture() + : d_ptr(new GLTexturePrivate()) +{ +} + +GLTexture::GLTexture(GLTexturePrivate& dd) + : d_ptr(&dd) +{ +} + +GLTexture::GLTexture(const GLTexture& tex) + : d_ptr(tex.d_ptr) +{ +} + +GLTexture::GLTexture(const QImage& image, GLenum target) + : d_ptr(new GLTexturePrivate()) +{ + Q_D(GLTexture); + + if (image.isNull()) + return; + + d->m_target = target; + + if (d->m_target != GL_TEXTURE_RECTANGLE_ARB) { + d->m_scale.setWidth(1.0 / image.width()); + d->m_scale.setHeight(1.0 / image.height()); + } else { + d->m_scale.setWidth(1.0); + d->m_scale.setHeight(1.0); + } + + d->m_size = image.size(); + d->m_yInverted = true; + d->m_canUseMipmaps = false; + d->m_mipLevels = 1; + + d->updateMatrix(); + + glGenTextures(1, &d->m_texture); + bind(); + + if (!GLPlatform::instance()->isGLES()) { + // Note: Blending is set up to expect premultiplied data, so non-premultiplied + // formats must always be converted. + struct { + GLenum internalFormat; + GLenum format; + GLenum type; + } static const table[] = { + { 0, 0, 0 }, // QImage::Format_Invalid + { 0, 0, 0 }, // QImage::Format_Mono + { 0, 0, 0 }, // QImage::Format_MonoLSB + { GL_R8, GL_RED, GL_UNSIGNED_BYTE }, // QImage::Format_Indexed8 + { GL_RGB8, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV }, // QImage::Format_RGB32 + { 0, 0, 0 }, // QImage::Format_ARGB32 + { GL_RGBA8, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV }, // QImage::Format_ARGB32_Premultiplied + { GL_RGB8, GL_BGR, GL_UNSIGNED_SHORT_5_6_5_REV }, // QImage::Format_RGB16 + { 0, 0, 0 }, // QImage::Format_ARGB8565_Premultiplied + { 0, 0, 0 }, // QImage::Format_RGB666 + { 0, 0, 0 }, // QImage::Format_ARGB6666_Premultiplied + { GL_RGB5, GL_BGRA, GL_UNSIGNED_SHORT_1_5_5_5_REV }, // QImage::Format_RGB555 + { 0, 0, 0 }, // QImage::Format_ARGB8555_Premultiplied + { GL_RGB8, GL_RGB, GL_UNSIGNED_BYTE }, // QImage::Format_RGB888 + { GL_RGB4, GL_BGRA, GL_UNSIGNED_SHORT_4_4_4_4_REV }, // QImage::Format_RGB444 + { GL_RGBA4, GL_BGRA, GL_UNSIGNED_SHORT_4_4_4_4_REV }, // QImage::Format_ARGB4444_Premultiplied + { GL_RGB8, GL_RGBA, GL_UNSIGNED_BYTE }, // QImage::Format_RGBX8888 + { 0, 0, 0 }, // QImage::Format_RGBA8888 + { GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE }, // QImage::Format_RGBA8888_Premultiplied + { GL_RGB10, GL_RGBA, GL_UNSIGNED_INT_2_10_10_10_REV }, // QImage::Format_BGR30 + { GL_RGB10_A2, GL_RGBA, GL_UNSIGNED_INT_2_10_10_10_REV }, // QImage::Format_A2BGR30_Premultiplied + { GL_RGB10, GL_BGRA, GL_UNSIGNED_INT_2_10_10_10_REV }, // QImage::Format_RGB30 + { GL_RGB10_A2, GL_BGRA, GL_UNSIGNED_INT_2_10_10_10_REV }, // QImage::Format_A2RGB30_Premultiplied + { GL_R8, GL_RED, GL_UNSIGNED_BYTE }, // QImage::Format_Alpha8 + { GL_R8, GL_RED, GL_UNSIGNED_BYTE }, // QImage::Format_Grayscale8 + }; + + QImage im; + GLenum internalFormat; + GLenum format; + GLenum type; + + const QImage::Format index = image.format(); + + if (index < sizeof(table) / sizeof(table[0]) && table[index].internalFormat && + !(index == QImage::Format_Indexed8 && image.colorCount() > 0)) { + internalFormat = table[index].internalFormat; + format = table[index].format; + type = table[index].type; + im = image; + } else { + im = image.convertToFormat(QImage::Format_ARGB32_Premultiplied); + internalFormat = GL_RGBA8; + format = GL_BGRA; + type = GL_UNSIGNED_INT_8_8_8_8_REV; + } + + d->m_internalFormat = internalFormat; + + if (d->s_supportsTextureStorage) { + glTexStorage2D(d->m_target, 1, internalFormat, im.width(), im.height()); + glTexSubImage2D(d->m_target, 0, 0, 0, im.width(), im.height(), + format, type, im.bits()); + d->m_immutable = true; + } else { + glTexParameteri(d->m_target, GL_TEXTURE_MAX_LEVEL, d->m_mipLevels - 1); + glTexImage2D(d->m_target, 0, internalFormat, im.width(), im.height(), 0, + format, type, im.bits()); + } + } else { + d->m_internalFormat = GL_RGBA8; + + if (d->s_supportsARGB32) { + const QImage im = image.convertToFormat(QImage::Format_ARGB32_Premultiplied); + glTexImage2D(d->m_target, 0, GL_BGRA_EXT, im.width(), im.height(), + 0, GL_BGRA_EXT, GL_UNSIGNED_BYTE, im.bits()); + } else { + const QImage im = image.convertToFormat(QImage::Format_RGBA8888_Premultiplied); + glTexImage2D(d->m_target, 0, GL_RGBA, im.width(), im.height(), + 0, GL_RGBA, GL_UNSIGNED_BYTE, im.bits()); + } + } + + unbind(); + setFilter(GL_LINEAR); +} + +GLTexture::GLTexture(const QPixmap& pixmap, GLenum target) + : GLTexture(pixmap.toImage(), target) +{ +} + +GLTexture::GLTexture(const QString& fileName) + : GLTexture(QImage(fileName)) +{ +} + +GLTexture::GLTexture(GLenum internalFormat, int width, int height, int levels, bool needsMutability) + : d_ptr(new GLTexturePrivate()) +{ + Q_D(GLTexture); + + d->m_target = GL_TEXTURE_2D; + d->m_scale.setWidth(1.0 / width); + d->m_scale.setHeight(1.0 / height); + d->m_size = QSize(width, height); + d->m_canUseMipmaps = levels > 1; + d->m_mipLevels = levels; + d->m_filter = levels > 1 ? GL_NEAREST_MIPMAP_LINEAR : GL_NEAREST; + + d->updateMatrix(); + + glGenTextures(1, &d->m_texture); + bind(); + + if (!GLPlatform::instance()->isGLES()) { + if (d->s_supportsTextureStorage && !needsMutability) { + glTexStorage2D(d->m_target, levels, internalFormat, width, height); + d->m_immutable = true; + } else { + glTexParameteri(d->m_target, GL_TEXTURE_MAX_LEVEL, levels - 1); + glTexImage2D(d->m_target, 0, internalFormat, width, height, 0, + GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, nullptr); + } + d->m_internalFormat = internalFormat; + } else { + // The format parameter in glTexSubImage() must match the internal format + // of the texture, so it's important that we allocate the texture with + // the format that will be used in update() and clear(). + const GLenum format = d->s_supportsARGB32 ? GL_BGRA_EXT : GL_RGBA; + glTexImage2D(d->m_target, 0, format, width, height, 0, + format, GL_UNSIGNED_BYTE, nullptr); + + // This is technically not true, but it means that code that calls + // internalFormat() won't need to be specialized for GLES2. + d->m_internalFormat = GL_RGBA8; + } + + unbind(); +} + +GLTexture::GLTexture(GLenum internalFormat, const QSize &size, int levels, bool needsMutability) + : GLTexture(internalFormat, size.width(), size.height(), levels, needsMutability) +{ +} + +GLTexture::GLTexture(GLuint textureId, GLenum internalFormat, const QSize &size, int levels) + : d_ptr(new GLTexturePrivate()) +{ + Q_D(GLTexture); + d->m_foreign = true; + d->m_texture = textureId; + d->m_target = GL_TEXTURE_2D; + d->m_scale.setWidth(1.0 / size.width()); + d->m_scale.setHeight(1.0 / size.height()); + d->m_size = size; + d->m_canUseMipmaps = levels > 1; + d->m_mipLevels = levels; + d->m_filter = levels > 1 ? GL_NEAREST_MIPMAP_LINEAR : GL_NEAREST; + d->m_internalFormat = internalFormat; + + d->updateMatrix(); +} + +GLTexture::~GLTexture() +{ +} + +GLTexture& GLTexture::operator = (const GLTexture& tex) +{ + d_ptr = tex.d_ptr; + return *this; +} + +GLTexturePrivate::GLTexturePrivate() + : m_texture(0) + , m_target(0) + , m_internalFormat(0) + , m_filter(GL_NEAREST) + , m_wrapMode(GL_REPEAT) + , m_yInverted(false) + , m_canUseMipmaps(false) + , m_markedDirty(false) + , m_filterChanged(true) + , m_wrapModeChanged(false) + , m_immutable(false) + , m_foreign(false) + , m_mipLevels(1) + , m_unnormalizeActive(0) + , m_normalizeActive(0) + , m_vbo(nullptr) +{ + ++s_textureObjectCounter; +} + +GLTexturePrivate::~GLTexturePrivate() +{ + delete m_vbo; + if (m_texture != 0 && !m_foreign) { + glDeleteTextures(1, &m_texture); + } + // Delete the FBO if this is the last Texture + if (--s_textureObjectCounter == 0 && s_fbo) { + glDeleteFramebuffers(1, &s_fbo); + s_fbo = 0; + } +} + +void GLTexturePrivate::initStatic() +{ + if (!GLPlatform::instance()->isGLES()) { + s_supportsFramebufferObjects = hasGLVersion(3, 0) || + hasGLExtension("GL_ARB_framebuffer_object") || hasGLExtension(QByteArrayLiteral("GL_EXT_framebuffer_object")); + s_supportsTextureStorage = hasGLVersion(4, 2) || hasGLExtension(QByteArrayLiteral("GL_ARB_texture_storage")); + s_supportsTextureSwizzle = hasGLVersion(3, 3) || hasGLExtension(QByteArrayLiteral("GL_ARB_texture_swizzle")); + // see https://www.opengl.org/registry/specs/ARB/texture_rg.txt + s_supportsTextureFormatRG = hasGLVersion(3, 0) || hasGLExtension(QByteArrayLiteral("GL_ARB_texture_rg")); + s_supportsARGB32 = true; + s_supportsUnpack = true; + } else { + s_supportsFramebufferObjects = true; + s_supportsTextureStorage = hasGLVersion(3, 0) || hasGLExtension(QByteArrayLiteral("GL_EXT_texture_storage")); + s_supportsTextureSwizzle = hasGLVersion(3, 0); + // see https://www.khronos.org/registry/gles/extensions/EXT/EXT_texture_rg.txt + s_supportsTextureFormatRG = hasGLVersion(3, 0) || hasGLExtension(QByteArrayLiteral("GL_EXT_texture_rg")); + + // QImage::Format_ARGB32_Premultiplied is a packed-pixel format, so it's only + // equivalent to GL_BGRA/GL_UNSIGNED_BYTE on little-endian systems. + s_supportsARGB32 = QSysInfo::ByteOrder == QSysInfo::LittleEndian && + hasGLExtension(QByteArrayLiteral("GL_EXT_texture_format_BGRA8888")); + + s_supportsUnpack = hasGLExtension(QByteArrayLiteral("GL_EXT_unpack_subimage")); + } +} + +void GLTexturePrivate::cleanup() +{ + s_supportsFramebufferObjects = false; + s_supportsARGB32 = false; +} + +bool GLTexture::isNull() const +{ + Q_D(const GLTexture); + return GL_NONE == d->m_texture; +} + +QSize GLTexture::size() const +{ + Q_D(const GLTexture); + return d->m_size; +} + +void GLTexture::update(const QImage &image, const QPoint &offset, const QRect &src) +{ + if (image.isNull() || isNull()) + return; + + Q_D(GLTexture); + Q_ASSERT(!d->m_foreign); + + bool useUnpack = !src.isNull() && d->s_supportsUnpack && d->s_supportsARGB32 && image.format() == QImage::Format_ARGB32_Premultiplied; + + int width = image.width(); + int height = image.height(); + QImage tmpImage; + + if (!src.isNull()) { + if (useUnpack) { + glPixelStorei(GL_UNPACK_ROW_LENGTH, image.width()); + glPixelStorei(GL_UNPACK_SKIP_PIXELS, src.x()); + glPixelStorei(GL_UNPACK_SKIP_ROWS, src.y()); + } else { + tmpImage = image.copy(src); + } + width = src.width(); + height = src.height(); + } + + const QImage &img = tmpImage.isNull() ? image : tmpImage; + + bind(); + + if (!GLPlatform::instance()->isGLES()) { + const QImage im = img.convertToFormat(QImage::Format_ARGB32_Premultiplied); + glTexSubImage2D(d->m_target, 0, offset.x(), offset.y(), width, height, + GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, im.bits()); + } else { + if (d->s_supportsARGB32) { + const QImage im = img.convertToFormat(QImage::Format_ARGB32_Premultiplied); + glTexSubImage2D(d->m_target, 0, offset.x(), offset.y(), width, height, + GL_BGRA_EXT, GL_UNSIGNED_BYTE, im.bits()); + } else { + const QImage im = img.convertToFormat(QImage::Format_RGBA8888_Premultiplied); + glTexSubImage2D(d->m_target, 0, offset.x(), offset.y(), width, height, + GL_RGBA, GL_UNSIGNED_BYTE, im.bits()); + } + } + + unbind(); + + if (useUnpack) { + glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0); + glPixelStorei(GL_UNPACK_SKIP_ROWS, 0); + } +} + +void GLTexture::discard() +{ + d_ptr = new GLTexturePrivate(); +} + +void GLTexture::bind() +{ + Q_D(GLTexture); + + glBindTexture(d->m_target, d->m_texture); + + if (d->m_markedDirty) { + d->onDamage(); + } + if (d->m_filterChanged) { + GLenum minFilter = GL_NEAREST; + GLenum magFilter = GL_NEAREST; + + switch (d->m_filter) { + case GL_NEAREST: + minFilter = magFilter = GL_NEAREST; + break; + + case GL_LINEAR: + minFilter = magFilter = GL_LINEAR; + break; + + case GL_NEAREST_MIPMAP_NEAREST: + case GL_NEAREST_MIPMAP_LINEAR: + magFilter = GL_NEAREST; + minFilter = d->m_canUseMipmaps ? d->m_filter : GL_NEAREST; + break; + + case GL_LINEAR_MIPMAP_NEAREST: + case GL_LINEAR_MIPMAP_LINEAR: + magFilter = GL_LINEAR; + minFilter = d->m_canUseMipmaps ? d->m_filter : GL_LINEAR; + break; + } + + glTexParameteri(d->m_target, GL_TEXTURE_MIN_FILTER, minFilter); + glTexParameteri(d->m_target, GL_TEXTURE_MAG_FILTER, magFilter); + + d->m_filterChanged = false; + } + if (d->m_wrapModeChanged) { + glTexParameteri(d->m_target, GL_TEXTURE_WRAP_S, d->m_wrapMode); + glTexParameteri(d->m_target, GL_TEXTURE_WRAP_T, d->m_wrapMode); + d->m_wrapModeChanged = false; + } +} + +void GLTexture::generateMipmaps() +{ + Q_D(GLTexture); + + if (d->m_canUseMipmaps && d->s_supportsFramebufferObjects) + glGenerateMipmap(d->m_target); +} + +void GLTexture::unbind() +{ + Q_D(GLTexture); + glBindTexture(d->m_target, 0); +} + +void GLTexture::render(const QRegion ®ion, const QRect& rect, bool hardwareClipping) +{ + Q_D(GLTexture); + if (rect.isEmpty()) + return; // nothing to paint and m_vbo is likely nullptr and d->m_cachedSize empty as well, #337090 + if (rect.size() != d->m_cachedSize) { + d->m_cachedSize = rect.size(); + QRect r(rect); + r.moveTo(0, 0); + if (!d->m_vbo) { + d->m_vbo = new GLVertexBuffer(KWin::GLVertexBuffer::Static); + } + + const float verts[ 4 * 2 ] = { + // NOTICE: r.x/y could be replaced by "0", but that would make it unreadable... + static_cast(r.x()), static_cast(r.y()), + static_cast(r.x()), static_cast(r.y() + rect.height()), + static_cast(r.x() + rect.width()), static_cast(r.y()), + static_cast(r.x() + rect.width()), static_cast(r.y() + rect.height()) + }; + + const float texWidth = (target() == GL_TEXTURE_RECTANGLE_ARB) ? width() : 1.0f; + const float texHeight = (target() == GL_TEXTURE_RECTANGLE_ARB) ? height() : 1.0f; + + const float texcoords[ 4 * 2 ] = { + 0.0f, d->m_yInverted ? 0.0f : texHeight, // y needs to be swapped (normalized coords) + 0.0f, d->m_yInverted ? texHeight : 0.0f, + texWidth, d->m_yInverted ? 0.0f : texHeight, + texWidth, d->m_yInverted ? texHeight : 0.0f + }; + + d->m_vbo->setData(4, 2, verts, texcoords); + } + d->m_vbo->render(region, GL_TRIANGLE_STRIP, hardwareClipping); +} + +GLuint GLTexture::texture() const +{ + Q_D(const GLTexture); + return d->m_texture; +} + +GLenum GLTexture::target() const +{ + Q_D(const GLTexture); + return d->m_target; +} + +GLenum GLTexture::filter() const +{ + Q_D(const GLTexture); + return d->m_filter; +} + +GLenum GLTexture::internalFormat() const +{ + Q_D(const GLTexture); + return d->m_internalFormat; +} + +void GLTexture::clear() +{ + Q_D(GLTexture); + Q_ASSERT(!d->m_foreign); + if (!GLTexturePrivate::s_fbo && GLRenderTarget::supported() && + GLPlatform::instance()->driver() != Driver_Catalyst) // fail. -> bug #323065 + glGenFramebuffers(1, &GLTexturePrivate::s_fbo); + + if (GLTexturePrivate::s_fbo) { + // Clear the texture + GLuint previousFramebuffer = 0; + glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, reinterpret_cast(&previousFramebuffer)); + if (GLTexturePrivate::s_fbo != previousFramebuffer) + glBindFramebuffer(GL_FRAMEBUFFER, GLTexturePrivate::s_fbo); + glClearColor(0, 0, 0, 0); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, d->m_texture, 0); + glClear(GL_COLOR_BUFFER_BIT); + if (GLTexturePrivate::s_fbo != previousFramebuffer) + glBindFramebuffer(GL_FRAMEBUFFER, previousFramebuffer); + } else { + if (const int size = width()*height()) { + uint32_t *buffer = new uint32_t[size]; + memset(buffer, 0, size*sizeof(uint32_t)); + bind(); + if (!GLPlatform::instance()->isGLES()) { + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width(), height(), + GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, buffer); + } else { + const GLenum format = d->s_supportsARGB32 ? GL_BGRA_EXT : GL_RGBA; + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width(), height(), + format, GL_UNSIGNED_BYTE, buffer); + } + unbind(); + delete[] buffer; + } + } +} + +bool GLTexture::isDirty() const +{ + Q_D(const GLTexture); + return d->m_markedDirty; +} + +void GLTexture::setFilter(GLenum filter) +{ + Q_D(GLTexture); + if (filter != d->m_filter) { + d->m_filter = filter; + d->m_filterChanged = true; + } +} + +void GLTexture::setWrapMode(GLenum mode) +{ + Q_D(GLTexture); + if (mode != d->m_wrapMode) { + d->m_wrapMode = mode; + d->m_wrapModeChanged=true; + } +} + +void GLTexturePrivate::onDamage() +{ + // No-op +} + +void GLTexture::setDirty() +{ + Q_D(GLTexture); + d->m_markedDirty = true; +} + +void GLTexturePrivate::updateMatrix() +{ + m_matrix[NormalizedCoordinates].setToIdentity(); + m_matrix[UnnormalizedCoordinates].setToIdentity(); + + if (m_target == GL_TEXTURE_RECTANGLE_ARB) + m_matrix[NormalizedCoordinates].scale(m_size.width(), m_size.height()); + else + m_matrix[UnnormalizedCoordinates].scale(1.0 / m_size.width(), 1.0 / m_size.height()); + + if (!m_yInverted) { + m_matrix[NormalizedCoordinates].translate(0.0, 1.0); + m_matrix[NormalizedCoordinates].scale(1.0, -1.0); + + m_matrix[UnnormalizedCoordinates].translate(0.0, m_size.height()); + m_matrix[UnnormalizedCoordinates].scale(1.0, -1.0); + } +} + +bool GLTexture::isYInverted() const +{ + Q_D(const GLTexture); + return d->m_yInverted; +} + +void GLTexture::setYInverted(bool inverted) +{ + Q_D(GLTexture); + if (d->m_yInverted == inverted) + return; + + d->m_yInverted = inverted; + d->updateMatrix(); +} + +void GLTexture::setSwizzle(GLenum red, GLenum green, GLenum blue, GLenum alpha) +{ + Q_D(GLTexture); + + if (!GLPlatform::instance()->isGLES()) { + const GLuint swizzle[] = { red, green, blue, alpha }; + glTexParameteriv(d->m_target, GL_TEXTURE_SWIZZLE_RGBA, (const GLint *) swizzle); + } else { + glTexParameteri(d->m_target, GL_TEXTURE_SWIZZLE_R, red); + glTexParameteri(d->m_target, GL_TEXTURE_SWIZZLE_G, green); + glTexParameteri(d->m_target, GL_TEXTURE_SWIZZLE_B, blue); + glTexParameteri(d->m_target, GL_TEXTURE_SWIZZLE_A, alpha); + } +} + +int GLTexture::width() const +{ + Q_D(const GLTexture); + return d->m_size.width(); +} + +int GLTexture::height() const +{ + Q_D(const GLTexture); + return d->m_size.height(); +} + +QMatrix4x4 GLTexture::matrix(TextureCoordinateType type) const +{ + Q_D(const GLTexture); + return d->m_matrix[type]; +} + +bool GLTexture::framebufferObjectSupported() +{ + return GLTexturePrivate::s_supportsFramebufferObjects; +} + +bool GLTexture::supportsSwizzle() +{ + return GLTexturePrivate::s_supportsTextureSwizzle; +} + +bool GLTexture::supportsFormatRG() +{ + return GLTexturePrivate::s_supportsTextureFormatRG; +} + +QImage GLTexture::toImage() const +{ + QImage ret(size(), QImage::Format_RGBA8888_Premultiplied); + glGetTextureImage(texture(), 0, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV, ret.sizeInBytes(), ret.bits()); + return ret; +} + +} // namespace KWin diff --git a/libkwineffects/kwingltexture.h b/libkwineffects/kwingltexture.h new file mode 100644 index 0000000..4dacd68 --- /dev/null +++ b/libkwineffects/kwingltexture.h @@ -0,0 +1,150 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006-2007 Rivo Laks + SPDX-FileCopyrightText: 2010, 2011 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_GLTEXTURE_H +#define KWIN_GLTEXTURE_H + +#include + +#include +#include +#include +#include +#include + +#include + +class QImage; +class QPixmap; + +/** @addtogroup kwineffects */ +/** @{ */ + +namespace KWin +{ + +class GLVertexBuffer; +class GLTexturePrivate; + +enum TextureCoordinateType { + NormalizedCoordinates = 0, + UnnormalizedCoordinates +}; + +class KWINGLUTILS_EXPORT GLTexture +{ +public: + GLTexture(); + GLTexture(const GLTexture& tex); + explicit GLTexture(const QImage& image, GLenum target = GL_TEXTURE_2D); + explicit GLTexture(const QPixmap& pixmap, GLenum target = GL_TEXTURE_2D); + explicit GLTexture(const QString& fileName); + GLTexture(GLenum internalFormat, int width, int height, int levels = 1, bool needsMutability = false); + explicit GLTexture(GLenum internalFormat, const QSize &size, int levels = 1, bool needsMutability = false); + + /** + * Create a GLTexture wrapper around an existing texture. + * Management of the underlying texture remains the responsibility of the caller. + * @since 5.18 + */ + explicit GLTexture(GLuint textureId, GLenum internalFormat, const QSize &size, int levels = 1); + virtual ~GLTexture(); + + GLTexture & operator = (const GLTexture& tex); + + bool isNull() const; + QSize size() const; + int width() const; + int height() const; + /** + * @since 4.7 + */ + bool isYInverted() const; + /** + * @since 4.8 + */ + void setYInverted(bool inverted); + + /** + * Specifies which component of a texel is placed in each respective + * component of the vector returned to the shader. + * + * Valid values are GL_RED, GL_GREEN, GL_BLUE, GL_ALPHA, GL_ONE and GL_ZERO. + * + * @see swizzleSupported() + * @since 5.2 + */ + void setSwizzle(GLenum red, GLenum green, GLenum blue, GLenum alpha); + + /** + * Returns a matrix that transforms texture coordinates of the given type, + * taking the texture target and the y-inversion flag into account. + * + * @since 4.11 + */ + QMatrix4x4 matrix(TextureCoordinateType type) const; + + void update(const QImage& image, const QPoint &offset = QPoint(0, 0), const QRect &src = QRect()); + virtual void discard(); + void bind(); + void unbind(); + void render(const QRegion ®ion, const QRect& rect, bool hardwareClipping = false); + + GLuint texture() const; + GLenum target() const; + GLenum filter() const; + GLenum internalFormat() const; + + QImage toImage() const; + + /** @short + * Make the texture fully transparent + */ + void clear(); + bool isDirty() const; + void setFilter(GLenum filter); + void setWrapMode(GLenum mode); + void setDirty(); + + void generateMipmaps(); + + static bool framebufferObjectSupported(); + + /** + * Returns true if texture swizzle is supported, and false otherwise + * + * Texture swizzle requires OpenGL 3.3, GL_ARB_texture_swizzle, or OpenGL ES 3.0. + * + * @since 5.2 + */ + static bool supportsSwizzle(); + + /** + * Returns @c true if texture formats R* are supported, and @c false otherwise. + * + * This requires OpenGL 3.0, GL_ARB_texture_rg or OpenGL ES 3.0 or GL_EXT_texture_rg. + * + * @since 5.2.1 + */ + static bool supportsFormatRG(); + +protected: + QExplicitlySharedDataPointer d_ptr; + GLTexture(GLTexturePrivate& dd); + +private: + Q_DECLARE_PRIVATE(GLTexture) +}; + +} // namespace + +/** @} */ + +#endif diff --git a/libkwineffects/kwingltexture_p.h b/libkwineffects/kwingltexture_p.h new file mode 100644 index 0000000..fdeb7d0 --- /dev/null +++ b/libkwineffects/kwingltexture_p.h @@ -0,0 +1,81 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006-2007 Rivo Laks + SPDX-FileCopyrightText: 2010, 2011 Martin Gräßlin + SPDX-FileCopyrightText: 2011 Philipp Knechtges + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_GLTEXTURE_P_H +#define KWIN_GLTEXTURE_P_H + +#include "kwinconfig.h" // KWIN_HAVE_OPENGL +#include "kwinglutils.h" +#include + +#include +#include +#include +#include +#include + +namespace KWin +{ +// forward declarations +class GLVertexBuffer; + +class KWINGLUTILS_EXPORT GLTexturePrivate + : public QSharedData +{ +public: + GLTexturePrivate(); + virtual ~GLTexturePrivate(); + + virtual void onDamage(); + + void updateMatrix(); + + GLuint m_texture; + GLenum m_target; + GLenum m_internalFormat; + GLenum m_filter; + GLenum m_wrapMode; + QSize m_size; + QSizeF m_scale; // to un-normalize GL_TEXTURE_2D + QMatrix4x4 m_matrix[2]; + bool m_yInverted; // texture is y-inverted + bool m_canUseMipmaps; + bool m_markedDirty; + bool m_filterChanged; + bool m_wrapModeChanged; + bool m_immutable; + bool m_foreign; + int m_mipLevels; + + int m_unnormalizeActive; // 0 - no, otherwise refcount + int m_normalizeActive; // 0 - no, otherwise refcount + GLVertexBuffer* m_vbo; + QSize m_cachedSize; + + static void initStatic(); + + static bool s_supportsFramebufferObjects; + static bool s_supportsARGB32; + static bool s_supportsUnpack; + static bool s_supportsTextureStorage; + static bool s_supportsTextureSwizzle; + static bool s_supportsTextureFormatRG; + static GLuint s_fbo; + static uint s_textureObjectCounter; +private: + friend void KWin::cleanupGL(); + static void cleanup(); + Q_DISABLE_COPY(GLTexturePrivate) +}; + +} // namespace + +#endif diff --git a/libkwineffects/kwinglutils.cpp b/libkwineffects/kwinglutils.cpp new file mode 100644 index 0000000..a5f6050 --- /dev/null +++ b/libkwineffects/kwinglutils.cpp @@ -0,0 +1,2308 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006-2007 Rivo Laks + SPDX-FileCopyrightText: 2010, 2011 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwinglutils.h" + +// need to call GLTexturePrivate::initStatic() +#include "kwingltexture_p.h" + +#include "kwineffects.h" +#include "kwinglplatform.h" +#include "logging_p.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#define DEBUG_GLRENDERTARGET 0 + +#ifdef __GNUC__ +# define likely(x) __builtin_expect(!!(x), 1) +# define unlikely(x) __builtin_expect(!!(x), 0) +#else +# define likely(x) (x) +# define unlikely(x) (x) +#endif + +namespace KWin +{ +// Variables +// List of all supported GL extensions +static QList glExtensions; + + +// Functions + +void initGL(const std::function &resolveFunction) +{ + // Get list of supported OpenGL extensions + if (hasGLVersion(3, 0)) { + int count; + glGetIntegerv(GL_NUM_EXTENSIONS, &count); + + for (int i = 0; i < count; i++) { + const QByteArray name = (const char *) glGetStringi(GL_EXTENSIONS, i); + glExtensions << name; + } + } else + glExtensions = QByteArray((const char*)glGetString(GL_EXTENSIONS)).split(' '); + + // handle OpenGL extensions functions + glResolveFunctions(resolveFunction); + + GLTexturePrivate::initStatic(); + GLRenderTarget::initStatic(); + GLVertexBuffer::initStatic(); +} + +void cleanupGL() +{ + ShaderManager::cleanup(); + GLTexturePrivate::cleanup(); + GLRenderTarget::cleanup(); + GLVertexBuffer::cleanup(); + GLPlatform::cleanup(); + + glExtensions.clear(); +} + +bool hasGLVersion(int major, int minor, int release) +{ + return GLPlatform::instance()->glVersion() >= kVersionNumber(major, minor, release); +} + +bool hasGLExtension(const QByteArray &extension) +{ + return glExtensions.contains(extension); +} + +QList openGLExtensions() +{ + return glExtensions; +} + +static QString formatGLError(GLenum err) +{ + switch(err) { + case GL_NO_ERROR: return QStringLiteral("GL_NO_ERROR"); + case GL_INVALID_ENUM: return QStringLiteral("GL_INVALID_ENUM"); + case GL_INVALID_VALUE: return QStringLiteral("GL_INVALID_VALUE"); + case GL_INVALID_OPERATION: return QStringLiteral("GL_INVALID_OPERATION"); + case GL_STACK_OVERFLOW: return QStringLiteral("GL_STACK_OVERFLOW"); + case GL_STACK_UNDERFLOW: return QStringLiteral("GL_STACK_UNDERFLOW"); + case GL_OUT_OF_MEMORY: return QStringLiteral("GL_OUT_OF_MEMORY"); + default: return QLatin1String("0x") + QString::number(err, 16); + } +} + +bool checkGLError(const char* txt) +{ + GLenum err = glGetError(); + if (err == GL_CONTEXT_LOST) { + qCWarning(LIBKWINGLUTILS) << "GL error: context lost"; + return true; + } + bool hasError = false; + while (err != GL_NO_ERROR) { + qCWarning(LIBKWINGLUTILS) << "GL error (" << txt << "): " << formatGLError(err); + hasError = true; + err = glGetError(); + if (err == GL_CONTEXT_LOST) { + qCWarning(LIBKWINGLUTILS) << "GL error: context lost"; + break; + } + } + return hasError; +} + +//**************************************** +// GLShader +//**************************************** + +GLShader::GLShader(unsigned int flags) + : mValid(false) + , mLocationsResolved(false) + , mExplicitLinking(flags & ExplicitLinking) +{ + mProgram = glCreateProgram(); +} + +GLShader::GLShader(const QString& vertexfile, const QString& fragmentfile, unsigned int flags) + : mValid(false) + , mLocationsResolved(false) + , mExplicitLinking(flags & ExplicitLinking) +{ + mProgram = glCreateProgram(); + loadFromFiles(vertexfile, fragmentfile); +} + +GLShader::~GLShader() +{ + if (mProgram) { + glDeleteProgram(mProgram); + } +} + +bool GLShader::loadFromFiles(const QString &vertexFile, const QString &fragmentFile) +{ + QFile vf(vertexFile); + if (!vf.open(QIODevice::ReadOnly)) { + qCCritical(LIBKWINGLUTILS) << "Couldn't open" << vertexFile << "for reading!"; + return false; + } + const QByteArray vertexSource = vf.readAll(); + + QFile ff(fragmentFile); + if (!ff.open(QIODevice::ReadOnly)) { + qCCritical(LIBKWINGLUTILS) << "Couldn't open" << fragmentFile << "for reading!"; + return false; + } + const QByteArray fragmentSource = ff.readAll(); + + return load(vertexSource, fragmentSource); +} + +bool GLShader::link() +{ + // Be optimistic + mValid = true; + + glLinkProgram(mProgram); + + // Get the program info log + int maxLength, length; + glGetProgramiv(mProgram, GL_INFO_LOG_LENGTH, &maxLength); + + QByteArray log(maxLength, 0); + glGetProgramInfoLog(mProgram, maxLength, &length, log.data()); + + // Make sure the program linked successfully + int status; + glGetProgramiv(mProgram, GL_LINK_STATUS, &status); + + if (status == 0) { + qCCritical(LIBKWINGLUTILS) << "Failed to link shader:" << "\n" << log; + mValid = false; + } else if (length > 0) { + qCDebug(LIBKWINGLUTILS) << "Shader link log:" << log; + } + + return mValid; +} + +const QByteArray GLShader::prepareSource(GLenum shaderType, const QByteArray &source) const +{ + Q_UNUSED(shaderType) + // Prepare the source code + QByteArray ba; + if (GLPlatform::instance()->isGLES() && GLPlatform::instance()->glslVersion() < kVersionNumber(3, 0)) { + ba.append("precision highp float;\n"); + } + ba.append(source); + if (GLPlatform::instance()->isGLES() && GLPlatform::instance()->glslVersion() >= kVersionNumber(3, 0)) { + ba.replace("#version 140", "#version 300 es\n\nprecision highp float;\n"); + } + + return ba; +} + +bool GLShader::compile(GLuint program, GLenum shaderType, const QByteArray &source) const +{ + GLuint shader = glCreateShader(shaderType); + + QByteArray preparedSource = prepareSource(shaderType, source); + const char* src = preparedSource.constData(); + glShaderSource(shader, 1, &src, nullptr); + + // Compile the shader + glCompileShader(shader); + + // Get the shader info log + int maxLength, length; + glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &maxLength); + + QByteArray log(maxLength, 0); + glGetShaderInfoLog(shader, maxLength, &length, log.data()); + + // Check the status + int status; + glGetShaderiv(shader, GL_COMPILE_STATUS, &status); + + if (status == 0) { + const char *typeName = (shaderType == GL_VERTEX_SHADER ? "vertex" : "fragment"); + qCCritical(LIBKWINGLUTILS) << "Failed to compile" << typeName << "shader:" << "\n" << log; + } else if (length > 0) + qCDebug(LIBKWINGLUTILS) << "Shader compile log:" << log; + + if (status != 0) + glAttachShader(program, shader); + + glDeleteShader(shader); + return status != 0; +} + +bool GLShader::load(const QByteArray &vertexSource, const QByteArray &fragmentSource) +{ + // Make sure shaders are actually supported + if (!(GLPlatform::instance()->supports(GLSL) && + // we lack shader branching for Texture2DRectangle everywhere - and it's probably not worth it + GLPlatform::instance()->supports(TextureNPOT))) { + qCCritical(LIBKWINGLUTILS) << "Shaders are not supported"; + return false; + } + + mValid = false; + + // Compile the vertex shader + if (!vertexSource.isEmpty()) { + bool success = compile(mProgram, GL_VERTEX_SHADER, vertexSource); + + if (!success) + return false; + } + + // Compile the fragment shader + if (!fragmentSource.isEmpty()) { + bool success = compile(mProgram, GL_FRAGMENT_SHADER, fragmentSource); + + if (!success) + return false; + } + + if (mExplicitLinking) + return true; + + // link() sets mValid + return link(); +} + +void GLShader::bindAttributeLocation(const char *name, int index) +{ + glBindAttribLocation(mProgram, index, name); +} + +void GLShader::bindFragDataLocation(const char *name, int index) +{ + if (!GLPlatform::instance()->isGLES() && (hasGLVersion(3, 0) || hasGLExtension(QByteArrayLiteral("GL_EXT_gpu_shader4")))) + glBindFragDataLocation(mProgram, index, name); +} + +void GLShader::bind() +{ + glUseProgram(mProgram); +} + +void GLShader::unbind() +{ + glUseProgram(0); +} + +void GLShader::resolveLocations() +{ + if (mLocationsResolved) + return; + + mMatrixLocation[TextureMatrix] = uniformLocation("textureMatrix"); + mMatrixLocation[ProjectionMatrix] = uniformLocation("projection"); + mMatrixLocation[ModelViewMatrix] = uniformLocation("modelview"); + mMatrixLocation[ModelViewProjectionMatrix] = uniformLocation("modelViewProjectionMatrix"); + mMatrixLocation[WindowTransformation] = uniformLocation("windowTransformation"); + mMatrixLocation[ScreenTransformation] = uniformLocation("screenTransformation"); + + mVec2Location[Offset] = uniformLocation("offset"); + + mVec4Location[ModulationConstant] = uniformLocation("modulation"); + + mFloatLocation[Saturation] = uniformLocation("saturation"); + + mColorLocation[Color] = uniformLocation("geometryColor"); + mVec4Location[TextureClamp] = uniformLocation("textureClamp"); + + mLocationsResolved = true; +} + +int GLShader::uniformLocation(const char *name) +{ + const int location = glGetUniformLocation(mProgram, name); + return location; +} + +bool GLShader::setUniform(GLShader::MatrixUniform uniform, const QMatrix4x4 &matrix) +{ + resolveLocations(); + return setUniform(mMatrixLocation[uniform], matrix); +} + +bool GLShader::setUniform(GLShader::Vec2Uniform uniform, const QVector2D &value) +{ + resolveLocations(); + return setUniform(mVec2Location[uniform], value); +} + +bool GLShader::setUniform(GLShader::Vec4Uniform uniform, const QVector4D &value) +{ + resolveLocations(); + return setUniform(mVec4Location[uniform], value); +} + +bool GLShader::setUniform(GLShader::FloatUniform uniform, float value) +{ + resolveLocations(); + return setUniform(mFloatLocation[uniform], value); +} + +bool GLShader::setUniform(GLShader::IntUniform uniform, int value) +{ + resolveLocations(); + return setUniform(mIntLocation[uniform], value); +} + +bool GLShader::setUniform(GLShader::ColorUniform uniform, const QVector4D &value) +{ + resolveLocations(); + return setUniform(mColorLocation[uniform], value); +} + +bool GLShader::setUniform(GLShader::ColorUniform uniform, const QColor &value) +{ + resolveLocations(); + return setUniform(mColorLocation[uniform], value); +} + +bool GLShader::setUniform(const char *name, float value) +{ + const int location = uniformLocation(name); + return setUniform(location, value); +} + +bool GLShader::setUniform(const char *name, int value) +{ + const int location = uniformLocation(name); + return setUniform(location, value); +} + +bool GLShader::setUniform(const char *name, const QVector2D& value) +{ + const int location = uniformLocation(name); + return setUniform(location, value); +} + +bool GLShader::setUniform(const char *name, const QVector3D& value) +{ + const int location = uniformLocation(name); + return setUniform(location, value); +} + +bool GLShader::setUniform(const char *name, const QVector4D& value) +{ + const int location = uniformLocation(name); + return setUniform(location, value); +} + +bool GLShader::setUniform(const char *name, const QMatrix4x4& value) +{ + const int location = uniformLocation(name); + return setUniform(location, value); +} + +bool GLShader::setUniform(const char *name, const QColor& color) +{ + const int location = uniformLocation(name); + return setUniform(location, color); +} + +bool GLShader::setUniform(int location, float value) +{ + if (location >= 0) { + glUniform1f(location, value); + } + return (location >= 0); +} + +bool GLShader::setUniform(int location, int value) +{ + if (location >= 0) { + glUniform1i(location, value); + } + return (location >= 0); +} + +bool GLShader::setUniform(int location, const QVector2D &value) +{ + if (location >= 0) { + glUniform2fv(location, 1, (const GLfloat*)&value); + } + return (location >= 0); +} + +bool GLShader::setUniform(int location, const QVector3D &value) +{ + if (location >= 0) { + glUniform3fv(location, 1, (const GLfloat*)&value); + } + return (location >= 0); +} + +bool GLShader::setUniform(int location, const QVector4D &value) +{ + if (location >= 0) { + glUniform4fv(location, 1, (const GLfloat*)&value); + } + return (location >= 0); +} + +bool GLShader::setUniform(int location, const QMatrix4x4 &value) +{ + if (location >= 0) { + GLfloat m[16]; + const auto *data = value.constData(); + // i is column, j is row for m + for (int i = 0; i < 16; ++i) { + m[i] = data[i]; + } + glUniformMatrix4fv(location, 1, GL_FALSE, m); + } + return (location >= 0); +} + +bool GLShader::setUniform(int location, const QColor &color) +{ + if (location >= 0) { + glUniform4f(location, color.redF(), color.greenF(), color.blueF(), color.alphaF()); + } + return (location >= 0); +} + +int GLShader::attributeLocation(const char* name) +{ + int location = glGetAttribLocation(mProgram, name); + return location; +} + +bool GLShader::setAttribute(const char* name, float value) +{ + int location = attributeLocation(name); + if (location >= 0) { + glVertexAttrib1f(location, value); + } + return (location >= 0); +} + +QMatrix4x4 GLShader::getUniformMatrix4x4(const char* name) +{ + int location = uniformLocation(name); + if (location >= 0) { + GLfloat m[16]; + glGetnUniformfv(mProgram, location, sizeof(m), m); + QMatrix4x4 matrix(m[0], m[4], m[8], m[12], + m[1], m[5], m[9], m[13], + m[2], m[6], m[10], m[14], + m[3], m[7], m[11], m[15]); + matrix.optimize(); + return matrix; + } else { + return QMatrix4x4(); + } +} + +//**************************************** +// ShaderManager +//**************************************** +ShaderManager *ShaderManager::s_shaderManager = nullptr; + +ShaderManager *ShaderManager::instance() +{ + if (!s_shaderManager) { + s_shaderManager = new ShaderManager(); + } + return s_shaderManager; +} + +void ShaderManager::cleanup() +{ + delete s_shaderManager; + s_shaderManager = nullptr; +} + +ShaderManager::ShaderManager() +{ + const qint64 coreVersionNumber = GLPlatform::instance()->isGLES() ? kVersionNumber(3, 0) : kVersionNumber(1, 40); + if (GLPlatform::instance()->glslVersion() >= coreVersionNumber) { + m_resourcePath = QStringLiteral(":/effect-shaders-1.40/"); + } else { + m_resourcePath = QStringLiteral(":/effect-shaders-1.10/"); + } +} + +ShaderManager::~ShaderManager() +{ + while (!m_boundShaders.isEmpty()) { + popShader(); + } + + qDeleteAll(m_shaderHash); + m_shaderHash.clear(); +} + +static bool fuzzyCompare(const QVector4D &lhs, const QVector4D &rhs) +{ + const float epsilon = 1.0f / 255.0f; + + return lhs[0] >= rhs[0] - epsilon && lhs[0] <= rhs[0] + epsilon && + lhs[1] >= rhs[1] - epsilon && lhs[1] <= rhs[1] + epsilon && + lhs[2] >= rhs[2] - epsilon && lhs[2] <= rhs[2] + epsilon && + lhs[3] >= rhs[3] - epsilon && lhs[3] <= rhs[3] + epsilon; +} + +static bool checkPixel(int x, int y, const QVector4D &expected, const char *file, int line) +{ + uint8_t data[4]; + glReadnPixels(x, y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, 4, data); + + const QVector4D pixel{data[0] / 255.f, data[1] / 255.f, data[2] / 255.f, data[3] / 255.f}; + + if (fuzzyCompare(pixel, expected)) + return true; + + QMessageLogger(file, line, nullptr).warning() << "Pixel was" << pixel << "expected" << expected; + return false; +} + +#define CHECK_PIXEL(x, y, expected) \ + checkPixel(x, y, expected, __FILE__, __LINE__) + +static QVector4D adjustSaturation(const QVector4D &color, float saturation) +{ + const float gray = QVector3D::dotProduct(color.toVector3D(), {0.2126, 0.7152, 0.0722}); + return QVector4D{gray, gray, gray, color.w()} * (1.0f - saturation) + color * saturation; +} + +bool ShaderManager::selfTest() +{ + bool pass = true; + + if (!GLRenderTarget::supported()) { + qCWarning(LIBKWINGLUTILS) << "Framebuffer objects not supported - skipping shader tests"; + return true; + } + if (GLPlatform::instance()->isNvidia() && GLPlatform::instance()->glRendererString().contains("Quadro")) { + qCWarning(LIBKWINGLUTILS) << "Skipping self test as it is reported to return false positive results on Quadro hardware"; + return true; + } + if (GLPlatform::instance()->isMesaDriver() && GLPlatform::instance()->mesaVersion() >= kVersionNumber(17, 0)) { + qCWarning(LIBKWINGLUTILS) << "Skipping self test as it is reported to return false positive results on Mesa drivers"; + return true; + } + + // Create the source texture + QImage image(2, 2, QImage::Format_ARGB32_Premultiplied); + image.setPixel(0, 0, 0xffff0000); // Red + image.setPixel(1, 0, 0xff00ff00); // Green + image.setPixel(0, 1, 0xff0000ff); // Blue + image.setPixel(1, 1, 0xffffffff); // White + + GLTexture src(image); + src.setFilter(GL_NEAREST); + + // Create the render target + GLTexture dst(GL_RGBA8, 32, 32); + + GLRenderTarget fbo(dst); + GLRenderTarget::pushRenderTarget(&fbo); + + // Set up the vertex buffer + GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer(); + + const GLVertexAttrib attribs[] { + { VA_Position, 2, GL_FLOAT, offsetof(GLVertex2D, position) }, + { VA_TexCoord, 2, GL_FLOAT, offsetof(GLVertex2D, texcoord) }, + }; + + vbo->setAttribLayout(attribs, 2, sizeof(GLVertex2D)); + + GLVertex2D *verts = (GLVertex2D*) vbo->map(6 * sizeof(GLVertex2D)); + verts[0] = GLVertex2D{{0, 0}, {0, 0}}; // Top left + verts[1] = GLVertex2D{{0, 32}, {0, 1}}; // Bottom left + verts[2] = GLVertex2D{{32, 0}, {1, 0}}; // Top right + + verts[3] = GLVertex2D{{32, 0}, {1, 0}}; // Top right + verts[4] = GLVertex2D{{0, 32}, {0, 1}}; // Bottom left + verts[5] = GLVertex2D{{32, 32}, {1, 1}}; // Bottom right + vbo->unmap(); + + vbo->bindArrays(); + + glViewport(0, 0, 32, 32); + glClearColor(0, 0, 0, 0); + + // Set up the projection matrix + QMatrix4x4 matrix; + matrix.ortho(QRect(0, 0, 32, 32)); + + // Bind the source texture + src.bind(); + + const QVector4D red {1.0f, 0.0f, 0.0f, 1.0f}; + const QVector4D green {0.0f, 1.0f, 0.0f, 1.0f}; + const QVector4D blue {0.0f, 0.0f, 1.0f, 1.0f}; + const QVector4D white {1.0f, 1.0f, 1.0f, 1.0f}; + + // Note: To see the line number in error messages, set + // QT_MESSAGE_PATTERN="%{message} (%{file}:%{line})" + + // Test solid color + GLShader *shader = pushShader(ShaderTrait::UniformColor); + if (shader->isValid()) { + glClear(GL_COLOR_BUFFER_BIT); + + shader->setUniform(GLShader::ModelViewProjectionMatrix, matrix); + shader->setUniform(GLShader::Color, green); + vbo->draw(GL_TRIANGLES, 0, 6); + + pass = CHECK_PIXEL(8, 24, green) && pass; + pass = CHECK_PIXEL(24, 24, green) && pass; + pass = CHECK_PIXEL(8, 8, green) && pass; + pass = CHECK_PIXEL(24, 8, green) && pass; + } else { + pass = false; + } + popShader(); + + // Test texture mapping + shader = pushShader(ShaderTrait::MapTexture); + if (shader->isValid()) { + glClear(GL_COLOR_BUFFER_BIT); + + shader->setUniform(GLShader::ModelViewProjectionMatrix, matrix); + vbo->draw(GL_TRIANGLES, 0, 6); + + pass = CHECK_PIXEL(8, 24, red) && pass; + pass = CHECK_PIXEL(24, 24, green) && pass; + pass = CHECK_PIXEL(8, 8, blue) && pass; + pass = CHECK_PIXEL(24, 8, white) && pass; + } else { + pass = false; + } + popShader(); + + // Test saturation filter + shader = pushShader(ShaderTrait::MapTexture | ShaderTrait::AdjustSaturation); + if (shader->isValid()) { + glClear(GL_COLOR_BUFFER_BIT); + + const float saturation = .3; + + shader->setUniform(GLShader::ModelViewProjectionMatrix, matrix); + shader->setUniform(GLShader::Saturation, saturation); + vbo->draw(GL_TRIANGLES, 0, 6); + + pass = CHECK_PIXEL(8, 24, adjustSaturation(red, saturation)) && pass; + pass = CHECK_PIXEL(24, 24, adjustSaturation(green, saturation)) && pass; + pass = CHECK_PIXEL(8, 8, adjustSaturation(blue, saturation)) && pass; + pass = CHECK_PIXEL(24, 8, adjustSaturation(white, saturation)) && pass; + } else { + pass = false; + } + popShader(); + + // Test modulation filter + shader = pushShader(ShaderTrait::MapTexture | ShaderTrait::Modulate); + if (shader->isValid()) { + glClear(GL_COLOR_BUFFER_BIT); + + const QVector4D modulation{.3f, .4f, .5f, .6f}; + + shader->setUniform(GLShader::ModelViewProjectionMatrix, matrix); + shader->setUniform(GLShader::ModulationConstant, modulation); + vbo->draw(GL_TRIANGLES, 0, 6); + + pass = CHECK_PIXEL(8, 24, red * modulation) && pass; + pass = CHECK_PIXEL(24, 24, green * modulation) && pass; + pass = CHECK_PIXEL(8, 8, blue * modulation) && pass; + pass = CHECK_PIXEL(24, 8, white * modulation) && pass; + } else { + pass = false; + } + popShader(); + + // Test saturation + modulation + shader = pushShader(ShaderTrait::MapTexture | ShaderTrait::AdjustSaturation | ShaderTrait::Modulate); + if (shader->isValid()) { + glClear(GL_COLOR_BUFFER_BIT); + + const QVector4D modulation{.3f, .4f, .5f, .6f}; + const float saturation = .3; + + shader->setUniform(GLShader::ModelViewProjectionMatrix, matrix); + shader->setUniform(GLShader::ModulationConstant, modulation); + shader->setUniform(GLShader::Saturation, saturation); + vbo->draw(GL_TRIANGLES, 0, 6); + + pass = CHECK_PIXEL(8, 24, adjustSaturation(red * modulation, saturation)) && pass; + pass = CHECK_PIXEL(24, 24, adjustSaturation(green * modulation, saturation)) && pass; + pass = CHECK_PIXEL(8, 8, adjustSaturation(blue * modulation, saturation)) && pass; + pass = CHECK_PIXEL(24, 8, adjustSaturation(white * modulation, saturation)) && pass; + } else { + pass = false; + } + popShader(); + + vbo->unbindArrays(); + GLRenderTarget::popRenderTarget(); + + return pass; +} + +QByteArray ShaderManager::generateVertexSource(ShaderTraits traits) const +{ + QByteArray source; + QTextStream stream(&source); + + GLPlatform * const gl = GLPlatform::instance(); + QByteArray attribute, varying; + + if (!gl->isGLES()) { + const bool glsl_140 = gl->glslVersion() >= kVersionNumber(1, 40); + + attribute = glsl_140 ? QByteArrayLiteral("in") : QByteArrayLiteral("attribute"); + varying = glsl_140 ? QByteArrayLiteral("out") : QByteArrayLiteral("varying"); + + if (glsl_140) + stream << "#version 140\n\n"; + } else { + const bool glsl_es_300 = gl->glslVersion() >= kVersionNumber(3, 0); + + attribute = glsl_es_300 ? QByteArrayLiteral("in") : QByteArrayLiteral("attribute"); + varying = glsl_es_300 ? QByteArrayLiteral("out") : QByteArrayLiteral("varying"); + + if (glsl_es_300) + stream << "#version 300 es\n\n"; + } + + stream << attribute << " vec4 position;\n"; + if (traits & ShaderTrait::MapTexture) { + stream << attribute << " vec4 texcoord;\n\n"; + stream << varying << " vec2 texcoord0;\n\n"; + } else + stream << "\n"; + + stream << "uniform mat4 modelViewProjectionMatrix;\n\n"; + + stream << "void main()\n{\n"; + if (traits & ShaderTrait::MapTexture) + stream << " texcoord0 = texcoord.st;\n"; + + stream << " gl_Position = modelViewProjectionMatrix * position;\n"; + stream << "}\n"; + + stream.flush(); + return source; +} + +QByteArray ShaderManager::generateFragmentSource(ShaderTraits traits) const +{ + QByteArray source; + QTextStream stream(&source); + + GLPlatform * const gl = GLPlatform::instance(); + QByteArray varying, output, textureLookup; + + if (!gl->isGLES()) { + const bool glsl_140 = gl->glslVersion() >= kVersionNumber(1, 40); + + if (glsl_140) + stream << "#version 140\n\n"; + + varying = glsl_140 ? QByteArrayLiteral("in") : QByteArrayLiteral("varying"); + textureLookup = glsl_140 ? QByteArrayLiteral("texture") : QByteArrayLiteral("texture2D"); + output = glsl_140 ? QByteArrayLiteral("fragColor") : QByteArrayLiteral("gl_FragColor"); + } else { + const bool glsl_es_300 = GLPlatform::instance()->glslVersion() >= kVersionNumber(3, 0); + + if (glsl_es_300) + stream << "#version 300 es\n\n"; + + // From the GLSL ES specification: + // + // "The fragment language has no default precision qualifier for floating point types." + stream << "precision highp float;\n\n"; + + varying = glsl_es_300 ? QByteArrayLiteral("in") : QByteArrayLiteral("varying"); + textureLookup = glsl_es_300 ? QByteArrayLiteral("texture") : QByteArrayLiteral("texture2D"); + output = glsl_es_300 ? QByteArrayLiteral("fragColor") : QByteArrayLiteral("gl_FragColor"); + } + + if (traits & ShaderTrait::MapTexture) { + stream << "uniform sampler2D sampler;\n"; + + if (traits & ShaderTrait::Modulate) + stream << "uniform vec4 modulation;\n"; + if (traits & ShaderTrait::AdjustSaturation) + stream << "uniform float saturation;\n"; + + stream << "\n" << varying << " vec2 texcoord0;\n"; + + } else if (traits & ShaderTrait::UniformColor) + stream << "uniform vec4 geometryColor;\n"; + + if (traits & ShaderTrait::ClampTexture) { + stream << "uniform vec4 textureClamp;\n"; + } + + if (output != QByteArrayLiteral("gl_FragColor")) + stream << "\nout vec4 " << output << ";\n"; + + stream << "\nvoid main(void)\n{\n"; + if (traits & ShaderTrait::MapTexture) { + stream << "vec2 texcoordC = texcoord0;\n"; + + if (traits & ShaderTrait::ClampTexture) { + stream << "texcoordC.x = clamp(texcoordC.x, textureClamp.x, textureClamp.z);\n"; + stream << "texcoordC.y = clamp(texcoordC.y, textureClamp.y, textureClamp.w);\n"; + } + + if (traits & (ShaderTrait::Modulate | ShaderTrait::AdjustSaturation)) { + stream << " vec4 texel = " << textureLookup << "(sampler, texcoordC);\n"; + if (traits & ShaderTrait::Modulate) + stream << " texel *= modulation;\n"; + if (traits & ShaderTrait::AdjustSaturation) + stream << " texel.rgb = mix(vec3(dot(texel.rgb, vec3(0.2126, 0.7152, 0.0722))), texel.rgb, saturation);\n"; + + stream << " " << output << " = texel;\n"; + } else { + stream << " " << output << " = " << textureLookup << "(sampler, texcoordC);\n"; + } + } else if (traits & ShaderTrait::UniformColor) + stream << " " << output << " = geometryColor;\n"; + + stream << "}"; + stream.flush(); + return source; +} + +GLShader *ShaderManager::generateShader(ShaderTraits traits) +{ + return generateCustomShader(traits); +} + +GLShader *ShaderManager::generateCustomShader(ShaderTraits traits, const QByteArray &vertexSource, const QByteArray &fragmentSource) +{ + const QByteArray vertex = vertexSource.isEmpty() ? generateVertexSource(traits) : vertexSource; + const QByteArray fragment = fragmentSource.isEmpty() ? generateFragmentSource(traits) : fragmentSource; + +#if 0 + qCDebug(LIBKWINGLUTILS) << "**************"; + qCDebug(LIBKWINGLUTILS) << vertex; + qCDebug(LIBKWINGLUTILS) << "**************"; + qCDebug(LIBKWINGLUTILS) << fragment; + qCDebug(LIBKWINGLUTILS) << "**************"; +#endif + + GLShader *shader = new GLShader(GLShader::ExplicitLinking); + shader->load(vertex, fragment); + + shader->bindAttributeLocation("position", VA_Position); + shader->bindAttributeLocation("texcoord", VA_TexCoord); + shader->bindFragDataLocation("fragColor", 0); + + shader->link(); + return shader; +} + +GLShader *ShaderManager::generateShaderFromResources(ShaderTraits traits, const QString &vertexFile, const QString &fragmentFile) +{ + auto loadShaderFile = [this] (const QString &fileName) { + QFile file(m_resourcePath + fileName); + if (file.open(QIODevice::ReadOnly)) { + return file.readAll(); + } + qCCritical(LIBKWINGLUTILS) << "Failed to read shader " << fileName; + return QByteArray(); + }; + QByteArray vertexSource; + QByteArray fragmentSource; + if (!vertexFile.isEmpty()) { + vertexSource = loadShaderFile(vertexFile); + if (vertexSource.isEmpty()) { + return new GLShader(); + } + } + if (!fragmentFile.isEmpty()) { + fragmentSource = loadShaderFile(fragmentFile); + if (fragmentSource.isEmpty()) { + return new GLShader(); + } + } + return generateCustomShader(traits, vertexSource, fragmentSource); +} + +GLShader *ShaderManager::shader(ShaderTraits traits) +{ + GLShader *shader = m_shaderHash.value(traits); + + if (!shader) { + shader = generateShader(traits); + m_shaderHash.insert(traits, shader); + } + + return shader; +} + +GLShader *ShaderManager::getBoundShader() const +{ + if (m_boundShaders.isEmpty()) { + return nullptr; + } else { + return m_boundShaders.top(); + } +} + +bool ShaderManager::isShaderBound() const +{ + return !m_boundShaders.isEmpty(); +} + +GLShader *ShaderManager::pushShader(ShaderTraits traits) +{ + GLShader *shader = this->shader(traits); + pushShader(shader); + return shader; +} + + +void ShaderManager::pushShader(GLShader *shader) +{ + // only bind shader if it is not already bound + if (shader != getBoundShader()) { + shader->bind(); + } + m_boundShaders.push(shader); +} + +void ShaderManager::popShader() +{ + if (m_boundShaders.isEmpty()) { + return; + } + GLShader *shader = m_boundShaders.pop(); + if (m_boundShaders.isEmpty()) { + // no more shader bound - unbind + shader->unbind(); + } else if (shader != m_boundShaders.top()) { + // only rebind if a different shader is on top of stack + m_boundShaders.top()->bind(); + } +} + +void ShaderManager::bindFragDataLocations(GLShader *shader) +{ + shader->bindFragDataLocation("fragColor", 0); +} + +void ShaderManager::bindAttributeLocations(GLShader *shader) const +{ + shader->bindAttributeLocation("vertex", VA_Position); + shader->bindAttributeLocation("texCoord", VA_TexCoord); +} + +GLShader *ShaderManager::loadShaderFromCode(const QByteArray &vertexSource, const QByteArray &fragmentSource) +{ + GLShader *shader = new GLShader(GLShader::ExplicitLinking); + shader->load(vertexSource, fragmentSource); + bindAttributeLocations(shader); + bindFragDataLocations(shader); + shader->link(); + return shader; +} + +/*** GLRenderTarget ***/ +bool GLRenderTarget::sSupported = false; +bool GLRenderTarget::s_blitSupported = false; +QStack GLRenderTarget::s_renderTargets = QStack(); +QSize GLRenderTarget::s_virtualScreenSize; +QRect GLRenderTarget::s_virtualScreenGeometry; +qreal GLRenderTarget::s_virtualScreenScale = 1.0; +GLint GLRenderTarget::s_virtualScreenViewport[4]; +GLuint GLRenderTarget::s_kwinFramebuffer = 0; + +void GLRenderTarget::initStatic() +{ + if (GLPlatform::instance()->isGLES()) { + sSupported = true; + s_blitSupported = hasGLVersion(3, 0); + } else { + sSupported = hasGLVersion(3, 0) || + hasGLExtension(QByteArrayLiteral("GL_ARB_framebuffer_object")) || + hasGLExtension(QByteArrayLiteral("GL_EXT_framebuffer_object")); + + s_blitSupported = hasGLVersion(3, 0) || + hasGLExtension(QByteArrayLiteral("GL_ARB_framebuffer_object")) || + hasGLExtension(QByteArrayLiteral("GL_EXT_framebuffer_blit")); + } +} + +void GLRenderTarget::cleanup() +{ + Q_ASSERT(s_renderTargets.isEmpty()); + sSupported = false; + s_blitSupported = false; +} + +bool GLRenderTarget::isRenderTargetBound() +{ + return !s_renderTargets.isEmpty(); +} + +bool GLRenderTarget::blitSupported() +{ + return s_blitSupported; +} + +void GLRenderTarget::pushRenderTarget(GLRenderTarget* target) +{ + if (s_renderTargets.isEmpty()) { + glGetIntegerv(GL_VIEWPORT, s_virtualScreenViewport); + } + target->enable(); + s_renderTargets.push(target); +} + +void GLRenderTarget::pushRenderTargets(QStack targets) +{ + if (s_renderTargets.isEmpty()) { + glGetIntegerv(GL_VIEWPORT, s_virtualScreenViewport); + } + targets.top()->enable(); + s_renderTargets.append(targets); +} + +GLRenderTarget* GLRenderTarget::popRenderTarget() +{ + GLRenderTarget* ret = s_renderTargets.pop(); + ret->setTextureDirty(); + + if (!s_renderTargets.isEmpty()) { + s_renderTargets.top()->enable(); + } else { + ret->disable(); + glViewport (s_virtualScreenViewport[0], s_virtualScreenViewport[1], s_virtualScreenViewport[2], s_virtualScreenViewport[3]); + } + + return ret; +} + +GLRenderTarget::GLRenderTarget() +{ + // Reset variables + mValid = false; + mTexture = GLTexture(); +} + +GLRenderTarget::GLRenderTarget(const GLTexture& color) +{ + // Reset variables + mValid = false; + + mTexture = color; + + // Make sure FBO is supported + if (sSupported && !mTexture.isNull()) { + initFBO(); + } else + qCCritical(LIBKWINGLUTILS) << "Render targets aren't supported!"; +} + +GLRenderTarget::~GLRenderTarget() +{ + if (mValid) { + glDeleteFramebuffers(1, &mFramebuffer); + } +} + +bool GLRenderTarget::enable() +{ + if (!mValid) { + initFBO(); + } + + if (!valid()) { + qCCritical(LIBKWINGLUTILS) << "Can't enable invalid render target!"; + return false; + } + + glBindFramebuffer(GL_FRAMEBUFFER, mFramebuffer); + glViewport(0, 0, mTexture.width(), mTexture.height()); + mTexture.setDirty(); + + return true; +} + +bool GLRenderTarget::disable() +{ + if (!mValid) { + initFBO(); + } + + if (!valid()) { + qCCritical(LIBKWINGLUTILS) << "Can't disable invalid render target!"; + return false; + } + + glBindFramebuffer(GL_FRAMEBUFFER, s_kwinFramebuffer); + mTexture.setDirty(); + + return true; +} + +static QString formatFramebufferStatus(GLenum status) +{ + switch(status) { + case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: + // An attachment is the wrong type / is invalid / has 0 width or height + return QStringLiteral("GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT"); + case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: + // There are no images attached to the framebuffer + return QStringLiteral("GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT"); + case GL_FRAMEBUFFER_UNSUPPORTED: + // A format or the combination of formats of the attachments is unsupported + return QStringLiteral("GL_FRAMEBUFFER_UNSUPPORTED"); + case GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_EXT: + // Not all attached images have the same width and height + return QStringLiteral("GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_EXT"); + case GL_FRAMEBUFFER_INCOMPLETE_FORMATS_EXT: + // The color attachments don't have the same format + return QStringLiteral("GL_FRAMEBUFFER_INCOMPLETE_FORMATS_EXT"); + case GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE_EXT: + // The attachments don't have the same number of samples + return QStringLiteral("GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE"); + case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_EXT: + // The draw buffer is missing + return QStringLiteral("GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER"); + case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_EXT: + // The read buffer is missing + return QStringLiteral("GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER"); + default: + return QStringLiteral("Unknown (0x") + QString::number(status, 16) + QStringLiteral(")"); + } +} + +void GLRenderTarget::initFBO() +{ +#if DEBUG_GLRENDERTARGET + GLenum err = glGetError(); + if (err != GL_NO_ERROR) + qCCritical(LIBKWINGLUTILS) << "Error status when entering GLRenderTarget::initFBO: " << formatGLError(err); +#endif + + glGenFramebuffers(1, &mFramebuffer); + +#if DEBUG_GLRENDERTARGET + if ((err = glGetError()) != GL_NO_ERROR) { + qCCritical(LIBKWINGLUTILS) << "glGenFramebuffers failed: " << formatGLError(err); + return; + } +#endif + + glBindFramebuffer(GL_FRAMEBUFFER, mFramebuffer); + +#if DEBUG_GLRENDERTARGET + if ((err = glGetError()) != GL_NO_ERROR) { + qCCritical(LIBKWINGLUTILS) << "glBindFramebuffer failed: " << formatGLError(err); + glDeleteFramebuffers(1, &mFramebuffer); + return; + } +#endif + + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + mTexture.target(), mTexture.texture(), 0); + +#if DEBUG_GLRENDERTARGET + if ((err = glGetError()) != GL_NO_ERROR) { + qCCritical(LIBKWINGLUTILS) << "glFramebufferTexture2D failed: " << formatGLError(err); + glBindFramebuffer(GL_FRAMEBUFFER, s_kwinFramebuffer); + glDeleteFramebuffers(1, &mFramebuffer); + return; + } +#endif + + const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + + glBindFramebuffer(GL_FRAMEBUFFER, s_kwinFramebuffer); + + if (status != GL_FRAMEBUFFER_COMPLETE) { + // We have an incomplete framebuffer, consider it invalid + if (status == 0) + qCCritical(LIBKWINGLUTILS) << "glCheckFramebufferStatus failed: " << formatGLError(glGetError()); + else + qCCritical(LIBKWINGLUTILS) << "Invalid framebuffer status: " << formatFramebufferStatus(status); + glDeleteFramebuffers(1, &mFramebuffer); + return; + } + + mValid = true; +} + +void GLRenderTarget::blitFromFramebuffer(const QRect &source, const QRect &destination, GLenum filter) +{ + if (!GLRenderTarget::blitSupported()) { + return; + } + + if (!mValid) { + initFBO(); + } + + GLRenderTarget::pushRenderTarget(this); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, mFramebuffer); + glBindFramebuffer(GL_READ_FRAMEBUFFER, s_kwinFramebuffer); + const QRect s = source.isNull() ? s_virtualScreenGeometry : source; + const QRect d = destination.isNull() ? QRect(0, 0, mTexture.width(), mTexture.height()) : destination; + + glBlitFramebuffer((s.x() - s_virtualScreenGeometry.x()) * s_virtualScreenScale, + (s_virtualScreenGeometry.height() - (s.y() - s_virtualScreenGeometry.y() + s.height())) * s_virtualScreenScale, + (s.x() - s_virtualScreenGeometry.x() + s.width()) * s_virtualScreenScale, + (s_virtualScreenGeometry.height() - (s.y() - s_virtualScreenGeometry.y())) * s_virtualScreenScale, + d.x(), mTexture.height() - d.y() - d.height(), d.x() + d.width(), mTexture.height() - d.y(), + GL_COLOR_BUFFER_BIT, filter); + GLRenderTarget::popRenderTarget(); +} + +void GLRenderTarget::attachTexture(const GLTexture& target) +{ + if (!mValid) { + initFBO(); + } + + if (mTexture.texture() == target.texture()) { + return; + } + + pushRenderTarget(this); + + mTexture = target; + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + mTexture.target(), mTexture.texture(), 0); + + popRenderTarget(); +} + +void GLRenderTarget::detachTexture() +{ + if (mTexture.isNull()) { + return; + } + + pushRenderTarget(this); + + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + mTexture.target(), 0, 0); + + popRenderTarget(); +} + + +// ------------------------------------------------------------------ + +static const uint16_t indices[] = { + 1, 0, 3, 3, 2, 1, 5, 4, 7, 7, 6, 5, 9, 8, 11, 11, 10, 9, + 13, 12, 15, 15, 14, 13, 17, 16, 19, 19, 18, 17, 21, 20, 23, 23, 22, 21, + 25, 24, 27, 27, 26, 25, 29, 28, 31, 31, 30, 29, 33, 32, 35, 35, 34, 33, + 37, 36, 39, 39, 38, 37, 41, 40, 43, 43, 42, 41, 45, 44, 47, 47, 46, 45, + 49, 48, 51, 51, 50, 49, 53, 52, 55, 55, 54, 53, 57, 56, 59, 59, 58, 57, + 61, 60, 63, 63, 62, 61, 65, 64, 67, 67, 66, 65, 69, 68, 71, 71, 70, 69, + 73, 72, 75, 75, 74, 73, 77, 76, 79, 79, 78, 77, 81, 80, 83, 83, 82, 81, + 85, 84, 87, 87, 86, 85, 89, 88, 91, 91, 90, 89, 93, 92, 95, 95, 94, 93, + 97, 96, 99, 99, 98, 97, 101, 100, 103, 103, 102, 101, 105, 104, 107, 107, 106, 105, + 109, 108, 111, 111, 110, 109, 113, 112, 115, 115, 114, 113, 117, 116, 119, 119, 118, 117, + 121, 120, 123, 123, 122, 121, 125, 124, 127, 127, 126, 125, 129, 128, 131, 131, 130, 129, + 133, 132, 135, 135, 134, 133, 137, 136, 139, 139, 138, 137, 141, 140, 143, 143, 142, 141, + 145, 144, 147, 147, 146, 145, 149, 148, 151, 151, 150, 149, 153, 152, 155, 155, 154, 153, + 157, 156, 159, 159, 158, 157, 161, 160, 163, 163, 162, 161, 165, 164, 167, 167, 166, 165, + 169, 168, 171, 171, 170, 169, 173, 172, 175, 175, 174, 173, 177, 176, 179, 179, 178, 177, + 181, 180, 183, 183, 182, 181, 185, 184, 187, 187, 186, 185, 189, 188, 191, 191, 190, 189, + 193, 192, 195, 195, 194, 193, 197, 196, 199, 199, 198, 197, 201, 200, 203, 203, 202, 201, + 205, 204, 207, 207, 206, 205, 209, 208, 211, 211, 210, 209, 213, 212, 215, 215, 214, 213, + 217, 216, 219, 219, 218, 217, 221, 220, 223, 223, 222, 221, 225, 224, 227, 227, 226, 225, + 229, 228, 231, 231, 230, 229, 233, 232, 235, 235, 234, 233, 237, 236, 239, 239, 238, 237, + 241, 240, 243, 243, 242, 241, 245, 244, 247, 247, 246, 245, 249, 248, 251, 251, 250, 249, + 253, 252, 255, 255, 254, 253, 257, 256, 259, 259, 258, 257, 261, 260, 263, 263, 262, 261, + 265, 264, 267, 267, 266, 265, 269, 268, 271, 271, 270, 269, 273, 272, 275, 275, 274, 273, + 277, 276, 279, 279, 278, 277, 281, 280, 283, 283, 282, 281, 285, 284, 287, 287, 286, 285, + 289, 288, 291, 291, 290, 289, 293, 292, 295, 295, 294, 293, 297, 296, 299, 299, 298, 297, + 301, 300, 303, 303, 302, 301, 305, 304, 307, 307, 306, 305, 309, 308, 311, 311, 310, 309, + 313, 312, 315, 315, 314, 313, 317, 316, 319, 319, 318, 317, 321, 320, 323, 323, 322, 321, + 325, 324, 327, 327, 326, 325, 329, 328, 331, 331, 330, 329, 333, 332, 335, 335, 334, 333, + 337, 336, 339, 339, 338, 337, 341, 340, 343, 343, 342, 341, 345, 344, 347, 347, 346, 345, + 349, 348, 351, 351, 350, 349, 353, 352, 355, 355, 354, 353, 357, 356, 359, 359, 358, 357, + 361, 360, 363, 363, 362, 361, 365, 364, 367, 367, 366, 365, 369, 368, 371, 371, 370, 369, + 373, 372, 375, 375, 374, 373, 377, 376, 379, 379, 378, 377, 381, 380, 383, 383, 382, 381, + 385, 384, 387, 387, 386, 385, 389, 388, 391, 391, 390, 389, 393, 392, 395, 395, 394, 393, + 397, 396, 399, 399, 398, 397, 401, 400, 403, 403, 402, 401, 405, 404, 407, 407, 406, 405, + 409, 408, 411, 411, 410, 409, 413, 412, 415, 415, 414, 413, 417, 416, 419, 419, 418, 417, + 421, 420, 423, 423, 422, 421, 425, 424, 427, 427, 426, 425, 429, 428, 431, 431, 430, 429, + 433, 432, 435, 435, 434, 433, 437, 436, 439, 439, 438, 437, 441, 440, 443, 443, 442, 441, + 445, 444, 447, 447, 446, 445, 449, 448, 451, 451, 450, 449, 453, 452, 455, 455, 454, 453, + 457, 456, 459, 459, 458, 457, 461, 460, 463, 463, 462, 461, 465, 464, 467, 467, 466, 465, + 469, 468, 471, 471, 470, 469, 473, 472, 475, 475, 474, 473, 477, 476, 479, 479, 478, 477, + 481, 480, 483, 483, 482, 481, 485, 484, 487, 487, 486, 485, 489, 488, 491, 491, 490, 489, + 493, 492, 495, 495, 494, 493, 497, 496, 499, 499, 498, 497, 501, 500, 503, 503, 502, 501, + 505, 504, 507, 507, 506, 505, 509, 508, 511, 511, 510, 509, 513, 512, 515, 515, 514, 513, + 517, 516, 519, 519, 518, 517, 521, 520, 523, 523, 522, 521, 525, 524, 527, 527, 526, 525, + 529, 528, 531, 531, 530, 529, 533, 532, 535, 535, 534, 533, 537, 536, 539, 539, 538, 537, + 541, 540, 543, 543, 542, 541, 545, 544, 547, 547, 546, 545, 549, 548, 551, 551, 550, 549, + 553, 552, 555, 555, 554, 553, 557, 556, 559, 559, 558, 557, 561, 560, 563, 563, 562, 561, + 565, 564, 567, 567, 566, 565, 569, 568, 571, 571, 570, 569, 573, 572, 575, 575, 574, 573, + 577, 576, 579, 579, 578, 577, 581, 580, 583, 583, 582, 581, 585, 584, 587, 587, 586, 585, + 589, 588, 591, 591, 590, 589, 593, 592, 595, 595, 594, 593, 597, 596, 599, 599, 598, 597, + 601, 600, 603, 603, 602, 601, 605, 604, 607, 607, 606, 605, 609, 608, 611, 611, 610, 609, + 613, 612, 615, 615, 614, 613, 617, 616, 619, 619, 618, 617, 621, 620, 623, 623, 622, 621, + 625, 624, 627, 627, 626, 625, 629, 628, 631, 631, 630, 629, 633, 632, 635, 635, 634, 633, + 637, 636, 639, 639, 638, 637, 641, 640, 643, 643, 642, 641, 645, 644, 647, 647, 646, 645, + 649, 648, 651, 651, 650, 649, 653, 652, 655, 655, 654, 653, 657, 656, 659, 659, 658, 657, + 661, 660, 663, 663, 662, 661, 665, 664, 667, 667, 666, 665, 669, 668, 671, 671, 670, 669, + 673, 672, 675, 675, 674, 673, 677, 676, 679, 679, 678, 677, 681, 680, 683, 683, 682, 681, + 685, 684, 687, 687, 686, 685, 689, 688, 691, 691, 690, 689, 693, 692, 695, 695, 694, 693, + 697, 696, 699, 699, 698, 697, 701, 700, 703, 703, 702, 701, 705, 704, 707, 707, 706, 705, + 709, 708, 711, 711, 710, 709, 713, 712, 715, 715, 714, 713, 717, 716, 719, 719, 718, 717, + 721, 720, 723, 723, 722, 721, 725, 724, 727, 727, 726, 725, 729, 728, 731, 731, 730, 729, + 733, 732, 735, 735, 734, 733, 737, 736, 739, 739, 738, 737, 741, 740, 743, 743, 742, 741, + 745, 744, 747, 747, 746, 745, 749, 748, 751, 751, 750, 749, 753, 752, 755, 755, 754, 753, + 757, 756, 759, 759, 758, 757, 761, 760, 763, 763, 762, 761, 765, 764, 767, 767, 766, 765, + 769, 768, 771, 771, 770, 769, 773, 772, 775, 775, 774, 773, 777, 776, 779, 779, 778, 777, + 781, 780, 783, 783, 782, 781, 785, 784, 787, 787, 786, 785, 789, 788, 791, 791, 790, 789, + 793, 792, 795, 795, 794, 793, 797, 796, 799, 799, 798, 797, 801, 800, 803, 803, 802, 801, + 805, 804, 807, 807, 806, 805, 809, 808, 811, 811, 810, 809, 813, 812, 815, 815, 814, 813, + 817, 816, 819, 819, 818, 817, 821, 820, 823, 823, 822, 821, 825, 824, 827, 827, 826, 825, + 829, 828, 831, 831, 830, 829, 833, 832, 835, 835, 834, 833, 837, 836, 839, 839, 838, 837, + 841, 840, 843, 843, 842, 841, 845, 844, 847, 847, 846, 845, 849, 848, 851, 851, 850, 849, + 853, 852, 855, 855, 854, 853, 857, 856, 859, 859, 858, 857, 861, 860, 863, 863, 862, 861, + 865, 864, 867, 867, 866, 865, 869, 868, 871, 871, 870, 869, 873, 872, 875, 875, 874, 873, + 877, 876, 879, 879, 878, 877, 881, 880, 883, 883, 882, 881, 885, 884, 887, 887, 886, 885, + 889, 888, 891, 891, 890, 889, 893, 892, 895, 895, 894, 893, 897, 896, 899, 899, 898, 897, + 901, 900, 903, 903, 902, 901, 905, 904, 907, 907, 906, 905, 909, 908, 911, 911, 910, 909, + 913, 912, 915, 915, 914, 913, 917, 916, 919, 919, 918, 917, 921, 920, 923, 923, 922, 921, + 925, 924, 927, 927, 926, 925, 929, 928, 931, 931, 930, 929, 933, 932, 935, 935, 934, 933, + 937, 936, 939, 939, 938, 937, 941, 940, 943, 943, 942, 941, 945, 944, 947, 947, 946, 945, + 949, 948, 951, 951, 950, 949, 953, 952, 955, 955, 954, 953, 957, 956, 959, 959, 958, 957, + 961, 960, 963, 963, 962, 961, 965, 964, 967, 967, 966, 965, 969, 968, 971, 971, 970, 969, + 973, 972, 975, 975, 974, 973, 977, 976, 979, 979, 978, 977, 981, 980, 983, 983, 982, 981, + 985, 984, 987, 987, 986, 985, 989, 988, 991, 991, 990, 989, 993, 992, 995, 995, 994, 993, + 997, 996, 999, 999, 998, 997, 1001, 1000, 1003, 1003, 1002, 1001, 1005, 1004, 1007, 1007, 1006, 1005, + 1009, 1008, 1011, 1011, 1010, 1009, 1013, 1012, 1015, 1015, 1014, 1013, 1017, 1016, 1019, 1019, 1018, 1017, + 1021, 1020, 1023, 1023, 1022, 1021, 1025, 1024, 1027, 1027, 1026, 1025, 1029, 1028, 1031, 1031, 1030, 1029, + 1033, 1032, 1035, 1035, 1034, 1033, 1037, 1036, 1039, 1039, 1038, 1037, 1041, 1040, 1043, 1043, 1042, 1041, + 1045, 1044, 1047, 1047, 1046, 1045, 1049, 1048, 1051, 1051, 1050, 1049, 1053, 1052, 1055, 1055, 1054, 1053, + 1057, 1056, 1059, 1059, 1058, 1057, 1061, 1060, 1063, 1063, 1062, 1061, 1065, 1064, 1067, 1067, 1066, 1065, + 1069, 1068, 1071, 1071, 1070, 1069, 1073, 1072, 1075, 1075, 1074, 1073, 1077, 1076, 1079, 1079, 1078, 1077, + 1081, 1080, 1083, 1083, 1082, 1081, 1085, 1084, 1087, 1087, 1086, 1085, 1089, 1088, 1091, 1091, 1090, 1089, + 1093, 1092, 1095, 1095, 1094, 1093, 1097, 1096, 1099, 1099, 1098, 1097, 1101, 1100, 1103, 1103, 1102, 1101, + 1105, 1104, 1107, 1107, 1106, 1105, 1109, 1108, 1111, 1111, 1110, 1109, 1113, 1112, 1115, 1115, 1114, 1113, + 1117, 1116, 1119, 1119, 1118, 1117, 1121, 1120, 1123, 1123, 1122, 1121, 1125, 1124, 1127, 1127, 1126, 1125, + 1129, 1128, 1131, 1131, 1130, 1129, 1133, 1132, 1135, 1135, 1134, 1133, 1137, 1136, 1139, 1139, 1138, 1137, + 1141, 1140, 1143, 1143, 1142, 1141, 1145, 1144, 1147, 1147, 1146, 1145, 1149, 1148, 1151, 1151, 1150, 1149, + 1153, 1152, 1155, 1155, 1154, 1153, 1157, 1156, 1159, 1159, 1158, 1157, 1161, 1160, 1163, 1163, 1162, 1161, + 1165, 1164, 1167, 1167, 1166, 1165, 1169, 1168, 1171, 1171, 1170, 1169, 1173, 1172, 1175, 1175, 1174, 1173, + 1177, 1176, 1179, 1179, 1178, 1177, 1181, 1180, 1183, 1183, 1182, 1181, 1185, 1184, 1187, 1187, 1186, 1185, + 1189, 1188, 1191, 1191, 1190, 1189, 1193, 1192, 1195, 1195, 1194, 1193, 1197, 1196, 1199, 1199, 1198, 1197, + 1201, 1200, 1203, 1203, 1202, 1201, 1205, 1204, 1207, 1207, 1206, 1205, 1209, 1208, 1211, 1211, 1210, 1209, + 1213, 1212, 1215, 1215, 1214, 1213, 1217, 1216, 1219, 1219, 1218, 1217, 1221, 1220, 1223, 1223, 1222, 1221, + 1225, 1224, 1227, 1227, 1226, 1225, 1229, 1228, 1231, 1231, 1230, 1229, 1233, 1232, 1235, 1235, 1234, 1233, + 1237, 1236, 1239, 1239, 1238, 1237, 1241, 1240, 1243, 1243, 1242, 1241, 1245, 1244, 1247, 1247, 1246, 1245, + 1249, 1248, 1251, 1251, 1250, 1249, 1253, 1252, 1255, 1255, 1254, 1253, 1257, 1256, 1259, 1259, 1258, 1257, + 1261, 1260, 1263, 1263, 1262, 1261, 1265, 1264, 1267, 1267, 1266, 1265, 1269, 1268, 1271, 1271, 1270, 1269, + 1273, 1272, 1275, 1275, 1274, 1273, 1277, 1276, 1279, 1279, 1278, 1277, 1281, 1280, 1283, 1283, 1282, 1281, + 1285, 1284, 1287, 1287, 1286, 1285, 1289, 1288, 1291, 1291, 1290, 1289, 1293, 1292, 1295, 1295, 1294, 1293, + 1297, 1296, 1299, 1299, 1298, 1297, 1301, 1300, 1303, 1303, 1302, 1301, 1305, 1304, 1307, 1307, 1306, 1305, + 1309, 1308, 1311, 1311, 1310, 1309, 1313, 1312, 1315, 1315, 1314, 1313, 1317, 1316, 1319, 1319, 1318, 1317, + 1321, 1320, 1323, 1323, 1322, 1321, 1325, 1324, 1327, 1327, 1326, 1325, 1329, 1328, 1331, 1331, 1330, 1329, + 1333, 1332, 1335, 1335, 1334, 1333, 1337, 1336, 1339, 1339, 1338, 1337, 1341, 1340, 1343, 1343, 1342, 1341, + 1345, 1344, 1347, 1347, 1346, 1345, 1349, 1348, 1351, 1351, 1350, 1349, 1353, 1352, 1355, 1355, 1354, 1353, + 1357, 1356, 1359, 1359, 1358, 1357, 1361, 1360, 1363, 1363, 1362, 1361, 1365, 1364, 1367, 1367, 1366, 1365, + 1369, 1368, 1371, 1371, 1370, 1369, 1373, 1372, 1375, 1375, 1374, 1373, 1377, 1376, 1379, 1379, 1378, 1377, + 1381, 1380, 1383, 1383, 1382, 1381, 1385, 1384, 1387, 1387, 1386, 1385, 1389, 1388, 1391, 1391, 1390, 1389, + 1393, 1392, 1395, 1395, 1394, 1393, 1397, 1396, 1399, 1399, 1398, 1397, 1401, 1400, 1403, 1403, 1402, 1401, + 1405, 1404, 1407, 1407, 1406, 1405, 1409, 1408, 1411, 1411, 1410, 1409, 1413, 1412, 1415, 1415, 1414, 1413, + 1417, 1416, 1419, 1419, 1418, 1417, 1421, 1420, 1423, 1423, 1422, 1421, 1425, 1424, 1427, 1427, 1426, 1425, + 1429, 1428, 1431, 1431, 1430, 1429, 1433, 1432, 1435, 1435, 1434, 1433, 1437, 1436, 1439, 1439, 1438, 1437, + 1441, 1440, 1443, 1443, 1442, 1441, 1445, 1444, 1447, 1447, 1446, 1445, 1449, 1448, 1451, 1451, 1450, 1449, + 1453, 1452, 1455, 1455, 1454, 1453, 1457, 1456, 1459, 1459, 1458, 1457, 1461, 1460, 1463, 1463, 1462, 1461, + 1465, 1464, 1467, 1467, 1466, 1465, 1469, 1468, 1471, 1471, 1470, 1469, 1473, 1472, 1475, 1475, 1474, 1473, + 1477, 1476, 1479, 1479, 1478, 1477, 1481, 1480, 1483, 1483, 1482, 1481, 1485, 1484, 1487, 1487, 1486, 1485, + 1489, 1488, 1491, 1491, 1490, 1489, 1493, 1492, 1495, 1495, 1494, 1493, 1497, 1496, 1499, 1499, 1498, 1497, + 1501, 1500, 1503, 1503, 1502, 1501, 1505, 1504, 1507, 1507, 1506, 1505, 1509, 1508, 1511, 1511, 1510, 1509, + 1513, 1512, 1515, 1515, 1514, 1513, 1517, 1516, 1519, 1519, 1518, 1517, 1521, 1520, 1523, 1523, 1522, 1521, + 1525, 1524, 1527, 1527, 1526, 1525, 1529, 1528, 1531, 1531, 1530, 1529, 1533, 1532, 1535, 1535, 1534, 1533, + 1537, 1536, 1539, 1539, 1538, 1537, 1541, 1540, 1543, 1543, 1542, 1541, 1545, 1544, 1547, 1547, 1546, 1545, + 1549, 1548, 1551, 1551, 1550, 1549, 1553, 1552, 1555, 1555, 1554, 1553, 1557, 1556, 1559, 1559, 1558, 1557, + 1561, 1560, 1563, 1563, 1562, 1561, 1565, 1564, 1567, 1567, 1566, 1565, 1569, 1568, 1571, 1571, 1570, 1569, + 1573, 1572, 1575, 1575, 1574, 1573, 1577, 1576, 1579, 1579, 1578, 1577, 1581, 1580, 1583, 1583, 1582, 1581, + 1585, 1584, 1587, 1587, 1586, 1585, 1589, 1588, 1591, 1591, 1590, 1589, 1593, 1592, 1595, 1595, 1594, 1593, + 1597, 1596, 1599, 1599, 1598, 1597, 1601, 1600, 1603, 1603, 1602, 1601, 1605, 1604, 1607, 1607, 1606, 1605, + 1609, 1608, 1611, 1611, 1610, 1609, 1613, 1612, 1615, 1615, 1614, 1613, 1617, 1616, 1619, 1619, 1618, 1617, + 1621, 1620, 1623, 1623, 1622, 1621, 1625, 1624, 1627, 1627, 1626, 1625, 1629, 1628, 1631, 1631, 1630, 1629, + 1633, 1632, 1635, 1635, 1634, 1633, 1637, 1636, 1639, 1639, 1638, 1637, 1641, 1640, 1643, 1643, 1642, 1641, + 1645, 1644, 1647, 1647, 1646, 1645, 1649, 1648, 1651, 1651, 1650, 1649, 1653, 1652, 1655, 1655, 1654, 1653, + 1657, 1656, 1659, 1659, 1658, 1657, 1661, 1660, 1663, 1663, 1662, 1661, 1665, 1664, 1667, 1667, 1666, 1665, + 1669, 1668, 1671, 1671, 1670, 1669, 1673, 1672, 1675, 1675, 1674, 1673, 1677, 1676, 1679, 1679, 1678, 1677, + 1681, 1680, 1683, 1683, 1682, 1681, 1685, 1684, 1687, 1687, 1686, 1685, 1689, 1688, 1691, 1691, 1690, 1689, + 1693, 1692, 1695, 1695, 1694, 1693, 1697, 1696, 1699, 1699, 1698, 1697, 1701, 1700, 1703, 1703, 1702, 1701, + 1705, 1704, 1707, 1707, 1706, 1705, 1709, 1708, 1711, 1711, 1710, 1709, 1713, 1712, 1715, 1715, 1714, 1713, + 1717, 1716, 1719, 1719, 1718, 1717, 1721, 1720, 1723, 1723, 1722, 1721, 1725, 1724, 1727, 1727, 1726, 1725, + 1729, 1728, 1731, 1731, 1730, 1729, 1733, 1732, 1735, 1735, 1734, 1733, 1737, 1736, 1739, 1739, 1738, 1737, + 1741, 1740, 1743, 1743, 1742, 1741, 1745, 1744, 1747, 1747, 1746, 1745, 1749, 1748, 1751, 1751, 1750, 1749, + 1753, 1752, 1755, 1755, 1754, 1753, 1757, 1756, 1759, 1759, 1758, 1757, 1761, 1760, 1763, 1763, 1762, 1761, + 1765, 1764, 1767, 1767, 1766, 1765, 1769, 1768, 1771, 1771, 1770, 1769, 1773, 1772, 1775, 1775, 1774, 1773, + 1777, 1776, 1779, 1779, 1778, 1777, 1781, 1780, 1783, 1783, 1782, 1781, 1785, 1784, 1787, 1787, 1786, 1785, + 1789, 1788, 1791, 1791, 1790, 1789, 1793, 1792, 1795, 1795, 1794, 1793, 1797, 1796, 1799, 1799, 1798, 1797, + 1801, 1800, 1803, 1803, 1802, 1801, 1805, 1804, 1807, 1807, 1806, 1805, 1809, 1808, 1811, 1811, 1810, 1809, + 1813, 1812, 1815, 1815, 1814, 1813, 1817, 1816, 1819, 1819, 1818, 1817, 1821, 1820, 1823, 1823, 1822, 1821, + 1825, 1824, 1827, 1827, 1826, 1825, 1829, 1828, 1831, 1831, 1830, 1829, 1833, 1832, 1835, 1835, 1834, 1833, + 1837, 1836, 1839, 1839, 1838, 1837, 1841, 1840, 1843, 1843, 1842, 1841, 1845, 1844, 1847, 1847, 1846, 1845, + 1849, 1848, 1851, 1851, 1850, 1849, 1853, 1852, 1855, 1855, 1854, 1853, 1857, 1856, 1859, 1859, 1858, 1857, + 1861, 1860, 1863, 1863, 1862, 1861, 1865, 1864, 1867, 1867, 1866, 1865, 1869, 1868, 1871, 1871, 1870, 1869, + 1873, 1872, 1875, 1875, 1874, 1873, 1877, 1876, 1879, 1879, 1878, 1877, 1881, 1880, 1883, 1883, 1882, 1881, + 1885, 1884, 1887, 1887, 1886, 1885, 1889, 1888, 1891, 1891, 1890, 1889, 1893, 1892, 1895, 1895, 1894, 1893, + 1897, 1896, 1899, 1899, 1898, 1897, 1901, 1900, 1903, 1903, 1902, 1901, 1905, 1904, 1907, 1907, 1906, 1905, + 1909, 1908, 1911, 1911, 1910, 1909, 1913, 1912, 1915, 1915, 1914, 1913, 1917, 1916, 1919, 1919, 1918, 1917, + 1921, 1920, 1923, 1923, 1922, 1921, 1925, 1924, 1927, 1927, 1926, 1925, 1929, 1928, 1931, 1931, 1930, 1929, + 1933, 1932, 1935, 1935, 1934, 1933, 1937, 1936, 1939, 1939, 1938, 1937, 1941, 1940, 1943, 1943, 1942, 1941, + 1945, 1944, 1947, 1947, 1946, 1945, 1949, 1948, 1951, 1951, 1950, 1949, 1953, 1952, 1955, 1955, 1954, 1953, + 1957, 1956, 1959, 1959, 1958, 1957, 1961, 1960, 1963, 1963, 1962, 1961, 1965, 1964, 1967, 1967, 1966, 1965, + 1969, 1968, 1971, 1971, 1970, 1969, 1973, 1972, 1975, 1975, 1974, 1973, 1977, 1976, 1979, 1979, 1978, 1977, + 1981, 1980, 1983, 1983, 1982, 1981, 1985, 1984, 1987, 1987, 1986, 1985, 1989, 1988, 1991, 1991, 1990, 1989, + 1993, 1992, 1995, 1995, 1994, 1993, 1997, 1996, 1999, 1999, 1998, 1997, 2001, 2000, 2003, 2003, 2002, 2001, + 2005, 2004, 2007, 2007, 2006, 2005, 2009, 2008, 2011, 2011, 2010, 2009, 2013, 2012, 2015, 2015, 2014, 2013, + 2017, 2016, 2019, 2019, 2018, 2017, 2021, 2020, 2023, 2023, 2022, 2021, 2025, 2024, 2027, 2027, 2026, 2025, + 2029, 2028, 2031, 2031, 2030, 2029, 2033, 2032, 2035, 2035, 2034, 2033, 2037, 2036, 2039, 2039, 2038, 2037, + 2041, 2040, 2043, 2043, 2042, 2041, 2045, 2044, 2047, 2047, 2046, 2045 +}; + +template +T align(T value, int bytes) +{ + return (value + bytes - 1) & ~T(bytes - 1); +} + +class IndexBuffer +{ +public: + IndexBuffer(); + ~IndexBuffer(); + + void accommodate(int count); + void bind(); + +private: + GLuint m_buffer; + size_t m_size; + int m_count; +}; + +IndexBuffer::IndexBuffer() +{ + // The maximum number of quads we can render with 16 bit indices is 16,384. + // But we start with 512 and grow the buffer as needed. + m_size = sizeof(indices); + m_count = m_size / (6 * sizeof(uint16_t)); + + glGenBuffers(1, &m_buffer); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_buffer); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); +} + +IndexBuffer::~IndexBuffer() +{ + glDeleteBuffers(1, &m_buffer); +} + +void IndexBuffer::accommodate(int count) +{ + // Check if we need to grow the buffer. + if (count <= m_count) + return; + + count = align(count, 128); + size_t size = 6 * sizeof(uint16_t) * count; + + // Create a new buffer object + GLuint buffer; + glGenBuffers(1, &buffer); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffer); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, size, nullptr, GL_STATIC_DRAW); + + // Use the GPU to copy the data from the old object to the new object, + glBindBuffer(GL_COPY_READ_BUFFER, m_buffer); + glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_ELEMENT_ARRAY_BUFFER, 0, 0, m_size); + glDeleteBuffers(1, &m_buffer); + glFlush(); // Needed to work around what appears to be a CP DMA issue in r600g + + // Map the new object and fill in the uninitialized section + const GLbitfield access = GL_MAP_WRITE_BIT | GL_MAP_UNSYNCHRONIZED_BIT | GL_MAP_INVALIDATE_RANGE_BIT; + uint16_t *map = (uint16_t *) glMapBufferRange(GL_ELEMENT_ARRAY_BUFFER, m_size, size - m_size, access); + + const uint16_t index[] = { 1, 0, 3, 3, 2, 1 }; + for (int i = m_count; i < count; i++) { + for (int j = 0; j < 6; j++) + *(map++) = i * 4 + index[j]; + } + + glUnmapBuffer(GL_ELEMENT_ARRAY_BUFFER); + m_buffer = buffer; + m_count = count; + m_size = size; +} + +void IndexBuffer::bind() +{ + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_buffer); +} + + + +// ------------------------------------------------------------------ + + +class BitRef +{ +public: + BitRef(uint32_t &bitfield, int bit) : m_bitfield(bitfield), m_mask(1 << bit) {} + + void operator = (bool val) { + if (val) + m_bitfield |= m_mask; + else + m_bitfield &= ~m_mask; + } + + operator bool () const { return m_bitfield & m_mask; } + +private: + uint32_t &m_bitfield; + int const m_mask; +}; + + +// ------------------------------------------------------------------ + + +class Bitfield +{ +public: + Bitfield() : m_bitfield(0) {} + Bitfield(uint32_t bits) : m_bitfield(bits) {} + + void set(int i) { m_bitfield |= (1 << i); } + void clear(int i) { m_bitfield &= ~(1 << i); } + + BitRef operator [] (int i) { return BitRef(m_bitfield, i); } + operator uint32_t () const { return m_bitfield; } + +private: + uint32_t m_bitfield; +}; + + +// ------------------------------------------------------------------ + + +class BitfieldIterator +{ +public: + BitfieldIterator(uint32_t bitfield) : m_bitfield(bitfield) {} + + bool hasNext() const { return m_bitfield != 0; } + + int next() { + const int bit = ffs(m_bitfield) - 1; + m_bitfield ^= (1 << bit); + return bit; + } + +private: + uint32_t m_bitfield; +}; + + + +// ------------------------------------------------------------------ + + + +struct VertexAttrib +{ + int size; + GLenum type; + int offset; +}; + + +// ------------------------------------------------------------------ + + + +struct BufferFence +{ + GLsync sync; + intptr_t nextEnd; + + bool signaled() const + { + GLint value; + glGetSynciv(sync, GL_SYNC_STATUS, 1, nullptr, &value); + return value == GL_SIGNALED; + } +}; + + +static void deleteAll(std::deque &fences) +{ + for (const BufferFence &fence : fences) + glDeleteSync(fence.sync); + + fences.clear(); +} + + + +// ------------------------------------------------------------------ + + + +template +struct FrameSizesArray +{ +public: + FrameSizesArray() { + m_array.fill(0); + } + + void push(size_t size) { + m_array[m_index] = size; + m_index = (m_index + 1) % Count; + } + + size_t average() const { + size_t sum = 0; + for (size_t size : m_array) + sum += size; + return sum / Count; + } + +private: + std::array m_array; + int m_index = 0; +}; + + + +//********************************* +// GLVertexBufferPrivate +//********************************* +class GLVertexBufferPrivate +{ +public: + GLVertexBufferPrivate(GLVertexBuffer::UsageHint usageHint) + : vertexCount(0) + , persistent(false) + , useColor(false) + , color(0, 0, 0, 255) + , bufferSize(0) + , bufferEnd(0) + , mappedSize(0) + , frameSize(0) + , nextOffset(0) + , baseAddress(0) + , map(nullptr) + { + glGenBuffers(1, &buffer); + + switch(usageHint) { + case GLVertexBuffer::Dynamic: + usage = GL_DYNAMIC_DRAW; + break; + case GLVertexBuffer::Static: + usage = GL_STATIC_DRAW; + break; + default: + usage = GL_STREAM_DRAW; + break; + } + } + + ~GLVertexBufferPrivate() { + deleteAll(fences); + + if (buffer != 0) { + glDeleteBuffers(1, &buffer); + map = nullptr; + } + } + + void interleaveArrays(float *array, int dim, const float *vertices, const float *texcoords, int count); + void bindArrays(); + void unbindArrays(); + void reallocateBuffer(size_t size); + GLvoid *mapNextFreeRange(size_t size); + void reallocatePersistentBuffer(size_t size); + bool awaitFence(intptr_t offset); + GLvoid *getIdleRange(size_t size); + + GLuint buffer; + GLenum usage; + int stride; + int vertexCount; + static GLVertexBuffer *streamingBuffer; + static bool haveBufferStorage; + static bool haveSyncFences; + static bool hasMapBufferRange; + static bool supportsIndexedQuads; + QByteArray dataStore; + bool persistent; + bool useColor; + QVector4D color; + size_t bufferSize; + intptr_t bufferEnd; + size_t mappedSize; + size_t frameSize; + intptr_t nextOffset; + intptr_t baseAddress; + uint8_t *map; + std::deque fences; + FrameSizesArray<4> frameSizes; + VertexAttrib attrib[VertexAttributeCount]; + Bitfield enabledArrays; + static IndexBuffer *s_indexBuffer; +}; + +bool GLVertexBufferPrivate::hasMapBufferRange = false; +bool GLVertexBufferPrivate::supportsIndexedQuads = false; +GLVertexBuffer *GLVertexBufferPrivate::streamingBuffer = nullptr; +bool GLVertexBufferPrivate::haveBufferStorage = false; +bool GLVertexBufferPrivate::haveSyncFences = false; +IndexBuffer *GLVertexBufferPrivate::s_indexBuffer = nullptr; + +void GLVertexBufferPrivate::interleaveArrays(float *dst, int dim, + const float *vertices, const float *texcoords, + int count) +{ + if (!texcoords) { + memcpy((void *) dst, vertices, dim * sizeof(float) * count); + return; + } + + switch (dim) + { + case 2: + for (int i = 0; i < count; i++) { + *(dst++) = *(vertices++); + *(dst++) = *(vertices++); + *(dst++) = *(texcoords++); + *(dst++) = *(texcoords++); + } + break; + + case 3: + for (int i = 0; i < count; i++) { + *(dst++) = *(vertices++); + *(dst++) = *(vertices++); + *(dst++) = *(vertices++); + *(dst++) = *(texcoords++); + *(dst++) = *(texcoords++); + } + break; + + default: + for (int i = 0; i < count; i++) { + for (int j = 0; j < dim; j++) + *(dst++) = *(vertices++); + + *(dst++) = *(texcoords++); + *(dst++) = *(texcoords++); + } + } +} + +void GLVertexBufferPrivate::bindArrays() +{ + if (useColor) { + GLShader *shader = ShaderManager::instance()->getBoundShader(); + shader->setUniform(GLShader::Color, color); + } + + glBindBuffer(GL_ARRAY_BUFFER, buffer); + + BitfieldIterator it(enabledArrays); + while (it.hasNext()) { + const int index = it.next(); + glVertexAttribPointer(index, attrib[index].size, attrib[index].type, GL_FALSE, stride, + (const GLvoid *) (baseAddress + attrib[index].offset)); + glEnableVertexAttribArray(index); + } +} + +void GLVertexBufferPrivate::unbindArrays() +{ + BitfieldIterator it(enabledArrays); + while (it.hasNext()) + glDisableVertexAttribArray(it.next()); +} + +void GLVertexBufferPrivate::reallocatePersistentBuffer(size_t size) +{ + if (buffer != 0) { + // This also unmaps and unbinds the buffer + glDeleteBuffers(1, &buffer); + buffer = 0; + + deleteAll(fences); + } + + if (buffer == 0) + glGenBuffers(1, &buffer); + + // Round the size up to 64 kb + size_t minSize = qMax(frameSizes.average() * 3, 128 * 1024); + bufferSize = align(qMax(size, minSize), 64 * 1024); + + const GLbitfield storage = GL_DYNAMIC_STORAGE_BIT; + const GLbitfield access = GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT; + + glBindBuffer(GL_ARRAY_BUFFER, buffer); + glBufferStorage(GL_ARRAY_BUFFER, bufferSize, nullptr, storage | access); + + map = (uint8_t *) glMapBufferRange(GL_ARRAY_BUFFER, 0, bufferSize, access); + + nextOffset = 0; + bufferEnd = bufferSize; +} + +bool GLVertexBufferPrivate::awaitFence(intptr_t end) +{ + // Skip fences until we reach the end offset + while (!fences.empty() && fences.front().nextEnd < end) { + glDeleteSync(fences.front().sync); + fences.pop_front(); + } + + Q_ASSERT(!fences.empty()); + + // Wait on the next fence + const BufferFence &fence = fences.front(); + + if (!fence.signaled()) { + qCDebug(LIBKWINGLUTILS) << "Stalling on VBO fence"; + const GLenum ret = glClientWaitSync(fence.sync, GL_SYNC_FLUSH_COMMANDS_BIT, 1000000000); + + if (ret == GL_TIMEOUT_EXPIRED || ret == GL_WAIT_FAILED) { + qCCritical(LIBKWINGLUTILS) << "Wait failed"; + return false; + } + } + + glDeleteSync(fence.sync); + + // Update the end pointer + bufferEnd = fence.nextEnd; + fences.pop_front(); + + return true; +} + +GLvoid *GLVertexBufferPrivate::getIdleRange(size_t size) +{ + if (unlikely(size > bufferSize)) + reallocatePersistentBuffer(size * 2); + + // Handle wrap-around + if (unlikely(nextOffset + size > bufferSize)) { + nextOffset = 0; + bufferEnd -= bufferSize; + + for (BufferFence &fence : fences) + fence.nextEnd -= bufferSize; + + // Emit a fence now + BufferFence fence; + fence.sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + fence.nextEnd = bufferSize; + fences.emplace_back(fence); + } + + if (unlikely(nextOffset + intptr_t(size) > bufferEnd)) { + if (!awaitFence(nextOffset + size)) + return nullptr; + } + + return map + nextOffset; +} + +void GLVertexBufferPrivate::reallocateBuffer(size_t size) +{ + // Round the size up to 4 Kb for streaming/dynamic buffers. + const size_t minSize = 32768; // Minimum size for streaming buffers + const size_t alloc = usage != GL_STATIC_DRAW ? align(qMax(size, minSize), 4096) : size; + + glBufferData(GL_ARRAY_BUFFER, alloc, nullptr, usage); + + bufferSize = alloc; +} + +GLvoid *GLVertexBufferPrivate::mapNextFreeRange(size_t size) +{ + GLbitfield access = GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_RANGE_BIT | GL_MAP_UNSYNCHRONIZED_BIT; + + if ((nextOffset + size) > bufferSize) { + // Reallocate the data store if it's too small. + if (size > bufferSize) { + reallocateBuffer(size); + } else { + access |= GL_MAP_INVALIDATE_BUFFER_BIT; + access ^= GL_MAP_UNSYNCHRONIZED_BIT; + } + + nextOffset = 0; + } + + return glMapBufferRange(GL_ARRAY_BUFFER, nextOffset, size, access); +} + + +//********************************* +// GLVertexBuffer +//********************************* +QRect GLVertexBuffer::s_virtualScreenGeometry; +qreal GLVertexBuffer::s_virtualScreenScale; + +GLVertexBuffer::GLVertexBuffer(UsageHint hint) + : d(new GLVertexBufferPrivate(hint)) +{ +} + +GLVertexBuffer::~GLVertexBuffer() +{ + delete d; +} + +void GLVertexBuffer::setData(const void *data, size_t size) +{ + GLvoid *ptr = map(size); + memcpy(ptr, data, size); + unmap(); +} + +void GLVertexBuffer::setData(int vertexCount, int dim, const float* vertices, const float* texcoords) +{ + const GLVertexAttrib layout[] = { + { VA_Position, dim, GL_FLOAT, 0 }, + { VA_TexCoord, 2, GL_FLOAT, int(dim * sizeof(float)) } + }; + + int stride = (texcoords ? dim + 2 : dim) * sizeof(float); + int attribCount = texcoords ? 2 : 1; + + setAttribLayout(layout, attribCount, stride); + setVertexCount(vertexCount); + + GLvoid *ptr = map(vertexCount * stride); + d->interleaveArrays((float *) ptr, dim, vertices, texcoords, vertexCount); + unmap(); +} + +GLvoid *GLVertexBuffer::map(size_t size) +{ + d->mappedSize = size; + d->frameSize += size; + + if (d->persistent) + return d->getIdleRange(size); + + glBindBuffer(GL_ARRAY_BUFFER, d->buffer); + + bool preferBufferSubData = GLPlatform::instance()->preferBufferSubData(); + + if (GLVertexBufferPrivate::hasMapBufferRange && !preferBufferSubData) + return (GLvoid *) d->mapNextFreeRange(size); + + // If we can't map the buffer we allocate local memory to hold the + // buffer data and return a pointer to it. The data will be submitted + // to the actual buffer object when the user calls unmap(). + if (size_t(d->dataStore.size()) < size) + d->dataStore.resize(size); + + return (GLvoid *) d->dataStore.data(); +} + +void GLVertexBuffer::unmap() +{ + if (d->persistent) { + d->baseAddress = d->nextOffset; + d->nextOffset += align(d->mappedSize, 16); // Align to 16 bytes for SSE + d->mappedSize = 0; + return; + } + + bool preferBufferSubData = GLPlatform::instance()->preferBufferSubData(); + + if (GLVertexBufferPrivate::hasMapBufferRange && !preferBufferSubData) { + glUnmapBuffer(GL_ARRAY_BUFFER); + + d->baseAddress = d->nextOffset; + d->nextOffset += align(d->mappedSize, 16); // Align to 16 bytes for SSE + } else { + // Upload the data from local memory to the buffer object + if (preferBufferSubData) { + if ((d->nextOffset + d->mappedSize) > d->bufferSize) { + d->reallocateBuffer(d->mappedSize); + d->nextOffset = 0; + } + + glBufferSubData(GL_ARRAY_BUFFER, d->nextOffset, d->mappedSize, d->dataStore.constData()); + + d->baseAddress = d->nextOffset; + d->nextOffset += align(d->mappedSize, 16); // Align to 16 bytes for SSE + } else { + glBufferData(GL_ARRAY_BUFFER, d->mappedSize, d->dataStore.data(), d->usage); + d->baseAddress = 0; + } + + // Free the local memory buffer if it's unlikely to be used again + if (d->usage == GL_STATIC_DRAW) + d->dataStore = QByteArray(); + + } + + d->mappedSize = 0; +} + +void GLVertexBuffer::setVertexCount(int count) +{ + d->vertexCount = count; +} + +void GLVertexBuffer::setAttribLayout(const GLVertexAttrib *attribs, int count, int stride) +{ + // Start by disabling all arrays + d->enabledArrays = 0; + + for (int i = 0; i < count; i++) { + const int index = attribs[i].index; + + Q_ASSERT(index >= 0 && index < VertexAttributeCount); + Q_ASSERT(!d->enabledArrays[index]); + + d->attrib[index].size = attribs[i].size; + d->attrib[index].type = attribs[i].type; + d->attrib[index].offset = attribs[i].relativeOffset; + + d->enabledArrays[index] = true; + } + + d->stride = stride; +} + +void GLVertexBuffer::render(GLenum primitiveMode) +{ + render(infiniteRegion(), primitiveMode, false); +} + +void GLVertexBuffer::render(const QRegion& region, GLenum primitiveMode, bool hardwareClipping) +{ + d->bindArrays(); + draw(region, primitiveMode, 0, d->vertexCount, hardwareClipping); + d->unbindArrays(); +} + +void GLVertexBuffer::bindArrays() +{ + d->bindArrays(); +} + +void GLVertexBuffer::unbindArrays() +{ + d->unbindArrays(); +} + +void GLVertexBuffer::draw(GLenum primitiveMode, int first, int count) +{ + draw(infiniteRegion(), primitiveMode, first, count, false); +} + +void GLVertexBuffer::draw(const QRegion ®ion, GLenum primitiveMode, int first, int count, bool hardwareClipping) +{ + if (primitiveMode == GL_QUADS) { + IndexBuffer *&indexBuffer = GLVertexBufferPrivate::s_indexBuffer; + + if (!indexBuffer) + indexBuffer = new IndexBuffer; + + indexBuffer->bind(); + indexBuffer->accommodate(count / 4); + + count = count * 6 / 4; + + if (!hardwareClipping) { + glDrawElementsBaseVertex(GL_TRIANGLES, count, GL_UNSIGNED_SHORT, nullptr, first); + } else { + // Clip using scissoring + for (const QRect &r : region) { + glScissor((r.x() - s_virtualScreenGeometry.x()) * s_virtualScreenScale, + (s_virtualScreenGeometry.height() + s_virtualScreenGeometry.y() - r.y() - r.height()) * s_virtualScreenScale, + r.width() * s_virtualScreenScale, + r.height() * s_virtualScreenScale); + glDrawElementsBaseVertex(GL_TRIANGLES, count, GL_UNSIGNED_SHORT, nullptr, first); + } + } + return; + } + + if (!hardwareClipping) { + glDrawArrays(primitiveMode, first, count); + } else { + // Clip using scissoring + for (const QRect &r : region) { + glScissor((r.x() - s_virtualScreenGeometry.x()) * s_virtualScreenScale, + (s_virtualScreenGeometry.height() + s_virtualScreenGeometry.y() - r.y() - r.height()) * s_virtualScreenScale, + r.width() * s_virtualScreenScale, + r.height() * s_virtualScreenScale); + glDrawArrays(primitiveMode, first, count); + } + } +} + +bool GLVertexBuffer::supportsIndexedQuads() +{ + return GLVertexBufferPrivate::supportsIndexedQuads; +} + +bool GLVertexBuffer::isUseColor() const +{ + return d->useColor; +} + +void GLVertexBuffer::setUseColor(bool enable) +{ + d->useColor = enable; +} + +void GLVertexBuffer::setColor(const QColor& color, bool enable) +{ + d->useColor = enable; + d->color = QVector4D(color.redF(), color.greenF(), color.blueF(), color.alphaF()); +} + +void GLVertexBuffer::reset() +{ + d->useColor = false; + d->color = QVector4D(0, 0, 0, 1); + d->vertexCount = 0; +} + +void GLVertexBuffer::endOfFrame() +{ + if (!d->persistent) + return; + + // Emit a fence if we have uploaded data + if (d->frameSize > 0) { + d->frameSizes.push(d->frameSize); + d->frameSize = 0; + + // Force the buffer to be reallocated at the beginning of the next frame + // if the average frame size is greater than half the size of the buffer + if (unlikely(d->frameSizes.average() > d->bufferSize / 2)) { + deleteAll(d->fences); + glDeleteBuffers(1, &d->buffer); + + d->buffer = 0; + d->bufferSize = 0; + d->nextOffset = 0; + d->map = nullptr; + } else { + BufferFence fence; + fence.sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + fence.nextEnd = d->nextOffset + d->bufferSize; + + d->fences.emplace_back(fence); + } + } +} + +void GLVertexBuffer::framePosted() +{ + if (!d->persistent) + return; + + // Remove finished fences from the list and update the bufferEnd offset + while (d->fences.size() > 1 && d->fences.front().signaled()) { + const BufferFence &fence = d->fences.front(); + glDeleteSync(fence.sync); + + d->bufferEnd = fence.nextEnd; + d->fences.pop_front(); + } +} + +void GLVertexBuffer::initStatic() +{ + if (GLPlatform::instance()->isGLES()) { + bool haveBaseVertex = hasGLExtension(QByteArrayLiteral("GL_OES_draw_elements_base_vertex")); + bool haveCopyBuffer = hasGLVersion(3, 0); + bool haveMapBufferRange = hasGLExtension(QByteArrayLiteral("GL_EXT_map_buffer_range")); + + GLVertexBufferPrivate::hasMapBufferRange = haveMapBufferRange; + GLVertexBufferPrivate::supportsIndexedQuads = haveBaseVertex && haveCopyBuffer && haveMapBufferRange; + GLVertexBufferPrivate::haveBufferStorage = hasGLExtension("GL_EXT_buffer_storage"); + GLVertexBufferPrivate::haveSyncFences = hasGLVersion(3, 0); + } else { + bool haveBaseVertex = hasGLVersion(3, 2) || hasGLExtension(QByteArrayLiteral("GL_ARB_draw_elements_base_vertex")); + bool haveCopyBuffer = hasGLVersion(3, 1) || hasGLExtension(QByteArrayLiteral("GL_ARB_copy_buffer")); + bool haveMapBufferRange = hasGLVersion(3, 0) || hasGLExtension(QByteArrayLiteral("GL_ARB_map_buffer_range")); + + GLVertexBufferPrivate::hasMapBufferRange = haveMapBufferRange; + GLVertexBufferPrivate::supportsIndexedQuads = haveBaseVertex && haveCopyBuffer && haveMapBufferRange; + GLVertexBufferPrivate::haveBufferStorage = hasGLVersion(4, 4) || hasGLExtension("GL_ARB_buffer_storage"); + GLVertexBufferPrivate::haveSyncFences = hasGLVersion(3, 2) || hasGLExtension("GL_ARB_sync"); + } + GLVertexBufferPrivate::s_indexBuffer = nullptr; + GLVertexBufferPrivate::streamingBuffer = new GLVertexBuffer(GLVertexBuffer::Stream); + + if (GLVertexBufferPrivate::haveBufferStorage && GLVertexBufferPrivate::haveSyncFences) { + if (qgetenv("KWIN_PERSISTENT_VBO") != QByteArrayLiteral("0")) { + GLVertexBufferPrivate::streamingBuffer->d->persistent = true; + } + } +} + +void GLVertexBuffer::cleanup() +{ + delete GLVertexBufferPrivate::s_indexBuffer; + GLVertexBufferPrivate::s_indexBuffer = nullptr; + GLVertexBufferPrivate::hasMapBufferRange = false; + GLVertexBufferPrivate::supportsIndexedQuads = false; + delete GLVertexBufferPrivate::streamingBuffer; + GLVertexBufferPrivate::streamingBuffer = nullptr; +} + +GLVertexBuffer *GLVertexBuffer::streamingBuffer() +{ + return GLVertexBufferPrivate::streamingBuffer; +} + +} // namespace diff --git a/libkwineffects/kwinglutils.h b/libkwineffects/kwinglutils.h new file mode 100644 index 0000000..c2e8493 --- /dev/null +++ b/libkwineffects/kwinglutils.h @@ -0,0 +1,818 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006-2007 Rivo Laks + SPDX-FileCopyrightText: 2010, 2011 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_GLUTILS_H +#define KWIN_GLUTILS_H + +// kwin +#include +#include "kwinglutils_funcs.h" +#include "kwingltexture.h" + +// Qt +#include +#include + +/** @addtogroup kwineffects */ +/** @{ */ + +class QVector2D; +class QVector3D; +class QVector4D; +class QMatrix4x4; + +template< class K, class V > class QHash; + + +namespace KWin +{ + +class GLVertexBuffer; +class GLVertexBufferPrivate; + +// Initializes OpenGL stuff. This includes resolving function pointers as +// well as checking for GL version and extensions +// Note that GL context has to be created by the time this function is called +typedef void (*resolveFuncPtr)(); +void KWINGLUTILS_EXPORT initGL(const std::function &resolveFunction); +// Cleans up all resources hold by the GL Context +void KWINGLUTILS_EXPORT cleanupGL(); + + +bool KWINGLUTILS_EXPORT hasGLVersion(int major, int minor, int release = 0); +// use for both OpenGL and GLX extensions +bool KWINGLUTILS_EXPORT hasGLExtension(const QByteArray &extension); + +// detect OpenGL error (add to various places in code to pinpoint the place) +bool KWINGLUTILS_EXPORT checkGLError(const char* txt); + +QList KWINGLUTILS_EXPORT openGLExtensions(); + +class KWINGLUTILS_EXPORT GLShader +{ +public: + enum Flags { + NoFlags = 0, + ExplicitLinking = (1 << 0) + }; + + GLShader(const QString &vertexfile, const QString &fragmentfile, unsigned int flags = NoFlags); + ~GLShader(); + + bool isValid() const { + return mValid; + } + + void bindAttributeLocation(const char *name, int index); + void bindFragDataLocation(const char *name, int index); + + bool link(); + + int uniformLocation(const char* name); + + bool setUniform(const char* name, float value); + bool setUniform(const char* name, int value); + bool setUniform(const char* name, const QVector2D& value); + bool setUniform(const char* name, const QVector3D& value); + bool setUniform(const char* name, const QVector4D& value); + bool setUniform(const char* name, const QMatrix4x4& value); + bool setUniform(const char* name, const QColor& color); + + bool setUniform(int location, float value); + bool setUniform(int location, int value); + bool setUniform(int location, const QVector2D &value); + bool setUniform(int location, const QVector3D &value); + bool setUniform(int location, const QVector4D &value); + bool setUniform(int location, const QMatrix4x4 &value); + bool setUniform(int location, const QColor &value); + + int attributeLocation(const char* name); + bool setAttribute(const char* name, float value); + /** + * @return The value of the uniform as a matrix + * @since 4.7 + */ + QMatrix4x4 getUniformMatrix4x4(const char* name); + + enum MatrixUniform { + TextureMatrix = 0, + ProjectionMatrix, + ModelViewMatrix, + ModelViewProjectionMatrix, + WindowTransformation, + ScreenTransformation, + MatrixCount + }; + + enum Vec2Uniform { + Offset, + Vec2UniformCount + }; + + enum Vec4Uniform { + ModulationConstant, + TextureClamp, + Vec4UniformCount + }; + + enum FloatUniform { + Saturation, + FloatUniformCount + }; + + enum IntUniform { + AlphaToOne, ///< @deprecated no longer used + IntUniformCount + }; + + enum ColorUniform { + Color, + ColorUniformCount + }; + + bool setUniform(MatrixUniform uniform, const QMatrix4x4 &matrix); + bool setUniform(Vec2Uniform uniform, const QVector2D &value); + bool setUniform(Vec4Uniform uniform, const QVector4D &value); + bool setUniform(FloatUniform uniform, float value); + bool setUniform(IntUniform uniform, int value); + bool setUniform(ColorUniform uniform, const QVector4D &value); + bool setUniform(ColorUniform uniform, const QColor &value); + +protected: + GLShader(unsigned int flags = NoFlags); + bool loadFromFiles(const QString& vertexfile, const QString& fragmentfile); + bool load(const QByteArray &vertexSource, const QByteArray &fragmentSource); + const QByteArray prepareSource(GLenum shaderType, const QByteArray &sourceCode) const; + bool compile(GLuint program, GLenum shaderType, const QByteArray &sourceCode) const; + void bind(); + void unbind(); + void resolveLocations(); + +private: + unsigned int mProgram; + bool mValid:1; + bool mLocationsResolved:1; + bool mExplicitLinking:1; + int mMatrixLocation[MatrixCount]; + int mVec2Location[Vec2UniformCount]; + int mVec4Location[Vec4UniformCount]; + int mFloatLocation[FloatUniformCount]; + int mIntLocation[IntUniformCount]; + int mColorLocation[ColorUniformCount]; + + friend class ShaderManager; +}; + + +enum class ShaderTrait { + MapTexture = (1 << 0), + UniformColor = (1 << 1), + Modulate = (1 << 2), + AdjustSaturation = (1 << 3), + ClampTexture = (1 << 4), +}; + +Q_DECLARE_FLAGS(ShaderTraits, ShaderTrait) + + +/** + * @short Manager for Shaders. + * + * This class provides some built-in shaders to be used by both compositing scene and effects. + * The ShaderManager provides methods to bind a built-in or a custom shader and keeps track of + * the shaders which have been bound. When a shader is unbound the previously bound shader + * will be rebound. + * + * @author Martin Gräßlin + * @since 4.7 + */ +class KWINGLUTILS_EXPORT ShaderManager +{ +public: + /** + * Returns a shader with the given traits, creating it if necessary. + */ + GLShader *shader(ShaderTraits traits); + + /** + * @return The currently bound shader or @c null if no shader is bound. + */ + GLShader *getBoundShader() const; + + /** + * @return @c true if a shader is bound, @c false otherwise + */ + bool isShaderBound() const; + + /** + * Pushes the current shader onto the stack and binds a shader + * with the given traits. + */ + GLShader *pushShader(ShaderTraits traits); + + /** + * Binds the @p shader. + * To unbind the shader use popShader. A previous bound shader will be rebound. + * To bind a built-in shader use the more specific method. + * @param shader The shader to be bound + * @see popShader + */ + void pushShader(GLShader *shader); + + /** + * Unbinds the currently bound shader and rebinds a previous stored shader. + * If there is no previous shader, no shader will be rebound. + * It is not safe to call this method if there is no bound shader. + * @see pushShader + * @see getBoundShader + */ + void popShader(); + + /** + * Creates a GLShader with the specified sources. + * The difference to GLShader is that it does not need to be loaded from files. + * @param vertexSource The source code of the vertex shader + * @param fragmentSource The source code of the fragment shader. + * @return The created shader + */ + GLShader *loadShaderFromCode(const QByteArray &vertexSource, const QByteArray &fragmentSource); + + /** + * Creates a custom shader with the given @p traits and custom @p vertexSource and or @p fragmentSource. + * If the @p vertexSource is empty a vertex shader with the given @p traits is generated. + * If it is not empty the @p vertexSource is used as the source for the vertex shader. + * + * The same applies for argument @p fragmentSource just for the fragment shader. + * + * So if both @p vertesSource and @p fragmentSource are provided the @p traits are ignored. + * If neither are provided a new shader following the @p traits is generated. + * + * @param traits The shader traits for generating the shader + * @param vertexSource optional vertex shader source code to be used instead of shader traits + * @param fragmentSource optional fragment shader source code to be used instead of shader traits + * @return new generated shader + * @since 5.6 + */ + GLShader *generateCustomShader(ShaderTraits traits, const QByteArray &vertexSource = QByteArray(), const QByteArray &fragmentSource = QByteArray()); + + /** + * Creates a custom shader with the given @p traits and custom @p vertexFile and or @p fragmentFile. + * The file names specified in @p vertexFile and @p fragmentFile are relative paths to the shaders + * resource file shipped together with KWin. This means this method can only be used for built-in + * effects, for 3rd party effects generateCustomShader should be used. + * + * If the @p vertexFile is empty a vertex shader with the given @p traits is generated. + * If it is not empty the @p vertexFile is used as the source for the vertex shader. + * + * The same applies for argument @p fragmentFile just for the fragment shader. + * + * So if both @p vertexFile and @p fragmentFile are provided the @p traits are ignored. + * If neither are provided a new shader following the @p traits is generated. + * + * @param traits The shader traits for generating the shader + * @param vertexFile optional vertex shader source code to be used instead of shader traits + * @param fragmentFile optional fragment shader source code to be used instead of shader traits + * @return new generated shader + * @see generateCustomShader + * @since 5.6 + */ + GLShader *generateShaderFromResources(ShaderTraits traits, const QString &vertexFile = QString(), const QString &fragmentFile = QString()); + + /** + * Compiles and tests the dynamically generated shaders. + * Returns true if successful and false otherwise. + */ + bool selfTest(); + + /** + * @return a pointer to the ShaderManager instance + */ + static ShaderManager *instance(); + + /** + * @internal + */ + static void cleanup(); + +private: + ShaderManager(); + ~ShaderManager(); + + void bindFragDataLocations(GLShader *shader); + void bindAttributeLocations(GLShader *shader) const; + + QByteArray generateVertexSource(ShaderTraits traits) const; + QByteArray generateFragmentSource(ShaderTraits traits) const; + GLShader *generateShader(ShaderTraits traits); + + QStack m_boundShaders; + QHash m_shaderHash; + QString m_resourcePath; + static ShaderManager *s_shaderManager; +}; + +/** + * An helper class to push a Shader on to ShaderManager's stack and ensuring that the Shader + * gets popped again from the stack automatically once the object goes out of life. + * + * How to use: + * @code + * { + * GLShader *myCustomShaderIWantToPush; + * ShaderBinder binder(myCustomShaderIWantToPush); + * // do stuff with the shader being pushed on the stack + * } + * // here the Shader is automatically popped as helper does no longer exist. + * @endcode + * + * @since 4.10 + */ +class KWINGLUTILS_EXPORT ShaderBinder +{ +public: + /** + * @brief Pushes the given @p shader to the ShaderManager's stack. + * + * @param shader The Shader to push on the stack + * @see ShaderManager::pushShader + */ + explicit ShaderBinder(GLShader *shader); + /** + * @brief Pushes the Shader with the given @p traits to the ShaderManager's stack. + * + * @param traits The traits describing the shader + * @see ShaderManager::pushShader + * @since 5.6 + */ + explicit ShaderBinder(ShaderTraits traits); + ~ShaderBinder(); + + /** + * @return The Shader pushed to the Stack. + */ + GLShader *shader(); + +private: + GLShader *m_shader; +}; + +inline +ShaderBinder::ShaderBinder(GLShader *shader) + : m_shader(shader) +{ + ShaderManager::instance()->pushShader(shader); +} + +inline +ShaderBinder::ShaderBinder(ShaderTraits traits) + : m_shader(nullptr) +{ + m_shader = ShaderManager::instance()->pushShader(traits); +} + +inline +ShaderBinder::~ShaderBinder() +{ + ShaderManager::instance()->popShader(); +} + +inline +GLShader* ShaderBinder::shader() +{ + return m_shader; +} + +/** + * @short Render target object + * + * Render target object enables you to render onto a texture. This texture can + * later be used to e.g. do post-processing of the scene. + * + * @author Rivo Laks + */ +class KWINGLUTILS_EXPORT GLRenderTarget +{ +public: + /** + * Constructs a GLRenderTarget + * @since 5.13 + */ + explicit GLRenderTarget(); + + /** + * Constructs a GLRenderTarget + * @param color texture where the scene will be rendered onto + */ + explicit GLRenderTarget(const GLTexture& color); + ~GLRenderTarget(); + + /** + * Enables this render target. + * All OpenGL commands from now on affect this render target until the + * @ref disable method is called + */ + bool enable(); + /** + * Disables this render target, activating whichever target was active + * when @ref enable was called. + */ + bool disable(); + + /** + * Sets the target texture + * @param target texture where the scene will be rendered on + * @since 4.8 + */ + void attachTexture(const GLTexture& target); + + /** + * Detaches the texture that is currently attached to this framebuffer object. + * @since 5.13 + */ + void detachTexture(); + + bool valid() const { + return mValid; + } + + void setTextureDirty() { + mTexture.setDirty(); + } + + static void initStatic(); + static bool supported() { + return sSupported; + } + + /** + * Pushes the render target stack of the input parameter in reverse order. + * @param targets The stack of GLRenderTargets + * @since 5.13 + */ + static void pushRenderTargets(QStack targets); + + static void pushRenderTarget(GLRenderTarget *target); + static GLRenderTarget *popRenderTarget(); + static bool isRenderTargetBound(); + /** + * Whether the GL_EXT_framebuffer_blit extension is supported. + * This functionality is not available in OpenGL ES 2.0. + * + * @returns whether framebuffer blitting is supported. + * @since 4.8 + */ + static bool blitSupported(); + + /** + * Blits the content of the current draw framebuffer into the texture attached to this FBO. + * + * Be aware that framebuffer blitting may not be supported on all hardware. Use blitSupported to check whether + * it is supported. + * @param source Geometry in screen coordinates which should be blitted, if not specified complete framebuffer is used + * @param destination Geometry in attached texture, if not specified complete texture is used as destination + * @param filter The filter to use if blitted content needs to be scaled. + * @see blitSupported + * @since 4.8 + */ + void blitFromFramebuffer(const QRect &source = QRect(), const QRect &destination = QRect(), GLenum filter = GL_LINEAR); + + /** + * Sets the virtual screen size to @p s. + * @since 5.2 + */ + static void setVirtualScreenSize(const QSize &s) { + s_virtualScreenSize = s; + } + + /** + * Sets the virtual screen geometry to @p g. + * This is the geometry of the OpenGL window currently being rendered to + * in the virtual geometry space the rendering geometries use. + * @see virtualScreenGeometry + * @since 5.9 + */ + static void setVirtualScreenGeometry(const QRect &g) { + s_virtualScreenGeometry = g; + } + + /** + * The geometry of the OpenGL window currently being rendered to + * in the virtual geometry space the rendering system uses. + * @see setVirtualScreenGeometry + * @since 5.9 + */ + static QRect virtualScreenGeometry() { + return s_virtualScreenGeometry; + } + + /** + * The scale of the OpenGL window currently being rendered to + * + * @returns the ratio between the virtual geometry space the rendering + * system uses and the target + * @since 5.10 + */ + static void setVirtualScreenScale(qreal scale) { + s_virtualScreenScale = scale; + } + + static qreal virtualScreenScale() { + return s_virtualScreenScale; + } + + /** + * The framebuffer of KWin's OpenGL window or other object currently being rendered to + * + * @since 5.18 + */ + static void setKWinFramebuffer(GLuint fb) { + s_kwinFramebuffer = fb; + } + + +protected: + void initFBO(); + + +private: + friend void KWin::cleanupGL(); + static void cleanup(); + static bool sSupported; + static bool s_blitSupported; + static QStack s_renderTargets; + static QSize s_virtualScreenSize; + static QRect s_virtualScreenGeometry; + static qreal s_virtualScreenScale; + static GLint s_virtualScreenViewport[4]; + static GLuint s_kwinFramebuffer; + + GLTexture mTexture; + bool mValid; + + GLuint mFramebuffer; +}; + +enum VertexAttributeType { + VA_Position = 0, + VA_TexCoord = 1, + VertexAttributeCount = 2 +}; + +/** + * Describes the format of a vertex attribute stored in a buffer object. + * + * The attribute format consists of the attribute index, the number of + * vector components, the data type, and the offset of the first element + * relative to the start of the vertex data. + */ +struct GLVertexAttrib +{ + int index; /** The attribute index */ + int size; /** The number of components [1..4] */ + GLenum type; /** The type (e.g. GL_FLOAT) */ + int relativeOffset; /** The relative offset of the attribute */ +}; + +/** + * @short Vertex Buffer Object + * + * This is a short helper class to use vertex buffer objects (VBO). A VBO can be used to buffer + * vertex data and to store them on graphics memory. It is the only allowed way to pass vertex + * data to the GPU in OpenGL ES 2 and OpenGL 3 with forward compatible mode. + * + * If VBOs are not supported on the used OpenGL profile this class falls back to legacy + * rendering using client arrays. Therefore this class should always be used for rendering geometries. + * + * @author Martin Gräßlin + * @since 4.6 + */ +class KWINGLUTILS_EXPORT GLVertexBuffer +{ +public: + /** + * Enum to define how often the vertex data in the buffer object changes. + */ + enum UsageHint { + Dynamic, ///< frequent changes, but used several times for rendering + Static, ///< No changes to data + Stream ///< Data only used once for rendering, updated very frequently + }; + + explicit GLVertexBuffer(UsageHint hint); + ~GLVertexBuffer(); + + /** + * Specifies how interleaved vertex attributes are laid out in + * the buffer object. + * + * Note that the attributes and the stride should be 32 bit aligned + * or a performance penalty may be incurred. + * + * For some hardware the optimal stride is a multiple of 32 bytes. + * + * Example: + * + * struct Vertex { + * QVector3D position; + * QVector2D texcoord; + * }; + * + * const GLVertexAttrib attribs[] = { + * { VA_Position, 3, GL_FLOAT, offsetof(Vertex, position) }, + * { VA_TexCoord, 2, GL_FLOAT, offsetof(Vertex, texcoord) } + * }; + * + * Vertex vertices[6]; + * vbo->setAttribLayout(attribs, 2, sizeof(Vertex)); + * vbo->setData(vertices, sizeof(vertices)); + */ + void setAttribLayout(const GLVertexAttrib *attribs, int count, int stride); + + /** + * Uploads data into the buffer object's data store. + */ + void setData(const void *data, size_t sizeInBytes); + + /** + * Sets the number of vertices that will be drawn by the render() method. + */ + void setVertexCount(int count); + + /** + * Sets the vertex data. + * @param numberVertices The number of vertices in the arrays + * @param dim The dimension of the vertices: 2 for x/y, 3 for x/y/z + * @param vertices The vertices, size must equal @a numberVertices * @a dim + * @param texcoords The texture coordinates for each vertex. + * Size must equal 2 * @a numberVertices. + */ + void setData(int numberVertices, int dim, const float* vertices, const float* texcoords); + + /** + * Maps an unused range of the data store into the client's address space. + * + * The data store will be reallocated if it is smaller than the given size. + * + * The buffer object is mapped for writing, not reading. Attempts to read from + * the mapped buffer range may result in system errors, including program + * termination. The data in the mapped region is undefined until it has been + * written to. If subsequent GL calls access unwritten memory, the results are + * undefined and system errors, including program termination, may occur. + * + * No GL calls that access the buffer object must be made while the buffer + * object is mapped. The returned pointer must not be passed as a parameter + * value to any GL function. + * + * It is assumed that the GL_ARRAY_BUFFER_BINDING will not be changed while + * the buffer object is mapped. + */ + GLvoid *map(size_t size); + + /** + * Flushes the mapped buffer range and unmaps the buffer. + */ + void unmap(); + + /** + * Binds the vertex arrays to the context. + */ + void bindArrays(); + + /** + * Disables the vertex arrays. + */ + void unbindArrays(); + + /** + * Draws count vertices beginning with first. + */ + void draw(GLenum primitiveMode, int first, int count); + + /** + * Draws count vertices beginning with first. + */ + void draw(const QRegion ®ion, GLenum primitiveMode, int first, int count, bool hardwareClipping = false); + + /** + * Renders the vertex data in given @a primitiveMode. + * Please refer to OpenGL documentation of glDrawArrays or glDrawElements for allowed + * values for @a primitiveMode. Best is to use GL_TRIANGLES or similar to be future + * compatible. + */ + void render(GLenum primitiveMode); + /** + * Same as above restricting painting to @a region if @a hardwareClipping is true. + * It's within the caller's responsibility to enable GL_SCISSOR_TEST. + */ + void render(const QRegion& region, GLenum primitiveMode, bool hardwareClipping = false); + /** + * Sets the color the geometry will be rendered with. + * For legacy rendering glColor is used before rendering the geometry. + * For core shader a uniform "geometryColor" is expected and is set. + * @param color The color to render the geometry + * @param enableColor Whether the geometry should be rendered with a color or not + * @see setUseColor + * @see isUseColor + * @since 4.7 + */ + void setColor(const QColor& color, bool enableColor = true); + /** + * @return @c true if geometry will be painted with a color, @c false otherwise + * @see setUseColor + * @see setColor + * @since 4.7 + */ + bool isUseColor() const; + /** + * Enables/Disables rendering the geometry with a color. + * If no color is set an opaque, black color is used. + * @param enable Enable/Disable rendering with color + * @see isUseColor + * @see setColor + * @since 4.7 + */ + void setUseColor(bool enable); + + /** + * Resets the instance to default values. + * Useful for shared buffers. + * @since 4.7 + */ + void reset(); + + /** + * Notifies the vertex buffer that we are done painting the frame. + * + * @internal + */ + void endOfFrame(); + + /** + * Notifies the vertex buffer that we have posted the frame. + * + * @internal + */ + void framePosted(); + + /** + * @internal + */ + static void initStatic(); + + /** + * @internal + */ + static void cleanup(); + + /** + * Returns true if indexed quad mode is supported, and false otherwise. + */ + static bool supportsIndexedQuads(); + + /** + * @return A shared VBO for streaming data + * @since 4.7 + */ + static GLVertexBuffer *streamingBuffer(); + + /** + * Sets the virtual screen geometry to @p g. + * This is the geometry of the OpenGL window currently being rendered to + * in the virtual geometry space the rendering geometries use. + * @since 5.9 + */ + static void setVirtualScreenGeometry(const QRect &g) { + s_virtualScreenGeometry = g; + } + + /** + * The scale of the OpenGL window currently being rendered to + * + * @returns the ratio between the virtual geometry space the rendering + * system uses and the target + * @since 5.11.3 + */ + static void setVirtualScreenScale(qreal s) { + s_virtualScreenScale = s; + } + +private: + GLVertexBufferPrivate* const d; + static QRect s_virtualScreenGeometry; + static qreal s_virtualScreenScale; +}; + +} // namespace + +Q_DECLARE_OPERATORS_FOR_FLAGS(KWin::ShaderTraits) + +/** @} */ + +#endif diff --git a/libkwineffects/kwinglutils_funcs.cpp b/libkwineffects/kwinglutils_funcs.cpp new file mode 100644 index 0000000..4bee3f3 --- /dev/null +++ b/libkwineffects/kwinglutils_funcs.cpp @@ -0,0 +1,91 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Rivo Laks + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwinglutils.h" +#include "kwinglplatform.h" + + +// Resolves given function, using getProcAddress +// Useful when functionality is defined in an extension with a different name +#define GL_RESOLVE_WITH_EXT( function, symbolName ) \ + function = (function ## _func)resolveFunction( #symbolName ); + +namespace KWin +{ + +static GLenum GetGraphicsResetStatus(); +static void ReadnPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, + GLenum type, GLsizei bufSize, GLvoid *data); +static void GetnUniformfv(GLuint program, GLint location, GLsizei bufSize, GLfloat *params); + +// GL_ARB_robustness / GL_EXT_robustness +glGetGraphicsResetStatus_func glGetGraphicsResetStatus; +glReadnPixels_func glReadnPixels; +glGetnUniformfv_func glGetnUniformfv; + +void glResolveFunctions(const std::function &resolveFunction) +{ + const bool haveArbRobustness = hasGLExtension(QByteArrayLiteral("GL_ARB_robustness")); + const bool haveExtRobustness = hasGLExtension(QByteArrayLiteral("GL_EXT_robustness")); + bool robustContext = false; + if (GLPlatform::instance()->isGLES()) { + if (haveExtRobustness) { + GLint value = 0; + glGetIntegerv(GL_CONTEXT_ROBUST_ACCESS_EXT, &value); + robustContext = (value != 0); + } + } else { + if (haveArbRobustness) { + if (hasGLVersion(3, 0)) { + GLint value = 0; + glGetIntegerv(GL_CONTEXT_FLAGS, &value); + if (value & GL_CONTEXT_FLAG_ROBUST_ACCESS_BIT_ARB) { + robustContext = true; + } + } else { + robustContext = true; + } + } + } + if (robustContext && haveArbRobustness) { + // See https://www.opengl.org/registry/specs/ARB/robustness.txt + GL_RESOLVE_WITH_EXT(glGetGraphicsResetStatus, glGetGraphicsResetStatusARB); + GL_RESOLVE_WITH_EXT(glReadnPixels, glReadnPixelsARB); + GL_RESOLVE_WITH_EXT(glGetnUniformfv, glGetnUniformfvARB); + } else if (robustContext && haveExtRobustness) { + // See https://www.khronos.org/registry/gles/extensions/EXT/EXT_robustness.txt + glGetGraphicsResetStatus = (glGetGraphicsResetStatus_func) resolveFunction("glGetGraphicsResetStatusEXT"); + glReadnPixels = (glReadnPixels_func) resolveFunction("glReadnPixelsEXT"); + glGetnUniformfv = (glGetnUniformfv_func) resolveFunction("glGetnUniformfvEXT"); + } else { + glGetGraphicsResetStatus = KWin::GetGraphicsResetStatus; + glReadnPixels = KWin::ReadnPixels; + glGetnUniformfv = KWin::GetnUniformfv; + } +} + +static GLenum GetGraphicsResetStatus() +{ + return GL_NO_ERROR; +} + +static void ReadnPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, + GLenum type, GLsizei bufSize, GLvoid *data) +{ + Q_UNUSED(bufSize) + glReadPixels(x, y, width, height, format, type, data); +} + +static void GetnUniformfv(GLuint program, GLint location, GLsizei bufSize, GLfloat *params) +{ + Q_UNUSED(bufSize) + glGetUniformfv(program, location, params); +} + +} // namespace diff --git a/libkwineffects/kwinglutils_funcs.h b/libkwineffects/kwinglutils_funcs.h new file mode 100644 index 0000000..aaa6a11 --- /dev/null +++ b/libkwineffects/kwinglutils_funcs.h @@ -0,0 +1,49 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Rivo Laks + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_GLUTILS_FUNCS_H +#define KWIN_GLUTILS_FUNCS_H + +#include + +#include +#include + +// qopengl.h declares GLdouble as a typedef of float when Qt is built +// with GLES support. This conflicts with the epoxy/gl_generated.h +// declaration, so we have to prevent the Qt header from being #included. +#define QOPENGL_H + +#ifndef QOPENGLF_APIENTRY +#define QOPENGLF_APIENTRY GLAPIENTRY +#endif + +#ifndef QOPENGLF_APIENTRYP +#define QOPENGLF_APIENTRYP GLAPIENTRYP +#endif + +namespace KWin +{ + +typedef void (*resolveFuncPtr)(); +void KWINGLUTILS_EXPORT glResolveFunctions(const std::function &resolveFunction); + +// GL_ARB_robustness / GL_EXT_robustness +using glGetGraphicsResetStatus_func = GLenum (*)(); +using glReadnPixels_func = void (*)(GLint x, GLint y, GLsizei width, GLsizei height, + GLenum format, GLenum type, GLsizei bufSize, GLvoid *data); +using glGetnUniformfv_func = void (*)(GLuint program, GLint location, GLsizei bufSize, GLfloat *params); + +extern KWINGLUTILS_EXPORT glGetGraphicsResetStatus_func glGetGraphicsResetStatus; +extern KWINGLUTILS_EXPORT glReadnPixels_func glReadnPixels; +extern KWINGLUTILS_EXPORT glGetnUniformfv_func glGetnUniformfv; + +} // namespace + +#endif // KWIN_GLUTILS_FUNCS_H diff --git a/libkwineffects/kwinxrenderutils.cpp b/libkwineffects/kwinxrenderutils.cpp new file mode 100644 index 0000000..1ee6f41 --- /dev/null +++ b/libkwineffects/kwinxrenderutils.cpp @@ -0,0 +1,282 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwinxrenderutils.h" +#include "logging_p.h" + +#include +#include +#include +#include + +namespace KWin +{ + +namespace XRenderUtils +{ +static xcb_connection_t *s_connection = nullptr; +static xcb_window_t s_rootWindow = XCB_WINDOW_NONE; +static XRenderPicture s_blendPicture(XCB_RENDER_PICTURE_NONE); + +void init(xcb_connection_t *connection, xcb_window_t rootWindow) +{ + s_connection = connection; + s_rootWindow = rootWindow; +} + +void cleanup() +{ + s_blendPicture = XRenderPicture(XCB_RENDER_PICTURE_NONE); + s_connection = nullptr; + s_rootWindow = XCB_WINDOW_NONE; +} + +} // namespace + +// adapted from Qt, because this really sucks ;) +xcb_render_color_t preMultiply(const QColor &c, float opacity) +{ + xcb_render_color_t color; + const uint A = c.alpha() * opacity, + R = c.red(), + G = c.green(), + B = c.blue(); + color.alpha = (A | A << 8); + color.red = (R | R << 8) * color.alpha / 0x10000; + color.green = (G | G << 8) * color.alpha / 0x10000; + color.blue = (B | B << 8) * color.alpha / 0x10000; + return color; +} + +XRenderPicture xRenderFill(const xcb_render_color_t &c) +{ + xcb_pixmap_t pixmap = xcb_generate_id(XRenderUtils::s_connection); + xcb_create_pixmap(XRenderUtils::s_connection, 32, pixmap, XRenderUtils::s_rootWindow, 1, 1); + XRenderPicture fill(pixmap, 32); + xcb_free_pixmap(XRenderUtils::s_connection, pixmap); + + uint32_t values[] = {true}; + xcb_render_change_picture(XRenderUtils::s_connection, fill, XCB_RENDER_CP_REPEAT, values); + + xcb_rectangle_t rect = {0, 0, 1, 1}; + xcb_render_fill_rectangles(XRenderUtils::s_connection, XCB_RENDER_PICT_OP_SRC, fill, c, 1, &rect); + return fill; +} + +XRenderPicture xRenderFill(const QColor &c) +{ + return xRenderFill(preMultiply(c)); +} + +XRenderPicture xRenderBlendPicture(double opacity) +{ + static xcb_render_color_t s_blendColor = {0, 0, 0, 0}; + s_blendColor.alpha = uint16_t(opacity * 0xffff); + if (XRenderUtils::s_blendPicture == XCB_RENDER_PICTURE_NONE) { + XRenderUtils::s_blendPicture = xRenderFill(s_blendColor); + } else { + xcb_rectangle_t rect = {0, 0, 1, 1}; + xcb_render_fill_rectangles(XRenderUtils::s_connection, XCB_RENDER_PICT_OP_SRC, XRenderUtils::s_blendPicture, s_blendColor, 1, &rect); + } + return XRenderUtils::s_blendPicture; +} + +static xcb_render_picture_t createPicture(xcb_pixmap_t pix, int depth) +{ + if (pix == XCB_PIXMAP_NONE) + return XCB_RENDER_PICTURE_NONE; + xcb_connection_t *c = XRenderUtils::s_connection; + static QHash s_renderFormats; + if (!s_renderFormats.contains(depth)) { + xcb_render_query_pict_formats_reply_t *formats = xcb_render_query_pict_formats_reply(c, xcb_render_query_pict_formats_unchecked(c), nullptr); + if (!formats) { + return XCB_RENDER_PICTURE_NONE; + } + for (xcb_render_pictforminfo_iterator_t it = xcb_render_query_pict_formats_formats_iterator(formats); + it.rem; + xcb_render_pictforminfo_next(&it)) { + if (it.data->depth == depth) { + s_renderFormats.insert(depth, it.data->id); + break; + } + } + free(formats); + } + QHash::const_iterator it = s_renderFormats.constFind(depth); + if (it == s_renderFormats.constEnd()) { + qCWarning(LIBKWINXRENDERUTILS) << "Could not find XRender format for depth" << depth; + return XCB_RENDER_PICTURE_NONE; + } + xcb_render_picture_t pic = xcb_generate_id(c); + xcb_render_create_picture(c, pic, pix, it.value(), 0, nullptr); + return pic; +} + +XRenderPicture::XRenderPicture(const QImage &img) +{ + fromImage(img); +} + +void XRenderPicture::fromImage(const QImage &img) +{ + xcb_connection_t *c = XRenderUtils::s_connection; + const int depth = img.depth(); + xcb_pixmap_t xpix = xcb_generate_id(c); + xcb_create_pixmap(c, depth, xpix, XRenderUtils::s_rootWindow, img.width(), img.height()); + + xcb_gcontext_t cid = xcb_generate_id(c); + xcb_create_gc(c, cid, xpix, 0, nullptr); + xcb_put_image(c, XCB_IMAGE_FORMAT_Z_PIXMAP, xpix, cid, img.width(), img.height(), + 0, 0, 0, depth, img.sizeInBytes(), img.constBits()); + xcb_free_gc(c, cid); + + d = new XRenderPictureData(createPicture(xpix, depth)); + xcb_free_pixmap(c, xpix); +} + +XRenderPicture::XRenderPicture(xcb_pixmap_t pix, int depth) + : d(new XRenderPictureData(createPicture(pix, depth))) +{ +} + +XRenderPictureData::~XRenderPictureData() +{ + if (picture != XCB_RENDER_PICTURE_NONE) { + Q_ASSERT(qApp); + xcb_render_free_picture(XRenderUtils::s_connection, picture); + } +} + +XFixesRegion::XFixesRegion(const QRegion ®ion) +{ + m_region = xcb_generate_id(XRenderUtils::s_connection); + QVector xrects; + xrects.reserve(region.rectCount()); + for (const QRect &rect : region) { + xcb_rectangle_t xrect; + xrect.x = rect.x(); + xrect.y = rect.y(); + xrect.width = rect.width(); + xrect.height = rect.height(); + xrects.append(xrect); + } + xcb_xfixes_create_region(XRenderUtils::s_connection, m_region, xrects.count(), xrects.constData()); +} + +XFixesRegion::~XFixesRegion() +{ + xcb_xfixes_destroy_region(XRenderUtils::s_connection, m_region); +} + +static xcb_render_picture_t s_offscreenTarget = XCB_RENDER_PICTURE_NONE; +static QStack s_scene_offscreenTargetStack; +static int s_renderOffscreen = 0; + +void scene_setXRenderOffscreenTarget(xcb_render_picture_t pix) +{ + s_offscreenTarget = pix; +} + +XRenderPicture *scene_xRenderOffscreenTarget() +{ + return s_scene_offscreenTargetStack.isEmpty() ? nullptr : s_scene_offscreenTargetStack.top(); +} + +void setXRenderOffscreen(bool b) +{ + b ? ++s_renderOffscreen : --s_renderOffscreen; + if (s_renderOffscreen < 0) { + s_renderOffscreen = 0; + qCWarning(LIBKWINXRENDERUTILS) << "*** SOMETHING IS MESSED UP WITH YOUR setXRenderOffscreen() USAGE ***"; + } +} + +void xRenderPushTarget(XRenderPicture *pic) +{ + s_scene_offscreenTargetStack.push(pic); + ++s_renderOffscreen; +} + +void xRenderPopTarget() +{ + s_scene_offscreenTargetStack.pop(); + --s_renderOffscreen; + if (s_renderOffscreen < 0) { + s_renderOffscreen = 0; + qCWarning(LIBKWINXRENDERUTILS) << "*** SOMETHING IS MESSED UP WITH YOUR xRenderPopTarget() USAGE ***"; + } +} + +bool xRenderOffscreen() +{ + return s_renderOffscreen; +} + +xcb_render_picture_t xRenderOffscreenTarget() +{ + return s_offscreenTarget; +} + +namespace XRenderUtils +{ + +struct PictFormatData +{ + PictFormatData() { + // Fetch the render pict formats + reply = xcb_render_query_pict_formats_reply(s_connection, + xcb_render_query_pict_formats_unchecked(s_connection), nullptr); + + // Init the visual ID -> format ID hash table + for (auto screens = xcb_render_query_pict_formats_screens_iterator(reply); screens.rem; xcb_render_pictscreen_next(&screens)) { + for (auto depths = xcb_render_pictscreen_depths_iterator(screens.data); depths.rem; xcb_render_pictdepth_next(&depths)) { + const xcb_render_pictvisual_t *visuals = xcb_render_pictdepth_visuals(depths.data); + const int len = xcb_render_pictdepth_visuals_length(depths.data); + + for (int i = 0; i < len; i++) + visualHash.insert(visuals[i].visual, visuals[i].format); + } + } + + // Init the format ID -> xcb_render_directformat_t* hash table + const xcb_render_pictforminfo_t *formats = xcb_render_query_pict_formats_formats(reply); + const int len = xcb_render_query_pict_formats_formats_length(reply); + + for (int i = 0; i < len; i++) { + if (formats[i].type == XCB_RENDER_PICT_TYPE_DIRECT) + formatInfoHash.insert(formats[i].id, &formats[i].direct); + } + } + + ~PictFormatData() { + free(reply); + } + + xcb_render_query_pict_formats_reply_t *reply; + QHash visualHash; + QHash formatInfoHash; +}; + +Q_GLOBAL_STATIC(PictFormatData, g_pictFormatData) + +xcb_render_pictformat_t findPictFormat(xcb_visualid_t visual) +{ + PictFormatData *d = g_pictFormatData; + return d->visualHash.value(visual); +} + +const xcb_render_directformat_t *findPictFormatInfo(xcb_render_pictformat_t format) +{ + PictFormatData *d = g_pictFormatData; + return d->formatInfoHash.value(format); +} + +} // namespace XRenderUtils + +} // namespace KWin diff --git a/libkwineffects/kwinxrenderutils.h b/libkwineffects/kwinxrenderutils.h new file mode 100644 index 0000000..61d377d --- /dev/null +++ b/libkwineffects/kwinxrenderutils.h @@ -0,0 +1,183 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_XRENDERUTILS_H +#define KWIN_XRENDERUTILS_H + +// KWin +#include +// Qt +#include +#include +#include +// XCB +#include + +class QColor; +class QPixmap; + +/** @addtogroup kwineffects */ +/** @{ */ + +namespace KWin +{ +/** + * dumps a QColor into a xcb_render_color_t + */ +KWINXRENDERUTILS_EXPORT xcb_render_color_t preMultiply(const QColor &c, float opacity = 1.0); + +/** @internal */ +class KWINXRENDERUTILS_EXPORT XRenderPictureData + : public QSharedData +{ +public: + explicit XRenderPictureData(xcb_render_picture_t pic = XCB_RENDER_PICTURE_NONE); + ~XRenderPictureData(); + xcb_render_picture_t value(); +private: + xcb_render_picture_t picture; + Q_DISABLE_COPY(XRenderPictureData) +}; + +/** + * @short Wrapper around XRender Picture. + * + * This class wraps XRender's Picture, providing proper initialization, + * convenience constructors and freeing of resources. + * It should otherwise act exactly like the Picture type. + */ +class KWINXRENDERUTILS_EXPORT XRenderPicture +{ +public: + explicit XRenderPicture(xcb_render_picture_t pic = XCB_RENDER_PICTURE_NONE); + explicit XRenderPicture(const QImage &img); + XRenderPicture(xcb_pixmap_t pix, int depth); + operator xcb_render_picture_t(); +private: + void fromImage(const QImage &img); + QExplicitlySharedDataPointer< XRenderPictureData > d; +}; + +class KWINXRENDERUTILS_EXPORT XFixesRegion +{ +public: + explicit XFixesRegion(const QRegion ®ion); + virtual ~XFixesRegion(); + + operator xcb_xfixes_region_t(); +private: + xcb_xfixes_region_t m_region; +}; + +inline +XRenderPictureData::XRenderPictureData(xcb_render_picture_t pic) + : picture(pic) +{ +} + +inline +xcb_render_picture_t XRenderPictureData::value() +{ + return picture; +} + +inline +XRenderPicture::XRenderPicture(xcb_render_picture_t pic) + : d(new XRenderPictureData(pic)) +{ +} + +inline +XRenderPicture::operator xcb_render_picture_t() +{ + return d->value(); +} + +inline +XFixesRegion::operator xcb_xfixes_region_t() +{ + return m_region; +} + +/** + * Static 1x1 picture used to deliver a black pixel with given opacity (for blending performance) + * Call and Use, the PixelPicture will stay, but may change it's opacity meanwhile. It's NOT threadsafe either + */ +KWINXRENDERUTILS_EXPORT XRenderPicture xRenderBlendPicture(double opacity); +/** + * Creates a 1x1 Picture filled with c + */ +KWINXRENDERUTILS_EXPORT XRenderPicture xRenderFill(const xcb_render_color_t &c); +KWINXRENDERUTILS_EXPORT XRenderPicture xRenderFill(const QColor &c); + +/** + * Allows to render a window into a (transparent) pixmap + * NOTICE: the result can be queried as xRenderWindowOffscreenTarget() + * NOTICE: it may be 0 + * NOTICE: when done call setXRenderWindowOffscreen(false) to continue normal render process + */ +KWINXRENDERUTILS_EXPORT void setXRenderOffscreen(bool b); + +/** + * Allows to define a persistent effect member as render target + * The window (including shadows) is rendered into the top left corner + * NOTICE: do NOT call setXRenderOffscreen(true) in addition! + * NOTICE: do not forget to xRenderPopTarget once you're done to continue the normal render process + */ +KWINXRENDERUTILS_EXPORT void xRenderPushTarget(XRenderPicture *pic); +KWINXRENDERUTILS_EXPORT void xRenderPopTarget(); + +/** + * Whether windows are currently rendered into an offscreen target buffer + */ +KWINXRENDERUTILS_EXPORT bool xRenderOffscreen(); +/** + * The offscreen buffer as set by the renderer because of setXRenderWindowOffscreen(true) + */ +KWINXRENDERUTILS_EXPORT xcb_render_picture_t xRenderOffscreenTarget(); + +/** + * NOTICE: HANDS OFF!!! + * scene_setXRenderWindowOffscreenTarget() is ONLY to be used by the renderer - DO NOT TOUCH! + */ +KWINXRENDERUTILS_EXPORT void scene_setXRenderOffscreenTarget(xcb_render_picture_t pix); +/** + * scene_xRenderWindowOffscreenTarget() is used by the scene to figure the target set by an effect + */ +KWINXRENDERUTILS_EXPORT XRenderPicture *scene_xRenderOffscreenTarget(); + +namespace XRenderUtils +{ +/** + * @internal + */ +KWINXRENDERUTILS_EXPORT void init(xcb_connection_t *connection, xcb_window_t rootWindow); + +/** + * Returns the Xrender format that corresponds to the given visual ID. + */ +KWINXRENDERUTILS_EXPORT xcb_render_pictformat_t findPictFormat(xcb_visualid_t visual); + +/** + * Returns the xcb_render_directformat_t for the given Xrender format. + */ +KWINXRENDERUTILS_EXPORT const xcb_render_directformat_t *findPictFormatInfo(xcb_render_pictformat_t format); + +/** + * @internal + */ +KWINXRENDERUTILS_EXPORT void cleanup(); + +} // namespace XRenderUtils + +} // namespace KWin + +/** @} */ + +#endif diff --git a/libkwineffects/logging.cpp b/libkwineffects/logging.cpp new file mode 100644 index 0000000..8fb308a --- /dev/null +++ b/libkwineffects/logging.cpp @@ -0,0 +1,12 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "logging_p.h" +Q_LOGGING_CATEGORY(LIBKWINEFFECTS, "libkwineffects", QtCriticalMsg) +Q_LOGGING_CATEGORY(LIBKWINGLUTILS, "libkwinglutils", QtCriticalMsg) +Q_LOGGING_CATEGORY(LIBKWINXRENDERUTILS, "libkwinxrenderutils", QtCriticalMsg) diff --git a/libkwineffects/logging_p.h b/libkwineffects/logging_p.h new file mode 100644 index 0000000..4f82a8b --- /dev/null +++ b/libkwineffects/logging_p.h @@ -0,0 +1,19 @@ +/* + 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_KWINEFFECTS_LOGGING_P_H +#define KWIN_KWINEFFECTS_LOGGING_P_H + +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(LIBKWINEFFECTS) +Q_DECLARE_LOGGING_CATEGORY(LIBKWINGLUTILS) +Q_DECLARE_LOGGING_CATEGORY(LIBKWINXRENDERUTILS) + +#endif diff --git a/linux_dmabuf.cpp b/linux_dmabuf.cpp new file mode 100644 index 0000000..0e30322 --- /dev/null +++ b/linux_dmabuf.cpp @@ -0,0 +1,77 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "linux_dmabuf.h" + +#include "wayland_server.h" + +#include + +namespace KWin +{ + +DmabufBuffer::DmabufBuffer(const QVector &planes, + uint32_t format, + const QSize &size, + Flags flags) + : KWaylandServer::LinuxDmabufUnstableV1Buffer(format, size) + , m_planes(planes) + , m_format(format) + , m_size(size) + , m_flags(flags) +{ + waylandServer()->addLinuxDmabufBuffer(this); +} + +DmabufBuffer::~DmabufBuffer() +{ + // Close all open file descriptors + for (int i = 0; i < m_planes.count(); i++) { + if (m_planes[i].fd != -1) + ::close(m_planes[i].fd); + m_planes[i].fd = -1; + } + if (waylandServer()) { + waylandServer()->removeLinuxDmabufBuffer(this); + } +} + +LinuxDmabuf::LinuxDmabuf() + : KWaylandServer::LinuxDmabufUnstableV1Interface::Impl() +{ + Q_ASSERT(waylandServer()); + waylandServer()->linuxDmabuf()->setImpl(this); +} + +LinuxDmabuf::~LinuxDmabuf() +{ + waylandServer()->linuxDmabuf()->setImpl(nullptr); +} + +using Plane = KWaylandServer::LinuxDmabufUnstableV1Interface::Plane; +using Flags = KWaylandServer::LinuxDmabufUnstableV1Interface::Flags; + +KWaylandServer::LinuxDmabufUnstableV1Buffer* LinuxDmabuf::importBuffer(const QVector &planes, + uint32_t format, + const QSize &size, + Flags flags) +{ + Q_UNUSED(planes) + Q_UNUSED(format) + Q_UNUSED(size) + Q_UNUSED(flags) + + return nullptr; +} + +void LinuxDmabuf::setSupportedFormatsAndModifiers(QHash > &set) +{ + waylandServer()->linuxDmabuf()->setSupportedFormatsWithModifiers(set); +} + +} diff --git a/linux_dmabuf.h b/linux_dmabuf.h new file mode 100644 index 0000000..8bb435e --- /dev/null +++ b/linux_dmabuf.h @@ -0,0 +1,63 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include + +#include + +#include + +namespace KWin +{ + +class KWIN_EXPORT DmabufBuffer : public KWaylandServer::LinuxDmabufUnstableV1Buffer +{ +public: + using Plane = KWaylandServer::LinuxDmabufUnstableV1Interface::Plane; + using Flags = KWaylandServer::LinuxDmabufUnstableV1Interface::Flags; + + DmabufBuffer(const QVector &planes, + uint32_t format, + const QSize &size, + Flags flags); + + ~DmabufBuffer() override; + + const QVector &planes() const { return m_planes; } + uint32_t format() const { return m_format; } + QSize size() const { return m_size; } + Flags flags() const { return m_flags; } + +private: + QVector m_planes; + uint32_t m_format; + QSize m_size; + Flags m_flags; +}; + +class KWIN_EXPORT LinuxDmabuf : public KWaylandServer::LinuxDmabufUnstableV1Interface::Impl +{ +public: + using Plane = KWaylandServer::LinuxDmabufUnstableV1Interface::Plane; + using Flags = KWaylandServer::LinuxDmabufUnstableV1Interface::Flags; + + explicit LinuxDmabuf(); + ~LinuxDmabuf() override; + + KWaylandServer::LinuxDmabufUnstableV1Buffer *importBuffer(const QVector &planes, + uint32_t format, + const QSize &size, + Flags flags) override; + +protected: + void setSupportedFormatsAndModifiers(QHash > &set); +}; + +} diff --git a/logind.cpp b/logind.cpp new file mode 100644 index 0000000..2887d81 --- /dev/null +++ b/logind.cpp @@ -0,0 +1,455 @@ +/* + 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 "logind.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#if HAVE_SYS_SYSMACROS_H +#include +#endif +#ifndef major +#include +#endif +#include +#include +#include "utils.h" + +struct DBusLogindSeat { + QString name; + QDBusObjectPath path; +}; + +QDBusArgument &operator<<(QDBusArgument &argument, const DBusLogindSeat &seat) +{ + argument.beginStructure(); + argument << seat.name << seat.path ; + argument.endStructure(); + return argument; +} + +const QDBusArgument &operator>>(const QDBusArgument &argument, DBusLogindSeat &seat) +{ + argument.beginStructure(); + argument >> seat.name >> seat.path; + argument.endStructure(); + return argument; +} + +Q_DECLARE_METATYPE(DBusLogindSeat) + +namespace KWin +{ + +const static QString s_login1Name = QStringLiteral("logind"); +const static QString s_login1Service = QStringLiteral("org.freedesktop.login1"); +const static QString s_login1Path = QStringLiteral("/org/freedesktop/login1"); +const static QString s_login1ManagerInterface = QStringLiteral("org.freedesktop.login1.Manager"); +const static QString s_login1SeatInterface = QStringLiteral("org.freedesktop.login1.Seat"); +const static QString s_login1SessionInterface = QStringLiteral("org.freedesktop.login1.Session"); +const static QString s_login1ActiveProperty = QStringLiteral("Active"); + +const static QString s_ck2Name = QStringLiteral("ConsoleKit"); +const static QString s_ck2Service = QStringLiteral("org.freedesktop.ConsoleKit"); +const static QString s_ck2Path = QStringLiteral("/org/freedesktop/ConsoleKit/Manager"); +const static QString s_ck2ManagerInterface = QStringLiteral("org.freedesktop.ConsoleKit.Manager"); +const static QString s_ck2SeatInterface = QStringLiteral("org.freedesktop.ConsoleKit.Seat"); +const static QString s_ck2SessionInterface = QStringLiteral("org.freedesktop.ConsoleKit.Session"); +const static QString s_ck2ActiveProperty = QStringLiteral("active"); + +const static QString s_dbusPropertiesInterface = QStringLiteral("org.freedesktop.DBus.Properties"); + + + +LogindIntegration *LogindIntegration::s_self = nullptr; + +LogindIntegration *LogindIntegration::create(QObject *parent) +{ + Q_ASSERT(!s_self); + s_self = new LogindIntegration(parent); + return s_self; +} + +LogindIntegration::LogindIntegration(const QDBusConnection &connection, QObject *parent) + : QObject(parent) + , m_bus(connection) + , m_connected(false) + , m_sessionControl(false) + , m_sessionActive(false) +{ + // check whether the logind service is registered + QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.DBus"), + QStringLiteral("/"), + QStringLiteral("org.freedesktop.DBus"), + QStringLiteral("ListNames")); + QDBusPendingReply async = m_bus.asyncCall(message); + QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(async, this); + connect(callWatcher, &QDBusPendingCallWatcher::finished, this, + [this](QDBusPendingCallWatcher *self) { + QDBusPendingReply reply = *self; + self->deleteLater(); + if (!reply.isValid()) { + return; + } + if (reply.value().contains(s_login1Service)) { + setupSessionController(SessionControllerLogind); + } else if (reply.value().contains(s_ck2Service)) { + setupSessionController(SessionControllerConsoleKit); + } + + } + ); +} + +LogindIntegration::LogindIntegration(QObject *parent) + : LogindIntegration(QDBusConnection::systemBus(), parent) +{ +} + +LogindIntegration::~LogindIntegration() +{ + s_self = nullptr; +} + +void LogindIntegration::setupSessionController(SessionController controller) +{ + if (controller == SessionControllerLogind) { + // We have the logind serivce, set it up and use it + m_sessionControllerName = s_login1Name; + m_sessionControllerService = s_login1Service; + m_sessionControllerPath = s_login1Path; + m_sessionControllerManagerInterface = s_login1ManagerInterface; + m_sessionControllerSeatInterface = s_login1SeatInterface; + m_sessionControllerSessionInterface = s_login1SessionInterface; + m_sessionControllerActiveProperty = s_login1ActiveProperty; + m_logindServiceWatcher = new QDBusServiceWatcher(m_sessionControllerService, + m_bus, + QDBusServiceWatcher::WatchForUnregistration | QDBusServiceWatcher::WatchForRegistration, + this); + connect(m_logindServiceWatcher, &QDBusServiceWatcher::serviceRegistered, this, &LogindIntegration::logindServiceRegistered); + connect(m_logindServiceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, + [this]() { + m_connected = false; + emit connectedChanged(); + } + ); + logindServiceRegistered(); + } else if (controller == SessionControllerConsoleKit) { + // We have the ConsoleKit serivce, set it up and use it + m_sessionControllerName = s_ck2Name; + m_sessionControllerService = s_ck2Service; + m_sessionControllerPath = s_ck2Path; + m_sessionControllerManagerInterface = s_ck2ManagerInterface; + m_sessionControllerSeatInterface = s_ck2SeatInterface; + m_sessionControllerSessionInterface = s_ck2SessionInterface; + m_sessionControllerActiveProperty = s_ck2ActiveProperty; + m_logindServiceWatcher = new QDBusServiceWatcher(m_sessionControllerService, + m_bus, + QDBusServiceWatcher::WatchForUnregistration | QDBusServiceWatcher::WatchForRegistration, + this); + connect(m_logindServiceWatcher, &QDBusServiceWatcher::serviceRegistered, this, &LogindIntegration::logindServiceRegistered); + connect(m_logindServiceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, + [this]() { + m_connected = false; + emit connectedChanged(); + } + ); + logindServiceRegistered(); + } +} + +void LogindIntegration::logindServiceRegistered() +{ + const QByteArray sessionId = qgetenv("XDG_SESSION_ID"); + QString methodName; + QVariantList args; + if (sessionId.isEmpty()) { + methodName = QStringLiteral("GetSessionByPID"); + args << (quint32) QCoreApplication::applicationPid(); + } else { + methodName = QStringLiteral("GetSession"); + args << QString::fromLocal8Bit(sessionId); + } + // get the current session + QDBusMessage message = QDBusMessage::createMethodCall(m_sessionControllerService, + m_sessionControllerPath, + m_sessionControllerManagerInterface, + methodName); + message.setArguments(args); + QDBusPendingReply session = m_bus.asyncCall(message); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(session, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, + [this](QDBusPendingCallWatcher *self) { + QDBusPendingReply reply = *self; + self->deleteLater(); + if (m_connected) { + return; + } + if (!reply.isValid()) { + qCDebug(KWIN_CORE) << "The session is not registered with " << m_sessionControllerName << " " << reply.error().message(); + return; + } + m_sessionPath = reply.value().path(); + qCDebug(KWIN_CORE) << "Session path:" << m_sessionPath; + m_connected = true; + connectSessionPropertiesChanged(); + // activate the session, in case we are not on it + QDBusMessage message = QDBusMessage::createMethodCall(m_sessionControllerService, + m_sessionPath, + m_sessionControllerSessionInterface, + QStringLiteral("Activate")); + // blocking on purpose + m_bus.call(message); + getSeat(); + getSessionActive(); + getVirtualTerminal(); + + emit connectedChanged(); + } + ); + + m_bus.connect(m_sessionControllerService, + m_sessionPath, + m_sessionControllerManagerInterface, + QStringLiteral("PrepareForSleep"), + this, + SIGNAL(prepareForSleep(bool))); +} + +void LogindIntegration::connectSessionPropertiesChanged() +{ + m_bus.connect(m_sessionControllerService, + m_sessionPath, + s_dbusPropertiesInterface, + QStringLiteral("PropertiesChanged"), + this, + SLOT(getSessionActive())); + m_bus.connect(m_sessionControllerService, + m_sessionPath, + s_dbusPropertiesInterface, + QStringLiteral("PropertiesChanged"), + this, + SLOT(getVirtualTerminal())); +} + +void LogindIntegration::getSessionActive() +{ + if (!m_connected || m_sessionPath.isEmpty()) { + return; + } + QDBusMessage message = QDBusMessage::createMethodCall(m_sessionControllerService, + m_sessionPath, + s_dbusPropertiesInterface, + QStringLiteral("Get")); + message.setArguments(QVariantList({m_sessionControllerSessionInterface, m_sessionControllerActiveProperty})); + QDBusPendingReply reply = m_bus.asyncCall(message); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, + [this](QDBusPendingCallWatcher *self) { + QDBusPendingReply reply = *self; + self->deleteLater(); + if (!reply.isValid()) { + qCDebug(KWIN_CORE) << "Failed to get Active Property of " << m_sessionControllerName << " session:" << reply.error().message(); + return; + } + const bool active = reply.value().toBool(); + if (m_sessionActive != active) { + m_sessionActive = active; + emit sessionActiveChanged(m_sessionActive); + } + } + ); +} + +void LogindIntegration::getVirtualTerminal() +{ + if (!m_connected || m_sessionPath.isEmpty()) { + return; + } + QDBusMessage message = QDBusMessage::createMethodCall(m_sessionControllerService, + m_sessionPath, + s_dbusPropertiesInterface, + QStringLiteral("Get")); + message.setArguments(QVariantList({m_sessionControllerSessionInterface, QStringLiteral("VTNr")})); + QDBusPendingReply reply = m_bus.asyncCall(message); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, + [this](QDBusPendingCallWatcher *self) { + QDBusPendingReply reply = *self; + self->deleteLater(); + if (!reply.isValid()) { + qCDebug(KWIN_CORE) << "Failed to get VTNr Property of " << m_sessionControllerName << " session:" << reply.error().message(); + return; + } + const int vt = reply.value().toUInt(); + if (m_vt != (int)vt) { + m_vt = vt; + emit virtualTerminalChanged(m_vt); + } + } + ); +} + +void LogindIntegration::takeControl() +{ + if (!m_connected || m_sessionPath.isEmpty() || m_sessionControl) { + return; + } + static bool s_recursionCheck = false; + if (s_recursionCheck) { + return; + } + s_recursionCheck = true; + + QDBusMessage message = QDBusMessage::createMethodCall(m_sessionControllerService, + m_sessionPath, + m_sessionControllerSessionInterface, + QStringLiteral("TakeControl")); + message.setArguments(QVariantList({QVariant(false)})); + QDBusPendingReply session = m_bus.asyncCall(message); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(session, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, + [this](QDBusPendingCallWatcher *self) { + s_recursionCheck = false; + QDBusPendingReply reply = *self; + self->deleteLater(); + if (!reply.isValid()) { + qCDebug(KWIN_CORE) << "Failed to get session control" << reply.error().message(); + emit hasSessionControlChanged(false); + return; + } + qCDebug(KWIN_CORE) << "Gained session control"; + m_sessionControl = true; + emit hasSessionControlChanged(true); + m_bus.connect(m_sessionControllerService, m_sessionPath, + m_sessionControllerSessionInterface, QStringLiteral("PauseDevice"), + this, SLOT(pauseDevice(uint,uint,QString))); + } + ); +} + +void LogindIntegration::releaseControl() +{ + if (!m_connected || m_sessionPath.isEmpty() || !m_sessionControl) { + return; + } + + QDBusMessage message = QDBusMessage::createMethodCall(m_sessionControllerService, + m_sessionPath, + m_sessionControllerSessionInterface, + QStringLiteral("ReleaseControl")); + m_bus.asyncCall(message); + m_sessionControl = false; + emit hasSessionControlChanged(false); +} + +int LogindIntegration::takeDevice(const char *path) +{ + struct stat st; + if (stat(path, &st) < 0) { + qCDebug(KWIN_CORE) << "Could not stat the path"; + return -1; + } + QDBusMessage message = QDBusMessage::createMethodCall(m_sessionControllerService, + m_sessionPath, + m_sessionControllerSessionInterface, + QStringLiteral("TakeDevice")); + message.setArguments(QVariantList({QVariant(major(st.st_rdev)), QVariant(minor(st.st_rdev))})); + // intended to be a blocking call + QDBusMessage reply = m_bus.call(message); + if (reply.type() == QDBusMessage::ErrorMessage) { + qCDebug(KWIN_CORE) << "Could not take device" << path << ", cause: " << reply.errorMessage(); + return -1; + } + + // The dup syscall removes the CLOEXEC flag as a side-effect. So use fcntl's F_DUPFD_CLOEXEC cmd. + return fcntl(reply.arguments().first().value().fileDescriptor(), F_DUPFD_CLOEXEC, 0); +} + +void LogindIntegration::releaseDevice(int fd) +{ + struct stat st; + if (fstat(fd, &st) < 0) { + qCDebug(KWIN_CORE) << "Could not stat the file descriptor"; + return; + } + QDBusMessage message = QDBusMessage::createMethodCall(m_sessionControllerService, + m_sessionPath, + m_sessionControllerSessionInterface, + QStringLiteral("ReleaseDevice")); + message.setArguments(QVariantList({QVariant(major(st.st_rdev)), QVariant(minor(st.st_rdev))})); + m_bus.asyncCall(message); +} + +void LogindIntegration::pauseDevice(uint devMajor, uint devMinor, const QString &type) +{ + if (QString::compare(type, QStringLiteral("pause"), Qt::CaseInsensitive) == 0) { + // unconditionally call complete + QDBusMessage message = QDBusMessage::createMethodCall(m_sessionControllerService, m_sessionPath, m_sessionControllerSessionInterface, QStringLiteral("PauseDeviceComplete")); + message.setArguments(QVariantList({QVariant(devMajor), QVariant(devMinor)})); + m_bus.asyncCall(message); + } +} + +void LogindIntegration::getSeat() +{ + if (m_sessionPath.isEmpty()) { + return; + } + qDBusRegisterMetaType(); + QDBusMessage message = QDBusMessage::createMethodCall(m_sessionControllerService, + m_sessionPath, + s_dbusPropertiesInterface, + QStringLiteral("Get")); + message.setArguments(QVariantList({m_sessionControllerSessionInterface, QStringLiteral("Seat")})); + QDBusPendingReply reply = m_bus.asyncCall(message); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, + [this](QDBusPendingCallWatcher *self) { + QDBusPendingReply reply = *self; + self->deleteLater(); + if (!reply.isValid()) { + qCDebug(KWIN_CORE) << "Failed to get Seat Property of " << m_sessionControllerName << " session:" << reply.error().message(); + return; + } + DBusLogindSeat seat = qdbus_cast(reply.value().value()); + const QString seatPath = seat.path.path(); + qCDebug(KWIN_CORE) << m_sessionControllerName << " seat:" << seat.name << "/" << seatPath; + if (m_seatPath != seatPath) { + m_seatPath = seatPath; + } + if (m_seatName != seat.name) { + m_seatName = seat.name; + } + } + ); +} + +void LogindIntegration::switchVirtualTerminal(quint32 vtNr) +{ + if (!m_connected || m_seatPath.isEmpty()) { + return; + } + QDBusMessage message = QDBusMessage::createMethodCall(m_sessionControllerService, + m_seatPath, + m_sessionControllerSeatInterface, + QStringLiteral("SwitchTo")); + message.setArguments(QVariantList{vtNr}); + m_bus.asyncCall(message); +} + +} // namespace diff --git a/logind.h b/logind.h new file mode 100644 index 0000000..9cda18e --- /dev/null +++ b/logind.h @@ -0,0 +1,102 @@ +/* + 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 +*/ +#ifndef KWIN_LOGIND_H +#define KWIN_LOGIND_H + +#include + +#include +#include + +class QDBusServiceWatcher; + +namespace KWin +{ + +class KWIN_EXPORT LogindIntegration : public QObject +{ + Q_OBJECT +public: + ~LogindIntegration() override; + + bool isConnected() const { + return m_connected; + } + bool hasSessionControl() const { + return m_sessionControl; + } + bool isActiveSession() const { + return m_sessionActive; + } + int vt() const { + return m_vt; + } + void switchVirtualTerminal(quint32 vtNr); + + void takeControl(); + void releaseControl(); + + int takeDevice(const char *path); + void releaseDevice(int fd); + + const QString seat() const { + return m_seatName; + } + +Q_SIGNALS: + void connectedChanged(); + void hasSessionControlChanged(bool); + void sessionActiveChanged(bool); + void virtualTerminalChanged(int); + void prepareForSleep(bool prepare); + +private Q_SLOTS: + void getSessionActive(); + void getVirtualTerminal(); + void pauseDevice(uint major, uint minor, const QString &type); + +private: + friend class LogindTest; + /** + * The DBusConnection argument is needed for the unit test. Logind uses the system bus + * on which the unit test's fake logind cannot register to. Thus the unit test need to + * be able to do everything over the session bus. This ctor allows the LogindTest to + * create a LogindIntegration which listens on the session bus. + */ + explicit LogindIntegration(const QDBusConnection &connection, QObject *parent = nullptr); + void logindServiceRegistered(); + void connectSessionPropertiesChanged(); + enum SessionController { + SessionControllerLogind, + SessionControllerConsoleKit, + }; + void setupSessionController(SessionController controller); + void getSeat(); + QDBusConnection m_bus; + QDBusServiceWatcher *m_logindServiceWatcher; + bool m_connected; + QString m_sessionPath; + bool m_sessionControl; + bool m_sessionActive; + int m_vt = -1; + QString m_seatName = QStringLiteral("seat0"); + QString m_seatPath; + QString m_sessionControllerName; + QString m_sessionControllerService; + QString m_sessionControllerPath; + QString m_sessionControllerManagerInterface; + QString m_sessionControllerSeatInterface; + QString m_sessionControllerSessionInterface; + QString m_sessionControllerActiveProperty; + KWIN_SINGLETON(LogindIntegration) +}; + +} + +#endif diff --git a/logo.png b/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..95b3fa8cd8bfde4fadab1d8ecce48fa1db211249 GIT binary patch literal 4200 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 jZG?4q`@bm6JB + SPDX-FileCopyrightText: 2003 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "main.h" +#include +// kwin +#include "platform.h" +#include "atoms.h" +#include "composite.h" +#include "cursor.h" +#include "input.h" +#include "logind.h" +#include "options.h" +#include "screens.h" +#include "screenlockerwatcher.h" +#include "sm.h" +#include "workspace.h" +#include "xcbutils.h" + +#include + +// KDE +#include +#include +#include +#include +#include +// Qt +#include +#include +#include +#include +#include +#include + +// system +#ifdef HAVE_UNISTD_H +#include +#endif // HAVE_UNISTD_H + +#ifdef HAVE_MALLOC_H +#include +#endif // HAVE_MALLOC_H + +// xcb +#include +#ifndef XCB_GE_GENERIC +#define XCB_GE_GENERIC 35 +#endif + +Q_DECLARE_METATYPE(KSharedConfigPtr) + +namespace KWin +{ + +Options* options; + +Atoms* atoms; + +int screen_number = -1; +bool is_multihead = false; + +int Application::crashes = 0; + +bool Application::isX11MultiHead() +{ + return is_multihead; +} + +void Application::setX11MultiHead(bool multiHead) +{ + is_multihead = multiHead; +} + +void Application::setX11ScreenNumber(int screenNumber) +{ + screen_number = screenNumber; +} + +int Application::x11ScreenNumber() +{ + return screen_number; +} + +Application::Application(Application::OperationMode mode, int &argc, char **argv) + : QApplication(argc, argv) + , m_eventFilter(new XcbEventFilter()) + , m_configLock(false) + , m_config() + , m_kxkbConfig() + , m_operationMode(mode) +{ + qRegisterMetaType("Options::WindowOperation"); + qRegisterMetaType(); + qRegisterMetaType("KWaylandServer::SurfaceInterface *"); + qRegisterMetaType(); +} + +void Application::setConfigLock(bool lock) +{ + m_configLock = lock; +} + +Application::OperationMode Application::operationMode() const +{ + return m_operationMode; +} + +void Application::setOperationMode(OperationMode mode) +{ + m_operationMode = mode; +} + +bool Application::shouldUseWaylandForCompositing() const +{ + return m_operationMode == OperationModeWaylandOnly || m_operationMode == OperationModeXwayland; +} + +void Application::start() +{ + setQuitOnLastWindowClosed(false); + + if (!m_config) { + m_config = KSharedConfig::openConfig(); + } + if (!m_config->isImmutable() && m_configLock) { + // TODO: This shouldn't be necessary + //config->setReadOnly( true ); + m_config->reparseConfiguration(); + } + if (!m_kxkbConfig) { + m_kxkbConfig = KSharedConfig::openConfig(QStringLiteral("kxkbrc"), KConfig::NoGlobals); + } + + performStartup(); +} + +Application::~Application() +{ + delete options; + destroyAtoms(); + destroyPlatform(); +} + +void Application::notifyStarted() +{ + emit started(); +} + +void Application::destroyAtoms() +{ + delete atoms; + atoms = nullptr; +} + +void Application::destroyPlatform() +{ + delete m_platform; + m_platform = nullptr; +} + +void Application::resetCrashesCount() +{ + crashes = 0; +} + +void Application::setCrashCount(int count) +{ + crashes = count; +} + +bool Application::wasCrash() +{ + return crashes > 0; +} + +static const char description[] = I18N_NOOP("KDE window manager"); + +void Application::createAboutData() +{ + KAboutData aboutData(QStringLiteral(KWIN_NAME), // The program name used internally + i18n("KWin"), // A displayable program name string + QStringLiteral(KWIN_VERSION_STRING), // The program version string + i18n(description), // Short description of what the app does + KAboutLicense::GPL, // The license this code is released under + i18n("(c) 1999-2019, The KDE Developers")); // Copyright Statement + + aboutData.addAuthor(i18n("Matthias Ettrich"), QString(), QStringLiteral("ettrich@kde.org")); + aboutData.addAuthor(i18n("Cristian Tibirna"), QString(), QStringLiteral("tibirna@kde.org")); + aboutData.addAuthor(i18n("Daniel M. Duley"), QString(), QStringLiteral("mosfet@kde.org")); + aboutData.addAuthor(i18n("LuboÅ¡ Luňák"), QString(), QStringLiteral("l.lunak@kde.org")); + aboutData.addAuthor(i18n("Martin Flöser"), QString(), QStringLiteral("mgraesslin@kde.org")); + aboutData.addAuthor(i18n("David Edmundson"), QStringLiteral("Maintainer"), QStringLiteral("davidedmundson@kde.org")); + aboutData.addAuthor(i18n("Roman Gilg"), QStringLiteral("Maintainer"), QStringLiteral("subdiff@gmail.com")); + aboutData.addAuthor(i18n("Vlad Zahorodnii"), QStringLiteral("Maintainer"), QStringLiteral("vlad.zahorodnii@kde.org")); + KAboutData::setApplicationData(aboutData); +} + +static const QString s_lockOption = QStringLiteral("lock"); +static const QString s_crashesOption = QStringLiteral("crashes"); + +void Application::setupCommandLine(QCommandLineParser *parser) +{ + QCommandLineOption lockOption(s_lockOption, i18n("Disable configuration options")); + QCommandLineOption crashesOption(s_crashesOption, i18n("Indicate that KWin has recently crashed n times"), QStringLiteral("n")); + + parser->setApplicationDescription(i18n("KDE window manager")); + parser->addOption(lockOption); + parser->addOption(crashesOption); + KAboutData::applicationData().setupCommandLine(parser); +} + +void Application::processCommandLine(QCommandLineParser *parser) +{ + KAboutData aboutData = KAboutData::applicationData(); + aboutData.processCommandLine(parser); + setConfigLock(parser->isSet(s_lockOption)); + Application::setCrashCount(parser->value(s_crashesOption).toInt()); +} + +void Application::setupTranslator() +{ + QTranslator *qtTranslator = new QTranslator(qApp); + qtTranslator->load("qt_" + QLocale::system().name(), + QLibraryInfo::location(QLibraryInfo::TranslationsPath)); + installTranslator(qtTranslator); +} + +void Application::setupMalloc() +{ +#ifdef M_TRIM_THRESHOLD + // Prevent fragmentation of the heap by malloc (glibc). + // + // The default threshold is 128*1024, which can result in a large memory usage + // due to fragmentation especially if we use the raster graphicssystem. On the + // otherside if the threshold is too low, free() starts to permanently ask the kernel + // about shrinking the heap. +#ifdef HAVE_UNISTD_H + const int pagesize = sysconf(_SC_PAGESIZE); +#else + const int pagesize = 4*1024; +#endif // HAVE_UNISTD_H + mallopt(M_TRIM_THRESHOLD, 5*pagesize); +#endif // M_TRIM_THRESHOLD +} + +void Application::setupLocalizedString() +{ + KLocalizedString::setApplicationDomain("kwin"); +} + +void Application::createWorkspace() +{ + // we want all QQuickWindows with an alpha buffer, do here as Workspace might create QQuickWindows + QQuickWindow::setDefaultAlphaBuffer(true); + + // This tries to detect compositing options and can use GLX. GLX problems + // (X errors) shouldn't cause kwin to abort, so this is out of the + // critical startup section where x errors cause kwin to abort. + + // create workspace. + (void) new Workspace(); + emit workspaceCreated(); +} + +void Application::createInput() +{ + ScreenLockerWatcher::create(this); + LogindIntegration::create(this); + auto input = InputRedirection::create(this); + input->init(); + m_platform->createPlatformCursor(this); +} + +void Application::createScreens() +{ + if (Screens::self()) { + return; + } + Screens::create(this); + emit screensCreated(); +} + +void Application::createAtoms() +{ + atoms = new Atoms; +} + +void Application::createOptions() +{ + options = new Options; +} + +void Application::installNativeX11EventFilter() +{ + installNativeEventFilter(m_eventFilter.data()); +} + +void Application::removeNativeX11EventFilter() +{ + removeNativeEventFilter(m_eventFilter.data()); +} + +void Application::destroyWorkspace() +{ + delete Workspace::self(); +} + +void Application::destroyCompositor() +{ + delete Compositor::self(); +} + +void Application::updateX11Time(xcb_generic_event_t *event) +{ + xcb_timestamp_t time = XCB_TIME_CURRENT_TIME; + const uint8_t eventType = event->response_type & ~0x80; + switch(eventType) { + case XCB_KEY_PRESS: + case XCB_KEY_RELEASE: + time = reinterpret_cast(event)->time; + break; + case XCB_BUTTON_PRESS: + case XCB_BUTTON_RELEASE: + time = reinterpret_cast(event)->time; + break; + case XCB_MOTION_NOTIFY: + time = reinterpret_cast(event)->time; + break; + case XCB_ENTER_NOTIFY: + case XCB_LEAVE_NOTIFY: + time = reinterpret_cast(event)->time; + break; + case XCB_FOCUS_IN: + case XCB_FOCUS_OUT: + case XCB_KEYMAP_NOTIFY: + case XCB_EXPOSE: + case XCB_GRAPHICS_EXPOSURE: + case XCB_NO_EXPOSURE: + case XCB_VISIBILITY_NOTIFY: + case XCB_CREATE_NOTIFY: + case XCB_DESTROY_NOTIFY: + case XCB_UNMAP_NOTIFY: + case XCB_MAP_NOTIFY: + case XCB_MAP_REQUEST: + case XCB_REPARENT_NOTIFY: + case XCB_CONFIGURE_NOTIFY: + case XCB_CONFIGURE_REQUEST: + case XCB_GRAVITY_NOTIFY: + case XCB_RESIZE_REQUEST: + case XCB_CIRCULATE_NOTIFY: + case XCB_CIRCULATE_REQUEST: + // no timestamp + return; + case XCB_PROPERTY_NOTIFY: + time = reinterpret_cast(event)->time; + break; + case XCB_SELECTION_CLEAR: + time = reinterpret_cast(event)->time; + break; + case XCB_SELECTION_REQUEST: + time = reinterpret_cast(event)->time; + break; + case XCB_SELECTION_NOTIFY: + time = reinterpret_cast(event)->time; + break; + case XCB_COLORMAP_NOTIFY: + case XCB_CLIENT_MESSAGE: + case XCB_MAPPING_NOTIFY: + case XCB_GE_GENERIC: + // no timestamp + return; + default: + // extension handling + if (Xcb::Extensions::self()) { + if (eventType == Xcb::Extensions::self()->shapeNotifyEvent()) { + time = reinterpret_cast(event)->server_time; + } + if (eventType == Xcb::Extensions::self()->damageNotifyEvent()) { + time = reinterpret_cast(event)->timestamp; + } + } + break; + } + setX11Time(time); +} + +bool XcbEventFilter::nativeEventFilter(const QByteArray &eventType, void *message, long int *result) +{ + Q_UNUSED(result) + if (eventType != "xcb_generic_event_t") { + return false; + } + auto event = static_cast(message); + kwinApp()->updateX11Time(event); + if (!Workspace::self()) { + // Workspace not yet created + return false; + } + return Workspace::self()->workspaceEvent(event); +} + +static bool s_useLibinput = false; + +void Application::setUseLibinput(bool use) +{ + s_useLibinput = use; +} + +bool Application::usesLibinput() +{ + return s_useLibinput; +} + +QProcessEnvironment Application::processStartupEnvironment() const +{ + return QProcessEnvironment::systemEnvironment(); +} + +void Application::initPlatform(const KPluginMetaData &plugin) +{ + Q_ASSERT(!m_platform); + m_platform = qobject_cast(plugin.instantiate()); + if (m_platform) { + m_platform->setParent(this); + // check whether it needs libinput + const QJsonObject &metaData = plugin.rawData(); + auto it = metaData.find(QStringLiteral("input")); + if (it != metaData.end()) { + if ((*it).isBool()) { + if (!(*it).toBool()) { + qCDebug(KWIN_CORE) << "Platform does not support input, enforcing libinput support"; + setUseLibinput(true); + } + } + } + } +} + +ApplicationWaylandAbstract::ApplicationWaylandAbstract(OperationMode mode, int &argc, char **argv) + : Application(mode, argc, argv) +{ +} + +ApplicationWaylandAbstract::~ApplicationWaylandAbstract() +{ +} + +} // namespace + diff --git a/main.h b/main.h new file mode 100644 index 0000000..04e84ca --- /dev/null +++ b/main.h @@ -0,0 +1,299 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef MAIN_H +#define MAIN_H + +#include +#include + +#include +// Qt +#include +#include +#include + +class KPluginMetaData; +class QCommandLineParser; + +namespace KWin +{ + +class Platform; + +class XcbEventFilter : public QAbstractNativeEventFilter +{ +public: + bool nativeEventFilter(const QByteArray &eventType, void *message, long int *result) override; +}; + +class KWIN_EXPORT Application : public QApplication +{ + Q_OBJECT + Q_PROPERTY(quint32 x11Time READ x11Time WRITE setX11Time) + Q_PROPERTY(quint32 x11RootWindow READ x11RootWindow CONSTANT) + Q_PROPERTY(void *x11Connection READ x11Connection NOTIFY x11ConnectionChanged) + Q_PROPERTY(int x11ScreenNumber READ x11ScreenNumber CONSTANT) + Q_PROPERTY(KSharedConfigPtr config READ config WRITE setConfig) + Q_PROPERTY(KSharedConfigPtr kxkbConfig READ kxkbConfig WRITE setKxkbConfig) +public: + /** + * @brief This enum provides the various operation modes of KWin depending on the available + * Windowing Systems at startup. For example whether KWin only talks to X11 or also to a Wayland + * Compositor. + * + */ + enum OperationMode { + /** + * @brief KWin uses only X11 for managing windows and compositing + */ + OperationModeX11, + /** + * @brief KWin uses only Wayland + */ + OperationModeWaylandOnly, + /** + * @brief KWin uses Wayland and controls a nested Xwayland server. + */ + OperationModeXwayland + }; + ~Application() override; + + void setConfigLock(bool lock); + + KSharedConfigPtr config() const { + return m_config; + } + void setConfig(KSharedConfigPtr config) { + m_config = std::move(config); + } + + KSharedConfigPtr kxkbConfig() const { + return m_kxkbConfig; + } + void setKxkbConfig(KSharedConfigPtr config) { + m_kxkbConfig = std::move(config); + } + + void start(); + /** + * @brief The operation mode used by KWin. + * + * @return OperationMode + */ + OperationMode operationMode() const; + void setOperationMode(OperationMode mode); + bool shouldUseWaylandForCompositing() const; + + void setupTranslator(); + void setupCommandLine(QCommandLineParser *parser); + void processCommandLine(QCommandLineParser *parser); + + xcb_timestamp_t x11Time() const { + return m_x11Time; + } + enum class TimestampUpdate { + OnlyIfLarger, + Always + }; + void setX11Time(xcb_timestamp_t timestamp, TimestampUpdate force = TimestampUpdate::OnlyIfLarger) { + if ((timestamp > m_x11Time || force == TimestampUpdate::Always) && timestamp != 0) { + m_x11Time = timestamp; + } + } + void updateX11Time(xcb_generic_event_t *event); + void createScreens(); + + static void setCrashCount(int count); + static bool wasCrash(); + + /** + * Creates the KAboutData object for the KWin instance and registers it as + * KAboutData::setApplicationData. + */ + static void createAboutData(); + + /** + * @returns the X11 Screen number. If not applicable it's set to @c -1. + */ + static int x11ScreenNumber(); + /** + * Sets the X11 screen number of this KWin instance to @p screenNumber. + */ + static void setX11ScreenNumber(int screenNumber); + /** + * @returns whether this is a multi head setup on X11. + */ + static bool isX11MultiHead(); + /** + * Sets whether this is a multi head setup on X11. + */ + static void setX11MultiHead(bool multiHead); + + /** + * @returns the X11 root window. + */ + xcb_window_t x11RootWindow() const { + return m_rootWindow; + } + + /** + * @returns the X11 xcb connection + */ + xcb_connection_t *x11Connection() const { + return m_connection; + } + + /** + * @returns the X11 default screen + */ + xcb_screen_t *x11DefaultScreen() const { + return m_defaultScreen; + } + + /** + * Returns @c true if we're in the middle of destroying the X11 connection. + */ + bool isClosingX11Connection() const { + return m_isClosingX11Connection; + } + +#ifdef KWIN_BUILD_ACTIVITIES + bool usesKActivities() const { + return m_useKActivities; + } + void setUseKActivities(bool use) { + m_useKActivities = use; + } +#endif + + virtual QProcessEnvironment processStartupEnvironment() const; + + void initPlatform(const KPluginMetaData &plugin); + Platform *platform() const { + return m_platform; + } + + bool isTerminating() const { + return m_terminating; + } + + static void setupMalloc(); + static void setupLocalizedString(); + + static bool usesLibinput(); + static void setUseLibinput(bool use); + +Q_SIGNALS: + void x11ConnectionChanged(); + void x11ConnectionAboutToBeDestroyed(); + void workspaceCreated(); + void screensCreated(); + void virtualTerminalCreated(); + void started(); + +protected: + Application(OperationMode mode, int &argc, char **argv); + virtual void performStartup() = 0; + + void notifyKSplash(); + void notifyStarted(); + void createInput(); + void createWorkspace(); + void createAtoms(); + void createOptions(); + void installNativeX11EventFilter(); + void removeNativeX11EventFilter(); + void destroyWorkspace(); + void destroyCompositor(); + /** + * Inheriting classes should use this method to set the X11 root window + * before accessing any X11 specific code pathes. + */ + void setX11RootWindow(xcb_window_t root) { + m_rootWindow = root; + } + /** + * Inheriting classes should use this method to set the xcb connection + * before accessing any X11 specific code pathes. + */ + void setX11Connection(xcb_connection_t *c) { + m_connection = c; + } + /** + * Inheriting classes should use this method to set the default screen + * before accessing any X11 specific code pathes. + */ + void setX11DefaultScreen(xcb_screen_t *screen) { + m_defaultScreen = screen; + } + void destroyAtoms(); + void destroyPlatform(); + + void setTerminating() { + m_terminating = true; + } + void setClosingX11Connection(bool set) { + m_isClosingX11Connection = set; + } + +protected: + static int crashes; + +private Q_SLOTS: + void resetCrashesCount(); + +private: + QScopedPointer m_eventFilter; + bool m_configLock; + KSharedConfigPtr m_config; + KSharedConfigPtr m_kxkbConfig; + OperationMode m_operationMode; + xcb_timestamp_t m_x11Time = XCB_TIME_CURRENT_TIME; + xcb_window_t m_rootWindow = XCB_WINDOW_NONE; + xcb_connection_t *m_connection = nullptr; + xcb_screen_t *m_defaultScreen = nullptr; +#ifdef KWIN_BUILD_ACTIVITIES + bool m_useKActivities = true; +#endif + Platform *m_platform = nullptr; + bool m_terminating = false; + bool m_isClosingX11Connection = false; +}; + +inline static Application *kwinApp() +{ + return static_cast(QCoreApplication::instance()); +} + +namespace Xwl +{ +class Xwayland; +} + +class KWIN_EXPORT ApplicationWaylandAbstract : public Application +{ + Q_OBJECT +public: + ~ApplicationWaylandAbstract() override = 0; +protected: + friend class Xwl::Xwayland; + + ApplicationWaylandAbstract(OperationMode mode, int &argc, char **argv); + virtual void setProcessStartupEnvironment(const QProcessEnvironment &environment) { + Q_UNUSED(environment); + } + virtual void startSession() {} +}; + + +} // namespace + +#endif diff --git a/main_wayland.cpp b/main_wayland.cpp new file mode 100644 index 0000000..bd2128e --- /dev/null +++ b/main_wayland.cpp @@ -0,0 +1,703 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "main_wayland.h" +#include "composite.h" +#include "virtualkeyboard.h" +#include "workspace.h" +#include +// kwin +#include "platform.h" +#include "effects.h" +#include "tabletmodemanager.h" + +#ifdef PipeWire_FOUND +#include "screencast/screencastmanager.h" +#endif +#include "wayland_server.h" +#include "xwl/xwayland.h" + +// KWayland +#include +#include + +// KDE +#include +#include +#include +#include +#include +#include + +// Qt +#include +#include +#include +#include +#include +#include +#include + +// system +#if HAVE_SYS_PRCTL_H +#include +#endif +#if HAVE_SYS_PROCCTL_H +#include +#endif + +#if HAVE_LIBCAP +#include +#endif + +#include + +#include +#include + +namespace KWin +{ + +static void sighandler(int) +{ + QApplication::exit(); +} + +void disableDrKonqi() +{ + KCrash::setDrKonqiEnabled(false); +} +// run immediately, before Q_CORE_STARTUP functions +// that would enable drkonqi +Q_CONSTRUCTOR_FUNCTION(disableDrKonqi) + +enum class RealTimeFlags +{ + DontReset, + ResetOnFork +}; + +namespace { +void gainRealTime(RealTimeFlags flags = RealTimeFlags::DontReset) +{ +#if HAVE_SCHED_RESET_ON_FORK + const int minPriority = sched_get_priority_min(SCHED_RR); + struct sched_param sp; + sp.sched_priority = minPriority; + int policy = SCHED_RR; + if (flags == RealTimeFlags::ResetOnFork) { + policy |= SCHED_RESET_ON_FORK; + } + sched_setscheduler(0, policy, &sp); +#else + Q_UNUSED(flags); +#endif +} +} + +//************************************ +// ApplicationWayland +//************************************ + +ApplicationWayland::ApplicationWayland(int &argc, char **argv) + : ApplicationWaylandAbstract(OperationModeWaylandOnly, argc, argv) +{ +} + +ApplicationWayland::~ApplicationWayland() +{ + setTerminating(); + if (!waylandServer()) { + return; + } + + if (auto *platform = kwinApp()->platform()) { + platform->prepareShutdown(); + } + // need to unload all effects prior to destroying X connection as they might do X calls + if (effects) { + static_cast(effects)->unloadAllEffects(); + } + delete m_xwayland; + m_xwayland = nullptr; + destroyWorkspace(); + waylandServer()->dispatch(); + + if (QStyle *s = style()) { + s->unpolish(this); + } + waylandServer()->terminateClientConnections(); + destroyCompositor(); +} + +void ApplicationWayland::performStartup() +{ + if (m_startXWayland) { + setOperationMode(OperationModeXwayland); + } + // first load options - done internally by a different thread + createOptions(); + waylandServer()->createInternalConnection(); + + // try creating the Wayland Backend + createInput(); + // now libinput thread has been created, adjust scheduler to not leak into other processes + gainRealTime(RealTimeFlags::ResetOnFork); + + VirtualKeyboard::create(this); + createBackend(); + TabletModeManager::create(this); +#ifdef PipeWire_FOUND + new ScreencastManager(this); +#endif +} + +void ApplicationWayland::createBackend() +{ + connect(platform(), &Platform::screensQueried, this, &ApplicationWayland::continueStartupWithScreens); + connect(platform(), &Platform::initFailed, this, + [] () { + std::cerr << "FATAL ERROR: backend failed to initialize, exiting now" << std::endl; + QCoreApplication::exit(1); + } + ); + platform()->init(); +} + +void ApplicationWayland::continueStartupWithScreens() +{ + disconnect(kwinApp()->platform(), &Platform::screensQueried, this, &ApplicationWayland::continueStartupWithScreens); + createScreens(); + WaylandCompositor::create(); + connect(Compositor::self(), &Compositor::sceneCreated, this, &ApplicationWayland::continueStartupWithScene); +} + +void ApplicationWayland::finalizeStartup() +{ + if (m_xwayland) { + disconnect(m_xwayland, &Xwl::Xwayland::errorOccurred, this, &ApplicationWayland::finalizeStartup); + disconnect(m_xwayland, &Xwl::Xwayland::started, this, &ApplicationWayland::finalizeStartup); + } + startSession(); + notifyStarted(); +} + +void ApplicationWayland::continueStartupWithScene() +{ + disconnect(Compositor::self(), &Compositor::sceneCreated, this, &ApplicationWayland::continueStartupWithScene); + + // Note that we start accepting client connections after creating the Workspace. + createWorkspace(); + + if (!waylandServer()->start()) { + qFatal("Failed to initialze the Wayland server, exiting now"); + } + + if (operationMode() == OperationModeWaylandOnly) { + finalizeStartup(); + return; + } + + m_xwayland = new Xwl::Xwayland(this); + connect(m_xwayland, &Xwl::Xwayland::errorOccurred, this, &ApplicationWayland::finalizeStartup); + connect(m_xwayland, &Xwl::Xwayland::started, this, &ApplicationWayland::finalizeStartup); + m_xwayland->start(); +} + +void ApplicationWayland::startSession() +{ + if (!m_inputMethodServerToStart.isEmpty()) { + QStringList arguments = KShell::splitArgs(m_inputMethodServerToStart); + if (!arguments.isEmpty()) { + QString program = arguments.takeFirst(); + int socket = dup(waylandServer()->createInputMethodConnection()); + if (socket >= 0) { + QProcessEnvironment environment = processStartupEnvironment(); + environment.insert(QStringLiteral("WAYLAND_SOCKET"), QByteArray::number(socket)); + environment.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("wayland")); + environment.remove("DISPLAY"); + environment.remove("WAYLAND_DISPLAY"); + QProcess *p = new Process(this); + p->setProcessChannelMode(QProcess::ForwardedErrorChannel); + connect(p, qOverload(&QProcess::finished), this, + [p] { + if (waylandServer()) { + waylandServer()->destroyInputMethodConnection(); + } + p->deleteLater(); + } + ); + p->setProcessEnvironment(environment); + p->setProgram(program); + p->setArguments(arguments); + p->start(); + connect(waylandServer(), &WaylandServer::terminatingInternalClientConnection, p, [p] { + p->kill(); + p->waitForFinished(); + }); + } + } else { + qWarning("Failed to launch the input method server: %s is an invalid command", + qPrintable(m_inputMethodServerToStart)); + } + } + + // start session + if (!m_sessionArgument.isEmpty()) { + QStringList arguments = KShell::splitArgs(m_sessionArgument); + if (!arguments.isEmpty()) { + QString program = arguments.takeFirst(); + QProcess *p = new Process(this); + p->setProcessChannelMode(QProcess::ForwardedErrorChannel); + p->setProcessEnvironment(processStartupEnvironment()); + connect(p, qOverload(&QProcess::finished), this, [p] (int code, QProcess::ExitStatus status) { + p->deleteLater(); + if (status == QProcess::CrashExit) { + qWarning() << "Session process has crashed"; + QCoreApplication::exit(-1); + return; + } + + if (code) { + qWarning() << "Session process exited with code" << code; + } + + QCoreApplication::exit(code); + }); + p->setProgram(program); + p->setArguments(arguments); + p->start(); + } else { + qWarning("Failed to launch the session process: %s is an invalid command", + qPrintable(m_sessionArgument)); + } + } + // start the applications passed to us as command line arguments + if (!m_applicationsToStart.isEmpty()) { + for (const QString &application: m_applicationsToStart) { + QStringList arguments = KShell::splitArgs(application); + if (arguments.isEmpty()) { + qWarning("Failed to launch application: %s is an invalid command", + qPrintable(application)); + continue; + } + QString program = arguments.takeFirst(); + // note: this will kill the started process when we exit + // this is going to happen anyway as we are the wayland and X server the app connects to + QProcess *p = new Process(this); + p->setProcessChannelMode(QProcess::ForwardedErrorChannel); + p->setProcessEnvironment(processStartupEnvironment()); + p->setProgram(program); + p->setArguments(arguments); + p->startDetached(); + p->deleteLater(); + } + } +} + +static const QString s_waylandPlugin = QStringLiteral("KWinWaylandWaylandBackend"); +static const QString s_x11Plugin = QStringLiteral("KWinWaylandX11Backend"); +static const QString s_fbdevPlugin = QStringLiteral("KWinWaylandFbdevBackend"); +#if HAVE_DRM +static const QString s_drmPlugin = QStringLiteral("KWinWaylandDrmBackend"); +#endif +#if HAVE_LIBHYBRIS +static const QString s_hwcomposerPlugin = QStringLiteral("KWinWaylandHwcomposerBackend"); +#endif +static const QString s_virtualPlugin = QStringLiteral("KWinWaylandVirtualBackend"); + +static QString automaticBackendSelection() +{ + if (qEnvironmentVariableIsSet("WAYLAND_DISPLAY")) { + return s_waylandPlugin; + } + if (qEnvironmentVariableIsSet("DISPLAY")) { + return s_x11Plugin; + } +#if HAVE_LIBHYBRIS + if (qEnvironmentVariableIsSet("ANDROID_ROOT")) { + return s_hwcomposerPlugin; + } +#endif +#if HAVE_DRM + return s_drmPlugin; +#endif + return s_fbdevPlugin; +} + +static void disablePtrace() +{ +#if HAVE_PR_SET_DUMPABLE + // check whether we are running under a debugger + const QFileInfo parent(QStringLiteral("/proc/%1/exe").arg(getppid())); + if (parent.isSymLink() && + (parent.symLinkTarget().endsWith(QLatin1String("/gdb")) || + parent.symLinkTarget().endsWith(QLatin1String("/gdbserver")) || + parent.symLinkTarget().endsWith(QLatin1String("/lldb-server")))) { + // debugger, don't adjust + return; + } + + // disable ptrace in kwin_wayland + prctl(PR_SET_DUMPABLE, 0); +#endif +#if HAVE_PROC_TRACE_CTL + // FreeBSD's rudimentary procfs does not support /proc//exe + // We could use the P_TRACED flag of the process to find out + // if the process is being debugged ond FreeBSD. + int mode = PROC_TRACE_CTL_DISABLE; + procctl(P_PID, getpid(), PROC_TRACE_CTL, &mode); +#endif + +} + +static void unsetDumpable(int sig) +{ +#if HAVE_PR_SET_DUMPABLE + prctl(PR_SET_DUMPABLE, 1); +#endif + signal(sig, SIG_IGN); + raise(sig); + return; +} + +void dropNiceCapability() +{ +#if HAVE_LIBCAP + cap_t caps = cap_get_proc(); + if (!caps) { + return; + } + cap_value_t capList[] = { CAP_SYS_NICE }; + if (cap_set_flag(caps, CAP_PERMITTED, 1, capList, CAP_CLEAR) == -1) { + cap_free(caps); + return; + } + if (cap_set_flag(caps, CAP_EFFECTIVE, 1, capList, CAP_CLEAR) == -1) { + cap_free(caps); + return; + } + cap_set_proc(caps); + cap_free(caps); +#endif +} + +} // namespace + +int main(int argc, char * argv[]) +{ + if (getuid() == 0) { + std::cerr << "kwin_wayland does not support running as root." << std::endl; + return 1; + } + KWin::disablePtrace(); + KWin::Application::setupMalloc(); + KWin::Application::setupLocalizedString(); + KWin::gainRealTime(); + KWin::dropNiceCapability(); + + if (signal(SIGTERM, KWin::sighandler) == SIG_IGN) + signal(SIGTERM, SIG_IGN); + if (signal(SIGINT, KWin::sighandler) == SIG_IGN) + signal(SIGINT, SIG_IGN); + if (signal(SIGHUP, KWin::sighandler) == SIG_IGN) + signal(SIGHUP, SIG_IGN); + signal(SIGABRT, KWin::unsetDumpable); + signal(SIGSEGV, KWin::unsetDumpable); + signal(SIGPIPE, SIG_IGN); + // ensure that no thread takes SIGUSR + sigset_t userSignals; + sigemptyset(&userSignals); + sigaddset(&userSignals, SIGUSR1); + sigaddset(&userSignals, SIGUSR2); + pthread_sigmask(SIG_BLOCK, &userSignals, nullptr); + + QProcessEnvironment environment = QProcessEnvironment::systemEnvironment(); + + // enforce our internal qpa plugin, unfortunately command line switch has precedence + setenv("QT_QPA_PLATFORM", "wayland-org.kde.kwin.qpa", true); + + qunsetenv("QT_DEVICE_PIXEL_RATIO"); + qputenv("QT_IM_MODULE", "qtvirtualkeyboard"); + qputenv("QSG_RENDER_LOOP", "basic"); + QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); + KWin::ApplicationWayland a(argc, argv); + a.setupTranslator(); + // reset QT_QPA_PLATFORM to a sane value for any processes started from KWin + setenv("QT_QPA_PLATFORM", "wayland", true); + + KWin::Application::createAboutData(); + KQuickAddons::QtQuickSettings::init(); + + const auto availablePlugins = KPluginLoader::findPlugins(QStringLiteral("org.kde.kwin.waylandbackends")); + auto hasPlugin = [&availablePlugins] (const QString &name) { + return std::any_of(availablePlugins.begin(), availablePlugins.end(), + [name] (const KPluginMetaData &plugin) { + return plugin.pluginId() == name; + } + ); + }; + const bool hasSizeOption = hasPlugin(KWin::s_x11Plugin) || hasPlugin(KWin::s_virtualPlugin); + const bool hasOutputCountOption = hasPlugin(KWin::s_x11Plugin); + const bool hasX11Option = hasPlugin(KWin::s_x11Plugin); + const bool hasVirtualOption = hasPlugin(KWin::s_virtualPlugin); + const bool hasWaylandOption = hasPlugin(KWin::s_waylandPlugin); + const bool hasFramebufferOption = hasPlugin(KWin::s_fbdevPlugin); +#if HAVE_DRM + const bool hasDrmOption = hasPlugin(KWin::s_drmPlugin); +#endif +#if HAVE_LIBHYBRIS + const bool hasHwcomposerOption = hasPlugin(KWin::s_hwcomposerPlugin); +#endif + + QCommandLineOption xwaylandOption(QStringLiteral("xwayland"), + i18n("Start a rootless Xwayland server.")); + QCommandLineOption waylandSocketOption(QStringList{QStringLiteral("s"), QStringLiteral("socket")}, + i18n("Name of the Wayland socket to listen on. If not set \"wayland-0\" is used."), + QStringLiteral("socket")); + QCommandLineOption framebufferOption(QStringLiteral("framebuffer"), + i18n("Render to framebuffer.")); + QCommandLineOption framebufferDeviceOption(QStringLiteral("fb-device"), + i18n("The framebuffer device to render to."), + QStringLiteral("fbdev")); + QCommandLineOption x11DisplayOption(QStringLiteral("x11-display"), + i18n("The X11 Display to use in windowed mode on platform X11."), + QStringLiteral("display")); + QCommandLineOption waylandDisplayOption(QStringLiteral("wayland-display"), + i18n("The Wayland Display to use in windowed mode on platform Wayland."), + QStringLiteral("display")); + QCommandLineOption virtualFbOption(QStringLiteral("virtual"), i18n("Render to a virtual framebuffer.")); + QCommandLineOption widthOption(QStringLiteral("width"), + i18n("The width for windowed mode. Default width is 1024."), + QStringLiteral("width")); + widthOption.setDefaultValue(QString::number(1024)); + QCommandLineOption heightOption(QStringLiteral("height"), + i18n("The height for windowed mode. Default height is 768."), + QStringLiteral("height")); + heightOption.setDefaultValue(QString::number(768)); + + QCommandLineOption scaleOption(QStringLiteral("scale"), + i18n("The scale for windowed mode. Default value is 1."), + QStringLiteral("scale")); + scaleOption.setDefaultValue(QString::number(1)); + + QCommandLineOption outputCountOption(QStringLiteral("output-count"), + i18n("The number of windows to open as outputs in windowed mode. Default value is 1"), + QStringLiteral("count")); + outputCountOption.setDefaultValue(QString::number(1)); + + QCommandLineParser parser; + a.setupCommandLine(&parser); + parser.addOption(xwaylandOption); + parser.addOption(waylandSocketOption); + if (hasX11Option) { + parser.addOption(x11DisplayOption); + } + if (hasWaylandOption) { + parser.addOption(waylandDisplayOption); + } + if (hasFramebufferOption) { + parser.addOption(framebufferOption); + parser.addOption(framebufferDeviceOption); + } + if (hasVirtualOption) { + parser.addOption(virtualFbOption); + } + if (hasSizeOption) { + parser.addOption(widthOption); + parser.addOption(heightOption); + parser.addOption(scaleOption); + } + if (hasOutputCountOption) { + parser.addOption(outputCountOption); + } +#if HAVE_LIBHYBRIS + QCommandLineOption hwcomposerOption(QStringLiteral("hwcomposer"), i18n("Use libhybris hwcomposer")); + if (hasHwcomposerOption) { + parser.addOption(hwcomposerOption); + } +#endif + QCommandLineOption libinputOption(QStringLiteral("libinput"), + i18n("Enable libinput support for input events processing. Note: never use in a nested session. (deprecated)")); + parser.addOption(libinputOption); +#if HAVE_DRM + QCommandLineOption drmOption(QStringLiteral("drm"), i18n("Render through drm node.")); + if (hasDrmOption) { + parser.addOption(drmOption); + } +#endif + + QCommandLineOption inputMethodOption(QStringLiteral("inputmethod"), + i18n("Input method that KWin starts."), + QStringLiteral("path/to/imserver")); + parser.addOption(inputMethodOption); + + QCommandLineOption listBackendsOption(QStringLiteral("list-backends"), + i18n("List all available backends and quit.")); + parser.addOption(listBackendsOption); + + QCommandLineOption screenLockerOption(QStringLiteral("lockscreen"), + i18n("Starts the session in locked mode.")); + parser.addOption(screenLockerOption); + + QCommandLineOption noScreenLockerOption(QStringLiteral("no-lockscreen"), + i18n("Starts the session without lock screen support.")); + parser.addOption(noScreenLockerOption); + + QCommandLineOption noGlobalShortcutsOption(QStringLiteral("no-global-shortcuts"), + i18n("Starts the session without global shortcuts support.")); + parser.addOption(noGlobalShortcutsOption); + + QCommandLineOption exitWithSessionOption(QStringLiteral("exit-with-session"), + i18n("Exit after the session application, which is started by KWin, closed."), + QStringLiteral("/path/to/session")); + parser.addOption(exitWithSessionOption); + + parser.addPositionalArgument(QStringLiteral("applications"), + i18n("Applications to start once Wayland and Xwayland server are started"), + QStringLiteral("[/path/to/application...]")); + + parser.process(a); + a.processCommandLine(&parser); + +#ifdef KWIN_BUILD_ACTIVITIES + a.setUseKActivities(false); +#endif + + if (parser.isSet(listBackendsOption)) { + for (const auto &plugin: availablePlugins) { + std::cout << std::setw(40) << std::left << qPrintable(plugin.name()) << qPrintable(plugin.description()) << std::endl; + } + return 0; + } + + if (parser.isSet(exitWithSessionOption)) { + a.setSessionArgument(parser.value(exitWithSessionOption)); + } + + KWin::Application::setUseLibinput(parser.isSet(libinputOption)); + + QString pluginName; + QSize initialWindowSize; + QByteArray deviceIdentifier; + int outputCount = 1; + qreal outputScale = 1; + +#if HAVE_DRM + if (hasDrmOption && parser.isSet(drmOption)) { + pluginName = KWin::s_drmPlugin; + } +#endif + + if (hasSizeOption) { + bool ok = false; + const int width = parser.value(widthOption).toInt(&ok); + if (!ok) { + std::cerr << "FATAL ERROR incorrect value for width" << std::endl; + return 1; + } + const int height = parser.value(heightOption).toInt(&ok); + if (!ok) { + std::cerr << "FATAL ERROR incorrect value for height" << std::endl; + return 1; + } + const qreal scale = parser.value(scaleOption).toDouble(&ok); + if (!ok || scale <= 0) { + std::cerr << "FATAL ERROR incorrect value for scale" << std::endl; + return 1; + } + + outputScale = scale; + initialWindowSize = QSize(width, height); + } + + if (hasOutputCountOption) { + bool ok = false; + const int count = parser.value(outputCountOption).toInt(&ok); + if (ok) { + outputCount = qMax(1, count); + } + } + + if (hasX11Option && parser.isSet(x11DisplayOption)) { + deviceIdentifier = parser.value(x11DisplayOption).toUtf8(); + pluginName = KWin::s_x11Plugin; + } else if (hasWaylandOption && parser.isSet(waylandDisplayOption)) { + deviceIdentifier = parser.value(waylandDisplayOption).toUtf8(); + pluginName = KWin::s_waylandPlugin; + } + + if (hasFramebufferOption && parser.isSet(framebufferOption)) { + pluginName = KWin::s_fbdevPlugin; + deviceIdentifier = parser.value(framebufferDeviceOption).toUtf8(); + } +#if HAVE_LIBHYBRIS + if (hasHwcomposerOption && parser.isSet(hwcomposerOption)) { + pluginName = KWin::s_hwcomposerPlugin; + } +#endif + if (hasVirtualOption && parser.isSet(virtualFbOption)) { + pluginName = KWin::s_virtualPlugin; + } + + if (pluginName.isEmpty()) { + std::cerr << "No backend specified through command line argument, trying auto resolution" << std::endl; + pluginName = KWin::automaticBackendSelection(); + } + + auto pluginIt = std::find_if(availablePlugins.begin(), availablePlugins.end(), + [&pluginName] (const KPluginMetaData &plugin) { + return plugin.pluginId() == pluginName; + } + ); + if (pluginIt == availablePlugins.end()) { + std::cerr << "FATAL ERROR: could not find a backend" << std::endl; + return 1; + } + + // TODO: create backend without having the server running + KWin::WaylandServer *server = KWin::WaylandServer::create(&a); + + KWin::WaylandServer::InitializationFlags flags; + if (parser.isSet(screenLockerOption)) { + flags = KWin::WaylandServer::InitializationFlag::LockScreen; + } else if (parser.isSet(noScreenLockerOption)) { + flags = KWin::WaylandServer::InitializationFlag::NoLockScreenIntegration; + } + if (parser.isSet(noGlobalShortcutsOption)) { + flags |= KWin::WaylandServer::InitializationFlag::NoGlobalShortcuts; + } + if (!server->init(parser.value(waylandSocketOption).toUtf8(), flags)) { + std::cerr << "FATAL ERROR: could not create Wayland server" << std::endl; + return 1; + } + + a.initPlatform(*pluginIt); + if (!a.platform()) { + std::cerr << "FATAL ERROR: could not instantiate a backend" << std::endl; + return 1; + } + if (!deviceIdentifier.isEmpty()) { + a.platform()->setDeviceIdentifier(deviceIdentifier); + } + if (initialWindowSize.isValid()) { + a.platform()->setInitialWindowSize(initialWindowSize); + } + a.platform()->setInitialOutputScale(outputScale); + a.platform()->setInitialOutputCount(outputCount); + + QObject::connect(&a, &KWin::Application::workspaceCreated, server, &KWin::WaylandServer::initWorkspace); + environment.insert(QStringLiteral("WAYLAND_DISPLAY"), server->display()->socketName()); + a.setProcessStartupEnvironment(environment); + a.setStartXwayland(parser.isSet(xwaylandOption)); + a.setApplicationsToStart(parser.positionalArguments()); + a.setInputMethodServerToStart(parser.value(inputMethodOption)); + a.start(); + + return a.exec(); +} diff --git a/main_wayland.h b/main_wayland.h new file mode 100644 index 0000000..c55c66e --- /dev/null +++ b/main_wayland.h @@ -0,0 +1,69 @@ +/* + 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 +*/ +#ifndef KWIN_MAIN_WAYLAND_H +#define KWIN_MAIN_WAYLAND_H +#include "main.h" +#include + +namespace KWin +{ +namespace Xwl +{ +class Xwayland; +} + +class ApplicationWayland : public ApplicationWaylandAbstract +{ + Q_OBJECT +public: + ApplicationWayland(int &argc, char **argv); + ~ApplicationWayland() override; + + void setStartXwayland(bool start) { + m_startXWayland = start; + } + void setApplicationsToStart(const QStringList &applications) { + m_applicationsToStart = applications; + } + void setInputMethodServerToStart(const QString &inputMethodServer) { + m_inputMethodServerToStart = inputMethodServer; + } + void setProcessStartupEnvironment(const QProcessEnvironment &environment) override { + m_environment = environment; + } + void setSessionArgument(const QString &session) { + m_sessionArgument = session; + } + + QProcessEnvironment processStartupEnvironment() const override { + return m_environment; + } + +protected: + void performStartup() override; + +private: + void createBackend(); + void continueStartupWithScreens(); + void continueStartupWithScene(); + void finalizeStartup(); + void startSession() override; + + bool m_startXWayland = false; + QStringList m_applicationsToStart; + QString m_inputMethodServerToStart; + QProcessEnvironment m_environment; + QString m_sessionArgument; + + Xwl::Xwayland *m_xwayland = nullptr; +}; + +} + +#endif diff --git a/main_x11.cpp b/main_x11.cpp new file mode 100644 index 0000000..ea53064 --- /dev/null +++ b/main_x11.cpp @@ -0,0 +1,487 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "main_x11.h" + +#include + +#include "platform.h" +#include "sm.h" +#include "workspace.h" +#include "xcbutils.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// system +#ifdef HAVE_UNISTD_H +#include +#endif // HAVE_UNISTD_H +#include + +Q_LOGGING_CATEGORY(KWIN_CORE, "kwin_core", QtCriticalMsg) + +namespace KWin +{ + +static void sighandler(int) +{ + QApplication::exit(); +} + +class AlternativeWMDialog : public QDialog +{ +public: + AlternativeWMDialog() + : QDialog() { + QWidget* mainWidget = new QWidget(this); + QVBoxLayout* layout = new QVBoxLayout(mainWidget); + QString text = i18n( + "KWin is unstable.\n" + "It seems to have crashed several times in a row.\n" + "You can select another window manager to run:"); + QLabel* textLabel = new QLabel(text, mainWidget); + layout->addWidget(textLabel); + wmList = new QComboBox(mainWidget); + wmList->setEditable(true); + layout->addWidget(wmList); + + addWM(QStringLiteral("metacity")); + addWM(QStringLiteral("openbox")); + addWM(QStringLiteral("fvwm2")); + addWM(QStringLiteral(KWIN_INTERNAL_NAME_X11)); + + QVBoxLayout *mainLayout = new QVBoxLayout(this); + mainLayout->addWidget(mainWidget); + QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + buttons->button(QDialogButtonBox::Ok)->setDefault(true); + connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + mainLayout->addWidget(buttons); + + raise(); + } + + void addWM(const QString& wm) { + // TODO: Check if WM is installed + if (!QStandardPaths::findExecutable(wm).isEmpty()) + wmList->addItem(wm); + } + QString selectedWM() const { + return wmList->currentText(); + } + +private: + QComboBox* wmList; +}; + +class KWinSelectionOwner : public KSelectionOwner +{ + Q_OBJECT +public: + explicit KWinSelectionOwner(int screen) + : KSelectionOwner(make_selection_atom(screen), screen) + { + } + +private: + bool genericReply(xcb_atom_t target_P, xcb_atom_t property_P, xcb_window_t requestor_P) override { + if (target_P == xa_version) { + int32_t version[] = { 2, 0 }; + xcb_change_property(connection(), XCB_PROP_MODE_REPLACE, requestor_P, + property_P, XCB_ATOM_INTEGER, 32, 2, version); + } else + return KSelectionOwner::genericReply(target_P, property_P, requestor_P); + return true; + } + + void replyTargets(xcb_atom_t property_P, xcb_window_t requestor_P) override { + KSelectionOwner::replyTargets(property_P, requestor_P); + xcb_atom_t atoms[ 1 ] = { xa_version }; + // PropModeAppend ! + xcb_change_property(connection(), XCB_PROP_MODE_APPEND, requestor_P, + property_P, XCB_ATOM_ATOM, 32, 1, atoms); + } + + void getAtoms() override { + KSelectionOwner::getAtoms(); + if (xa_version == XCB_ATOM_NONE) { + const QByteArray name(QByteArrayLiteral("VERSION")); + ScopedCPointer atom(xcb_intern_atom_reply( + connection(), + xcb_intern_atom_unchecked(connection(), false, name.length(), name.constData()), + nullptr)); + if (!atom.isNull()) { + xa_version = atom->atom; + } + } + } + + xcb_atom_t make_selection_atom(int screen_P) { + if (screen_P < 0) + screen_P = QX11Info::appScreen(); + QByteArray screen(QByteArrayLiteral("WM_S")); + screen.append(QByteArray::number(screen_P)); + ScopedCPointer atom(xcb_intern_atom_reply( + connection(), + xcb_intern_atom_unchecked(connection(), false, screen.length(), screen.constData()), + nullptr)); + if (atom.isNull()) { + return XCB_ATOM_NONE; + } + return atom->atom; + } + static xcb_atom_t xa_version; +}; +xcb_atom_t KWinSelectionOwner::xa_version = XCB_ATOM_NONE; + +//************************************ +// ApplicationX11 +//************************************ + +ApplicationX11::ApplicationX11(int &argc, char **argv) + : Application(OperationModeX11, argc, argv) + , owner() + , m_replace(false) +{ + setX11Connection(QX11Info::connection()); + setX11RootWindow(QX11Info::appRootWindow()); +} + +ApplicationX11::~ApplicationX11() +{ + setTerminating(); + destroyCompositor(); + destroyWorkspace(); + if (!owner.isNull() && owner->ownerWindow() != XCB_WINDOW_NONE) // If there was no --replace (no new WM) + Xcb::setInputFocus(XCB_INPUT_FOCUS_POINTER_ROOT); +} + +void ApplicationX11::setReplace(bool replace) +{ + m_replace = replace; +} + +void ApplicationX11::lostSelection() +{ + sendPostedEvents(); + destroyCompositor(); + destroyWorkspace(); + // Remove windowmanager privileges + Xcb::selectInput(rootWindow(), XCB_EVENT_MASK_PROPERTY_CHANGE); + quit(); +} + + +static xcb_screen_t *findXcbScreen(xcb_connection_t *connection, int screen) +{ + for (xcb_screen_iterator_t it = xcb_setup_roots_iterator(xcb_get_setup(connection)); + it.rem; + --screen, xcb_screen_next(&it)) { + if (screen == 0) { + return it.data; + } + } + return nullptr; +} + +void ApplicationX11::performStartup() +{ + crashChecking(); + + if (Application::x11ScreenNumber() == -1) { + Application::setX11ScreenNumber(QX11Info::appScreen()); + } + setX11DefaultScreen(findXcbScreen(x11Connection(), x11ScreenNumber())); + + owner.reset(new KWinSelectionOwner(Application::x11ScreenNumber())); + connect(owner.data(), &KSelectionOwner::failedToClaimOwnership, []{ + fputs(i18n("kwin: unable to claim manager selection, another wm running? (try using --replace)\n").toLocal8Bit().constData(), stderr); + ::exit(1); + }); + connect(owner.data(), SIGNAL(lostOwnership()), SLOT(lostSelection())); + connect(owner.data(), &KSelectionOwner::claimedOwnership, [this]{ + installNativeX11EventFilter(); + // first load options - done internally by a different thread + createOptions(); + + // Check whether another windowmanager is running + const uint32_t maskValues[] = {XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT}; + ScopedCPointer redirectCheck(xcb_request_check(connection(), + xcb_change_window_attributes_checked(connection(), + rootWindow(), + XCB_CW_EVENT_MASK, + maskValues))); + if (!redirectCheck.isNull()) { + fputs(i18n("kwin: another window manager is running (try using --replace)\n").toLocal8Bit().constData(), stderr); + if (!wasCrash()) // if this is a crash-restart, DrKonqi may have stopped the process w/o killing the connection + ::exit(1); + } + + createInput(); + + connect(platform(), &Platform::screensQueried, this, + [this] { + createWorkspace(); + + Xcb::sync(); // Trigger possible errors, there's still a chance to abort + + notifyKSplash(); + + notifyStarted(); + } + ); + connect(platform(), &Platform::initFailed, this, + [] () { + std::cerr << "FATAL ERROR: backend failed to initialize, exiting now" << std::endl; + ::exit(1); + } + ); + platform()->init(); + }); + // we need to do an XSync here, otherwise the QPA might crash us later on + Xcb::sync(); + owner->claim(m_replace || wasCrash(), true); + + createAtoms(); +} + +bool ApplicationX11::notify(QObject* o, QEvent* e) +{ + if (e->spontaneous() && Workspace::self()->workspaceEvent(e)) + return true; + return QApplication::notify(o, e); +} + +void ApplicationX11::setupCrashHandler() +{ + KCrash::setEmergencySaveFunction(ApplicationX11::crashHandler); +} + +void ApplicationX11::crashChecking() +{ + setupCrashHandler(); + if (crashes >= 4) { + // Something has gone seriously wrong + AlternativeWMDialog dialog; + QString cmd = QStringLiteral(KWIN_INTERNAL_NAME_X11); + if (dialog.exec() == QDialog::Accepted) + cmd = dialog.selectedWM(); + else + ::exit(1); + if (cmd.length() > 500) { + qCDebug(KWIN_CORE) << "Command is too long, truncating"; + cmd = cmd.left(500); + } + qCDebug(KWIN_CORE) << "Starting" << cmd << "and exiting"; + char buf[1024]; + sprintf(buf, "%s &", cmd.toLatin1().data()); + system(buf); + ::exit(1); + } + if (crashes >= 2) { + // Disable compositing if we have had too many crashes + qCDebug(KWIN_CORE) << "Too many crashes recently, disabling compositing"; + KConfigGroup compgroup(KSharedConfig::openConfig(), "Compositing"); + compgroup.writeEntry("Enabled", false); + } + // Reset crashes count if we stay up for more that 15 seconds + QTimer::singleShot(15 * 1000, this, SLOT(resetCrashesCount())); +} + +void ApplicationX11::notifyKSplash() +{ + // Tell KSplash that KWin has started + QDBusMessage ksplashProgressMessage = QDBusMessage::createMethodCall(QStringLiteral("org.kde.KSplash"), + QStringLiteral("/KSplash"), + QStringLiteral("org.kde.KSplash"), + QStringLiteral("setStage")); + ksplashProgressMessage.setArguments(QList() << QStringLiteral("wm")); + QDBusConnection::sessionBus().asyncCall(ksplashProgressMessage); +} + +void ApplicationX11::crashHandler(int signal) +{ + crashes++; + + fprintf(stderr, "Application::crashHandler() called with signal %d; recent crashes: %d\n", signal, crashes); + char cmd[1024]; + sprintf(cmd, "%s --crashes %d &", + QFile::encodeName(QCoreApplication::applicationFilePath()).constData(), crashes); + + sleep(1); + system(cmd); +} + +} // namespace + +int main(int argc, char * argv[]) +{ + KWin::Application::setupMalloc(); + KWin::Application::setupLocalizedString(); + + int primaryScreen = 0; + xcb_connection_t *c = xcb_connect(nullptr, &primaryScreen); + if (!c || xcb_connection_has_error(c)) { + fprintf(stderr, "%s: FATAL ERROR while trying to open display %s\n", + argv[0], qgetenv("DISPLAY").constData()); + exit(1); + } + + const int number_of_screens = xcb_setup_roots_length(xcb_get_setup(c)); + xcb_disconnect(c); + c = nullptr; + + // multi head + auto isMultiHead = []() -> bool { + QByteArray multiHead = qgetenv("KDE_MULTIHEAD"); + if (!multiHead.isEmpty()) { + return (multiHead.toLower() == "true"); + } + return true; + }; + if (number_of_screens != 1 && isMultiHead()) { + KWin::Application::setX11MultiHead(true); + KWin::Application::setX11ScreenNumber(primaryScreen); + int pos; // Temporarily needed to reconstruct DISPLAY var if multi-head + QByteArray display_name = qgetenv("DISPLAY"); + + if ((pos = display_name.lastIndexOf('.')) != -1) + display_name.remove(pos, 10); // 10 is enough to be sure we removed ".s" + + for (int i = 0; i < number_of_screens; i++) { + // If execution doesn't pass by here, then kwin + // acts exactly as previously + if (i != KWin::Application::x11ScreenNumber() && fork() == 0) { + KWin::Application::setX11ScreenNumber(i); + QByteArray dBusSuffix = qgetenv("KWIN_DBUS_SERVICE_SUFFIX"); + if (!dBusSuffix.isNull()) { + dBusSuffix.append("."); + } + dBusSuffix.append(QByteArrayLiteral("head-")).append(QByteArray::number(i)); + qputenv("KWIN_DBUS_SERVICE_SUFFIX", dBusSuffix); + // Break here because we are the child process, we don't + // want to fork() anymore + break; + } + } + // In the next statement, display_name shouldn't contain a screen + // number. If it had it, it was removed at the "pos" check + const QString envir = QStringLiteral("DISPLAY=%1.%2") + .arg(display_name.data()) + .arg(KWin::Application::x11ScreenNumber()); + + if (putenv(strdup(envir.toLatin1().constData()))) { + fprintf(stderr, "%s: WARNING: unable to set DISPLAY environment variable\n", argv[0]); + perror("putenv()"); + } + } + + if (signal(SIGTERM, KWin::sighandler) == SIG_IGN) + signal(SIGTERM, SIG_IGN); + if (signal(SIGINT, KWin::sighandler) == SIG_IGN) + signal(SIGINT, SIG_IGN); + if (signal(SIGHUP, KWin::sighandler) == SIG_IGN) + signal(SIGHUP, SIG_IGN); + signal(SIGPIPE, SIG_IGN); + + // Disable the glib event loop integration, since it seems to be responsible + // for several bug reports about high CPU usage (bug #239963) + setenv("QT_NO_GLIB", "1", true); + + // enforce xcb plugin, unfortunately command line switch has precedence + setenv("QT_QPA_PLATFORM", "xcb", true); + + qunsetenv("QT_DEVICE_PIXEL_RATIO"); + qunsetenv("QT_SCALE_FACTOR"); + QCoreApplication::setAttribute(Qt::AA_DisableHighDpiScaling); + // KSMServer talks to us directly on DBus. + QCoreApplication::setAttribute(Qt::AA_DisableSessionManager); + + KWin::ApplicationX11 a(argc, argv); + a.setupTranslator(); + + KWin::Application::createAboutData(); + KQuickAddons::QtQuickSettings::init(); + + // disables vsync for any QtQuick windows we create (BUG 406180) + QSurfaceFormat format = QSurfaceFormat::defaultFormat(); + format.setSwapInterval(0); + QSurfaceFormat::setDefaultFormat(format); + + QCommandLineOption replaceOption(QStringLiteral("replace"), i18n("Replace already-running ICCCM2.0-compliant window manager")); + + QCommandLineParser parser; + a.setupCommandLine(&parser); + parser.addOption(replaceOption); +#ifdef KWIN_BUILD_ACTIVITIES + QCommandLineOption noActivitiesOption(QStringLiteral("no-kactivities"), + i18n("Disable KActivities integration.")); + parser.addOption(noActivitiesOption); +#endif + + parser.process(a); + a.processCommandLine(&parser); + a.setReplace(parser.isSet(replaceOption)); +#ifdef KWIN_BUILD_ACTIVITIES + if (parser.isSet(noActivitiesOption)) { + a.setUseKActivities(false); + } +#endif + + // perform sanity checks + if (a.platformName().toLower() != QStringLiteral("xcb")) { + fprintf(stderr, "%s: FATAL ERROR expecting platform xcb but got platform %s\n", + argv[0], qPrintable(a.platformName())); + exit(1); + } + if (!QX11Info::display()) { + fprintf(stderr, "%s: FATAL ERROR KWin requires Xlib support in the xcb plugin. Do not configure Qt with -no-xcb-xlib\n", + argv[0]); + exit(1); + } + + // find and load the X11 platform plugin + const auto plugins = KPluginLoader::findPluginsById(QStringLiteral("org.kde.kwin.platforms"), + QStringLiteral("KWinX11Platform")); + if (plugins.isEmpty()) { + std::cerr << "FATAL ERROR: KWin could not find the KWinX11Platform plugin" << std::endl; + return 1; + } + a.initPlatform(plugins.first()); + if (!a.platform()) { + std::cerr << "FATAL ERROR: could not instantiate the platform plugin" << std::endl; + return 1; + } + + a.start(); + + return a.exec(); +} + +#include "main_x11.moc" diff --git a/main_x11.h b/main_x11.h new file mode 100644 index 0000000..a360b87 --- /dev/null +++ b/main_x11.h @@ -0,0 +1,47 @@ +/* + 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 +*/ +#ifndef KWIN_MAIN_X11_H +#define KWIN_MAIN_X11_H +#include "main.h" + +namespace KWin +{ + +class KWinSelectionOwner; + +class ApplicationX11 : public Application +{ + Q_OBJECT +public: + ApplicationX11(int &argc, char **argv); + ~ApplicationX11() override; + + void setReplace(bool replace); + +protected: + void performStartup() override; + bool notify(QObject *o, QEvent *e) override; + +private Q_SLOTS: + void lostSelection(); + +private: + void crashChecking(); + void setupCrashHandler(); + void notifyKSplash(); + + static void crashHandler(int signal); + + QScopedPointer owner; + bool m_replace; +}; + +} + +#endif diff --git a/modifier_only_shortcuts.cpp b/modifier_only_shortcuts.cpp new file mode 100644 index 0000000..c68c1eb --- /dev/null +++ b/modifier_only_shortcuts.cpp @@ -0,0 +1,94 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016, 2017 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "modifier_only_shortcuts.h" +#include "input_event.h" +#include "options.h" +#include "screenlockerwatcher.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include +#include + +namespace KWin +{ + +ModifierOnlyShortcuts::ModifierOnlyShortcuts() + : QObject() + , InputEventSpy() +{ + connect(ScreenLockerWatcher::self(), &ScreenLockerWatcher::locked, this, &ModifierOnlyShortcuts::reset); +} + +ModifierOnlyShortcuts::~ModifierOnlyShortcuts() = default; + +void ModifierOnlyShortcuts::keyEvent(KeyEvent *event) +{ + if (event->isAutoRepeat()) { + return; + } + if (event->type() == QEvent::KeyPress) { + const bool wasEmpty = m_pressedKeys.isEmpty(); + m_pressedKeys.insert(event->nativeScanCode()); + if (wasEmpty && m_pressedKeys.size() == 1 && + !ScreenLockerWatcher::self()->isLocked() && + m_buttonPressCount == 0 && + m_cachedMods == Qt::NoModifier) { + m_modifier = Qt::KeyboardModifier(int(event->modifiersRelevantForGlobalShortcuts())); + } else { + m_modifier = Qt::NoModifier; + } + } else if (!m_pressedKeys.isEmpty()) { + m_pressedKeys.remove(event->nativeScanCode()); + if (m_pressedKeys.isEmpty() && + event->modifiersRelevantForGlobalShortcuts() == Qt::NoModifier && + workspace() && !workspace()->globalShortcutsDisabled()) { + if (m_modifier != Qt::NoModifier) { + const auto list = options->modifierOnlyDBusShortcut(m_modifier); + if (list.size() >= 4) { + if (!waylandServer() || !waylandServer()->isKeyboardShortcutsInhibited()) { + auto call = QDBusMessage::createMethodCall(list.at(0), list.at(1), list.at(2), list.at(3)); + QVariantList args; + for (int i = 4; i < list.size(); ++i) { + args << list.at(i); + } + call.setArguments(args); + QDBusConnection::sessionBus().asyncCall(call); + } + } + } + } + m_modifier = Qt::NoModifier; + } else { + m_modifier = Qt::NoModifier; + } + m_cachedMods = event->modifiersRelevantForGlobalShortcuts(); +} + +void ModifierOnlyShortcuts::pointerEvent(MouseEvent *event) +{ + if (event->type() == QEvent::MouseMove) { + return; + } + if (event->type() == QEvent::MouseButtonPress) { + m_buttonPressCount++; + } else if (event->type() == QEvent::MouseButtonRelease) { + m_buttonPressCount--; + } + reset(); +} + +void ModifierOnlyShortcuts::wheelEvent(WheelEvent *event) +{ + Q_UNUSED(event) + reset(); +} + +} diff --git a/modifier_only_shortcuts.h b/modifier_only_shortcuts.h new file mode 100644 index 0000000..58a268e --- /dev/null +++ b/modifier_only_shortcuts.h @@ -0,0 +1,45 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016, 2017 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_MODIFIER_ONLY_SHORTCUTS_H +#define KWIN_MODIFIER_ONLY_SHORTCUTS_H + +#include "input_event_spy.h" +#include + +#include +#include + +namespace KWin +{ + +class KWIN_EXPORT ModifierOnlyShortcuts : public QObject, public InputEventSpy +{ + Q_OBJECT +public: + explicit ModifierOnlyShortcuts(); + ~ModifierOnlyShortcuts() override; + + void keyEvent(KeyEvent *event) override; + void pointerEvent(MouseEvent *event) override; + void wheelEvent(WheelEvent *event) override; + + void reset() { + m_modifier = Qt::NoModifier; + } + +private: + Qt::KeyboardModifier m_modifier = Qt::NoModifier; + Qt::KeyboardModifiers m_cachedMods; + uint m_buttonPressCount = 0; + QSet m_pressedKeys; +}; + +} + +#endif diff --git a/moving_client_x11_filter.cpp b/moving_client_x11_filter.cpp new file mode 100644 index 0000000..79bf8df --- /dev/null +++ b/moving_client_x11_filter.cpp @@ -0,0 +1,51 @@ +/* + 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 "moving_client_x11_filter.h" +#include "x11client.h" +#include "workspace.h" +#include +#include + +namespace KWin +{ + +MovingClientX11Filter::MovingClientX11Filter() + : X11EventFilter(QVector{XCB_KEY_PRESS, XCB_MOTION_NOTIFY, XCB_BUTTON_PRESS, XCB_BUTTON_RELEASE}) +{ +} + +bool MovingClientX11Filter::event(xcb_generic_event_t *event) +{ + auto client = dynamic_cast(workspace()->moveResizeClient()); + if (!client) { + return false; + } + auto testWindow = [client, event] (xcb_window_t window) { + return client->moveResizeGrabWindow() == window && client->windowEvent(event); + }; + + const uint8_t eventType = event->response_type & ~0x80; + switch (eventType) { + case XCB_KEY_PRESS: { + int keyQt; + xcb_key_press_event_t *keyEvent = reinterpret_cast(event); + KKeyServer::xcbKeyPressEventToQt(keyEvent, &keyQt); + client->keyPressEvent(keyQt, keyEvent->time); + return true; + } + case XCB_BUTTON_PRESS: + case XCB_BUTTON_RELEASE: + return testWindow(reinterpret_cast(event)->event); + case XCB_MOTION_NOTIFY: + return testWindow(reinterpret_cast(event)->event); + } + return false; +} + +} diff --git a/moving_client_x11_filter.h b/moving_client_x11_filter.h new file mode 100644 index 0000000..356e399 --- /dev/null +++ b/moving_client_x11_filter.h @@ -0,0 +1,27 @@ +/* + 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 +*/ +#ifndef KWIN_MOVING_CLIENT_X11_FILTER_H +#define KWIN_MOVING_CLIENT_X11_FILTER_H +#include "x11eventfilter.h" + +namespace KWin +{ + +class MovingClientX11Filter : public X11EventFilter +{ +public: + explicit MovingClientX11Filter(); + + bool event(xcb_generic_event_t *event) override; +}; + +} + +#endif + diff --git a/netinfo.cpp b/netinfo.cpp new file mode 100644 index 0000000..518aa0c --- /dev/null +++ b/netinfo.cpp @@ -0,0 +1,295 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +// own +#include "netinfo.h" +// kwin +#include "x11client.h" +#include "rootinfo_filter.h" +#include "virtualdesktops.h" +#include "workspace.h" +// Qt +#include + +namespace KWin +{ +extern int screen_number; + +RootInfo *RootInfo::s_self = nullptr; + +RootInfo *RootInfo::create() +{ + Q_ASSERT(!s_self); + xcb_window_t supportWindow = xcb_generate_id(connection()); + const uint32_t values[] = {true}; + xcb_create_window(connection(), XCB_COPY_FROM_PARENT, supportWindow, KWin::rootWindow(), + 0, 0, 1, 1, 0, XCB_COPY_FROM_PARENT, + XCB_COPY_FROM_PARENT, XCB_CW_OVERRIDE_REDIRECT, values); + const uint32_t lowerValues[] = { XCB_STACK_MODE_BELOW }; // See usage in layers.cpp + // we need to do the lower window with a roundtrip, otherwise NETRootInfo is not functioning + ScopedCPointer error(xcb_request_check(connection(), + xcb_configure_window_checked(connection(), supportWindow, XCB_CONFIG_WINDOW_STACK_MODE, lowerValues))); + if (!error.isNull()) { + qCDebug(KWIN_CORE) << "Error occurred while lowering support window: " << error->error_code; + } + + const NET::Properties properties = NET::Supported | + NET::SupportingWMCheck | + NET::ClientList | + NET::ClientListStacking | + NET::DesktopGeometry | + NET::NumberOfDesktops | + NET::CurrentDesktop | + NET::ActiveWindow | + NET::WorkArea | + NET::CloseWindow | + NET::DesktopNames | + NET::WMName | + NET::WMVisibleName | + NET::WMDesktop | + NET::WMWindowType | + NET::WMState | + NET::WMStrut | + NET::WMIconGeometry | + NET::WMIcon | + NET::WMPid | + NET::WMMoveResize | + NET::WMFrameExtents | + NET::WMPing; + const NET::WindowTypes types = NET::NormalMask | + NET::DesktopMask | + NET::DockMask | + NET::ToolbarMask | + NET::MenuMask | + NET::DialogMask | + NET::OverrideMask | + NET::UtilityMask | + NET::SplashMask; // No compositing window types here unless we support them also as managed window types + const NET::States states = NET::Modal | + //NET::Sticky | // Large desktops not supported (and probably never will be) + NET::MaxVert | + NET::MaxHoriz | + NET::Shaded | + NET::SkipTaskbar | + NET::KeepAbove | + //NET::StaysOnTop | // The same like KeepAbove + NET::SkipPager | + NET::Hidden | + NET::FullScreen | + NET::KeepBelow | + NET::DemandsAttention | + NET::SkipSwitcher | + NET::Focused; + NET::Properties2 properties2 = NET::WM2UserTime | + NET::WM2StartupId | + NET::WM2AllowedActions | + NET::WM2RestackWindow | + NET::WM2MoveResizeWindow | + NET::WM2ExtendedStrut | + NET::WM2KDETemporaryRules | + NET::WM2ShowingDesktop | + NET::WM2DesktopLayout | + NET::WM2FullPlacement | + NET::WM2FullscreenMonitors | + NET::WM2KDEShadow | + NET::WM2OpaqueRegion | + NET::WM2GTKFrameExtents; +#ifdef KWIN_BUILD_ACTIVITIES + properties2 |= NET::WM2Activities; +#endif + const NET::Actions actions = NET::ActionMove | + NET::ActionResize | + NET::ActionMinimize | + NET::ActionShade | + //NET::ActionStick | // Sticky state is not supported + NET::ActionMaxVert | + NET::ActionMaxHoriz | + NET::ActionFullScreen | + NET::ActionChangeDesktop | + NET::ActionClose; + + s_self = new RootInfo(supportWindow, "KWin", properties, types, states, properties2, actions, screen_number); + return s_self; +} + +void RootInfo::destroy() +{ + if (!s_self) { + return; + } + xcb_window_t supportWindow = s_self->supportWindow(); + delete s_self; + s_self = nullptr; + xcb_destroy_window(connection(), supportWindow); +} + +RootInfo::RootInfo(xcb_window_t w, const char *name, NET::Properties properties, NET::WindowTypes types, + NET::States states, NET::Properties2 properties2, NET::Actions actions, int scr) + : NETRootInfo(connection(), w, name, properties, types, states, properties2, actions, scr) + , m_activeWindow(activeWindow()) + , m_eventFilter(std::make_unique(this)) +{ +} + +void RootInfo::changeNumberOfDesktops(int n) +{ + VirtualDesktopManager::self()->setCount(n); +} + +void RootInfo::changeCurrentDesktop(int d) +{ + VirtualDesktopManager::self()->setCurrent(d); +} + +void RootInfo::changeActiveWindow(xcb_window_t w, NET::RequestSource src, xcb_timestamp_t timestamp, xcb_window_t active_window) +{ + Workspace *workspace = Workspace::self(); + if (X11Client *c = workspace->findClient(Predicate::WindowMatch, w)) { + if (timestamp == XCB_CURRENT_TIME) + timestamp = c->userTime(); + if (src != NET::FromApplication && src != FromTool) + src = NET::FromTool; + if (src == NET::FromTool) + workspace->activateClient(c, true); // force + else if (c == workspace->mostRecentlyActivatedClient()) { + return; // WORKAROUND? With > 1 plasma activities, we cause this ourselves. bug #240673 + } else { // NET::FromApplication + X11Client *c2; + if (workspace->allowClientActivation(c, timestamp, false, true)) + workspace->activateClient(c); + // if activation of the requestor's window would be allowed, allow activation too + else if (active_window != XCB_WINDOW_NONE + && (c2 = workspace->findClient(Predicate::WindowMatch, active_window)) != nullptr + && workspace->allowClientActivation(c2, + timestampCompare(timestamp, c2->userTime() > 0 ? timestamp : c2->userTime()), false, true)) { + workspace->activateClient(c); + } else + c->demandAttention(); + } + } +} + +void RootInfo::restackWindow(xcb_window_t w, RequestSource src, xcb_window_t above, int detail, xcb_timestamp_t timestamp) +{ + if (X11Client *c = Workspace::self()->findClient(Predicate::WindowMatch, w)) { + if (timestamp == XCB_CURRENT_TIME) + timestamp = c->userTime(); + if (src != NET::FromApplication && src != FromTool) + src = NET::FromTool; + c->restackWindow(above, detail, src, timestamp, true); + } +} + +void RootInfo::closeWindow(xcb_window_t w) +{ + X11Client *c = Workspace::self()->findClient(Predicate::WindowMatch, w); + if (c) + c->closeWindow(); +} + +void RootInfo::moveResize(xcb_window_t w, int x_root, int y_root, unsigned long direction) +{ + X11Client *c = Workspace::self()->findClient(Predicate::WindowMatch, w); + if (c) { + updateXTime(); // otherwise grabbing may have old timestamp - this message should include timestamp + c->NETMoveResize(x_root, y_root, (Direction)direction); + } +} + +void RootInfo::moveResizeWindow(xcb_window_t w, int flags, int x, int y, int width, int height) +{ + X11Client *c = Workspace::self()->findClient(Predicate::WindowMatch, w); + if (c) + c->NETMoveResizeWindow(flags, x, y, width, height); +} + +void RootInfo::gotPing(xcb_window_t w, xcb_timestamp_t timestamp) +{ + if (X11Client *c = Workspace::self()->findClient(Predicate::WindowMatch, w)) + c->gotPing(timestamp); +} + +void RootInfo::changeShowingDesktop(bool showing) +{ + Workspace::self()->setShowingDesktop(showing); +} + +void RootInfo::setActiveClient(AbstractClient *client) +{ + const xcb_window_t w = client ? client->window() : xcb_window_t{XCB_WINDOW_NONE}; + if (m_activeWindow == w) { + return; + } + m_activeWindow = w; + setActiveWindow(m_activeWindow); +} + +// **************************************** +// WinInfo +// **************************************** + +WinInfo::WinInfo(X11Client *c, xcb_window_t window, + xcb_window_t rwin, NET::Properties properties, NET::Properties2 properties2) + : NETWinInfo(connection(), window, rwin, properties, properties2, NET::WindowManager), m_client(c) +{ +} + +void WinInfo::changeDesktop(int desktop) +{ + Workspace::self()->sendClientToDesktop(m_client, desktop, true); +} + +void WinInfo::changeFullscreenMonitors(NETFullscreenMonitors topology) +{ + m_client->updateFullscreenMonitors(topology); +} + +void WinInfo::changeState(NET::States state, NET::States mask) +{ + mask &= ~NET::Sticky; // KWin doesn't support large desktops, ignore + mask &= ~NET::Hidden; // clients are not allowed to change this directly + state &= mask; // for safety, clear all other bits + + if ((mask & NET::FullScreen) != 0 && (state & NET::FullScreen) == 0) + m_client->setFullScreen(false, false); + if ((mask & NET::Max) == NET::Max) + m_client->setMaximize(state & NET::MaxVert, state & NET::MaxHoriz); + else if (mask & NET::MaxVert) + m_client->setMaximize(state & NET::MaxVert, m_client->maximizeMode() & MaximizeHorizontal); + else if (mask & NET::MaxHoriz) + m_client->setMaximize(m_client->maximizeMode() & MaximizeVertical, state & NET::MaxHoriz); + + if (mask & NET::Shaded) + m_client->setShade(state & NET::Shaded ? ShadeNormal : ShadeNone); + if (mask & NET::KeepAbove) + m_client->setKeepAbove((state & NET::KeepAbove) != 0); + if (mask & NET::KeepBelow) + m_client->setKeepBelow((state & NET::KeepBelow) != 0); + if (mask & NET::SkipTaskbar) + m_client->setOriginalSkipTaskbar((state & NET::SkipTaskbar) != 0); + if (mask & NET::SkipPager) + m_client->setSkipPager((state & NET::SkipPager) != 0); + if (mask & NET::SkipSwitcher) + m_client->setSkipSwitcher((state & NET::SkipSwitcher) != 0); + if (mask & NET::DemandsAttention) + m_client->demandAttention((state & NET::DemandsAttention) != 0); + if (mask & NET::Modal) + m_client->setModal((state & NET::Modal) != 0); + // unsetting fullscreen first, setting it last (because e.g. maximize works only for !isFullScreen() ) + if ((mask & NET::FullScreen) != 0 && (state & NET::FullScreen) != 0) + m_client->setFullScreen(true, false); +} + +void WinInfo::disable() +{ + m_client = nullptr; // only used when the object is passed to Deleted +} + +} // namespace diff --git a/netinfo.h b/netinfo.h new file mode 100644 index 0000000..a3ef624 --- /dev/null +++ b/netinfo.h @@ -0,0 +1,83 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_NETINFO_H +#define KWIN_NETINFO_H + +#include + +#include +#include + +namespace KWin +{ + +class AbstractClient; +class RootInfoFilter; +class X11Client; + +/** + * NET WM Protocol handler class + */ +class RootInfo : public NETRootInfo +{ +public: + static RootInfo *create(); + static void destroy(); + + void setActiveClient(AbstractClient *client); + +protected: + void changeNumberOfDesktops(int n) override; + void changeCurrentDesktop(int d) override; + void changeActiveWindow(xcb_window_t w, NET::RequestSource src, xcb_timestamp_t timestamp, xcb_window_t active_window) override; + void closeWindow(xcb_window_t w) override; + void moveResize(xcb_window_t w, int x_root, int y_root, unsigned long direction) override; + void moveResizeWindow(xcb_window_t w, int flags, int x, int y, int width, int height) override; + void gotPing(xcb_window_t w, xcb_timestamp_t timestamp) override; + void restackWindow(xcb_window_t w, RequestSource source, xcb_window_t above, int detail, xcb_timestamp_t timestamp) override; + void changeShowingDesktop(bool showing) override; + +private: + RootInfo(xcb_window_t w, const char* name, NET::Properties properties, NET::WindowTypes types, + NET::States states, NET::Properties2 properties2, NET::Actions actions, int scr = -1); + static RootInfo *s_self; + friend RootInfo *rootInfo(); + + xcb_window_t m_activeWindow; + std::unique_ptr m_eventFilter; +}; + +inline RootInfo *rootInfo() +{ + return RootInfo::s_self; +} + +/** + * NET WM Protocol handler class + */ +class WinInfo : public NETWinInfo +{ +public: + WinInfo(X11Client *c, xcb_window_t window, + xcb_window_t rwin, NET::Properties properties, NET::Properties2 properties2); + void changeDesktop(int desktop) override; + void changeFullscreenMonitors(NETFullscreenMonitors topology) override; + void changeState(NET::States state, NET::States mask) override; + void disable(); + +private: + X11Client *m_client; +}; + +} // KWin + +#endif diff --git a/onscreennotification.cpp b/onscreennotification.cpp new file mode 100644 index 0000000..cc7bdcd --- /dev/null +++ b/onscreennotification.cpp @@ -0,0 +1,233 @@ +/* + SPDX-FileCopyrightText: 2016 Martin Graesslin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +*/ + +#include "onscreennotification.h" +#include "input.h" +#include "input_event.h" +#include "input_event_spy.h" +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +namespace KWin +{ + +class OnScreenNotificationInputEventSpy : public InputEventSpy +{ +public: + explicit OnScreenNotificationInputEventSpy(OnScreenNotification *parent); + + void pointerEvent(MouseEvent *event) override; +private: + OnScreenNotification *m_parent; +}; + +OnScreenNotificationInputEventSpy::OnScreenNotificationInputEventSpy(OnScreenNotification *parent) + : m_parent(parent) +{ +} + +void OnScreenNotificationInputEventSpy::pointerEvent(MouseEvent *event) +{ + if (event->type() != QEvent::MouseMove) { + return; + } + + m_parent->setContainsPointer(m_parent->geometry().contains(event->globalPos())); +} + + +OnScreenNotification::OnScreenNotification(QObject *parent) + : QObject(parent) + , m_timer(new QTimer(this)) +{ + m_timer->setSingleShot(true); + connect(m_timer, &QTimer::timeout, this, std::bind(&OnScreenNotification::setVisible, this, false)); + connect(this, &OnScreenNotification::visibleChanged, this, + [this] { + if (m_visible) { + show(); + } else { + m_timer->stop(); + m_spy.reset(); + m_containsPointer = false; + } + } + ); +} + +OnScreenNotification::~OnScreenNotification() +{ + if (QQuickWindow *w = qobject_cast(m_mainItem.data())) { + w->hide(); + w->destroy(); + } +} + +void OnScreenNotification::setConfig(KSharedConfigPtr config) +{ + m_config = config; +} + +void OnScreenNotification::setEngine(QQmlEngine *engine) +{ + m_qmlEngine = engine; +} + +bool OnScreenNotification::isVisible() const +{ + return m_visible; +} + +void OnScreenNotification::setVisible(bool visible) +{ + if (m_visible == visible) { + return; + } + m_visible = visible; + emit visibleChanged(); +} + +QString OnScreenNotification::message() const +{ + return m_message; +} + +void OnScreenNotification::setMessage(const QString &message) +{ + if (m_message == message) { + return; + } + m_message = message; + emit messageChanged(); +} + +QString OnScreenNotification::iconName() const +{ + return m_iconName; +} + +void OnScreenNotification::setIconName(const QString &iconName) +{ + if (m_iconName == iconName) { + return; + } + m_iconName = iconName; + emit iconNameChanged(); +} + +int OnScreenNotification::timeout() const +{ + return m_timer->interval(); +} + +void OnScreenNotification::setTimeout(int timeout) +{ + if (m_timer->interval() == timeout) { + return; + } + m_timer->setInterval(timeout); + emit timeoutChanged(); +} + +void OnScreenNotification::show() +{ + Q_ASSERT(m_visible); + ensureQmlContext(); + ensureQmlComponent(); + createInputSpy(); + if (m_timer->interval() != 0) { + m_timer->start(); + } +} + +void OnScreenNotification::ensureQmlContext() +{ + Q_ASSERT(m_qmlEngine); + if (!m_qmlContext.isNull()) { + return; + } + m_qmlContext.reset(new QQmlContext(m_qmlEngine)); + m_qmlContext->setContextProperty(QStringLiteral("osd"), this); +} + +void OnScreenNotification::ensureQmlComponent() +{ + Q_ASSERT(m_config); + Q_ASSERT(m_qmlEngine); + if (!m_qmlComponent.isNull()) { + return; + } + m_qmlComponent.reset(new QQmlComponent(m_qmlEngine)); + const QString fileName = QStandardPaths::locate(QStandardPaths::GenericDataLocation, + m_config->group(QStringLiteral("OnScreenNotification")).readEntry("QmlPath", QStringLiteral(KWIN_NAME "/onscreennotification/plasma/main.qml"))); + if (fileName.isEmpty()) { + return; + } + m_qmlComponent->loadUrl(QUrl::fromLocalFile(fileName)); + if (!m_qmlComponent->isError()) { + m_mainItem.reset(m_qmlComponent->create(m_qmlContext.data())); + } else { + m_qmlComponent.reset(); + } +} + +void OnScreenNotification::createInputSpy() +{ + Q_ASSERT(m_spy.isNull()); + if (auto w = qobject_cast(m_mainItem.data())) { + m_spy.reset(new OnScreenNotificationInputEventSpy(this)); + input()->installInputEventSpy(m_spy.data()); + if (!m_animation) { + m_animation = new QPropertyAnimation(w, "opacity", this); + m_animation->setStartValue(1.0); + m_animation->setEndValue(0.0); + m_animation->setDuration(250); + m_animation->setEasingCurve(QEasingCurve::InOutCubic); + } + } +} + +QRect OnScreenNotification::geometry() const +{ + if (QQuickWindow *w = qobject_cast(m_mainItem.data())) { + return w->geometry(); + } + return QRect(); +} + +void OnScreenNotification::setContainsPointer(bool contains) +{ + if (m_containsPointer == contains) { + return; + } + m_containsPointer = contains; + if (!m_animation) { + return; + } + m_animation->setDirection(m_containsPointer ? QAbstractAnimation::Forward : QAbstractAnimation::Backward); + m_animation->start(); +} + +void OnScreenNotification::setSkipCloseAnimation(bool skip) +{ + if (QQuickWindow *w = qobject_cast(m_mainItem.data())) { + w->setProperty("KWIN_SKIP_CLOSE_ANIMATION", skip); + } +} + +} // namespace KWin diff --git a/onscreennotification.h b/onscreennotification.h new file mode 100644 index 0000000..6c5d853 --- /dev/null +++ b/onscreennotification.h @@ -0,0 +1,80 @@ +/* + SPDX-FileCopyrightText: 2016 Martin Graesslin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +*/ + +#ifndef KWIN_ONSCREENNOTIFICATION_H +#define KWIN_ONSCREENNOTIFICATION_H + +#include + +#include + +class QPropertyAnimation; +class QTimer; +class QQmlContext; +class QQmlComponent; +class QQmlEngine; + +namespace KWin { + +class OnScreenNotificationInputEventSpy; + +class OnScreenNotification : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool visible READ isVisible WRITE setVisible NOTIFY visibleChanged) + Q_PROPERTY(QString message READ message WRITE setMessage NOTIFY messageChanged) + Q_PROPERTY(QString iconName READ iconName WRITE setIconName NOTIFY iconNameChanged) + Q_PROPERTY(int timeout READ timeout WRITE setTimeout NOTIFY timeoutChanged) + +public: + explicit OnScreenNotification(QObject *parent = nullptr); + ~OnScreenNotification() override; + bool isVisible() const; + QString message() const; + QString iconName() const; + int timeout() const; + + QRect geometry() const; + + void setVisible(bool m_visible); + void setMessage(const QString &message); + void setIconName(const QString &iconName); + void setTimeout(int timeout); + + void setConfig(KSharedConfigPtr config); + void setEngine(QQmlEngine *engine); + + void setContainsPointer(bool contains); + void setSkipCloseAnimation(bool skip); + +Q_SIGNALS: + void visibleChanged(); + void messageChanged(); + void iconNameChanged(); + void timeoutChanged(); + +private: + void show(); + void ensureQmlContext(); + void ensureQmlComponent(); + void createInputSpy(); + bool m_visible = false; + QString m_message; + QString m_iconName; + QTimer *m_timer; + KSharedConfigPtr m_config; + QScopedPointer m_qmlContext; + QScopedPointer m_qmlComponent; + QQmlEngine *m_qmlEngine = nullptr; + QScopedPointer m_mainItem; + QScopedPointer m_spy; + QPropertyAnimation *m_animation = nullptr; + bool m_containsPointer = false; +}; +} + +#endif // KWIN_ONSCREENNOTIFICATION_H diff --git a/options.cpp b/options.cpp new file mode 100644 index 0000000..4bce7ee --- /dev/null +++ b/options.cpp @@ -0,0 +1,1103 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "options.h" +#include "config-kwin.h" +#include "utils.h" +#include "platform.h" + +#ifndef KCMRULES + +#include + +#include "screens.h" +#include "settings.h" +#include +#include + +#endif //KCMRULES + +namespace KWin +{ + +#ifndef KCMRULES + +int currentRefreshRate() +{ + return Options::currentRefreshRate(); +} + +int Options::currentRefreshRate() +{ + int rate = -1; + QString syncScreenName(QLatin1String("primary screen")); + if (options->refreshRate() > 0) { // use manually configured refresh rate + rate = options->refreshRate(); + } else if (Screens::self()->count() > 0) { + // prefer the refreshrate calculated from the screens mode information + // at least the nvidia driver reports 50Hz BS ... *again*! + int syncScreen = 0; + if (Screens::self()->count() > 1) { + const QByteArray syncDisplayDevice(qgetenv("__GL_SYNC_DISPLAY_DEVICE")); + // if __GL_SYNC_DISPLAY_DEVICE is exported, the GPU shall sync to that device + // so we try to use its refresh rate + if (!syncDisplayDevice.isEmpty()) { + for (int i = 0; i < Screens::self()->count(); ++i) { + if (Screens::self()->name(i) == syncDisplayDevice) { + syncScreenName = Screens::self()->name(i); + syncScreen = i; + break; + } + } + } + } + rate = qRound(Screens::self()->refreshRate(syncScreen)); // TODO forward float precision? + } + + // 0Hz or less is invalid, so we fallback to a default rate + if (rate <= 0) + rate = 60; // and not shitty 50Hz for sure! *grrr* + + // QTimer gives us 1msec (1000Hz) at best, so we ignore anything higher; + // however, additional throttling prevents very high rates from taking place anyway + else if (rate > 1000) + rate = 1000; + qCDebug(KWIN_CORE) << "Vertical Refresh rate " << rate << "Hz (" << syncScreenName << ")"; + return rate; +} + +Options::Options(QObject *parent) + : QObject(parent) + , m_settings(new Settings(kwinApp()->config())) + , m_focusPolicy(ClickToFocus) + , m_nextFocusPrefersMouse(false) + , m_clickRaise(false) + , m_autoRaise(false) + , m_autoRaiseInterval(0) + , m_delayFocusInterval(0) + , m_shadeHover(false) + , m_shadeHoverInterval(0) + , m_separateScreenFocus(false) + , m_placement(Placement::NoPlacement) + , m_borderSnapZone(0) + , m_windowSnapZone(0) + , m_centerSnapZone(0) + , m_snapOnlyWhenOverlapping(false) + , m_rollOverDesktops(false) + , m_focusStealingPreventionLevel(0) + , m_killPingTimeout(0) + , m_hideUtilityWindowsForInactive(false) + , m_xwaylandCrashPolicy(Options::defaultXwaylandCrashPolicy()) + , m_xwaylandMaxCrashCount(Options::defaultXwaylandMaxCrashCount()) + , m_compositingMode(Options::defaultCompositingMode()) + , m_useCompositing(Options::defaultUseCompositing()) + , m_hiddenPreviews(Options::defaultHiddenPreviews()) + , m_glSmoothScale(Options::defaultGlSmoothScale()) + , m_xrenderSmoothScale(Options::defaultXrenderSmoothScale()) + , m_maxFpsInterval(Options::defaultMaxFpsInterval()) + , m_refreshRate(Options::defaultRefreshRate()) + , m_vBlankTime(Options::defaultVBlankTime()) + , m_glStrictBinding(Options::defaultGlStrictBinding()) + , m_glStrictBindingFollowsDriver(Options::defaultGlStrictBindingFollowsDriver()) + , m_glCoreProfile(Options::defaultGLCoreProfile()) + , m_glPreferBufferSwap(Options::defaultGlPreferBufferSwap()) + , m_glPlatformInterface(Options::defaultGlPlatformInterface()) + , m_windowsBlockCompositing(true) + , OpTitlebarDblClick(Options::defaultOperationTitlebarDblClick()) + , CmdActiveTitlebar1(Options::defaultCommandActiveTitlebar1()) + , CmdActiveTitlebar2(Options::defaultCommandActiveTitlebar2()) + , CmdActiveTitlebar3(Options::defaultCommandActiveTitlebar3()) + , CmdInactiveTitlebar1(Options::defaultCommandInactiveTitlebar1()) + , CmdInactiveTitlebar2(Options::defaultCommandInactiveTitlebar2()) + , CmdInactiveTitlebar3(Options::defaultCommandInactiveTitlebar3()) + , CmdTitlebarWheel(Options::defaultCommandTitlebarWheel()) + , CmdWindow1(Options::defaultCommandWindow1()) + , CmdWindow2(Options::defaultCommandWindow2()) + , CmdWindow3(Options::defaultCommandWindow3()) + , CmdWindowWheel(Options::defaultCommandWindowWheel()) + , CmdAll1(Options::defaultCommandAll1()) + , CmdAll2(Options::defaultCommandAll2()) + , CmdAll3(Options::defaultCommandAll3()) + , CmdAllWheel(Options::defaultCommandAllWheel()) + , CmdAllModKey(Options::defaultKeyCmdAllModKey()) + , electric_border_maximize(false) + , electric_border_tiling(false) + , electric_border_corner_ratio(0.0) + , borderless_maximized_windows(false) + , show_geometry_tip(false) + , condensed_title(false) +{ + m_settings->setDefaults(); + syncFromKcfgc(); + + m_configWatcher = KConfigWatcher::create(m_settings->sharedConfig()); + connect(m_configWatcher.data(), &KConfigWatcher::configChanged, this, [this](const KConfigGroup &group, const QByteArrayList &names) { + if (group.name() == QLatin1String("KDE") && names.contains(QByteArrayLiteral("AnimationDurationFactor"))) { + emit animationSpeedChanged(); + } + }); +} + +Options::~Options() +{ +} + +void Options::setFocusPolicy(FocusPolicy focusPolicy) +{ + if (m_focusPolicy == focusPolicy) { + return; + } + m_focusPolicy = focusPolicy; + emit focusPolicyChanged(); + if (m_focusPolicy == ClickToFocus) { + setAutoRaise(false); + setAutoRaiseInterval(0); + setDelayFocusInterval(0); + } +} + +void Options::setNextFocusPrefersMouse(bool nextFocusPrefersMouse) +{ + if (m_nextFocusPrefersMouse == nextFocusPrefersMouse) { + return; + } + m_nextFocusPrefersMouse = nextFocusPrefersMouse; + emit nextFocusPrefersMouseChanged(); +} + +void Options::setXwaylandCrashPolicy(XwaylandCrashPolicy crashPolicy) +{ + if (m_xwaylandCrashPolicy == crashPolicy) { + return; + } + m_xwaylandCrashPolicy = crashPolicy; + emit xwaylandCrashPolicyChanged(); +} + +void Options::setXwaylandMaxCrashCount(int maxCrashCount) +{ + if (m_xwaylandMaxCrashCount == maxCrashCount) { + return; + } + m_xwaylandMaxCrashCount = maxCrashCount; + emit xwaylandMaxCrashCountChanged(); +} + +void Options::setClickRaise(bool clickRaise) +{ + if (m_autoRaise) { + // important: autoRaise implies ClickRaise + clickRaise = true; + } + if (m_clickRaise == clickRaise) { + return; + } + m_clickRaise = clickRaise; + emit clickRaiseChanged(); +} + +void Options::setAutoRaise(bool autoRaise) +{ + if (m_focusPolicy == ClickToFocus) { + autoRaise = false; + } + if (m_autoRaise == autoRaise) { + return; + } + m_autoRaise = autoRaise; + if (m_autoRaise) { + // important: autoRaise implies ClickRaise + setClickRaise(true); + } + emit autoRaiseChanged(); +} + +void Options::setAutoRaiseInterval(int autoRaiseInterval) +{ + if (m_focusPolicy == ClickToFocus) { + autoRaiseInterval = 0; + } + if (m_autoRaiseInterval == autoRaiseInterval) { + return; + } + m_autoRaiseInterval = autoRaiseInterval; + emit autoRaiseIntervalChanged(); +} + +void Options::setDelayFocusInterval(int delayFocusInterval) +{ + if (m_focusPolicy == ClickToFocus) { + delayFocusInterval = 0; + } + if (m_delayFocusInterval == delayFocusInterval) { + return; + } + m_delayFocusInterval = delayFocusInterval; + emit delayFocusIntervalChanged(); +} + +void Options::setShadeHover(bool shadeHover) +{ + if (m_shadeHover == shadeHover) { + return; + } + m_shadeHover = shadeHover; + emit shadeHoverChanged(); +} + +void Options::setShadeHoverInterval(int shadeHoverInterval) +{ + if (m_shadeHoverInterval == shadeHoverInterval) { + return; + } + m_shadeHoverInterval = shadeHoverInterval; + emit shadeHoverIntervalChanged(); +} + +void Options::setSeparateScreenFocus(bool separateScreenFocus) +{ + if (m_separateScreenFocus == separateScreenFocus) { + return; + } + m_separateScreenFocus = separateScreenFocus; + emit separateScreenFocusChanged(m_separateScreenFocus); +} + +void Options::setPlacement(int placement) +{ + if (m_placement == static_cast(placement)) { + return; + } + m_placement = static_cast(placement); + emit placementChanged(); +} + +void Options::setBorderSnapZone(int borderSnapZone) +{ + if (m_borderSnapZone == borderSnapZone) { + return; + } + m_borderSnapZone = borderSnapZone; + emit borderSnapZoneChanged(); +} + +void Options::setWindowSnapZone(int windowSnapZone) +{ + if (m_windowSnapZone == windowSnapZone) { + return; + } + m_windowSnapZone = windowSnapZone; + emit windowSnapZoneChanged(); +} + +void Options::setCenterSnapZone(int centerSnapZone) +{ + if (m_centerSnapZone == centerSnapZone) { + return; + } + m_centerSnapZone = centerSnapZone; + emit centerSnapZoneChanged(); +} + +void Options::setSnapOnlyWhenOverlapping(bool snapOnlyWhenOverlapping) +{ + if (m_snapOnlyWhenOverlapping == snapOnlyWhenOverlapping) { + return; + } + m_snapOnlyWhenOverlapping = snapOnlyWhenOverlapping; + emit snapOnlyWhenOverlappingChanged(); +} + +void Options::setRollOverDesktops(bool rollOverDesktops) +{ + if (m_rollOverDesktops == rollOverDesktops) { + return; + } + m_rollOverDesktops = rollOverDesktops; + emit rollOverDesktopsChanged(m_rollOverDesktops); +} + +void Options::setFocusStealingPreventionLevel(int focusStealingPreventionLevel) +{ + if (!focusPolicyIsReasonable()) { + focusStealingPreventionLevel = 0; + } + if (m_focusStealingPreventionLevel == focusStealingPreventionLevel) { + return; + } + m_focusStealingPreventionLevel = qMax(0, qMin(4, focusStealingPreventionLevel)); + emit focusStealingPreventionLevelChanged(); +} + +void Options::setOperationTitlebarDblClick(WindowOperation operationTitlebarDblClick) +{ + if (OpTitlebarDblClick == operationTitlebarDblClick) { + return; + } + OpTitlebarDblClick = operationTitlebarDblClick; + emit operationTitlebarDblClickChanged(); +} + +void Options::setOperationMaxButtonLeftClick(WindowOperation op) +{ + if (opMaxButtonLeftClick == op) { + return; + } + opMaxButtonLeftClick = op; + emit operationMaxButtonLeftClickChanged(); +} + +void Options::setOperationMaxButtonRightClick(WindowOperation op) +{ + if (opMaxButtonRightClick == op) { + return; + } + opMaxButtonRightClick = op; + emit operationMaxButtonRightClickChanged(); +} + +void Options::setOperationMaxButtonMiddleClick(WindowOperation op) +{ + if (opMaxButtonMiddleClick == op) { + return; + } + opMaxButtonMiddleClick = op; + emit operationMaxButtonMiddleClickChanged(); +} + +void Options::setCommandActiveTitlebar1(MouseCommand commandActiveTitlebar1) +{ + if (CmdActiveTitlebar1 == commandActiveTitlebar1) { + return; + } + CmdActiveTitlebar1 = commandActiveTitlebar1; + emit commandActiveTitlebar1Changed(); +} + +void Options::setCommandActiveTitlebar2(MouseCommand commandActiveTitlebar2) +{ + if (CmdActiveTitlebar2 == commandActiveTitlebar2) { + return; + } + CmdActiveTitlebar2 = commandActiveTitlebar2; + emit commandActiveTitlebar2Changed(); +} + +void Options::setCommandActiveTitlebar3(MouseCommand commandActiveTitlebar3) +{ + if (CmdActiveTitlebar3 == commandActiveTitlebar3) { + return; + } + CmdActiveTitlebar3 = commandActiveTitlebar3; + emit commandActiveTitlebar3Changed(); +} + +void Options::setCommandInactiveTitlebar1(MouseCommand commandInactiveTitlebar1) +{ + if (CmdInactiveTitlebar1 == commandInactiveTitlebar1) { + return; + } + CmdInactiveTitlebar1 = commandInactiveTitlebar1; + emit commandInactiveTitlebar1Changed(); +} + +void Options::setCommandInactiveTitlebar2(MouseCommand commandInactiveTitlebar2) +{ + if (CmdInactiveTitlebar2 == commandInactiveTitlebar2) { + return; + } + CmdInactiveTitlebar2 = commandInactiveTitlebar2; + emit commandInactiveTitlebar2Changed(); +} + +void Options::setCommandInactiveTitlebar3(MouseCommand commandInactiveTitlebar3) +{ + if (CmdInactiveTitlebar3 == commandInactiveTitlebar3) { + return; + } + CmdInactiveTitlebar3 = commandInactiveTitlebar3; + emit commandInactiveTitlebar3Changed(); +} + +void Options::setCommandWindow1(MouseCommand commandWindow1) +{ + if (CmdWindow1 == commandWindow1) { + return; + } + CmdWindow1 = commandWindow1; + emit commandWindow1Changed(); +} + +void Options::setCommandWindow2(MouseCommand commandWindow2) +{ + if (CmdWindow2 == commandWindow2) { + return; + } + CmdWindow2 = commandWindow2; + emit commandWindow2Changed(); +} + +void Options::setCommandWindow3(MouseCommand commandWindow3) +{ + if (CmdWindow3 == commandWindow3) { + return; + } + CmdWindow3 = commandWindow3; + emit commandWindow3Changed(); +} + +void Options::setCommandWindowWheel(MouseCommand commandWindowWheel) +{ + if (CmdWindowWheel == commandWindowWheel) { + return; + } + CmdWindowWheel = commandWindowWheel; + emit commandWindowWheelChanged(); +} + +void Options::setCommandAll1(MouseCommand commandAll1) +{ + if (CmdAll1 == commandAll1) { + return; + } + CmdAll1 = commandAll1; + emit commandAll1Changed(); +} + +void Options::setCommandAll2(MouseCommand commandAll2) +{ + if (CmdAll2 == commandAll2) { + return; + } + CmdAll2 = commandAll2; + emit commandAll2Changed(); +} + +void Options::setCommandAll3(MouseCommand commandAll3) +{ + if (CmdAll3 == commandAll3) { + return; + } + CmdAll3 = commandAll3; + emit commandAll3Changed(); +} + +void Options::setKeyCmdAllModKey(uint keyCmdAllModKey) +{ + if (CmdAllModKey == keyCmdAllModKey) { + return; + } + CmdAllModKey = keyCmdAllModKey; + emit keyCmdAllModKeyChanged(); +} + +void Options::setShowGeometryTip(bool showGeometryTip) +{ + if (show_geometry_tip == showGeometryTip) { + return; + } + show_geometry_tip = showGeometryTip; + emit showGeometryTipChanged(); +} + +void Options::setCondensedTitle(bool condensedTitle) +{ + if (condensed_title == condensedTitle) { + return; + } + condensed_title = condensedTitle; + emit condensedTitleChanged(); +} + +void Options::setElectricBorderMaximize(bool electricBorderMaximize) +{ + if (electric_border_maximize == electricBorderMaximize) { + return; + } + electric_border_maximize = electricBorderMaximize; + emit electricBorderMaximizeChanged(); +} + +void Options::setElectricBorderTiling(bool electricBorderTiling) +{ + if (electric_border_tiling == electricBorderTiling) { + return; + } + electric_border_tiling = electricBorderTiling; + emit electricBorderTilingChanged(); +} + +void Options::setElectricBorderCornerRatio(float electricBorderCornerRatio) +{ + if (electric_border_corner_ratio == electricBorderCornerRatio) { + return; + } + electric_border_corner_ratio = electricBorderCornerRatio; + emit electricBorderCornerRatioChanged(); +} + +void Options::setBorderlessMaximizedWindows(bool borderlessMaximizedWindows) +{ + if (borderless_maximized_windows == borderlessMaximizedWindows) { + return; + } + borderless_maximized_windows = borderlessMaximizedWindows; + emit borderlessMaximizedWindowsChanged(); +} + +void Options::setKillPingTimeout(int killPingTimeout) +{ + if (m_killPingTimeout == killPingTimeout) { + return; + } + m_killPingTimeout = killPingTimeout; + emit killPingTimeoutChanged(); +} + +void Options::setHideUtilityWindowsForInactive(bool hideUtilityWindowsForInactive) +{ + if (m_hideUtilityWindowsForInactive == hideUtilityWindowsForInactive) { + return; + } + m_hideUtilityWindowsForInactive = hideUtilityWindowsForInactive; + emit hideUtilityWindowsForInactiveChanged(); +} + +void Options::setCompositingMode(int compositingMode) +{ + if (m_compositingMode == static_cast(compositingMode)) { + return; + } + m_compositingMode = static_cast(compositingMode); + emit compositingModeChanged(); +} + +void Options::setUseCompositing(bool useCompositing) +{ + if (m_useCompositing == useCompositing) { + return; + } + m_useCompositing = useCompositing; + emit useCompositingChanged(); +} + +void Options::setHiddenPreviews(int hiddenPreviews) +{ + if (m_hiddenPreviews == static_cast(hiddenPreviews)) { + return; + } + m_hiddenPreviews = static_cast(hiddenPreviews); + emit hiddenPreviewsChanged(); +} + +void Options::setGlSmoothScale(int glSmoothScale) +{ + if (m_glSmoothScale == glSmoothScale) { + return; + } + m_glSmoothScale = glSmoothScale; + emit glSmoothScaleChanged(); +} + +void Options::setXrenderSmoothScale(bool xrenderSmoothScale) +{ + if (m_xrenderSmoothScale == xrenderSmoothScale) { + return; + } + m_xrenderSmoothScale = xrenderSmoothScale; + emit xrenderSmoothScaleChanged(); +} + +void Options::setMaxFpsInterval(qint64 maxFpsInterval) +{ + if (m_maxFpsInterval == maxFpsInterval) { + return; + } + m_maxFpsInterval = maxFpsInterval; + emit maxFpsIntervalChanged(); +} + +void Options::setRefreshRate(uint refreshRate) +{ + if (m_refreshRate == refreshRate) { + return; + } + m_refreshRate = refreshRate; + emit refreshRateChanged(); +} + +void Options::setVBlankTime(qint64 vBlankTime) +{ + if (m_vBlankTime == vBlankTime) { + return; + } + m_vBlankTime = vBlankTime; + emit vBlankTimeChanged(); +} + +void Options::setGlStrictBinding(bool glStrictBinding) +{ + if (m_glStrictBinding == glStrictBinding) { + return; + } + m_glStrictBinding = glStrictBinding; + emit glStrictBindingChanged(); +} + +void Options::setGlStrictBindingFollowsDriver(bool glStrictBindingFollowsDriver) +{ + if (m_glStrictBindingFollowsDriver == glStrictBindingFollowsDriver) { + return; + } + m_glStrictBindingFollowsDriver = glStrictBindingFollowsDriver; + emit glStrictBindingFollowsDriverChanged(); +} + +void Options::setGLCoreProfile(bool value) +{ + if (m_glCoreProfile == value) { + return; + } + m_glCoreProfile = value; + emit glCoreProfileChanged(); +} + +void Options::setWindowsBlockCompositing(bool value) +{ + if (m_windowsBlockCompositing == value) { + return; + } + m_windowsBlockCompositing = value; + emit windowsBlockCompositingChanged(); +} + +void Options::setGlPreferBufferSwap(char glPreferBufferSwap) +{ + if (glPreferBufferSwap == 'a') { + // buffer copying is very fast with the nvidia blob + // but due to restrictions in DRI2 *incredibly* slow for all MESA drivers + // see https://www.x.org/releases/X11R7.7/doc/dri2proto/dri2proto.txt, item 2.5 + if (GLPlatform::instance()->driver() == Driver_NVidia) + glPreferBufferSwap = CopyFrontBuffer; + else if (GLPlatform::instance()->driver() != Driver_Unknown) // undetected, finally resolved when context is initialized + glPreferBufferSwap = ExtendDamage; + } + if (m_glPreferBufferSwap == (GlSwapStrategy)glPreferBufferSwap) { + return; + } + m_glPreferBufferSwap = (GlSwapStrategy)glPreferBufferSwap; + emit glPreferBufferSwapChanged(); +} + +void Options::setGlPlatformInterface(OpenGLPlatformInterface interface) +{ + // check environment variable + const QByteArray envOpenGLInterface(qgetenv("KWIN_OPENGL_INTERFACE")); + if (!envOpenGLInterface.isEmpty()) { + if (qstrcmp(envOpenGLInterface, "egl") == 0) { + qCDebug(KWIN_CORE) << "Forcing EGL native interface through environment variable"; + interface = EglPlatformInterface; + } else if (qstrcmp(envOpenGLInterface, "glx") == 0) { + qCDebug(KWIN_CORE) << "Forcing GLX native interface through environment variable"; + interface = GlxPlatformInterface; + } + } + if (kwinApp()->shouldUseWaylandForCompositing() && interface == GlxPlatformInterface) { + // Glx is impossible on Wayland, enforce egl + qCDebug(KWIN_CORE) << "Forcing EGL native interface for Wayland mode"; + interface = EglPlatformInterface; + } +#if !HAVE_EPOXY_GLX + qCDebug(KWIN_CORE) << "Forcing EGL native interface as compiled without GLX support"; + interface = EglPlatformInterface; +#endif + if (QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGLES) { + qCDebug(KWIN_CORE) << "Forcing EGL native interface as Qt uses OpenGL ES"; + interface = EglPlatformInterface; + } else if (qstrcmp(qgetenv("KWIN_COMPOSE"), "O2ES") == 0) { + qCDebug(KWIN_CORE) << "Forcing EGL native interface as OpenGL ES requested through KWIN_COMPOSE environment variable."; + interface = EglPlatformInterface; + } + + if (m_glPlatformInterface == interface) { + return; + } + m_glPlatformInterface = interface; + emit glPlatformInterfaceChanged(); +} + +void Options::reparseConfiguration() +{ + m_settings->config()->reparseConfiguration(); +} + +void Options::updateSettings() +{ + loadConfig(); + // Read button tooltip animation effect from kdeglobals + // Since we want to allow users to enable window decoration tooltips + // and not kstyle tooltips and vise-versa, we don't read the + // "EffectNoTooltip" setting from kdeglobals. + + +// QToolTip::setGloballyEnabled( d->show_tooltips ); +// KDE4 this probably needs to be done manually in clients + + // Driver-specific config detection + reloadCompositingSettings(); + + emit configChanged(); +} + +void Options::loadConfig() +{ + m_settings->load(); + + syncFromKcfgc(); + + // Electric borders + KConfigGroup config(m_settings->config(), "Windows"); + OpTitlebarDblClick = windowOperation(config.readEntry("TitlebarDoubleClickCommand", "Maximize"), true); + setOperationMaxButtonLeftClick(windowOperation(config.readEntry("MaximizeButtonLeftClickCommand", "Maximize"), true)); + setOperationMaxButtonMiddleClick(windowOperation(config.readEntry("MaximizeButtonMiddleClickCommand", "Maximize (vertical only)"), true)); + setOperationMaxButtonRightClick(windowOperation(config.readEntry("MaximizeButtonRightClickCommand", "Maximize (horizontal only)"), true)); + + // Mouse bindings + config = KConfigGroup(m_settings->config(), "MouseBindings"); + // TODO: add properties for missing options + CmdTitlebarWheel = mouseWheelCommand(config.readEntry("CommandTitlebarWheel", "Nothing")); + CmdAllModKey = (config.readEntry("CommandAllKey", "Meta") == QStringLiteral("Meta")) ? Qt::Key_Meta : Qt::Key_Alt; + CmdAllWheel = mouseWheelCommand(config.readEntry("CommandAllWheel", "Nothing")); + setCommandActiveTitlebar1(mouseCommand(config.readEntry("CommandActiveTitlebar1", "Raise"), true)); + setCommandActiveTitlebar2(mouseCommand(config.readEntry("CommandActiveTitlebar2", "Nothing"), true)); + setCommandActiveTitlebar3(mouseCommand(config.readEntry("CommandActiveTitlebar3", "Operations menu"), true)); + setCommandInactiveTitlebar1(mouseCommand(config.readEntry("CommandInactiveTitlebar1", "Activate and raise"), true)); + setCommandInactiveTitlebar2(mouseCommand(config.readEntry("CommandInactiveTitlebar2", "Nothing"), true)); + setCommandInactiveTitlebar3(mouseCommand(config.readEntry("CommandInactiveTitlebar3", "Operations menu"), true)); + setCommandWindow1(mouseCommand(config.readEntry("CommandWindow1", "Activate, raise and pass click"), false)); + setCommandWindow2(mouseCommand(config.readEntry("CommandWindow2", "Activate and pass click"), false)); + setCommandWindow3(mouseCommand(config.readEntry("CommandWindow3", "Activate and pass click"), false)); + setCommandWindowWheel(mouseCommand(config.readEntry("CommandWindowWheel", "Scroll"), false)); + setCommandAll1(mouseCommand(config.readEntry("CommandAll1", "Move"), false)); + setCommandAll2(mouseCommand(config.readEntry("CommandAll2", "Toggle raise and lower"), false)); + setCommandAll3(mouseCommand(config.readEntry("CommandAll3", "Resize"), false)); + + // TODO: should they be moved into reloadCompositingSettings? + config = KConfigGroup(m_settings->config(), "Compositing"); + setMaxFpsInterval(1 * 1000 * 1000 * 1000 / config.readEntry("MaxFPS", Options::defaultMaxFps())); + setRefreshRate(config.readEntry("RefreshRate", Options::defaultRefreshRate())); + setVBlankTime(config.readEntry("VBlankTime", Options::defaultVBlankTime()) * 1000); // config in micro, value in nano resolution + + // Modifier Only Shortcuts + config = KConfigGroup(m_settings->config(), "ModifierOnlyShortcuts"); + m_modifierOnlyShortcuts.clear(); + if (config.hasKey("Shift")) { + m_modifierOnlyShortcuts.insert(Qt::ShiftModifier, config.readEntry("Shift", QStringList())); + } + if (config.hasKey("Control")) { + m_modifierOnlyShortcuts.insert(Qt::ControlModifier, config.readEntry("Control", QStringList())); + } + if (config.hasKey("Alt")) { + m_modifierOnlyShortcuts.insert(Qt::AltModifier, config.readEntry("Alt", QStringList())); + } + m_modifierOnlyShortcuts.insert(Qt::MetaModifier, config.readEntry("Meta", QStringList{QStringLiteral("org.kde.plasmashell"), + QStringLiteral("/PlasmaShell"), + QStringLiteral("org.kde.PlasmaShell"), + QStringLiteral("activateLauncherMenu")})); +} + +void Options::syncFromKcfgc() +{ + setShowGeometryTip(m_settings->geometryTip()); + setCondensedTitle(m_settings->condensedTitle()); + setFocusPolicy(m_settings->focusPolicy()); + setNextFocusPrefersMouse(m_settings->nextFocusPrefersMouse()); + setSeparateScreenFocus(m_settings->separateScreenFocus()); + setRollOverDesktops(m_settings->rollOverDesktops()); + setFocusStealingPreventionLevel(m_settings->focusStealingPreventionLevel()); + setXwaylandCrashPolicy(m_settings->xwaylandCrashPolicy()); + setXwaylandMaxCrashCount(m_settings->xwaylandMaxCrashCount()); + +#ifdef KWIN_BUILD_DECORATIONS + setPlacement(m_settings->placement()); +#else + setPlacement(Placement::Maximizing); +#endif + + setAutoRaise(m_settings->autoRaise()); + setAutoRaiseInterval(m_settings->autoRaiseInterval()); + setDelayFocusInterval(m_settings->delayFocusInterval()); + setShadeHover(m_settings->shadeHover()); + setShadeHoverInterval(m_settings->shadeHoverInterval()); + setClickRaise(m_settings->clickRaise()); + setBorderSnapZone(m_settings->borderSnapZone()); + setWindowSnapZone(m_settings->windowSnapZone()); + setCenterSnapZone(m_settings->centerSnapZone()); + setSnapOnlyWhenOverlapping(m_settings->snapOnlyWhenOverlapping()); + setKillPingTimeout(m_settings->killPingTimeout()); + setHideUtilityWindowsForInactive(m_settings->hideUtilityWindowsForInactive()); + setBorderlessMaximizedWindows(m_settings->borderlessMaximizedWindows()); + setElectricBorderMaximize(m_settings->electricBorderMaximize()); + setElectricBorderTiling(m_settings->electricBorderTiling()); + setElectricBorderCornerRatio(m_settings->electricBorderCornerRatio()); + setWindowsBlockCompositing(m_settings->windowsBlockCompositing()); + +} + +bool Options::loadCompositingConfig (bool force) +{ + KConfigGroup config(m_settings->config(), "Compositing"); + + bool useCompositing = false; + CompositingType compositingMode = NoCompositing; + QString compositingBackend = config.readEntry("Backend", "OpenGL"); + if (compositingBackend == QStringLiteral("XRender")) + compositingMode = XRenderCompositing; + else if (compositingBackend == "QPainter") + compositingMode = QPainterCompositing; + else + compositingMode = OpenGLCompositing; + + if (const char *c = getenv("KWIN_COMPOSE")) { + switch(c[0]) { + case 'O': + qCDebug(KWIN_CORE) << "Compositing forced to OpenGL mode by environment variable"; + compositingMode = OpenGLCompositing; + useCompositing = true; + break; + case 'X': + qCDebug(KWIN_CORE) << "Compositing forced to XRender mode by environment variable"; + compositingMode = XRenderCompositing; + useCompositing = true; + break; + case 'Q': + qCDebug(KWIN_CORE) << "Compositing forced to QPainter mode by environment variable"; + compositingMode = QPainterCompositing; + useCompositing = true; + break; + case 'N': + if (getenv("KDE_FAILSAFE")) + qCDebug(KWIN_CORE) << "Compositing disabled forcefully by KDE failsafe mode"; + else + qCDebug(KWIN_CORE) << "Compositing disabled forcefully by environment variable"; + compositingMode = NoCompositing; + break; + default: + qCDebug(KWIN_CORE) << "Unknown KWIN_COMPOSE mode set, ignoring"; + break; + } + } + setCompositingMode(compositingMode); + + const bool platformSupportsNoCompositing = kwinApp()->platform()->supportedCompositors().contains(NoCompositing); + if (m_compositingMode == NoCompositing && platformSupportsNoCompositing) { + setUseCompositing(false); + return false; // do not even detect compositing preferences if explicitly disabled + } + + // it's either enforced by env or by initial resume from "suspend" or we check the settings + setUseCompositing(useCompositing || force || config.readEntry("Enabled", Options::defaultUseCompositing() || !platformSupportsNoCompositing)); + + if (!m_useCompositing) + return false; // not enforced or necessary and not "enabled" by settings + return true; +} + +void Options::reloadCompositingSettings(bool force) +{ + if (!loadCompositingConfig(force)) { + return; + } + m_settings->load(); + syncFromKcfgc(); + + // Compositing settings + KConfigGroup config(m_settings->config(), "Compositing"); + + setGlSmoothScale(qBound(-1, config.readEntry("GLTextureFilter", Options::defaultGlSmoothScale()), 2)); + setGlStrictBindingFollowsDriver(!config.hasKey("GLStrictBinding")); + if (!isGlStrictBindingFollowsDriver()) { + setGlStrictBinding(config.readEntry("GLStrictBinding", Options::defaultGlStrictBinding())); + } + setGLCoreProfile(config.readEntry("GLCore", Options::defaultGLCoreProfile())); + + char c = 0; + const QString s = config.readEntry("GLPreferBufferSwap", QString(Options::defaultGlPreferBufferSwap())); + if (!s.isEmpty()) + c = s.at(0).toLatin1(); + if (c != 'a' && c != 'c' && c != 'p' && c != 'e') + c = 0; + setGlPreferBufferSwap(c); + + m_xrenderSmoothScale = config.readEntry("XRenderSmoothScale", false); + + HiddenPreviews previews = Options::defaultHiddenPreviews(); + // 4 - off, 5 - shown, 6 - always, other are old values + int hps = config.readEntry("HiddenPreviews", 5); + if (hps == 4) + previews = HiddenPreviewsNever; + else if (hps == 5) + previews = HiddenPreviewsShown; + else if (hps == 6) + previews = HiddenPreviewsAlways; + setHiddenPreviews(previews); + + auto interfaceToKey = [](OpenGLPlatformInterface interface) { + switch (interface) { + case GlxPlatformInterface: + return QStringLiteral("glx"); + case EglPlatformInterface: + return QStringLiteral("egl"); + default: + return QString(); + } + }; + auto keyToInterface = [](const QString &key) { + if (key == QStringLiteral("glx")) { + return GlxPlatformInterface; + } else if (key == QStringLiteral("egl")) { + return EglPlatformInterface; + } + return defaultGlPlatformInterface(); + }; + setGlPlatformInterface(keyToInterface(config.readEntry("GLPlatformInterface", interfaceToKey(m_glPlatformInterface)))); +} + +// restricted should be true for operations that the user may not be able to repeat +// if the window is moved out of the workspace (e.g. if the user moves a window +// by the titlebar, and moves it too high beneath Kicker at the top edge, they +// may not be able to move it back, unless they know about Meta+LMB) +Options::WindowOperation Options::windowOperation(const QString &name, bool restricted) +{ + if (name == QStringLiteral("Move")) + return restricted ? MoveOp : UnrestrictedMoveOp; + else if (name == QStringLiteral("Resize")) + return restricted ? ResizeOp : UnrestrictedResizeOp; + else if (name == QStringLiteral("Maximize")) + return MaximizeOp; + else if (name == QStringLiteral("Minimize")) + return MinimizeOp; + else if (name == QStringLiteral("Close")) + return CloseOp; + else if (name == QStringLiteral("OnAllDesktops")) + return OnAllDesktopsOp; + else if (name == QStringLiteral("Shade")) + return ShadeOp; + else if (name == QStringLiteral("Operations")) + return OperationsOp; + else if (name == QStringLiteral("Maximize (vertical only)")) + return VMaximizeOp; + else if (name == QStringLiteral("Maximize (horizontal only)")) + return HMaximizeOp; + else if (name == QStringLiteral("Lower")) + return LowerOp; + return NoOp; +} + +Options::MouseCommand Options::mouseCommand(const QString &name, bool restricted) +{ + QString lowerName = name.toLower(); + if (lowerName == QStringLiteral("raise")) return MouseRaise; + if (lowerName == QStringLiteral("lower")) return MouseLower; + if (lowerName == QStringLiteral("operations menu")) return MouseOperationsMenu; + if (lowerName == QStringLiteral("toggle raise and lower")) return MouseToggleRaiseAndLower; + if (lowerName == QStringLiteral("activate and raise")) return MouseActivateAndRaise; + if (lowerName == QStringLiteral("activate and lower")) return MouseActivateAndLower; + if (lowerName == QStringLiteral("activate")) return MouseActivate; + if (lowerName == QStringLiteral("activate, raise and pass click")) return MouseActivateRaiseAndPassClick; + if (lowerName == QStringLiteral("activate and pass click")) return MouseActivateAndPassClick; + if (lowerName == QStringLiteral("scroll")) return MouseNothing; + if (lowerName == QStringLiteral("activate and scroll")) return MouseActivateAndPassClick; + if (lowerName == QStringLiteral("activate, raise and scroll")) return MouseActivateRaiseAndPassClick; + if (lowerName == QStringLiteral("activate, raise and move")) + return restricted ? MouseActivateRaiseAndMove : MouseActivateRaiseAndUnrestrictedMove; + if (lowerName == QStringLiteral("move")) return restricted ? MouseMove : MouseUnrestrictedMove; + if (lowerName == QStringLiteral("resize")) return restricted ? MouseResize : MouseUnrestrictedResize; + if (lowerName == QStringLiteral("shade")) return MouseShade; + if (lowerName == QStringLiteral("minimize")) return MouseMinimize; + if (lowerName == QStringLiteral("close")) return MouseClose; + if (lowerName == QStringLiteral("increase opacity")) return MouseOpacityMore; + if (lowerName == QStringLiteral("decrease opacity")) return MouseOpacityLess; + if (lowerName == QStringLiteral("nothing")) return MouseNothing; + return MouseNothing; +} + +Options::MouseWheelCommand Options::mouseWheelCommand(const QString &name) +{ + QString lowerName = name.toLower(); + if (lowerName == QStringLiteral("raise/lower")) return MouseWheelRaiseLower; + if (lowerName == QStringLiteral("shade/unshade")) return MouseWheelShadeUnshade; + if (lowerName == QStringLiteral("maximize/restore")) return MouseWheelMaximizeRestore; + if (lowerName == QStringLiteral("above/below")) return MouseWheelAboveBelow; + if (lowerName == QStringLiteral("previous/next desktop")) return MouseWheelPreviousNextDesktop; + if (lowerName == QStringLiteral("change opacity")) return MouseWheelChangeOpacity; + if (lowerName == QStringLiteral("nothing")) return MouseWheelNothing; + return MouseWheelNothing; +} + +bool Options::showGeometryTip() const +{ + return show_geometry_tip; +} + +bool Options::condensedTitle() const +{ + return condensed_title; +} + +Options::MouseCommand Options::wheelToMouseCommand(MouseWheelCommand com, int delta) const +{ + switch(com) { + case MouseWheelRaiseLower: + return delta > 0 ? MouseRaise : MouseLower; + case MouseWheelShadeUnshade: + return delta > 0 ? MouseSetShade : MouseUnsetShade; + case MouseWheelMaximizeRestore: + return delta > 0 ? MouseMaximize : MouseRestore; + case MouseWheelAboveBelow: + return delta > 0 ? MouseAbove : MouseBelow; + case MouseWheelPreviousNextDesktop: + return delta > 0 ? MousePreviousDesktop : MouseNextDesktop; + case MouseWheelChangeOpacity: + return delta > 0 ? MouseOpacityMore : MouseOpacityLess; + default: + return MouseNothing; + } +} +#endif + +double Options::animationTimeFactor() const +{ + #ifndef KCMRULES + return m_settings->animationDurationFactor(); +#else + return 0; +#endif +} + +Options::WindowOperation Options::operationMaxButtonClick(Qt::MouseButtons button) const +{ + return button == Qt::RightButton ? opMaxButtonRightClick : + button == Qt::MiddleButton ? opMaxButtonMiddleClick : + opMaxButtonLeftClick; +} + +QStringList Options::modifierOnlyDBusShortcut(Qt::KeyboardModifier mod) const +{ + return m_modifierOnlyShortcuts.value(mod); +} + +bool Options::isUseCompositing() const +{ + return m_useCompositing || kwinApp()->platform()->requiresCompositing(); +} + +} // namespace diff --git a/options.h b/options.h new file mode 100644 index 0000000..6d72017 --- /dev/null +++ b/options.h @@ -0,0 +1,927 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_OPTIONS_H +#define KWIN_OPTIONS_H + +#include "main.h" +#include "placement.h" + +#include + +namespace KWin +{ + +// Whether to keep all windows mapped when compositing (i.e. whether to have +// actively updated window pixmaps). +enum HiddenPreviews { + // The normal mode with regard to mapped windows. Hidden (minimized, etc.) + // and windows on inactive virtual desktops are not mapped, their pixmaps + // are only their icons. + HiddenPreviewsNever, + // Like normal mode, but shown windows (i.e. on inactive virtual desktops) + // are kept mapped, only hidden windows are unmapped. + HiddenPreviewsShown, + // All windows are kept mapped regardless of their state. + HiddenPreviewsAlways +}; + +/** + * This enum type specifies whether the Xwayland server must be restarted after a crash. + */ +enum XwaylandCrashPolicy { + Stop, + Restart, +}; + +class Settings; + +class KWIN_EXPORT Options : public QObject +{ + Q_OBJECT + Q_ENUMS(FocusPolicy) + Q_ENUMS(XwaylandCrashPolicy) + Q_ENUMS(GlSwapStrategy) + Q_ENUMS(MouseCommand) + Q_ENUMS(MouseWheelCommand) + Q_ENUMS(WindowOperation) + + Q_PROPERTY(FocusPolicy focusPolicy READ focusPolicy WRITE setFocusPolicy NOTIFY focusPolicyChanged) + Q_PROPERTY(XwaylandCrashPolicy xwaylandCrashPolicy READ xwaylandCrashPolicy WRITE setXwaylandCrashPolicy NOTIFY xwaylandCrashPolicyChanged) + Q_PROPERTY(int xwaylandMaxCrashCount READ xwaylandMaxCrashCount WRITE setXwaylandMaxCrashCount NOTIFY xwaylandMaxCrashCountChanged) + Q_PROPERTY(bool nextFocusPrefersMouse READ isNextFocusPrefersMouse WRITE setNextFocusPrefersMouse NOTIFY nextFocusPrefersMouseChanged) + /** + * Whether clicking on a window raises it in FocusFollowsMouse + * mode or not. + */ + Q_PROPERTY(bool clickRaise READ isClickRaise WRITE setClickRaise NOTIFY clickRaiseChanged) + /** + * Whether autoraise is enabled FocusFollowsMouse mode or not. + */ + Q_PROPERTY(bool autoRaise READ isAutoRaise WRITE setAutoRaise NOTIFY autoRaiseChanged) + /** + * Autoraise interval. + */ + Q_PROPERTY(int autoRaiseInterval READ autoRaiseInterval WRITE setAutoRaiseInterval NOTIFY autoRaiseIntervalChanged) + /** + * Delayed focus interval. + */ + Q_PROPERTY(int delayFocusInterval READ delayFocusInterval WRITE setDelayFocusInterval NOTIFY delayFocusIntervalChanged) + /** + * Whether shade hover is enabled or not. + */ + Q_PROPERTY(bool shadeHover READ isShadeHover WRITE setShadeHover NOTIFY shadeHoverChanged) + /** + * Shade hover interval. + */ + Q_PROPERTY(int shadeHoverInterval READ shadeHoverInterval WRITE setShadeHoverInterval NOTIFY shadeHoverIntervalChanged) + /** + * Whether to see Xinerama screens separately for focus (in Alt+Tab, when activating next client) + */ + Q_PROPERTY(bool separateScreenFocus READ isSeparateScreenFocus WRITE setSeparateScreenFocus NOTIFY separateScreenFocusChanged) + Q_PROPERTY(int placement READ placement WRITE setPlacement NOTIFY placementChanged) + Q_PROPERTY(bool focusPolicyIsReasonable READ focusPolicyIsReasonable NOTIFY focusPolicyIsResonableChanged) + /** + * The size of the zone that triggers snapping on desktop borders. + */ + Q_PROPERTY(int borderSnapZone READ borderSnapZone WRITE setBorderSnapZone NOTIFY borderSnapZoneChanged) + /** + * The size of the zone that triggers snapping with other windows. + */ + Q_PROPERTY(int windowSnapZone READ windowSnapZone WRITE setWindowSnapZone NOTIFY windowSnapZoneChanged) + /** + * The size of the zone that triggers snapping on the screen center. + */ + Q_PROPERTY(int centerSnapZone READ centerSnapZone WRITE setCenterSnapZone NOTIFY centerSnapZoneChanged) + /** + * Snap only when windows will overlap. + */ + Q_PROPERTY(bool snapOnlyWhenOverlapping READ isSnapOnlyWhenOverlapping WRITE setSnapOnlyWhenOverlapping NOTIFY snapOnlyWhenOverlappingChanged) + /** + * Whether or not we roll over to the other edge when switching desktops past the edge. + */ + Q_PROPERTY(bool rollOverDesktops READ isRollOverDesktops WRITE setRollOverDesktops NOTIFY rollOverDesktopsChanged) + /** + * 0 - 4 , see Workspace::allowClientActivation() + */ + Q_PROPERTY(int focusStealingPreventionLevel READ focusStealingPreventionLevel WRITE setFocusStealingPreventionLevel NOTIFY focusStealingPreventionLevelChanged) + Q_PROPERTY(KWin::Options::WindowOperation operationTitlebarDblClick READ operationTitlebarDblClick WRITE setOperationTitlebarDblClick NOTIFY operationTitlebarDblClickChanged) + Q_PROPERTY(KWin::Options::WindowOperation operationMaxButtonLeftClick READ operationMaxButtonLeftClick WRITE setOperationMaxButtonLeftClick NOTIFY operationMaxButtonLeftClickChanged) + Q_PROPERTY(KWin::Options::WindowOperation operationMaxButtonMiddleClick READ operationMaxButtonMiddleClick WRITE setOperationMaxButtonMiddleClick NOTIFY operationMaxButtonMiddleClickChanged) + Q_PROPERTY(KWin::Options::WindowOperation operationMaxButtonRightClick READ operationMaxButtonRightClick WRITE setOperationMaxButtonRightClick NOTIFY operationMaxButtonRightClickChanged) + Q_PROPERTY(MouseCommand commandActiveTitlebar1 READ commandActiveTitlebar1 WRITE setCommandActiveTitlebar1 NOTIFY commandActiveTitlebar1Changed) + Q_PROPERTY(MouseCommand commandActiveTitlebar2 READ commandActiveTitlebar2 WRITE setCommandActiveTitlebar2 NOTIFY commandActiveTitlebar2Changed) + Q_PROPERTY(MouseCommand commandActiveTitlebar3 READ commandActiveTitlebar3 WRITE setCommandActiveTitlebar3 NOTIFY commandActiveTitlebar3Changed) + Q_PROPERTY(MouseCommand commandInactiveTitlebar1 READ commandInactiveTitlebar1 WRITE setCommandInactiveTitlebar1 NOTIFY commandInactiveTitlebar1Changed) + Q_PROPERTY(MouseCommand commandInactiveTitlebar2 READ commandInactiveTitlebar2 WRITE setCommandInactiveTitlebar2 NOTIFY commandInactiveTitlebar2Changed) + Q_PROPERTY(MouseCommand commandInactiveTitlebar3 READ commandInactiveTitlebar3 WRITE setCommandInactiveTitlebar3 NOTIFY commandInactiveTitlebar3Changed) + Q_PROPERTY(MouseCommand commandWindow1 READ commandWindow1 WRITE setCommandWindow1 NOTIFY commandWindow1Changed) + Q_PROPERTY(MouseCommand commandWindow2 READ commandWindow2 WRITE setCommandWindow2 NOTIFY commandWindow2Changed) + Q_PROPERTY(MouseCommand commandWindow3 READ commandWindow3 WRITE setCommandWindow3 NOTIFY commandWindow3Changed) + Q_PROPERTY(MouseCommand commandWindowWheel READ commandWindowWheel WRITE setCommandWindowWheel NOTIFY commandWindowWheelChanged) + Q_PROPERTY(MouseCommand commandAll1 READ commandAll1 WRITE setCommandAll1 NOTIFY commandAll1Changed) + Q_PROPERTY(MouseCommand commandAll2 READ commandAll2 WRITE setCommandAll2 NOTIFY commandAll2Changed) + Q_PROPERTY(MouseCommand commandAll3 READ commandAll3 WRITE setCommandAll3 NOTIFY commandAll3Changed) + Q_PROPERTY(uint keyCmdAllModKey READ keyCmdAllModKey WRITE setKeyCmdAllModKey NOTIFY keyCmdAllModKeyChanged) + /** + * Whether the Geometry Tip should be shown during a window move/resize. + */ + Q_PROPERTY(bool showGeometryTip READ showGeometryTip WRITE setShowGeometryTip NOTIFY showGeometryTipChanged) + /** + * Whether the visible name should be condensed. + */ + Q_PROPERTY(bool condensedTitle READ condensedTitle WRITE setCondensedTitle NOTIFY condensedTitleChanged) + /** + * Whether a window gets maximized when it reaches top screen edge while being moved. + */ + Q_PROPERTY(bool electricBorderMaximize READ electricBorderMaximize WRITE setElectricBorderMaximize NOTIFY electricBorderMaximizeChanged) + /** + * Whether a window is tiled to half screen when reaching left or right screen edge while been moved. + */ + Q_PROPERTY(bool electricBorderTiling READ electricBorderTiling WRITE setElectricBorderTiling NOTIFY electricBorderTilingChanged) + /** + * Whether a window is tiled to half screen when reaching left or right screen edge while been moved. + */ + Q_PROPERTY(float electricBorderCornerRatio READ electricBorderCornerRatio WRITE setElectricBorderCornerRatio NOTIFY electricBorderCornerRatioChanged) + Q_PROPERTY(bool borderlessMaximizedWindows READ borderlessMaximizedWindows WRITE setBorderlessMaximizedWindows NOTIFY borderlessMaximizedWindowsChanged) + /** + * timeout before non-responding application will be killed after attempt to close. + */ + Q_PROPERTY(int killPingTimeout READ killPingTimeout WRITE setKillPingTimeout NOTIFY killPingTimeoutChanged) + /** + * Whether to hide utility windows for inactive applications. + */ + Q_PROPERTY(bool hideUtilityWindowsForInactive READ isHideUtilityWindowsForInactive WRITE setHideUtilityWindowsForInactive NOTIFY hideUtilityWindowsForInactiveChanged) + Q_PROPERTY(int compositingMode READ compositingMode WRITE setCompositingMode NOTIFY compositingModeChanged) + Q_PROPERTY(bool useCompositing READ isUseCompositing WRITE setUseCompositing NOTIFY useCompositingChanged) + Q_PROPERTY(int hiddenPreviews READ hiddenPreviews WRITE setHiddenPreviews NOTIFY hiddenPreviewsChanged) + /** + * 0 = no, 1 = yes when transformed, + * 2 = try trilinear when transformed; else 1, + * -1 = auto + */ + Q_PROPERTY(int glSmoothScale READ glSmoothScale WRITE setGlSmoothScale NOTIFY glSmoothScaleChanged) + Q_PROPERTY(bool xrenderSmoothScale READ isXrenderSmoothScale WRITE setXrenderSmoothScale NOTIFY xrenderSmoothScaleChanged) + Q_PROPERTY(qint64 maxFpsInterval READ maxFpsInterval WRITE setMaxFpsInterval NOTIFY maxFpsIntervalChanged) + Q_PROPERTY(uint refreshRate READ refreshRate WRITE setRefreshRate NOTIFY refreshRateChanged) + Q_PROPERTY(qint64 vBlankTime READ vBlankTime WRITE setVBlankTime NOTIFY vBlankTimeChanged) + Q_PROPERTY(bool glStrictBinding READ isGlStrictBinding WRITE setGlStrictBinding NOTIFY glStrictBindingChanged) + /** + * Whether strict binding follows the driver or has been overwritten by a user defined config value. + * If @c true glStrictBinding is set by the OpenGL Scene during initialization. + * If @c false glStrictBinding is set from a config value and not updated during scene initialization. + */ + Q_PROPERTY(bool glStrictBindingFollowsDriver READ isGlStrictBindingFollowsDriver WRITE setGlStrictBindingFollowsDriver NOTIFY glStrictBindingFollowsDriverChanged) + Q_PROPERTY(bool glCoreProfile READ glCoreProfile WRITE setGLCoreProfile NOTIFY glCoreProfileChanged) + Q_PROPERTY(GlSwapStrategy glPreferBufferSwap READ glPreferBufferSwap WRITE setGlPreferBufferSwap NOTIFY glPreferBufferSwapChanged) + Q_PROPERTY(KWin::OpenGLPlatformInterface glPlatformInterface READ glPlatformInterface WRITE setGlPlatformInterface NOTIFY glPlatformInterfaceChanged) + Q_PROPERTY(bool windowsBlockCompositing READ windowsBlockCompositing WRITE setWindowsBlockCompositing NOTIFY windowsBlockCompositingChanged) +public: + + explicit Options(QObject *parent = nullptr); + ~Options() override; + + void updateSettings(); + + /** + * This enum type is used to specify the focus policy. + * + * Note that FocusUnderMouse and FocusStrictlyUnderMouse are not + * particulary useful. They are only provided for old-fashined + * die-hard UNIX people ;-) + */ + enum FocusPolicy { + /** + * Clicking into a window activates it. This is also the default. + */ + ClickToFocus, + /** + * Moving the mouse pointer actively onto a normal window activates it. + * For convenience, the desktop and windows on the dock are excluded. + * They require clicking. + */ + FocusFollowsMouse, + /** + * The window that happens to be under the mouse pointer becomes active. + * The invariant is: no window can have focus that is not under the mouse. + * This also means that Alt-Tab won't work properly and popup dialogs are + * usually unsable with the keyboard. Note that the desktop and windows on + * the dock are excluded for convenience. They get focus only when clicking + * on it. + */ + FocusUnderMouse, + /** + * This is even worse than FocusUnderMouse. Only the window under the mouse + * pointer is active. If the mouse points nowhere, nothing has the focus. If + * the mouse points onto the desktop, the desktop has focus. The same holds + * for windows on the dock. + */ + FocusStrictlyUnderMouse + }; + + FocusPolicy focusPolicy() const { + return m_focusPolicy; + } + bool isNextFocusPrefersMouse() const { + return m_nextFocusPrefersMouse; + } + + XwaylandCrashPolicy xwaylandCrashPolicy() const { + return m_xwaylandCrashPolicy; + } + int xwaylandMaxCrashCount() const { + return m_xwaylandMaxCrashCount; + } + + /** + * Whether clicking on a window raises it in FocusFollowsMouse + * mode or not. + */ + bool isClickRaise() const { + return m_clickRaise; + } + + /** + * Whether autoraise is enabled FocusFollowsMouse mode or not. + */ + bool isAutoRaise() const { + return m_autoRaise; + } + + /** + * Autoraise interval + */ + int autoRaiseInterval() const { + return m_autoRaiseInterval; + } + + /** + * Delayed focus interval. + */ + int delayFocusInterval() const { + return m_delayFocusInterval; + } + + /** + * Whether shade hover is enabled or not. + */ + bool isShadeHover() const { + return m_shadeHover; + } + + /** + * Shade hover interval. + */ + int shadeHoverInterval() { + return m_shadeHoverInterval; + } + + /** + * Whether to see Xinerama screens separately for focus (in Alt+Tab, when activating next client) + */ + bool isSeparateScreenFocus() const { + return m_separateScreenFocus; + } + + Placement::Policy placement() const { + return m_placement; + } + + bool focusPolicyIsReasonable() { + return m_focusPolicy == ClickToFocus || m_focusPolicy == FocusFollowsMouse; + } + + /** + * The size of the zone that triggers snapping on desktop borders. + */ + int borderSnapZone() const { + return m_borderSnapZone; + } + + /** + * The size of the zone that triggers snapping with other windows. + */ + int windowSnapZone() const { + return m_windowSnapZone; + } + + /** + * The size of the zone that triggers snapping on the screen center. + */ + int centerSnapZone() const { + return m_centerSnapZone; + } + + + /** + * Snap only when windows will overlap. + */ + bool isSnapOnlyWhenOverlapping() const { + return m_snapOnlyWhenOverlapping; + } + + /** + * Whether or not we roll over to the other edge when switching desktops past the edge. + */ + bool isRollOverDesktops() const { + return m_rollOverDesktops; + } + + /** + * Returns the focus stealing prevention level. + * + * @see allowClientActivation + */ + int focusStealingPreventionLevel() const { + return m_focusStealingPreventionLevel; + } + + enum WindowOperation { + MaximizeOp = 5000, + RestoreOp, + MinimizeOp, + MoveOp, + UnrestrictedMoveOp, + ResizeOp, + UnrestrictedResizeOp, + CloseOp, + OnAllDesktopsOp, + ShadeOp, + KeepAboveOp, + KeepBelowOp, + OperationsOp, + WindowRulesOp, + ToggleStoreSettingsOp = WindowRulesOp, ///< @obsolete + HMaximizeOp, + VMaximizeOp, + LowerOp, + FullScreenOp, + NoBorderOp, + NoOp, + SetupWindowShortcutOp, + ApplicationRulesOp, + }; + + WindowOperation operationTitlebarDblClick() const { + return OpTitlebarDblClick; + } + WindowOperation operationMaxButtonLeftClick() const { + return opMaxButtonLeftClick; + } + WindowOperation operationMaxButtonRightClick() const { + return opMaxButtonRightClick; + } + WindowOperation operationMaxButtonMiddleClick() const { + return opMaxButtonMiddleClick; + } + WindowOperation operationMaxButtonClick(Qt::MouseButtons button) const; + + + enum MouseCommand { + MouseRaise, MouseLower, MouseOperationsMenu, MouseToggleRaiseAndLower, + MouseActivateAndRaise, MouseActivateAndLower, MouseActivate, + MouseActivateRaiseAndPassClick, MouseActivateAndPassClick, + MouseMove, MouseUnrestrictedMove, + MouseActivateRaiseAndMove, MouseActivateRaiseAndUnrestrictedMove, + MouseResize, MouseUnrestrictedResize, + MouseShade, MouseSetShade, MouseUnsetShade, + MouseMaximize, MouseRestore, MouseMinimize, + MouseNextDesktop, MousePreviousDesktop, + MouseAbove, MouseBelow, + MouseOpacityMore, MouseOpacityLess, + MouseClose, + MouseNothing + }; + + enum MouseWheelCommand { + MouseWheelRaiseLower, MouseWheelShadeUnshade, MouseWheelMaximizeRestore, + MouseWheelAboveBelow, MouseWheelPreviousNextDesktop, + MouseWheelChangeOpacity, + MouseWheelNothing + }; + + MouseCommand operationTitlebarMouseWheel(int delta) const { + return wheelToMouseCommand(CmdTitlebarWheel, delta); + } + MouseCommand operationWindowMouseWheel(int delta) const { + return wheelToMouseCommand(CmdAllWheel, delta); + } + + MouseCommand commandActiveTitlebar1() const { + return CmdActiveTitlebar1; + } + MouseCommand commandActiveTitlebar2() const { + return CmdActiveTitlebar2; + } + MouseCommand commandActiveTitlebar3() const { + return CmdActiveTitlebar3; + } + MouseCommand commandInactiveTitlebar1() const { + return CmdInactiveTitlebar1; + } + MouseCommand commandInactiveTitlebar2() const { + return CmdInactiveTitlebar2; + } + MouseCommand commandInactiveTitlebar3() const { + return CmdInactiveTitlebar3; + } + MouseCommand commandWindow1() const { + return CmdWindow1; + } + MouseCommand commandWindow2() const { + return CmdWindow2; + } + MouseCommand commandWindow3() const { + return CmdWindow3; + } + MouseCommand commandWindowWheel() const { + return CmdWindowWheel; + } + MouseCommand commandAll1() const { + return CmdAll1; + } + MouseCommand commandAll2() const { + return CmdAll2; + } + MouseCommand commandAll3() const { + return CmdAll3; + } + MouseWheelCommand commandAllWheel() const { + return CmdAllWheel; + } + uint keyCmdAllModKey() const { + return CmdAllModKey; + } + Qt::KeyboardModifier commandAllModifier() const { + switch (CmdAllModKey) { + case Qt::Key_Alt: + return Qt::AltModifier; + case Qt::Key_Meta: + return Qt::MetaModifier; + default: + Q_UNREACHABLE(); + } + } + + static WindowOperation windowOperation(const QString &name, bool restricted); + static MouseCommand mouseCommand(const QString &name, bool restricted); + static MouseWheelCommand mouseWheelCommand(const QString &name); + + /** + * @returns true if the Geometry Tip should be shown during a window move/resize. + */ + bool showGeometryTip() const; + + /** + * Returns whether the user prefers his caption clean. + */ + bool condensedTitle() const; + + /** + * @returns true if a window gets maximized when it reaches top screen edge + * while being moved. + */ + bool electricBorderMaximize() const { + return electric_border_maximize; + } + /** + * @returns true if window is tiled to half screen when reaching left or + * right screen edge while been moved. + */ + bool electricBorderTiling() const { + return electric_border_tiling; + } + /** + * @returns the factor that determines the corner part of the edge (ie. 0.1 means tiny corner) + */ + float electricBorderCornerRatio() const { + return electric_border_corner_ratio; + } + + bool borderlessMaximizedWindows() const { + return borderless_maximized_windows; + } + + /** + * Timeout before non-responding application will be killed after attempt to close. + */ + int killPingTimeout() const { + return m_killPingTimeout; + } + + /** + * Whether to hide utility windows for inactive applications. + */ + bool isHideUtilityWindowsForInactive() const { + return m_hideUtilityWindowsForInactive; + } + + /** + * Returns the animation time factor for desktop effects. + */ + double animationTimeFactor() const; + + //---------------------- + // Compositing settings + void reloadCompositingSettings(bool force = false); + CompositingType compositingMode() const { + return m_compositingMode; + } + void setCompositingMode(CompositingType mode) { + m_compositingMode = mode; + } + // Separate to mode so the user can toggle + bool isUseCompositing() const; + + // General preferences + HiddenPreviews hiddenPreviews() const { + return m_hiddenPreviews; + } + // OpenGL + // 0 = no, 1 = yes when transformed, + // 2 = try trilinear when transformed; else 1, + // -1 = auto + int glSmoothScale() const { + return m_glSmoothScale; + } + // XRender + bool isXrenderSmoothScale() const { + return m_xrenderSmoothScale; + } + + qint64 maxFpsInterval() const { + return m_maxFpsInterval; + } + // Settings that should be auto-detected + uint refreshRate() const { + return m_refreshRate; + } + qint64 vBlankTime() const { + return m_vBlankTime; + } + bool isGlStrictBinding() const { + return m_glStrictBinding; + } + bool isGlStrictBindingFollowsDriver() const { + return m_glStrictBindingFollowsDriver; + } + bool glCoreProfile() const { + return m_glCoreProfile; + } + OpenGLPlatformInterface glPlatformInterface() const { + return m_glPlatformInterface; + } + + enum GlSwapStrategy { NoSwapEncourage = 0, CopyFrontBuffer = 'c', PaintFullScreen = 'p', ExtendDamage = 'e', AutoSwapStrategy = 'a' }; + GlSwapStrategy glPreferBufferSwap() const { + return m_glPreferBufferSwap; + } + + bool windowsBlockCompositing() const + { + return m_windowsBlockCompositing; + } + + QStringList modifierOnlyDBusShortcut(Qt::KeyboardModifier mod) const; + + // setters + void setFocusPolicy(FocusPolicy focusPolicy); + void setXwaylandCrashPolicy(XwaylandCrashPolicy crashPolicy); + void setXwaylandMaxCrashCount(int maxCrashCount); + void setNextFocusPrefersMouse(bool nextFocusPrefersMouse); + void setClickRaise(bool clickRaise); + void setAutoRaise(bool autoRaise); + void setAutoRaiseInterval(int autoRaiseInterval); + void setDelayFocusInterval(int delayFocusInterval); + void setShadeHover(bool shadeHover); + void setShadeHoverInterval(int shadeHoverInterval); + void setSeparateScreenFocus(bool separateScreenFocus); + void setPlacement(int placement); + void setBorderSnapZone(int borderSnapZone); + void setWindowSnapZone(int windowSnapZone); + void setCenterSnapZone(int centerSnapZone); + void setSnapOnlyWhenOverlapping(bool snapOnlyWhenOverlapping); + void setRollOverDesktops(bool rollOverDesktops); + void setFocusStealingPreventionLevel(int focusStealingPreventionLevel); + void setOperationTitlebarDblClick(WindowOperation operationTitlebarDblClick); + void setOperationMaxButtonLeftClick(WindowOperation op); + void setOperationMaxButtonRightClick(WindowOperation op); + void setOperationMaxButtonMiddleClick(WindowOperation op); + void setCommandActiveTitlebar1(MouseCommand commandActiveTitlebar1); + void setCommandActiveTitlebar2(MouseCommand commandActiveTitlebar2); + void setCommandActiveTitlebar3(MouseCommand commandActiveTitlebar3); + void setCommandInactiveTitlebar1(MouseCommand commandInactiveTitlebar1); + void setCommandInactiveTitlebar2(MouseCommand commandInactiveTitlebar2); + void setCommandInactiveTitlebar3(MouseCommand commandInactiveTitlebar3); + void setCommandWindow1(MouseCommand commandWindow1); + void setCommandWindow2(MouseCommand commandWindow2); + void setCommandWindow3(MouseCommand commandWindow3); + void setCommandWindowWheel(MouseCommand commandWindowWheel); + void setCommandAll1(MouseCommand commandAll1); + void setCommandAll2(MouseCommand commandAll2); + void setCommandAll3(MouseCommand commandAll3); + void setKeyCmdAllModKey(uint keyCmdAllModKey); + void setShowGeometryTip(bool showGeometryTip); + void setCondensedTitle(bool condensedTitle); + void setElectricBorderMaximize(bool electricBorderMaximize); + void setElectricBorderTiling(bool electricBorderTiling); + void setElectricBorderCornerRatio(float electricBorderCornerRatio); + void setBorderlessMaximizedWindows(bool borderlessMaximizedWindows); + void setKillPingTimeout(int killPingTimeout); + void setHideUtilityWindowsForInactive(bool hideUtilityWindowsForInactive); + void setCompositingMode(int compositingMode); + void setUseCompositing(bool useCompositing); + void setHiddenPreviews(int hiddenPreviews); + void setGlSmoothScale(int glSmoothScale); + void setXrenderSmoothScale(bool xrenderSmoothScale); + void setMaxFpsInterval(qint64 maxFpsInterval); + void setRefreshRate(uint refreshRate); + void setVBlankTime(qint64 vBlankTime); + void setGlStrictBinding(bool glStrictBinding); + void setGlStrictBindingFollowsDriver(bool glStrictBindingFollowsDriver); + void setGLCoreProfile(bool glCoreProfile); + void setGlPreferBufferSwap(char glPreferBufferSwap); + void setGlPlatformInterface(OpenGLPlatformInterface interface); + void setWindowsBlockCompositing(bool set); + + // default values + static WindowOperation defaultOperationTitlebarDblClick() { + return MaximizeOp; + } + static WindowOperation defaultOperationMaxButtonLeftClick() { + return MaximizeOp; + } + static WindowOperation defaultOperationMaxButtonRightClick() { + return HMaximizeOp; + } + static WindowOperation defaultOperationMaxButtonMiddleClick() { + return VMaximizeOp; + } + static MouseCommand defaultCommandActiveTitlebar1() { + return MouseRaise; + } + static MouseCommand defaultCommandActiveTitlebar2() { + return MouseNothing; + } + static MouseCommand defaultCommandActiveTitlebar3() { + return MouseOperationsMenu; + } + static MouseCommand defaultCommandInactiveTitlebar1() { + return MouseActivateAndRaise; + } + static MouseCommand defaultCommandInactiveTitlebar2() { + return MouseNothing; + } + static MouseCommand defaultCommandInactiveTitlebar3() { + return MouseOperationsMenu; + } + static MouseCommand defaultCommandWindow1() { + return MouseActivateRaiseAndPassClick; + } + static MouseCommand defaultCommandWindow2() { + return MouseActivateAndPassClick; + } + static MouseCommand defaultCommandWindow3() { + return MouseActivateAndPassClick; + } + static MouseCommand defaultCommandWindowWheel() { + return MouseNothing; + } + static MouseCommand defaultCommandAll1() { + return MouseUnrestrictedMove; + } + static MouseCommand defaultCommandAll2() { + return MouseToggleRaiseAndLower; + } + static MouseCommand defaultCommandAll3() { + return MouseUnrestrictedResize; + } + static MouseWheelCommand defaultCommandTitlebarWheel() { + return MouseWheelNothing; + } + static MouseWheelCommand defaultCommandAllWheel() { + return MouseWheelNothing; + } + static uint defaultKeyCmdAllModKey() { + return Qt::Key_Alt; + } + static CompositingType defaultCompositingMode() { + return OpenGLCompositing; + } + static bool defaultUseCompositing() { + return true; + } + static HiddenPreviews defaultHiddenPreviews() { + return HiddenPreviewsShown; + } + static int defaultGlSmoothScale() { + return 2; + } + static bool defaultXrenderSmoothScale() { + return false; + } + static qint64 defaultMaxFpsInterval() { + return (1 * 1000 * 1000 * 1000) /60.0; // nanoseconds / Hz + } + static int defaultMaxFps() { + return 60; + } + static uint defaultRefreshRate() { + return 0; + } + static uint defaultVBlankTime() { + return 6000; // 6ms + } + static bool defaultGlStrictBinding() { + return true; + } + static bool defaultGlStrictBindingFollowsDriver() { + return true; + } + static bool defaultGLCoreProfile() { + return false; + } + static GlSwapStrategy defaultGlPreferBufferSwap() { + return AutoSwapStrategy; + } + static OpenGLPlatformInterface defaultGlPlatformInterface() { + return kwinApp()->shouldUseWaylandForCompositing() ? EglPlatformInterface : GlxPlatformInterface; + } + static XwaylandCrashPolicy defaultXwaylandCrashPolicy() { + return XwaylandCrashPolicy::Restart; + } + static int defaultXwaylandMaxCrashCount() { + return 3; + } + /** + * Performs loading all settings except compositing related. + */ + void loadConfig(); + /** + * Performs loading of compositing settings which do not depend on OpenGL. + */ + bool loadCompositingConfig(bool force); + void reparseConfiguration(); + + static int currentRefreshRate(); + + //---------------------- +Q_SIGNALS: + // for properties + void focusPolicyChanged(); + void focusPolicyIsResonableChanged(); + void xwaylandCrashPolicyChanged(); + void xwaylandMaxCrashCountChanged(); + void nextFocusPrefersMouseChanged(); + void clickRaiseChanged(); + void autoRaiseChanged(); + void autoRaiseIntervalChanged(); + void delayFocusIntervalChanged(); + void shadeHoverChanged(); + void shadeHoverIntervalChanged(); + void separateScreenFocusChanged(bool); + void placementChanged(); + void borderSnapZoneChanged(); + void windowSnapZoneChanged(); + void centerSnapZoneChanged(); + void snapOnlyWhenOverlappingChanged(); + void rollOverDesktopsChanged(bool enabled); + void focusStealingPreventionLevelChanged(); + void operationTitlebarDblClickChanged(); + void operationMaxButtonLeftClickChanged(); + void operationMaxButtonRightClickChanged(); + void operationMaxButtonMiddleClickChanged(); + void commandActiveTitlebar1Changed(); + void commandActiveTitlebar2Changed(); + void commandActiveTitlebar3Changed(); + void commandInactiveTitlebar1Changed(); + void commandInactiveTitlebar2Changed(); + void commandInactiveTitlebar3Changed(); + void commandWindow1Changed(); + void commandWindow2Changed(); + void commandWindow3Changed(); + void commandWindowWheelChanged(); + void commandAll1Changed(); + void commandAll2Changed(); + void commandAll3Changed(); + void keyCmdAllModKeyChanged(); + void showGeometryTipChanged(); + void condensedTitleChanged(); + void electricBorderMaximizeChanged(); + void electricBorderTilingChanged(); + void electricBorderCornerRatioChanged(); + void borderlessMaximizedWindowsChanged(); + void killPingTimeoutChanged(); + void hideUtilityWindowsForInactiveChanged(); + void compositingModeChanged(); + void useCompositingChanged(); + void hiddenPreviewsChanged(); + void glSmoothScaleChanged(); + void xrenderSmoothScaleChanged(); + void maxFpsIntervalChanged(); + void refreshRateChanged(); + void vBlankTimeChanged(); + void glStrictBindingChanged(); + void glStrictBindingFollowsDriverChanged(); + void glCoreProfileChanged(); + void glPreferBufferSwapChanged(); + void glPlatformInterfaceChanged(); + void windowsBlockCompositingChanged(); + void animationSpeedChanged(); + + void configChanged(); + +private: + void setElectricBorders(int borders); + void syncFromKcfgc(); + QScopedPointer m_settings; + KConfigWatcher::Ptr m_configWatcher; + + FocusPolicy m_focusPolicy; + bool m_nextFocusPrefersMouse; + bool m_clickRaise; + bool m_autoRaise; + int m_autoRaiseInterval; + int m_delayFocusInterval; + bool m_shadeHover; + int m_shadeHoverInterval; + bool m_separateScreenFocus; + Placement::Policy m_placement; + int m_borderSnapZone; + int m_windowSnapZone; + int m_centerSnapZone; + bool m_snapOnlyWhenOverlapping; + bool m_rollOverDesktops; + int m_focusStealingPreventionLevel; + int m_killPingTimeout; + bool m_hideUtilityWindowsForInactive; + XwaylandCrashPolicy m_xwaylandCrashPolicy; + int m_xwaylandMaxCrashCount; + + CompositingType m_compositingMode; + bool m_useCompositing; + HiddenPreviews m_hiddenPreviews; + int m_glSmoothScale; + bool m_xrenderSmoothScale; + qint64 m_maxFpsInterval; + // Settings that should be auto-detected + uint m_refreshRate; + qint64 m_vBlankTime; + bool m_glStrictBinding; + bool m_glStrictBindingFollowsDriver; + bool m_glCoreProfile; + GlSwapStrategy m_glPreferBufferSwap; + OpenGLPlatformInterface m_glPlatformInterface; + bool m_windowsBlockCompositing; + + WindowOperation OpTitlebarDblClick; + WindowOperation opMaxButtonRightClick = defaultOperationMaxButtonRightClick(); + WindowOperation opMaxButtonMiddleClick = defaultOperationMaxButtonMiddleClick(); + WindowOperation opMaxButtonLeftClick = defaultOperationMaxButtonRightClick(); + + // mouse bindings + MouseCommand CmdActiveTitlebar1; + MouseCommand CmdActiveTitlebar2; + MouseCommand CmdActiveTitlebar3; + MouseCommand CmdInactiveTitlebar1; + MouseCommand CmdInactiveTitlebar2; + MouseCommand CmdInactiveTitlebar3; + MouseWheelCommand CmdTitlebarWheel; + MouseCommand CmdWindow1; + MouseCommand CmdWindow2; + MouseCommand CmdWindow3; + MouseCommand CmdWindowWheel; + MouseCommand CmdAll1; + MouseCommand CmdAll2; + MouseCommand CmdAll3; + MouseWheelCommand CmdAllWheel; + uint CmdAllModKey; + + bool electric_border_maximize; + bool electric_border_tiling; + float electric_border_corner_ratio; + bool borderless_maximized_windows; + bool show_geometry_tip; + bool condensed_title; + + QHash m_modifierOnlyShortcuts; + + MouseCommand wheelToMouseCommand(MouseWheelCommand com, int delta) const; +}; + +extern KWIN_EXPORT Options* options; + +} // namespace + +Q_DECLARE_METATYPE(KWin::Options::WindowOperation) +Q_DECLARE_METATYPE(KWin::OpenGLPlatformInterface) + +#endif diff --git a/org.kde.KWin.Session.xml b/org.kde.KWin.Session.xml new file mode 100644 index 0000000..4e6c96d --- /dev/null +++ b/org.kde.KWin.Session.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/org.kde.KWin.VirtualDesktopManager.xml b/org.kde.KWin.VirtualDesktopManager.xml new file mode 100644 index 0000000..3283764 --- /dev/null +++ b/org.kde.KWin.VirtualDesktopManager.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/org.kde.KWin.xml b/org.kde.KWin.xml new file mode 100644 index 0000000..b810c23 --- /dev/null +++ b/org.kde.KWin.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/org.kde.kappmenu.xml b/org.kde.kappmenu.xml new file mode 100644 index 0000000..d29d3ee --- /dev/null +++ b/org.kde.kappmenu.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/org.kde.kwin.ColorCorrect.xml b/org.kde.kwin.ColorCorrect.xml new file mode 100644 index 0000000..4dd8ab7 --- /dev/null +++ b/org.kde.kwin.ColorCorrect.xml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/org.kde.kwin.Compositing.xml b/org.kde.kwin.Compositing.xml new file mode 100644 index 0000000..88ff9e4 --- /dev/null +++ b/org.kde.kwin.Compositing.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/org.kde.kwin.Effects.xml b/org.kde.kwin.Effects.xml new file mode 100644 index 0000000..ebb8898 --- /dev/null +++ b/org.kde.kwin.Effects.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/osd.cpp b/osd.cpp new file mode 100644 index 0000000..518b30c --- /dev/null +++ b/osd.cpp @@ -0,0 +1,68 @@ +/* + SPDX-FileCopyrightText: 2016 Martin Graesslin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +*/ +#include "osd.h" +#include "onscreennotification.h" +#include "main.h" +#include "workspace.h" +#include "scripting/scripting.h" + +#include + +namespace KWin +{ +namespace OSD +{ + +static OnScreenNotification *create() +{ + auto osd = new OnScreenNotification(workspace()); + osd->setConfig(kwinApp()->config()); + osd->setEngine(Scripting::self()->qmlEngine()); + return osd; +} + +static OnScreenNotification *osd() +{ + static OnScreenNotification *s_osd = create(); + return s_osd; +} + +void show(const QString &message, const QString &iconName, int timeout) +{ + if (!kwinApp()->shouldUseWaylandForCompositing()) { + // FIXME: only supported on Wayland + return; + } + auto notification = osd(); + notification->setIconName(iconName); + notification->setMessage(message); + notification->setTimeout(timeout); + notification->setVisible(true); +} + +void show(const QString &message, int timeout) +{ + show(message, QString(), timeout); +} + +void show(const QString &message, const QString &iconName) +{ + show(message, iconName, 0); +} + +void hide(HideFlags flags) +{ + if (!kwinApp()->shouldUseWaylandForCompositing()) { + // FIXME: only supported on Wayland + return; + } + osd()->setSkipCloseAnimation(flags.testFlag(HideFlag::SkipCloseAnimation)); + osd()->setVisible(false); +} + +} +} diff --git a/osd.h b/osd.h new file mode 100644 index 0000000..fdafb50 --- /dev/null +++ b/osd.h @@ -0,0 +1,31 @@ +/* + SPDX-FileCopyrightText: 2016 Martin Graesslin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +*/ + +#ifndef KWIN_OSD_H +#define KWIN_OSD_H + +#include +#include + +namespace KWin +{ +namespace OSD +{ + +void show(const QString &message, const QString &iconName = QString()); +void show(const QString &message, int timeout); +void show(const QString &message, const QString &iconName, int timeout); +enum class HideFlag { + SkipCloseAnimation = 1 +}; +Q_DECLARE_FLAGS(HideFlags, HideFlag) +void hide(HideFlags flags = HideFlags()); + +} +} + +#endif diff --git a/outline.cpp b/outline.cpp new file mode 100644 index 0000000..66415d1 --- /dev/null +++ b/outline.cpp @@ -0,0 +1,178 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2011 Arthur Arlt + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +// own +#include "outline.h" +// KWin +#include "composite.h" +#include "main.h" +#include "platform.h" +#include "scripting/scripting.h" +#include "utils.h" +// Frameworks +#include +// Qt +#include +#include +#include +#include +#include +#include + +namespace KWin { + +KWIN_SINGLETON_FACTORY(Outline) + +Outline::Outline(QObject *parent) + : QObject(parent) + , m_active(false) +{ + connect(Compositor::self(), SIGNAL(compositingToggled(bool)), SLOT(compositingChanged())); +} + +Outline::~Outline() +{ +} + +void Outline::show() +{ + if (m_visual.isNull()) { + createHelper(); + } + if (m_visual.isNull()) { + // something went wrong + return; + } + m_visual->show(); + m_active = true; + emit activeChanged(); +} + +void Outline::hide() +{ + if (!m_active) { + return; + } + m_active = false; + emit activeChanged(); + if (m_visual.isNull()) { + return; + } + m_visual->hide(); +} + +void Outline::show(const QRect& outlineGeometry) +{ + show(outlineGeometry, QRect()); +} + +void Outline::show(const QRect &outlineGeometry, const QRect &visualParentGeometry) +{ + setGeometry(outlineGeometry); + setVisualParentGeometry(visualParentGeometry); + show(); +} + +void Outline::setGeometry(const QRect& outlineGeometry) +{ + if (m_outlineGeometry == outlineGeometry) { + return; + } + m_outlineGeometry = outlineGeometry; + emit geometryChanged(); + emit unifiedGeometryChanged(); +} + +void Outline::setVisualParentGeometry(const QRect &visualParentGeometry) +{ + if (m_visualParentGeometry == visualParentGeometry) { + return; + } + m_visualParentGeometry = visualParentGeometry; + emit visualParentGeometryChanged(); + emit unifiedGeometryChanged(); +} + +QRect Outline::unifiedGeometry() const +{ + return m_outlineGeometry | m_visualParentGeometry; +} + +void Outline::createHelper() +{ + if (!m_visual.isNull()) { + return; + } + m_visual.reset(kwinApp()->platform()->createOutline(this)); +} + +void Outline::compositingChanged() +{ + m_visual.reset(); + if (m_active) { + show(); + } +} + +OutlineVisual::OutlineVisual(Outline *outline) + : m_outline(outline) +{ +} + +OutlineVisual::~OutlineVisual() +{ +} + +CompositedOutlineVisual::CompositedOutlineVisual(Outline *outline) + : OutlineVisual(outline) + , m_qmlContext() + , m_qmlComponent() + , m_mainItem() +{ +} + +CompositedOutlineVisual::~CompositedOutlineVisual() +{ +} + +void CompositedOutlineVisual::hide() +{ + if (QQuickWindow *w = qobject_cast(m_mainItem.data())) { + w->hide(); + w->destroy(); + } +} + +void CompositedOutlineVisual::show() +{ + if (m_qmlContext.isNull()) { + m_qmlContext.reset(new QQmlContext(Scripting::self()->qmlEngine())); + m_qmlContext->setContextProperty(QStringLiteral("outline"), outline()); + } + if (m_qmlComponent.isNull()) { + m_qmlComponent.reset(new QQmlComponent(Scripting::self()->qmlEngine())); + const QString fileName = QStandardPaths::locate(QStandardPaths::GenericDataLocation, + kwinApp()->config()->group(QStringLiteral("Outline")).readEntry("QmlPath", QStringLiteral(KWIN_NAME "/outline/plasma/outline.qml"))); + if (fileName.isEmpty()) { + qCDebug(KWIN_CORE) << "Could not locate outline.qml"; + return; + } + m_qmlComponent->loadUrl(QUrl::fromLocalFile(fileName)); + if (m_qmlComponent->isError()) { + qCDebug(KWIN_CORE) << "Component failed to load: " << m_qmlComponent->errors(); + } else { + m_mainItem.reset(m_qmlComponent->create(m_qmlContext.data())); + } + if (auto w = qobject_cast(m_mainItem.data())) { + w->setProperty("__kwin_outline", true); + } + } +} + +} // namespace diff --git a/outline.h b/outline.h new file mode 100644 index 0000000..f3117bb --- /dev/null +++ b/outline.h @@ -0,0 +1,183 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2011 Arthur Arlt + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_OUTLINE_H +#define KWIN_OUTLINE_H +#include +#include +#include + +#include + +class QQmlContext; +class QQmlComponent; + +namespace KWin { +class OutlineVisual; + +/** + * @short This class is used to show the outline of a given geometry. + * + * The class renders an outline by using four windows. One for each border of + * the geometry. It is possible to replace the outline with an effect. If an + * effect is available the effect will be used, otherwise the outline will be + * rendered by using the X implementation. + * + * @author Arthur Arlt + * @since 4.7 + */ +class Outline : public QObject { + Q_OBJECT + Q_PROPERTY(QRect geometry READ geometry NOTIFY geometryChanged) + Q_PROPERTY(QRect visualParentGeometry READ visualParentGeometry NOTIFY visualParentGeometryChanged) + Q_PROPERTY(QRect unifiedGeometry READ unifiedGeometry NOTIFY unifiedGeometryChanged) + Q_PROPERTY(bool active READ isActive NOTIFY activeChanged) +public: + ~Outline() override; + + /** + * Set the outline geometry. + * To show the outline use showOutline. + * @param outlineGeometry The geometry of the outline to be shown + * @see showOutline + */ + void setGeometry(const QRect &outlineGeometry); + + /** + * Set the visual parent geometry. + * This is the geometry from which the will emerge. + * @param visualParentGeometry The visual geometry of the visual parent + * @see showOutline + */ + void setVisualParentGeometry(const QRect &visualParentGeometry); + + /** + * Shows the outline of a window using either an effect or the X implementation. + * To stop the outline process use hideOutline. + * @see hideOutline + */ + void show(); + + /** + * Shows the outline for the given @p outlineGeometry. + * This is the same as setOutlineGeometry followed by showOutline directly. + * To stop the outline process use hideOutline. + * @param outlineGeometry The geometry of the outline to be shown + * @see hideOutline + */ + void show(const QRect &outlineGeometry); + + /** + * Shows the outline for the given @p outlineGeometry animated from @p visualParentGeometry. + * This is the same as setOutlineGeometry followed by setVisualParentGeometry + * and then showOutline. + * To stop the outline process use hideOutline. + * @param outlineGeometry The geometry of the outline to be shown + * @param visualParentGeometry The geometry from where the outline should emerge + * @see hideOutline + * @since 5.10 + */ + void show(const QRect &outlineGeometry, const QRect &visualParentGeometry); + + /** + * Hides shown outline. + * @see showOutline + */ + void hide(); + + const QRect &geometry() const; + const QRect &visualParentGeometry() const; + QRect unifiedGeometry() const; + + bool isActive() const; + +private Q_SLOTS: + void compositingChanged(); + +Q_SIGNALS: + void activeChanged(); + void geometryChanged(); + void unifiedGeometryChanged(); + void visualParentGeometryChanged(); + +private: + void createHelper(); + QScopedPointer m_visual; + QRect m_outlineGeometry; + QRect m_visualParentGeometry; + bool m_active; + KWIN_SINGLETON(Outline) +}; + +class KWIN_EXPORT OutlineVisual +{ +public: + OutlineVisual(Outline *outline); + virtual ~OutlineVisual(); + virtual void show() = 0; + virtual void hide() = 0; +protected: + Outline *outline(); + const Outline *outline() const; +private: + Outline *m_outline; +}; + +class CompositedOutlineVisual : public OutlineVisual +{ +public: + CompositedOutlineVisual(Outline *outline); + ~CompositedOutlineVisual() override; + void show() override; + void hide() override; +private: + QScopedPointer m_qmlContext; + QScopedPointer m_qmlComponent; + QScopedPointer m_mainItem; +}; + +inline +bool Outline::isActive() const +{ + return m_active; +} + +inline +const QRect &Outline::geometry() const +{ + return m_outlineGeometry; +} + +inline +const QRect &Outline::visualParentGeometry() const +{ + return m_visualParentGeometry; +} + +inline +Outline *OutlineVisual::outline() +{ + return m_outline; +} + +inline +const Outline *OutlineVisual::outline() const +{ + return m_outline; +} + +inline +Outline *outline() +{ + return Outline::self(); +} + +} + +#endif diff --git a/outputscreens.cpp b/outputscreens.cpp new file mode 100644 index 0000000..94373af --- /dev/null +++ b/outputscreens.cpp @@ -0,0 +1,119 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "outputscreens.h" +#include "platform.h" +#include "abstract_output.h" + +namespace KWin +{ + +OutputScreens::OutputScreens(Platform *platform, QObject *parent) + : Screens(parent), + m_platform(platform) +{ +} + +OutputScreens::~OutputScreens() = default; + +void OutputScreens::init() +{ + updateCount(); + KWin::Screens::init(); + emit changed(); +} + +QString OutputScreens::name(int screen) const +{ + if (AbstractOutput *output = findOutput(screen)) { + return output->name(); + } + return QString(); +} + +bool OutputScreens::isInternal(int screen) const +{ + if (AbstractOutput *output = findOutput(screen)) { + return output->isInternal(); + } + return false; +} + +QRect OutputScreens::geometry(int screen) const +{ + if (AbstractOutput *output = findOutput(screen)) { + return output->geometry(); + } + return QRect(); +} + +QSize OutputScreens::size(int screen) const +{ + if (AbstractOutput *output = findOutput(screen)) { + return output->geometry().size(); + } + return QSize(); +} + +qreal OutputScreens::scale(int screen) const +{ + if (AbstractOutput *output = findOutput(screen)) { + return output->scale(); + } + return 1.0; +} + +QSizeF OutputScreens::physicalSize(int screen) const +{ + if (AbstractOutput *output = findOutput(screen)) { + return output->physicalSize(); + } + return QSizeF(); +} + +float OutputScreens::refreshRate(int screen) const +{ + if (AbstractOutput *output = findOutput(screen)) { + return output->refreshRate() / 1000.0; + } + return 60.0; +} + +void OutputScreens::updateCount() +{ + setCount(m_platform->enabledOutputs().size()); +} + +int OutputScreens::number(const QPoint &pos) const +{ + int bestScreen = 0; + int minDistance = INT_MAX; + const auto outputs = m_platform->enabledOutputs(); + for (int i = 0; i < outputs.size(); ++i) { + const QRect &geo = outputs[i]->geometry(); + if (geo.contains(pos)) { + return i; + } + int distance = QPoint(geo.topLeft() - pos).manhattanLength(); + distance = qMin(distance, QPoint(geo.topRight() - pos).manhattanLength()); + distance = qMin(distance, QPoint(geo.bottomRight() - pos).manhattanLength()); + distance = qMin(distance, QPoint(geo.bottomLeft() - pos).manhattanLength()); + if (distance < minDistance) { + minDistance = distance; + bestScreen = i; + } + } + return bestScreen; +} + +AbstractOutput *OutputScreens::findOutput(int screen) const +{ + return m_platform->findOutput(screen); +} + +} // namespace diff --git a/outputscreens.h b/outputscreens.h new file mode 100644 index 0000000..71dcaaf --- /dev/null +++ b/outputscreens.h @@ -0,0 +1,49 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_OUTPUTSCREENS_H +#define KWIN_OUTPUTSCREENS_H + +#include "screens.h" + +namespace KWin +{ + +class AbstractOutput; + +/** + * @brief Implementation for backends with Outputs + */ +class KWIN_EXPORT OutputScreens : public Screens +{ + Q_OBJECT +public: + OutputScreens(Platform *platform, QObject *parent = nullptr); + ~OutputScreens() override; + + void init() override; + QString name(int screen) const override; + bool isInternal(int screen) const override; + QSizeF physicalSize(int screen) const override; + QRect geometry(int screen) const override; + QSize size(int screen) const override; + qreal scale(int screen) const override; + float refreshRate(int screen) const override; + void updateCount() override; + int number(const QPoint &pos) const override; + +protected: + Platform *m_platform; + +private: + AbstractOutput *findOutput(int screen) const; +}; + +} + +#endif // KWIN_OUTPUTSCREENS_H diff --git a/overlaywindow.cpp b/overlaywindow.cpp new file mode 100644 index 0000000..094ab51 --- /dev/null +++ b/overlaywindow.cpp @@ -0,0 +1,21 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2011 Arthur Arlt + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "overlaywindow.h" + +namespace KWin { +OverlayWindow::OverlayWindow() +{ +} + +OverlayWindow::~OverlayWindow() +{ +} + +} // namespace KWin diff --git a/overlaywindow.h b/overlaywindow.h new file mode 100644 index 0000000..4b0c4d6 --- /dev/null +++ b/overlaywindow.h @@ -0,0 +1,41 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2011 Arthur Arlt + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_OVERLAYWINDOW_H +#define KWIN_OVERLAYWINDOW_H + +#include +// xcb +#include + +#include + +namespace KWin { +class KWIN_EXPORT OverlayWindow { +public: + virtual ~OverlayWindow(); + /// Creates XComposite overlay window, call initOverlay() afterwards + virtual bool create() = 0; + /// Init overlay and the destination window in it + virtual void setup(xcb_window_t window) = 0; + virtual void show() = 0; + virtual void hide() = 0; // hides and resets overlay window + virtual void setShape(const QRegion& reg) = 0; + virtual void resize(const QSize &size) = 0; + /// Destroys XComposite overlay window + virtual void destroy() = 0; + virtual xcb_window_t window() const = 0; + virtual bool isVisible() const = 0; + virtual void setVisibility(bool visible) = 0; +protected: + OverlayWindow(); +}; +} // namespace + +#endif //KWIN_OVERLAYWINDOW_H diff --git a/placement.cpp b/placement.cpp new file mode 100644 index 0000000..590903c --- /dev/null +++ b/placement.cpp @@ -0,0 +1,985 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 1997-2002 Cristian Tibirna + SPDX-FileCopyrightText: 2003 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "placement.h" + +#ifndef KCMRULES +#include "workspace.h" +#include "x11client.h" +#include "cursor.h" +#include "options.h" +#include "rules.h" +#include "screens.h" +#endif + +#include +#include +#include + +namespace KWin +{ + +#ifndef KCMRULES + +KWIN_SINGLETON_FACTORY(Placement) + +Placement::Placement(QObject*) +{ + reinitCascading(0); +} + +Placement::~Placement() +{ + s_self = nullptr; +} + +/** + * Places the client \a c according to the workspace's layout policy + */ +void Placement::place(AbstractClient *c, const QRect &area) +{ + Policy policy = c->rules()->checkPlacement(Default); + if (policy != Default) { + place(c, area, policy); + return; + } + + if (c->isUtility()) + placeUtility(c, area, options->placement()); + else if (c->isDialog()) + placeDialog(c, area, options->placement()); + else if (c->isSplash()) + placeOnMainWindow(c, area); // on mainwindow, if any, otherwise centered + else if (c->isOnScreenDisplay() || c->isNotification() || c->isCriticalNotification()) + placeOnScreenDisplay(c, area); + else if (c->isTransient() && c->hasTransientPlacementHint()) + placeTransient(c); + else if (c->isTransient() && c->surface()) + placeDialog(c, area, options->placement()); + else + place(c, area, options->placement()); +} + +void Placement::place(AbstractClient *c, const QRect &area, Policy policy, Policy nextPlacement) +{ + if (policy == Unknown) + policy = Default; + if (policy == Default) + policy = options->placement(); + if (policy == NoPlacement) + return; + else if (policy == Random) + placeAtRandom(c, area, nextPlacement); + else if (policy == Cascade) + placeCascaded(c, area, nextPlacement); + else if (policy == Centered) + placeCentered(c, area, nextPlacement); + else if (policy == ZeroCornered) + placeZeroCornered(c, area, nextPlacement); + else if (policy == UnderMouse) + placeUnderMouse(c, area, nextPlacement); + else if (policy == OnMainWindow) + placeOnMainWindow(c, area, nextPlacement); + else if (policy == Maximizing) + placeMaximizing(c, area, nextPlacement); + else + placeSmart(c, area, nextPlacement); + + if (options->borderSnapZone()) { + // snap to titlebar / snap to window borders on inner screen edges + const QRect geo(c->frameGeometry()); + QPoint corner = geo.topLeft(); + const QMargins frameMargins = c->frameMargins(); + AbstractClient::Position titlePos = c->titlebarPosition(); + + const QRect fullRect = workspace()->clientArea(FullArea, c); + if (!(c->maximizeMode() & MaximizeHorizontal)) { + if (titlePos != AbstractClient::PositionRight && geo.right() == fullRect.right()) { + corner.rx() += frameMargins.right(); + } + if (titlePos != AbstractClient::PositionLeft && geo.left() == fullRect.left()) { + corner.rx() -= frameMargins.left(); + } + } + if (!(c->maximizeMode() & MaximizeVertical)) { + if (titlePos != AbstractClient::PositionBottom && geo.bottom() == fullRect.bottom()) { + corner.ry() += frameMargins.bottom(); + } + if (titlePos != AbstractClient::PositionTop && geo.top() == fullRect.top()) { + corner.ry() -= frameMargins.top(); + } + } + c->move(corner); + } +} + +/** + * Place the client \a c according to a simply "random" placement algorithm. + */ +void Placement::placeAtRandom(AbstractClient* c, const QRect& area, Policy /*next*/) +{ + Q_ASSERT(area.isValid()); + + const int step = 24; + static int px = step; + static int py = 2 * step; + int tx, ty; + + if (px < area.x()) { + px = area.x(); + } + if (py < area.y()) { + py = area.y(); + } + + px += step; + py += 2 * step; + + if (px > area.width() / 2) { + px = area.x() + step; + } + if (py > area.height() / 2) { + py = area.y() + step; + } + tx = px; + ty = py; + if (tx + c->width() > area.right()) { + tx = area.right() - c->width(); + if (tx < 0) + tx = 0; + px = area.x(); + } + if (ty + c->height() > area.bottom()) { + ty = area.bottom() - c->height(); + if (ty < 0) + ty = 0; + py = area.y(); + } + c->move(tx, ty); +} + +// TODO: one day, there'll be C++11 ... +static inline bool isIrrelevant(const AbstractClient *client, const AbstractClient *regarding, int desktop) +{ + if (!client) + return true; + if (client == regarding) + return true; + if (!client->isShown(false)) + return true; + if (!client->isOnDesktop(desktop)) + return true; + if (!client->isOnCurrentActivity()) + return true; + if (client->isDesktop()) + return true; + return false; +} + +/** + * Place the client \a c according to a really smart placement algorithm :-) + */ +void Placement::placeSmart(AbstractClient* c, const QRect& area, Policy /*next*/) +{ + Q_ASSERT(area.isValid()); + + /* + * SmartPlacement by Cristian Tibirna (tibirna@kde.org) + * adapted for kwm (16-19jan98) and for kwin (16Nov1999) using (with + * permission) ideas from fvwm, authored by + * Anthony Martin (amartin@engr.csulb.edu). + * Xinerama supported added by Balaji Ramani (balaji@yablibli.com) + * with ideas from xfce. + */ + + if (!c->frameGeometry().isValid()) { + return; + } + + const int none = 0, h_wrong = -1, w_wrong = -2; // overlap types + long int overlap, min_overlap = 0; + int x_optimal, y_optimal; + int possible; + int desktop = c->desktop() == 0 || c->isOnAllDesktops() ? VirtualDesktopManager::self()->current() : c->desktop(); + + int cxl, cxr, cyt, cyb; //temp coords + int xl, xr, yt, yb; //temp coords + int basket; //temp holder + + // get the maximum allowed windows space + int x = area.left(); + int y = area.top(); + x_optimal = x; y_optimal = y; + + //client gabarit + int ch = c->height() - 1; + int cw = c->width() - 1; + + bool first_pass = true; //CT lame flag. Don't like it. What else would do? + + //loop over possible positions + do { + //test if enough room in x and y directions + if (y + ch > area.bottom() && ch < area.height()) { + overlap = h_wrong; // this throws the algorithm to an exit + } else if (x + cw > area.right()) { + overlap = w_wrong; + } else { + overlap = none; //initialize + + cxl = x; cxr = x + cw; + cyt = y; cyb = y + ch; + for (auto l = workspace()->stackingOrder().constBegin(); l != workspace()->stackingOrder().constEnd() ; ++l) { + AbstractClient *client = qobject_cast(*l); + if (isIrrelevant(client, c, desktop)) { + continue; + } + xl = client->x(); yt = client->y(); + xr = xl + client->width(); yb = yt + client->height(); + + //if windows overlap, calc the overall overlapping + if ((cxl < xr) && (cxr > xl) && + (cyt < yb) && (cyb > yt)) { + xl = qMax(cxl, xl); xr = qMin(cxr, xr); + yt = qMax(cyt, yt); yb = qMin(cyb, yb); + if (client->keepAbove()) + overlap += 16 * (xr - xl) * (yb - yt); + else if (client->keepBelow() && !client->isDock()) // ignore KeepBelow windows + overlap += 0; // for placement (see X11Client::belongsToLayer() for Dock) + else + overlap += (xr - xl) * (yb - yt); + } + } + } + + //CT first time we get no overlap we stop. + if (overlap == none) { + x_optimal = x; + y_optimal = y; + break; + } + + if (first_pass) { + first_pass = false; + min_overlap = overlap; + } + //CT save the best position and the minimum overlap up to now + else if (overlap >= none && overlap < min_overlap) { + min_overlap = overlap; + x_optimal = x; + y_optimal = y; + } + + // really need to loop? test if there's any overlap + if (overlap > none) { + + possible = area.right(); + if (possible - cw > x) possible -= cw; + + // compare to the position of each client on the same desk + for (auto l = workspace()->stackingOrder().constBegin(); l != workspace()->stackingOrder().constEnd() ; ++l) { + AbstractClient *client = qobject_cast(*l); + if (isIrrelevant(client, c, desktop)) { + continue; + } + + xl = client->x(); yt = client->y(); + xr = xl + client->width(); yb = yt + client->height(); + + // if not enough room above or under the current tested client + // determine the first non-overlapped x position + if ((y < yb) && (yt < ch + y)) { + + if ((xr > x) && (possible > xr)) possible = xr; + + basket = xl - cw; + if ((basket > x) && (possible > basket)) possible = basket; + } + } + x = possible; + } + + // ... else ==> not enough x dimension (overlap was wrong on horizontal) + else if (overlap == w_wrong) { + x = area.left(); + possible = area.bottom(); + + if (possible - ch > y) possible -= ch; + + //test the position of each window on the desk + for (auto l = workspace()->stackingOrder().constBegin(); l != workspace()->stackingOrder().constEnd() ; ++l) { + AbstractClient *client = qobject_cast(*l); + if (isIrrelevant(client, c, desktop)) { + continue; + } + + xl = client->x(); yt = client->y(); + xr = xl + client->width(); yb = yt + client->height(); + + // if not enough room to the left or right of the current tested client + // determine the first non-overlapped y position + if ((yb > y) && (possible > yb)) possible = yb; + + basket = yt - ch; + if ((basket > y) && (possible > basket)) possible = basket; + } + y = possible; + } + } while ((overlap != none) && (overlap != h_wrong) && (y < area.bottom())); + + if (ch >= area.height()) { + y_optimal = area.top(); + } + + // place the window + c->move(x_optimal, y_optimal); + +} + +void Placement::reinitCascading(int desktop) +{ + // desktop == 0 - reinit all + if (desktop == 0) { + cci.clear(); + for (uint i = 0; i < VirtualDesktopManager::self()->count(); ++i) { + DesktopCascadingInfo inf; + inf.pos = QPoint(-1, -1); + inf.col = 0; + inf.row = 0; + cci.append(inf); + } + } else { + cci[desktop - 1].pos = QPoint(-1, -1); + cci[desktop - 1].col = cci[desktop - 1].row = 0; + } +} + +QPoint Workspace::cascadeOffset(const AbstractClient *c) const +{ + QRect area = clientArea(PlacementArea, c->frameGeometry().center(), c->desktop()); + return QPoint(area.width()/48, area.height()/48); +} + +/** + * Place windows in a cascading order, remembering positions for each desktop + */ +void Placement::placeCascaded(AbstractClient *c, const QRect &area, Policy nextPlacement) +{ + Q_ASSERT(area.isValid()); + + if (!c->frameGeometry().isValid()) { + return; + } + + /* cascadePlacement by Cristian Tibirna (tibirna@kde.org) (30Jan98) + */ + // work coords + int xp, yp; + + //CT how do I get from the 'Client' class the size that NW squarish "handle" + const QPoint delta = workspace()->cascadeOffset(c); + + const int dn = c->desktop() == 0 || c->isOnAllDesktops() ? (VirtualDesktopManager::self()->current() - 1) : (c->desktop() - 1); + + // initialize often used vars: width and height of c; we gain speed + const int ch = c->height(); + const int cw = c->width(); + const int X = area.left(); + const int Y = area.top(); + const int H = area.height(); + const int W = area.width(); + + if (nextPlacement == Unknown) + nextPlacement = Smart; + + //initialize if needed + if (cci[dn].pos.x() < 0 || cci[dn].pos.x() < X || cci[dn].pos.y() < Y) { + cci[dn].pos = QPoint(X, Y); + cci[dn].col = cci[dn].row = 0; + } + + + xp = cci[dn].pos.x(); + yp = cci[dn].pos.y(); + + //here to touch in case people vote for resize on placement + if ((yp + ch) > H) yp = Y; + + if ((xp + cw) > W) { + if (!yp) { + place(c, area, nextPlacement); + return; + } else xp = X; + } + + //if this isn't the first window + if (cci[dn].pos.x() != X && cci[dn].pos.y() != Y) { + /* The following statements cause an internal compiler error with + * egcs-2.91.66 on SuSE Linux 6.3. The equivalent forms compile fine. + * 22-Dec-1999 CS + * + * if (xp != X && yp == Y) xp = delta.x() * (++(cci[dn].col)); + * if (yp != Y && xp == X) yp = delta.y() * (++(cci[dn].row)); + */ + if (xp != X && yp == Y) { + ++(cci[dn].col); + xp = delta.x() * cci[dn].col; + } + if (yp != Y && xp == X) { + ++(cci[dn].row); + yp = delta.y() * cci[dn].row; + } + + // last resort: if still doesn't fit, smart place it + if (((xp + cw) > W - X) || ((yp + ch) > H - Y)) { + place(c, area, nextPlacement); + return; + } + } + + // place the window + c->move(QPoint(xp, yp)); + + // new position + cci[dn].pos = QPoint(xp + delta.x(), yp + delta.y()); +} + +/** + * Place windows centered, on top of all others + */ +void Placement::placeCentered(AbstractClient* c, const QRect& area, Policy /*next*/) +{ + Q_ASSERT(area.isValid()); + + const int xp = area.left() + (area.width() - c->width()) / 2; + const int yp = area.top() + (area.height() - c->height()) / 2; + + // place the window + c->move(QPoint(xp, yp)); +} + +/** + * Place windows in the (0,0) corner, on top of all others + */ +void Placement::placeZeroCornered(AbstractClient* c, const QRect& area, Policy /*next*/) +{ + Q_ASSERT(area.isValid()); + + // get the maximum allowed windows space and desk's origin + c->move(area.topLeft()); +} + +void Placement::placeUtility(AbstractClient *c, const QRect &area, Policy /*next*/) +{ +// TODO kwin should try to place utility windows next to their mainwindow, +// preferably at the right edge, and going down if there are more of them +// if there's not enough place outside the mainwindow, it should prefer +// top-right corner + // use the default placement for now + place(c, area, Default); +} + +void Placement::placeOnScreenDisplay(AbstractClient *c, const QRect &area) +{ + Q_ASSERT(area.isValid()); + + // place at lower area of the screen + const int x = area.left() + (area.width() - c->width()) / 2; + const int y = area.top() + 2 * area.height() / 3 - c->height() / 2; + + c->move(QPoint(x, y)); +} + +void Placement::placeTransient(AbstractClient *c) +{ + const auto parent = c->transientFor(); + const QRect screen = Workspace::self()->clientArea(parent->isFullScreen() ? FullScreenArea : PlacementArea, parent); + const QRect popupGeometry = c->transientPlacement(screen); + c->setFrameGeometry(popupGeometry); + + + // Potentially a client could set no constraint adjustments + // and we'll be offscreen. + + // The spec implies we should place window the offscreen. However, + // practically Qt doesn't set any constraint adjustments yet so we can't. + // Also kwin generally doesn't let clients do what they want + if (!screen.contains(c->frameGeometry())) { + c->keepInArea(screen); + } +} + +void Placement::placeDialog(AbstractClient *c, const QRect &area, Policy nextPlacement) +{ + placeOnMainWindow(c, area, nextPlacement); +} + +void Placement::placeUnderMouse(AbstractClient *c, const QRect &area, Policy /*next*/) +{ + Q_ASSERT(area.isValid()); + + QRect geom = c->frameGeometry(); + geom.moveCenter(Cursors::self()->mouse()->pos()); + c->move(geom.topLeft()); + c->keepInArea(area); // make sure it's kept inside workarea +} + +void Placement::placeOnMainWindow(AbstractClient *c, const QRect &area, Policy nextPlacement) +{ + Q_ASSERT(area.isValid()); + + if (nextPlacement == Unknown) + nextPlacement = Centered; + if (nextPlacement == Maximizing) // maximize if needed + placeMaximizing(c, area, NoPlacement); + auto mainwindows = c->mainClients(); + AbstractClient* place_on = nullptr; + AbstractClient* place_on2 = nullptr; + int mains_count = 0; + for (auto it = mainwindows.constBegin(); + it != mainwindows.constEnd(); + ++it) { + if (mainwindows.count() > 1 && (*it)->isSpecialWindow()) + continue; // don't consider toolbars etc when placing + ++mains_count; + place_on2 = *it; + if ((*it)->isOnCurrentDesktop()) { + if (place_on == nullptr) + place_on = *it; + else { + // two or more on current desktop -> center + // That's the default at least. However, with maximizing placement + // policy as the default, the dialog should be either maximized or + // made as large as its maximum size and then placed centered. + // So the nextPlacement argument allows chaining. In this case, nextPlacement + // is Maximizing and it will call placeCentered(). + place(c, area, Centered); + return; + } + } + } + if (place_on == nullptr) { + // 'mains_count' is used because it doesn't include ignored mainwindows + if (mains_count != 1) { + place(c, area, Centered); + return; + } + place_on = place_on2; // use the only window filtered together with 'mains_count' + } + if (place_on->isDesktop()) { + place(c, area, Centered); + return; + } + QRect geom = c->frameGeometry(); + geom.moveCenter(place_on->frameGeometry().center()); + c->move(geom.topLeft()); + // get area again, because the mainwindow may be on different xinerama screen + const QRect placementArea = workspace()->clientArea(PlacementArea, c); + c->keepInArea(placementArea); // make sure it's kept inside workarea +} + +void Placement::placeMaximizing(AbstractClient *c, const QRect &area, Policy nextPlacement) +{ + Q_ASSERT(area.isValid()); + + if (nextPlacement == Unknown) + nextPlacement = Smart; + if (c->isMaximizable() && c->maxSize().width() >= area.width() && c->maxSize().height() >= area.height()) { + if (workspace()->clientArea(MaximizeArea, c) == area) + c->maximize(MaximizeFull); + else { // if the geometry doesn't match default maximize area (xinerama case?), + // it's probably better to use the given area + c->setFrameGeometry(area); + } + } else { + c->resizeWithChecks(c->maxSize().boundedTo(area.size())); + place(c, area, nextPlacement); + } +} + +void Placement::cascadeDesktop() +{ + Workspace *ws = Workspace::self(); + const int desktop = VirtualDesktopManager::self()->current(); + reinitCascading(desktop); + foreach (Toplevel *toplevel, ws->stackingOrder()) { + auto client = qobject_cast(toplevel); + if (!client || + (!client->isOnCurrentDesktop()) || + (client->isMinimized()) || + (client->isOnAllDesktops()) || + (!client->isMovable())) + continue; + const QRect placementArea = workspace()->clientArea(PlacementArea, client); + placeCascaded(client, placementArea); + } +} + +void Placement::unclutterDesktop() +{ + const auto &clients = Workspace::self()->allClientList(); + for (int i = clients.size() - 1; i >= 0; i--) { + auto client = clients.at(i); + if ((!client->isOnCurrentDesktop()) || + (client->isMinimized()) || + (client->isOnAllDesktops()) || + (!client->isMovable())) + continue; + const QRect placementArea = workspace()->clientArea(PlacementArea, client); + placeSmart(client, placementArea); + } +} + +#endif + + +Placement::Policy Placement::policyFromString(const QString& policy, bool no_special) +{ + if (policy == QStringLiteral("NoPlacement")) + return NoPlacement; + else if (policy == QStringLiteral("Default") && !no_special) + return Default; + else if (policy == QStringLiteral("Random")) + return Random; + else if (policy == QStringLiteral("Cascade")) + return Cascade; + else if (policy == QStringLiteral("Centered")) + return Centered; + else if (policy == QStringLiteral("ZeroCornered")) + return ZeroCornered; + else if (policy == QStringLiteral("UnderMouse")) + return UnderMouse; + else if (policy == QStringLiteral("OnMainWindow") && !no_special) + return OnMainWindow; + else if (policy == QStringLiteral("Maximizing")) + return Maximizing; + else + return Smart; +} + +const char* Placement::policyToString(Policy policy) +{ + const char* const policies[] = { + "NoPlacement", "Default", "XXX should never see", "Random", "Smart", "Cascade", "Centered", + "ZeroCornered", "UnderMouse", "OnMainWindow", "Maximizing" + }; + Q_ASSERT(policy < int(sizeof(policies) / sizeof(policies[ 0 ]))); + return policies[ policy ]; +} + + +#ifndef KCMRULES + +// ******************** +// Workspace +// ******************** + +void AbstractClient::packTo(int left, int top) +{ + workspace()->updateFocusMousePosition(Cursors::self()->mouse()->pos()); // may cause leave event; + + const int oldScreen = screen(); + move(left, top); + if (screen() != oldScreen) { + workspace()->sendClientToScreen(this, screen()); // checks rule validity + if (maximizeMode() != MaximizeRestore) + checkWorkspacePosition(); + } +} + +/** + * Moves active window left until in bumps into another window or workarea edge. + */ +void Workspace::slotWindowPackLeft() +{ + if (active_client && active_client->isMovable()) + active_client->packTo(packPositionLeft(active_client, active_client->frameGeometry().left(), true), + active_client->y()); +} + +void Workspace::slotWindowPackRight() +{ + if (active_client && active_client->isMovable()) + active_client->packTo(packPositionRight(active_client, active_client->frameGeometry().right(), true) + - active_client->width() + 1, active_client->y()); +} + +void Workspace::slotWindowPackUp() +{ + if (active_client && active_client->isMovable()) + active_client->packTo(active_client->x(), + packPositionUp(active_client, active_client->frameGeometry().top(), true)); +} + +void Workspace::slotWindowPackDown() +{ + if (active_client && active_client->isMovable()) + active_client->packTo(active_client->x(), + packPositionDown(active_client, active_client->frameGeometry().bottom(), true) - active_client->height() + 1); +} + +void Workspace::slotWindowGrowHorizontal() +{ + if (active_client) + active_client->growHorizontal(); +} + +void AbstractClient::growHorizontal() +{ + if (!isResizable() || isShade()) + return; + QRect geom = frameGeometry(); + geom.setRight(workspace()->packPositionRight(this, geom.right(), true)); + QSize adjsize = constrainFrameSize(geom.size(), SizeModeFixedW); + if (frameGeometry().size() == adjsize && geom.size() != adjsize && resizeIncrements().width() > 1) { // take care of size increments + int newright = workspace()->packPositionRight(this, geom.right() + resizeIncrements().width() - 1, true); + // check that it hasn't grown outside of the area, due to size increments + // TODO this may be wrong? + if (workspace()->clientArea(MovementArea, + QPoint((x() + newright) / 2, frameGeometry().center().y()), desktop()).right() >= newright) + geom.setRight(newright); + } + geom.setSize(constrainFrameSize(geom.size(), SizeModeFixedW)); + geom.setSize(constrainFrameSize(geom.size(), SizeModeFixedH)); + workspace()->updateFocusMousePosition(Cursors::self()->mouse()->pos()); // may cause leave event; + setFrameGeometry(geom); +} + +void Workspace::slotWindowShrinkHorizontal() +{ + if (active_client) + active_client->shrinkHorizontal(); +} + +void AbstractClient::shrinkHorizontal() +{ + if (!isResizable() || isShade()) + return; + QRect geom = frameGeometry(); + geom.setRight(workspace()->packPositionLeft(this, geom.right(), false)); + if (geom.width() <= 1) + return; + geom.setSize(constrainFrameSize(geom.size(), SizeModeFixedW)); + if (geom.width() > 20) { + workspace()->updateFocusMousePosition(Cursors::self()->mouse()->pos()); // may cause leave event; + setFrameGeometry(geom); + } +} + +void Workspace::slotWindowGrowVertical() +{ + if (active_client) + active_client->growVertical(); +} + +void AbstractClient::growVertical() +{ + if (!isResizable() || isShade()) + return; + QRect geom = frameGeometry(); + geom.setBottom(workspace()->packPositionDown(this, geom.bottom(), true)); + QSize adjsize = constrainFrameSize(geom.size(), SizeModeFixedH); + if (frameGeometry().size() == adjsize && geom.size() != adjsize && resizeIncrements().height() > 1) { // take care of size increments + int newbottom = workspace()->packPositionDown(this, geom.bottom() + resizeIncrements().height() - 1, true); + // check that it hasn't grown outside of the area, due to size increments + if (workspace()->clientArea(MovementArea, + QPoint(frameGeometry().center().x(), (y() + newbottom) / 2), desktop()).bottom() >= newbottom) + geom.setBottom(newbottom); + } + geom.setSize(constrainFrameSize(geom.size(), SizeModeFixedH)); + workspace()->updateFocusMousePosition(Cursors::self()->mouse()->pos()); // may cause leave event; + setFrameGeometry(geom); +} + + +void Workspace::slotWindowShrinkVertical() +{ + if (active_client) + active_client->shrinkVertical(); +} + +void AbstractClient::shrinkVertical() +{ + if (!isResizable() || isShade()) + return; + QRect geom = frameGeometry(); + geom.setBottom(workspace()->packPositionUp(this, geom.bottom(), false)); + if (geom.height() <= 1) + return; + geom.setSize(constrainFrameSize(geom.size(), SizeModeFixedH)); + if (geom.height() > 20) { + workspace()->updateFocusMousePosition(Cursors::self()->mouse()->pos()); // may cause leave event; + setFrameGeometry(geom); + } +} + +void Workspace::quickTileWindow(QuickTileMode mode) +{ + if (!active_client) { + return; + } + + // If the user invokes two of these commands in a one second period, try to + // combine them together to enable easy and intuitive corner tiling +#define FLAG(name) QuickTileMode(QuickTileFlag::name) + if (!m_quickTileCombineTimer->isActive()) { + m_quickTileCombineTimer->start(1000); + m_lastTilingMode = mode; + } else { + if ( + ( (m_lastTilingMode == FLAG(Left) || m_lastTilingMode == FLAG(Right)) && (mode == FLAG(Top) || mode == FLAG(Bottom)) ) + || + ( (m_lastTilingMode == FLAG(Top) || m_lastTilingMode == FLAG(Bottom)) && (mode == FLAG(Left) || mode == FLAG(Right)) ) +#undef FLAG + ) { + mode |= m_lastTilingMode; + } + m_quickTileCombineTimer->stop(); + } + + active_client->setQuickTileMode(mode, true); +} + +int Workspace::packPositionLeft(const AbstractClient *client, int oldX, bool leftEdge) const +{ + int newX = clientArea(MaximizeArea, client).left(); + if (oldX <= newX) { // try another Xinerama screen + newX = clientArea(MaximizeArea, + QPoint(client->frameGeometry().left() - 1, client->frameGeometry().center().y()), client->desktop()).left(); + } + if (client->titlebarPosition() != AbstractClient::PositionLeft) { + const int right = newX - client->frameMargins().left(); + QRect frameGeometry = client->frameGeometry(); + frameGeometry.moveRight(right); + if (screens()->intersecting(frameGeometry) < 2) { + newX = right; + } + } + if (oldX <= newX) { + return oldX; + } + const int desktop = client->desktop() == 0 || client->isOnAllDesktops() ? VirtualDesktopManager::self()->current() : client->desktop(); + for (auto it = m_allClients.constBegin(), end = m_allClients.constEnd(); it != end; ++it) { + if (isIrrelevant(*it, client, desktop)) { + continue; + } + const int x = leftEdge ? (*it)->frameGeometry().right() + 1 : (*it)->frameGeometry().left() - 1; + if (x > newX && x < oldX + && !(client->frameGeometry().top() > (*it)->frameGeometry().bottom() // they overlap in Y direction + || client->frameGeometry().bottom() < (*it)->frameGeometry().top())) { + newX = x; + } + } + return newX; +} + +int Workspace::packPositionRight(const AbstractClient *client, int oldX, bool rightEdge) const +{ + int newX = clientArea(MaximizeArea, client).right(); + if (oldX >= newX) { // try another Xinerama screen + newX = clientArea(MaximizeArea, + QPoint(client->frameGeometry().right() + 1, client->frameGeometry().center().y()), client->desktop()).right(); + } + if (client->titlebarPosition() != AbstractClient::PositionRight) { + const int right = newX + client->frameMargins().right(); + QRect frameGeometry = client->frameGeometry(); + frameGeometry.moveRight(right); + if (screens()->intersecting(frameGeometry) < 2) { + newX = right; + } + } + if (oldX >= newX) { + return oldX; + } + const int desktop = client->desktop() == 0 || client->isOnAllDesktops() ? VirtualDesktopManager::self()->current() : client->desktop(); + for (auto it = m_allClients.constBegin(), end = m_allClients.constEnd(); it != end; ++it) { + if (isIrrelevant(*it, client, desktop)) { + continue; + } + const int x = rightEdge ? (*it)->frameGeometry().left() - 1 : (*it)->frameGeometry().right() + 1; + if (x < newX && x > oldX + && !(client->frameGeometry().top() > (*it)->frameGeometry().bottom() + || client->frameGeometry().bottom() < (*it)->frameGeometry().top())) { + newX = x; + } + } + return newX; +} + +int Workspace::packPositionUp(const AbstractClient *client, int oldY, bool topEdge) const +{ + int newY = clientArea(MaximizeArea, client).top(); + if (oldY <= newY) { // try another Xinerama screen + newY = clientArea(MaximizeArea, + QPoint(client->frameGeometry().center().x(), client->frameGeometry().top() - 1), client->desktop()).top(); + } + if (client->titlebarPosition() != AbstractClient::PositionTop) { + const int top = newY - client->frameMargins().top(); + QRect frameGeometry = client->frameGeometry(); + frameGeometry.moveTop(top); + if (screens()->intersecting(frameGeometry) < 2) { + newY = top; + } + } + if (oldY <= newY) { + return oldY; + } + const int desktop = client->desktop() == 0 || client->isOnAllDesktops() ? VirtualDesktopManager::self()->current() : client->desktop(); + for (auto it = m_allClients.constBegin(), end = m_allClients.constEnd(); it != end; ++it) { + if (isIrrelevant(*it, client, desktop)) { + continue; + } + const int y = topEdge ? (*it)->frameGeometry().bottom() + 1 : (*it)->frameGeometry().top() - 1; + if (y > newY && y < oldY + && !(client->frameGeometry().left() > (*it)->frameGeometry().right() // they overlap in X direction + || client->frameGeometry().right() < (*it)->frameGeometry().left())) { + newY = y; + } + } + return newY; +} + +int Workspace::packPositionDown(const AbstractClient *client, int oldY, bool bottomEdge) const +{ + int newY = clientArea(MaximizeArea, client).bottom(); + if (oldY >= newY) { // try another Xinerama screen + newY = clientArea(MaximizeArea, + QPoint(client->frameGeometry().center().x(), client->frameGeometry().bottom() + 1), client->desktop()).bottom(); + } + if (client->titlebarPosition() != AbstractClient::PositionBottom) { + const int bottom = newY + client->frameMargins().bottom(); + QRect frameGeometry = client->frameGeometry(); + frameGeometry.moveBottom(bottom); + if (screens()->intersecting(frameGeometry) < 2) { + newY = bottom; + } + } + if (oldY >= newY) { + return oldY; + } + const int desktop = client->desktop() == 0 || client->isOnAllDesktops() ? VirtualDesktopManager::self()->current() : client->desktop(); + for (auto it = m_allClients.constBegin(), end = m_allClients.constEnd(); it != end; ++it) { + if (isIrrelevant(*it, client, desktop)) { + continue; + } + const int y = bottomEdge ? (*it)->frameGeometry().top() - 1 : (*it)->frameGeometry().bottom() + 1; + if (y < newY && y > oldY + && !(client->frameGeometry().left() > (*it)->frameGeometry().right() + || client->frameGeometry().right() < (*it)->frameGeometry().left())) { + newY = y; + } + } + return newY; +} + +#endif + +} // namespace diff --git a/placement.h b/placement.h new file mode 100644 index 0000000..96482c4 --- /dev/null +++ b/placement.h @@ -0,0 +1,99 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 1997-2002 Cristian Tibirna + SPDX-FileCopyrightText: 2003 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_PLACEMENT_H +#define KWIN_PLACEMENT_H +// KWin +#include +// Qt +#include +#include +#include + +class QObject; + +namespace KWin +{ + +class AbstractClient; + +class KWIN_EXPORT Placement +{ +public: + virtual ~Placement(); + + /** + * Placement policies. How workspace decides the way windows get positioned + * on the screen. The better the policy, the heavier the resource use. + * Normally you don't have to worry. What the WM adds to the startup time + * is nil compared to the creation of the window itself in the memory + */ + enum Policy { + NoPlacement, // not really a placement + Default, // special, means to use the global default + Unknown, // special, means the function should use its default + Random, + Smart, + Cascade, + Centered, + ZeroCornered, + UnderMouse, // special + OnMainWindow, // special + Maximizing + }; + + void place(AbstractClient *c, const QRect &area); + + void placeAtRandom(AbstractClient* c, const QRect& area, Policy next = Unknown); + void placeCascaded(AbstractClient* c, const QRect& area, Policy next = Unknown); + void placeSmart(AbstractClient* c, const QRect& area, Policy next = Unknown); + void placeMaximizing(AbstractClient* c, const QRect& area, Policy next = Unknown); + void placeCentered(AbstractClient* c, const QRect& area, Policy next = Unknown); + void placeZeroCornered(AbstractClient* c, const QRect& area, Policy next = Unknown); + void placeDialog(AbstractClient* c, const QRect& area, Policy next = Unknown); + void placeUtility(AbstractClient* c, const QRect& area, Policy next = Unknown); + void placeOnScreenDisplay(AbstractClient* c, const QRect& area); + + void reinitCascading(int desktop); + + /** + * Cascades all clients on the current desktop + */ + void cascadeDesktop(); + /** + * Unclutters the current desktop by smart-placing all clients again. + */ + void unclutterDesktop(); + + static Policy policyFromString(const QString& policy, bool no_special); + static const char* policyToString(Policy policy); + +private: + void place(AbstractClient *c, const QRect &area, Policy policy, Policy nextPlacement = Unknown); + void placeUnderMouse(AbstractClient *c, const QRect &area, Policy next = Unknown); + void placeOnMainWindow(AbstractClient *c, const QRect &area, Policy next = Unknown); + void placeTransient(AbstractClient *c); + + //CT needed for cascading+ + struct DesktopCascadingInfo { + QPoint pos; + int col; + int row; + }; + + QList cci; + + KWIN_SINGLETON(Placement) +}; + +} // namespace + +#endif diff --git a/plasma-kwin_wayland.service.in b/plasma-kwin_wayland.service.in new file mode 100644 index 0000000..90f539a --- /dev/null +++ b/plasma-kwin_wayland.service.in @@ -0,0 +1,7 @@ +[Unit] +Description=KDE Window Manager + +[Service] +ExecStart=@CMAKE_INSTALL_FULL_BINDIR@/kwin_wayland +BusName=org.kde.KWin +Slice=session.slice diff --git a/plasma-kwin_x11.service.in b/plasma-kwin_x11.service.in new file mode 100644 index 0000000..2297fd5 --- /dev/null +++ b/plasma-kwin_x11.service.in @@ -0,0 +1,8 @@ +[Unit] +Description=KDE Window Manager +Wants=plasma-kcminit.service + +[Service] +ExecStart=@CMAKE_INSTALL_FULL_BINDIR@/kwin_x11 --replace +BusName=org.kde.KWin +Slice=session.slice diff --git a/platform.cpp b/platform.cpp new file mode 100644 index 0000000..edf3760 --- /dev/null +++ b/platform.cpp @@ -0,0 +1,581 @@ +/* + 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 "platform.h" + +#include "abstract_output.h" +#include +#include "composite.h" +#include "cursor.h" +#include "effects.h" +#include +#include "overlaywindow.h" +#include "outline.h" +#include "pointer_input.h" +#include "scene.h" +#include "screens.h" +#include "screenedge.h" +#include "wayland_server.h" +#include "colorcorrection/manager.h" + +#include +#include + +#include + +#include + +namespace KWin +{ + +Platform::Platform(QObject *parent) + : QObject(parent) + , m_eglDisplay(EGL_NO_DISPLAY) +{ + setSoftWareCursor(false); + m_colorCorrect = new ColorCorrect::Manager(this); + connect(Cursors::self(), &Cursors::currentCursorRendered, this, &Platform::cursorRendered); +} + +Platform::~Platform() +{ + if (m_eglDisplay != EGL_NO_DISPLAY) { + eglTerminate(m_eglDisplay); + } +} + +PlatformCursorImage Platform::cursorImage() const +{ + Cursor* cursor = Cursors::self()->currentCursor(); + return PlatformCursorImage(cursor->image(), cursor->hotspot()); +} + +void Platform::hideCursor() +{ + m_hideCursorCounter++; + if (m_hideCursorCounter == 1) { + doHideCursor(); + } +} + +void Platform::doHideCursor() +{ +} + +void Platform::showCursor() +{ + m_hideCursorCounter--; + if (m_hideCursorCounter == 0) { + doShowCursor(); + } +} + +void Platform::doShowCursor() +{ +} + +Screens *Platform::createScreens(QObject *parent) +{ + Q_UNUSED(parent) + return nullptr; +} + +OpenGLBackend *Platform::createOpenGLBackend() +{ + return nullptr; +} + +QPainterBackend *Platform::createQPainterBackend() +{ + return nullptr; +} + +void Platform::prepareShutdown() +{ + setOutputsEnabled(false); +} + +Edge *Platform::createScreenEdge(ScreenEdges *edges) +{ + return new Edge(edges); +} + +void Platform::createPlatformCursor(QObject *parent) +{ + new InputRedirectionCursor(parent); +} + +void Platform::requestOutputsChange(KWaylandServer::OutputConfigurationInterface *config) +{ + if (!m_supportsOutputChanges) { + qCWarning(KWIN_CORE) << "This backend does not support configuration changes."; + config->setFailed(); + return; + } + + using Enablement = KWaylandServer::OutputDeviceInterface::Enablement; + + const auto changes = config->changes(); + + //process all non-disabling changes + for (auto it = changes.begin(); it != changes.end(); it++) { + const KWaylandServer::OutputChangeSet *changeset = it.value(); + + auto output = findOutput(it.key()->uuid()); + if (!output) { + qCWarning(KWIN_CORE) << "Could NOT find output matching " << it.key()->uuid(); + continue; + } + + if (changeset->enabledChanged() && + changeset->enabled() == Enablement::Enabled) { + output->setEnabled(true); + } + output->applyChanges(changeset); + } + + //process any disable requests + for (auto it = changes.begin(); it != changes.end(); it++) { + const KWaylandServer::OutputChangeSet *changeset = it.value(); + + if (changeset->enabledChanged() && + changeset->enabled() == Enablement::Disabled) { + if (enabledOutputs().count() == 1) { + // TODO: check beforehand this condition and set failed otherwise + // TODO: instead create a dummy output? + qCWarning(KWIN_CORE) << "Not disabling final screen" << it.key()->uuid(); + continue; + } + auto output = findOutput(it.key()->uuid()); + if (!output) { + qCWarning(KWIN_CORE) << "Could NOT find output matching " << it.key()->uuid(); + continue; + } + output->setEnabled(false); + } + } + emit screens()->changed(); + config->setApplied(); +} + +AbstractOutput *Platform::findOutput(int screenId) +{ + return enabledOutputs().value(screenId); +} + +AbstractOutput *Platform::findOutput(const QByteArray &uuid) +{ + const auto outs = outputs(); + auto it = std::find_if(outs.constBegin(), outs.constEnd(), + [uuid](AbstractOutput *output) { + return output->uuid() == uuid; } + ); + if (it != outs.constEnd()) { + return *it; + } + return nullptr; +} + +void Platform::setSoftWareCursor(bool set) +{ + if (qEnvironmentVariableIsSet("KWIN_FORCE_SW_CURSOR")) { + set = true; + } + if (m_softWareCursor == set) { + return; + } + m_softWareCursor = set; + if (m_softWareCursor) { + connect(Cursors::self(), &Cursors::positionChanged, this, &Platform::triggerCursorRepaint); + connect(Cursors::self(), &Cursors::currentCursorChanged, this, &Platform::triggerCursorRepaint); + } else { + disconnect(Cursors::self(), &Cursors::positionChanged, this, &Platform::triggerCursorRepaint); + disconnect(Cursors::self(), &Cursors::currentCursorChanged, this, &Platform::triggerCursorRepaint); + } + triggerCursorRepaint(); +} + +void Platform::triggerCursorRepaint() +{ + if (!Compositor::self()) { + return; + } + Compositor::self()->addRepaint(m_cursor.lastRenderedGeometry); + Compositor::self()->addRepaint(Cursors::self()->currentCursor()->geometry()); +} + +void Platform::cursorRendered(const QRect &geometry) +{ + if (m_softWareCursor) { + m_cursor.lastRenderedGeometry = geometry; + } +} + +void Platform::keyboardKeyPressed(quint32 key, quint32 time) +{ + if (!input()) { + return; + } + input()->processKeyboardKey(key, InputRedirection::KeyboardKeyPressed, time); +} + +void Platform::keyboardKeyReleased(quint32 key, quint32 time) +{ + if (!input()) { + return; + } + input()->processKeyboardKey(key, InputRedirection::KeyboardKeyReleased, time); +} + +void Platform::keyboardModifiers(uint32_t modsDepressed, uint32_t modsLatched, uint32_t modsLocked, uint32_t group) +{ + if (!input()) { + return; + } + input()->processKeyboardModifiers(modsDepressed, modsLatched, modsLocked, group); +} + +void Platform::keymapChange(int fd, uint32_t size) +{ + if (!input()) { + return; + } + input()->processKeymapChange(fd, size); +} + +void Platform::pointerAxisHorizontal(qreal delta, quint32 time, qint32 discreteDelta, InputRedirection::PointerAxisSource source) +{ + if (!input()) { + return; + } + input()->processPointerAxis(InputRedirection::PointerAxisHorizontal, delta, discreteDelta, source, time); +} + +void Platform::pointerAxisVertical(qreal delta, quint32 time, qint32 discreteDelta, InputRedirection::PointerAxisSource source) +{ + if (!input()) { + return; + } + input()->processPointerAxis(InputRedirection::PointerAxisVertical, delta, discreteDelta, source, time); +} + +void Platform::pointerButtonPressed(quint32 button, quint32 time) +{ + if (!input()) { + return; + } + input()->processPointerButton(button, InputRedirection::PointerButtonPressed, time); +} + +void Platform::pointerButtonReleased(quint32 button, quint32 time) +{ + if (!input()) { + return; + } + input()->processPointerButton(button, InputRedirection::PointerButtonReleased, time); +} + +void Platform::pointerMotion(const QPointF &position, quint32 time) +{ + if (!input()) { + return; + } + input()->processPointerMotion(position, time); +} + +void Platform::touchCancel() +{ + if (!input()) { + return; + } + input()->cancelTouch(); +} + +void Platform::touchDown(qint32 id, const QPointF &pos, quint32 time) +{ + if (!input()) { + return; + } + input()->processTouchDown(id, pos, time); +} + +void Platform::touchFrame() +{ + if (!input()) { + return; + } + input()->touchFrame(); +} + +void Platform::touchMotion(qint32 id, const QPointF &pos, quint32 time) +{ + if (!input()) { + return; + } + input()->processTouchMotion(id, pos, time); +} + +void Platform::touchUp(qint32 id, quint32 time) +{ + if (!input()) { + return; + } + input()->processTouchUp(id, time); +} + +void Platform::processSwipeGestureBegin(int fingerCount, quint32 time) +{ + if (!input()) { + return; + } + input()->pointer()->processSwipeGestureBegin(fingerCount, time); +} + +void Platform::processSwipeGestureUpdate(const QSizeF &delta, quint32 time) +{ + if (!input()) { + return; + } + input()->pointer()->processSwipeGestureUpdate(delta, time); +} + +void Platform::processSwipeGestureEnd(quint32 time) +{ + if (!input()) { + return; + } + input()->pointer()->processSwipeGestureEnd(time); +} + +void Platform::processSwipeGestureCancelled(quint32 time) +{ + if (!input()) { + return; + } + input()->pointer()->processSwipeGestureCancelled(time); +} + +void Platform::processPinchGestureBegin(int fingerCount, quint32 time) +{ + if (!input()) { + return; + } + input()->pointer()->processPinchGestureBegin(fingerCount, time); +} + +void Platform::processPinchGestureUpdate(qreal scale, qreal angleDelta, const QSizeF &delta, quint32 time) +{ + if (!input()) { + return; + } + input()->pointer()->processPinchGestureUpdate(scale, angleDelta, delta, time); +} + +void Platform::processPinchGestureEnd(quint32 time) +{ + if (!input()) { + return; + } + input()->pointer()->processPinchGestureEnd(time); +} + +void Platform::processPinchGestureCancelled(quint32 time) +{ + if (!input()) { + return; + } + input()->pointer()->processPinchGestureCancelled(time); +} + +void Platform::repaint(const QRect &rect) +{ + if (!Compositor::self()) { + return; + } + Compositor::self()->addRepaint(rect); +} + +void Platform::setReady(bool ready) +{ + if (m_ready == ready) { + return; + } + m_ready = ready; + emit readyChanged(m_ready); +} + +void Platform::warpPointer(const QPointF &globalPos) +{ + Q_UNUSED(globalPos) +} + +bool Platform::supportsSurfacelessContext() const +{ + Compositor *compositor = Compositor::self(); + if (Q_UNLIKELY(!compositor)) { + return false; + } + if (Scene *scene = compositor->scene()) { + return scene->supportsSurfacelessContext(); + } + return false; +} + +EGLDisplay KWin::Platform::sceneEglDisplay() const +{ + return m_eglDisplay; +} + +void Platform::setSceneEglDisplay(EGLDisplay display) +{ + m_eglDisplay = display; +} + +QSize Platform::screenSize() const +{ + return QSize(); +} + +QVector Platform::screenGeometries() const +{ + return QVector({QRect(QPoint(0, 0), screenSize())}); +} + +QVector Platform::screenScales() const +{ + return QVector({1}); +} + +bool Platform::requiresCompositing() const +{ + return true; +} + +bool Platform::compositingPossible() const +{ + return true; +} + +QString Platform::compositingNotPossibleReason() const +{ + return QString(); +} + +bool Platform::openGLCompositingIsBroken() const +{ + return false; +} + +void Platform::createOpenGLSafePoint(OpenGLSafePoint safePoint) +{ + Q_UNUSED(safePoint) +} + +void Platform::startInteractiveWindowSelection(std::function callback, const QByteArray &cursorName) +{ + if (!input()) { + callback(nullptr); + return; + } + input()->startInteractiveWindowSelection(callback, cursorName); +} + +void Platform::startInteractivePositionSelection(std::function callback) +{ + if (!input()) { + callback(QPoint(-1, -1)); + return; + } + input()->startInteractivePositionSelection(callback); +} + +void Platform::setupActionForGlobalAccel(QAction *action) +{ + Q_UNUSED(action) +} + +OverlayWindow *Platform::createOverlayWindow() +{ + return nullptr; +} + +static quint32 monotonicTime() +{ + timespec ts; + + const int result = clock_gettime(CLOCK_MONOTONIC, &ts); + if (result) + qCWarning(KWIN_CORE, "Failed to query monotonic time: %s", strerror(errno)); + + return ts.tv_sec * 1000 + ts.tv_nsec / 1000000L; +} + +void Platform::updateXTime() +{ + switch (kwinApp()->operationMode()) { + case Application::OperationModeX11: + kwinApp()->setX11Time(QX11Info::getTimestamp(), Application::TimestampUpdate::Always); + break; + + case Application::OperationModeXwayland: + kwinApp()->setX11Time(monotonicTime(), Application::TimestampUpdate::Always); + break; + + default: + // Do not update the current X11 time stamp if it's the Wayland only session. + break; + } +} + +OutlineVisual *Platform::createOutline(Outline *outline) +{ + if (Compositor::compositing()) { + return new CompositedOutlineVisual(outline); + } + return nullptr; +} + +Decoration::Renderer *Platform::createDecorationRenderer(Decoration::DecoratedClientImpl *client) +{ + if (Compositor::self()->scene()) { + return Compositor::self()->scene()->createDecorationRenderer(client); + } + return nullptr; +} + +void Platform::invertScreen() +{ + if (effects) { + if (Effect *inverter = static_cast(effects)->provides(Effect::ScreenInversion)) { + qCDebug(KWIN_CORE) << "inverting screen using Effect plugin"; + QMetaObject::invokeMethod(inverter, "toggleScreenInversion", Qt::DirectConnection); + } + } +} + +void Platform::createEffectsHandler(Compositor *compositor, Scene *scene) +{ + new EffectsHandlerImpl(compositor, scene); +} + +QString Platform::supportInformation() const +{ + return QStringLiteral("Name: %1\n").arg(metaObject()->className()); +} + +EGLContext Platform::sceneEglGlobalShareContext() const +{ + return m_globalShareContext; +} + +void Platform::setSceneEglGlobalShareContext(EGLContext context) +{ + m_globalShareContext = context; +} + +} diff --git a/platform.h b/platform.h new file mode 100644 index 0000000..9b96065 --- /dev/null +++ b/platform.h @@ -0,0 +1,573 @@ +/* + 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_PLATFORM_H +#define KWIN_PLATFORM_H +#include +#include +#include +#include +#include "fixqopengl.h" +#include "input.h" + +#include +#include + +#include + +class QAction; + +namespace KWaylandServer { +class OutputConfigurationInterface; +} + +namespace KWin +{ +namespace ColorCorrect { +class Manager; +} + +class AbstractOutput; +class Edge; +class Compositor; +class DmaBufTexture; +class OverlayWindow; +class OpenGLBackend; +class Outline; +class OutlineVisual; +class QPainterBackend; +class Scene; +class Screens; +class ScreenEdges; +class Toplevel; + +namespace Decoration +{ +class Renderer; +class DecoratedClientImpl; +} + +class KWIN_EXPORT Outputs : public QVector +{ +public: + Outputs(){}; + template + Outputs(const QVector &other) { + resize(other.size()); + std::copy(other.constBegin(), other.constEnd(), begin()); + } +}; + +class KWIN_EXPORT Platform : public QObject +{ + Q_OBJECT +public: + ~Platform() override; + + virtual void init() = 0; + virtual Screens *createScreens(QObject *parent = nullptr); + virtual OpenGLBackend *createOpenGLBackend(); + virtual QPainterBackend *createQPainterBackend(); + virtual DmaBufTexture *createDmaBufTexture(const QSize &size) { + Q_UNUSED(size); + return nullptr; + } + + /** + * Informs the Platform that it is about to go down and shall do appropriate cleanup. + * Child classes can override this function but must call the parent implementation in + * the end. + */ + virtual void prepareShutdown(); + + /** + * Allows the platform to create a platform specific screen edge. + * The default implementation creates a Edge. + */ + virtual Edge *createScreenEdge(ScreenEdges *parent); + /** + * Allows the platform to create a platform specific Cursor. + * The default implementation creates an InputRedirectionCursor. + */ + virtual void createPlatformCursor(QObject *parent = nullptr); + virtual void warpPointer(const QPointF &globalPos); + /** + * Whether our Compositing EGL display allows a surface less context + * so that a sharing context could be created. + */ + bool supportsSurfacelessContext() const; + /** + * The EGLDisplay used by the compositing scene. + */ + EGLDisplay sceneEglDisplay() const; + void setSceneEglDisplay(EGLDisplay display); + /** + * The EGLContext used by the compositing scene. + */ + virtual EGLContext sceneEglContext() const { + return m_context; + } + /** + * Sets the @p context used by the compositing scene. + */ + void setSceneEglContext(EGLContext context) { + m_context = context; + } + /** + * Returns the compositor-wide shared EGL context. This function may return EGL_NO_CONTEXT + * if the underlying rendering backend does not use EGL. + * + * Note that the returned context should never be made current. Instead, create a context + * that shares with this one and make the new context current. + */ + EGLContext sceneEglGlobalShareContext() const; + /** + * Sets the global share context to @a context. This function is intended to be called only + * by rendering backends. + */ + void setSceneEglGlobalShareContext(EGLContext context); + /** + * The first (in case of multiple) EGLSurface used by the compositing scene. + */ + EGLSurface sceneEglSurface() const { + return m_surface; + } + /** + * Sets the first @p surface used by the compositing scene. + * @see sceneEglSurface + */ + void setSceneEglSurface(EGLSurface surface) { + m_surface = surface; + } + + /** + * The EglConfig used by the compositing scene. + */ + EGLConfig sceneEglConfig() const { + return m_eglConfig; + } + /** + * Sets the @p config used by the compositing scene. + * @see sceneEglConfig + */ + void setSceneEglConfig(EGLConfig config) { + m_eglConfig = config; + } + + /** + * Implementing subclasses should provide a size in case the backend represents + * a basic screen and uses the BasicScreens. + * + * Base implementation returns an invalid size. + */ + virtual QSize screenSize() const; + /** + * Implementing subclasses should provide all geometries in case the backend represents + * a basic screen and uses the BasicScreens. + * + * Base implementation returns one QRect positioned at 0/0 with screenSize() as size. + */ + virtual QVector screenGeometries() const; + + /** + * Implementing subclasses should provide all geometries in case the backend represents + * a basic screen and uses the BasicScreens. + * + * Base implementation returns a screen with a scale of 1. + */ + virtual QVector screenScales() const; + /** + * Implement this method to receive configuration change requests through KWayland's + * OutputManagement interface. + * + * Base implementation warns that the current backend does not implement this + * functionality. + */ + void requestOutputsChange(KWaylandServer::OutputConfigurationInterface *config); + + /** + * Whether the Platform requires compositing for rendering. + * Default implementation returns @c true. If the implementing Platform allows to be used + * without compositing (e.g. rendering is done by the windowing system), re-implement this method. + */ + virtual bool requiresCompositing() const; + /** + * Whether Compositing is possible in the Platform. + * Returning @c false in this method makes only sense if requiresCompositing returns @c false. + * + * The default implementation returns @c true. + * @see requiresCompositing + */ + virtual bool compositingPossible() const; + /** + * Returns a user facing text explaining why compositing is not possible in case + * compositingPossible returns @c false. + * + * The default implementation returns an empty string. + * @see compositingPossible + */ + virtual QString compositingNotPossibleReason() const; + /** + * Whether OpenGL compositing is broken. + * The Platform can implement this method if it is able to detect whether OpenGL compositing + * broke (e.g. triggered a crash in a previous run). + * + * Default implementation returns @c false. + * @see createOpenGLSafePoint + */ + virtual bool openGLCompositingIsBroken() const; + enum class OpenGLSafePoint { + PreInit, + PostInit, + PreFrame, + PostFrame, + PostLastGuardedFrame + }; + /** + * This method is invoked before and after creating the OpenGL rendering Scene. + * An implementing Platform can use it to detect crashes triggered by the OpenGL implementation. + * This can be used for openGLCompositingIsBroken. + * + * The default implementation does nothing. + * @see openGLCompositingIsBroken. + */ + virtual void createOpenGLSafePoint(OpenGLSafePoint safePoint); + + /** + * Starts an interactive window selection process. + * + * Once the user selected a window the @p callback is invoked with the selected Toplevel as + * argument. In case the user cancels the interactive window selection or selecting a window is currently + * not possible (e.g. screen locked) the @p callback is invoked with a @c nullptr argument. + * + * During the interactive window selection the cursor is turned into a crosshair cursor unless + * @p cursorName is provided. The argument @p cursorName is a QByteArray instead of Qt::CursorShape + * to support the "pirate" cursor for kill window which is not wrapped by Qt::CursorShape. + * + * The default implementation forwards to InputRedirection. + * + * @param callback The function to invoke once the interactive window selection ends + * @param cursorName The optional name of the cursor shape to use, default is crosshair + */ + virtual void startInteractiveWindowSelection(std::function callback, const QByteArray &cursorName = QByteArray()); + + /** + * Starts an interactive position selection process. + * + * Once the user selected a position on the screen the @p callback is invoked with + * the selected point as argument. In case the user cancels the interactive position selection + * or selecting a position is currently not possible (e.g. screen locked) the @p callback + * is invoked with a point at @c -1 as x and y argument. + * + * During the interactive window selection the cursor is turned into a crosshair cursor. + * + * The default implementation forwards to InputRedirection. + * + * @param callback The function to invoke once the interactive position selection ends + */ + virtual void startInteractivePositionSelection(std::function callback); + + /** + * Platform specific preparation for an @p action which is used for KGlobalAccel. + * + * A platform might need to do preparation for an @p action before + * it can be used with KGlobalAccel. + * + * Code using KGlobalAccel should invoke this method for the @p action + * prior to setting up any shortcuts and connections. + * + * The default implementation does nothing. + * + * @param action The action which will be used with KGlobalAccel. + * @since 5.10 + */ + virtual void setupActionForGlobalAccel(QAction *action); + + bool usesSoftwareCursor() const { + return m_softWareCursor; + } + + /** + * Returns a PlatformCursorImage. By default this is created by softwareCursor and + * softwareCursorHotspot. An implementing subclass can use this to provide a better + * suited PlatformCursorImage. + * + * @see softwareCursor + * @see softwareCursorHotspot + * @since 5.9 + */ + virtual PlatformCursorImage cursorImage() const; + + /** + * The Platform cursor image should be hidden. + * @see showCursor + * @see doHideCursor + * @see isCursorHidden + * @since 5.9 + */ + void hideCursor(); + + /** + * The Platform cursor image should be shown again. + * @see hideCursor + * @see doShowCursor + * @see isCursorHidden + * @since 5.9 + */ + void showCursor(); + + /** + * Whether the cursor is currently hidden. + * @see showCursor + * @see hideCursor + * @since 5.9 + */ + bool isCursorHidden() const { + return m_hideCursorCounter > 0; + } + bool isReady() const { + return m_ready; + } + void setInitialWindowSize(const QSize &size) { + m_initialWindowSize = size; + } + void setDeviceIdentifier(const QByteArray &identifier) { + m_deviceIdentifier = identifier; + } + bool supportsPointerWarping() const { + return m_pointerWarping; + } + bool areOutputsEnabled() const { + return m_outputsEnabled; + } + void setOutputsEnabled(bool enabled) { + m_outputsEnabled = enabled; + } + int initialOutputCount() const { + return m_initialOutputCount; + } + void setInitialOutputCount(int count) { + m_initialOutputCount = count; + } + qreal initialOutputScale() const { + return m_initialOutputScale; + } + void setInitialOutputScale(qreal scale) { + m_initialOutputScale = scale; + } + + /** + * Creates the OverlayWindow required for X11 based compositors. + * Default implementation returns @c nullptr. + */ + virtual OverlayWindow *createOverlayWindow(); + + /** + * Queries the current X11 time stamp of the X server. + */ + void updateXTime(); + + /** + * Creates the OutlineVisual for the given @p outline. + * Default implementation creates an OutlineVisual suited for composited usage. + */ + virtual OutlineVisual *createOutline(Outline *outline); + + /** + * Creates the Decoration::Renderer for the given @p client. + * + * The default implementation creates a Renderer suited for the Compositor, @c nullptr if there is no Compositor. + */ + virtual Decoration::Renderer *createDecorationRenderer(Decoration::DecoratedClientImpl *client); + + /** + * Platform specific way to invert the screen. + * Default implementation invokes the invert effect + */ + virtual void invertScreen(); + + /** + * Default implementation creates an EffectsHandlerImp; + */ + virtual void createEffectsHandler(Compositor *compositor, Scene *scene); + + /** + * The CompositingTypes supported by the Platform. + * The first item should be the most preferred one. + * @since 5.11 + */ + virtual QVector supportedCompositors() const = 0; + + /** + * Whether gamma control is supported by the backend. + * @since 5.12 + */ + bool supportsGammaControl() const { + return m_supportsGammaControl; + } + + ColorCorrect::Manager *colorCorrectManager() { + return m_colorCorrect; + } + + // outputs with connections (org_kde_kwin_outputdevice) + virtual Outputs outputs() const { + return Outputs(); + } + // actively compositing outputs (wl_output) + virtual Outputs enabledOutputs() const { + return Outputs(); + } + AbstractOutput *findOutput(int screenId); + AbstractOutput *findOutput(const QByteArray &uuid); + + /** + * A string of information to include in kwin debug output + * It should not be translated. + * + * The base implementation prints the name. + * @since 5.12 + */ + virtual QString supportInformation() const; + + /** + * The compositor plugin which got selected from @ref supportedCompositors. + * Prior to selecting a compositor this returns @c NoCompositing. + * + * This method allows the platforms to limit the offerings in @ref supportedCompositors + * in case they do not support runtime compositor switching + */ + CompositingType selectedCompositor() const + { + return m_selectedCompositor; + } + /** + * Used by Compositor to set the used compositor. + */ + void setSelectedCompositor(CompositingType type) + { + m_selectedCompositor = type; + } + +public Q_SLOTS: + void pointerMotion(const QPointF &position, quint32 time); + void pointerButtonPressed(quint32 button, quint32 time); + void pointerButtonReleased(quint32 button, quint32 time); + void pointerAxisHorizontal(qreal delta, quint32 time, qint32 discreteDelta = 0, + InputRedirection::PointerAxisSource source = InputRedirection::PointerAxisSourceUnknown); + void pointerAxisVertical(qreal delta, quint32 time, qint32 discreteDelta = 0, + InputRedirection::PointerAxisSource source = InputRedirection::PointerAxisSourceUnknown); + void keyboardKeyPressed(quint32 key, quint32 time); + void keyboardKeyReleased(quint32 key, quint32 time); + void keyboardModifiers(uint32_t modsDepressed, uint32_t modsLatched, uint32_t modsLocked, uint32_t group); + void keymapChange(int fd, uint32_t size); + void touchDown(qint32 id, const QPointF &pos, quint32 time); + void touchUp(qint32 id, quint32 time); + void touchMotion(qint32 id, const QPointF &pos, quint32 time); + void touchCancel(); + void touchFrame(); + + void processSwipeGestureBegin(int fingerCount, quint32 time); + void processSwipeGestureUpdate(const QSizeF &delta, quint32 time); + void processSwipeGestureEnd(quint32 time); + void processSwipeGestureCancelled(quint32 time); + void processPinchGestureBegin(int fingerCount, quint32 time); + void processPinchGestureUpdate(qreal scale, qreal angleDelta, const QSizeF &delta, quint32 time); + void processPinchGestureEnd(quint32 time); + void processPinchGestureCancelled(quint32 time); + + void cursorRendered(const QRect &geometry); + +Q_SIGNALS: + void screensQueried(); + void initFailed(); + void readyChanged(bool); + /** + * Emitted by backends using a one screen (nested window) approach and when the size of that changes. + */ + void screenSizeChanged(); + +protected: + explicit Platform(QObject *parent = nullptr); + void setSoftWareCursor(bool set); + void repaint(const QRect &rect); + void setReady(bool ready); + QSize initialWindowSize() const { + return m_initialWindowSize; + } + QByteArray deviceIdentifier() const { + return m_deviceIdentifier; + } + void setSupportsPointerWarping(bool set) { + m_pointerWarping = set; + } + void setSupportsGammaControl(bool set) { + m_supportsGammaControl = set; + } + + /** + * Whether the backend is supposed to change the configuration of outputs. + */ + void supportsOutputChanges() { + m_supportsOutputChanges = true; + } + + /** + * Actual platform specific way to hide the cursor. + * Sub-classes need to implement if they support hiding the cursor. + * + * This method is invoked by hideCursor if the cursor needs to be hidden. + * The default implementation does nothing. + * + * @see doShowCursor + * @see hideCursor + * @see showCursor + */ + virtual void doHideCursor(); + /** + * Actual platform specific way to show the cursor. + * Sub-classes need to implement if they support showing the cursor. + * + * This method is invoked by showCursor if the cursor needs to be shown again. + * + * @see doShowCursor + * @see hideCursor + * @see showCursor + */ + virtual void doShowCursor(); + +private: + void triggerCursorRepaint(); + bool m_softWareCursor = false; + struct { + QRect lastRenderedGeometry; + } m_cursor; + bool m_ready = false; + QSize m_initialWindowSize; + QByteArray m_deviceIdentifier; + bool m_pointerWarping = false; + bool m_outputsEnabled = true; + int m_initialOutputCount = 1; + qreal m_initialOutputScale = 1; + EGLDisplay m_eglDisplay; + EGLConfig m_eglConfig = nullptr; + EGLContext m_context = EGL_NO_CONTEXT; + EGLContext m_globalShareContext = EGL_NO_CONTEXT; + EGLSurface m_surface = EGL_NO_SURFACE; + int m_hideCursorCounter = 0; + ColorCorrect::Manager *m_colorCorrect = nullptr; + bool m_supportsGammaControl = false; + bool m_supportsOutputChanges = false; + CompositingType m_selectedCompositor = NoCompositing; +}; + +} + +Q_DECLARE_INTERFACE(KWin::Platform, "org.kde.kwin.Platform") + +#endif diff --git a/platformsupport/CMakeLists.txt b/platformsupport/CMakeLists.txt new file mode 100644 index 0000000..f395a74 --- /dev/null +++ b/platformsupport/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(scenes) diff --git a/platformsupport/scenes/CMakeLists.txt b/platformsupport/scenes/CMakeLists.txt new file mode 100644 index 0000000..6e560cb --- /dev/null +++ b/platformsupport/scenes/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(qpainter) +add_subdirectory(opengl) diff --git a/platformsupport/scenes/opengl/CMakeLists.txt b/platformsupport/scenes/opengl/CMakeLists.txt new file mode 100644 index 0000000..6ece689 --- /dev/null +++ b/platformsupport/scenes/opengl/CMakeLists.txt @@ -0,0 +1,24 @@ +set(SCENE_OPENGL_BACKEND_SRCS + abstract_egl_backend.cpp + backend.cpp + egl_dmabuf.cpp + swap_profiler.cpp + texture.cpp +) + +include_directories(${CMAKE_SOURCE_DIR}) + +include(ECMQtDeclareLoggingCategory) +ecm_qt_declare_logging_category(SCENE_OPENGL_BACKEND_SRCS + HEADER + logging.h + IDENTIFIER + KWIN_OPENGL + CATEGORY_NAME + kwin_scene_opengl + DEFAULT_SEVERITY + Critical +) + +add_library(SceneOpenGLBackend STATIC ${SCENE_OPENGL_BACKEND_SRCS}) +target_link_libraries(SceneOpenGLBackend Qt5::Core Qt5::Widgets KF5::CoreAddons KF5::ConfigCore KF5::WindowSystem Plasma::KWaylandServer) diff --git a/platformsupport/scenes/opengl/abstract_egl_backend.cpp b/platformsupport/scenes/opengl/abstract_egl_backend.cpp new file mode 100644 index 0000000..284fdc6 --- /dev/null +++ b/platformsupport/scenes/opengl/abstract_egl_backend.cpp @@ -0,0 +1,686 @@ +/* + 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 "abstract_egl_backend.h" +#include "egl_dmabuf.h" +#include "kwineglext.h" +#include "texture.h" +#include "composite.h" +#include "egl_context_attribute_builder.h" +#include "options.h" +#include "platform.h" +#include "scene.h" +#include "wayland_server.h" +#include "abstract_wayland_output.h" +#include +#include +#include +// kwin libs +#include +#include +#include +// Qt +#include +#include + +#include + +namespace KWin +{ + +typedef GLboolean(*eglBindWaylandDisplayWL_func)(EGLDisplay dpy, wl_display *display); +typedef GLboolean(*eglUnbindWaylandDisplayWL_func)(EGLDisplay dpy, wl_display *display); +typedef GLboolean(*eglQueryWaylandBufferWL_func)(EGLDisplay dpy, struct wl_resource *buffer, EGLint attribute, EGLint *value); +eglBindWaylandDisplayWL_func eglBindWaylandDisplayWL = nullptr; +eglUnbindWaylandDisplayWL_func eglUnbindWaylandDisplayWL = nullptr; +eglQueryWaylandBufferWL_func eglQueryWaylandBufferWL = nullptr; + +static EGLContext s_globalShareContext = EGL_NO_CONTEXT; + +static bool isOpenGLES_helper() +{ + if (qstrcmp(qgetenv("KWIN_COMPOSE"), "O2ES") == 0) { + return true; + } + return QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGLES; +} + +static bool ensureGlobalShareContext() +{ + const EGLDisplay eglDisplay = kwinApp()->platform()->sceneEglDisplay(); + const EGLConfig eglConfig = kwinApp()->platform()->sceneEglConfig(); + + if (s_globalShareContext != EGL_NO_CONTEXT) { + return true; + } + + std::vector attribs; + if (isOpenGLES_helper()) { + EglOpenGLESContextAttributeBuilder builder; + builder.setVersion(2); + attribs = builder.build(); + } else { + EglContextAttributeBuilder builder; + attribs = builder.build(); + } + + s_globalShareContext = eglCreateContext(eglDisplay, eglConfig, EGL_NO_CONTEXT, attribs.data()); + if (s_globalShareContext == EGL_NO_CONTEXT) { + qCWarning(KWIN_OPENGL, "Failed to create global share context: 0x%x", eglGetError()); + } + + kwinApp()->platform()->setSceneEglGlobalShareContext(s_globalShareContext); + + return s_globalShareContext != EGL_NO_CONTEXT; +} + +static void destroyGlobalShareContext() +{ + const EGLDisplay eglDisplay = kwinApp()->platform()->sceneEglDisplay(); + if (eglDisplay == EGL_NO_DISPLAY || s_globalShareContext == EGL_NO_CONTEXT) { + return; + } + eglDestroyContext(eglDisplay, s_globalShareContext); + s_globalShareContext = EGL_NO_CONTEXT; + kwinApp()->platform()->setSceneEglGlobalShareContext(EGL_NO_CONTEXT); +} + +AbstractEglBackend::AbstractEglBackend() + : QObject(nullptr) + , OpenGLBackend() +{ + connect(Compositor::self(), &Compositor::aboutToDestroy, this, &AbstractEglBackend::teardown); +} + +AbstractEglBackend::~AbstractEglBackend() +{ + delete m_dmaBuf; +} + +void AbstractEglBackend::teardown() +{ + if (eglUnbindWaylandDisplayWL && m_display != EGL_NO_DISPLAY) { + eglUnbindWaylandDisplayWL(m_display, *(WaylandServer::self()->display())); + } + destroyGlobalShareContext(); +} + +void AbstractEglBackend::cleanup() +{ + cleanupGL(); + doneCurrent(); + eglDestroyContext(m_display, m_context); + cleanupSurfaces(); + eglReleaseThread(); + kwinApp()->platform()->setSceneEglContext(EGL_NO_CONTEXT); + kwinApp()->platform()->setSceneEglSurface(EGL_NO_SURFACE); + kwinApp()->platform()->setSceneEglConfig(nullptr); +} + +void AbstractEglBackend::cleanupSurfaces() +{ + if (m_surface != EGL_NO_SURFACE) { + eglDestroySurface(m_display, m_surface); + } +} + +bool AbstractEglBackend::initEglAPI() +{ + EGLint major, minor; + if (eglInitialize(m_display, &major, &minor) == EGL_FALSE) { + qCWarning(KWIN_OPENGL) << "eglInitialize failed"; + EGLint error = eglGetError(); + if (error != EGL_SUCCESS) { + qCWarning(KWIN_OPENGL) << "Error during eglInitialize " << error; + } + return false; + } + EGLint error = eglGetError(); + if (error != EGL_SUCCESS) { + qCWarning(KWIN_OPENGL) << "Error during eglInitialize " << error; + return false; + } + qCDebug(KWIN_OPENGL) << "Egl Initialize succeeded"; + + if (eglBindAPI(isOpenGLES() ? EGL_OPENGL_ES_API : EGL_OPENGL_API) == EGL_FALSE) { + qCCritical(KWIN_OPENGL) << "bind OpenGL API failed"; + return false; + } + qCDebug(KWIN_OPENGL) << "EGL version: " << major << "." << minor; + const QByteArray eglExtensions = eglQueryString(m_display, EGL_EXTENSIONS); + setExtensions(eglExtensions.split(' ')); + setSupportsSurfacelessContext(hasExtension(QByteArrayLiteral("EGL_KHR_surfaceless_context"))); + return true; +} + +typedef void (*eglFuncPtr)(); +static eglFuncPtr getProcAddress(const char* name) +{ + return eglGetProcAddress(name); +} + +void AbstractEglBackend::initKWinGL() +{ + GLPlatform *glPlatform = GLPlatform::instance(); + glPlatform->detect(EglPlatformInterface); + options->setGlPreferBufferSwap(options->glPreferBufferSwap()); // resolve autosetting + if (options->glPreferBufferSwap() == Options::AutoSwapStrategy) + options->setGlPreferBufferSwap('e'); // for unknown drivers - should not happen + glPlatform->printResults(); + initGL(&getProcAddress); +} + +void AbstractEglBackend::initBufferAge() +{ + setSupportsBufferAge(false); + + if (hasExtension(QByteArrayLiteral("EGL_EXT_buffer_age"))) { + const QByteArray useBufferAge = qgetenv("KWIN_USE_BUFFER_AGE"); + + if (useBufferAge != "0") + setSupportsBufferAge(true); + } + + setSupportsPartialUpdate(hasExtension(QByteArrayLiteral("EGL_KHR_partial_update"))); + setSupportsSwapBuffersWithDamage(hasExtension(QByteArrayLiteral("EGL_EXT_swap_buffers_with_damage"))); +} + +void AbstractEglBackend::initWayland() +{ + if (!WaylandServer::self()) { + return; + } + if (hasExtension(QByteArrayLiteral("EGL_WL_bind_wayland_display"))) { + eglBindWaylandDisplayWL = (eglBindWaylandDisplayWL_func)eglGetProcAddress("eglBindWaylandDisplayWL"); + eglUnbindWaylandDisplayWL = (eglUnbindWaylandDisplayWL_func)eglGetProcAddress("eglUnbindWaylandDisplayWL"); + eglQueryWaylandBufferWL = (eglQueryWaylandBufferWL_func)eglGetProcAddress("eglQueryWaylandBufferWL"); + // only bind if not already done + if (waylandServer()->display()->eglDisplay() != eglDisplay()) { + if (!eglBindWaylandDisplayWL(eglDisplay(), *(WaylandServer::self()->display()))) { + eglUnbindWaylandDisplayWL = nullptr; + eglQueryWaylandBufferWL = nullptr; + } else { + waylandServer()->display()->setEglDisplay(eglDisplay()); + } + } + } + + Q_ASSERT(!m_dmaBuf); + m_dmaBuf = EglDmabuf::factory(this); +} + +void AbstractEglBackend::initClientExtensions() +{ + // Get the list of client extensions + const char* clientExtensionsCString = eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS); + const QByteArray clientExtensionsString = QByteArray::fromRawData(clientExtensionsCString, qstrlen(clientExtensionsCString)); + if (clientExtensionsString.isEmpty()) { + // If eglQueryString() returned NULL, the implementation doesn't support + // EGL_EXT_client_extensions. Expect an EGL_BAD_DISPLAY error. + (void) eglGetError(); + } + + m_clientExtensions = clientExtensionsString.split(' '); +} + +bool AbstractEglBackend::hasClientExtension(const QByteArray &ext) const +{ + return m_clientExtensions.contains(ext); +} + +bool AbstractEglBackend::makeCurrent() +{ + if (QOpenGLContext *context = QOpenGLContext::currentContext()) { + // Workaround to tell Qt that no QOpenGLContext is current + context->doneCurrent(); + } + const bool current = eglMakeCurrent(m_display, m_surface, m_surface, m_context); + return current; +} + +void AbstractEglBackend::doneCurrent() +{ + eglMakeCurrent(m_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); +} + +bool AbstractEglBackend::isOpenGLES() const +{ + return isOpenGLES_helper(); +} + +bool AbstractEglBackend::createContext() +{ + if (!ensureGlobalShareContext()) { + return false; + } + + const bool haveRobustness = hasExtension(QByteArrayLiteral("EGL_EXT_create_context_robustness")); + const bool haveCreateContext = hasExtension(QByteArrayLiteral("EGL_KHR_create_context")); + const bool haveContextPriority = hasExtension(QByteArrayLiteral("EGL_IMG_context_priority")); + + std::vector> candidates; + if (isOpenGLES()) { + if (haveCreateContext && haveRobustness && haveContextPriority) { + auto glesRobustPriority = std::unique_ptr(new EglOpenGLESContextAttributeBuilder); + glesRobustPriority->setVersion(2); + glesRobustPriority->setRobust(true); + glesRobustPriority->setHighPriority(true); + candidates.push_back(std::move(glesRobustPriority)); + } + if (haveCreateContext && haveRobustness) { + auto glesRobust = std::unique_ptr(new EglOpenGLESContextAttributeBuilder); + glesRobust->setVersion(2); + glesRobust->setRobust(true); + candidates.push_back(std::move(glesRobust)); + } + if (haveContextPriority) { + auto glesPriority = std::unique_ptr(new EglOpenGLESContextAttributeBuilder); + glesPriority->setVersion(2); + glesPriority->setHighPriority(true); + candidates.push_back(std::move(glesPriority)); + } + auto gles = std::unique_ptr(new EglOpenGLESContextAttributeBuilder); + gles->setVersion(2); + candidates.push_back(std::move(gles)); + } else { + if (options->glCoreProfile() && haveCreateContext) { + if (haveRobustness && haveContextPriority) { + auto robustCorePriority = std::unique_ptr(new EglContextAttributeBuilder); + robustCorePriority->setVersion(3, 1); + robustCorePriority->setRobust(true); + robustCorePriority->setHighPriority(true); + candidates.push_back(std::move(robustCorePriority)); + } + if (haveRobustness) { + auto robustCore = std::unique_ptr(new EglContextAttributeBuilder); + robustCore->setVersion(3, 1); + robustCore->setRobust(true); + candidates.push_back(std::move(robustCore)); + } + if (haveContextPriority) { + auto corePriority = std::unique_ptr(new EglContextAttributeBuilder); + corePriority->setVersion(3, 1); + corePriority->setHighPriority(true); + candidates.push_back(std::move(corePriority)); + } + auto core = std::unique_ptr(new EglContextAttributeBuilder); + core->setVersion(3, 1); + candidates.push_back(std::move(core)); + } + if (haveRobustness && haveCreateContext && haveContextPriority) { + auto robustPriority = std::unique_ptr(new EglContextAttributeBuilder); + robustPriority->setRobust(true); + robustPriority->setHighPriority(true); + candidates.push_back(std::move(robustPriority)); + } + if (haveRobustness && haveCreateContext) { + auto robust = std::unique_ptr(new EglContextAttributeBuilder); + robust->setRobust(true); + candidates.push_back(std::move(robust)); + } + candidates.emplace_back(new EglContextAttributeBuilder); + } + + EGLContext ctx = EGL_NO_CONTEXT; + for (auto it = candidates.begin(); it != candidates.end(); it++) { + const auto attribs = (*it)->build(); + ctx = eglCreateContext(m_display, config(), s_globalShareContext, attribs.data()); + if (ctx != EGL_NO_CONTEXT) { + qCDebug(KWIN_OPENGL) << "Created EGL context with attributes:" << (*it).get(); + break; + } + } + + if (ctx == EGL_NO_CONTEXT) { + qCCritical(KWIN_OPENGL) << "Create Context failed"; + return false; + } + m_context = ctx; + kwinApp()->platform()->setSceneEglContext(m_context); + return true; +} + +void AbstractEglBackend::setEglDisplay(const EGLDisplay &display) { + m_display = display; + kwinApp()->platform()->setSceneEglDisplay(display); +} + +void AbstractEglBackend::setConfig(const EGLConfig &config) +{ + m_config = config; + kwinApp()->platform()->setSceneEglConfig(config); +} + +void AbstractEglBackend::setSurface(const EGLSurface &surface) +{ + m_surface = surface; + kwinApp()->platform()->setSceneEglSurface(surface); +} + +QSharedPointer AbstractEglBackend::textureForOutput(AbstractOutput *requestedOutput) const +{ + QSharedPointer texture(new GLTexture(GL_RGBA8, requestedOutput->pixelSize())); + GLRenderTarget renderTarget(*texture); + + const QRect geo = requestedOutput->geometry(); + QRect invGeo(geo.left(), geo.bottom(), geo.width(), -geo.height()); + renderTarget.blitFromFramebuffer(invGeo); + return texture; +} + +AbstractEglTexture::AbstractEglTexture(SceneOpenGLTexture *texture, AbstractEglBackend *backend) + : SceneOpenGLTexturePrivate() + , q(texture) + , m_backend(backend) + , m_image(EGL_NO_IMAGE_KHR) +{ + m_target = GL_TEXTURE_2D; +} + +AbstractEglTexture::~AbstractEglTexture() +{ + if (m_image != EGL_NO_IMAGE_KHR) { + eglDestroyImageKHR(m_backend->eglDisplay(), m_image); + } +} + +OpenGLBackend *AbstractEglTexture::backend() +{ + return m_backend; +} + +bool AbstractEglTexture::loadTexture(WindowPixmap *pixmap) +{ + // FIXME: Refactor this method. + + const auto &buffer = pixmap->buffer(); + if (buffer.isNull()) { + if (updateFromFBO(pixmap->fbo())) { + return true; + } + if (loadInternalImageObject(pixmap)) { + return true; + } + return false; + } + // try Wayland loading + if (auto s = pixmap->surface()) { + s->resetTrackedDamage(); + } + if (buffer->linuxDmabufBuffer()) { + return loadDmabufTexture(buffer); + } else if (buffer->shmBuffer()) { + return loadShmTexture(buffer); + } + return loadEglTexture(buffer); +} + +void AbstractEglTexture::updateTexture(WindowPixmap *pixmap) +{ + // FIXME: Refactor this method. + + const auto &buffer = pixmap->buffer(); + if (buffer.isNull()) { + if (updateFromFBO(pixmap->fbo())) { + return; + } + if (updateFromInternalImageObject(pixmap)) { + return; + } + return; + } + auto s = pixmap->surface(); + if (EglDmabufBuffer *dmabuf = static_cast(buffer->linuxDmabufBuffer())) { + q->bind(); + glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, (GLeglImageOES) dmabuf->images()[0]); //TODO + q->unbind(); + if (m_image != EGL_NO_IMAGE_KHR) { + eglDestroyImageKHR(m_backend->eglDisplay(), m_image); + } + m_image = EGL_NO_IMAGE_KHR; // The wl_buffer has ownership of the image + // The origin in a dmabuf-buffer is at the upper-left corner, so the meaning + // of Y-inverted is the inverse of OpenGL. + q->setYInverted(!(dmabuf->flags() & KWaylandServer::LinuxDmabufUnstableV1Interface::YInverted)); + if (s) { + s->resetTrackedDamage(); + } + return; + } + if (!buffer->shmBuffer()) { + q->bind(); + EGLImageKHR image = attach(buffer); + q->unbind(); + if (image != EGL_NO_IMAGE_KHR) { + if (m_image != EGL_NO_IMAGE_KHR) { + eglDestroyImageKHR(m_backend->eglDisplay(), m_image); + } + m_image = image; + } + if (s) { + s->resetTrackedDamage(); + } + return; + } + // shm fallback + const QImage &image = buffer->data(); + if (image.isNull() || !s) { + return; + } + Q_ASSERT(image.size() == m_size); + const QRegion damage = s->mapToBuffer(s->trackedDamage()); + s->resetTrackedDamage(); + + // TODO: this should be shared with GLTexture::update + createTextureSubImage(image, damage); +} + +bool AbstractEglTexture::createTextureImage(const QImage &image) +{ + if (image.isNull()) { + return false; + } + + glGenTextures(1, &m_texture); + q->setFilter(GL_LINEAR); + q->setWrapMode(GL_CLAMP_TO_EDGE); + + const QSize &size = image.size(); + q->bind(); + GLenum format = 0; + switch (image.format()) { + case QImage::Format_ARGB32: + case QImage::Format_ARGB32_Premultiplied: + format = GL_RGBA8; + break; + case QImage::Format_RGB32: + format = GL_RGB8; + break; + default: + return false; + } + if (GLPlatform::instance()->isGLES()) { + if (s_supportsARGB32 && format == GL_RGBA8) { + const QImage im = image.convertToFormat(QImage::Format_ARGB32_Premultiplied); + glTexImage2D(m_target, 0, GL_BGRA_EXT, im.width(), im.height(), + 0, GL_BGRA_EXT, GL_UNSIGNED_BYTE, im.bits()); + } else { + const QImage im = image.convertToFormat(QImage::Format_RGBA8888_Premultiplied); + glTexImage2D(m_target, 0, GL_RGBA, im.width(), im.height(), + 0, GL_RGBA, GL_UNSIGNED_BYTE, im.bits()); + } + } else { + glTexImage2D(m_target, 0, format, size.width(), size.height(), 0, + GL_BGRA, GL_UNSIGNED_BYTE, image.bits()); + } + q->unbind(); + q->setYInverted(true); + m_size = size; + updateMatrix(); + return true; +} + +void AbstractEglTexture::createTextureSubImage(const QImage &image, const QRegion &damage) +{ + q->bind(); + if (GLPlatform::instance()->isGLES()) { + if (s_supportsARGB32 && (image.format() == QImage::Format_ARGB32 || image.format() == QImage::Format_ARGB32_Premultiplied)) { + const QImage im = image.convertToFormat(QImage::Format_ARGB32_Premultiplied); + for (const QRect &rect : damage) { + glTexSubImage2D(m_target, 0, rect.x(), rect.y(), rect.width(), rect.height(), + GL_BGRA_EXT, GL_UNSIGNED_BYTE, im.copy(rect).bits()); + } + } else { + const QImage im = image.convertToFormat(QImage::Format_RGBA8888_Premultiplied); + for (const QRect &rect : damage) { + glTexSubImage2D(m_target, 0, rect.x(), rect.y(), rect.width(), rect.height(), + GL_RGBA, GL_UNSIGNED_BYTE, im.copy(rect).bits()); + } + } + } else { + const QImage im = image.convertToFormat(QImage::Format_ARGB32_Premultiplied); + for (const QRect &rect : damage) { + glTexSubImage2D(m_target, 0, rect.x(), rect.y(), rect.width(), rect.height(), + GL_BGRA, GL_UNSIGNED_BYTE, im.copy(rect).bits()); + } + } + q->unbind(); +} + +bool AbstractEglTexture::loadShmTexture(const QPointer< KWaylandServer::BufferInterface > &buffer) +{ + return createTextureImage(buffer->data()); +} + +bool AbstractEglTexture::loadEglTexture(const QPointer< KWaylandServer::BufferInterface > &buffer) +{ + if (!eglQueryWaylandBufferWL) { + return false; + } + if (!buffer->resource()) { + return false; + } + + glGenTextures(1, &m_texture); + q->setWrapMode(GL_CLAMP_TO_EDGE); + q->setFilter(GL_LINEAR); + q->bind(); + m_image = attach(buffer); + q->unbind(); + + if (EGL_NO_IMAGE_KHR == m_image) { + qCDebug(KWIN_OPENGL) << "failed to create egl image"; + q->discard(); + return false; + } + + return true; +} + +bool AbstractEglTexture::loadDmabufTexture(const QPointer< KWaylandServer::BufferInterface > &buffer) +{ + auto *dmabuf = static_cast(buffer->linuxDmabufBuffer()); + if (!dmabuf || dmabuf->images()[0] == EGL_NO_IMAGE_KHR) { + qCritical(KWIN_OPENGL) << "Invalid dmabuf-based wl_buffer"; + q->discard(); + return false; + } + + Q_ASSERT(m_image == EGL_NO_IMAGE_KHR); + + glGenTextures(1, &m_texture); + q->setWrapMode(GL_CLAMP_TO_EDGE); + q->setFilter(GL_NEAREST); + q->bind(); + glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, (GLeglImageOES) dmabuf->images()[0]); + q->unbind(); + + m_size = dmabuf->size(); + q->setYInverted(!(dmabuf->flags() & KWaylandServer::LinuxDmabufUnstableV1Interface::YInverted)); + updateMatrix(); + + return true; +} + +bool AbstractEglTexture::loadInternalImageObject(WindowPixmap *pixmap) +{ + return createTextureImage(pixmap->internalImage()); +} + +EGLImageKHR AbstractEglTexture::attach(const QPointer< KWaylandServer::BufferInterface > &buffer) +{ + EGLint format, yInverted; + eglQueryWaylandBufferWL(m_backend->eglDisplay(), buffer->resource(), EGL_TEXTURE_FORMAT, &format); + if (format != EGL_TEXTURE_RGB && format != EGL_TEXTURE_RGBA) { + qCDebug(KWIN_OPENGL) << "Unsupported texture format: " << format; + return EGL_NO_IMAGE_KHR; + } + if (!eglQueryWaylandBufferWL(m_backend->eglDisplay(), buffer->resource(), EGL_WAYLAND_Y_INVERTED_WL, &yInverted)) { + // if EGL_WAYLAND_Y_INVERTED_WL is not supported wl_buffer should be treated as if value were EGL_TRUE + yInverted = EGL_TRUE; + } + + const EGLint attribs[] = { + EGL_WAYLAND_PLANE_WL, 0, + EGL_NONE + }; + EGLImageKHR image = eglCreateImageKHR(m_backend->eglDisplay(), EGL_NO_CONTEXT, EGL_WAYLAND_BUFFER_WL, + (EGLClientBuffer)buffer->resource(), attribs); + if (image != EGL_NO_IMAGE_KHR) { + glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, (GLeglImageOES)image); + m_size = buffer->size(); + updateMatrix(); + q->setYInverted(yInverted); + } + return image; +} + +bool AbstractEglTexture::updateFromFBO(const QSharedPointer &fbo) +{ + if (fbo.isNull()) { + return false; + } + m_texture = fbo->texture(); + m_size = fbo->size(); + q->setWrapMode(GL_CLAMP_TO_EDGE); + q->setFilter(GL_LINEAR); + q->setYInverted(false); + updateMatrix(); + return true; +} + +static QRegion scale(const QRegion ®ion, qreal scaleFactor) +{ + if (scaleFactor == 1) { + return region; + } + + QRegion scaled; + for (const QRect &rect : region) { + scaled += QRect(rect.topLeft() * scaleFactor, rect.size() * scaleFactor); + } + return scaled; +} + +bool AbstractEglTexture::updateFromInternalImageObject(WindowPixmap *pixmap) +{ + const QImage image = pixmap->internalImage(); + if (image.isNull()) { + return false; + } + + if (m_size != image.size()) { + glDeleteTextures(1, &m_texture); + return loadInternalImageObject(pixmap); + } + + createTextureSubImage(image, scale(pixmap->toplevel()->damage(), image.devicePixelRatio())); + + return true; +} + +} diff --git a/platformsupport/scenes/opengl/abstract_egl_backend.h b/platformsupport/scenes/opengl/abstract_egl_backend.h new file mode 100644 index 0000000..d8137f3 --- /dev/null +++ b/platformsupport/scenes/opengl/abstract_egl_backend.h @@ -0,0 +1,119 @@ +/* + 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_ABSTRACT_EGL_BACKEND_H +#define KWIN_ABSTRACT_EGL_BACKEND_H +#include "backend.h" +#include "texture.h" + +#include +#include +#include + +class QOpenGLFramebufferObject; + +namespace KWaylandServer +{ +class BufferInterface; +} + +namespace KWin +{ + +class EglDmabuf; +class AbstractOutput; + +class KWIN_EXPORT AbstractEglBackend : public QObject, public OpenGLBackend +{ + Q_OBJECT +public: + ~AbstractEglBackend() override; + bool makeCurrent() override; + void doneCurrent() override; + + EGLDisplay eglDisplay() const { + return m_display; + } + EGLContext context() const { + return m_context; + } + EGLSurface surface() const { + return m_surface; + } + EGLConfig config() const { + return m_config; + } + + QSharedPointer textureForOutput(AbstractOutput *output) const override; + +protected: + AbstractEglBackend(); + void setEglDisplay(const EGLDisplay &display); + void setSurface(const EGLSurface &surface); + void setConfig(const EGLConfig &config); + void cleanup(); + virtual void cleanupSurfaces(); + bool initEglAPI(); + void initKWinGL(); + void initBufferAge(); + void initClientExtensions(); + void initWayland(); + bool hasClientExtension(const QByteArray &ext) const; + bool isOpenGLES() const; + + bool createContext(); + +private: + void teardown(); + + EGLDisplay m_display = EGL_NO_DISPLAY; + EGLSurface m_surface = EGL_NO_SURFACE; + EGLContext m_context = EGL_NO_CONTEXT; + EGLConfig m_config = nullptr; + QList m_clientExtensions; + EglDmabuf *m_dmaBuf = nullptr; +}; + +class KWIN_EXPORT AbstractEglTexture : public SceneOpenGLTexturePrivate +{ +public: + ~AbstractEglTexture() override; + bool loadTexture(WindowPixmap *pixmap) override; + void updateTexture(WindowPixmap *pixmap) override; + OpenGLBackend *backend() override; + +protected: + AbstractEglTexture(SceneOpenGLTexture *texture, AbstractEglBackend *backend); + EGLImageKHR image() const { + return m_image; + } + void setImage(const EGLImageKHR &img) { + m_image = img; + } + SceneOpenGLTexture *texture() const { + return q; + } + +private: + void createTextureSubImage(const QImage &image, const QRegion &damage); + bool createTextureImage(const QImage &image); + bool loadShmTexture(const QPointer &buffer); + bool loadEglTexture(const QPointer &buffer); + bool loadDmabufTexture(const QPointer< KWaylandServer::BufferInterface > &buffer); + bool loadInternalImageObject(WindowPixmap *pixmap); + EGLImageKHR attach(const QPointer &buffer); + bool updateFromFBO(const QSharedPointer &fbo); + bool updateFromInternalImageObject(WindowPixmap *pixmap); + SceneOpenGLTexture *q; + AbstractEglBackend *m_backend; + EGLImageKHR m_image; +}; + +} + +#endif diff --git a/platformsupport/scenes/opengl/backend.cpp b/platformsupport/scenes/opengl/backend.cpp new file mode 100644 index 0000000..46a68a3 --- /dev/null +++ b/platformsupport/scenes/opengl/backend.cpp @@ -0,0 +1,119 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2009, 2010, 2011 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "backend.h" +#include +#include + +#include "screens.h" + +#include + +namespace KWin +{ + +OpenGLBackend::OpenGLBackend() + : m_syncsToVBlank(false) + , m_blocksForRetrace(false) + , m_directRendering(false) + , m_haveBufferAge(false) + , m_failed(false) +{ +} + +OpenGLBackend::~OpenGLBackend() +{ +} + +void OpenGLBackend::setFailed(const QString &reason) +{ + qCWarning(KWIN_OPENGL) << "Creating the OpenGL rendering failed: " << reason; + m_failed = true; +} + +void OpenGLBackend::idle() +{ + if (hasPendingFlush()) { + effects->makeOpenGLContextCurrent(); + present(); + } +} + +void OpenGLBackend::addToDamageHistory(const QRegion ®ion) +{ + if (m_damageHistory.count() > 10) + m_damageHistory.removeLast(); + + m_damageHistory.prepend(region); +} + +QRegion OpenGLBackend::accumulatedDamageHistory(int bufferAge) const +{ + QRegion region; + + // Note: An age of zero means the buffer contents are undefined + if (bufferAge > 0 && bufferAge <= m_damageHistory.count()) { + for (int i = 0; i < bufferAge - 1; i++) + region |= m_damageHistory[i]; + } else { + const QSize &s = screens()->size(); + region = QRegion(0, 0, s.width(), s.height()); + } + + return region; +} + +OverlayWindow* OpenGLBackend::overlayWindow() const +{ + return nullptr; +} + +QRegion OpenGLBackend::prepareRenderingForScreen(int screenId) +{ + // fallback to repaint complete screen + return screens()->geometry(screenId); +} + +void OpenGLBackend::endRenderingFrameForScreen(int screenId, const QRegion &damage, const QRegion &damagedRegion) +{ + Q_UNUSED(screenId) + Q_UNUSED(damage) + Q_UNUSED(damagedRegion) +} + +bool OpenGLBackend::perScreenRendering() const +{ + return false; +} + +void OpenGLBackend::copyPixels(const QRegion ®ion) +{ + const int height = screens()->size().height(); + for (const QRect &r : region) { + const int x0 = r.x(); + const int y0 = height - r.y() - r.height(); + const int x1 = r.x() + r.width(); + const int y1 = height - r.y(); + + glBlitFramebuffer(x0, y0, x1, y1, x0, y0, x1, y1, GL_COLOR_BUFFER_BIT, GL_NEAREST); + } +} + +QSharedPointer OpenGLBackend::textureForOutput(AbstractOutput* output) const +{ + Q_UNUSED(output) + return {}; +} + +void OpenGLBackend::aboutToStartPainting(const QRegion &damage) +{ + Q_UNUSED(damage) +} + +} diff --git a/platformsupport/scenes/opengl/backend.h b/platformsupport/scenes/opengl/backend.h new file mode 100644 index 0000000..c7fdb56 --- /dev/null +++ b/platformsupport/scenes/opengl/backend.h @@ -0,0 +1,348 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2009, 2010, 2011 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_SCENE_OPENGL_BACKEND_H +#define KWIN_SCENE_OPENGL_BACKEND_H + +#include +#include + +#include + +namespace KWin +{ +class AbstractOutput; +class OpenGLBackend; +class OverlayWindow; +class SceneOpenGL; +class SceneOpenGLTexture; +class SceneOpenGLTexturePrivate; +class WindowPixmap; +class GLTexture; + +/** + * @brief The OpenGLBackend creates and holds the OpenGL context and is responsible for Texture from Pixmap. + * + * The OpenGLBackend is an abstract base class used by the SceneOpenGL to abstract away the differences + * between various OpenGL windowing systems such as GLX and EGL. + * + * A concrete implementation has to create and release the OpenGL context in a way so that the + * SceneOpenGL does not have to care about it. + * + * In addition a major task for this class is to generate the SceneOpenGLTexturePrivate which is + * able to perform the texture from pixmap operation in the given backend. + * + * @author Martin Gräßlin + */ +class KWIN_EXPORT OpenGLBackend +{ +public: + OpenGLBackend(); + virtual ~OpenGLBackend(); + + virtual void init() = 0; + /** + * @return Time passes since start of rendering current frame. + * @see startRenderTimer + */ + qint64 renderTime() { + return m_renderTimer.nsecsElapsed(); + } + virtual void screenGeometryChanged(const QSize &size) = 0; + virtual SceneOpenGLTexturePrivate *createBackendTexture(SceneOpenGLTexture *texture) = 0; + + /** + * @brief Backend specific code to prepare the rendering of a frame including flushing the + * previously rendered frame to the screen if the backend works this way. + * + * @return A region that if not empty will be repainted in addition to the damaged region + */ + virtual QRegion prepareRenderingFrame() = 0; + + /** + * Notifies about starting to paint. + * + * @p damage contains the reported damage as suggested by windows and effects on prepaint calls. + */ + virtual void aboutToStartPainting(const QRegion &damage); + + /** + * @brief Backend specific code to handle the end of rendering a frame. + * + * @param renderedRegion The possibly larger region that has been rendered + * @param damagedRegion The damaged region that should be posted + */ + virtual void endRenderingFrame(const QRegion &damage, const QRegion &damagedRegion) = 0; + virtual void endRenderingFrameForScreen(int screenId, const QRegion &damage, const QRegion &damagedRegion); + virtual bool makeCurrent() = 0; + virtual void doneCurrent() = 0; + virtual bool usesOverlayWindow() const = 0; + /** + * Whether the rendering needs to be split per screen. + * Default implementation returns @c false. + */ + virtual bool perScreenRendering() const; + virtual QRegion prepareRenderingForScreen(int screenId); + /** + * @brief Compositor is going into idle mode, flushes any pending paints. + */ + void idle(); + + /** + * @return bool Whether the scene needs to flush a frame. + */ + bool hasPendingFlush() const { + return !m_lastDamage.isEmpty(); + } + + /** + * @brief Returns the OverlayWindow used by the backend. + * + * A backend does not have to use an OverlayWindow, this is mostly for the X world. + * In case the backend does not use an OverlayWindow it is allowed to return @c null. + * It's the task of the caller to check whether it is @c null. + * + * @return :OverlayWindow* + */ + virtual OverlayWindow *overlayWindow() const; + /** + * @brief Whether the creation of the Backend failed. + * + * The SceneOpenGL should test whether the Backend got constructed correctly. If this method + * returns @c true, the SceneOpenGL should not try to start the rendering. + * + * @return bool @c true if the creation of the Backend failed, @c false otherwise. + */ + bool isFailed() const { + return m_failed; + } + /** + * @brief Whether the Backend provides VSync. + * + * Currently only the GLX backend can provide VSync. + * + * @return bool @c true if VSync support is available, @c false otherwise + */ + bool syncsToVBlank() const { + return m_syncsToVBlank; + } + /** + * @brief Whether VSync blocks execution until the screen is in the retrace + * + * Case for waitVideoSync and non triple buffering buffer swaps + * + */ + bool blocksForRetrace() const { + return m_blocksForRetrace; + } + /** + * @brief Whether the backend uses direct rendering. + * + * Some OpenGLScene modes require direct rendering. E.g. the OpenGL 2 should not be used + * if direct rendering is not supported by the Scene. + * + * @return bool @c true if the GL context is direct, @c false if indirect + */ + bool isDirectRendering() const { + return m_directRendering; + } + + bool supportsBufferAge() const { + return m_haveBufferAge; + } + + bool supportsPartialUpdate() const + { + return m_havePartialUpdate; + } + bool supportsSwapBuffersWithDamage() const + { + return m_haveSwapBuffersWithDamage; + } + + bool supportsSurfacelessContext() const + { + return m_haveSurfacelessContext; + } + + /** + * Returns the damage that has accumulated since a buffer of the given age was presented. + */ + QRegion accumulatedDamageHistory(int bufferAge) const; + + /** + * Saves the given region to damage history. + */ + void addToDamageHistory(const QRegion ®ion); + + /** + * The backend specific extensions (e.g. EGL/GLX extensions). + * + * Not the OpenGL (ES) extension! + */ + QList extensions() const { + return m_extensions; + } + + /** + * @returns whether the backend specific extensions contains @p extension. + */ + bool hasExtension(const QByteArray &extension) const { + return m_extensions.contains(extension); + } + + /** + * Copy a region of pixels from the current read to the current draw buffer + */ + void copyPixels(const QRegion ®ion); + + virtual QSharedPointer textureForOutput(AbstractOutput *output) const; + +protected: + /** + * @brief Backend specific flushing of frame to screen. + */ + virtual void present() = 0; + /** + * @brief Sets the backend initialization to failed. + * + * This method should be called by the concrete subclass in case the initialization failed. + * The given @p reason is logged as a warning. + * + * @param reason The reason why the initialization failed. + */ + void setFailed(const QString &reason); + /** + * @brief Sets whether the backend provides VSync. + * + * Should be called by the concrete subclass once it is determined whether VSync is supported. + * If the subclass does not call this method, the backend defaults to @c false. + * @param enabled @c true if VSync support available, @c false otherwise. + */ + void setSyncsToVBlank(bool enabled) { + m_syncsToVBlank = enabled; + } + /** + * @brief Sets whether the VSync iplementation blocks + * + * Should be called by the concrete subclass once it is determined how VSync works. + * If the subclass does not call this method, the backend defaults to @c false. + * @param enabled @c true if VSync blocks, @c false otherwise. + */ + void setBlocksForRetrace(bool enabled) { + m_blocksForRetrace = enabled; + } + /** + * @brief Sets whether the OpenGL context is direct. + * + * Should be called by the concrete subclass once it is determined whether the OpenGL context is + * direct or indirect. + * If the subclass does not call this method, the backend defaults to @c false. + * + * @param direct @c true if the OpenGL context is direct, @c false if indirect + */ + void setIsDirectRendering(bool direct) { + m_directRendering = direct; + } + + void setSupportsBufferAge(bool value) { + m_haveBufferAge = value; + } + + void setSupportsPartialUpdate(bool value) + { + m_havePartialUpdate = value; + } + + void setSupportsSwapBuffersWithDamage(bool value) + { + m_haveSwapBuffersWithDamage = value; + } + + void setSupportsSurfacelessContext(bool value) + { + m_haveSurfacelessContext = value; + } + + /** + * @return const QRegion& Damage of previously rendered frame + */ + const QRegion &lastDamage() const { + return m_lastDamage; + } + void setLastDamage(const QRegion &damage) { + m_lastDamage = damage; + } + /** + * @brief Starts the timer for how long it takes to render the frame. + * + * @see renderTime + */ + void startRenderTimer() { + m_renderTimer.start(); + } + + /** + * Sets the platform-specific @p extensions. + * + * These are the EGL/GLX extensions, not the OpenGL extensions + */ + void setExtensions(const QList &extensions) { + m_extensions = extensions; + } + +private: + /** + * @brief Whether VSync is available and used, defaults to @c false. + */ + bool m_syncsToVBlank; + /** + * @brief Whether present() will block execution until the next vertical retrace @c false. + */ + bool m_blocksForRetrace; + /** + * @brief Whether direct rendering is used, defaults to @c false. + */ + bool m_directRendering; + /** + * @brief Whether the backend supports GLX_EXT_buffer_age / EGL_EXT_buffer_age. + */ + bool m_haveBufferAge; + /** + * @brief Whether the backend supports EGL_KHR_partial_update + */ + bool m_havePartialUpdate; + bool m_haveSwapBuffersWithDamage = false; + /** + * @brief Whether the backend supports EGL_KHR_surfaceless_context. + */ + bool m_haveSurfacelessContext = false; + /** + * @brief Whether the initialization failed, of course default to @c false. + */ + bool m_failed; + /** + * @brief Damaged region of previously rendered frame. + */ + QRegion m_lastDamage; + /** + * @brief The damage history for the past 10 frames. + */ + QList m_damageHistory; + /** + * @brief Timer to measure how long a frame renders. + */ + QElapsedTimer m_renderTimer; + + QList m_extensions; +}; + +} + +#endif diff --git a/platformsupport/scenes/opengl/drm_fourcc.h b/platformsupport/scenes/opengl/drm_fourcc.h new file mode 100644 index 0000000..eacaf13 --- /dev/null +++ b/platformsupport/scenes/opengl/drm_fourcc.h @@ -0,0 +1,421 @@ +/* + * SPDX-FileCopyrightText: 2011 Intel Corporation + * + * 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 + * VA LINUX SYSTEMS AND/OR ITS SUPPLIERS BE LIABLE FOR ANY CLAIM, DAMAGES OR + * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +#ifndef DRM_FOURCC_H +#define DRM_FOURCC_H + +//#include "drm.h" + +// These typedefs are copied from drm.h +#if defined(__linux__) + +#include +#include + +#else /* One of the BSDs */ +typedef uint32_t __u32; +typedef uint64_t __u64; +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +#define fourcc_code(a, b, c, d) ((__u32)(a) | ((__u32)(b) << 8) | \ + ((__u32)(c) << 16) | ((__u32)(d) << 24)) + +#define DRM_FORMAT_BIG_ENDIAN (1<<31) /* format is big endian instead of little endian */ + +/* color index */ +#define DRM_FORMAT_C8 fourcc_code('C', '8', ' ', ' ') /* [7:0] C */ + +/* 8 bpp Red */ +#define DRM_FORMAT_R8 fourcc_code('R', '8', ' ', ' ') /* [7:0] R */ + +/* 16 bpp Red */ +#define DRM_FORMAT_R16 fourcc_code('R', '1', '6', ' ') /* [15:0] R little endian */ + +/* 16 bpp RG */ +#define DRM_FORMAT_RG88 fourcc_code('R', 'G', '8', '8') /* [15:0] R:G 8:8 little endian */ +#define DRM_FORMAT_GR88 fourcc_code('G', 'R', '8', '8') /* [15:0] G:R 8:8 little endian */ + +/* 32 bpp RG */ +#define DRM_FORMAT_RG1616 fourcc_code('R', 'G', '3', '2') /* [31:0] R:G 16:16 little endian */ +#define DRM_FORMAT_GR1616 fourcc_code('G', 'R', '3', '2') /* [31:0] G:R 16:16 little endian */ + +/* 8 bpp RGB */ +#define DRM_FORMAT_RGB332 fourcc_code('R', 'G', 'B', '8') /* [7:0] R:G:B 3:3:2 */ +#define DRM_FORMAT_BGR233 fourcc_code('B', 'G', 'R', '8') /* [7:0] B:G:R 2:3:3 */ + +/* 16 bpp RGB */ +#define DRM_FORMAT_XRGB4444 fourcc_code('X', 'R', '1', '2') /* [15:0] x:R:G:B 4:4:4:4 little endian */ +#define DRM_FORMAT_XBGR4444 fourcc_code('X', 'B', '1', '2') /* [15:0] x:B:G:R 4:4:4:4 little endian */ +#define DRM_FORMAT_RGBX4444 fourcc_code('R', 'X', '1', '2') /* [15:0] R:G:B:x 4:4:4:4 little endian */ +#define DRM_FORMAT_BGRX4444 fourcc_code('B', 'X', '1', '2') /* [15:0] B:G:R:x 4:4:4:4 little endian */ + +#define DRM_FORMAT_ARGB4444 fourcc_code('A', 'R', '1', '2') /* [15:0] A:R:G:B 4:4:4:4 little endian */ +#define DRM_FORMAT_ABGR4444 fourcc_code('A', 'B', '1', '2') /* [15:0] A:B:G:R 4:4:4:4 little endian */ +#define DRM_FORMAT_RGBA4444 fourcc_code('R', 'A', '1', '2') /* [15:0] R:G:B:A 4:4:4:4 little endian */ +#define DRM_FORMAT_BGRA4444 fourcc_code('B', 'A', '1', '2') /* [15:0] B:G:R:A 4:4:4:4 little endian */ + +#define DRM_FORMAT_XRGB1555 fourcc_code('X', 'R', '1', '5') /* [15:0] x:R:G:B 1:5:5:5 little endian */ +#define DRM_FORMAT_XBGR1555 fourcc_code('X', 'B', '1', '5') /* [15:0] x:B:G:R 1:5:5:5 little endian */ +#define DRM_FORMAT_RGBX5551 fourcc_code('R', 'X', '1', '5') /* [15:0] R:G:B:x 5:5:5:1 little endian */ +#define DRM_FORMAT_BGRX5551 fourcc_code('B', 'X', '1', '5') /* [15:0] B:G:R:x 5:5:5:1 little endian */ + +#define DRM_FORMAT_ARGB1555 fourcc_code('A', 'R', '1', '5') /* [15:0] A:R:G:B 1:5:5:5 little endian */ +#define DRM_FORMAT_ABGR1555 fourcc_code('A', 'B', '1', '5') /* [15:0] A:B:G:R 1:5:5:5 little endian */ +#define DRM_FORMAT_RGBA5551 fourcc_code('R', 'A', '1', '5') /* [15:0] R:G:B:A 5:5:5:1 little endian */ +#define DRM_FORMAT_BGRA5551 fourcc_code('B', 'A', '1', '5') /* [15:0] B:G:R:A 5:5:5:1 little endian */ + +#define DRM_FORMAT_RGB565 fourcc_code('R', 'G', '1', '6') /* [15:0] R:G:B 5:6:5 little endian */ +#define DRM_FORMAT_BGR565 fourcc_code('B', 'G', '1', '6') /* [15:0] B:G:R 5:6:5 little endian */ + +/* 24 bpp RGB */ +#define DRM_FORMAT_RGB888 fourcc_code('R', 'G', '2', '4') /* [23:0] R:G:B little endian */ +#define DRM_FORMAT_BGR888 fourcc_code('B', 'G', '2', '4') /* [23:0] B:G:R little endian */ + +/* 32 bpp RGB */ +#define DRM_FORMAT_XRGB8888 fourcc_code('X', 'R', '2', '4') /* [31:0] x:R:G:B 8:8:8:8 little endian */ +#define DRM_FORMAT_XBGR8888 fourcc_code('X', 'B', '2', '4') /* [31:0] x:B:G:R 8:8:8:8 little endian */ +#define DRM_FORMAT_RGBX8888 fourcc_code('R', 'X', '2', '4') /* [31:0] R:G:B:x 8:8:8:8 little endian */ +#define DRM_FORMAT_BGRX8888 fourcc_code('B', 'X', '2', '4') /* [31:0] B:G:R:x 8:8:8:8 little endian */ + +#define DRM_FORMAT_ARGB8888 fourcc_code('A', 'R', '2', '4') /* [31:0] A:R:G:B 8:8:8:8 little endian */ +#define DRM_FORMAT_ABGR8888 fourcc_code('A', 'B', '2', '4') /* [31:0] A:B:G:R 8:8:8:8 little endian */ +#define DRM_FORMAT_RGBA8888 fourcc_code('R', 'A', '2', '4') /* [31:0] R:G:B:A 8:8:8:8 little endian */ +#define DRM_FORMAT_BGRA8888 fourcc_code('B', 'A', '2', '4') /* [31:0] B:G:R:A 8:8:8:8 little endian */ + +#define DRM_FORMAT_XRGB2101010 fourcc_code('X', 'R', '3', '0') /* [31:0] x:R:G:B 2:10:10:10 little endian */ +#define DRM_FORMAT_XBGR2101010 fourcc_code('X', 'B', '3', '0') /* [31:0] x:B:G:R 2:10:10:10 little endian */ +#define DRM_FORMAT_RGBX1010102 fourcc_code('R', 'X', '3', '0') /* [31:0] R:G:B:x 10:10:10:2 little endian */ +#define DRM_FORMAT_BGRX1010102 fourcc_code('B', 'X', '3', '0') /* [31:0] B:G:R:x 10:10:10:2 little endian */ + +#define DRM_FORMAT_ARGB2101010 fourcc_code('A', 'R', '3', '0') /* [31:0] A:R:G:B 2:10:10:10 little endian */ +#define DRM_FORMAT_ABGR2101010 fourcc_code('A', 'B', '3', '0') /* [31:0] A:B:G:R 2:10:10:10 little endian */ +#define DRM_FORMAT_RGBA1010102 fourcc_code('R', 'A', '3', '0') /* [31:0] R:G:B:A 10:10:10:2 little endian */ +#define DRM_FORMAT_BGRA1010102 fourcc_code('B', 'A', '3', '0') /* [31:0] B:G:R:A 10:10:10:2 little endian */ + +/* packed YCbCr */ +#define DRM_FORMAT_YUYV fourcc_code('Y', 'U', 'Y', 'V') /* [31:0] Cr0:Y1:Cb0:Y0 8:8:8:8 little endian */ +#define DRM_FORMAT_YVYU fourcc_code('Y', 'V', 'Y', 'U') /* [31:0] Cb0:Y1:Cr0:Y0 8:8:8:8 little endian */ +#define DRM_FORMAT_UYVY fourcc_code('U', 'Y', 'V', 'Y') /* [31:0] Y1:Cr0:Y0:Cb0 8:8:8:8 little endian */ +#define DRM_FORMAT_VYUY fourcc_code('V', 'Y', 'U', 'Y') /* [31:0] Y1:Cb0:Y0:Cr0 8:8:8:8 little endian */ + +#define DRM_FORMAT_AYUV fourcc_code('A', 'Y', 'U', 'V') /* [31:0] A:Y:Cb:Cr 8:8:8:8 little endian */ + +/* + * 2 plane RGB + A + * index 0 = RGB plane, same format as the corresponding non _A8 format has + * index 1 = A plane, [7:0] A + */ +#define DRM_FORMAT_XRGB8888_A8 fourcc_code('X', 'R', 'A', '8') +#define DRM_FORMAT_XBGR8888_A8 fourcc_code('X', 'B', 'A', '8') +#define DRM_FORMAT_RGBX8888_A8 fourcc_code('R', 'X', 'A', '8') +#define DRM_FORMAT_BGRX8888_A8 fourcc_code('B', 'X', 'A', '8') +#define DRM_FORMAT_RGB888_A8 fourcc_code('R', '8', 'A', '8') +#define DRM_FORMAT_BGR888_A8 fourcc_code('B', '8', 'A', '8') +#define DRM_FORMAT_RGB565_A8 fourcc_code('R', '5', 'A', '8') +#define DRM_FORMAT_BGR565_A8 fourcc_code('B', '5', 'A', '8') + +/* + * 2 plane YCbCr + * index 0 = Y plane, [7:0] Y + * index 1 = Cr:Cb plane, [15:0] Cr:Cb little endian + * or + * index 1 = Cb:Cr plane, [15:0] Cb:Cr little endian + */ +#define DRM_FORMAT_NV12 fourcc_code('N', 'V', '1', '2') /* 2x2 subsampled Cr:Cb plane */ +#define DRM_FORMAT_NV21 fourcc_code('N', 'V', '2', '1') /* 2x2 subsampled Cb:Cr plane */ +#define DRM_FORMAT_NV16 fourcc_code('N', 'V', '1', '6') /* 2x1 subsampled Cr:Cb plane */ +#define DRM_FORMAT_NV61 fourcc_code('N', 'V', '6', '1') /* 2x1 subsampled Cb:Cr plane */ +#define DRM_FORMAT_NV24 fourcc_code('N', 'V', '2', '4') /* non-subsampled Cr:Cb plane */ +#define DRM_FORMAT_NV42 fourcc_code('N', 'V', '4', '2') /* non-subsampled Cb:Cr plane */ + +/* + * 3 plane YCbCr + * index 0: Y plane, [7:0] Y + * index 1: Cb plane, [7:0] Cb + * index 2: Cr plane, [7:0] Cr + * or + * index 1: Cr plane, [7:0] Cr + * index 2: Cb plane, [7:0] Cb + */ +#define DRM_FORMAT_YUV410 fourcc_code('Y', 'U', 'V', '9') /* 4x4 subsampled Cb (1) and Cr (2) planes */ +#define DRM_FORMAT_YVU410 fourcc_code('Y', 'V', 'U', '9') /* 4x4 subsampled Cr (1) and Cb (2) planes */ +#define DRM_FORMAT_YUV411 fourcc_code('Y', 'U', '1', '1') /* 4x1 subsampled Cb (1) and Cr (2) planes */ +#define DRM_FORMAT_YVU411 fourcc_code('Y', 'V', '1', '1') /* 4x1 subsampled Cr (1) and Cb (2) planes */ +#define DRM_FORMAT_YUV420 fourcc_code('Y', 'U', '1', '2') /* 2x2 subsampled Cb (1) and Cr (2) planes */ +#define DRM_FORMAT_YVU420 fourcc_code('Y', 'V', '1', '2') /* 2x2 subsampled Cr (1) and Cb (2) planes */ +#define DRM_FORMAT_YUV422 fourcc_code('Y', 'U', '1', '6') /* 2x1 subsampled Cb (1) and Cr (2) planes */ +#define DRM_FORMAT_YVU422 fourcc_code('Y', 'V', '1', '6') /* 2x1 subsampled Cr (1) and Cb (2) planes */ +#define DRM_FORMAT_YUV444 fourcc_code('Y', 'U', '2', '4') /* non-subsampled Cb (1) and Cr (2) planes */ +#define DRM_FORMAT_YVU444 fourcc_code('Y', 'V', '2', '4') /* non-subsampled Cr (1) and Cb (2) planes */ + + +/* + * Format Modifiers: + * + * Format modifiers describe, typically, a re-ordering or modification + * of the data in a plane of an FB. This can be used to express tiled/ + * swizzled formats, or compression, or a combination of the two. + * + * The upper 8 bits of the format modifier are a vendor-id as assigned + * below. The lower 56 bits are assigned as vendor sees fit. + */ + +/* Vendor Ids: */ +#define DRM_FORMAT_MOD_NONE 0 +#define DRM_FORMAT_MOD_VENDOR_NONE 0 +#define DRM_FORMAT_MOD_VENDOR_INTEL 0x01 +#define DRM_FORMAT_MOD_VENDOR_AMD 0x02 +#define DRM_FORMAT_MOD_VENDOR_NV 0x03 +#define DRM_FORMAT_MOD_VENDOR_SAMSUNG 0x04 +#define DRM_FORMAT_MOD_VENDOR_QCOM 0x05 +#define DRM_FORMAT_MOD_VENDOR_VIVANTE 0x06 +#define DRM_FORMAT_MOD_VENDOR_BROADCOM 0x07 +/* add more to the end as needed */ + +#define DRM_FORMAT_RESERVED ((1ULL << 56) - 1) + +#define fourcc_mod_code(vendor, val) \ + ((((__u64)DRM_FORMAT_MOD_VENDOR_## vendor) << 56) | (val & 0x00ffffffffffffffULL)) + +/* + * Format Modifier tokens: + * + * When adding a new token please document the layout with a code comment, + * similar to the fourcc codes above. drm_fourcc.h is considered the + * authoritative source for all of these. + */ + +/* + * Invalid Modifier + * + * This modifier can be used as a sentinel to terminate the format modifiers + * list, or to initialize a variable with an invalid modifier. It might also be + * used to report an error back to userspace for certain APIs. + */ +#define DRM_FORMAT_MOD_INVALID fourcc_mod_code(NONE, DRM_FORMAT_RESERVED) + +/* + * Linear Layout + * + * Just plain linear layout. Note that this is different from no specifying any + * modifier (e.g. not setting DRM_MODE_FB_MODIFIERS in the DRM_ADDFB2 ioctl), + * which tells the driver to also take driver-internal information into account + * and so might actually result in a tiled framebuffer. + */ +#define DRM_FORMAT_MOD_LINEAR fourcc_mod_code(NONE, 0) + +/* Intel framebuffer modifiers */ + +/* + * Intel X-tiling layout + * + * This is a tiled layout using 4Kb tiles (except on gen2 where the tiles 2Kb) + * in row-major layout. Within the tile bytes are laid out row-major, with + * a platform-dependent stride. On top of that the memory can apply + * platform-depending swizzling of some higher address bits into bit6. + * + * This format is highly platforms specific and not useful for cross-driver + * sharing. It exists since on a given platform it does uniquely identify the + * layout in a simple way for i915-specific userspace. + */ +#define I915_FORMAT_MOD_X_TILED fourcc_mod_code(INTEL, 1) + +/* + * Intel Y-tiling layout + * + * This is a tiled layout using 4Kb tiles (except on gen2 where the tiles 2Kb) + * in row-major layout. Within the tile bytes are laid out in OWORD (16 bytes) + * chunks column-major, with a platform-dependent height. On top of that the + * memory can apply platform-depending swizzling of some higher address bits + * into bit6. + * + * This format is highly platforms specific and not useful for cross-driver + * sharing. It exists since on a given platform it does uniquely identify the + * layout in a simple way for i915-specific userspace. + */ +#define I915_FORMAT_MOD_Y_TILED fourcc_mod_code(INTEL, 2) + +/* + * Intel Yf-tiling layout + * + * This is a tiled layout using 4Kb tiles in row-major layout. + * Within the tile pixels are laid out in 16 256 byte units / sub-tiles which + * are arranged in four groups (two wide, two high) with column-major layout. + * Each group therefore consits out of four 256 byte units, which are also laid + * out as 2x2 column-major. + * 256 byte units are made out of four 64 byte blocks of pixels, producing + * either a square block or a 2:1 unit. + * 64 byte blocks of pixels contain four pixel rows of 16 bytes, where the width + * in pixel depends on the pixel depth. + */ +#define I915_FORMAT_MOD_Yf_TILED fourcc_mod_code(INTEL, 3) + +/* + * Intel color control surface (CCS) for render compression + * + * The framebuffer format must be one of the 8:8:8:8 RGB formats. + * The main surface will be plane index 0 and must be Y/Yf-tiled, + * the CCS will be plane index 1. + * + * Each CCS tile matches a 1024x512 pixel area of the main surface. + * To match certain aspects of the 3D hardware the CCS is + * considered to be made up of normal 128Bx32 Y tiles, Thus + * the CCS pitch must be specified in multiples of 128 bytes. + * + * In reality the CCS tile appears to be a 64Bx64 Y tile, composed + * of QWORD (8 bytes) chunks instead of OWORD (16 bytes) chunks. + * But that fact is not relevant unless the memory is accessed + * directly. + */ +#define I915_FORMAT_MOD_Y_TILED_CCS fourcc_mod_code(INTEL, 4) +#define I915_FORMAT_MOD_Yf_TILED_CCS fourcc_mod_code(INTEL, 5) + +/* + * Tiled, NV12MT, grouped in 64 (pixels) x 32 (lines) -sized macroblocks + * + * Macroblocks are laid in a Z-shape, and each pixel data is following the + * standard NV12 style. + * As for NV12, an image is the result of two frame buffers: one for Y, + * one for the interleaved Cb/Cr components (1/2 the height of the Y buffer). + * Alignment requirements are (for each buffer): + * - multiple of 128 pixels for the width + * - multiple of 32 pixels for the height + * + * For more information: see https://linuxtv.org/downloads/v4l-dvb-apis/re32.html + */ +#define DRM_FORMAT_MOD_SAMSUNG_64_32_TILE fourcc_mod_code(SAMSUNG, 1) + +/* Vivante framebuffer modifiers */ + +/* + * Vivante 4x4 tiling layout + * + * This is a simple tiled layout using tiles of 4x4 pixels in a row-major + * layout. + */ +#define DRM_FORMAT_MOD_VIVANTE_TILED fourcc_mod_code(VIVANTE, 1) + +/* + * Vivante 64x64 super-tiling layout + * + * This is a tiled layout using 64x64 pixel super-tiles, where each super-tile + * contains 8x4 groups of 2x4 tiles of 4x4 pixels (like above) each, all in row- + * major layout. + * + * For more information: see + * https://github.com/etnaviv/etna_viv/blob/master/doc/hardware.md#texture-tiling + */ +#define DRM_FORMAT_MOD_VIVANTE_SUPER_TILED fourcc_mod_code(VIVANTE, 2) + +/* + * Vivante 4x4 tiling layout for dual-pipe + * + * Same as the 4x4 tiling layout, except every second 4x4 pixel tile starts at a + * different base address. Offsets from the base addresses are therefore halved + * compared to the non-split tiled layout. + */ +#define DRM_FORMAT_MOD_VIVANTE_SPLIT_TILED fourcc_mod_code(VIVANTE, 3) + +/* + * Vivante 64x64 super-tiling layout for dual-pipe + * + * Same as the 64x64 super-tiling layout, except every second 4x4 pixel tile + * starts at a different base address. Offsets from the base addresses are + * therefore halved compared to the non-split super-tiled layout. + */ +#define DRM_FORMAT_MOD_VIVANTE_SPLIT_SUPER_TILED fourcc_mod_code(VIVANTE, 4) + +/* NVIDIA Tegra frame buffer modifiers */ + +/* + * Some modifiers take parameters, for example the number of vertical GOBs in + * a block. Reserve the lower 32 bits for parameters + */ +#define __fourcc_mod_tegra_mode_shift 32 +#define fourcc_mod_tegra_code(val, params) \ + fourcc_mod_code(NV, ((((__u64)val) << __fourcc_mod_tegra_mode_shift) | params)) +#define fourcc_mod_tegra_mod(m) \ + (m & ~((1ULL << __fourcc_mod_tegra_mode_shift) - 1)) +#define fourcc_mod_tegra_param(m) \ + (m & ((1ULL << __fourcc_mod_tegra_mode_shift) - 1)) + +/* + * Tegra Tiled Layout, used by Tegra 2, 3 and 4. + * + * Pixels are arranged in simple tiles of 16 x 16 bytes. + */ +#define NV_FORMAT_MOD_TEGRA_TILED fourcc_mod_tegra_code(1, 0) + +/* + * Tegra 16Bx2 Block Linear layout, used by TK1/TX1 + * + * Pixels are arranged in 64x8 Groups Of Bytes (GOBs). GOBs are then stacked + * vertically by a power of 2 (1 to 32 GOBs) to form a block. + * + * Within a GOB, data is ordered as 16B x 2 lines sectors laid in Z-shape. + * + * Parameter 'v' is the log2 encoding of the number of GOBs stacked vertically. + * Valid values are: + * + * 0 == ONE_GOB + * 1 == TWO_GOBS + * 2 == FOUR_GOBS + * 3 == EIGHT_GOBS + * 4 == SIXTEEN_GOBS + * 5 == THIRTYTWO_GOBS + * + * Chapter 20 "Pixel Memory Formats" of the Tegra X1 TRM describes this format + * in full detail. + */ +#define NV_FORMAT_MOD_TEGRA_16BX2_BLOCK(v) fourcc_mod_tegra_code(2, v) + +/* + * Broadcom VC4 "T" format + * + * This is the primary layout that the V3D GPU can texture from (it + * can't do linear). The T format has: + * + * - 64b utiles of pixels in a raster-order grid according to cpp. It's 4x4 + * pixels at 32 bit depth. + * + * - 1k subtiles made of a 4x4 raster-order grid of 64b utiles (so usually + * 16x16 pixels). + * + * - 4k tiles made of a 2x2 grid of 1k subtiles (so usually 32x32 pixels). On + * even 4k tile rows, they're arranged as (BL, TL, TR, BR), and on odd rows + * they're (TR, BR, BL, TL), where bottom left is start of memory. + * + * - an image made of 4k tiles in rows either left-to-right (even rows of 4k + * tiles) or right-to-left (odd rows of 4k tiles). + */ +#define DRM_FORMAT_MOD_BROADCOM_VC4_T_TILED fourcc_mod_code(BROADCOM, 1) + +#if defined(__cplusplus) +} +#endif + +#endif /* DRM_FOURCC_H */ diff --git a/platformsupport/scenes/opengl/egl_dmabuf.cpp b/platformsupport/scenes/opengl/egl_dmabuf.cpp new file mode 100644 index 0000000..45d3251 --- /dev/null +++ b/platformsupport/scenes/opengl/egl_dmabuf.cpp @@ -0,0 +1,443 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Roman Gilg + SPDX-FileCopyrightText: 2018 Fredrik Höglund + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "egl_dmabuf.h" + +#include "drm_fourcc.h" +#include "kwineglext.h" +#include "../../../wayland_server.h" + +#include + +namespace KWin +{ + +typedef EGLBoolean (*eglQueryDmaBufFormatsEXT_func) (EGLDisplay dpy, EGLint max_formats, EGLint *formats, EGLint *num_formats); +typedef EGLBoolean (*eglQueryDmaBufModifiersEXT_func) (EGLDisplay dpy, EGLint format, EGLint max_modifiers, EGLuint64KHR *modifiers, EGLBoolean *external_only, EGLint *num_modifiers); +eglQueryDmaBufFormatsEXT_func eglQueryDmaBufFormatsEXT = nullptr; +eglQueryDmaBufModifiersEXT_func eglQueryDmaBufModifiersEXT = nullptr; + +struct YuvPlane +{ + int widthDivisor; + int heightDivisor; + uint32_t format; + int planeIndex; +}; + +struct YuvFormat +{ + uint32_t format; + int inputPlanes; + int outputPlanes; + int textureType; + struct YuvPlane planes[3]; +}; + +YuvFormat yuvFormats[] = { + { + DRM_FORMAT_YUYV, + 1, 2, + EGL_TEXTURE_Y_XUXV_WL, + { + { + 1, 1, + DRM_FORMAT_GR88, + 0 + }, + { + 2, 1, + DRM_FORMAT_ARGB8888, + 0 + } + } + }, + { + DRM_FORMAT_NV12, + 2, 2, + EGL_TEXTURE_Y_UV_WL, + { + { + 1, 1, + DRM_FORMAT_R8, + 0 + }, + { + 2, 2, + DRM_FORMAT_GR88, + 1 + } + } + }, + { + DRM_FORMAT_YUV420, + 3, 3, + EGL_TEXTURE_Y_U_V_WL, + { + { + 1, 1, + DRM_FORMAT_R8, + 0 + }, + { + 2, 2, + DRM_FORMAT_R8, + 1 + }, + { + 2, 2, + DRM_FORMAT_R8, + 2 + } + } + }, + { + DRM_FORMAT_YUV444, + 3, 3, + EGL_TEXTURE_Y_U_V_WL, + { + { + 1, 1, + DRM_FORMAT_R8, + 0 + }, + { + 1, 1, + DRM_FORMAT_R8, + 1 + }, + { + 1, 1, + DRM_FORMAT_R8, + 2 + } + } + } +}; + +EglDmabufBuffer::EglDmabufBuffer(EGLImage image, + const QVector &planes, + uint32_t format, + const QSize &size, + Flags flags, + EglDmabuf *interfaceImpl) + : EglDmabufBuffer(planes, format, size, flags, interfaceImpl) +{ + m_importType = ImportType::Direct; + addImage(image); +} + + +EglDmabufBuffer::EglDmabufBuffer(const QVector &planes, + uint32_t format, + const QSize &size, + Flags flags, + EglDmabuf *interfaceImpl) + : DmabufBuffer(planes, format, size, flags) + , m_interfaceImpl(interfaceImpl) +{ + m_importType = ImportType::Conversion; +} + +EglDmabufBuffer::~EglDmabufBuffer() +{ + removeImages(); +} + +void EglDmabufBuffer::setInterfaceImplementation(EglDmabuf *interfaceImpl) +{ + m_interfaceImpl = interfaceImpl; +} + +void EglDmabufBuffer::addImage(EGLImage image) +{ + m_images << image; +} + +void EglDmabufBuffer::removeImages() +{ + for (auto image : m_images) { + eglDestroyImageKHR(m_interfaceImpl->m_backend->eglDisplay(), image); + } + m_images.clear(); +} + +using Plane = KWaylandServer::LinuxDmabufUnstableV1Interface::Plane; +using Flags = KWaylandServer::LinuxDmabufUnstableV1Interface::Flags; + +EGLImage EglDmabuf::createImage(const QVector &planes, + uint32_t format, + const QSize &size) +{ + const bool hasModifiers = eglQueryDmaBufModifiersEXT != nullptr && + planes[0].modifier != DRM_FORMAT_MOD_INVALID; + + QVector attribs; + attribs << EGL_WIDTH << size.width() + << EGL_HEIGHT << size.height() + << EGL_LINUX_DRM_FOURCC_EXT << EGLint(format) + + << EGL_DMA_BUF_PLANE0_FD_EXT << planes[0].fd + << EGL_DMA_BUF_PLANE0_OFFSET_EXT << EGLint(planes[0].offset) + << EGL_DMA_BUF_PLANE0_PITCH_EXT << EGLint(planes[0].stride); + + if (hasModifiers) { + attribs + << EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT << EGLint(planes[0].modifier & 0xffffffff) + << EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT << EGLint(planes[0].modifier >> 32); + } + + if (planes.count() > 1) { + attribs + << EGL_DMA_BUF_PLANE1_FD_EXT << planes[1].fd + << EGL_DMA_BUF_PLANE1_OFFSET_EXT << EGLint(planes[1].offset) + << EGL_DMA_BUF_PLANE1_PITCH_EXT << EGLint(planes[1].stride); + + if (hasModifiers) { + attribs + << EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT << EGLint(planes[1].modifier & 0xffffffff) + << EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT << EGLint(planes[1].modifier >> 32); + } + } + + if (planes.count() > 2) { + attribs + << EGL_DMA_BUF_PLANE2_FD_EXT << planes[2].fd + << EGL_DMA_BUF_PLANE2_OFFSET_EXT << EGLint(planes[2].offset) + << EGL_DMA_BUF_PLANE2_PITCH_EXT << EGLint(planes[2].stride); + + if (hasModifiers) { + attribs + << EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT << EGLint(planes[2].modifier & 0xffffffff) + << EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT << EGLint(planes[2].modifier >> 32); + } + } + + if (eglQueryDmaBufModifiersEXT != nullptr && planes.count() > 3) { + attribs + << EGL_DMA_BUF_PLANE3_FD_EXT << planes[3].fd + << EGL_DMA_BUF_PLANE3_OFFSET_EXT << EGLint(planes[3].offset) + << EGL_DMA_BUF_PLANE3_PITCH_EXT << EGLint(planes[3].stride); + + if (hasModifiers) { + attribs + << EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT << EGLint(planes[3].modifier & 0xffffffff) + << EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT << EGLint(planes[3].modifier >> 32); + } + } + + attribs << EGL_NONE; + + EGLImage image = eglCreateImageKHR(m_backend->eglDisplay(), + EGL_NO_CONTEXT, + EGL_LINUX_DMA_BUF_EXT, + (EGLClientBuffer) nullptr, + attribs.data()); + if (image == EGL_NO_IMAGE_KHR) { + return nullptr; + } + + return image; +} + +KWaylandServer::LinuxDmabufUnstableV1Buffer* EglDmabuf::importBuffer(const QVector &planes, + uint32_t format, + const QSize &size, + Flags flags) +{ + Q_ASSERT(planes.count() > 0); + + // Try first to import as a single image + if (auto *img = createImage(planes, format, size)) { + return new EglDmabufBuffer(img, planes, format, size, flags, this); + } + + // TODO: to enable this we must be able to store multiple textures per window pixmap + // and when on window draw do yuv to rgb transformation per shader (see Weston) +// // not a single image, try yuv import +// return yuvImport(planes, format, size, flags); + + return nullptr; +} + +KWaylandServer::LinuxDmabufUnstableV1Buffer* EglDmabuf::yuvImport(const QVector &planes, + uint32_t format, + const QSize &size, + Flags flags) +{ + YuvFormat yuvFormat; + for (YuvFormat f : yuvFormats) { + if (f.format == format) { + yuvFormat = f; + break; + } + } + if (yuvFormat.format == 0) { + return nullptr; + } + if (planes.count() != yuvFormat.inputPlanes) { + return nullptr; + } + + auto *buf = new EglDmabufBuffer(planes, format, size, flags, this); + + for (int i = 0; i < yuvFormat.outputPlanes; i++) { + int planeIndex = yuvFormat.planes[i].planeIndex; + Plane plane = { + planes[planeIndex].fd, + planes[planeIndex].offset, + planes[planeIndex].stride, + planes[planeIndex].modifier + }; + const auto planeFormat = yuvFormat.planes[i].format; + const auto planeSize = QSize(size.width() / yuvFormat.planes[i].widthDivisor, + size.height() / yuvFormat.planes[i].heightDivisor); + auto *image = createImage(QVector(1, plane), + planeFormat, + planeSize); + if (!image) { + delete buf; + return nullptr; + } + buf->addImage(image); + } + // TODO: add buf import properties + return buf; +} + +EglDmabuf* EglDmabuf::factory(AbstractEglBackend *backend) +{ + if (!backend->hasExtension(QByteArrayLiteral("EGL_EXT_image_dma_buf_import"))) { + return nullptr; + } + + if (backend->hasExtension(QByteArrayLiteral("EGL_EXT_image_dma_buf_import_modifiers"))) { + eglQueryDmaBufFormatsEXT = (eglQueryDmaBufFormatsEXT_func) eglGetProcAddress("eglQueryDmaBufFormatsEXT"); + eglQueryDmaBufModifiersEXT = (eglQueryDmaBufModifiersEXT_func) eglGetProcAddress("eglQueryDmaBufModifiersEXT"); + } + + if (eglQueryDmaBufFormatsEXT == nullptr) { + return nullptr; + } + + return new EglDmabuf(backend); +} + +EglDmabuf::EglDmabuf(AbstractEglBackend *backend) + : LinuxDmabuf() + , m_backend(backend) +{ + auto prevBuffersSet = waylandServer()->linuxDmabufBuffers(); + for (auto *buffer : prevBuffersSet) { + auto *buf = static_cast(buffer); + buf->setInterfaceImplementation(this); + buf->addImage(createImage(buf->planes(), buf->format(), buf->size())); + } + setSupportedFormatsAndModifiers(); +} + +EglDmabuf::~EglDmabuf() +{ + auto curBuffers = waylandServer()->linuxDmabufBuffers(); + for (auto *buffer : curBuffers) { + auto *buf = static_cast(buffer); + buf->removeImages(); + } +} + +const uint32_t s_multiPlaneFormats[] = { + DRM_FORMAT_XRGB8888_A8, + DRM_FORMAT_XBGR8888_A8, + DRM_FORMAT_RGBX8888_A8, + DRM_FORMAT_BGRX8888_A8, + DRM_FORMAT_RGB888_A8, + DRM_FORMAT_BGR888_A8, + DRM_FORMAT_RGB565_A8, + DRM_FORMAT_BGR565_A8, + + DRM_FORMAT_NV12, + DRM_FORMAT_NV21, + DRM_FORMAT_NV16, + DRM_FORMAT_NV61, + DRM_FORMAT_NV24, + DRM_FORMAT_NV42, + + DRM_FORMAT_YUV410, + DRM_FORMAT_YVU410, + DRM_FORMAT_YUV411, + DRM_FORMAT_YVU411, + DRM_FORMAT_YUV420, + DRM_FORMAT_YVU420, + DRM_FORMAT_YUV422, + DRM_FORMAT_YVU422, + DRM_FORMAT_YUV444, + DRM_FORMAT_YVU444 +}; + +void filterFormatsWithMultiplePlanes(QVector &formats) +{ + QVector::iterator it = formats.begin(); + while (it != formats.end()) { + for (auto linuxFormat : s_multiPlaneFormats) { + if (*it == linuxFormat) { + qDebug() << "Filter multi-plane format" << *it; + it = formats.erase(it); + it--; + break; + } + } + it++; + } +} + +void EglDmabuf::setSupportedFormatsAndModifiers() +{ + const EGLDisplay eglDisplay = m_backend->eglDisplay(); + EGLint count = 0; + EGLBoolean success = eglQueryDmaBufFormatsEXT(eglDisplay, 0, nullptr, &count); + + if (!success || count == 0) { + return; + } + + QVector formats(count); + if (!eglQueryDmaBufFormatsEXT(eglDisplay, count, (EGLint *) formats.data(), &count)) { + return; + } + + filterFormatsWithMultiplePlanes(formats); + + QHash > set; + + for (auto format : qAsConst(formats)) { + if (eglQueryDmaBufModifiersEXT != nullptr) { + count = 0; + success = eglQueryDmaBufModifiersEXT(eglDisplay, format, 0, nullptr, nullptr, &count); + + if (success && count > 0) { + QVector modifiers(count); + if (eglQueryDmaBufModifiersEXT(eglDisplay, + format, count, modifiers.data(), + nullptr, &count)) { + QSet modifiersSet; + for (auto mod : qAsConst(modifiers)) { + modifiersSet.insert(mod); + } + set.insert(format, modifiersSet); + continue; + } + } + } + set.insert(format, QSet()); + } + + LinuxDmabuf::setSupportedFormatsAndModifiers(set); +} + +} diff --git a/platformsupport/scenes/opengl/egl_dmabuf.h b/platformsupport/scenes/opengl/egl_dmabuf.h new file mode 100644 index 0000000..e08febe --- /dev/null +++ b/platformsupport/scenes/opengl/egl_dmabuf.h @@ -0,0 +1,93 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Roman Gilg + SPDX-FileCopyrightText: 2018 Fredrik Höglund + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "../../../linux_dmabuf.h" + +#include "abstract_egl_backend.h" + +#include + +namespace KWin +{ +class EglDmabuf; + +class EglDmabufBuffer : public DmabufBuffer +{ +public: + using Plane = KWaylandServer::LinuxDmabufUnstableV1Interface::Plane; + using Flags = KWaylandServer::LinuxDmabufUnstableV1Interface::Flags; + + enum class ImportType { + Direct, + Conversion + }; + + EglDmabufBuffer(EGLImage image, + const QVector &planes, + uint32_t format, + const QSize &size, + Flags flags, + EglDmabuf *interfaceImpl); + + EglDmabufBuffer(const QVector &planes, + uint32_t format, + const QSize &size, + Flags flags, + EglDmabuf *interfaceImpl); + + ~EglDmabufBuffer() override; + + void setInterfaceImplementation(EglDmabuf *interfaceImpl); + void addImage(EGLImage image); + void removeImages(); + + QVector images() const { return m_images; } + +private: + QVector m_images; + EglDmabuf *m_interfaceImpl; + ImportType m_importType; +}; + +class EglDmabuf : public LinuxDmabuf +{ +public: + using Plane = KWaylandServer::LinuxDmabufUnstableV1Interface::Plane; + using Flags = KWaylandServer::LinuxDmabufUnstableV1Interface::Flags; + + static EglDmabuf* factory(AbstractEglBackend *backend); + + explicit EglDmabuf(AbstractEglBackend *backend); + ~EglDmabuf() override; + + KWaylandServer::LinuxDmabufUnstableV1Buffer *importBuffer(const QVector &planes, + uint32_t format, + const QSize &size, + Flags flags) override; + +private: + EGLImage createImage(const QVector &planes, + uint32_t format, + const QSize &size); + + KWaylandServer::LinuxDmabufUnstableV1Buffer *yuvImport(const QVector &planes, + uint32_t format, + const QSize &size, + Flags flags); + + void setSupportedFormatsAndModifiers(); + + AbstractEglBackend *m_backend; + + friend class EglDmabufBuffer; +}; + +} diff --git a/platformsupport/scenes/opengl/kwineglext.h b/platformsupport/scenes/opengl/kwineglext.h new file mode 100644 index 0000000..cae312a --- /dev/null +++ b/platformsupport/scenes/opengl/kwineglext.h @@ -0,0 +1,65 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018 Fredrik Höglund + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWINEGLEXT_H +#define KWINEGLEXT_H + +#include + +#ifndef EGL_WL_bind_wayland_display +#define EGL_WAYLAND_BUFFER_WL 0x31D5 +#define EGL_WAYLAND_PLANE_WL 0x31D6 +#define EGL_TEXTURE_Y_U_V_WL 0x31D7 +#define EGL_TEXTURE_Y_UV_WL 0x31D8 +#define EGL_TEXTURE_Y_XUXV_WL 0x31D9 +#define EGL_TEXTURE_EXTERNAL_WL 0x31DA +#define EGL_WAYLAND_Y_INVERTED_WL 0x31DB +#endif // EGL_WL_bind_wayland_display + +#ifndef EGL_EXT_image_dma_buf_import +#define EGL_LINUX_DMA_BUF_EXT 0x3270 +#define EGL_LINUX_DRM_FOURCC_EXT 0x3271 +#define EGL_DMA_BUF_PLANE0_FD_EXT 0x3272 +#define EGL_DMA_BUF_PLANE0_OFFSET_EXT 0x3273 +#define EGL_DMA_BUF_PLANE0_PITCH_EXT 0x3274 +#define EGL_DMA_BUF_PLANE1_FD_EXT 0x3275 +#define EGL_DMA_BUF_PLANE1_OFFSET_EXT 0x3276 +#define EGL_DMA_BUF_PLANE1_PITCH_EXT 0x3277 +#define EGL_DMA_BUF_PLANE2_FD_EXT 0x3278 +#define EGL_DMA_BUF_PLANE2_OFFSET_EXT 0x3279 +#define EGL_DMA_BUF_PLANE2_PITCH_EXT 0x327A +#define EGL_YUV_COLOR_SPACE_HINT_EXT 0x327B +#define EGL_SAMPLE_RANGE_HINT_EXT 0x327C +#define EGL_YUV_CHROMA_HORIZONTAL_SITING_HINT_EXT 0x327D +#define EGL_YUV_CHROMA_VERTICAL_SITING_HINT_EXT 0x327E +#define EGL_ITU_REC601_EXT 0x327F +#define EGL_ITU_REC709_EXT 0x3280 +#define EGL_ITU_REC2020_EXT 0x3281 +#define EGL_YUV_FULL_RANGE_EXT 0x3282 +#define EGL_YUV_NARROW_RANGE_EXT 0x3283 +#define EGL_YUV_CHROMA_SITING_0_EXT 0x3284 +#define EGL_YUV_CHROMA_SITING_0_5_EXT 0x3285 +#endif // EGL_EXT_image_dma_buf_import + +#ifndef EGL_EXT_image_dma_buf_import_modifiers +#define EGL_DMA_BUF_PLANE3_FD_EXT 0x3440 +#define EGL_DMA_BUF_PLANE3_OFFSET_EXT 0x3441 +#define EGL_DMA_BUF_PLANE3_PITCH_EXT 0x3442 +#define EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT 0x3443 +#define EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT 0x3444 +#define EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT 0x3445 +#define EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT 0x3446 +#define EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT 0x3447 +#define EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT 0x3448 +#define EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT 0x3449 +#define EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT 0x344A +#endif // EGL_EXT_image_dma_buf_import_modifiers + +#endif // KWINEGLEXT_H diff --git a/platformsupport/scenes/opengl/swap_profiler.cpp b/platformsupport/scenes/opengl/swap_profiler.cpp new file mode 100644 index 0000000..06dfb23 --- /dev/null +++ b/platformsupport/scenes/opengl/swap_profiler.cpp @@ -0,0 +1,46 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2009, 2010, 2011 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "swap_profiler.h" +#include + +namespace KWin +{ + +SwapProfiler::SwapProfiler() +{ + init(); +} + +void SwapProfiler::init() +{ + m_time = 2 * 1000*1000; // we start with a long time mean of 2ms ... + m_counter = 0; +} + +void SwapProfiler::begin() +{ + m_timer.start(); +} + +char SwapProfiler::end() +{ + // .. and blend in actual values. + // this way we prevent extremes from killing our long time mean + m_time = (10*m_time + m_timer.nsecsElapsed())/11; + if (++m_counter > 500) { + const bool blocks = m_time > 1000 * 1000; // 1ms, i get ~250µs and ~7ms w/o triple buffering... + qCDebug(KWIN_OPENGL) << "Triple buffering detection:" << QString(blocks ? QStringLiteral("NOT available") : QStringLiteral("Available")) << + " - Mean block time:" << m_time/(1000.0*1000.0) << "ms"; + return blocks ? 'd' : 't'; + } + return 0; +} + +} diff --git a/platformsupport/scenes/opengl/swap_profiler.h b/platformsupport/scenes/opengl/swap_profiler.h new file mode 100644 index 0000000..2942d03 --- /dev/null +++ b/platformsupport/scenes/opengl/swap_profiler.h @@ -0,0 +1,42 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2009, 2010, 2011 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_SCENE_OPENGL_SWAP_PROFILER_H +#define KWIN_SCENE_OPENGL_SWAP_PROFILER_H + +#include +#include + +namespace KWin +{ + +/** + * @short Profiler to detect whether we have triple buffering + * The strategy is to start setBlocksForRetrace(false) but assume blocking and have the system prove that assumption wrong + */ +class KWIN_EXPORT SwapProfiler +{ +public: + SwapProfiler(); + void init(); + void begin(); + /** + * @return char being 'd' for double, 't' for triple (or more - but non-blocking) buffering and + * 0 (NOT '0') otherwise, so you can act on "if (char result = SwapProfiler::end()) { fooBar(); } + */ + char end(); +private: + QElapsedTimer m_timer; + qint64 m_time; + int m_counter; +}; + +} + +#endif diff --git a/platformsupport/scenes/opengl/texture.cpp b/platformsupport/scenes/opengl/texture.cpp new file mode 100644 index 0000000..30d6c9a --- /dev/null +++ b/platformsupport/scenes/opengl/texture.cpp @@ -0,0 +1,69 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2009, 2010, 2011 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "texture.h" +#include "backend.h" +#include "scene.h" + +namespace KWin +{ + +SceneOpenGLTexture::SceneOpenGLTexture(OpenGLBackend *backend) + : GLTexture(*backend->createBackendTexture(this)) +{ +} + +SceneOpenGLTexture::~SceneOpenGLTexture() +{ +} + +SceneOpenGLTexture& SceneOpenGLTexture::operator = (const SceneOpenGLTexture& tex) +{ + d_ptr = tex.d_ptr; + return *this; +} + +void SceneOpenGLTexture::discard() +{ + d_ptr = d_func()->backend()->createBackendTexture(this); +} + +bool SceneOpenGLTexture::load(WindowPixmap *pixmap) +{ + if (!pixmap->isValid()) { + return false; + } + + // decrease the reference counter for the old texture + d_ptr = d_func()->backend()->createBackendTexture(this); //new TexturePrivate(); + + Q_D(SceneOpenGLTexture); + return d->loadTexture(pixmap); +} + +void SceneOpenGLTexture::updateFromPixmap(WindowPixmap *pixmap) +{ + Q_D(SceneOpenGLTexture); + d->updateTexture(pixmap); +} + +SceneOpenGLTexturePrivate::SceneOpenGLTexturePrivate() +{ +} + +SceneOpenGLTexturePrivate::~SceneOpenGLTexturePrivate() +{ +} + +void SceneOpenGLTexturePrivate::updateTexture(WindowPixmap *pixmap) +{ + Q_UNUSED(pixmap) +} + +} diff --git a/platformsupport/scenes/opengl/texture.h b/platformsupport/scenes/opengl/texture.h new file mode 100644 index 0000000..ae40cb0 --- /dev/null +++ b/platformsupport/scenes/opengl/texture.h @@ -0,0 +1,59 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2009, 2010, 2011 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include +#include + +namespace KWin +{ + +class OpenGLBackend; +class SceneOpenGLTexturePrivate; +class WindowPixmap; + +class SceneOpenGLTexture : public GLTexture +{ +public: + explicit SceneOpenGLTexture(OpenGLBackend *backend); + ~SceneOpenGLTexture() override; + + SceneOpenGLTexture & operator = (const SceneOpenGLTexture& tex); + + void discard() override final; + +private: + SceneOpenGLTexture(SceneOpenGLTexturePrivate& dd); + + bool load(WindowPixmap *pixmap); + void updateFromPixmap(WindowPixmap *pixmap); + + Q_DECLARE_PRIVATE(SceneOpenGLTexture) + + friend class OpenGLWindowPixmap; +}; + +class SceneOpenGLTexturePrivate : public GLTexturePrivate +{ +public: + ~SceneOpenGLTexturePrivate() override; + + virtual bool loadTexture(WindowPixmap *pixmap) = 0; + virtual void updateTexture(WindowPixmap *pixmap); + virtual OpenGLBackend *backend() = 0; + +protected: + SceneOpenGLTexturePrivate(); + +private: + Q_DISABLE_COPY(SceneOpenGLTexturePrivate) +}; + +} diff --git a/platformsupport/scenes/qpainter/CMakeLists.txt b/platformsupport/scenes/qpainter/CMakeLists.txt new file mode 100644 index 0000000..ca079b7 --- /dev/null +++ b/platformsupport/scenes/qpainter/CMakeLists.txt @@ -0,0 +1,16 @@ +set(SCENE_QPAINTER_BACKEND_SRCS backend.cpp) + +include(ECMQtDeclareLoggingCategory) +ecm_qt_declare_logging_category(SCENE_QPAINTER_BACKEND_SRCS + HEADER + logging.h + IDENTIFIER + KWIN_QPAINTER + CATEGORY_NAME + kwin_scene_qpainter + DEFAULT_SEVERITY + Critical +) + +add_library(SceneQPainterBackend STATIC ${SCENE_QPAINTER_BACKEND_SRCS}) +target_link_libraries(SceneQPainterBackend Qt5::Core) diff --git a/platformsupport/scenes/qpainter/backend.cpp b/platformsupport/scenes/qpainter/backend.cpp new file mode 100644 index 0000000..2dd518c --- /dev/null +++ b/platformsupport/scenes/qpainter/backend.cpp @@ -0,0 +1,57 @@ +/* + 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 "backend.h" +#include + +#include + +namespace KWin +{ + +QPainterBackend::QPainterBackend() + : m_failed(false) +{ +} + +QPainterBackend::~QPainterBackend() +{ +} + +OverlayWindow* QPainterBackend::overlayWindow() +{ + return nullptr; +} + +void QPainterBackend::showOverlay() +{ +} + +void QPainterBackend::screenGeometryChanged(const QSize &size) +{ + Q_UNUSED(size) +} + +void QPainterBackend::setFailed(const QString &reason) +{ + qCWarning(KWIN_QPAINTER) << "Creating the QPainter backend failed: " << reason; + m_failed = true; +} + +bool QPainterBackend::perScreenRendering() const +{ + return false; +} + +QImage *QPainterBackend::bufferForScreen(int screenId) +{ + Q_UNUSED(screenId) + return buffer(); +} + +} diff --git a/platformsupport/scenes/qpainter/backend.h b/platformsupport/scenes/qpainter/backend.h new file mode 100644 index 0000000..2fca898 --- /dev/null +++ b/platformsupport/scenes/qpainter/backend.h @@ -0,0 +1,97 @@ +/* + 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 KWIN_SCENE_QPAINTER_BACKEND_H +#define KWIN_SCENE_QPAINTER_BACKEND_H + +class QImage; +class QRegion; +class QSize; +class QString; + +namespace KWin { +class OverlayWindow; + +class QPainterBackend +{ +public: + virtual ~QPainterBackend(); + virtual void present(int mask, const QRegion &damage) = 0; + + /** + * @brief Returns the OverlayWindow used by the backend. + * + * A backend does not have to use an OverlayWindow, this is mostly for the X world. + * In case the backend does not use an OverlayWindow it is allowed to return @c null. + * It's the task of the caller to check whether it is @c null. + * + * @return :OverlayWindow* + */ + virtual OverlayWindow *overlayWindow(); + virtual bool usesOverlayWindow() const = 0; + virtual void prepareRenderingFrame() = 0; + /** + * @brief Shows the Overlay Window + * + * Default implementation does nothing. + */ + virtual void showOverlay(); + /** + * @brief React on screen geometry changes. + * + * Default implementation does nothing. Override if specific functionality is required. + * + * @param size The new screen size + */ + virtual void screenGeometryChanged(const QSize &size); + /** + * @brief Whether the creation of the Backend failed. + * + * The SceneQPainter should test whether the Backend got constructed correctly. If this method + * returns @c true, the SceneQPainter should not try to start the rendering. + * + * @return bool @c true if the creation of the Backend failed, @c false otherwise. + */ + bool isFailed() const { + return m_failed; + } + + virtual QImage *buffer() = 0; + /** + * Overload for the case that there is a different buffer per screen. + * Default implementation just calls buffer. + * @param screenId The id of the screen as used in Screens + * @todo Get a better identifier for screen then a counter variable + */ + virtual QImage *bufferForScreen(int screenId); + virtual bool needsFullRepaint() const = 0; + /** + * Whether the rendering needs to be split per screen. + * Default implementation returns @c false. + */ + virtual bool perScreenRendering() const; + +protected: + QPainterBackend(); + /** + * @brief Sets the backend initialization to failed. + * + * This method should be called by the concrete subclass in case the initialization failed. + * The given @p reason is logged as a warning. + * + * @param reason The reason why the initialization failed. + */ + void setFailed(const QString &reason); + +private: + bool m_failed; +}; + +} // KWin + +#endif diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt new file mode 100644 index 0000000..9f26622 --- /dev/null +++ b/plugins/CMakeLists.txt @@ -0,0 +1,11 @@ +add_subdirectory(kglobalaccel) +add_subdirectory(qpa) +add_subdirectory(idletime) +add_subdirectory(platforms) +add_subdirectory(scenes) +add_subdirectory(windowsystem) +add_subdirectory(kpackage) + +if (KWIN_BUILD_DECORATIONS) + add_subdirectory(kdecorations) +endif() diff --git a/plugins/idletime/CMakeLists.txt b/plugins/idletime/CMakeLists.txt new file mode 100644 index 0000000..1325755 --- /dev/null +++ b/plugins/idletime/CMakeLists.txt @@ -0,0 +1,18 @@ +set(idletime_plugin_SRCS + poller.cpp +) + +add_library(KF5IdleTimeKWinWaylandPrivatePlugin MODULE ${idletime_plugin_SRCS}) +set_target_properties(KF5IdleTimeKWinWaylandPrivatePlugin PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/kf5/org.kde.kidletime.platforms/") +target_link_libraries(KF5IdleTimeKWinWaylandPrivatePlugin + KF5::IdleTime + KF5::WaylandClient + kwin +) + +install( + TARGETS + KF5IdleTimeKWinWaylandPrivatePlugin + DESTINATION + ${PLUGIN_INSTALL_DIR}/kf5/org.kde.kidletime.platforms/ +) diff --git a/plugins/idletime/kwin.json b/plugins/idletime/kwin.json new file mode 100644 index 0000000..aaf6fd0 --- /dev/null +++ b/plugins/idletime/kwin.json @@ -0,0 +1,3 @@ +{ + "platforms": ["wayland-org.kde.kwin.qpa"] +} diff --git a/plugins/idletime/poller.cpp b/plugins/idletime/poller.cpp new file mode 100644 index 0000000..2b58305 --- /dev/null +++ b/plugins/idletime/poller.cpp @@ -0,0 +1,123 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "poller.h" +#include "../../wayland_server.h" + +#include +#include +#include + +Poller::Poller(QObject *parent) + : AbstractSystemPoller(parent) +{ + connect(KWin::waylandServer(), &KWin::WaylandServer::terminatingInternalClientConnection, this, + [this] { + qDeleteAll(m_timeouts); + m_timeouts.clear(); + delete m_seat; + m_seat = nullptr; + delete m_idle; + m_idle = nullptr; + } + ); +} + +Poller::~Poller() = default; + +bool Poller::isAvailable() +{ + return true; +} + +bool Poller::setUpPoller() +{ + auto registry = KWin::waylandServer()->internalClientRegistry(); + if (!m_seat) { + const auto iface = registry->interface(KWayland::Client::Registry::Interface::Seat); + m_seat = registry->createSeat(iface.name, iface.version, this); + } + if (!m_idle) { + const auto iface = registry->interface(KWayland::Client::Registry::Interface::Idle); + m_idle = registry->createIdle(iface.name, iface.version, this); + } + return m_seat->isValid() && m_idle->isValid(); +} + +void Poller::unloadPoller() +{ +} + +void Poller::addTimeout(int nextTimeout) +{ + if (m_timeouts.contains(nextTimeout)) { + return; + } + if (!m_idle) { + return; + } + auto timeout = m_idle->getTimeout(nextTimeout, m_seat, this); + m_timeouts.insert(nextTimeout, timeout); + connect(timeout, &KWayland::Client::IdleTimeout::idle, this, + [this, nextTimeout] { + emit timeoutReached(nextTimeout); + } + ); + connect(timeout, &KWayland::Client::IdleTimeout::resumeFromIdle, this, &Poller::resumingFromIdle); +} + +void Poller::removeTimeout(int nextTimeout) +{ + auto it = m_timeouts.find(nextTimeout); + if (it == m_timeouts.end()) { + return; + } + delete it.value(); + m_timeouts.erase(it); +} + +QList< int > Poller::timeouts() const +{ + return QList(); +} + +void Poller::catchIdleEvent() +{ + if (m_catchResumeTimeout) { + // already setup + return; + } + if (!m_idle) { + return; + } + m_catchResumeTimeout = m_idle->getTimeout(0, m_seat, this); + connect(m_catchResumeTimeout, &KWayland::Client::IdleTimeout::resumeFromIdle, this, + [this] { + stopCatchingIdleEvents(); + emit resumingFromIdle(); + } + ); +} + +void Poller::stopCatchingIdleEvents() +{ + delete m_catchResumeTimeout; + m_catchResumeTimeout = nullptr; +} + +int Poller::forcePollRequest() +{ + return 0; +} + +void Poller::simulateUserActivity() +{ + for (auto it = m_timeouts.constBegin(); it != m_timeouts.constEnd(); ++it) { + it.value()->simulateUserActivity(); + } +} diff --git a/plugins/idletime/poller.h b/plugins/idletime/poller.h new file mode 100644 index 0000000..0057b76 --- /dev/null +++ b/plugins/idletime/poller.h @@ -0,0 +1,56 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef POLLER_H +#define POLLER_H + +#include + +#include + +namespace KWayland +{ +namespace Client +{ +class Seat; +class Idle; +class IdleTimeout; +} +} + +class Poller : public AbstractSystemPoller +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.kde.kidletime.AbstractSystemPoller" FILE "kwin.json") + Q_INTERFACES(AbstractSystemPoller) + +public: + Poller(QObject *parent = nullptr); + ~Poller() override; + + bool isAvailable() override; + bool setUpPoller() override; + void unloadPoller() override; + +public Q_SLOTS: + void addTimeout(int nextTimeout) override; + void removeTimeout(int nextTimeout) override; + QList timeouts() const override; + int forcePollRequest() override; + void catchIdleEvent() override; + void stopCatchingIdleEvents() override; + void simulateUserActivity() override; + +private: + KWayland::Client::Seat *m_seat = nullptr; + KWayland::Client::Idle *m_idle = nullptr; + KWayland::Client::IdleTimeout *m_catchResumeTimeout = nullptr; + QHash m_timeouts; +}; + +#endif diff --git a/plugins/kdecorations/CMakeLists.txt b/plugins/kdecorations/CMakeLists.txt new file mode 100644 index 0000000..2bce237 --- /dev/null +++ b/plugins/kdecorations/CMakeLists.txt @@ -0,0 +1,2 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"kwin_clients\") +add_subdirectory(aurorae) diff --git a/plugins/kdecorations/Messages.sh b/plugins/kdecorations/Messages.sh new file mode 100644 index 0000000..15752f1 --- /dev/null +++ b/plugins/kdecorations/Messages.sh @@ -0,0 +1,4 @@ +#! /usr/bin/env bash +$EXTRACTRC `find . -name \*.ui` >> rc.cpp || exit 11 +$XGETTEXT `find . -name \*.cpp` -o $podir/kwin_clients.pot +rm -f rc.cpp diff --git a/plugins/kdecorations/aurorae/AUTHORS b/plugins/kdecorations/aurorae/AUTHORS new file mode 100644 index 0000000..4d11340 --- /dev/null +++ b/plugins/kdecorations/aurorae/AUTHORS @@ -0,0 +1 @@ +Martin Gräßlin kde [at] martin [minus] graesslin [dot] com Developer and Maintainer \ No newline at end of file diff --git a/plugins/kdecorations/aurorae/CMakeLists.txt b/plugins/kdecorations/aurorae/CMakeLists.txt new file mode 100644 index 0000000..d383cfb --- /dev/null +++ b/plugins/kdecorations/aurorae/CMakeLists.txt @@ -0,0 +1,4 @@ +add_subdirectory(src) +#add_subdirectory(themes/example-deco) + +add_subdirectory(themes) diff --git a/plugins/kdecorations/aurorae/README b/plugins/kdecorations/aurorae/README new file mode 100644 index 0000000..72e833a --- /dev/null +++ b/plugins/kdecorations/aurorae/README @@ -0,0 +1,6 @@ +Aurorae is a themeable window decoration for KWin. + +It supports theme files consisting of several SVG files for decoration and buttons. Themes can be +installed and selected directly in the configuration module of KWin decorations. + +Please have a look at theme-description on how to write a theme file. \ No newline at end of file diff --git a/plugins/kdecorations/aurorae/TODO b/plugins/kdecorations/aurorae/TODO new file mode 100644 index 0000000..4e30caa --- /dev/null +++ b/plugins/kdecorations/aurorae/TODO @@ -0,0 +1,3 @@ + * Button positions are not updated after theme change + * Delete themes from selection + * Get Hot New Stuff support \ No newline at end of file diff --git a/plugins/kdecorations/aurorae/src/CMakeLists.txt b/plugins/kdecorations/aurorae/src/CMakeLists.txt new file mode 100644 index 0000000..cd6c0ac --- /dev/null +++ b/plugins/kdecorations/aurorae/src/CMakeLists.txt @@ -0,0 +1,71 @@ +########### decoration ############### +include_directories( + ./lib + ${CMAKE_CURRENT_BINARY_DIR} + ${CMAKE_CURRENT_SOURCE_DIR} +) + +set(kwin5_aurorae_PART_SRCS + aurorae.cpp + decorationoptions.cpp + lib/auroraetheme.cpp + lib/themeconfig.cpp +) + +add_library(kwin5_aurorae MODULE ${kwin5_aurorae_PART_SRCS}) +set_target_properties(kwin5_aurorae PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/org.kde.kdecoration2/") + +target_link_libraries(kwin5_aurorae + KDecoration2::KDecoration + kwineffects + KF5::ConfigWidgets + KF5::I18n + KF5::Package + KF5::WindowSystem + Qt5::Quick + Qt5::UiTools +) + +install(TARGETS kwin5_aurorae DESTINATION ${PLUGIN_INSTALL_DIR}/org.kde.kdecoration2) + +set(decoration_plugin_SRCS + colorhelper.cpp + decorationoptions.cpp + decorationplugin.cpp +) + +add_library(decorationplugin SHARED ${decoration_plugin_SRCS}) +set_target_properties(decorationplugin PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/org/kde/kwin/decoration/") +target_link_libraries(decorationplugin + KDecoration2::KDecoration + KF5::ConfigWidgets + Qt5::Quick +) +install(TARGETS decorationplugin DESTINATION ${QML_INSTALL_DIR}/org/kde/kwin/decoration) + +########### install files ############### + +install(FILES aurorae.knsrc DESTINATION ${KDE_INSTALL_KNSRCDIR}) +install( + FILES + qml/AppMenuButton.qml + qml/AuroraeButton.qml + qml/AuroraeButtonGroup.qml + qml/AuroraeMaximizeButton.qml + qml/Decoration.qml + qml/DecorationButton.qml + qml/MenuButton.qml + qml/aurorae.qml + DESTINATION + ${DATA_INSTALL_DIR}/kwin/aurorae) +set(QMLFILES + qml/AppMenuButton.qml + qml/ButtonGroup.qml + qml/Decoration.qml + qml/DecorationButton.qml + qml/MenuButton.qml + qml/qmldir +) +install(FILES ${QMLFILES} DESTINATION ${QML_INSTALL_DIR}/org/kde/kwin/decoration) +file(COPY ${QMLFILES} DESTINATION ${CMAKE_BINARY_DIR}/bin/org/kde/kwin/decoration/) +install(FILES kwindecoration.desktop DESTINATION ${SERVICETYPES_INSTALL_DIR}) diff --git a/plugins/kdecorations/aurorae/src/aurorae.cpp b/plugins/kdecorations/aurorae/src/aurorae.cpp new file mode 100644 index 0000000..5242cb7 --- /dev/null +++ b/plugins/kdecorations/aurorae/src/aurorae.cpp @@ -0,0 +1,749 @@ +/* + SPDX-FileCopyrightText: 2009, 2010, 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "aurorae.h" +#include "auroraetheme.h" +#include "config-kwin.h" +#include "kwineffectquickview.h" +// qml imports +#include "decorationoptions.h" +// KDecoration2 +#include +#include +#include +// KDE +#include +#include +#include +#include +#include +#include +#include +#include +#include +// Qt +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +K_PLUGIN_FACTORY_WITH_JSON(AuroraeDecoFactory, + "aurorae.json", + registerPlugin(); + registerPlugin(QStringLiteral("themes")); + registerPlugin(QStringLiteral("kcmodule")); + ) + +namespace Aurorae +{ + +class Helper +{ +public: + void ref(); + void unref(); + QQmlComponent *component(const QString &theme); + QQmlContext *rootContext(); + QQmlComponent *svgComponent() { + return m_svgComponent.data(); + } + + static Helper &instance(); +private: + Helper() = default; + void init(); + QQmlComponent *loadComponent(const QString &themeName); + int m_refCount = 0; + QScopedPointer m_engine; + QHash m_components; + QScopedPointer m_svgComponent; +}; + +Helper &Helper::instance() +{ + static Helper s_helper; + return s_helper; +} + +void Helper::ref() +{ + m_refCount++; + if (m_refCount == 1) { + m_engine.reset(new QQmlEngine); + init(); + } +} + +void Helper::unref() +{ + m_refCount--; + if (m_refCount == 0) { + // cleanup + m_svgComponent.reset(); + m_engine.reset(); + m_components.clear(); + } +} + +static const QString s_defaultTheme = QStringLiteral("kwin4_decoration_qml_plastik"); +static const QString s_qmlPackageFolder = QStringLiteral(KWIN_NAME "/decorations/"); +/* + * KDecoration2::BorderSize doesn't map to the indices used for the Aurorae SVG Button Sizes. + * BorderSize defines None and NoSideBorder as index 0 and 1. These do not make sense for Button + * Size, thus we need to perform a mapping between the enum value and the config value. + */ +static const int s_indexMapper = 2; + +QQmlComponent *Helper::component(const QString &themeName) +{ + // maybe it's an SVG theme? + if (themeName.startsWith(QLatin1String("__aurorae__svg__"))) { + if (m_svgComponent.isNull()) { + /* use logic from KDeclarative::setupBindings(): + "addImportPath adds the path at the beginning, so to honour user's + paths we need to traverse the list in reverse order" */ + QStringListIterator paths(QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("module/imports"), QStandardPaths::LocateDirectory)); + paths.toBack(); + while (paths.hasPrevious()) { + m_engine->addImportPath(paths.previous()); + } + m_svgComponent.reset(new QQmlComponent(m_engine.data())); + m_svgComponent->loadUrl(QUrl(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kwin/aurorae/aurorae.qml")))); + } + // verify that the theme exists + if (!QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("aurorae/themes/%1/%1rc").arg(themeName.mid(16))).isEmpty()) { + return m_svgComponent.data(); + } + } + // try finding the QML package + auto it = m_components.constFind(themeName); + if (it != m_components.constEnd()) { + return it.value(); + } + auto component = loadComponent(themeName); + if (component) { + m_components.insert(themeName, component); + return component; + } + // try loading default component + if (themeName != s_defaultTheme) { + return loadComponent(s_defaultTheme); + } + return nullptr; +} + +QQmlComponent *Helper::loadComponent(const QString &themeName) +{ + qCDebug(AURORAE) << "Trying to load QML Decoration " << themeName; + const QString internalname = themeName.toLower(); + + const auto offers = KPackage::PackageLoader::self()->findPackages(QStringLiteral("KWin/Decoration"), s_qmlPackageFolder, + [internalname] (const KPluginMetaData &data) { + return data.pluginId().compare(internalname, Qt::CaseInsensitive) == 0; + } + ); + if (offers.isEmpty()) { + qCCritical(AURORAE) << "Couldn't find QML Decoration " << themeName; + // TODO: what to do in error case? + return nullptr; + } + const KPluginMetaData &service = offers.first(); + const QString pluginName = service.pluginId(); + const QString scriptName = service.value(QStringLiteral("X-Plasma-MainScript")); + const QString file = QStandardPaths::locate(QStandardPaths::GenericDataLocation, s_qmlPackageFolder + pluginName + QLatin1String("/contents/") + scriptName); + if (file.isNull()) { + qCDebug(AURORAE) << "Could not find script file for " << pluginName; + // TODO: what to do in error case? + return nullptr; + } + // setup the QML engine + /* use logic from KDeclarative::setupBindings(): + "addImportPath adds the path at the beginning, so to honour user's + paths we need to traverse the list in reverse order" */ + QStringListIterator paths(QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("module/imports"), QStandardPaths::LocateDirectory)); + paths.toBack(); + while (paths.hasPrevious()) { + m_engine->addImportPath(paths.previous()); + } + QQmlComponent *component = new QQmlComponent(m_engine.data(), m_engine.data()); + component->loadUrl(QUrl::fromLocalFile(file)); + return component; +} + +QQmlContext *Helper::rootContext() +{ + return m_engine->rootContext(); +} + +void Helper::init() +{ + // we need to first load our decoration plugin + // once it's loaded we can provide the Borders and access them from C++ side + // so let's try to locate our plugin: + QString pluginPath; + for (const QString &path : m_engine->importPathList()) { + QDirIterator it(path, QDirIterator::Subdirectories); + while (it.hasNext()) { + it.next(); + QFileInfo fileInfo = it.fileInfo(); + if (!fileInfo.isFile()) { + continue; + } + if (!fileInfo.path().endsWith(QLatin1String("/org/kde/kwin/decoration"))) { + continue; + } + if (fileInfo.fileName() == QLatin1String("libdecorationplugin.so")) { + pluginPath = fileInfo.absoluteFilePath(); + break; + } + } + if (!pluginPath.isEmpty()) { + break; + } + } + m_engine->importPlugin(pluginPath, "org.kde.kwin.decoration", nullptr); + qmlRegisterType("org.kde.kwin.decoration", 0, 1, "Borders"); + + qmlRegisterType(); + qmlRegisterType(); + qRegisterMetaType(); +} + +static QString findTheme(const QVariantList &args) +{ + if (args.isEmpty()) { + return QString(); + } + const auto map = args.first().toMap(); + auto it = map.constFind(QStringLiteral("theme")); + if (it == map.constEnd()) { + return QString(); + } + return it.value().toString(); +} + +Decoration::Decoration(QObject *parent, const QVariantList &args) + : KDecoration2::Decoration(parent, args) + , m_item(nullptr) + , m_borders(nullptr) + , m_maximizedBorders(nullptr) + , m_extendedBorders(nullptr) + , m_padding(nullptr) + , m_themeName(s_defaultTheme) + , m_view(nullptr) +{ + m_themeName = findTheme(args); + Helper::instance().ref(); + Helper::instance().rootContext()->setContextProperty(QStringLiteral("decorationSettings"), settings().data()); +} + +Decoration::~Decoration() +{ + delete m_qmlContext; + delete m_view; + Helper::instance().unref(); +} + +void Decoration::init() +{ + KDecoration2::Decoration::init(); + auto s = settings(); + connect(s.data(), &KDecoration2::DecorationSettings::reconfigured, this, &Decoration::configChanged); + + m_qmlContext = new QQmlContext(Helper::instance().rootContext(), this); + m_qmlContext->setContextProperty(QStringLiteral("decoration"), this); + auto component = Helper::instance().component(m_themeName); + if (!component) { + return; + } + if (component == Helper::instance().svgComponent()) { + // load SVG theme + const QString themeName = m_themeName.mid(16); + KConfig config(QLatin1String("aurorae/themes/") + themeName + QLatin1Char('/') + themeName + QLatin1String("rc"), + KConfig::FullConfig, QStandardPaths::GenericDataLocation); + AuroraeTheme *theme = new AuroraeTheme(this); + theme->loadTheme(themeName, config); + theme->setBorderSize(s->borderSize()); + connect(s.data(), &KDecoration2::DecorationSettings::borderSizeChanged, theme, &AuroraeTheme::setBorderSize); + auto readButtonSize = [this, theme] { + const KSharedConfigPtr conf = KSharedConfig::openConfig(QStringLiteral("auroraerc")); + const KConfigGroup themeGroup(conf, m_themeName.mid(16)); + theme->setButtonSize((KDecoration2::BorderSize)(themeGroup.readEntry("ButtonSize", + int(KDecoration2::BorderSize::Normal) - s_indexMapper) + s_indexMapper)); + }; + connect(this, &Decoration::configChanged, theme, readButtonSize); + readButtonSize(); +// m_theme->setTabDragMimeType(tabDragMimeType()); + m_qmlContext->setContextProperty(QStringLiteral("auroraeTheme"), theme); + } + m_item = qobject_cast< QQuickItem* >(component->create(m_qmlContext)); + if (!m_item) { + if (component->isError()) { + const auto errors = component->errors(); + for (const auto &error: errors) { + qCWarning(AURORAE) << error; + } + } + return; + } + + m_item->setParent(m_qmlContext); + + QVariant visualParent = property("visualParent"); + if (visualParent.isValid()) { + m_item->setParentItem(visualParent.value()); + visualParent.value()->setProperty("drawBackground", false); + } else { + m_view = new KWin::EffectQuickView(this, KWin::EffectQuickView::ExportMode::Image); + m_item->setParentItem(m_view->contentItem()); + auto updateSize = [this]() { m_item->setSize(m_view->contentItem()->size()); }; + updateSize(); + connect(m_view->contentItem(), &QQuickItem::widthChanged, m_item, updateSize); + connect(m_view->contentItem(), &QQuickItem::heightChanged, m_item, updateSize); + connect(m_view, &KWin::EffectQuickView::repaintNeeded, this, &Decoration::updateBuffer); + } + setupBorders(m_item); + + + // TODO: Is there a more efficient way to react to border changes? + auto trackBorders = [this](KWin::Borders *borders) { + if (!borders) { + return; + } + connect(borders, &KWin::Borders::leftChanged, this, &Decoration::updateBorders); + connect(borders, &KWin::Borders::rightChanged, this, &Decoration::updateBorders); + connect(borders, &KWin::Borders::topChanged, this, &Decoration::updateBorders); + connect(borders, &KWin::Borders::bottomChanged, this, &Decoration::updateBorders); + }; + trackBorders(m_borders); + trackBorders(m_maximizedBorders); + if (m_extendedBorders) { + updateExtendedBorders(); + connect(m_extendedBorders, &KWin::Borders::leftChanged, this, &Decoration::updateExtendedBorders); + connect(m_extendedBorders, &KWin::Borders::rightChanged, this, &Decoration::updateExtendedBorders); + connect(m_extendedBorders, &KWin::Borders::topChanged, this, &Decoration::updateExtendedBorders); + connect(m_extendedBorders, &KWin::Borders::bottomChanged, this, &Decoration::updateExtendedBorders); + } + + auto decorationClient = clientPointer(); + connect(decorationClient, &KDecoration2::DecoratedClient::maximizedChanged, this, &Decoration::updateBorders, Qt::QueuedConnection); + connect(decorationClient, &KDecoration2::DecoratedClient::shadedChanged, this, &Decoration::updateBorders); + updateBorders(); + if (m_view) { + auto resizeWindow = [this] { + QRect rect(QPoint(0, 0), size()); + if (m_padding && !clientPointer()->isMaximized()) { + rect = rect.adjusted(-m_padding->left(), -m_padding->top(), m_padding->right(), m_padding->bottom()); + } + m_view->setGeometry(rect); + }; + connect(this, &Decoration::bordersChanged, this, resizeWindow); + connect(decorationClient, &KDecoration2::DecoratedClient::widthChanged, this, resizeWindow); + connect(decorationClient, &KDecoration2::DecoratedClient::heightChanged, this, resizeWindow); + connect(decorationClient, &KDecoration2::DecoratedClient::maximizedChanged, this, resizeWindow); + connect(decorationClient, &KDecoration2::DecoratedClient::shadedChanged, this, resizeWindow); + resizeWindow(); + updateBuffer(); + } else { + // create a dummy shadow for the configuration interface + if (m_padding) { + auto s = QSharedPointer::create(); + s->setPadding(*m_padding); + s->setInnerShadowRect(QRect(m_padding->left(), m_padding->top(), 1, 1)); + setShadow(s); + } + } +} + +QVariant Decoration::readConfig(const QString &key, const QVariant &defaultValue) +{ + KSharedConfigPtr config = KSharedConfig::openConfig(QStringLiteral("auroraerc")); + return config->group(m_themeName).readEntry(key, defaultValue); +} + +void Decoration::setupBorders(QQuickItem *item) +{ + m_borders = item->findChild(QStringLiteral("borders")); + m_maximizedBorders = item->findChild(QStringLiteral("maximizedBorders")); + m_extendedBorders = item->findChild(QStringLiteral("extendedBorders")); + m_padding = item->findChild(QStringLiteral("padding")); +} + +void Decoration::updateBorders() +{ + KWin::Borders *b = m_borders; + if (clientPointer()->isMaximized() && m_maximizedBorders) { + b = m_maximizedBorders; + } + if (!b) { + return; + } + setBorders(*b); + + updateExtendedBorders(); +} + +void Decoration::paint(QPainter *painter, const QRect &repaintRegion) +{ + Q_UNUSED(repaintRegion) + if (!m_view) { + return; + } + painter->fillRect(rect(), Qt::transparent); + painter->drawImage(rect(), m_view->bufferAsImage(), m_contentRect); +} + +void Decoration::updateShadow() +{ + if (!m_view) { + return; + } + bool updateShadow = false; + const auto oldShadow = shadow(); + if (m_padding && + (m_padding->left() > 0 || m_padding->top() > 0 || m_padding->right() > 0 || m_padding->bottom() > 0) && + !clientPointer()->isMaximized()) { + if (oldShadow.isNull()) { + updateShadow = true; + } else { + // compare padding + if (oldShadow->padding() != *m_padding) { + updateShadow = true; + } + } + const QImage m_buffer = m_view->bufferAsImage(); + + QImage img(m_buffer.size(), QImage::Format_ARGB32_Premultiplied); + img.fill(Qt::transparent); + QPainter p(&img); + // top + p.drawImage(0, 0, m_buffer, 0, 0, img.width(), m_padding->top()); + // left + p.drawImage(0, m_padding->top(), m_buffer, 0, m_padding->top(), m_padding->left(), m_buffer.height() - m_padding->top()); + // bottom + p.drawImage(m_padding->left(), m_buffer.height() - m_padding->bottom(), m_buffer, + m_padding->left(), m_buffer.height() - m_padding->bottom(), + m_buffer.width() - m_padding->left(), m_padding->bottom()); + // right + p.drawImage(m_buffer.width() - m_padding->right(), m_padding->top(), m_buffer, + m_buffer.width() - m_padding->right(), m_padding->top(), + m_padding->right(), m_buffer.height() - m_padding->top() - m_padding->bottom()); + if (!updateShadow) { + updateShadow = (oldShadow->shadow() != img); + } + if (updateShadow) { + auto s = QSharedPointer::create(); + s->setShadow(img); + s->setPadding(*m_padding); + s->setInnerShadowRect(QRect(m_padding->left(), + m_padding->top(), + m_buffer.width() - m_padding->left() - m_padding->right(), + m_buffer.height() - m_padding->top() - m_padding->bottom())); + setShadow(s); + } + } else { + if (!oldShadow.isNull()) { + setShadow(QSharedPointer()); + } + } +} + +void Decoration::hoverEnterEvent(QHoverEvent *event) +{ + if (m_view) { + event->setAccepted(false); + m_view->forwardMouseEvent(event); + } + KDecoration2::Decoration::hoverEnterEvent(event); +} + +void Decoration::hoverLeaveEvent(QHoverEvent *event) +{ + if (m_view) { + m_view->forwardMouseEvent(event); + } + KDecoration2::Decoration::hoverLeaveEvent(event); +} + +void Decoration::hoverMoveEvent(QHoverEvent *event) +{ + if (m_view) { + // turn a hover event into a mouse because we don't follow hovers as we don't think we have focus + QMouseEvent cloneEvent(QEvent::MouseMove, event->posF(), Qt::NoButton, Qt::NoButton, Qt::NoModifier); + event->setAccepted(false); + m_view->forwardMouseEvent(&cloneEvent); + event->setAccepted(cloneEvent.isAccepted()); + } + KDecoration2::Decoration::hoverMoveEvent(event); +} + +void Decoration::mouseMoveEvent(QMouseEvent *event) +{ + if (m_view) { + m_view->forwardMouseEvent(event); + } + KDecoration2::Decoration::mouseMoveEvent(event); +} + +void Decoration::mousePressEvent(QMouseEvent *event) +{ + if (m_view) { + m_view->forwardMouseEvent(event); + if (event->button() == Qt::LeftButton) { + if (!m_doubleClickTimer.hasExpired(QGuiApplication::styleHints()->mouseDoubleClickInterval())) { + QMouseEvent dc(QEvent::MouseButtonDblClick, event->localPos(), event->windowPos(), event->screenPos(), event->button(), event->buttons(), event->modifiers()); + m_view->forwardMouseEvent(&dc); + } + } + m_doubleClickTimer.invalidate(); + } + KDecoration2::Decoration::mousePressEvent(event); +} + +void Decoration::mouseReleaseEvent(QMouseEvent *event) +{ + if (m_view) { + m_view->forwardMouseEvent(event); + if (event->isAccepted() && event->button() == Qt::LeftButton) { + m_doubleClickTimer.start(); + } + } + KDecoration2::Decoration::mouseReleaseEvent(event); +} + +void Decoration::installTitleItem(QQuickItem *item) +{ + auto update = [this, item] { + QRect rect = item->mapRectToScene(item->childrenRect()).toRect(); + if (rect.isNull()) { + rect = item->parentItem()->mapRectToScene(QRectF(item->x(), item->y(), item->width(), item->height())).toRect(); + } + setTitleBar(rect); + }; + update(); + connect(item, &QQuickItem::widthChanged, this, update); + connect(item, &QQuickItem::heightChanged, this, update); + connect(item, &QQuickItem::xChanged, this, update); + connect(item, &QQuickItem::yChanged, this, update); +} + +void Decoration::updateExtendedBorders() +{ + // extended sizes + const int extSize = settings()->largeSpacing(); + int extLeft = m_extendedBorders->left(); + int extRight = m_extendedBorders->right(); + int extBottom = m_extendedBorders->bottom(); + + if (settings()->borderSize() == KDecoration2::BorderSize::None) { + if (!clientPointer()->isMaximizedHorizontally()) { + extLeft = qMax(m_extendedBorders->left(), extSize); + extRight = qMax(m_extendedBorders->right(), extSize); + } + if (!clientPointer()->isMaximizedVertically()) { + extBottom = qMax(m_extendedBorders->bottom(), extSize); + } + + } else if (settings()->borderSize() == KDecoration2::BorderSize::NoSides && !clientPointer()->isMaximizedHorizontally() ) { + extLeft = qMax(m_extendedBorders->left(), extSize); + extRight = qMax(m_extendedBorders->right(), extSize); + } + + setResizeOnlyBorders(QMargins(extLeft, 0, extRight, extBottom)); +} + +void Decoration::updateBuffer() +{ + m_contentRect = QRect(QPoint(0, 0), m_view->bufferAsImage().size()); + if (m_padding && + (m_padding->left() > 0 || m_padding->top() > 0 || m_padding->right() > 0 || m_padding->bottom() > 0) && + !clientPointer()->isMaximized()) { + m_contentRect = m_contentRect.adjusted(m_padding->left(), m_padding->top(), -m_padding->right(), -m_padding->bottom()); + } + updateShadow(); + update(); +} + +KDecoration2::DecoratedClient *Decoration::clientPointer() const +{ + return client().toStrongRef().data(); +} + +ThemeFinder::ThemeFinder(QObject *parent, const QVariantList &args) + : QObject(parent) +{ + Q_UNUSED(args) + init(); +} + +void ThemeFinder::init() +{ + findAllQmlThemes(); + findAllSvgThemes(); +} + +void ThemeFinder::findAllQmlThemes() +{ + const auto offers = KPackage::PackageLoader::self()->findPackages(QStringLiteral("KWin/Decoration"), s_qmlPackageFolder); + for (const auto &offer : offers) { + m_themes.insert(offer.name(), offer.pluginId()); + } +} + +void ThemeFinder::findAllSvgThemes() +{ + QStringList themes; + const QStringList dirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("aurorae/themes/"), QStandardPaths::LocateDirectory); + QStringList themeDirectories; + for (const QString &dir : dirs) { + QDir directory = QDir(dir); + for (const QString &themeDir : directory.entryList(QDir::AllDirs | QDir::NoDotAndDotDot)) { + themeDirectories << dir + themeDir; + } + } + for (const QString &dir : themeDirectories) { + for (const QString & file : QDir(dir).entryList(QStringList() << QStringLiteral("metadata.desktop"))) { + themes.append(dir + '/' + file); + } + } + for (const QString & theme : themes) { + int themeSepIndex = theme.lastIndexOf('/', -1); + QString themeRoot = theme.left(themeSepIndex); + int themeNameSepIndex = themeRoot.lastIndexOf('/', -1); + QString packageName = themeRoot.right(themeRoot.length() - themeNameSepIndex - 1); + + KDesktopFile df(theme); + QString name = df.readName(); + if (name.isEmpty()) { + name = packageName; + } + + m_themes.insert(name, QString(QLatin1String("__aurorae__svg__") + packageName)); + } +} + +static const QString s_configUiPath = QStringLiteral("kwin/decorations/%1/contents/ui/config.ui"); +static const QString s_configXmlPath = QStringLiteral("kwin/decorations/%1/contents/config/main.xml"); + +bool ThemeFinder::hasConfiguration(const QString &theme) const +{ + if (theme.startsWith(QLatin1String("__aurorae__svg__"))) { + return true; + } + const QString ui = QStandardPaths::locate(QStandardPaths::GenericDataLocation, + s_configUiPath.arg(theme)); + const QString xml = QStandardPaths::locate(QStandardPaths::GenericDataLocation, + s_configXmlPath.arg(theme)); + return !(ui.isEmpty() || xml.isEmpty()); +} + +ConfigurationModule::ConfigurationModule(QWidget *parent, const QVariantList &args) + : KCModule(parent, args) + , m_theme(findTheme(args)) + , m_buttonSize(int(KDecoration2::BorderSize::Normal) - s_indexMapper) +{ + setLayout(new QVBoxLayout(this)); + init(); +} + +void ConfigurationModule::init() +{ + if (m_theme.startsWith(QLatin1String("__aurorae__svg__"))) { + // load the generic setting module + initSvg(); + } else { + initQml(); + } +} + +void ConfigurationModule::initSvg() +{ + QWidget *form = new QWidget(this); + form->setLayout(new QHBoxLayout(form)); + QComboBox *sizes = new QComboBox(form); + sizes->addItem(i18nc("@item:inlistbox Button size:", "Tiny")); + sizes->addItem(i18nc("@item:inlistbox Button size:", "Normal")); + sizes->addItem(i18nc("@item:inlistbox Button size:", "Large")); + sizes->addItem(i18nc("@item:inlistbox Button size:", "Very Large")); + sizes->addItem(i18nc("@item:inlistbox Button size:", "Huge")); + sizes->addItem(i18nc("@item:inlistbox Button size:", "Very Huge")); + sizes->addItem(i18nc("@item:inlistbox Button size:", "Oversized")); + sizes->setObjectName(QStringLiteral("kcfg_ButtonSize")); + + QLabel *label = new QLabel(i18n("Button size:"), form); + label->setBuddy(sizes); + form->layout()->addWidget(label); + form->layout()->addWidget(sizes); + + layout()->addWidget(form); + + KCoreConfigSkeleton *skel = new KCoreConfigSkeleton(KSharedConfig::openConfig(QStringLiteral("auroraerc")), this); + skel->setCurrentGroup(m_theme.mid(16)); + skel->addItemInt(QStringLiteral("ButtonSize"), + m_buttonSize, + int(KDecoration2::BorderSize::Normal) - s_indexMapper, + QStringLiteral("ButtonSize")); + addConfig(skel, form); +} + +void ConfigurationModule::initQml() +{ + const QString ui = QStandardPaths::locate(QStandardPaths::GenericDataLocation, + s_configUiPath.arg(m_theme)); + const QString xml = QStandardPaths::locate(QStandardPaths::GenericDataLocation, + s_configXmlPath.arg(m_theme)); + if (ui.isEmpty() || xml.isEmpty()) { + return; + } + KLocalizedTranslator *translator = new KLocalizedTranslator(this); + QCoreApplication::instance()->installTranslator(translator); + const KDesktopFile metaData(QStandardPaths::locate(QStandardPaths::GenericDataLocation, + QStringLiteral("kwin/decorations/%1/metadata.desktop").arg(m_theme))); + const QString translationDomain = metaData.desktopGroup().readEntry("X-KWin-Config-TranslationDomain", QString()); + if (!translationDomain.isEmpty()) { + translator->setTranslationDomain(translationDomain); + } + // load the KConfigSkeleton + QFile configFile(xml); + KSharedConfigPtr auroraeConfig = KSharedConfig::openConfig("auroraerc"); + KConfigGroup configGroup = auroraeConfig->group(m_theme); + m_skeleton = new KConfigLoader(configGroup, &configFile, this); + // load the ui file + QUiLoader *loader = new QUiLoader(this); + loader->setLanguageChangeEnabled(true); + QFile uiFile(ui); + uiFile.open(QFile::ReadOnly); + QWidget *customConfigForm = loader->load(&uiFile, this); + translator->addContextToMonitor(customConfigForm->objectName()); + uiFile.close(); + layout()->addWidget(customConfigForm); + // connect the ui file with the skeleton + addConfig(m_skeleton, customConfigForm); + + // send a custom event to the translator to retranslate using our translator + QEvent le(QEvent::LanguageChange); + QCoreApplication::sendEvent(customConfigForm, &le); +} + +} + +#include "aurorae.moc" diff --git a/plugins/kdecorations/aurorae/src/aurorae.h b/plugins/kdecorations/aurorae/src/aurorae.h new file mode 100644 index 0000000..035aaee --- /dev/null +++ b/plugins/kdecorations/aurorae/src/aurorae.h @@ -0,0 +1,119 @@ +/* + SPDX-FileCopyrightText: 2009, 2010, 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef AURORAE_H +#define AURORAE_H + +#include +#include +#include +#include + +class QQmlComponent; +class QQmlContext; +class QQmlEngine; +class QQuickItem; + +class KConfigLoader; + +namespace KWin +{ +class Borders; +class EffectQuickView; +} + +namespace Aurorae +{ + +class Decoration : public KDecoration2::Decoration +{ + Q_OBJECT + Q_PROPERTY(KDecoration2::DecoratedClient* client READ clientPointer CONSTANT) +public: + explicit Decoration(QObject *parent = nullptr, const QVariantList &args = QVariantList()); + ~Decoration() override; + + void paint(QPainter *painter, const QRect &repaintRegion) override; + + Q_INVOKABLE QVariant readConfig(const QString &key, const QVariant &defaultValue = QVariant()); + + KDecoration2::DecoratedClient *clientPointer() const; + +public Q_SLOTS: + void init() override; + void installTitleItem(QQuickItem *item); + + void updateShadow(); + +Q_SIGNALS: + void configChanged(); + +protected: + void hoverEnterEvent(QHoverEvent *event) override; + void hoverLeaveEvent(QHoverEvent *event) override; + void hoverMoveEvent(QHoverEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + +private: + void setupBorders(QQuickItem *item); + void updateBorders(); + void updateBuffer(); + void updateExtendedBorders(); + + QRect m_contentRect; //the geometry of the part of the buffer that is not a shadow when buffer was created. + QQuickItem *m_item = nullptr; + QQmlContext *m_qmlContext = nullptr; + KWin::Borders *m_borders; + KWin::Borders *m_maximizedBorders; + KWin::Borders *m_extendedBorders; + KWin::Borders *m_padding; + QString m_themeName; + + KWin::EffectQuickView *m_view; + QElapsedTimer m_doubleClickTimer; +}; + +class ThemeFinder : public QObject +{ + Q_OBJECT + Q_PROPERTY(QVariantMap themes READ themes) +public: + explicit ThemeFinder(QObject *parent = nullptr, const QVariantList &args = QVariantList()); + + QVariantMap themes() const { + return m_themes; + } + +public Q_SLOTS: + bool hasConfiguration(const QString &theme) const; + +private: + void init(); + void findAllQmlThemes(); + void findAllSvgThemes(); + QVariantMap m_themes; +}; + +class ConfigurationModule : public KCModule +{ + Q_OBJECT +public: + ConfigurationModule(QWidget *parent, const QVariantList &args); + +private: + void init(); + void initSvg(); + void initQml(); + QString m_theme; + KConfigLoader *m_skeleton = nullptr; + int m_buttonSize; +}; + +} + +#endif diff --git a/plugins/kdecorations/aurorae/src/aurorae.json b/plugins/kdecorations/aurorae/src/aurorae.json new file mode 100644 index 0000000..716e7e4 --- /dev/null +++ b/plugins/kdecorations/aurorae/src/aurorae.json @@ -0,0 +1,22 @@ +{ + "KPlugin": { + "Id": "org.kde.kwin.aurorae", + "ServiceTypes": [ + "org.kde.kdecoration2" + ] + }, + "Type": "Service", + "X-KDE-Library": "kwin5_aurorae", + "X-KDE-PluginInfo-EnabledByDefault": true, + "X-KDE-PluginInfo-Name": "org.kde.kwin.aurorae", + "X-KDE-ServiceTypes": [ + "org.kde.kdecoration2" + ], + "org.kde.kdecoration2": { + "KNewStuff": "aurorae.knsrc", + "blur": true, + "defaultTheme": "kwin4_decoration_qml_plastik", + "themeListKeyword": "themes", + "themes": true + } +} diff --git a/plugins/kdecorations/aurorae/src/aurorae.knsrc b/plugins/kdecorations/aurorae/src/aurorae.knsrc new file mode 100644 index 0000000..009988c --- /dev/null +++ b/plugins/kdecorations/aurorae/src/aurorae.knsrc @@ -0,0 +1,46 @@ +[KNewStuff3] +Name=Aurorae Window Decorations +Name[az]=Aurorae Pəncərə Dekorasiyası +Name[ca]=Decoracions de finestra Aurorae +Name[ca@valencia]=Decoracions de finestra Aurorae +Name[cs]=Dekorace oken Aurorae +Name[da]=Aurorae vinduesdekorationer +Name[de]=Aurorae-Fensterdekoration +Name[el]=Διακοσμήσεις παραθύρου Aurorae +Name[en_GB]=Aurorae Window Decorations +Name[es]=Decoraciones de las ventanas Aurorae +Name[et]=Aurorae aknadekoratsioonid +Name[eu]=Aurorae leihoentzako apaingarriak +Name[fi]=Aurorae-ikkunakehykset +Name[fr]=Décorations de fenêtres Aurorae +Name[gl]=Decoracións de xanela de Aurorae +Name[hu]=Aurorae ablakdekorációk +Name[ia]=Decorationes de fenestra Aurorae +Name[id]=Dekorasi Window Aurorae +Name[it]=Decorazioni delle finestre Aurorae +Name[ko]=Aurorae ì°½ 장식 +Name[lt]=Aurorae langų dekoracijos +Name[nl]=Aurorea-vensterdecoraties +Name[nn]=Aurorae-vindaugsdekorasjonar +Name[pl]=Ozdoby okienne Aurorae +Name[pt]=Decorações das Janelas do Aurorae +Name[pt_BR]=Decorações da janela Aurorae +Name[ro]=Decorații de fereastră Aurorae +Name[ru]=Оформления окон Aurorae для KWin +Name[sk]=Dekorácie okien Aurorae +Name[sl]=Okraski oken Aurorae +Name[sr]=Декорације прозора за Ауроре +Name[sr@ijekavian]=Декорације прозора за Ауроре +Name[sr@ijekavianlatin]=Dekoracije prozora za Aurore +Name[sr@latin]=Dekoracije prozora za Aurore +Name[sv]=Aurora-fönsterdekorationer +Name[tr]=Aurorae Pencere Dekorasyonları +Name[uk]=Обрамлення вікон Aurorae +Name[x-test]=xxAurorae Window Decorationsxx +Name[zh_CN]=Aurorae 窗口装饰 +Name[zh_TW]=Aurorae 視窗裝飾 + +ProvidersUrl=https://download.kde.org/ocs/providers.xml +Categories=Window Decoration Aurorae +Uncompress=archive +TargetDir=aurorae/themes diff --git a/plugins/kdecorations/aurorae/src/colorhelper.cpp b/plugins/kdecorations/aurorae/src/colorhelper.cpp new file mode 100644 index 0000000..88e6d33 --- /dev/null +++ b/plugins/kdecorations/aurorae/src/colorhelper.cpp @@ -0,0 +1,52 @@ +/* + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "colorhelper.h" + +#include + +ColorHelper::ColorHelper(QObject *parent) + : QObject(parent) +{ +} + +ColorHelper::~ColorHelper() +{ +} + +QColor ColorHelper::shade(const QColor &color, ColorHelper::ShadeRole role) +{ + return KColorScheme::shade(color, static_cast(role)); +} + +QColor ColorHelper::shade(const QColor &color, ColorHelper::ShadeRole role, qreal contrast) +{ + return KColorScheme::shade(color, static_cast(role), contrast); +} + +qreal ColorHelper::contrast() const +{ + return KColorScheme::contrastF(); +} + +QColor ColorHelper::multiplyAlpha(const QColor &color, qreal alpha) +{ + QColor retCol(color); + retCol.setAlphaF(color.alphaF() * alpha); + return retCol; +} + +QColor ColorHelper::background(bool active, ColorHelper::BackgroundRole role) const +{ + KColorScheme kcs(active ? QPalette::Active : QPalette::Inactive, KColorScheme::Button); + return kcs.background(static_cast(role)).color(); +} + +QColor ColorHelper::foreground(bool active, ColorHelper::ForegroundRole role) const +{ + KColorScheme kcs(active ? QPalette::Active : QPalette::Inactive, KColorScheme::Button); + return kcs.foreground(static_cast(role)).color(); +} + diff --git a/plugins/kdecorations/aurorae/src/colorhelper.h b/plugins/kdecorations/aurorae/src/colorhelper.h new file mode 100644 index 0000000..f439485 --- /dev/null +++ b/plugins/kdecorations/aurorae/src/colorhelper.h @@ -0,0 +1,230 @@ +/* + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef COLOR_HELPER_H +#define COLOR_HELPER_H + +#include +#include + +/** + * @short Helper to manipulate colors. + * + * Exports a few functions from KColorScheme. + */ +class ColorHelper : public QObject +{ + Q_OBJECT + Q_ENUMS(ShadeRole) + Q_ENUMS(ForegroundRole) + Q_ENUMS(BackgroundRole) + /** + * Same as KGlobalSettings::contrastF. + */ + Q_PROPERTY(qreal contrast READ contrast CONSTANT) +public: + explicit ColorHelper(QObject *parent = nullptr); + ~ColorHelper() override; + /** + * This enumeration describes the color shade being selected from the given + * set. + * + * Color shades are used to draw "3d" elements, such as frames and bevels. + * They are neither foreground nor background colors. Text should not be + * painted over a shade, and shades should not be used to draw text. + */ + enum ShadeRole { + /** + * The light color is lighter than dark() or shadow() and contrasts + * with the base color. + */ + LightShade, + /** + * The midlight color is in between base() and light(). + */ + MidlightShade, + /** + * The mid color is in between base() and dark(). + */ + MidShade, + /** + * The dark color is in between mid() and shadow(). + */ + DarkShade, + /** + * The shadow color is darker than light() or midlight() and contrasts + * the base color. + */ + ShadowShade + }; + /** + * This enumeration describes the background color being selected from the + * given set. + * + * Background colors are suitable for drawing under text, and should never + * be used to draw text. In combination with one of the overloads of + * KColorScheme::shade, they may be used to generate colors for drawing + * frames, bevels, and similar decorations. + */ + enum BackgroundRole { + /** + * Normal background. + */ + NormalBackground = 0, + /** + * Alternate background; for example, for use in lists. + * + * This color may be the same as BackgroundNormal, especially in sets + * other than View and Window. + */ + AlternateBackground = 1, + /** + * Third color; for example, items which are new, active, requesting + * attention, etc. + * + * Alerting the user that a certain field must be filled out would be a + * good usage (although NegativeBackground could be used to the same + * effect, depending on what you are trying to achieve). Unlike + * ActiveText, this should not be used for mouseover effects. + */ + ActiveBackground = 2, + /** + * Fourth color; corresponds to (unvisited) links. + * + * Exactly what this might be used for is somewhat harder to qualify; + * it might be used for bookmarks, as a 'you can click here' indicator, + * or to highlight recent content (i.e. in a most-recently-accessed + * list). + */ + LinkBackground = 3, + /** + * Fifth color; corresponds to visited links. + * + * This can also be used to indicate "not recent" content, especially + * when a color is needed to denote content which is "old" or + * "archival". + */ + VisitedBackground = 4, + /** + * Sixth color; for example, errors, untrusted content, etc. + */ + NegativeBackground = 5, + /** + * Seventh color; for example, warnings, secure/encrypted content. + */ + NeutralBackground = 6, + /** + * Eigth color; for example, success messages, trusted content. + */ + PositiveBackground = 7 + }; + + /** + * This enumeration describes the foreground color being selected from the + * given set. + * + * Foreground colors are suitable for drawing text or glyphs (such as the + * symbols on window decoration buttons, assuming a suitable background + * brush is used), and should never be used to draw backgrounds. + * + * For window decorations, the following is suggested, but not set in + * stone: + * @li Maximize - PositiveText + * @li Minimize - NeutralText + * @li Close - NegativeText + * @li WhatsThis - LinkText + * @li Sticky - ActiveText + */ + enum ForegroundRole { + /** + * Normal foreground. + */ + NormalText = 0, + /** + * Second color; for example, comments, items which are old, inactive + * or disabled. Generally used for things that are meant to be "less + * important". InactiveText is not the same role as NormalText in the + * inactive state. + */ + InactiveText = 1, + /** + * Third color; for example items which are new, active, requesting + * attention, etc. May be used as a hover color for clickable items. + */ + ActiveText = 2, + /** + * Fourth color; use for (unvisited) links. May also be used for other + * clickable items or content that indicates relationships, items that + * indicate somewhere the user can visit, etc. + */ + LinkText = 3, + /** + * Fifth color; used for (visited) links. As with LinkText, may be used + * for items that have already been "visited" or accessed. May also be + * used to indicate "historical" (i.e. "old") items or information, + * especially if InactiveText is being used in the same context to + * express something different. + */ + VisitedText = 4, + /** + * Sixth color; for example, errors, untrusted content, deletions, + * etc. + */ + NegativeText = 5, + /** + * Seventh color; for example, warnings, secure/encrypted content. + */ + NeutralText = 6, + /** + * Eigth color; for example, additions, success messages, trusted + * content. + */ + PositiveText = 7 + }; + /** + * Retrieve the requested shade color, using the specified color as the + * base color and the system contrast setting. + * + * @note Shades are chosen such that all shades would contrast with the + * base color. This means that if base is very dark, the 'dark' shades will + * be lighter than the base color, with midlight() == shadow(). + * Conversely, if the base color is very light, the 'light' shades will be + * darker than the base color, with light() == mid(). + */ + Q_INVOKABLE QColor shade(const QColor& color, ShadeRole role); + Q_INVOKABLE QColor shade(const QColor& color, ShadeRole role, qreal contrast); + /** + * Retrieve the requested shade color, using the specified color as the + * base color and the specified contrast. + * + * @param contrast Amount roughly specifying the contrast by which to + * adjust the base color, between -1.0 and 1.0 (values between 0.0 and 1.0 + * correspond to the value from KGlobalSettings::contrastF) + * + * @note Shades are chosen such that all shades would contrast with the + * base color. This means that if base is very dark, the 'dark' shades will + * be lighter than the base color, with midlight() == shadow(). + * Conversely, if the base color is very light, the 'light' shades will be + * darker than the base color, with light() == mid(). + * + * @see KColorUtils::shade + */ + Q_INVOKABLE QColor multiplyAlpha(const QColor& color, qreal alpha); + /** + * Retrieve the requested background brush's color for the @p active button. + * @param active Whether the active or inactive palette should be used. + */ + Q_INVOKABLE QColor background(bool active, BackgroundRole role = NormalBackground) const; + + /** + * Retrieve the requested foreground brush's color for the @p active button. + * @param active Whether the active or inactive palette should be used. + */ + Q_INVOKABLE QColor foreground(bool active, ForegroundRole role = NormalText) const; + + qreal contrast() const; +}; + +#endif diff --git a/plugins/kdecorations/aurorae/src/decorationoptions.cpp b/plugins/kdecorations/aurorae/src/decorationoptions.cpp new file mode 100644 index 0000000..1018c68 --- /dev/null +++ b/plugins/kdecorations/aurorae/src/decorationoptions.cpp @@ -0,0 +1,258 @@ +/* + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "decorationoptions.h" +#include +#include +#include +#include +#include +#include + +namespace KWin +{ + +ColorSettings::ColorSettings(const QPalette &pal) +{ + init(pal); +} + +void ColorSettings::update(const QPalette &pal) +{ + init(pal); +} + +void ColorSettings::init(const QPalette &pal) +{ + m_palette = pal; + KConfigGroup wmConfig(KSharedConfig::openConfig(QStringLiteral("kdeglobals")), QStringLiteral("WM")); + m_activeFrameColor = wmConfig.readEntry("frame", pal.color(QPalette::Active, QPalette::Window)); + m_inactiveFrameColor = wmConfig.readEntry("inactiveFrame", m_activeFrameColor); + m_activeTitleBarColor = wmConfig.readEntry("activeBackground", pal.color(QPalette::Active, QPalette::Highlight)); + m_inactiveTitleBarColor = wmConfig.readEntry("inactiveBackground", m_inactiveFrameColor); + m_activeTitleBarBlendColor = wmConfig.readEntry("activeBlend", m_activeTitleBarColor.darker(110)); + m_inactiveTitleBarBlendColor = wmConfig.readEntry("inactiveBlend", m_inactiveTitleBarColor.darker(110)); + m_activeFontColor = wmConfig.readEntry("activeForeground", pal.color(QPalette::Active, QPalette::HighlightedText)); + m_inactiveFontColor = wmConfig.readEntry("inactiveForeground", m_activeFontColor.darker()); + m_activeButtonColor = wmConfig.readEntry("activeTitleBtnBg", m_activeFrameColor.lighter(130)); + m_inactiveButtonColor = wmConfig.readEntry("inactiveTitleBtnBg", m_inactiveFrameColor.lighter(130)); + m_activeHandle = wmConfig.readEntry("handle", m_activeFrameColor); + m_inactiveHandle = wmConfig.readEntry("inactiveHandle", m_activeHandle); +} + + +DecorationOptions::DecorationOptions(QObject *parent) + : QObject(parent) + , m_active(true) + , m_decoration(nullptr) + , m_colors(ColorSettings(QPalette())) +{ + connect(this, &DecorationOptions::decorationChanged, this, &DecorationOptions::slotActiveChanged); + connect(this, &DecorationOptions::decorationChanged, this, &DecorationOptions::colorsChanged); + connect(this, &DecorationOptions::decorationChanged, this, &DecorationOptions::fontChanged); + connect(this, &DecorationOptions::decorationChanged, this, &DecorationOptions::titleButtonsChanged); +} + +DecorationOptions::~DecorationOptions() +{ +} + +QColor DecorationOptions::borderColor() const +{ + return m_active ? m_colors.activeFrame() : m_colors.inactiveFrame(); +} + +QColor DecorationOptions::buttonColor() const +{ + return m_active ? m_colors.activeButtonColor() : m_colors.inactiveButtonColor(); +} + +QColor DecorationOptions::fontColor() const +{ + return m_active ? m_colors.activeFont() : m_colors.inactiveFont(); +} + +QColor DecorationOptions::resizeHandleColor() const +{ + return m_active ? m_colors.activeHandle() : m_colors.inactiveHandle(); +} + +QColor DecorationOptions::titleBarBlendColor() const +{ + return m_active ? m_colors.activeTitleBarBlendColor() : m_colors.inactiveTitleBarBlendColor(); +} + +QColor DecorationOptions::titleBarColor() const +{ + return m_active ? m_colors.activeTitleBarColor() : m_colors.inactiveTitleBarColor(); +} + +QFont DecorationOptions::titleFont() const +{ + return m_decoration ? m_decoration->settings()->font() : QFont(); +} + +static int decorationButton(KDecoration2::DecorationButtonType type) +{ + switch (type) { + case KDecoration2::DecorationButtonType::Menu: + return DecorationOptions::DecorationButtonMenu; + case KDecoration2::DecorationButtonType::ApplicationMenu: + return DecorationOptions::DecorationButtonApplicationMenu; + case KDecoration2::DecorationButtonType::OnAllDesktops: + return DecorationOptions::DecorationButtonOnAllDesktops; + case KDecoration2::DecorationButtonType::Minimize: + return DecorationOptions::DecorationButtonMinimize; + case KDecoration2::DecorationButtonType::Maximize: + return DecorationOptions::DecorationButtonMaximizeRestore; + case KDecoration2::DecorationButtonType::Close: + return DecorationOptions::DecorationButtonClose; + case KDecoration2::DecorationButtonType::ContextHelp: + return DecorationOptions::DecorationButtonQuickHelp; + case KDecoration2::DecorationButtonType::Shade: + return DecorationOptions::DecorationButtonShade; + case KDecoration2::DecorationButtonType::KeepBelow: + return DecorationOptions::DecorationButtonKeepBelow; + case KDecoration2::DecorationButtonType::KeepAbove: + return DecorationOptions::DecorationButtonKeepAbove; + default: + return DecorationOptions::DecorationButtonNone; + } +} + +QList DecorationOptions::titleButtonsLeft() const +{ + QList ret; + if (m_decoration) { + for (auto it : m_decoration->settings()->decorationButtonsLeft()) { + ret << decorationButton(it); + } + } + return ret; +} + +QList DecorationOptions::titleButtonsRight() const +{ + QList ret; + if (m_decoration) { + for (auto it : m_decoration->settings()->decorationButtonsRight()) { + ret << decorationButton(it); + } + } + return ret; +} + +KDecoration2::Decoration *DecorationOptions::decoration() const +{ + return m_decoration; +} + +void DecorationOptions::setDecoration(KDecoration2::Decoration *decoration) +{ + if (m_decoration == decoration) { + return; + } + if (m_decoration) { + // disconnect from existing decoration + disconnect(m_decoration->client().toStrongRef().data(), &KDecoration2::DecoratedClient::activeChanged, this, &DecorationOptions::slotActiveChanged); + auto s = m_decoration->settings(); + disconnect(s.data(), &KDecoration2::DecorationSettings::fontChanged, this, &DecorationOptions::fontChanged); + disconnect(s.data(), &KDecoration2::DecorationSettings::decorationButtonsLeftChanged, this, &DecorationOptions::titleButtonsChanged); + disconnect(s.data(), &KDecoration2::DecorationSettings::decorationButtonsRightChanged, this, &DecorationOptions::titleButtonsChanged); + disconnect(m_paletteConnection); + } + m_decoration = decoration; + connect(m_decoration->client().toStrongRef().data(), &KDecoration2::DecoratedClient::activeChanged, this, &DecorationOptions::slotActiveChanged); + m_paletteConnection = connect(m_decoration->client().toStrongRef().data(), &KDecoration2::DecoratedClient::paletteChanged, this, + [this] (const QPalette &pal) { + m_colors.update(pal); + emit colorsChanged(); + } + ); + auto s = m_decoration->settings(); + connect(s.data(), &KDecoration2::DecorationSettings::fontChanged, this, &DecorationOptions::fontChanged); + connect(s.data(), &KDecoration2::DecorationSettings::decorationButtonsLeftChanged, this, &DecorationOptions::titleButtonsChanged); + connect(s.data(), &KDecoration2::DecorationSettings::decorationButtonsRightChanged, this, &DecorationOptions::titleButtonsChanged); + emit decorationChanged(); +} + +void DecorationOptions::slotActiveChanged() +{ + if (!m_decoration) { + return; + } + if (m_active == m_decoration->client().toStrongRef().data()->isActive()) { + return; + } + m_active = m_decoration->client().toStrongRef().data()->isActive(); + emit colorsChanged(); + emit fontChanged(); +} + +int DecorationOptions::mousePressAndHoldInterval() const +{ + return QGuiApplication::styleHints()->mousePressAndHoldInterval(); +} + +Borders::Borders(QObject *parent) + : QObject(parent) + , m_left(0) + , m_right(0) + , m_top(0) + , m_bottom(0) +{ +} + +Borders::~Borders() +{ +} + +#define SETTER( methodName, name ) \ +void Borders::methodName(int name) \ +{ \ + if (m_##name == name) { \ + return; \ + } \ + m_##name = name; \ + emit name##Changed(); \ +} + +SETTER(setLeft, left) +SETTER(setRight, right) +SETTER(setTop, top) +SETTER(setBottom, bottom) + +#undef SETTER + +void Borders::setAllBorders(int border) +{ + setBorders(border); + setTitle(border); +} + +void Borders::setBorders(int border) +{ + setSideBorders(border); + setBottom(border); +} + +void Borders::setSideBorders(int border) +{ + setLeft(border); + setRight(border); +} + +void Borders::setTitle(int value) +{ + setTop(value); +} + +Borders::operator QMargins() const +{ + return QMargins(m_left, m_top, m_right, m_bottom); +} + +} // namespace + diff --git a/plugins/kdecorations/aurorae/src/decorationoptions.h b/plugins/kdecorations/aurorae/src/decorationoptions.h new file mode 100644 index 0000000..4f0a393 --- /dev/null +++ b/plugins/kdecorations/aurorae/src/decorationoptions.h @@ -0,0 +1,307 @@ +/* + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_DECORATION_OPTIONS_H +#define KWIN_DECORATION_OPTIONS_H + +#include + +#include +#include +#include +#include + +namespace KWin +{ + +// TODO: move to deco API +class ColorSettings +{ +public: + ColorSettings(const QPalette &pal); + + void update(const QPalette &pal); + + const QColor &titleBarColor(bool active) const { + return active ? m_activeTitleBarColor : m_inactiveTitleBarColor; + } + const QColor &activeTitleBarColor() const { + return m_activeTitleBarColor; + } + const QColor &inactiveTitleBarColor() const { + return m_inactiveTitleBarColor; + } + const QColor &activeTitleBarBlendColor() const { + return m_activeTitleBarBlendColor; + } + const QColor &inactiveTitleBarBlendColor() const { + return m_inactiveTitleBarBlendColor; + } + const QColor &frame(bool active) const { + return active ? m_activeFrameColor : m_inactiveFrameColor; + } + const QColor &activeFrame() const { + return m_activeFrameColor; + } + const QColor &inactiveFrame() const { + return m_inactiveFrameColor; + } + const QColor &font(bool active) const { + return active ? m_activeFontColor : m_inactiveFontColor; + } + const QColor &activeFont() const { + return m_activeFontColor; + } + const QColor &inactiveFont() const { + return m_inactiveFontColor; + } + const QColor &activeButtonColor() const { + return m_activeButtonColor; + } + const QColor &inactiveButtonColor() const { + return m_inactiveButtonColor; + } + const QColor &activeHandle() const { + return m_activeHandle; + } + const QColor &inactiveHandle() const { + return m_inactiveHandle; + } + const QPalette &palette() const { + return m_palette; + } +private: + void init(const QPalette &pal); + QColor m_activeTitleBarColor; + QColor m_inactiveTitleBarColor; + QColor m_activeTitleBarBlendColor; + QColor m_inactiveTitleBarBlendColor; + QColor m_activeFrameColor; + QColor m_inactiveFrameColor; + QColor m_activeFontColor; + QColor m_inactiveFontColor; + QColor m_activeButtonColor; + QColor m_inactiveButtonColor; + QColor m_activeHandle; + QColor m_inactiveHandle; + QPalette m_palette; +}; + +/** + * @short Common Window Decoration Options. + * + * This Class provides common window decoration options which can be used, but do not have to + * be used by a window decoration. The class provides properties for global settings such as + * color, font and decoration button position. + * + * If a window decoration wants to follow the global color scheme it should honor the values + * provided by the properties. + * + * In any case it makes sense to respect the font settings for the decoration as this is also + * an accessibility feature. + * + * In order to use the options in a QML based window decoration an instance of this object needs + * to be created and the as a context property available "decoration" needs to be passed to the + * DecorationOptions instance: + * + * @code + * DecorationOptions { + * id: options + * deco: decoration + * } + * @endcode + */ +class DecorationOptions : public QObject +{ + Q_OBJECT + Q_ENUMS(BorderSize) + Q_ENUMS(DecorationButton) + /** + * The decoration Object for which this set of options should be used. The decoration is + * required to get the correct colors and fonts depending on whether the decoration represents + * an active or inactive window. + * + * Best pass the decoration object available as a context property to this property. + */ + Q_PROPERTY(KDecoration2::Decoration *deco READ decoration WRITE setDecoration NOTIFY decorationChanged) + /** + * The color for the titlebar depending on the decoration's active state. + */ + Q_PROPERTY(QColor titleBarColor READ titleBarColor NOTIFY colorsChanged) + /** + * The blend color for the titlebar depending on the decoration's active state. + */ + Q_PROPERTY(QColor titleBarBlendColor READ titleBarBlendColor NOTIFY colorsChanged) + /** + * The titlebar text color depending on the decoration's active state. + */ + Q_PROPERTY(QColor fontColor READ fontColor NOTIFY colorsChanged) + /** + * The color to use for titlebar buttons depending on the decoration's active state. + */ + Q_PROPERTY(QColor buttonColor READ buttonColor NOTIFY colorsChanged) + /** + * The color for the window frame (border) depending on the decoration's active state. + */ + Q_PROPERTY(QColor borderColor READ borderColor NOTIFY colorsChanged) + /** + * The color for the resize handle depending on the decoration's active state. + */ + Q_PROPERTY(QColor resizeHandleColor READ resizeHandleColor NOTIFY colorsChanged) + /** + * The font to be used for the decoration caption depending on the decoration's active state. + */ + Q_PROPERTY(QFont titleFont READ titleFont NOTIFY fontChanged) + /** + * The buttons to be positioned on the left side of the titlebar from left to right. + */ + Q_PROPERTY(QList titleButtonsLeft READ titleButtonsLeft NOTIFY titleButtonsChanged) + /** + * The buttons to be positioned on the right side of the titlebar from left to right. + */ + Q_PROPERTY(QList titleButtonsRight READ titleButtonsRight NOTIFY titleButtonsChanged) + Q_PROPERTY(int mousePressAndHoldInterval READ mousePressAndHoldInterval CONSTANT) +public: + enum BorderSize { + BorderNone, ///< No borders except title + BorderNoSides, ///< No borders on sides + BorderTiny, ///< Minimal borders + BorderNormal, ///< Standard size borders, the default setting + BorderLarge, ///< Larger borders + BorderVeryLarge, ///< Very large borders + BorderHuge, ///< Huge borders + BorderVeryHuge, ///< Very huge borders + BorderOversized ///< Oversized borders + }; + /** + * Enum values to identify the decorations buttons which should be used + * by the decoration. + * + */ + enum DecorationButton { + /** + * Invalid button value. A decoration should not create a button for + * this type. + */ + DecorationButtonNone, + DecorationButtonMenu, + DecorationButtonApplicationMenu, + DecorationButtonOnAllDesktops, + DecorationButtonQuickHelp, + DecorationButtonMinimize, + DecorationButtonMaximizeRestore, + DecorationButtonClose, + DecorationButtonKeepAbove, + DecorationButtonKeepBelow, + DecorationButtonShade, + DecorationButtonResize, + /** + * The decoration should create an empty spacer instead of a button for + * this type. + */ + DecorationButtonExplicitSpacer + }; + explicit DecorationOptions(QObject *parent = nullptr); + ~DecorationOptions() override; + + QColor titleBarColor() const; + QColor titleBarBlendColor() const; + QColor fontColor() const; + QColor buttonColor() const; + QColor borderColor() const; + QColor resizeHandleColor() const; + QFont titleFont() const; + QList titleButtonsLeft() const; + QList titleButtonsRight() const; + KDecoration2::Decoration *decoration() const; + void setDecoration(KDecoration2::Decoration *decoration); + + int mousePressAndHoldInterval() const; + +Q_SIGNALS: + void colorsChanged(); + void fontChanged(); + void decorationChanged(); + void titleButtonsChanged(); + +private Q_SLOTS: + void slotActiveChanged(); + +private: + bool m_active; + KDecoration2::Decoration *m_decoration; + ColorSettings m_colors; + QMetaObject::Connection m_paletteConnection; +}; + +class Borders : public QObject +{ + Q_OBJECT + Q_PROPERTY(int left READ left WRITE setLeft NOTIFY leftChanged) + Q_PROPERTY(int right READ right WRITE setRight NOTIFY rightChanged) + Q_PROPERTY(int top READ top WRITE setTop NOTIFY topChanged) + Q_PROPERTY(int bottom READ bottom WRITE setBottom NOTIFY bottomChanged) +public: + Borders(QObject *parent = nullptr); + ~Borders() override; + int left() const; + int right() const; + int top() const; + int bottom() const; + + void setLeft(int left); + void setRight(int right); + void setTop(int top); + void setBottom(int bottom); + + operator QMargins() const; + +public Q_SLOTS: + /** + * Sets all four borders to @p value. + */ + void setAllBorders(int value); + /** + * Sets all borders except the title border to @p value. + */ + void setBorders(int value); + /** + * Sets the side borders (e.g. if title is on top, the left and right borders) + * to @p value. + */ + void setSideBorders(int value); + /** + * Sets the title border to @p value. + */ + void setTitle(int value); + +Q_SIGNALS: + void leftChanged(); + void rightChanged(); + void topChanged(); + void bottomChanged(); + +private: + int m_left; + int m_right; + int m_top; + int m_bottom; +}; + +#define GETTER( name ) \ +inline int Borders::name() const \ +{ \ + return m_##name;\ +}\ + +GETTER(left) +GETTER(right) +GETTER(top) +GETTER(bottom) + +#undef GETTER + +} // namespace +#endif // KWIN_DECORATION_OPTIONS_H diff --git a/plugins/kdecorations/aurorae/src/decorationplugin.cpp b/plugins/kdecorations/aurorae/src/decorationplugin.cpp new file mode 100644 index 0000000..a5110e4 --- /dev/null +++ b/plugins/kdecorations/aurorae/src/decorationplugin.cpp @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "decorationplugin.h" +#include "colorhelper.h" +#include "decorationoptions.h" +#include + +void DecorationPlugin::registerTypes(const char *uri) +{ + Q_ASSERT(QLatin1String(uri) == QLatin1String("org.kde.kwin.decoration")); + qmlRegisterType(uri, 0, 1, "ColorHelper"); + qmlRegisterType(uri, 0, 1, "DecorationOptions"); + qmlRegisterType(uri, 0, 1, "Borders"); +} + diff --git a/plugins/kdecorations/aurorae/src/decorationplugin.h b/plugins/kdecorations/aurorae/src/decorationplugin.h new file mode 100644 index 0000000..624d5dd --- /dev/null +++ b/plugins/kdecorations/aurorae/src/decorationplugin.h @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef DECORATION_PLUGIN_H +#define DECORATION_PLUGIN_H +#include + +class DecorationPlugin : public QQmlExtensionPlugin +{ + Q_PLUGIN_METADATA(IID "org.kde.kwin.decoration") + Q_OBJECT +public: + void registerTypes(const char *uri) override; +}; + +#endif diff --git a/plugins/kdecorations/aurorae/src/kwindecoration.desktop b/plugins/kdecorations/aurorae/src/kwindecoration.desktop new file mode 100644 index 0000000..b34291b --- /dev/null +++ b/plugins/kdecorations/aurorae/src/kwindecoration.desktop @@ -0,0 +1,62 @@ +[Desktop Entry] +Type=ServiceType +X-KDE-ServiceType=KWin/Decoration + +Comment=KWin Window Decoration +Comment[ar]=زخارف نوافذ «نوافذك» +Comment[az]=KWin Pəncərə Dekorasiyası +Comment[bs]=KWin Dekoracije prozora +Comment[ca]=Decoració de les finestres del KWin +Comment[ca@valencia]=Decoració de les finestres de KWin +Comment[cs]=Dekorace oken KWin +Comment[da]=KWin vinduesdekoration +Comment[de]=KWin-Fensterdekoration +Comment[el]=Διακοσμήσεις παραθύρου KWin +Comment[en_GB]=KWin Window Decoration +Comment[es]=Decoración de las ventanas de KWin +Comment[et]=KWini akna dekoratsioon +Comment[eu]=KWin leihoen apainketa +Comment[fi]=KWin-ikkunakehys +Comment[fr]=Décorations de fenêtres KWin +Comment[ga]=Maisiúchán Fuinneog KWin +Comment[gl]=Decoración de xanela de Kwin +Comment[he]=מסגרת החלונות +Comment[hu]=KWin ablakdekoráció +Comment[ia]=Decorationes de fenestra de KWin +Comment[id]=Dekorasi Window KWin +Comment[is]=KWin gluggaskreytingar +Comment[it]=Decorazione delle finestre di KWin +Comment[ja]=KWin ウィンドウの飾り +Comment[kk]=KWin терезе безендіруі +Comment[ko]=KWin ì°½ 장식 +Comment[lt]=KWin langų dekoracijos +Comment[mr]=के-विन चौकट सजावट +Comment[nb]=KWin Vinduspynt +Comment[nds]=KWin-Finsterdekoratschoon +Comment[nl]=KWin vensterdecoratie +Comment[nn]=KWin vindaugsdekorasjon +Comment[pa]=KWin ਵਿੰਡੋ ਸਜਾਵਟ +Comment[pl]=Wygląd okien KWin +Comment[pt]=Decoração das Janelas do KWin +Comment[pt_BR]=Decorações de janelas do KWin +Comment[ro]=Decorații de fereastră KWin +Comment[ru]=Оформление окон KWin +Comment[sk]=Dekorácie okien KWin +Comment[sl]=Okraski oken KWin +Comment[sr]=К‑винова декорација прозора +Comment[sr@ijekavian]=К‑винова декорација прозора +Comment[sr@ijekavianlatin]=KWinova dekoracija prozora +Comment[sr@latin]=KWinova dekoracija prozora +Comment[sv]=Kwin-fönsterdekorationer +Comment[tr]=KWin Pencere Dekorasyonu +Comment[uk]=Обрамлення вікон KWin +Comment[vi]=Khung viền cá»­a sổ KWin +Comment[x-test]=xxKWin Window Decorationxx +Comment[zh_CN]=KWin 窗口装饰 +Comment[zh_TW]=KWin 視窗裝飾 + +[PropertyDef::X-Plasma-MainScript] +Type=QString + +[PropertyDef::X-KWin-Config-TranslationDomain] +Type=QString diff --git a/plugins/kdecorations/aurorae/src/lib/auroraetheme.cpp b/plugins/kdecorations/aurorae/src/lib/auroraetheme.cpp new file mode 100644 index 0000000..d3af485 --- /dev/null +++ b/plugins/kdecorations/aurorae/src/lib/auroraetheme.cpp @@ -0,0 +1,499 @@ +/* + Library for Aurorae window decoration themes. + SPDX-FileCopyrightText: 2009, 2010, 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later + +*/ + +#include "auroraetheme.h" +#include "themeconfig.h" +// Qt +#include +#include +#include +// KDE +#include +#include + +Q_LOGGING_CATEGORY(AURORAE, "aurorae", QtCriticalMsg) + +namespace Aurorae { + +/************************************************ +* AuroraeThemePrivate +************************************************/ +class AuroraeThemePrivate +{ +public: + AuroraeThemePrivate(); + ~AuroraeThemePrivate(); + void initButtonFrame(AuroraeButtonType type); + QString themeName; + Aurorae::ThemeConfig themeConfig; + QHash< AuroraeButtonType, QString > pathes; + bool activeCompositing; + KDecoration2::BorderSize borderSize; + KDecoration2::BorderSize buttonSize; + QString dragMimeType; + QString decorationPath; +}; + +AuroraeThemePrivate::AuroraeThemePrivate() + :activeCompositing(true) + , borderSize(KDecoration2::BorderSize::Normal) + , buttonSize(KDecoration2::BorderSize::Normal) +{ +} + +AuroraeThemePrivate::~AuroraeThemePrivate() +{ +} + +void AuroraeThemePrivate::initButtonFrame(AuroraeButtonType type) +{ + QString file(QLatin1String("aurorae/themes/") + themeName + QLatin1Char('/') + AuroraeTheme::mapButtonToName(type) + QLatin1String(".svg")); + QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, file); + if (path.isEmpty()) { + // let's look for svgz + file += QLatin1String("z"); + path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, file); + } + if (!path.isEmpty()) { + pathes[ type ] = path; + } else { + qCDebug(AURORAE) << "No button for: " << AuroraeTheme::mapButtonToName(type); + } +} + +/************************************************ +* AuroraeTheme +************************************************/ +AuroraeTheme::AuroraeTheme(QObject* parent) + : QObject(parent) + , d(new AuroraeThemePrivate) +{ + connect(this, SIGNAL(themeChanged()), SIGNAL(borderSizesChanged())); + connect(this, SIGNAL(buttonSizesChanged()), SIGNAL(borderSizesChanged())); +} + +AuroraeTheme::~AuroraeTheme() +{ + delete d; +} + +bool AuroraeTheme::isValid() const +{ + return !d->themeName.isNull(); +} + +void AuroraeTheme::loadTheme(const QString &name) +{ + KConfig conf(QStringLiteral("auroraerc")); + KConfig config(QLatin1String("aurorae/themes/") + name + QLatin1Char('/') + name + QLatin1String("rc"), + KConfig::FullConfig, QStandardPaths::GenericDataLocation); + KConfigGroup themeGroup(&conf, name); + loadTheme(name, config); +} + +void AuroraeTheme::loadTheme(const QString &name, const KConfig &config) +{ + d->themeName = name; + QString file(QLatin1String("aurorae/themes/") + d->themeName + QLatin1String("/decoration.svg")); + QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, file); + if (path.isEmpty()) { + file += QLatin1String("z"); + path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, file); + } + if (path.isEmpty()) { + qCDebug(AURORAE) << "Could not find decoration svg: aborting"; + d->themeName.clear(); + return; + } + d->decorationPath = path; + + // load the buttons + d->initButtonFrame(MinimizeButton); + d->initButtonFrame(MaximizeButton); + d->initButtonFrame(RestoreButton); + d->initButtonFrame(CloseButton); + d->initButtonFrame(AllDesktopsButton); + d->initButtonFrame(KeepAboveButton); + d->initButtonFrame(KeepBelowButton); + d->initButtonFrame(ShadeButton); + d->initButtonFrame(HelpButton); + + d->themeConfig.load(config); + emit themeChanged(); +} + +bool AuroraeTheme::hasButton(AuroraeButtonType button) const +{ + return d->pathes.contains(button); +} + +QLatin1String AuroraeTheme::mapButtonToName(AuroraeButtonType type) +{ + switch(type) { + case MinimizeButton: + return QLatin1String("minimize"); + case MaximizeButton: + return QLatin1String("maximize"); + case RestoreButton: + return QLatin1String("restore"); + case CloseButton: + return QLatin1String("close"); + case AllDesktopsButton: + return QLatin1String("alldesktops"); + case KeepAboveButton: + return QLatin1String("keepabove"); + case KeepBelowButton: + return QLatin1String("keepbelow"); + case ShadeButton: + return QLatin1String("shade"); + case HelpButton: + return QLatin1String("help"); + case MenuButton: + return QLatin1String("menu"); + case AppMenuButton: + return QLatin1String("appmenu"); + default: + return QLatin1String(""); + } +} + +const QString &AuroraeTheme::themeName() const +{ + return d->themeName; +} + +void AuroraeTheme::borders(int& left, int& top, int& right, int& bottom, bool maximized) const +{ + const qreal titleHeight = qMax((qreal)d->themeConfig.titleHeight(), + d->themeConfig.buttonHeight()*buttonSizeFactor() + + d->themeConfig.buttonMarginTop()); + if (maximized) { + const qreal title = titleHeight + d->themeConfig.titleEdgeTopMaximized() + d->themeConfig.titleEdgeBottomMaximized(); + switch ((DecorationPosition)d->themeConfig.decorationPosition()) { + case DecorationTop: + left = right = bottom = 0; + top = title; + break; + case DecorationBottom: + left = right = top = 0; + bottom = title; + break; + case DecorationLeft: + top = right = bottom = 0; + left = title; + break; + case DecorationRight: + left = top = bottom = 0; + right = title; + break; + default: + left = right = bottom = top = 0; + break; + } + } else { + int minMargin; + int maxMargin; + switch (d->borderSize) { + case KDecoration2::BorderSize::NoSides: + case KDecoration2::BorderSize::Tiny: + minMargin = 1; + maxMargin = 4; + break; + case KDecoration2::BorderSize::Normal: + minMargin = 4; + maxMargin = 6; + break; + case KDecoration2::BorderSize::Large: + minMargin = 6; + maxMargin = 8; + break; + case KDecoration2::BorderSize::VeryLarge: + minMargin = 8; + maxMargin = 12; + break; + case KDecoration2::BorderSize::Huge: + minMargin = 12; + maxMargin = 20; + break; + case KDecoration2::BorderSize::VeryHuge: + minMargin = 23; + maxMargin = 30; + break; + case KDecoration2::BorderSize::Oversized: + minMargin = 36; + maxMargin = 48; + break; + default: + minMargin = 0; + maxMargin = 0; + } + + left = qBound(minMargin, d->themeConfig.borderLeft(), maxMargin); + right = qBound(minMargin, d->themeConfig.borderRight(), maxMargin); + bottom = qBound(minMargin, d->themeConfig.borderBottom(), maxMargin); + + if (d->borderSize == KDecoration2::BorderSize::None) { + left = 0; + right = 0; + bottom = 0; + } else if (d->borderSize == KDecoration2::BorderSize::NoSides) { + left = 0; + right = 0; + } + + const qreal title = titleHeight + d->themeConfig.titleEdgeTop() + d->themeConfig.titleEdgeBottom(); + switch ((DecorationPosition)d->themeConfig.decorationPosition()) { + case DecorationTop: + top = title; + break; + case DecorationBottom: + bottom = title; + break; + case DecorationLeft: + left = title; + break; + case DecorationRight: + right = title; + break; + default: + left = right = bottom = top = 0; + break; + } + } +} + +int AuroraeTheme::bottomBorder() const +{ + int left, top, right, bottom; + left = top = right = bottom = 0; + borders(left, top, right, bottom, false); + return bottom; +} + +int AuroraeTheme::leftBorder() const +{ + int left, top, right, bottom; + left = top = right = bottom = 0; + borders(left, top, right, bottom, false); + return left; +} + +int AuroraeTheme::rightBorder() const +{ + int left, top, right, bottom; + left = top = right = bottom = 0; + borders(left, top, right, bottom, false); + return right; +} + +int AuroraeTheme::topBorder() const +{ + int left, top, right, bottom; + left = top = right = bottom = 0; + borders(left, top, right, bottom, false); + return top; +} + +int AuroraeTheme::bottomBorderMaximized() const +{ + int left, top, right, bottom; + left = top = right = bottom = 0; + borders(left, top, right, bottom, true); + return bottom; +} + +int AuroraeTheme::leftBorderMaximized() const +{ + int left, top, right, bottom; + left = top = right = bottom = 0; + borders(left, top, right, bottom, true); + return left; +} + +int AuroraeTheme::rightBorderMaximized() const +{ + int left, top, right, bottom; + left = top = right = bottom = 0; + borders(left, top, right, bottom, true); + return right; +} + +int AuroraeTheme::topBorderMaximized() const +{ + int left, top, right, bottom; + left = top = right = bottom = 0; + borders(left, top, right, bottom, true); + return top; +} + +void AuroraeTheme::padding(int& left, int& top, int& right, int& bottom) const +{ + left = d->themeConfig.paddingLeft(); + top = d->themeConfig.paddingTop(); + right = d->themeConfig.paddingRight(); + bottom = d->themeConfig.paddingBottom(); +} + +#define THEME_CONFIG( prototype ) \ +int AuroraeTheme::prototype ( ) const \ +{ \ + return d->themeConfig.prototype( ); \ +} + +THEME_CONFIG(paddingBottom) +THEME_CONFIG(paddingLeft) +THEME_CONFIG(paddingRight) +THEME_CONFIG(paddingTop) +THEME_CONFIG(buttonWidth) +THEME_CONFIG(buttonWidthMinimize) +THEME_CONFIG(buttonWidthMaximizeRestore) +THEME_CONFIG(buttonWidthClose) +THEME_CONFIG(buttonWidthAllDesktops) +THEME_CONFIG(buttonWidthKeepAbove) +THEME_CONFIG(buttonWidthKeepBelow) +THEME_CONFIG(buttonWidthShade) +THEME_CONFIG(buttonWidthHelp) +THEME_CONFIG(buttonWidthMenu) +THEME_CONFIG(buttonWidthAppMenu) +THEME_CONFIG(buttonHeight) +THEME_CONFIG(buttonSpacing) +THEME_CONFIG(buttonMarginTop) +THEME_CONFIG(explicitButtonSpacer) +THEME_CONFIG(animationTime) +THEME_CONFIG(titleEdgeLeft) +THEME_CONFIG(titleEdgeRight) +THEME_CONFIG(titleEdgeTop) +THEME_CONFIG(titleEdgeLeftMaximized) +THEME_CONFIG(titleEdgeRightMaximized) +THEME_CONFIG(titleEdgeTopMaximized) +THEME_CONFIG(titleBorderLeft) +THEME_CONFIG(titleBorderRight) +THEME_CONFIG(titleHeight) + +#undef THEME_CONFIG + +#define THEME_CONFIG_TYPE( rettype, prototype ) \ +rettype AuroraeTheme::prototype ( ) const \ +{\ + return d->themeConfig.prototype(); \ +} + +THEME_CONFIG_TYPE(QColor, activeTextColor) +THEME_CONFIG_TYPE(QColor, inactiveTextColor) +THEME_CONFIG_TYPE(Qt::Alignment, alignment) +THEME_CONFIG_TYPE(Qt::Alignment, verticalAlignment) + +#undef THEME_CONFIG_TYPE + +QString AuroraeTheme::decorationPath() const +{ + return d->decorationPath; +} + +#define BUTTON_PATH( prototype, buttonType ) \ +QString AuroraeTheme::prototype ( ) const \ +{ \ + if (hasButton( buttonType )) { \ + return d->pathes[ buttonType ]; \ + } else { \ + return QString(); \ + } \ +}\ + +BUTTON_PATH(minimizeButtonPath, MinimizeButton) +BUTTON_PATH(maximizeButtonPath, MaximizeButton) +BUTTON_PATH(restoreButtonPath, RestoreButton) +BUTTON_PATH(closeButtonPath, CloseButton) +BUTTON_PATH(allDesktopsButtonPath, AllDesktopsButton) +BUTTON_PATH(keepAboveButtonPath, KeepAboveButton) +BUTTON_PATH(keepBelowButtonPath, KeepBelowButton) +BUTTON_PATH(shadeButtonPath, ShadeButton) +BUTTON_PATH(helpButtonPath, HelpButton) + +#undef BUTTON_PATH + +void AuroraeTheme::titleEdges(int &left, int &top, int &right, int &bottom, bool maximized) const +{ + if (maximized) { + left = d->themeConfig.titleEdgeLeftMaximized(); + top = d->themeConfig.titleEdgeTopMaximized(); + right = d->themeConfig.titleEdgeRightMaximized(); + bottom = d->themeConfig.titleEdgeBottomMaximized(); + } else { + left = d->themeConfig.titleEdgeLeft(); + top = d->themeConfig.titleEdgeTop(); + right = d->themeConfig.titleEdgeRight(); + bottom = d->themeConfig.titleEdgeBottom(); + } +} + +bool AuroraeTheme::isCompositingActive() const +{ + return d->activeCompositing; +} + +void AuroraeTheme::setCompositingActive(bool active) +{ + d->activeCompositing = active; +} + +void AuroraeTheme::setBorderSize(KDecoration2::BorderSize size) +{ + if (d->borderSize == size) { + return; + } + d->borderSize = size; + emit borderSizesChanged(); +} + +void AuroraeTheme::setButtonSize(KDecoration2::BorderSize size) +{ + if (d->buttonSize == size) { + return; + } + d->buttonSize = size; + emit buttonSizesChanged(); +} + +void AuroraeTheme::setTabDragMimeType(const QString &mime) +{ + d->dragMimeType = mime; +} + +const QString &AuroraeTheme::tabDragMimeType() const +{ + return d->dragMimeType; +} + +qreal AuroraeTheme::buttonSizeFactor() const +{ + switch (d->buttonSize) { + case KDecoration2::BorderSize::Tiny: + return 0.8; + case KDecoration2::BorderSize::Large: + return 1.2; + case KDecoration2::BorderSize::VeryLarge: + return 1.4; + case KDecoration2::BorderSize::Huge: + return 1.6; + case KDecoration2::BorderSize::VeryHuge: + return 1.8; + case KDecoration2::BorderSize::Oversized: + return 2.0; + case KDecoration2::BorderSize::Normal: // fall through + default: + return 1.0; + } +} + +DecorationPosition AuroraeTheme::decorationPosition() const +{ + return (DecorationPosition)d->themeConfig.decorationPosition(); +} + +} // namespace diff --git a/plugins/kdecorations/aurorae/src/lib/auroraetheme.h b/plugins/kdecorations/aurorae/src/lib/auroraetheme.h new file mode 100644 index 0000000..6c1a049 --- /dev/null +++ b/plugins/kdecorations/aurorae/src/lib/auroraetheme.h @@ -0,0 +1,217 @@ +/* + Library for Aurorae window decoration themes. + SPDX-FileCopyrightText: 2009, 2010, 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later + +*/ + +#ifndef AURORAETHEME_H +#define AURORAETHEME_H + +// #include "libaurorae_export.h" + +#include + +#include + +#include + +Q_DECLARE_LOGGING_CATEGORY(AURORAE) + +class KConfig; + +namespace Aurorae { +class AuroraeThemePrivate; +class ThemeConfig; + +enum AuroraeButtonType { + MinimizeButton = 0, + MaximizeButton, + RestoreButton, + CloseButton, + AllDesktopsButton, + KeepAboveButton, + KeepBelowButton, + ShadeButton, + HelpButton, + MenuButton, + AppMenuButton +}; + +enum DecorationPosition { + DecorationTop = 0, + DecorationLeft, + DecorationRight, + DecorationBottom +}; + +class /*LIBAURORAE_EXPORT*/ AuroraeTheme : public QObject +{ + Q_OBJECT + Q_PROPERTY(int borderLeft READ leftBorder NOTIFY borderSizesChanged) + Q_PROPERTY(int borderRight READ rightBorder NOTIFY borderSizesChanged) + Q_PROPERTY(int borderTop READ topBorder NOTIFY borderSizesChanged) + Q_PROPERTY(int borderBottom READ bottomBorder NOTIFY borderSizesChanged) + Q_PROPERTY(int borderLeftMaximized READ leftBorderMaximized NOTIFY borderSizesChanged) + Q_PROPERTY(int borderRightMaximized READ rightBorderMaximized NOTIFY borderSizesChanged) + Q_PROPERTY(int borderTopMaximized READ topBorderMaximized NOTIFY borderSizesChanged) + Q_PROPERTY(int borderBottomMaximized READ bottomBorderMaximized NOTIFY borderSizesChanged) + Q_PROPERTY(int paddingLeft READ paddingLeft NOTIFY themeChanged) + Q_PROPERTY(int paddingRight READ paddingRight NOTIFY themeChanged) + Q_PROPERTY(int paddingTop READ paddingTop NOTIFY themeChanged) + Q_PROPERTY(int paddingBottom READ paddingBottom NOTIFY themeChanged) + Q_PROPERTY(QString themeName READ themeName NOTIFY themeChanged) + Q_PROPERTY(int buttonHeight READ buttonHeight NOTIFY themeChanged) + Q_PROPERTY(int buttonWidth READ buttonWidth NOTIFY themeChanged) + Q_PROPERTY(int buttonWidthMinimize READ buttonWidthMinimize NOTIFY themeChanged) + Q_PROPERTY(int buttonWidthMaximizeRestore READ buttonWidthMaximizeRestore NOTIFY themeChanged) + Q_PROPERTY(int buttonWidthClose READ buttonWidthClose NOTIFY themeChanged) + Q_PROPERTY(int buttonWidthAllDesktops READ buttonWidthAllDesktops NOTIFY themeChanged) + Q_PROPERTY(int buttonWidthKeepAbove READ buttonWidthKeepAbove NOTIFY themeChanged) + Q_PROPERTY(int buttonWidthKeepBelow READ buttonWidthKeepBelow NOTIFY themeChanged) + Q_PROPERTY(int buttonWidthShade READ buttonWidthShade NOTIFY themeChanged) + Q_PROPERTY(int buttonWidthHelp READ buttonWidthHelp NOTIFY themeChanged) + Q_PROPERTY(int buttonWidthMenu READ buttonWidthMenu NOTIFY themeChanged) + Q_PROPERTY(int buttonWidthAppMenu READ buttonWidthAppMenu NOTIFY themeChanged) + Q_PROPERTY(int buttonSpacing READ buttonSpacing NOTIFY themeChanged) + Q_PROPERTY(int buttonMarginTop READ buttonMarginTop NOTIFY themeChanged) + Q_PROPERTY(int explicitButtonSpacer READ explicitButtonSpacer NOTIFY themeChanged) + Q_PROPERTY(qreal buttonSizeFactor READ buttonSizeFactor NOTIFY buttonSizesChanged) + Q_PROPERTY(int animationTime READ animationTime NOTIFY themeChanged) + Q_PROPERTY(int titleEdgeLeft READ titleEdgeLeft NOTIFY themeChanged) + Q_PROPERTY(int titleEdgeRight READ titleEdgeRight NOTIFY themeChanged) + Q_PROPERTY(int titleEdgeTop READ titleEdgeTop NOTIFY themeChanged) + Q_PROPERTY(int titleEdgeLeftMaximized READ titleEdgeLeftMaximized NOTIFY themeChanged) + Q_PROPERTY(int titleEdgeRightMaximized READ titleEdgeRightMaximized NOTIFY themeChanged) + Q_PROPERTY(int titleEdgeTopMaximized READ titleEdgeTopMaximized NOTIFY themeChanged) + Q_PROPERTY(int titleBorderRight READ titleBorderRight NOTIFY themeChanged) + Q_PROPERTY(int titleBorderLeft READ titleBorderLeft NOTIFY themeChanged) + Q_PROPERTY(int titleHeight READ titleHeight NOTIFY themeChanged) + Q_PROPERTY(QString decorationPath READ decorationPath NOTIFY themeChanged) + Q_PROPERTY(QString minimizeButtonPath READ minimizeButtonPath NOTIFY themeChanged) + Q_PROPERTY(QString maximizeButtonPath READ maximizeButtonPath NOTIFY themeChanged) + Q_PROPERTY(QString restoreButtonPath READ restoreButtonPath NOTIFY themeChanged) + Q_PROPERTY(QString closeButtonPath READ closeButtonPath NOTIFY themeChanged) + Q_PROPERTY(QString allDesktopsButtonPath READ allDesktopsButtonPath NOTIFY themeChanged) + Q_PROPERTY(QString keepAboveButtonPath READ keepAboveButtonPath NOTIFY themeChanged) + Q_PROPERTY(QString keepBelowButtonPath READ keepBelowButtonPath NOTIFY themeChanged) + Q_PROPERTY(QString shadeButtonPath READ shadeButtonPath NOTIFY themeChanged) + Q_PROPERTY(QString helpButtonPath READ helpButtonPath NOTIFY themeChanged) + Q_PROPERTY(QColor activeTextColor READ activeTextColor NOTIFY themeChanged) + Q_PROPERTY(QColor inactiveTextColor READ inactiveTextColor NOTIFY themeChanged) + Q_PROPERTY(Qt::Alignment horizontalAlignment READ alignment NOTIFY themeChanged) + Q_PROPERTY(Qt::Alignment verticalAlignment READ verticalAlignment NOTIFY themeChanged) +public: + explicit AuroraeTheme(QObject* parent = nullptr); + ~AuroraeTheme() override; + // TODO: KSharedConfigPtr + void loadTheme(const QString &name, const KConfig &config); + bool isValid() const; + const QString &themeName() const; + int leftBorder() const; + int rightBorder() const; + int topBorder() const; + int bottomBorder() const; + int leftBorderMaximized() const; + int rightBorderMaximized() const; + int topBorderMaximized() const; + int bottomBorderMaximized() const; + int paddingLeft() const; + int paddingRight() const; + int paddingTop() const; + int paddingBottom() const; + int buttonWidth() const; + int buttonWidthMinimize() const; + int buttonWidthMaximizeRestore() const; + int buttonWidthClose() const; + int buttonWidthAllDesktops() const; + int buttonWidthKeepAbove() const; + int buttonWidthKeepBelow() const; + int buttonWidthShade() const; + int buttonWidthHelp() const; + int buttonWidthMenu() const; + int buttonWidthAppMenu() const; + int buttonHeight() const; + int buttonSpacing() const; + int buttonMarginTop() const; + int explicitButtonSpacer() const; + int animationTime() const; + int titleEdgeLeft() const; + int titleEdgeRight() const; + int titleEdgeTop() const; + int titleEdgeLeftMaximized() const; + int titleEdgeRightMaximized() const; + int titleEdgeTopMaximized() const; + int titleBorderLeft() const; + int titleBorderRight() const; + int titleHeight() const; + QString decorationPath() const; + QString minimizeButtonPath() const; + QString maximizeButtonPath() const; + QString restoreButtonPath() const; + QString closeButtonPath() const; + QString allDesktopsButtonPath() const; + QString keepAboveButtonPath() const; + QString keepBelowButtonPath() const; + QString shadeButtonPath() const; + QString helpButtonPath() const; + QColor activeTextColor() const; + QColor inactiveTextColor() const; + Qt::Alignment alignment() const; + Qt::Alignment verticalAlignment() const; + /** + * Sets the title edges according to maximized state. + * Title edges are global to all windows. + */ + void titleEdges(int &left, int &top, int &right, int &bottom, bool maximized) const; + void setCompositingActive(bool active); + bool isCompositingActive() const; + + /** + * @returns true if the theme contains a FrameSvg for specified button. + */ + bool hasButton(AuroraeButtonType button) const; + void setBorderSize(KDecoration2::BorderSize size); + /** + * Sets the size of the buttons. + * The available sizes are identical to border sizes, therefore BorderSize is used. + * @param size The buttons size + */ + void setButtonSize(KDecoration2::BorderSize size); + qreal buttonSizeFactor() const; + + DecorationPosition decorationPosition() const; + + void setTabDragMimeType(const QString &mime); + const QString &tabDragMimeType() const; + + // TODO: move to namespace + static QLatin1String mapButtonToName(AuroraeButtonType type); + +public Q_SLOTS: + void loadTheme(const QString &name); + +Q_SIGNALS: + void themeChanged(); + void buttonSizesChanged(); + void borderSizesChanged(); + +private: + /** + * Sets the borders according to maximized state. + * Borders are global to all windows. + */ + void borders(int &left, int &top, int &right, int &bottom, bool maximized) const; + /** + * Sets the padding according. + * Padding is global to all windows. + */ + void padding(int &left, int &top, int &right, int &bottom) const; + + AuroraeThemePrivate* const d; +}; + +} // namespace + +#endif // AURORAETHEME_H diff --git a/plugins/kdecorations/aurorae/src/lib/themeconfig.cpp b/plugins/kdecorations/aurorae/src/lib/themeconfig.cpp new file mode 100644 index 0000000..c95fd02 --- /dev/null +++ b/plugins/kdecorations/aurorae/src/lib/themeconfig.cpp @@ -0,0 +1,190 @@ +/* + SPDX-FileCopyrightText: 2009 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "themeconfig.h" + +#include +#include + +#include +#include + +namespace Aurorae +{ + +ThemeConfig::ThemeConfig() + : m_activeTextColor(defaultActiveTextColor()) + , m_activeFocusedTextColor(defaultActiveFocusedTextColor()) + , m_activeUnfocusedTextColor(defaultActiveUnfocusedTextColor()) + , m_inactiveTextColor(defaultInactiveTextColor()) + , m_inactiveFocusedTextColor(defaultInactiveFocusedTextColor()) + , m_inactiveUnfocusedTextColor(defaultInactiveUnfocusedTextColor()) + , m_activeTextShadowColor(defaultActiveTextShadowColor()) + , m_inactiveTextShadowColor(defaultInactiveTextShadowColor()) + , m_textShadowOffsetX(defaultTextShadowOffsetX()) + , m_textShadowOffsetY(defaultTextShadowOffsetY()) + , m_useTextShadow(defaultUseTextShadow()) + , m_haloActive(defaultHaloActive()) + , m_haloInactive(defaultHaloInactive()) + , m_alignment(defaultAlignment()) + , m_verticalAlignment(defaultVerticalAlignment()) + // borders + , m_borderLeft(defaultBorderLeft()) + , m_borderRight(defaultBorderRight()) + , m_borderBottom(defaultBorderBottom()) + , m_borderTop(defaultBorderTop()) + // title + , m_titleEdgeTop(defaultTitleEdgeTop()) + , m_titleEdgeBottom(defaultTitleEdgeBottom()) + , m_titleEdgeLeft(defaultTitleEdgeLeft()) + , m_titleEdgeRight(defaultTitleEdgeRight()) + , m_titleEdgeTopMaximized(defaultTitleEdgeTopMaximized()) + , m_titleEdgeBottomMaximized(defaultTitleEdgeBottomMaximized()) + , m_titleEdgeLeftMaximized(defaultTitleEdgeLeftMaximized()) + , m_titleEdgeRightMaximized(defaultTitleEdgeRightMaximized()) + , m_titleBorderLeft(defaultTitleBorderLeft()) + , m_titleBorderRight(defaultTitleBorderRight()) + , m_titleHeight(defaultTitleHeight()) + // buttons + , m_buttonWidth(defaultButtonWidth()) + , m_buttonWidthMinimize(defaultButtonWidthMinimize()) + , m_buttonWidthMaximizeRestore(defaultButtonWidthMaximizeRestore()) + , m_buttonWidthClose(defaultButtonWidthClose()) + , m_buttonWidthAllDesktops(defaultButtonWidthAllDesktops()) + , m_buttonWidthKeepAbove(defaultButtonWidthKeepAbove()) + , m_buttonWidthKeepBelow(defaultButtonWidthKeepBelow()) + , m_buttonWidthShade(defaultButtonWidthShade()) + , m_buttonWidthHelp(defaultButtonWidthHelp()) + , m_buttonWidthMenu(defaultButtonWidthMenu()) + , m_buttonWidthAppMenu(defaultButtonWidthAppMenu()) + , m_buttonHeight(defaultButtonHeight()) + , m_buttonSpacing(defaultButtonSpacing()) + , m_buttonMarginTop(defaultButtonMarginTop()) + , m_explicitButtonSpacer(defaultExplicitButtonSpacer()) + // padding + , m_paddingLeft(defaultPaddingLeft()) + , m_paddingRight(defaultPaddingRight()) + , m_paddingTop(defaultPaddingTop()) + , m_paddingBottom(defaultPaddingBottom()) + , m_animationTime(defaultAnimationTime()) + , m_shadow(defaultShadow()) + , m_decorationPosition(defaultDecorationPosition()) +{ +} + +void ThemeConfig::load(const KConfig &conf) +{ + KConfigGroup general(&conf, "General"); + m_activeTextColor = general.readEntry("ActiveTextColor", defaultActiveTextColor()); + m_inactiveTextColor = general.readEntry("InactiveTextColor", defaultInactiveTextColor()); + m_activeFocusedTextColor = general.readEntry("ActiveFocusedTabColor", m_activeTextColor); + m_activeUnfocusedTextColor = general.readEntry("ActiveUnfocusedTabColor", m_inactiveTextColor); + m_inactiveFocusedTextColor = general.readEntry("InactiveFocusedTabColor", m_inactiveTextColor); + m_inactiveUnfocusedTextColor = general.readEntry("InactiveUnfocusedTabColor", m_inactiveTextColor); + m_useTextShadow = general.readEntry("UseTextShadow", defaultUseTextShadow()); + m_activeTextShadowColor = general.readEntry("ActiveTextShadowColor", defaultActiveTextColor()); + m_inactiveTextShadowColor = general.readEntry("InactiveTextShadowColor", defaultInactiveTextColor()); + m_textShadowOffsetX = general.readEntry("TextShadowOffsetX", defaultTextShadowOffsetX()); + m_textShadowOffsetY = general.readEntry("TextShadowOffsetY", defaultTextShadowOffsetY()); + m_haloActive = general.readEntry("HaloActive", defaultHaloActive()); + m_haloInactive = general.readEntry("HaloInactive", defaultHaloInactive()); + QString alignment = (general.readEntry("TitleAlignment", "Left")).toLower(); + if (alignment == QStringLiteral("left")) { + m_alignment = Qt::AlignLeft; + } + else if (alignment == QStringLiteral("center")) { + m_alignment = Qt::AlignHCenter; + } + else { + m_alignment = Qt::AlignRight; + } + alignment = (general.readEntry("TitleVerticalAlignment", "Center")).toLower(); + if (alignment == QStringLiteral("top")) { + m_verticalAlignment = Qt::AlignTop; + } + else if (alignment == QStringLiteral("center")) { + m_verticalAlignment = Qt::AlignVCenter; + } + else { + m_verticalAlignment = Qt::AlignBottom; + } + m_animationTime = general.readEntry("Animation", defaultAnimationTime()); + m_shadow = general.readEntry("Shadow", defaultShadow()); + m_decorationPosition = general.readEntry("DecorationPosition", defaultDecorationPosition()); + + qreal scaleFactor = 1; + QScreen *primary = QGuiApplication::primaryScreen(); + if (primary) { + const qreal dpi = primary->logicalDotsPerInchX(); + scaleFactor = dpi / 96.0f; + } + + KConfigGroup border(&conf, QStringLiteral("Layout")); + // default values taken from KCommonDecoration::layoutMetric() in kcommondecoration.cpp + m_borderLeft = qRound(scaleFactor * border.readEntry("BorderLeft", defaultBorderLeft())); + m_borderRight = qRound(scaleFactor * border.readEntry("BorderRight", defaultBorderRight())); + m_borderBottom = qRound(scaleFactor * border.readEntry("BorderBottom", defaultBorderBottom())); + m_borderTop = qRound(scaleFactor * border.readEntry("BorderTop", defaultBorderTop())); + + m_titleEdgeTop = qRound(scaleFactor * border.readEntry("TitleEdgeTop", defaultTitleEdgeTop())); + m_titleEdgeBottom = qRound(scaleFactor * border.readEntry("TitleEdgeBottom", defaultTitleEdgeBottom())); + m_titleEdgeLeft = qRound(scaleFactor * border.readEntry("TitleEdgeLeft", defaultTitleEdgeLeft())); + m_titleEdgeRight = qRound(scaleFactor * border.readEntry("TitleEdgeRight", defaultTitleEdgeRight())); + m_titleEdgeTopMaximized = qRound(scaleFactor * border.readEntry("TitleEdgeTopMaximized", defaultTitleEdgeTopMaximized())); + m_titleEdgeBottomMaximized = qRound(scaleFactor * border.readEntry("TitleEdgeBottomMaximized", defaultTitleEdgeBottomMaximized())); + m_titleEdgeLeftMaximized = qRound(scaleFactor * border.readEntry("TitleEdgeLeftMaximized", defaultTitleEdgeLeftMaximized())); + m_titleEdgeRightMaximized = qRound(scaleFactor * border.readEntry("TitleEdgeRightMaximized", defaultTitleEdgeRightMaximized())); + m_titleBorderLeft = qRound(scaleFactor * border.readEntry("TitleBorderLeft", defaultTitleBorderLeft())); + m_titleBorderRight = qRound(scaleFactor * border.readEntry("TitleBorderRight", defaultTitleBorderRight())); + m_titleHeight = qRound(scaleFactor * border.readEntry("TitleHeight", defaultTitleHeight())); + + m_buttonWidth = border.readEntry("ButtonWidth", defaultButtonWidth()); + m_buttonWidthMinimize = qRound(scaleFactor * border.readEntry("ButtonWidthMinimize", m_buttonWidth)); + m_buttonWidthMaximizeRestore = qRound(scaleFactor * border.readEntry("ButtonWidthMaximizeRestore", m_buttonWidth)); + m_buttonWidthClose = qRound(scaleFactor * border.readEntry("ButtonWidthClose", m_buttonWidth)); + m_buttonWidthAllDesktops = qRound(scaleFactor * border.readEntry("ButtonWidthAlldesktops", m_buttonWidth)); + m_buttonWidthKeepAbove = qRound(scaleFactor * border.readEntry("ButtonWidthKeepabove", m_buttonWidth)); + m_buttonWidthKeepBelow = qRound(scaleFactor * border.readEntry("ButtonWidthKeepbelow", m_buttonWidth)); + m_buttonWidthShade = qRound(scaleFactor * border.readEntry("ButtonWidthShade", m_buttonWidth)); + m_buttonWidthHelp = qRound(scaleFactor * border.readEntry("ButtonWidthHelp", m_buttonWidth)); + m_buttonWidthMenu = qRound(scaleFactor * border.readEntry("ButtonWidthMenu", m_buttonWidth)); + m_buttonWidthAppMenu = qRound(scaleFactor * border.readEntry("ButtonWidthAppMenu", m_buttonWidthMenu)); + m_buttonWidth = qRound(m_buttonWidth * scaleFactor); + m_buttonHeight = qRound(scaleFactor * border.readEntry("ButtonHeight", defaultButtonHeight())); + m_buttonSpacing = qRound(scaleFactor * border.readEntry("ButtonSpacing", defaultButtonSpacing())); + m_buttonMarginTop = qRound(scaleFactor * border.readEntry("ButtonMarginTop", defaultButtonMarginTop())); + m_explicitButtonSpacer = qRound(scaleFactor * border.readEntry("ExplicitButtonSpacer", defaultExplicitButtonSpacer())); + + m_paddingLeft = qRound(scaleFactor * border.readEntry("PaddingLeft", defaultPaddingLeft())); + m_paddingRight = qRound(scaleFactor * border.readEntry("PaddingRight", defaultPaddingRight())); + m_paddingTop = qRound(scaleFactor * border.readEntry("PaddingTop", defaultPaddingTop())); + m_paddingBottom = qRound(scaleFactor * border.readEntry("PaddingBottom", defaultPaddingBottom())); +} + +QColor ThemeConfig::activeTextColor(bool useTabs, bool focused) const +{ + if (!useTabs) { + return m_activeTextColor; + } + if (focused) { + return m_activeFocusedTextColor; + } else { + return m_activeUnfocusedTextColor; + } +} + +QColor ThemeConfig::inactiveTextColor(bool useTabs, bool focused) const +{ + if (!useTabs) { + return m_inactiveTextColor; + } + if (focused) { + return m_inactiveFocusedTextColor; + } else { + return m_inactiveUnfocusedTextColor; + } +} + +} //namespace diff --git a/plugins/kdecorations/aurorae/src/lib/themeconfig.h b/plugins/kdecorations/aurorae/src/lib/themeconfig.h new file mode 100644 index 0000000..c68edc3 --- /dev/null +++ b/plugins/kdecorations/aurorae/src/lib/themeconfig.h @@ -0,0 +1,402 @@ +/* + SPDX-FileCopyrightText: 2009 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef THEMECONFIG_H +#define THEMECONFIG_H +// This class encapsulates all theme config values +// it's a separate class as it's needed by both deco and config dialog + +#include +#include + +class KConfig; + +namespace Aurorae +{ +class ThemeConfig +{ +public: + ThemeConfig(); + void load(const KConfig &conf); + ~ThemeConfig() {}; + // active window + QColor activeTextColor(bool useTabs = false, bool focused = true) const; + // inactive window + QColor inactiveTextColor(bool useTabs = false, bool focused = true) const; + QColor activeTextShadowColor() const { + return m_activeTextShadowColor; + } + QColor inactiveTextShadowColor() const { + return m_inactiveTextShadowColor; + } + int textShadowOffsetX() const { + return m_textShadowOffsetX; + } + int textShadowOffsetY() const { + return m_textShadowOffsetY; + } + bool useTextShadow() const { + return m_useTextShadow; + } + bool haloActive() const { + return m_haloActive; + } + bool haloInactive() const { + return m_haloInactive; + } + // Alignment + Qt::Alignment alignment() const { + return m_alignment; + }; + Qt::Alignment verticalAlignment() const { + return m_verticalAlignment; + } + int animationTime() const { + return m_animationTime; + } + // Borders + int borderLeft() const { + return m_borderLeft; + } + int borderRight() const { + return m_borderRight; + } + int borderBottom() const { + return m_borderBottom; + } + int borderTop() const { + return m_borderTop; + } + + int titleEdgeTop() const { + return m_titleEdgeTop; + } + int titleEdgeBottom() const { + return m_titleEdgeBottom; + } + int titleEdgeLeft() const { + return m_titleEdgeLeft; + } + int titleEdgeRight() const { + return m_titleEdgeRight; + } + int titleEdgeTopMaximized() const { + return m_titleEdgeTopMaximized; + } + int titleEdgeBottomMaximized() const { + return m_titleEdgeBottomMaximized; + } + int titleEdgeLeftMaximized() const { + return m_titleEdgeLeftMaximized; + } + int titleEdgeRightMaximized() const { + return m_titleEdgeRightMaximized; + } + int titleBorderLeft() const { + return m_titleBorderLeft; + } + int titleBorderRight() const { + return m_titleBorderRight; + } + int titleHeight() const { + return m_titleHeight; + } + + int buttonWidth() const { + return m_buttonWidth; + } + int buttonWidthMinimize() const { + return m_buttonWidthMinimize; + } + int buttonWidthMaximizeRestore() const { + return m_buttonWidthMaximizeRestore; + } + int buttonWidthClose() const { + return m_buttonWidthClose; + } + int buttonWidthAllDesktops() const { + return m_buttonWidthAllDesktops; + } + int buttonWidthKeepAbove() const { + return m_buttonWidthKeepAbove; + } + int buttonWidthKeepBelow() const { + return m_buttonWidthKeepBelow; + } + int buttonWidthShade() const { + return m_buttonWidthShade; + } + int buttonWidthHelp() const { + return m_buttonWidthHelp; + } + int buttonWidthMenu() const { + return m_buttonWidthMenu; + } + int buttonWidthAppMenu() const { + return m_buttonWidthAppMenu; + } + int buttonHeight() const { + return m_buttonHeight; + } + int buttonSpacing() const { + return m_buttonSpacing; + } + int buttonMarginTop() const { + return m_buttonMarginTop; + } + int explicitButtonSpacer() const { + return m_explicitButtonSpacer; + } + + int paddingLeft() const { + return m_paddingLeft; + } + int paddingRight() const { + return m_paddingRight; + } + int paddingTop() const { + return m_paddingTop; + } + int paddingBottom() const { + return m_paddingBottom; + } + + bool shadow() const { + return m_shadow; + } + + int decorationPosition() const { + return m_decorationPosition; + } + + static QColor defaultActiveTextColor() { + return QColor(Qt::black); + } + static QColor defaultActiveFocusedTextColor() { + return QColor(Qt::black); + } + static QColor defaultActiveUnfocusedTextColor() { + return QColor(Qt::black); + } + static QColor defaultInactiveTextColor() { + return QColor(Qt::black); + } + static QColor defaultInactiveFocusedTextColor() { + return QColor(Qt::black); + } + static QColor defaultInactiveUnfocusedTextColor() { + return QColor(Qt::black); + } + static QColor defaultActiveTextShadowColor() { + return QColor(Qt::white); + } + static QColor defaultInactiveTextShadowColor() { + return QColor(Qt::white); + } + static int defaultTextShadowOffsetX() { + return 0; + } + static int defaultTextShadowOffsetY() { + return 0; + } + static bool defaultUseTextShadow() { + return false; + } + static bool defaultHaloActive() { + return false; + } + static bool defaultHaloInactive() { + return false; + } + static Qt::Alignment defaultAlignment() { + return Qt::AlignLeft; + } + static Qt::Alignment defaultVerticalAlignment() { + return Qt::AlignVCenter; + } + // borders + static int defaultBorderLeft() { + return 5; + } + static int defaultBorderRight() { + return 5; + } + static int defaultBorderBottom() { + return 5; + } + static int defaultBorderTop() { + return 0; + } + // title + static int defaultTitleEdgeTop() { + return 5; + } + static int defaultTitleEdgeBottom() { + return 5; + } + static int defaultTitleEdgeLeft() { + return 5; + } + static int defaultTitleEdgeRight() { + return 5; + } + static int defaultTitleEdgeTopMaximized() { + return 0; + } + static int defaultTitleEdgeBottomMaximized() { + return 0; + } + static int defaultTitleEdgeLeftMaximized() { + return 0; + } + static int defaultTitleEdgeRightMaximized() { + return 0; + } + static int defaultTitleBorderLeft() { + return 5; + } + static int defaultTitleBorderRight() { + return 5; + } + static int defaultTitleHeight() { + return 20; + } + // buttons + static int defaultButtonWidth() { + return 20; + } + static int defaultButtonWidthMinimize() { + return defaultButtonWidth(); + } + static int defaultButtonWidthMaximizeRestore() { + return defaultButtonWidth(); + } + static int defaultButtonWidthClose() { + return defaultButtonWidth(); + } + static int defaultButtonWidthAllDesktops() { + return defaultButtonWidth(); + } + static int defaultButtonWidthKeepAbove() { + return defaultButtonWidth(); + } + static int defaultButtonWidthKeepBelow() { + return defaultButtonWidth(); + } + static int defaultButtonWidthShade() { + return defaultButtonWidth(); + } + static int defaultButtonWidthHelp() { + return defaultButtonWidth(); + } + static int defaultButtonWidthMenu() { + return defaultButtonWidth(); + } + static int defaultButtonWidthAppMenu() { + return defaultButtonWidthMenu(); + } + static int defaultButtonHeight() { + return 20; + } + static int defaultButtonSpacing() { + return 5; + } + static int defaultButtonMarginTop() { + return 0; + } + static int defaultExplicitButtonSpacer() { + return 10; + } + // padding + static int defaultPaddingLeft() { + return 0; + } + static int defaultPaddingRight() { + return 0; + } + static int defaultPaddingTop() { + return 0; + } + static int defaultPaddingBottom() { + return 0; + } + static int defaultAnimationTime() { + return 0; + } + static bool defaultShadow() { + return true; + } + static int defaultDecorationPosition() { + return 0; + } + +private: + QColor m_activeTextColor; + QColor m_activeFocusedTextColor; + QColor m_activeUnfocusedTextColor; + QColor m_inactiveTextColor; + QColor m_inactiveFocusedTextColor; + QColor m_inactiveUnfocusedTextColor; + QColor m_activeTextShadowColor; + QColor m_inactiveTextShadowColor; + int m_textShadowOffsetX; + int m_textShadowOffsetY; + bool m_useTextShadow; + bool m_haloActive; + bool m_haloInactive; + Qt::Alignment m_alignment; + Qt::Alignment m_verticalAlignment; + // borders + int m_borderLeft; + int m_borderRight; + int m_borderBottom; + int m_borderTop; + + // title + int m_titleEdgeTop; + int m_titleEdgeBottom; + int m_titleEdgeLeft; + int m_titleEdgeRight; + int m_titleEdgeTopMaximized; + int m_titleEdgeBottomMaximized; + int m_titleEdgeLeftMaximized; + int m_titleEdgeRightMaximized; + int m_titleBorderLeft; + int m_titleBorderRight; + int m_titleHeight; + + // buttons + int m_buttonWidth; + int m_buttonWidthMinimize; + int m_buttonWidthMaximizeRestore; + int m_buttonWidthClose; + int m_buttonWidthAllDesktops; + int m_buttonWidthKeepAbove; + int m_buttonWidthKeepBelow; + int m_buttonWidthShade; + int m_buttonWidthHelp; + int m_buttonWidthMenu; + int m_buttonWidthAppMenu; + int m_buttonHeight; + int m_buttonSpacing; + int m_buttonMarginTop; + int m_explicitButtonSpacer; + + // padding + int m_paddingLeft; + int m_paddingRight; + int m_paddingTop; + int m_paddingBottom; + + int m_animationTime; + + bool m_shadow; + + int m_decorationPosition; +}; + +} + +#endif diff --git a/plugins/kdecorations/aurorae/src/qml/AppMenuButton.qml b/plugins/kdecorations/aurorae/src/qml/AppMenuButton.qml new file mode 100644 index 0000000..e6cdfc5 --- /dev/null +++ b/plugins/kdecorations/aurorae/src/qml/AppMenuButton.qml @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +import QtQuick 2.0 +import org.kde.kquickcontrolsaddons 2.0 as KQuickControlsAddons +import org.kde.kwin.decoration 0.1 + +DecorationButton { + id: appMenuButton + buttonType: DecorationOptions.DecorationButtonApplicationMenu + visible: decoration.client.hasApplicationMenu + KQuickControlsAddons.QIconItem { + icon: decoration.client.icon + anchors.fill: parent + } +} diff --git a/plugins/kdecorations/aurorae/src/qml/AuroraeButton.qml b/plugins/kdecorations/aurorae/src/qml/AuroraeButton.qml new file mode 100644 index 0000000..69e9603 --- /dev/null +++ b/plugins/kdecorations/aurorae/src/qml/AuroraeButton.qml @@ -0,0 +1,204 @@ +/* + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +import QtQuick 2.0 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.kwin.decoration 0.1 + +DecorationButton { + function widthForButton() { + switch (buttonType) { + case DecorationOptions.DecorationButtonMenu: + // menu + return auroraeTheme.buttonWidthMenu; + case DecorationOptions.DecorationButtonApplicationMenu: + // app menu + return auroraeTheme.buttonWidthAppMenu; + case DecorationOptions.DecorationButtonOnAllDesktops: + // all desktops + return auroraeTheme.buttonWidthAllDesktops; + case DecorationOptions.DecorationButtonQuickHelp: + // help + return auroraeTheme.buttonWidthHelp; + case DecorationOptions.DecorationButtonMinimize: + // minimize + return auroraeTheme.buttonWidthMinimize; + case DecorationOptions.DecorationButtonMaximizeRestore: + // maximize + return auroraeTheme.buttonWidthMaximizeRestore; + case DecorationOptions.DecorationButtonClose: + // close + return auroraeTheme.buttonWidthClose; + case DecorationOptions.DecorationButtonKeepAbove: + // keep above + return auroraeTheme.buttonWidthKeepAbove; + case DecorationOptions.DecorationButtonKeepBelow: + // keep below + return auroraeTheme.buttonWidthKeepBelow; + case DecorationOptions.DecorationButtonShade: + // shade + return auroraeTheme.buttonWidthShade; + default: + return auroraeTheme.buttonWidth; + } + } + function pathForButton() { + switch (buttonType) { + case DecorationOptions.DecorationButtonOnAllDesktops: + // all desktops + return auroraeTheme.allDesktopsButtonPath; + case DecorationOptions.DecorationButtonQuickHelp: + // help + return auroraeTheme.helpButtonPath; + case DecorationOptions.DecorationButtonMinimize: + // minimize + return auroraeTheme.minimizeButtonPath; + case DecorationOptions.DecorationButtonMaximizeRestore: + // maximize + return auroraeTheme.maximizeButtonPath; + case DecorationOptions.DecorationButtonMaximizeRestore + 100: + // maximize + return auroraeTheme.restoreButtonPath; + case DecorationOptions.DecorationButtonClose: + // close + return auroraeTheme.closeButtonPath; + case DecorationOptions.DecorationButtonKeepAbove: + // keep above + return auroraeTheme.keepAboveButtonPath; + case DecorationOptions.DecorationButtonKeepBelow: + // keep below + return auroraeTheme.keepBelowButtonPath; + case DecorationOptions.DecorationButtonShade: + // shade + return auroraeTheme.shadeButtonPath; + default: + return ""; + } + } + width: widthForButton() * auroraeTheme.buttonSizeFactor + height: auroraeTheme.buttonHeight * auroraeTheme.buttonSizeFactor + PlasmaCore.FrameSvg { + property bool supportsHover: hasElementPrefix("hover") + property bool supportsPressed: hasElementPrefix("pressed") + property bool supportsDeactivated: hasElementPrefix("deactivated") + property bool supportsInactive: hasElementPrefix("inactive") + property bool supportsInactiveHover: hasElementPrefix("hover-inactive") + property bool supportsInactivePressed: hasElementPrefix("pressed-inactive") + property bool supportsInactiveDeactivated: hasElementPrefix("deactivated-inactive") + id: buttonSvg + imagePath: pathForButton() + } + PlasmaCore.FrameSvgItem { + id: buttonActive + property bool shown: (decoration.client.active || !buttonSvg.supportsInactive) && ((!pressed && !toggled) || !buttonSvg.supportsPressed) && (!hovered || !buttonSvg.supportsHover) && (enabled || !buttonSvg.supportsDeactivated) + anchors.fill: parent + imagePath: buttonSvg.imagePath + prefix: "active" + opacity: shown ? 1 : 0 + Behavior on opacity { + NumberAnimation { + duration: auroraeTheme.animationTime + } + } + } + PlasmaCore.FrameSvgItem { + id: buttonActiveHover + property bool shown: hovered && !pressed && !toggled && buttonSvg.supportsHover && (decoration.client.active || !buttonSvg.supportsInactiveHover) + anchors.fill: parent + imagePath: buttonSvg.imagePath + prefix: "hover" + opacity: shown ? 1 : 0 + Behavior on opacity { + NumberAnimation { + duration: auroraeTheme.animationTime + } + } + } + PlasmaCore.FrameSvgItem { + id: buttonActivePressed + property bool shown: (toggled || pressed) && buttonSvg.supportsPressed && (decoration.client.active || !buttonSvg.supportsInactivePressed) + anchors.fill: parent + imagePath: buttonSvg.imagePath + prefix: "pressed" + opacity: shown ? 1 : 0 + Behavior on opacity { + NumberAnimation { + duration: auroraeTheme.animationTime + } + } + } + PlasmaCore.FrameSvgItem { + id: buttonActiveDeactivated + property bool shown: !enabled && buttonSvg.supportsDeactivated && (decoration.client.active || !buttonSvg.supportsInactiveDeactivated) + anchors.fill: parent + imagePath: buttonSvg.imagePath + prefix: "deactivated" + opacity: shown ? 1 : 0 + Behavior on opacity { + NumberAnimation { + duration: auroraeTheme.animationTime + } + } + } + PlasmaCore.FrameSvgItem { + id: buttonInactive + property bool shown: !decoration.client.active && buttonSvg.supportsInactive && !hovered && !pressed && !toggled && enabled + anchors.fill: parent + imagePath: buttonSvg.imagePath + prefix: "inactive" + opacity: shown ? 1 : 0 + Behavior on opacity { + NumberAnimation { + duration: auroraeTheme.animationTime + } + } + } + PlasmaCore.FrameSvgItem { + id: buttonInactiveHover + property bool shown: !decoration.client.active && hovered && !pressed && !toggled && buttonSvg.supportsInactiveHover + anchors.fill: parent + imagePath: buttonSvg.imagePath + prefix: "hover-inactive" + opacity: shown ? 1 : 0 + Behavior on opacity { + NumberAnimation { + duration: auroraeTheme.animationTime + } + } + } + PlasmaCore.FrameSvgItem { + id: buttonInactivePressed + property bool shown: !decoration.client.active && (toggled || pressed) && buttonSvg.supportsInactivePressed + anchors.fill: parent + imagePath: buttonSvg.imagePath + prefix: "pressed-inactive" + opacity: shown ? 1 : 0 + Behavior on opacity { + NumberAnimation { + duration: auroraeTheme.animationTime + } + } + } + PlasmaCore.FrameSvgItem { + id: buttonInactiveDeactivated + property bool shown: !decoration.client.active && !enabled && buttonSvg.supportsInactiveDeactivated + anchors.fill: parent + imagePath: buttonSvg.imagePath + prefix: "deactivated-inactive" + opacity: shown ? 1 : 0 + Behavior on opacity { + NumberAnimation { + duration: auroraeTheme.animationTime + } + } + } + Component.onCompleted: { + if (buttonType == DecorationOptions.DecorationButtonQuickHelp && !decoration.client.providesContextHelp) { + visible = false; + } else { + visible = buttonSvg.imagePath != ""; + } + } +} diff --git a/plugins/kdecorations/aurorae/src/qml/AuroraeButtonGroup.qml b/plugins/kdecorations/aurorae/src/qml/AuroraeButtonGroup.qml new file mode 100644 index 0000000..cfc53fa --- /dev/null +++ b/plugins/kdecorations/aurorae/src/qml/AuroraeButtonGroup.qml @@ -0,0 +1,49 @@ +/* + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +import QtQuick 2.0 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.kwin.decoration 0.1 + +Item { + function createButtons() { + var component = Qt.createComponent("AuroraeButton.qml"); + for (var i=0; i + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +import QtQuick 2.0 +import org.kde.kwin.decoration 0.1 + +Item { + id: button + width: auroraeTheme.buttonWidthMaximizeRestore * auroraeTheme.buttonSizeFactor + height: auroraeTheme.buttonHeight * auroraeTheme.buttonSizeFactor + property bool hovered: false + property bool pressed: false + property bool toggled: false + AuroraeButton { + id: maximizeButton + anchors.fill: parent + buttonType: DecorationOptions.DecorationButtonMaximizeRestore + opacity: (!decoration.client.maximized || auroraeTheme.restoreButtonPath == "") ? 1 : 0 + hovered: button.hovered + pressed: button.pressed + toggled: button.toggled + Behavior on opacity { + NumberAnimation { + duration: auroraeTheme.animationTime + } + } + } + AuroraeButton { + id: restoreButton + anchors.fill: parent + buttonType: DecorationOptions.DecorationButtonMaximizeRestore + 100 + opacity: (decoration.client.maximized && auroraeTheme.restoreButtonPath != "") ? 1 : 0 + hovered: button.hovered + pressed: button.pressed + toggled: button.toggled + Behavior on opacity { + NumberAnimation { + duration: auroraeTheme.animationTime + } + } + } + MouseArea { + anchors.fill: parent + acceptedButtons: { + return Qt.LeftButton | Qt.RightButton | Qt.MiddleButton; + } + hoverEnabled: true + onEntered: button.hovered = true + onExited: button.hovered = false + onPressed: button.pressed = true + onReleased: button.pressed = false + onClicked: { + decoration.requestToggleMaximization(mouse.button); + } + } +} diff --git a/plugins/kdecorations/aurorae/src/qml/ButtonGroup.qml b/plugins/kdecorations/aurorae/src/qml/ButtonGroup.qml new file mode 100644 index 0000000..e44c431 --- /dev/null +++ b/plugins/kdecorations/aurorae/src/qml/ButtonGroup.qml @@ -0,0 +1,90 @@ +/* + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +import QtQuick 2.0 +import org.kde.kwin.decoration 0.1 + +Item { + function createButtons() { + for (var i=0; i + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +import QtQuick 2.0 +import org.kde.kwin.decoration 0.1 + +Item { + property QtObject borders: Borders { + objectName: "borders" + } + property QtObject maximizedBorders: Borders { + objectName: "maximizedBorders" + } + property QtObject extendedBorders: Borders { + objectName: "extendedBorders" + } + property QtObject padding: Borders { + objectName: "padding" + } + property bool alpha: true + width: decoration.client.width + decoration.borderLeft + decoration.borderRight + (decoration.client.maximized ? 0 : (padding.left + padding.right)) + height: (decoration.client.shaded ? 0 : decoration.client.height) + decoration.borderTop + (decoration.client.shaded ? 0 : decoration.borderBottom) + (decoration.client.maximized ? 0 : (padding.top + padding.bottom)) +} diff --git a/plugins/kdecorations/aurorae/src/qml/DecorationButton.qml b/plugins/kdecorations/aurorae/src/qml/DecorationButton.qml new file mode 100644 index 0000000..ace6c18 --- /dev/null +++ b/plugins/kdecorations/aurorae/src/qml/DecorationButton.qml @@ -0,0 +1,114 @@ +/* + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +import QtQuick 2.0 +import org.kde.kwin.decoration 0.1 + +Item { + id: button + property int buttonType : DecorationOptions.DecorationButtonNone + property bool hovered: false + property bool pressed: false + property bool toggled: false + enabled: { + switch (button.buttonType) { + case DecorationOptions.DecorationButtonClose: + return decoration.client.closeable; + case DecorationOptions.DecorationButtonMaximizeRestore: + return decoration.client.maximizeable; + case DecorationOptions.DecorationButtonMinimize: + return decoration.client.minimizeable; + case DecorationOptions.DecorationButtonExplicitSpacer: + return false; + default: + return true; + } + } + MouseArea { + anchors.fill: parent + acceptedButtons: { + switch (button.buttonType) { + case DecorationOptions.DecorationButtonMenu: + return Qt.LeftButton | Qt.RightButton; + case DecorationOptions.DecorationButtonMaximizeRestore: + return Qt.LeftButton | Qt.RightButton | Qt.MiddleButton; + default: + return Qt.LeftButton; + } + } + hoverEnabled: true + onEntered: button.hovered = true + onExited: button.hovered = false + onPressed: button.pressed = true + onReleased: button.pressed = false + onClicked: { + switch (button.buttonType) { + case DecorationOptions.DecorationButtonMenu: + // menu + decoration.requestShowWindowMenu(); + break; + case DecorationOptions.DecorationButtonApplicationMenu: + // app menu + var pos = button.mapToItem(null, 0, 0); // null = "map to scene" + decoration.requestShowApplicationMenu(Qt.rect(pos.x, pos.y, button.width, button.height), 0) + break; + case DecorationOptions.DecorationButtonOnAllDesktops: + // all desktops + decoration.requestToggleOnAllDesktops(); + break; + case DecorationOptions.DecorationButtonQuickHelp: + // help + decoration.requestContextHelp(); + break; + case DecorationOptions.DecorationButtonMinimize: + // minimize + decoration.requestMinimize(); + break; + case DecorationOptions.DecorationButtonMaximizeRestore: + // maximize + decoration.requestToggleMaximization(mouse.button); + break; + case DecorationOptions.DecorationButtonClose: + // close + decoration.requestClose(); + break; + case DecorationOptions.DecorationButtonKeepAbove: + // keep above + decoration.requestToggleKeepAbove(); + break; + case DecorationOptions.DecorationButtonKeepBelow: + // keep below + decoration.requestToggleKeepBelow(); + break; + case DecorationOptions.DecorationButtonShade: + // shade + decoration.requestToggleShade(); + break; + } + } + onDoubleClicked: { + if (button.buttonType == DecorationOptions.DecorationButtonMenu) { + decoration.requestClose(); + } + } + Component.onCompleted: { + switch (button.buttonType) { + case DecorationOptions.DecorationButtonOnAllDesktops: + // all desktops + button.toggled = Qt.binding(function() { return decoration.client.onAllDesktops; }); + break; + case DecorationOptions.DecorationButtonKeepAbove: + button.toggled = Qt.binding(function() { return decoration.client.keepAbove; }); + break; + case DecorationOptions.DecorationButtonKeepBelow: + button.toggled = Qt.binding(function() { return decoration.client.keepBelow; }); + break; + case DecorationOptions.DecorationButtonShade: + button.toggled = Qt.binding(function() { return decoration.client.shaded; }); + break; + } + } + } +} diff --git a/plugins/kdecorations/aurorae/src/qml/MenuButton.qml b/plugins/kdecorations/aurorae/src/qml/MenuButton.qml new file mode 100644 index 0000000..154cd81 --- /dev/null +++ b/plugins/kdecorations/aurorae/src/qml/MenuButton.qml @@ -0,0 +1,70 @@ +/* + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +import QtQuick 2.0 +import org.kde.kquickcontrolsaddons 2.0 as KQuickControlsAddons +import org.kde.kwin.decoration 0.1 + +DecorationButton { + property bool closeOnDoubleClick: decorationSettings.closeOnDoubleClickOnMenu + id: menuButton + buttonType: DecorationOptions.DecorationButtonMenu + KQuickControlsAddons.QIconItem { + icon: decoration.client.icon + anchors.fill: parent + } + DecorationOptions { + id: options + deco: decoration + } + Timer { + id: timer + interval: options.mousePressAndHoldInterval + repeat: false + onTriggered: decoration.requestShowWindowMenu() + } + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + onPressed: { + parent.pressed = true; + // we need a timer to figure out whether there is a double click in progress or not + // if we have a "normal" click we want to open the context menu. This would eat our + // second click of the double click. To properly get the double click we have to wait + // the double click delay to ensure that it was only a single click. + if (timer.running) { + timer.stop(); + } else if (menuButton.closeOnDoubleClick) { + timer.start(); + } + } + onReleased: { + parent.pressed = false; + timer.stop(); + } + onExited: { + if (!parent.pressed) { + return; + } + if (timer.running) { + timer.stop(); + } + parent.pressed = false; + } + onClicked: { + // for right clicks we show the menu instantly + // and if the option is disabled we always show menu directly + if (!menuButton.closeOnDoubleClick || mouse.button == Qt.RightButton) { + decoration.requestShowWindowMenu(); + timer.stop(); + } + } + onDoubleClicked: { + if (menuButton.closeOnDoubleClick) { + decoration.requestClose(); + } + } + } +} diff --git a/plugins/kdecorations/aurorae/src/qml/aurorae.qml b/plugins/kdecorations/aurorae/src/qml/aurorae.qml new file mode 100644 index 0000000..ab6e06d --- /dev/null +++ b/plugins/kdecorations/aurorae/src/qml/aurorae.qml @@ -0,0 +1,220 @@ +/* + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +import QtQuick 2.0 +import org.kde.kwin.decoration 0.1 +import org.kde.plasma.core 2.0 as PlasmaCore + +Decoration { + id: root + property bool animate: false + Component.onCompleted: { + borders.left = Qt.binding(function() { return Math.max(0, auroraeTheme.borderLeft);}); + borders.right = Qt.binding(function() { return Math.max(0, auroraeTheme.borderRight);}); + borders.top = Qt.binding(function() { return Math.max(0, auroraeTheme.borderTop);}); + borders.bottom = Qt.binding(function() { return Math.max(0, auroraeTheme.borderBottom);}); + maximizedBorders.left = Qt.binding(function() { return Math.max(0, auroraeTheme.borderLeftMaximized);}); + maximizedBorders.right = Qt.binding(function() { return Math.max(0, auroraeTheme.borderRightMaximized);}); + maximizedBorders.bottom = Qt.binding(function() { return Math.max(0, auroraeTheme.borderBottomMaximized);}); + maximizedBorders.top = Qt.binding(function() { return Math.max(0, auroraeTheme.borderTopMaximized);}); + padding.left = auroraeTheme.paddingLeft; + padding.right = auroraeTheme.paddingRight; + padding.bottom = auroraeTheme.paddingBottom; + padding.top = auroraeTheme.paddingTop; + root.animate = true; + } + DecorationOptions { + id: options + deco: decoration + } + Item { + id: titleRect + x: decoration.client.maximized ? maximizedBorders.left : borders.left + y: decoration.client.maximized ? 0 : root.borders.bottom + width: decoration.client.width//parent.width - x - (decoration.client.maximized ? maximizedBorders.right : borders.right) + height: decoration.client.maximized ? maximizedBorders.top : borders.top + Component.onCompleted: { + decoration.installTitleItem(titleRect); + } + } + PlasmaCore.FrameSvg { + property bool supportsInactive: hasElementPrefix("decoration-inactive") + property bool supportsMaximized: hasElementPrefix("decoration-maximized") + property bool supportsMaximizedInactive: hasElementPrefix("decoration-maximized-inactive") + property bool supportsInnerBorder: hasElementPrefix("innerborder") + property bool supportsInnerBorderInactive: hasElementPrefix("innerborder-inactive") + id: backgroundSvg + imagePath: auroraeTheme.decorationPath + } + PlasmaCore.FrameSvgItem { + id: decorationActive + property bool shown: (!decoration.client.maximized || !backgroundSvg.supportsMaximized) && (decoration.client.active || !backgroundSvg.supportsInactive) + anchors.fill: parent + imagePath: backgroundSvg.imagePath + prefix: "decoration" + opacity: shown ? 1 : 0 + enabledBorders: decoration.client.maximized ? PlasmaCore.FrameSvg.NoBorder : PlasmaCore.FrameSvg.TopBorder | PlasmaCore.FrameSvg.BottomBorder | PlasmaCore.FrameSvg.LeftBorder | PlasmaCore.FrameSvg.RightBorder + Behavior on opacity { + enabled: root.animate + NumberAnimation { + duration: auroraeTheme.animationTime + } + } + } + PlasmaCore.FrameSvgItem { + id: decorationInactive + anchors.fill: parent + imagePath: backgroundSvg.imagePath + prefix: "decoration-inactive" + opacity: (!decoration.client.active && backgroundSvg.supportsInactive) ? 1 : 0 + enabledBorders: decoration.client.maximized ? PlasmaCore.FrameSvg.NoBorder : PlasmaCore.FrameSvg.TopBorder | PlasmaCore.FrameSvg.BottomBorder | PlasmaCore.FrameSvg.LeftBorder | PlasmaCore.FrameSvg.RightBorder + Behavior on opacity { + enabled: root.animate + NumberAnimation { + duration: auroraeTheme.animationTime + } + } + } + PlasmaCore.FrameSvgItem { + id: decorationMaximized + property bool shown: decoration.client.maximized && backgroundSvg.supportsMaximized && (decoration.client.active || !backgroundSvg.supportsMaximizedInactive) + anchors { + left: parent.left + right: parent.right + top: parent.top + leftMargin: 0 + rightMargin: 0 + topMargin: 0 + } + imagePath: backgroundSvg.imagePath + prefix: "decoration-maximized" + height: parent.maximizedBorders.top + opacity: shown ? 1 : 0 + enabledBorders: PlasmaCore.FrameSvg.NoBorder + Behavior on opacity { + enabled: root.animate + NumberAnimation { + duration: auroraeTheme.animationTime + } + } + } + PlasmaCore.FrameSvgItem { + id: decorationMaximizedInactive + anchors { + left: parent.left + right: parent.right + top: parent.top + leftMargin: 0 + rightMargin: 0 + topMargin: 0 + } + imagePath: backgroundSvg.imagePath + prefix: "decoration-maximized-inactive" + height: parent.maximizedBorders.top + opacity: (!decoration.client.active && decoration.client.maximized && backgroundSvg.supportsMaximizedInactive) ? 1 : 0 + enabledBorders: PlasmaCore.FrameSvg.NoBorder + Behavior on opacity { + enabled: root.animate + NumberAnimation { + duration: auroraeTheme.animationTime + } + } + } + AuroraeButtonGroup { + id: leftButtonGroup + buttons: options.titleButtonsLeft + width: childrenRect.width + animate: root.animate + anchors { + left: root.left + leftMargin: decoration.client.maximized ? auroraeTheme.titleEdgeLeftMaximized : (auroraeTheme.titleEdgeLeft + root.padding.left) + } + } + AuroraeButtonGroup { + id: rightButtonGroup + buttons: options.titleButtonsRight + width: childrenRect.width + animate: root.animate + anchors { + right: root.right + rightMargin: decoration.client.maximized ? auroraeTheme.titleEdgeRightMaximized : (auroraeTheme.titleEdgeRight + root.padding.right) + } + } + Text { + id: caption + text: decoration.client.caption + textFormat: Text.PlainText + horizontalAlignment: auroraeTheme.horizontalAlignment + verticalAlignment: auroraeTheme.verticalAlignment + elide: Text.ElideRight + height: Math.max(auroraeTheme.titleHeight, auroraeTheme.buttonHeight * auroraeTheme.buttonSizeFactor) + color: decoration.client.active ? auroraeTheme.activeTextColor : auroraeTheme.inactiveTextColor + font: options.titleFont + renderType: Text.NativeRendering + anchors { + left: leftButtonGroup.right + right: rightButtonGroup.left + top: root.top + topMargin: decoration.client.maximized ? auroraeTheme.titleEdgeTopMaximized : (auroraeTheme.titleEdgeTop + root.padding.top) + leftMargin: auroraeTheme.titleBorderLeft + rightMargin: auroraeTheme.titleBorderRight + } + Behavior on color { + enabled: root.animate + ColorAnimation { + duration: auroraeTheme.animationTime + } + } + } + PlasmaCore.FrameSvgItem { + id: innerBorder + anchors { + fill: parent + leftMargin: parent.padding.left + parent.borders.left - margins.left + rightMargin: parent.padding.right + parent.borders.right - margins.right + topMargin: parent.padding.top + parent.borders.top - margins.top + bottomMargin: parent.padding.bottom + parent.borders.bottom - margins.bottom + } + visible: parent.borders.left > fixedMargins.left + && parent.borders.right > fixedMargins.right + && parent.borders.top > fixedMargins.top + && parent.borders.bottom > fixedMargins.bottom + + imagePath: backgroundSvg.imagePath + prefix: "innerborder" + opacity: (decoration.client.active && !decoration.client.maximized && backgroundSvg.supportsInnerBorder) ? 1 : 0 + Behavior on opacity { + enabled: root.animate + NumberAnimation { + duration: auroraeTheme.animationTime + } + } + } + PlasmaCore.FrameSvgItem { + id: innerBorderInactive + anchors { + fill: parent + leftMargin: parent.padding.left + parent.borders.left - margins.left + rightMargin: parent.padding.right + parent.borders.right - margins.right + topMargin: parent.padding.top + parent.borders.top - margins.top + bottomMargin: parent.padding.bottom + parent.borders.bottom - margins.bottom + } + + visible: parent.borders.left > fixedMargins.left + && parent.borders.right > fixedMargins.right + && parent.borders.top > fixedMargins.top + && parent.borders.bottom > fixedMargins.bottom + + imagePath: backgroundSvg.imagePath + prefix: "innerborder-inactive" + opacity: (!decoration.client.active && !decoration.client.maximized && backgroundSvg.supportsInnerBorderInactive) ? 1 : 0 + Behavior on opacity { + enabled: root.animate + NumberAnimation { + duration: auroraeTheme.animationTime + } + } + } +} diff --git a/plugins/kdecorations/aurorae/src/qml/qmldir b/plugins/kdecorations/aurorae/src/qml/qmldir new file mode 100644 index 0000000..a383ebc --- /dev/null +++ b/plugins/kdecorations/aurorae/src/qml/qmldir @@ -0,0 +1,8 @@ +module org.kde.kwin.decoration +plugin decorationplugin + +Decoration 0.1 Decoration.qml +DecorationButton 0.1 DecorationButton.qml +MenuButton 0.1 MenuButton.qml +AppMenuButton 0.1 AppMenuButton.qml +ButtonGroup 0.1 ButtonGroup.qml diff --git a/plugins/kdecorations/aurorae/theme-description b/plugins/kdecorations/aurorae/theme-description new file mode 100644 index 0000000..e5380f5 --- /dev/null +++ b/plugins/kdecorations/aurorae/theme-description @@ -0,0 +1,163 @@ +DESCRIPTION OF AURORAE +====================== + +Aurorae is a theme engine for KWin window decorations. It is built against the unstable API of KWin +in KDE 4.3. Aurorae uses SVG to render the decoration and buttons and there is a simple config file +for configuring the theme details. + +This theme engine uses Plasma technologie to render the window decoration. Every detail can be +themed by the usage of SVG. The theme engine uses Plasma's FrameSvg, so you can provide SVG files +containing borders. This is described in more detail in techbase: +https://techbase.kde.org/Development/Tutorials/Plasma5/ThemeDetails + +The theme consists of one folder containing svgz files for decoration and buttons, one KConfig file +for the theme details and one metadata.desktop file which you can use to name your theme, author +information, etc. + +Although the engine uses Plasma technology, it isn't Plasma. So it does not know anything about +Plasmoids and you will never be able to put Plasmoids into the decoration. That is out of scope of +this engine. + +Aurorae uses the features provided by KWin 4.3. So the themes can provide their own decoration +shadows and it is recommended that your themes provide those. The engine supports ARGB decoration +which is enabled by default. If you provide a theme using translucency, please make sure, that it +works without compositing as well. + +Window Decoration +================= +The window decoration has to be provided in file "decoration.svgz". This svg has to contain all the +elements required for a Plasma theme background. The decoration has to use the element prefix +"decoration". + +If you want to provide a different style for inactive windows you can add it to the same svg. The +inactive elements must have the element prefix "decoration-inactive". The theme engine tests for +this prefix and if not provided inactive windows will be rendered with the same style as active +windows. + +You have to provide a special decoration for opaque mode, that is when compositing is not active. +This opaque decoration is used for generating the window mask. The element prefix is +"decoration-opaque" for active and "decoration-opaque-inactive" for inactive windows. The mask is +generated from the active window. + +Maximized Windows +----------------- +In order to better support maximized windows there exists a special frame svg called +"decoration-maximized". In the same way as for the general decoration you can specify a version for +inactive, opaque and inactive-opaque. This results in the following names: + * decoration-maximized + * decoration-maximized-inactive + * decoration-maximized-opaque + * decoration-maximized-opaque-inactive + +In all cases only the center element will be used. There is no need to specify borders. Please note +that in case of a window with translucent widgets the center element will be stretched to the size +of the complete window. + +The following fallback strategy is used: if inactive is not present it falls back to the active. +If opaque is not present it falls back to the translucent. If none of the maximized elements are +present the center element of the decoration is used! + +In order to support Fitts' Law all TitleEdge Settings are set to 0. So the buttons will be directly +next to the screen edges. You have the possibility to overwrite these settins (see below). + +Buttons +======= +You have to provide a svgz file for each button your theme should contain. If you do not provide a +file for a button type the engine will not include that button, so your decoration will miss it. +There is no fallback to a default theme. The buttons are rendered using Plasma's FrameSvg as well. +So you have to provide the "center" element. Borders are not supported + +You can provide the following buttons: + * close + * minimize + * maximize + * restore + * alldesktops + * keepabove + * keepbelow + * shade + * resize + * help + +Each button can have different states. So a button could be hovered, pressed, deactivated and you +might want to provide different styles for active and inactive windows. You can use the following +element prefix to provide styles for the buttons: + * active (normal button for active window) + * inactive (normal button for inactive window) + * hover (hover state for active window) + * hover-inactive (hover state for inactive window) + * pressed (button is pressed) + * pressed-inactive (pressed inactive button) + * deactivated (button cannot be clicked, e.g. window cannot be closed) + * deactivated-inactive (same for inactive windows) + +You have at least to provide the active element. All other elements are optional and the active +element is always used as a fallback. If you provide the inactive element, this is used as a +fallback for the inactive window. That is, if you provide a hover element, but none for inactive, +the inactive window will not have a hover effect. Same is true for pressed and deactivated. +Reasonable that means if you provide a deactivated and an inactive element you want to provide a +deactivated-inactive element as well. + +Configuration file +================== +The configuration file is a normal KConfig file. You have to give it the name of your decoration +with suffix "rc". So if your theme has the name "deco", your config file will be named "decorc". +The following section shows the possible options with their default values. + +[General] +TitleAlignment=Left # vorizontal alignment of window title +TitleVerticalAlignment=Center # vertical alignment of window title +Animation=0 # animation duration in msec when hovering a button and on active/inactive change +ActiveTextColor=0,0,0,255 # title text color of active window +InactiveTextColor=0,0,0,255 # title text color of inactive window +UseTextShadow=false # Draw Shadow behind title text +ActiveTextShadowColor=255,255,255,255 # Shadow text color of active window +InactiveTextShadowColor=255,255,255,255 # Shadow text color of active window +TextShadowOffsetX=0 # Offset of shadow in x direction +TextShadowOffsetY=0 # Offset of shadow in y direction +HaloActive=false # Draw halo behing title of active window (since 4.5) +HaloInactive=false # Draw halo behing title of inactive window (since 4.5) +LeftButtons=MS # buttons in left button group (see http://api.kde.org/4.x-api/kdebase-workspace-apidocs/kwin/lib/html/classKDecorationOptions.html#8ad12d76c93c5f1a12ea07b30f92d2fa) +RightButtons=HIA__X # buttons in right button group +Shadow=true # decoration provides shadows: you have to add padding + +[Layout] # uses Layout Manager (see http://api.kde.org/4.x-api/kdebase-workspace-apidocs/kwin/lib/html/classKCommonDecoration.html#7932f74c28432ad8de232f1c6e8751ce) +BorderLeft=5 +BorderRight=5 +BorderBottom=5 +TitleEdgeTop=5 +TitleEdgeBottom=5 +TitleEdgeLeft=5 +TitleEdgeRight=5 +TitleEdgeTopMaximized=0 +TitleEdgeBottomMaximized=0 +TitleEdgeLeftMaximized=0 +TitleEdgeRightMaximized=0 +TitleBorderLeft=5 +TitleBorderRight=5 +TitleHeight=20 +ButtonWidth=20 +ButtonWidthMinimize=? # optional - default depends on ButtonWidth +ButtonWidthMaximizeRestore=? # optional - default depends on ButtonWidth +ButtonWidthClose=? # optional - default depends on ButtonWidth +ButtonWidthAlldesktops=? # optional - default depends on ButtonWidth +ButtonWidthKeepabove=? # optional - default depends on ButtonWidth +ButtonWidthKeepbelow=? # optional - default depends on ButtonWidth +ButtonWidthShade=? # optional - default depends on ButtonWidth +ButtonWidthHelp=? # optional - default depends on ButtonWidth +ButtonWidthMenu=? # optional - default depends on ButtonWidth +ButtonHeight=20 +ButtonSpacing=5 +ButtonMarginTop=0 +ExplicitButtonSpacer=10 +PaddingTop=0 # Padding added to provide shadows +PaddingBottom=0 # Padding added to provide shadows +PaddingRight=0 # Padding added to provide shadows +PaddingLeft=0 # Padding added to provide shadows + +Packaging +========= +All theme files (decoration, buttons, metadata.desktop and configuration file) have to be stored in +one directory with the name of the theme (this has to be identical to the one used for the config +file). You have to create a tar.gz archive from that directory. This archive is the theme, which +can be installed in the kcm for window decorations. diff --git a/plugins/kdecorations/aurorae/themes/CMakeLists.txt b/plugins/kdecorations/aurorae/themes/CMakeLists.txt new file mode 100644 index 0000000..286310f --- /dev/null +++ b/plugins/kdecorations/aurorae/themes/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(plastik) diff --git a/plugins/kdecorations/aurorae/themes/plastik/CMakeLists.txt b/plugins/kdecorations/aurorae/themes/plastik/CMakeLists.txt new file mode 100644 index 0000000..2699d64 --- /dev/null +++ b/plugins/kdecorations/aurorae/themes/plastik/CMakeLists.txt @@ -0,0 +1,10 @@ +add_subdirectory(code) + +install(DIRECTORY package/ + DESTINATION ${DATA_INSTALL_DIR}/${KWIN_NAME}/decorations/kwin4_decoration_qml_plastik) + +install(FILES package/metadata.desktop + DESTINATION ${SERVICES_INSTALL_DIR}/${KWIN_NAME} + RENAME kwin4_decoration_qml_plastik.desktop) + +file(COPY package/ DESTINATION ${CMAKE_BINARY_DIR}/bin/kwin/decorations/kwin4_decoration_qml_plastik) diff --git a/plugins/kdecorations/aurorae/themes/plastik/code/CMakeLists.txt b/plugins/kdecorations/aurorae/themes/plastik/code/CMakeLists.txt new file mode 100644 index 0000000..5463539 --- /dev/null +++ b/plugins/kdecorations/aurorae/themes/plastik/code/CMakeLists.txt @@ -0,0 +1,11 @@ +set(plastik_plugin_SRCS + plastikbutton.cpp + plastikplugin.cpp +) + +add_library(plastikplugin SHARED ${plastik_plugin_SRCS}) +set_target_properties(plastikplugin PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/org/kde/kwin/decorations/plastik") +target_link_libraries(plastikplugin Qt5::Core Qt5::Quick KF5::ConfigWidgets) +install(TARGETS plastikplugin DESTINATION ${QML_INSTALL_DIR}/org/kde/kwin/decorations/plastik) +install(FILES qmldir DESTINATION ${QML_INSTALL_DIR}/org/kde/kwin/decorations/plastik) +file(COPY qmldir DESTINATION ${CMAKE_BINARY_DIR}/bin/org/kde/kwin/decorations/plastik) diff --git a/plugins/kdecorations/aurorae/themes/plastik/code/plastikbutton.cpp b/plugins/kdecorations/aurorae/themes/plastik/code/plastikbutton.cpp new file mode 100644 index 0000000..56c21bd --- /dev/null +++ b/plugins/kdecorations/aurorae/themes/plastik/code/plastikbutton.cpp @@ -0,0 +1,459 @@ +/* + SPDX-FileCopyrightText: 2012 Martin Gräßlin + SPDX-FileCopyrightText: 2003-2005 Sandro Giessl + + based on the window decoration "Web": + SPDX-FileCopyrightText: 2001 Rik Hemsley (rikkus) + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "plastikbutton.h" +#include +#include +#include +#include + +namespace KWin +{ + +PlastikButtonProvider::PlastikButtonProvider() + : QQuickImageProvider(Pixmap) +{ +} + +QPixmap PlastikButtonProvider::requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) +{ + int origSize = requestedSize.isValid() ? qMin(requestedSize.width(), requestedSize.height()) : 10; + if (size) { + *size = QSize(origSize, origSize); + } + QStringList idParts = id.split(QStringLiteral("/")); + if (idParts.isEmpty()) { + // incorrect id + return QQuickImageProvider::requestPixmap(id, size, requestedSize); + } + bool active = false; + bool toggled = false; + bool shadow = false; + if (idParts.length() > 1 && idParts.at(1) == QStringLiteral("true")) { + active = true; + } + if (idParts.length() > 2 && idParts.at(2) == QStringLiteral("true")) { + toggled = true; + } + if (idParts.length() > 3 && idParts.at(3) == QStringLiteral("true")) { + shadow = true; + } + ButtonIcon button; + switch (static_cast(idParts[0].toInt())) { + case DecorationButtonClose: + button = CloseIcon; + break; + case DecorationButtonMaximizeRestore: + if (toggled) { + button = MaxRestoreIcon; + } else { + button = MaxIcon; + } + break; + case DecorationButtonMinimize: + button = MinIcon; + break; + case DecorationButtonQuickHelp: + button = HelpIcon; + break; + case DecorationButtonOnAllDesktops: + if (toggled) { + button = NotOnAllDesktopsIcon; + } else { + button = OnAllDesktopsIcon; + } + break; + case DecorationButtonKeepAbove: + if (toggled) { + button = NoKeepAboveIcon; + } else { + button = KeepAboveIcon; + } + break; + case DecorationButtonKeepBelow: + if (toggled) { + button = NoKeepBelowIcon; + } else { + button = KeepBelowIcon; + } + break; + case DecorationButtonShade: + if (toggled) { + button = UnShadeIcon; + } else { + button = ShadeIcon; + } + break; + case DecorationButtonApplicationMenu: + button = AppMenuIcon; + break; + default: + // not recognized icon + return QQuickImageProvider::requestPixmap(id, size, requestedSize); + } + return icon(button, origSize, active, shadow); +} + +QPixmap PlastikButtonProvider::icon(ButtonIcon icon, int size, bool active, bool shadow) +{ + Q_UNUSED(active); + if (size%2 == 0) + --size; + + QPixmap image(size, size); + image.fill(Qt::transparent); + QPainter p(&image); + KConfigGroup wmConfig(KSharedConfig::openConfig(QStringLiteral("kdeglobals")), QStringLiteral("WM")); + const QColor color = wmConfig.readEntry("activeForeground", QPalette().color(QPalette::Active, QPalette::HighlightedText)); + + if (shadow) { + p.setPen(KColorScheme::shade(color, KColorScheme::ShadowShade)); + } else { + p.setPen(color); + } + + const QRect r = image.rect(); + + // line widths + int lwTitleBar = 1; + if (r.width() > 16) { + lwTitleBar = 4; + } else if (r.width() > 4) { + lwTitleBar = 2; + } + int lwArrow = 1; + if (r.width() > 16) { + lwArrow = 4; + } else if (r.width() > 7) { + lwArrow = 2; + } + + switch(icon) { + case CloseIcon: + { + int lineWidth = 1; + if (r.width() > 16) { + lineWidth = 3; + } else if (r.width() > 4) { + lineWidth = 2; + } + + drawObject(p, DiagonalLine, r.x(), r.y(), r.width(), lineWidth); + drawObject(p, CrossDiagonalLine, r.x(), r.bottom(), r.width(), lineWidth); + + break; + } + + case MaxIcon: + { + int lineWidth2 = 1; // frame + if (r.width() > 16) { + lineWidth2 = 2; + } else if (r.width() > 4) { + lineWidth2 = 1; + } + + drawObject(p, HorizontalLine, r.x(), r.top(), r.width(), lwTitleBar); + drawObject(p, HorizontalLine, r.x(), r.bottom()-(lineWidth2-1), r.width(), lineWidth2); + drawObject(p, VerticalLine, r.x(), r.top(), r.height(), lineWidth2); + drawObject(p, VerticalLine, r.right()-(lineWidth2-1), r.top(), r.height(), lineWidth2); + + break; + } + + case MaxRestoreIcon: + { + int lineWidth2 = 1; // frame + if (r.width() > 16) { + lineWidth2 = 2; + } else if (r.width() > 4) { + lineWidth2 = 1; + } + + int margin1, margin2; + margin1 = margin2 = lineWidth2*2; + if (r.width() < 8) + margin1 = 1; + + // background window + drawObject(p, HorizontalLine, r.x()+margin1, r.top(), r.width()-margin1, lineWidth2); + drawObject(p, HorizontalLine, r.right()-margin2, r.bottom()-(lineWidth2-1)-margin1, margin2, lineWidth2); + drawObject(p, VerticalLine, r.x()+margin1, r.top(), margin2, lineWidth2); + drawObject(p, VerticalLine, r.right()-(lineWidth2-1), r.top(), r.height()-margin1, lineWidth2); + + // foreground window + drawObject(p, HorizontalLine, r.x(), r.top()+margin2, r.width()-margin2, lwTitleBar); + drawObject(p, HorizontalLine, r.x(), r.bottom()-(lineWidth2-1), r.width()-margin2, lineWidth2); + drawObject(p, VerticalLine, r.x(), r.top()+margin2, r.height(), lineWidth2); + drawObject(p, VerticalLine, r.right()-(lineWidth2-1)-margin2, r.top()+margin2, r.height(), lineWidth2); + + break; + } + + case MinIcon: + { + drawObject(p, HorizontalLine, r.x(), r.bottom()-(lwTitleBar-1), r.width(), lwTitleBar); + + break; + } + + case HelpIcon: + { + int center = r.x()+r.width()/2 -1; + int side = r.width()/4; + + // paint a question mark... code is quite messy, to be cleaned up later...! :o + + if (r.width() > 16) { + int lineWidth = 3; + + // top bar + drawObject(p, HorizontalLine, center-side+3, r.y(), 2*side-3-1, lineWidth); + // top bar rounding + drawObject(p, CrossDiagonalLine, center-side-1, r.y()+5, 6, lineWidth); + drawObject(p, DiagonalLine, center+side-3, r.y(), 5, lineWidth); + // right bar + drawObject(p, VerticalLine, center+side+2-lineWidth, r.y()+3, r.height()-(2*lineWidth+side+2+1), lineWidth); + // bottom bar + drawObject(p, CrossDiagonalLine, center, r.bottom()-2*lineWidth, side+2, lineWidth); + drawObject(p, HorizontalLine, center, r.bottom()-3*lineWidth+2, lineWidth, lineWidth); + // the dot + drawObject(p, HorizontalLine, center, r.bottom()-(lineWidth-1), lineWidth, lineWidth); + } else if (r.width() > 8) { + int lineWidth = 2; + + // top bar + drawObject(p, HorizontalLine, center-(side-1), r.y(), 2*side-1, lineWidth); + // top bar rounding + if (r.width() > 9) { + drawObject(p, CrossDiagonalLine, center-side-1, r.y()+3, 3, lineWidth); + } else { + drawObject(p, CrossDiagonalLine, center-side-1, r.y()+2, 3, lineWidth); + } + drawObject(p, DiagonalLine, center+side-1, r.y(), 3, lineWidth); + // right bar + drawObject(p, VerticalLine, center+side+2-lineWidth, r.y()+2, r.height()-(2*lineWidth+side+1), lineWidth); + // bottom bar + drawObject(p, CrossDiagonalLine, center, r.bottom()-2*lineWidth+1, side+2, lineWidth); + // the dot + drawObject(p, HorizontalLine, center, r.bottom()-(lineWidth-1), lineWidth, lineWidth); + } else { + int lineWidth = 1; + + // top bar + drawObject(p, HorizontalLine, center-(side-1), r.y(), 2*side, lineWidth); + // top bar rounding + drawObject(p, CrossDiagonalLine, center-side-1, r.y()+1, 2, lineWidth); + // right bar + drawObject(p, VerticalLine, center+side+1, r.y(), r.height()-(side+2+1), lineWidth); + // bottom bar + drawObject(p, CrossDiagonalLine, center, r.bottom()-2, side+2, lineWidth); + // the dot + drawObject(p, HorizontalLine, center, r.bottom(), 1, 1); + } + + break; + } + + case NotOnAllDesktopsIcon: + { + int lwMark = r.width()-lwTitleBar*2-2; + if (lwMark < 1) + lwMark = 3; + + drawObject(p, HorizontalLine, r.x()+(r.width()-lwMark)/2, r.y()+(r.height()-lwMark)/2, lwMark, lwMark); + + // Fall through to OnAllDesktopsIcon intended! + Q_FALLTHROUGH(); + } + case OnAllDesktopsIcon: + { + // horizontal bars + drawObject(p, HorizontalLine, r.x()+lwTitleBar, r.y(), r.width()-2*lwTitleBar, lwTitleBar); + drawObject(p, HorizontalLine, r.x()+lwTitleBar, r.bottom()-(lwTitleBar-1), r.width()-2*lwTitleBar, lwTitleBar); + // vertical bars + drawObject(p, VerticalLine, r.x(), r.y()+lwTitleBar, r.height()-2*lwTitleBar, lwTitleBar); + drawObject(p, VerticalLine, r.right()-(lwTitleBar-1), r.y()+lwTitleBar, r.height()-2*lwTitleBar, lwTitleBar); + + + break; + } + + case NoKeepAboveIcon: + { + int center = r.x()+r.width()/2; + + // arrow + drawObject(p, CrossDiagonalLine, r.x(), center+2*lwArrow, center-r.x(), lwArrow); + drawObject(p, DiagonalLine, r.x()+center, r.y()+1+2*lwArrow, center-r.x(), lwArrow); + if (lwArrow>1) + drawObject(p, HorizontalLine, center-(lwArrow-2), r.y()+2*lwArrow, (lwArrow-2)*2, lwArrow); + + // Fall through to KeepAboveIcon intended! + Q_FALLTHROUGH(); + } + case KeepAboveIcon: + { + int center = r.x()+r.width()/2; + + // arrow + drawObject(p, CrossDiagonalLine, r.x(), center, center-r.x(), lwArrow); + drawObject(p, DiagonalLine, r.x()+center, r.y()+1, center-r.x(), lwArrow); + if (lwArrow>1) + drawObject(p, HorizontalLine, center-(lwArrow-2), r.y(), (lwArrow-2)*2, lwArrow); + + break; + } + + case NoKeepBelowIcon: + { + int center = r.x()+r.width()/2; + + // arrow + drawObject(p, DiagonalLine, r.x(), center-2*lwArrow, center-r.x(), lwArrow); + drawObject(p, CrossDiagonalLine, r.x()+center, r.bottom()-1-2*lwArrow, center-r.x(), lwArrow); + if (lwArrow>1) + drawObject(p, HorizontalLine, center-(lwArrow-2), r.bottom()-(lwArrow-1)-2*lwArrow, (lwArrow-2)*2, lwArrow); + + // Fall through to KeepBelowIcon intended! + Q_FALLTHROUGH(); + } + case KeepBelowIcon: + { + int center = r.x()+r.width()/2; + + // arrow + drawObject(p, DiagonalLine, r.x(), center, center-r.x(), lwArrow); + drawObject(p, CrossDiagonalLine, r.x()+center, r.bottom()-1, center-r.x(), lwArrow); + if (lwArrow>1) + drawObject(p, HorizontalLine, center-(lwArrow-2), r.bottom()-(lwArrow-1), (lwArrow-2)*2, lwArrow); + + break; + } + + case ShadeIcon: + { + drawObject(p, HorizontalLine, r.x(), r.y(), r.width(), lwTitleBar); + + break; + } + + case UnShadeIcon: + { + int lw1 = 1; + int lw2 = 1; + if (r.width() > 16) { + lw1 = 4; + lw2 = 2; + } else if (r.width() > 7) { + lw1 = 2; + lw2 = 1; + } + + int h = qMax( (r.width()/2), (lw1+2*lw2) ); + + // horizontal bars + drawObject(p, HorizontalLine, r.x(), r.y(), r.width(), lw1); + drawObject(p, HorizontalLine, r.x(), r.x()+h-(lw2-1), r.width(), lw2); + // vertical bars + drawObject(p, VerticalLine, r.x(), r.y(), h, lw2); + drawObject(p, VerticalLine, r.right()-(lw2-1), r.y(), h, lw2); + + break; + } + case AppMenuIcon: + { + drawObject(p, HorizontalLine, r.x(), r.top()+(lwTitleBar-1), r.width(), lwTitleBar); + drawObject(p, HorizontalLine, r.x(), r.center().y(), r.width(), lwTitleBar); + drawObject(p, HorizontalLine, r.x(), r.bottom()-(lwTitleBar-1), r.width(), lwTitleBar); + break; + } + + default: + break; + } + + p.end(); + + return image; +} + +void PlastikButtonProvider::drawObject(QPainter &p, Object object, int x, int y, int length, int lineWidth) +{ + switch(object) { + case DiagonalLine: + if (lineWidth <= 1) { + for (int i = 0; i < length; ++i) { + p.drawPoint(x+i,y+i); + } + } else if (lineWidth <= 2) { + for (int i = 0; i < length; ++i) { + p.drawPoint(x+i,y+i); + } + for (int i = 0; i < (length-1); ++i) { + p.drawPoint(x+1+i,y+i); + p.drawPoint(x+i,y+1+i); + } + } else { + for (int i = 1; i < (length-1); ++i) { + p.drawPoint(x+i,y+i); + } + for (int i = 0; i < (length-1); ++i) { + p.drawPoint(x+1+i,y+i); + p.drawPoint(x+i,y+1+i); + } + for (int i = 0; i < (length-2); ++i) { + p.drawPoint(x+2+i,y+i); + p.drawPoint(x+i,y+2+i); + } + } + break; + case CrossDiagonalLine: + if (lineWidth <= 1) { + for (int i = 0; i < length; ++i) { + p.drawPoint(x+i,y-i); + } + } else if (lineWidth <= 2) { + for (int i = 0; i < length; ++i) { + p.drawPoint(x+i,y-i); + } + for (int i = 0; i < (length-1); ++i) { + p.drawPoint(x+1+i,y-i); + p.drawPoint(x+i,y-1-i); + } + } else { + for (int i = 1; i < (length-1); ++i) { + p.drawPoint(x+i,y-i); + } + for (int i = 0; i < (length-1); ++i) { + p.drawPoint(x+1+i,y-i); + p.drawPoint(x+i,y-1-i); + } + for (int i = 0; i < (length-2); ++i) { + p.drawPoint(x+2+i,y-i); + p.drawPoint(x+i,y-2-i); + } + } + break; + case HorizontalLine: + for (int i = 0; i < lineWidth; ++i) { + p.drawLine(x,y+i, x+length-1, y+i); + } + break; + case VerticalLine: + for (int i = 0; i < lineWidth; ++i) { + p.drawLine(x+i,y, x+i, y+length-1); + } + break; + default: + break; + } +} + +} // namespace diff --git a/plugins/kdecorations/aurorae/themes/plastik/code/plastikbutton.h b/plugins/kdecorations/aurorae/themes/plastik/code/plastikbutton.h new file mode 100644 index 0000000..046f359 --- /dev/null +++ b/plugins/kdecorations/aurorae/themes/plastik/code/plastikbutton.h @@ -0,0 +1,73 @@ +/* + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_PLASTIK_BUTTON_H +#define KWIN_PLASTIK_BUTTON_H + +#include + +namespace KWin +{ + +class PlastikButtonProvider : public QQuickImageProvider +{ +public: + explicit PlastikButtonProvider(); + QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) override; + +private: + enum ButtonIcon { + CloseIcon = 0, + MaxIcon, + MaxRestoreIcon, + MinIcon, + HelpIcon, + OnAllDesktopsIcon, + NotOnAllDesktopsIcon, + KeepAboveIcon, + NoKeepAboveIcon, + KeepBelowIcon, + NoKeepBelowIcon, + ShadeIcon, + UnShadeIcon, + AppMenuIcon, + NumButtonIcons + }; + enum Object { + HorizontalLine, + VerticalLine, + DiagonalLine, + CrossDiagonalLine + }; + enum DecorationButton { + /** + * Invalid button value. A decoration should not create a button for + * this type. + */ + DecorationButtonNone, + DecorationButtonMenu, + DecorationButtonApplicationMenu, + DecorationButtonOnAllDesktops, + DecorationButtonQuickHelp, + DecorationButtonMinimize, + DecorationButtonMaximizeRestore, + DecorationButtonClose, + DecorationButtonKeepAbove, + DecorationButtonKeepBelow, + DecorationButtonShade, + DecorationButtonResize, + /** + * The decoration should create an empty spacer instead of a button for + * this type. + */ + DecorationButtonExplicitSpacer + }; + QPixmap icon(ButtonIcon icon, int size, bool active, bool shadow); + void drawObject(QPainter &p, Object object, int x, int y, int length, int lineWidth); +}; + +} // namespace + +#endif // KWIN_PLASTIK_BUTTON_H diff --git a/plugins/kdecorations/aurorae/themes/plastik/code/plastikplugin.cpp b/plugins/kdecorations/aurorae/themes/plastik/code/plastikplugin.cpp new file mode 100644 index 0000000..4074779 --- /dev/null +++ b/plugins/kdecorations/aurorae/themes/plastik/code/plastikplugin.cpp @@ -0,0 +1,21 @@ +/* + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "plastikplugin.h" +#include "plastikbutton.h" +#include + +void PlastikPlugin::registerTypes(const char *uri) +{ + // Need to register something to tell Qt that it loaded (QTBUG-84571) + qmlRegisterModule(uri, 1, 0); +} + +void PlastikPlugin::initializeEngine(QQmlEngine *engine, const char *uri) +{ + Q_ASSERT(QLatin1String(uri) == QLatin1String("org.kde.kwin.decorations.plastik")); + engine->addImageProvider(QLatin1String("plastik"), new KWin::PlastikButtonProvider()); + QQmlExtensionPlugin::initializeEngine(engine, uri); +} diff --git a/plugins/kdecorations/aurorae/themes/plastik/code/plastikplugin.h b/plugins/kdecorations/aurorae/themes/plastik/code/plastikplugin.h new file mode 100644 index 0000000..4f28cd5 --- /dev/null +++ b/plugins/kdecorations/aurorae/themes/plastik/code/plastikplugin.h @@ -0,0 +1,20 @@ +/* + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef PLASTIK_PLUGIN_H +#define PLASTIK_PLUGIN_H + +#include + +class PlastikPlugin : public QQmlExtensionPlugin +{ + Q_PLUGIN_METADATA(IID "org.kde.kwin.decorations.plastik") + Q_OBJECT +public: + void registerTypes(const char *uri) override; + void initializeEngine(QQmlEngine *engine, const char *uri) override; +}; + +#endif // PLASTIK_PLUGIN_H diff --git a/plugins/kdecorations/aurorae/themes/plastik/code/qmldir b/plugins/kdecorations/aurorae/themes/plastik/code/qmldir new file mode 100644 index 0000000..aa7bf16 --- /dev/null +++ b/plugins/kdecorations/aurorae/themes/plastik/code/qmldir @@ -0,0 +1,2 @@ +module org.kde.kwin.decorations.plastik +plugin plastikplugin diff --git a/plugins/kdecorations/aurorae/themes/plastik/package/contents/config/main.xml b/plugins/kdecorations/aurorae/themes/plastik/package/contents/config/main.xml new file mode 100644 index 0000000..2054553 --- /dev/null +++ b/plugins/kdecorations/aurorae/themes/plastik/package/contents/config/main.xml @@ -0,0 +1,25 @@ + + + + + + + true + + + false + + + false + + + true + + + true + + + diff --git a/plugins/kdecorations/aurorae/themes/plastik/package/contents/ui/PlastikButton.qml b/plugins/kdecorations/aurorae/themes/plastik/package/contents/ui/PlastikButton.qml new file mode 100644 index 0000000..4eb7b0e --- /dev/null +++ b/plugins/kdecorations/aurorae/themes/plastik/package/contents/ui/PlastikButton.qml @@ -0,0 +1,149 @@ +/* + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +import QtQuick 2.0 +import org.kde.kwin.decoration 0.1 +import org.kde.kwin.decorations.plastik 1.0 + +DecorationButton { + id: button + function colorize() { + var highlightColor = null; + if (button.pressed) { + if (button.buttonType == DecorationOptions.DecorationButtonClose) { + highlightColor = colorHelper.foreground(decoration.client.active, ColorHelper.NegativeText); + } else { + highlightColor = options.titleBarColor; + } + highlightColor = colorHelper.shade(highlightColor, ColorHelper.ShadowShade); + highlightColor = colorHelper.multiplyAlpha(highlightColor, 0.3); + } else if (button.hovered) { + if (button.buttonType == DecorationOptions.DecorationButtonClose) { + highlightColor = colorHelper.foreground(decoration.client.active, ColorHelper.NegativeText); + } else { + highlightColor = options.titleBarColor; + } + highlightColor = colorHelper.shade(highlightColor, ColorHelper.LightShade, Math.min(1.0, colorHelper.contrast + 0.4)); + highlightColor = colorHelper.multiplyAlpha(highlightColor, 0.6); + } + if (highlightColor) { + button.surfaceTop = Qt.tint(button.baseSurfaceTop, highlightColor); + button.surfaceBottom = Qt.tint(button.baseSurfaceBottom, highlightColor); + highlightColor = colorHelper.multiplyAlpha(highlightColor, 0.4); + button.conturTop = Qt.tint(button.baseConturTop, highlightColor); + button.conturBottom = Qt.tint(button.baseConturBottom, highlightColor); + } else { + button.conturTop = button.baseConturTop; + button.conturBottom = button.baseConturBottom; + button.surfaceTop = button.baseSurfaceTop; + button.surfaceBottom = button.baseSurfaceBottom; + } + } + property real size + property color conturTop + property color conturBottom + property color surfaceTop + property color surfaceBottom + property color baseConturTop: colorHelper.shade(options.titleBarColor, ColorHelper.DarkShade, colorHelper.contrast - 0.4) + property color baseConturBottom: colorHelper.shade(options.titleBarColor, ColorHelper.MidShade) + property color baseSurfaceTop: colorHelper.shade(options.titleBarColor, ColorHelper.MidlightShade, colorHelper.contrast - 0.4) + property color baseSurfaceBottom: colorHelper.shade(options.titleBarColor, ColorHelper.LightShade, colorHelper.contrast - 0.4) + Behavior on conturTop { + ColorAnimation { duration: root.animateButtons ? root.animationDuration : 0 } + } + Behavior on conturBottom { + ColorAnimation { duration: root.animateButtons ? root.animationDuration : 0 } + } + Behavior on surfaceTop { + ColorAnimation { duration: root.animateButtons ? root.animationDuration : 0 } + } + Behavior on surfaceBottom { + ColorAnimation { duration: root.animateButtons ? root.animationDuration : 0 } + } + width: size + height: size + Rectangle { + radius: 2 + smooth: true + anchors.fill: parent + gradient: Gradient { + GradientStop { + position: 0.0 + color: button.conturTop + } + GradientStop { + position: 1.0 + color: button.conturBottom + } + } + Rectangle { + radius: 2 + smooth: true + anchors { + fill: parent + leftMargin: 1 + rightMargin: 1 + topMargin: 1 + bottomMargin: 1 + } + gradient: Gradient { + GradientStop { + position: 0.0 + color: button.surfaceTop + } + GradientStop { + position: 1.0 + color: button.surfaceBottom + } + } + } + } + Item { + property int imageWidth: button.width > 14 ? button.width - 2 * Math.floor(button.width/3.5) : button.width - 6 + property int imageHeight: button.height > 14 ? button.height - 2 * Math.floor(button.height/3.5) : button.height - 6 + property string source: "image://plastik/" + button.buttonType + "/" + decoration.client.active + "/" + ((buttonType == DecorationOptions.DecorationButtonMaximizeRestore) ? decoration.client.maximized : button.toggled) + anchors.fill: parent + Image { + id: shadowImage + x: button.x + button.width / 2 - width / 2 + 1 + y: button.y + button.height / 2 - height / 2 + 1 + source: parent.source + "/true" + width: parent.imageWidth + height: parent.imageHeight + sourceSize.width: width + sourceSize.height: height + visible: !button.pressed + } + Image { + id: image + x: button.x + button.width / 2 - width / 2 + y: button.y + button.height / 2 - height / 2 + (button.pressed ? 1 : 0) + source: parent.source + width: parent.imageWidth + height: parent.imageHeight + sourceSize.width: width + sourceSize.height: height + } + } + Component.onCompleted: { + colorize(); + if (buttonType == DecorationOptions.DecorationButtonQuickHelp) { + visible = Qt.binding(function() { return decoration.client.providesContextHelp}); + } + if (buttonType == DecorationOptions.DecorationButtonApplicationMenu) { + visible = Qt.binding(function() { return decoration.client.hasApplicationMenu; }); + } + } + onHoveredChanged: colorize() + onPressedChanged: colorize() + Connections { + target: decoration.client + onActiveChanged: button.colorize() + } + Connections { + target: options + onColorsChanged: button.colorize(); + } +} diff --git a/plugins/kdecorations/aurorae/themes/plastik/package/contents/ui/config.ui b/plugins/kdecorations/aurorae/themes/plastik/package/contents/ui/config.ui new file mode 100644 index 0000000..8052111 --- /dev/null +++ b/plugins/kdecorations/aurorae/themes/plastik/package/contents/ui/config.ui @@ -0,0 +1,88 @@ + + + PlastikConfigDialog + + + + 0 + 0 + 541 + 176 + + + + Config Dialog + + + + 0 + + + + + Title &Alignment + + + + + + Left + + + + + + + Center + + + + + + + Right + + + + + + + + + + Check this option if the window border should be painted in the titlebar color. Otherwise it will be painted in the background color. + + + Colored window border + + + + + + + + + + Check this option if you want the buttons to fade in when the mouse pointer hovers over them and fade out again when it moves away. + + + Animate buttons + + + + + + + + KButtonGroup + QGroupBox +
kbuttongroup.h
+ 1 +
+
+ + kcfg_animateButtons + + + +
diff --git a/plugins/kdecorations/aurorae/themes/plastik/package/contents/ui/main.qml b/plugins/kdecorations/aurorae/themes/plastik/package/contents/ui/main.qml new file mode 100644 index 0000000..b0d0886 --- /dev/null +++ b/plugins/kdecorations/aurorae/themes/plastik/package/contents/ui/main.qml @@ -0,0 +1,414 @@ +/* + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +import QtQuick 2.0 +import org.kde.kwin.decoration 0.1 +import org.kde.kwin.decorations.plastik 1.0 + +Decoration { + function readBorderSize() { + switch (borderSize) { + case DecorationOptions.BorderTiny: + borders.setBorders(3); + extendedBorders.setAllBorders(0); + break; + case DecorationOptions.BorderLarge: + borders.setBorders(8); + extendedBorders.setAllBorders(0); + break; + case DecorationOptions.BorderVeryLarge: + borders.setBorders(12); + extendedBorders.setAllBorders(0); + break; + case DecorationOptions.BorderHuge: + borders.setBorders(18); + extendedBorders.setAllBorders(0); + break; + case DecorationOptions.BorderVeryHuge: + borders.setBorders(27); + extendedBorders.setAllBorders(0); + break; + case DecorationOptions.BorderOversized: + borders.setBorders(40); + extendedBorders.setAllBorders(0); + break; + case DecorationOptions.BorderNoSides: + borders.setBorders(4); + borders.setSideBorders(1); + extendedBorders.setSideBorders(3); + break; + case DecorationOptions.BorderNone: + borders.setBorders(1); + extendedBorders.setBorders(3); + break; + case DecorationOptions.BorderNormal: // fall through to default + default: + borders.setBorders(4); + extendedBorders.setAllBorders(0); + break; + } + } + function readConfig() { + var titleAlignLeft = decoration.readConfig("titleAlignLeft", true); + var titleAlignCenter = decoration.readConfig("titleAlignCenter", false); + var titleAlignRight = decoration.readConfig("titleAlignRight", false); + if (titleAlignRight) { + root.titleAlignment = Text.AlignRight; + } else if (titleAlignCenter) { + root.titleAlignment = Text.AlignHCenter; + } else { + if (!titleAlignLeft) { + console.log("Error reading title alignment: all alignment options are false"); + } + root.titleAlignment = Text.AlignLeft; + } + root.animateButtons = decoration.readConfig("animateButtons", true); + if (decoration.animationsSupported) { + root.animationDuration = 150; + root.animateButtons = false; + } + } + ColorHelper { + id: colorHelper + } + DecorationOptions { + id: options + deco: decoration + } + property int borderSize: decorationSettings.borderSize + property alias buttonSize: titleRow.captionHeight + property alias titleAlignment: caption.horizontalAlignment + property color titleBarColor: options.titleBarColor + // set by readConfig after Component completed, ensures that buttons do not flicker + property int animationDuration: 0 + property bool animateButtons: true + Behavior on titleBarColor { + ColorAnimation { + duration: root.animationDuration + } + } + id: root + alpha: false + Rectangle { + color: root.titleBarColor + anchors { + fill: parent + } + border { + width: decoration.client.maximized ? 0 : 2 + color: colorHelper.shade(root.titleBarColor, ColorHelper.DarkShade) + } + Rectangle { + id: borderLeft + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + leftMargin: 1 + bottomMargin: 1 + topMargin: 1 + } + visible: !decoration.client.maximized + width: root.borders.left + color: root.titleBarColor + Rectangle { + width: 1 + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + } + color: colorHelper.shade(root.titleBarColor, ColorHelper.LightShade, colorHelper.contrast - (decoration.client.active ? 0.4 : 0.8)) + } + } + Rectangle { + id: borderRight + anchors { + right: parent.right + top: parent.top + bottom: parent.bottom + rightMargin: 1 + bottomMargin: 1 + topMargin: 1 + } + visible: !decoration.client.maximized + width: root.borders.right -1 + color: root.titleBarColor + Rectangle { + width: 1 + anchors { + bottom: parent.bottom + top: parent.top + right: parent.right + } + color: colorHelper.shade(root.titleBarColor, ColorHelper.DarkShade, colorHelper.contrast - (decoration.client.active ? 0.4 : 0.8)) + } + } + Rectangle { + id: borderBottom + anchors { + left: parent.right + right: parent.left + bottom: parent.bottom + leftMargin: 1 + rightMargin: 1 + } + height: root.borders.bottom + visible: !decoration.client.maximized + color: root.titleBarColor + Rectangle { + height: 1 + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + color: colorHelper.shade(root.titleBarColor, ColorHelper.DarkShade, colorHelper.contrast - (decoration.client.active ? 0.4 : 0.8)) + } + } + + Rectangle { + id: top + property int topMargin: 1 + property real normalHeight: titleRow.normalHeight + topMargin + 1 + property real maximizedHeight: titleRow.maximizedHeight + 1 + height: decoration.client.maximized ? maximizedHeight : normalHeight + anchors { + left: parent.left + right: parent.right + top: parent.top + topMargin: decoration.client.maximized ? 0 : top.topMargin + leftMargin: decoration.client.maximized ? 0 : 2 + rightMargin: decoration.client.maximized ? 0 : 2 + } + gradient: Gradient { + id: topGradient + GradientStop { + position: 0.0 + color: colorHelper.shade(root.titleBarColor, ColorHelper.MidlightShade, colorHelper.contrast - 0.4) + } + GradientStop { + id: middleGradientStop + position: 4.0/(decoration.client.maximized ? top.maximizedHeight : top.normalHeight) + color: colorHelper.shade(root.titleBarColor, ColorHelper.MidShade, colorHelper.contrast - 0.4) + } + GradientStop { + position: 1.0 + color: root.titleBarColor + } + } + Rectangle { + height: 1 + anchors { + top: top.top + left: top.left + right: top.right + } + visible: !decoration.client.maximized + color: colorHelper.shade(root.titleBarColor, ColorHelper.LightShade, colorHelper.contrast - (decoration.client.active ? 0.4 : 0.8)) + } + + Item { + id: titleRow + property real captionHeight: caption.implicitHeight + 4 + property int topMargin: 3 + property int bottomMargin: 1 + property real normalHeight: captionHeight + bottomMargin + topMargin + property real maximizedHeight: captionHeight + bottomMargin + anchors { + left: parent.left + right: parent.right + top: parent.top + topMargin: decoration.client.maximized ? 0 : titleRow.topMargin + leftMargin: decoration.client.maximized ? 0 : 3 + rightMargin: decoration.client.maximized ? 0 : 3 + bottomMargin: titleRow.bottomMargin + } + ButtonGroup { + id: leftButtonGroup + spacing: 1 + explicitSpacer: root.buttonSize + menuButton: menuButtonComponent + appMenuButton: appMenuButtonComponent + minimizeButton: minimizeButtonComponent + maximizeButton: maximizeButtonComponent + keepBelowButton: keepBelowButtonComponent + keepAboveButton: keepAboveButtonComponent + helpButton: helpButtonComponent + shadeButton: shadeButtonComponent + allDesktopsButton: stickyButtonComponent + closeButton: closeButtonComponent + buttons: options.titleButtonsLeft + anchors { + top: parent.top + left: parent.left + } + } + Text { + id: caption + textFormat: Text.PlainText + anchors { + top: parent.top + left: leftButtonGroup.right + right: rightButtonGroup.left + rightMargin: 5 + leftMargin: 5 + topMargin: 3 + } + color: options.fontColor + Behavior on color { + ColorAnimation { duration: root.animationDuration } + } + text: decoration.client.caption + font: options.titleFont + elide: Text.ElideMiddle + renderType: Text.NativeRendering + } + ButtonGroup { + id: rightButtonGroup + spacing: 1 + explicitSpacer: root.buttonSize + menuButton: menuButtonComponent + appMenuButton: appMenuButtonComponent + minimizeButton: minimizeButtonComponent + maximizeButton: maximizeButtonComponent + keepBelowButton: keepBelowButtonComponent + keepAboveButton: keepAboveButtonComponent + helpButton: helpButtonComponent + shadeButton: shadeButtonComponent + allDesktopsButton: stickyButtonComponent + closeButton: closeButtonComponent + buttons: options.titleButtonsRight + anchors { + top: parent.top + right: parent.right + } + } + Component.onCompleted: { + decoration.installTitleItem(titleRow); + } + } + } + + Item { + id: innerBorder + anchors.fill: parent + + Rectangle { + anchors { + left: parent.left + right: parent.right + } + height: 1 + y: top.height - 1 + visible: decoration.client.maximized + color: colorHelper.shade(root.titleBarColor, ColorHelper.MidShade) + } + + Rectangle { + anchors { + fill: parent + leftMargin: root.borders.left - 1 + rightMargin: root.borders.right + topMargin: root.borders.top - 1 + bottomMargin: root.borders.bottom + } + border { + width: 1 + color: colorHelper.shade(root.titleBarColor, ColorHelper.MidShade) + } + visible: !decoration.client.maximized + color: root.titleBarColor + } + } + } + + Component { + id: maximizeButtonComponent + PlastikButton { + objectName: "maximizeButton" + buttonType: DecorationOptions.DecorationButtonMaximizeRestore + size: root.buttonSize + } + } + Component { + id: keepBelowButtonComponent + PlastikButton { + buttonType: DecorationOptions.DecorationButtonKeepBelow + size: root.buttonSize + } + } + Component { + id: keepAboveButtonComponent + PlastikButton { + buttonType: DecorationOptions.DecorationButtonKeepAbove + size: root.buttonSize + } + } + Component { + id: helpButtonComponent + PlastikButton { + buttonType: DecorationOptions.DecorationButtonQuickHelp + size: root.buttonSize + } + } + Component { + id: minimizeButtonComponent + PlastikButton { + buttonType: DecorationOptions.DecorationButtonMinimize + size: root.buttonSize + } + } + Component { + id: shadeButtonComponent + PlastikButton { + buttonType: DecorationOptions.DecorationButtonShade + size: root.buttonSize + } + } + Component { + id: stickyButtonComponent + PlastikButton { + buttonType: DecorationOptions.DecorationButtonOnAllDesktops + size: root.buttonSize + } + } + Component { + id: closeButtonComponent + PlastikButton { + buttonType: DecorationOptions.DecorationButtonClose + size: root.buttonSize + } + } + Component { + id: menuButtonComponent + MenuButton { + width: root.buttonSize + height: root.buttonSize + } + } + Component { + id: appMenuButtonComponent + PlastikButton { + buttonType: DecorationOptions.DecorationButtonApplicationMenu + size: root.buttonSize + } + } + Component.onCompleted: { + borders.setBorders(4); + borders.setTitle(top.normalHeight); + maximizedBorders.setTitle(top.maximizedHeight); + readBorderSize(); + readConfig(); + } + Connections { + target: decoration + onConfigChanged: root.readConfig() + } + Connections { + target: decorationSettings + onBorderSizeChanged: root.readBorderSize(); + } +} diff --git a/plugins/kdecorations/aurorae/themes/plastik/package/metadata.desktop b/plugins/kdecorations/aurorae/themes/plastik/package/metadata.desktop new file mode 100644 index 0000000..ca2572d --- /dev/null +++ b/plugins/kdecorations/aurorae/themes/plastik/package/metadata.desktop @@ -0,0 +1,151 @@ +[Desktop Entry] +Name=Plastik +Name[af]=Plastiek +Name[ar]=بلاستك +Name[az]=Plastik +Name[be]=Plastik +Name[be@latin]=Plastik +Name[bg]=Пластик +Name[bn]=প্লাস্টিক +Name[bn_IN]=Plastik (প্লাস্টিক) +Name[br]=Plastik +Name[bs]=Plastika +Name[ca]=Plastik +Name[ca@valencia]=Plastik +Name[cs]=Plastik +Name[csb]=Plastik +Name[cy]=Plastik +Name[da]=Plastik +Name[de]=Plastik +Name[el]=Plastik +Name[en_GB]=Plastik +Name[eo]=Plastik +Name[es]=Plastik +Name[et]=Plastik +Name[eu]=Plastik +Name[fa]=پلاستیک +Name[fi]=Plastik +Name[fr]=Plastik +Name[fy]=Plastyk +Name[ga]=Plastik +Name[gl]=Plastik +Name[gu]=પ્લાસ્ટિક +Name[he]=פלסטיק +Name[hi]=प्लास्टिक +Name[hne]=प्लास्टिक +Name[hr]=Plastika +Name[hsb]=Plastik +Name[hu]=Plastik +Name[ia]=Plastik +Name[id]=Plastik +Name[is]=Plastik +Name[it]=Plastica +Name[ja]=Plastik +Name[ka]=Пластик +Name[kk]=Пластик +Name[km]=ប្ល៉ាស្ទិក +Name[kn]=ಪ್ಲಾಸ್ಟಿಕ್ +Name[ko]=Plastik +Name[ku]=Plastik +Name[lt]=Plastik +Name[lv]=Plastik +Name[mai]=प्लास्टिक +Name[mk]=Пластик +Name[ml]=പ്ലാസ്റ്റിക് +Name[mr]=प्लास्टिक +Name[ms]=Plastik +Name[nb]=Plastik +Name[nds]=Plastik +Name[ne]=प्लास्टिक +Name[nl]=Plastik +Name[nn]=Plastik +Name[pa]=ਪਲਾਸਟਿਕ +Name[pl]=Plastik +Name[pt]=Plastik +Name[pt_BR]=Plastik +Name[ro]=Plastik +Name[ru]=Пластик +Name[se]=Plastihkka +Name[si]=ප්ලාස්ටික් +Name[sk]=Plastik +Name[sl]=Plastik +Name[sr]=Пластика +Name[sr@ijekavian]=Пластика +Name[sr@ijekavianlatin]=Plastika +Name[sr@latin]=Plastika +Name[sv]=Plastik +Name[ta]=திட்டம் +Name[te]=ప్లాస్టిక్ +Name[th]=รูปแบบพลาสติก +Name[tr]=Plastik +Name[ug]=پىلاستىك +Name[uk]=Пластик +Name[uz]=Plastik +Name[uz@cyrillic]=Пластик +Name[vi]=Chất dẻo +Name[wa]=Plastike +Name[x-test]=xxPlastikxx +Name[zh_CN]=Plastik +Name[zh_TW]=Plastik +Comment=The classic theme known from KDE 3 +Comment[ar]=السّمة الكلاسيكيّة المعروفة من كدي 3 +Comment[ast]=El tema clásicu y conocíu de KDE 3 +Comment[az]=KDE 3 zamanından tanınan klassik mövzu +Comment[bs]=Klasična tema iz KDE 3 +Comment[ca]=El tema clàssic conegut des del KDE 3 +Comment[ca@valencia]=El tema clàssic conegut des de KDE 3 +Comment[cs]=Klasický motiv známý z KDE 3 +Comment[da]=Det klassiske tema som er kendt fra KDE 3 +Comment[de]=Das klassische Design aus KDE 3 +Comment[el]=Το γνωστό κλασικό θέμα από το KDE 3 +Comment[en_GB]=The classic theme known from KDE 3 +Comment[es]=El tema clásico conocido desde KDE 3 +Comment[et]=Klassikaline KDE 3 ajast tuntud teema +Comment[eu]=KDE 3gatik ezaguna den gai klasikoa +Comment[fi]=KDE 3:sta tuttu klassinen teema +Comment[fr]=Le thème classique connu depuis KDE 3 +Comment[gl]=O tema clásico de KDE 3 +Comment[he]=הערכה הקלאסית של KDE 3 +Comment[hu]=A KDE 3-ból ismert klasszikus téma +Comment[ia]=Le thema classic cognoscite ex KDE 3 +Comment[id]=Tema klasik yang dikenal dari KDE 3 +Comment[it]=Il tema classico conosciuto da KDE 3 +Comment[ja]=KDE 3 からのクラシックテーマ +Comment[kk]=KDE3-тің классикалық нақышы +Comment[ko]=KDE 3의 고전 테마 +Comment[lt]=Klasikinis apipavidalinimas, žinomas iÅ¡ KDE 3 +Comment[mr]=केडीई 3 मधील क्लासिक शैली +Comment[nb]=Det klassiske temaet kjent fra KDE 3 +Comment[nds]=Dat klass'sche Muster ut KDE 3 +Comment[nl]=Het klassieke thema bekend van KDE 3 +Comment[nn]=Klassisk KDE 3-tema +Comment[pl]=Klasyczny wystój znany z KDE 3 +Comment[pt]=O tema clássico conhecido do KDE 3 +Comment[pt_BR]=Tema clássico do KDE 3 +Comment[ro]=Tematica clasică cunoscută din KDE 3 +Comment[ru]=Классическая тема, известная со времён KDE 3. +Comment[sk]=Klasická téma známa z KDE 3 +Comment[sl]=Klasična tema, znana iz KDE 3 +Comment[sr]=Класична тема позната из КДЕ‑а 3 +Comment[sr@ijekavian]=Класична тема позната из КДЕ‑а 3 +Comment[sr@ijekavianlatin]=Klasična tema poznata iz KDE‑a 3 +Comment[sr@latin]=Klasična tema poznata iz KDE‑a 3 +Comment[sv]=Det klassiska temat känt frÃ¥n KDE 3 +Comment[tr]=KDE 3'ten bilinen alışılmış tema +Comment[uk]=Класична тема, відома з часів KDE 3 +Comment[vi]=Sắc thái cổ điển được biết đến từ KDE 3 +Comment[x-test]=xxThe classic theme known from KDE 3xx +Comment[zh_CN]=KDE 3 时代的经典主题 +Comment[zh_TW]=從 KDE3 以來的傳統主題 + +X-Plasma-MainScript=ui/main.qml +X-KDE-PluginInfo-Author=Martin Gräßlin +X-KDE-PluginInfo-Email=mgraesslin@kde.org +X-KDE-PluginInfo-Name=kwin4_decoration_qml_plastik +X-KDE-PluginInfo-Version=1.0 + +X-KDE-PluginInfo-License=GPL +X-KDE-ServiceTypes=KWin/Decoration +X-KWin-Config-TranslationDomain=kwin_clients +Type=Service + diff --git a/plugins/kglobalaccel/CMakeLists.txt b/plugins/kglobalaccel/CMakeLists.txt new file mode 100644 index 0000000..96651f4 --- /dev/null +++ b/plugins/kglobalaccel/CMakeLists.txt @@ -0,0 +1,17 @@ +set(kglobalaccel_plugin_SRCS + kglobalaccel_plugin.cpp +) + +add_library(KF5GlobalAccelPrivateKWin MODULE ${kglobalaccel_plugin_SRCS}) +set_target_properties(KF5GlobalAccelPrivateKWin PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/org.kde.kglobalaccel5.platforms/") +target_link_libraries(KF5GlobalAccelPrivateKWin + KF5::GlobalAccelPrivate + kwin +) + +install( + TARGETS + KF5GlobalAccelPrivateKWin + DESTINATION + ${PLUGIN_INSTALL_DIR}/org.kde.kglobalaccel5.platforms/ +) diff --git a/plugins/kglobalaccel/kglobalaccel_plugin.cpp b/plugins/kglobalaccel/kglobalaccel_plugin.cpp new file mode 100644 index 0000000..b7f9dee --- /dev/null +++ b/plugins/kglobalaccel/kglobalaccel_plugin.cpp @@ -0,0 +1,47 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kglobalaccel_plugin.h" +#include "../../input.h" + +#include + +KGlobalAccelImpl::KGlobalAccelImpl(QObject *parent) + : KGlobalAccelInterface(parent) +{ +} + +KGlobalAccelImpl::~KGlobalAccelImpl() = default; + +bool KGlobalAccelImpl::grabKey(int key, bool grab) +{ + Q_UNUSED(key) + Q_UNUSED(grab) + return true; +} + +void KGlobalAccelImpl::setEnabled(bool enabled) +{ + if (m_shuttingDown) { + return; + } + static KWin::InputRedirection *s_input = KWin::InputRedirection::self(); + if (!s_input) { + qFatal("This plugin is intended to be used with KWin and this is not KWin, exiting now"); + } else { + if (!m_inputDestroyedConnection) { + m_inputDestroyedConnection = connect(s_input, &QObject::destroyed, this, [this] { m_shuttingDown = true; }); + } + } + s_input->registerGlobalAccel(enabled ? this : nullptr); +} + +bool KGlobalAccelImpl::checkKeyPressed(int keyQt) +{ + return keyPressed(keyQt); +} diff --git a/plugins/kglobalaccel/kglobalaccel_plugin.h b/plugins/kglobalaccel/kglobalaccel_plugin.h new file mode 100644 index 0000000..7ee1778 --- /dev/null +++ b/plugins/kglobalaccel/kglobalaccel_plugin.h @@ -0,0 +1,37 @@ +/* + 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 KGLOBALACCEL_PLUGIN_H +#define KGLOBALACCEL_PLUGIN_H + +#include + +#include + +class KGlobalAccelImpl : public KGlobalAccelInterface +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.kde.kglobalaccel5.KGlobalAccelInterface" FILE "kwin.json") + Q_INTERFACES(KGlobalAccelInterface) + +public: + KGlobalAccelImpl(QObject *parent = nullptr); + ~KGlobalAccelImpl() override; + + bool grabKey(int key, bool grab) override; + void setEnabled(bool) override; + +public Q_SLOTS: + bool checkKeyPressed(int keyQt); + +private: + bool m_shuttingDown = false; + QMetaObject::Connection m_inputDestroyedConnection; +}; + +#endif diff --git a/plugins/kglobalaccel/kwin.json b/plugins/kglobalaccel/kwin.json new file mode 100644 index 0000000..0a2fc35 --- /dev/null +++ b/plugins/kglobalaccel/kwin.json @@ -0,0 +1,3 @@ +{ + "platforms": ["org.kde.kwin"] +} diff --git a/plugins/kpackage/CMakeLists.txt b/plugins/kpackage/CMakeLists.txt new file mode 100644 index 0000000..7c3f5d1 --- /dev/null +++ b/plugins/kpackage/CMakeLists.txt @@ -0,0 +1,5 @@ +add_subdirectory(aurorae) +add_subdirectory(decoration) +add_subdirectory(effect) +add_subdirectory(scripts) +add_subdirectory(windowswitcher) diff --git a/plugins/kpackage/aurorae/CMakeLists.txt b/plugins/kpackage/aurorae/CMakeLists.txt new file mode 100644 index 0000000..6b1ef2b --- /dev/null +++ b/plugins/kpackage/aurorae/CMakeLists.txt @@ -0,0 +1,17 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"kwin_package_aurorae\") + +set(aurorae_SRCS + aurorae.cpp +) + +add_library(kwin_packagestructure_aurorae MODULE ${aurorae_SRCS}) + +target_link_libraries(kwin_packagestructure_aurorae + KF5::I18n + KF5::Package +) + +kcoreaddons_desktop_to_json(kwin_packagestructure_aurorae kwin-packagestructure-aurorae.desktop) + +install(TARGETS kwin_packagestructure_aurorae DESTINATION ${KDE_INSTALL_PLUGINDIR}/kpackage/packagestructure) + diff --git a/plugins/kpackage/aurorae/aurorae.cpp b/plugins/kpackage/aurorae/aurorae.cpp new file mode 100644 index 0000000..79cf2fb --- /dev/null +++ b/plugins/kpackage/aurorae/aurorae.cpp @@ -0,0 +1,72 @@ +/* + SPDX-FileCopyrightText: 2017 Demitrius Belai + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "aurorae.h" + +#include + +void AuroraePackage::initPackage(KPackage::Package *package) +{ + package->setContentsPrefixPaths(QStringList()); + package->setDefaultPackageRoot(QStringLiteral("aurorae/themes/")); + + package->addFileDefinition("decoration", QStringLiteral("decoration.svgz"), + i18n("Window Decoration")); + package->setRequired("decoration", true); + + package->addFileDefinition("close", QStringLiteral("close.svgz"), + i18n("Close Button")); + + package->addFileDefinition("minimize", QStringLiteral("minimize.svgz"), + i18n("Minimize Button")); + + package->addFileDefinition("maximize", QStringLiteral("maximize.svgz"), + i18n("Maximize Button")); + + package->addFileDefinition("restore", QStringLiteral("restore.svgz"), + i18n("Restore Button")); + + package->addFileDefinition("alldesktops", QStringLiteral("alldesktops.svgz"), + i18n("Sticky Button")); + + package->addFileDefinition("keepabove", QStringLiteral("keepabove.svgz"), + i18n("Keepabove Button")); + + package->addFileDefinition("keepbelow", QStringLiteral("keepbelow.svgz"), + i18n("Keepbelow Button")); + + package->addFileDefinition("shade", QStringLiteral("shade.svgz"), + i18n("Shade Button")); + + package->addFileDefinition("help", QStringLiteral("help.svgz"), + i18n("Help Button")); + + package->addFileDefinition("configrc", QStringLiteral("configrc"), + i18n("Configuration file")); + + QStringList mimetypes; + mimetypes << QStringLiteral("image/svg+xml-compressed"); + package->setDefaultMimeTypes(mimetypes); +} + +void AuroraePackage::pathChanged(KPackage::Package *package) +{ + if (package->path().isEmpty()) { + return; + } + + KPluginMetaData md(package->metadata().metaDataFileName()); + + if (!md.pluginId().isEmpty()) { + QString configrc = md.pluginId() + "rc"; + package->addFileDefinition("configrc", configrc, i18n("Configuration file")); + } +} + +K_EXPORT_KPACKAGE_PACKAGE_WITH_JSON(AuroraePackage, "kwin-packagestructure-aurorae.json") + +#include "aurorae.moc" + diff --git a/plugins/kpackage/aurorae/aurorae.h b/plugins/kpackage/aurorae/aurorae.h new file mode 100644 index 0000000..e5c1b1c --- /dev/null +++ b/plugins/kpackage/aurorae/aurorae.h @@ -0,0 +1,20 @@ +/* + SPDX-FileCopyrightText: 2017 Demitrius Belai + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef AURORAEPACKAGE_H +#define AURORAEPACKAGE_H + +#include + +class AuroraePackage : public KPackage::PackageStructure +{ +public: + AuroraePackage(QObject*, const QVariantList &) {} + void initPackage(KPackage::Package *package) override; + void pathChanged(KPackage::Package *package) override; +}; + +#endif diff --git a/plugins/kpackage/aurorae/kwin-packagestructure-aurorae.desktop b/plugins/kpackage/aurorae/kwin-packagestructure-aurorae.desktop new file mode 100644 index 0000000..4498929 --- /dev/null +++ b/plugins/kpackage/aurorae/kwin-packagestructure-aurorae.desktop @@ -0,0 +1,49 @@ +[Desktop Entry] +Name=KWin Aurorae +Name[az]=KWin Aurorae +Name[ca]=Aurorae del KWin +Name[ca@valencia]=Aurorae de KWin +Name[cs]=KWin Aurorae +Name[da]=KWin Aurorae +Name[de]=KWin Aurorae +Name[el]=KWin Aurorae +Name[en_GB]=KWin Aurorae +Name[es]=Aurorae de KWin +Name[et]=KWin Aurorae +Name[eu]=KWin Aurorae +Name[fi]=KWin Aurorae +Name[fr]=Module Aurorae de KWin +Name[gl]=Aurorae de KWin +Name[hu]=KWin Aurorae +Name[ia]=KWin Aurorae +Name[id]=Aurorae KWin +Name[it]=Aurorae di Kwin +Name[ko]=KWin Aurorae +Name[lt]=KWin Aurorae +Name[nl]=KWin Aurorae +Name[nn]=KWin Aurorae +Name[pl]=KWin Aurorae +Name[pt]=Aurora do KWin +Name[pt_BR]=KWin Aurorae +Name[ro]=KWin Aurorae +Name[ru]=Оформление окон Aurorae для KWin +Name[sk]=KWin Aurorae +Name[sl]=KWin Aurorae +Name[sr]=К‑винова аурора +Name[sr@ijekavian]=К‑винова аурора +Name[sr@ijekavianlatin]=KWinova aurora +Name[sr@latin]=KWinova aurora +Name[sv]=Kwin Aurora +Name[tr]=KWin Aurorae +Name[uk]=KWin Aurorae +Name[x-test]=xxKWin Auroraexx +Name[zh_CN]=KWin 极光 +Name[zh_TW]=KWin Aurorae +Type=Service +X-KDE-ServiceTypes=KPackage/PackageStructure +X-KDE-Library=kwin_packagestructure_aurorae + +X-KDE-PluginInfo-Author=Demitrius Belai +X-KDE-PluginInfo-Email=demitriusbelai@gmail.com +X-KDE-PluginInfo-Name=KWin/Aurorae +X-KDE-PluginInfo-Version=1 diff --git a/plugins/kpackage/decoration/CMakeLists.txt b/plugins/kpackage/decoration/CMakeLists.txt new file mode 100644 index 0000000..bfb6065 --- /dev/null +++ b/plugins/kpackage/decoration/CMakeLists.txt @@ -0,0 +1,17 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"kwin_package_decoration\") + +set(decoration_SRCS + decoration.cpp +) + +add_library(kwin_packagestructure_decoration MODULE ${decoration_SRCS}) + +target_link_libraries(kwin_packagestructure_decoration + KF5::I18n + KF5::Package +) + +kcoreaddons_desktop_to_json(kwin_packagestructure_decoration kwin-packagestructure-decoration.desktop) + +install(TARGETS kwin_packagestructure_decoration DESTINATION ${KDE_INSTALL_PLUGINDIR}/kpackage/packagestructure) + diff --git a/plugins/kpackage/decoration/decoration.cpp b/plugins/kpackage/decoration/decoration.cpp new file mode 100644 index 0000000..64c609f --- /dev/null +++ b/plugins/kpackage/decoration/decoration.cpp @@ -0,0 +1,49 @@ +/* + SPDX-FileCopyrightText: 2017 Demitrius Belai + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "decoration.h" + +#include + +void DecorationPackage::initPackage(KPackage::Package *package) +{ + package->setDefaultPackageRoot(QStringLiteral("kwin/decorations/")); + + package->addDirectoryDefinition("config", QStringLiteral("config"), i18n("Configuration Definitions")); + QStringList mimetypes; + mimetypes << QStringLiteral("text/xml"); + package->setMimeTypes("config", mimetypes); + + package->addDirectoryDefinition("ui", QStringLiteral("ui"), i18n("User Interface")); + + package->addDirectoryDefinition("code", QStringLiteral("code"), i18n("Executable Scripts")); + + package->addFileDefinition("mainscript", QStringLiteral("code/main.qml"), i18n("Main Script File")); + package->setRequired("mainscript", true); + + mimetypes.clear(); + mimetypes << QStringLiteral("text/plain"); + package->setMimeTypes("decoration", mimetypes); +} + +void DecorationPackage::pathChanged(KPackage::Package *package) +{ + if (package->path().isEmpty()) { + return; + } + + KPluginMetaData md(package->metadata().metaDataFileName()); + QString mainScript = md.value("X-Plasma-MainScript"); + + if (!mainScript.isEmpty()) { + package->addFileDefinition("mainscript", mainScript, i18n("Main Script File")); + } +} + +K_EXPORT_KPACKAGE_PACKAGE_WITH_JSON(DecorationPackage, "kwin-packagestructure-decoration.json") + +#include "decoration.moc" + diff --git a/plugins/kpackage/decoration/decoration.h b/plugins/kpackage/decoration/decoration.h new file mode 100644 index 0000000..f91e175 --- /dev/null +++ b/plugins/kpackage/decoration/decoration.h @@ -0,0 +1,20 @@ +/* + SPDX-FileCopyrightText: 2017 Demitrius Belai + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef DECORATIONPACKAGE_H +#define DECORATIONPACKAGE_H + +#include + +class DecorationPackage : public KPackage::PackageStructure +{ +public: + DecorationPackage(QObject*, const QVariantList &) {} + void initPackage(KPackage::Package *package) override; + void pathChanged(KPackage::Package *package) override; +}; + +#endif diff --git a/plugins/kpackage/decoration/kwin-packagestructure-decoration.desktop b/plugins/kpackage/decoration/kwin-packagestructure-decoration.desktop new file mode 100644 index 0000000..c7ce610 --- /dev/null +++ b/plugins/kpackage/decoration/kwin-packagestructure-decoration.desktop @@ -0,0 +1,50 @@ +[Desktop Entry] +Name=KWin Decoration +Name[az]=KWin Dekorasiyası +Name[ca]=Decoració del KWin +Name[ca@valencia]=Decoració de KWin +Name[cs]=Dekorace KWin +Name[da]=KWin-dekoration +Name[de]=KWin-Dekoration +Name[el]=Διακοσμήσεις KWin +Name[en_GB]=KWin Decoration +Name[es]=Decoración de KWin +Name[et]=KWini dekoratsioon +Name[eu]=KWin apainketa +Name[fi]=KWin-ikkunakehykset +Name[fr]=Décorations KWin +Name[gl]=Decoración de KWin +Name[hu]=KWin dekoráció +Name[ia]=Decorationes de KWin +Name[id]=Dekorasi KWin +Name[it]=Decorazioni di KWin +Name[ko]=KWin 장식 +Name[lt]=KWin dekoracijos +Name[nl]=KWin-decoraties +Name[nn]=KWin-dekorasjon +Name[pa]=KWin ਸਜਾਵਟ +Name[pl]=Wygląd KWin +Name[pt]=Decorações do KWin +Name[pt_BR]=Decoração do KWin +Name[ro]=Decorație KWin +Name[ru]=Оформление окон для KWin +Name[sk]=Dekorácie KWin +Name[sl]=Okraski KWin +Name[sr]=К‑винова декорација +Name[sr@ijekavian]=К‑винова декорација +Name[sr@ijekavianlatin]=KWinova dekoracija +Name[sr@latin]=KWinova dekoracija +Name[sv]=Kwin-dekoration +Name[tr]=KWin Dekorasyonu +Name[uk]=Обрамлення вікон KWin +Name[x-test]=xxKWin Decorationxx +Name[zh_CN]=KWin 装饰 +Name[zh_TW]=KWin 裝飾 +Type=Service +X-KDE-ServiceTypes=KPackage/PackageStructure +X-KDE-Library=kwin_packagestructure_decoration + +X-KDE-PluginInfo-Author=Demitrius Belai +X-KDE-PluginInfo-Email=demitriusbelai@gmail.com +X-KDE-PluginInfo-Name=KWin/Decoration +X-KDE-PluginInfo-Version=1 diff --git a/plugins/kpackage/effect/CMakeLists.txt b/plugins/kpackage/effect/CMakeLists.txt new file mode 100644 index 0000000..cc09ec4 --- /dev/null +++ b/plugins/kpackage/effect/CMakeLists.txt @@ -0,0 +1,16 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"kwin_package_effect\") + +set(effect_SRCS + effect.cpp +) + +add_library(kwin_packagestructure_effect MODULE ${effect_SRCS}) + +target_link_libraries(kwin_packagestructure_effect + KF5::I18n + KF5::Package +) + +kcoreaddons_desktop_to_json(kwin_packagestructure_effect kwin-packagestructure-effect.desktop) + +install(TARGETS kwin_packagestructure_effect DESTINATION ${KDE_INSTALL_PLUGINDIR}/kpackage/packagestructure) diff --git a/plugins/kpackage/effect/effect.cpp b/plugins/kpackage/effect/effect.cpp new file mode 100644 index 0000000..c73f906 --- /dev/null +++ b/plugins/kpackage/effect/effect.cpp @@ -0,0 +1,50 @@ +/* + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "effect.h" + +#include + +EffectPackageStructure::EffectPackageStructure(QObject *parent, const QVariantList &args) + : KPackage::PackageStructure(parent, args) +{ +} + +void EffectPackageStructure::initPackage(KPackage::Package *package) +{ + package->setDefaultPackageRoot(QStringLiteral("kwin/effects/")); + + package->addDirectoryDefinition("code", QStringLiteral("code"), i18n("Executable Scripts")); + package->setMimeTypes("code", {QStringLiteral("text/plain")}); + + package->addFileDefinition("mainscript", QStringLiteral("code/main.js"), i18n("Main Script File")); + package->setRequired("mainscript", true); + + package->addFileDefinition("config", QStringLiteral("config/main.xml"), i18n("Configuration Definition File")); + package->setMimeTypes("config", {QStringLiteral("text/xml")}); + + package->addFileDefinition("configui", QStringLiteral("ui/config.ui"), i18n("KCM User Interface File")); + package->setMimeTypes("configui", {QStringLiteral("text/xml")}); +} + +void EffectPackageStructure::pathChanged(KPackage::Package *package) +{ + if (package->path().isEmpty()) { + return; + } + + const KPluginMetaData md(package->metadata().metaDataFileName()); + const QString mainScript = md.value("X-Plasma-MainScript"); + if (mainScript.isEmpty()) { + return; + } + + package->addFileDefinition("mainscript", mainScript, i18n("Main Script File")); +} + +K_EXPORT_KPACKAGE_PACKAGE_WITH_JSON(EffectPackageStructure, "kwin-packagestructure-effect.json") + +#include "effect.moc" diff --git a/plugins/kpackage/effect/effect.h b/plugins/kpackage/effect/effect.h new file mode 100644 index 0000000..767398b --- /dev/null +++ b/plugins/kpackage/effect/effect.h @@ -0,0 +1,20 @@ +/* + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +class EffectPackageStructure : public KPackage::PackageStructure +{ + Q_OBJECT + +public: + EffectPackageStructure(QObject *parent = nullptr, const QVariantList &args = {}); + + void initPackage(KPackage::Package *package) override; + void pathChanged(KPackage::Package *package) override; +}; diff --git a/plugins/kpackage/effect/kwin-packagestructure-effect.desktop b/plugins/kpackage/effect/kwin-packagestructure-effect.desktop new file mode 100644 index 0000000..2502364 --- /dev/null +++ b/plugins/kpackage/effect/kwin-packagestructure-effect.desktop @@ -0,0 +1,44 @@ +[Desktop Entry] +Name=KWin Effect +Name[az]=KWin Effekti +Name[ca]=Efecte del KWin +Name[ca@valencia]=Efecte de KWin +Name[cs]=Efekt KWinu +Name[da]=KWin-effekt +Name[de]=KWin-Effekt +Name[el]=Εφέ KWin +Name[en_GB]=KWin Effect +Name[es]=Efecto de KWin +Name[et]=KWini efekt +Name[eu]=KWin efektua +Name[fi]=KWin-tehoste +Name[fr]=Effet KWin +Name[gl]=Efecto de KWin +Name[hu]=KWin effektus +Name[ia]=Effecto de KWin +Name[id]=Efek KWin +Name[it]=Effetto di KWin +Name[ko]=KWin 효과 +Name[lt]=KWin efektas +Name[nl]=KWin-effect +Name[nn]=KWin-effekt +Name[pl]=Efekt KWin +Name[pt]=Efeito do KWin +Name[pt_BR]=Efeito do KWin +Name[ro]=Efect KWin +Name[ru]=Эффект диспетчера окон +Name[sk]=Efekty KWin +Name[sl]=Učinek KWin +Name[sv]=Kwin-effekt +Name[uk]=Ефект KWin +Name[x-test]=xxKWin Effectxx +Name[zh_CN]=KWin 效果 +Name[zh_TW]=KWin 效果 +Type=Service +X-KDE-ServiceTypes=KPackage/PackageStructure +X-KDE-Library=kwin_packagestructure_effect + +X-KDE-PluginInfo-Author=Vlad Zahorodnii +X-KDE-PluginInfo-Email=vlad.zahorodnii@kde.org +X-KDE-PluginInfo-Name=KWin/Effect +X-KDE-PluginInfo-Version=1 diff --git a/plugins/kpackage/scripts/CMakeLists.txt b/plugins/kpackage/scripts/CMakeLists.txt new file mode 100644 index 0000000..f0a5636 --- /dev/null +++ b/plugins/kpackage/scripts/CMakeLists.txt @@ -0,0 +1,16 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"kwin_package_scripts\") + +set(scripts_SRCS + scripts.cpp +) + +add_library(kwin_packagestructure_scripts MODULE ${scripts_SRCS}) + +target_link_libraries(kwin_packagestructure_scripts + KF5::I18n + KF5::Package +) + +kcoreaddons_desktop_to_json(kwin_packagestructure_scripts kwin-packagestructure-scripts.desktop) + +install(TARGETS kwin_packagestructure_scripts DESTINATION ${KDE_INSTALL_PLUGINDIR}/kpackage/packagestructure) diff --git a/plugins/kpackage/scripts/kwin-packagestructure-scripts.desktop b/plugins/kpackage/scripts/kwin-packagestructure-scripts.desktop new file mode 100644 index 0000000..671847a --- /dev/null +++ b/plugins/kpackage/scripts/kwin-packagestructure-scripts.desktop @@ -0,0 +1,50 @@ +[Desktop Entry] +Name=KWin Script +Name[az]=Kwin skripti +Name[ca]=Script del KWin +Name[ca@valencia]=Script de KWin +Name[cs]=Skript KWinu +Name[da]=KWin-script +Name[de]=KWin-Skript +Name[el]=Σενάριο KWin +Name[en_GB]=KWin Script +Name[es]=Guion de KWin +Name[et]=KWini skript +Name[eu]=KWin scripta +Name[fi]=KWin-skripti +Name[fr]=Script KWin +Name[gl]=Script de KWin +Name[hu]=KWin szkript +Name[ia]=Script de KWin +Name[id]=Skrip KWin +Name[it]=Script di KWin +Name[ko]=KWin 스크립트 +Name[lt]=KWin scenarijus +Name[nl]=KWin-script +Name[nn]=KWin-skript +Name[pa]=KWin ਸਕ੍ਰਿਪਟ +Name[pl]=Skrypt KWin +Name[pt]=Programa do KWin +Name[pt_BR]=Script do KWin +Name[ro]=Script KWin +Name[ru]=Сценарий KWin +Name[sk]=KWin skript +Name[sl]=Skript KWin +Name[sr]=К‑винова скрипта +Name[sr@ijekavian]=К‑винова скрипта +Name[sr@ijekavianlatin]=KWinova skripta +Name[sr@latin]=KWinova skripta +Name[sv]=Kwin-skript +Name[tr]=KWin Betiği +Name[uk]=Скрипт KWin +Name[x-test]=xxKWin Scriptxx +Name[zh_CN]=KWin 脚本 +Name[zh_TW]=KWin 指令稿 +Type=Service +X-KDE-ServiceTypes=KPackage/PackageStructure +X-KDE-Library=kwin_packagestructure_scripts + +X-KDE-PluginInfo-Author=Marco Martin +X-KDE-PluginInfo-Email=notmart@gmail.com +X-KDE-PluginInfo-Name=KWin/Script +X-KDE-PluginInfo-Version=1 diff --git a/plugins/kpackage/scripts/scripts.cpp b/plugins/kpackage/scripts/scripts.cpp new file mode 100644 index 0000000..18d868f --- /dev/null +++ b/plugins/kpackage/scripts/scripts.cpp @@ -0,0 +1,49 @@ +/* + SPDX-FileCopyrightText: 2017 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "scripts.h" + +#include + +void ScriptsPackage::initPackage(KPackage::Package *package) +{ + package->setDefaultPackageRoot(QStringLiteral("kwin/scripts/")); + + package->addDirectoryDefinition("config", QStringLiteral("config"), i18n("Configuration Definitions")); + QStringList mimetypes; + mimetypes << QStringLiteral("text/xml"); + package->setMimeTypes("config", mimetypes); + + package->addDirectoryDefinition("ui", QStringLiteral("ui"), i18n("User Interface")); + + package->addDirectoryDefinition("code", QStringLiteral("code"), i18n("Executable Scripts")); + + package->addFileDefinition("mainscript", QStringLiteral("code/main.js"), i18n("Main Script File")); + package->setRequired("mainscript", true); + + mimetypes.clear(); + mimetypes << QStringLiteral("text/plain"); + package->setMimeTypes("scripts", mimetypes); +} + +void ScriptsPackage::pathChanged(KPackage::Package *package) +{ + if (package->path().isEmpty()) { + return; + } + + KPluginMetaData md(package->metadata().metaDataFileName()); + QString mainScript = md.value("X-Plasma-MainScript"); + + if (!mainScript.isEmpty()) { + package->addFileDefinition("mainscript", mainScript, i18n("Main Script File")); + } +} + +K_EXPORT_KPACKAGE_PACKAGE_WITH_JSON(ScriptsPackage, "kwin-packagestructure-scripts.json") + +#include "scripts.moc" + diff --git a/plugins/kpackage/scripts/scripts.h b/plugins/kpackage/scripts/scripts.h new file mode 100644 index 0000000..661a3e8 --- /dev/null +++ b/plugins/kpackage/scripts/scripts.h @@ -0,0 +1,20 @@ +/* + SPDX-FileCopyrightText: 2017 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef SCRIPTSPACKAGE_H +#define SCRIPTSPACKAGE_H + +#include + +class ScriptsPackage : public KPackage::PackageStructure +{ +public: + ScriptsPackage(QObject*, const QVariantList &) {} + void initPackage(KPackage::Package *package) override; + void pathChanged(KPackage::Package *package) override; +}; + +#endif diff --git a/plugins/kpackage/windowswitcher/CMakeLists.txt b/plugins/kpackage/windowswitcher/CMakeLists.txt new file mode 100644 index 0000000..d0496d4 --- /dev/null +++ b/plugins/kpackage/windowswitcher/CMakeLists.txt @@ -0,0 +1,16 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"kwin_package_windowswitcher\") + +set(windowswitcher_SRCS + windowswitcher.cpp +) + +add_library(kwin_packagestructure_windowswitcher MODULE ${windowswitcher_SRCS}) + +target_link_libraries(kwin_packagestructure_windowswitcher + KF5::I18n + KF5::Package +) + +kcoreaddons_desktop_to_json(kwin_packagestructure_windowswitcher kwin-packagestructure-windowswitcher.desktop) + +install(TARGETS kwin_packagestructure_windowswitcher DESTINATION ${KDE_INSTALL_PLUGINDIR}/kpackage/packagestructure) diff --git a/plugins/kpackage/windowswitcher/kwin-packagestructure-windowswitcher.desktop b/plugins/kpackage/windowswitcher/kwin-packagestructure-windowswitcher.desktop new file mode 100644 index 0000000..1fbbaf6 --- /dev/null +++ b/plugins/kpackage/windowswitcher/kwin-packagestructure-windowswitcher.desktop @@ -0,0 +1,50 @@ +[Desktop Entry] +Name=KWin Window Switcher +Name[az]=KWin pəncərə dəyişdiricisi +Name[ca]=Commutador de finestres del KWin +Name[ca@valencia]=Commutador de finestres de KWin +Name[cs]=Přepínač oken KWin +Name[da]=KWin vinduesskifter +Name[de]=KWin-Fensterwechsler +Name[el]=Εφαρμογή εναλλαγής παραθύρων Kwin +Name[en_GB]=KWin Window Switcher +Name[es]=Cambiador de ventanas de KWin +Name[et]=KWini aknavahetaja +Name[eu]=KWin leiho-aldatzailea +Name[fi]=KWin-ikkunanvalitsin +Name[fr]=Sélecteur de fenêtres de KWin +Name[gl]=Selector de xanela de KWin +Name[hu]=KWin ablakváltó +Name[ia]=Commutator de fenestra de KWin +Name[id]=Pengalih Window KWin +Name[it]=Scambiafinestre di KWin +Name[ko]=KWin ì°½ 전환기 +Name[lt]=KWin langų perjungiklis +Name[nl]=KWin-vensterwisselaar +Name[nn]=KWin-vindaugsbytar +Name[pa]=KWin ਵਿੰਡੋ ਸਵਿੱਚਰ +Name[pl]=Przełącznik okien KWin +Name[pt]=Mudança de Janelas do KWin +Name[pt_BR]=Layout do seletor de janelas do KWin +Name[ro]=Comutator de ferestre KWin +Name[ru]=Переключатель окон для KWin +Name[sk]=Prepínač okien KWin +Name[sl]=Preklopnik oken KWin +Name[sr]=К‑винов мењач прозора +Name[sr@ijekavian]=К‑винов мењач прозора +Name[sr@ijekavianlatin]=KWinov menjač prozora +Name[sr@latin]=KWinov menjač prozora +Name[sv]=Kwin-fönsterbyte +Name[tr]=KWin Pencere Değiştirici +Name[uk]=Перемикач вікон KWin +Name[x-test]=xxKWin Window Switcherxx +Name[zh_CN]=KWin 窗口切换器 +Name[zh_TW]=KWin 視窗切換器 +Type=Service +X-KDE-ServiceTypes=KPackage/PackageStructure +X-KDE-Library=kwin_packagestructure_windowswitcher + +X-KDE-PluginInfo-Author=Marco Martin +X-KDE-PluginInfo-Email=notmart@gmail.com +X-KDE-PluginInfo-Name=KWin/WindowSwitcher +X-KDE-PluginInfo-Version=1 diff --git a/plugins/kpackage/windowswitcher/windowswitcher.cpp b/plugins/kpackage/windowswitcher/windowswitcher.cpp new file mode 100644 index 0000000..ddce5ac --- /dev/null +++ b/plugins/kpackage/windowswitcher/windowswitcher.cpp @@ -0,0 +1,49 @@ +/* + SPDX-FileCopyrightText: 2017 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "windowswitcher.h" + +#include + +void SwitcherPackage::initPackage(KPackage::Package *package) +{ + package->setDefaultPackageRoot(QStringLiteral("kwin/tabbox/")); + + package->addDirectoryDefinition("config", QStringLiteral("config"), i18n("Configuration Definitions")); + QStringList mimetypes; + mimetypes << QStringLiteral("text/xml"); + package->setMimeTypes("config", mimetypes); + + package->addDirectoryDefinition("ui", QStringLiteral("ui"), i18n("User Interface")); + + package->addDirectoryDefinition("code", QStringLiteral("code"), i18n("Executable windowswitcher")); + + package->addFileDefinition("mainscript", QStringLiteral("ui/main.qml"), i18n("Main Script File")); + package->setRequired("mainscript", true); + + mimetypes.clear(); + mimetypes << QStringLiteral("text/plain"); + package->setMimeTypes("windowswitcher", mimetypes); +} + +void SwitcherPackage::pathChanged(KPackage::Package *package) +{ + if (package->path().isEmpty()) { + return; + } + + KPluginMetaData md(package->metadata().metaDataFileName()); + QString mainScript = md.value("X-Plasma-MainScript"); + + if (!mainScript.isEmpty()) { + package->addFileDefinition("mainscript", mainScript, i18n("Main Script File")); + } +} + +K_EXPORT_KPACKAGE_PACKAGE_WITH_JSON(SwitcherPackage, "kwin-packagestructure-windowswitcher.json") + +#include "windowswitcher.moc" + diff --git a/plugins/kpackage/windowswitcher/windowswitcher.h b/plugins/kpackage/windowswitcher/windowswitcher.h new file mode 100644 index 0000000..0f9f568 --- /dev/null +++ b/plugins/kpackage/windowswitcher/windowswitcher.h @@ -0,0 +1,20 @@ +/* + SPDX-FileCopyrightText: 2017 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef WINDOWSWITCHER_H +#define WINDOWSWITCHER_H + +#include + +class SwitcherPackage : public KPackage::PackageStructure +{ +public: + SwitcherPackage(QObject*, const QVariantList &) {} + void initPackage(KPackage::Package *package) override; + void pathChanged(KPackage::Package *package) override; +}; + +#endif diff --git a/plugins/platforms/CMakeLists.txt b/plugins/platforms/CMakeLists.txt new file mode 100644 index 0000000..eef7aed --- /dev/null +++ b/plugins/platforms/CMakeLists.txt @@ -0,0 +1,12 @@ +if (HAVE_DRM) + add_subdirectory(drm) +endif() +if (HAVE_LINUX_FB_H) + add_subdirectory(fbdev) +endif() +if (HAVE_LIBHYBRIS) + add_subdirectory(hwcomposer) +endif() +add_subdirectory(virtual) +add_subdirectory(wayland) +add_subdirectory(x11) diff --git a/plugins/platforms/drm/CMakeLists.txt b/plugins/platforms/drm/CMakeLists.txt new file mode 100644 index 0000000..87e916f --- /dev/null +++ b/plugins/platforms/drm/CMakeLists.txt @@ -0,0 +1,46 @@ +set(DRM_SOURCES + drm_backend.cpp + drm_object.cpp + drm_object_connector.cpp + drm_object_crtc.cpp + drm_object_plane.cpp + drm_output.cpp + drm_buffer.cpp + drm_inputeventfilter.cpp + edid.cpp + logging.cpp + scene_qpainter_drm_backend.cpp + screens_drm.cpp +) + +if (HAVE_GBM) + set(DRM_SOURCES ${DRM_SOURCES} + egl_gbm_backend.cpp + drm_buffer_gbm.cpp + gbm_surface.cpp + gbm_dmabuf.cpp + ) +endif() + +if (HAVE_EGL_STREAMS) + set(DRM_SOURCES ${DRM_SOURCES} + egl_stream_backend.cpp + ) +endif() + +include_directories(${CMAKE_SOURCE_DIR}/platformsupport/scenes/opengl) + +add_library(KWinWaylandDrmBackend MODULE ${DRM_SOURCES}) +set_target_properties(KWinWaylandDrmBackend PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/org.kde.kwin.waylandbackends/") +target_link_libraries(KWinWaylandDrmBackend kwin Libdrm::Libdrm SceneQPainterBackend SceneOpenGLBackend) + +if (HAVE_GBM) + target_link_libraries(KWinWaylandDrmBackend gbm::gbm) +endif() + +install( + TARGETS + KWinWaylandDrmBackend + DESTINATION + ${PLUGIN_INSTALL_DIR}/org.kde.kwin.waylandbackends/ +) diff --git a/plugins/platforms/drm/drm.json b/plugins/platforms/drm/drm.json new file mode 100644 index 0000000..6d58fbb --- /dev/null +++ b/plugins/platforms/drm/drm.json @@ -0,0 +1,84 @@ +{ + "KPlugin": { + "Description": "Render through drm node.", + "Description[az]": "DRM vasitəsi ilə formalaşdırmaq.", + "Description[ca@valencia]": "Renderitza mitjançant el node DRM.", + "Description[ca]": "Renderitza mitjançant el node del DRM.", + "Description[da]": "Render igennem drm-knude.", + "Description[de]": "In DRM-Knoten rendern.", + "Description[el]": "Αποτύπωση μέσω λειτουργίας drm.", + "Description[en_GB]": "Render through drm node.", + "Description[es]": "Renderizar a través del nodo DRM.", + "Description[et]": "Renderdamine drm režiimis.", + "Description[eu]": "Errendatu DRM-korapilunea erabiliz.", + "Description[fi]": "Hahmonna DRM-solmun läpi.", + "Description[fr]": "Rendre par le biais d'un nœud de rendu « DRM ».", + "Description[gl]": "Renderizar a través dun nodo de DRM.", + "Description[hu]": "Renderelés drm node-on.", + "Description[id]": "Render melalui simpul drm.", + "Description[it]": "Resa tramite nodo drm.", + "Description[ko]": "DRM 노드로 렌더링합니다.", + "Description[lt]": "Atvaizduoti per drm veikseną.", + "Description[nl]": "Via drm-node renderen.", + "Description[nn]": "Teikn opp gjennom drm-node.", + "Description[pl]": "Wyświetlaj przez węzeł drm.", + "Description[pt]": "Desenhar através de um nó DRM.", + "Description[pt_BR]": "Renderizar pelo nó de DRM.", + "Description[ro]": "Randează prin nod DRM.", + "Description[ru]": "Отрисовка через DRM", + "Description[sk]": "RenderovaÅ¥ cez režim drm.", + "Description[sl]": "IzriÅ¡i preko vozlišča drm.", + "Description[sr@ijekavian]": "Рендеровање кроз ДРМ чвор.", + "Description[sr@ijekavianlatin]": "Renderovanje kroz DRM čvor.", + "Description[sr@latin]": "Renderovanje kroz DRM čvor.", + "Description[sr]": "Рендеровање кроз ДРМ чвор.", + "Description[sv]": "Återge via DRM-nod.", + "Description[tr]": "Drm düğümü aracılığıyla gerçekle.", + "Description[uk]": "Обробляти через вузол DRM.", + "Description[x-test]": "xxRender through drm node.xx", + "Description[zh_CN]": "通过 drm 结点渲染。", + "Description[zh_TW]": "透過 drm 節點成像。", + "Id": "KWinWaylandDrmBackend", + "Name": "drm", + "Name[az]": "drm", + "Name[ca@valencia]": "DRM", + "Name[ca]": "DRM", + "Name[cs]": "drm", + "Name[da]": "drm", + "Name[de]": "DRM", + "Name[el]": "drm", + "Name[en_GB]": "drm", + "Name[es]": "drm", + "Name[et]": "drm", + "Name[eu]": "DRM", + "Name[fi]": "drm", + "Name[fr]": "drm", + "Name[gl]": "drm", + "Name[hu]": "drm", + "Name[ia]": "drm", + "Name[id]": "drm", + "Name[it]": "drm", + "Name[ko]": "drm", + "Name[lt]": "drm", + "Name[nl]": "drm", + "Name[nn]": "drm", + "Name[pl]": "drm", + "Name[pt]": "DRM", + "Name[pt_BR]": "drm", + "Name[ro]": "drm", + "Name[ru]": "drm", + "Name[sk]": "drm", + "Name[sl]": "drm", + "Name[sr@ijekavian]": "ДРМ", + "Name[sr@ijekavianlatin]": "DRM", + "Name[sr@latin]": "DRM", + "Name[sr]": "ДРМ", + "Name[sv]": "DRM", + "Name[tr]": "drm", + "Name[uk]": "drm", + "Name[x-test]": "xxdrmxx", + "Name[zh_CN]": "drm", + "Name[zh_TW]": "drm" + }, + "input": false +} diff --git a/plugins/platforms/drm/drm_backend.cpp b/plugins/platforms/drm/drm_backend.cpp new file mode 100644 index 0000000..ba7134c --- /dev/null +++ b/plugins/platforms/drm/drm_backend.cpp @@ -0,0 +1,829 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "drm_backend.h" +#include "drm_output.h" +#include "drm_object_connector.h" +#include "drm_object_crtc.h" +#include "drm_object_plane.h" +#include "composite.h" +#include "cursor.h" +#include "logging.h" +#include "logind.h" +#include "main.h" +#include "scene_qpainter_drm_backend.h" +#include "screens_drm.h" +#include "udev.h" +#include "wayland_server.h" +#if HAVE_GBM +#include "egl_gbm_backend.h" +#include +#include "gbm_dmabuf.h" +#endif +#if HAVE_EGL_STREAMS +#include "egl_stream_backend.h" +#endif +// KWayland +#include +// KF5 +#include +#include +#include +#include +// Qt +#include +#include +#include +// system +#include +#include +// drm +#include +#include +#include + +#ifndef DRM_CAP_CURSOR_WIDTH +#define DRM_CAP_CURSOR_WIDTH 0x8 +#endif + +#ifndef DRM_CAP_CURSOR_HEIGHT +#define DRM_CAP_CURSOR_HEIGHT 0x9 +#endif + +#define KWIN_DRM_EVENT_CONTEXT_VERSION 2 + +namespace KWin +{ + +DrmBackend::DrmBackend(QObject *parent) + : Platform(parent) + , m_udev(new Udev) + , m_udevMonitor(m_udev->monitor()) + , m_dpmsFilter() +{ + setSupportsGammaControl(true); + supportsOutputChanges(); +} + +DrmBackend::~DrmBackend() +{ +#if HAVE_GBM + if (m_gbmDevice) { + gbm_device_destroy(m_gbmDevice); + } +#endif + if (m_fd >= 0) { + // wait for pageflips + while (m_pageFlipsPending != 0) { + QCoreApplication::processEvents(QEventLoop::WaitForMoreEvents); + } + + qDeleteAll(m_planes); + qDeleteAll(m_crtcs); + qDeleteAll(m_connectors); + close(m_fd); + } +} + +void DrmBackend::init() +{ + LogindIntegration *logind = LogindIntegration::self(); + auto takeControl = [logind, this]() { + if (logind->hasSessionControl()) { + openDrm(); + } else { + logind->takeControl(); + connect(logind, &LogindIntegration::hasSessionControlChanged, this, &DrmBackend::openDrm); + } + }; + if (logind->isConnected()) { + takeControl(); + } else { + connect(logind, &LogindIntegration::connectedChanged, this, takeControl); + } + connect(logind, &LogindIntegration::prepareForSleep, this, [this] (bool active) { + if (!active) { + turnOutputsOn(); + } + }); +} + +void DrmBackend::prepareShutdown() +{ + writeOutputsConfiguration(); + for (DrmOutput *output : m_outputs) { + output->teardown(); + } + Platform::prepareShutdown(); +} + +Outputs DrmBackend::outputs() const +{ + return m_outputs; +} + +Outputs DrmBackend::enabledOutputs() const +{ + return m_enabledOutputs; +} + +void DrmBackend::createDpmsFilter() +{ + if (!m_dpmsFilter.isNull()) { + // already another output is off + return; + } + m_dpmsFilter.reset(new DpmsInputEventFilter(this)); + input()->prependInputEventFilter(m_dpmsFilter.data()); +} + +void DrmBackend::turnOutputsOn() +{ + m_dpmsFilter.reset(); + for (auto it = m_enabledOutputs.constBegin(), end = m_enabledOutputs.constEnd(); it != end; it++) { + (*it)->updateDpms(KWaylandServer::OutputInterface::DpmsMode::On); + } +} + +void DrmBackend::checkOutputsAreOn() +{ + if (m_dpmsFilter.isNull()) { + // already disabled, all outputs are on + return; + } + for (auto it = m_enabledOutputs.constBegin(), end = m_enabledOutputs.constEnd(); it != end; it++) { + if (!(*it)->isDpmsEnabled()) { + // dpms still disabled, need to keep the filter + return; + } + } + // all outputs are on, disable the filter + m_dpmsFilter.reset(); +} + +void DrmBackend::activate(bool active) +{ + if (active) { + qCDebug(KWIN_DRM) << "Activating session."; + reactivate(); + } else { + qCDebug(KWIN_DRM) << "Deactivating session."; + deactivate(); + } +} + +void DrmBackend::reactivate() +{ + if (m_active) { + return; + } + m_active = true; + if (!usesSoftwareCursor()) { + Cursor* cursor = Cursors::self()->mouse(); + const QPoint cp = cursor->pos() - cursor->hotspot(); + for (auto it = m_outputs.constBegin(); it != m_outputs.constEnd(); ++it) { + DrmOutput *o = *it; + // only relevant in atomic mode + o->m_modesetRequested = true; + o->m_crtc->blank(); + o->showCursor(); + o->moveCursor(cursor, cp); + } + } + // restart compositor + m_pageFlipsPending = 0; + if (Compositor *compositor = Compositor::self()) { + compositor->bufferSwapComplete(); + compositor->addRepaintFull(); + } +} + +void DrmBackend::deactivate() +{ + if (!m_active) { + return; + } + // block compositor + if (m_pageFlipsPending == 0 && Compositor::self()) { + Compositor::self()->aboutToSwapBuffers(); + } + // hide cursor and disable + for (auto it = m_outputs.constBegin(); it != m_outputs.constEnd(); ++it) { + DrmOutput *o = *it; + o->hideCursor(); + } + m_active = false; +} + +void DrmBackend::pageFlipHandler(int fd, unsigned int frame, unsigned int sec, unsigned int usec, void *data) +{ + Q_UNUSED(fd) + Q_UNUSED(frame) + Q_UNUSED(sec) + Q_UNUSED(usec) + auto output = reinterpret_cast(data); + + output->pageFlipped(); + output->m_backend->m_pageFlipsPending--; + if (output->m_backend->m_pageFlipsPending == 0) { + // TODO: improve, this currently means we wait for all page flips or all outputs. + // It would be better to driver the repaint per output + + if (Compositor::self()) { + Compositor::self()->bufferSwapComplete(); + } + } +} + +void DrmBackend::openDrm() +{ + connect(LogindIntegration::self(), &LogindIntegration::sessionActiveChanged, this, &DrmBackend::activate); + UdevDevice::Ptr device = m_udev->primaryGpu(); + if (!device) { + qCWarning(KWIN_DRM) << "Did not find a GPU"; + return; + } + m_devNode = qEnvironmentVariableIsSet("KWIN_DRM_DEVICE_NODE") ? qgetenv("KWIN_DRM_DEVICE_NODE") : QByteArray(device->devNode()); + int fd = LogindIntegration::self()->takeDevice(m_devNode.constData()); + if (fd < 0) { + qCWarning(KWIN_DRM) << "failed to open drm device at" << m_devNode; + return; + } + m_fd = fd; + m_active = true; + QSocketNotifier *notifier = new QSocketNotifier(m_fd, QSocketNotifier::Read, this); + connect(notifier, &QSocketNotifier::activated, this, + [this] { + if (!LogindIntegration::self()->isActiveSession()) { + return; + } + drmEventContext e; + memset(&e, 0, sizeof e); + e.version = KWIN_DRM_EVENT_CONTEXT_VERSION; + e.page_flip_handler = pageFlipHandler; + drmHandleEvent(m_fd, &e); + } + ); + m_drmId = device->sysNum(); + +#if HAVE_EGL_STREAMS + if (qEnvironmentVariableIsSet("KWIN_DRM_USE_EGL_STREAMS")) { + m_useEglStreams = true; + } else { + // If KWIN_DRM_USE_EGL_STREAMS is not set and we know that we are running with + // the nvidia proprietary driver, enable the EGLStreams backend anyway. + DrmScopedPointer version(drmGetVersion(fd)); + m_useEglStreams = version->name == QByteArrayLiteral("nvidia-drm"); + } +#endif + + // trying to activate Atomic Mode Setting (this means also Universal Planes) + if (!qEnvironmentVariableIsSet("KWIN_DRM_NO_AMS")) { + if (drmSetClientCap(m_fd, DRM_CLIENT_CAP_ATOMIC, 1) == 0) { + qCDebug(KWIN_DRM) << "Using Atomic Mode Setting."; + m_atomicModeSetting = true; + + DrmScopedPointer planeResources(drmModeGetPlaneResources(m_fd)); + if (!planeResources) { + qCWarning(KWIN_DRM) << "Failed to get plane resources. Falling back to legacy mode"; + m_atomicModeSetting = false; + } + + if (m_atomicModeSetting) { + qCDebug(KWIN_DRM) << "Number of planes:" << planeResources->count_planes; + + // create the plane objects + for (unsigned int i = 0; i < planeResources->count_planes; ++i) { + DrmScopedPointer kplane(drmModeGetPlane(m_fd, planeResources->planes[i])); + DrmPlane *p = new DrmPlane(kplane->plane_id, m_fd); + if (p->atomicInit()) { + m_planes << p; + if (p->type() == DrmPlane::TypeIndex::Overlay) { + m_overlayPlanes << p; + } + } else { + delete p; + } + } + + if (m_planes.isEmpty()) { + qCWarning(KWIN_DRM) << "Failed to create any plane. Falling back to legacy mode"; + m_atomicModeSetting = false; + } + } + } else { + qCWarning(KWIN_DRM) << "drmSetClientCap for Atomic Mode Setting failed. Using legacy mode."; + } + } + + initCursor(); + if (!updateOutputs()) + return; + + if (m_outputs.isEmpty()) { + qCDebug(KWIN_DRM) << "No connected outputs found on startup."; + } + + // setup udevMonitor + if (m_udevMonitor) { + m_udevMonitor->filterSubsystemDevType("drm"); + const int fd = m_udevMonitor->fd(); + if (fd != -1) { + QSocketNotifier *notifier = new QSocketNotifier(fd, QSocketNotifier::Read, this); + connect(notifier, &QSocketNotifier::activated, this, + [this] { + auto device = m_udevMonitor->getDevice(); + if (!device) { + return; + } + if (device->sysNum() != m_drmId) { + return; + } + if (device->hasProperty("HOTPLUG", "1")) { + qCDebug(KWIN_DRM) << "Received hot plug event for monitored drm device"; + updateOutputs(); + updateCursor(); + } + } + ); + m_udevMonitor->enable(); + } + } + setReady(true); +} + +bool DrmBackend::updateOutputs() +{ + if (m_fd < 0) { + return false; + } + + DrmScopedPointer resources(drmModeGetResources(m_fd)); + if (!resources) { + qCWarning(KWIN_DRM) << "drmModeGetResources failed"; + return false; + } + + auto oldConnectors = m_connectors; + for (int i = 0; i < resources->count_connectors; ++i) { + const uint32_t currentConnector = resources->connectors[i]; + auto it = std::find_if(m_connectors.constBegin(), m_connectors.constEnd(), [currentConnector] (DrmConnector *c) { return c->id() == currentConnector; }); + if (it == m_connectors.constEnd()) { + auto c = new DrmConnector(currentConnector, m_fd); + if (m_atomicModeSetting && !c->atomicInit()) { + delete c; + continue; + } + m_connectors << c; + } else { + oldConnectors.removeOne(*it); + } + } + + auto oldCrtcs = m_crtcs; + for (int i = 0; i < resources->count_crtcs; ++i) { + const uint32_t currentCrtc = resources->crtcs[i]; + auto it = std::find_if(m_crtcs.constBegin(), m_crtcs.constEnd(), [currentCrtc] (DrmCrtc *c) { return c->id() == currentCrtc; }); + if (it == m_crtcs.constEnd()) { + auto c = new DrmCrtc(currentCrtc, this, i); + if (m_atomicModeSetting && !c->atomicInit()) { + delete c; + continue; + } + m_crtcs << c; + } else { + oldCrtcs.removeOne(*it); + } + } + + for (auto c : qAsConst(oldConnectors)) { + m_connectors.removeOne(c); + } + for (auto c : qAsConst(oldCrtcs)) { + m_crtcs.removeOne(c); + } + + QVector connectedOutputs; + QVector pendingConnectors; + + // split up connected connectors in already or not yet assigned ones + for (DrmConnector *con : qAsConst(m_connectors)) { + if (!con->isConnected()) { + continue; + } + + if (DrmOutput *o = findOutput(con->id())) { + connectedOutputs << o; + } else { + pendingConnectors << con; + } + } + + // check for outputs which got removed + QVector removedOutputs; + auto it = m_outputs.begin(); + while (it != m_outputs.end()) { + if (connectedOutputs.contains(*it)) { + it++; + continue; + } + DrmOutput *removed = *it; + it = m_outputs.erase(it); + m_enabledOutputs.removeOne(removed); + emit outputRemoved(removed); + removedOutputs.append(removed); + } + + // now check new connections + for (DrmConnector *con : qAsConst(pendingConnectors)) { + DrmScopedPointer connector(drmModeGetConnector(m_fd, con->id())); + if (!connector) { + continue; + } + if (connector->count_modes == 0) { + continue; + } + bool outputDone = false; + + QVector encoders = con->encoders(); + for (auto encId : qAsConst(encoders)) { + DrmScopedPointer encoder(drmModeGetEncoder(m_fd, encId)); + if (!encoder) { + continue; + } + for (DrmCrtc *crtc : qAsConst(m_crtcs)) { + if (!(encoder->possible_crtcs & (1 << crtc->resIndex()))) { + continue; + } + + // check if crtc isn't used yet -- currently we don't allow multiple outputs on one crtc (cloned mode) + auto it = std::find_if(connectedOutputs.constBegin(), connectedOutputs.constEnd(), + [crtc] (DrmOutput *o) { + return o->m_crtc == crtc; + } + ); + if (it != connectedOutputs.constEnd()) { + continue; + } + + // we found a suitable encoder+crtc + // TODO: we could avoid these lib drm calls if we store all struct data in DrmCrtc and DrmConnector in the beginning + DrmScopedPointer modeCrtc(drmModeGetCrtc(m_fd, crtc->id())); + if (!modeCrtc) { + continue; + } + + DrmOutput *output = new DrmOutput(this); + con->setOutput(output); + output->m_conn = con; + crtc->setOutput(output); + output->m_crtc = crtc; + + if (modeCrtc->mode_valid) { + output->m_mode = modeCrtc->mode; + } else { + output->m_mode = connector->modes[0]; + } + qCDebug(KWIN_DRM) << "For new output use mode " << output->m_mode.name; + + if (!output->init(connector.data())) { + qCWarning(KWIN_DRM) << "Failed to create output for connector " << con->id(); + delete output; + continue; + } + if (!output->initCursor(m_cursorSize)) { + setSoftWareCursor(true); + } + qCDebug(KWIN_DRM) << "Found new output with uuid" << output->uuid(); + + connectedOutputs << output; + emit outputAdded(output); + outputDone = true; + break; + } + if (outputDone) { + break; + } + } + } + std::sort(connectedOutputs.begin(), connectedOutputs.end(), [] (DrmOutput *a, DrmOutput *b) { return a->m_conn->id() < b->m_conn->id(); }); + m_outputs = connectedOutputs; + m_enabledOutputs = connectedOutputs; + readOutputsConfiguration(); + updateOutputsEnabled(); + if (!m_outputs.isEmpty()) { + emit screensQueried(); + } + + for(DrmOutput* removedOutput : removedOutputs) { + removedOutput->teardown(); + removedOutput->m_crtc = nullptr; + removedOutput->m_conn = nullptr; + } + qDeleteAll(oldConnectors); + qDeleteAll(oldCrtcs); + return true; +} + +void DrmBackend::readOutputsConfiguration() +{ + if (m_outputs.isEmpty()) { + return; + } + const QByteArray uuid = generateOutputConfigurationUuid(); + const auto outputGroup = kwinApp()->config()->group("DrmOutputs"); + const auto configGroup = outputGroup.group(uuid); + // default position goes from left to right + QPoint pos(0, 0); + for (auto it = m_outputs.begin(); it != m_outputs.end(); ++it) { + qCDebug(KWIN_DRM) << "Reading output configuration for [" << uuid << "] ["<< (*it)->uuid() << "]"; + const auto outputConfig = configGroup.group((*it)->uuid()); + (*it)->setGlobalPos(outputConfig.readEntry("Position", pos)); + // TODO: add mode + if (outputConfig.hasKey("Scale")) + (*it)->setScale(outputConfig.readEntry("Scale", 1.0)); + pos.setX(pos.x() + (*it)->geometry().width()); + } +} + +void DrmBackend::writeOutputsConfiguration() +{ + if (m_outputs.isEmpty()) { + return; + } + const QByteArray uuid = generateOutputConfigurationUuid(); + auto configGroup = KSharedConfig::openConfig()->group("DrmOutputs").group(uuid); + // default position goes from left to right + for (auto it = m_outputs.cbegin(); it != m_outputs.cend(); ++it) { + qCDebug(KWIN_DRM) << "Writing output configuration for [" << uuid << "] ["<< (*it)->uuid() << "]"; + auto outputConfig = configGroup.group((*it)->uuid()); + outputConfig.writeEntry("Scale", (*it)->scale()); + } +} + +QByteArray DrmBackend::generateOutputConfigurationUuid() const +{ + auto it = m_outputs.constBegin(); + if (m_outputs.size() == 1) { + // special case: one output + return (*it)->uuid(); + } + QCryptographicHash hash(QCryptographicHash::Md5); + for (; it != m_outputs.constEnd(); ++it) { + hash.addData((*it)->uuid()); + } + return hash.result().toHex().left(10); +} + +void DrmBackend::enableOutput(DrmOutput *output, bool enable) +{ + if (enable) { + Q_ASSERT(!m_enabledOutputs.contains(output)); + m_enabledOutputs << output; + emit outputAdded(output); + } else { + Q_ASSERT(m_enabledOutputs.contains(output)); + m_enabledOutputs.removeOne(output); + Q_ASSERT(!m_enabledOutputs.contains(output)); + emit outputRemoved(output); + } + updateOutputsEnabled(); + checkOutputsAreOn(); + emit screensQueried(); +} + +DrmOutput *DrmBackend::findOutput(quint32 connector) +{ + auto it = std::find_if(m_outputs.constBegin(), m_outputs.constEnd(), [connector] (DrmOutput *o) { + return o->m_conn->id() == connector; + }); + if (it != m_outputs.constEnd()) { + return *it; + } + return nullptr; +} + +bool DrmBackend::present(DrmBuffer *buffer, DrmOutput *output) +{ + if (!buffer || buffer->bufferId() == 0) { + if (m_deleteBufferAfterPageFlip) { + delete buffer; + } + return false; + } + + if (output->present(buffer)) { + m_pageFlipsPending++; + if (m_pageFlipsPending == 1 && Compositor::self()) { + Compositor::self()->aboutToSwapBuffers(); + } + return true; + } else if (m_deleteBufferAfterPageFlip) { + delete buffer; + } + return false; +} + +void DrmBackend::initCursor() +{ + +#if HAVE_EGL_STREAMS + // Hardware cursors aren't currently supported with EGLStream backend, + // possibly an NVIDIA driver bug + if (m_useEglStreams) { + setSoftWareCursor(true); + } +#endif + + m_cursorEnabled = waylandServer()->seat()->hasPointer(); + connect(waylandServer()->seat(), &KWaylandServer::SeatInterface::hasPointerChanged, this, + [this] { + m_cursorEnabled = waylandServer()->seat()->hasPointer(); + if (usesSoftwareCursor()) { + return; + } + for (auto it = m_outputs.constBegin(); it != m_outputs.constEnd(); ++it) { + if (m_cursorEnabled) { + if (!(*it)->showCursor()) { + setSoftWareCursor(true); + } + } else { + (*it)->hideCursor(); + } + } + } + ); + uint64_t capability = 0; + QSize cursorSize; + if (drmGetCap(m_fd, DRM_CAP_CURSOR_WIDTH, &capability) == 0) { + cursorSize.setWidth(capability); + } else { + cursorSize.setWidth(64); + } + if (drmGetCap(m_fd, DRM_CAP_CURSOR_HEIGHT, &capability) == 0) { + cursorSize.setHeight(capability); + } else { + cursorSize.setHeight(64); + } + m_cursorSize = cursorSize; + // now we have screens and can set cursors, so start tracking + connect(Cursors::self(), &Cursors::currentCursorChanged, this, &DrmBackend::updateCursor); + connect(Cursors::self(), &Cursors::positionChanged, this, &DrmBackend::moveCursor); +} + +void DrmBackend::setCursor() +{ + if (m_cursorEnabled) { + for (auto it = m_outputs.constBegin(); it != m_outputs.constEnd(); ++it) { + if (!(*it)->showCursor()) { + setSoftWareCursor(true); + } + } + } +} + +void DrmBackend::updateCursor() +{ + if (usesSoftwareCursor()) { + return; + } + if (isCursorHidden()) { + return; + } + + auto cursor = Cursors::self()->currentCursor(); + const QImage &cursorImage = cursor->image(); + if (cursorImage.isNull()) { + doHideCursor(); + return; + } + for (auto it = m_outputs.constBegin(); it != m_outputs.constEnd(); ++it) { + (*it)->updateCursor(); + } + + setCursor(); + + moveCursor(cursor, cursor->pos()); +} + +void DrmBackend::doShowCursor() +{ + updateCursor(); +} + +void DrmBackend::doHideCursor() +{ + if (!m_cursorEnabled || usesSoftwareCursor()) { + return; + } + for (auto it = m_outputs.constBegin(); it != m_outputs.constEnd(); ++it) { + (*it)->hideCursor(); + } +} + +void DrmBackend::moveCursor(Cursor *cursor, const QPoint &pos) +{ + if (!m_cursorEnabled || isCursorHidden() || usesSoftwareCursor()) { + return; + } + for (auto it = m_outputs.constBegin(); it != m_outputs.constEnd(); ++it) { + (*it)->moveCursor(cursor, pos); + } +} + +Screens *DrmBackend::createScreens(QObject *parent) +{ + return new DrmScreens(this, parent); +} + +QPainterBackend *DrmBackend::createQPainterBackend() +{ + m_deleteBufferAfterPageFlip = false; + return new DrmQPainterBackend(this); +} + +OpenGLBackend *DrmBackend::createOpenGLBackend() +{ +#if HAVE_EGL_STREAMS + if (m_useEglStreams) { + m_deleteBufferAfterPageFlip = false; + return new EglStreamBackend(this); + } +#endif + +#if HAVE_GBM + m_deleteBufferAfterPageFlip = true; + return new EglGbmBackend(this); +#else + return Platform::createOpenGLBackend(); +#endif +} + +DrmDumbBuffer *DrmBackend::createBuffer(const QSize &size) +{ + DrmDumbBuffer *b = new DrmDumbBuffer(m_fd, size); + return b; +} + +#if HAVE_GBM +DrmSurfaceBuffer *DrmBackend::createBuffer(const std::shared_ptr &surface) +{ + DrmSurfaceBuffer *b = new DrmSurfaceBuffer(m_fd, surface); + return b; +} +#endif + +void DrmBackend::updateOutputsEnabled() +{ + bool enabled = false; + for (auto it = m_enabledOutputs.constBegin(); it != m_enabledOutputs.constEnd(); ++it) { + enabled = enabled || (*it)->isDpmsEnabled(); + } + setOutputsEnabled(enabled); +} + +QVector DrmBackend::supportedCompositors() const +{ + if (selectedCompositor() != NoCompositing) { + return {selectedCompositor()}; + } +#if HAVE_GBM + return QVector{OpenGLCompositing, QPainterCompositing}; +#elif HAVE_EGL_STREAMS + return m_useEglStreams ? + QVector{OpenGLCompositing, QPainterCompositing} : + QVector{QPainterCompositing}; +#else + return QVector{QPainterCompositing}; +#endif +} + +QString DrmBackend::supportInformation() const +{ + QString supportInfo; + QDebug s(&supportInfo); + s.nospace(); + s << "Name: " << "DRM" << Qt::endl; + s << "Active: " << m_active << Qt::endl; + s << "Atomic Mode Setting: " << m_atomicModeSetting << Qt::endl; +#if HAVE_EGL_STREAMS + s << "Using EGL Streams: " << m_useEglStreams << Qt::endl; +#endif + return supportInfo; +} + +DmaBufTexture *DrmBackend::createDmaBufTexture(const QSize &size) +{ +#if HAVE_GBM + return GbmDmaBuf::createBuffer(size, m_gbmDevice); +#else + return nullptr; +#endif +} + +} diff --git a/plugins/platforms/drm/drm_backend.h b/plugins/platforms/drm/drm_backend.h new file mode 100644 index 0000000..54669f9 --- /dev/null +++ b/plugins/platforms/drm/drm_backend.h @@ -0,0 +1,196 @@ +/* + 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_DRM_BACKEND_H +#define KWIN_DRM_BACKEND_H +#include "platform.h" +#include "input.h" + +#include "drm_buffer.h" +#if HAVE_GBM +#include "drm_buffer_gbm.h" +#endif +#include "drm_inputeventfilter.h" +#include "drm_pointer.h" + +#include +#include +#include +#include +#include +#include + +#include + +struct gbm_bo; +struct gbm_device; +struct gbm_surface; + +namespace KWin +{ + +class Udev; +class UdevMonitor; + +class DrmOutput; +class DrmPlane; +class DrmCrtc; +class DrmConnector; +class GbmSurface; +class Cursor; + +class KWIN_EXPORT DrmBackend : public Platform +{ + Q_OBJECT + Q_INTERFACES(KWin::Platform) + Q_PLUGIN_METADATA(IID "org.kde.kwin.Platform" FILE "drm.json") +public: + explicit DrmBackend(QObject *parent = nullptr); + ~DrmBackend() override; + + Screens *createScreens(QObject *parent = nullptr) override; + QPainterBackend *createQPainterBackend() override; + OpenGLBackend* createOpenGLBackend() override; + DmaBufTexture *createDmaBufTexture(const QSize &size) override; + + void init() override; + void prepareShutdown() override; + + DrmDumbBuffer *createBuffer(const QSize &size); +#if HAVE_GBM + DrmSurfaceBuffer *createBuffer(const std::shared_ptr &surface); +#endif + bool present(DrmBuffer *buffer, DrmOutput *output); + + int fd() const { + return m_fd; + } + Outputs outputs() const override; + Outputs enabledOutputs() const override; + QVector drmOutputs() const { + return m_outputs; + } + QVector drmEnabledOutputs() const { + return m_enabledOutputs; + } + + void enableOutput(DrmOutput *output, bool enable); + + QVector planes() const { + return m_planes; + } + QVector overlayPlanes() const { + return m_overlayPlanes; + } + + void createDpmsFilter(); + void checkOutputsAreOn(); + + // QPainter reuses buffers + bool deleteBufferAfterPageFlip() const { + return m_deleteBufferAfterPageFlip; + } + // returns use of AMS, default is not/legacy + bool atomicModeSetting() const { + return m_atomicModeSetting; + } + + void setGbmDevice(gbm_device *device) { + m_gbmDevice = device; + } + gbm_device *gbmDevice() const { + return m_gbmDevice; + } + + QByteArray devNode() const { + return m_devNode; + } + +#if HAVE_EGL_STREAMS + bool useEglStreams() const { + return m_useEglStreams; + } +#endif + + QVector supportedCompositors() const override; + + QString supportInformation() const override; + + bool isCursorEnabled() const { + return m_cursorEnabled; + }; + +public Q_SLOTS: + void turnOutputsOn(); + +Q_SIGNALS: + /** + * Emitted whenever an output is removed/disabled + */ + void outputRemoved(KWin::DrmOutput *output); + /** + * Emitted whenever an output is added/enabled + */ + void outputAdded(KWin::DrmOutput *output); + +protected: + + void doHideCursor() override; + void doShowCursor() override; + +private: + static void pageFlipHandler(int fd, unsigned int frame, unsigned int sec, unsigned int usec, void *data); + void openDrm(); + void activate(bool active); + void reactivate(); + void deactivate(); + bool updateOutputs(); + void setCursor(); + void updateCursor(); + void moveCursor(Cursor *cursor, const QPoint &pos); + void initCursor(); + void readOutputsConfiguration(); + void writeOutputsConfiguration(); + QByteArray generateOutputConfigurationUuid() const; + DrmOutput *findOutput(quint32 connector); + void updateOutputsEnabled(); + QScopedPointer m_udev; + QScopedPointer m_udevMonitor; + int m_fd = -1; + int m_drmId = 0; + // all crtcs + QVector m_crtcs; + // all connectors + QVector m_connectors; + // active output pipelines (planes + crtc + encoder + connector) + QVector m_outputs; + // active and enabled pipelines (above + wl_output) + QVector m_enabledOutputs; + + bool m_deleteBufferAfterPageFlip; + bool m_atomicModeSetting = false; + bool m_cursorEnabled = false; + QSize m_cursorSize; + int m_pageFlipsPending = 0; + bool m_active = false; + QByteArray m_devNode; +#if HAVE_EGL_STREAMS + bool m_useEglStreams = false; +#endif + // all available planes: primarys, cursors and overlays + QVector m_planes; + QVector m_overlayPlanes; + QScopedPointer m_dpmsFilter; + gbm_device *m_gbmDevice = nullptr; +}; + + +} + +#endif + diff --git a/plugins/platforms/drm/drm_buffer.cpp b/plugins/platforms/drm/drm_buffer.cpp new file mode 100644 index 0000000..9508eca --- /dev/null +++ b/plugins/platforms/drm/drm_buffer.cpp @@ -0,0 +1,97 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "drm_buffer.h" + +#include "logging.h" + +// system +#include +// c++ +#include +// drm +#include +#include + +namespace KWin +{ + +DrmBuffer:: DrmBuffer(int fd) + : m_fd(fd) +{ +} + +// DrmDumbBuffer +DrmDumbBuffer::DrmDumbBuffer(int fd, const QSize &size) + : DrmBuffer(fd) +{ + m_size = size; + drm_mode_create_dumb createArgs; + memset(&createArgs, 0, sizeof createArgs); + createArgs.bpp = 32; + createArgs.width = size.width(); + createArgs.height = size.height(); + if (drmIoctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &createArgs) != 0) { + qCWarning(KWIN_DRM) << "DRM_IOCTL_MODE_CREATE_DUMB failed"; + return; + } + m_handle = createArgs.handle; + m_bufferSize = createArgs.size; + m_stride = createArgs.pitch; + if (drmModeAddFB(fd, size.width(), size.height(), 24, 32, + m_stride, createArgs.handle, &m_bufferId) != 0) { + qCWarning(KWIN_DRM) << "drmModeAddFB failed with errno" << errno; + } +} + +DrmDumbBuffer::~DrmDumbBuffer() +{ + if (m_bufferId) { + drmModeRmFB(fd(), m_bufferId); + } + + delete m_image; + if (m_memory) { + munmap(m_memory, m_bufferSize); + } + if (m_handle) { + drm_mode_destroy_dumb destroyArgs; + destroyArgs.handle = m_handle; + drmIoctl(fd(), DRM_IOCTL_MODE_DESTROY_DUMB, &destroyArgs); + } +} + +bool DrmDumbBuffer::needsModeChange(DrmBuffer *b) const { + if (DrmDumbBuffer *db = dynamic_cast(b)) { + return m_stride != db->stride(); + } else { + return true; + } +} + +bool DrmDumbBuffer::map(QImage::Format format) +{ + if (!m_handle || !m_bufferId) { + return false; + } + drm_mode_map_dumb mapArgs; + memset(&mapArgs, 0, sizeof mapArgs); + mapArgs.handle = m_handle; + if (drmIoctl(fd(), DRM_IOCTL_MODE_MAP_DUMB, &mapArgs) != 0) { + return false; + } + void *address = mmap(nullptr, m_bufferSize, PROT_WRITE, MAP_SHARED, fd(), mapArgs.offset); + if (address == MAP_FAILED) { + return false; + } + m_memory = address; + m_image = new QImage((uchar*)m_memory, m_size.width(), m_size.height(), m_stride, format); + return !m_image->isNull(); +} + +} diff --git a/plugins/platforms/drm/drm_buffer.h b/plugins/platforms/drm/drm_buffer.h new file mode 100644 index 0000000..73dac12 --- /dev/null +++ b/plugins/platforms/drm/drm_buffer.h @@ -0,0 +1,77 @@ +/* + 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_DRM_BUFFER_H +#define KWIN_DRM_BUFFER_H + +#include +#include + +namespace KWin +{ + +class DrmBuffer +{ +public: + DrmBuffer(int fd); + virtual ~DrmBuffer() = default; + + virtual bool needsModeChange(DrmBuffer *b) const {Q_UNUSED(b) return false;} + + quint32 bufferId() const { + return m_bufferId; + } + + const QSize &size() const { + return m_size; + } + + virtual void releaseGbm() {} + + int fd() const { + return m_fd; + } + +protected: + quint32 m_bufferId = 0; + QSize m_size; + int m_fd; +}; + +class DrmDumbBuffer : public DrmBuffer +{ +public: + DrmDumbBuffer(int fd, const QSize &size); + ~DrmDumbBuffer() override; + + bool needsModeChange(DrmBuffer *b) const override; + + bool map(QImage::Format format = QImage::Format_RGB32); + quint32 handle() const { + return m_handle; + } + QImage *image() const { + return m_image; + } + + quint32 stride() const { + return m_stride; + } + +private: + quint32 m_handle = 0; + quint64 m_bufferSize = 0; + void *m_memory = nullptr; + QImage *m_image = nullptr; + quint32 m_stride = 0; +}; + +} + +#endif + diff --git a/plugins/platforms/drm/drm_buffer_gbm.cpp b/plugins/platforms/drm/drm_buffer_gbm.cpp new file mode 100644 index 0000000..1bf3c7a --- /dev/null +++ b/plugins/platforms/drm/drm_buffer_gbm.cpp @@ -0,0 +1,58 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Roman Gilg + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "drm_buffer_gbm.h" +#include "gbm_surface.h" + +#include "logging.h" + +// system +#include +// c++ +#include +// drm +#include +#include +#include + +namespace KWin +{ + +// DrmSurfaceBuffer +DrmSurfaceBuffer::DrmSurfaceBuffer(int fd, const std::shared_ptr &surface) + : DrmBuffer(fd) + , m_surface(surface) +{ + m_bo = m_surface->lockFrontBuffer(); + if (!m_bo) { + qCWarning(KWIN_DRM) << "Locking front buffer failed"; + return; + } + m_size = QSize(gbm_bo_get_width(m_bo), gbm_bo_get_height(m_bo)); + if (drmModeAddFB(fd, m_size.width(), m_size.height(), 24, 32, gbm_bo_get_stride(m_bo), gbm_bo_get_handle(m_bo).u32, &m_bufferId) != 0) { + qCWarning(KWIN_DRM) << "drmModeAddFB failed"; + } + gbm_bo_set_user_data(m_bo, this, nullptr); +} + +DrmSurfaceBuffer::~DrmSurfaceBuffer() +{ + if (m_bufferId) { + drmModeRmFB(fd(), m_bufferId); + } + releaseGbm(); +} + +void DrmSurfaceBuffer::releaseGbm() +{ + m_surface->releaseBuffer(m_bo); + m_bo = nullptr; +} + +} diff --git a/plugins/platforms/drm/drm_buffer_gbm.h b/plugins/platforms/drm/drm_buffer_gbm.h new file mode 100644 index 0000000..d27b303 --- /dev/null +++ b/plugins/platforms/drm/drm_buffer_gbm.h @@ -0,0 +1,56 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Roman Gilg + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_DRM_BUFFER_GBM_H +#define KWIN_DRM_BUFFER_GBM_H + +#include "drm_buffer.h" + +#include + +struct gbm_bo; + +namespace KWin +{ + +class GbmSurface; + +class DrmSurfaceBuffer : public DrmBuffer +{ +public: + DrmSurfaceBuffer(int fd, const std::shared_ptr &surface); + ~DrmSurfaceBuffer() override; + + bool needsModeChange(DrmBuffer *b) const override { + if (DrmSurfaceBuffer *sb = dynamic_cast(b)) { + return hasBo() != sb->hasBo(); + } else { + return true; + } + } + + bool hasBo() const { + return m_bo != nullptr; + } + + gbm_bo* getBo() const { + return m_bo; + } + + void releaseGbm() override; + +private: + std::shared_ptr m_surface; + gbm_bo *m_bo = nullptr; +}; + +} + +#endif + diff --git a/plugins/platforms/drm/drm_inputeventfilter.cpp b/plugins/platforms/drm/drm_inputeventfilter.cpp new file mode 100644 index 0000000..7620295 --- /dev/null +++ b/plugins/platforms/drm/drm_inputeventfilter.cpp @@ -0,0 +1,104 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "drm_inputeventfilter.h" +#include "drm_backend.h" +#include "wayland_server.h" + +#include + +#include + +namespace KWin +{ + +DpmsInputEventFilter::DpmsInputEventFilter(DrmBackend *backend) + : InputEventFilter() + , m_backend(backend) +{ +} + +DpmsInputEventFilter::~DpmsInputEventFilter() = default; + +bool DpmsInputEventFilter::pointerEvent(QMouseEvent *event, quint32 nativeButton) +{ + Q_UNUSED(event) + Q_UNUSED(nativeButton) + notify(); + return true; +} + +bool DpmsInputEventFilter::wheelEvent(QWheelEvent *event) +{ + Q_UNUSED(event) + notify(); + return true; +} + +bool DpmsInputEventFilter::keyEvent(QKeyEvent *event) +{ + Q_UNUSED(event) + notify(); + return true; +} + +bool DpmsInputEventFilter::touchDown(qint32 id, const QPointF &pos, quint32 time) +{ + Q_UNUSED(pos) + Q_UNUSED(time) + if (m_touchPoints.isEmpty()) { + if (!m_doubleTapTimer.isValid()) { + // this is the first tap + m_doubleTapTimer.start(); + } else { + if (m_doubleTapTimer.elapsed() < qApp->doubleClickInterval()) { + m_secondTap = true; + } else { + // took too long. Let's consider it a new click + m_doubleTapTimer.restart(); + } + } + } else { + // not a double tap + m_doubleTapTimer.invalidate(); + m_secondTap = false; + } + m_touchPoints << id; + return true; +} + +bool DpmsInputEventFilter::touchUp(qint32 id, quint32 time) +{ + m_touchPoints.removeAll(id); + if (m_touchPoints.isEmpty() && m_doubleTapTimer.isValid() && m_secondTap) { + if (m_doubleTapTimer.elapsed() < qApp->doubleClickInterval()) { + waylandServer()->seat()->setTimestamp(time); + notify(); + } + m_doubleTapTimer.invalidate(); + m_secondTap = false; + } + return true; +} + +bool DpmsInputEventFilter::touchMotion(qint32 id, const QPointF &pos, quint32 time) +{ + Q_UNUSED(id) + Q_UNUSED(pos) + Q_UNUSED(time) + // ignore the event + return true; +} + +void DpmsInputEventFilter::notify() +{ + // queued to not modify the list of event filters while filtering + QMetaObject::invokeMethod(m_backend, "turnOutputsOn", Qt::QueuedConnection); +} + +} diff --git a/plugins/platforms/drm/drm_inputeventfilter.h b/plugins/platforms/drm/drm_inputeventfilter.h new file mode 100644 index 0000000..73cef2e --- /dev/null +++ b/plugins/platforms/drm/drm_inputeventfilter.h @@ -0,0 +1,45 @@ +/* + 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_DRM_INPUTEVENTFILTER_H +#define KWIN_DRM_INPUTEVENTFILTER_H +#include "input.h" + +#include + +namespace KWin +{ + +class DrmBackend; + +class DpmsInputEventFilter : public InputEventFilter +{ +public: + DpmsInputEventFilter(DrmBackend *backend); + ~DpmsInputEventFilter() override; + + bool pointerEvent(QMouseEvent *event, quint32 nativeButton) override; + bool wheelEvent(QWheelEvent *event) override; + bool keyEvent(QKeyEvent *event) override; + bool touchDown(qint32 id, const QPointF &pos, quint32 time) override; + bool touchMotion(qint32 id, const QPointF &pos, quint32 time) override; + bool touchUp(qint32 id, quint32 time) override; + +private: + void notify(); + DrmBackend *m_backend; + QElapsedTimer m_doubleTapTimer; + QVector m_touchPoints; + bool m_secondTap = false; +}; + +} + + +#endif + diff --git a/plugins/platforms/drm/drm_object.cpp b/plugins/platforms/drm/drm_object.cpp new file mode 100644 index 0000000..7d21d4a --- /dev/null +++ b/plugins/platforms/drm/drm_object.cpp @@ -0,0 +1,180 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "drm_object.h" +#include "drm_pointer.h" + +#include "logging.h" + +namespace KWin +{ + +/* + * Definitions for class DrmObject + */ + +DrmObject::DrmObject(uint32_t object_id, int fd) + : m_fd(fd) + , m_id(object_id) +{ +} + +DrmObject::~DrmObject() +{ + for (auto *p : m_props) { + delete p; + } +} + +void DrmObject::setPropertyNames(QVector &&vector) +{ + m_propsNames = std::move(vector); + m_props.fill(nullptr, m_propsNames.size()); +} + +void DrmObject::initProp(int n, drmModeObjectProperties *properties, QVector enumNames) +{ + for (unsigned int i = 0; i < properties->count_props; ++i) { + DrmScopedPointer prop( drmModeGetProperty(fd(), properties->props[i]) ); + if (!prop) { + qCWarning(KWIN_DRM) << "Getting property" << i << "failed"; + continue; + } + + if (prop->name == m_propsNames[n]) { + qCDebug(KWIN_DRM).nospace() << m_id << ": " << prop->name << "' (id " << prop->prop_id + << "): " << properties->prop_values[i]; + m_props[n] = new Property(prop.data(), properties->prop_values[i], enumNames); + return; + } + } + qCWarning(KWIN_DRM) << "Initializing property" << m_propsNames[n] << "failed"; +} + +bool DrmObject::atomicPopulate(drmModeAtomicReq *req) const +{ + return doAtomicPopulate(req, 0); +} + +bool DrmObject::doAtomicPopulate(drmModeAtomicReq *req, int firstProperty) const +{ + bool ret = true; + + for (int i = firstProperty; i < m_props.size(); i++) { + auto property = m_props.at(i); + if (!property) { + continue; + } + ret &= atomicAddProperty(req, property); + } + + if (!ret) { + qCWarning(KWIN_DRM) << "Failed to populate atomic object" << m_id; + return false; + } + return true; +} + +void DrmObject::setValue(int prop, uint64_t new_value) +{ + Q_ASSERT(prop < m_props.size()); + auto property = m_props.at(prop); + if (property) { + property->setValue(new_value); + } +} + +bool DrmObject::propHasEnum(int prop, uint64_t value) const +{ + auto property = m_props.at(prop); + return property ? property->hasEnum(value) : false; +} + +bool DrmObject::atomicAddProperty(drmModeAtomicReq *req, Property *property) const +{ + if (drmModeAtomicAddProperty(req, m_id, property->propId(), property->value()) <= 0) { + qCWarning(KWIN_DRM) << "Adding property" << property->name() + << "to atomic commit failed for object" << this; + return false; + } + return true; +} + +/* + * Definitions for struct Prop + */ + +DrmObject::Property::Property(drmModePropertyRes *prop, uint64_t val, QVector enumNames) + : m_propId(prop->prop_id) + , m_propName(prop->name) + , m_value(val) +{ + if (!enumNames.isEmpty()) { + qCDebug(KWIN_DRM) << m_propName << " can have enums:" << enumNames; + m_enumNames = enumNames; + initEnumMap(prop); + } +} + +DrmObject::Property::~Property() = default; + +void DrmObject::Property::initEnumMap(drmModePropertyRes *prop) +{ + if ( ( !(prop->flags & DRM_MODE_PROP_ENUM) && !(prop->flags & DRM_MODE_PROP_BITMASK) ) + || prop->count_enums < 1 ) { + qCWarning(KWIN_DRM) << "Property '" << prop->name << "' ( id =" + << m_propId << ") should be enum valued, but it is not."; + return; + } + + const int nameCount = m_enumNames.size(); + m_enumMap.resize(nameCount); + + qCDebug(KWIN_DRM).nospace() << "Available are " << prop->count_enums << + " enums. Query their runtime values:"; + + for (int i = 0; i < prop->count_enums; i++) { + struct drm_mode_property_enum *en = &prop->enums[i]; + int j = 0; + + while (QByteArray(en->name) != m_enumNames[j]) { + j++; + if (j == nameCount) { + qCWarning(KWIN_DRM).nospace() << m_propName << " has unrecognized enum '" + << en->name << "'"; + break; + } + } + + if (j < nameCount) { + qCDebug(KWIN_DRM).nospace() << "Enum '" << en->name + << "': runtime-value = " << en->value; + m_enumMap[j] = en->value; + } + } + + if (KWIN_DRM().isDebugEnabled()) { + for (int i = 0; i < m_enumMap.size(); i++) { + if (m_value == m_enumMap[i]) { + // TODO: This does not work with bitmask properties, because from kernel we get the + // values for some reason as the shift distance instead of the full value. + // See: https://github.com/torvalds/linux/blob/6794862a/drivers/ + // gpu/drm/drm_blend.c#L267 + qCDebug(KWIN_DRM) << "=>" << m_propName + << "with mapped enum value" << m_enumNames[i]; + } + } + } +} + +} + +QDebug& operator<<(QDebug& s, const KWin::DrmObject* obj) +{ + return s.nospace() << "DrmObject(" << obj->id() << ", output:" << obj->output() << ", fd: "<< obj->fd() << ')'; +} diff --git a/plugins/platforms/drm/drm_object.h b/plugins/platforms/drm/drm_object.h new file mode 100644 index 0000000..6b16cb9 --- /dev/null +++ b/plugins/platforms/drm/drm_object.h @@ -0,0 +1,141 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include +#include + +// drm +#include + + +namespace KWin +{ + +class DrmBackend; +class DrmOutput; + +class DrmObject +{ +public: + /** + * Create DRM object representation. + * @param object_id provided by the kernel + * @param fd of the DRM device + */ + DrmObject(uint32_t object_id, int fd); + virtual ~DrmObject(); + + /** + * Must be called to query necessary data directly after creation. + * @return true when initializing was successful + */ + virtual bool atomicInit() = 0; + + uint32_t id() const { + return m_id; + } + + DrmOutput *output() const { + return m_output; + } + void setOutput(DrmOutput* output) { + m_output = output; + } + + int fd() const { + return m_fd; + } + + /** + * Populate an atomic request with data of this object. + * @param req the atomic request + * @return true when the request was successfully populated + */ + virtual bool atomicPopulate(drmModeAtomicReq *req) const; + + void setValue(int prop, uint64_t new_value); + bool propHasEnum(int prop, uint64_t value) const; + +protected: + /** + * Initialize properties of object. Only derived classes know names and quantities of + * properties. + * + * @return true when properties have been initialized successfully + */ + virtual bool initProps() = 0; + + void setPropertyNames(QVector &&vector); + void initProp(int n, drmModeObjectProperties *properties, + QVector enumNames = QVector(0)); + + bool doAtomicPopulate(drmModeAtomicReq *req, int firstProperty) const; + + class Property; + bool atomicAddProperty(drmModeAtomicReq *req, Property *property) const; + + int m_fd; + const uint32_t m_id; + DrmOutput *m_output = nullptr; + + // for comparison with received name of DRM object + QVector m_props; + + class Property + { + public: + Property(drmModePropertyRes *prop, uint64_t val, QVector enumNames); + virtual ~Property(); + + void initEnumMap(drmModePropertyRes *prop); + + /** + * For properties of enum type the enum map identifies the kernel runtime values, + * which must be queried beforehand. + * + * @param n the index to the enum + * @return the runtime enum value corresponding with enum index @param n + */ + uint64_t enumMap(int n) const { + return m_enumMap[n]; // TODO: test on index out of bounds? + } + bool hasEnum(uint64_t value) const { + return m_enumMap.contains(value); + } + + uint32_t propId() const { + return m_propId; + } + uint64_t value() const { + return m_value; + } + void setValue(uint64_t new_value) { + m_value = new_value; + } + const QByteArray &name() const { + return m_propName; + } + + private: + uint32_t m_propId = 0; + QByteArray m_propName; + + uint64_t m_value = 0; + QVector m_enumMap; + QVector m_enumNames; + }; + +private: + QVector m_propsNames; +}; + +} + +QDebug& operator<<(QDebug& stream, const KWin::DrmObject*); diff --git a/plugins/platforms/drm/drm_object_connector.cpp b/plugins/platforms/drm/drm_object_connector.cpp new file mode 100644 index 0000000..feff985 --- /dev/null +++ b/plugins/platforms/drm/drm_object_connector.cpp @@ -0,0 +1,70 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "drm_object_connector.h" +#include "drm_pointer.h" +#include "logging.h" + +namespace KWin +{ + +DrmConnector::DrmConnector(uint32_t connector_id, int fd) + : DrmObject(connector_id, fd) +{ + DrmScopedPointer con(drmModeGetConnector(fd, connector_id)); + if (!con) { + return; + } + for (int i = 0; i < con->count_encoders; ++i) { + m_encoders << con->encoders[i]; + } +} + +DrmConnector::~DrmConnector() = default; + +bool DrmConnector::atomicInit() +{ + qCDebug(KWIN_DRM) << "Creating connector" << m_id; + + if (!initProps()) { + return false; + } + return true; +} + +bool DrmConnector::initProps() +{ + setPropertyNames( { + QByteArrayLiteral("CRTC_ID"), + }); + + DrmScopedPointer properties( + drmModeObjectGetProperties(fd(), m_id, DRM_MODE_OBJECT_CONNECTOR)); + if (!properties) { + qCWarning(KWIN_DRM) << "Failed to get properties for connector " << m_id ; + return false; + } + + int propCount = int(PropertyIndex::Count); + for (int j = 0; j < propCount; ++j) { + initProp(j, properties.data()); + } + + return true; +} + +bool DrmConnector::isConnected() +{ + DrmScopedPointer con(drmModeGetConnector(fd(), m_id)); + if (!con) { + return false; + } + return con->connection == DRM_MODE_CONNECTED; +} + +} diff --git a/plugins/platforms/drm/drm_object_connector.h b/plugins/platforms/drm/drm_object_connector.h new file mode 100644 index 0000000..7e7cc44 --- /dev/null +++ b/plugins/platforms/drm/drm_object_connector.h @@ -0,0 +1,46 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_DRM_OBJECT_CONNECTOR_H +#define KWIN_DRM_OBJECT_CONNECTOR_H + +#include "drm_object.h" + +namespace KWin +{ + +class DrmConnector : public DrmObject +{ +public: + DrmConnector(uint32_t connector_id, int fd); + + ~DrmConnector() override; + + bool atomicInit() override; + + enum class PropertyIndex { + CrtcId = 0, + Count + }; + + QVector encoders() { + return m_encoders; + } + + bool initProps() override; + bool isConnected(); + + +private: + QVector m_encoders; +}; + +} + +#endif + diff --git a/plugins/platforms/drm/drm_object_crtc.cpp b/plugins/platforms/drm/drm_object_crtc.cpp new file mode 100644 index 0000000..4f8ff86 --- /dev/null +++ b/plugins/platforms/drm/drm_object_crtc.cpp @@ -0,0 +1,122 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "drm_object_crtc.h" +#include "drm_backend.h" +#include "drm_output.h" +#include "drm_buffer.h" +#include "drm_pointer.h" +#include "logging.h" + +namespace KWin +{ + +DrmCrtc::DrmCrtc(uint32_t crtc_id, DrmBackend *backend, int resIndex) + : DrmObject(crtc_id, backend->fd()), + m_resIndex(resIndex), + m_backend(backend) +{ + DrmScopedPointer modeCrtc(drmModeGetCrtc(backend->fd(), crtc_id)); + if (modeCrtc) { + m_gammaRampSize = modeCrtc->gamma_size; + } +} + +DrmCrtc::~DrmCrtc() +{ +} + +bool DrmCrtc::atomicInit() +{ + qCDebug(KWIN_DRM) << "Atomic init for CRTC:" << resIndex() << "id:" << m_id; + + if (!initProps()) { + return false; + } + return true; +} + +bool DrmCrtc::initProps() +{ + setPropertyNames({ + QByteArrayLiteral("MODE_ID"), + QByteArrayLiteral("ACTIVE"), + }); + + DrmScopedPointer properties( + drmModeObjectGetProperties(fd(), m_id, DRM_MODE_OBJECT_CRTC)); + if (!properties) { + qCWarning(KWIN_DRM) << "Failed to get properties for crtc " << m_id ; + return false; + } + + int propCount = int(PropertyIndex::Count); + for (int j = 0; j < propCount; ++j) { + initProp(j, properties.data()); + } + + return true; +} + +void DrmCrtc::flipBuffer() +{ + if (m_currentBuffer && m_backend->deleteBufferAfterPageFlip() && m_currentBuffer != m_nextBuffer) { + delete m_currentBuffer; + } + m_currentBuffer = m_nextBuffer; + m_nextBuffer = nullptr; + + delete m_blackBuffer; + m_blackBuffer = nullptr; +} + +bool DrmCrtc::blank() +{ + if (!m_output) { + return false; + } + + if (m_backend->atomicModeSetting()) { + return false; + } + + if (!m_blackBuffer) { + DrmDumbBuffer *blackBuffer = m_backend->createBuffer(m_output->pixelSize()); + if (!blackBuffer->map()) { + delete blackBuffer; + return false; + } + blackBuffer->image()->fill(Qt::black); + m_blackBuffer = blackBuffer; + } + + if (m_output->setModeLegacy(m_blackBuffer)) { + if (m_currentBuffer && m_backend->deleteBufferAfterPageFlip()) { + delete m_currentBuffer; + delete m_nextBuffer; + } + m_currentBuffer = nullptr; + m_nextBuffer = nullptr; + return true; + } + return false; +} + +bool DrmCrtc::setGammaRamp(const GammaRamp &gamma) +{ + uint16_t *red = const_cast(gamma.red()); + uint16_t *green = const_cast(gamma.green()); + uint16_t *blue = const_cast(gamma.blue()); + + const bool isError = drmModeCrtcSetGamma(m_backend->fd(), m_id, + gamma.size(), red, green, blue); + + return !isError; +} + +} diff --git a/plugins/platforms/drm/drm_object_crtc.h b/plugins/platforms/drm/drm_object_crtc.h new file mode 100644 index 0000000..c750ea2 --- /dev/null +++ b/plugins/platforms/drm/drm_object_crtc.h @@ -0,0 +1,74 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_DRM_OBJECT_CRTC_H +#define KWIN_DRM_OBJECT_CRTC_H + +#include "drm_object.h" + +namespace KWin +{ + +class DrmBackend; +class DrmBuffer; +class DrmDumbBuffer; +class GammaRamp; + +class DrmCrtc : public DrmObject +{ +public: + DrmCrtc(uint32_t crtc_id, DrmBackend *backend, int resIndex); + + ~DrmCrtc() override; + + bool atomicInit() override; + + enum class PropertyIndex { + ModeId = 0, + Active, + Count + }; + + bool initProps() override; + + int resIndex() const { + return m_resIndex; + } + + DrmBuffer *current() { + return m_currentBuffer; + } + DrmBuffer *next() { + return m_nextBuffer; + } + void setNext(DrmBuffer *buffer) { + m_nextBuffer = buffer; + } + + void flipBuffer(); + bool blank(); + + int gammaRampSize() const { + return m_gammaRampSize; + } + bool setGammaRamp(const GammaRamp &gamma); + +private: + int m_resIndex; + uint32_t m_gammaRampSize = 0; + + DrmBuffer *m_currentBuffer = nullptr; + DrmBuffer *m_nextBuffer = nullptr; + DrmDumbBuffer *m_blackBuffer = nullptr; + DrmBackend *m_backend; +}; + +} + +#endif + diff --git a/plugins/platforms/drm/drm_object_plane.cpp b/plugins/platforms/drm/drm_object_plane.cpp new file mode 100644 index 0000000..7cf8476 --- /dev/null +++ b/plugins/platforms/drm/drm_object_plane.cpp @@ -0,0 +1,179 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "drm_object_plane.h" +#include "drm_buffer.h" +#include "drm_pointer.h" +#include "logging.h" + +namespace KWin +{ + +DrmPlane::DrmPlane(uint32_t plane_id, int fd) + : DrmObject(plane_id, fd) +{ +} + +DrmPlane::~DrmPlane() +{ + delete m_current; + delete m_next; +} + +bool DrmPlane::atomicInit() +{ + qCDebug(KWIN_DRM) << "Atomic init for plane:" << m_id; + DrmScopedPointer p(drmModeGetPlane(fd(), m_id)); + + if (!p) { + qCWarning(KWIN_DRM) << "Failed to get kernel plane" << m_id; + return false; + } + + m_possibleCrtcs = p->possible_crtcs; + + int count_formats = p->count_formats; + m_formats.resize(count_formats); + for (int i = 0; i < count_formats; i++) { + m_formats[i] = p->formats[i]; + } + + if (!initProps()) { + return false; + } + return true; +} + +bool DrmPlane::atomicPopulate(drmModeAtomicReq *req) const +{ + return doAtomicPopulate(req, 1); +} + +bool DrmPlane::initProps() +{ + setPropertyNames( { + QByteArrayLiteral("type"), + QByteArrayLiteral("SRC_X"), + QByteArrayLiteral("SRC_Y"), + QByteArrayLiteral("SRC_W"), + QByteArrayLiteral("SRC_H"), + QByteArrayLiteral("CRTC_X"), + QByteArrayLiteral("CRTC_Y"), + QByteArrayLiteral("CRTC_W"), + QByteArrayLiteral("CRTC_H"), + QByteArrayLiteral("FB_ID"), + QByteArrayLiteral("CRTC_ID"), + QByteArrayLiteral("rotation") + }); + + QVector typeNames = { + QByteArrayLiteral("Overlay"), + QByteArrayLiteral("Primary"), + QByteArrayLiteral("Cursor") + }; + + const QVector rotationNames{ + QByteArrayLiteral("rotate-0"), + QByteArrayLiteral("rotate-90"), + QByteArrayLiteral("rotate-180"), + QByteArrayLiteral("rotate-270"), + QByteArrayLiteral("reflect-x"), + QByteArrayLiteral("reflect-y") + }; + + DrmScopedPointer properties( + drmModeObjectGetProperties(fd(), m_id, DRM_MODE_OBJECT_PLANE)); + if (!properties){ + qCWarning(KWIN_DRM) << "Failed to get properties for plane " << m_id ; + return false; + } + + int propCount = int(PropertyIndex::Count); + for (int j = 0; j < propCount; ++j) { + if (j == int(PropertyIndex::Type)) { + initProp(j, properties.data(), typeNames); + } else if (j == int(PropertyIndex::Rotation)) { + initProp(j, properties.data(), rotationNames); + m_supportedTransformations = Transformations(); + + auto checkSupport = [j, this] (uint64_t value, Transformation t) { + if (propHasEnum(j, value)) { + m_supportedTransformations |= t; + } + }; + checkSupport(0, Transformation::Rotate0); + checkSupport(1, Transformation::Rotate90); + checkSupport(2, Transformation::Rotate180); + checkSupport(3, Transformation::Rotate270); + checkSupport(4, Transformation::ReflectX); + checkSupport(5, Transformation::ReflectY); + + qCDebug(KWIN_DRM) << "Supported Transformations on plane" << m_id << "are" << m_supportedTransformations; + } else { + initProp(j, properties.data()); + } + } + + return true; +} + +DrmPlane::TypeIndex DrmPlane::type() +{ + auto property = m_props.at(int(PropertyIndex::Type)); + if (!property) { + return TypeIndex::Overlay; + } + int typeCount = int(TypeIndex::Count); + for (int i = 0; i < typeCount; i++) { + if (property->enumMap(i) == property->value()) { + return TypeIndex(i); + } + } + return TypeIndex::Overlay; +} + +void DrmPlane::setNext(DrmBuffer *b) +{ + if (auto property = m_props.at(int(PropertyIndex::FbId))) { + property->setValue(b ? b->bufferId() : 0); + } + m_next = b; +} + +void DrmPlane::setTransformation(Transformations t) +{ + // TODO: When being pedantic, this should go through the enum mapping. Just remember + // that these are the shift distance, not the shifted value. + if (auto property = m_props.at(int(PropertyIndex::Rotation))) { + property->setValue(int(t)); + } +} + +DrmPlane::Transformations DrmPlane::transformation() +{ + if (auto property = m_props.at(int(PropertyIndex::Rotation))) { + return Transformations(int(property->value())); + } + return Transformations(Transformation::Rotate0); +} + +void DrmPlane::flipBuffer() +{ + m_current = m_next; + m_next = nullptr; +} + +void DrmPlane::flipBufferWithDelete() +{ + if (m_current != m_next) { + delete m_current; + } + flipBuffer(); +} + +} diff --git a/plugins/platforms/drm/drm_object_plane.h b/plugins/platforms/drm/drm_object_plane.h new file mode 100644 index 0000000..df1b92a --- /dev/null +++ b/plugins/platforms/drm/drm_object_plane.h @@ -0,0 +1,114 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "drm_object.h" + +#include +#include + +namespace KWin +{ + +class DrmBuffer; + +class DrmPlane : public DrmObject +{ + Q_GADGET +public: + DrmPlane(uint32_t plane_id, int fd); + + ~DrmPlane() override; + + enum class PropertyIndex { + Type = 0, + SrcX, + SrcY, + SrcW, + SrcH, + CrtcX, + CrtcY, + CrtcW, + CrtcH, + FbId, + CrtcId, + Rotation, + Count + }; + Q_ENUM(PropertyIndex) + + enum class TypeIndex { + Overlay = 0, + Primary, + Cursor, + Count + }; + Q_ENUM(TypeIndex) + + enum class Transformation { + Rotate0 = 1 << 0, + Rotate90 = 1 << 1, + Rotate180 = 1 << 2, + Rotate270 = 1 << 3, + ReflectX = 1 << 4, + ReflectY = 1 << 5 + }; + Q_ENUM(Transformation) + Q_DECLARE_FLAGS(Transformations, Transformation); + + bool atomicInit() override; + bool initProps() override; + TypeIndex type(); + + bool isCrtcSupported(int resIndex) const { + return (m_possibleCrtcs & (1 << resIndex)); + } + QVector formats() const { + return m_formats; + } + + DrmBuffer *current() const { + return m_current; + } + DrmBuffer *next() const { + return m_next; + } + void setCurrent(DrmBuffer *b) { + m_current = b; + } + void setNext(DrmBuffer *b); + void setTransformation(Transformations t); + Transformations transformation(); + + void flipBuffer(); + void flipBufferWithDelete(); + + Transformations supportedTransformations() const { + return m_supportedTransformations; + } + + bool atomicPopulate(drmModeAtomicReq *req) const override; + +private: + DrmBuffer *m_current = nullptr; + DrmBuffer *m_next = nullptr; + + // TODO: See weston drm_output_check_plane_format for future use of these member variables + QVector m_formats; // Possible formats, which can be presented on this plane + + // TODO: when using overlay planes in the future: restrict possible screens / crtcs of planes + uint32_t m_possibleCrtcs; + + Transformations m_supportedTransformations = Transformation::Rotate0; +}; + +} + +Q_DECLARE_OPERATORS_FOR_FLAGS(KWin::DrmPlane::Transformations) + diff --git a/plugins/platforms/drm/drm_output.cpp b/plugins/platforms/drm/drm_output.cpp new file mode 100644 index 0000000..6fe8485 --- /dev/null +++ b/plugins/platforms/drm/drm_output.cpp @@ -0,0 +1,1045 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "drm_output.h" +#include "drm_backend.h" +#include "drm_object_plane.h" +#include "drm_object_crtc.h" +#include "drm_object_connector.h" + +#include "composite.h" +#include "cursor.h" +#include "logind.h" +#include "logging.h" +#include "main.h" +#include "screens_drm.h" +#include "wayland_server.h" +// KWayland +#include +// KF5 +#include +#include +#include +// Qt +#include +#include +#include +// c++ +#include +// drm +#include +#include +#include + +namespace KWin +{ + +DrmOutput::DrmOutput(DrmBackend *backend) + : AbstractWaylandOutput(backend) + , m_backend(backend) +{ +} + +DrmOutput::~DrmOutput() +{ + Q_ASSERT(!m_pageFlipPending); + teardown(); +} + +void DrmOutput::teardown() +{ + if (m_deleted) { + return; + } + m_deleted = true; + hideCursor(); + m_crtc->blank(); + + if (m_primaryPlane) { + // TODO: when having multiple planes, also clean up these + m_primaryPlane->setOutput(nullptr); + + if (m_backend->deleteBufferAfterPageFlip()) { + delete m_primaryPlane->current(); + } + m_primaryPlane->setCurrent(nullptr); + } + if (m_cursorPlane) { + m_cursorPlane->setOutput(nullptr); + } + + m_crtc->setOutput(nullptr); + m_conn->setOutput(nullptr); + + m_cursor[0].reset(nullptr); + m_cursor[1].reset(nullptr); + if (!m_pageFlipPending) { + deleteLater(); + } //else will be deleted in the page flip handler + //this is needed so that the pageflipcallback handle isn't deleted +} + +void DrmOutput::releaseGbm() +{ + if (DrmBuffer *b = m_crtc->current()) { + b->releaseGbm(); + } + if (m_primaryPlane && m_primaryPlane->current()) { + m_primaryPlane->current()->releaseGbm(); + } +} + +bool DrmOutput::hideCursor() +{ + return drmModeSetCursor(m_backend->fd(), m_crtc->id(), 0, 0, 0) == 0; +} + +bool DrmOutput::showCursor(DrmDumbBuffer *c) +{ + const QSize &s = c->size(); + return drmModeSetCursor(m_backend->fd(), m_crtc->id(), c->handle(), s.width(), s.height()) == 0; +} + +bool DrmOutput::showCursor() +{ + if (m_deleted) { + return false; + } + + if (Q_UNLIKELY(m_backend->usesSoftwareCursor())) { + qCCritical(KWIN_DRM) << "DrmOutput::showCursor should never be called when software cursor is enabled"; + return true; + } + + const bool ret = showCursor(m_cursor[m_cursorIndex].data()); + if (!ret) { + return ret; + } + + if (m_hasNewCursor) { + m_cursorIndex = (m_cursorIndex + 1) % 2; + m_hasNewCursor = false; + } + + return ret; +} + +void DrmOutput::updateCursor() +{ + if (m_deleted) { + return; + } + const Cursor *cursor = Cursors::self()->currentCursor(); + const QImage cursorImage = cursor->image(); + if (cursorImage.isNull()) { + return; + } + m_hasNewCursor = true; + QImage *c = m_cursor[m_cursorIndex]->image(); + c->fill(Qt::transparent); + + QPainter p; + p.begin(c); + p.setWorldTransform(logicalToNativeMatrix(cursor->rect(), scale(), transform()).toTransform()); + p.drawImage(QPoint(0, 0), cursorImage); + p.end(); +} + +void DrmOutput::moveCursor(Cursor* cursor, const QPoint &globalPos) +{ + const QMatrix4x4 hotspotMatrix = logicalToNativeMatrix(cursor->rect(), scale(), transform()); + const QMatrix4x4 monitorMatrix = logicalToNativeMatrix(geometry(), scale(), transform()); + + QPoint pos = monitorMatrix.map(globalPos); + pos -= hotspotMatrix.map(cursor->hotspot()); + + drmModeMoveCursor(m_backend->fd(), m_crtc->id(), pos.x(), pos.y()); +} + +static QHash s_connectorNames = { + {DRM_MODE_CONNECTOR_Unknown, QByteArrayLiteral("Unknown")}, + {DRM_MODE_CONNECTOR_VGA, QByteArrayLiteral("VGA")}, + {DRM_MODE_CONNECTOR_DVII, QByteArrayLiteral("DVI-I")}, + {DRM_MODE_CONNECTOR_DVID, QByteArrayLiteral("DVI-D")}, + {DRM_MODE_CONNECTOR_DVIA, QByteArrayLiteral("DVI-A")}, + {DRM_MODE_CONNECTOR_Composite, QByteArrayLiteral("Composite")}, + {DRM_MODE_CONNECTOR_SVIDEO, QByteArrayLiteral("SVIDEO")}, + {DRM_MODE_CONNECTOR_LVDS, QByteArrayLiteral("LVDS")}, + {DRM_MODE_CONNECTOR_Component, QByteArrayLiteral("Component")}, + {DRM_MODE_CONNECTOR_9PinDIN, QByteArrayLiteral("DIN")}, + {DRM_MODE_CONNECTOR_DisplayPort, QByteArrayLiteral("DP")}, + {DRM_MODE_CONNECTOR_HDMIA, QByteArrayLiteral("HDMI-A")}, + {DRM_MODE_CONNECTOR_HDMIB, QByteArrayLiteral("HDMI-B")}, + {DRM_MODE_CONNECTOR_TV, QByteArrayLiteral("TV")}, + {DRM_MODE_CONNECTOR_eDP, QByteArrayLiteral("eDP")}, + {DRM_MODE_CONNECTOR_VIRTUAL, QByteArrayLiteral("Virtual")}, + {DRM_MODE_CONNECTOR_DSI, QByteArrayLiteral("DSI")}, +#ifdef DRM_MODE_CONNECTOR_DPI + {DRM_MODE_CONNECTOR_DPI, QByteArrayLiteral("DPI")}, +#endif +}; + +namespace { +quint64 refreshRateForMode(_drmModeModeInfo *m) +{ + // Calculate higher precision (mHz) refresh rate + // logic based on Weston, see compositor-drm.c + quint64 refreshRate = (m->clock * 1000000LL / m->htotal + m->vtotal / 2) / m->vtotal; + if (m->flags & DRM_MODE_FLAG_INTERLACE) { + refreshRate *= 2; + } + if (m->flags & DRM_MODE_FLAG_DBLSCAN) { + refreshRate /= 2; + } + if (m->vscan > 1) { + refreshRate /= m->vscan; + } + return refreshRate; +} +} + +bool DrmOutput::init(drmModeConnector *connector) +{ + initEdid(connector); + initDpms(connector); + initUuid(); + if (m_backend->atomicModeSetting()) { + if (!initPrimaryPlane()) { + return false; + } + } + + setInternal(connector->connector_type == DRM_MODE_CONNECTOR_LVDS || connector->connector_type == DRM_MODE_CONNECTOR_eDP + || connector->connector_type == DRM_MODE_CONNECTOR_DSI); + setDpmsSupported(true); + initOutputDevice(connector); + + if (!m_backend->atomicModeSetting() && !m_crtc->blank()) { + // We use legacy mode and the initial output blank failed. + return false; + } + + updateDpms(KWaylandServer::OutputInterface::DpmsMode::On); + return true; +} + +void DrmOutput::initUuid() +{ + QCryptographicHash hash(QCryptographicHash::Md5); + hash.addData(QByteArray::number(m_conn->id())); + hash.addData(m_edid.eisaId()); + hash.addData(m_edid.monitorName()); + hash.addData(m_edid.serialNumber()); + m_uuid = hash.result().toHex().left(10); +} + +void DrmOutput::initOutputDevice(drmModeConnector *connector) +{ + QString manufacturer; + if (!m_edid.vendor().isEmpty()) { + manufacturer = QString::fromLatin1(m_edid.vendor()); + } else if (!m_edid.eisaId().isEmpty()) { + manufacturer = QString::fromLatin1(m_edid.eisaId()); + } + + QString connectorName = s_connectorNames.value(connector->connector_type, QByteArrayLiteral("Unknown")) + QStringLiteral("-") + QString::number(connector->connector_type_id); + QString modelName; + + if (!m_edid.monitorName().isEmpty()) { + QString m = QString::fromLatin1(m_edid.monitorName()); + if (!m_edid.serialNumber().isEmpty()) { + m.append('/'); + m.append(QString::fromLatin1(m_edid.serialNumber())); + } + modelName = m; + } else if (!m_edid.serialNumber().isEmpty()) { + modelName = QString::fromLatin1(m_edid.serialNumber()); + } else { + modelName = i18n("unknown"); + } + + const QString model = connectorName + QStringLiteral("-") + modelName; + + // read in mode information + QVector modes; + for (int i = 0; i < connector->count_modes; ++i) { + // TODO: in AMS here we could read and store for later every mode's blob_id + // would simplify isCurrentMode(..) and presentAtomically(..) in case of mode set + auto *m = &connector->modes[i]; + KWaylandServer::OutputDeviceInterface::ModeFlags deviceflags; + if (isCurrentMode(m)) { + deviceflags |= KWaylandServer::OutputDeviceInterface::ModeFlag::Current; + } + if (m->type & DRM_MODE_TYPE_PREFERRED) { + deviceflags |= KWaylandServer::OutputDeviceInterface::ModeFlag::Preferred; + } + + KWaylandServer::OutputDeviceInterface::Mode mode; + mode.id = i; + mode.size = QSize(m->hdisplay, m->vdisplay); + mode.flags = deviceflags; + mode.refreshRate = refreshRateForMode(m); + modes << mode; + } + + QSize physicalSize = !m_edid.physicalSize().isEmpty() ? m_edid.physicalSize() : QSize(connector->mmWidth, connector->mmHeight); + // the size might be completely borked. E.g. Samsung SyncMaster 2494HS reports 160x90 while in truth it's 520x292 + // as this information is used to calculate DPI info, it's going to result in everything being huge + const QByteArray unknown = QByteArrayLiteral("unknown"); + KConfigGroup group = kwinApp()->config()->group("EdidOverwrite").group(m_edid.eisaId().isEmpty() ? unknown : m_edid.eisaId()) + .group(m_edid.monitorName().isEmpty() ? unknown : m_edid.monitorName()) + .group(m_edid.serialNumber().isEmpty() ? unknown : m_edid.serialNumber()); + if (group.hasKey("PhysicalSize")) { + const QSize overwriteSize = group.readEntry("PhysicalSize", physicalSize); + qCWarning(KWIN_DRM) << "Overwriting monitor physical size for" << m_edid.eisaId() << "/" << m_edid.monitorName() << "/" << m_edid.serialNumber() << " from " << physicalSize << "to " << overwriteSize; + physicalSize = overwriteSize; + } + setName(connectorName); + initInterfaces(model, manufacturer, m_uuid, physicalSize, modes); +} + +bool DrmOutput::isCurrentMode(const drmModeModeInfo *mode) const +{ + return mode->clock == m_mode.clock + && mode->hdisplay == m_mode.hdisplay + && mode->hsync_start == m_mode.hsync_start + && mode->hsync_end == m_mode.hsync_end + && mode->htotal == m_mode.htotal + && mode->hskew == m_mode.hskew + && mode->vdisplay == m_mode.vdisplay + && mode->vsync_start == m_mode.vsync_start + && mode->vsync_end == m_mode.vsync_end + && mode->vtotal == m_mode.vtotal + && mode->vscan == m_mode.vscan + && mode->vrefresh == m_mode.vrefresh + && mode->flags == m_mode.flags + && mode->type == m_mode.type + && qstrcmp(mode->name, m_mode.name) == 0; +} +void DrmOutput::initEdid(drmModeConnector *connector) +{ + DrmScopedPointer edid; + for (int i = 0; i < connector->count_props; ++i) { + DrmScopedPointer property(drmModeGetProperty(m_backend->fd(), connector->props[i])); + if (!property) { + continue; + } + if ((property->flags & DRM_MODE_PROP_BLOB) && qstrcmp(property->name, "EDID") == 0) { + edid.reset(drmModeGetPropertyBlob(m_backend->fd(), connector->prop_values[i])); + } + } + if (!edid) { + return; + } + + m_edid = Edid(edid->data, edid->length); + if (!m_edid.isValid()) { + qCWarning(KWIN_DRM, "Couldn't parse EDID for connector with id %d", m_conn->id()); + } +} + +bool DrmOutput::initPrimaryPlane() +{ + for (int i = 0; i < m_backend->planes().size(); ++i) { + DrmPlane* p = m_backend->planes()[i]; + if (!p) { + continue; + } + if (p->type() != DrmPlane::TypeIndex::Primary) { + continue; + } + if (p->output()) { // Plane already has an output + continue; + } + if (m_primaryPlane) { // Output already has a primary plane + continue; + } + if (!p->isCrtcSupported(m_crtc->resIndex())) { + continue; + } + p->setOutput(this); + m_primaryPlane = p; + qCDebug(KWIN_DRM) << "Initialized primary plane" << p->id() << "on CRTC" << m_crtc->id(); + return true; + } + qCCritical(KWIN_DRM) << "Failed to initialize primary plane."; + return false; +} + +bool DrmOutput::initCursorPlane() // TODO: Add call in init (but needs layer support in general first) +{ + for (int i = 0; i < m_backend->planes().size(); ++i) { + DrmPlane* p = m_backend->planes()[i]; + if (!p) { + continue; + } + if (p->type() != DrmPlane::TypeIndex::Cursor) { + continue; + } + if (p->output()) { // Plane already has an output + continue; + } + if (m_cursorPlane) { // Output already has a cursor plane + continue; + } + if (!p->isCrtcSupported(m_crtc->resIndex())) { + continue; + } + p->setOutput(this); + m_cursorPlane = p; + qCDebug(KWIN_DRM) << "Initialized cursor plane" << p->id() << "on CRTC" << m_crtc->id(); + return true; + } + return false; +} + +bool DrmOutput::initCursor(const QSize &cursorSize) +{ + auto createCursor = [this, cursorSize] (int index) { + m_cursor[index].reset(m_backend->createBuffer(cursorSize)); + if (!m_cursor[index]->map(QImage::Format_ARGB32_Premultiplied)) { + return false; + } + return true; + }; + if (!createCursor(0) || !createCursor(1)) { + return false; + } + return true; +} + +void DrmOutput::initDpms(drmModeConnector *connector) +{ + for (int i = 0; i < connector->count_props; ++i) { + DrmScopedPointer property(drmModeGetProperty(m_backend->fd(), connector->props[i])); + if (!property) { + continue; + } + if (qstrcmp(property->name, "DPMS") == 0) { + m_dpms.swap(property); + break; + } + } +} + +void DrmOutput::updateEnablement(bool enable) +{ + if (enable) { + m_dpmsModePending = DpmsMode::On; + if (m_backend->atomicModeSetting()) { + atomicEnable(); + } else { + if (dpmsLegacyApply()) { + m_backend->enableOutput(this, true); + } + } + + } else { + m_dpmsModePending = DpmsMode::Off; + if (m_backend->atomicModeSetting()) { + atomicDisable(); + } else { + if (dpmsLegacyApply()) { + m_backend->enableOutput(this, false); + } + } + } +} + +void DrmOutput::atomicEnable() +{ + m_modesetRequested = true; + + if (m_atomicOffPending) { + Q_ASSERT(m_pageFlipPending); + m_atomicOffPending = false; + } + m_backend->enableOutput(this, true); + + if (Compositor *compositor = Compositor::self()) { + compositor->addRepaintFull(); + } +} + +void DrmOutput::atomicDisable() +{ + m_modesetRequested = true; + + m_backend->enableOutput(this, false); + m_atomicOffPending = true; + if (!m_pageFlipPending) { + dpmsAtomicOff(); + } +} + +static DrmOutput::DpmsMode fromWaylandDpmsMode(KWaylandServer::OutputInterface::DpmsMode wlMode) +{ + using namespace KWaylandServer; + switch (wlMode) { + case OutputInterface::DpmsMode::On: + return DrmOutput::DpmsMode::On; + case OutputInterface::DpmsMode::Standby: + return DrmOutput::DpmsMode::Standby; + case OutputInterface::DpmsMode::Suspend: + return DrmOutput::DpmsMode::Suspend; + case OutputInterface::DpmsMode::Off: + return DrmOutput::DpmsMode::Off; + default: + Q_UNREACHABLE(); + } +} + +static KWaylandServer::OutputInterface::DpmsMode toWaylandDpmsMode(DrmOutput::DpmsMode mode) +{ + using namespace KWaylandServer; + switch (mode) { + case DrmOutput::DpmsMode::On: + return OutputInterface::DpmsMode::On; + case DrmOutput::DpmsMode::Standby: + return OutputInterface::DpmsMode::Standby; + case DrmOutput::DpmsMode::Suspend: + return OutputInterface::DpmsMode::Suspend; + case DrmOutput::DpmsMode::Off: + return OutputInterface::DpmsMode::Off; + default: + Q_UNREACHABLE(); + } +} + +void DrmOutput::updateDpms(KWaylandServer::OutputInterface::DpmsMode mode) +{ + if (m_dpms.isNull() || !isEnabled()) { + return; + } + + const auto drmMode = fromWaylandDpmsMode(mode); + + if (drmMode == m_dpmsModePending) { + qCDebug(KWIN_DRM) << "New DPMS mode equals old mode. DPMS unchanged."; + waylandOutput()->setDpmsMode(mode); + return; + } + + m_dpmsModePending = drmMode; + + if (m_backend->atomicModeSetting()) { + m_modesetRequested = true; + if (drmMode == DpmsMode::On) { + if (m_atomicOffPending) { + Q_ASSERT(m_pageFlipPending); + m_atomicOffPending = false; + } + dpmsFinishOn(); + } else { + m_atomicOffPending = true; + if (!m_pageFlipPending) { + dpmsAtomicOff(); + } + } + } else { + dpmsLegacyApply(); + } +} + +void DrmOutput::dpmsFinishOn() +{ + qCDebug(KWIN_DRM) << "DPMS mode set for output" << m_crtc->id() << "to On."; + + waylandOutput()->setDpmsMode(toWaylandDpmsMode(DpmsMode::On)); + + m_backend->checkOutputsAreOn(); + if (!m_backend->atomicModeSetting()) { + m_crtc->blank(); + } + if (Compositor *compositor = Compositor::self()) { + compositor->addRepaintFull(); + } +} + +void DrmOutput::dpmsFinishOff() +{ + qCDebug(KWIN_DRM) << "DPMS mode set for output" << m_crtc->id() << "to Off."; + + if (isEnabled()) { + waylandOutput()->setDpmsMode(toWaylandDpmsMode(m_dpmsModePending)); + m_backend->createDpmsFilter(); + } else { + waylandOutput()->setDpmsMode(toWaylandDpmsMode(DpmsMode::Off)); + } +} + +bool DrmOutput::dpmsLegacyApply() +{ + if (drmModeConnectorSetProperty(m_backend->fd(), m_conn->id(), + m_dpms->prop_id, uint64_t(m_dpmsModePending)) < 0) { + m_dpmsModePending = m_dpmsMode; + qCWarning(KWIN_DRM) << "Setting DPMS failed"; + return false; + } + if (m_dpmsModePending == DpmsMode::On) { + dpmsFinishOn(); + } else { + dpmsFinishOff(); + } + m_dpmsMode = m_dpmsModePending; + return true; +} + +DrmPlane::Transformations outputToPlaneTransform(DrmOutput::Transform transform) + { + using OutTrans = DrmOutput::Transform; + using PlaneTrans = DrmPlane::Transformation; + + // TODO: Do we want to support reflections (flips)? + + switch (transform) { + case OutTrans::Normal: + case OutTrans::Flipped: + return PlaneTrans::Rotate0; + case OutTrans::Rotated90: + case OutTrans::Flipped90: + return PlaneTrans::Rotate90; + case OutTrans::Rotated180: + case OutTrans::Flipped180: + return PlaneTrans::Rotate180; + case OutTrans::Rotated270: + case OutTrans::Flipped270: + return PlaneTrans::Rotate270; + default: + Q_UNREACHABLE(); + } +} + +bool DrmOutput::hardwareTransforms() const +{ + if (!m_primaryPlane) { + return false; + } + return m_primaryPlane->transformation() == outputToPlaneTransform(transform()); +} + +void DrmOutput::updateTransform(Transform transform) +{ + const auto planeTransform = outputToPlaneTransform(transform); + + if (m_primaryPlane) { + // At the moment we have to exclude hardware transforms for vertical buffers. + // For that we need to support other buffers and graceful fallback from atomic tests. + // Reason is that standard linear buffers are not suitable. + const bool isPortrait = transform == Transform::Rotated90 + || transform == Transform::Flipped90 + || transform == Transform::Rotated270 + || transform == Transform::Flipped270; + + if (!qEnvironmentVariableIsSet("KWIN_DRM_SW_ROTATIONS_ONLY") && + (m_primaryPlane->supportedTransformations() & planeTransform) && + !isPortrait) { + m_primaryPlane->setTransformation(planeTransform); + } else { + m_primaryPlane->setTransformation(DrmPlane::Transformation::Rotate0); + } + } + m_modesetRequested = true; + + // show cursor only if is enabled, i.e if pointer device is presentP + if (m_backend->isCursorEnabled() && !m_backend->usesSoftwareCursor()) { + // the cursor might need to get rotated + updateCursor(); + showCursor(); + } +} + +void DrmOutput::updateMode(int modeIndex) +{ + // get all modes on the connector + DrmScopedPointer connector(drmModeGetConnector(m_backend->fd(), m_conn->id())); + if (connector->count_modes <= modeIndex) { + // TODO: error? + return; + } + if (isCurrentMode(&connector->modes[modeIndex])) { + // nothing to do + return; + } + m_mode = connector->modes[modeIndex]; + m_modesetRequested = true; + setWaylandMode(); +} + +void DrmOutput::setWaylandMode() +{ + AbstractWaylandOutput::setWaylandMode(QSize(m_mode.hdisplay, m_mode.vdisplay), + refreshRateForMode(&m_mode)); +} + +void DrmOutput::pageFlipped() +{ + // In legacy mode we might get a page flip through a blank. + Q_ASSERT(m_pageFlipPending || !m_backend->atomicModeSetting()); + m_pageFlipPending = false; + + if (m_deleted) { + deleteLater(); + return; + } + + if (!m_crtc) { + return; + } + // Egl based surface buffers get destroyed, QPainter based dumb buffers not + // TODO: split up DrmOutput in two for dumb and egl/gbm surface buffer compatible subclasses completely? + if (m_backend->deleteBufferAfterPageFlip()) { + if (m_backend->atomicModeSetting()) { + if (!m_primaryPlane->next()) { + // on manual vt switch + // TODO: when we later use overlay planes it might happen, that we have a page flip with only + // damage on one of these, and therefore the primary plane has no next buffer + // -> Then we don't want to return here! + if (m_primaryPlane->current()) { + m_primaryPlane->current()->releaseGbm(); + } + return; + } + for (DrmPlane *p : m_nextPlanesFlipList) { + p->flipBufferWithDelete(); + } + m_nextPlanesFlipList.clear(); + } else { + if (!m_crtc->next()) { + // on manual vt switch + if (DrmBuffer *b = m_crtc->current()) { + b->releaseGbm(); + } + } + m_crtc->flipBuffer(); + } + } else { + if (m_backend->atomicModeSetting()){ + for (DrmPlane *p : m_nextPlanesFlipList) { + p->flipBuffer(); + } + m_nextPlanesFlipList.clear(); + } else { + m_crtc->flipBuffer(); + } + m_crtc->flipBuffer(); + } + + if (m_atomicOffPending) { + dpmsAtomicOff(); + } +} + +bool DrmOutput::present(DrmBuffer *buffer) +{ + if (m_dpmsModePending != DpmsMode::On) { + return false; + } + if (m_backend->atomicModeSetting()) { + return presentAtomically(buffer); + } else { + return presentLegacy(buffer); + } +} + +bool DrmOutput::dpmsAtomicOff() +{ + m_atomicOffPending = false; + + // TODO: With multiple planes: deactivate all of them here + delete m_primaryPlane->next(); + m_primaryPlane->setNext(nullptr); + m_nextPlanesFlipList << m_primaryPlane; + + if (!doAtomicCommit(AtomicCommitMode::Test)) { + qCDebug(KWIN_DRM) << "Atomic test commit to Dpms Off failed. Aborting."; + return false; + } + if (!doAtomicCommit(AtomicCommitMode::Real)) { + qCDebug(KWIN_DRM) << "Atomic commit to Dpms Off failed. This should have never happened! Aborting."; + return false; + } + m_nextPlanesFlipList.clear(); + dpmsFinishOff(); + + return true; +} + +bool DrmOutput::presentAtomically(DrmBuffer *buffer) +{ + if (!LogindIntegration::self()->isActiveSession()) { + qCWarning(KWIN_DRM) << "Logind session not active."; + return false; + } + + if (m_pageFlipPending) { + qCWarning(KWIN_DRM) << "Page not yet flipped."; + return false; + } + +#if HAVE_EGL_STREAMS + if (m_backend->useEglStreams() && !m_modesetRequested) { + // EglStreamBackend queues normal page flips through EGL, + // modesets are still performed through DRM-KMS + m_pageFlipPending = true; + return true; + } +#endif + + m_primaryPlane->setNext(buffer); + m_nextPlanesFlipList << m_primaryPlane; + + if (!doAtomicCommit(AtomicCommitMode::Test)) { + //TODO: When we use planes for layered rendering, fallback to renderer instead. Also for direct scanout? + //TODO: Probably should undo setNext and reset the flip list + qCDebug(KWIN_DRM) << "Atomic test commit failed. Aborting present."; + // go back to previous state + if (m_lastWorkingState.valid) { + m_mode = m_lastWorkingState.mode; + setTransform(m_lastWorkingState.transform); + setGlobalPos(m_lastWorkingState.globalPos); + if (m_primaryPlane) { + m_primaryPlane->setTransformation(m_lastWorkingState.planeTransformations); + } + m_modesetRequested = true; + if (m_backend->isCursorEnabled()) { + // the cursor might need to get rotated + updateCursor(); + showCursor(); + } + // TODO: forward to OutputInterface and OutputDeviceInterface + setWaylandMode(); + emit screens()->changed(); + } + return false; + } + const bool wasModeset = m_modesetRequested; + if (!doAtomicCommit(AtomicCommitMode::Real)) { + qCDebug(KWIN_DRM) << "Atomic commit failed. This should have never happened! Aborting present."; + //TODO: Probably should undo setNext and reset the flip list + return false; + } + if (wasModeset) { + // store current mode set as new good state + m_lastWorkingState.mode = m_mode; + m_lastWorkingState.transform = transform(); + m_lastWorkingState.globalPos = globalPos(); + if (m_primaryPlane) { + m_lastWorkingState.planeTransformations = m_primaryPlane->transformation(); + } + m_lastWorkingState.valid = true; + } + m_pageFlipPending = true; + return true; +} + +bool DrmOutput::presentLegacy(DrmBuffer *buffer) +{ + if (m_crtc->next()) { + return false; + } + if (!LogindIntegration::self()->isActiveSession()) { + m_crtc->setNext(buffer); + return false; + } + + // Do we need to set a new mode first? + if (!m_crtc->current() || m_crtc->current()->needsModeChange(buffer)) { + if (!setModeLegacy(buffer)) { + return false; + } + } + const bool ok = drmModePageFlip(m_backend->fd(), m_crtc->id(), buffer->bufferId(), DRM_MODE_PAGE_FLIP_EVENT, this) == 0; + if (ok) { + m_crtc->setNext(buffer); + } else { + qCWarning(KWIN_DRM) << "Page flip failed:" << strerror(errno); + } + return ok; +} + +bool DrmOutput::setModeLegacy(DrmBuffer *buffer) +{ + uint32_t connId = m_conn->id(); + if (drmModeSetCrtc(m_backend->fd(), m_crtc->id(), buffer->bufferId(), 0, 0, &connId, 1, &m_mode) == 0) { + return true; + } else { + qCWarning(KWIN_DRM) << "Mode setting failed"; + return false; + } +} + +bool DrmOutput::doAtomicCommit(AtomicCommitMode mode) +{ + drmModeAtomicReq *req = drmModeAtomicAlloc(); + + auto errorHandler = [this, mode, req] () { + if (mode == AtomicCommitMode::Test) { + // TODO: when we later test overlay planes, make sure we change only the right stuff back + } + if (req) { + drmModeAtomicFree(req); + } + + if (m_dpmsMode != m_dpmsModePending) { + qCWarning(KWIN_DRM) << "Setting DPMS failed"; + m_dpmsModePending = m_dpmsMode; + if (m_dpmsMode != DpmsMode::On) { + dpmsFinishOff(); + } + } + + // TODO: see above, rework later for overlay planes! + for (DrmPlane *p : m_nextPlanesFlipList) { + p->setNext(nullptr); + } + m_nextPlanesFlipList.clear(); + + }; + + if (!req) { + qCWarning(KWIN_DRM) << "DRM: couldn't allocate atomic request"; + errorHandler(); + return false; + } + + uint32_t flags = 0; + + // Do we need to set a new mode? + if (m_modesetRequested) { + if (m_dpmsModePending == DpmsMode::On) { + if (drmModeCreatePropertyBlob(m_backend->fd(), &m_mode, sizeof(m_mode), &m_blobId) != 0) { + qCWarning(KWIN_DRM) << "Failed to create property blob"; + errorHandler(); + return false; + } + } + if (!atomicReqModesetPopulate(req, m_dpmsModePending == DpmsMode::On)){ + qCWarning(KWIN_DRM) << "Failed to populate Atomic Modeset"; + errorHandler(); + return false; + } + flags |= DRM_MODE_ATOMIC_ALLOW_MODESET; + } + + if (mode == AtomicCommitMode::Real) { + if (m_dpmsModePending == DpmsMode::On) { + if (!(flags & DRM_MODE_ATOMIC_ALLOW_MODESET)) { + // TODO: Evaluating this condition should only be necessary, as long as we expect older kernels than 4.10. + flags |= DRM_MODE_ATOMIC_NONBLOCK; + } + +#if HAVE_EGL_STREAMS + if (!m_backend->useEglStreams()) + // EglStreamBackend uses the NV_output_drm_flip_event EGL extension + // to register the flip event through eglStreamConsumerAcquireAttribNV +#endif + flags |= DRM_MODE_PAGE_FLIP_EVENT; + } + } else { + flags |= DRM_MODE_ATOMIC_TEST_ONLY; + } + + bool ret = true; + // TODO: Make sure when we use more than one plane at a time, that we go through this list in the right order. + for (int i = m_nextPlanesFlipList.size() - 1; 0 <= i; i-- ) { + DrmPlane *p = m_nextPlanesFlipList[i]; + ret &= p->atomicPopulate(req); + } + + if (!ret) { + qCWarning(KWIN_DRM) << "Failed to populate atomic planes. Abort atomic commit!"; + errorHandler(); + return false; + } + + if (drmModeAtomicCommit(m_backend->fd(), req, flags, this)) { + qCWarning(KWIN_DRM) << "Atomic request failed to commit:" << strerror(errno); + errorHandler(); + return false; + } + + if (mode == AtomicCommitMode::Real && (flags & DRM_MODE_ATOMIC_ALLOW_MODESET)) { + qCDebug(KWIN_DRM) << "Atomic Modeset successful."; + m_modesetRequested = false; + m_dpmsMode = m_dpmsModePending; + } + + drmModeAtomicFree(req); + return true; +} + +bool DrmOutput::atomicReqModesetPopulate(drmModeAtomicReq *req, bool enable) +{ + if (enable) { + const QSize mSize = modeSize(); + const QSize sourceSize = hardwareTransforms() ? pixelSize() : mSize; + + m_primaryPlane->setValue(int(DrmPlane::PropertyIndex::SrcX), 0); + m_primaryPlane->setValue(int(DrmPlane::PropertyIndex::SrcY), 0); + m_primaryPlane->setValue(int(DrmPlane::PropertyIndex::SrcW), sourceSize.width() << 16); + m_primaryPlane->setValue(int(DrmPlane::PropertyIndex::SrcH), sourceSize.height() << 16); + m_primaryPlane->setValue(int(DrmPlane::PropertyIndex::CrtcW), mSize.width()); + m_primaryPlane->setValue(int(DrmPlane::PropertyIndex::CrtcH), mSize.height()); + m_primaryPlane->setValue(int(DrmPlane::PropertyIndex::CrtcId), m_crtc->id()); + } else { + if (m_backend->deleteBufferAfterPageFlip()) { + delete m_primaryPlane->current(); + delete m_primaryPlane->next(); + } + m_primaryPlane->setCurrent(nullptr); + m_primaryPlane->setNext(nullptr); + + m_primaryPlane->setValue(int(DrmPlane::PropertyIndex::SrcX), 0); + m_primaryPlane->setValue(int(DrmPlane::PropertyIndex::SrcY), 0); + m_primaryPlane->setValue(int(DrmPlane::PropertyIndex::SrcW), 0); + m_primaryPlane->setValue(int(DrmPlane::PropertyIndex::SrcH), 0); + m_primaryPlane->setValue(int(DrmPlane::PropertyIndex::CrtcW), 0); + m_primaryPlane->setValue(int(DrmPlane::PropertyIndex::CrtcH), 0); + m_primaryPlane->setValue(int(DrmPlane::PropertyIndex::CrtcId), 0); + } + m_conn->setValue(int(DrmConnector::PropertyIndex::CrtcId), enable ? m_crtc->id() : 0); + m_crtc->setValue(int(DrmCrtc::PropertyIndex::ModeId), enable ? m_blobId : 0); + m_crtc->setValue(int(DrmCrtc::PropertyIndex::Active), enable); + + bool ret = true; + ret &= m_conn->atomicPopulate(req); + ret &= m_crtc->atomicPopulate(req); + + return ret; +} + +bool DrmOutput::supportsTransformations() const +{ + if (!m_primaryPlane) { + return false; + } + const auto transformations = m_primaryPlane->supportedTransformations(); + return transformations.testFlag(DrmPlane::Transformation::Rotate90) + || transformations.testFlag(DrmPlane::Transformation::Rotate180) + || transformations.testFlag(DrmPlane::Transformation::Rotate270); +} + +int DrmOutput::gammaRampSize() const +{ + return m_crtc->gammaRampSize(); +} + +bool DrmOutput::setGammaRamp(const GammaRamp &gamma) +{ + return m_crtc->setGammaRamp(gamma); +} + +} + +QDebug& operator<<(QDebug& s, const KWin::DrmOutput* output) +{ + if (!output) + return s.nospace() << "DrmOutput()"; + return s.nospace() << "DrmOutput(" << output->name() << ", crtc:" << output->crtc() << ", connector:" << output->connector() << ", geometry:" << output->geometry() << ')'; +} diff --git a/plugins/platforms/drm/drm_output.h b/plugins/platforms/drm/drm_output.h new file mode 100644 index 0000000..72bb1b9 --- /dev/null +++ b/plugins/platforms/drm/drm_output.h @@ -0,0 +1,179 @@ +/* + 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_DRM_OUTPUT_H +#define KWIN_DRM_OUTPUT_H + +#include "abstract_wayland_output.h" +#include "drm_pointer.h" +#include "drm_object.h" +#include "drm_object_plane.h" +#include "edid.h" + +#include +#include +#include +#include +#include + +namespace KWin +{ + +class DrmBackend; +class DrmBuffer; +class DrmDumbBuffer; +class DrmPlane; +class DrmConnector; +class DrmCrtc; +class Cursor; + +class KWIN_EXPORT DrmOutput : public AbstractWaylandOutput +{ + Q_OBJECT +public: + ///deletes the output, calling this whilst a page flip is pending will result in an error + ~DrmOutput() override; + ///queues deleting the output after a page flip has completed. + void teardown(); + void releaseGbm(); + bool showCursor(DrmDumbBuffer *buffer); + bool showCursor(); + bool hideCursor(); + void updateCursor(); + void moveCursor(Cursor* cursor, const QPoint &globalPos); + bool init(drmModeConnector *connector); + bool present(DrmBuffer *buffer); + void pageFlipped(); + + // These values are defined by the kernel + enum class DpmsMode { + On = DRM_MODE_DPMS_ON, + Standby = DRM_MODE_DPMS_STANDBY, + Suspend = DRM_MODE_DPMS_SUSPEND, + Off = DRM_MODE_DPMS_OFF + }; + Q_ENUM(DpmsMode); + bool isDpmsEnabled() const { + // We care for current as well as pending mode in order to allow first present in AMS. + return m_dpmsModePending == DpmsMode::On; + } + + DpmsMode dpmsMode() const { + return m_dpmsMode; + } + DpmsMode dpmsModePending() const { + return m_dpmsModePending; + } + + const DrmCrtc *crtc() const { + return m_crtc; + } + const DrmConnector *connector() const { + return m_conn; + } + const DrmPlane *primaryPlane() const { + return m_primaryPlane; + } + + bool initCursor(const QSize &cursorSize); + + bool supportsTransformations() const; + + /** + * Drm planes might be capable of realizing the current output transform without usage + * of compositing. This is a getter to query the current state of that + * + * @return true if the hardware realizes the transform without further assistance + */ + bool hardwareTransforms() const; + +private: + friend class DrmBackend; + friend class DrmCrtc; // TODO: For use of setModeLegacy. Remove later when we allow multiple connectors per crtc + // and save the connector ids in the DrmCrtc instance. + DrmOutput(DrmBackend *backend); + + bool presentAtomically(DrmBuffer *buffer); + + enum class AtomicCommitMode { + Test, + Real + }; + bool doAtomicCommit(AtomicCommitMode mode); + + bool presentLegacy(DrmBuffer *buffer); + bool setModeLegacy(DrmBuffer *buffer); + void initEdid(drmModeConnector *connector); + void initDpms(drmModeConnector *connector); + void initOutputDevice(drmModeConnector *connector); + + bool isCurrentMode(const drmModeModeInfo *mode) const; + void initUuid(); + bool initPrimaryPlane(); + bool initCursorPlane(); + + void atomicEnable(); + void atomicDisable(); + void updateEnablement(bool enable) override; + + bool dpmsAtomicOff(); + bool dpmsLegacyApply(); + + void dpmsFinishOn(); + void dpmsFinishOff(); + + bool atomicReqModesetPopulate(drmModeAtomicReq *req, bool enable); + void updateDpms(KWaylandServer::OutputInterface::DpmsMode mode) override; + void updateMode(int modeIndex) override; + void setWaylandMode(); + + void updateTransform(Transform transform) override; + + int gammaRampSize() const override; + bool setGammaRamp(const GammaRamp &gamma) override; + + DrmBackend *m_backend; + DrmConnector *m_conn = nullptr; + DrmCrtc *m_crtc = nullptr; + bool m_lastGbm = false; + drmModeModeInfo m_mode; + Edid m_edid; + DrmScopedPointer m_dpms; + DpmsMode m_dpmsMode = DpmsMode::On; + DpmsMode m_dpmsModePending = DpmsMode::On; + QByteArray m_uuid; + + uint32_t m_blobId = 0; + DrmPlane* m_primaryPlane = nullptr; + DrmPlane* m_cursorPlane = nullptr; + QVector m_nextPlanesFlipList; + bool m_pageFlipPending = false; + bool m_atomicOffPending = false; + bool m_modesetRequested = true; + + struct { + Transform transform; + drmModeModeInfo mode; + DrmPlane::Transformations planeTransformations; + QPoint globalPos; + bool valid = false; + } m_lastWorkingState; + QScopedPointer m_cursor[2]; + int m_cursorIndex = 0; + bool m_hasNewCursor = false; + bool m_deleted = false; +}; + +} + +Q_DECLARE_METATYPE(KWin::DrmOutput*) + +QDebug& operator<<(QDebug& stream, const KWin::DrmOutput *); + +#endif + diff --git a/plugins/platforms/drm/drm_pointer.h b/plugins/platforms/drm/drm_pointer.h new file mode 100644 index 0000000..fa70d1c --- /dev/null +++ b/plugins/platforms/drm/drm_pointer.h @@ -0,0 +1,147 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_DRM_POINTER_H +#define KWIN_DRM_POINTER_H + +#include + +#include +#include + +namespace KWin +{ + +template +struct DrmDeleter; + +template <> +struct DrmDeleter +{ + static void cleanup(drmModeAtomicReq *req) + { + drmModeAtomicFree(req); + } +}; + +template <> +struct DrmDeleter +{ + static void cleanup(drmModeConnector *connector) + { + drmModeFreeConnector(connector); + } +}; + +template <> +struct DrmDeleter +{ + static void cleanup(drmModeCrtc *crtc) + { + drmModeFreeCrtc(crtc); + } +}; + +template <> +struct DrmDeleter +{ + static void cleanup(drmModeFB *fb) + { + drmModeFreeFB(fb); + } +}; + +template <> +struct DrmDeleter +{ + static void cleanup(drmModeEncoder *encoder) + { + drmModeFreeEncoder(encoder); + } +}; + +template <> +struct DrmDeleter +{ + static void cleanup(drmModeModeInfo *info) + { + drmModeFreeModeInfo(info); + } +}; + +template <> +struct DrmDeleter +{ + static void cleanup(drmModeObjectProperties *properties) + { + drmModeFreeObjectProperties(properties); + } +}; + +template <> +struct DrmDeleter +{ + static void cleanup(drmModePlane *plane) + { + drmModeFreePlane(plane); + } +}; + +template <> +struct DrmDeleter +{ + static void cleanup(drmModePlaneRes *resources) + { + drmModeFreePlaneResources(resources); + } +}; + +template <> +struct DrmDeleter +{ + static void cleanup(drmModePropertyRes *property) + { + drmModeFreeProperty(property); + } +}; + +template <> +struct DrmDeleter +{ + static void cleanup(drmModePropertyBlobRes *blob) + { + drmModeFreePropertyBlob(blob); + } +}; + +template <> +struct DrmDeleter +{ + static void cleanup(drmModeRes *resources) + { + drmModeFreeResources(resources); + } +}; + +template <> +struct DrmDeleter +{ + static void cleanup(drmVersion *version) + { + drmFreeVersion(version); + } +}; + +template +using DrmScopedPointer = QScopedPointer>; + +} + +#endif + diff --git a/plugins/platforms/drm/edid.cpp b/plugins/platforms/drm/edid.cpp new file mode 100644 index 0000000..61dc1a7 --- /dev/null +++ b/plugins/platforms/drm/edid.cpp @@ -0,0 +1,206 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Flöser + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "edid.h" +#include "config-kwin.h" + +#include +#include + +namespace KWin +{ + +static bool verifyHeader(const uint8_t *data) +{ + if (data[0] != 0x0 || data[7] != 0x0) { + return false; + } + + return std::all_of(data + 1, data + 7, + [](uint8_t byte) { return byte == 0xff; }); +} + +static QSize parsePhysicalSize(const uint8_t *data) +{ + // Convert physical size from centimeters to millimeters. + return QSize(data[0x15], data[0x16]) * 10; +} + +static QByteArray parsePnpId(const uint8_t *data) +{ + // Decode PNP ID from three 5 bit words packed into 2 bytes: + // + // | Byte | Bit | + // | | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | + // ---------------------------------------- + // | 1 | 0)| (4| 3 | 2 | 1 | 0)| (4| 3 | + // | | * | Character 1 | Char 2| + // ---------------------------------------- + // | 2 | 2 | 1 | 0)| (4| 3 | 2 | 1 | 0)| + // | | Character2| Character 3 | + // ---------------------------------------- + const uint offset = 0x8; + + char pnpId[4]; + pnpId[0] = 'A' + ((data[offset + 0] >> 2) & 0x1f) - 1; + pnpId[1] = 'A' + (((data[offset + 0] & 0x3) << 3) | ((data[offset + 1] >> 5) & 0x7)) - 1; + pnpId[2] = 'A' + (data[offset + 1] & 0x1f) - 1; + pnpId[3] = '\0'; + + return QByteArray(pnpId); +} + + +static QByteArray parseEisaId(const uint8_t *data) +{ + for (int i = 72; i <= 108; i += 18) { + // Skip the block if it isn't used as monitor descriptor. + if (data[i]) { + continue; + } + if (data[i + 1]) { + continue; + } + + // We have found the EISA ID, it's stored as ASCII. + if (data[i + 3] == 0xfe) { + return QByteArray(reinterpret_cast(&data[i + 5]), 12).trimmed(); + } + } + + // If there isn't an ASCII EISA ID descriptor, try to decode PNP ID + return parsePnpId(data); +} + +static QByteArray parseMonitorName(const uint8_t *data) +{ + for (int i = 72; i <= 108; i += 18) { + // Skip the block if it isn't used as monitor descriptor. + if (data[i]) { + continue; + } + if (data[i + 1]) { + continue; + } + + // We have found the monitor name, it's stored as ASCII. + if (data[i + 3] == 0xfc) { + return QByteArray(reinterpret_cast(&data[i + 5]), 12).trimmed(); + } + } + + return QByteArray(); +} + +static QByteArray parseSerialNumber(const uint8_t *data) +{ + for (int i = 72; i <= 108; i += 18) { + // Skip the block if it isn't used as monitor descriptor. + if (data[i]) { + continue; + } + if (data[i + 1]) { + continue; + } + + // We have found the serial number, it's stored as ASCII. + if (data[i + 3] == 0xff) { + return QByteArray(reinterpret_cast(&data[i + 5]), 12).trimmed(); + } + } + + // Maybe there isn't an ASCII serial number descriptor, so use this instead. + const uint32_t offset = 0xc; + + uint32_t serialNumber = data[offset + 0]; + serialNumber |= uint32_t(data[offset + 1]) << 8; + serialNumber |= uint32_t(data[offset + 2]) << 16; + serialNumber |= uint32_t(data[offset + 3]) << 24; + if (serialNumber) { + return QByteArray::number(serialNumber); + } + + return QByteArray(); +} + +static QByteArray parseVendor(const uint8_t *data) +{ + const auto pnpId = parsePnpId(data); + + // Map to vendor name + QFile pnpFile(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("hwdata/pnp.ids"))); + if (pnpFile.exists() && pnpFile.open(QIODevice::ReadOnly)) { + while (!pnpFile.atEnd()) { + const auto line = pnpFile.readLine(); + if (line.startsWith(pnpId)) { + return line.mid(4).trimmed(); + } + } + } + + return {}; +} + +Edid::Edid() +{ +} + +Edid::Edid(const void *data, uint32_t size) +{ + const uint8_t *bytes = static_cast(data); + + if (size < 128) { + return; + } + + if (!verifyHeader(bytes)) { + return; + } + + m_physicalSize = parsePhysicalSize(bytes); + m_eisaId = parseEisaId(bytes); + m_monitorName = parseMonitorName(bytes); + m_serialNumber = parseSerialNumber(bytes); + m_vendor = parseVendor(bytes); + + m_isValid = true; +} + +bool Edid::isValid() const +{ + return m_isValid; +} + +QSize Edid::physicalSize() const +{ + return m_physicalSize; +} + +QByteArray Edid::eisaId() const +{ + return m_eisaId; +} + +QByteArray Edid::monitorName() const +{ + return m_monitorName; +} + +QByteArray Edid::serialNumber() const +{ + return m_serialNumber; +} + +QByteArray Edid::vendor() const +{ + return m_vendor; +} + +} // namespace KWin diff --git a/plugins/platforms/drm/edid.h b/plugins/platforms/drm/edid.h new file mode 100644 index 0000000..05349c8 --- /dev/null +++ b/plugins/platforms/drm/edid.h @@ -0,0 +1,68 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +namespace KWin +{ + +/** + * Helper class that can be used for parsing EDID blobs. + * + * http://read.pudn.com/downloads110/ebook/456020/E-EDID%20Standard.pdf + */ +class Edid +{ +public: + Edid(); + Edid(const void *data, uint32_t size); + + /** + * Whether this instance of EDID is valid. + */ + bool isValid() const; + + /** + * Returns physical dimensions of the monitor, in millimeters. + */ + QSize physicalSize() const; + + /** + * Returns EISA ID of the manufacturer of the monitor. + */ + QByteArray eisaId() const; + + /** + * Returns the product name of the monitor. + */ + QByteArray monitorName() const; + + /** + * Returns the serial number of the monitor. + */ + QByteArray serialNumber() const; + + /** + * Returns the name of the vendor. + */ + QByteArray vendor() const; + +private: + QSize m_physicalSize; + QByteArray m_vendor; + QByteArray m_eisaId; + QByteArray m_monitorName; + QByteArray m_serialNumber; + bool m_isValid = false; +}; + +} // namespace KWin diff --git a/plugins/platforms/drm/egl_gbm_backend.cpp b/plugins/platforms/drm/egl_gbm_backend.cpp new file mode 100644 index 0000000..378e6c2 --- /dev/null +++ b/plugins/platforms/drm/egl_gbm_backend.cpp @@ -0,0 +1,656 @@ +/* + 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 "egl_gbm_backend.h" +// kwin +#include "composite.h" +#include "drm_backend.h" +#include "drm_output.h" +#include "gbm_surface.h" +#include "logging.h" +#include "options.h" +#include "screens.h" +// kwin libs +#include +#include +// system +#include + +namespace KWin +{ + +EglGbmBackend::EglGbmBackend(DrmBackend *drmBackend) + : AbstractEglBackend() + , m_backend(drmBackend) +{ + // Egl is always direct rendering. + setIsDirectRendering(true); + setSyncsToVBlank(true); + connect(m_backend, &DrmBackend::outputAdded, this, &EglGbmBackend::createOutput); + connect(m_backend, &DrmBackend::outputRemoved, this, &EglGbmBackend::removeOutput); +} + +EglGbmBackend::~EglGbmBackend() +{ + cleanup(); +} + +void EglGbmBackend::cleanupSurfaces() +{ + for (auto it = m_outputs.begin(); it != m_outputs.end(); ++it) { + cleanupOutput(*it); + } + m_outputs.clear(); +} + +void EglGbmBackend::cleanupFramebuffer(Output &output) +{ + if (!output.render.framebuffer) { + return; + } + glDeleteTextures(1, &output.render.texture); + output.render.texture = 0; + glDeleteFramebuffers(1, &output.render.framebuffer); + output.render.framebuffer = 0; +} + +void EglGbmBackend::cleanupOutput(Output &output) +{ + cleanupFramebuffer(output); + output.output->releaseGbm(); + + if (output.eglSurface != EGL_NO_SURFACE) { + eglDestroySurface(eglDisplay(), output.eglSurface); + } +} + +bool EglGbmBackend::initializeEgl() +{ + initClientExtensions(); + EGLDisplay display = m_backend->sceneEglDisplay(); + + // Use eglGetPlatformDisplayEXT() to get the display pointer + // if the implementation supports it. + if (display == EGL_NO_DISPLAY) { + const bool hasMesaGBM = hasClientExtension(QByteArrayLiteral("EGL_MESA_platform_gbm")); + const bool hasKHRGBM = hasClientExtension(QByteArrayLiteral("EGL_KHR_platform_gbm")); + const GLenum platform = hasMesaGBM ? EGL_PLATFORM_GBM_MESA : EGL_PLATFORM_GBM_KHR; + + if (!hasClientExtension(QByteArrayLiteral("EGL_EXT_platform_base")) || + (!hasMesaGBM && !hasKHRGBM)) { + setFailed("Missing one or more extensions between EGL_EXT_platform_base, " + "EGL_MESA_platform_gbm, EGL_KHR_platform_gbm"); + return false; + } + + auto device = gbm_create_device(m_backend->fd()); + if (!device) { + setFailed("Could not create gbm device"); + return false; + } + m_backend->setGbmDevice(device); + + display = eglGetPlatformDisplayEXT(platform, device, nullptr); + } + + if (display == EGL_NO_DISPLAY) { + return false; + } + setEglDisplay(display); + return initEglAPI(); +} + +void EglGbmBackend::init() +{ + if (!initializeEgl()) { + setFailed("Could not initialize egl"); + return; + } + if (!initRenderingContext()) { + setFailed("Could not initialize rendering context"); + return; + } + + initKWinGL(); + initBufferAge(); + initWayland(); +} + +bool EglGbmBackend::initRenderingContext() +{ + initBufferConfigs(); + if (!createContext()) { + return false; + } + + const auto outputs = m_backend->drmOutputs(); + + for (DrmOutput *drmOutput: outputs) { + createOutput(drmOutput); + } + + if (m_outputs.isEmpty()) { + qCCritical(KWIN_DRM) << "Create Window Surfaces failed"; + return false; + } + + // Set our first surface as the one for the abstract backend, just to make it happy. + setSurface(m_outputs.first().eglSurface); + + return makeContextCurrent(m_outputs.first()); +} + +std::shared_ptr EglGbmBackend::createGbmSurface(const QSize &size) const +{ + auto gbmSurface = std::make_shared(m_backend->gbmDevice(), + size.width(), size.height(), + GBM_FORMAT_XRGB8888, + GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING); + if (!gbmSurface) { + qCCritical(KWIN_DRM) << "Creating GBM surface failed"; + return nullptr; + } + return gbmSurface; +} + +EGLSurface EglGbmBackend::createEglSurface(std::shared_ptr gbmSurface) const +{ + auto eglSurface = eglCreatePlatformWindowSurfaceEXT(eglDisplay(), config(), + (void *)(gbmSurface->surface()), nullptr); + if (eglSurface == EGL_NO_SURFACE) { + qCCritical(KWIN_DRM) << "Creating EGL surface failed"; + return EGL_NO_SURFACE; + } + return eglSurface; +} + +bool EglGbmBackend::resetOutput(Output &output, DrmOutput *drmOutput) +{ + output.output = drmOutput; + const QSize size = drmOutput->hardwareTransforms() ? drmOutput->pixelSize() : + drmOutput->modeSize(); + + auto gbmSurface = createGbmSurface(size); + if (!gbmSurface) { + return false; + } + auto eglSurface = createEglSurface(gbmSurface); + if (eglSurface == EGL_NO_SURFACE) { + return false; + } + + // destroy previous surface + if (output.eglSurface != EGL_NO_SURFACE) { + if (surface() == output.eglSurface) { + setSurface(eglSurface); + } + eglDestroySurface(eglDisplay(), output.eglSurface); + } + output.eglSurface = eglSurface; + output.gbmSurface = gbmSurface; + + resetFramebuffer(output); + return true; +} + +void EglGbmBackend::createOutput(DrmOutput *drmOutput) +{ + Output newOutput; + if (resetOutput(newOutput, drmOutput)) { + connect(drmOutput, &DrmOutput::modeChanged, this, + [drmOutput, this] { + auto it = std::find_if(m_outputs.begin(), m_outputs.end(), + [drmOutput] (const auto &output) { + return output.output == drmOutput; + } + ); + if (it == m_outputs.end()) { + return; + } + resetOutput(*it, drmOutput); + } + ); + m_outputs << newOutput; + } +} + +void EglGbmBackend::removeOutput(DrmOutput *drmOutput) +{ + auto it = std::find_if(m_outputs.begin(), m_outputs.end(), + [drmOutput] (const Output &output) { + return output.output == drmOutput; + } + ); + if (it == m_outputs.end()) { + return; + } + + cleanupOutput(*it); + m_outputs.erase(it); +} + +const float vertices[] = { + -1.0f, 1.0f, + -1.0f, -1.0f, + 1.0f, -1.0f, + + -1.0f, 1.0f, + 1.0f, -1.0f, + 1.0f, 1.0f, +}; + +const float texCoords[] = { + 0.0f, 1.0f, + 0.0f, 0.0f, + 1.0f, 0.0f, + + 0.0f, 1.0f, + 1.0f, 0.0f, + 1.0f, 1.0f +}; + +bool EglGbmBackend::resetFramebuffer(Output &output) +{ + cleanupFramebuffer(output); + + if (output.output->hardwareTransforms()) { + // No need for an extra render target. + return true; + } + + makeContextCurrent(output); + + glGenFramebuffers(1, &output.render.framebuffer); + glBindFramebuffer(GL_FRAMEBUFFER, output.render.framebuffer); + GLRenderTarget::setKWinFramebuffer(output.render.framebuffer); + + glGenTextures(1, &output.render.texture); + glBindTexture(GL_TEXTURE_2D, output.render.texture); + + const QSize texSize = output.output->pixelSize(); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texSize.width(), texSize.height(), + 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST ); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + + glBindTexture(GL_TEXTURE_2D, 0); + + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, + output.render.texture, 0); + + if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + qCWarning(KWIN_DRM) << "Error: framebuffer not complete"; + return false; + } + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + GLRenderTarget::setKWinFramebuffer(0); + return true; +} + +void EglGbmBackend::initRenderTarget(Output &output) +{ + if (output.render.vbo) { + // Already initialized. + return; + } + std::shared_ptr vbo(new GLVertexBuffer(KWin::GLVertexBuffer::Static)); + vbo->setData(6, 2, vertices, texCoords); + output.render.vbo = vbo; +} + +void EglGbmBackend::renderFramebufferToSurface(Output &output) +{ + if (!output.render.framebuffer) { + // No additional render target. + return; + } + initRenderTarget(output); + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + GLRenderTarget::setKWinFramebuffer(0); + + const auto size = output.output->modeSize(); + glViewport(0, 0, size.width(), size.height()); + + auto shader = ShaderManager::instance()->pushShader(ShaderTrait::MapTexture); + + QMatrix4x4 mvpMatrix; + + const DrmOutput *drmOutput = output.output; + switch (drmOutput->transform()) { + case DrmOutput::Transform::Normal: + case DrmOutput::Transform::Flipped: + break; + case DrmOutput::Transform::Rotated90: + case DrmOutput::Transform::Flipped90: + mvpMatrix.rotate(90, 0, 0, 1); + break; + case DrmOutput::Transform::Rotated180: + case DrmOutput::Transform::Flipped180: + mvpMatrix.rotate(180, 0, 0, 1); + break; + case DrmOutput::Transform::Rotated270: + case DrmOutput::Transform::Flipped270: + mvpMatrix.rotate(270, 0, 0, 1); + break; + } + switch (drmOutput->transform()) { + case DrmOutput::Transform::Flipped: + case DrmOutput::Transform::Flipped90: + case DrmOutput::Transform::Flipped180: + case DrmOutput::Transform::Flipped270: + mvpMatrix.scale(-1, 1); + break; + default: + break; + } + + shader->setUniform(GLShader::ModelViewProjectionMatrix, mvpMatrix); + + glBindTexture(GL_TEXTURE_2D, output.render.texture); + output.render.vbo->render(GL_TRIANGLES); + ShaderManager::instance()->popShader(); +} + +void EglGbmBackend::prepareRenderFramebuffer(const Output &output) const +{ + // When render.framebuffer is 0 we may just reset to the screen framebuffer. + glBindFramebuffer(GL_FRAMEBUFFER, output.render.framebuffer); + GLRenderTarget::setKWinFramebuffer(output.render.framebuffer); +} + +bool EglGbmBackend::makeContextCurrent(const Output &output) const +{ + const EGLSurface surface = output.eglSurface; + if (surface == EGL_NO_SURFACE) { + return false; + } + if (eglMakeCurrent(eglDisplay(), surface, surface, context()) == EGL_FALSE) { + qCCritical(KWIN_DRM) << "Make Context Current failed"; + return false; + } + EGLint error = eglGetError(); + if (error != EGL_SUCCESS) { + qCWarning(KWIN_DRM) << "Error occurred while creating context " << error; + return false; + } + return true; +} + +bool EglGbmBackend::initBufferConfigs() +{ + const EGLint config_attribs[] = { + EGL_SURFACE_TYPE, EGL_WINDOW_BIT, + EGL_RED_SIZE, 1, + EGL_GREEN_SIZE, 1, + EGL_BLUE_SIZE, 1, + EGL_ALPHA_SIZE, 0, + EGL_RENDERABLE_TYPE, isOpenGLES() ? EGL_OPENGL_ES2_BIT : EGL_OPENGL_BIT, + EGL_CONFIG_CAVEAT, EGL_NONE, + EGL_NONE, + }; + + EGLint count; + EGLConfig configs[1024]; + if (!eglChooseConfig(eglDisplay(), config_attribs, configs, + sizeof(configs) / sizeof(EGLConfig), + &count)) { + qCCritical(KWIN_DRM) << "choose config failed"; + return false; + } + + qCDebug(KWIN_DRM) << "EGL buffer configs count:" << count; + + // Loop through all configs, choosing the first one that has suitable format. + for (EGLint i = 0; i < count; i++) { + EGLint gbmFormat; + // Query some configuration parameters, to show in debug log. + eglGetConfigAttrib(eglDisplay(), configs[i], EGL_NATIVE_VISUAL_ID, &gbmFormat); + + if (KWIN_DRM().isDebugEnabled()) { + // GBM formats are declared as FOURCC code (integer from ASCII chars, so use this fact). + char gbmFormatStr[sizeof(EGLint) + 1] = {0}; + memcpy(gbmFormatStr, &gbmFormat, sizeof(EGLint)); + + // Query number of bits for color channel. + EGLint blueSize, redSize, greenSize, alphaSize; + eglGetConfigAttrib(eglDisplay(), configs[i], EGL_RED_SIZE, &redSize); + eglGetConfigAttrib(eglDisplay(), configs[i], EGL_GREEN_SIZE, &greenSize); + eglGetConfigAttrib(eglDisplay(), configs[i], EGL_BLUE_SIZE, &blueSize); + eglGetConfigAttrib(eglDisplay(), configs[i], EGL_ALPHA_SIZE, &alphaSize); + qCDebug(KWIN_DRM) << " EGL config #" << i << " has GBM FOURCC format:" << gbmFormatStr + << "; color sizes (RGBA order):" + << redSize << greenSize << blueSize << alphaSize; + } + + if ((gbmFormat == GBM_FORMAT_XRGB8888) || (gbmFormat == GBM_FORMAT_ARGB8888)) { + setConfig(configs[i]); + return true; + } + } + + qCCritical(KWIN_DRM) << "Choosing EGL config did not return a suitable config. There were" + << count << "configs."; + return false; +} + +void EglGbmBackend::present() +{ + Q_UNREACHABLE(); + // Not in use. This backend does per-screen rendering. +} + +static QVector regionToRects(const QRegion ®ion, AbstractWaylandOutput *output) +{ + const int height = output->modeSize().height(); + + const QMatrix4x4 matrix = DrmOutput::logicalToNativeMatrix(output->geometry(), + output->scale(), + output->transform()); + + QVector rects; + rects.reserve(region.rectCount() * 4); + for (const QRect &_rect : region) { + const QRect rect = matrix.mapRect(_rect); + + rects << rect.left(); + rects << height - (rect.y() + rect.height()); + rects << rect.width(); + rects << rect.height(); + } + return rects; +} + +void EglGbmBackend::aboutToStartPainting(const QRegion &damagedRegion) +{ + // See EglGbmBackend::endRenderingFrameForScreen comment for the reason why we only support screenId=0 + if (m_outputs.count() > 1) { + return; + } + + const Output &output = m_outputs.at(0); + if (output.bufferAge > 0 && !damagedRegion.isEmpty() && supportsPartialUpdate()) { + const QRegion region = damagedRegion & output.output->geometry(); + + QVector rects = regionToRects(region, output.output); + const bool correct = eglSetDamageRegionKHR(eglDisplay(), output.eglSurface, + rects.data(), rects.count()/4); + if (!correct) { + qCWarning(KWIN_DRM) << "failed eglSetDamageRegionKHR" << eglGetError(); + } + } +} + +void EglGbmBackend::presentOnOutput(Output &output, const QRegion &damagedRegion) +{ + if (supportsSwapBuffersWithDamage()) { + QVector rects = regionToRects(output.damageHistory.constFirst(), output.output); + eglSwapBuffersWithDamageEXT(eglDisplay(), output.eglSurface, + rects.data(), rects.count()/4); + } else { + eglSwapBuffers(eglDisplay(), output.eglSurface); + } + output.buffer = m_backend->createBuffer(output.gbmSurface); + + Q_EMIT output.output->outputChange(damagedRegion); + m_backend->present(output.buffer, output.output); + + if (supportsBufferAge()) { + eglQuerySurface(eglDisplay(), output.eglSurface, EGL_BUFFER_AGE_EXT, &output.bufferAge); + } +} + +void EglGbmBackend::screenGeometryChanged(const QSize &size) +{ + Q_UNUSED(size) + // TODO, create new buffer? +} + +SceneOpenGLTexturePrivate *EglGbmBackend::createBackendTexture(SceneOpenGLTexture *texture) +{ + return new EglGbmTexture(texture, this); +} + +QRegion EglGbmBackend::prepareRenderingFrame() +{ + startRenderTimer(); + return QRegion(); +} + +void EglGbmBackend::setViewport(const Output &output) const +{ + const QSize &overall = screens()->size(); + const QRect &v = output.output->geometry(); + qreal scale = output.output->scale(); + + glViewport(-v.x() * scale, (v.height() - overall.height() + v.y()) * scale, + overall.width() * scale, overall.height() * scale); +} + +QRegion EglGbmBackend::prepareRenderingForScreen(int screenId) +{ + const Output &output = m_outputs.at(screenId); + + makeContextCurrent(output); + prepareRenderFramebuffer(output); + setViewport(output); + + if (supportsBufferAge()) { + QRegion region; + + // Note: An age of zero means the buffer contents are undefined + if (output.bufferAge > 0 && output.bufferAge <= output.damageHistory.count()) { + for (int i = 0; i < output.bufferAge - 1; i++) + region |= output.damageHistory[i]; + } else { + region = output.output->geometry(); + } + + return region; + } + return output.output->geometry(); +} + +void EglGbmBackend::endRenderingFrame(const QRegion &renderedRegion, const QRegion &damagedRegion) +{ + Q_UNUSED(renderedRegion) + Q_UNUSED(damagedRegion) +} + +void EglGbmBackend::endRenderingFrameForScreen(int screenId, + const QRegion &renderedRegion, + const QRegion &damagedRegion) +{ + Output &output = m_outputs[screenId]; + renderFramebufferToSurface(output); + + if (damagedRegion.intersected(output.output->geometry()).isEmpty() && screenId == 0) { + + // If the damaged region of a window is fully occluded, the only + // rendering done, if any, will have been to repair a reused back + // buffer, making it identical to the front buffer. + // + // In this case we won't post the back buffer. Instead we'll just + // set the buffer age to 1, so the repaired regions won't be + // rendered again in the next frame. + if (!renderedRegion.intersected(output.output->geometry()).isEmpty()) + glFlush(); + + for (auto &output: m_outputs) { + output.bufferAge = 1; + } + return; + } + presentOnOutput(output, damagedRegion); + + // Save the damaged region to history + // Note: damage history is only collected for the first screen. For any other screen full + // repaints are triggered. This is due to a limitation in Scene::paintGenericScreen which resets + // the Toplevel's repaint. So multiple calls to Scene::paintScreen as it's done in multi-output + // rendering only have correct damage information for the first screen. If we try to track + // damage nevertheless, it creates artifacts. So for the time being we work around the problem + // by only supporting buffer age on the first output. To properly support buffer age on all + // outputs the rendering needs to be refactored in general. + if (supportsBufferAge() && screenId == 0) { + if (output.damageHistory.count() > 10) { + output.damageHistory.removeLast(); + } + output.damageHistory.prepend(damagedRegion.intersected(output.output->geometry())); + } +} + +bool EglGbmBackend::usesOverlayWindow() const +{ + return false; +} + +bool EglGbmBackend::perScreenRendering() const +{ + return true; +} + +QSharedPointer EglGbmBackend::textureForOutput(AbstractOutput *abstractOutput) const +{ + const QVector::const_iterator itOutput = std::find_if(m_outputs.begin(), m_outputs.end(), + [abstractOutput] (const auto &output) { + return output.output == abstractOutput; + } + ); + if (itOutput == m_outputs.end()) { + return {}; + } + + DrmOutput *drmOutput = itOutput->output; + if (!drmOutput->hardwareTransforms()) { + const auto glTexture = QSharedPointer::create(itOutput->render.texture, GL_RGBA8, drmOutput->pixelSize()); + glTexture->setYInverted(true); + return glTexture; + } + + EGLImageKHR image = eglCreateImageKHR(eglDisplay(), nullptr, EGL_NATIVE_PIXMAP_KHR, itOutput->buffer->getBo(), nullptr); + if (image == EGL_NO_IMAGE_KHR) { + qCWarning(KWIN_DRM) << "Failed to record frame: Error creating EGLImageKHR - " << glGetError(); + return {}; + } + + return QSharedPointer::create(eglDisplay(), image, GL_RGBA8, drmOutput->modeSize()); +} + +/************************************************ + * EglTexture + ************************************************/ + +EglGbmTexture::EglGbmTexture(KWin::SceneOpenGLTexture *texture, EglGbmBackend *backend) + : AbstractEglTexture(texture, backend) +{ +} + +EglGbmTexture::~EglGbmTexture() = default; + +} diff --git a/plugins/platforms/drm/egl_gbm_backend.h b/plugins/platforms/drm/egl_gbm_backend.h new file mode 100644 index 0000000..b4d991a --- /dev/null +++ b/plugins/platforms/drm/egl_gbm_backend.h @@ -0,0 +1,115 @@ +/* + 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_EGL_GBM_BACKEND_H +#define KWIN_EGL_GBM_BACKEND_H +#include "abstract_egl_backend.h" + +#include + +struct gbm_surface; + +namespace KWin +{ +class AbstractOutput; +class DrmBackend; +class DrmBuffer; +class DrmSurfaceBuffer; +class DrmOutput; +class GbmSurface; + +/** + * @brief OpenGL Backend using Egl on a GBM surface. + */ +class EglGbmBackend : public AbstractEglBackend +{ + Q_OBJECT +public: + EglGbmBackend(DrmBackend *drmBackend); + ~EglGbmBackend() override; + void screenGeometryChanged(const QSize &size) override; + SceneOpenGLTexturePrivate *createBackendTexture(SceneOpenGLTexture *texture) override; + QRegion prepareRenderingFrame() override; + void endRenderingFrame(const QRegion &renderedRegion, const QRegion &damagedRegion) override; + void endRenderingFrameForScreen(int screenId, const QRegion &damage, const QRegion &damagedRegion) override; + bool usesOverlayWindow() const override; + bool perScreenRendering() const override; + QRegion prepareRenderingForScreen(int screenId) override; + void init() override; + + QSharedPointer textureForOutput(AbstractOutput *requestedOutput) const override; + +protected: + void present() override; + void cleanupSurfaces() override; + void aboutToStartPainting(const QRegion &damage) override; + +private: + bool initializeEgl(); + bool initBufferConfigs(); + bool initRenderingContext(); + + struct Output { + DrmOutput *output = nullptr; + DrmSurfaceBuffer *buffer = nullptr; + std::shared_ptr gbmSurface; + EGLSurface eglSurface = EGL_NO_SURFACE; + int bufferAge = 0; + /** + * @brief The damage history for the past 10 frames. + */ + QList damageHistory; + + struct { + GLuint framebuffer = 0; + GLuint texture = 0; + std::shared_ptr vbo; + } render; + }; + + void createOutput(DrmOutput *drmOutput); + bool resetOutput(Output &output, DrmOutput *drmOutput); + std::shared_ptr createGbmSurface(const QSize &size) const; + EGLSurface createEglSurface(std::shared_ptr gbmSurface) const; + + bool makeContextCurrent(const Output &output) const; + void setViewport(const Output &output) const; + + bool resetFramebuffer(Output &output); + void initRenderTarget(Output &output); + + void prepareRenderFramebuffer(const Output &output) const; + void renderFramebufferToSurface(Output &output); + + void presentOnOutput(Output &output, const QRegion &damagedRegion); + + void removeOutput(DrmOutput *drmOutput); + void cleanupOutput(Output &output); + void cleanupFramebuffer(Output &output); + + DrmBackend *m_backend; + QVector m_outputs; + friend class EglGbmTexture; +}; + +/** + * @brief Texture using an EGLImageKHR. + */ +class EglGbmTexture : public AbstractEglTexture +{ +public: + ~EglGbmTexture() override; + +private: + friend class EglGbmBackend; + EglGbmTexture(SceneOpenGLTexture *texture, EglGbmBackend *backend); +}; + +} // namespace + +#endif diff --git a/plugins/platforms/drm/egl_stream_backend.cpp b/plugins/platforms/drm/egl_stream_backend.cpp new file mode 100644 index 0000000..c9c3643 --- /dev/null +++ b/plugins/platforms/drm/egl_stream_backend.cpp @@ -0,0 +1,677 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 NVIDIA Inc. + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "egl_stream_backend.h" +#include "composite.h" +#include "drm_backend.h" +#include "drm_output.h" +#include "drm_object_crtc.h" +#include "drm_object_plane.h" +#include "logging.h" +#include "logind.h" +#include "options.h" +#include "scene.h" +#include "screens.h" +#include "wayland_server.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace KWin +{ + +typedef EGLStreamKHR (*PFNEGLCREATESTREAMATTRIBNV)(EGLDisplay, EGLAttrib *); +typedef EGLBoolean (*PFNEGLGETOUTPUTLAYERSEXT)(EGLDisplay, EGLAttrib *, EGLOutputLayerEXT *, EGLint, EGLint *); +typedef EGLBoolean (*PFNEGLSTREAMCONSUMEROUTPUTEXT)(EGLDisplay, EGLStreamKHR, EGLOutputLayerEXT); +typedef EGLSurface (*PFNEGLCREATESTREAMPRODUCERSURFACEKHR)(EGLDisplay, EGLConfig, EGLStreamKHR, EGLint *); +typedef EGLBoolean (*PFNEGLDESTROYSTREAMKHR)(EGLDisplay, EGLStreamKHR); +typedef EGLBoolean (*PFNEGLSTREAMCONSUMERACQUIREATTRIBNV)(EGLDisplay, EGLStreamKHR, EGLAttrib *); +typedef EGLBoolean (*PFNEGLSTREAMCONSUMERGLTEXTUREEXTERNALKHR)(EGLDisplay, EGLStreamKHR); +typedef EGLBoolean (*PFNEGLQUERYSTREAMATTRIBNV)(EGLDisplay, EGLStreamKHR, EGLenum, EGLAttrib *); +typedef EGLBoolean (*PFNEGLSTREAMCONSUMERRELEASEKHR)(EGLDisplay, EGLStreamKHR); +typedef EGLBoolean (*PFNEGLQUERYWAYLANDBUFFERWL)(EGLDisplay, wl_resource *, EGLint, EGLint *); +PFNEGLCREATESTREAMATTRIBNV pEglCreateStreamAttribNV = nullptr; +PFNEGLGETOUTPUTLAYERSEXT pEglGetOutputLayersEXT = nullptr; +PFNEGLSTREAMCONSUMEROUTPUTEXT pEglStreamConsumerOutputEXT = nullptr; +PFNEGLCREATESTREAMPRODUCERSURFACEKHR pEglCreateStreamProducerSurfaceKHR = nullptr; +PFNEGLDESTROYSTREAMKHR pEglDestroyStreamKHR = nullptr; +PFNEGLSTREAMCONSUMERACQUIREATTRIBNV pEglStreamConsumerAcquireAttribNV = nullptr; +PFNEGLSTREAMCONSUMERGLTEXTUREEXTERNALKHR pEglStreamConsumerGLTextureExternalKHR = nullptr; +PFNEGLQUERYSTREAMATTRIBNV pEglQueryStreamAttribNV = nullptr; +PFNEGLSTREAMCONSUMERRELEASEKHR pEglStreamConsumerReleaseKHR = nullptr; +PFNEGLQUERYWAYLANDBUFFERWL pEglQueryWaylandBufferWL = nullptr; + +#ifndef EGL_CONSUMER_AUTO_ACQUIRE_EXT +#define EGL_CONSUMER_AUTO_ACQUIRE_EXT 0x332B +#endif + +#ifndef EGL_DRM_MASTER_FD_EXT +#define EGL_DRM_MASTER_FD_EXT 0x333C +#endif + +#ifndef EGL_DRM_FLIP_EVENT_DATA_NV +#define EGL_DRM_FLIP_EVENT_DATA_NV 0x333E +#endif + +#ifndef EGL_WAYLAND_EGLSTREAM_WL +#define EGL_WAYLAND_EGLSTREAM_WL 0x334B +#endif + +#ifndef EGL_WAYLAND_Y_INVERTED_WL +#define EGL_WAYLAND_Y_INVERTED_WL 0x31DB +#endif + +EglStreamBackend::EglStreamBackend(DrmBackend *b) + : AbstractEglBackend(), m_backend(b) +{ + setIsDirectRendering(true); + setSyncsToVBlank(true); + connect(m_backend, &DrmBackend::outputAdded, this, &EglStreamBackend::createOutput); + connect(m_backend, &DrmBackend::outputRemoved, this, + [this] (DrmOutput *output) { + auto it = std::find_if(m_outputs.begin(), m_outputs.end(), + [output] (const Output &o) { + return o.output == output; + }); + if (it == m_outputs.end()) { + return; + } + cleanupOutput(*it); + m_outputs.erase(it); + }); +} + +EglStreamBackend::~EglStreamBackend() +{ + cleanup(); +} + +void EglStreamBackend::cleanupSurfaces() +{ + for (auto it = m_outputs.constBegin(); it != m_outputs.constEnd(); ++it) { + cleanupOutput(*it); + } + m_outputs.clear(); +} + +void EglStreamBackend::cleanupOutput(const Output &o) +{ + if (o.buffer != nullptr) { + delete o.buffer; + } + if (o.eglSurface != EGL_NO_SURFACE) { + eglDestroySurface(eglDisplay(), o.eglSurface); + } + if (o.eglStream != EGL_NO_STREAM_KHR) { + pEglDestroyStreamKHR(eglDisplay(), o.eglStream); + } +} + +bool EglStreamBackend::initializeEgl() +{ + initClientExtensions(); + EGLDisplay display = m_backend->sceneEglDisplay(); + if (display == EGL_NO_DISPLAY) { + if (!hasClientExtension(QByteArrayLiteral("EGL_EXT_device_base")) && + !(hasClientExtension(QByteArrayLiteral("EGL_EXT_device_query")) && + hasClientExtension(QByteArrayLiteral("EGL_EXT_device_enumeration")))) { + setFailed("Missing required EGL client extension: " + "EGL_EXT_device_base or " + "EGL_EXT_device_query and EGL_EXT_device_enumeration"); + return false; + } + + // Try to find the EGLDevice corresponding to our DRM device file + int numDevices; + eglQueryDevicesEXT(0, nullptr, &numDevices); + QVector devices(numDevices); + eglQueryDevicesEXT(numDevices, devices.data(), &numDevices); + for (EGLDeviceEXT device : devices) { + const char *drmDeviceFile = eglQueryDeviceStringEXT(device, EGL_DRM_DEVICE_FILE_EXT); + if (m_backend->devNode() != drmDeviceFile) { + continue; + } + + const char *deviceExtensionCString = eglQueryDeviceStringEXT(device, EGL_EXTENSIONS); + QByteArray deviceExtensions = QByteArray::fromRawData(deviceExtensionCString, + qstrlen(deviceExtensionCString)); + if (!deviceExtensions.split(' ').contains(QByteArrayLiteral("EGL_EXT_device_drm"))) { + continue; + } + + EGLint platformAttribs[] = { + EGL_DRM_MASTER_FD_EXT, m_backend->fd(), + EGL_NONE + }; + display = eglGetPlatformDisplayEXT(EGL_PLATFORM_DEVICE_EXT, device, platformAttribs); + break; + } + } + + if (display == EGL_NO_DISPLAY) { + setFailed("No suitable EGL device found"); + return false; + } + + setEglDisplay(display); + if (!initEglAPI()) { + return false; + } + + const QVector requiredExtensions = { + QByteArrayLiteral("EGL_EXT_output_base"), + QByteArrayLiteral("EGL_EXT_output_drm"), + QByteArrayLiteral("EGL_KHR_stream"), + QByteArrayLiteral("EGL_KHR_stream_producer_eglsurface"), + QByteArrayLiteral("EGL_EXT_stream_consumer_egloutput"), + QByteArrayLiteral("EGL_NV_stream_attrib"), + QByteArrayLiteral("EGL_EXT_stream_acquire_mode"), + QByteArrayLiteral("EGL_KHR_stream_consumer_gltexture"), + QByteArrayLiteral("EGL_WL_wayland_eglstream") + }; + for (const QByteArray &ext : requiredExtensions) { + if (!hasExtension(ext)) { + setFailed(QStringLiteral("Missing required EGL extension: ") + ext); + return false; + } + } + + pEglCreateStreamAttribNV = (PFNEGLCREATESTREAMATTRIBNV)eglGetProcAddress("eglCreateStreamAttribNV"); + pEglGetOutputLayersEXT = (PFNEGLGETOUTPUTLAYERSEXT)eglGetProcAddress("eglGetOutputLayersEXT"); + pEglStreamConsumerOutputEXT = (PFNEGLSTREAMCONSUMEROUTPUTEXT)eglGetProcAddress("eglStreamConsumerOutputEXT"); + pEglCreateStreamProducerSurfaceKHR = (PFNEGLCREATESTREAMPRODUCERSURFACEKHR)eglGetProcAddress("eglCreateStreamProducerSurfaceKHR"); + pEglDestroyStreamKHR = (PFNEGLDESTROYSTREAMKHR)eglGetProcAddress("eglDestroyStreamKHR"); + pEglStreamConsumerAcquireAttribNV = (PFNEGLSTREAMCONSUMERACQUIREATTRIBNV)eglGetProcAddress("eglStreamConsumerAcquireAttribNV"); + pEglStreamConsumerGLTextureExternalKHR = (PFNEGLSTREAMCONSUMERGLTEXTUREEXTERNALKHR)eglGetProcAddress("eglStreamConsumerGLTextureExternalKHR"); + pEglQueryStreamAttribNV = (PFNEGLQUERYSTREAMATTRIBNV)eglGetProcAddress("eglQueryStreamAttribNV"); + pEglStreamConsumerReleaseKHR = (PFNEGLSTREAMCONSUMERRELEASEKHR)eglGetProcAddress("eglStreamConsumerReleaseKHR"); + pEglQueryWaylandBufferWL = (PFNEGLQUERYWAYLANDBUFFERWL)eglGetProcAddress("eglQueryWaylandBufferWL"); + return true; +} + +EglStreamBackend::StreamTexture *EglStreamBackend::lookupStreamTexture(KWaylandServer::SurfaceInterface *surface) +{ + auto it = m_streamTextures.find(surface); + return it != m_streamTextures.end() ? + &it.value() : + nullptr; +} + +void EglStreamBackend::attachStreamConsumer(KWaylandServer::SurfaceInterface *surface, + void *eglStream, + wl_array *attribs) +{ + QVector streamAttribs; + streamAttribs << EGL_WAYLAND_EGLSTREAM_WL << (EGLAttrib)eglStream; + EGLAttrib *attribArray = (EGLAttrib *)attribs->data; + for (unsigned int i = 0; i < attribs->size; ++i) { + streamAttribs << attribArray[i]; + } + streamAttribs << EGL_NONE; + + EGLStreamKHR stream = pEglCreateStreamAttribNV(eglDisplay(), streamAttribs.data()); + if (stream == EGL_NO_STREAM_KHR) { + qCWarning(KWIN_DRM) << "Failed to create EGL stream"; + return; + } + + GLuint texture; + StreamTexture *st = lookupStreamTexture(surface); + if (st != nullptr) { + pEglDestroyStreamKHR(eglDisplay(), st->stream); + st->stream = stream; + texture = st->texture; + } else { + StreamTexture newSt = { stream, 0 }; + glGenTextures(1, &newSt.texture); + m_streamTextures.insert(surface, newSt); + texture = newSt.texture; + + connect(surface, &KWaylandServer::SurfaceInterface::destroyed, this, + [surface, this]() { + const StreamTexture &st = m_streamTextures.take(surface); + pEglDestroyStreamKHR(eglDisplay(), st.stream); + glDeleteTextures(1, &st.texture); + }); + } + + glBindTexture(GL_TEXTURE_EXTERNAL_OES, texture); + if (!pEglStreamConsumerGLTextureExternalKHR(eglDisplay(), stream)) { + qCWarning(KWIN_DRM) << "Failed to bind EGL stream to texture"; + } + glBindTexture(GL_TEXTURE_EXTERNAL_OES, 0); +} + +void EglStreamBackend::init() +{ + if (!m_backend->atomicModeSetting()) { + setFailed("EGLStream backend requires atomic modesetting"); + return; + } + + if (!initializeEgl()) { + setFailed("Failed to initialize EGL api"); + return; + } + if (!initRenderingContext()) { + setFailed("Failed to initialize rendering context"); + return; + } + + initKWinGL(); + setSupportsBufferAge(false); + initWayland(); + + using namespace KWaylandServer; + m_eglStreamControllerInterface = waylandServer()->display()->createEglStreamControllerInterface(); + connect(m_eglStreamControllerInterface, &EglStreamControllerInterface::streamConsumerAttached, this, + &EglStreamBackend::attachStreamConsumer); +} + +bool EglStreamBackend::initRenderingContext() +{ + initBufferConfigs(); + + if (!createContext()) { + return false; + } + + const auto outputs = m_backend->drmOutputs(); + for (DrmOutput *drmOutput : outputs) { + createOutput(drmOutput); + } + if (m_outputs.isEmpty()) { + qCCritical(KWIN_DRM) << "Failed to create output surface"; + return false; + } + // set our first surface as the one for the abstract backend + setSurface(m_outputs.first().eglSurface); + + return makeContextCurrent(m_outputs.first()); +} + +bool EglStreamBackend::resetOutput(Output &o, DrmOutput *drmOutput) +{ + o.output = drmOutput; + if (o.buffer != nullptr) { + delete o.buffer; + } + // dumb buffer used for modesetting + o.buffer = m_backend->createBuffer(drmOutput->pixelSize()); + + EGLAttrib streamAttribs[] = { + EGL_STREAM_FIFO_LENGTH_KHR, 0, // mailbox mode + EGL_CONSUMER_AUTO_ACQUIRE_EXT, EGL_FALSE, + EGL_NONE + }; + EGLStreamKHR stream = pEglCreateStreamAttribNV(eglDisplay(), streamAttribs); + if (stream == EGL_NO_STREAM_KHR) { + qCCritical(KWIN_DRM) << "Failed to create EGL stream for output"; + return false; + } + + EGLAttrib outputAttribs[3]; + if (drmOutput->primaryPlane()) { + outputAttribs[0] = EGL_DRM_PLANE_EXT; + outputAttribs[1] = drmOutput->primaryPlane()->id(); + } else { + outputAttribs[0] = EGL_DRM_CRTC_EXT; + outputAttribs[1] = drmOutput->crtc()->id(); + } + outputAttribs[2] = EGL_NONE; + EGLint numLayers; + EGLOutputLayerEXT outputLayer; + pEglGetOutputLayersEXT(eglDisplay(), outputAttribs, &outputLayer, 1, &numLayers); + if (numLayers == 0) { + qCCritical(KWIN_DRM) << "No EGL output layers found"; + return false; + } + + pEglStreamConsumerOutputEXT(eglDisplay(), stream, outputLayer); + EGLint streamProducerAttribs[] = { + EGL_WIDTH, drmOutput->pixelSize().width(), + EGL_HEIGHT, drmOutput->pixelSize().height(), + EGL_NONE + }; + EGLSurface eglSurface = pEglCreateStreamProducerSurfaceKHR(eglDisplay(), config(), stream, + streamProducerAttribs); + if (eglSurface == EGL_NO_SURFACE) { + qCCritical(KWIN_DRM) << "Failed to create EGL surface for output"; + return false; + } + + if (o.eglSurface != EGL_NO_SURFACE) { + if (surface() == o.eglSurface) { + setSurface(eglSurface); + } + eglDestroySurface(eglDisplay(), o.eglSurface); + } + + if (o.eglStream != EGL_NO_STREAM_KHR) { + pEglDestroyStreamKHR(eglDisplay(), o.eglStream); + } + + o.eglStream = stream; + o.eglSurface = eglSurface; + return true; +} + +void EglStreamBackend::createOutput(DrmOutput *drmOutput) +{ + Output o; + if (!resetOutput(o, drmOutput)) { + return; + } + + connect(drmOutput, &DrmOutput::modeChanged, this, + [drmOutput, this] { + auto it = std::find_if(m_outputs.begin(), m_outputs.end(), + [drmOutput] (const auto &o) { + return o.output == drmOutput; + } + ); + if (it == m_outputs.end()) { + return; + } + resetOutput(*it, drmOutput); + } + ); + m_outputs << o; +} + +bool EglStreamBackend::makeContextCurrent(const Output &output) +{ + const EGLSurface surface = output.eglSurface; + if (surface == EGL_NO_SURFACE) { + return false; + } + + if (eglMakeCurrent(eglDisplay(), surface, surface, context()) == EGL_FALSE) { + qCCritical(KWIN_DRM) << "Failed to make EGL context current"; + return false; + } + + EGLint error = eglGetError(); + if (error != EGL_SUCCESS) { + qCWarning(KWIN_DRM) << "Error occurred while making EGL context current" << error; + return false; + } + + const QSize &overall = screens()->size(); + const QRect &v = output.output->geometry(); + qreal scale = output.output->scale(); + glViewport(-v.x() * scale, (v.height() - overall.height() + v.y()) * scale, + overall.width() * scale, overall.height() * scale); + return true; +} + +bool EglStreamBackend::initBufferConfigs() +{ + const EGLint configAttribs[] = { + EGL_SURFACE_TYPE, EGL_STREAM_BIT_KHR, + EGL_RED_SIZE, 1, + EGL_GREEN_SIZE, 1, + EGL_BLUE_SIZE, 1, + EGL_ALPHA_SIZE, 0, + EGL_RENDERABLE_TYPE, isOpenGLES() ? EGL_OPENGL_ES2_BIT : EGL_OPENGL_BIT, + EGL_CONFIG_CAVEAT, EGL_NONE, + EGL_NONE, + }; + EGLint count; + EGLConfig config; + if (!eglChooseConfig(eglDisplay(), configAttribs, &config, 1, &count)) { + qCCritical(KWIN_DRM) << "Failed to query available EGL configs"; + return false; + } + if (count == 0) { + qCCritical(KWIN_DRM) << "No suitable EGL config found"; + return false; + } + + setConfig(config); + return true; +} + +void EglStreamBackend::present() +{ + for (auto &o : m_outputs) { + makeContextCurrent(o); + presentOnOutput(o); + } +} + +void EglStreamBackend::presentOnOutput(EglStreamBackend::Output &o) +{ + eglSwapBuffers(eglDisplay(), o.eglSurface); + if (!m_backend->present(o.buffer, o.output)) { + return; + } + + EGLAttrib acquireAttribs[] = { + EGL_DRM_FLIP_EVENT_DATA_NV, (EGLAttrib)o.output, + EGL_NONE, + }; + if (!pEglStreamConsumerAcquireAttribNV(eglDisplay(), o.eglStream, acquireAttribs)) { + qCWarning(KWIN_DRM) << "Failed to acquire output EGL stream frame"; + } +} + +void EglStreamBackend::screenGeometryChanged(const QSize &size) +{ + Q_UNUSED(size) +} + +SceneOpenGLTexturePrivate *EglStreamBackend::createBackendTexture(SceneOpenGLTexture *texture) +{ + return new EglStreamTexture(texture, this); +} + +QRegion EglStreamBackend::prepareRenderingFrame() +{ + startRenderTimer(); + return QRegion(); +} + +QRegion EglStreamBackend::prepareRenderingForScreen(int screenId) +{ + const Output &o = m_outputs.at(screenId); + makeContextCurrent(o); + return o.output->geometry(); +} + +void EglStreamBackend::endRenderingFrame(const QRegion &renderedRegion, const QRegion &damagedRegion) +{ + Q_UNUSED(renderedRegion) + Q_UNUSED(damagedRegion) +} + +void EglStreamBackend::endRenderingFrameForScreen(int screenId, const QRegion &renderedRegion, const QRegion &damagedRegion) +{ + Q_UNUSED(renderedRegion); + Q_UNUSED(damagedRegion); + Output &o = m_outputs[screenId]; + presentOnOutput(o); +} + +bool EglStreamBackend::usesOverlayWindow() const +{ + return false; +} + +bool EglStreamBackend::perScreenRendering() const +{ + return true; +} + +/************************************************ + * EglTexture + ************************************************/ + +EglStreamTexture::EglStreamTexture(SceneOpenGLTexture *texture, EglStreamBackend *backend) + : AbstractEglTexture(texture, backend), m_backend(backend), m_fbo(0), m_rbo(0) +{ +} + +EglStreamTexture::~EglStreamTexture() +{ + glDeleteRenderbuffers(1, &m_rbo); + glDeleteFramebuffers(1, &m_fbo); +} + +bool EglStreamTexture::acquireStreamFrame(EGLStreamKHR stream) +{ + EGLAttrib streamState; + if (!pEglQueryStreamAttribNV(m_backend->eglDisplay(), stream, + EGL_STREAM_STATE_KHR, &streamState)) { + qCWarning(KWIN_DRM) << "Failed to query EGL stream state"; + return false; + } + + if (streamState == EGL_STREAM_STATE_NEW_FRAME_AVAILABLE_KHR) { + if (pEglStreamConsumerAcquireAttribNV(m_backend->eglDisplay(), stream, nullptr)) { + return true; + } else { + qCWarning(KWIN_DRM) << "Failed to acquire EGL stream frame"; + } + } + + // Re-use previous texture contents if no new frame is available + // or if acquisition fails for some reason + return false; +} + +void EglStreamTexture::createFbo() +{ + glDeleteRenderbuffers(1, &m_rbo); + glDeleteFramebuffers(1, &m_fbo); + + glGenFramebuffers(1, &m_fbo); + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); + glGenRenderbuffers(1, &m_rbo); + glBindRenderbuffer(GL_RENDERBUFFER, m_rbo); + glRenderbufferStorage(GL_RENDERBUFFER, m_format, m_size.width(), m_size.height()); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, m_rbo); + glBindRenderbuffer(GL_RENDERBUFFER, 0); + glBindFramebuffer(GL_FRAMEBUFFER, 0); +} + +// Renders the contents of the given EXTERNAL_OES texture +// to the scratch framebuffer, then copies this to m_texture +void EglStreamTexture::copyExternalTexture(GLuint tex) +{ + GLint oldViewport[4], oldProgram; + glGetIntegerv(GL_VIEWPORT, oldViewport); + glViewport(0, 0, m_size.width(), m_size.height()); + glGetIntegerv(GL_CURRENT_PROGRAM, &oldProgram); + glUseProgram(0); + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); + glBindRenderbuffer(GL_RENDERBUFFER, m_rbo); + glBindTexture(GL_TEXTURE_EXTERNAL_OES, tex); + glEnable(GL_TEXTURE_EXTERNAL_OES); + + GLfloat yTop = texture()->isYInverted() ? 0 : 1; + glBegin(GL_QUADS); + glTexCoord2f(0, yTop); + glVertex2f(-1, 1); + glTexCoord2f(0, 1 - yTop); + glVertex2f(-1, -1); + glTexCoord2f(1, 1 - yTop); + glVertex2f(1, -1); + glTexCoord2f(1, yTop); + glVertex2f(1, 1); + glEnd(); + + texture()->bind(); + glCopyTexImage2D(m_target, 0, m_format, 0, 0, m_size.width(), m_size.height(), 0); + texture()->unbind(); + + glDisable(GL_TEXTURE_EXTERNAL_OES); + glBindTexture(GL_TEXTURE_EXTERNAL_OES, 0); + glBindRenderbuffer(GL_RENDERBUFFER, 0); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glUseProgram(oldProgram); + glViewport(oldViewport[0], oldViewport[1], oldViewport[2], oldViewport[3]); +} + +bool EglStreamTexture::attachBuffer(KWaylandServer::BufferInterface *buffer) +{ + QSize oldSize = m_size; + m_size = buffer->size(); + GLenum oldFormat = m_format; + m_format = buffer->hasAlphaChannel() ? GL_RGBA : GL_RGB; + + EGLint yInverted, wasYInverted = texture()->isYInverted(); + if (!pEglQueryWaylandBufferWL(m_backend->eglDisplay(), buffer->resource(), EGL_WAYLAND_Y_INVERTED_WL, &yInverted)) { + yInverted = EGL_TRUE; + } + texture()->setYInverted(yInverted); + updateMatrix(); + + return oldSize != m_size || + oldFormat != m_format || + wasYInverted != texture()->isYInverted(); +} + +bool EglStreamTexture::loadTexture(WindowPixmap *pixmap) +{ + using namespace KWaylandServer; + SurfaceInterface *surface = pixmap->surface(); + const EglStreamBackend::StreamTexture *st = m_backend->lookupStreamTexture(surface); + if (!pixmap->buffer().isNull() && st != nullptr) { + + glGenTextures(1, &m_texture); + texture()->setWrapMode(GL_CLAMP_TO_EDGE); + texture()->setFilter(GL_LINEAR); + + attachBuffer(surface->buffer()); + createFbo(); + surface->resetTrackedDamage(); + + if (acquireStreamFrame(st->stream)) { + copyExternalTexture(st->texture); + if (!pEglStreamConsumerReleaseKHR(m_backend->eglDisplay(), st->stream)) { + qCWarning(KWIN_DRM) << "Failed to release EGL stream"; + } + } + return true; + } else { + // Not an EGLStream surface + return AbstractEglTexture::loadTexture(pixmap); + } +} + +void EglStreamTexture::updateTexture(WindowPixmap *pixmap) +{ + using namespace KWaylandServer; + SurfaceInterface *surface = pixmap->surface(); + const EglStreamBackend::StreamTexture *st = m_backend->lookupStreamTexture(surface); + if (!pixmap->buffer().isNull() && st != nullptr) { + + if (attachBuffer(surface->buffer())) { + createFbo(); + } + surface->resetTrackedDamage(); + + if (acquireStreamFrame(st->stream)) { + copyExternalTexture(st->texture); + if (!pEglStreamConsumerReleaseKHR(m_backend->eglDisplay(), st->stream)) { + qCWarning(KWIN_DRM) << "Failed to release EGL stream"; + } + } + } else { + // Not an EGLStream surface + AbstractEglTexture::updateTexture(pixmap); + } +} + +} // namespace diff --git a/plugins/platforms/drm/egl_stream_backend.h b/plugins/platforms/drm/egl_stream_backend.h new file mode 100644 index 0000000..c72eb77 --- /dev/null +++ b/plugins/platforms/drm/egl_stream_backend.h @@ -0,0 +1,104 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 NVIDIA Inc. + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_EGL_STREAM_BACKEND_H +#define KWIN_EGL_STREAM_BACKEND_H +#include "abstract_egl_backend.h" +#include +#include +#include + +namespace KWin +{ + +class DrmBackend; +class DrmOutput; +class DrmBuffer; + +/** + * @brief OpenGL Backend using Egl with an EGLDevice. + */ +class EglStreamBackend : public AbstractEglBackend +{ + Q_OBJECT +public: + EglStreamBackend(DrmBackend *b); + ~EglStreamBackend() override; + void screenGeometryChanged(const QSize &size) override; + SceneOpenGLTexturePrivate *createBackendTexture(SceneOpenGLTexture *texture) override; + QRegion prepareRenderingFrame() override; + void endRenderingFrame(const QRegion &renderedRegion, const QRegion &damagedRegion) override; + void endRenderingFrameForScreen(int screenId, const QRegion &damage, const QRegion &damagedRegion) override; + bool usesOverlayWindow() const override; + bool perScreenRendering() const override; + QRegion prepareRenderingForScreen(int screenId) override; + void init() override; + +protected: + void present() override; + void cleanupSurfaces() override; + +private: + bool initializeEgl(); + bool initBufferConfigs(); + bool initRenderingContext(); + struct StreamTexture + { + EGLStreamKHR stream; + GLuint texture; + }; + StreamTexture *lookupStreamTexture(KWaylandServer::SurfaceInterface *surface); + void attachStreamConsumer(KWaylandServer::SurfaceInterface *surface, + void *eglStream, + wl_array *attribs); + struct Output + { + DrmOutput *output = nullptr; + DrmBuffer *buffer = nullptr; + EGLSurface eglSurface = EGL_NO_SURFACE; + EGLStreamKHR eglStream = EGL_NO_STREAM_KHR; + }; + bool resetOutput(Output &output, DrmOutput *drmOutput); + bool makeContextCurrent(const Output &output); + void presentOnOutput(Output &output); + void cleanupOutput(const Output &output); + void createOutput(DrmOutput *output); + + DrmBackend *m_backend; + QVector m_outputs; + KWaylandServer::EglStreamControllerInterface *m_eglStreamControllerInterface; + QHash m_streamTextures; + + friend class EglStreamTexture; +}; + +/** + * @brief External texture bound to an EGLStreamKHR. + */ +class EglStreamTexture : public AbstractEglTexture +{ +public: + ~EglStreamTexture() override; + bool loadTexture(WindowPixmap *pixmap) override; + void updateTexture(WindowPixmap *pixmap) override; + +private: + EglStreamTexture(SceneOpenGLTexture *texture, EglStreamBackend *backend); + bool acquireStreamFrame(EGLStreamKHR stream); + void createFbo(); + void copyExternalTexture(GLuint tex); + bool attachBuffer(KWaylandServer::BufferInterface *buffer); + EglStreamBackend *m_backend; + GLuint m_fbo, m_rbo; + GLenum m_format; + friend class EglStreamBackend; +}; + +} // namespace + +#endif diff --git a/plugins/platforms/drm/gbm_dmabuf.cpp b/plugins/platforms/drm/gbm_dmabuf.cpp new file mode 100644 index 0000000..866ac7a --- /dev/null +++ b/plugins/platforms/drm/gbm_dmabuf.cpp @@ -0,0 +1,71 @@ +/* + SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "gbm_dmabuf.h" +#include "kwineglimagetexture.h" +#include "platformsupport/scenes/opengl/drm_fourcc.h" +#include "main.h" +#include "platform.h" +#include + +namespace KWin +{ + +GbmDmaBuf::GbmDmaBuf(GLTexture *texture, gbm_bo *bo, int fd) + : DmaBufTexture(texture) + , m_bo(bo) + , m_fd(fd) +{} + +GbmDmaBuf::~GbmDmaBuf() +{ + m_texture.reset(nullptr); + + close(m_fd); + gbm_bo_destroy(m_bo); +} + + +KWin::GbmDmaBuf *GbmDmaBuf::createBuffer(const QSize &size, gbm_device *device) +{ + if (!device) { + return nullptr; + } + + auto bo = gbm_bo_create(device, size.width(), size.height(), GBM_BO_FORMAT_ARGB8888, GBM_BO_USE_RENDERING | GBM_BO_USE_LINEAR); + + if (!bo) { + gbm_bo_destroy(bo); + return nullptr; + } + + const int fd = gbm_bo_get_fd(bo); + if (fd < 0) { + gbm_bo_destroy(bo); + return nullptr; + } + + EGLint importAttributes[] = { + EGL_WIDTH, EGLint(gbm_bo_get_width(bo)), + EGL_HEIGHT, EGLint(gbm_bo_get_height(bo)), + EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_ARGB8888, + EGL_DMA_BUF_PLANE0_FD_EXT, fd, + EGL_DMA_BUF_PLANE0_OFFSET_EXT, EGLint(gbm_bo_get_offset(bo, 0)), + EGL_DMA_BUF_PLANE0_PITCH_EXT, EGLint(gbm_bo_get_stride(bo)), + EGL_NONE + }; + + EGLDisplay display = kwinApp()->platform()->sceneEglDisplay(); + EGLImageKHR destinationImage = eglCreateImageKHR(display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, importAttributes); + if (destinationImage == EGL_NO_IMAGE_KHR) { + return nullptr; + } + + return new GbmDmaBuf(new KWin::EGLImageTexture(display, destinationImage, GL_RGBA8, size), bo, fd); +} + +} + diff --git a/plugins/platforms/drm/gbm_dmabuf.h b/plugins/platforms/drm/gbm_dmabuf.h new file mode 100644 index 0000000..bd3c203 --- /dev/null +++ b/plugins/platforms/drm/gbm_dmabuf.h @@ -0,0 +1,38 @@ +/* + SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "dmabuftexture.h" +#include +#include +#include + +namespace KWin +{ + +class GbmDmaBuf : public DmaBufTexture +{ +public: + ~GbmDmaBuf(); + + int fd() const override + { + return m_fd; + } + quint32 stride() const override { + return gbm_bo_get_stride(m_bo); + } + + static GbmDmaBuf *createBuffer(const QSize &size, gbm_device *device); + +private: + GbmDmaBuf(GLTexture *texture, gbm_bo *bo, int fd); + struct gbm_bo *const m_bo; + const int m_fd; +}; + +} diff --git a/plugins/platforms/drm/gbm_surface.cpp b/plugins/platforms/drm/gbm_surface.cpp new file mode 100644 index 0000000..6357c31 --- /dev/null +++ b/plugins/platforms/drm/gbm_surface.cpp @@ -0,0 +1,44 @@ +/* + 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 "gbm_surface.h" + +#include + +namespace KWin +{ + +GbmSurface::GbmSurface(gbm_device *gbm, uint32_t width, uint32_t height, uint32_t format, uint32_t flags) + : m_surface(gbm_surface_create(gbm, width, height, format, flags)) +{ +} + +GbmSurface::~GbmSurface() +{ + if (m_surface) { + gbm_surface_destroy(m_surface); + } +} + +gbm_bo *GbmSurface::lockFrontBuffer() +{ + if (!m_surface) { + return nullptr; + } + return gbm_surface_lock_front_buffer(m_surface); +} + +void GbmSurface::releaseBuffer(gbm_bo *bo) +{ + if (!bo || !m_surface) { + return; + } + gbm_surface_release_buffer(m_surface, bo); +} + +} diff --git a/plugins/platforms/drm/gbm_surface.h b/plugins/platforms/drm/gbm_surface.h new file mode 100644 index 0000000..5dd40b0 --- /dev/null +++ b/plugins/platforms/drm/gbm_surface.h @@ -0,0 +1,44 @@ +/* + 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 +*/ +#ifndef KWIN_DRM_GBM_SURFACE_H +#define KWIN_DRM_GBM_SURFACE_H + +#include + +struct gbm_bo; +struct gbm_device; +struct gbm_surface; + +namespace KWin +{ + +class GbmSurface +{ +public: + explicit GbmSurface(gbm_device *gbm, uint32_t width, uint32_t height, uint32_t format, uint32_t flags); + ~GbmSurface(); + + gbm_bo *lockFrontBuffer(); + void releaseBuffer(gbm_bo *bo); + + operator bool() const { + return m_surface != nullptr; + } + + gbm_surface* surface() const { + return m_surface; + } + +private: + gbm_surface *m_surface; +}; + +} + +#endif diff --git a/plugins/platforms/drm/logging.cpp b/plugins/platforms/drm/logging.cpp new file mode 100644 index 0000000..20102dc --- /dev/null +++ b/plugins/platforms/drm/logging.cpp @@ -0,0 +1,10 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "logging.h" +Q_LOGGING_CATEGORY(KWIN_DRM, "kwin_wayland_drm", QtCriticalMsg) diff --git a/plugins/platforms/drm/logging.h b/plugins/platforms/drm/logging.h new file mode 100644 index 0000000..5af0b77 --- /dev/null +++ b/plugins/platforms/drm/logging.h @@ -0,0 +1,15 @@ +/* + 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_DRM_LOGGING_H +#define KWIN_DRM_LOGGING_H +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(KWIN_DRM) +#endif diff --git a/plugins/platforms/drm/scene_qpainter_drm_backend.cpp b/plugins/platforms/drm/scene_qpainter_drm_backend.cpp new file mode 100644 index 0000000..b3d8fd2 --- /dev/null +++ b/plugins/platforms/drm/scene_qpainter_drm_backend.cpp @@ -0,0 +1,135 @@ +/* + 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 "scene_qpainter_drm_backend.h" +#include "drm_backend.h" +#include "drm_output.h" +#include "logind.h" + +namespace KWin +{ + +DrmQPainterBackend::DrmQPainterBackend(DrmBackend *backend) + : QObject() + , QPainterBackend() + , m_backend(backend) +{ + const auto outputs = m_backend->drmOutputs(); + for (auto output: outputs) { + initOutput(output); + } + connect(m_backend, &DrmBackend::outputAdded, this, &DrmQPainterBackend::initOutput); + connect(m_backend, &DrmBackend::outputRemoved, this, + [this] (DrmOutput *o) { + auto it = std::find_if(m_outputs.begin(), m_outputs.end(), + [o] (const Output &output) { + return output.output == o; + } + ); + if (it == m_outputs.end()) { + return; + } + delete (*it).buffer[0]; + delete (*it).buffer[1]; + m_outputs.erase(it); + } + ); +} + +DrmQPainterBackend::~DrmQPainterBackend() +{ + for (auto it = m_outputs.begin(); it != m_outputs.end(); ++it) { + delete (*it).buffer[0]; + delete (*it).buffer[1]; + } +} + +void DrmQPainterBackend::initOutput(DrmOutput *output) +{ + Output o; + auto initBuffer = [&o, output, this] (int index) { + o.buffer[index] = m_backend->createBuffer(output->pixelSize()); + if (o.buffer[index]->map()) { + o.buffer[index]->image()->fill(Qt::black); + } + }; + connect(output, &DrmOutput::modeChanged, this, + [output, this] { + auto it = std::find_if(m_outputs.begin(), m_outputs.end(), + [output] (const auto &o) { + return o.output == output; + } + ); + if (it == m_outputs.end()) { + return; + } + delete (*it).buffer[0]; + delete (*it).buffer[1]; + auto initBuffer = [it, output, this] (int index) { + it->buffer[index] = m_backend->createBuffer(output->pixelSize()); + if (it->buffer[index]->map()) { + it->buffer[index]->image()->fill(Qt::black); + } + }; + initBuffer(0); + initBuffer(1); + } + ); + initBuffer(0); + initBuffer(1); + o.output = output; + m_outputs << o; +} + +QImage *DrmQPainterBackend::buffer() +{ + return bufferForScreen(0); +} + +QImage *DrmQPainterBackend::bufferForScreen(int screenId) +{ + const Output &o = m_outputs.at(screenId); + return o.buffer[o.index]->image(); +} + +bool DrmQPainterBackend::needsFullRepaint() const +{ + return true; +} + +void DrmQPainterBackend::prepareRenderingFrame() +{ + for (auto it = m_outputs.begin(); it != m_outputs.end(); ++it) { + (*it).index = ((*it).index + 1) % 2; + } +} + +void DrmQPainterBackend::present(int mask, const QRegion &damage) +{ + Q_UNUSED(mask) + Q_UNUSED(damage) + if (!LogindIntegration::self()->isActiveSession()) { + return; + } + for (auto it = m_outputs.begin(); it != m_outputs.end(); ++it) { + const Output &o = *it; + m_backend->present(o.buffer[o.index], o.output); + } +} + +bool DrmQPainterBackend::usesOverlayWindow() const +{ + return false; +} + +bool DrmQPainterBackend::perScreenRendering() const +{ + return true; +} + +} diff --git a/plugins/platforms/drm/scene_qpainter_drm_backend.h b/plugins/platforms/drm/scene_qpainter_drm_backend.h new file mode 100644 index 0000000..f201e42 --- /dev/null +++ b/plugins/platforms/drm/scene_qpainter_drm_backend.h @@ -0,0 +1,49 @@ +/* + 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_SCENE_QPAINTER_DRM_BACKEND_H +#define KWIN_SCENE_QPAINTER_DRM_BACKEND_H +#include +#include +#include + +namespace KWin +{ + +class DrmBackend; +class DrmDumbBuffer; +class DrmOutput; + +class DrmQPainterBackend : public QObject, public QPainterBackend +{ + Q_OBJECT +public: + DrmQPainterBackend(DrmBackend *backend); + ~DrmQPainterBackend() override; + + QImage *buffer() override; + QImage *bufferForScreen(int screenId) override; + bool needsFullRepaint() const override; + bool usesOverlayWindow() const override; + void prepareRenderingFrame() override; + void present(int mask, const QRegion &damage) override; + bool perScreenRendering() const override; + +private: + void initOutput(DrmOutput *output); + struct Output { + DrmDumbBuffer *buffer[2]; + DrmOutput *output; + int index = 0; + }; + QVector m_outputs; + DrmBackend *m_backend; +}; +} + +#endif diff --git a/plugins/platforms/drm/screens_drm.cpp b/plugins/platforms/drm/screens_drm.cpp new file mode 100644 index 0000000..8345444 --- /dev/null +++ b/plugins/platforms/drm/screens_drm.cpp @@ -0,0 +1,35 @@ +/* + 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 "screens_drm.h" +#include "drm_backend.h" +#include "drm_output.h" + +namespace KWin +{ + +DrmScreens::DrmScreens(DrmBackend *backend, QObject *parent) + : OutputScreens(backend, parent) + , m_backend(backend) +{ + connect(backend, &DrmBackend::screensQueried, this, &DrmScreens::updateCount); + connect(backend, &DrmBackend::screensQueried, this, &DrmScreens::changed); +} + +DrmScreens::~DrmScreens() = default; + +bool DrmScreens::supportsTransformations(int screen) const +{ + const auto enOuts = m_backend->drmEnabledOutputs(); + if (screen >= enOuts.size()) { + return false; + } + return enOuts.at(screen)->supportsTransformations(); +} + +} diff --git a/plugins/platforms/drm/screens_drm.h b/plugins/platforms/drm/screens_drm.h new file mode 100644 index 0000000..1f08f1f --- /dev/null +++ b/plugins/platforms/drm/screens_drm.h @@ -0,0 +1,31 @@ +/* + 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_SCREENS_DRM_H +#define KWIN_SCREENS_DRM_H +#include "outputscreens.h" + +namespace KWin +{ +class DrmBackend; + +class DrmScreens : public OutputScreens +{ + Q_OBJECT +public: + DrmScreens(DrmBackend *backend, QObject *parent = nullptr); + ~DrmScreens() override; + + bool supportsTransformations(int screen) const override; + + DrmBackend *m_backend; +}; + +} + +#endif diff --git a/plugins/platforms/fbdev/CMakeLists.txt b/plugins/platforms/fbdev/CMakeLists.txt new file mode 100644 index 0000000..3cfa5f4 --- /dev/null +++ b/plugins/platforms/fbdev/CMakeLists.txt @@ -0,0 +1,16 @@ +set(FBDEV_SOURCES + fb_backend.cpp + logging.cpp + scene_qpainter_fb_backend.cpp +) + +add_library(KWinWaylandFbdevBackend MODULE ${FBDEV_SOURCES}) +set_target_properties(KWinWaylandFbdevBackend PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/org.kde.kwin.waylandbackends/") +target_link_libraries(KWinWaylandFbdevBackend kwin SceneQPainterBackend) + +install( + TARGETS + KWinWaylandFbdevBackend + DESTINATION + ${PLUGIN_INSTALL_DIR}/org.kde.kwin.waylandbackends/ +) diff --git a/plugins/platforms/fbdev/fb_backend.cpp b/plugins/platforms/fbdev/fb_backend.cpp new file mode 100644 index 0000000..c74335c --- /dev/null +++ b/plugins/platforms/fbdev/fb_backend.cpp @@ -0,0 +1,274 @@ +/* + 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 "fb_backend.h" + +#include "composite.h" +#include "logging.h" +#include "logind.h" +#include "scene_qpainter_fb_backend.h" +#include "outputscreens.h" +#include "virtual_terminal.h" +#include "udev.h" +// system +#include +#include +#include +#include +// Linux +#include + +namespace KWin +{ + +FramebufferOutput::FramebufferOutput(QObject *parent): + AbstractWaylandOutput(parent) +{ + setName("FB-0"); +} + +void FramebufferOutput::init(const QSize &pixelSize, const QSize &physicalSize) +{ + KWaylandServer::OutputDeviceInterface::Mode mode; + mode.id = 0; + mode.size = pixelSize; + mode.flags = KWaylandServer::OutputDeviceInterface::ModeFlag::Current; + mode.refreshRate = 60000; // TODO: get actual refresh rate of fb device? + initInterfaces("model_TODO", "manufacturer_TODO", "UUID_TODO", physicalSize, { mode }); +} + +FramebufferBackend::FramebufferBackend(QObject *parent) + : Platform(parent) +{ +} + +FramebufferBackend::~FramebufferBackend() +{ + unmap(); + if (m_fd >= 0) { + close(m_fd); + } +} + +Screens *FramebufferBackend::createScreens(QObject *parent) +{ + return new OutputScreens(this, parent); +} + +QPainterBackend *FramebufferBackend::createQPainterBackend() +{ + return new FramebufferQPainterBackend(this); +} + +void FramebufferBackend::init() +{ + setSoftWareCursor(true); + LogindIntegration *logind = LogindIntegration::self(); + auto takeControl = [logind, this]() { + if (logind->hasSessionControl()) { + openFrameBuffer(); + } else { + logind->takeControl(); + connect(logind, &LogindIntegration::hasSessionControlChanged, this, &FramebufferBackend::openFrameBuffer); + } + }; + if (logind->isConnected()) { + takeControl(); + } else { + connect(logind, &LogindIntegration::connectedChanged, this, takeControl); + } + VirtualTerminal::create(this); +} + +void FramebufferBackend::openFrameBuffer() +{ + VirtualTerminal::self()->init(); + QString framebufferDevice = deviceIdentifier().constData(); + if (framebufferDevice.isEmpty()) { + framebufferDevice = QString(Udev().primaryFramebuffer()->devNode()); + } + int fd = LogindIntegration::self()->takeDevice(framebufferDevice.toUtf8().constData()); + qCDebug(KWIN_FB) << "Using frame buffer device:" << framebufferDevice; + if (fd < 0) { + qCWarning(KWIN_FB) << "Failed to open frame buffer device:" << framebufferDevice << "through logind, trying without"; + } + fd = open(framebufferDevice.toUtf8().constData(), O_RDWR | O_CLOEXEC); + if (fd < 0) { + qCWarning(KWIN_FB) << "failed to open frame buffer device:" << framebufferDevice; + emit initFailed(); + return; + } + m_fd = fd; + if (!handleScreenInfo()) { + qCWarning(KWIN_FB) << "failed to handle framebuffer information"; + emit initFailed(); + return; + } + initImageFormat(); + if (m_imageFormat == QImage::Format_Invalid) { + emit initFailed(); + return; + } + setReady(true); + emit screensQueried(); +} + +bool FramebufferBackend::handleScreenInfo() +{ + if (m_fd < 0) { + return false; + } + + fb_var_screeninfo varinfo; + fb_fix_screeninfo fixinfo; + + // Probe the device for screen information. + if (ioctl(m_fd, FBIOGET_FSCREENINFO, &fixinfo) < 0 || ioctl(m_fd, FBIOGET_VSCREENINFO, &varinfo) < 0) { + return false; + } + + // Activate the framebuffer device, assuming this is a non-primary framebuffer device + varinfo.activate = FB_ACTIVATE_NOW | FB_ACTIVATE_FORCE; + ioctl(m_fd, FBIOPUT_VSCREENINFO, &varinfo); + + // Probe the device for new screen information. + if (ioctl(m_fd, FBIOGET_VSCREENINFO, &varinfo) < 0) { + return false; + } + + auto *output = new FramebufferOutput(this); + output->init(QSize(varinfo.xres, varinfo.yres), QSize(varinfo.width, varinfo.height)); + m_outputs << output; + + m_id = QByteArray(fixinfo.id); + m_red = {varinfo.red.offset, varinfo.red.length}; + m_green = {varinfo.green.offset, varinfo.green.length}; + m_blue = {varinfo.blue.offset, varinfo.blue.length}; + m_alpha = {varinfo.transp.offset, varinfo.transp.length}; + m_bitsPerPixel = varinfo.bits_per_pixel; + m_bufferLength = fixinfo.smem_len; + m_bytesPerLine = fixinfo.line_length; + + return true; +} + +void FramebufferBackend::map() +{ + if (m_memory) { + // already mapped; + return; + } + if (m_fd < 0) { + // not valid + return; + } + void *mem = mmap(nullptr, m_bufferLength, PROT_WRITE, MAP_SHARED, m_fd, 0); + if (mem == MAP_FAILED) { + qCWarning(KWIN_FB) << "Failed to mmap frame buffer"; + return; + } + m_memory = mem; +} + +void FramebufferBackend::unmap() +{ + if (!m_memory) { + return; + } + if (munmap(m_memory, m_bufferLength) < 0) { + qCWarning(KWIN_FB) << "Failed to munmap frame buffer"; + } + m_memory = nullptr; +} + +QSize FramebufferBackend::screenSize() const +{ + if (m_outputs.isEmpty()) { + return QSize(); + } + return m_outputs[0]->pixelSize(); +} + +QImage::Format FramebufferBackend::imageFormat() const +{ + return m_imageFormat; +} + +void FramebufferBackend::initImageFormat() +{ + if (m_fd < 0) { + return; + } + + qCDebug(KWIN_FB) << "Bits Per Pixel: " << m_bitsPerPixel; + qCDebug(KWIN_FB) << "Buffer Length: " << m_bufferLength; + qCDebug(KWIN_FB) << "Bytes Per Line: " << m_bytesPerLine; + qCDebug(KWIN_FB) << "Alpha Length: " << m_alpha.length; + qCDebug(KWIN_FB) << "Red Length: " << m_red.length; + qCDebug(KWIN_FB) << "Green Length: " << m_green.length; + qCDebug(KWIN_FB) << "Blue Length: " << m_blue.length; + qCDebug(KWIN_FB) << "Blue Offset: " << m_blue.offset; + qCDebug(KWIN_FB) << "Green Offset: " << m_green.offset; + qCDebug(KWIN_FB) << "Red Offset: " << m_red.offset; + qCDebug(KWIN_FB) << "Alpha Offset: " << m_alpha.offset; + + if (m_bitsPerPixel == 32 && + m_red.length == 8 && + m_green.length == 8 && + m_blue.length == 8 && + m_blue.offset == 0 && + m_green.offset == 8 && + m_red.offset == 16) { + qCDebug(KWIN_FB) << "Framebuffer format is RGB32"; + m_imageFormat = QImage::Format_RGB32; + } else if (m_bitsPerPixel == 32 && + m_red.length == 8 && + m_green.length == 8 && + m_blue.length == 8 && + m_alpha.length == 8 && + m_red.offset == 0 && + m_green.offset == 8 && + m_blue.offset == 16 && + m_alpha.offset == 24) { + qCDebug(KWIN_FB) << "Framebuffer format is RGBA8888"; + m_imageFormat = QImage::Format_RGBA8888; + } else if (m_bitsPerPixel == 24 && + m_red.length == 8 && + m_green.length == 8 && + m_blue.length == 8 && + m_blue.offset == 0 && + m_green.offset == 8 && + m_red.offset == 16) { + qCDebug(KWIN_FB) << "Framebuffer Format is RGB888"; + m_bgr = true; + m_imageFormat = QImage::Format_RGB888; + } else if (m_bitsPerPixel == 16 && + m_red.length == 5 && + m_green.length == 6 && + m_blue.length == 5 && + m_blue.offset == 0 && + m_green.offset == 5 && + m_red.offset == 11) { + qCDebug(KWIN_FB) << "Framebuffer Format is RGB16"; + m_imageFormat = QImage::Format_RGB16; + } else { + qCWarning(KWIN_FB) << "Framebuffer format is unknown"; + } +} + +Outputs FramebufferBackend::outputs() const +{ + return m_outputs; +} + +Outputs FramebufferBackend::enabledOutputs() const +{ + return m_outputs; +} + +} diff --git a/plugins/platforms/fbdev/fb_backend.h b/plugins/platforms/fbdev/fb_backend.h new file mode 100644 index 0000000..4cfe3e3 --- /dev/null +++ b/plugins/platforms/fbdev/fb_backend.h @@ -0,0 +1,108 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + SPDX-FileCopyrightText: 2019 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_FB_BACKEND_H +#define KWIN_FB_BACKEND_H +#include "abstract_wayland_output.h" +#include "platform.h" + +#include +#include + +namespace KWin +{ + +class FramebufferOutput : public AbstractWaylandOutput +{ + Q_OBJECT + +public: + FramebufferOutput(QObject *parent = nullptr); + ~FramebufferOutput() override = default; + + void init(const QSize &pixelSize, const QSize &physicalSize); +}; + +class KWIN_EXPORT FramebufferBackend : public Platform +{ + Q_OBJECT + Q_INTERFACES(KWin::Platform) + Q_PLUGIN_METADATA(IID "org.kde.kwin.Platform" FILE "fbdev.json") +public: + explicit FramebufferBackend(QObject *parent = nullptr); + ~FramebufferBackend() override; + + Screens *createScreens(QObject *parent = nullptr) override; + QPainterBackend *createQPainterBackend() override; + + QSize screenSize() const override; + + void init() override; + + bool isValid() const { + return m_fd >= 0; + } + + void map(); + void unmap(); + void *mappedMemory() const { + return m_memory; + } + int bytesPerLine() const { + return m_bytesPerLine; + } + int bufferSize() const { + return m_bufferLength; + } + quint32 bitsPerPixel() const { + return m_bitsPerPixel; + } + QImage::Format imageFormat() const; + /** + * @returns whether the imageFormat is BGR instead of RGB. + */ + bool isBGR() const { + return m_bgr; + } + + Outputs outputs() const override; + Outputs enabledOutputs() const override; + + QVector supportedCompositors() const override { + return QVector{QPainterCompositing}; + } + +private: + void openFrameBuffer(); + bool handleScreenInfo(); + void initImageFormat(); + + QVector m_outputs; + + QByteArray m_id; + struct Color { + quint32 offset; + quint32 length; + }; + Color m_red; + Color m_green; + Color m_blue; + Color m_alpha; + quint32 m_bitsPerPixel = 0; + int m_fd = -1; + quint32 m_bufferLength = 0; + int m_bytesPerLine = 0; + void *m_memory = nullptr; + QImage::Format m_imageFormat = QImage::Format_Invalid; + bool m_bgr = false; +}; + +} + +#endif diff --git a/plugins/platforms/fbdev/fbdev.json b/plugins/platforms/fbdev/fbdev.json new file mode 100644 index 0000000..0ff46b9 --- /dev/null +++ b/plugins/platforms/fbdev/fbdev.json @@ -0,0 +1,84 @@ +{ + "KPlugin": { + "Description": "Render to framebuffer.", + "Description[az]": "Çərçivə tamponunun formalaşdırılması.", + "Description[ca@valencia]": "Renderitza en el «framebuffer».", + "Description[ca]": "Renderitza al «framebuffer».", + "Description[da]": "Render til framebuffer.", + "Description[de]": "In Framebuffer rendern.", + "Description[el]": "Αποτύπωση σε ενδιάμεση μνήμη πλαισίων.", + "Description[en_GB]": "Render to framebuffer.", + "Description[es]": "Renderizar en el «framebuffer».", + "Description[et]": "Renderdamine kaadripuhvris.", + "Description[eu]": "Errendatu framebuffer batera.", + "Description[fi]": "Hahmonna framebufferiin.", + "Description[fr]": "Rendre sur le « framebuffer ».", + "Description[gl]": "Renderizar no búfer de fotogramas.", + "Description[hu]": "Renderelés framebufferbe.", + "Description[id]": "Render untuk framebuffer.", + "Description[it]": "Resa su framebuffer.", + "Description[ko]": "프레임버퍼에 렌더링합니다.", + "Description[lt]": "Atvaizduoti į vaizdų atnaujinimo buferį.", + "Description[nl]": "Naar framebuffer renderen.", + "Description[nn]": "Teikn opp til biletbuffer.", + "Description[pl]": "Wyświetlaj w buforze klatek.", + "Description[pt]": "Desenhar no 'framebuffer'.", + "Description[pt_BR]": "Renderizar no framebuffer.", + "Description[ro]": "Randează pe framebuffer.", + "Description[ru]": "Отрисовка во фреймбуфер", + "Description[sk]": "RenderovaÅ¥ do framebuffera.", + "Description[sl]": "IzriÅ¡i v medpomnilnik sličic.", + "Description[sr@ijekavian]": "Рендеровање у кадробафер.", + "Description[sr@ijekavianlatin]": "Renderovanje u kadrobafer.", + "Description[sr@latin]": "Renderovanje u kadrobafer.", + "Description[sr]": "Рендеровање у кадробафер.", + "Description[sv]": "Återge i rambuffer.", + "Description[tr]": "Çerçeve tamponuna gerçekle.", + "Description[uk]": "Обробляти до буфера кадрів.", + "Description[x-test]": "xxRender to framebuffer.xx", + "Description[zh_CN]": "渲染到帧缓冲。", + "Description[zh_TW]": "成像至 framebuffer。", + "Id": "KWinWaylandFbdevBackend", + "Name": "framebuffer", + "Name[az]": "çərçivə tamponu", + "Name[ca@valencia]": "Framebuffer", + "Name[ca]": "Framebuffer", + "Name[cs]": "framebuffer", + "Name[da]": "framebuffer", + "Name[de]": "Framebuffer", + "Name[el]": "ενδιάμεση μνήμη πλαισίων", + "Name[en_GB]": "framebuffer", + "Name[es]": "framebuffer", + "Name[et]": "framebuffer", + "Name[eu]": "framebuffer", + "Name[fi]": "framebuffer", + "Name[fr]": "framebuffer", + "Name[gl]": "framebuffer", + "Name[hu]": "framebuffer", + "Name[ia]": "framebuffer", + "Name[id]": "framebuffer", + "Name[it]": "framebuffer", + "Name[ko]": "framebuffer", + "Name[lt]": "framebuffer", + "Name[nl]": "framebuffer", + "Name[nn]": "biletbuffer", + "Name[pl]": "bufor klatek", + "Name[pt]": "'Framebuffer'", + "Name[pt_BR]": "framebuffer", + "Name[ro]": "framebuffer", + "Name[ru]": "framebuffer", + "Name[sk]": "framebuffer", + "Name[sl]": "medpomnilnik sličic", + "Name[sr@ijekavian]": "Кадробафер", + "Name[sr@ijekavianlatin]": "Kadrobafer", + "Name[sr@latin]": "Kadrobafer", + "Name[sr]": "Кадробафер", + "Name[sv]": "rambuffer", + "Name[tr]": "çerçeve tampon", + "Name[uk]": "framebuffer", + "Name[x-test]": "xxframebufferxx", + "Name[zh_CN]": "framebuffer", + "Name[zh_TW]": "framebuffer" + }, + "input": false +} diff --git a/plugins/platforms/fbdev/logging.cpp b/plugins/platforms/fbdev/logging.cpp new file mode 100644 index 0000000..467e898 --- /dev/null +++ b/plugins/platforms/fbdev/logging.cpp @@ -0,0 +1,10 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "logging.h" +Q_LOGGING_CATEGORY(KWIN_FB, "kwin_wayland_framebuffer", QtCriticalMsg) diff --git a/plugins/platforms/fbdev/logging.h b/plugins/platforms/fbdev/logging.h new file mode 100644 index 0000000..e619fba --- /dev/null +++ b/plugins/platforms/fbdev/logging.h @@ -0,0 +1,15 @@ +/* + 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_FB_LOGGING_H +#define KWIN_FB_LOGGING_H +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(KWIN_FB) +#endif diff --git a/plugins/platforms/fbdev/scene_qpainter_fb_backend.cpp b/plugins/platforms/fbdev/scene_qpainter_fb_backend.cpp new file mode 100644 index 0000000..110213c --- /dev/null +++ b/plugins/platforms/fbdev/scene_qpainter_fb_backend.cpp @@ -0,0 +1,95 @@ +/* + 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 "scene_qpainter_fb_backend.h" +#include "fb_backend.h" +#include "composite.h" +#include "logind.h" +#include "cursor.h" +#include "virtual_terminal.h" +// Qt +#include + +namespace KWin +{ +FramebufferQPainterBackend::FramebufferQPainterBackend(FramebufferBackend *backend) + : QObject() + , QPainterBackend() + , m_renderBuffer(backend->screenSize(), QImage::Format_RGB32) + , m_backend(backend) + , m_needsFullRepaint(true) +{ + m_renderBuffer.fill(Qt::black); + m_backend->map(); + + m_backBuffer = QImage((uchar*)m_backend->mappedMemory(), + m_backend->bytesPerLine() / (m_backend->bitsPerPixel() / 8), + m_backend->bufferSize() / m_backend->bytesPerLine(), + m_backend->bytesPerLine(), m_backend->imageFormat()); + m_backBuffer.fill(Qt::black); + + connect(VirtualTerminal::self(), &VirtualTerminal::activeChanged, this, + [] (bool active) { + if (active) { + Compositor::self()->bufferSwapComplete(); + Compositor::self()->addRepaintFull(); + } else { + Compositor::self()->aboutToSwapBuffers(); + } + } + ); +} + +FramebufferQPainterBackend::~FramebufferQPainterBackend() = default; + +QImage* FramebufferQPainterBackend::buffer() +{ + return bufferForScreen(0); +} + +QImage* FramebufferQPainterBackend::bufferForScreen(int screenId) +{ + Q_UNUSED(screenId) + return &m_renderBuffer; +} + +bool FramebufferQPainterBackend::needsFullRepaint() const +{ + return m_needsFullRepaint; +} + +void FramebufferQPainterBackend::prepareRenderingFrame() +{ + m_needsFullRepaint = true; +} + +void FramebufferQPainterBackend::present(int mask, const QRegion &damage) +{ + Q_UNUSED(mask) + Q_UNUSED(damage) + + if (!LogindIntegration::self()->isActiveSession()) { + return; + } + m_needsFullRepaint = false; + + QPainter p(&m_backBuffer); + p.drawImage(QPoint(0, 0), m_backend->isBGR() ? m_renderBuffer.rgbSwapped() : m_renderBuffer); +} + +bool FramebufferQPainterBackend::usesOverlayWindow() const +{ + return false; +} + +bool FramebufferQPainterBackend::perScreenRendering() const +{ + return true; +} + +} diff --git a/plugins/platforms/fbdev/scene_qpainter_fb_backend.h b/plugins/platforms/fbdev/scene_qpainter_fb_backend.h new file mode 100644 index 0000000..7ea657a --- /dev/null +++ b/plugins/platforms/fbdev/scene_qpainter_fb_backend.h @@ -0,0 +1,51 @@ +/* + 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_SCENE_QPAINTER_FB_BACKEND_H +#define KWIN_SCENE_QPAINTER_FB_BACKEND_H +#include + +#include +#include + +namespace KWin +{ +class FramebufferBackend; + +class FramebufferQPainterBackend : public QObject, public QPainterBackend +{ + Q_OBJECT +public: + FramebufferQPainterBackend(FramebufferBackend *backend); + ~FramebufferQPainterBackend() override; + + QImage *buffer() override; + QImage *bufferForScreen(int screenId) override; + bool needsFullRepaint() const override; + bool usesOverlayWindow() const override; + void prepareRenderingFrame() override; + void present(int mask, const QRegion &damage) override; + bool perScreenRendering() const override; + +private: + /** + * @brief mapped memory buffer on fb device + */ + QImage m_renderBuffer; + /** + * @brief buffer to draw into + */ + QImage m_backBuffer; + + FramebufferBackend *m_backend; + bool m_needsFullRepaint; +}; + +} + +#endif diff --git a/plugins/platforms/hwcomposer/CMakeLists.txt b/plugins/platforms/hwcomposer/CMakeLists.txt new file mode 100644 index 0000000..39ed8ad --- /dev/null +++ b/plugins/platforms/hwcomposer/CMakeLists.txt @@ -0,0 +1,26 @@ +set(HWCOMPOSER_SOURCES + egl_hwcomposer_backend.cpp + hwcomposer_backend.cpp + logging.cpp + screens_hwcomposer.cpp +) + +include_directories(${CMAKE_SOURCE_DIR}/platformsupport/scenes/opengl) +add_library(KWinWaylandHwcomposerBackend MODULE ${HWCOMPOSER_SOURCES}) +set_target_properties(KWinWaylandHwcomposerBackend PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/org.kde.kwin.waylandbackends/") +target_link_libraries(KWinWaylandHwcomposerBackend + kwin + + SceneOpenGLBackend + + libhybris::hwcomposer + libhybris::hybriseglplatform + libhybris::libhardware +) + +install( + TARGETS + KWinWaylandHwcomposerBackend + DESTINATION + ${PLUGIN_INSTALL_DIR}/org.kde.kwin.waylandbackends/ +) diff --git a/plugins/platforms/hwcomposer/egl_hwcomposer_backend.cpp b/plugins/platforms/hwcomposer/egl_hwcomposer_backend.cpp new file mode 100644 index 0000000..06255bd --- /dev/null +++ b/plugins/platforms/hwcomposer/egl_hwcomposer_backend.cpp @@ -0,0 +1,176 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-3.0-or-later +*/ +#include "egl_hwcomposer_backend.h" +#include "hwcomposer_backend.h" +#include "logging.h" + +namespace KWin +{ + +EglHwcomposerBackend::EglHwcomposerBackend(HwcomposerBackend *backend) + : AbstractEglBackend() + , m_backend(backend) +{ + // EGL is always direct rendering + setIsDirectRendering(true); + setSyncsToVBlank(true); +} + +EglHwcomposerBackend::~EglHwcomposerBackend() +{ + cleanup(); +} + +bool EglHwcomposerBackend::initializeEgl() +{ + // cannot use initClientExtensions as that crashes in libhybris + qputenv("EGL_PLATFORM", QByteArrayLiteral("hwcomposer")); + EGLDisplay display = m_backend->sceneEglDisplay(); + + if (display == EGL_NO_DISPLAY) { + display = eglGetDisplay(nullptr); + } + if (display == EGL_NO_DISPLAY) { + return false; + } + setEglDisplay(display); + return initEglAPI(); +} + +void EglHwcomposerBackend::init() +{ + if (!initializeEgl()) { + setFailed("Failed to initialize egl"); + return; + } + if (!initRenderingContext()) { + setFailed("Could not initialize rendering context"); + return; + } + + initKWinGL(); + initBufferAge(); + initWayland(); +} + +bool EglHwcomposerBackend::initBufferConfigs() +{ + const EGLint config_attribs[] = { + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_ALPHA_SIZE, 8, + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL_NONE, + }; + + EGLint count; + EGLConfig configs[1024]; + if (eglChooseConfig(eglDisplay(), config_attribs, configs, 1, &count) == EGL_FALSE) { + qCCritical(KWIN_HWCOMPOSER) << "choose config failed"; + return false; + } + if (count != 1) { + qCCritical(KWIN_HWCOMPOSER) << "choose config did not return a config" << count; + return false; + } + setConfig(configs[0]); + + return true; +} + +bool EglHwcomposerBackend::initRenderingContext() +{ + if (!initBufferConfigs()) { + return false; + } + + if (!createContext()) { + return false; + } + + m_nativeSurface = m_backend->createSurface(); + EGLSurface surface = eglCreateWindowSurface(eglDisplay(), config(), (EGLNativeWindowType)static_cast(m_nativeSurface), nullptr); + if (surface == EGL_NO_SURFACE) { + qCCritical(KWIN_HWCOMPOSER) << "Create surface failed"; + return false; + } + setSurface(surface); + + return makeContextCurrent(); +} + +bool EglHwcomposerBackend::makeContextCurrent() +{ + if (eglMakeCurrent(eglDisplay(), surface(), surface(), context()) == EGL_FALSE) { + qCCritical(KWIN_HWCOMPOSER) << "Make Context Current failed"; + return false; + } + + EGLint error = eglGetError(); + if (error != EGL_SUCCESS) { + qCWarning(KWIN_HWCOMPOSER) << "Error occurred while creating context " << error; + return false; + } + + const QSize overall = m_backend->size(); + glViewport(0, 0, overall.width(), overall.height()); + + return true; +} + +void EglHwcomposerBackend::present() +{ + if (lastDamage().isEmpty()) { + return; + } + + eglSwapBuffers(eglDisplay(), surface()); + setLastDamage(QRegion()); +} + +void EglHwcomposerBackend::screenGeometryChanged(const QSize &size) +{ + Q_UNUSED(size) +} + +QRegion EglHwcomposerBackend::prepareRenderingFrame() +{ + present(); + + // TODO: buffer age? + startRenderTimer(); + // triggers always a full repaint + return QRegion(QRect(QPoint(0, 0), m_backend->size())); +} + +void EglHwcomposerBackend::endRenderingFrame(const QRegion &renderedRegion, const QRegion &damagedRegion) +{ + Q_UNUSED(damagedRegion) + setLastDamage(renderedRegion); +} + +SceneOpenGLTexturePrivate *EglHwcomposerBackend::createBackendTexture(SceneOpenGLTexture *texture) +{ + return new EglHwcomposerTexture(texture, this); +} + +bool EglHwcomposerBackend::usesOverlayWindow() const +{ + return false; +} + +EglHwcomposerTexture::EglHwcomposerTexture(SceneOpenGLTexture *texture, EglHwcomposerBackend *backend) + : AbstractEglTexture(texture, backend) +{ +} + +EglHwcomposerTexture::~EglHwcomposerTexture() = default; + +} diff --git a/plugins/platforms/hwcomposer/egl_hwcomposer_backend.h b/plugins/platforms/hwcomposer/egl_hwcomposer_backend.h new file mode 100644 index 0000000..db0ba4d --- /dev/null +++ b/plugins/platforms/hwcomposer/egl_hwcomposer_backend.h @@ -0,0 +1,55 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-3.0-or-later +*/ +#ifndef KWIN_EGL_HWCOMPOSER_BACKEND_H +#define KWIN_EGL_HWCOMPOSER_BACKEND_H +#include "abstract_egl_backend.h" + +namespace KWin +{ + +class HwcomposerBackend; +class HwcomposerWindow; + +class EglHwcomposerBackend : public AbstractEglBackend +{ +public: + EglHwcomposerBackend(HwcomposerBackend *backend); + virtual ~EglHwcomposerBackend(); + bool usesOverlayWindow() const override; + SceneOpenGLTexturePrivate *createBackendTexture(SceneOpenGLTexture *texture) override; + void screenGeometryChanged(const QSize &size) override; + QRegion prepareRenderingFrame() override; + void endRenderingFrame(const QRegion &renderedRegion, const QRegion &damagedRegion) override; + void init() override; + +protected: + void present() override; + +private: + bool initializeEgl(); + bool initRenderingContext(); + bool initBufferConfigs(); + bool makeContextCurrent(); + HwcomposerBackend *m_backend; + HwcomposerWindow *m_nativeSurface = nullptr; +}; + +class EglHwcomposerTexture : public AbstractEglTexture +{ +public: + virtual ~EglHwcomposerTexture(); + +private: + friend class EglHwcomposerBackend; + EglHwcomposerTexture(SceneOpenGLTexture *texture, EglHwcomposerBackend *backend); +}; + +} + +#endif diff --git a/plugins/platforms/hwcomposer/hwcomposer.json b/plugins/platforms/hwcomposer/hwcomposer.json new file mode 100644 index 0000000..e536b52 --- /dev/null +++ b/plugins/platforms/hwcomposer/hwcomposer.json @@ -0,0 +1,83 @@ +{ + "KPlugin": { + "Description": "Render through hwcomposer through libhybris.", + "Description[az]": "Libhybris vasitəsi ilə hwcomposer içindən formalaşdırmaq.", + "Description[ca@valencia]": "Renderitza mitjançant «hwcomposer» mitjançant «libhybris».", + "Description[ca]": "Renderitza mitjançant el «hwcomposer» mitjançant «libhybris».", + "Description[da]": "Rendér igennem hwcomposer igennem libhybris.", + "Description[de]": "In hwcomposer mit libhybris rendern.", + "Description[el]": "Αποτύπωση μέσω hwcomposer μέσω libhybris.", + "Description[en_GB]": "Render through hwcomposer through libhybris.", + "Description[es]": "Renderizar a través «hwcomposer» mediante «libhybris».", + "Description[et]": "Renderdamine hwcomposeris libhybrise abil.", + "Description[eu]": "Errendatu hwcomposer libhybris bidez erabiliz.", + "Description[fi]": "Hahmonna hwcomposerin läpi libhybristä käyttäen.", + "Description[fr]": "Rendre par le biais de « hwcomposer » via « libhybris ».", + "Description[gl]": "Renderizar a través de hwcomposer a través de libhybris.", + "Description[hu]": "Renderelés hwcomposerrel libhybrisen keresztül.", + "Description[id]": "Render melalui hwcomposer melalui libhybris.", + "Description[it]": "Resa tramite hwcomposer attraverso libhybris.", + "Description[ko]": "libhybris를 통하여 hwcomposer로 렌더링합니다.", + "Description[lt]": "Atvaizduoti per hwcomposer per libhybris.", + "Description[nl]": "Render via hwcomposer via libhybris.", + "Description[nn]": "Teikn opp via hwcomposer gjennom libhybris.", + "Description[pl]": "Wyświetlaj przez sprzętowy kompozytor przez libhybris.", + "Description[pt]": "Desenhar através do Hwcomposer, usando a libhybris.", + "Description[pt_BR]": "Renderizar através do hwcomposer e libhybris.", + "Description[ro]": "Randează prin hwcomposer prin libhybris.", + "Description[ru]": "Отрисовка через hwcomposer с использованием libhybris.", + "Description[sk]": "RenderovaÅ¥ cez hwcomposer cez libhybris.", + "Description[sl]": "IzriÅ¡i preko hwcomposer-ja in libhybris.", + "Description[sr@ijekavian]": "Рендеровање кроз ХВ‑композер кроз libhybris.", + "Description[sr@ijekavianlatin]": "Renderovanje kroz HWcomposer kroz libhybris.", + "Description[sr@latin]": "Renderovanje kroz HWcomposer kroz libhybris.", + "Description[sr]": "Рендеровање кроз ХВ‑композер кроз libhybris.", + "Description[sv]": "Återge via pÃ¥ hÃ¥rdvarusammansättare via libhybris.", + "Description[tr]": "libhybris aracılığıyla hwcomposer içinden gerçekle.", + "Description[uk]": "Обробляти за допомогою апаратного засобу композиції через libhybris.", + "Description[x-test]": "xxRender through hwcomposer through libhybris.xx", + "Description[zh_CN]": "使用 libhybris 通过 hwcomposer 渲染。", + "Description[zh_TW]": "透過 libhybris 成像到 hwcomposer。", + "Id": "KWinWaylandHwcomposerBackend", + "Name": "hwcomposer", + "Name[az]": "hwcomposer", + "Name[ca@valencia]": "hwcomposer", + "Name[ca]": "hwcomposer", + "Name[cs]": "hwcomposer", + "Name[da]": "hwcomposer", + "Name[de]": "hwcomposer", + "Name[el]": "hwcomposer", + "Name[en_GB]": "hwcomposer", + "Name[es]": "hwcomposer", + "Name[et]": "hwcomposer", + "Name[eu]": "hwcomposer", + "Name[fi]": "hwcomposer", + "Name[fr]": "hwcomposer", + "Name[gl]": "hwcomposer", + "Name[hu]": "hwcomposer", + "Name[id]": "hwcomposer", + "Name[it]": "hwcomposer", + "Name[ko]": "hwcomposer", + "Name[lt]": "hwcomposer", + "Name[nl]": "hwcomposer", + "Name[nn]": "hwcomposer", + "Name[pl]": "sprzętowy kompozytor", + "Name[pt]": "Hwcomposer", + "Name[pt_BR]": "hwcomposer", + "Name[ro]": "hwcomposer", + "Name[ru]": "hwcomposer", + "Name[sk]": "hwcomposer", + "Name[sl]": "hwcomposer", + "Name[sr@ijekavian]": "ХВ‑композер", + "Name[sr@ijekavianlatin]": "HWcomposer", + "Name[sr@latin]": "HWcomposer", + "Name[sr]": "ХВ‑композер", + "Name[sv]": "hÃ¥rdvarusammansättare", + "Name[tr]": "hwcomposer", + "Name[uk]": "hwcomposer", + "Name[x-test]": "xxhwcomposerxx", + "Name[zh_CN]": "hwcomposer", + "Name[zh_TW]": "hwcomposer" + }, + "input": false +} diff --git a/plugins/platforms/hwcomposer/hwcomposer_backend.cpp b/plugins/platforms/hwcomposer/hwcomposer_backend.cpp new file mode 100644 index 0000000..411010b --- /dev/null +++ b/plugins/platforms/hwcomposer/hwcomposer_backend.cpp @@ -0,0 +1,538 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-3.0-or-later +*/ +#include "egl_hwcomposer_backend.h" +#include "hwcomposer_backend.h" +#include "logging.h" +#include "screens_hwcomposer.h" +#include "composite.h" +#include "input.h" +#include "main.h" +#include "wayland_server.h" +// KWayland +#include +// KDE +#include +// Qt +#include +#include +// hybris/android +#include +#include +// linux +#include + +// based on test_hwcomposer.c from libhybris project (Apache 2 licensed) + +using namespace KWaylandServer; + +namespace KWin +{ + +BacklightInputEventFilter::BacklightInputEventFilter(HwcomposerBackend *backend) + : InputEventFilter() + , m_backend(backend) +{ +} + +BacklightInputEventFilter::~BacklightInputEventFilter() = default; + +bool BacklightInputEventFilter::pointerEvent(QMouseEvent *event, quint32 nativeButton) +{ + Q_UNUSED(event) + Q_UNUSED(nativeButton) + if (!m_backend->isBacklightOff()) { + return false; + } + toggleBacklight(); + return true; +} + +bool BacklightInputEventFilter::wheelEvent(QWheelEvent *event) +{ + Q_UNUSED(event) + if (!m_backend->isBacklightOff()) { + return false; + } + toggleBacklight(); + return true; +} + +bool BacklightInputEventFilter::keyEvent(QKeyEvent *event) +{ + if (event->key() == Qt::Key_PowerOff && event->type() == QEvent::KeyRelease) { + toggleBacklight(); + return true; + } + return m_backend->isBacklightOff(); +} + +bool BacklightInputEventFilter::touchDown(qint32 id, const QPointF &pos, quint32 time) +{ + Q_UNUSED(pos) + Q_UNUSED(time) + if (!m_backend->isBacklightOff()) { + return false; + } + if (m_touchPoints.isEmpty()) { + if (!m_doubleTapTimer.isValid()) { + // this is the first tap + m_doubleTapTimer.start(); + } else { + if (m_doubleTapTimer.elapsed() < qApp->doubleClickInterval()) { + m_secondTap = true; + } else { + // took too long. Let's consider it a new click + m_doubleTapTimer.restart(); + } + } + } else { + // not a double tap + m_doubleTapTimer.invalidate(); + m_secondTap = false; + } + m_touchPoints << id; + return true; +} + +bool BacklightInputEventFilter::touchUp(qint32 id, quint32 time) +{ + Q_UNUSED(time) + m_touchPoints.removeAll(id); + if (!m_backend->isBacklightOff()) { + return false; + } + if (m_touchPoints.isEmpty() && m_doubleTapTimer.isValid() && m_secondTap) { + if (m_doubleTapTimer.elapsed() < qApp->doubleClickInterval()) { + toggleBacklight(); + } + m_doubleTapTimer.invalidate(); + m_secondTap = false; + } + return true; +} + +bool BacklightInputEventFilter::touchMotion(qint32 id, const QPointF &pos, quint32 time) +{ + Q_UNUSED(id) + Q_UNUSED(pos) + Q_UNUSED(time) + return m_backend->isBacklightOff(); +} + +void BacklightInputEventFilter::toggleBacklight() +{ + // queued to not modify the list of event filters while filtering + QMetaObject::invokeMethod(m_backend, "toggleBlankOutput", Qt::QueuedConnection); +} + +HwcomposerBackend::HwcomposerBackend(QObject *parent) + : Platform(parent) +{ + if (!QDBusConnection::sessionBus().connect(QStringLiteral("org.kde.Solid.PowerManagement"), + QStringLiteral("/org/kde/Solid/PowerManagement/Actions/BrightnessControl"), + QStringLiteral("org.kde.Solid.PowerManagement.Actions.BrightnessControl"), + QStringLiteral("brightnessChanged"), this, + SLOT(screenBrightnessChanged(int)))) { + qCWarning(KWIN_HWCOMPOSER) << "Failed to connect to brightness control"; + } +} + +HwcomposerBackend::~HwcomposerBackend() +{ + if (!m_outputBlank) { + toggleBlankOutput(); + } +} + +void HwcomposerBackend::init() +{ + hw_module_t *hwcModule = nullptr; + if (hw_get_module(HWC_HARDWARE_MODULE_ID, (const hw_module_t **)&hwcModule) != 0) { + qCWarning(KWIN_HWCOMPOSER) << "Failed to get hwcomposer module"; + emit initFailed(); + return; + } + + hwc_composer_device_1_t *hwcDevice = nullptr; + if (hwc_open_1(hwcModule, &hwcDevice) != 0) { + qCWarning(KWIN_HWCOMPOSER) << "Failed to open hwcomposer device"; + emit initFailed(); + return; + } + + // unblank, setPowerMode? + m_device = hwcDevice; + + m_hwcVersion = m_device->common.version; + if ((m_hwcVersion & 0xffff0000) == 0) { + // Assume header version is always 1 + uint32_t header_version = 1; + // Legacy version encoding + m_hwcVersion = (m_hwcVersion << 16) | header_version; + } + + // register callbacks + hwc_procs_t *procs = new hwc_procs_t; + procs->invalidate = [] (const struct hwc_procs* procs) { + Q_UNUSED(procs) + }; + procs->vsync = [] (const struct hwc_procs* procs, int disp, int64_t timestamp) { + Q_UNUSED(procs) + if (disp != 0) { + return; + } + dynamic_cast(kwinApp()->platform())->wakeVSync(); + }; + procs->hotplug = [] (const struct hwc_procs* procs, int disp, int connected) { + Q_UNUSED(procs) + Q_UNUSED(disp) + Q_UNUSED(connected) + }; + m_device->registerProcs(m_device, procs); + + //move to HwcomposerOutput + signal + + initLights(); + toggleBlankOutput(); + m_filter.reset(new BacklightInputEventFilter(this)); + input()->prependInputEventFilter(m_filter.data()); + + // get display configuration + m_output.reset(new HwcomposerOutput(hwcDevice)); + if (!m_output->isValid()) { + emit initFailed(); + return; + } + + if (m_output->refreshRate() != 0) { + m_vsyncInterval = 1000000/m_output->refreshRate(); + } + + if (m_lights) { + using namespace KWaylandServer; + + auto updateDpms = [this] { + if (!m_output || !m_output->waylandOutput()) { + m_output->waylandOutput()->setDpmsMode(m_outputBlank ? OutputInterface::DpmsMode::Off : OutputInterface::DpmsMode::On); + } + }; + connect(this, &HwcomposerBackend::outputBlankChanged, this, updateDpms); + + connect(m_output.data(), &HwcomposerOutput::dpmsModeRequested, this, + [this] (KWaylandServer::OutputInterface::DpmsMode mode) { + if (mode == OutputInterface::DpmsMode::On) { + if (m_outputBlank) { + toggleBlankOutput(); + } + } else { + if (!m_outputBlank) { + toggleBlankOutput(); + } + } + } + ); + } + + emit screensQueried(); + setReady(true); +} + +QSize HwcomposerBackend::size() const +{ + if (m_output) { + return m_output->pixelSize(); + } + return QSize(); +} + +QSize HwcomposerBackend::screenSize() const +{ + if (m_output) { + return m_output->pixelSize() / m_output->scale(); + } + return QSize(); +} + +int HwcomposerBackend::scale() const + { + if (m_output) { + return m_output->scale(); + } + return 1; +} + +void HwcomposerBackend::initLights() +{ + hw_module_t *lightsModule = nullptr; + if (hw_get_module(LIGHTS_HARDWARE_MODULE_ID, (const hw_module_t **)&lightsModule) != 0) { + qCWarning(KWIN_HWCOMPOSER) << "Failed to get lights module"; + return; + } + light_device_t *lightsDevice = nullptr; + if (lightsModule->methods->open(lightsModule, LIGHT_ID_BACKLIGHT, (hw_device_t **)&lightsDevice) != 0) { + qCWarning(KWIN_HWCOMPOSER) << "Failed to create lights device"; + return; + } + m_lights = lightsDevice; +} + +void HwcomposerBackend::toggleBlankOutput() +{ + if (!m_device) { + return; + } + m_outputBlank = !m_outputBlank; + toggleScreenBrightness(); + +#if defined(HWC_DEVICE_API_VERSION_1_4) || defined(HWC_DEVICE_API_VERSION_1_5) + if (m_hwcVersion > HWC_DEVICE_API_VERSION_1_3) + m_device->setPowerMode(m_device, 0, m_outputBlank ? HWC_POWER_MODE_OFF : HWC_POWER_MODE_NORMAL); + else +#endif + m_device->blank(m_device, 0, m_outputBlank ? 1 : 0); + + // only disable Vsync, enable happens after next frame rendered + if (m_outputBlank) { + enableVSync(false); + } + // enable/disable compositor repainting when blanked + setOutputsEnabled(!m_outputBlank); + if (Compositor *compositor = Compositor::self()) { + if (!m_outputBlank) { + compositor->addRepaintFull(); + } + } + emit outputBlankChanged(); +} + +void HwcomposerBackend::toggleScreenBrightness() +{ + if (!m_lights) { + return; + } + const int brightness = m_outputBlank ? 0 : m_oldScreenBrightness; + struct light_state_t state; + state.flashMode = LIGHT_FLASH_NONE; + state.brightnessMode = BRIGHTNESS_MODE_USER; + + state.color = (int)((0xffU << 24) | (brightness << 16) | + (brightness << 8) | brightness); + m_lights->set_light(m_lights, &state); +} + +void HwcomposerBackend::enableVSync(bool enable) +{ + if (m_hasVsync == enable) { + return; + } + const int result = m_device->eventControl(m_device, 0, HWC_EVENT_VSYNC, enable ? 1: 0); + m_hasVsync = enable && (result == 0); +} + +HwcomposerWindow *HwcomposerBackend::createSurface() +{ + return new HwcomposerWindow(this); +} + +Screens *HwcomposerBackend::createScreens(QObject *parent) +{ + return new HwcomposerScreens(this, parent); +} + +Outputs HwcomposerBackend::outputs() const +{ + if (!m_output.isNull()) { + return QVector({m_output.data()}); + } + return {}; +} + +Outputs HwcomposerBackend::enabledOutputs() const +{ + return outputs(); +} + + +OpenGLBackend *HwcomposerBackend::createOpenGLBackend() +{ + return new EglHwcomposerBackend(this); +} + +void HwcomposerBackend::waitVSync() +{ + if (!m_hasVsync) { + return; + } + m_vsyncMutex.lock(); + m_vsyncWaitCondition.wait(&m_vsyncMutex, m_vsyncInterval); + m_vsyncMutex.unlock(); +} + +void HwcomposerBackend::wakeVSync() +{ + m_vsyncMutex.lock(); + m_vsyncWaitCondition.wakeAll(); + m_vsyncMutex.unlock(); +} + +static void initLayer(hwc_layer_1_t *layer, const hwc_rect_t &rect, int layerCompositionType) +{ + memset(layer, 0, sizeof(hwc_layer_1_t)); + layer->compositionType = layerCompositionType; + layer->hints = 0; + layer->flags = 0; + layer->handle = 0; + layer->transform = 0; + layer->blending = HWC_BLENDING_NONE; +#ifdef HWC_DEVICE_API_VERSION_1_3 + layer->sourceCropf.top = 0.0f; + layer->sourceCropf.left = 0.0f; + layer->sourceCropf.bottom = (float) rect.bottom; + layer->sourceCropf.right = (float) rect.right; +#else + layer->sourceCrop = rect; +#endif + layer->displayFrame = rect; + layer->visibleRegionScreen.numRects = 1; + layer->visibleRegionScreen.rects = &layer->displayFrame; + layer->acquireFenceFd = -1; + layer->releaseFenceFd = -1; + layer->planeAlpha = 0xFF; +#ifdef HWC_DEVICE_API_VERSION_1_5 + layer->surfaceDamage.numRects = 0; +#endif +} + +HwcomposerWindow::HwcomposerWindow(HwcomposerBackend *backend) + : HWComposerNativeWindow(backend->size().width(), backend->size().height(), HAL_PIXEL_FORMAT_RGBA_8888) + , m_backend(backend) +{ + setBufferCount(3); + + size_t size = sizeof(hwc_display_contents_1_t) + 2 * sizeof(hwc_layer_1_t); + hwc_display_contents_1_t *list = (hwc_display_contents_1_t*)malloc(size); + m_list = (hwc_display_contents_1_t**)malloc(HWC_NUM_DISPLAY_TYPES * sizeof(hwc_display_contents_1_t *)); + for (int i = 0; i < HWC_NUM_DISPLAY_TYPES; ++i) { + m_list[i] = nullptr; + } + // Assign buffer only to the first item, otherwise you get tearing + // if passed the same to multiple places + // see https://github.com/mer-hybris/qt5-qpa-hwcomposer-plugin/commit/f1d802151e8a4f5d10d60eb8de8e07552b93a34a + m_list[0] = list; + const hwc_rect_t rect = { + 0, + 0, + m_backend->size().width(), + m_backend->size().height() + }; + initLayer(&list->hwLayers[0], rect, HWC_FRAMEBUFFER); + initLayer(&list->hwLayers[1], rect, HWC_FRAMEBUFFER_TARGET); + + list->retireFenceFd = -1; + list->flags = HWC_GEOMETRY_CHANGED; + list->numHwLayers = 2; +} + +HwcomposerWindow::~HwcomposerWindow() +{ + // TODO: cleanup +} + +void HwcomposerWindow::present(HWComposerNativeWindowBuffer *buffer) +{ + m_backend->waitVSync(); + hwc_composer_device_1_t *device = m_backend->device(); + + auto fblayer = &m_list[0]->hwLayers[1]; + fblayer->handle = buffer->handle; + fblayer->acquireFenceFd = getFenceBufferFd(buffer); + fblayer->releaseFenceFd = -1; + + int err = device->prepare(device, 1, m_list); + Q_ASSERT(err == 0); + + err = device->set(device, 1, m_list); + Q_ASSERT(err == 0); + m_backend->enableVSync(true); + setFenceBufferFd(buffer, fblayer->releaseFenceFd); + + if (m_list[0]->retireFenceFd != -1) { + close(m_list[0]->retireFenceFd); + m_list[0]->retireFenceFd = -1; + } + m_list[0]->flags = 0; +} + +HwcomposerOutput::HwcomposerOutput(hwc_composer_device_1_t *device) + : AbstractWaylandOutput() + , m_device(device) +{ + uint32_t configs[5]; + size_t numConfigs = 5; + if (device->getDisplayConfigs(device, 0, configs, &numConfigs) != 0) { + qCWarning(KWIN_HWCOMPOSER) << "Failed to get hwcomposer display configurations"; + return; + } + + int32_t attr_values[5]; + uint32_t attributes[] = { + HWC_DISPLAY_WIDTH, + HWC_DISPLAY_HEIGHT, + HWC_DISPLAY_DPI_X, + HWC_DISPLAY_DPI_Y, + HWC_DISPLAY_VSYNC_PERIOD , + HWC_DISPLAY_NO_ATTRIBUTE + }; + device->getDisplayAttributes(device, 0, configs[0], attributes, attr_values); + QSize pixelSize(attr_values[0], attr_values[1]); + if (pixelSize.isEmpty()) { + return; + } + + QSizeF physicalSize; + if (attr_values[2] != 0 && attr_values[3] != 0) { + static const qreal factor = 25.4; + physicalSize = QSizeF(qreal(pixelSize.width() * 1000) / qreal(attr_values[2]) * factor, + qreal(pixelSize.height() * 1000) / qreal(attr_values[3]) * factor); + } else { + // couldn't read physical size, assume 96 dpi + physicalSize = pixelSize / 3.8; + } + + OutputDeviceInterface::Mode mode; + mode.id = 0; + mode.size = pixelSize; + mode.flags = OutputDeviceInterface::ModeFlag::Current | OutputDeviceInterface::ModeFlag::Preferred; + mode.refreshRate = (attr_values[4] == 0) ? 60000 : 10E11/attr_values[4]; + + initInterfaces(QString(), QString(), QByteArray(), physicalSize.toSize(), {mode}); + setInternal(true); + setDpmsSupported(true); + + const auto outputGroup = kwinApp()->config()->group("HWComposerOutputs").group("0"); + setScale(outputGroup.readEntry("Scale", 1)); + setWaylandMode(pixelSize, mode.refreshRate); +} + +HwcomposerOutput::~HwcomposerOutput() +{ + hwc_close_1(m_device); +} + +bool HwcomposerOutput::isValid() const +{ + return isEnabled(); +} + +void HwcomposerOutput::updateDpms(KWaylandServer::OutputInterface::DpmsMode mode) +{ + emit dpmsModeRequested(mode); +} + +} diff --git a/plugins/platforms/hwcomposer/hwcomposer_backend.h b/plugins/platforms/hwcomposer/hwcomposer_backend.h new file mode 100644 index 0000000..cc6fa7e --- /dev/null +++ b/plugins/platforms/hwcomposer/hwcomposer_backend.h @@ -0,0 +1,155 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-3.0-or-later +*/ +#ifndef KWIN_HWCOMPOSER_BACKEND_H +#define KWIN_HWCOMPOSER_BACKEND_H +#include "platform.h" +#include "abstract_wayland_output.h" +#include "input.h" + +#include +#include +#include + +#include +// libhybris +#include +#include +// needed as hwcomposer_window.h includes EGL which on non-arm includes Xlib +#include + +typedef struct hwc_display_contents_1 hwc_display_contents_1_t; +typedef struct hwc_layer_1 hwc_layer_1_t; +typedef struct hwc_composer_device_1 hwc_composer_device_1_t; +struct light_device_t; + +class HWComposerNativeWindowBuffer; + +namespace KWin +{ + +class HwcomposerWindow; +class BacklightInputEventFilter; + +class HwcomposerOutput : public AbstractWaylandOutput +{ + Q_OBJECT +public: + HwcomposerOutput(hwc_composer_device_1_t *device); + ~HwcomposerOutput() override; + bool isValid() const; + + void updateDpms(KWaylandServer::OutputInterface::DpmsMode mode) override; +Q_SIGNALS: + void dpmsModeRequested(KWaylandServer::OutputInterface::DpmsMode mode); +private: + QSize m_pixelSize; + hwc_composer_device_1_t *m_device; +}; + +class HwcomposerBackend : public Platform +{ + Q_OBJECT + Q_INTERFACES(KWin::Platform) + Q_PLUGIN_METADATA(IID "org.kde.kwin.Platform" FILE "hwcomposer.json") +public: + explicit HwcomposerBackend(QObject *parent = nullptr); + virtual ~HwcomposerBackend(); + + void init() override; + Screens *createScreens(QObject *parent = nullptr) override; + OpenGLBackend *createOpenGLBackend() override; + + Outputs outputs() const override; + Outputs enabledOutputs() const override; + + QSize size() const; + QSize screenSize() const override; + + int scale() const; + + HwcomposerWindow *createSurface(); + + hwc_composer_device_1_t *device() const { + return m_device; + } + void enableVSync(bool enable); + void waitVSync(); + void wakeVSync(); + + bool isBacklightOff() const { + return m_outputBlank; + } + + QVector supportedCompositors() const override { + return QVector{OpenGLCompositing}; + } + +Q_SIGNALS: + void outputBlankChanged(); + +private Q_SLOTS: + void toggleBlankOutput(); + void screenBrightnessChanged(int brightness) { + m_oldScreenBrightness = brightness; + } + +private: + void initLights(); + void toggleScreenBrightness(); + hwc_composer_device_1_t *m_device = nullptr; + light_device_t *m_lights = nullptr; + bool m_outputBlank = true; + int m_vsyncInterval = 16; + uint32_t m_hwcVersion; + int m_oldScreenBrightness = 0x7f; + bool m_hasVsync = false; + QMutex m_vsyncMutex; + QWaitCondition m_vsyncWaitCondition; + QScopedPointer m_filter; + QScopedPointer m_output; +}; + +class HwcomposerWindow : public HWComposerNativeWindow +{ +public: + virtual ~HwcomposerWindow(); + + void present(HWComposerNativeWindowBuffer *buffer); + +private: + friend HwcomposerBackend; + HwcomposerWindow(HwcomposerBackend *backend); + HwcomposerBackend *m_backend; + hwc_display_contents_1_t **m_list; +}; + +class BacklightInputEventFilter : public InputEventFilter +{ +public: + BacklightInputEventFilter(HwcomposerBackend *backend); + virtual ~BacklightInputEventFilter(); + + bool pointerEvent(QMouseEvent *event, quint32 nativeButton) override; + bool wheelEvent(QWheelEvent *event) override; + bool keyEvent(QKeyEvent *event) override; + bool touchDown(qint32 id, const QPointF &pos, quint32 time) override; + bool touchMotion(qint32 id, const QPointF &pos, quint32 time) override; + bool touchUp(qint32 id, quint32 time) override; + +private: + void toggleBacklight(); + HwcomposerBackend *m_backend; + QElapsedTimer m_doubleTapTimer; + QVector m_touchPoints; + bool m_secondTap = false; +}; + +} + +#endif diff --git a/plugins/platforms/hwcomposer/logging.cpp b/plugins/platforms/hwcomposer/logging.cpp new file mode 100644 index 0000000..8d19be3 --- /dev/null +++ b/plugins/platforms/hwcomposer/logging.cpp @@ -0,0 +1,10 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-3.0-or-later +*/ +#include "logging.h" +Q_LOGGING_CATEGORY(KWIN_HWCOMPOSER, "kwin_wayland_hwcomposer", QtCriticalMsg) diff --git a/plugins/platforms/hwcomposer/logging.h b/plugins/platforms/hwcomposer/logging.h new file mode 100644 index 0000000..ed62926 --- /dev/null +++ b/plugins/platforms/hwcomposer/logging.h @@ -0,0 +1,15 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-3.0-or-later +*/ +#ifndef KWIN_FB_LOGGING_H +#define KWIN_FB_LOGGING_H +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(KWIN_HWCOMPOSER) +#endif diff --git a/plugins/platforms/hwcomposer/screens_hwcomposer.cpp b/plugins/platforms/hwcomposer/screens_hwcomposer.cpp new file mode 100644 index 0000000..933b162 --- /dev/null +++ b/plugins/platforms/hwcomposer/screens_hwcomposer.cpp @@ -0,0 +1,23 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-3.0-or-later +*/ +#include "screens_hwcomposer.h" +#include "hwcomposer_backend.h" + +namespace KWin +{ + +HwcomposerScreens::HwcomposerScreens(HwcomposerBackend *backend, QObject *parent) + : OutputScreens(backend, parent) + , m_backend(backend) +{ + connect(m_backend, &HwcomposerBackend::screensQueried, this, &OutputScreens::updateCount); + connect(m_backend, &HwcomposerBackend::screensQueried, this, &OutputScreens::changed); +} + +} diff --git a/plugins/platforms/hwcomposer/screens_hwcomposer.h b/plugins/platforms/hwcomposer/screens_hwcomposer.h new file mode 100644 index 0000000..1f2ac2c --- /dev/null +++ b/plugins/platforms/hwcomposer/screens_hwcomposer.h @@ -0,0 +1,30 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-3.0-or-later +*/ +#ifndef KWIN_SCREENS_HWCOMPOSER_H +#define KWIN_SCREENS_HWCOMPOSER_H +#include "outputscreens.h" + +namespace KWin +{ +class HwcomposerBackend; + +class HwcomposerScreens : public OutputScreens +{ + Q_OBJECT +public: + HwcomposerScreens(HwcomposerBackend *backend, QObject *parent = nullptr); + virtual ~HwcomposerScreens() = default; + +private: + HwcomposerBackend *m_backend; +}; + +} + +#endif diff --git a/plugins/platforms/virtual/CMakeLists.txt b/plugins/platforms/virtual/CMakeLists.txt new file mode 100644 index 0000000..ae63d08 --- /dev/null +++ b/plugins/platforms/virtual/CMakeLists.txt @@ -0,0 +1,22 @@ +set(VIRTUAL_SOURCES + egl_gbm_backend.cpp + scene_qpainter_virtual_backend.cpp + screens_virtual.cpp + virtual_backend.cpp + virtual_output.cpp +) + +include_directories(${CMAKE_SOURCE_DIR}/platformsupport/scenes/opengl) +include(ECMQtDeclareLoggingCategory) +ecm_qt_declare_logging_category(VIRTUAL_SOURCES HEADER logging.h IDENTIFIER KWIN_VIRTUAL CATEGORY_NAME kwin_platform_virtual DEFAULT_SEVERITY Critical) + +add_library(KWinWaylandVirtualBackend MODULE ${VIRTUAL_SOURCES}) +set_target_properties(KWinWaylandVirtualBackend PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/org.kde.kwin.waylandbackends/") +target_link_libraries(KWinWaylandVirtualBackend kwin SceneQPainterBackend SceneOpenGLBackend) + +install( + TARGETS + KWinWaylandVirtualBackend + DESTINATION + ${PLUGIN_INSTALL_DIR}/org.kde.kwin.waylandbackends/ +) diff --git a/plugins/platforms/virtual/egl_gbm_backend.cpp b/plugins/platforms/virtual/egl_gbm_backend.cpp new file mode 100644 index 0000000..e0e1272 --- /dev/null +++ b/plugins/platforms/virtual/egl_gbm_backend.cpp @@ -0,0 +1,238 @@ +/* + 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 "egl_gbm_backend.h" +// kwin +#include "composite.h" +#include "virtual_backend.h" +#include "options.h" +#include "screens.h" +#include +// kwin libs +#include +#include +// Qt +#include + +#ifndef EGL_PLATFORM_SURFACELESS_MESA +#define EGL_PLATFORM_SURFACELESS_MESA 0x31DD +#endif + +namespace KWin +{ + +EglGbmBackend::EglGbmBackend(VirtualBackend *b) + : AbstractEglBackend() + , m_backend(b) +{ + // Egl is always direct rendering + setIsDirectRendering(true); +} + +EglGbmBackend::~EglGbmBackend() +{ + while (GLRenderTarget::isRenderTargetBound()) { + GLRenderTarget::popRenderTarget(); + } + delete m_fbo; + delete m_backBuffer; + cleanup(); +} + +bool EglGbmBackend::initializeEgl() +{ + initClientExtensions(); + EGLDisplay display = m_backend->sceneEglDisplay(); + + // Use eglGetPlatformDisplayEXT() to get the display pointer + // if the implementation supports it. + if (display == EGL_NO_DISPLAY) { + // first try surfaceless + if (hasClientExtension(QByteArrayLiteral("EGL_MESA_platform_surfaceless"))) { + display = eglGetPlatformDisplayEXT(EGL_PLATFORM_SURFACELESS_MESA, EGL_DEFAULT_DISPLAY, nullptr); + } else { + qCWarning(KWIN_VIRTUAL) << "Extension EGL_MESA_platform_surfaceless not available"; + } + } + + if (display == EGL_NO_DISPLAY) + return false; + setEglDisplay(display); + return initEglAPI(); +} + +void EglGbmBackend::init() +{ + if (!initializeEgl()) { + setFailed("Could not initialize egl"); + return; + } + if (!initRenderingContext()) { + setFailed("Could not initialize rendering context"); + return; + } + + initKWinGL(); + + m_backBuffer = new GLTexture(GL_RGB8, screens()->size().width(), screens()->size().height()); + m_fbo = new GLRenderTarget(*m_backBuffer); + if (!m_fbo->valid()) { + setFailed("Could not create framebuffer object"); + return; + } + GLRenderTarget::pushRenderTarget(m_fbo); + if (!m_fbo->isRenderTargetBound()) { + setFailed("Failed to bind framebuffer object"); + return; + } + if (checkGLError("Init")) { + setFailed("Error during init of EglGbmBackend"); + return; + } + + setSupportsBufferAge(false); + initWayland(); +} + +bool EglGbmBackend::initRenderingContext() +{ + initBufferConfigs(); + + if (!supportsSurfacelessContext()) { + qCWarning(KWIN_VIRTUAL) << "EGL_KHR_surfaceless_context extension is unavailable"; + return false; + } + + if (!createContext()) { + return false; + } + + return makeCurrent(); +} + +bool EglGbmBackend::initBufferConfigs() +{ + const EGLint config_attribs[] = { + EGL_SURFACE_TYPE, EGL_WINDOW_BIT, + EGL_RED_SIZE, 1, + EGL_GREEN_SIZE, 1, + EGL_BLUE_SIZE, 1, + EGL_ALPHA_SIZE, 0, + EGL_RENDERABLE_TYPE, isOpenGLES() ? EGL_OPENGL_ES2_BIT : EGL_OPENGL_BIT, + EGL_CONFIG_CAVEAT, EGL_NONE, + EGL_NONE, + }; + + EGLint count; + EGLConfig configs[1024]; + if (eglChooseConfig(eglDisplay(), config_attribs, configs, 1, &count) == EGL_FALSE) { + return false; + } + if (count != 1) { + return false; + } + setConfig(configs[0]); + + return true; +} + +void EglGbmBackend::present() +{ + Compositor::self()->aboutToSwapBuffers(); + + eglSwapBuffers(eglDisplay(), surface()); + setLastDamage(QRegion()); + + Compositor::self()->bufferSwapComplete(); +} + +void EglGbmBackend::screenGeometryChanged(const QSize &size) +{ + Q_UNUSED(size) + // TODO, create new buffer? +} + +SceneOpenGLTexturePrivate *EglGbmBackend::createBackendTexture(SceneOpenGLTexture *texture) +{ + return new EglGbmTexture(texture, this); +} + +QRegion EglGbmBackend::prepareRenderingFrame() +{ + if (!lastDamage().isEmpty()) { + present(); + } + startRenderTimer(); + if (!GLRenderTarget::isRenderTargetBound()) { + GLRenderTarget::pushRenderTarget(m_fbo); + } + return QRegion(0, 0, screens()->size().width(), screens()->size().height()); +} + +static void convertFromGLImage(QImage &img, int w, int h) +{ + // from QtOpenGL/qgl.cpp + // SPDX-FileCopyrightText: 2010 Nokia Corporation and /or its subsidiary(-ies) + // see https://github.com/qt/qtbase/blob/dev/src/opengl/qgl.cpp + if (QSysInfo::ByteOrder == QSysInfo::BigEndian) { + // OpenGL gives RGBA; Qt wants ARGB + uint *p = reinterpret_cast(img.bits()); + uint *end = p + w * h; + while (p < end) { + uint a = *p << 24; + *p = (*p >> 8) | a; + p++; + } + } else { + // OpenGL gives ABGR (i.e. RGBA backwards); Qt wants ARGB + for (int y = 0; y < h; y++) { + uint *q = reinterpret_cast(img.scanLine(y)); + for (int x = 0; x < w; ++x) { + const uint pixel = *q; + *q = ((pixel << 16) & 0xff0000) | ((pixel >> 16) & 0xff) + | (pixel & 0xff00ff00); + + q++; + } + } + + } + img = img.mirrored(); +} + +void EglGbmBackend::endRenderingFrame(const QRegion &renderedRegion, const QRegion &damagedRegion) +{ + Q_UNUSED(damagedRegion) + glFlush(); + if (m_backend->saveFrames()) { + QImage img = QImage(QSize(m_backBuffer->width(), m_backBuffer->height()), QImage::Format_ARGB32); + glReadnPixels(0, 0, m_backBuffer->width(), m_backBuffer->height(), GL_RGBA, GL_UNSIGNED_BYTE, img.sizeInBytes(), (GLvoid*)img.bits()); + convertFromGLImage(img, m_backBuffer->width(), m_backBuffer->height()); + img.save(QStringLiteral("%1/%2.png").arg(m_backend->saveFrames()).arg(QString::number(m_frameCounter++))); + } + GLRenderTarget::popRenderTarget(); + setLastDamage(renderedRegion); +} + +bool EglGbmBackend::usesOverlayWindow() const +{ + return false; +} + +/************************************************ + * EglTexture + ************************************************/ + +EglGbmTexture::EglGbmTexture(KWin::SceneOpenGLTexture *texture, EglGbmBackend *backend) + : AbstractEglTexture(texture, backend) +{ +} + +EglGbmTexture::~EglGbmTexture() = default; + +} // namespace diff --git a/plugins/platforms/virtual/egl_gbm_backend.h b/plugins/platforms/virtual/egl_gbm_backend.h new file mode 100644 index 0000000..350b8b0 --- /dev/null +++ b/plugins/platforms/virtual/egl_gbm_backend.h @@ -0,0 +1,63 @@ +/* + 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_EGL_GBM_BACKEND_H +#define KWIN_EGL_GBM_BACKEND_H +#include "abstract_egl_backend.h" + +namespace KWin +{ +class VirtualBackend; +class GLTexture; +class GLRenderTarget; + +/** + * @brief OpenGL Backend using Egl on a GBM surface. + */ +class EglGbmBackend : public AbstractEglBackend +{ +public: + EglGbmBackend(VirtualBackend *b); + ~EglGbmBackend() override; + void screenGeometryChanged(const QSize &size) override; + SceneOpenGLTexturePrivate *createBackendTexture(SceneOpenGLTexture *texture) override; + QRegion prepareRenderingFrame() override; + void endRenderingFrame(const QRegion &renderedRegion, const QRegion &damagedRegion) override; + bool usesOverlayWindow() const override; + void init() override; + +protected: + void present() override; + +private: + bool initializeEgl(); + bool initBufferConfigs(); + bool initRenderingContext(); + VirtualBackend *m_backend; + GLTexture *m_backBuffer = nullptr; + GLRenderTarget *m_fbo = nullptr; + int m_frameCounter = 0; + friend class EglGbmTexture; +}; + +/** + * @brief Texture using an EGLImageKHR. + */ +class EglGbmTexture : public AbstractEglTexture +{ +public: + ~EglGbmTexture() override; + +private: + friend class EglGbmBackend; + EglGbmTexture(SceneOpenGLTexture *texture, EglGbmBackend *backend); +}; + +} // namespace + +#endif diff --git a/plugins/platforms/virtual/scene_qpainter_virtual_backend.cpp b/plugins/platforms/virtual/scene_qpainter_virtual_backend.cpp new file mode 100644 index 0000000..4604ec0 --- /dev/null +++ b/plugins/platforms/virtual/scene_qpainter_virtual_backend.cpp @@ -0,0 +1,78 @@ +/* + 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 "scene_qpainter_virtual_backend.h" +#include "virtual_backend.h" +#include "cursor.h" +#include "screens.h" + +#include + +namespace KWin +{ +VirtualQPainterBackend::VirtualQPainterBackend(VirtualBackend *backend) + : QPainterBackend() + , m_backend(backend) +{ + connect(screens(), &Screens::changed, this, &VirtualQPainterBackend::createOutputs); + createOutputs(); +} + +VirtualQPainterBackend::~VirtualQPainterBackend() = default; + +QImage *VirtualQPainterBackend::buffer() +{ + return &m_backBuffers[0]; +} + +QImage *VirtualQPainterBackend::bufferForScreen(int screen) +{ + return &m_backBuffers[screen]; +} + +bool VirtualQPainterBackend::needsFullRepaint() const +{ + return true; +} + +void VirtualQPainterBackend::prepareRenderingFrame() +{ +} + +void VirtualQPainterBackend::createOutputs() +{ + m_backBuffers.clear(); + for (int i = 0; i < screens()->count(); ++i) { + QImage buffer(screens()->size(i) * screens()->scale(i), QImage::Format_RGB32); + buffer.fill(Qt::black); + m_backBuffers << buffer; + } +} + +void VirtualQPainterBackend::present(int mask, const QRegion &damage) +{ + Q_UNUSED(mask) + Q_UNUSED(damage) + if (m_backend->saveFrames()) { + for (int i=0; i < m_backBuffers.size() ; i++) { + m_backBuffers[i].save(QStringLiteral("%1/screen%2-%3.png").arg(m_backend->screenshotDirPath(), QString::number(i), QString::number(m_frameCounter++))); + } + } +} + +bool VirtualQPainterBackend::usesOverlayWindow() const +{ + return false; +} + +bool VirtualQPainterBackend::perScreenRendering() const +{ + return true; +} + +} diff --git a/plugins/platforms/virtual/scene_qpainter_virtual_backend.h b/plugins/platforms/virtual/scene_qpainter_virtual_backend.h new file mode 100644 index 0000000..d690f1a --- /dev/null +++ b/plugins/platforms/virtual/scene_qpainter_virtual_backend.h @@ -0,0 +1,47 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_SCENE_QPAINTER_VIRTUAL_BACKEND_H +#define KWIN_SCENE_QPAINTER_VIRTUAL_BACKEND_H + +#include + +#include +#include + +namespace KWin +{ + +class VirtualBackend; + +class VirtualQPainterBackend : public QObject, public QPainterBackend +{ + Q_OBJECT +public: + VirtualQPainterBackend(VirtualBackend *backend); + ~VirtualQPainterBackend() override; + + QImage *buffer() override; + QImage *bufferForScreen(int screenId) override; + bool needsFullRepaint() const override; + bool usesOverlayWindow() const override; + void prepareRenderingFrame() override; + void present(int mask, const QRegion &damage) override; + bool perScreenRendering() const override; + +private: + void createOutputs(); + + QVector m_backBuffers; + VirtualBackend *m_backend; + int m_frameCounter = 0; +}; + +} + +#endif diff --git a/plugins/platforms/virtual/screens_virtual.cpp b/plugins/platforms/virtual/screens_virtual.cpp new file mode 100644 index 0000000..6089ecd --- /dev/null +++ b/plugins/platforms/virtual/screens_virtual.cpp @@ -0,0 +1,42 @@ +/* + 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 "screens_virtual.h" +#include "virtual_backend.h" +#include "virtual_output.h" + +namespace KWin +{ + +VirtualScreens::VirtualScreens(VirtualBackend *backend, QObject *parent) + : OutputScreens(backend, parent) + , m_backend(backend) +{ +} + +VirtualScreens::~VirtualScreens() = default; + +void VirtualScreens::init() +{ + updateCount(); + KWin::Screens::init(); + + connect(m_backend, &VirtualBackend::virtualOutputsSet, this, + [this] (bool countChanged) { + if (countChanged) { + setCount(m_backend->outputs().size()); + } else { + emit changed(); + } + } + ); + + emit changed(); +} + +} diff --git a/plugins/platforms/virtual/screens_virtual.h b/plugins/platforms/virtual/screens_virtual.h new file mode 100644 index 0000000..3b2e3d7 --- /dev/null +++ b/plugins/platforms/virtual/screens_virtual.h @@ -0,0 +1,34 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_SCREENS_VIRTUAL_H +#define KWIN_SCREENS_VIRTUAL_H +#include "outputscreens.h" +#include + +namespace KWin +{ +class VirtualBackend; + +class VirtualScreens : public OutputScreens +{ + Q_OBJECT +public: + VirtualScreens(VirtualBackend *backend, QObject *parent = nullptr); + ~VirtualScreens() override; + void init() override; + +private: + void createOutputs(); + VirtualBackend *m_backend; +}; + +} + +#endif + diff --git a/plugins/platforms/virtual/virtual.json b/plugins/platforms/virtual/virtual.json new file mode 100644 index 0000000..8d8852a --- /dev/null +++ b/plugins/platforms/virtual/virtual.json @@ -0,0 +1,84 @@ +{ + "KPlugin": { + "Description": "Render to a virtual framebuffer.", + "Description[az]": "Virtual çərçivə tamponunun formalaşdırılması.", + "Description[ca@valencia]": "Renderitza en un «framebuffer» virtual.", + "Description[ca]": "Renderitza a un «framebuffer» virtual.", + "Description[da]": "Rendér til en virtuel framebuffer.", + "Description[de]": "In virtuellen Framebuffer rendern.", + "Description[el]": "Αποτύπωση σε εικονική ενδιάμεση μνήμη πλαισίων.", + "Description[en_GB]": "Render to a virtual framebuffer.", + "Description[es]": "Renderizar en un «framebuffer» virtual.", + "Description[et]": "Renderdamine virtuaalses kaadripuhvris.", + "Description[eu]": "Errendatu alegiazko framebuffer batera.", + "Description[fi]": "Hahmonna virtuaaliseen framebufferiin.", + "Description[fr]": "Rendre sur un « framebuffer » factice.", + "Description[gl]": "Renderizar nun búfer de fotogramas virtual.", + "Description[hu]": "Renderelés egy virtuális framebufferbe.", + "Description[id]": "Render untuk sebuah virtual framebuffer.", + "Description[it]": "Resa su un framebuffer virtuale.", + "Description[ko]": "가상 프레임버퍼에 렌더링합니다.", + "Description[lt]": "Atvaizduoti į virtualų vaizdų atnaujinimo buferį.", + "Description[nl]": "Naar een virtuele framebuffer renderen.", + "Description[nn]": "Teikn opp til virtuell biletbuffer.", + "Description[pl]": "Wyświetlaj w wirtualnym buforze klatek.", + "Description[pt]": "Desenhar num 'framebuffer' virtual.", + "Description[pt_BR]": "Renderizar no framebuffer virtual.", + "Description[ro]": "Randează pe framebuffer virtual.", + "Description[ru]": "Отрисовка в виртуальный фреймбуфер", + "Description[sk]": "RenderovaÅ¥ na virtuálny framebuffer.", + "Description[sl]": "IzriÅ¡i v navidezni medpomnilnik sličic.", + "Description[sr@ijekavian]": "Рендеровање у виртуелни кадробафер.", + "Description[sr@ijekavianlatin]": "Renderovanje u virtuelni kadrobafer.", + "Description[sr@latin]": "Renderovanje u virtuelni kadrobafer.", + "Description[sr]": "Рендеровање у виртуелни кадробафер.", + "Description[sv]": "Återge i en virtuell rambuffer.", + "Description[tr]": "Sanal bir çerçeve tamponuna gerçekle.", + "Description[uk]": "Обробляти до віртуального буфера кадрів.", + "Description[x-test]": "xxRender to a virtual framebuffer.xx", + "Description[zh_CN]": "渲染到虚拟帧缓冲。", + "Description[zh_TW]": "成像到虛擬影格緩衝區。", + "Id": "KWinWaylandVirtualBackend", + "Name": "virtual", + "Name[az]": "virtual", + "Name[ca@valencia]": "Virtual", + "Name[ca]": "Virtual", + "Name[cs]": "virtuální", + "Name[da]": "virtuel", + "Name[de]": "Virtuell", + "Name[el]": "εικονικό", + "Name[en_GB]": "virtual", + "Name[es]": "virtual", + "Name[et]": "virtual", + "Name[eu]": "alegiazkoa", + "Name[fi]": "virtual", + "Name[fr]": "virtual", + "Name[gl]": "virtual", + "Name[hu]": "virtuális", + "Name[ia]": "virtual", + "Name[id]": "virtual", + "Name[it]": "virtual", + "Name[ko]": "virtual", + "Name[lt]": "virtualus", + "Name[nl]": "virtueel", + "Name[nn]": "virtuell", + "Name[pl]": "wirtualne", + "Name[pt]": "virtual", + "Name[pt_BR]": "virtual", + "Name[ro]": "virtual", + "Name[ru]": "virtual", + "Name[sk]": "virtual", + "Name[sl]": "navidezno", + "Name[sr@ijekavian]": "Виртуелно", + "Name[sr@ijekavianlatin]": "Virtuelno", + "Name[sr@latin]": "Virtuelno", + "Name[sr]": "Виртуелно", + "Name[sv]": "virtuell", + "Name[tr]": "sanal", + "Name[uk]": "virtual", + "Name[x-test]": "xxvirtualxx", + "Name[zh_CN]": "virtual", + "Name[zh_TW]": "虛擬" + }, + "input": true +} diff --git a/plugins/platforms/virtual/virtual_backend.cpp b/plugins/platforms/virtual/virtual_backend.cpp new file mode 100644 index 0000000..8c44277 --- /dev/null +++ b/plugins/platforms/virtual/virtual_backend.cpp @@ -0,0 +1,133 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "virtual_backend.h" +#include "virtual_output.h" +#include "scene_qpainter_virtual_backend.h" +#include "screens_virtual.h" +#include "wayland_server.h" +#include "egl_gbm_backend.h" +// Qt +#include +// KWayland +#include +// system +#include +#include +#include + +namespace KWin +{ + +VirtualBackend::VirtualBackend(QObject *parent) + : Platform(parent) +{ + if (qEnvironmentVariableIsSet("KWIN_WAYLAND_VIRTUAL_SCREENSHOTS")) { + m_screenshotDir.reset(new QTemporaryDir); + if (!m_screenshotDir->isValid()) { + m_screenshotDir.reset(); + } + if (!m_screenshotDir.isNull()) { + qDebug() << "Screenshots saved to: " << m_screenshotDir->path(); + } + } + setSupportsPointerWarping(true); + setSupportsGammaControl(true); +} + +VirtualBackend::~VirtualBackend() +{ +} + +void VirtualBackend::init() +{ + /* + * Some tests currently expect one output present at start, + * others set them explicitly. + * + * TODO: rewrite all tests to explicitly set the outputs. + */ + if (!m_outputs.size()) { + VirtualOutput *dummyOutput = new VirtualOutput(this); + dummyOutput->init(QPoint(0, 0), initialWindowSize()); + m_outputs << dummyOutput ; + m_enabledOutputs << dummyOutput ; + } + + setSoftWareCursor(true); + setReady(true); + waylandServer()->seat()->setHasPointer(true); + waylandServer()->seat()->setHasKeyboard(true); + waylandServer()->seat()->setHasTouch(true); + + emit screensQueried(); +} + +QString VirtualBackend::screenshotDirPath() const +{ + if (m_screenshotDir.isNull()) { + return QString(); + } + return m_screenshotDir->path(); +} + +Screens *VirtualBackend::createScreens(QObject *parent) +{ + return new VirtualScreens(this, parent); +} + +QPainterBackend *VirtualBackend::createQPainterBackend() +{ + return new VirtualQPainterBackend(this); +} + +OpenGLBackend *VirtualBackend::createOpenGLBackend() +{ + return new EglGbmBackend(this); +} + +Outputs VirtualBackend::outputs() const +{ + return m_outputs; +} + +Outputs VirtualBackend::enabledOutputs() const +{ + return m_enabledOutputs; +} + +void VirtualBackend::setVirtualOutputs(int count, QVector geometries, QVector scales) +{ + Q_ASSERT(geometries.size() == 0 || geometries.size() == count); + Q_ASSERT(scales.size() == 0 || scales.size() == count); + + bool countChanged = m_outputs.size() != count; + qDeleteAll(m_outputs.begin(), m_outputs.end()); + m_outputs.resize(count); + m_enabledOutputs.resize(count); + + int sumWidth = 0; + for (int i = 0; i < count; i++) { + VirtualOutput *vo = new VirtualOutput(this); + if (geometries.size()) { + const QRect geo = geometries.at(i); + vo->init(geo.topLeft(), geo.size()); + } else { + vo->init(QPoint(sumWidth, 0), initialWindowSize()); + sumWidth += initialWindowSize().width(); + } + if (scales.size()) { + vo->setScale(scales.at(i)); + } + m_outputs[i] = m_enabledOutputs[i] = vo; + } + + emit virtualOutputsSet(countChanged); +} + +} diff --git a/plugins/platforms/virtual/virtual_backend.h b/plugins/platforms/virtual/virtual_backend.h new file mode 100644 index 0000000..73b5890 --- /dev/null +++ b/plugins/platforms/virtual/virtual_backend.h @@ -0,0 +1,68 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_VIRTUAL_BACKEND_H +#define KWIN_VIRTUAL_BACKEND_H +#include "platform.h" + +#include + +#include +#include + +class QTemporaryDir; + +namespace KWin +{ +class VirtualOutput; + +class KWIN_EXPORT VirtualBackend : public Platform +{ + Q_OBJECT + Q_INTERFACES(KWin::Platform) + Q_PLUGIN_METADATA(IID "org.kde.kwin.Platform" FILE "virtual.json") + +public: + VirtualBackend(QObject *parent = nullptr); + ~VirtualBackend() override; + void init() override; + + bool saveFrames() const { + return !m_screenshotDir.isNull(); + } + QString screenshotDirPath() const; + + Screens *createScreens(QObject *parent = nullptr) override; + QPainterBackend* createQPainterBackend() override; + OpenGLBackend *createOpenGLBackend() override; + + Q_INVOKABLE void setVirtualOutputs(int count, QVector geometries = QVector(), QVector scales = QVector()); + + Outputs outputs() const override; + Outputs enabledOutputs() const override; + + QVector supportedCompositors() const override { + if (selectedCompositor() != NoCompositing) { + return {selectedCompositor()}; + } + return QVector{OpenGLCompositing, QPainterCompositing}; + } + +Q_SIGNALS: + void virtualOutputsSet(bool countChanged); + +private: + QVector m_outputs; + QVector m_enabledOutputs; + + QScopedPointer m_screenshotDir; +}; + +} + +#endif diff --git a/plugins/platforms/virtual/virtual_output.cpp b/plugins/platforms/virtual/virtual_output.cpp new file mode 100644 index 0000000..bfcaabd --- /dev/null +++ b/plugins/platforms/virtual/virtual_output.cpp @@ -0,0 +1,44 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "virtual_output.h" + +namespace KWin +{ + +VirtualOutput::VirtualOutput(QObject *parent) + : AbstractWaylandOutput() +{ + Q_UNUSED(parent); + static int identifier = -1; + identifier++; + setName("Virtual-" + QString::number(identifier)); +} + +VirtualOutput::~VirtualOutput() +{ +} + +void VirtualOutput::init(const QPoint &logicalPosition, const QSize &pixelSize) +{ + KWaylandServer::OutputDeviceInterface::Mode mode; + mode.id = 0; + mode.size = pixelSize; + mode.flags = KWaylandServer::OutputDeviceInterface::ModeFlag::Current; + mode.refreshRate = 60000; // TODO + initInterfaces("model_TODO", "manufacturer_TODO", "UUID_TODO", pixelSize, { mode }); + setGeometry(QRect(logicalPosition, pixelSize)); +} + +void VirtualOutput::setGeometry(const QRect &geo) +{ + // TODO: set mode to have updated pixelSize + setGlobalPos(geo.topLeft()); +} + +} diff --git a/plugins/platforms/virtual/virtual_output.h b/plugins/platforms/virtual/virtual_output.h new file mode 100644 index 0000000..56e8ad7 --- /dev/null +++ b/plugins/platforms/virtual/virtual_output.h @@ -0,0 +1,51 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_VIRTUAL_OUTPUT_H +#define KWIN_VIRTUAL_OUTPUT_H + +#include "abstract_wayland_output.h" + +#include +#include + +namespace KWin +{ +class VirtualBackend; + +class VirtualOutput : public AbstractWaylandOutput +{ + Q_OBJECT + +public: + VirtualOutput(QObject *parent = nullptr); + ~VirtualOutput() override; + + void init(const QPoint &logicalPosition, const QSize &pixelSize); + + void setGeometry(const QRect &geo); + + int gammaRampSize() const override { + return m_gammaSize; + } + bool setGammaRamp(const GammaRamp &gamma) override { + Q_UNUSED(gamma); + return m_gammaResult; + } + +private: + Q_DISABLE_COPY(VirtualOutput); + friend class VirtualBackend; + + int m_gammaSize = 200; + bool m_gammaResult = true; +}; + +} + +#endif diff --git a/plugins/platforms/wayland/CMakeLists.txt b/plugins/platforms/wayland/CMakeLists.txt new file mode 100644 index 0000000..700b551 --- /dev/null +++ b/plugins/platforms/wayland/CMakeLists.txt @@ -0,0 +1,27 @@ +set(WAYLAND_BACKEND_SOURCES + logging.cpp + scene_qpainter_wayland_backend.cpp + wayland_backend.cpp + wayland_output.cpp + ../drm/gbm_dmabuf.cpp +) + +if (HAVE_WAYLAND_EGL) + set(WAYLAND_BACKEND_SOURCES egl_wayland_backend.cpp ${WAYLAND_BACKEND_SOURCES}) +endif() + +include_directories(${CMAKE_SOURCE_DIR}/platformsupport/scenes/opengl) +add_library(KWinWaylandWaylandBackend MODULE ${WAYLAND_BACKEND_SOURCES}) +set_target_properties(KWinWaylandWaylandBackend PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/org.kde.kwin.waylandbackends/") +target_link_libraries(KWinWaylandWaylandBackend kwin KF5::WaylandClient SceneQPainterBackend) + +if (HAVE_WAYLAND_EGL) + target_link_libraries(KWinWaylandWaylandBackend SceneOpenGLBackend Wayland::Egl gbm::gbm) +endif() + +install( + TARGETS + KWinWaylandWaylandBackend + DESTINATION + ${PLUGIN_INSTALL_DIR}/org.kde.kwin.waylandbackends/ +) diff --git a/plugins/platforms/wayland/egl_wayland_backend.cpp b/plugins/platforms/wayland/egl_wayland_backend.cpp new file mode 100644 index 0000000..0d0c9b7 --- /dev/null +++ b/plugins/platforms/wayland/egl_wayland_backend.cpp @@ -0,0 +1,457 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Roman Gilg + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#define WL_EGL_PLATFORM 1 + +#include "egl_wayland_backend.h" + +#include "wayland_backend.h" +#include "wayland_output.h" + +#include "composite.h" +#include "logging.h" +#include "options.h" + +#include "wayland_server.h" +#include "screens.h" + +#include +#include + +// kwin libs +#include + +// KDE +#include +#include +#include + +// Qt +#include +#include + +namespace KWin +{ +namespace Wayland +{ + +EglWaylandOutput::EglWaylandOutput(WaylandOutput *output, QObject *parent) + : QObject(parent) + , m_waylandOutput(output) +{ +} + +bool EglWaylandOutput::init(EglWaylandBackend *backend) +{ + auto surface = m_waylandOutput->surface(); + const QSize &size = m_waylandOutput->geometry().size(); + auto overlay = wl_egl_window_create(*surface, size.width(), size.height()); + if (!overlay) { + qCCritical(KWIN_WAYLAND_BACKEND) << "Creating Wayland Egl window failed"; + return false; + } + m_overlay = overlay; + + EGLSurface eglSurface = EGL_NO_SURFACE; + if (backend->havePlatformBase()) { + eglSurface = eglCreatePlatformWindowSurfaceEXT(backend->eglDisplay(), backend->config(), (void *) overlay, nullptr); + } else { + eglSurface = eglCreateWindowSurface(backend->eglDisplay(), backend->config(), overlay, nullptr); + } + if (eglSurface == EGL_NO_SURFACE) { + qCCritical(KWIN_WAYLAND_BACKEND) << "Create Window Surface failed"; + return false; + } + m_eglSurface = eglSurface; + + connect(m_waylandOutput, &WaylandOutput::sizeChanged, this, &EglWaylandOutput::updateSize); + connect(m_waylandOutput, &WaylandOutput::modeChanged, this, &EglWaylandOutput::updateMode); + + return true; +} + +void EglWaylandOutput::updateSize(const QSize &size) +{ + wl_egl_window_resize(m_overlay, size.width(), size.height(), 0, 0); +} + +void EglWaylandOutput::updateMode() +{ + updateSize(m_waylandOutput->geometry().size()); +} + +EglWaylandBackend::EglWaylandBackend(WaylandBackend *b) + : AbstractEglBackend() + , m_backend(b) +{ + if (!m_backend) { + setFailed("Wayland Backend has not been created"); + return; + } + qCDebug(KWIN_WAYLAND_BACKEND) << "Connected to Wayland display?" << (m_backend->display() ? "yes" : "no" ); + if (!m_backend->display()) { + setFailed("Could not connect to Wayland compositor"); + return; + } + + // Egl is always direct rendering + setIsDirectRendering(true); + + connect(m_backend, &WaylandBackend::outputAdded, this, &EglWaylandBackend::createEglWaylandOutput); + connect(m_backend, &WaylandBackend::outputRemoved, this, + [this] (WaylandOutput *output) { + auto it = std::find_if(m_outputs.begin(), m_outputs.end(), + [output] (const EglWaylandOutput *o) { + return o->m_waylandOutput == output; + } + ); + if (it == m_outputs.end()) { + return; + } + cleanupOutput(*it); + m_outputs.erase(it); + } + ); +} + +EglWaylandBackend::~EglWaylandBackend() +{ + cleanup(); +} + +void EglWaylandBackend::cleanupSurfaces() +{ + for (auto o : m_outputs) { + cleanupOutput(o); + } + m_outputs.clear(); +} + +bool EglWaylandBackend::createEglWaylandOutput(WaylandOutput *waylandOutput) +{ + auto *output = new EglWaylandOutput(waylandOutput, this); + if (!output->init(this)) { + return false; + } + m_outputs << output; + return true; +} + +void EglWaylandBackend::cleanupOutput(EglWaylandOutput *output) +{ + wl_egl_window_destroy(output->m_overlay); +} + +bool EglWaylandBackend::initializeEgl() +{ + initClientExtensions(); + EGLDisplay display = m_backend->sceneEglDisplay(); + + // Use eglGetPlatformDisplayEXT() to get the display pointer + // if the implementation supports it. + if (display == EGL_NO_DISPLAY) { + m_havePlatformBase = hasClientExtension(QByteArrayLiteral("EGL_EXT_platform_base")); + if (m_havePlatformBase) { + // Make sure that the wayland platform is supported + if (!hasClientExtension(QByteArrayLiteral("EGL_EXT_platform_wayland"))) + return false; + + display = eglGetPlatformDisplayEXT(EGL_PLATFORM_WAYLAND_EXT, m_backend->display(), nullptr); + } else { + display = eglGetDisplay(m_backend->display()); + } + } + + if (display == EGL_NO_DISPLAY) + return false; + setEglDisplay(display); + return initEglAPI(); +} + +void EglWaylandBackend::init() +{ + if (!initializeEgl()) { + setFailed("Could not initialize egl"); + return; + } + if (!initRenderingContext()) { + setFailed("Could not initialize rendering context"); + return; + } + + initKWinGL(); + initBufferAge(); + initWayland(); +} + +bool EglWaylandBackend::initRenderingContext() +{ + initBufferConfigs(); + + if (!createContext()) { + return false; + } + + auto waylandOutputs = m_backend->waylandOutputs(); + + // we only allow to start with at least one output + if (waylandOutputs.isEmpty()) { + return false; + } + + for (auto *out : waylandOutputs) { + if (!createEglWaylandOutput(out)) { + return false; + } + } + + if (m_outputs.isEmpty()) { + qCCritical(KWIN_WAYLAND_BACKEND) << "Create Window Surfaces failed"; + return false; + } + + auto *firstOutput = m_outputs.first(); + // set our first surface as the one for the abstract backend, just to make it happy + setSurface(firstOutput->m_eglSurface); + return makeContextCurrent(firstOutput); +} + +bool EglWaylandBackend::makeContextCurrent(EglWaylandOutput *output) +{ + const EGLSurface eglSurface = output->m_eglSurface; + if (eglSurface == EGL_NO_SURFACE) { + return false; + } + if (eglMakeCurrent(eglDisplay(), eglSurface, eglSurface, context()) == EGL_FALSE) { + qCCritical(KWIN_WAYLAND_BACKEND) << "Make Context Current failed"; + return false; + } + + EGLint error = eglGetError(); + if (error != EGL_SUCCESS) { + qCWarning(KWIN_WAYLAND_BACKEND) << "Error occurred while creating context " << error; + return false; + } + + const QRect &v = output->m_waylandOutput->geometry(); + + //The output is in scaled coordinates + const qreal scale = 1; + + const QSize overall = screens()->size(); + glViewport(-v.x() * scale, (v.height() - overall.height() + v.y()) * scale, + overall.width() * scale, overall.height() * scale); + return true; +} + +bool EglWaylandBackend::initBufferConfigs() +{ + const EGLint config_attribs[] = { + EGL_SURFACE_TYPE, EGL_WINDOW_BIT, + EGL_RED_SIZE, 1, + EGL_GREEN_SIZE, 1, + EGL_BLUE_SIZE, 1, + EGL_ALPHA_SIZE, 0, + EGL_RENDERABLE_TYPE, isOpenGLES() ? EGL_OPENGL_ES2_BIT : EGL_OPENGL_BIT, + EGL_CONFIG_CAVEAT, EGL_NONE, + EGL_NONE, + }; + + EGLint count; + EGLConfig configs[1024]; + if (eglChooseConfig(eglDisplay(), config_attribs, configs, 1, &count) == EGL_FALSE) { + qCCritical(KWIN_WAYLAND_BACKEND) << "choose config failed"; + return false; + } + if (count != 1) { + qCCritical(KWIN_WAYLAND_BACKEND) << "choose config did not return a config" << count; + return false; + } + setConfig(configs[0]); + + return true; +} + +void EglWaylandBackend::present() +{ + for (auto *output: qAsConst(m_outputs)) { + makeContextCurrent(output); + presentOnSurface(output, output->m_waylandOutput->geometry()); + } +} + +static QVector regionToRects(const QRegion ®ion, AbstractWaylandOutput *output) +{ + const int height = output->modeSize().height(); + const QMatrix4x4 matrix = WaylandOutput::logicalToNativeMatrix(output->geometry(), + output->scale(), + output->transform()); + + QVector rects; + rects.reserve(region.rectCount() * 4); + for (const QRect &_rect : region) { + const QRect rect = matrix.mapRect(_rect); + + rects << rect.left(); + rects << height - (rect.y() + rect.height()); + rects << rect.width(); + rects << rect.height(); + } + return rects; +} + +void EglWaylandBackend::aboutToStartPainting(const QRegion &damagedRegion) +{ + EglWaylandOutput* output = m_outputs.at(0); + if (output->m_bufferAge > 0 && !damagedRegion.isEmpty() && supportsPartialUpdate()) { + const QRegion region = damagedRegion & output->m_waylandOutput->geometry(); + + QVector rects = regionToRects(region, output->m_waylandOutput); + const bool correct = eglSetDamageRegionKHR(eglDisplay(), output->m_eglSurface, + rects.data(), rects.count()/4); + if (!correct) { + qCWarning(KWIN_WAYLAND_BACKEND) << "failed eglSetDamageRegionKHR" << eglGetError(); + } + } +} + +void EglWaylandBackend::presentOnSurface(EglWaylandOutput *output, const QRegion &damage) +{ + output->m_waylandOutput->surface()->setupFrameCallback(); + if (!m_swapping) { + m_swapping = true; + Compositor::self()->aboutToSwapBuffers(); + } + + Q_EMIT output->m_waylandOutput->outputChange(damage); + + if (supportsSwapBuffersWithDamage() && !output->m_damageHistory.isEmpty()) { + QVector rects = regionToRects(output->m_damageHistory.constFirst(), output->m_waylandOutput); + eglSwapBuffersWithDamageEXT(eglDisplay(), output->m_eglSurface, + rects.data(), rects.count()/4); + } else { + eglSwapBuffers(eglDisplay(), output->m_eglSurface); + } + + if (supportsBufferAge()) { + eglQuerySurface(eglDisplay(), output->m_eglSurface, EGL_BUFFER_AGE_EXT, &output->m_bufferAge); + } + +} + +void EglWaylandBackend::screenGeometryChanged(const QSize &size) +{ + Q_UNUSED(size) + // no backend specific code needed + // TODO: base implementation in OpenGLBackend + + // The back buffer contents are now undefined + for (auto *output : qAsConst(m_outputs)) { + output->m_bufferAge = 0; + } +} + +SceneOpenGLTexturePrivate *EglWaylandBackend::createBackendTexture(SceneOpenGLTexture *texture) +{ + return new EglWaylandTexture(texture, this); +} + +QRegion EglWaylandBackend::prepareRenderingFrame() +{ + eglWaitNative(EGL_CORE_NATIVE_ENGINE); + startRenderTimer(); + m_swapping = false; + return QRegion(); +} + +QRegion EglWaylandBackend::prepareRenderingForScreen(int screenId) +{ + auto *output = m_outputs.at(screenId); + makeContextCurrent(output); + if (supportsBufferAge()) { + QRegion region; + + // Note: An age of zero means the buffer contents are undefined + if (output->m_bufferAge > 0 && output->m_bufferAge <= output->m_damageHistory.count()) { + for (int i = 0; i < output->m_bufferAge - 1; i++) + region |= output->m_damageHistory[i]; + } else { + region = output->m_waylandOutput->geometry(); + } + + return region; + } + return QRegion(); +} + +void EglWaylandBackend::endRenderingFrame(const QRegion &renderedRegion, const QRegion &damagedRegion) +{ + Q_UNUSED(renderedRegion) + Q_UNUSED(damagedRegion) +} + +void EglWaylandBackend::endRenderingFrameForScreen(int screenId, const QRegion &renderedRegion, const QRegion &damagedRegion) +{ + EglWaylandOutput *output = m_outputs[screenId]; + QRegion damage = damagedRegion.intersected(output->m_waylandOutput->geometry()); + if (damage.isEmpty() && screenId == 0) { + + // If the damaged region of a window is fully occluded, the only + // rendering done, if any, will have been to repair a reused back + // buffer, making it identical to the front buffer. + // + // In this case we won't post the back buffer. Instead we'll just + // set the buffer age to 1, so the repaired regions won't be + // rendered again in the next frame. + if (!renderedRegion.intersected(output->m_waylandOutput->geometry()).isEmpty()) { + glFlush(); + } + + for (auto *o : qAsConst(m_outputs)) { + o->m_bufferAge = 1; + } + return; + } + presentOnSurface(output, damage); + + // Save the damaged region to history + // Note: damage history is only collected for the first screen. See EglGbmBackend + // for mor information regarding this limitation. + if (supportsBufferAge() && screenId == 0) { + if (output->m_damageHistory.count() > 10) { + output->m_damageHistory.removeLast(); + } + + output->m_damageHistory.prepend(damage); + } +} + +bool EglWaylandBackend::usesOverlayWindow() const +{ + return false; +} + +bool EglWaylandBackend::perScreenRendering() const +{ + return true; +} + +/************************************************ + * EglTexture + ************************************************/ + +EglWaylandTexture::EglWaylandTexture(KWin::SceneOpenGLTexture *texture, KWin::Wayland::EglWaylandBackend *backend) + : AbstractEglTexture(texture, backend) +{ +} + +EglWaylandTexture::~EglWaylandTexture() = default; + +} +} diff --git a/plugins/platforms/wayland/egl_wayland_backend.h b/plugins/platforms/wayland/egl_wayland_backend.h new file mode 100644 index 0000000..9a82915 --- /dev/null +++ b/plugins/platforms/wayland/egl_wayland_backend.h @@ -0,0 +1,124 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + SPDX-FileCopyrightText: 2019 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_EGL_WAYLAND_BACKEND_H +#define KWIN_EGL_WAYLAND_BACKEND_H +#include "abstract_egl_backend.h" +// wayland +#include + +class QTemporaryFile; +struct wl_buffer; +struct wl_shm; + +namespace KWin +{ + +namespace Wayland +{ +class WaylandBackend; +class WaylandOutput; +class EglWaylandBackend; + +class EglWaylandOutput : public QObject +{ + Q_OBJECT +public: + EglWaylandOutput(WaylandOutput *output, QObject *parent = nullptr); + ~EglWaylandOutput() override = default; + + bool init(EglWaylandBackend *backend); + void updateSize(const QSize &size); + void updateMode(); + +private: + WaylandOutput *m_waylandOutput; + wl_egl_window *m_overlay = nullptr; + EGLSurface m_eglSurface = EGL_NO_SURFACE; + int m_bufferAge = 0; + /** + * @brief The damage history for the past 10 frames. + */ + QVector m_damageHistory; + + friend class EglWaylandBackend; +}; + +/** + * @brief OpenGL Backend using Egl on a Wayland surface. + * + * This Backend is the basis for a session compositor running on top of a Wayland system compositor. + * It creates a Surface as large as the screen and maps it as a fullscreen shell surface on the + * system compositor. The OpenGL context is created on the Wayland surface, so for rendering X11 is + * not involved. + * + * Also in repainting the backend is currently still rather limited. Only supported mode is fullscreen + * repaints, which is obviously not optimal. Best solution is probably to go for buffer_age extension + * and make it the only available solution next to fullscreen repaints. + */ +class EglWaylandBackend : public AbstractEglBackend +{ + Q_OBJECT +public: + EglWaylandBackend(WaylandBackend *b); + ~EglWaylandBackend() override; + void screenGeometryChanged(const QSize &size) override; + SceneOpenGLTexturePrivate *createBackendTexture(SceneOpenGLTexture *texture) override; + QRegion prepareRenderingFrame() override; + QRegion prepareRenderingForScreen(int screenId) override; + void endRenderingFrame(const QRegion &renderedRegion, const QRegion &damagedRegion) override; + void endRenderingFrameForScreen(int screenId, const QRegion &damage, const QRegion &damagedRegion) override; + bool usesOverlayWindow() const override; + bool perScreenRendering() const override; + void init() override; + + bool havePlatformBase() const { + return m_havePlatformBase; + } + + void aboutToStartPainting(const QRegion &damage) override; + +private: + bool initializeEgl(); + bool initBufferConfigs(); + bool initRenderingContext(); + + bool createEglWaylandOutput(WaylandOutput *output); + + void cleanupSurfaces() override; + void cleanupOutput(EglWaylandOutput *output); + + bool makeContextCurrent(EglWaylandOutput *output); + void present() override; + void presentOnSurface(EglWaylandOutput *output, const QRegion &damagedRegion); + + WaylandBackend *m_backend; + QVector m_outputs; + bool m_havePlatformBase; + bool m_swapping = false; + friend class EglWaylandTexture; +}; + +/** + * @brief Texture using an EGLImageKHR. + */ +class EglWaylandTexture : public AbstractEglTexture +{ +public: + ~EglWaylandTexture() override; + +private: + friend class EglWaylandBackend; + EglWaylandTexture(SceneOpenGLTexture *texture, EglWaylandBackend *backend); +}; + +} +} + +#endif diff --git a/plugins/platforms/wayland/logging.cpp b/plugins/platforms/wayland/logging.cpp new file mode 100644 index 0000000..a5c80a1 --- /dev/null +++ b/plugins/platforms/wayland/logging.cpp @@ -0,0 +1,10 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "logging.h" +Q_LOGGING_CATEGORY(KWIN_WAYLAND_BACKEND, "kwin_wayland_backend", QtCriticalMsg) diff --git a/plugins/platforms/wayland/logging.h b/plugins/platforms/wayland/logging.h new file mode 100644 index 0000000..0911d66 --- /dev/null +++ b/plugins/platforms/wayland/logging.h @@ -0,0 +1,15 @@ +/* + 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_LOGGING_H +#define KWIN_WAYLAND_LOGGING_H +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(KWIN_WAYLAND_BACKEND) +#endif diff --git a/plugins/platforms/wayland/scene_qpainter_wayland_backend.cpp b/plugins/platforms/wayland/scene_qpainter_wayland_backend.cpp new file mode 100644 index 0000000..38b4486 --- /dev/null +++ b/plugins/platforms/wayland/scene_qpainter_wayland_backend.cpp @@ -0,0 +1,199 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Roman Gilg + SPDX-FileCopyrightText: 2013, 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "scene_qpainter_wayland_backend.h" +#include "wayland_backend.h" +#include "wayland_output.h" + +#include "composite.h" +#include "logging.h" + +#include +#include +#include + +namespace KWin +{ +namespace Wayland +{ + +WaylandQPainterOutput::WaylandQPainterOutput(WaylandOutput *output, QObject *parent) + : QObject(parent) + , m_waylandOutput(output) +{ +} + +WaylandQPainterOutput::~WaylandQPainterOutput() +{ + if (m_buffer) { + m_buffer.toStrongRef()->setUsed(false); + } +} + +bool WaylandQPainterOutput::init(KWayland::Client::ShmPool *pool) +{ + m_pool = pool; + m_backBuffer = QImage(QSize(), QImage::Format_RGB32); + + connect(pool, &KWayland::Client::ShmPool::poolResized, this, &WaylandQPainterOutput::remapBuffer); + connect(m_waylandOutput, &WaylandOutput::sizeChanged, this, &WaylandQPainterOutput::updateSize); + + return true; +} + +void WaylandQPainterOutput::remapBuffer() +{ + if (!m_buffer) { + return; + } + auto b = m_buffer.toStrongRef(); + if (!b->isUsed()){ + return; + } + const QSize size = m_backBuffer.size(); + m_backBuffer = QImage(b->address(), size.width(), size.height(), QImage::Format_RGB32); + qCDebug(KWIN_WAYLAND_BACKEND) << "Remapped back buffer of surface" << m_waylandOutput->surface(); +} + +void WaylandQPainterOutput::updateSize(const QSize &size) +{ + Q_UNUSED(size) + if (!m_buffer) { + return; + } + m_buffer.toStrongRef()->setUsed(false); + m_buffer.clear(); +} + +void WaylandQPainterOutput::present(const QRegion &damage) +{ + auto s = m_waylandOutput->surface(); + s->attachBuffer(m_buffer); + s->damage(damage); + s->commit(); +} + +void WaylandQPainterOutput::prepareRenderingFrame() +{ + if (m_buffer) { + auto b = m_buffer.toStrongRef(); + if (b->isReleased()) { + // we can re-use this buffer + b->setReleased(false); + return; + } else { + // buffer is still in use, get a new one + b->setUsed(false); + } + } + m_buffer.clear(); + + const QSize size(m_waylandOutput->geometry().size()); + + m_buffer = m_pool->getBuffer(size, size.width() * 4); + if (!m_buffer) { + qCDebug(KWIN_WAYLAND_BACKEND) << "Did not get a new Buffer from Shm Pool"; + m_backBuffer = QImage(); + return; + } + + auto b = m_buffer.toStrongRef(); + b->setUsed(true); + + m_backBuffer = QImage(b->address(), size.width(), size.height(), QImage::Format_RGB32); + m_backBuffer.fill(Qt::transparent); +// qCDebug(KWIN_WAYLAND_BACKEND) << "Created a new back buffer for output surface" << m_waylandOutput->surface(); +} + +WaylandQPainterBackend::WaylandQPainterBackend(Wayland::WaylandBackend *b) + : QPainterBackend() + , m_backend(b) + , m_needsFullRepaint(true) +{ + + const auto waylandOutputs = m_backend->waylandOutputs(); + for (auto *output: waylandOutputs) { + createOutput(output); + } + connect(m_backend, &WaylandBackend::outputAdded, this, &WaylandQPainterBackend::createOutput); + connect(m_backend, &WaylandBackend::outputRemoved, this, + [this] (WaylandOutput *waylandOutput) { + auto it = std::find_if(m_outputs.begin(), m_outputs.end(), + [waylandOutput] (WaylandQPainterOutput *output) { + return output->m_waylandOutput == waylandOutput; + } + ); + if (it == m_outputs.end()) { + return; + } + delete *it; + m_outputs.erase(it); + } + ); +} + +WaylandQPainterBackend::~WaylandQPainterBackend() +{ +} + +bool WaylandQPainterBackend::usesOverlayWindow() const +{ + return false; +} + +bool WaylandQPainterBackend::perScreenRendering() const +{ + return true; +} + +void WaylandQPainterBackend::createOutput(WaylandOutput *waylandOutput) +{ + auto *output = new WaylandQPainterOutput(waylandOutput, this); + output->init(m_backend->shmPool()); + m_outputs << output; +} + +void WaylandQPainterBackend::present(int mask, const QRegion &damage) +{ + Q_UNUSED(mask) + + Compositor::self()->aboutToSwapBuffers(); + m_needsFullRepaint = false; + + for (auto *output : m_outputs) { + output->present(damage); + } +} + +QImage *WaylandQPainterBackend::buffer() +{ + return bufferForScreen(0); +} + +QImage *WaylandQPainterBackend::bufferForScreen(int screenId) +{ + auto *output = m_outputs[screenId]; + return &output->m_backBuffer; +} + +void WaylandQPainterBackend::prepareRenderingFrame() +{ + for (auto *output : m_outputs) { + output->prepareRenderingFrame(); + } + m_needsFullRepaint = true; +} + +bool WaylandQPainterBackend::needsFullRepaint() const +{ + return m_needsFullRepaint; +} + +} +} diff --git a/plugins/platforms/wayland/scene_qpainter_wayland_backend.h b/plugins/platforms/wayland/scene_qpainter_wayland_backend.h new file mode 100644 index 0000000..d271b55 --- /dev/null +++ b/plugins/platforms/wayland/scene_qpainter_wayland_backend.h @@ -0,0 +1,91 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Roman Gilg + SPDX-FileCopyrightText: 2013, 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_SCENE_QPAINTER_WAYLAND_BACKEND_H +#define KWIN_SCENE_QPAINTER_WAYLAND_BACKEND_H + +#include + +#include +#include +#include + +namespace KWayland +{ +namespace Client +{ +class ShmPool; +class Buffer; +} +} + +namespace KWin +{ +namespace Wayland +{ +class WaylandBackend; +class WaylandOutput; +class WaylandQPainterBackend; + +class WaylandQPainterOutput : public QObject +{ + Q_OBJECT +public: + WaylandQPainterOutput(WaylandOutput *output, QObject *parent = nullptr); + ~WaylandQPainterOutput() override; + + bool init(KWayland::Client::ShmPool *pool); + void updateSize(const QSize &size); + void remapBuffer(); + + void prepareRenderingFrame(); + void present(const QRegion &damage); + +private: + WaylandOutput *m_waylandOutput; + KWayland::Client::ShmPool *m_pool; + + QWeakPointer m_buffer; + QImage m_backBuffer; + + friend class WaylandQPainterBackend; +}; + +class WaylandQPainterBackend : public QObject, public QPainterBackend +{ + Q_OBJECT +public: + explicit WaylandQPainterBackend(WaylandBackend *b); + ~WaylandQPainterBackend() override; + + bool usesOverlayWindow() const override; + + QImage *buffer() override; + QImage *bufferForScreen(int screenId) override; + + void present(int mask, const QRegion& damage) override; + void prepareRenderingFrame() override; + + bool needsFullRepaint() const override; + bool perScreenRendering() const override; + +private: + void createOutput(WaylandOutput *waylandOutput); + void frameRendered(); + + WaylandBackend *m_backend; + bool m_needsFullRepaint; + + QVector m_outputs; +}; + +} +} + +#endif diff --git a/plugins/platforms/wayland/wayland.json b/plugins/platforms/wayland/wayland.json new file mode 100644 index 0000000..50f20c7 --- /dev/null +++ b/plugins/platforms/wayland/wayland.json @@ -0,0 +1,83 @@ +{ + "KPlugin": { + "Description": "Render to a nested window on running Wayland compositor.", + "Description[az]": "Wayland birləşdiricisində iç-içə keçmiş pəncərələri formalaşdırmaq.", + "Description[ca@valencia]": "Renderitza en una finestra imbricada en un compositor Wayland en execució.", + "Description[ca]": "Renderitza a una finestra imbricada en un compositor Wayland en execució.", + "Description[da]": "Rendér til et indlejret vindue pÃ¥ kørende Wayland-compositor.", + "Description[de]": "In ein eingebettetes Fenster auf dem laufenden Wayland-Kompositor rendern.", + "Description[el]": "Αποτύπωση σε εμφωλευμένο παράθυρο κατά την εκτέλεση του συνθέτη Wayland.", + "Description[en_GB]": "Render to a nested window on running Wayland compositor.", + "Description[es]": "Renderizar en una ventana anidada en el compositor Wayland en ejecución.", + "Description[et]": "Renderdamine töötava Waylandi komposiitori pesastatud aknas.", + "Description[eu]": "Errendatu Wayland konposatzailean habiaratutako leiho batera.", + "Description[fi]": "Hahmonna sisäkkäiseen ikkunaan, jota hallitsee Wayland-koostin.", + "Description[fr]": "Rendre sur une fenêtre imbriquée sur un compositeur Wayland en cours de fonctionnement.", + "Description[gl]": "Renderizar unha xanela aniñada no compositor de Wayland en execución.", + "Description[hu]": "Renderelés egy Wayland kompozitoron futó beágyazott ablakba.", + "Description[id]": "Render untuk sebuah window tersarang pada Wayland compositor yang berjalan.", + "Description[it]": "Resa in una finestra nidificata su compositore Wayland in esecuzione.", + "Description[ko]": "Wayland 컴포지터에서 실행 중인 창에 렌더링합니다.", + "Description[lt]": "Atvaizduoti į įdėtinį langą, veikiančiame Wayland kompozitoriuje.", + "Description[nl]": "Render naar een genest venster in een werkende Wayland-compositor.", + "Description[nn]": "Teikn opp til innebygd vindauge pÃ¥ køyrande Wayland-samansetjar.", + "Description[pl]": "Wyświetlaj w zagnieżdżonym oknie w kompozytorze Wayland.", + "Description[pt]": "Desenhar numa janela encadeada no compositor de Wayland em execução.", + "Description[pt_BR]": "Renderizar uma janela encadeada no compositor Wayland em execução.", + "Description[ru]": "Отрисовка во вложенное окно компоновщика Wayland.", + "Description[sk]": "RenderovaÅ¥ na vnorené okno na bežiaci kompozítor Wayland.", + "Description[sl]": "IzriÅ¡i v gnezdeno okno na upravljalniku skladnje Wayland.", + "Description[sr@ijekavian]": "Рендеровање у угнежђени прозор на вејланд слагачу.", + "Description[sr@ijekavianlatin]": "Renderovanje u ugnežđeni prozor na Wayland slagaču.", + "Description[sr@latin]": "Renderovanje u ugnežđeni prozor na Wayland slagaču.", + "Description[sr]": "Рендеровање у угнежђени прозор на вејланд слагачу.", + "Description[sv]": "Återge till ett nästlat fönster pÃ¥ Wayland-sammansättare som kör.", + "Description[tr]": "Wayland birleştirici çalıştıran iç içe geçmiş bir pencerede gerçekle.", + "Description[uk]": "Обробляти у вкладене вікно запущеного засобу композиції Wayland.", + "Description[x-test]": "xxRender to a nested window on running Wayland compositor.xx", + "Description[zh_CN]": "渲染到 Wayland 混成器上的嵌套窗口中", + "Description[zh_TW]": "成像到執行中的 Wayland 的巢狀視窗。", + "Id": "KWinWaylandWaylandBackend", + "Name": "wayland", + "Name[az]": "wayland", + "Name[ca@valencia]": "Wayland", + "Name[ca]": "Wayland", + "Name[cs]": "wayland", + "Name[da]": "wayland", + "Name[de]": "Wayland", + "Name[el]": "wayland", + "Name[en_GB]": "wayland", + "Name[es]": "wayland", + "Name[et]": "wayland", + "Name[eu]": "wayland", + "Name[fi]": "wayland", + "Name[fr]": "wayland", + "Name[gl]": "wayland", + "Name[hu]": "wayland", + "Name[ia]": "wayland", + "Name[id]": "wayland", + "Name[it]": "wayland", + "Name[ko]": "wayland", + "Name[lt]": "wayland", + "Name[nl]": "wayland", + "Name[nn]": "wayland", + "Name[pl]": "wayland", + "Name[pt]": "Wayland", + "Name[pt_BR]": "wayland", + "Name[ro]": "wayland", + "Name[ru]": "wayland", + "Name[sk]": "wayland", + "Name[sl]": "wayland", + "Name[sr@ijekavian]": "Вејланд", + "Name[sr@ijekavianlatin]": "Wayland", + "Name[sr@latin]": "Wayland", + "Name[sr]": "Вејланд", + "Name[sv]": "Wayland", + "Name[tr]": "wayland", + "Name[uk]": "wayland", + "Name[x-test]": "xxwaylandxx", + "Name[zh_CN]": "wayland", + "Name[zh_TW]": "wayland" + }, + "input": true +} diff --git a/plugins/platforms/wayland/wayland_backend.cpp b/plugins/platforms/wayland/wayland_backend.cpp new file mode 100644 index 0000000..f79d9cd --- /dev/null +++ b/plugins/platforms/wayland/wayland_backend.cpp @@ -0,0 +1,843 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Roman Gilg + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "wayland_backend.h" + +#if HAVE_WAYLAND_EGL +#include "egl_wayland_backend.h" +#endif +#include "logging.h" +#include "scene_qpainter_wayland_backend.h" +#include "wayland_output.h" + +#include "composite.h" +#include "cursor.h" +#include "input.h" +#include "main.h" +#include "outputscreens.h" +#include "pointer_input.h" +#include "screens.h" +#include "wayland_server.h" +#include "../drm/gbm_dmabuf.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include +#include +#include + +namespace KWin +{ +namespace Wayland +{ + +using namespace KWayland::Client; + +WaylandCursor::WaylandCursor(WaylandBackend *backend) + : QObject(backend) + , m_backend(backend) +{ + resetSurface(); +} + +void WaylandCursor::resetSurface() +{ + delete m_surface; + m_surface = backend()->compositor()->createSurface(this); +} + +void WaylandCursor::init() +{ + installImage(); +} + +WaylandCursor::~WaylandCursor() +{ + delete m_surface; +} + +void WaylandCursor::installImage() +{ + const QImage image = Cursors::self()->currentCursor()->image(); + if (image.isNull() || image.size().isEmpty()) { + doInstallImage(nullptr, QSize()); + return; + } + + auto buffer = m_backend->shmPool()->createBuffer(image).toStrongRef(); + wl_buffer *imageBuffer = *buffer.data(); + doInstallImage(imageBuffer, image.size()); +} + +void WaylandCursor::doInstallImage(wl_buffer *image, const QSize &size) +{ + auto *pointer = m_backend->seat()->pointer(); + if (!pointer || !pointer->isValid()) { + return; + } + pointer->setCursor(m_surface, image ? Cursors::self()->currentCursor()->hotspot() : QPoint()); + drawSurface(image, size); +} + +void WaylandCursor::drawSurface(wl_buffer *image, const QSize &size) +{ + m_surface->attachBuffer(image); + m_surface->damage(QRect(QPoint(0,0), size)); + m_surface->commit(Surface::CommitFlag::None); + m_backend->flush(); +} + +WaylandSubSurfaceCursor::WaylandSubSurfaceCursor(WaylandBackend *backend) + : WaylandCursor(backend) +{ +} + +void WaylandSubSurfaceCursor::init() +{ + if (auto *pointer = backend()->seat()->pointer()) { + pointer->hideCursor(); + } +} + +WaylandSubSurfaceCursor::~WaylandSubSurfaceCursor() +{ + delete m_subSurface; +} + +void WaylandSubSurfaceCursor::changeOutput(WaylandOutput *output) +{ + delete m_subSurface; + m_subSurface = nullptr; + m_output = output; + if (!output) { + return; + } + createSubSurface(); + surface()->commit(); +} + +void WaylandSubSurfaceCursor::createSubSurface() +{ + if (m_subSurface) { + return; + } + if (!m_output) { + return; + } + resetSurface(); + m_subSurface = backend()->subCompositor()->createSubSurface(surface(), m_output->surface(), this); + m_subSurface->setMode(SubSurface::Mode::Desynchronized); +} + +void WaylandSubSurfaceCursor::doInstallImage(wl_buffer *image, const QSize &size) +{ + if (!image) { + delete m_subSurface; + m_subSurface = nullptr; + return; + } + createSubSurface(); + // cursor position might have changed due to different cursor hot spot + move(input()->pointer()->pos()); + drawSurface(image, size); +} + +QPointF WaylandSubSurfaceCursor::absoluteToRelativePosition(const QPointF &position) +{ + return position - m_output->geometry().topLeft() - Cursors::self()->currentCursor()->hotspot(); +} + +void WaylandSubSurfaceCursor::move(const QPointF &globalPosition) +{ + auto *output = backend()->getOutputAt(globalPosition.toPoint()); + if (!m_output || (output && m_output != output)) { + changeOutput(output); + if (!m_output) { + // cursor might be off the grid + return; + } + installImage(); + return; + } + if (!m_subSurface) { + return; + } + // place the sub-surface relative to the output it is on and factor in the hotspot + const auto relativePosition = globalPosition.toPoint() - Cursors::self()->currentCursor()->hotspot() - m_output->geometry().topLeft(); + m_subSurface->setPosition(relativePosition); + Compositor::self()->addRepaintFull(); +} + +WaylandSeat::WaylandSeat(wl_seat *seat, WaylandBackend *backend) + : QObject(nullptr) + , m_seat(new Seat(this)) + , m_pointer(nullptr) + , m_keyboard(nullptr) + , m_touch(nullptr) + , m_enteredSerial(0) + , m_backend(backend) +{ + m_seat->setup(seat); + connect(m_seat, &Seat::hasKeyboardChanged, this, + [this](bool hasKeyboard) { + if (hasKeyboard) { + m_keyboard = m_seat->createKeyboard(this); + connect(m_keyboard, &Keyboard::keyChanged, this, + [this](quint32 key, Keyboard::KeyState state, quint32 time) { + switch (state) { + case Keyboard::KeyState::Pressed: + if (key == KEY_RIGHTCTRL) { + m_backend->togglePointerLock(); + } + m_backend->keyboardKeyPressed(key, time); + break; + case Keyboard::KeyState::Released: + m_backend->keyboardKeyReleased(key, time); + break; + default: + Q_UNREACHABLE(); + } + } + ); + connect(m_keyboard, &Keyboard::modifiersChanged, this, + [this](quint32 depressed, quint32 latched, quint32 locked, quint32 group) { + m_backend->keyboardModifiers(depressed, latched, locked, group); + } + ); + connect(m_keyboard, &Keyboard::keymapChanged, this, + [this](int fd, quint32 size) { + m_backend->keymapChange(fd, size); + } + ); + } else { + destroyKeyboard(); + } + } + ); + connect(m_seat, &Seat::hasPointerChanged, this, + [this](bool hasPointer) { + if (hasPointer && !m_pointer) { + m_pointer = m_seat->createPointer(this); + setupPointerGestures(); + connect(m_pointer, &Pointer::entered, this, + [this](quint32 serial, const QPointF &relativeToSurface) { + Q_UNUSED(relativeToSurface) + m_enteredSerial = serial; + } + ); + connect(m_pointer, &Pointer::motion, this, + [this](const QPointF &relativeToSurface, quint32 time) { + m_backend->pointerMotionRelativeToOutput(relativeToSurface, time); + } + ); + connect(m_pointer, &Pointer::buttonStateChanged, this, + [this](quint32 serial, quint32 time, quint32 button, Pointer::ButtonState state) { + Q_UNUSED(serial) + switch (state) { + case Pointer::ButtonState::Pressed: + m_backend->pointerButtonPressed(button, time); + break; + case Pointer::ButtonState::Released: + m_backend->pointerButtonReleased(button, time); + break; + default: + Q_UNREACHABLE(); + } + } + ); + // TODO: Send discreteDelta and source as well. + connect(m_pointer, &Pointer::axisChanged, this, + [this](quint32 time, Pointer::Axis axis, qreal delta) { + switch (axis) { + case Pointer::Axis::Horizontal: + m_backend->pointerAxisHorizontal(delta, time); + break; + case Pointer::Axis::Vertical: + m_backend->pointerAxisVertical(delta, time); + break; + default: + Q_UNREACHABLE(); + } + } + ); + } else { + destroyPointer(); + } + } + ); + connect(m_seat, &Seat::hasTouchChanged, + [this] (bool hasTouch) { + if (hasTouch && !m_touch) { + m_touch = m_seat->createTouch(this); + connect(m_touch, &Touch::sequenceCanceled, m_backend, &Platform::touchCancel); + connect(m_touch, &Touch::frameEnded, m_backend, &Platform::touchFrame); + connect(m_touch, &Touch::sequenceStarted, this, + [this] (TouchPoint *tp) { + m_backend->touchDown(tp->id(), tp->position(), tp->time()); + } + ); + connect(m_touch, &Touch::pointAdded, this, + [this] (TouchPoint *tp) { + m_backend->touchDown(tp->id(), tp->position(), tp->time()); + } + ); + connect(m_touch, &Touch::pointRemoved, this, + [this] (TouchPoint *tp) { + m_backend->touchUp(tp->id(), tp->time()); + } + ); + connect(m_touch, &Touch::pointMoved, this, + [this] (TouchPoint *tp) { + m_backend->touchMotion(tp->id(), tp->position(), tp->time()); + } + ); + } else { + destroyTouch(); + } + } + ); + WaylandServer *server = waylandServer(); + if (server) { + using namespace KWaylandServer; + SeatInterface *si = server->seat(); + connect(m_seat, &Seat::hasKeyboardChanged, si, &SeatInterface::setHasKeyboard); + connect(m_seat, &Seat::hasPointerChanged, si, &SeatInterface::setHasPointer); + connect(m_seat, &Seat::hasTouchChanged, si, &SeatInterface::setHasTouch); + connect(m_seat, &Seat::nameChanged, si, &SeatInterface::setName); + } +} + +void WaylandBackend::pointerMotionRelativeToOutput(const QPointF &position, quint32 time) +{ + auto outputIt = std::find_if(m_outputs.constBegin(), m_outputs.constEnd(), [this](WaylandOutput *wo) { + return wo->surface() == m_seat->pointer()->enteredSurface(); + }); + Q_ASSERT(outputIt != m_outputs.constEnd()); + const QPointF outputPosition = (*outputIt)->geometry().topLeft() + position; + Platform::pointerMotion(outputPosition, time); +} + +WaylandSeat::~WaylandSeat() +{ + destroyPointer(); + destroyKeyboard(); + destroyTouch(); +} + +void WaylandSeat::destroyPointer() +{ + delete m_pinchGesture; + m_pinchGesture = nullptr; + delete m_swipeGesture; + m_swipeGesture = nullptr; + delete m_pointer; + m_pointer = nullptr; +} + +void WaylandSeat::destroyKeyboard() +{ + delete m_keyboard; + m_keyboard = nullptr; +} + +void WaylandSeat::destroyTouch() +{ + delete m_touch; + m_touch = nullptr; +} + +void WaylandSeat::setupPointerGestures() +{ + if (!m_pointer || !m_gesturesInterface) { + return; + } + if (m_pinchGesture || m_swipeGesture) { + return; + } + m_pinchGesture = m_gesturesInterface->createPinchGesture(m_pointer, this); + m_swipeGesture = m_gesturesInterface->createSwipeGesture(m_pointer, this); + connect(m_pinchGesture, &PointerPinchGesture::started, m_backend, + [this] (quint32 serial, quint32 time) { + Q_UNUSED(serial); + m_backend->processPinchGestureBegin(m_pinchGesture->fingerCount(), time); + } + ); + connect(m_pinchGesture, &PointerPinchGesture::updated, m_backend, + [this] (const QSizeF &delta, qreal scale, qreal rotation, quint32 time) { + m_backend->processPinchGestureUpdate(scale, rotation, delta, time); + } + ); + connect(m_pinchGesture, &PointerPinchGesture::ended, m_backend, + [this] (quint32 serial, quint32 time) { + Q_UNUSED(serial) + m_backend->processPinchGestureEnd(time); + } + ); + connect(m_pinchGesture, &PointerPinchGesture::cancelled, m_backend, + [this] (quint32 serial, quint32 time) { + Q_UNUSED(serial) + m_backend->processPinchGestureCancelled(time); + } + ); + + connect(m_swipeGesture, &PointerSwipeGesture::started, m_backend, + [this] (quint32 serial, quint32 time) { + Q_UNUSED(serial) + m_backend->processSwipeGestureBegin(m_swipeGesture->fingerCount(), time); + } + ); + connect(m_swipeGesture, &PointerSwipeGesture::updated, m_backend, &Platform::processSwipeGestureUpdate); + connect(m_swipeGesture, &PointerSwipeGesture::ended, m_backend, + [this] (quint32 serial, quint32 time) { + Q_UNUSED(serial) + m_backend->processSwipeGestureEnd(time); + } + ); + connect(m_swipeGesture, &PointerSwipeGesture::cancelled, m_backend, + [this] (quint32 serial, quint32 time) { + Q_UNUSED(serial) + m_backend->processSwipeGestureCancelled(time); + } + ); +} + +WaylandBackend::WaylandBackend(QObject *parent) + : Platform(parent) + , m_display(nullptr) + , m_eventQueue(new EventQueue(this)) + , m_registry(new Registry(this)) + , m_compositor(new KWayland::Client::Compositor(this)) + , m_subCompositor(new KWayland::Client::SubCompositor(this)) + , m_shm(new ShmPool(this)) + , m_connectionThreadObject(new ConnectionThread(nullptr)) + , m_connectionThread(nullptr) +{ + supportsOutputChanges(); + connect(this, &WaylandBackend::connectionFailed, this, &WaylandBackend::initFailed); + + + char const *drm_render_node = "/dev/dri/renderD128"; + m_drmFileDescriptor = open(drm_render_node, O_RDWR); + if (m_drmFileDescriptor < 0) { + qCWarning(KWIN_WAYLAND_BACKEND) << "Failed to open drm render node" << drm_render_node; + m_gbmDevice = nullptr; + return; + } + m_gbmDevice = gbm_create_device(m_drmFileDescriptor); +} + +WaylandBackend::~WaylandBackend() +{ + if (m_pointerConstraints) { + m_pointerConstraints->release(); + } + delete m_waylandCursor; + + m_eventQueue->release(); + qDeleteAll(m_outputs); + + if (m_xdgShell) { + m_xdgShell->release(); + } + m_subCompositor->release(); + m_compositor->release(); + m_registry->release(); + delete m_seat; + m_shm->release(); + + m_connectionThread->quit(); + m_connectionThread->wait(); + m_connectionThreadObject->deleteLater(); + gbm_device_destroy(m_gbmDevice); + close(m_drmFileDescriptor); + + qCDebug(KWIN_WAYLAND_BACKEND) << "Destroyed Wayland display"; +} + +void WaylandBackend::init() +{ + connect(m_registry, &Registry::compositorAnnounced, this, + [this](quint32 name) { + m_compositor->setup(m_registry->bindCompositor(name, 1)); + } + ); + connect(m_registry, &Registry::subCompositorAnnounced, this, + [this](quint32 name) { + m_subCompositor->setup(m_registry->bindSubCompositor(name, 1)); + } + ); + connect(m_registry, &Registry::seatAnnounced, this, + [this](quint32 name) { + if (Application::usesLibinput()) { + return; + } + m_seat = new WaylandSeat(m_registry->bindSeat(name, 2), this); + } + ); + connect(m_registry, &Registry::shmAnnounced, this, + [this](quint32 name) { + m_shm->setup(m_registry->bindShm(name, 1)); + } + ); + connect(m_registry, &Registry::relativePointerManagerUnstableV1Announced, this, + [this](quint32 name, quint32 version) { + if (m_relativePointerManager) { + return; + } + m_relativePointerManager = m_registry->createRelativePointerManager(name, version, this); + if (m_pointerConstraints) { + emit pointerLockSupportedChanged(); + } + } + ); + connect(m_registry, &Registry::pointerConstraintsUnstableV1Announced, this, + [this](quint32 name, quint32 version) { + if (m_pointerConstraints) { + return; + } + m_pointerConstraints = m_registry->createPointerConstraints(name, version, this); + if (m_relativePointerManager) { + emit pointerLockSupportedChanged(); + } + } + ); + connect(m_registry, &Registry::interfacesAnnounced, this, &WaylandBackend::createOutputs); + connect(m_registry, &Registry::interfacesAnnounced, this, + [this] { + if (!m_seat) { + return; + } + const auto gi = m_registry->interface(Registry::Interface::PointerGesturesUnstableV1); + if (gi.name == 0) { + return; + } + auto gesturesInterface = m_registry->createPointerGestures(gi.name, gi.version, m_seat); + m_seat->installGesturesInterface(gesturesInterface); + + m_waylandCursor = new WaylandCursor(this); + } + ); + if (!deviceIdentifier().isEmpty()) { + m_connectionThreadObject->setSocketName(deviceIdentifier()); + } + connect(Cursors::self(), &Cursors::currentCursorChanged, this, + [this] { + if (!m_seat) { + return; + } + m_waylandCursor->installImage(); + auto c = Cursors::self()->currentCursor(); + c->rendered(c->geometry()); + } + ); + connect(this, &WaylandBackend::pointerLockChanged, this, [this] (bool locked) { + delete m_waylandCursor; + if (locked) { + Q_ASSERT(!m_relativePointer); + m_waylandCursor = new WaylandSubSurfaceCursor(this); + m_waylandCursor->move(input()->pointer()->pos()); + m_relativePointer = m_relativePointerManager->createRelativePointer(m_seat->pointer(), this); + if (!m_relativePointer->isValid()) { + return; + } + connect(m_relativePointer, &RelativePointer::relativeMotion, + this, &WaylandBackend::relativeMotionHandler); + } else { + delete m_relativePointer; + m_relativePointer = nullptr; + m_waylandCursor = new WaylandCursor(this); + } + m_waylandCursor->init(); + }); + initConnection(); +} + +void WaylandBackend::relativeMotionHandler(const QSizeF &delta, const QSizeF &deltaNonAccelerated, quint64 timestamp) +{ + Q_UNUSED(deltaNonAccelerated) + Q_ASSERT(m_waylandCursor); + + const auto oldGlobalPos = input()->pointer()->pos(); + const QPointF newPos = oldGlobalPos + QPointF(delta.width(), delta.height()); + m_waylandCursor->move(newPos); + Platform::pointerMotion(newPos, timestamp); +} + +void WaylandBackend::initConnection() +{ + connect(m_connectionThreadObject, &ConnectionThread::connected, this, + [this]() { + // create the event queue for the main gui thread + m_display = m_connectionThreadObject->display(); + m_eventQueue->setup(m_connectionThreadObject); + m_registry->setEventQueue(m_eventQueue); + // setup registry + m_registry->create(m_display); + m_registry->setup(); + }, + Qt::QueuedConnection); + connect(m_connectionThreadObject, &ConnectionThread::connectionDied, this, + [this]() { + setReady(false); + emit systemCompositorDied(); + delete m_seat; + m_shm->destroy(); + + qDeleteAll(m_outputs); + m_outputs.clear(); + + if (m_xdgShell) { + m_xdgShell->destroy(); + } + m_subCompositor->destroy(); + m_compositor->destroy(); + m_registry->destroy(); + m_eventQueue->destroy(); + if (m_display) { + m_display = nullptr; + } + }, + Qt::QueuedConnection); + connect(m_connectionThreadObject, &ConnectionThread::failed, this, &WaylandBackend::connectionFailed, Qt::QueuedConnection); + + m_connectionThread = new QThread(this); + m_connectionThreadObject->moveToThread(m_connectionThread); + m_connectionThread->start(); + + m_connectionThreadObject->initConnection(); +} + +void WaylandBackend::updateScreenSize(WaylandOutput *output) +{ + auto it = std::find(m_outputs.constBegin(), m_outputs.constEnd(), output); + + int nextLogicalPosition = output->geometry().topRight().x(); + while (++it != m_outputs.constEnd()) { + const QRect geo = (*it)->geometry(); + (*it)->setGeometry(QPoint(nextLogicalPosition, 0), geo.size()); + nextLogicalPosition = geo.topRight().x(); + } +} + +void WaylandBackend::createOutputs() +{ + using namespace KWayland::Client; + + const auto ssdManagerIface = m_registry->interface(Registry::Interface::ServerSideDecorationManager); + ServerSideDecorationManager *ssdManager = ssdManagerIface.name == 0 ? nullptr : + m_registry->createServerSideDecorationManager(ssdManagerIface.name, ssdManagerIface.version, this); + + + const auto xdgIface = m_registry->interface(Registry::Interface::XdgShellStable); + if (xdgIface.name != 0) { + m_xdgShell = m_registry->createXdgShell(xdgIface.name, xdgIface.version, this); + } + + // we need to multiply the initial window size with the scale in order to + // create an output window of this size in the end + const int pixelWidth = initialWindowSize().width() * initialOutputScale() + 0.5; + const int pixelHeight = initialWindowSize().height() * initialOutputScale() + 0.5; + const int logicalWidth = initialWindowSize().width(); + + int logicalWidthSum = 0; + for (int i = 0; i < initialOutputCount(); i++) { + auto surface = m_compositor->createSurface(this); + if (!surface || !surface->isValid()) { + qCCritical(KWIN_WAYLAND_BACKEND) << "Creating Wayland Surface failed"; + return; + } + + if (ssdManager) { + auto decoration = ssdManager->create(surface, this); + connect(decoration, &ServerSideDecoration::modeChanged, this, + [decoration] { + if (decoration->mode() != ServerSideDecoration::Mode::Server) { + decoration->requestMode(ServerSideDecoration::Mode::Server); + } + } + ); + } + + WaylandOutput *waylandOutput = nullptr; + + if (m_xdgShell && m_xdgShell->isValid()) { + waylandOutput = new XdgShellOutput(surface, m_xdgShell, this, i+1); + } + + if (!waylandOutput) { + qCCritical(KWIN_WAYLAND_BACKEND) << "Binding to all shell interfaces failed for output" << i; + return; + } + + waylandOutput->init(QPoint(logicalWidthSum, 0), QSize(pixelWidth, pixelHeight)); + + connect(waylandOutput, &WaylandOutput::sizeChanged, this, [this, waylandOutput](const QSize &size) { + Q_UNUSED(size) + updateScreenSize(waylandOutput); + Compositor::self()->addRepaintFull(); + }); + connect(waylandOutput, &WaylandOutput::frameRendered, this, &WaylandBackend::checkBufferSwap); + + logicalWidthSum += logicalWidth; + m_outputs << waylandOutput; + } + setReady(true); + emit screensQueried(); +} + +Screens *WaylandBackend::createScreens(QObject *parent) +{ + return new OutputScreens(this, parent); +} + +OpenGLBackend *WaylandBackend::createOpenGLBackend() +{ +#if HAVE_WAYLAND_EGL + return new EglWaylandBackend(this); +#else + return nullptr; +#endif +} + +QPainterBackend *WaylandBackend::createQPainterBackend() +{ + return new WaylandQPainterBackend(this); +} + +void WaylandBackend::checkBufferSwap() +{ + const bool allRendered = std::all_of(m_outputs.constBegin(), m_outputs.constEnd(), [](WaylandOutput *o) { + return o->rendered(); + }); + if (!allRendered) { + // need to wait more + // TODO: what if one does not need to be rendered (no damage)? + return; + } + Compositor::self()->bufferSwapComplete(); + + for (auto *output : qAsConst(m_outputs)) { + output->resetRendered(); + } +} + +void WaylandBackend::flush() +{ + if (m_connectionThreadObject) { + m_connectionThreadObject->flush(); + } +} + +WaylandOutput* WaylandBackend::getOutputAt(const QPointF &globalPosition) +{ + const auto pos = globalPosition.toPoint(); + auto checkPosition = [pos](WaylandOutput *output) { + return output->geometry().contains(pos); + }; + auto it = std::find_if(m_outputs.constBegin(), m_outputs.constEnd(), checkPosition); + return it == m_outputs.constEnd() ? nullptr : *it; +} + +bool WaylandBackend::supportsPointerLock() +{ + return m_pointerConstraints && m_relativePointerManager; +} + +void WaylandBackend::togglePointerLock() +{ + if (!m_pointerConstraints) { + return; + } + if (!m_relativePointerManager) { + return; + } + if (!m_seat) { + return; + } + auto pointer = m_seat->pointer(); + if (!pointer) { + return; + } + if (m_outputs.isEmpty()) { + return; + } + + for (auto output : m_outputs) { + output->lockPointer(m_seat->pointer(), !m_pointerLockRequested); + } + m_pointerLockRequested = !m_pointerLockRequested; + flush(); +} + +bool WaylandBackend::pointerIsLocked() +{ + for (auto *output : m_outputs) { + if (output->pointerIsLocked()) { + return true; + } + } + return false; +} + +QVector WaylandBackend::supportedCompositors() const +{ + if (selectedCompositor() != NoCompositing) { + return {selectedCompositor()}; + } +#if HAVE_WAYLAND_EGL + return QVector{OpenGLCompositing, QPainterCompositing}; +#else + return QVector{QPainterCompositing}; +#endif +} + +Outputs WaylandBackend::outputs() const +{ + return m_outputs; +} + +Outputs WaylandBackend::enabledOutputs() const +{ + // all outputs are enabled + return m_outputs; +} + +DmaBufTexture *WaylandBackend::createDmaBufTexture(const QSize& size) +{ + return GbmDmaBuf::createBuffer(size, m_gbmDevice); +} + +} + +} // KWin diff --git a/plugins/platforms/wayland/wayland_backend.h b/plugins/platforms/wayland/wayland_backend.h new file mode 100644 index 0000000..4dfeda6 --- /dev/null +++ b/plugins/platforms/wayland/wayland_backend.h @@ -0,0 +1,273 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Roman Gilg + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_WAYLAND_BACKEND_H +#define KWIN_WAYLAND_BACKEND_H +// KWin +#include "platform.h" +#include +#include +// Qt +#include +#include +#include +#include +#include + +class QTemporaryFile; +struct wl_buffer; +struct wl_display; +struct wl_event_queue; +struct wl_seat; +struct gbm_device; + +namespace KWayland +{ +namespace Client +{ +class Buffer; +class ShmPool; +class Compositor; +class ConnectionThread; +class EventQueue; +class Keyboard; +class Pointer; +class PointerConstraints; +class PointerGestures; +class PointerSwipeGesture; +class PointerPinchGesture; +class Registry; +class RelativePointer; +class RelativePointerManager; +class Seat; +class SubCompositor; +class SubSurface; +class Surface; +class Touch; +class XdgShell; +} +} + +namespace KWin +{ +namespace Wayland +{ + +class WaylandBackend; +class WaylandSeat; +class WaylandOutput; + +class WaylandCursor : public QObject +{ + Q_OBJECT +public: + explicit WaylandCursor(WaylandBackend *backend); + ~WaylandCursor() override; + + virtual void init(); + virtual void move(const QPointF &globalPosition) { + Q_UNUSED(globalPosition) + } + + void installImage(); + +protected: + void resetSurface(); + virtual void doInstallImage(wl_buffer *image, const QSize &size); + void drawSurface(wl_buffer *image, const QSize &size); + + KWayland::Client::Surface *surface() const { + return m_surface; + } + WaylandBackend *backend() const { + return m_backend; + } + +private: + WaylandBackend *m_backend; + KWayland::Client::Pointer *m_pointer; + KWayland::Client::Surface *m_surface = nullptr; +}; + +class WaylandSubSurfaceCursor : public WaylandCursor +{ + Q_OBJECT +public: + explicit WaylandSubSurfaceCursor(WaylandBackend *backend); + ~WaylandSubSurfaceCursor() override; + + void init() override; + + void move(const QPointF &globalPosition) override; + +private: + void changeOutput(WaylandOutput *output); + void doInstallImage(wl_buffer *image, const QSize &size) override; + void createSubSurface(); + + QPointF absoluteToRelativePosition(const QPointF &position); + WaylandOutput *m_output = nullptr; + KWayland::Client::SubSurface *m_subSurface = nullptr; +}; + +class WaylandSeat : public QObject +{ + Q_OBJECT +public: + WaylandSeat(wl_seat *seat, WaylandBackend *backend); + ~WaylandSeat() override; + + KWayland::Client::Pointer *pointer() const { + return m_pointer; + } + + void installGesturesInterface(KWayland::Client::PointerGestures *gesturesInterface) { + m_gesturesInterface = gesturesInterface; + setupPointerGestures(); + } + +private: + void destroyPointer(); + void destroyKeyboard(); + void destroyTouch(); + void setupPointerGestures(); + + KWayland::Client::Seat *m_seat; + KWayland::Client::Pointer *m_pointer; + KWayland::Client::Keyboard *m_keyboard; + KWayland::Client::Touch *m_touch; + KWayland::Client::PointerGestures *m_gesturesInterface = nullptr; + KWayland::Client::PointerPinchGesture *m_pinchGesture = nullptr; + KWayland::Client::PointerSwipeGesture *m_swipeGesture = nullptr; + + uint32_t m_enteredSerial; + + WaylandBackend *m_backend; +}; + +/** +* @brief Class encapsulating all Wayland data structures needed by the Egl backend. +* +* It creates the connection to the Wayland Compositor, sets up the registry and creates +* the Wayland output surfaces and its shell mappings. +*/ +class KWIN_EXPORT WaylandBackend : public Platform +{ + Q_OBJECT + Q_INTERFACES(KWin::Platform) + Q_PLUGIN_METADATA(IID "org.kde.kwin.Platform" FILE "wayland.json") +public: + explicit WaylandBackend(QObject *parent = nullptr); + ~WaylandBackend() override; + void init() override; + wl_display *display(); + KWayland::Client::Compositor *compositor(); + KWayland::Client::SubCompositor *subCompositor(); + KWayland::Client::ShmPool *shmPool(); + + Screens *createScreens(QObject *parent = nullptr) override; + OpenGLBackend *createOpenGLBackend() override; + QPainterBackend *createQPainterBackend() override; + DmaBufTexture *createDmaBufTexture(const QSize &size) override; + + void flush(); + + WaylandSeat *seat() const { + return m_seat; + } + KWayland::Client::PointerConstraints *pointerConstraints() const { + return m_pointerConstraints; + } + + void pointerMotionRelativeToOutput(const QPointF &position, quint32 time); + + bool supportsPointerLock(); + void togglePointerLock(); + bool pointerIsLocked(); + + QVector supportedCompositors() const override; + + void checkBufferSwap(); + + WaylandOutput* getOutputAt(const QPointF &globalPosition); + Outputs outputs() const override; + Outputs enabledOutputs() const override; + QVector waylandOutputs() const { + return m_outputs; + } + +Q_SIGNALS: + void outputAdded(WaylandOutput *output); + void outputRemoved(WaylandOutput *output); + + void systemCompositorDied(); + void connectionFailed(); + + void pointerLockSupportedChanged(); + void pointerLockChanged(bool locked); + +private: + void initConnection(); + void createOutputs(); + + void updateScreenSize(WaylandOutput *output); + void relativeMotionHandler(const QSizeF &delta, const QSizeF &deltaNonAccelerated, quint64 timestamp); + + wl_display *m_display; + KWayland::Client::EventQueue *m_eventQueue; + KWayland::Client::Registry *m_registry; + KWayland::Client::Compositor *m_compositor; + KWayland::Client::SubCompositor *m_subCompositor; + KWayland::Client::XdgShell *m_xdgShell = nullptr; + KWayland::Client::ShmPool *m_shm; + KWayland::Client::ConnectionThread *m_connectionThreadObject; + + WaylandSeat *m_seat = nullptr; + KWayland::Client::RelativePointer *m_relativePointer = nullptr; + KWayland::Client::RelativePointerManager *m_relativePointerManager = nullptr; + KWayland::Client::PointerConstraints *m_pointerConstraints = nullptr; + + QThread *m_connectionThread; + QVector m_outputs; + + WaylandCursor *m_waylandCursor = nullptr; + + bool m_pointerLockRequested = false; + int m_drmFileDescriptor = 0; + gbm_device *m_gbmDevice; +}; + +inline +wl_display *WaylandBackend::display() +{ + return m_display; +} + +inline +KWayland::Client::Compositor *WaylandBackend::compositor() +{ + return m_compositor; +} + +inline +KWayland::Client::SubCompositor *WaylandBackend::subCompositor() +{ + return m_subCompositor; +} + +inline +KWayland::Client::ShmPool* WaylandBackend::shmPool() +{ + return m_shm; +} + +} // namespace Wayland +} // namespace KWin + +#endif // KWIN_WAYLAND_BACKEND_H diff --git a/plugins/platforms/wayland/wayland_output.cpp b/plugins/platforms/wayland/wayland_output.cpp new file mode 100644 index 0000000..86cb043 --- /dev/null +++ b/plugins/platforms/wayland/wayland_output.cpp @@ -0,0 +1,172 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "wayland_output.h" +#include "wayland_backend.h" + +#include "wayland_server.h" + +#include +#include + +#include + +#include + +namespace KWin +{ +namespace Wayland +{ + +using namespace KWayland::Client; + +WaylandOutput::WaylandOutput(Surface *surface, WaylandBackend *backend) + : AbstractWaylandOutput(backend) + , m_surface(surface) + , m_backend(backend) +{ + static int identifier = -1; + identifier++; + setName("WL-" + QString::number(identifier)); + + connect(surface, &Surface::frameRendered, [this] { + m_rendered = true; + emit frameRendered(); + }); +} + +WaylandOutput::~WaylandOutput() +{ + m_surface->destroy(); + delete m_surface; +} + +void WaylandOutput::init(const QPoint &logicalPosition, const QSize &pixelSize) +{ + KWaylandServer::OutputDeviceInterface::Mode mode; + mode.id = 0; + mode.size = pixelSize; + mode.flags = KWaylandServer::OutputDeviceInterface::ModeFlag::Current; + mode.refreshRate = 60000; // TODO: can we get refresh rate data from Wayland host? + initInterfaces("model_TODO", "manufacturer_TODO", "UUID_TODO", pixelSize, { mode }); + setGeometry(logicalPosition, pixelSize); + setScale(backend()->initialOutputScale()); +} + +void WaylandOutput::setGeometry(const QPoint &logicalPosition, const QSize &pixelSize) +{ + // TODO: set mode to have updated pixelSize + Q_UNUSED(pixelSize) + + setGlobalPos(logicalPosition); +} + +XdgShellOutput::XdgShellOutput(Surface *surface, XdgShell *xdgShell, WaylandBackend *backend, int number) + : WaylandOutput(surface, backend) + , m_number(number) +{ + m_xdgShellSurface = xdgShell->createSurface(surface, this); + updateWindowTitle(); + + connect(m_xdgShellSurface, &XdgShellSurface::configureRequested, this, &XdgShellOutput::handleConfigure); + connect(m_xdgShellSurface, &XdgShellSurface::closeRequested, qApp, &QCoreApplication::quit); + + connect(backend, &WaylandBackend::pointerLockSupportedChanged, this, &XdgShellOutput::updateWindowTitle); + connect(backend, &WaylandBackend::pointerLockChanged, this, [this](bool locked) { + if (locked) { + if (!m_hasPointerLock) { + // some other output has locked the pointer + // this surface can stop trying to lock the pointer + lockPointer(nullptr, false); + // set it true for the other surface + m_hasPointerLock = true; + } + } else { + // just try unlocking + lockPointer(nullptr, false); + } + updateWindowTitle(); + }); + + surface->commit(Surface::CommitFlag::None); +} + +XdgShellOutput::~XdgShellOutput() +{ + m_xdgShellSurface->destroy(); + delete m_xdgShellSurface; +} + +void XdgShellOutput::handleConfigure(const QSize &size, XdgShellSurface::States states, quint32 serial) +{ + Q_UNUSED(states); + if (size.width() > 0 && size.height() > 0) { + setGeometry(geometry().topLeft(), size); + emit sizeChanged(size); + } + m_xdgShellSurface->ackConfigure(serial); +} + +void XdgShellOutput::updateWindowTitle() +{ + QString grab; + if (m_hasPointerLock) { + grab = i18n("Press right control to ungrab pointer"); + } else if (backend()->pointerConstraints()) { + grab = i18n("Press right control key to grab pointer"); + } + const QString title = i18nc("Title of nested KWin Wayland with Wayland socket identifier as argument", + "KDE Wayland Compositor #%1 (%2)", m_number, waylandServer()->display()->socketName()); + + if (grab.isEmpty()) { + m_xdgShellSurface->setTitle(title); + } else { + m_xdgShellSurface->setTitle(title + QStringLiteral(" — ") + grab); + } +} + +void XdgShellOutput::lockPointer(Pointer *pointer, bool lock) +{ + if (!lock) { + const bool surfaceWasLocked = m_pointerLock && m_hasPointerLock; + delete m_pointerLock; + m_pointerLock = nullptr; + m_hasPointerLock = false; + if (surfaceWasLocked) { + emit backend()->pointerLockChanged(false); + } + return; + } + + Q_ASSERT(!m_pointerLock); + m_pointerLock = backend()->pointerConstraints()->lockPointer(surface(), pointer, nullptr, + PointerConstraints::LifeTime::OneShot, + this); + if (!m_pointerLock->isValid()) { + delete m_pointerLock; + m_pointerLock = nullptr; + return; + } + connect(m_pointerLock, &LockedPointer::locked, this, + [this] { + m_hasPointerLock = true; + emit backend()->pointerLockChanged(true); + } + ); + connect(m_pointerLock, &LockedPointer::unlocked, this, + [this] { + delete m_pointerLock; + m_pointerLock = nullptr; + m_hasPointerLock = false; + emit backend()->pointerLockChanged(false); + } + ); +} + +} +} diff --git a/plugins/platforms/wayland/wayland_output.h b/plugins/platforms/wayland/wayland_output.h new file mode 100644 index 0000000..ffaa439 --- /dev/null +++ b/plugins/platforms/wayland/wayland_output.h @@ -0,0 +1,111 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_WAYLAND_OUTPUT_H +#define KWIN_WAYLAND_OUTPUT_H + +#include "abstract_wayland_output.h" + +#include + +#include + +namespace KWayland +{ +namespace Client +{ +class Surface; + +class Shell; +class ShellSurface; + +class Pointer; +class LockedPointer; +} +} + +namespace KWin +{ +namespace Wayland +{ +class WaylandBackend; + +class WaylandOutput : public AbstractWaylandOutput +{ + Q_OBJECT +public: + WaylandOutput(KWayland::Client::Surface *surface, WaylandBackend *backend); + ~WaylandOutput() override; + + void init(const QPoint &logicalPosition, const QSize &pixelSize); + + virtual void lockPointer(KWayland::Client::Pointer *pointer, bool lock) { + Q_UNUSED(pointer) + Q_UNUSED(lock) + } + + virtual bool pointerIsLocked() { return false; } + + /** + * @brief defines the geometry of the output + * @param logicalPosition top left position of the output in compositor space + * @param pixelSize output size as seen from the outside + */ + void setGeometry(const QPoint &logicalPosition, const QSize &pixelSize); + + KWayland::Client::Surface* surface() const { + return m_surface; + } + + bool rendered() const { + return m_rendered; + } + void resetRendered() { + m_rendered = false; + } + +Q_SIGNALS: + void sizeChanged(const QSize &size); + void frameRendered(); + +protected: + WaylandBackend *backend() { + return m_backend; + } + +private: + KWayland::Client::Surface *m_surface; + WaylandBackend *m_backend; + + bool m_rendered = false; +}; + +class XdgShellOutput : public WaylandOutput +{ +public: + XdgShellOutput(KWayland::Client::Surface *surface, + KWayland::Client::XdgShell *xdgShell, + WaylandBackend *backend, int number); + ~XdgShellOutput() override; + + void lockPointer(KWayland::Client::Pointer *pointer, bool lock) override; + +private: + void handleConfigure(const QSize &size, KWayland::Client::XdgShellSurface::States states, quint32 serial); + void updateWindowTitle(); + + KWayland::Client::XdgShellSurface *m_xdgShellSurface = nullptr; + int m_number; + KWayland::Client::LockedPointer *m_pointerLock = nullptr; + bool m_hasPointerLock = false; +}; + +} +} + +#endif diff --git a/plugins/platforms/x11/CMakeLists.txt b/plugins/platforms/x11/CMakeLists.txt new file mode 100644 index 0000000..c62bb44 --- /dev/null +++ b/plugins/platforms/x11/CMakeLists.txt @@ -0,0 +1,5 @@ +add_subdirectory(common) +add_subdirectory(standalone) +if (X11_XCB_FOUND) + add_subdirectory(windowed) +endif() diff --git a/plugins/platforms/x11/common/CMakeLists.txt b/plugins/platforms/x11/common/CMakeLists.txt new file mode 100644 index 0000000..d52ff25 --- /dev/null +++ b/plugins/platforms/x11/common/CMakeLists.txt @@ -0,0 +1,9 @@ +if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-int-to-pointer-cast") +endif() +if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-int-to-void-pointer-cast") +endif() +include_directories(${CMAKE_SOURCE_DIR}/platformsupport/scenes/opengl) +add_library(eglx11common STATIC eglonxbackend.cpp) +target_link_libraries(eglx11common kwin) diff --git a/plugins/platforms/x11/common/eglonxbackend.cpp b/plugins/platforms/x11/common/eglonxbackend.cpp new file mode 100644 index 0000000..c727bc5 --- /dev/null +++ b/plugins/platforms/x11/common/eglonxbackend.cpp @@ -0,0 +1,533 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2010, 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "eglonxbackend.h" +// kwin +#include "main.h" +#include "options.h" +#include "overlaywindow.h" +#include "platform.h" +#include "scene.h" +#include "screens.h" +#include "xcbutils.h" +#include "texture.h" +// kwin libs +#include +#include +// Qt +#include +#include +#include +// system +#include + +Q_LOGGING_CATEGORY(KWIN_CORE, "kwin_core", QtCriticalMsg) + +namespace KWin +{ + +EglOnXBackend::EglOnXBackend(Display *display) + : AbstractEglBackend() + , m_overlayWindow(kwinApp()->platform()->createOverlayWindow()) + , surfaceHasSubPost(0) + , m_bufferAge(0) + , m_usesOverlayWindow(true) + , m_connection(connection()) + , m_x11Display(display) + , m_rootWindow(rootWindow()) + , m_x11ScreenNumber(kwinApp()->x11ScreenNumber()) +{ + // Egl is always direct rendering + setIsDirectRendering(true); +} + +EglOnXBackend::EglOnXBackend(xcb_connection_t *connection, Display *display, xcb_window_t rootWindow, int screenNumber, xcb_window_t renderingWindow) + : AbstractEglBackend() + , m_overlayWindow(nullptr) + , surfaceHasSubPost(0) + , m_bufferAge(0) + , m_usesOverlayWindow(false) + , m_connection(connection) + , m_x11Display(display) + , m_rootWindow(rootWindow) + , m_x11ScreenNumber(screenNumber) + , m_renderingWindow(renderingWindow) +{ + // Egl is always direct rendering + setIsDirectRendering(true); +} + +static bool gs_tripleBufferUndetected = true; +static bool gs_tripleBufferNeedsDetection = false; + +EglOnXBackend::~EglOnXBackend() +{ + if (isFailed() && m_overlayWindow) { + m_overlayWindow->destroy(); + } + cleanup(); + + gs_tripleBufferUndetected = true; + gs_tripleBufferNeedsDetection = false; + + if (m_overlayWindow) { + if (overlayWindow()->window()) { + overlayWindow()->destroy(); + } + delete m_overlayWindow; + } +} + +void EglOnXBackend::init() +{ + qputenv("EGL_PLATFORM", "x11"); + if (!initRenderingContext()) { + setFailed(QStringLiteral("Could not initialize rendering context")); + return; + } + + initKWinGL(); + if (!hasExtension(QByteArrayLiteral("EGL_KHR_image")) && + (!hasExtension(QByteArrayLiteral("EGL_KHR_image_base")) || + !hasExtension(QByteArrayLiteral("EGL_KHR_image_pixmap")))) { + setFailed(QStringLiteral("Required support for binding pixmaps to EGLImages not found, disabling compositing")); + return; + } + if (!hasGLExtension(QByteArrayLiteral("GL_OES_EGL_image"))) { + setFailed(QStringLiteral("Required extension GL_OES_EGL_image not found, disabling compositing")); + return; + } + + // check for EGL_NV_post_sub_buffer and whether it can be used on the surface + if (hasExtension(QByteArrayLiteral("EGL_NV_post_sub_buffer"))) { + if (eglQuerySurface(eglDisplay(), surface(), EGL_POST_SUB_BUFFER_SUPPORTED_NV, &surfaceHasSubPost) == EGL_FALSE) { + EGLint error = eglGetError(); + if (error != EGL_SUCCESS && error != EGL_BAD_ATTRIBUTE) { + setFailed(QStringLiteral("query surface failed")); + return; + } else { + surfaceHasSubPost = EGL_FALSE; + } + } + } + + setSyncsToVBlank(false); + setBlocksForRetrace(false); + gs_tripleBufferNeedsDetection = false; + m_swapProfiler.init(); + if (surfaceHasSubPost) { + qCDebug(KWIN_CORE) << "EGL implementation and surface support eglPostSubBufferNV, let's use it"; + + if (options->glPreferBufferSwap() != Options::NoSwapEncourage) { + // check if swap interval 1 is supported + EGLint val; + eglGetConfigAttrib(eglDisplay(), config(), EGL_MAX_SWAP_INTERVAL, &val); + if (val >= 1) { + if (eglSwapInterval(eglDisplay(), 1)) { + qCDebug(KWIN_CORE) << "Enabled v-sync"; + setSyncsToVBlank(true); + const QByteArray tripleBuffer = qgetenv("KWIN_TRIPLE_BUFFER"); + if (!tripleBuffer.isEmpty()) { + setBlocksForRetrace(qstrcmp(tripleBuffer, "0") == 0); + gs_tripleBufferUndetected = false; + } + gs_tripleBufferNeedsDetection = gs_tripleBufferUndetected; + } + } else { + qCWarning(KWIN_CORE) << "Cannot enable v-sync as max. swap interval is" << val; + } + } else { + // disable v-sync + eglSwapInterval(eglDisplay(), 0); + } + } else { + /* In the GLX backend, we fall back to using glCopyPixels if we have no extension providing support for partial screen updates. + * However, that does not work in EGL - glCopyPixels with glDrawBuffer(GL_FRONT); does nothing. + * Hence we need EGL to preserve the backbuffer for us, so that we can draw the partial updates on it and call + * eglSwapBuffers() for each frame. eglSwapBuffers() then does the copy (no page flip possible in this mode), + * which means it is slow and not synced to the v-blank. */ + qCWarning(KWIN_CORE) << "eglPostSubBufferNV not supported, have to enable buffer preservation - which breaks v-sync and performance"; + eglSurfaceAttrib(eglDisplay(), surface(), EGL_SWAP_BEHAVIOR, EGL_BUFFER_PRESERVED); + } + + initWayland(); +} + +bool EglOnXBackend::initRenderingContext() +{ + initClientExtensions(); + EGLDisplay dpy = kwinApp()->platform()->sceneEglDisplay(); + + // Use eglGetPlatformDisplayEXT() to get the display pointer + // if the implementation supports it. + if (dpy == EGL_NO_DISPLAY) { + const bool havePlatformBase = hasClientExtension(QByteArrayLiteral("EGL_EXT_platform_base")); + setHavePlatformBase(havePlatformBase); + if (havePlatformBase) { + // Make sure that the X11 platform is supported + if (!hasClientExtension(QByteArrayLiteral("EGL_EXT_platform_x11")) && + !hasClientExtension(QByteArrayLiteral("EGL_KHR_platform_x11"))) { + qCWarning(KWIN_CORE) << "EGL_EXT_platform_base is supported, but neither EGL_EXT_platform_x11 nor EGL_KHR_platform_x11 is supported." + << "Cannot create EGLDisplay on X11"; + return false; + } + + const int attribs[] = { + EGL_PLATFORM_X11_SCREEN_EXT, m_x11ScreenNumber, + EGL_NONE + }; + + dpy = eglGetPlatformDisplayEXT(EGL_PLATFORM_X11_EXT, m_x11Display, attribs); + } else { + dpy = eglGetDisplay(m_x11Display); + } + } + + if (dpy == EGL_NO_DISPLAY) { + qCWarning(KWIN_CORE) << "Failed to get the EGLDisplay"; + return false; + } + setEglDisplay(dpy); + initEglAPI(); + + initBufferConfigs(); + + if (m_usesOverlayWindow) { + if (!overlayWindow()->create()) { + qCCritical(KWIN_CORE) << "Could not get overlay window"; + return false; + } else { + overlayWindow()->setup(None); + } + } + if (!createSurfaces()) { + qCCritical(KWIN_CORE) << "Creating egl surface failed"; + return false; + } + + if (!createContext()) { + qCCritical(KWIN_CORE) << "Create OpenGL context failed"; + return false; + } + + if (!makeContextCurrent(surface())) { + qCCritical(KWIN_CORE) << "Make Context Current failed"; + return false; + } + + EGLint error = eglGetError(); + if (error != EGL_SUCCESS) { + qCWarning(KWIN_CORE) << "Error occurred while creating context " << error; + return false; + } + + return true; +} + +bool EglOnXBackend::createSurfaces() +{ + xcb_window_t window = XCB_WINDOW_NONE; + if (m_overlayWindow) { + window = m_overlayWindow->window(); + } else if (m_renderingWindow) { + window = m_renderingWindow; + } + + EGLSurface surface = createSurface(window); + + if (surface == EGL_NO_SURFACE) { + return false; + } + setSurface(surface); + return true; +} + +EGLSurface EglOnXBackend::createSurface(xcb_window_t window) +{ + if (window == XCB_WINDOW_NONE) { + return EGL_NO_SURFACE; + } + + EGLSurface surface = EGL_NO_SURFACE; + if (havePlatformBase()) { + // Note: Window is 64 bits on a 64-bit architecture whereas xcb_window_t is + // always 32 bits. eglCreatePlatformWindowSurfaceEXT() expects the + // native_window parameter to be pointer to a Window, so this variable + // cannot be an xcb_window_t. + surface = eglCreatePlatformWindowSurfaceEXT(eglDisplay(), config(), (void *) &window, nullptr); + } else { + surface = eglCreateWindowSurface(eglDisplay(), config(), window, nullptr); + } + + return surface; +} + +bool EglOnXBackend::initBufferConfigs() +{ + initBufferAge(); + const EGLint config_attribs[] = { + EGL_SURFACE_TYPE, EGL_WINDOW_BIT | (supportsBufferAge() ? 0 : EGL_SWAP_BEHAVIOR_PRESERVED_BIT), + EGL_RED_SIZE, 1, + EGL_GREEN_SIZE, 1, + EGL_BLUE_SIZE, 1, + EGL_ALPHA_SIZE, 0, + EGL_RENDERABLE_TYPE, isOpenGLES() ? EGL_OPENGL_ES2_BIT : EGL_OPENGL_BIT, + EGL_CONFIG_CAVEAT, EGL_NONE, + EGL_NONE, + }; + + EGLint count; + EGLConfig configs[1024]; + if (eglChooseConfig(eglDisplay(), config_attribs, configs, 1024, &count) == EGL_FALSE) { + qCCritical(KWIN_CORE) << "choose config failed"; + return false; + } + + ScopedCPointer attribs(xcb_get_window_attributes_reply(m_connection, + xcb_get_window_attributes_unchecked(m_connection, m_rootWindow), + nullptr)); + if (!attribs) { + qCCritical(KWIN_CORE) << "Failed to get window attributes of root window"; + return false; + } + + setConfig(configs[0]); + for (int i = 0; i < count; i++) { + EGLint val; + if (eglGetConfigAttrib(eglDisplay(), configs[i], EGL_NATIVE_VISUAL_ID, &val) == EGL_FALSE) { + qCCritical(KWIN_CORE) << "egl get config attrib failed"; + } + if (uint32_t(val) == attribs->visual) { + setConfig(configs[i]); + break; + } + } + return true; +} + +void EglOnXBackend::present() +{ + if (lastDamage().isEmpty()) + return; + + presentSurface(surface(), lastDamage(), screens()->geometry()); + + setLastDamage(QRegion()); + if (!supportsBufferAge()) { + eglWaitGL(); + xcb_flush(m_connection); + } +} + +void EglOnXBackend::presentSurface(EGLSurface surface, const QRegion &damage, const QRect &screenGeometry) +{ + if (damage.isEmpty()) { + return; + } + const bool fullRepaint = supportsBufferAge() || (damage == screenGeometry); + + if (fullRepaint || !surfaceHasSubPost) { + if (gs_tripleBufferNeedsDetection) { + eglWaitGL(); + m_swapProfiler.begin(); + } + // the entire screen changed, or we cannot do partial updates (which implies we enabled surface preservation) + eglSwapBuffers(eglDisplay(), surface); + if (gs_tripleBufferNeedsDetection) { + eglWaitGL(); + if (char result = m_swapProfiler.end()) { + gs_tripleBufferUndetected = gs_tripleBufferNeedsDetection = false; + if (result == 'd' && GLPlatform::instance()->driver() == Driver_NVidia) { + // TODO this is a workaround, we should get __GL_YIELD set before libGL checks it + if (qstrcmp(qgetenv("__GL_YIELD"), "USLEEP")) { + options->setGlPreferBufferSwap(0); + eglSwapInterval(eglDisplay(), 0); + result = 0; // hint proper behavior + qCWarning(KWIN_CORE) << "\nIt seems you are using the nvidia driver without triple buffering\n" + "You must export __GL_YIELD=\"USLEEP\" to prevent large CPU overhead on synced swaps\n" + "Preferably, enable the TripleBuffer Option in the xorg.conf Device\n" + "For this reason, the tearing prevention has been disabled.\n" + "See https://bugs.kde.org/show_bug.cgi?id=322060\n"; + } + } + setBlocksForRetrace(result == 'd'); + } + } + if (supportsBufferAge()) { + eglQuerySurface(eglDisplay(), surface, EGL_BUFFER_AGE_EXT, &m_bufferAge); + } + } else { + // a part of the screen changed, and we can use eglPostSubBufferNV to copy the updated area + for (const QRect &r : damage) { + eglPostSubBufferNV(eglDisplay(), surface, r.left(), screenGeometry.height() - r.bottom() - 1, r.width(), r.height()); + } + } +} + +void EglOnXBackend::screenGeometryChanged(const QSize &size) +{ + Q_UNUSED(size) + + // TODO: base implementation in OpenGLBackend + + // The back buffer contents are now undefined + m_bufferAge = 0; +} + +SceneOpenGLTexturePrivate *EglOnXBackend::createBackendTexture(SceneOpenGLTexture *texture) +{ + return new EglTexture(texture, this); +} + +QRegion EglOnXBackend::prepareRenderingFrame() +{ + QRegion repaint; + + if (gs_tripleBufferNeedsDetection) { + // the composite timer floors the repaint frequency. This can pollute our triple buffering + // detection because the glXSwapBuffers call for the new frame has to wait until the pending + // one scanned out. + // So we compensate for that by waiting an extra milisecond to give the driver the chance to + // fllush the buffer queue + usleep(1000); + } + + present(); + + if (supportsBufferAge()) + repaint = accumulatedDamageHistory(m_bufferAge); + + startRenderTimer(); + eglWaitNative(EGL_CORE_NATIVE_ENGINE); + + return repaint; +} + +void EglOnXBackend::endRenderingFrame(const QRegion &renderedRegion, const QRegion &damagedRegion) +{ + if (damagedRegion.isEmpty()) { + setLastDamage(QRegion()); + + // If the damaged region of a window is fully occluded, the only + // rendering done, if any, will have been to repair a reused back + // buffer, making it identical to the front buffer. + // + // In this case we won't post the back buffer. Instead we'll just + // set the buffer age to 1, so the repaired regions won't be + // rendered again in the next frame. + if (!renderedRegion.isEmpty()) + glFlush(); + + m_bufferAge = 1; + return; + } + + setLastDamage(renderedRegion); + + if (!blocksForRetrace()) { + // This also sets lastDamage to empty which prevents the frame from + // being posted again when prepareRenderingFrame() is called. + present(); + } else { + // Make sure that the GPU begins processing the command stream + // now and not the next time prepareRenderingFrame() is called. + glFlush(); + } + + if (m_overlayWindow && overlayWindow()->window()) // show the window only after the first pass, + overlayWindow()->show(); // since that pass may take long + + // Save the damaged region to history + if (supportsBufferAge()) + addToDamageHistory(damagedRegion); +} + +bool EglOnXBackend::usesOverlayWindow() const +{ + return m_usesOverlayWindow; +} + +OverlayWindow* EglOnXBackend::overlayWindow() const +{ + return m_overlayWindow; +} + +bool EglOnXBackend::makeContextCurrent(const EGLSurface &surface) +{ + return eglMakeCurrent(eglDisplay(), surface, surface, context()) == EGL_TRUE; +} + +/************************************************ + * EglTexture + ************************************************/ + +EglTexture::EglTexture(KWin::SceneOpenGLTexture *texture, KWin::EglOnXBackend *backend) + : AbstractEglTexture(texture, backend) + , m_backend(backend) +{ +} + +EglTexture::~EglTexture() = default; + +bool EglTexture::loadTexture(WindowPixmap *pixmap) +{ + // first try the Wayland enabled loading + if (AbstractEglTexture::loadTexture(pixmap)) { + return true; + } + // did not succeed, try on X11 + return loadTexture(pixmap->pixmap(), pixmap->toplevel()->bufferGeometry().size()); +} + +bool EglTexture::loadTexture(xcb_pixmap_t pix, const QSize &size) +{ + if (!m_backend->isX11TextureFromPixmapSupported()) { + return false; + } + + if (pix == XCB_NONE) + return false; + + glGenTextures(1, &m_texture); + auto q = texture(); + q->setWrapMode(GL_CLAMP_TO_EDGE); + q->setFilter(GL_LINEAR); + q->bind(); + const EGLint attribs[] = { + EGL_IMAGE_PRESERVED_KHR, EGL_TRUE, + EGL_NONE + }; + setImage(eglCreateImageKHR(m_backend->eglDisplay(), EGL_NO_CONTEXT, EGL_NATIVE_PIXMAP_KHR, + (EGLClientBuffer)pix, attribs)); + + if (EGL_NO_IMAGE_KHR == image()) { + qCDebug(KWIN_CORE) << "failed to create egl image"; + q->unbind(); + q->discard(); + return false; + } + glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, (GLeglImageOES)image()); + q->unbind(); + q->setYInverted(true); + m_size = size; + updateMatrix(); + return true; +} + +void KWin::EglTexture::onDamage() +{ + if (options->isGlStrictBinding()) { + // This is just implemented to be consistent with + // the example in mesa/demos/src/egl/opengles1/texture_from_pixmap.c + eglWaitNative(EGL_CORE_NATIVE_ENGINE); + glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, (GLeglImageOES) image()); + } + GLTexturePrivate::onDamage(); +} + +} // namespace diff --git a/plugins/platforms/x11/common/eglonxbackend.h b/plugins/platforms/x11/common/eglonxbackend.h new file mode 100644 index 0000000..34cc1f3 --- /dev/null +++ b/plugins/platforms/x11/common/eglonxbackend.h @@ -0,0 +1,97 @@ +/* + 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 +*/ +#ifndef KWIN_EGL_ON_X_BACKEND_H +#define KWIN_EGL_ON_X_BACKEND_H +#include "abstract_egl_backend.h" +#include "swap_profiler.h" + +#include + +namespace KWin +{ + +/** + * @brief OpenGL Backend using Egl windowing system over an X overlay window. + */ +class KWIN_EXPORT EglOnXBackend : public AbstractEglBackend +{ +public: + EglOnXBackend(Display *display); + explicit EglOnXBackend(xcb_connection_t *connection, Display *display, xcb_window_t rootWindow, int screenNumber, xcb_window_t renderingWindow); + ~EglOnXBackend() override; + void screenGeometryChanged(const QSize &size) override; + SceneOpenGLTexturePrivate *createBackendTexture(SceneOpenGLTexture *texture) override; + QRegion prepareRenderingFrame() override; + void endRenderingFrame(const QRegion &damage, const QRegion &damagedRegion) override; + OverlayWindow* overlayWindow() const override; + bool usesOverlayWindow() const override; + void init() override; + + bool isX11TextureFromPixmapSupported() const { + return m_x11TextureFromPixmapSupported; + } + +protected: + void present() override; + void presentSurface(EGLSurface surface, const QRegion &damage, const QRect &screenGeometry); + virtual bool createSurfaces(); + EGLSurface createSurface(xcb_window_t window); + void setHavePlatformBase(bool have) { + m_havePlatformBase = have; + } + bool havePlatformBase() const { + return m_havePlatformBase; + } + bool makeContextCurrent(const EGLSurface &surface); + + void setX11TextureFromPixmapSupported(bool set) { + m_x11TextureFromPixmapSupported = set; + } + +private: + bool initBufferConfigs(); + bool initRenderingContext(); + /** + * @brief The OverlayWindow used by this Backend. + */ + OverlayWindow *m_overlayWindow; + int surfaceHasSubPost; + int m_bufferAge; + bool m_usesOverlayWindow; + xcb_connection_t *m_connection; + Display *m_x11Display; + xcb_window_t m_rootWindow; + int m_x11ScreenNumber; + xcb_window_t m_renderingWindow = XCB_WINDOW_NONE; + bool m_havePlatformBase = false; + bool m_x11TextureFromPixmapSupported = true; + SwapProfiler m_swapProfiler; + friend class EglTexture; +}; + +/** + * @brief Texture using an EGLImageKHR. + */ +class EglTexture : public AbstractEglTexture +{ +public: + ~EglTexture() override; + void onDamage() override; + bool loadTexture(WindowPixmap *pixmap) override; + +private: + bool loadTexture(xcb_pixmap_t pix, const QSize &size); + friend class EglOnXBackend; + EglTexture(SceneOpenGLTexture *texture, EglOnXBackend *backend); + EglOnXBackend *m_backend; +}; + +} // namespace + +#endif // KWIN_EGL_ON_X_BACKEND_H diff --git a/plugins/platforms/x11/common/ge_event_mem_mover.h b/plugins/platforms/x11/common/ge_event_mem_mover.h new file mode 100644 index 0000000..9540d55 --- /dev/null +++ b/plugins/platforms/x11/common/ge_event_mem_mover.h @@ -0,0 +1,44 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include + +#include + +namespace KWin +{ + +class GeEventMemMover +{ +public: + GeEventMemMover(xcb_generic_event_t *event) + : m_event(reinterpret_cast(event)) + { + // xcb event structs contain stuff that wasn't on the wire, the full_sequence field + // adds an extra 4 bytes and generic events cookie data is on the wire right after the standard 32 bytes. + // Move this data back to have the same layout in memory as it was on the wire + // and allow casting, overwriting the full_sequence field. + memmove((char*) m_event + 32, (char*) m_event + 36, m_event->length * 4); + } + ~GeEventMemMover() + { + // move memory layout back, so that Qt can do the same without breaking + memmove((char*) m_event + 36, (char *) m_event + 32, m_event->length * 4); + } + + xcb_ge_generic_event_t *operator->() const { + return m_event; + } + +private: + xcb_ge_generic_event_t *m_event; +}; + +} diff --git a/plugins/platforms/x11/standalone/CMakeLists.txt b/plugins/platforms/x11/standalone/CMakeLists.txt new file mode 100644 index 0000000..e9dce7a --- /dev/null +++ b/plugins/platforms/x11/standalone/CMakeLists.txt @@ -0,0 +1,44 @@ +set(X11PLATFORM_SOURCES + edge.cpp + effects_mouse_interception_x11_filter.cpp + effects_x11.cpp + logging.cpp + non_composited_outline.cpp + overlaywindow_x11.cpp + screenedges_filter.cpp + screens_xrandr.cpp + windowselector.cpp + x11_decoration_renderer.cpp + x11_output.cpp + x11_platform.cpp + x11cursor.cpp + xfixes_cursor_event_filter.cpp +) + +if (X11_Xinput_FOUND) + set(X11PLATFORM_SOURCES ${X11PLATFORM_SOURCES} xinputintegration.cpp) +endif() + +if (HAVE_EPOXY_GLX) + set(X11PLATFORM_SOURCES ${X11PLATFORM_SOURCES} glxbackend.cpp glx_context_attribute_builder.cpp) +endif() + +include_directories(${CMAKE_SOURCE_DIR}/platformsupport/scenes/opengl) + +add_library(KWinX11Platform MODULE ${X11PLATFORM_SOURCES}) +set_target_properties(KWinX11Platform PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/org.kde.kwin.platforms/") +target_link_libraries(KWinX11Platform eglx11common kwin kwinxrenderutils SceneOpenGLBackend Qt5::X11Extras XCB::CURSOR KF5::Crash ) +if (X11_Xinput_FOUND) + target_link_libraries(KWinX11Platform ${X11_Xinput_LIB}) +endif() + +if (HAVE_DL_LIBRARY) + target_link_libraries(KWinX11Platform ${DL_LIBRARY}) +endif() + +install( + TARGETS + KWinX11Platform + DESTINATION + ${PLUGIN_INSTALL_DIR}/org.kde.kwin.platforms/ +) diff --git a/plugins/platforms/x11/standalone/edge.cpp b/plugins/platforms/x11/standalone/edge.cpp new file mode 100644 index 0000000..232e662 --- /dev/null +++ b/plugins/platforms/x11/standalone/edge.cpp @@ -0,0 +1,133 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2011 Arthur Arlt + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + Since the functionality provided in this class has been moved from + class Workspace, it is not clear who exactly has written the code. + The list below contains the copyright holders of the class Workspace. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + SPDX-FileCopyrightText: 2009 Lucas Murray + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "edge.h" +#include "atoms.h" +#include "cursor.h" + +namespace KWin +{ + +WindowBasedEdge::WindowBasedEdge(ScreenEdges *parent) + : Edge(parent) + , m_window(XCB_WINDOW_NONE) + , m_approachWindow(XCB_WINDOW_NONE) +{ +} + +WindowBasedEdge::~WindowBasedEdge() +{ +} + +void WindowBasedEdge::doActivate() +{ + createWindow(); + createApproachWindow(); + doUpdateBlocking(); +} + +void WindowBasedEdge::doDeactivate() +{ + m_window.reset(); + m_approachWindow.reset(); +} + +void WindowBasedEdge::createWindow() +{ + if (m_window.isValid()) { + return; + } + const uint32_t mask = XCB_CW_OVERRIDE_REDIRECT | XCB_CW_EVENT_MASK; + const uint32_t values[] = { + true, + XCB_EVENT_MASK_ENTER_WINDOW | XCB_EVENT_MASK_LEAVE_WINDOW | XCB_EVENT_MASK_POINTER_MOTION + }; + m_window.create(geometry(), XCB_WINDOW_CLASS_INPUT_ONLY, mask, values); + m_window.map(); + // Set XdndAware on the windows, so that DND enter events are received (#86998) + xcb_atom_t version = 4; // XDND version + xcb_change_property(connection(), XCB_PROP_MODE_REPLACE, m_window, + atoms->xdnd_aware, XCB_ATOM_ATOM, 32, 1, (unsigned char*)(&version)); +} + +void WindowBasedEdge::createApproachWindow() +{ + if (!activatesForPointer()) { + return; + } + if (m_approachWindow.isValid()) { + return; + } + if (!approachGeometry().isValid()) { + return; + } + const uint32_t mask = XCB_CW_OVERRIDE_REDIRECT | XCB_CW_EVENT_MASK; + const uint32_t values[] = { + true, + XCB_EVENT_MASK_ENTER_WINDOW | XCB_EVENT_MASK_LEAVE_WINDOW | XCB_EVENT_MASK_POINTER_MOTION + }; + m_approachWindow.create(approachGeometry(), XCB_WINDOW_CLASS_INPUT_ONLY, mask, values); + m_approachWindow.map(); +} + +void WindowBasedEdge::doGeometryUpdate() +{ + m_window.setGeometry(geometry()); + if (m_approachWindow.isValid()) { + m_approachWindow.setGeometry(approachGeometry()); + } +} + +void WindowBasedEdge::doStartApproaching() +{ + if (!activatesForPointer()) { + return; + } + m_approachWindow.unmap(); + Cursor *cursor = Cursors::self()->mouse(); +#ifndef KWIN_UNIT_TEST + m_cursorPollingConnection = connect(cursor, &Cursor::posChanged, this, &WindowBasedEdge::updateApproaching); +#endif + cursor->startMousePolling(); +} + +void WindowBasedEdge::doStopApproaching() +{ + if (!m_cursorPollingConnection) { + return; + } + disconnect(m_cursorPollingConnection); + m_cursorPollingConnection = QMetaObject::Connection(); + Cursors::self()->mouse()->stopMousePolling(); + m_approachWindow.map(); +} + +void WindowBasedEdge::doUpdateBlocking() +{ + if (!isReserved()) { + return; + } + if (isBlocked()) { + m_window.unmap(); + m_approachWindow.unmap(); + } else { + m_window.map(); + m_approachWindow.map(); + } +} + +} diff --git a/plugins/platforms/x11/standalone/edge.h b/plugins/platforms/x11/standalone/edge.h new file mode 100644 index 0000000..ae1d83d --- /dev/null +++ b/plugins/platforms/x11/standalone/edge.h @@ -0,0 +1,68 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2011 Arthur Arlt + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + Since the functionality provided in this class has been moved from + class Workspace, it is not clear who exactly has written the code. + The list below contains the copyright holders of the class Workspace. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + SPDX-FileCopyrightText: 2009 Lucas Murray + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_EDGE_H +#define KWIN_EDGE_H +#include "screenedge.h" +#include "xcbutils.h" + +namespace KWin +{ + +class WindowBasedEdge : public Edge +{ + Q_OBJECT +public: + explicit WindowBasedEdge(ScreenEdges *parent); + ~WindowBasedEdge() override; + + quint32 window() const override; + /** + * The approach window is a special window to notice when get close to the screen border but + * not yet triggering the border. + */ + quint32 approachWindow() const override; + +protected: + void doGeometryUpdate() override; + void doActivate() override; + void doDeactivate() override; + void doStartApproaching() override; + void doStopApproaching() override; + void doUpdateBlocking() override; + +private: + void createWindow(); + void createApproachWindow(); + Xcb::Window m_window; + Xcb::Window m_approachWindow; + QMetaObject::Connection m_cursorPollingConnection; +}; + +inline quint32 WindowBasedEdge::window() const +{ + return m_window; +} + +inline quint32 WindowBasedEdge::approachWindow() const +{ + return m_approachWindow; +} + +} + +#endif diff --git a/plugins/platforms/x11/standalone/effects_mouse_interception_x11_filter.cpp b/plugins/platforms/x11/standalone/effects_mouse_interception_x11_filter.cpp new file mode 100644 index 0000000..0f4b511 --- /dev/null +++ b/plugins/platforms/x11/standalone/effects_mouse_interception_x11_filter.cpp @@ -0,0 +1,96 @@ +/* + 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 "effects_mouse_interception_x11_filter.h" +#include "effects.h" +#include "utils.h" + +#include + +namespace KWin +{ + +EffectsMouseInterceptionX11Filter::EffectsMouseInterceptionX11Filter(xcb_window_t window, EffectsHandlerImpl *effects) + : X11EventFilter(QVector{XCB_BUTTON_PRESS, XCB_BUTTON_RELEASE, XCB_MOTION_NOTIFY}) + , m_effects(effects) + , m_window(window) +{ +} + +bool EffectsMouseInterceptionX11Filter::event(xcb_generic_event_t *event) +{ + const uint8_t eventType = event->response_type & ~0x80; + if (eventType == XCB_BUTTON_PRESS || eventType == XCB_BUTTON_RELEASE) { + auto *me = reinterpret_cast(event); + if (m_window == me->event) { + const bool isWheel = me->detail >= 4 && me->detail <= 7; + if (isWheel) { + if (eventType != XCB_BUTTON_PRESS) { + return false; + } + QPoint angleDelta; + switch (me->detail) { + case 4: + angleDelta.setY(120); + break; + case 5: + angleDelta.setY(-120); + break; + case 6: + angleDelta.setX(120); + break; + case 7: + angleDelta.setX(-120); + break; + } + + const Qt::MouseButtons buttons = x11ToQtMouseButtons(me->state); + const Qt::KeyboardModifiers modifiers = x11ToQtKeyboardModifiers(me->state); + + if (modifiers & Qt::AltModifier) { + int x = angleDelta.x(); + int y = angleDelta.y(); + + angleDelta.setX(y); + angleDelta.setY(x); + // After Qt > 5.14 simplify to + // angleDelta = angleDelta.transposed(); + } + + if (angleDelta.y()) { + QWheelEvent ev(QPoint(me->event_x, me->event_y), angleDelta.y(), buttons, modifiers, Qt::Vertical); + return m_effects->checkInputWindowEvent(&ev); + } else if (angleDelta.x()) { + QWheelEvent ev(QPoint(me->event_x, me->event_y), angleDelta.x(), buttons, modifiers, Qt::Horizontal); + return m_effects->checkInputWindowEvent(&ev); + } + } + const Qt::MouseButton button = x11ToQtMouseButton(me->detail); + Qt::MouseButtons buttons = x11ToQtMouseButtons(me->state); + const QEvent::Type type = (eventType == XCB_BUTTON_PRESS) ? QEvent::MouseButtonPress : QEvent::MouseButtonRelease; + if (type == QEvent::MouseButtonPress) { + buttons |= button; + } else { + buttons &= ~button; + } + QMouseEvent ev(type, QPoint(me->event_x, me->event_y), QPoint(me->root_x, me->root_y), + button, buttons, x11ToQtKeyboardModifiers(me->state)); + return m_effects->checkInputWindowEvent(&ev); + } + } else if (eventType == XCB_MOTION_NOTIFY) { + const auto *me = reinterpret_cast(event); + if (m_window == me->event) { + QMouseEvent ev(QEvent::MouseMove, QPoint(me->event_x, me->event_y), QPoint(me->root_x, me->root_y), + Qt::NoButton, x11ToQtMouseButtons(me->state), x11ToQtKeyboardModifiers(me->state)); + return m_effects->checkInputWindowEvent(&ev); + } + } + return false; +} + +} diff --git a/plugins/platforms/x11/standalone/effects_mouse_interception_x11_filter.h b/plugins/platforms/x11/standalone/effects_mouse_interception_x11_filter.h new file mode 100644 index 0000000..695a6d7 --- /dev/null +++ b/plugins/platforms/x11/standalone/effects_mouse_interception_x11_filter.h @@ -0,0 +1,32 @@ +/* + 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 +*/ +#ifndef KWIN_EFFECTS_MOUSE_INTERCEPTION_X11_FILTER_H +#define KWIN_EFFECTS_MOUSE_INTERCEPTION_X11_FILTER_H + +#include "x11eventfilter.h" + +namespace KWin +{ +class EffectsHandlerImpl; + +class EffectsMouseInterceptionX11Filter : public X11EventFilter +{ +public: + explicit EffectsMouseInterceptionX11Filter(xcb_window_t window, EffectsHandlerImpl *effects); + + bool event(xcb_generic_event_t *event) override; + +private: + EffectsHandlerImpl *m_effects; + xcb_window_t m_window; +}; + +} + +#endif diff --git a/plugins/platforms/x11/standalone/effects_x11.cpp b/plugins/platforms/x11/standalone/effects_x11.cpp new file mode 100644 index 0000000..fb15823 --- /dev/null +++ b/plugins/platforms/x11/standalone/effects_x11.cpp @@ -0,0 +1,113 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2010, 2011, 2017 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "effects_x11.h" +#include "effects_mouse_interception_x11_filter.h" +#include "cursor.h" +#include "screenedge.h" +#include "screens.h" +#include "utils.h" +#include "workspace.h" + +#include + +namespace KWin +{ + +EffectsHandlerImplX11::EffectsHandlerImplX11(Compositor *compositor, Scene *scene) + : EffectsHandlerImpl(compositor, scene) +{ + connect(this, &EffectsHandlerImpl::screenGeometryChanged, this, + [this] (const QSize &size) { + if (m_mouseInterceptionWindow.isValid()) { + m_mouseInterceptionWindow.setGeometry(QRect(0, 0, size.width(), size.height())); + } + } + ); +} + +EffectsHandlerImplX11::~EffectsHandlerImplX11() +{ + // EffectsHandlerImpl tries to unload all effects when it's destroyed. + // The routine that unloads effects makes some calls (indirectly) to + // doUngrabKeyboard and doStopMouseInterception, which are virtual. + // Given that any call to a virtual function in the destructor of a base + // class will never go to a derived class, we have to unload effects + // here. Yeah, this is quite a bit ugly but it's fine; someday, X11 + // will be dead (or not?). + unloadAllEffects(); +} + +bool EffectsHandlerImplX11::doGrabKeyboard() +{ + bool ret = grabXKeyboard(); + if (!ret) + return false; + // Workaround for Qt 5.9 regression introduced with 2b34aefcf02f09253473b096eb4faffd3e62b5f4 + // we no longer get any events for the root window, one needs to call winId() on the desktop window + // TODO: change effects event handling to create the appropriate QKeyEvent without relying on Qt + // as it's done already in the Wayland case. + qApp->desktop()->winId(); + return ret; +} + +void EffectsHandlerImplX11::doUngrabKeyboard() +{ + ungrabXKeyboard(); +} + +void EffectsHandlerImplX11::doStartMouseInterception(Qt::CursorShape shape) +{ + // NOTE: it is intended to not perform an XPointerGrab on X11. See documentation in kwineffects.h + // The mouse grab is implemented by using a full screen input only window + if (!m_mouseInterceptionWindow.isValid()) { + const QSize &s = screens()->size(); + const QRect geo(0, 0, s.width(), s.height()); + const uint32_t mask = XCB_CW_OVERRIDE_REDIRECT | XCB_CW_EVENT_MASK; + const uint32_t values[] = { + true, + XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_BUTTON_RELEASE | XCB_EVENT_MASK_POINTER_MOTION + }; + m_mouseInterceptionWindow.reset(Xcb::createInputWindow(geo, mask, values)); + defineCursor(shape); + } else { + defineCursor(shape); + } + m_mouseInterceptionWindow.map(); + m_mouseInterceptionWindow.raise(); + m_x11MouseInterception = std::make_unique(m_mouseInterceptionWindow, this); + // Raise electric border windows above the input windows + // so they can still be triggered. + ScreenEdges::self()->ensureOnTop(); +} + +void EffectsHandlerImplX11::doStopMouseInterception() +{ + m_mouseInterceptionWindow.unmap(); + m_x11MouseInterception.reset(); + Workspace::self()->stackScreenEdgesUnderOverrideRedirect(); +} + +void EffectsHandlerImplX11::defineCursor(Qt::CursorShape shape) +{ + const xcb_cursor_t c = Cursors::self()->mouse()->x11Cursor(shape); + if (c != XCB_CURSOR_NONE) { + m_mouseInterceptionWindow.defineCursor(c); + } +} + +void EffectsHandlerImplX11::doCheckInputWindowStacking() +{ + m_mouseInterceptionWindow.raise(); + // Raise electric border windows above the input windows + // so they can still be triggered. TODO: Do both at once. + ScreenEdges::self()->ensureOnTop(); +} + +} diff --git a/plugins/platforms/x11/standalone/effects_x11.h b/plugins/platforms/x11/standalone/effects_x11.h new file mode 100644 index 0000000..7e44224 --- /dev/null +++ b/plugins/platforms/x11/standalone/effects_x11.h @@ -0,0 +1,47 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2010, 2011, 2017 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_EFFECTS_X11_H +#define KWIN_EFFECTS_X11_H + +#include "effects.h" +#include "xcbutils.h" + +#include + +namespace KWin +{ +class EffectsMouseInterceptionX11Filter; + +class EffectsHandlerImplX11 : public EffectsHandlerImpl +{ + Q_OBJECT +public: + explicit EffectsHandlerImplX11(Compositor *compositor, Scene *scene); + ~EffectsHandlerImplX11() override; + + void defineCursor(Qt::CursorShape shape) override; + +protected: + bool doGrabKeyboard() override; + void doUngrabKeyboard() override; + + void doStartMouseInterception(Qt::CursorShape shape) override; + void doStopMouseInterception() override; + + void doCheckInputWindowStacking() override; + +private: + Xcb::Window m_mouseInterceptionWindow; + std::unique_ptr m_x11MouseInterception; +}; + +} + +#endif diff --git a/plugins/platforms/x11/standalone/glx_context_attribute_builder.cpp b/plugins/platforms/x11/standalone/glx_context_attribute_builder.cpp new file mode 100644 index 0000000..163fd78 --- /dev/null +++ b/plugins/platforms/x11/standalone/glx_context_attribute_builder.cpp @@ -0,0 +1,42 @@ +/* + 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 "glx_context_attribute_builder.h" +#include + +#ifndef GLX_GENERATE_RESET_ON_VIDEO_MEMORY_PURGE_NV +#define GLX_GENERATE_RESET_ON_VIDEO_MEMORY_PURGE_NV 0x20F7 +#endif + +namespace KWin +{ + +std::vector GlxContextAttributeBuilder::build() const +{ + std::vector attribs; + if (isVersionRequested()) { + attribs.emplace_back(GLX_CONTEXT_MAJOR_VERSION_ARB); + attribs.emplace_back(majorVersion()); + attribs.emplace_back(GLX_CONTEXT_MINOR_VERSION_ARB); + attribs.emplace_back(minorVersion()); + } + if (isRobust()) { + attribs.emplace_back(GLX_CONTEXT_FLAGS_ARB); + attribs.emplace_back(GLX_CONTEXT_ROBUST_ACCESS_BIT_ARB); + attribs.emplace_back(GLX_CONTEXT_RESET_NOTIFICATION_STRATEGY_ARB); + attribs.emplace_back(GLX_LOSE_CONTEXT_ON_RESET_ARB); + if (isResetOnVideoMemoryPurge()) { + attribs.emplace_back(GLX_GENERATE_RESET_ON_VIDEO_MEMORY_PURGE_NV); + attribs.emplace_back(GL_TRUE); + } + } + attribs.emplace_back(0); + return attribs; +} + +} diff --git a/plugins/platforms/x11/standalone/glx_context_attribute_builder.h b/plugins/platforms/x11/standalone/glx_context_attribute_builder.h new file mode 100644 index 0000000..460f519 --- /dev/null +++ b/plugins/platforms/x11/standalone/glx_context_attribute_builder.h @@ -0,0 +1,21 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include "abstract_opengl_context_attribute_builder.h" + +namespace KWin +{ + +class GlxContextAttributeBuilder : public AbstractOpenGLContextAttributeBuilder +{ +public: + std::vector build() const override; +}; + +} diff --git a/plugins/platforms/x11/standalone/glxbackend.cpp b/plugins/platforms/x11/standalone/glxbackend.cpp new file mode 100644 index 0000000..1096199 --- /dev/null +++ b/plugins/platforms/x11/standalone/glxbackend.cpp @@ -0,0 +1,961 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + Based on glcompmgr code by Felix Bellaby. + Using code from Compiz and Beryl. + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +// own +#include "glxbackend.h" +#include "logging.h" +#include "glx_context_attribute_builder.h" +// kwin +#include "options.h" +#include "overlaywindow.h" +#include "composite.h" +#include "platform.h" +#include "scene.h" +#include "screens.h" +#include "xcbutils.h" +#include "texture.h" +// kwin libs +#include +#include +#include +#include +// Qt +#include +#include +#include +#include +// system +#include + +#include +#include +#if HAVE_DL_LIBRARY +#include +#endif + +#ifndef XCB_GLX_BUFFER_SWAP_COMPLETE +#define XCB_GLX_BUFFER_SWAP_COMPLETE 1 +typedef struct xcb_glx_buffer_swap_complete_event_t { + uint8_t response_type; /**< */ + uint8_t pad0; /**< */ + uint16_t sequence; /**< */ + uint16_t event_type; /**< */ + uint8_t pad1[2]; /**< */ + xcb_glx_drawable_t drawable; /**< */ + uint32_t ust_hi; /**< */ + uint32_t ust_lo; /**< */ + uint32_t msc_hi; /**< */ + uint32_t msc_lo; /**< */ + uint32_t sbc; /**< */ +} xcb_glx_buffer_swap_complete_event_t; +#endif + +#include +#include + +namespace KWin +{ + +SwapEventFilter::SwapEventFilter(xcb_drawable_t drawable, xcb_glx_drawable_t glxDrawable) + : X11EventFilter(Xcb::Extensions::self()->glxEventBase() + XCB_GLX_BUFFER_SWAP_COMPLETE), + m_drawable(drawable), + m_glxDrawable(glxDrawable) +{ +} + +bool SwapEventFilter::event(xcb_generic_event_t *event) +{ + xcb_glx_buffer_swap_complete_event_t *ev = + reinterpret_cast(event); + + // The drawable field is the X drawable when the event was synthesized + // by a WireToEvent handler, and the GLX drawable when the event was + // received over the wire + if (ev->drawable == m_drawable || ev->drawable == m_glxDrawable) { + Compositor::self()->bufferSwapComplete(); + return true; + } + + return false; +} + + +// ----------------------------------------------------------------------- + + + +GlxBackend::GlxBackend(Display *display) + : OpenGLBackend() + , m_overlayWindow(kwinApp()->platform()->createOverlayWindow()) + , window(None) + , fbconfig(nullptr) + , glxWindow(None) + , ctx(nullptr) + , m_bufferAge(0) + , haveSwapInterval(false) + , m_x11Display(display) +{ + // Ensures calls to glXSwapBuffers will always block until the next + // retrace when using the proprietary NVIDIA driver. This must be + // set before libGL.so is loaded. + setenv("__GL_MaxFramesAllowed", "1", true); + + // Force initialization of GLX integration in the Qt's xcb backend + // to make it call XESetWireToEvent callbacks, which is required + // by Mesa when using DRI2. + QOpenGLContext::supportsThreadedOpenGL(); +} + +static bool gs_tripleBufferUndetected = true; +static bool gs_tripleBufferNeedsDetection = false; + +GlxBackend::~GlxBackend() +{ + if (isFailed()) { + m_overlayWindow->destroy(); + } + // TODO: cleanup in error case + // do cleanup after initBuffer() + cleanupGL(); + doneCurrent(); + EffectQuickView::setShareContext(nullptr); + + gs_tripleBufferUndetected = true; + gs_tripleBufferNeedsDetection = false; + + if (ctx) + glXDestroyContext(display(), ctx); + + if (glxWindow) + glXDestroyWindow(display(), glxWindow); + + if (window) + XDestroyWindow(display(), window); + + qDeleteAll(m_fbconfigHash); + m_fbconfigHash.clear(); + + overlayWindow()->destroy(); + delete m_overlayWindow; +} + +typedef void (*glXFuncPtr)(); + +static glXFuncPtr getProcAddress(const char* name) +{ + glXFuncPtr ret = nullptr; +#if HAVE_EPOXY_GLX + ret = glXGetProcAddress((const GLubyte*) name); +#endif +#if HAVE_DL_LIBRARY + if (ret == nullptr) + ret = (glXFuncPtr) dlsym(RTLD_DEFAULT, name); +#endif + return ret; +} +glXSwapIntervalMESA_func glXSwapIntervalMESA; + +void GlxBackend::init() +{ + // Require at least GLX 1.3 + if (!checkVersion()) { + setFailed(QStringLiteral("Requires at least GLX 1.3")); + return; + } + + initExtensions(); + + // resolve glXSwapIntervalMESA if available + if (hasExtension(QByteArrayLiteral("GLX_MESA_swap_control"))) { + glXSwapIntervalMESA = (glXSwapIntervalMESA_func) getProcAddress("glXSwapIntervalMESA"); + } else { + glXSwapIntervalMESA = nullptr; + } + + initVisualDepthHashTable(); + + if (!initBuffer()) { + setFailed(QStringLiteral("Could not initialize the buffer")); + return; + } + + if (!initRenderingContext()) { + setFailed(QStringLiteral("Could not initialize rendering context")); + return; + } + + // Initialize OpenGL + GLPlatform *glPlatform = GLPlatform::instance(); + glPlatform->detect(GlxPlatformInterface); + options->setGlPreferBufferSwap(options->glPreferBufferSwap()); // resolve autosetting + if (options->glPreferBufferSwap() == Options::AutoSwapStrategy) + options->setGlPreferBufferSwap('e'); // for unknown drivers - should not happen + glPlatform->printResults(); + initGL(&getProcAddress); + + // Check whether certain features are supported + m_haveMESACopySubBuffer = hasExtension(QByteArrayLiteral("GLX_MESA_copy_sub_buffer")); + m_haveMESASwapControl = hasExtension(QByteArrayLiteral("GLX_MESA_swap_control")); + m_haveEXTSwapControl = hasExtension(QByteArrayLiteral("GLX_EXT_swap_control")); + m_haveSGISwapControl = hasExtension(QByteArrayLiteral("GLX_SGI_swap_control")); + // only enable Intel swap event if env variable is set, see BUG 342582 + m_haveINTELSwapEvent = hasExtension(QByteArrayLiteral("GLX_INTEL_swap_event")) + && qgetenv("KWIN_USE_INTEL_SWAP_EVENT") == QByteArrayLiteral("1"); + + if (m_haveINTELSwapEvent) { + m_swapEventFilter = std::make_unique(window, glxWindow); + glXSelectEvent(display(), glxWindow, GLX_BUFFER_SWAP_COMPLETE_INTEL_MASK); + } + + haveSwapInterval = m_haveMESASwapControl || m_haveEXTSwapControl || m_haveSGISwapControl; + + setSupportsBufferAge(false); + + if (hasExtension(QByteArrayLiteral("GLX_EXT_buffer_age"))) { + const QByteArray useBufferAge = qgetenv("KWIN_USE_BUFFER_AGE"); + + if (useBufferAge != "0") + setSupportsBufferAge(true); + } + + setSyncsToVBlank(false); + setBlocksForRetrace(false); + haveWaitSync = false; + gs_tripleBufferNeedsDetection = false; + m_swapProfiler.init(); + const bool wantSync = options->glPreferBufferSwap() != Options::NoSwapEncourage; + if (wantSync && glXIsDirect(display(), ctx)) { + if (haveSwapInterval) { // glXSwapInterval is preferred being more reliable + setSwapInterval(1); + setSyncsToVBlank(true); + const QByteArray tripleBuffer = qgetenv("KWIN_TRIPLE_BUFFER"); + if (!tripleBuffer.isEmpty()) { + setBlocksForRetrace(qstrcmp(tripleBuffer, "0") == 0); + gs_tripleBufferUndetected = false; + } + gs_tripleBufferNeedsDetection = gs_tripleBufferUndetected; + } else if (hasExtension(QByteArrayLiteral("GLX_SGI_video_sync"))) { + unsigned int sync; + if (glXGetVideoSyncSGI(&sync) == 0 && glXWaitVideoSyncSGI(1, 0, &sync) == 0) { + setSyncsToVBlank(true); + setBlocksForRetrace(true); + haveWaitSync = true; + } else + qCWarning(KWIN_X11STANDALONE) << "NO VSYNC! glXSwapInterval is not supported, glXWaitVideoSync is supported but broken"; + } else + qCWarning(KWIN_X11STANDALONE) << "NO VSYNC! neither glSwapInterval nor glXWaitVideoSync are supported"; + } else { + // disable v-sync (if possible) + setSwapInterval(0); + } + if (glPlatform->isVirtualBox()) { + // VirtualBox does not support glxQueryDrawable + // this should actually be in kwinglutils_funcs, but QueryDrawable seems not to be provided by an extension + // and the GLPlatform has not been initialized at the moment when initGLX() is called. + glXQueryDrawable = nullptr; + } + + setIsDirectRendering(bool(glXIsDirect(display(), ctx))); + + qCDebug(KWIN_X11STANDALONE) << "Direct rendering:" << isDirectRendering(); +} + +bool GlxBackend::checkVersion() +{ + int major, minor; + glXQueryVersion(display(), &major, &minor); + return kVersionNumber(major, minor) >= kVersionNumber(1, 3); +} + +void GlxBackend::initExtensions() +{ + const QByteArray string = (const char *) glXQueryExtensionsString(display(), QX11Info::appScreen()); + setExtensions(string.split(' ')); +} + +bool GlxBackend::initRenderingContext() +{ + const bool direct = true; + + // Use glXCreateContextAttribsARB() when it's available + if (hasExtension(QByteArrayLiteral("GLX_ARB_create_context"))) { + const bool have_robustness = hasExtension(QByteArrayLiteral("GLX_ARB_create_context_robustness")); + const bool haveVideoMemoryPurge = hasExtension(QByteArrayLiteral("GLX_NV_robustness_video_memory_purge")); + + std::vector candidates; + if (options->glCoreProfile()) { + if (have_robustness) { + if (haveVideoMemoryPurge) { + GlxContextAttributeBuilder purgeMemoryCore; + purgeMemoryCore.setVersion(3, 1); + purgeMemoryCore.setRobust(true); + purgeMemoryCore.setResetOnVideoMemoryPurge(true); + candidates.emplace_back(std::move(purgeMemoryCore)); + } + GlxContextAttributeBuilder robustCore; + robustCore.setVersion(3, 1); + robustCore.setRobust(true); + candidates.emplace_back(std::move(robustCore)); + } + GlxContextAttributeBuilder core; + core.setVersion(3, 1); + candidates.emplace_back(std::move(core)); + } else { + if (have_robustness) { + if (haveVideoMemoryPurge) { + GlxContextAttributeBuilder purgeMemoryLegacy; + purgeMemoryLegacy.setRobust(true); + purgeMemoryLegacy.setResetOnVideoMemoryPurge(true); + candidates.emplace_back(std::move(purgeMemoryLegacy)); + } + GlxContextAttributeBuilder robustLegacy; + robustLegacy.setRobust(true); + candidates.emplace_back(std::move(robustLegacy)); + } + GlxContextAttributeBuilder legacy; + legacy.setVersion(2, 1); + candidates.emplace_back(std::move(legacy)); + } + for (auto it = candidates.begin(); it != candidates.end(); it++) { + const auto attribs = it->build(); + ctx = glXCreateContextAttribsARB(display(), fbconfig, nullptr, true, attribs.data()); + if (ctx) { + qCDebug(KWIN_X11STANDALONE) << "Created GLX context with attributes:" << &(*it); + break; + } + } + } + + if (!ctx) + ctx = glXCreateNewContext(display(), fbconfig, GLX_RGBA_TYPE, nullptr, direct); + + if (!ctx) { + qCDebug(KWIN_X11STANDALONE) << "Failed to create an OpenGL context."; + return false; + } + + if (!glXMakeCurrent(display(), glxWindow, ctx)) { + qCDebug(KWIN_X11STANDALONE) << "Failed to make the OpenGL context current."; + glXDestroyContext(display(), ctx); + ctx = nullptr; + return false; + } + + auto qtContext = new QOpenGLContext; + QGLXNativeContext native(ctx, display()); + qtContext->setNativeHandle(QVariant::fromValue(native)); + qtContext->create(); + EffectQuickView::setShareContext(std::unique_ptr(qtContext)); + + return true; +} + +bool GlxBackend::initBuffer() +{ + if (!initFbConfig()) + return false; + + if (overlayWindow()->create()) { + xcb_connection_t * const c = connection(); + + // Try to create double-buffered window in the overlay + xcb_visualid_t visual; + glXGetFBConfigAttrib(display(), fbconfig, GLX_VISUAL_ID, (int *) &visual); + + if (!visual) { + qCCritical(KWIN_X11STANDALONE) << "The GLXFBConfig does not have an associated X visual"; + return false; + } + + xcb_colormap_t colormap = xcb_generate_id(c); + xcb_create_colormap(c, false, colormap, rootWindow(), visual); + + const QSize size = screens()->size(); + + window = xcb_generate_id(c); + xcb_create_window(c, visualDepth(visual), window, overlayWindow()->window(), + 0, 0, size.width(), size.height(), 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, + visual, XCB_CW_COLORMAP, &colormap); + + glxWindow = glXCreateWindow(display(), fbconfig, window, nullptr); + overlayWindow()->setup(window); + } else { + qCCritical(KWIN_X11STANDALONE) << "Failed to create overlay window"; + return false; + } + + return true; +} + +bool GlxBackend::initFbConfig() +{ + const int attribs[] = { + GLX_RENDER_TYPE, GLX_RGBA_BIT, + GLX_DRAWABLE_TYPE, GLX_WINDOW_BIT, + GLX_RED_SIZE, 1, + GLX_GREEN_SIZE, 1, + GLX_BLUE_SIZE, 1, + GLX_ALPHA_SIZE, 0, + GLX_DEPTH_SIZE, 0, + GLX_STENCIL_SIZE, 0, + GLX_CONFIG_CAVEAT, GLX_NONE, + GLX_DOUBLEBUFFER, true, + 0 + }; + + const int attribs_srgb[] = { + GLX_RENDER_TYPE, GLX_RGBA_BIT, + GLX_DRAWABLE_TYPE, GLX_WINDOW_BIT, + GLX_RED_SIZE, 1, + GLX_GREEN_SIZE, 1, + GLX_BLUE_SIZE, 1, + GLX_ALPHA_SIZE, 0, + GLX_DEPTH_SIZE, 0, + GLX_STENCIL_SIZE, 0, + GLX_CONFIG_CAVEAT, GLX_NONE, + GLX_DOUBLEBUFFER, true, + GLX_FRAMEBUFFER_SRGB_CAPABLE_ARB, true, + 0 + }; + + bool llvmpipe = false; + + // Note that we cannot use GLPlatform::driver() here, because it has not been initialized at this point + if (hasExtension(QByteArrayLiteral("GLX_MESA_query_renderer"))) { + const QByteArray device = glXQueryRendererStringMESA(display(), DefaultScreen(display()), 0, GLX_RENDERER_DEVICE_ID_MESA); + if (device.contains(QByteArrayLiteral("llvmpipe"))) { + llvmpipe = true; + } + } + + // Try to find a double buffered sRGB capable configuration + int count = 0; + GLXFBConfig *configs = nullptr; + + // Don't request an sRGB configuration with LLVMpipe when the default depth is 16. See bug #408594. + if (!llvmpipe || Xcb::defaultDepth() > 16) { + configs = glXChooseFBConfig(display(), DefaultScreen(display()), attribs_srgb, &count); + } + + if (count == 0) { + // Try to find a double buffered non-sRGB capable configuration + configs = glXChooseFBConfig(display(), DefaultScreen(display()), attribs, &count); + } + + struct FBConfig { + GLXFBConfig config; + int depth; + int stencil; + }; + + std::deque candidates; + + for (int i = 0; i < count; i++) { + int depth, stencil; + glXGetFBConfigAttrib(display(), configs[i], GLX_DEPTH_SIZE, &depth); + glXGetFBConfigAttrib(display(), configs[i], GLX_STENCIL_SIZE, &stencil); + + candidates.emplace_back(FBConfig{configs[i], depth, stencil}); + } + + if (count > 0) + XFree(configs); + + std::stable_sort(candidates.begin(), candidates.end(), [](const FBConfig &left, const FBConfig &right) { + if (left.depth < right.depth) + return true; + + if (left.stencil < right.stencil) + return true; + + return false; + }); + + if (candidates.size() > 0) { + fbconfig = candidates.front().config; + + int fbconfig_id, visual_id, red, green, blue, alpha, depth, stencil, srgb; + glXGetFBConfigAttrib(display(), fbconfig, GLX_FBCONFIG_ID, &fbconfig_id); + glXGetFBConfigAttrib(display(), fbconfig, GLX_VISUAL_ID, &visual_id); + glXGetFBConfigAttrib(display(), fbconfig, GLX_RED_SIZE, &red); + glXGetFBConfigAttrib(display(), fbconfig, GLX_GREEN_SIZE, &green); + glXGetFBConfigAttrib(display(), fbconfig, GLX_BLUE_SIZE, &blue); + glXGetFBConfigAttrib(display(), fbconfig, GLX_ALPHA_SIZE, &alpha); + glXGetFBConfigAttrib(display(), fbconfig, GLX_DEPTH_SIZE, &depth); + glXGetFBConfigAttrib(display(), fbconfig, GLX_STENCIL_SIZE, &stencil); + glXGetFBConfigAttrib(display(), fbconfig, GLX_FRAMEBUFFER_SRGB_CAPABLE_ARB, &srgb); + + qCDebug(KWIN_X11STANDALONE, "Choosing GLXFBConfig %#x X visual %#x depth %d RGBA %d:%d:%d:%d ZS %d:%d sRGB: %d", + fbconfig_id, visual_id, visualDepth(visual_id), red, green, blue, alpha, depth, stencil, srgb); + } + + if (fbconfig == nullptr) { + qCCritical(KWIN_X11STANDALONE) << "Failed to find a usable framebuffer configuration"; + return false; + } + + return true; +} + +void GlxBackend::initVisualDepthHashTable() +{ + const xcb_setup_t *setup = xcb_get_setup(connection()); + + for (auto screen = xcb_setup_roots_iterator(setup); screen.rem; xcb_screen_next(&screen)) { + for (auto depth = xcb_screen_allowed_depths_iterator(screen.data); depth.rem; xcb_depth_next(&depth)) { + const int len = xcb_depth_visuals_length(depth.data); + const xcb_visualtype_t *visuals = xcb_depth_visuals(depth.data); + + for (int i = 0; i < len; i++) + m_visualDepthHash.insert(visuals[i].visual_id, depth.data->depth); + } + } +} + +int GlxBackend::visualDepth(xcb_visualid_t visual) const +{ + return m_visualDepthHash.value(visual); +} + +static inline int bitCount(uint32_t mask) +{ +#if defined(__GNUC__) + return __builtin_popcount(mask); +#else + int count = 0; + + while (mask) { + count += (mask & 1); + mask >>= 1; + } + + return count; +#endif +} + +FBConfigInfo *GlxBackend::infoForVisual(xcb_visualid_t visual) +{ + auto it = m_fbconfigHash.constFind(visual); + if (it != m_fbconfigHash.constEnd()) { + return it.value(); + } + + FBConfigInfo *info = new FBConfigInfo; + m_fbconfigHash.insert(visual, info); + info->fbconfig = nullptr; + info->bind_texture_format = 0; + info->texture_targets = 0; + info->y_inverted = 0; + info->mipmap = 0; + + const xcb_render_pictformat_t format = XRenderUtils::findPictFormat(visual); + const xcb_render_directformat_t *direct = XRenderUtils::findPictFormatInfo(format); + + if (!direct) { + qCCritical(KWIN_X11STANDALONE).nospace() << "Could not find a picture format for visual 0x" << Qt::hex << visual; + return info; + } + + const int red_bits = bitCount(direct->red_mask); + const int green_bits = bitCount(direct->green_mask); + const int blue_bits = bitCount(direct->blue_mask); + const int alpha_bits = bitCount(direct->alpha_mask); + + const int depth = visualDepth(visual); + + const auto rgb_sizes = std::tie(red_bits, green_bits, blue_bits); + + const int attribs[] = { + GLX_RENDER_TYPE, GLX_RGBA_BIT, + GLX_DRAWABLE_TYPE, GLX_WINDOW_BIT | GLX_PIXMAP_BIT, + GLX_X_VISUAL_TYPE, GLX_TRUE_COLOR, + GLX_X_RENDERABLE, True, + GLX_CONFIG_CAVEAT, int(GLX_DONT_CARE), // The ARGB32 visual is marked non-conformant in Catalyst + GLX_FRAMEBUFFER_SRGB_CAPABLE_EXT, int(GLX_DONT_CARE), // The ARGB32 visual is marked sRGB capable in mesa/i965 + GLX_BUFFER_SIZE, red_bits + green_bits + blue_bits + alpha_bits, + GLX_RED_SIZE, red_bits, + GLX_GREEN_SIZE, green_bits, + GLX_BLUE_SIZE, blue_bits, + GLX_ALPHA_SIZE, alpha_bits, + GLX_STENCIL_SIZE, 0, + GLX_DEPTH_SIZE, 0, + 0 + }; + + int count = 0; + GLXFBConfig *configs = glXChooseFBConfig(display(), DefaultScreen(display()), attribs, &count); + + if (count < 1) { + qCCritical(KWIN_X11STANDALONE).nospace() << "Could not find a framebuffer configuration for visual 0x" << Qt::hex << visual; + return info; + } + + struct FBConfig { + GLXFBConfig config; + int depth; + int stencil; + int format; + }; + + std::deque candidates; + + for (int i = 0; i < count; i++) { + int red, green, blue; + glXGetFBConfigAttrib(display(), configs[i], GLX_RED_SIZE, &red); + glXGetFBConfigAttrib(display(), configs[i], GLX_GREEN_SIZE, &green); + glXGetFBConfigAttrib(display(), configs[i], GLX_BLUE_SIZE, &blue); + + if (std::tie(red, green, blue) != rgb_sizes) + continue; + + xcb_visualid_t visual; + glXGetFBConfigAttrib(display(), configs[i], GLX_VISUAL_ID, (int *) &visual); + + if (visualDepth(visual) != depth) + continue; + + int bind_rgb, bind_rgba; + glXGetFBConfigAttrib(display(), configs[i], GLX_BIND_TO_TEXTURE_RGBA_EXT, &bind_rgba); + glXGetFBConfigAttrib(display(), configs[i], GLX_BIND_TO_TEXTURE_RGB_EXT, &bind_rgb); + + if (!bind_rgb && !bind_rgba) + continue; + + int depth, stencil; + glXGetFBConfigAttrib(display(), configs[i], GLX_DEPTH_SIZE, &depth); + glXGetFBConfigAttrib(display(), configs[i], GLX_STENCIL_SIZE, &stencil); + + int texture_format; + if (alpha_bits) + texture_format = bind_rgba ? GLX_TEXTURE_FORMAT_RGBA_EXT : GLX_TEXTURE_FORMAT_RGB_EXT; + else + texture_format = bind_rgb ? GLX_TEXTURE_FORMAT_RGB_EXT : GLX_TEXTURE_FORMAT_RGBA_EXT; + + candidates.emplace_back(FBConfig{configs[i], depth, stencil, texture_format}); + } + + if (count > 0) + XFree(configs); + + std::stable_sort(candidates.begin(), candidates.end(), [](const FBConfig &left, const FBConfig &right) { + if (left.depth < right.depth) + return true; + + if (left.stencil < right.stencil) + return true; + + return false; + }); + + if (candidates.size() > 0) { + const FBConfig &candidate = candidates.front(); + + int y_inverted, texture_targets; + glXGetFBConfigAttrib(display(), candidate.config, GLX_BIND_TO_TEXTURE_TARGETS_EXT, &texture_targets); + glXGetFBConfigAttrib(display(), candidate.config, GLX_Y_INVERTED_EXT, &y_inverted); + + info->fbconfig = candidate.config; + info->bind_texture_format = candidate.format; + info->texture_targets = texture_targets; + info->y_inverted = y_inverted; + info->mipmap = 0; + } + + if (info->fbconfig) { + int fbc_id = 0; + int visual_id = 0; + + glXGetFBConfigAttrib(display(), info->fbconfig, GLX_FBCONFIG_ID, &fbc_id); + glXGetFBConfigAttrib(display(), info->fbconfig, GLX_VISUAL_ID, &visual_id); + + qCDebug(KWIN_X11STANDALONE).nospace() << "Using FBConfig 0x" << Qt::hex << fbc_id << " for visual 0x" << Qt::hex << visual_id; + } + + return info; +} + +void GlxBackend::setSwapInterval(int interval) +{ + if (m_haveEXTSwapControl) + glXSwapIntervalEXT(display(), glxWindow, interval); + else if (m_haveMESASwapControl) + glXSwapIntervalMESA(interval); + else if (m_haveSGISwapControl) + glXSwapIntervalSGI(interval); +} + +void GlxBackend::waitSync() +{ + // NOTE that vsync has no effect with indirect rendering + if (haveWaitSync) { + uint sync; +#if 0 + // TODO: why precisely is this important? + // the sync counter /can/ perform multiple steps during glXGetVideoSync & glXWaitVideoSync + // but this only leads to waiting for two frames??!? + glXGetVideoSync(&sync); + glXWaitVideoSync(2, (sync + 1) % 2, &sync); +#else + glXWaitVideoSyncSGI(1, 0, &sync); +#endif + } +} + +void GlxBackend::present() +{ + if (lastDamage().isEmpty()) + return; + + const QSize &screenSize = screens()->size(); + const QRegion displayRegion(0, 0, screenSize.width(), screenSize.height()); + const bool fullRepaint = supportsBufferAge() || (lastDamage() == displayRegion); + + if (fullRepaint) { + if (m_haveINTELSwapEvent) + Compositor::self()->aboutToSwapBuffers(); + + if (haveSwapInterval) { + if (gs_tripleBufferNeedsDetection) { + glXWaitGL(); + m_swapProfiler.begin(); + } + glXSwapBuffers(display(), glxWindow); + if (gs_tripleBufferNeedsDetection) { + glXWaitGL(); + if (char result = m_swapProfiler.end()) { + gs_tripleBufferUndetected = gs_tripleBufferNeedsDetection = false; + setBlocksForRetrace(result == 'd'); + } + } + } else { + waitSync(); + glXSwapBuffers(display(), glxWindow); + } + if (supportsBufferAge()) { + glXQueryDrawable(display(), glxWindow, GLX_BACK_BUFFER_AGE_EXT, (GLuint *) &m_bufferAge); + } + } else if (m_haveMESACopySubBuffer) { + for (const QRect &r : lastDamage()) { + // convert to OpenGL coordinates + int y = screenSize.height() - r.y() - r.height(); + glXCopySubBufferMESA(display(), glxWindow, r.x(), y, r.width(), r.height()); + } + } else { // Copy Pixels (horribly slow on Mesa) + glDrawBuffer(GL_FRONT); + copyPixels(lastDamage()); + glDrawBuffer(GL_BACK); + } + + setLastDamage(QRegion()); + if (!supportsBufferAge()) { + glXWaitGL(); + XFlush(display()); + } +} + +void GlxBackend::screenGeometryChanged(const QSize &size) +{ + doneCurrent(); + + XMoveResizeWindow(display(), window, 0, 0, size.width(), size.height()); + overlayWindow()->setup(window); + Xcb::sync(); + + makeCurrent(); + glViewport(0, 0, size.width(), size.height()); + + // The back buffer contents are now undefined + m_bufferAge = 0; +} + +SceneOpenGLTexturePrivate *GlxBackend::createBackendTexture(SceneOpenGLTexture *texture) +{ + return new GlxTexture(texture, this); +} + +QRegion GlxBackend::prepareRenderingFrame() +{ + QRegion repaint; + + if (gs_tripleBufferNeedsDetection) { + // the composite timer floors the repaint frequency. This can pollute our triple buffering + // detection because the glXSwapBuffers call for the new frame has to wait until the pending + // one scanned out. + // So we compensate for that by waiting an extra milisecond to give the driver the chance to + // fllush the buffer queue + usleep(1000); + } + + present(); + + if (supportsBufferAge()) + repaint = accumulatedDamageHistory(m_bufferAge); + + startRenderTimer(); + glXWaitX(); + + return repaint; +} + +void GlxBackend::endRenderingFrame(const QRegion &renderedRegion, const QRegion &damagedRegion) +{ + if (damagedRegion.isEmpty()) { + setLastDamage(QRegion()); + + // If the damaged region of a window is fully occluded, the only + // rendering done, if any, will have been to repair a reused back + // buffer, making it identical to the front buffer. + // + // In this case we won't post the back buffer. Instead we'll just + // set the buffer age to 1, so the repaired regions won't be + // rendered again in the next frame. + if (!renderedRegion.isEmpty()) + glFlush(); + + m_bufferAge = 1; + return; + } + + setLastDamage(renderedRegion); + + if (!blocksForRetrace()) { + // This also sets lastDamage to empty which prevents the frame from + // being posted again when prepareRenderingFrame() is called. + present(); + } else { + // Make sure that the GPU begins processing the command stream + // now and not the next time prepareRenderingFrame() is called. + glFlush(); + } + + if (overlayWindow()->window()) // show the window only after the first pass, + overlayWindow()->show(); // since that pass may take long + + // Save the damaged region to history + if (supportsBufferAge()) + addToDamageHistory(damagedRegion); +} + +bool GlxBackend::makeCurrent() +{ + if (QOpenGLContext *context = QOpenGLContext::currentContext()) { + // Workaround to tell Qt that no QOpenGLContext is current + context->doneCurrent(); + } + const bool current = glXMakeCurrent(display(), glxWindow, ctx); + return current; +} + +void GlxBackend::doneCurrent() +{ + glXMakeCurrent(display(), None, nullptr); +} + +OverlayWindow* GlxBackend::overlayWindow() const +{ + return m_overlayWindow; +} + +bool GlxBackend::usesOverlayWindow() const +{ + return true; +} + +/******************************************************** + * GlxTexture + *******************************************************/ +GlxTexture::GlxTexture(SceneOpenGLTexture *texture, GlxBackend *backend) + : SceneOpenGLTexturePrivate() + , q(texture) + , m_backend(backend) + , m_glxpixmap(None) +{ +} + +GlxTexture::~GlxTexture() +{ + if (m_glxpixmap != None) { + if (!options->isGlStrictBinding()) { + glXReleaseTexImageEXT(display(), m_glxpixmap, GLX_FRONT_LEFT_EXT); + } + glXDestroyPixmap(display(), m_glxpixmap); + m_glxpixmap = None; + } +} + +void GlxTexture::onDamage() +{ + if (options->isGlStrictBinding() && m_glxpixmap) { + glXReleaseTexImageEXT(display(), m_glxpixmap, GLX_FRONT_LEFT_EXT); + glXBindTexImageEXT(display(), m_glxpixmap, GLX_FRONT_LEFT_EXT, nullptr); + } + GLTexturePrivate::onDamage(); +} + +bool GlxTexture::loadTexture(xcb_pixmap_t pixmap, const QSize &size, xcb_visualid_t visual) +{ + if (pixmap == XCB_NONE || size.isEmpty() || visual == XCB_NONE) + return false; + + const FBConfigInfo *info = m_backend->infoForVisual(visual); + if (!info || info->fbconfig == nullptr) + return false; + + if (info->texture_targets & GLX_TEXTURE_2D_BIT_EXT) { + m_target = GL_TEXTURE_2D; + m_scale.setWidth(1.0f / m_size.width()); + m_scale.setHeight(1.0f / m_size.height()); + } else { + Q_ASSERT(info->texture_targets & GLX_TEXTURE_RECTANGLE_BIT_EXT); + + m_target = GL_TEXTURE_RECTANGLE; + m_scale.setWidth(1.0f); + m_scale.setHeight(1.0f); + } + + const int attrs[] = { + GLX_TEXTURE_FORMAT_EXT, info->bind_texture_format, + GLX_MIPMAP_TEXTURE_EXT, false, + GLX_TEXTURE_TARGET_EXT, m_target == GL_TEXTURE_2D ? GLX_TEXTURE_2D_EXT : GLX_TEXTURE_RECTANGLE_EXT, + 0 + }; + + m_glxpixmap = glXCreatePixmap(display(), info->fbconfig, pixmap, attrs); + m_size = size; + m_yInverted = info->y_inverted ? true : false; + m_canUseMipmaps = false; + + glGenTextures(1, &m_texture); + + q->setDirty(); + q->setFilter(GL_NEAREST); + + glBindTexture(m_target, m_texture); + glXBindTexImageEXT(display(), m_glxpixmap, GLX_FRONT_LEFT_EXT, nullptr); + + updateMatrix(); + return true; +} + +bool GlxTexture::loadTexture(WindowPixmap *pixmap) +{ + Toplevel *t = pixmap->toplevel(); + return loadTexture(pixmap->pixmap(), t->bufferGeometry().size(), t->visual()); +} + +OpenGLBackend *GlxTexture::backend() +{ + return m_backend; +} + +} // namespace diff --git a/plugins/platforms/x11/standalone/glxbackend.h b/plugins/platforms/x11/standalone/glxbackend.h new file mode 100644 index 0000000..321108e --- /dev/null +++ b/plugins/platforms/x11/standalone/glxbackend.h @@ -0,0 +1,139 @@ +/* + 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 +*/ +#ifndef KWIN_GLX_BACKEND_H +#define KWIN_GLX_BACKEND_H +#include "backend.h" +#include "texture.h" +#include "swap_profiler.h" +#include "x11eventfilter.h" + +#include +#include +#include +#include + +namespace KWin +{ + +// GLX_MESA_swap_interval +using glXSwapIntervalMESA_func = int (*)(unsigned int interval); +extern glXSwapIntervalMESA_func glXSwapIntervalMESA; + +class FBConfigInfo +{ +public: + GLXFBConfig fbconfig; + int bind_texture_format; + int texture_targets; + int y_inverted; + int mipmap; +}; + + +// ------------------------------------------------------------------ + + +class SwapEventFilter : public X11EventFilter +{ +public: + SwapEventFilter(xcb_drawable_t drawable, xcb_glx_drawable_t glxDrawable); + bool event(xcb_generic_event_t *event) override; + +private: + xcb_drawable_t m_drawable; + xcb_glx_drawable_t m_glxDrawable; +}; + + +/** + * @brief OpenGL Backend using GLX over an X overlay window. + */ +class GlxBackend : public OpenGLBackend +{ +public: + GlxBackend(Display *display); + ~GlxBackend() override; + void screenGeometryChanged(const QSize &size) override; + SceneOpenGLTexturePrivate *createBackendTexture(SceneOpenGLTexture *texture) override; + QRegion prepareRenderingFrame() override; + void endRenderingFrame(const QRegion &damage, const QRegion &damagedRegion) override; + bool makeCurrent() override; + void doneCurrent() override; + OverlayWindow* overlayWindow() const override; + bool usesOverlayWindow() const override; + void init() override; + +protected: + void present() override; + +private: + bool initBuffer(); + bool checkVersion(); + void initExtensions(); + void waitSync(); + bool initRenderingContext(); + bool initFbConfig(); + void initVisualDepthHashTable(); + void setSwapInterval(int interval); + Display *display() const { + return m_x11Display; + } + + int visualDepth(xcb_visualid_t visual) const; + FBConfigInfo *infoForVisual(xcb_visualid_t visual); + + /** + * @brief The OverlayWindow used by this Backend. + */ + OverlayWindow *m_overlayWindow; + Window window; + GLXFBConfig fbconfig; + GLXWindow glxWindow; + GLXContext ctx; + QHash m_fbconfigHash; + QHash m_visualDepthHash; + std::unique_ptr m_swapEventFilter; + int m_bufferAge; + bool m_haveMESACopySubBuffer = false; + bool m_haveMESASwapControl = false; + bool m_haveEXTSwapControl = false; + bool m_haveSGISwapControl = false; + bool m_haveINTELSwapEvent = false; + bool haveSwapInterval = false; + bool haveWaitSync = false; + Display *m_x11Display; + SwapProfiler m_swapProfiler; + friend class GlxTexture; +}; + +/** + * @brief Texture using an GLXPixmap. + */ +class GlxTexture : public SceneOpenGLTexturePrivate +{ +public: + ~GlxTexture() override; + void onDamage() override; + bool loadTexture(WindowPixmap *pixmap) override; + OpenGLBackend *backend() override; + +private: + friend class GlxBackend; + GlxTexture(SceneOpenGLTexture *texture, GlxBackend *backend); + bool loadTexture(xcb_pixmap_t pix, const QSize &size, xcb_visualid_t visual); + Display *display() const { + return m_backend->m_x11Display; + } + SceneOpenGLTexture *q; + GlxBackend *m_backend; + GLXPixmap m_glxpixmap; // the glx pixmap the texture is bound to +}; + +} // namespace +#endif // KWIN_GLX_BACKEND_H diff --git a/plugins/platforms/x11/standalone/logging.cpp b/plugins/platforms/x11/standalone/logging.cpp new file mode 100644 index 0000000..9eaeeb8 --- /dev/null +++ b/plugins/platforms/x11/standalone/logging.cpp @@ -0,0 +1,10 @@ +/* + 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 "logging.h" +Q_LOGGING_CATEGORY(KWIN_X11STANDALONE, "kwin_platform_x11_standalone", QtCriticalMsg) diff --git a/plugins/platforms/x11/standalone/logging.h b/plugins/platforms/x11/standalone/logging.h new file mode 100644 index 0000000..0881c9d --- /dev/null +++ b/plugins/platforms/x11/standalone/logging.h @@ -0,0 +1,15 @@ +/* + 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 KWIN_X11_LOGGING_H +#define KWIN_X11_LOGGING_H +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(KWIN_X11STANDALONE) +#endif diff --git a/plugins/platforms/x11/standalone/non_composited_outline.cpp b/plugins/platforms/x11/standalone/non_composited_outline.cpp new file mode 100644 index 0000000..3c27597 --- /dev/null +++ b/plugins/platforms/x11/standalone/non_composited_outline.cpp @@ -0,0 +1,139 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2011 Arthur Arlt + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +// own +#include "non_composited_outline.h" +// KWin libs +#include +// xcb +#include + +namespace KWin +{ + +NonCompositedOutlineVisual::NonCompositedOutlineVisual(Outline *outline) + : OutlineVisual(outline) + , m_initialized(false) +{ +} + +NonCompositedOutlineVisual::~NonCompositedOutlineVisual() +{ +} + +void NonCompositedOutlineVisual::show() +{ + if (!m_initialized) { + const QRect geo(0, 0, 1, 1); + const uint32_t values[] = {true}; + // TODO: use template variant + m_leftOutline.create(geo, XCB_CW_OVERRIDE_REDIRECT, values); + m_rightOutline.create(geo, XCB_CW_OVERRIDE_REDIRECT, values); + m_topOutline.create(geo, XCB_CW_OVERRIDE_REDIRECT, values); + m_bottomOutline.create(geo, XCB_CW_OVERRIDE_REDIRECT, values); + m_initialized = true; + } + + const int defaultDepth = Xcb::defaultDepth(); + + const QRect &outlineGeometry = outline()->geometry(); + // left/right parts are between top/bottom, they don't reach as far as the corners + const uint16_t verticalWidth = 5; + const uint16_t verticalHeight = outlineGeometry.height() - 10; + const uint16_t horizontalWidth = outlineGeometry.width(); + const uint horizontalHeight = 5; + m_leftOutline.setGeometry(outlineGeometry.x(), outlineGeometry.y() + 5, verticalWidth, verticalHeight); + m_rightOutline.setGeometry(outlineGeometry.x() + outlineGeometry.width() - 5, outlineGeometry.y() + 5, verticalWidth, verticalHeight); + m_topOutline.setGeometry(outlineGeometry.x(), outlineGeometry.y(), horizontalWidth, horizontalHeight); + m_bottomOutline.setGeometry(outlineGeometry.x(), outlineGeometry.y() + outlineGeometry.height() - 5, horizontalWidth, horizontalHeight); + + const xcb_render_color_t white = {0xffff, 0xffff, 0xffff, 0xffff}; + QColor qGray(Qt::gray); + const xcb_render_color_t gray = { + uint16_t(0xffff * qGray.redF()), + uint16_t(0xffff * qGray.greenF()), + uint16_t(0xffff * qGray.blueF()), + 0xffff + }; + const xcb_render_color_t black = {0, 0, 0, 0xffff}; + { + xcb_pixmap_t xpix = xcb_generate_id(connection()); + xcb_create_pixmap(connection(), defaultDepth, xpix, rootWindow(), verticalWidth, verticalHeight); + XRenderPicture pic(xpix, defaultDepth); + + xcb_rectangle_t rect = {0, 0, 5, verticalHeight}; + xcb_render_fill_rectangles(connection(), XCB_RENDER_PICT_OP_SRC, pic, white, 1, &rect); + rect.x = 1; + rect.width = 3; + xcb_render_fill_rectangles(connection(), XCB_RENDER_PICT_OP_SRC, pic, gray, 1, &rect); + rect.x = 2; + rect.width = 1; + xcb_render_fill_rectangles(connection(), XCB_RENDER_PICT_OP_SRC, pic, black, 1, &rect); + + m_leftOutline.setBackgroundPixmap(xpix); + m_rightOutline.setBackgroundPixmap(xpix); + // According to the XSetWindowBackgroundPixmap documentation the pixmap can be freed. + xcb_free_pixmap(connection(), xpix); + } + { + xcb_pixmap_t xpix = xcb_generate_id(connection()); + xcb_create_pixmap(connection(), defaultDepth, xpix, rootWindow(), horizontalWidth, horizontalHeight); + XRenderPicture pic(xpix, defaultDepth); + + xcb_rectangle_t rect = {0, 0, horizontalWidth, horizontalHeight}; + xcb_render_fill_rectangles(connection(), XCB_RENDER_PICT_OP_SRC, pic, white, 1, &rect); + xcb_rectangle_t grayRects[] = { + {1, 1, uint16_t(horizontalWidth -2), 3}, + {1, 4, 3, 1}, + {int16_t(horizontalWidth - 4), 4, 3, 1} + }; + xcb_render_fill_rectangles(connection(), XCB_RENDER_PICT_OP_SRC, pic, gray, 3, grayRects); + xcb_rectangle_t blackRects[] = { + {2, 2, uint16_t(horizontalWidth -4), 1}, + {2, 3, 1, 2}, + {int16_t(horizontalWidth - 3), 3, 1, 2} + }; + xcb_render_fill_rectangles(connection(), XCB_RENDER_PICT_OP_SRC, pic, black, 3, blackRects); + m_topOutline.setBackgroundPixmap(xpix); + // According to the XSetWindowBackgroundPixmap documentation the pixmap can be freed. + xcb_free_pixmap(connection(), xpix); + } + { + xcb_pixmap_t xpix = xcb_generate_id(connection()); + xcb_create_pixmap(connection(), defaultDepth, xpix, rootWindow(), outlineGeometry.width(), 5); + XRenderPicture pic(xpix, defaultDepth); + + xcb_rectangle_t rect = {0, 0, horizontalWidth, horizontalHeight}; + xcb_render_fill_rectangles(connection(), XCB_RENDER_PICT_OP_SRC, pic, white, 1, &rect); + xcb_rectangle_t grayRects[] = { + {1, 1, uint16_t(horizontalWidth -2), 3}, + {1, 0, 3, 1}, + {int16_t(horizontalWidth - 4), 0, 3, 1} + }; + xcb_render_fill_rectangles(connection(), XCB_RENDER_PICT_OP_SRC, pic, gray, 3, grayRects); + xcb_rectangle_t blackRects[] = { + {2, 2, uint16_t(horizontalWidth -4), 1}, + {2, 0, 1, 2}, + {int16_t(horizontalWidth - 3), 0, 1, 2} + }; + xcb_render_fill_rectangles(connection(), XCB_RENDER_PICT_OP_SRC, pic, black, 3, blackRects); + m_bottomOutline.setBackgroundPixmap(xpix); + // According to the XSetWindowBackgroundPixmap documentation the pixmap can be freed. + xcb_free_pixmap(connection(), xpix); + } + forEachWindow(&Xcb::Window::clear); + forEachWindow(&Xcb::Window::map); +} + +void NonCompositedOutlineVisual::hide() +{ + forEachWindow(&Xcb::Window::unmap); +} + +} // namespace diff --git a/plugins/platforms/x11/standalone/non_composited_outline.h b/plugins/platforms/x11/standalone/non_composited_outline.h new file mode 100644 index 0000000..6b3f1a1 --- /dev/null +++ b/plugins/platforms/x11/standalone/non_composited_outline.h @@ -0,0 +1,48 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2011 Arthur Arlt + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_NON_COMPOSITED_OUTLINE_H +#define KWIN_NON_COMPOSITED_OUTLINE_H +#include "outline.h" +#include "xcbutils.h" + +namespace KWin +{ + +class NonCompositedOutlineVisual : public OutlineVisual +{ +public: + NonCompositedOutlineVisual(Outline *outline); + ~NonCompositedOutlineVisual() override; + void show() override; + void hide() override; + +private: + // TODO: variadic template arguments for adding method arguments + template + void forEachWindow(T method); + bool m_initialized; + Xcb::Window m_topOutline; + Xcb::Window m_rightOutline; + Xcb::Window m_bottomOutline; + Xcb::Window m_leftOutline; +}; + +template +inline +void NonCompositedOutlineVisual::forEachWindow(T method) +{ + (m_topOutline.*method)(); + (m_rightOutline.*method)(); + (m_bottomOutline.*method)(); + (m_leftOutline.*method)(); +} + +} + +#endif diff --git a/plugins/platforms/x11/standalone/overlaywindow_x11.cpp b/plugins/platforms/x11/standalone/overlaywindow_x11.cpp new file mode 100644 index 0000000..3320764 --- /dev/null +++ b/plugins/platforms/x11/standalone/overlaywindow_x11.cpp @@ -0,0 +1,190 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2011 Arthur Arlt + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "overlaywindow_x11.h" + +#include "kwinglobals.h" +#include "composite.h" +#include "screens.h" +#include "utils.h" +#include "xcbutils.h" + +#include + +#include +#include +#if XCB_COMPOSITE_MAJOR_VERSION > 0 || XCB_COMPOSITE_MINOR_VERSION >= 3 +#define KWIN_HAVE_XCOMPOSITE_OVERLAY +#endif + +namespace KWin { +OverlayWindowX11::OverlayWindowX11() + : OverlayWindow() + , X11EventFilter(QVector{XCB_EXPOSE, XCB_VISIBILITY_NOTIFY}) + , m_visible(true) + , m_shown(false) + , m_window(XCB_WINDOW_NONE) +{ +} + +OverlayWindowX11::~OverlayWindowX11() +{ +} + +bool OverlayWindowX11::create() +{ + Q_ASSERT(m_window == XCB_WINDOW_NONE); + if (!Xcb::Extensions::self()->isCompositeOverlayAvailable()) + return false; + if (!Xcb::Extensions::self()->isShapeInputAvailable()) // needed in setupOverlay() + return false; +#ifdef KWIN_HAVE_XCOMPOSITE_OVERLAY + Xcb::OverlayWindow overlay(rootWindow()); + if (overlay.isNull()) { + return false; + } + m_window = overlay->overlay_win; + if (m_window == XCB_WINDOW_NONE) + return false; + resize(screens()->size()); + return true; +#else + return false; +#endif +} + +void OverlayWindowX11::setup(xcb_window_t window) +{ + Q_ASSERT(m_window != XCB_WINDOW_NONE); + Q_ASSERT(Xcb::Extensions::self()->isShapeInputAvailable()); + setNoneBackgroundPixmap(m_window); + m_shape = QRegion(); + const QSize &s = screens()->size(); + setShape(QRect(0, 0, s.width(), s.height())); + if (window != XCB_WINDOW_NONE) { + setNoneBackgroundPixmap(window); + setupInputShape(window); + } + const uint32_t eventMask = XCB_EVENT_MASK_VISIBILITY_CHANGE; + xcb_change_window_attributes(connection(), m_window, XCB_CW_EVENT_MASK, &eventMask); +} + +void OverlayWindowX11::setupInputShape(xcb_window_t window) +{ + xcb_shape_rectangles(connection(), XCB_SHAPE_SO_SET, XCB_SHAPE_SK_INPUT, XCB_CLIP_ORDERING_UNSORTED, window, 0, 0, 0, nullptr); +} + +void OverlayWindowX11::setNoneBackgroundPixmap(xcb_window_t window) +{ + const uint32_t mask = XCB_BACK_PIXMAP_NONE; + xcb_change_window_attributes(connection(), window, XCB_CW_BACK_PIXMAP, &mask); +} + +void OverlayWindowX11::show() +{ + Q_ASSERT(m_window != XCB_WINDOW_NONE); + if (m_shown) + return; + xcb_map_subwindows(connection(), m_window); + xcb_map_window(connection(), m_window); + m_shown = true; +} + +void OverlayWindowX11::hide() +{ + Q_ASSERT(m_window != XCB_WINDOW_NONE); + xcb_unmap_window(connection(), m_window); + m_shown = false; + const QSize &s = screens()->size(); + setShape(QRect(0, 0, s.width(), s.height())); +} + +void OverlayWindowX11::setShape(const QRegion& reg) +{ + // Avoid setting the same shape again, it causes flicker (apparently it is not a no-op + // and triggers something). + if (reg == m_shape) + return; + const QVector xrects = Xcb::regionToRects(reg); + xcb_shape_rectangles(connection(), XCB_SHAPE_SO_SET, XCB_SHAPE_SK_BOUNDING, XCB_CLIP_ORDERING_UNSORTED, + m_window, 0, 0, xrects.count(), xrects.data()); + setupInputShape(m_window); + m_shape = reg; +} + +void OverlayWindowX11::resize(const QSize &size) +{ + Q_ASSERT(m_window != XCB_WINDOW_NONE); + const uint32_t geometry[2] = { + static_cast(size.width()), + static_cast(size.height()) + }; + xcb_configure_window(connection(), m_window, XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT, geometry); + setShape(QRegion(0, 0, size.width(), size.height())); +} + +bool OverlayWindowX11::isVisible() const +{ + return m_visible; +} + +void OverlayWindowX11::setVisibility(bool visible) +{ + m_visible = visible; +} + +void OverlayWindowX11::destroy() +{ + if (m_window == XCB_WINDOW_NONE) + return; + // reset the overlay shape + const QSize &s = screens()->size(); + xcb_rectangle_t rec = { 0, 0, static_cast(s.width()), static_cast(s.height()) }; + xcb_shape_rectangles(connection(), XCB_SHAPE_SO_SET, XCB_SHAPE_SK_BOUNDING, XCB_CLIP_ORDERING_UNSORTED, m_window, 0, 0, 1, &rec); + xcb_shape_rectangles(connection(), XCB_SHAPE_SO_SET, XCB_SHAPE_SK_INPUT, XCB_CLIP_ORDERING_UNSORTED, m_window, 0, 0, 1, &rec); +#ifdef KWIN_HAVE_XCOMPOSITE_OVERLAY + xcb_composite_release_overlay_window(connection(), m_window); +#endif + m_window = XCB_WINDOW_NONE; + m_shown = false; +} + +xcb_window_t OverlayWindowX11::window() const +{ + return m_window; +} + +bool OverlayWindowX11::event(xcb_generic_event_t *event) +{ + const uint8_t eventType = event->response_type & ~0x80; + if (eventType == XCB_EXPOSE) { + const auto *expose = reinterpret_cast(event); + if (expose->window == rootWindow() // root window needs repainting + || (m_window != XCB_WINDOW_NONE && expose->window == m_window)) { // overlay needs repainting + Compositor::self()->addRepaint(expose->x, expose->y, expose->width, expose->height); + } + } else if (eventType == XCB_VISIBILITY_NOTIFY) { + const auto *visibility = reinterpret_cast(event); + if (m_window != XCB_WINDOW_NONE && visibility->window == m_window) { + bool was_visible = isVisible(); + setVisibility((visibility->state != XCB_VISIBILITY_FULLY_OBSCURED)); + auto compositor = Compositor::self(); + if (!was_visible && m_visible) { + // hack for #154825 + compositor->addRepaintFull(); + QTimer::singleShot(2000, compositor, &Compositor::addRepaintFull); + } + compositor->scheduleRepaint(); + } + } + return false; +} + +} // namespace KWin + diff --git a/plugins/platforms/x11/standalone/overlaywindow_x11.h b/plugins/platforms/x11/standalone/overlaywindow_x11.h new file mode 100644 index 0000000..1f5a89c --- /dev/null +++ b/plugins/platforms/x11/standalone/overlaywindow_x11.h @@ -0,0 +1,46 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2011 Arthur Arlt + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_OVERLAYWINDOW_X11_H +#define KWIN_OVERLAYWINDOW_X11_H + +#include "../../../../overlaywindow.h" +#include "../../../../x11eventfilter.h" + +namespace KWin { +class KWIN_EXPORT OverlayWindowX11 : public OverlayWindow, public X11EventFilter { +public: + OverlayWindowX11(); + ~OverlayWindowX11() override; + /// Creates XComposite overlay window, call initOverlay() afterwards + bool create() override; + /// Init overlay and the destination window in it + void setup(xcb_window_t window) override; + void show() override; + void hide() override; // hides and resets overlay window + void setShape(const QRegion& reg) override; + void resize(const QSize &size) override; + /// Destroys XComposite overlay window + void destroy() override; + xcb_window_t window() const override; + bool isVisible() const override; + void setVisibility(bool visible) override; + + bool event(xcb_generic_event_t *event) override; +private: + void setNoneBackgroundPixmap(xcb_window_t window); + void setupInputShape(xcb_window_t window); + bool m_visible; + bool m_shown; // For showOverlay() + QRegion m_shape; + xcb_window_t m_window; +}; +} // namespace + +#endif //KWIN_OVERLAYWINDOW_H diff --git a/plugins/platforms/x11/standalone/screenedges_filter.cpp b/plugins/platforms/x11/standalone/screenedges_filter.cpp new file mode 100644 index 0000000..23ca5d6 --- /dev/null +++ b/plugins/platforms/x11/standalone/screenedges_filter.cpp @@ -0,0 +1,54 @@ +/* + 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 "screenedges_filter.h" +#include "atoms.h" +#include "screenedge.h" + +#include +#include + +namespace KWin +{ + +ScreenEdgesFilter::ScreenEdgesFilter() + : X11EventFilter(QVector{XCB_MOTION_NOTIFY, XCB_ENTER_NOTIFY, XCB_CLIENT_MESSAGE}) +{ +} + +bool ScreenEdgesFilter::event(xcb_generic_event_t *event) +{ + const uint8_t eventType = event->response_type & ~0x80; + switch (eventType) { + case XCB_MOTION_NOTIFY: { + const auto mouseEvent = reinterpret_cast(event); + const QPoint rootPos(mouseEvent->root_x, mouseEvent->root_y); + if (QWidget::mouseGrabber()) { + ScreenEdges::self()->check(rootPos, QDateTime::fromMSecsSinceEpoch(xTime(), Qt::UTC), true); + } else { + ScreenEdges::self()->check(rootPos, QDateTime::fromMSecsSinceEpoch(mouseEvent->time, Qt::UTC)); + } + // not filtered out + break; + } + case XCB_ENTER_NOTIFY: { + const auto enter = reinterpret_cast(event); + return ScreenEdges::self()->handleEnterNotifiy(enter->event, QPoint(enter->root_x, enter->root_y), QDateTime::fromMSecsSinceEpoch(enter->time, Qt::UTC)); + } + case XCB_CLIENT_MESSAGE: { + const auto ce = reinterpret_cast(event); + if (ce->type != atoms->xdnd_position) { + return false; + } + return ScreenEdges::self()->handleDndNotify(ce->window, QPoint(ce->data.data32[2] >> 16, ce->data.data32[2] & 0xffff)); + } + } + return false; +} + +} diff --git a/plugins/platforms/x11/standalone/screenedges_filter.h b/plugins/platforms/x11/standalone/screenedges_filter.h new file mode 100644 index 0000000..57e34ee --- /dev/null +++ b/plugins/platforms/x11/standalone/screenedges_filter.h @@ -0,0 +1,26 @@ +/* + 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 +*/ +#ifndef KWIN_SCREENEDGES_FILTER_H +#define KWIN_SCREENEDGES_FILTER_H +#include "x11eventfilter.h" + +namespace KWin +{ + +class ScreenEdgesFilter : public X11EventFilter +{ +public: + explicit ScreenEdgesFilter(); + + bool event(xcb_generic_event_t *event) override; +}; + +} + +#endif diff --git a/plugins/platforms/x11/standalone/screens_xrandr.cpp b/plugins/platforms/x11/standalone/screens_xrandr.cpp new file mode 100644 index 0000000..3e46df1 --- /dev/null +++ b/plugins/platforms/x11/standalone/screens_xrandr.cpp @@ -0,0 +1,95 @@ +/* + 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 "screens_xrandr.h" +#include "x11_platform.h" + +#ifndef KWIN_UNIT_TEST +#include "composite.h" +#include "options.h" +#include "workspace.h" +#endif +#include "xcbutils.h" + + +namespace KWin +{ + +XRandRScreens::XRandRScreens(X11StandalonePlatform *backend, QObject *parent) + : OutputScreens(backend, parent) + , X11EventFilter(Xcb::Extensions::self()->randrNotifyEvent()) + , m_backend(backend) +{ +} + +XRandRScreens::~XRandRScreens() = default; + +void XRandRScreens::init() +{ + KWin::Screens::init(); + // we need to call ScreenResources at least once to be able to use current + m_backend->initOutputs(); + setCount(m_backend->outputs().count()); + emit changed(); + +#ifndef KWIN_UNIT_TEST + connect(this, &XRandRScreens::changed, this, [] { + if (!workspace()->compositing()) { + return; + } + if (Compositor::self()->refreshRate() == Options::currentRefreshRate()) { + return; + } + // desktopResized() should take care of when the size or + // shape of the desktop has changed, but we also want to + // catch refresh rate changes + Compositor::self()->reinitialize(); + }); +#endif +} + +void XRandRScreens::updateCount() +{ + m_backend->updateOutputs(); + setCount(m_backend->outputs().count()); +} + +bool XRandRScreens::event(xcb_generic_event_t *event) +{ + Q_ASSERT((event->response_type & ~0x80) == Xcb::Extensions::self()->randrNotifyEvent()); + // let's try to gather a few XRandR events, unlikely that there is just one + startChangedTimer(); + + // update default screen + auto *xrrEvent = reinterpret_cast(event); + xcb_screen_t *screen = kwinApp()->x11DefaultScreen(); + if (xrrEvent->rotation & (XCB_RANDR_ROTATION_ROTATE_90 | XCB_RANDR_ROTATION_ROTATE_270)) { + screen->width_in_pixels = xrrEvent->height; + screen->height_in_pixels = xrrEvent->width; + screen->width_in_millimeters = xrrEvent->mheight; + screen->height_in_millimeters = xrrEvent->mwidth; + } else { + screen->width_in_pixels = xrrEvent->width; + screen->height_in_pixels = xrrEvent->height; + screen->width_in_millimeters = xrrEvent->mwidth; + screen->height_in_millimeters = xrrEvent->mheight; + } + + return false; +} + +QSize XRandRScreens::displaySize() const +{ + xcb_screen_t *screen = kwinApp()->x11DefaultScreen(); + if (!screen) { + return Screens::size(); + } + return QSize(screen->width_in_pixels, screen->height_in_pixels); +} + +} // namespace diff --git a/plugins/platforms/x11/standalone/screens_xrandr.h b/plugins/platforms/x11/standalone/screens_xrandr.h new file mode 100644 index 0000000..dd44f7e --- /dev/null +++ b/plugins/platforms/x11/standalone/screens_xrandr.h @@ -0,0 +1,40 @@ +/* + 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 +*/ +#ifndef KWIN_SCREENS_XRANDR_H +#define KWIN_SCREENS_XRANDR_H +// kwin +#include "outputscreens.h" +#include "x11eventfilter.h" + +namespace KWin +{ +class X11StandalonePlatform; + +class XRandRScreens : public OutputScreens, public X11EventFilter +{ + Q_OBJECT +public: + XRandRScreens(X11StandalonePlatform *backend, QObject *parent = nullptr); + ~XRandRScreens() override; + void init() override; + + QSize displaySize() const override; + + using QObject::event; + bool event(xcb_generic_event_t *event) override; + +private: + void updateCount() override; + + X11StandalonePlatform *m_backend; +}; + +} // namespace + +#endif diff --git a/plugins/platforms/x11/standalone/windowselector.cpp b/plugins/platforms/x11/standalone/windowselector.cpp new file mode 100644 index 0000000..18a0810 --- /dev/null +++ b/plugins/platforms/x11/standalone/windowselector.cpp @@ -0,0 +1,262 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "windowselector.h" +#include "x11client.h" +#include "cursor.h" +#include "unmanaged.h" +#include "workspace.h" +#include "xcbutils.h" +// XLib +#include +#include +#include +// XCB +#include + +namespace KWin +{ + +WindowSelector::WindowSelector() + : X11EventFilter(QVector{XCB_BUTTON_PRESS, + XCB_BUTTON_RELEASE, + XCB_MOTION_NOTIFY, + XCB_ENTER_NOTIFY, + XCB_LEAVE_NOTIFY, + XCB_KEY_PRESS, + XCB_KEY_RELEASE, + XCB_FOCUS_IN, + XCB_FOCUS_OUT + }) + , m_active(false) +{ +} + +WindowSelector::~WindowSelector() +{ +} + +void WindowSelector::start(std::function callback, const QByteArray &cursorName) +{ + if (m_active) { + callback(nullptr); + return; + } + + m_active = activate(cursorName); + if (!m_active) { + callback(nullptr); + return; + } + m_callback = callback; +} + +void WindowSelector::start(std::function callback) +{ + if (m_active) { + callback(QPoint(-1, -1)); + return; + } + + m_active = activate(); + if (!m_active) { + callback(QPoint(-1, -1)); + return; + } + m_pointSelectionFallback = callback; +} + +bool WindowSelector::activate(const QByteArray &cursorName) +{ + xcb_cursor_t cursor = createCursor(cursorName); + + xcb_connection_t *c = connection(); + ScopedCPointer grabPointer(xcb_grab_pointer_reply(c, xcb_grab_pointer_unchecked(c, false, rootWindow(), + XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_BUTTON_RELEASE | + XCB_EVENT_MASK_POINTER_MOTION | + XCB_EVENT_MASK_ENTER_WINDOW | XCB_EVENT_MASK_LEAVE_WINDOW, + XCB_GRAB_MODE_ASYNC, XCB_GRAB_MODE_ASYNC, XCB_WINDOW_NONE, + cursor, XCB_TIME_CURRENT_TIME), nullptr)); + if (grabPointer.isNull() || grabPointer->status != XCB_GRAB_STATUS_SUCCESS) { + return false; + } + const bool grabbed = grabXKeyboard(); + if (grabbed) { + grabXServer(); + } else { + xcb_ungrab_pointer(connection(), XCB_TIME_CURRENT_TIME); + } + return grabbed; +} + +xcb_cursor_t WindowSelector::createCursor(const QByteArray &cursorName) +{ + if (cursorName.isEmpty()) { + return Cursors::self()->mouse()->x11Cursor(Qt::CrossCursor); + } + xcb_cursor_t cursor = Cursors::self()->mouse()->x11Cursor(cursorName); + if (cursor != XCB_CURSOR_NONE) { + return cursor; + } + if (cursorName == QByteArrayLiteral("pirate")) { + // special handling for font pirate cursor + static xcb_cursor_t kill_cursor = XCB_CURSOR_NONE; + if (kill_cursor != XCB_CURSOR_NONE) { + return kill_cursor; + } + // fallback on font + xcb_connection_t *c = connection(); + const xcb_font_t cursorFont = xcb_generate_id(c); + xcb_open_font(c, cursorFont, strlen ("cursor"), "cursor"); + cursor = xcb_generate_id(c); + xcb_create_glyph_cursor(c, cursor, cursorFont, cursorFont, + XC_pirate, /* source character glyph */ + XC_pirate + 1, /* mask character glyph */ + 0, 0, 0, 0, 0, 0); /* r b g r b g */ + kill_cursor = cursor; + } + return cursor; +} + +void WindowSelector::processEvent(xcb_generic_event_t *event) +{ + if (event->response_type == XCB_BUTTON_RELEASE) { + xcb_button_release_event_t *buttonEvent = reinterpret_cast(event); + handleButtonRelease(buttonEvent->detail, buttonEvent->child); + } else if (event->response_type == XCB_KEY_PRESS) { + xcb_key_press_event_t *keyEvent = reinterpret_cast(event); + handleKeyPress(keyEvent->detail, keyEvent->state); + } +} + +bool WindowSelector::event(xcb_generic_event_t *event) +{ + if (!m_active) { + return false; + } + processEvent(event); + + return true; +} + +void WindowSelector::handleButtonRelease(xcb_button_t button, xcb_window_t window) +{ + if (button == XCB_BUTTON_INDEX_3) { + cancelCallback(); + release(); + return; + } + if (button == XCB_BUTTON_INDEX_1 || button == XCB_BUTTON_INDEX_2) { + if (m_callback) { + selectWindowId(window); + } else if (m_pointSelectionFallback) { + m_pointSelectionFallback(Cursors::self()->mouse()->pos()); + } + release(); + return; + } +} + +void WindowSelector::handleKeyPress(xcb_keycode_t keycode, uint16_t state) +{ + xcb_key_symbols_t *symbols = xcb_key_symbols_alloc(connection()); + xcb_keysym_t kc = xcb_key_symbols_get_keysym(symbols, keycode, 0); + int mx = 0; + int my = 0; + const bool returnPressed = (kc == XK_Return) || (kc == XK_space); + const bool escapePressed = (kc == XK_Escape); + if (kc == XK_Left) { + mx = -10; + } + if (kc == XK_Right) { + mx = 10; + } + if (kc == XK_Up) { + my = -10; + } + if (kc == XK_Down) { + my = 10; + } + if (state & XCB_MOD_MASK_CONTROL) { + mx /= 10; + my /= 10; + } + Cursors::self()->mouse()->setPos(Cursors::self()->mouse()->pos() + QPoint(mx, my)); + if (returnPressed) { + if (m_callback) { + selectWindowUnderPointer(); + } else if (m_pointSelectionFallback) { + m_pointSelectionFallback(Cursors::self()->mouse()->pos()); + } + } + if (returnPressed || escapePressed) { + if (escapePressed) { + cancelCallback(); + } + release(); + } + xcb_key_symbols_free(symbols); +} + +void WindowSelector::selectWindowUnderPointer() +{ + Xcb::Pointer pointer(rootWindow()); + if (!pointer.isNull() && pointer->child != XCB_WINDOW_NONE) { + selectWindowId(pointer->child); + } +} + +void WindowSelector::release() +{ + ungrabXKeyboard(); + xcb_ungrab_pointer(connection(), XCB_TIME_CURRENT_TIME); + ungrabXServer(); + m_active = false; + m_callback = std::function(); + m_pointSelectionFallback = std::function(); +} + +void WindowSelector::selectWindowId(xcb_window_t window_to_select) +{ + if (window_to_select == XCB_WINDOW_NONE) { + m_callback(nullptr); + return; + } + xcb_window_t window = window_to_select; + X11Client *client = nullptr; + while (true) { + client = Workspace::self()->findClient(Predicate::FrameIdMatch, window); + if (client) { + break; // Found the client + } + Xcb::Tree tree(window); + if (window == tree->root) { + // We didn't find the client, probably an override-redirect window + break; + } + window = tree->parent; // Go up + } + if (client) { + m_callback(client); + } else { + m_callback(Workspace::self()->findUnmanaged(window)); + } +} + +void WindowSelector::cancelCallback() +{ + if (m_callback) { + m_callback(nullptr); + } else if (m_pointSelectionFallback) { + m_pointSelectionFallback(QPoint(-1, -1)); + } +} + +} // namespace diff --git a/plugins/platforms/x11/standalone/windowselector.h b/plugins/platforms/x11/standalone/windowselector.h new file mode 100644 index 0000000..51163c2 --- /dev/null +++ b/plugins/platforms/x11/standalone/windowselector.h @@ -0,0 +1,59 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_WINDOWSELECTOR_H +#define KWIN_WINDOWSELECTOR_H + +#include "x11eventfilter.h" + +#include + +#include + +class QPoint; + +namespace KWin +{ +class Toplevel; + +class WindowSelector : public X11EventFilter +{ +public: + + WindowSelector(); + ~WindowSelector() override; + + void start(std::function callback, const QByteArray &cursorName); + void start(std::function callback); + bool isActive() const { + return m_active; + } + void processEvent(xcb_generic_event_t *event); + + bool event(xcb_generic_event_t *event) override; + +private: + xcb_cursor_t createCursor(const QByteArray &cursorName); + void release(); + void selectWindowUnderPointer(); + void handleKeyPress(xcb_keycode_t keycode, uint16_t state); + void handleButtonRelease(xcb_button_t button, xcb_window_t window); + void selectWindowId(xcb_window_t window_to_kill); + bool activate(const QByteArray &cursorName = QByteArray()); + void cancelCallback(); + bool m_active; + std::function m_callback; + std::function m_pointSelectionFallback; +}; + +} // namespace + +#endif diff --git a/plugins/platforms/x11/standalone/x11.json b/plugins/platforms/x11/standalone/x11.json new file mode 100644 index 0000000..9a77c65 --- /dev/null +++ b/plugins/platforms/x11/standalone/x11.json @@ -0,0 +1,81 @@ +{ + "KPlugin": { + "Description": "Platform plugin for standalone x11 in kwin_x11.", + "Description[az]": "Kwin_x11 -də müstəqil X11 üçün platforma əlavəsi.", + "Description[ca@valencia]": "Connector de plataforma per a una X11 autònoma en kwin_x11.", + "Description[ca]": "Connector de plataforma per a una X11 autònoma en el kwin_x11.", + "Description[da]": "Platform-plugin til standalone x11 i kwin_x11.", + "Description[de]": "Plattform-Modul für ein selbstständiges X11 in kwin_x11.", + "Description[el]": "Πρόσθετο πλατφόρμας για αυτόνομο x11 και kwin_x11.", + "Description[en_GB]": "Platform plugin for standalone x11 in kwin_x11.", + "Description[es]": "Complemento de Platform para X11 autónomo en kwin_x11.", + "Description[et]": "Autonoomse x11 platvormi plugin kwin_x11-s", + "Description[eu]": "Plataformaren plugina x11 autonomoarentzako, kwin_x11 barruan.", + "Description[fi]": "Alustaliitännäinen itsenäiselle x11:lle kwin_x11:ssä.", + "Description[fr]": "Module de plate-forme pour x11 autonomes, au sein de kwin_x11", + "Description[gl]": "Complemento de plataforma para x11 independente en kwin_x11.", + "Description[hu]": "Platformbővítmény önálló x11-hez a kwin_x11-ben.", + "Description[id]": "Plugin platform untuk standalone x11 dalam kwin_x11.", + "Description[it]": "Estensione di piattaforma per x11 autonomo in kwin_x11.", + "Description[ko]": "kwin_x11의 단독 X11 플러그인입니다.", + "Description[lt]": "Platformos priedas, skirtas kwin_x11 viduje esančiam, atskiram x11", + "Description[nl]": "Platform plug-in voor alleenstaande x11 in kwin_x11.", + "Description[nn]": "Plattformtillegg for frittstÃ¥ande X11 i kwin_x11.", + "Description[pl]": "Wtyczka platformy dla wolnostojącego x11 w kwin_x11.", + "Description[pt]": "'Plugin' de plataformas para um X11 autónomo no X11 do KWin.", + "Description[pt_BR]": "Plugin de plataforma para um x11 independente no kwin_x11.", + "Description[ru]": "Подключаемый модуль выделенного х11 в kwin_x11.", + "Description[sk]": "Platformový plugin pre standalone x11 v kwin_x11.", + "Description[sl]": "Okoljski vstavek za samostojni x11 v kwin_x11.", + "Description[sr@ijekavian]": "Платформски прикључак за самостални Икс11 у К‑вину.", + "Description[sr@ijekavianlatin]": "Platformski priključak za samostalni X11 u KWinu.", + "Description[sr@latin]": "Platformski priključak za samostalni X11 u KWinu.", + "Description[sr]": "Платформски прикључак за самостални Икс11 у К‑вину.", + "Description[sv]": "Plattforminsticksprogram för fristÃ¥ende X11 i kwin_x11", + "Description[tr]": "Kwin_x11'de bağımsız x11 için platform eklentisi.", + "Description[uk]": "Додаток платформи для окремого x11 у kwin_x11.", + "Description[x-test]": "xxPlatform plugin for standalone x11 in kwin_x11.xx", + "Description[zh_CN]": "kwin_x11 中的独立 x11 的平台插件", + "Description[zh_TW]": "在 kwin_x11 中供 standalone 的 x11 使用的平臺外掛程式。", + "Id": "KWinX11Platform", + "Name": "x11-standalone", + "Name[az]": "x11-standalone", + "Name[ca@valencia]": "X11-autònoma", + "Name[ca]": "X11-autònoma", + "Name[da]": "x11-standalone", + "Name[de]": "Selbständiges-x11", + "Name[el]": "x11-αυτόνομος", + "Name[en_GB]": "x11-standalone", + "Name[es]": "x11-autónomo", + "Name[et]": "x11-standalone", + "Name[eu]": "x11-autonomoa", + "Name[fi]": "x11-standalone", + "Name[fr]": "x11-standalone", + "Name[gl]": "x11-independente", + "Name[hu]": "x11-standalone", + "Name[id]": "x11-standalone", + "Name[it]": "X11-alone", + "Name[ko]": "x11-standalone", + "Name[lt]": "x11-atskiras", + "Name[nl]": "x11-alleenstaand", + "Name[nn]": "X11 frittstÃ¥ande", + "Name[pl]": "x11-wolnostojący", + "Name[pt]": "x11-autónomo", + "Name[pt_BR]": "x11-standalone", + "Name[ro]": "x11-standalone", + "Name[ru]": "x11-standalone", + "Name[sk]": "x11-standalone", + "Name[sl]": "x11-samostojno", + "Name[sr@ijekavian]": "Икс11 самостални", + "Name[sr@ijekavianlatin]": "X11 samostalni", + "Name[sr@latin]": "X11 samostalni", + "Name[sr]": "Икс11 самостални", + "Name[sv]": "FristÃ¥ende X11", + "Name[tr]": "x11-standalone", + "Name[uk]": "x11-окремий", + "Name[x-test]": "xxx11-standalonexx", + "Name[zh_CN]": "x11-standalone", + "Name[zh_TW]": "x11-standalone" + }, + "input": true +} diff --git a/plugins/platforms/x11/standalone/x11_decoration_renderer.cpp b/plugins/platforms/x11/standalone/x11_decoration_renderer.cpp new file mode 100644 index 0000000..bb2096c --- /dev/null +++ b/plugins/platforms/x11/standalone/x11_decoration_renderer.cpp @@ -0,0 +1,98 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "x11_decoration_renderer.h" +#include "decorations/decoratedclient.h" +#include "x11client.h" +#include "deleted.h" + +#include + +#include +#include + +#include + +namespace KWin +{ +namespace Decoration +{ + +X11Renderer::X11Renderer(DecoratedClientImpl *client) + : Renderer(client) + , m_scheduleTimer(new QTimer(this)) + , m_gc(XCB_NONE) +{ + // delay any rendering to end of event cycle to catch multiple updates per cycle + m_scheduleTimer->setSingleShot(true); + m_scheduleTimer->setInterval(0); + connect(m_scheduleTimer, &QTimer::timeout, this, &X11Renderer::render); + connect(this, &Renderer::renderScheduled, m_scheduleTimer, static_cast(&QTimer::start)); +} + +X11Renderer::~X11Renderer() +{ + if (m_gc != XCB_NONE) { + xcb_free_gc(connection(), m_gc); + } +} + +void X11Renderer::reparent(Deleted *deleted) +{ + if (m_scheduleTimer->isActive()) { + m_scheduleTimer->stop(); + } + disconnect(m_scheduleTimer, &QTimer::timeout, this, &X11Renderer::render); + disconnect(this, &Renderer::renderScheduled, m_scheduleTimer, static_cast(&QTimer::start)); + Renderer::reparent(deleted); +} + +void X11Renderer::render() +{ + if (!client()) { + return; + } + const QRegion scheduled = getScheduled(); + if (scheduled.isEmpty()) { + return; + } + xcb_connection_t *c = connection(); + if (m_gc == XCB_NONE) { + m_gc = xcb_generate_id(c); + xcb_create_gc(c, m_gc, client()->client()->frameId(), 0, nullptr); + } + + QRect left, top, right, bottom; + client()->client()->layoutDecorationRects(left, top, right, bottom); + + const QRect geometry = scheduled.boundingRect(); + left = left.intersected(geometry); + top = top.intersected(geometry); + right = right.intersected(geometry); + bottom = bottom.intersected(geometry); + + auto renderPart = [this, c](const QRect &geo) { + if (!geo.isValid()) { + return; + } + QImage image = renderToImage(geo); + xcb_put_image(c, XCB_IMAGE_FORMAT_Z_PIXMAP, client()->client()->frameId(), m_gc, + image.width(), image.height(), geo.x(), geo.y(), 0, client()->client()->depth(), + image.sizeInBytes(), image.constBits()); + }; + renderPart(left); + renderPart(top); + renderPart(right); + renderPart(bottom); + + xcb_flush(c); + resetImageSizesDirty(); +} + +} +} diff --git a/plugins/platforms/x11/standalone/x11_decoration_renderer.h b/plugins/platforms/x11/standalone/x11_decoration_renderer.h new file mode 100644 index 0000000..5a54e4d --- /dev/null +++ b/plugins/platforms/x11/standalone/x11_decoration_renderer.h @@ -0,0 +1,44 @@ +/* + 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 +*/ +#ifndef KWIN_DECORATION_X11_RENDERER_H +#define KWIN_DECORATION_X11_RENDERER_H + +#include "decorations/decorationrenderer.h" + +#include + +class QTimer; + +namespace KWin +{ + +namespace Decoration +{ + +class X11Renderer : public Renderer +{ + Q_OBJECT +public: + explicit X11Renderer(DecoratedClientImpl *client); + ~X11Renderer() override; + + void reparent(Deleted *deleted) override; + +protected: + void render() override; + +private: + QTimer *m_scheduleTimer; + xcb_gcontext_t m_gc; +}; + +} +} + +#endif diff --git a/plugins/platforms/x11/standalone/x11_output.cpp b/plugins/platforms/x11/standalone/x11_output.cpp new file mode 100644 index 0000000..6a2d348 --- /dev/null +++ b/plugins/platforms/x11/standalone/x11_output.cpp @@ -0,0 +1,95 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "x11_output.h" +#include "screens.h" + +namespace KWin +{ + +X11Output::X11Output(QObject *parent) + : AbstractOutput(parent) +{ +} + +QString X11Output::name() const +{ + return m_name; +} + +void X11Output::setName(QString set) +{ + m_name = set; +} + +QRect X11Output::geometry() const +{ + if (m_geometry.isValid()) { + return m_geometry; + } + return QRect(QPoint(0, 0), Screens::self()->displaySize()); // xinerama, lacks RandR +} + +void X11Output::setGeometry(QRect set) +{ + m_geometry = set; +} + +int X11Output::refreshRate() const +{ + return m_refreshRate; +} + +void X11Output::setRefreshRate(int set) +{ + m_refreshRate = set; +} + +int X11Output::gammaRampSize() const +{ + return m_gammaRampSize; +} + +bool X11Output::setGammaRamp(const GammaRamp &gamma) +{ + if (m_crtc == XCB_NONE) { + return false; + } + + xcb_randr_set_crtc_gamma(connection(), m_crtc, gamma.size(), gamma.red(), + gamma.green(), gamma.blue()); + + return true; +} + +void X11Output::setCrtc(xcb_randr_crtc_t crtc) +{ + m_crtc = crtc; +} + +void X11Output::setGammaRampSize(int size) +{ + m_gammaRampSize = size; +} + +QSize X11Output::physicalSize() const +{ + return m_physicalSize; +} + +void X11Output::setPhysicalSize(const QSize &size) +{ + m_physicalSize = size; +} + +QSize X11Output::pixelSize() const +{ + return geometry().size(); +} + +} diff --git a/plugins/platforms/x11/standalone/x11_output.h b/plugins/platforms/x11/standalone/x11_output.h new file mode 100644 index 0000000..71b2640 --- /dev/null +++ b/plugins/platforms/x11/standalone/x11_output.h @@ -0,0 +1,67 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_X11_OUTPUT_H +#define KWIN_X11_OUTPUT_H + +#include "abstract_output.h" +#include + +#include +#include + +#include + +namespace KWin +{ + +/** + * X11 output representation + */ +class KWIN_EXPORT X11Output : public AbstractOutput +{ + Q_OBJECT + +public: + explicit X11Output(QObject *parent = nullptr); + ~X11Output() override = default; + + QString name() const override; + void setName(QString set); + + QRect geometry() const override; + void setGeometry(QRect set); + + int refreshRate() const override; + void setRefreshRate(int set); + + int gammaRampSize() const override; + bool setGammaRamp(const GammaRamp &gamma) override; + + QSize physicalSize() const override; + void setPhysicalSize(const QSize &size); + + QSize pixelSize() const override; + +private: + void setCrtc(xcb_randr_crtc_t crtc); + void setGammaRampSize(int size); + + xcb_randr_crtc_t m_crtc = XCB_NONE; + QString m_name; + QRect m_geometry; + QSize m_physicalSize; + int m_gammaRampSize; + int m_refreshRate; + + friend class X11StandalonePlatform; +}; + +} + +#endif diff --git a/plugins/platforms/x11/standalone/x11_platform.cpp b/plugins/platforms/x11/standalone/x11_platform.cpp new file mode 100644 index 0000000..71758f7 --- /dev/null +++ b/plugins/platforms/x11/standalone/x11_platform.cpp @@ -0,0 +1,540 @@ +/* + 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 "x11_platform.h" +#include "x11cursor.h" +#include "edge.h" +#include "windowselector.h" +#include +#include +#if HAVE_EPOXY_GLX +#include "glxbackend.h" +#endif +#if HAVE_X11_XINPUT +#include "xinputintegration.h" +#endif +#include "abstract_client.h" +#include "effects_x11.h" +#include "eglonxbackend.h" +#include "keyboard_input.h" +#include "logging.h" +#include "screens_xrandr.h" +#include "screenedges_filter.h" +#include "options.h" +#include "overlaywindow_x11.h" +#include "non_composited_outline.h" +#include "workspace.h" +#include "x11_decoration_renderer.h" +#include "x11_output.h" +#include "xcbutils.h" + +#include + +#include +#include +#include + +#include +#include +#include + +namespace KWin +{ + +X11StandalonePlatform::X11StandalonePlatform(QObject *parent) + : Platform(parent) + , m_x11Display(QX11Info::display()) +{ +#if HAVE_X11_XINPUT + if (!qEnvironmentVariableIsSet("KWIN_NO_XI2")) { + m_xinputIntegration = new XInputIntegration(m_x11Display, this); + m_xinputIntegration->init(); + if (!m_xinputIntegration->hasXinput()) { + delete m_xinputIntegration; + m_xinputIntegration = nullptr; + } else { + connect(kwinApp(), &Application::workspaceCreated, m_xinputIntegration, &XInputIntegration::startListening); + } + } +#endif + + setSupportsGammaControl(true); +} + +X11StandalonePlatform::~X11StandalonePlatform() +{ + if (m_openGLFreezeProtectionThread) { + m_openGLFreezeProtectionThread->quit(); + m_openGLFreezeProtectionThread->wait(); + delete m_openGLFreezeProtectionThread; + } + if (isReady()) { + XRenderUtils::cleanup(); + } +} + +void X11StandalonePlatform::init() +{ + if (!QX11Info::isPlatformX11()) { + emit initFailed(); + return; + } + XRenderUtils::init(kwinApp()->x11Connection(), kwinApp()->x11RootWindow()); + setReady(true); + emit screensQueried(); +} + +Screens *X11StandalonePlatform::createScreens(QObject *parent) +{ + return new XRandRScreens(this, parent); +} + +OpenGLBackend *X11StandalonePlatform::createOpenGLBackend() +{ + switch (options->glPlatformInterface()) { +#if HAVE_EPOXY_GLX + case GlxPlatformInterface: + if (hasGlx()) { + return new GlxBackend(m_x11Display); + } else { + qCWarning(KWIN_X11STANDALONE) << "Glx not available, trying EGL instead."; + // no break, needs fall-through + Q_FALLTHROUGH(); + } +#endif + case EglPlatformInterface: + return new EglOnXBackend(m_x11Display); + default: + // no backend available + return nullptr; + } +} + +Edge *X11StandalonePlatform::createScreenEdge(ScreenEdges *edges) +{ + if (m_screenEdgesFilter.isNull()) { + m_screenEdgesFilter.reset(new ScreenEdgesFilter); + } + return new WindowBasedEdge(edges); +} + +void X11StandalonePlatform::createPlatformCursor(QObject *parent) +{ + auto c = new X11Cursor(parent, m_xinputIntegration != nullptr); +#if HAVE_X11_XINPUT + if (m_xinputIntegration) { + m_xinputIntegration->setCursor(c); + // we know we have xkb already + auto xkb = input()->keyboard()->xkb(); + xkb->setConfig(kwinApp()->kxkbConfig()); + xkb->reconfigure(); + } +#endif +} + +bool X11StandalonePlatform::requiresCompositing() const +{ + return false; +} + +bool X11StandalonePlatform::openGLCompositingIsBroken() const +{ + const QString unsafeKey(QLatin1String("OpenGLIsUnsafe") + (kwinApp()->isX11MultiHead() ? QString::number(kwinApp()->x11ScreenNumber()) : QString())); + return KConfigGroup(kwinApp()->config(), "Compositing").readEntry(unsafeKey, false); +} + +QString X11StandalonePlatform::compositingNotPossibleReason() const +{ + // first off, check whether we figured that we'll crash on detection because of a buggy driver + KConfigGroup gl_workaround_group(kwinApp()->config(), "Compositing"); + const QString unsafeKey(QLatin1String("OpenGLIsUnsafe") + (kwinApp()->isX11MultiHead() ? QString::number(kwinApp()->x11ScreenNumber()) : QString())); + if (gl_workaround_group.readEntry("Backend", "OpenGL") == QLatin1String("OpenGL") && + gl_workaround_group.readEntry(unsafeKey, false)) + return i18n("OpenGL compositing (the default) has crashed KWin in the past.
" + "This was most likely due to a driver bug." + "

If you think that you have meanwhile upgraded to a stable driver,
" + "you can reset this protection but be aware that this might result in an immediate crash!

" + "

Alternatively, you might want to use the XRender backend instead.

"); + + if (!Xcb::Extensions::self()->isCompositeAvailable() || !Xcb::Extensions::self()->isDamageAvailable()) { + return i18n("Required X extensions (XComposite and XDamage) are not available."); + } +#if !defined( KWIN_HAVE_XRENDER_COMPOSITING ) + if (!hasGlx()) + return i18n("GLX/OpenGL are not available and only OpenGL support is compiled."); +#else + if (!(hasGlx() + || (Xcb::Extensions::self()->isRenderAvailable() && Xcb::Extensions::self()->isFixesAvailable()))) { + return i18n("GLX/OpenGL and XRender/XFixes are not available."); + } +#endif + return QString(); +} + +bool X11StandalonePlatform::compositingPossible() const +{ + // first off, check whether we figured that we'll crash on detection because of a buggy driver + KConfigGroup gl_workaround_group(kwinApp()->config(), "Compositing"); + const QString unsafeKey(QLatin1String("OpenGLIsUnsafe") + (kwinApp()->isX11MultiHead() ? QString::number(kwinApp()->x11ScreenNumber()) : QString())); + if (gl_workaround_group.readEntry("Backend", "OpenGL") == QLatin1String("OpenGL") && + gl_workaround_group.readEntry(unsafeKey, false)) + return false; + + + if (!Xcb::Extensions::self()->isCompositeAvailable()) { + qCDebug(KWIN_CORE) << "No composite extension available"; + return false; + } + if (!Xcb::Extensions::self()->isDamageAvailable()) { + qCDebug(KWIN_CORE) << "No damage extension available"; + return false; + } + if (hasGlx()) + return true; +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + if (Xcb::Extensions::self()->isRenderAvailable() && Xcb::Extensions::self()->isFixesAvailable()) + return true; +#endif + if (QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGLES) { + return true; + } else if (qstrcmp(qgetenv("KWIN_COMPOSE"), "O2ES") == 0) { + return true; + } + qCDebug(KWIN_CORE) << "No OpenGL or XRender/XFixes support"; + return false; +} + +bool X11StandalonePlatform::hasGlx() +{ + return Xcb::Extensions::self()->hasGlx(); +} + +void X11StandalonePlatform::createOpenGLSafePoint(OpenGLSafePoint safePoint) +{ + const QString unsafeKey(QLatin1String("OpenGLIsUnsafe") + (kwinApp()->isX11MultiHead() ? QString::number(kwinApp()->x11ScreenNumber()) : QString())); + auto group = KConfigGroup(kwinApp()->config(), "Compositing"); + switch (safePoint) { + case OpenGLSafePoint::PreInit: + group.writeEntry(unsafeKey, true); + group.sync(); + // Deliberately continue with PreFrame + Q_FALLTHROUGH(); + case OpenGLSafePoint::PreFrame: + if (m_openGLFreezeProtectionThread == nullptr) { + Q_ASSERT(m_openGLFreezeProtection == nullptr); + m_openGLFreezeProtectionThread = new QThread(this); + m_openGLFreezeProtectionThread->setObjectName("FreezeDetector"); + m_openGLFreezeProtectionThread->start(); + m_openGLFreezeProtection = new QTimer; + m_openGLFreezeProtection->setInterval(15000); + m_openGLFreezeProtection->setSingleShot(true); + m_openGLFreezeProtection->start(); + const QString configName = kwinApp()->config()->name(); + m_openGLFreezeProtection->moveToThread(m_openGLFreezeProtectionThread); + connect(m_openGLFreezeProtection, &QTimer::timeout, m_openGLFreezeProtection, + [configName] { + const QString unsafeKey(QLatin1String("OpenGLIsUnsafe") + (kwinApp()->isX11MultiHead() ? QString::number(kwinApp()->x11ScreenNumber()) : QString())); + auto group = KConfigGroup(KSharedConfig::openConfig(configName), "Compositing"); + group.writeEntry(unsafeKey, true); + group.sync(); + KCrash::setDrKonqiEnabled(false); + qFatal("Freeze in OpenGL initialization detected"); + }, Qt::DirectConnection); + } else { + Q_ASSERT(m_openGLFreezeProtection); + QMetaObject::invokeMethod(m_openGLFreezeProtection, "start", Qt::QueuedConnection); + } + break; + case OpenGLSafePoint::PostInit: + group.writeEntry(unsafeKey, false); + group.sync(); + // Deliberately continue with PostFrame + Q_FALLTHROUGH(); + case OpenGLSafePoint::PostFrame: + QMetaObject::invokeMethod(m_openGLFreezeProtection, "stop", Qt::QueuedConnection); + break; + case OpenGLSafePoint::PostLastGuardedFrame: + m_openGLFreezeProtection->deleteLater(); + m_openGLFreezeProtection = nullptr; + m_openGLFreezeProtectionThread->quit(); + m_openGLFreezeProtectionThread->wait(); + delete m_openGLFreezeProtectionThread; + m_openGLFreezeProtectionThread = nullptr; + break; + } +} + +PlatformCursorImage X11StandalonePlatform::cursorImage() const +{ + auto c = kwinApp()->x11Connection(); + QScopedPointer cursor( + xcb_xfixes_get_cursor_image_reply(c, + xcb_xfixes_get_cursor_image_unchecked(c), + nullptr)); + if (cursor.isNull()) { + return PlatformCursorImage(); + } + + QImage qcursorimg((uchar *) xcb_xfixes_get_cursor_image_cursor_image(cursor.data()), cursor->width, cursor->height, + QImage::Format_ARGB32_Premultiplied); + // deep copy of image as the data is going to be freed + return PlatformCursorImage(qcursorimg.copy(), QPoint(cursor->xhot, cursor->yhot)); +} + +void X11StandalonePlatform::doHideCursor() +{ + xcb_xfixes_hide_cursor(kwinApp()->x11Connection(), kwinApp()->x11RootWindow()); +} + +void X11StandalonePlatform::doShowCursor() +{ + xcb_xfixes_show_cursor(kwinApp()->x11Connection(), kwinApp()->x11RootWindow()); +} + +void X11StandalonePlatform::startInteractiveWindowSelection(std::function callback, const QByteArray &cursorName) +{ + if (m_windowSelector.isNull()) { + m_windowSelector.reset(new WindowSelector); + } + m_windowSelector->start(callback, cursorName); +} + +void X11StandalonePlatform::startInteractivePositionSelection(std::function callback) +{ + if (m_windowSelector.isNull()) { + m_windowSelector.reset(new WindowSelector); + } + m_windowSelector->start(callback); +} + +void X11StandalonePlatform::setupActionForGlobalAccel(QAction *action) +{ + connect(action, &QAction::triggered, kwinApp(), [action] { + QVariant timestamp = action->property("org.kde.kglobalaccel.activationTimestamp"); + bool ok = false; + const quint32 t = timestamp.toULongLong(&ok); + if (ok) { + kwinApp()->setX11Time(t); + } + }); +} + +OverlayWindow *X11StandalonePlatform::createOverlayWindow() +{ + return new OverlayWindowX11(); +} + +OutlineVisual *X11StandalonePlatform::createOutline(Outline *outline) +{ + // first try composited Outline + auto ret = Platform::createOutline(outline); + if (!ret) { + ret = new NonCompositedOutlineVisual(outline); + } + return ret; +} + +Decoration::Renderer *X11StandalonePlatform::createDecorationRenderer(Decoration::DecoratedClientImpl *client) +{ + auto renderer = Platform::createDecorationRenderer(client); + if (!renderer) { + renderer = new Decoration::X11Renderer(client); + } + return renderer; +} + +void X11StandalonePlatform::invertScreen() +{ + using namespace Xcb::RandR; + bool succeeded = false; + + if (Xcb::Extensions::self()->isRandrAvailable()) { + const auto active_client = workspace()->activeClient(); + ScreenResources res((active_client && active_client->window() != XCB_WINDOW_NONE) ? active_client->window() : rootWindow()); + + if (!res.isNull()) { + for (int j = 0; j < res->num_crtcs; ++j) { + auto crtc = res.crtcs()[j]; + CrtcGamma gamma(crtc); + if (gamma.isNull()) { + continue; + } + if (gamma->size) { + qCDebug(KWIN_CORE) << "inverting screen using xcb_randr_set_crtc_gamma"; + const int half = gamma->size / 2 + 1; + + uint16_t *red = gamma.red(); + uint16_t *green = gamma.green(); + uint16_t *blue = gamma.blue(); + for (int i = 0; i < half; ++i) { + auto invert = [&gamma, i](uint16_t *ramp) { + qSwap(ramp[i], ramp[gamma->size - 1 - i]); + }; + invert(red); + invert(green); + invert(blue); + } + xcb_randr_set_crtc_gamma(connection(), crtc, gamma->size, red, green, blue); + succeeded = true; + } + } + } + } + if (!succeeded) { + Platform::invertScreen(); + } +} + +void X11StandalonePlatform::createEffectsHandler(Compositor *compositor, Scene *scene) +{ + new EffectsHandlerImplX11(compositor, scene); +} + +QVector X11StandalonePlatform::supportedCompositors() const +{ + QVector compositors; +#if HAVE_EPOXY_GLX + compositors << OpenGLCompositing; +#endif +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + compositors << XRenderCompositing; +#endif + compositors << NoCompositing; + return compositors; +} + +void X11StandalonePlatform::initOutputs() +{ + doUpdateOutputs(); +} + +void X11StandalonePlatform::updateOutputs() +{ + doUpdateOutputs(); +} + +template +void X11StandalonePlatform::doUpdateOutputs() +{ + auto fallback = [this]() { + auto *o = new X11Output(this); + o->setGammaRampSize(0); + o->setRefreshRate(-1.0f); + o->setName(QStringLiteral("Xinerama")); + m_outputs << o; + }; + + // TODO: instead of resetting all outputs, check if new output is added/removed + // or still available and leave still available outputs in m_outputs + // untouched (like in DRM backend) + qDeleteAll(m_outputs); + m_outputs.clear(); + + if (!Xcb::Extensions::self()->isRandrAvailable()) { + fallback(); + return; + } + T resources(rootWindow()); + if (resources.isNull()) { + fallback(); + return; + } + xcb_randr_crtc_t *crtcs = resources.crtcs(); + xcb_randr_mode_info_t *modes = resources.modes(); + + QVector infos(resources->num_crtcs); + for (int i = 0; i < resources->num_crtcs; ++i) { + infos[i] = Xcb::RandR::CrtcInfo(crtcs[i], resources->config_timestamp); + } + + for (int i = 0; i < resources->num_crtcs; ++i) { + Xcb::RandR::CrtcInfo info(infos.at(i)); + + xcb_randr_output_t *outputs = info.outputs(); + QVector outputInfos(outputs ? resources->num_outputs : 0); + if (outputs) { + for (int i = 0; i < resources->num_outputs; ++i) { + outputInfos[i] = Xcb::RandR::OutputInfo(outputs[i], resources->config_timestamp); + } + } + + float refreshRate = -1.0f; + for (int j = 0; j < resources->num_modes; ++j) { + if (info->mode == modes[j].id) { + if (modes[j].htotal != 0 && modes[j].vtotal != 0) { // BUG 313996 + // refresh rate calculation - WTF was wikipedia 1998 when I needed it? + int dotclock = modes[j].dot_clock, + vtotal = modes[j].vtotal; + if (modes[j].mode_flags & XCB_RANDR_MODE_FLAG_INTERLACE) + dotclock *= 2; + if (modes[j].mode_flags & XCB_RANDR_MODE_FLAG_DOUBLE_SCAN) + vtotal *= 2; + refreshRate = dotclock/float(modes[j].htotal*vtotal); + } + break; // found mode + } + } + + const QRect geo = info.rect(); + if (geo.isValid()) { + xcb_randr_crtc_t crtc = crtcs[i]; + + // TODO: Perhaps the output has to save the inherited gamma ramp and + // restore it during tear down. Currently neither standalone x11 nor + // drm platform do this. + Xcb::RandR::CrtcGamma gamma(crtc); + + auto *o = new X11Output(this); + o->setCrtc(crtc); + o->setGammaRampSize(gamma.isNull() ? 0 : gamma->size); + o->setGeometry(geo); + o->setRefreshRate(refreshRate * 1000); + + for (int j = 0; j < info->num_outputs; ++j) { + Xcb::RandR::OutputInfo outputInfo(outputInfos.at(j)); + if (outputInfo->crtc != crtc) { + continue; + } + QSize physicalSize(outputInfo->mm_width, outputInfo->mm_height); + switch (info->rotation) { + case XCB_RANDR_ROTATION_ROTATE_0: + case XCB_RANDR_ROTATION_ROTATE_180: + break; + case XCB_RANDR_ROTATION_ROTATE_90: + case XCB_RANDR_ROTATION_ROTATE_270: + physicalSize.transpose(); + break; + case XCB_RANDR_ROTATION_REFLECT_X: + case XCB_RANDR_ROTATION_REFLECT_Y: + break; + } + o->setName(outputInfo.name()); + o->setPhysicalSize(physicalSize); + break; + } + + m_outputs << o; + } + } + + if (m_outputs.isEmpty()) { + fallback(); + } +} + +Outputs X11StandalonePlatform::outputs() const +{ + return m_outputs; +} + +Outputs X11StandalonePlatform::enabledOutputs() const +{ + return m_outputs; +} + +} diff --git a/plugins/platforms/x11/standalone/x11_platform.h b/plugins/platforms/x11/standalone/x11_platform.h new file mode 100644 index 0000000..ed4820e --- /dev/null +++ b/plugins/platforms/x11/standalone/x11_platform.h @@ -0,0 +1,98 @@ +/* + 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 KWIN_X11_PLATFORM_H +#define KWIN_X11_PLATFORM_H +#include "platform.h" + +#include + +#include + +#include + +namespace KWin +{ +class XInputIntegration; +class WindowSelector; +class X11EventFilter; +class X11Output; + +class KWIN_EXPORT X11StandalonePlatform : public Platform +{ + Q_OBJECT + Q_INTERFACES(KWin::Platform) + Q_PLUGIN_METADATA(IID "org.kde.kwin.Platform" FILE "x11.json") +public: + X11StandalonePlatform(QObject *parent = nullptr); + ~X11StandalonePlatform() override; + void init() override; + + Screens *createScreens(QObject *parent = nullptr) override; + OpenGLBackend *createOpenGLBackend() override; + Edge *createScreenEdge(ScreenEdges *parent) override; + void createPlatformCursor(QObject *parent = nullptr) override; + bool requiresCompositing() const override; + bool compositingPossible() const override; + QString compositingNotPossibleReason() const override; + bool openGLCompositingIsBroken() const override; + void createOpenGLSafePoint(OpenGLSafePoint safePoint) override; + void startInteractiveWindowSelection(std::function callback, const QByteArray &cursorName = QByteArray()) override; + void startInteractivePositionSelection(std::function callback) override; + + PlatformCursorImage cursorImage() const override; + + void setupActionForGlobalAccel(QAction *action) override; + + OverlayWindow *createOverlayWindow() override; + OutlineVisual *createOutline(Outline *outline) override; + Decoration::Renderer *createDecorationRenderer(Decoration::DecoratedClientImpl *client) override; + + void invertScreen() override; + + void createEffectsHandler(Compositor *compositor, Scene *scene) override; + QVector supportedCompositors() const override; + + void initOutputs(); + void updateOutputs(); + + Outputs outputs() const override; + Outputs enabledOutputs() const override; + +protected: + void doHideCursor() override; + void doShowCursor() override; + +private: + /** + * Tests whether GLX is supported and returns @c true + * in case KWin is compiled with OpenGL support and GLX + * is available. + * + * If KWin is compiled with OpenGL ES or without OpenGL at + * all, @c false is returned. + * @returns @c true if GLX is available, @c false otherwise and if not build with OpenGL support. + */ + static bool hasGlx(); + + template + void doUpdateOutputs(); + + XInputIntegration *m_xinputIntegration = nullptr; + QThread *m_openGLFreezeProtectionThread = nullptr; + QTimer *m_openGLFreezeProtection = nullptr; + Display *m_x11Display; + QScopedPointer m_windowSelector; + QScopedPointer m_screenEdgesFilter; + + QVector m_outputs; +}; + +} + +#endif diff --git a/plugins/platforms/x11/standalone/x11cursor.cpp b/plugins/platforms/x11/standalone/x11cursor.cpp new file mode 100644 index 0000000..ad61221 --- /dev/null +++ b/plugins/platforms/x11/standalone/x11cursor.cpp @@ -0,0 +1,186 @@ +/* + 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 "x11cursor.h" +#include "input.h" +#include "keyboard_input.h" +#include "utils.h" +#include "xcbutils.h" +#include "xfixes_cursor_event_filter.h" + +#include +#include + +#include + +namespace KWin +{ + +X11Cursor::X11Cursor(QObject *parent, bool xInputSupport) + : Cursor(parent) + , m_timeStamp(XCB_TIME_CURRENT_TIME) + , m_buttonMask(0) + , m_resetTimeStampTimer(new QTimer(this)) + , m_mousePollingTimer(new QTimer(this)) + , m_hasXInput(xInputSupport) + , m_needsPoll(false) +{ + Cursors::self()->setMouse(this); + m_resetTimeStampTimer->setSingleShot(true); + connect(m_resetTimeStampTimer, SIGNAL(timeout()), SLOT(resetTimeStamp())); + // TODO: How often do we really need to poll? + m_mousePollingTimer->setInterval(50); + connect(m_mousePollingTimer, SIGNAL(timeout()), SLOT(mousePolled())); + + connect(this, &Cursor::themeChanged, this, [this] { m_cursors.clear(); }); + + if (m_hasXInput) { + connect(qApp->eventDispatcher(), &QAbstractEventDispatcher::aboutToBlock, this, &X11Cursor::aboutToBlock); + } + +#ifndef KCMRULES + connect(kwinApp(), &Application::workspaceCreated, this, + [this] { + if (Xcb::Extensions::self()->isFixesAvailable()) { + m_xfixesFilter = std::make_unique(this); + } + } + ); +#endif +} + +X11Cursor::~X11Cursor() +{ +} + +void X11Cursor::doSetPos() +{ + const QPoint &pos = currentPos(); + xcb_warp_pointer(connection(), XCB_WINDOW_NONE, rootWindow(), 0, 0, 0, 0, pos.x(), pos.y()); + // call default implementation to emit signal + Cursor::doSetPos(); +} + +void X11Cursor::doGetPos() +{ + if (m_timeStamp != XCB_TIME_CURRENT_TIME && + m_timeStamp == xTime()) { + // time stamps did not change, no need to query again + return; + } + m_timeStamp = xTime(); + Xcb::Pointer pointer(rootWindow()); + if (pointer.isNull()) { + return; + } + m_buttonMask = pointer->mask; + updatePos(pointer->root_x, pointer->root_y); + m_resetTimeStampTimer->start(0); +} + +void X11Cursor::resetTimeStamp() +{ + m_timeStamp = XCB_TIME_CURRENT_TIME; +} + +void X11Cursor::aboutToBlock() +{ + if (m_needsPoll) { + mousePolled(); + m_needsPoll = false; + } +} + +void X11Cursor::doStartMousePolling() +{ + if (!m_hasXInput) { + m_mousePollingTimer->start(); + } +} + +void X11Cursor::doStopMousePolling() +{ + if (!m_hasXInput) { + m_mousePollingTimer->stop(); + } +} + +void X11Cursor::doStartCursorTracking() +{ + xcb_xfixes_select_cursor_input(connection(), rootWindow(), XCB_XFIXES_CURSOR_NOTIFY_MASK_DISPLAY_CURSOR); +} + +void X11Cursor::doStopCursorTracking() +{ + xcb_xfixes_select_cursor_input(connection(), rootWindow(), 0); +} + +void X11Cursor::mousePolled() +{ + static QPoint lastPos = currentPos(); + static uint16_t lastMask = m_buttonMask; + doGetPos(); // Update if needed + if (lastPos != currentPos() || lastMask != m_buttonMask) { + emit mouseChanged(currentPos(), lastPos, + x11ToQtMouseButtons(m_buttonMask), x11ToQtMouseButtons(lastMask), + x11ToQtKeyboardModifiers(m_buttonMask), x11ToQtKeyboardModifiers(lastMask)); + lastPos = currentPos(); + lastMask = m_buttonMask; + } +} + +xcb_cursor_t X11Cursor::getX11Cursor(CursorShape shape) +{ + return getX11Cursor(shape.name()); +} + +xcb_cursor_t X11Cursor::getX11Cursor(const QByteArray &name) +{ + auto it = m_cursors.constFind(name); + if (it != m_cursors.constEnd()) { + return it.value(); + } + return createCursor(name); +} + +xcb_cursor_t X11Cursor::createCursor(const QByteArray &name) +{ + if (name.isEmpty()) { + return XCB_CURSOR_NONE; + } + xcb_cursor_context_t *ctx; + if (xcb_cursor_context_new(kwinApp()->x11Connection(), kwinApp()->x11DefaultScreen(), &ctx) < 0) { + return XCB_CURSOR_NONE; + } + xcb_cursor_t cursor = xcb_cursor_load_cursor(ctx, name.constData()); + if (cursor == XCB_CURSOR_NONE) { + const auto &names = cursorAlternativeNames(name); + for (auto cit = names.begin(); cit != names.end(); ++cit) { + cursor = xcb_cursor_load_cursor(ctx, (*cit).constData()); + if (cursor != XCB_CURSOR_NONE) { + break; + } + } + } + if (cursor != XCB_CURSOR_NONE) { + m_cursors.insert(name, cursor); + } + xcb_cursor_context_free(ctx); + return cursor; +} + +void X11Cursor::notifyCursorChanged() +{ + if (!isCursorTracking()) { + // cursor change tracking is currently disabled, so don't emit signal + return; + } + emit cursorChanged(); +} + +} diff --git a/plugins/platforms/x11/standalone/x11cursor.h b/plugins/platforms/x11/standalone/x11cursor.h new file mode 100644 index 0000000..adf302b --- /dev/null +++ b/plugins/platforms/x11/standalone/x11cursor.h @@ -0,0 +1,74 @@ +/* + 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 KWIN_X11CURSOR_H +#define KWIN_X11CURSOR_H +#include "cursor.h" + +#include + +namespace KWin +{ +class XFixesCursorEventFilter; + +class KWIN_EXPORT X11Cursor : public Cursor +{ + Q_OBJECT +public: + X11Cursor(QObject *parent, bool xInputSupport = false); + ~X11Cursor() override; + + void schedulePoll() { + m_needsPoll = true; + } + + /** + * @internal + * + * Called from X11 event handler. + */ + void notifyCursorChanged(); + +protected: + xcb_cursor_t getX11Cursor(CursorShape shape) override; + xcb_cursor_t getX11Cursor(const QByteArray &name) override; + void doSetPos() override; + void doGetPos() override; + void doStartMousePolling() override; + void doStopMousePolling() override; + void doStartCursorTracking() override; + void doStopCursorTracking() override; + +private Q_SLOTS: + /** + * Because of QTimer's and the impossibility to get events for all mouse + * movements (at least I haven't figured out how) the position needs + * to be also refetched after each return to the event loop. + */ + void resetTimeStamp(); + void mousePolled(); + void aboutToBlock(); +private: + xcb_cursor_t createCursor(const QByteArray &name); + QHash m_cursors; + xcb_timestamp_t m_timeStamp; + uint16_t m_buttonMask; + QTimer *m_resetTimeStampTimer; + QTimer *m_mousePollingTimer; + bool m_hasXInput; + bool m_needsPoll; + + std::unique_ptr m_xfixesFilter; + + friend class Cursor; +}; + + +} + +#endif diff --git a/plugins/platforms/x11/standalone/xfixes_cursor_event_filter.cpp b/plugins/platforms/x11/standalone/xfixes_cursor_event_filter.cpp new file mode 100644 index 0000000..aa04ed0 --- /dev/null +++ b/plugins/platforms/x11/standalone/xfixes_cursor_event_filter.cpp @@ -0,0 +1,29 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "xfixes_cursor_event_filter.h" +#include "x11cursor.h" +#include "xcbutils.h" + +namespace KWin +{ + +XFixesCursorEventFilter::XFixesCursorEventFilter(X11Cursor *cursor) + : X11EventFilter(QVector{Xcb::Extensions::self()->fixesCursorNotifyEvent()}) + , m_cursor(cursor) +{ +} + +bool XFixesCursorEventFilter::event(xcb_generic_event_t *event) +{ + Q_UNUSED(event); + m_cursor->notifyCursorChanged(); + return false; +} + +} diff --git a/plugins/platforms/x11/standalone/xfixes_cursor_event_filter.h b/plugins/platforms/x11/standalone/xfixes_cursor_event_filter.h new file mode 100644 index 0000000..888e9c7 --- /dev/null +++ b/plugins/platforms/x11/standalone/xfixes_cursor_event_filter.h @@ -0,0 +1,30 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_XFIXES_CURSOR_EVENT_FILTER_H +#define KWIN_XFIXES_CURSOR_EVENT_FILTER_H +#include "x11eventfilter.h" + +namespace KWin +{ +class X11Cursor; + +class XFixesCursorEventFilter : public X11EventFilter +{ +public: + explicit XFixesCursorEventFilter(X11Cursor *cursor); + + bool event(xcb_generic_event_t *event) override; + +private: + X11Cursor *m_cursor; +}; + +} + +#endif diff --git a/plugins/platforms/x11/standalone/xinputintegration.cpp b/plugins/platforms/x11/standalone/xinputintegration.cpp new file mode 100644 index 0000000..0bc5b91 --- /dev/null +++ b/plugins/platforms/x11/standalone/xinputintegration.cpp @@ -0,0 +1,276 @@ +/* + 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 "xinputintegration.h" +#include "main.h" +#include "logging.h" +#include "gestures.h" +#include "platform.h" +#include "screenedge.h" +#include "x11cursor.h" +#include "ge_event_mem_mover.h" + +#include "input.h" +#include "x11eventfilter.h" +#include "modifier_only_shortcuts.h" +#include + +#include +#include + +#include + +namespace KWin +{ + +static inline qreal fixed1616ToReal(FP1616 val) +{ + return (val) * 1.0 / (1 << 16); +} + +class XInputEventFilter : public X11EventFilter +{ +public: + XInputEventFilter(int xi_opcode) + : X11EventFilter(XCB_GE_GENERIC, xi_opcode, QVector{XI_RawMotion, XI_RawButtonPress, XI_RawButtonRelease, XI_RawKeyPress, XI_RawKeyRelease, XI_TouchBegin, XI_TouchUpdate, XI_TouchOwnership, XI_TouchEnd}) + {} + ~XInputEventFilter() override = default; + + bool event(xcb_generic_event_t *event) override { + GeEventMemMover ge(event); + switch (ge->event_type) { + case XI_RawKeyPress: { + auto re = reinterpret_cast(event); + kwinApp()->platform()->keyboardKeyPressed(re->detail - 8, re->time); + break; + } + case XI_RawKeyRelease: { + auto re = reinterpret_cast(event); + kwinApp()->platform()->keyboardKeyReleased(re->detail - 8, re->time); + break; + } + case XI_RawButtonPress: { + auto e = reinterpret_cast(event); + switch (e->detail) { + // TODO: this currently ignores left handed settings, for current usage not needed + // if we want to use also for global mouse shortcuts, this needs to reflect state correctly + case XCB_BUTTON_INDEX_1: + kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, e->time); + break; + case XCB_BUTTON_INDEX_2: + kwinApp()->platform()->pointerButtonPressed(BTN_MIDDLE, e->time); + break; + case XCB_BUTTON_INDEX_3: + kwinApp()->platform()->pointerButtonPressed(BTN_RIGHT, e->time); + break; + case XCB_BUTTON_INDEX_4: + case XCB_BUTTON_INDEX_5: + // vertical axis, ignore on press + break; + // TODO: further buttons, horizontal scrolling? + } + } + if (m_x11Cursor) { + m_x11Cursor->schedulePoll(); + } + break; + case XI_RawButtonRelease: { + auto e = reinterpret_cast(event); + switch (e->detail) { + // TODO: this currently ignores left handed settings, for current usage not needed + // if we want to use also for global mouse shortcuts, this needs to reflect state correctly + case XCB_BUTTON_INDEX_1: + kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, e->time); + break; + case XCB_BUTTON_INDEX_2: + kwinApp()->platform()->pointerButtonReleased(BTN_MIDDLE, e->time); + break; + case XCB_BUTTON_INDEX_3: + kwinApp()->platform()->pointerButtonReleased(BTN_RIGHT, e->time); + break; + case XCB_BUTTON_INDEX_4: + kwinApp()->platform()->pointerAxisVertical(120, e->time); + break; + case XCB_BUTTON_INDEX_5: + kwinApp()->platform()->pointerAxisVertical(-120, e->time); + break; + // TODO: further buttons, horizontal scrolling? + } + } + if (m_x11Cursor) { + m_x11Cursor->schedulePoll(); + } + break; + case XI_TouchBegin: { + auto e = reinterpret_cast(event); + m_lastTouchPositions.insert(e->detail, QPointF(fixed1616ToReal(e->event_x), fixed1616ToReal(e->event_y))); + break; + } + case XI_TouchUpdate: { + auto e = reinterpret_cast(event); + const QPointF touchPosition = QPointF(fixed1616ToReal(e->event_x), fixed1616ToReal(e->event_y)); + if (e->detail == m_trackingTouchId) { + const auto last = m_lastTouchPositions.value(e->detail); + ScreenEdges::self()->gestureRecognizer()->updateSwipeGesture(QSizeF(touchPosition.x() - last.x(), touchPosition.y() - last.y())); + } + m_lastTouchPositions.insert(e->detail, touchPosition); + break; + } + case XI_TouchEnd: { + auto e = reinterpret_cast(event); + if (e->detail == m_trackingTouchId) { + ScreenEdges::self()->gestureRecognizer()->endSwipeGesture(); + } + m_lastTouchPositions.remove(e->detail); + m_trackingTouchId = 0; + break; + } + case XI_TouchOwnership: { + auto e = reinterpret_cast(event); + auto it = m_lastTouchPositions.constFind(e->touchid); + if (it == m_lastTouchPositions.constEnd()) { + XIAllowTouchEvents(display(), e->deviceid, e->sourceid, e->touchid, XIRejectTouch); + } else { + if (ScreenEdges::self()->gestureRecognizer()->startSwipeGesture(it.value()) > 0) { + m_trackingTouchId = e->touchid; + } + XIAllowTouchEvents(display(), e->deviceid, e->sourceid, e->touchid, m_trackingTouchId == e->touchid ? XIAcceptTouch : XIRejectTouch); + } + break; + } + default: + if (m_x11Cursor) { + m_x11Cursor->schedulePoll(); + } + break; + } + return false; + } + + void setCursor(const QPointer &cursor) { + m_x11Cursor = cursor; + } + void setDisplay(Display *display) { + m_x11Display = display; + } + +private: + Display *display() const { + return m_x11Display; + } + + QPointer m_x11Cursor; + Display *m_x11Display = nullptr; + uint32_t m_trackingTouchId = 0; + QHash m_lastTouchPositions; +}; + +class XKeyPressReleaseEventFilter : public X11EventFilter +{ +public: + XKeyPressReleaseEventFilter(uint32_t type) + : X11EventFilter(type) + {} + ~XKeyPressReleaseEventFilter() override = default; + + bool event(xcb_generic_event_t *event) override { + xcb_key_press_event_t *ke = reinterpret_cast(event); + if (ke->event == ke->root) { + const uint8_t eventType = event->response_type & ~0x80; + if (eventType == XCB_KEY_PRESS) { + kwinApp()->platform()->keyboardKeyPressed(ke->detail - 8, ke->time); + } else { + kwinApp()->platform()->keyboardKeyReleased(ke->detail - 8, ke->time); + } + } + return false; + } +}; + +XInputIntegration::XInputIntegration(Display *display, QObject *parent) + : QObject(parent) + , m_x11Display(display) +{ +} + +XInputIntegration::~XInputIntegration() = default; + +void XInputIntegration::init() +{ + Display *dpy = display(); + int xi_opcode, event, error; + // init XInput extension + if (!XQueryExtension(dpy, "XInputExtension", &xi_opcode, &event, &error)) { + qCDebug(KWIN_X11STANDALONE) << "XInputExtension not present"; + return; + } + + // verify that the XInput extension is at at least version 2.0 + int major = 2, minor = 2; + int result = XIQueryVersion(dpy, &major, &minor); + if (result != Success) { + qCDebug(KWIN_X11STANDALONE) << "Failed to init XInput 2.2, trying 2.0"; + minor = 0; + if (XIQueryVersion(dpy, &major, &minor) != Success) { + qCDebug(KWIN_X11STANDALONE) << "Failed to init XInput"; + return; + } + } + m_hasXInput = true; + m_xiOpcode = xi_opcode; + m_majorVersion = major; + m_minorVersion = minor; + qCDebug(KWIN_X11STANDALONE) << "Has XInput support" << m_majorVersion << "." << m_minorVersion; +} + +void XInputIntegration::setCursor(X11Cursor *cursor) +{ + m_x11Cursor = QPointer(cursor); +} + +void XInputIntegration::startListening() +{ + // this assumes KWin is the only one setting events on the root window + // given Qt's source code this seems to be true. If it breaks, we need to change + XIEventMask evmasks[1]; + unsigned char mask1[XIMaskLen(XI_LASTEVENT)]; + + memset(mask1, 0, sizeof(mask1)); + + XISetMask(mask1, XI_RawMotion); + XISetMask(mask1, XI_RawButtonPress); + XISetMask(mask1, XI_RawButtonRelease); + if (m_majorVersion >= 2 && m_minorVersion >= 1) { + // we need to listen to all events, which is only available with XInput 2.1 + XISetMask(mask1, XI_RawKeyPress); + XISetMask(mask1, XI_RawKeyRelease); + } + if (m_majorVersion >=2 && m_minorVersion >= 2) { + // touch events since 2.2 + XISetMask(mask1, XI_TouchBegin); + XISetMask(mask1, XI_TouchUpdate); + XISetMask(mask1, XI_TouchOwnership); + XISetMask(mask1, XI_TouchEnd); + } + + evmasks[0].deviceid = XIAllMasterDevices; + evmasks[0].mask_len = sizeof(mask1); + evmasks[0].mask = mask1; + XISelectEvents(display(), rootWindow(), evmasks, 1); + + m_xiEventFilter.reset(new XInputEventFilter(m_xiOpcode)); + m_xiEventFilter->setCursor(m_x11Cursor); + m_xiEventFilter->setDisplay(display()); + m_keyPressFilter.reset(new XKeyPressReleaseEventFilter(XCB_KEY_PRESS)); + m_keyReleaseFilter.reset(new XKeyPressReleaseEventFilter(XCB_KEY_RELEASE)); + + // install the input event spies also relevant for X11 platform + input()->installInputEventSpy(new ModifierOnlyShortcuts); +} + +} diff --git a/plugins/platforms/x11/standalone/xinputintegration.h b/plugins/platforms/x11/standalone/xinputintegration.h new file mode 100644 index 0000000..1959d17 --- /dev/null +++ b/plugins/platforms/x11/standalone/xinputintegration.h @@ -0,0 +1,58 @@ +/* + 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 KWIN_XINPUTINTEGRATION_H +#define KWIN_XINPUTINTEGRATION_H + +#include +#include +#include +typedef struct _XDisplay Display; + +namespace KWin +{ + +class XInputEventFilter; +class XKeyPressReleaseEventFilter; +class X11Cursor; + +class XInputIntegration : public QObject +{ + Q_OBJECT +public: + explicit XInputIntegration(Display *display, QObject *parent); + ~XInputIntegration() override; + + void init(); + void startListening(); + + bool hasXinput() const { + return m_hasXInput; + } + void setCursor(X11Cursor *cursor); + +private: + Display *display() const { + return m_x11Display; + } + + bool m_hasXInput = false; + int m_xiOpcode = 0; + int m_majorVersion = 0; + int m_minorVersion = 0; + QPointer m_x11Cursor; + Display *m_x11Display; + + QScopedPointer m_xiEventFilter; + QScopedPointer m_keyPressFilter; + QScopedPointer m_keyReleaseFilter; +}; + +} + +#endif diff --git a/plugins/platforms/x11/windowed/CMakeLists.txt b/plugins/platforms/x11/windowed/CMakeLists.txt new file mode 100644 index 0000000..815c0ee --- /dev/null +++ b/plugins/platforms/x11/windowed/CMakeLists.txt @@ -0,0 +1,22 @@ +set(X11BACKEND_SOURCES + egl_x11_backend.cpp + logging.cpp + scene_qpainter_x11_backend.cpp + x11windowed_backend.cpp + x11windowed_output.cpp +) + +include_directories(${CMAKE_SOURCE_DIR}/platformsupport/scenes/opengl) +add_library(KWinWaylandX11Backend MODULE ${X11BACKEND_SOURCES}) +set_target_properties(KWinWaylandX11Backend PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/org.kde.kwin.waylandbackends/") +target_link_libraries(KWinWaylandX11Backend eglx11common kwin kwinxrenderutils X11::XCB SceneQPainterBackend SceneOpenGLBackend) +if (X11_Xinput_FOUND) + target_link_libraries(KWinWaylandX11Backend ${X11_Xinput_LIB}) +endif() + +install( + TARGETS + KWinWaylandX11Backend + DESTINATION + ${PLUGIN_INSTALL_DIR}/org.kde.kwin.waylandbackends/ +) diff --git a/plugins/platforms/x11/windowed/egl_x11_backend.cpp b/plugins/platforms/x11/windowed/egl_x11_backend.cpp new file mode 100644 index 0000000..1ca9071 --- /dev/null +++ b/plugins/platforms/x11/windowed/egl_x11_backend.cpp @@ -0,0 +1,110 @@ +/* + 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 "egl_x11_backend.h" +// kwin +#include "screens.h" +#include "x11windowed_backend.h" +// kwin libs +#include + +namespace KWin +{ + +EglX11Backend::EglX11Backend(X11WindowedBackend *backend) + : EglOnXBackend(backend->connection(), backend->display(), backend->rootWindow(), backend->screenNumer(), XCB_WINDOW_NONE) + , m_backend(backend) +{ + setX11TextureFromPixmapSupported(false); +} + +EglX11Backend::~EglX11Backend() = default; + +void EglX11Backend::cleanupSurfaces() +{ + for (auto it = m_surfaces.begin(); it != m_surfaces.end(); ++it) { + eglDestroySurface(eglDisplay(), *it); + } +} + +bool EglX11Backend::createSurfaces() +{ + for (int i = 0; i < screens()->count(); ++i) { + EGLSurface s = createSurface(m_backend->windowForScreen(i)); + if (s == EGL_NO_SURFACE) { + return false; + } + m_surfaces << s; + } + if (m_surfaces.isEmpty()) { + return false; + } + setSurface(m_surfaces.first()); + return true; +} + +void EglX11Backend::present() +{ + for (int i = 0; i < screens()->count(); ++i) { + EGLSurface s = m_surfaces.at(i); + makeContextCurrent(s); + setupViewport(i); + presentSurface(s, screens()->geometry(i), screens()->geometry(i)); + } + eglWaitGL(); + xcb_flush(m_backend->connection()); +} + +QRegion EglX11Backend::prepareRenderingFrame() +{ + startRenderTimer(); + return QRegion(); +} + +void EglX11Backend::endRenderingFrame(const QRegion &renderedRegion, const QRegion &damagedRegion) +{ + Q_UNUSED(renderedRegion) + Q_UNUSED(damagedRegion) +} + +bool EglX11Backend::usesOverlayWindow() const +{ + return false; +} + +bool EglX11Backend::perScreenRendering() const +{ + return true; +} + +QRegion EglX11Backend::prepareRenderingForScreen(int screenId) +{ + makeContextCurrent(m_surfaces.at(screenId)); + setupViewport(screenId); + return screens()->geometry(screenId); +} + +void EglX11Backend::setupViewport(int screenId) +{ + // TODO: ensure the viewport is set correctly each time + const QSize &overall = screens()->size(); + const QRect &v = screens()->geometry(screenId); + // TODO: are the values correct? + + qreal scale = screens()->scale(screenId); + glViewport(-v.x(), v.height() - overall.height() + v.y(), overall.width() * scale, overall.height() * scale); +} + +void EglX11Backend::endRenderingFrameForScreen(int screenId, const QRegion &renderedRegion, const QRegion &damagedRegion) +{ + Q_UNUSED(damagedRegion) + const QRect &outputGeometry = screens()->geometry(screenId); + presentSurface(m_surfaces.at(screenId), renderedRegion, outputGeometry); +} + +} // namespace diff --git a/plugins/platforms/x11/windowed/egl_x11_backend.h b/plugins/platforms/x11/windowed/egl_x11_backend.h new file mode 100644 index 0000000..9d87149 --- /dev/null +++ b/plugins/platforms/x11/windowed/egl_x11_backend.h @@ -0,0 +1,46 @@ +/* + 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_EGL_X11_BACKEND_H +#define KWIN_EGL_X11_BACKEND_H +#include "eglonxbackend.h" + +namespace KWin +{ + +class X11WindowedBackend; + +/** + * @brief OpenGL Backend using Egl windowing system over an X overlay window. + */ +class EglX11Backend : public EglOnXBackend +{ +public: + explicit EglX11Backend(X11WindowedBackend *backend); + ~EglX11Backend() override; + QRegion prepareRenderingFrame() override; + void endRenderingFrame(const QRegion &damage, const QRegion &damagedRegion) override; + bool usesOverlayWindow() const override; + bool perScreenRendering() const override; + QRegion prepareRenderingForScreen(int screenId) override; + void endRenderingFrameForScreen(int screenId, const QRegion &damage, const QRegion &damagedRegion) override; + +protected: + void present() override; + void cleanupSurfaces() override; + bool createSurfaces() override; + +private: + void setupViewport(int screenId); + QVector m_surfaces; + X11WindowedBackend *m_backend; +}; + +} // namespace + +#endif diff --git a/plugins/platforms/x11/windowed/logging.cpp b/plugins/platforms/x11/windowed/logging.cpp new file mode 100644 index 0000000..939dc0b --- /dev/null +++ b/plugins/platforms/x11/windowed/logging.cpp @@ -0,0 +1,10 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "logging.h" +Q_LOGGING_CATEGORY(KWIN_X11WINDOWED, "kwin_wayland_x11windowed", QtCriticalMsg) diff --git a/plugins/platforms/x11/windowed/logging.h b/plugins/platforms/x11/windowed/logging.h new file mode 100644 index 0000000..ee4a33c --- /dev/null +++ b/plugins/platforms/x11/windowed/logging.h @@ -0,0 +1,15 @@ +/* + 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_X11_LOGGING_H +#define KWIN_X11_LOGGING_H +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(KWIN_X11WINDOWED) +#endif diff --git a/plugins/platforms/x11/windowed/scene_qpainter_x11_backend.cpp b/plugins/platforms/x11/windowed/scene_qpainter_x11_backend.cpp new file mode 100644 index 0000000..5670f9e --- /dev/null +++ b/plugins/platforms/x11/windowed/scene_qpainter_x11_backend.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 "scene_qpainter_x11_backend.h" +#include "x11windowed_backend.h" +#include "screens.h" + +namespace KWin +{ +X11WindowedQPainterBackend::X11WindowedQPainterBackend(X11WindowedBackend *backend) + : QPainterBackend() + , m_backend(backend) +{ + connect(screens(), &Screens::changed, this, &X11WindowedQPainterBackend::createOutputs); + createOutputs(); +} + +X11WindowedQPainterBackend::~X11WindowedQPainterBackend() +{ + qDeleteAll(m_outputs); + if (m_gc) { + xcb_free_gc(m_backend->connection(), m_gc); + } +} + +void X11WindowedQPainterBackend::createOutputs() +{ + qDeleteAll(m_outputs); + m_outputs.clear(); + for (int i = 0; i < screens()->count(); ++i) { + Output *output = new Output; + output->window = m_backend->windowForScreen(i); + output->buffer = QImage(screens()->size(i) * screens()->scale(i), QImage::Format_RGB32); + output->buffer.fill(Qt::black); + m_outputs << output; + } + m_needsFullRepaint = true; +} + +QImage *X11WindowedQPainterBackend::buffer() +{ + return bufferForScreen(0); +} + +QImage *X11WindowedQPainterBackend::bufferForScreen(int screen) +{ + return &m_outputs.at(screen)->buffer; +} + +bool X11WindowedQPainterBackend::needsFullRepaint() const +{ + return m_needsFullRepaint; +} + +void X11WindowedQPainterBackend::prepareRenderingFrame() +{ +} + +void X11WindowedQPainterBackend::present(int mask, const QRegion &damage) +{ + Q_UNUSED(mask) + Q_UNUSED(damage) + xcb_connection_t *c = m_backend->connection(); + const xcb_window_t window = m_backend->window(); + if (m_gc == XCB_NONE) { + m_gc = xcb_generate_id(c); + xcb_create_gc(c, m_gc, window, 0, nullptr); + } + for (auto it = m_outputs.constBegin(); it != m_outputs.constEnd(); ++it) { + // TODO: only update changes? + const QImage &buffer = (*it)->buffer; + xcb_put_image(c, XCB_IMAGE_FORMAT_Z_PIXMAP, (*it)->window, m_gc, + buffer.width(), buffer.height(), 0, 0, 0, 24, + buffer.sizeInBytes(), buffer.constBits()); + } +} + +bool X11WindowedQPainterBackend::usesOverlayWindow() const +{ + return false; +} + +bool X11WindowedQPainterBackend::perScreenRendering() const +{ + return true; +} + +} diff --git a/plugins/platforms/x11/windowed/scene_qpainter_x11_backend.h b/plugins/platforms/x11/windowed/scene_qpainter_x11_backend.h new file mode 100644 index 0000000..e9266e2 --- /dev/null +++ b/plugins/platforms/x11/windowed/scene_qpainter_x11_backend.h @@ -0,0 +1,54 @@ +/* + 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_SCENE_QPAINTER_X11_BACKEND_H +#define KWIN_SCENE_QPAINTER_X11_BACKEND_H + +#include + +#include +#include +#include + +#include + +namespace KWin +{ + +class X11WindowedBackend; + +class X11WindowedQPainterBackend : public QObject, public QPainterBackend +{ + Q_OBJECT +public: + X11WindowedQPainterBackend(X11WindowedBackend *backend); + ~X11WindowedQPainterBackend() override; + + QImage *buffer() override; + QImage *bufferForScreen(int screenId) override; + bool needsFullRepaint() const override; + bool usesOverlayWindow() const override; + void prepareRenderingFrame() override; + void present(int mask, const QRegion &damage) override; + bool perScreenRendering() const override; + +private: + void createOutputs(); + bool m_needsFullRepaint = true; + xcb_gcontext_t m_gc = XCB_NONE; + X11WindowedBackend *m_backend; + struct Output { + xcb_window_t window; + QImage buffer; + }; + QVector m_outputs; +}; + +} + +#endif diff --git a/plugins/platforms/x11/windowed/x11.json b/plugins/platforms/x11/windowed/x11.json new file mode 100644 index 0000000..2e07be0 --- /dev/null +++ b/plugins/platforms/x11/windowed/x11.json @@ -0,0 +1,83 @@ +{ + "KPlugin": { + "Description": "Render to a nested window on X11 windowing system.", + "Description[az]": "X11 pəncərə sistemində iç-içə keçmiş pəncərənin formalaşdırılması.", + "Description[ca@valencia]": "Renderitza a una finestra imbricada en el sistema de finestres X11.", + "Description[ca]": "Renderitza a una finestra imbricada en el sistema de finestres X11.", + "Description[da]": "Rendér til et indlejret vindue pÃ¥ X11-vinduessystemet.", + "Description[de]": "In ein eingebettetes Fenster auf X11-Fenstersystemen rendern.", + "Description[el]": "Αποτύπωση σε εμφωλευμένο παράθυρο σε παραθυρικό σύστημα X11.", + "Description[en_GB]": "Render to a nested window on X11 windowing system.", + "Description[es]": "Renderizar en una ventana anidada en el sistema de ventanas X11.", + "Description[et]": "Pesastatud akna renderdamine X11 aknasüsteemis.", + "Description[eu]": "Errendatu X11 leiho sistemaren leiho habiaratu batera.", + "Description[fi]": "Hahmonna X11-ikkunointijärjestelmän sisäkkäiseen ikkunaan.", + "Description[fr]": "Rendre sur une fenêtre imbriquée sur le système de fenêtrage X11.", + "Description[gl]": "Renderizar unha xanela aniñada no sistema de xanelas X11.", + "Description[hu]": "Renderelés egy X11 ablakkezelő rendszeren futó beágyazott ablakba.", + "Description[id]": "Render untuk sebuah window tersarang pada sistem perwindowan X11", + "Description[it]": "Resa in una finestra nidificata su sistema di finestre X11.", + "Description[ko]": "X11 ì°½ 시스템에서 실행 중인 창에 렌더링합니다.", + "Description[lt]": "Atvaizduoti į įdėtinį langą X11 langų sistemoje.", + "Description[nl]": "Render naar een genest venster in het X11 windowingsysteem.", + "Description[nn]": "Teikn opp til innebygd vindauge pÃ¥ køyrande X11-system.", + "Description[pl]": "Wyświetlaj w zagnieżdżonym oknie w systemie okien X11.", + "Description[pt]": "Desenhar numa janela encadeada no sistema de janelas X11.", + "Description[pt_BR]": "Renderizar uma janela encadeada no sistema de janelas X11.", + "Description[ru]": "Отрисовка во вложенном окне оконной системы X11.", + "Description[sk]": "RenderovaÅ¥ na vnorené okno na systém okien X11.", + "Description[sl]": "IzriÅ¡i v gnezdeno okno na okenskem sistemu X11.", + "Description[sr@ijekavian]": "Рендеровање у угнежђени прозор на прозорском систему Икс11.", + "Description[sr@ijekavianlatin]": "Renderovanje u ugnežđeni prozor na prozorskom sistemu X11.", + "Description[sr@latin]": "Renderovanje u ugnežđeni prozor na prozorskom sistemu X11.", + "Description[sr]": "Рендеровање у угнежђени прозор на прозорском систему Икс11.", + "Description[sv]": "Återge till ett nästlat fönster pÃ¥ X11-fönstersystem.", + "Description[tr]": "X11 pencere sisteminde iç içe geçmiş bir pencereye gerçekle.", + "Description[uk]": "Обробляти у вкладене вікно системи керування вікнами X11.", + "Description[x-test]": "xxRender to a nested window on X11 windowing system.xx", + "Description[zh_CN]": "渲染到 X11 窗口系统上的嵌套窗口中", + "Description[zh_TW]": "成像到 X11 視窗系統的巢狀視窗。", + "Id": "KWinWaylandX11Backend", + "Name": "x11", + "Name[az]": "x11", + "Name[ca@valencia]": "X11", + "Name[ca]": "X11", + "Name[cs]": "x11", + "Name[da]": "x11", + "Name[de]": "X11", + "Name[el]": "x11", + "Name[en_GB]": "x11", + "Name[es]": "x11", + "Name[et]": "x11", + "Name[eu]": "x11", + "Name[fi]": "x11", + "Name[fr]": "x11", + "Name[gl]": "x11", + "Name[hu]": "x11", + "Name[ia]": "x11", + "Name[id]": "x11", + "Name[it]": "x11", + "Name[ko]": "x11", + "Name[lt]": "x11", + "Name[nl]": "x11", + "Name[nn]": "x11", + "Name[pl]": "x11", + "Name[pt]": "X11", + "Name[pt_BR]": "x11", + "Name[ro]": "x11", + "Name[ru]": "x11", + "Name[sk]": "x11", + "Name[sl]": "x11", + "Name[sr@ijekavian]": "Икс11", + "Name[sr@ijekavianlatin]": "X11", + "Name[sr@latin]": "X11", + "Name[sr]": "Икс11", + "Name[sv]": "X11", + "Name[tr]": "x11", + "Name[uk]": "x11", + "Name[x-test]": "xxx11xx", + "Name[zh_CN]": "x11", + "Name[zh_TW]": "x11" + }, + "input": true +} diff --git a/plugins/platforms/x11/windowed/x11windowed_backend.cpp b/plugins/platforms/x11/windowed/x11windowed_backend.cpp new file mode 100644 index 0000000..9b24c03 --- /dev/null +++ b/plugins/platforms/x11/windowed/x11windowed_backend.cpp @@ -0,0 +1,534 @@ +/* + 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 "x11windowed_backend.h" +#include "x11windowed_output.h" +#include "scene_qpainter_x11_backend.h" +#include "logging.h" +#include "wayland_server.h" +#include "xcbutils.h" +#include "egl_x11_backend.h" +#include "outputscreens.h" +#include +#include +#include +// KDE +#include +#include +#include +#include +// kwayland +#include +#include +// xcb +#include +// X11 +#if HAVE_X11_XINPUT +#include "ge_event_mem_mover.h" +#include +#include +#endif +// system +#include +#include + +namespace KWin +{ + +X11WindowedBackend::X11WindowedBackend(QObject *parent) + : Platform(parent) +{ + setSupportsPointerWarping(true); + connect(this, &X11WindowedBackend::sizeChanged, this, &X11WindowedBackend::screenSizeChanged); +} + +X11WindowedBackend::~X11WindowedBackend() +{ + if (m_connection) { + if (m_keySymbols) { + xcb_key_symbols_free(m_keySymbols); + } + if (m_cursor) { + xcb_free_cursor(m_connection, m_cursor); + } + xcb_disconnect(m_connection); + } +} + +void X11WindowedBackend::init() +{ + int screen = 0; + xcb_connection_t *c = nullptr; + Display *xDisplay = XOpenDisplay(deviceIdentifier().constData()); + if (xDisplay) { + c = XGetXCBConnection(xDisplay); + XSetEventQueueOwner(xDisplay, XCBOwnsEventQueue); + screen = XDefaultScreen(xDisplay); + } + if (c && !xcb_connection_has_error(c)) { + m_connection = c; + m_screenNumber = screen; + m_display = xDisplay; + for (xcb_screen_iterator_t it = xcb_setup_roots_iterator(xcb_get_setup(m_connection)); + it.rem; + --screen, xcb_screen_next(&it)) { + if (screen == m_screenNumber) { + m_screen = it.data; + } + } + initXInput(); + XRenderUtils::init(m_connection, m_screen->root); + createOutputs(); + connect(kwinApp(), &Application::workspaceCreated, this, &X11WindowedBackend::startEventReading); + connect(Cursors::self(), &Cursors::currentCursorChanged, this, + [this] { + KWin::Cursor* c = KWin::Cursors::self()->currentCursor(); + createCursor(c->image(), c->hotspot()); + } + ); + setReady(true); + waylandServer()->seat()->setHasPointer(true); + waylandServer()->seat()->setHasKeyboard(true); + if (m_hasXInput) { + waylandServer()->seat()->setHasTouch(true); + } + emit screensQueried(); + } else { + emit initFailed(); + } +} + +void X11WindowedBackend::initXInput() +{ +#if HAVE_X11_XINPUT + int xi_opcode, event, error; + // init XInput extension + if (!XQueryExtension(m_display, "XInputExtension", &xi_opcode, &event, &error)) { + qCDebug(KWIN_X11WINDOWED) << "XInputExtension not present"; + return; + } + + // verify that the XInput extension is at at least version 2.0 + int major = 2, minor = 2; + int result = XIQueryVersion(m_display, &major, &minor); + if (result != Success) { + qCDebug(KWIN_X11WINDOWED) << "Failed to init XInput 2.2, trying 2.0"; + minor = 0; + if (XIQueryVersion(m_display, &major, &minor) != Success) { + qCDebug(KWIN_X11WINDOWED) << "Failed to init XInput"; + return; + } + } + m_xiOpcode = xi_opcode; + m_majorVersion = major; + m_minorVersion = minor; + m_hasXInput = m_majorVersion >=2 && m_minorVersion >= 2; +#endif +} + +X11WindowedOutput *X11WindowedBackend::findOutput(xcb_window_t window) const +{ + auto it = std::find_if(m_outputs.constBegin(), m_outputs.constEnd(), + [window] (X11WindowedOutput *output) { + return output->window() == window; + } + ); + if (it != m_outputs.constEnd()) { + return *it; + } + return nullptr; +} + +void X11WindowedBackend::createOutputs() +{ + Xcb::Atom protocolsAtom(QByteArrayLiteral("WM_PROTOCOLS"), false, m_connection); + Xcb::Atom deleteWindowAtom(QByteArrayLiteral("WM_DELETE_WINDOW"), false, m_connection); + + // we need to multiply the initial window size with the scale in order to + // create an output window of this size in the end + const int pixelWidth = initialWindowSize().width() * initialOutputScale() + 0.5; + const int pixelHeight = initialWindowSize().height() * initialOutputScale() + 0.5; + const int logicalWidth = initialWindowSize().width(); + + int logicalWidthSum = 0; + for (int i = 0; i < initialOutputCount(); ++i) { + auto *output = new X11WindowedOutput(this); + output->init(QPoint(logicalWidthSum, 0), QSize(pixelWidth, pixelHeight)); + + m_protocols = protocolsAtom; + m_deleteWindowProtocol = deleteWindowAtom; + + xcb_change_property(m_connection, + XCB_PROP_MODE_REPLACE, + output->window(), + m_protocols, + XCB_ATOM_ATOM, + 32, 1, + &m_deleteWindowProtocol); + + logicalWidthSum += logicalWidth; + m_outputs << output; + } + + updateWindowTitle(); + + xcb_flush(m_connection); +} + +void X11WindowedBackend::startEventReading() +{ + QSocketNotifier *notifier = new QSocketNotifier(xcb_get_file_descriptor(m_connection), QSocketNotifier::Read, this); + auto processXcbEvents = [this] { + while (auto event = xcb_poll_for_event(m_connection)) { + handleEvent(event); + free(event); + } + xcb_flush(m_connection); + }; + connect(notifier, &QSocketNotifier::activated, this, processXcbEvents); + connect(QCoreApplication::eventDispatcher(), &QAbstractEventDispatcher::aboutToBlock, this, processXcbEvents); + connect(QCoreApplication::eventDispatcher(), &QAbstractEventDispatcher::awake, this, processXcbEvents); +} + +#if HAVE_X11_XINPUT + +static inline qreal fixed1616ToReal(FP1616 val) +{ + return (val) * 1.0 / (1 << 16); +} +#endif + +void X11WindowedBackend::handleEvent(xcb_generic_event_t *e) +{ + const uint8_t eventType = e->response_type & ~0x80; + switch (eventType) { + case XCB_BUTTON_PRESS: + case XCB_BUTTON_RELEASE: + handleButtonPress(reinterpret_cast(e)); + break; + case XCB_MOTION_NOTIFY: { + auto event = reinterpret_cast(e); + const X11WindowedOutput *output = findOutput(event->event); + if (!output) { + break; + } + const QPointF position = output->mapFromGlobal(QPointF(event->root_x, event->root_y)); + pointerMotion(position, event->time); + } + break; + case XCB_KEY_PRESS: + case XCB_KEY_RELEASE: { + auto event = reinterpret_cast(e); + if (eventType == XCB_KEY_PRESS) { + if (!m_keySymbols) { + m_keySymbols = xcb_key_symbols_alloc(m_connection); + } + const xcb_keysym_t kc = xcb_key_symbols_get_keysym(m_keySymbols, event->detail, 0); + if (kc == XK_Control_R) { + grabKeyboard(event->time); + } + keyboardKeyPressed(event->detail - 8, event->time); + } else { + keyboardKeyReleased(event->detail - 8, event->time); + } + } + break; + case XCB_CONFIGURE_NOTIFY: + updateSize(reinterpret_cast(e)); + break; + case XCB_ENTER_NOTIFY: { + auto event = reinterpret_cast(e); + const X11WindowedOutput *output = findOutput(event->event); + if (!output) { + break; + } + const QPointF position = output->mapFromGlobal(QPointF(event->root_x, event->root_y)); + pointerMotion(position, event->time); + } + break; + case XCB_CLIENT_MESSAGE: + handleClientMessage(reinterpret_cast(e)); + break; + case XCB_EXPOSE: + handleExpose(reinterpret_cast(e)); + break; + case XCB_MAPPING_NOTIFY: + if (m_keySymbols) { + xcb_refresh_keyboard_mapping(m_keySymbols, reinterpret_cast(e)); + } + break; +#if HAVE_X11_XINPUT + case XCB_GE_GENERIC: { + GeEventMemMover ge(e); + auto te = reinterpret_cast(e); + const X11WindowedOutput *output = findOutput(te->event); + if (!output) { + break; + } + + const QPointF position = output->mapFromGlobal(QPointF(fixed1616ToReal(te->root_x), fixed1616ToReal(te->root_y))); + + switch (ge->event_type) { + + case XI_TouchBegin: { + touchDown(te->detail, position, te->time); + touchFrame(); + break; + } + case XI_TouchUpdate: { + touchMotion(te->detail, position, te->time); + touchFrame(); + break; + } + case XI_TouchEnd: { + touchUp(te->detail, te->time); + touchFrame(); + break; + } + case XI_TouchOwnership: { + auto te = reinterpret_cast(e); + XIAllowTouchEvents(m_display, te->deviceid, te->sourceid, te->touchid, XIAcceptTouch); + break; + } + } + break; + } +#endif + default: + break; + } +} + +void X11WindowedBackend::grabKeyboard(xcb_timestamp_t time) +{ + const bool oldState = m_keyboardGrabbed; + if (m_keyboardGrabbed) { + xcb_ungrab_keyboard(m_connection, time); + xcb_ungrab_pointer(m_connection, time); + m_keyboardGrabbed = false; + } else { + const auto c = xcb_grab_keyboard_unchecked(m_connection, false, window(), time, + XCB_GRAB_MODE_ASYNC, XCB_GRAB_MODE_ASYNC); + ScopedCPointer grab(xcb_grab_keyboard_reply(m_connection, c, nullptr)); + if (grab.isNull()) { + return; + } + if (grab->status == XCB_GRAB_STATUS_SUCCESS) { + const auto c = xcb_grab_pointer_unchecked(m_connection, false, window(), + XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_BUTTON_RELEASE | + XCB_EVENT_MASK_POINTER_MOTION | + XCB_EVENT_MASK_ENTER_WINDOW | XCB_EVENT_MASK_LEAVE_WINDOW, + XCB_GRAB_MODE_ASYNC, XCB_GRAB_MODE_ASYNC, + window(), XCB_CURSOR_NONE, time); + ScopedCPointer grab(xcb_grab_pointer_reply(m_connection, c, nullptr)); + if (grab.isNull() || grab->status != XCB_GRAB_STATUS_SUCCESS) { + xcb_ungrab_keyboard(m_connection, time); + return; + } + m_keyboardGrabbed = true; + } + } + if (oldState != m_keyboardGrabbed) { + updateWindowTitle(); + xcb_flush(m_connection); + } +} + +void X11WindowedBackend::updateWindowTitle() +{ + const QString grab = m_keyboardGrabbed ? i18n("Press right control to ungrab input") : i18n("Press right control key to grab input"); + const QString title = QStringLiteral("%1 (%2) - %3").arg(i18n("KDE Wayland Compositor")) + .arg(waylandServer()->display()->socketName()) + .arg(grab); + for (auto it = m_outputs.constBegin(); it != m_outputs.constEnd(); ++it) { + (*it)->setWindowTitle(title); + } +} + +void X11WindowedBackend::handleClientMessage(xcb_client_message_event_t *event) +{ + auto it = std::find_if(m_outputs.begin(), m_outputs.end(), + [event] (X11WindowedOutput *output) { return output->window() == event->window; } + ); + if (it == m_outputs.end()) { + return; + } + if (event->type == m_protocols && m_protocols != XCB_ATOM_NONE) { + if (event->data.data32[0] == m_deleteWindowProtocol && m_deleteWindowProtocol != XCB_ATOM_NONE) { + if (m_outputs.count() == 1) { + qCDebug(KWIN_X11WINDOWED) << "Backend window is going to be closed, shutting down."; + QCoreApplication::quit(); + } else { + // remove the window + qCDebug(KWIN_X11WINDOWED) << "Removing one output window."; + + auto removedOutput = *it; + it = m_outputs.erase(it); + + // update the sizes + int x = removedOutput->internalPosition().x(); + for (; it != m_outputs.end(); ++it) { + (*it)->setGeometry(QPoint(x, 0), (*it)->pixelSize()); + x += (*it)->geometry().width(); + } + + delete removedOutput; + QMetaObject::invokeMethod(screens(), "updateCount"); + } + } + } +} + +void X11WindowedBackend::handleButtonPress(xcb_button_press_event_t *event) +{ + const X11WindowedOutput *output = findOutput(event->event); + if (!output) { + return; + } + bool const pressed = (event->response_type & ~0x80) == XCB_BUTTON_PRESS; + if (event->detail >= XCB_BUTTON_INDEX_4 && event->detail <= 7) { + // wheel + if (!pressed) { + return; + } + const int delta = (event->detail == XCB_BUTTON_INDEX_4 || event->detail == 6) ? -1 : 1; + static const qreal s_defaultAxisStepDistance = 10.0; + if (event->detail > 5) { + pointerAxisHorizontal(delta * s_defaultAxisStepDistance, event->time, delta); + } else { + pointerAxisVertical(delta * s_defaultAxisStepDistance, event->time, delta); + } + return; + } + uint32_t button = 0; + switch (event->detail) { + case XCB_BUTTON_INDEX_1: + button = BTN_LEFT; + break; + case XCB_BUTTON_INDEX_2: + button = BTN_MIDDLE; + break; + case XCB_BUTTON_INDEX_3: + button = BTN_RIGHT; + break; + default: + button = event->detail + BTN_LEFT - 1; + return; + } + + const QPointF position = output->mapFromGlobal(QPointF(event->root_x, event->root_y)); + pointerMotion(position, event->time); + + if (pressed) { + pointerButtonPressed(button, event->time); + } else { + pointerButtonReleased(button, event->time); + } +} + +void X11WindowedBackend::handleExpose(xcb_expose_event_t *event) +{ + repaint(QRect(event->x, event->y, event->width, event->height)); +} + +void X11WindowedBackend::updateSize(xcb_configure_notify_event_t *event) +{ + X11WindowedOutput *output = findOutput(event->window); + if (!output) { + return; + } + + output->setHostPosition(QPoint(event->x, event->y)); + + const QSize s = QSize(event->width, event->height); + if (s != output->pixelSize()) { + output->setGeometry(output->internalPosition(), s); + } + emit sizeChanged(); +} + +void X11WindowedBackend::createCursor(const QImage &srcImage, const QPoint &hotspot) +{ + const xcb_pixmap_t pix = xcb_generate_id(m_connection); + const xcb_gcontext_t gc = xcb_generate_id(m_connection); + const xcb_cursor_t cid = xcb_generate_id(m_connection); + + //right now on X we only have one scale between all screens, and we know we will have at least one screen + const qreal outputScale = screenScales().first(); + const QSize targetSize = srcImage.size() * outputScale / srcImage.devicePixelRatio(); + const QImage img = srcImage.scaled(targetSize, Qt::KeepAspectRatio); + + xcb_create_pixmap(m_connection, 32, pix, m_screen->root, img.width(), img.height()); + xcb_create_gc(m_connection, gc, pix, 0, nullptr); + + xcb_put_image(m_connection, XCB_IMAGE_FORMAT_Z_PIXMAP, pix, gc, img.width(), img.height(), 0, 0, 0, 32, img.sizeInBytes(), img.constBits()); + + XRenderPicture pic(pix, 32); + xcb_render_create_cursor(m_connection, cid, pic, qRound(hotspot.x() * outputScale), qRound(hotspot.y() * outputScale)); + for (auto it = m_outputs.constBegin(); it != m_outputs.constEnd(); ++it) { + xcb_change_window_attributes(m_connection, (*it)->window(), XCB_CW_CURSOR, &cid); + } + + xcb_free_pixmap(m_connection, pix); + xcb_free_gc(m_connection, gc); + if (m_cursor) { + xcb_free_cursor(m_connection, m_cursor); + } + m_cursor = cid; + xcb_flush(m_connection); +} + +xcb_window_t X11WindowedBackend::rootWindow() const +{ + if (!m_screen) { + return XCB_WINDOW_NONE; + } + return m_screen->root; +} + +Screens *X11WindowedBackend::createScreens(QObject *parent) +{ + return new OutputScreens(this, parent); +} + +OpenGLBackend *X11WindowedBackend::createOpenGLBackend() +{ + return new EglX11Backend(this); +} + +QPainterBackend *X11WindowedBackend::createQPainterBackend() +{ + return new X11WindowedQPainterBackend(this); +} + +void X11WindowedBackend::warpPointer(const QPointF &globalPos) +{ + const xcb_window_t w = m_outputs.at(0)->window(); + xcb_warp_pointer(m_connection, w, w, 0, 0, 0, 0, globalPos.x(), globalPos.y()); + xcb_flush(m_connection); +} + +xcb_window_t X11WindowedBackend::windowForScreen(int screen) const +{ + if (screen > m_outputs.count()) { + return XCB_WINDOW_NONE; + } + return m_outputs.at(screen)->window(); +} + +Outputs X11WindowedBackend::outputs() const +{ + return m_outputs; +} + +Outputs X11WindowedBackend::enabledOutputs() const +{ + return m_outputs; +} + +} diff --git a/plugins/platforms/x11/windowed/x11windowed_backend.h b/plugins/platforms/x11/windowed/x11windowed_backend.h new file mode 100644 index 0000000..4347b94 --- /dev/null +++ b/plugins/platforms/x11/windowed/x11windowed_backend.h @@ -0,0 +1,114 @@ +/* + 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_X11WINDOWED_BACKEND_H +#define KWIN_X11WINDOWED_BACKEND_H +#include "platform.h" + +#include + +#include +#include + +#include + +struct _XDisplay; +typedef struct _XDisplay Display; +typedef struct _XCBKeySymbols xcb_key_symbols_t; +class NETWinInfo; + +namespace KWin +{ +class X11WindowedOutput; + +class KWIN_EXPORT X11WindowedBackend : public Platform +{ + Q_OBJECT + Q_INTERFACES(KWin::Platform) + Q_PLUGIN_METADATA(IID "org.kde.kwin.Platform" FILE "x11.json") + Q_PROPERTY(QSize size READ screenSize NOTIFY sizeChanged) +public: + X11WindowedBackend(QObject *parent = nullptr); + ~X11WindowedBackend() override; + void init() override; + + xcb_connection_t *connection() const { + return m_connection; + } + xcb_screen_t *screen() const { + return m_screen; + } + int screenNumer() const { + return m_screenNumber; + } + xcb_window_t window() const { + return windowForScreen(0); + } + xcb_window_t windowForScreen(int screen) const; + Display *display() const { + return m_display; + } + xcb_window_t rootWindow() const; + bool hasXInput() const { + return m_hasXInput; + } + + Screens *createScreens(QObject *parent = nullptr) override; + OpenGLBackend *createOpenGLBackend() override; + QPainterBackend* createQPainterBackend() override; + void warpPointer(const QPointF &globalPos) override; + + QVector supportedCompositors() const override { + if (selectedCompositor() != NoCompositing) { + return {selectedCompositor()}; + } + return QVector{OpenGLCompositing, QPainterCompositing}; + } + + Outputs outputs() const override; + Outputs enabledOutputs() const override; + +Q_SIGNALS: + void sizeChanged(); + +private: + void createOutputs(); + void startEventReading(); + void grabKeyboard(xcb_timestamp_t time); + void updateWindowTitle(); + void handleEvent(xcb_generic_event_t *event); + void handleClientMessage(xcb_client_message_event_t *event); + void handleButtonPress(xcb_button_press_event_t *event); + void handleExpose(xcb_expose_event_t *event); + void updateSize(xcb_configure_notify_event_t *event); + void createCursor(const QImage &img, const QPoint &hotspot); + void initXInput(); + X11WindowedOutput *findOutput(xcb_window_t window) const; + + xcb_connection_t *m_connection = nullptr; + xcb_screen_t *m_screen = nullptr; + xcb_key_symbols_t *m_keySymbols = nullptr; + int m_screenNumber = 0; + + xcb_atom_t m_protocols = XCB_ATOM_NONE; + xcb_atom_t m_deleteWindowProtocol = XCB_ATOM_NONE; + xcb_cursor_t m_cursor = XCB_CURSOR_NONE; + Display *m_display = nullptr; + bool m_keyboardGrabbed = false; + + bool m_hasXInput = false; + int m_xiOpcode = 0; + int m_majorVersion = 0; + int m_minorVersion = 0; + + QVector m_outputs; +}; + +} + +#endif diff --git a/plugins/platforms/x11/windowed/x11windowed_output.cpp b/plugins/platforms/x11/windowed/x11windowed_output.cpp new file mode 100644 index 0000000..fb61018 --- /dev/null +++ b/plugins/platforms/x11/windowed/x11windowed_output.cpp @@ -0,0 +1,157 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "x11windowed_output.h" + +#include "x11windowed_backend.h" + +#include + +#if HAVE_X11_XINPUT +#include +#endif + +#include + +namespace KWin +{ + +X11WindowedOutput::X11WindowedOutput(X11WindowedBackend *backend) + : AbstractWaylandOutput(backend) + , m_backend(backend) +{ + m_window = xcb_generate_id(m_backend->connection()); + + static int identifier = -1; + identifier++; + setName("X11-" + QString::number(identifier)); +} + +X11WindowedOutput::~X11WindowedOutput() +{ + xcb_unmap_window(m_backend->connection(), m_window); + xcb_destroy_window(m_backend->connection(), m_window); + delete m_winInfo; + xcb_flush(m_backend->connection()); +} + +void X11WindowedOutput::init(const QPoint &logicalPosition, const QSize &pixelSize) +{ + KWaylandServer::OutputDeviceInterface::Mode mode; + mode.id = 0; + mode.size = pixelSize; + mode.flags = KWaylandServer::OutputDeviceInterface::ModeFlag::Current; + mode.refreshRate = 60000; // TODO: get refresh rate via randr + + // Physicial size must be adjusted, such that QPA calculates correct sizes of + // internal elements. + const QSize physicalSize = pixelSize / 96.0 * 25.4 / m_backend->initialOutputScale(); + initInterfaces("model_TODO", "manufacturer_TODO", "UUID_TODO", physicalSize, { mode }); + setGeometry(logicalPosition, pixelSize); + setScale(m_backend->initialOutputScale()); + + uint32_t mask = XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK; + const uint32_t values[] = { + m_backend->screen()->black_pixel, + XCB_EVENT_MASK_KEY_PRESS | + XCB_EVENT_MASK_KEY_RELEASE | + XCB_EVENT_MASK_BUTTON_PRESS | + XCB_EVENT_MASK_BUTTON_RELEASE | + XCB_EVENT_MASK_POINTER_MOTION | + XCB_EVENT_MASK_ENTER_WINDOW | + XCB_EVENT_MASK_LEAVE_WINDOW | + XCB_EVENT_MASK_STRUCTURE_NOTIFY | + XCB_EVENT_MASK_EXPOSURE + }; + xcb_create_window(m_backend->connection(), + XCB_COPY_FROM_PARENT, + m_window, + m_backend->screen()->root, + 0, 0, + pixelSize.width(), pixelSize.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, + mask, values); + + // select xinput 2 events + initXInputForWindow(); + + m_winInfo = new NETWinInfo(m_backend->connection(), + m_window, + m_backend->screen()->root, + NET::WMWindowType, NET::Properties2()); + + m_winInfo->setWindowType(NET::Normal); + m_winInfo->setPid(QCoreApplication::applicationPid()); + QIcon windowIcon = QIcon::fromTheme(QStringLiteral("kwin")); + auto addIcon = [&windowIcon, this] (const QSize &size) { + if (windowIcon.actualSize(size) != size) { + return; + } + NETIcon icon; + QImage windowImage = windowIcon.pixmap(size).toImage(); + icon.data = windowImage.bits(); + icon.size.width = size.width(); + icon.size.height = size.height(); + m_winInfo->setIcon(icon, false); + }; + addIcon(QSize(16, 16)); + addIcon(QSize(32, 32)); + addIcon(QSize(48, 48)); + + xcb_map_window(m_backend->connection(), m_window); +} + +void X11WindowedOutput::initXInputForWindow() +{ + if (!m_backend->hasXInput()) { + return; + } +#if HAVE_X11_XINPUT + XIEventMask evmasks[1]; + unsigned char mask1[XIMaskLen(XI_LASTEVENT)]; + + memset(mask1, 0, sizeof(mask1)); + XISetMask(mask1, XI_TouchBegin); + XISetMask(mask1, XI_TouchUpdate); + XISetMask(mask1, XI_TouchOwnership); + XISetMask(mask1, XI_TouchEnd); + evmasks[0].deviceid = XIAllMasterDevices; + evmasks[0].mask_len = sizeof(mask1); + evmasks[0].mask = mask1; + XISelectEvents(m_backend->display(), m_window, evmasks, 1); +#endif +} + +void X11WindowedOutput::setGeometry(const QPoint &logicalPosition, const QSize &pixelSize) +{ + // TODO: set mode to have updated pixelSize + Q_UNUSED(pixelSize); + setGlobalPos(logicalPosition); +} + +void X11WindowedOutput::setWindowTitle(const QString &title) +{ + m_winInfo->setName(title.toUtf8().constData()); +} + +QPoint X11WindowedOutput::internalPosition() const +{ + return geometry().topLeft(); +} + +void X11WindowedOutput::setHostPosition(const QPoint &pos) +{ + m_hostPosition = pos; +} + +QPointF X11WindowedOutput::mapFromGlobal(const QPointF &pos) const +{ + return (pos - hostPosition() + internalPosition()) / scale(); +} + +} diff --git a/plugins/platforms/x11/windowed/x11windowed_output.h b/plugins/platforms/x11/windowed/x11windowed_output.h new file mode 100644 index 0000000..7e9c515 --- /dev/null +++ b/plugins/platforms/x11/windowed/x11windowed_output.h @@ -0,0 +1,76 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_X11WINDOWED_OUTPUT_H +#define KWIN_X11WINDOWED_OUTPUT_H + +#include "abstract_wayland_output.h" +#include + +#include +#include +#include + +#include + +class NETWinInfo; + +namespace KWin +{ +class X11WindowedBackend; + +/** + * Wayland outputs in a nested X11 setup + */ +class KWIN_EXPORT X11WindowedOutput : public AbstractWaylandOutput +{ + Q_OBJECT +public: + explicit X11WindowedOutput(X11WindowedBackend *backend); + ~X11WindowedOutput() override; + + void init(const QPoint &logicalPosition, const QSize &pixelSize); + + xcb_window_t window() const { + return m_window; + } + + QPoint internalPosition() const; + QPoint hostPosition() const { + return m_hostPosition; + } + void setHostPosition(const QPoint &pos); + + void setWindowTitle(const QString &title); + + /** + * @brief defines the geometry of the output + * @param logicalPosition top left position of the output in compositor space + * @param pixelSize output size as seen from the outside + */ + void setGeometry(const QPoint &logicalPosition, const QSize &pixelSize); + + /** + * Translates the global X11 screen coordinate @p pos to output coordinates. + */ + QPointF mapFromGlobal(const QPointF &pos) const; + +private: + void initXInputForWindow(); + + xcb_window_t m_window = XCB_WINDOW_NONE; + NETWinInfo *m_winInfo = nullptr; + + QPoint m_hostPosition; + + X11WindowedBackend *m_backend; +}; + +} + +#endif diff --git a/plugins/qpa/CMakeLists.txt b/plugins/qpa/CMakeLists.txt new file mode 100644 index 0000000..1eca68d --- /dev/null +++ b/plugins/qpa/CMakeLists.txt @@ -0,0 +1,40 @@ +include_directories(${Qt5Core_PRIVATE_INCLUDE_DIRS}) +include_directories(${Qt5Gui_PRIVATE_INCLUDE_DIRS}) + +set(QPA_SOURCES + backingstore.cpp + eglhelpers.cpp + eglplatformcontext.cpp + integration.cpp + main.cpp + offscreensurface.cpp + platformcursor.cpp + screen.cpp + window.cpp +) + +include(ECMQtDeclareLoggingCategory) +ecm_qt_declare_logging_category(QPA_SOURCES HEADER logging.h IDENTIFIER KWIN_QPA CATEGORY_NAME kwin_qpa_plugin DEFAULT_SEVERITY Critical) + +add_library(KWinQpaPlugin MODULE ${QPA_SOURCES}) +set_target_properties(KWinQpaPlugin PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/platforms/") + +set(QT5PLATFORMSUPPORT_LIBS + Qt5::FontDatabaseSupportPrivate + Qt5::ThemeSupportPrivate + Qt5::EventDispatcherSupportPrivate +) + +target_link_libraries(KWinQpaPlugin + ${QT5PLATFORMSUPPORT_LIBS} + ${FREETYPE_LIBRARIES} # Must be after QT5PLATFORMSUPPORT_LIBS + Fontconfig::Fontconfig + kwin +) + +install( + TARGETS + KWinQpaPlugin + DESTINATION + ${PLUGIN_INSTALL_DIR}/platforms/ +) diff --git a/plugins/qpa/backingstore.cpp b/plugins/qpa/backingstore.cpp new file mode 100644 index 0000000..1a0a4c9 --- /dev/null +++ b/plugins/qpa/backingstore.cpp @@ -0,0 +1,92 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "backingstore.h" +#include "window.h" + +#include "internal_client.h" + +namespace KWin +{ +namespace QPA +{ + +BackingStore::BackingStore(QWindow *window) + : QPlatformBackingStore(window) +{ +} + +BackingStore::~BackingStore() = default; + +QPaintDevice *BackingStore::paintDevice() +{ + return &m_backBuffer; +} + +void BackingStore::resize(const QSize &size, const QRegion &staticContents) +{ + Q_UNUSED(staticContents) + + if (m_backBuffer.size() == size) { + return; + } + + const QPlatformWindow *platformWindow = static_cast(window()->handle()); + const qreal devicePixelRatio = platformWindow->devicePixelRatio(); + + m_backBuffer = QImage(size * devicePixelRatio, QImage::Format_ARGB32_Premultiplied); + m_backBuffer.setDevicePixelRatio(devicePixelRatio); + + m_frontBuffer = QImage(size * devicePixelRatio, QImage::Format_ARGB32_Premultiplied); + m_frontBuffer.setDevicePixelRatio(devicePixelRatio); +} + +static void blitImage(const QImage &source, QImage &target, const QRect &rect) +{ + Q_ASSERT(source.format() == QImage::Format_ARGB32_Premultiplied); + Q_ASSERT(target.format() == QImage::Format_ARGB32_Premultiplied); + + const int devicePixelRatio = target.devicePixelRatio(); + + const int x = rect.x() * devicePixelRatio; + const int y = rect.y() * devicePixelRatio; + const int width = rect.width() * devicePixelRatio; + const int height = rect.height() * devicePixelRatio; + + for (int i = y; i < y + height; ++i) { + const uint32_t *in = reinterpret_cast(source.scanLine(i)); + uint32_t *out = reinterpret_cast(target.scanLine(i)); + std::copy(in + x, in + x + width, out + x); + } +} + +static void blitImage(const QImage &source, QImage &target, const QRegion ®ion) +{ + for (const QRect &rect : region) { + blitImage(source, target, rect); + } +} + +void BackingStore::flush(QWindow *window, const QRegion ®ion, const QPoint &offset) +{ + Q_UNUSED(offset) + + Window *platformWindow = static_cast(window->handle()); + InternalClient *client = platformWindow->client(); + if (!client) { + return; + } + + blitImage(m_backBuffer, m_frontBuffer, region); + + client->present(m_frontBuffer, region); +} + +} +} diff --git a/plugins/qpa/backingstore.h b/plugins/qpa/backingstore.h new file mode 100644 index 0000000..51741a8 --- /dev/null +++ b/plugins/qpa/backingstore.h @@ -0,0 +1,42 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_QPA_BACKINGSTORE_H +#define KWIN_QPA_BACKINGSTORE_H + +#include +#include "fixqopengl.h" +#include + +#include + +namespace KWin +{ +namespace QPA +{ + +class BackingStore : public QPlatformBackingStore +{ +public: + explicit BackingStore(QWindow *window); + ~BackingStore() override; + + QPaintDevice *paintDevice() override; + void flush(QWindow *window, const QRegion ®ion, const QPoint &offset) override; + void resize(const QSize &size, const QRegion &staticContents) override; + +private: + QImage m_backBuffer; + QImage m_frontBuffer; +}; + +} +} + +#endif diff --git a/plugins/qpa/eglhelpers.cpp b/plugins/qpa/eglhelpers.cpp new file mode 100644 index 0000000..db3169a --- /dev/null +++ b/plugins/qpa/eglhelpers.cpp @@ -0,0 +1,105 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Flöser + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "eglhelpers.h" + +#include + +#include + +namespace KWin +{ +namespace QPA +{ + +bool isOpenGLES() +{ + if (qstrcmp(qgetenv("KWIN_COMPOSE"), "O2ES") == 0) { + return true; + } + + return QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGLES; +} + +EGLConfig configFromFormat(EGLDisplay display, const QSurfaceFormat &surfaceFormat, EGLint surfaceType) +{ + // qMax as these values are initialized to -1 by default. + const EGLint redSize = qMax(surfaceFormat.redBufferSize(), 0); + const EGLint greenSize = qMax(surfaceFormat.greenBufferSize(), 0); + const EGLint blueSize = qMax(surfaceFormat.blueBufferSize(), 0); + const EGLint alphaSize = qMax(surfaceFormat.alphaBufferSize(), 0); + const EGLint depthSize = qMax(surfaceFormat.depthBufferSize(), 0); + const EGLint stencilSize = qMax(surfaceFormat.stencilBufferSize(), 0); + + const EGLint renderableType = isOpenGLES() ? EGL_OPENGL_ES2_BIT : EGL_OPENGL_BIT; + + // Not setting samples as QtQuick doesn't need it. + const QVector attributes { + EGL_SURFACE_TYPE, surfaceType, + EGL_RED_SIZE, redSize, + EGL_GREEN_SIZE, greenSize, + EGL_BLUE_SIZE, blueSize, + EGL_ALPHA_SIZE, alphaSize, + EGL_DEPTH_SIZE, depthSize, + EGL_STENCIL_SIZE, stencilSize, + EGL_RENDERABLE_TYPE, renderableType, + EGL_NONE + }; + + EGLint configCount; + EGLConfig configs[1024]; + if (!eglChooseConfig(display, attributes.data(), configs, 1, &configCount)) { + // FIXME: Don't bail out yet, we should try to find the most suitable config. + qCWarning(KWIN_QPA, "eglChooseConfig failed: %x", eglGetError()); + return EGL_NO_CONFIG_KHR; + } + + if (configCount != 1) { + qCWarning(KWIN_QPA) << "eglChooseConfig did not return any configs"; + return EGL_NO_CONFIG_KHR; + } + + return configs[0]; +} + +QSurfaceFormat formatFromConfig(EGLDisplay display, EGLConfig config) +{ + int redSize = 0; + int blueSize = 0; + int greenSize = 0; + int alphaSize = 0; + int stencilSize = 0; + int depthSize = 0; + int sampleCount = 0; + + eglGetConfigAttrib(display, config, EGL_RED_SIZE, &redSize); + eglGetConfigAttrib(display, config, EGL_GREEN_SIZE, &greenSize); + eglGetConfigAttrib(display, config, EGL_BLUE_SIZE, &blueSize); + eglGetConfigAttrib(display, config, EGL_ALPHA_SIZE, &alphaSize); + eglGetConfigAttrib(display, config, EGL_STENCIL_SIZE, &stencilSize); + eglGetConfigAttrib(display, config, EGL_DEPTH_SIZE, &depthSize); + eglGetConfigAttrib(display, config, EGL_SAMPLES, &sampleCount); + + QSurfaceFormat format; + format.setRedBufferSize(redSize); + format.setGreenBufferSize(greenSize); + format.setBlueBufferSize(blueSize); + format.setAlphaBufferSize(alphaSize); + format.setStencilBufferSize(stencilSize); + format.setDepthBufferSize(depthSize); + format.setSamples(sampleCount); + format.setRenderableType(isOpenGLES() ? QSurfaceFormat::OpenGLES : QSurfaceFormat::OpenGL); + format.setStereo(false); + + return format; +} + +} // namespace QPA +} // namespace KWin diff --git a/plugins/qpa/eglhelpers.h b/plugins/qpa/eglhelpers.h new file mode 100644 index 0000000..80f633b --- /dev/null +++ b/plugins/qpa/eglhelpers.h @@ -0,0 +1,29 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include "fixqopengl.h" +#include + +#include + +namespace KWin +{ +namespace QPA +{ + +bool isOpenGLES(); + +EGLConfig configFromFormat(EGLDisplay display, const QSurfaceFormat &surfaceFormat, EGLint surfaceType = 0); +QSurfaceFormat formatFromConfig(EGLDisplay display, EGLConfig config); + +} // namespace QPA +} // namespace KWin diff --git a/plugins/qpa/eglplatformcontext.cpp b/plugins/qpa/eglplatformcontext.cpp new file mode 100644 index 0000000..cbdb7e0 --- /dev/null +++ b/plugins/qpa/eglplatformcontext.cpp @@ -0,0 +1,268 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "eglplatformcontext.h" +#include "egl_context_attribute_builder.h" +#include "eglhelpers.h" +#include "internal_client.h" +#include "offscreensurface.h" +#include "platform.h" +#include "window.h" + +#include "logging.h" + +#include +#include + +#include + +#include + +namespace KWin +{ +namespace QPA +{ + +EGLPlatformContext::EGLPlatformContext(QOpenGLContext *context, EGLDisplay display) + : m_eglDisplay(display) +{ + create(context->format(), kwinApp()->platform()->sceneEglGlobalShareContext()); +} + +EGLPlatformContext::~EGLPlatformContext() +{ + if (m_context != EGL_NO_CONTEXT) { + eglDestroyContext(m_eglDisplay, m_context); + } +} + +EGLDisplay EGLPlatformContext::eglDisplay() const +{ + return m_eglDisplay; +} + +EGLContext EGLPlatformContext::eglContext() const +{ + return m_context; +} + +static EGLSurface eglSurfaceForPlatformSurface(QPlatformSurface *surface) +{ + if (surface->surface()->surfaceClass() == QSurface::Window) { + return static_cast(surface)->eglSurface(); + } else { + return static_cast(surface)->eglSurface(); + } +} + +bool EGLPlatformContext::makeCurrent(QPlatformSurface *surface) +{ + const EGLSurface eglSurface = eglSurfaceForPlatformSurface(surface); + + const bool ok = eglMakeCurrent(eglDisplay(), eglSurface, eglSurface, eglContext()); + if (!ok) { + qCWarning(KWIN_QPA, "eglMakeCurrent failed: %x", eglGetError()); + return false; + } + + if (surface->surface()->surfaceClass() == QSurface::Window) { + // QOpenGLContextPrivate::setCurrentContext will be called after this + // method returns, but that's too late, as we need a current context in + // order to bind the content framebuffer object. + QOpenGLContextPrivate::setCurrentContext(context()); + + Window *window = static_cast(surface); + window->bindContentFBO(); + } + + return true; +} + +void EGLPlatformContext::doneCurrent() +{ + eglMakeCurrent(m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); +} + +bool EGLPlatformContext::isValid() const +{ + return m_context != EGL_NO_CONTEXT; +} + +bool EGLPlatformContext::isSharing() const +{ + return false; +} + +QSurfaceFormat EGLPlatformContext::format() const +{ + return m_format; +} + +QFunctionPointer EGLPlatformContext::getProcAddress(const char *procName) +{ + return eglGetProcAddress(procName); +} + +void EGLPlatformContext::swapBuffers(QPlatformSurface *surface) +{ + if (surface->surface()->surfaceClass() == QSurface::Window) { + Window *window = static_cast(surface); + InternalClient *client = window->client(); + if (!client) { + return; + } + context()->makeCurrent(surface->surface()); + glFlush(); + client->present(window->swapFBO()); + window->bindContentFBO(); + } +} + +GLuint EGLPlatformContext::defaultFramebufferObject(QPlatformSurface *surface) const +{ + if (Window *window = dynamic_cast(surface)) { + const auto &fbo = window->contentFBO(); + if (!fbo.isNull()) { + return fbo->handle(); + } + qCDebug(KWIN_QPA) << "No default framebuffer object for internal window"; + } + + return 0; +} + +void EGLPlatformContext::create(const QSurfaceFormat &format, EGLContext shareContext) +{ + if (!eglBindAPI(isOpenGLES() ? EGL_OPENGL_ES_API : EGL_OPENGL_API)) { + qCWarning(KWIN_QPA, "eglBindAPI failed: 0x%x", eglGetError()); + return; + } + + m_config = configFromFormat(m_eglDisplay, format); + if (m_config == EGL_NO_CONFIG_KHR) { + qCWarning(KWIN_QPA) << "Could not find suitable EGLConfig for" << format; + return; + } + + m_format = formatFromConfig(m_eglDisplay, m_config); + + const QByteArray eglExtensions = eglQueryString(eglDisplay(), EGL_EXTENSIONS); + const QList extensions = eglExtensions.split(' '); + const bool haveRobustness = extensions.contains(QByteArrayLiteral("EGL_EXT_create_context_robustness")); + const bool haveCreateContext = extensions.contains(QByteArrayLiteral("EGL_KHR_create_context")); + const bool haveContextPriority = extensions.contains(QByteArrayLiteral("EGL_IMG_context_priority")); + + std::vector> candidates; + if (isOpenGLES()) { + if (haveCreateContext && haveRobustness && haveContextPriority) { + auto glesRobustPriority = std::unique_ptr(new EglOpenGLESContextAttributeBuilder); + glesRobustPriority->setVersion(2); + glesRobustPriority->setRobust(true); + glesRobustPriority->setHighPriority(true); + candidates.push_back(std::move(glesRobustPriority)); + } + if (haveCreateContext && haveRobustness) { + auto glesRobust = std::unique_ptr(new EglOpenGLESContextAttributeBuilder); + glesRobust->setVersion(2); + glesRobust->setRobust(true); + candidates.push_back(std::move(glesRobust)); + } + if (haveContextPriority) { + auto glesPriority = std::unique_ptr(new EglOpenGLESContextAttributeBuilder); + glesPriority->setVersion(2); + glesPriority->setHighPriority(true); + candidates.push_back(std::move(glesPriority)); + } + auto gles = std::unique_ptr(new EglOpenGLESContextAttributeBuilder); + gles->setVersion(2); + candidates.push_back(std::move(gles)); + } else { + // Try to create a 3.1 core context + if (m_format.majorVersion() >= 3 && haveCreateContext) { + if (haveRobustness && haveContextPriority) { + auto robustCorePriority = std::unique_ptr(new EglContextAttributeBuilder); + robustCorePriority->setVersion(m_format.majorVersion(), m_format.minorVersion()); + robustCorePriority->setRobust(true); + robustCorePriority->setForwardCompatible(true); + if (m_format.profile() == QSurfaceFormat::CoreProfile) { + robustCorePriority->setCoreProfile(true); + } else if (m_format.profile() == QSurfaceFormat::CompatibilityProfile) { + robustCorePriority->setCompatibilityProfile(true); + } + robustCorePriority->setHighPriority(true); + candidates.push_back(std::move(robustCorePriority)); + } + if (haveRobustness) { + auto robustCore = std::unique_ptr(new EglContextAttributeBuilder); + robustCore->setVersion(m_format.majorVersion(), m_format.minorVersion()); + robustCore->setRobust(true); + robustCore->setForwardCompatible(true); + if (m_format.profile() == QSurfaceFormat::CoreProfile) { + robustCore->setCoreProfile(true); + } else if (m_format.profile() == QSurfaceFormat::CompatibilityProfile) { + robustCore->setCompatibilityProfile(true); + } + candidates.push_back(std::move(robustCore)); + } + if (haveContextPriority) { + auto corePriority = std::unique_ptr(new EglContextAttributeBuilder); + corePriority->setVersion(m_format.majorVersion(), m_format.minorVersion()); + corePriority->setForwardCompatible(true); + if (m_format.profile() == QSurfaceFormat::CoreProfile) { + corePriority->setCoreProfile(true); + } else if (m_format.profile() == QSurfaceFormat::CompatibilityProfile) { + corePriority->setCompatibilityProfile(true); + } + corePriority->setHighPriority(true); + candidates.push_back(std::move(corePriority)); + } + auto core = std::unique_ptr(new EglContextAttributeBuilder); + core->setVersion(m_format.majorVersion(), m_format.minorVersion()); + core->setForwardCompatible(true); + if (m_format.profile() == QSurfaceFormat::CoreProfile) { + core->setCoreProfile(true); + } else if (m_format.profile() == QSurfaceFormat::CompatibilityProfile) { + core->setCompatibilityProfile(true); + } + candidates.push_back(std::move(core)); + } + if (haveRobustness && haveCreateContext && haveContextPriority) { + auto robustPriority = std::unique_ptr(new EglContextAttributeBuilder); + robustPriority->setRobust(true); + robustPriority->setHighPriority(true); + candidates.push_back(std::move(robustPriority)); + } + if (haveRobustness && haveCreateContext) { + auto robust = std::unique_ptr(new EglContextAttributeBuilder); + robust->setRobust(true); + candidates.push_back(std::move(robust)); + } + candidates.emplace_back(new EglContextAttributeBuilder); + } + + EGLContext context = EGL_NO_CONTEXT; + for (auto it = candidates.begin(); it != candidates.end(); it++) { + const auto attribs = (*it)->build(); + context = eglCreateContext(eglDisplay(), m_config, shareContext, attribs.data()); + if (context != EGL_NO_CONTEXT) { + qCDebug(KWIN_QPA) << "Created EGL context with attributes:" << (*it).get(); + break; + } + } + + if (context == EGL_NO_CONTEXT) { + qCWarning(KWIN_QPA) << "Failed to create EGL context"; + return; + } + m_context = context; +} + +} // namespace QPA +} // namespace KWin diff --git a/plugins/qpa/eglplatformcontext.h b/plugins/qpa/eglplatformcontext.h new file mode 100644 index 0000000..b582d84 --- /dev/null +++ b/plugins/qpa/eglplatformcontext.h @@ -0,0 +1,51 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include "fixqopengl.h" +#include +#include + +namespace KWin +{ +namespace QPA +{ + +class EGLPlatformContext : public QPlatformOpenGLContext +{ +public: + EGLPlatformContext(QOpenGLContext *context, EGLDisplay display); + ~EGLPlatformContext() override; + + bool makeCurrent(QPlatformSurface *surface) override; + void doneCurrent() override; + QSurfaceFormat format() const override; + bool isValid() const override; + bool isSharing() const override; + GLuint defaultFramebufferObject(QPlatformSurface *surface) const override; + QFunctionPointer getProcAddress(const char *procName) override; + void swapBuffers(QPlatformSurface *surface) override; + + EGLDisplay eglDisplay() const; + EGLContext eglContext() const; + +private: + void create(const QSurfaceFormat &format, EGLContext shareContext); + + EGLDisplay m_eglDisplay; + EGLConfig m_config = EGL_NO_CONFIG_KHR; + EGLContext m_context = EGL_NO_CONTEXT; + QSurfaceFormat m_format; +}; + +} // namespace QPA +} // namespace KWin diff --git a/plugins/qpa/integration.cpp b/plugins/qpa/integration.cpp new file mode 100644 index 0000000..ace782a --- /dev/null +++ b/plugins/qpa/integration.cpp @@ -0,0 +1,159 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "integration.h" +#include "backingstore.h" +#include "eglplatformcontext.h" +#include "logging.h" +#include "offscreensurface.h" +#include "screen.h" +#include "window.h" +#include "../../main.h" +#include "../../platform.h" +#include "../../screens.h" + +#include +#include + +#include +#include + +#include +#include +#include + +namespace KWin +{ + +namespace QPA +{ + +Integration::Integration() + : QObject() + , QPlatformIntegration() + , m_fontDb(new QGenericUnixFontDatabase()) +{ +} + +Integration::~Integration() +{ + for (QPlatformScreen *platformScreen : m_screens) { + QWindowSystemInterface::handleScreenRemoved(platformScreen); + } +} + +bool Integration::hasCapability(Capability cap) const +{ + switch (cap) { + case ThreadedPixmaps: + return true; + case OpenGL: + return true; + case ThreadedOpenGL: + return false; + case BufferQueueingOpenGL: + return false; + case MultipleWindows: + case NonFullScreenWindows: + return true; + case RasterGLSurface: + return false; + default: + return QPlatformIntegration::hasCapability(cap); + } +} + +void Integration::initialize() +{ + connect(kwinApp(), &Application::screensCreated, this, + [this] { + connect(screens(), &Screens::changed, this, &Integration::initScreens); + initScreens(); + } + ); + QPlatformIntegration::initialize(); + auto dummyScreen = new Screen(-1); + QWindowSystemInterface::handleScreenAdded(dummyScreen); + m_screens << dummyScreen; +} + +QAbstractEventDispatcher *Integration::createEventDispatcher() const +{ + return new QUnixEventDispatcherQPA; +} + +QPlatformBackingStore *Integration::createPlatformBackingStore(QWindow *window) const +{ + return new BackingStore(window); +} + +QPlatformWindow *Integration::createPlatformWindow(QWindow *window) const +{ + return new Window(window); +} + +QPlatformOffscreenSurface *Integration::createPlatformOffscreenSurface(QOffscreenSurface *surface) const +{ + return new OffscreenSurface(surface); +} + +QPlatformFontDatabase *Integration::fontDatabase() const +{ + return m_fontDb.data(); +} + +QPlatformTheme *Integration::createPlatformTheme(const QString &name) const +{ + return QGenericUnixTheme::createUnixTheme(name); +} + +QStringList Integration::themeNames() const +{ + if (qEnvironmentVariableIsSet("KDE_FULL_SESSION")) { + return QStringList({QStringLiteral("kde")}); + } + return QStringList({QLatin1String(QGenericUnixTheme::name)}); +} + +QPlatformOpenGLContext *Integration::createPlatformOpenGLContext(QOpenGLContext *context) const +{ + if (kwinApp()->platform()->sceneEglGlobalShareContext() == EGL_NO_CONTEXT) { + qCWarning(KWIN_QPA) << "Attempting to create a QOpenGLContext before the scene is initialized"; + return nullptr; + } + const EGLDisplay eglDisplay = kwinApp()->platform()->sceneEglDisplay(); + if (eglDisplay != EGL_NO_DISPLAY) { + EGLPlatformContext *platformContext = new EGLPlatformContext(context, eglDisplay); + return platformContext; + } + return nullptr; +} + +void Integration::initScreens() +{ + QVector newScreens; + newScreens.reserve(qMax(screens()->count(), 1)); + for (int i = 0; i < screens()->count(); i++) { + auto screen = new Screen(i); + QWindowSystemInterface::handleScreenAdded(screen); + newScreens << screen; + } + if (newScreens.isEmpty()) { + auto dummyScreen = new Screen(-1); + QWindowSystemInterface::handleScreenAdded(dummyScreen); + newScreens << dummyScreen; + } + while (!m_screens.isEmpty()) { + QWindowSystemInterface::handleScreenRemoved(m_screens.takeLast()); + } + m_screens = newScreens; +} + +} +} diff --git a/plugins/qpa/integration.h b/plugins/qpa/integration.h new file mode 100644 index 0000000..03c37a1 --- /dev/null +++ b/plugins/qpa/integration.h @@ -0,0 +1,58 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_QPA_INTEGRATION_H +#define KWIN_QPA_INTEGRATION_H + +#include +#include "fixqopengl.h" + +#include +#include +#include + +namespace KWin +{ +namespace QPA +{ + +class Screen; + +class Integration : public QObject, public QPlatformIntegration +{ + Q_OBJECT +public: + explicit Integration(); + ~Integration() override; + + bool hasCapability(Capability cap) const override; + QPlatformWindow *createPlatformWindow(QWindow *window) const override; + QPlatformOffscreenSurface *createPlatformOffscreenSurface(QOffscreenSurface *surface) const override; + QPlatformBackingStore *createPlatformBackingStore(QWindow *window) const override; + QAbstractEventDispatcher *createEventDispatcher() const override; + QPlatformFontDatabase *fontDatabase() const override; + QStringList themeNames() const override; + QPlatformTheme *createPlatformTheme(const QString &name) const override; + QPlatformOpenGLContext *createPlatformOpenGLContext(QOpenGLContext *context) const override; + + void initialize() override; + +private: + void initScreens(); + + QScopedPointer m_fontDb; + QPlatformNativeInterface *m_nativeInterface; + Screen *m_dummyScreen = nullptr; + QVector m_screens; +}; + +} +} + +#endif diff --git a/plugins/qpa/kwin.json b/plugins/qpa/kwin.json new file mode 100644 index 0000000..d820f2a --- /dev/null +++ b/plugins/qpa/kwin.json @@ -0,0 +1,3 @@ +{ + "Keys": [ "wayland-org.kde.kwin.qpa" ] +} diff --git a/plugins/qpa/main.cpp b/plugins/qpa/main.cpp new file mode 100644 index 0000000..efd236b --- /dev/null +++ b/plugins/qpa/main.cpp @@ -0,0 +1,37 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "integration.h" +#include + +#include + +class KWinIntegrationPlugin : public QPlatformIntegrationPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID QPlatformIntegrationFactoryInterface_iid FILE "kwin.json") +public: + using QPlatformIntegrationPlugin::create; + QPlatformIntegration *create(const QString &system, const QStringList ¶mList) override; +}; + +QPlatformIntegration *KWinIntegrationPlugin::create(const QString &system, const QStringList ¶mList) +{ + Q_UNUSED(paramList) + if (!QCoreApplication::applicationFilePath().endsWith(QLatin1String("kwin_wayland")) && !qEnvironmentVariableIsSet("KWIN_FORCE_OWN_QPA")) { + // Not KWin + return nullptr; + } + if (system.compare(QLatin1String("wayland-org.kde.kwin.qpa"), Qt::CaseInsensitive) == 0) { + // create our integration + return new KWin::QPA::Integration; + } + return nullptr; +} + +#include "main.moc" diff --git a/plugins/qpa/offscreensurface.cpp b/plugins/qpa/offscreensurface.cpp new file mode 100644 index 0000000..a44bda5 --- /dev/null +++ b/plugins/qpa/offscreensurface.cpp @@ -0,0 +1,71 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "offscreensurface.h" +#include "eglhelpers.h" +#include "main.h" +#include "platform.h" + +#include + +namespace KWin +{ +namespace QPA +{ + +OffscreenSurface::OffscreenSurface(QOffscreenSurface *surface) + : QPlatformOffscreenSurface(surface) + , m_eglDisplay(kwinApp()->platform()->sceneEglDisplay()) +{ + const QSize size = surface->size(); + + EGLConfig config = configFromFormat(m_eglDisplay, surface->requestedFormat(), EGL_PBUFFER_BIT); + if (config == EGL_NO_CONFIG_KHR) { + return; + } + + const EGLint attributes[] = { + EGL_WIDTH, size.width(), + EGL_HEIGHT, size.height(), + EGL_NONE + }; + + m_surface = eglCreatePbufferSurface(m_eglDisplay, config, attributes); + if (m_surface == EGL_NO_SURFACE) { + return; + } + + // Requested and actual surface format might be different. + m_format = formatFromConfig(m_eglDisplay, config); +} + +OffscreenSurface::~OffscreenSurface() +{ + if (m_surface != EGL_NO_SURFACE) { + eglDestroySurface(m_eglDisplay, m_surface); + } +} + +QSurfaceFormat OffscreenSurface::format() const +{ + return m_format; +} + +bool OffscreenSurface::isValid() const +{ + return m_surface != EGL_NO_SURFACE; +} + +EGLSurface OffscreenSurface::eglSurface() const +{ + return m_surface; +} + +} // namespace QPA +} // namespace KWin diff --git a/plugins/qpa/offscreensurface.h b/plugins/qpa/offscreensurface.h new file mode 100644 index 0000000..9a17e0e --- /dev/null +++ b/plugins/qpa/offscreensurface.h @@ -0,0 +1,42 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include "fixqopengl.h" +#include + +#include + +namespace KWin +{ +namespace QPA +{ + +class OffscreenSurface : public QPlatformOffscreenSurface +{ +public: + explicit OffscreenSurface(QOffscreenSurface *surface); + ~OffscreenSurface() override; + + QSurfaceFormat format() const override; + bool isValid() const override; + + EGLSurface eglSurface() const; + +private: + QSurfaceFormat m_format; + + EGLDisplay m_eglDisplay = EGL_NO_DISPLAY; + EGLSurface m_surface = EGL_NO_SURFACE; +}; + +} // namespace QPA +} // namespace KWin diff --git a/plugins/qpa/platformcursor.cpp b/plugins/qpa/platformcursor.cpp new file mode 100644 index 0000000..49ae91e --- /dev/null +++ b/plugins/qpa/platformcursor.cpp @@ -0,0 +1,42 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "platformcursor.h" +#include "../../cursor.h" + +namespace KWin +{ +namespace QPA +{ + +PlatformCursor::PlatformCursor() + : QPlatformCursor() +{ +} + +PlatformCursor::~PlatformCursor() = default; + +QPoint PlatformCursor::pos() const +{ + return Cursors::self()->mouse()->pos(); +} + +void PlatformCursor::setPos(const QPoint &pos) +{ + Cursors::self()->mouse()->setPos(pos); +} + +void PlatformCursor::changeCursor(QCursor *windowCursor, QWindow *window) +{ + Q_UNUSED(windowCursor) + Q_UNUSED(window) + // TODO: implement +} + +} +} diff --git a/plugins/qpa/platformcursor.h b/plugins/qpa/platformcursor.h new file mode 100644 index 0000000..11b2fd3 --- /dev/null +++ b/plugins/qpa/platformcursor.h @@ -0,0 +1,32 @@ +/* + 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 KWIN_QPA_PLATFORMCURSOR_H +#define KWIN_QPA_PLATFORMCURSOR_H + +#include + +namespace KWin +{ +namespace QPA +{ + +class PlatformCursor : public QPlatformCursor +{ +public: + PlatformCursor(); + ~PlatformCursor() override; + QPoint pos() const override; + void setPos(const QPoint &pos) override; + void changeCursor(QCursor *windowCursor, QWindow *window) override; +}; + +} +} + +#endif diff --git a/plugins/qpa/screen.cpp b/plugins/qpa/screen.cpp new file mode 100644 index 0000000..95719c8 --- /dev/null +++ b/plugins/qpa/screen.cpp @@ -0,0 +1,68 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "screen.h" +#include "platformcursor.h" +#include "screens.h" + +namespace KWin +{ +namespace QPA +{ + +Screen::Screen(int screen) + : QPlatformScreen() + , m_screen(screen) + , m_cursor(new PlatformCursor) +{ +} + +Screen::~Screen() = default; + +int Screen::depth() const +{ + return 32; +} + +QImage::Format Screen::format() const +{ + return QImage::Format_ARGB32_Premultiplied; +} + +QRect Screen::geometry() const +{ + return m_screen != -1 ? screens()->geometry(m_screen) : QRect(0, 0, 1, 1); +} + +QSizeF Screen::physicalSize() const +{ + return m_screen != -1 ? screens()->physicalSize(m_screen) : QPlatformScreen::physicalSize(); +} + +QPlatformCursor *Screen::cursor() const +{ + return m_cursor.data(); +} + +QDpi Screen::logicalDpi() const +{ + static int forceDpi = qEnvironmentVariableIsSet("QT_WAYLAND_FORCE_DPI") ? qEnvironmentVariableIntValue("QT_WAYLAND_FORCE_DPI") : -1; + if (forceDpi > 0) { + return QDpi(forceDpi, forceDpi); + } + + return QDpi(96, 96); +} + +qreal Screen::devicePixelRatio() const +{ + return m_screen != -1 ? screens()->scale(m_screen) : 1.0; +} + +} +} diff --git a/plugins/qpa/screen.h b/plugins/qpa/screen.h new file mode 100644 index 0000000..c21ce5d --- /dev/null +++ b/plugins/qpa/screen.h @@ -0,0 +1,43 @@ +/* + 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_QPA_SCREEN_H +#define KWIN_QPA_SCREEN_H + +#include +#include + +namespace KWin +{ +namespace QPA +{ +class PlatformCursor; + +class Screen : public QPlatformScreen +{ +public: + explicit Screen(int screen); + ~Screen() override; + + QRect geometry() const override; + int depth() const override; + QImage::Format format() const override; + QSizeF physicalSize() const override; + QPlatformCursor *cursor() const override; + QDpi logicalDpi() const override; + qreal devicePixelRatio() const override; + +private: + int m_screen; + QScopedPointer m_cursor; +}; + +} +} + +#endif diff --git a/plugins/qpa/window.cpp b/plugins/qpa/window.cpp new file mode 100644 index 0000000..4996e7a --- /dev/null +++ b/plugins/qpa/window.cpp @@ -0,0 +1,195 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "window.h" +#include "eglhelpers.h" +#include "platform.h" +#include "screens.h" + +#include "internal_client.h" + +#include + +#include +#include + +namespace KWin +{ +namespace QPA +{ +static quint32 s_windowId = 0; + +Window::Window(QWindow *window) + : QPlatformWindow(window) + , m_eglDisplay(kwinApp()->platform()->sceneEglDisplay()) + , m_windowId(++s_windowId) + , m_scale(screens()->maxScale()) +{ + if (window->surfaceType() == QSurface::OpenGLSurface) { + // The window will use OpenGL for drawing. + if (!kwinApp()->platform()->supportsSurfacelessContext()) { + createPbuffer(); + } + } +} + +Window::~Window() +{ + if (m_eglSurface != EGL_NO_SURFACE) { + eglDestroySurface(m_eglDisplay, m_eglSurface); + } + unmap(); +} + +void Window::setVisible(bool visible) +{ + if (visible) { + map(); + } else { + unmap(); + } + + QPlatformWindow::setVisible(visible); +} + +QSurfaceFormat Window::format() const +{ + return m_format; +} + +void Window::setGeometry(const QRect &rect) +{ + const QRect &oldRect = geometry(); + QPlatformWindow::setGeometry(rect); + if (rect.x() != oldRect.x()) { + emit window()->xChanged(rect.x()); + } + if (rect.y() != oldRect.y()) { + emit window()->yChanged(rect.y()); + } + if (rect.width() != oldRect.width()) { + emit window()->widthChanged(rect.width()); + } + if (rect.height() != oldRect.height()) { + emit window()->heightChanged(rect.height()); + } + + const QSize nativeSize = rect.size() * m_scale; + + if (m_contentFBO) { + if (m_contentFBO->size() != nativeSize) { + m_resized = true; + } + } + QWindowSystemInterface::handleGeometryChange(window(), geometry()); +} + +WId Window::winId() const +{ + return m_windowId; +} + +qreal Window::devicePixelRatio() const +{ + return m_scale; +} + +void Window::bindContentFBO() +{ + if (m_resized || !m_contentFBO) { + createFBO(); + } + m_contentFBO->bind(); +} + +const QSharedPointer &Window::contentFBO() const +{ + return m_contentFBO; +} + +QSharedPointer Window::swapFBO() +{ + QSharedPointer fbo = m_contentFBO; + m_contentFBO.clear(); + return fbo; +} + +InternalClient *Window::client() const +{ + return m_handle; +} + +void Window::createFBO() +{ + const QRect &r = geometry(); + if (m_contentFBO && r.size().isEmpty()) { + return; + } + const QSize nativeSize = r.size() * m_scale; + m_contentFBO.reset(new QOpenGLFramebufferObject(nativeSize.width(), nativeSize.height(), QOpenGLFramebufferObject::CombinedDepthStencil)); + if (!m_contentFBO->isValid()) { + qCWarning(KWIN_QPA) << "Content FBO is not valid"; + } + m_resized = false; +} + +void Window::createPbuffer() +{ + const QSurfaceFormat requestedFormat = window()->requestedFormat(); + const EGLConfig config = configFromFormat(m_eglDisplay, + requestedFormat, + EGL_PBUFFER_BIT); + if (config == EGL_NO_CONFIG_KHR) { + qCWarning(KWIN_QPA) << "Could not find any EGL config for:" << requestedFormat; + return; + } + + // The size doesn't matter as we render into a framebuffer object. + const EGLint attribs[] = { + EGL_WIDTH, 16, + EGL_HEIGHT, 16, + EGL_NONE + }; + + m_eglSurface = eglCreatePbufferSurface(m_eglDisplay, config, attribs); + if (m_eglSurface != EGL_NO_SURFACE) { + m_format = formatFromConfig(m_eglDisplay, config); + } else { + qCWarning(KWIN_QPA, "Failed to create a pbuffer for window: 0x%x", eglGetError()); + } +} + +void Window::map() +{ + if (m_handle) { + return; + } + + m_handle = new InternalClient(window()); +} + +void Window::unmap() +{ + if (!m_handle) { + return; + } + + m_handle->destroyClient(); + m_handle = nullptr; + + m_contentFBO = nullptr; +} + +EGLSurface Window::eglSurface() const +{ + return m_eglSurface; +} + +} +} diff --git a/plugins/qpa/window.h b/plugins/qpa/window.h new file mode 100644 index 0000000..c1a7e75 --- /dev/null +++ b/plugins/qpa/window.h @@ -0,0 +1,68 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_QPA_WINDOW_H +#define KWIN_QPA_WINDOW_H + +#include +#include "fixqopengl.h" +#include + +#include +#include + +class QOpenGLFramebufferObject; + +namespace KWin +{ + +class InternalClient; + +namespace QPA +{ + +class Window : public QPlatformWindow +{ +public: + explicit Window(QWindow *window); + ~Window() override; + + QSurfaceFormat format() const override; + void setVisible(bool visible) override; + void setGeometry(const QRect &rect) override; + WId winId() const override; + qreal devicePixelRatio() const override; + + void bindContentFBO(); + const QSharedPointer &contentFBO() const; + QSharedPointer swapFBO(); + + InternalClient *client() const; + EGLSurface eglSurface() const; + +private: + void createFBO(); + void createPbuffer(); + void map(); + void unmap(); + + QSurfaceFormat m_format; + QPointer m_handle; + QSharedPointer m_contentFBO; + EGLDisplay m_eglDisplay = EGL_NO_DISPLAY; + EGLSurface m_eglSurface = EGL_NO_SURFACE; + quint32 m_windowId; + bool m_resized = false; + int m_scale = 1; +}; + +} +} + +#endif diff --git a/plugins/scenes/CMakeLists.txt b/plugins/scenes/CMakeLists.txt new file mode 100644 index 0000000..5c55688 --- /dev/null +++ b/plugins/scenes/CMakeLists.txt @@ -0,0 +1,5 @@ +add_subdirectory(opengl) +add_subdirectory(qpainter) +if (KWIN_BUILD_XRENDER_COMPOSITING) + add_subdirectory(xrender) +endif() diff --git a/plugins/scenes/opengl/CMakeLists.txt b/plugins/scenes/opengl/CMakeLists.txt new file mode 100644 index 0000000..19e85f7 --- /dev/null +++ b/plugins/scenes/opengl/CMakeLists.txt @@ -0,0 +1,32 @@ +set(SCENE_OPENGL_SRCS + lanczosfilter.cpp + scene_opengl.cpp +) + +include(ECMQtDeclareLoggingCategory) +ecm_qt_declare_logging_category( + SCENE_OPENGL_SRCS HEADER + logging.h + IDENTIFIER + KWIN_OPENGL + CATEGORY_NAME + kwin_scene_opengl + DEFAULT_SEVERITY + Critical +) + +qt5_add_resources(SCENE_OPENGL_SRCS resources.qrc) + +add_library(KWinSceneOpenGL MODULE ${SCENE_OPENGL_SRCS}) +set_target_properties(KWinSceneOpenGL PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/org.kde.kwin.scenes/") +target_link_libraries(KWinSceneOpenGL + kwin + SceneOpenGLBackend +) + +install( + TARGETS + KWinSceneOpenGL + DESTINATION + ${PLUGIN_INSTALL_DIR}/org.kde.kwin.scenes/ +) diff --git a/plugins/scenes/opengl/lanczosfilter.cpp b/plugins/scenes/opengl/lanczosfilter.cpp new file mode 100644 index 0000000..3d63a00 --- /dev/null +++ b/plugins/scenes/opengl/lanczosfilter.cpp @@ -0,0 +1,414 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2010 Fredrik Höglund + SPDX-FileCopyrightText: 2010 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "lanczosfilter.h" +#include "x11client.h" +#include "deleted.h" +#include "effects.h" +#include "screens.h" +#include "unmanaged.h" +#include "options.h" +#include "workspace.h" + +#include + +#include +#include + +#include + +#include +#include + +#include + +namespace KWin +{ + +LanczosFilter::LanczosFilter(Scene *parent) + : QObject(parent) + , m_offscreenTex(nullptr) + , m_offscreenTarget(nullptr) + , m_inited(false) + , m_shader(nullptr) + , m_uOffsets(0) + , m_uKernel(0) + , m_scene(parent) +{ +} + +LanczosFilter::~LanczosFilter() +{ + delete m_offscreenTarget; + delete m_offscreenTex; +} + +void LanczosFilter::init() +{ + if (m_inited) + return; + m_inited = true; + const bool force = (qstrcmp(qgetenv("KWIN_FORCE_LANCZOS"), "1") == 0); + if (force) { + qCWarning(KWIN_OPENGL) << "Lanczos Filter forced on by environment variable"; + } + + if (!force && options->glSmoothScale() != 2) + return; // disabled by config + if (!GLRenderTarget::supported()) + return; + + GLPlatform *gl = GLPlatform::instance(); + if (!force) { + // The lanczos filter is reported to be broken with the Intel driver prior SandyBridge + if (gl->driver() == Driver_Intel && gl->chipClass() < SandyBridge) + return; + // also radeon before R600 has trouble + if (gl->isRadeon() && gl->chipClass() < R600) + return; + // and also for software emulation (e.g. llvmpipe) + if (gl->isSoftwareEmulation()) { + return; + } + } + QFile ff(gl->glslVersion() >= kVersionNumber(1, 40) ? + QStringLiteral(":/scenes/opengl/shaders/1.40/lanczos-fragment.glsl") : + QStringLiteral(":/scenes/opengl/shaders/1.10/lanczos-fragment.glsl")); + if (!ff.open(QIODevice::ReadOnly)) { + qCDebug(KWIN_OPENGL) << "Failed to open lanczos shader"; + return; + } + m_shader.reset(ShaderManager::instance()->generateCustomShader(ShaderTrait::MapTexture, QByteArray(), ff.readAll())); + if (m_shader->isValid()) { + ShaderBinder binder(m_shader.data()); + m_uKernel = m_shader->uniformLocation("kernel"); + m_uOffsets = m_shader->uniformLocation("offsets"); + } else { + qCDebug(KWIN_OPENGL) << "Shader is not valid"; + m_shader.reset(); + } +} + + +void LanczosFilter::updateOffscreenSurfaces() +{ + const QSize &s = screens()->size(); + int w = s.width(); + int h = s.height(); + + if (!m_offscreenTex || m_offscreenTex->width() != w || m_offscreenTex->height() != h) { + if (m_offscreenTex) { + delete m_offscreenTex; + delete m_offscreenTarget; + } + m_offscreenTex = new GLTexture(GL_RGBA8, w, h); + m_offscreenTex->setFilter(GL_LINEAR); + m_offscreenTex->setWrapMode(GL_CLAMP_TO_EDGE); + m_offscreenTarget = new GLRenderTarget(*m_offscreenTex); + } +} + +static float sinc(float x) +{ + return std::sin(x * M_PI) / (x * M_PI); +} + +static float lanczos(float x, float a) +{ + if (qFuzzyCompare(x + 1.0, 1.0)) + return 1.0; + + if (qAbs(x) >= a) + return 0.0; + + return sinc(x) * sinc(x / a); +} + +void LanczosFilter::createKernel(float delta, int *size) +{ + const float a = 2.0; + + // The two outermost samples always fall at points where the lanczos + // function returns 0, so we'll skip them. + const int sampleCount = qBound(3, qCeil(delta * a) * 2 + 1 - 2, 29); + const int center = sampleCount / 2; + const int kernelSize = center + 1; + const float factor = 1.0 / delta; + + QVector values(kernelSize); + float sum = 0; + + for (int i = 0; i < kernelSize; i++) { + const float val = lanczos(i * factor, a); + sum += i > 0 ? val * 2 : val; + values[i] = val; + } + + m_kernel.fill(QVector4D()); + + // Normalize the kernel + for (int i = 0; i < kernelSize; i++) { + const float val = values[i] / sum; + m_kernel[i] = QVector4D(val, val, val, val); + } + + *size = kernelSize; +} + +void LanczosFilter::createOffsets(int count, float width, Qt::Orientation direction) +{ + m_offsets.fill(QVector2D()); + for (int i = 0; i < count; i++) { + m_offsets[i] = (direction == Qt::Horizontal) ? + QVector2D(i / width, 0) : QVector2D(0, i / width); + } +} + +void LanczosFilter::performPaint(EffectWindowImpl* w, int mask, QRegion region, WindowPaintData& data) +{ + if (data.xScale() < 0.9 || data.yScale() < 0.9) { + if (!m_inited) + init(); + const QRect screenRect = Workspace::self()->clientArea(ScreenArea, w->screen(), w->desktop()); + // window geometry may not be bigger than screen geometry to fit into the FBO + QRect winGeo(w->expandedGeometry()); + if (m_shader && winGeo.width() <= screenRect.width() && winGeo.height() <= screenRect.height()) { + winGeo.translate(-w->geometry().topLeft()); + double left = winGeo.left(); + double top = winGeo.top(); + double width = winGeo.right() - left; + double height = winGeo.bottom() - top; + + int tx = data.xTranslation() + w->x() + left * data.xScale(); + int ty = data.yTranslation() + w->y() + top * data.yScale(); + int tw = width * data.xScale(); + int th = height * data.yScale(); + const QRect textureRect(tx, ty, tw, th); + const bool hardwareClipping = !(QRegion(textureRect)-region).isEmpty(); + + int sw = width; + int sh = height; + + GLTexture *cachedTexture = static_cast< GLTexture*>(w->data(LanczosCacheRole).value()); + if (cachedTexture) { + if (cachedTexture->width() == tw && cachedTexture->height() == th) { + cachedTexture->bind(); + if (hardwareClipping) { + glEnable(GL_SCISSOR_TEST); + } + + glEnable(GL_BLEND); + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + + const qreal rgb = data.brightness() * data.opacity(); + const qreal a = data.opacity(); + + ShaderBinder binder(ShaderTrait::MapTexture | ShaderTrait::Modulate | ShaderTrait::AdjustSaturation); + GLShader *shader = binder.shader(); + QMatrix4x4 mvp = data.screenProjectionMatrix(); + mvp.translate(textureRect.x(), textureRect.y()); + shader->setUniform(GLShader::ModelViewProjectionMatrix, mvp); + shader->setUniform(GLShader::ModulationConstant, QVector4D(rgb, rgb, rgb, a)); + shader->setUniform(GLShader::Saturation, data.saturation()); + + cachedTexture->render(region, textureRect, hardwareClipping); + + glDisable(GL_BLEND); + if (hardwareClipping) { + glDisable(GL_SCISSOR_TEST); + } + cachedTexture->unbind(); + m_timer.start(5000, this); + return; + } else { + // offscreen texture not matching - delete + delete cachedTexture; + cachedTexture = nullptr; + w->setData(LanczosCacheRole, QVariant()); + } + } + + WindowPaintData thumbData = data; + thumbData.setXScale(1.0); + thumbData.setYScale(1.0); + thumbData.setXTranslation(-w->x() - left); + thumbData.setYTranslation(-w->y() - top); + thumbData.setBrightness(1.0); + thumbData.setOpacity(1.0); + thumbData.setSaturation(1.0); + + // Bind the offscreen FBO and draw the window on it unscaled + updateOffscreenSurfaces(); + GLRenderTarget::pushRenderTarget(m_offscreenTarget); + + QMatrix4x4 modelViewProjectionMatrix; + modelViewProjectionMatrix.ortho(0, m_offscreenTex->width(), m_offscreenTex->height(), 0 , 0, 65535); + thumbData.setProjectionMatrix(modelViewProjectionMatrix); + + glClearColor(0.0, 0.0, 0.0, 0.0); + glClear(GL_COLOR_BUFFER_BIT); + w->sceneWindow()->performPaint(mask, infiniteRegion(), thumbData); + + // Create a scratch texture and copy the rendered window into it + GLTexture tex(GL_RGBA8, sw, sh); + tex.setFilter(GL_LINEAR); + tex.setWrapMode(GL_CLAMP_TO_EDGE); + tex.bind(); + + glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, m_offscreenTex->height() - sh, sw, sh); + + // Set up the shader for horizontal scaling + float dx = sw / float(tw); + int kernelSize; + createKernel(dx, &kernelSize); + createOffsets(kernelSize, sw, Qt::Horizontal); + + ShaderManager::instance()->pushShader(m_shader.data()); + m_shader->setUniform(GLShader::ModelViewProjectionMatrix, modelViewProjectionMatrix); + setUniforms(); + + // Draw the window back into the FBO, this time scaled horizontally + glClear(GL_COLOR_BUFFER_BIT); + QVector verts; + QVector texCoords; + verts.reserve(12); + texCoords.reserve(12); + + texCoords << 1.0 << 0.0; verts << tw << 0.0; // Top right + texCoords << 0.0 << 0.0; verts << 0.0 << 0.0; // Top left + texCoords << 0.0 << 1.0; verts << 0.0 << sh; // Bottom left + texCoords << 0.0 << 1.0; verts << 0.0 << sh; // Bottom left + texCoords << 1.0 << 1.0; verts << tw << sh; // Bottom right + texCoords << 1.0 << 0.0; verts << tw << 0.0; // Top right + GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer(); + vbo->reset(); + vbo->setData(6, 2, verts.constData(), texCoords.constData()); + vbo->render(GL_TRIANGLES); + + // At this point we don't need the scratch texture anymore + tex.unbind(); + tex.discard(); + + // create scratch texture for second rendering pass + GLTexture tex2(GL_RGBA8, tw, sh); + tex2.setFilter(GL_LINEAR); + tex2.setWrapMode(GL_CLAMP_TO_EDGE); + tex2.bind(); + + glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, m_offscreenTex->height() - sh, tw, sh); + + // Set up the shader for vertical scaling + float dy = sh / float(th); + createKernel(dy, &kernelSize); + createOffsets(kernelSize, m_offscreenTex->height(), Qt::Vertical); + setUniforms(); + + // Now draw the horizontally scaled window in the FBO at the right + // coordinates on the screen, while scaling it vertically and blending it. + glClear(GL_COLOR_BUFFER_BIT); + + verts.clear(); + + verts << tw << 0.0; // Top right + verts << 0.0 << 0.0; // Top left + verts << 0.0 << th; // Bottom left + verts << 0.0 << th; // Bottom left + verts << tw << th; // Bottom right + verts << tw << 0.0; // Top right + vbo->setData(6, 2, verts.constData(), texCoords.constData()); + vbo->render(GL_TRIANGLES); + + tex2.unbind(); + tex2.discard(); + ShaderManager::instance()->popShader(); + + // create cache texture + GLTexture *cache = new GLTexture(GL_RGBA8, tw, th); + + cache->setFilter(GL_LINEAR); + cache->setWrapMode(GL_CLAMP_TO_EDGE); + cache->bind(); + glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, m_offscreenTex->height() - th, tw, th); + GLRenderTarget::popRenderTarget(); + + if (hardwareClipping) { + glEnable(GL_SCISSOR_TEST); + } + + glEnable(GL_BLEND); + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + + const qreal rgb = data.brightness() * data.opacity(); + const qreal a = data.opacity(); + + ShaderBinder binder(ShaderTrait::MapTexture | ShaderTrait::Modulate | ShaderTrait::AdjustSaturation); + GLShader *shader = binder.shader(); + QMatrix4x4 mvp = data.screenProjectionMatrix(); + mvp.translate(textureRect.x(), textureRect.y()); + shader->setUniform(GLShader::ModelViewProjectionMatrix, mvp); + shader->setUniform(GLShader::ModulationConstant, QVector4D(rgb, rgb, rgb, a)); + shader->setUniform(GLShader::Saturation, data.saturation()); + + cache->render(region, textureRect, hardwareClipping); + + glDisable(GL_BLEND); + + if (hardwareClipping) { + glDisable(GL_SCISSOR_TEST); + } + + cache->unbind(); + w->setData(LanczosCacheRole, QVariant::fromValue(static_cast(cache))); + + // Delete the offscreen surface after 5 seconds + m_timer.start(5000, this); + return; + } + } // if ( effects->compositingType() == KWin::OpenGLCompositing ) + w->sceneWindow()->performPaint(mask, region, data); +} // End of function + +void LanczosFilter::timerEvent(QTimerEvent *event) +{ + if (event->timerId() == m_timer.timerId()) { + m_timer.stop(); + + m_scene->makeOpenGLContextCurrent(); + + delete m_offscreenTarget; + delete m_offscreenTex; + m_offscreenTarget = nullptr; + m_offscreenTex = nullptr; + + workspace()->forEachToplevel([this](Toplevel *toplevel) { + discardCacheTexture(toplevel->effectWindow()); + }); + + m_scene->doneOpenGLContextCurrent(); + } +} + +void LanczosFilter::discardCacheTexture(EffectWindow *w) +{ + QVariant cachedTextureVariant = w->data(LanczosCacheRole); + if (cachedTextureVariant.isValid()) { + delete static_cast< GLTexture*>(cachedTextureVariant.value()); + w->setData(LanczosCacheRole, QVariant()); + } +} + +void LanczosFilter::setUniforms() +{ + glUniform2fv(m_uOffsets, m_offsets.size(), (const GLfloat*)m_offsets.data()); + glUniform4fv(m_uKernel, m_kernel.size(), (const GLfloat*)m_kernel.data()); +} + +} // namespace + diff --git a/plugins/scenes/opengl/lanczosfilter.h b/plugins/scenes/opengl/lanczosfilter.h new file mode 100644 index 0000000..1e54efc --- /dev/null +++ b/plugins/scenes/opengl/lanczosfilter.h @@ -0,0 +1,65 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2010 Fredrik Höglund + SPDX-FileCopyrightText: 2010 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_LANCZOSFILTER_P_H +#define KWIN_LANCZOSFILTER_P_H + +#include +#include +#include +#include +#include +#include + +namespace KWin +{ + +class EffectWindow; +class EffectWindowImpl; +class WindowPaintData; +class GLTexture; +class GLRenderTarget; +class GLShader; +class Scene; + +class LanczosFilter : public QObject +{ + Q_OBJECT + +public: + explicit LanczosFilter(Scene *parent); + ~LanczosFilter() override; + void performPaint(EffectWindowImpl* w, int mask, QRegion region, WindowPaintData& data); + +protected: + void timerEvent(QTimerEvent*) override; +private: + void init(); + void updateOffscreenSurfaces(); + void setUniforms(); + void discardCacheTexture(EffectWindow *w); + + void createKernel(float delta, int *kernelSize); + void createOffsets(int count, float width, Qt::Orientation direction); + GLTexture *m_offscreenTex; + GLRenderTarget *m_offscreenTarget; + QBasicTimer m_timer; + bool m_inited; + QScopedPointer m_shader; + int m_uOffsets; + int m_uKernel; + std::array m_offsets; + std::array m_kernel; + Scene *m_scene; +}; + +} // namespace + +#endif // KWIN_LANCZOSFILTER_P_H diff --git a/plugins/scenes/opengl/opengl.json b/plugins/scenes/opengl/opengl.json new file mode 100644 index 0000000..830c622 --- /dev/null +++ b/plugins/scenes/opengl/opengl.json @@ -0,0 +1,80 @@ +{ + "CompositingType": 1, + "KPlugin": { + "Description": "KWin Compositor plugin rendering through OpenGL", + "Description[az]": "OpenGL vasitəsi ilə qoşulmuş KWin birləşdirici modulunu formalaşdırmaq", + "Description[ca@valencia]": "Connector del Compositor de KWin que renderitza a través d'OpenGL", + "Description[ca]": "Connector del Compositor del KWin que renderitza a través de l'OpenGL", + "Description[da]": "KWin-compositorplugin som renderer via OpenGL", + "Description[de]": "KWin-Compositor-Modul zum Rendern mit OpenGL", + "Description[en_GB]": "KWin Compositor plugin rendering through OpenGL", + "Description[es]": "Complemento compositor de KWin renderizando mediante OpenGL", + "Description[et]": "KWini komposiitori plugin renderdamiseks OpenGL abil", + "Description[eu]": "Kwin konposatzailearen plugina OpenGL bidez errendatzen", + "Description[fi]": "OpenGL:llä hahmontava KWin-koostajaliitännäinen", + "Description[fr]": "Module du compositeur KWin effectuant le rendu avec OpenGL", + "Description[gl]": "Complemento de compositor de KWin que renderiza a través de OpenGL.", + "Description[hu]": "KWin összeállító bővítmény OpenGL leképezéssel", + "Description[id]": "Plugin KWin Compositor perenderan melalui OpenGL", + "Description[it]": "Estensione del compositore di KWin per la resa tramite OpenGL", + "Description[ko]": "OpenGL로 렌더링하는 KWin 컴포지터 플러그인", + "Description[lt]": "KWin kompozitoriaus priedas atvaizdavimui per OpenGL", + "Description[nl]": "KWin-compositor-plug-in rendering via OpenGL", + "Description[nn]": "KWin-samansetjartillegg som brukar OpenGL", + "Description[pl]": "Wtyczka kompozytora KWin wyświetlająca przez OpenGL", + "Description[pt]": "'Plugin' de Composição do KWin com desenho via OpenGL", + "Description[pt_BR]": "Plugin do compositor KWin renderizando pelo OpenGL", + "Description[ru]": "Отрисовка подключаемым модулем компоновщика KWin через OpenGL", + "Description[sk]": "Renderovací plugin kompozítora KWin cez OpenGL", + "Description[sl]": "Izrisovanje vstavka upravljalnika skladnje KWin preko OpenGL-ja", + "Description[sr@ijekavian]": "К‑винов прикључак слагача за рендеровање кроз опенГЛ", + "Description[sr@ijekavianlatin]": "KWinov priključak slagača za renderovanje kroz OpenGL", + "Description[sr@latin]": "KWinov priključak slagača za renderovanje kroz OpenGL", + "Description[sr]": "К‑винов прикључак слагача за рендеровање кроз опенГЛ", + "Description[sv]": "Kwin sammansättningsinsticksprogram Ã¥terger via OpenGL", + "Description[tr]": "OpenGL üzerinden gerçekleme yapan KWin Dizgici eklentisi", + "Description[uk]": "Додаток засобу композиції KWin для обробки з використанням OpenGL", + "Description[x-test]": "xxKWin Compositor plugin rendering through OpenGLxx", + "Description[zh_CN]": "使用 OpenGL 渲染的 KWin 混成插件", + "Description[zh_TW]": "透過 OpenGL 繪製 KWin 合成器附加元件", + "Id": "KWinSceneOpenGL", + "Name": "SceneOpenGL", + "Name[az]": "SceneOpenGL", + "Name[ca@valencia]": "SceneOpenGL", + "Name[ca]": "SceneOpenGL", + "Name[cs]": "SceneOpenGL", + "Name[da]": "SceneOpenGL", + "Name[de]": "SceneOpenGL", + "Name[en_GB]": "SceneOpenGL", + "Name[es]": "SceneOpenGL", + "Name[et]": "SceneOpenGL", + "Name[eu]": "SceneOpenGL", + "Name[fi]": "SceneOpenGL", + "Name[fr]": "SceneOpenGL", + "Name[gl]": "SceneOpenGL", + "Name[hu]": "SceneOpenGL", + "Name[id]": "SceneOpenGL", + "Name[it]": "SceneOpenGL", + "Name[ko]": "SceneOpenGL", + "Name[lt]": "SceneOpenGL", + "Name[nl]": "SceneOpenGL", + "Name[nn]": "SceneOpenGL", + "Name[pl]": "OpenGL sceny", + "Name[pt]": "SceneOpenGL", + "Name[pt_BR]": "SceneOpenGL", + "Name[ro]": "SceneOpenGL", + "Name[ru]": "SceneOpenGL", + "Name[sk]": "SceneOpenGL", + "Name[sl]": "SceneOpenGL", + "Name[sr@ijekavian]": "ОпенГЛ-сцена", + "Name[sr@ijekavianlatin]": "OpenGL-scena", + "Name[sr@latin]": "OpenGL-scena", + "Name[sr]": "ОпенГЛ-сцена", + "Name[sv]": "Scen OpenGL", + "Name[tr]": "SceneOpenGL", + "Name[uk]": "SceneOpenGL", + "Name[x-test]": "xxSceneOpenGLxx", + "Name[zh_CN]": "SceneOpenGL", + "Name[zh_TW]": "SceneOpenGL" + } +} diff --git a/plugins/scenes/opengl/resources.qrc b/plugins/scenes/opengl/resources.qrc new file mode 100644 index 0000000..8fbd27d --- /dev/null +++ b/plugins/scenes/opengl/resources.qrc @@ -0,0 +1,6 @@ + + + shaders/1.10/lanczos-fragment.glsl + shaders/1.40/lanczos-fragment.glsl + + diff --git a/plugins/scenes/opengl/scene_opengl.cpp b/plugins/scenes/opengl/scene_opengl.cpp new file mode 100644 index 0000000..8419514 --- /dev/null +++ b/plugins/scenes/opengl/scene_opengl.cpp @@ -0,0 +1,2766 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2009, 2010, 2011 Martin Gräßlin + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + Based on glcompmgr code by Felix Bellaby. + Using code from Compiz and Beryl. + + Explicit command stream synchronization based on the sample + implementation by James Jones , + + SPDX-FileCopyrightText: 2011 NVIDIA Corporation + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "scene_opengl.h" + +#include "platform.h" +#include "wayland_server.h" +#include "platformsupport/scenes/opengl/texture.h" + +#include +#include + +#include "utils.h" +#include "x11client.h" +#include "composite.h" +#include "deleted.h" +#include "effects.h" +#include "lanczosfilter.h" +#include "main.h" +#include "overlaywindow.h" +#include "screens.h" +#include "cursor.h" +#include "decorations/decoratedclient.h" +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +// HACK: workaround for libepoxy < 1.3 +#ifndef GL_GUILTY_CONTEXT_RESET +#define GL_GUILTY_CONTEXT_RESET 0x8253 +#endif +#ifndef GL_INNOCENT_CONTEXT_RESET +#define GL_INNOCENT_CONTEXT_RESET 0x8254 +#endif +#ifndef GL_UNKNOWN_CONTEXT_RESET +#define GL_UNKNOWN_CONTEXT_RESET 0x8255 +#endif + +namespace KWin +{ + +extern int currentRefreshRate(); + + +/** + * SyncObject represents a fence used to synchronize operations in + * the kwin command stream with operations in the X command stream. + */ +class SyncObject +{ +public: + enum State { Ready, TriggerSent, Waiting, Done, Resetting }; + + SyncObject(); + ~SyncObject(); + + State state() const { return m_state; } + + void trigger(); + void wait(); + bool finish(); + void reset(); + void finishResetting(); + +private: + State m_state; + GLsync m_sync; + xcb_sync_fence_t m_fence; + xcb_get_input_focus_cookie_t m_reset_cookie; +}; + +SyncObject::SyncObject() +{ + m_state = Ready; + + xcb_connection_t * const c = connection(); + + m_fence = xcb_generate_id(c); + xcb_sync_create_fence(c, rootWindow(), m_fence, false); + xcb_flush(c); + + m_sync = glImportSyncEXT(GL_SYNC_X11_FENCE_EXT, m_fence, 0); +} + +SyncObject::~SyncObject() +{ + // If glDeleteSync is called before the xcb fence is signalled + // the nvidia driver (the only one to implement GL_SYNC_X11_FENCE_EXT) + // deadlocks waiting for the fence to be signalled. + // To avoid this, make sure the fence is signalled before + // deleting the sync. + if (m_state == Resetting || m_state == Ready){ + trigger(); + // The flush is necessary! + // The trigger command needs to be sent to the X server. + xcb_flush(connection()); + } + xcb_sync_destroy_fence(connection(), m_fence); + glDeleteSync(m_sync); + + if (m_state == Resetting) + xcb_discard_reply(connection(), m_reset_cookie.sequence); +} + +void SyncObject::trigger() +{ + Q_ASSERT(m_state == Ready || m_state == Resetting); + + // Finish resetting the fence if necessary + if (m_state == Resetting) + finishResetting(); + + xcb_sync_trigger_fence(connection(), m_fence); + m_state = TriggerSent; +} + +void SyncObject::wait() +{ + if (m_state != TriggerSent) + return; + + glWaitSync(m_sync, 0, GL_TIMEOUT_IGNORED); + m_state = Waiting; +} + +bool SyncObject::finish() +{ + if (m_state == Done) + return true; + + // Note: It is possible that we never inserted a wait for the fence. + // This can happen if we ended up not rendering the damaged + // window because it is fully occluded. + Q_ASSERT(m_state == TriggerSent || m_state == Waiting); + + // Check if the fence is signaled + GLint value; + glGetSynciv(m_sync, GL_SYNC_STATUS, 1, nullptr, &value); + + if (value != GL_SIGNALED) { + qCDebug(KWIN_OPENGL) << "Waiting for X fence to finish"; + + // Wait for the fence to become signaled with a one second timeout + const GLenum result = glClientWaitSync(m_sync, 0, 1000000000); + + switch (result) { + case GL_TIMEOUT_EXPIRED: + qCWarning(KWIN_OPENGL) << "Timeout while waiting for X fence"; + return false; + + case GL_WAIT_FAILED: + qCWarning(KWIN_OPENGL) << "glClientWaitSync() failed"; + return false; + } + } + + m_state = Done; + return true; +} + +void SyncObject::reset() +{ + Q_ASSERT(m_state == Done); + + xcb_connection_t * const c = connection(); + + // Send the reset request along with a sync request. + // We use the cookie to ensure that the server has processed the reset + // request before we trigger the fence and call glWaitSync(). + // Otherwise there is a race condition between the reset finishing and + // the glWaitSync() call. + xcb_sync_reset_fence(c, m_fence); + m_reset_cookie = xcb_get_input_focus(c); + xcb_flush(c); + + m_state = Resetting; +} + +void SyncObject::finishResetting() +{ + Q_ASSERT(m_state == Resetting); + free(xcb_get_input_focus_reply(connection(), m_reset_cookie, nullptr)); + m_state = Ready; +} + + + +// ----------------------------------------------------------------------- + + + +/** + * SyncManager manages a set of fences used for explicit synchronization + * with the X command stream. + */ +class SyncManager +{ +public: + enum { MaxFences = 4 }; + + SyncManager(); + ~SyncManager(); + + SyncObject *nextFence(); + bool updateFences(); + +private: + std::array m_fences; + int m_next; +}; + +SyncManager::SyncManager() + : m_next(0) +{ +} + +SyncManager::~SyncManager() +{ +} + +SyncObject *SyncManager::nextFence() +{ + SyncObject *fence = &m_fences[m_next]; + m_next = (m_next + 1) % MaxFences; + return fence; +} + +bool SyncManager::updateFences() +{ + for (int i = 0; i < qMin(2, MaxFences - 1); i++) { + const int index = (m_next + i) % MaxFences; + SyncObject &fence = m_fences[index]; + + switch (fence.state()) { + case SyncObject::Ready: + break; + + case SyncObject::TriggerSent: + case SyncObject::Waiting: + if (!fence.finish()) + return false; + fence.reset(); + break; + + // Should not happen in practice since we always reset the fence + // after finishing it + case SyncObject::Done: + fence.reset(); + break; + + case SyncObject::Resetting: + fence.finishResetting(); + break; + } + } + + return true; +} + + +// ----------------------------------------------------------------------- + +/************************************************ + * SceneOpenGL + ***********************************************/ + +SceneOpenGL::SceneOpenGL(OpenGLBackend *backend, QObject *parent) + : Scene(parent) + , init_ok(true) + , m_backend(backend) + , m_syncManager(nullptr) + , m_currentFence(nullptr) +{ + if (m_backend->isFailed()) { + init_ok = false; + return; + } + if (!viewportLimitsMatched(screens()->size())) + return; + + // perform Scene specific checks + GLPlatform *glPlatform = GLPlatform::instance(); + if (!glPlatform->isGLES() && !hasGLExtension(QByteArrayLiteral("GL_ARB_texture_non_power_of_two")) + && !hasGLExtension(QByteArrayLiteral("GL_ARB_texture_rectangle"))) { + qCCritical(KWIN_OPENGL) << "GL_ARB_texture_non_power_of_two and GL_ARB_texture_rectangle missing"; + init_ok = false; + return; // error + } + if (glPlatform->isMesaDriver() && glPlatform->mesaVersion() < kVersionNumber(10, 0)) { + qCCritical(KWIN_OPENGL) << "KWin requires at least Mesa 10.0 for OpenGL compositing."; + init_ok = false; + return; + } + + m_debug = qstrcmp(qgetenv("KWIN_GL_DEBUG"), "1") == 0; + initDebugOutput(); + + // set strict binding + if (options->isGlStrictBindingFollowsDriver()) { + options->setGlStrictBinding(!glPlatform->supports(LooseBinding)); + } + + bool haveSyncObjects = glPlatform->isGLES() + ? hasGLVersion(3, 0) + : hasGLVersion(3, 2) || hasGLExtension("GL_ARB_sync"); + + if (hasGLExtension("GL_EXT_x11_sync_object") && haveSyncObjects && kwinApp()->operationMode() == Application::OperationModeX11) { + const QByteArray useExplicitSync = qgetenv("KWIN_EXPLICIT_SYNC"); + + if (useExplicitSync != "0") { + qCDebug(KWIN_OPENGL) << "Initializing fences for synchronization with the X command stream"; + m_syncManager = new SyncManager; + } else { + qCDebug(KWIN_OPENGL) << "Explicit synchronization with the X command stream disabled by environment variable"; + } + } +} + +SceneOpenGL::~SceneOpenGL() +{ + if (init_ok) { + makeOpenGLContextCurrent(); + } + SceneOpenGL::EffectFrame::cleanup(); + + delete m_syncManager; + + // backend might be still needed for a different scene + delete m_backend; +} + + +void SceneOpenGL::initDebugOutput() +{ + const bool have_KHR_debug = hasGLExtension(QByteArrayLiteral("GL_KHR_debug")); + const bool have_ARB_debug = hasGLExtension(QByteArrayLiteral("GL_ARB_debug_output")); + if (!have_KHR_debug && !have_ARB_debug) + return; + + if (!have_ARB_debug) { + // if we don't have ARB debug, but only KHR debug we need to verify whether the context is a debug context + // it should work without as well, but empirical tests show: no it doesn't + if (GLPlatform::instance()->isGLES()) { + if (!hasGLVersion(3, 2)) { + // empirical data shows extension doesn't work + return; + } + } else if (!hasGLVersion(3, 0)) { + return; + } + // can only be queried with either OpenGL >= 3.0 or OpenGL ES of at least 3.1 + GLint value = 0; + glGetIntegerv(GL_CONTEXT_FLAGS, &value); + if (!(value & GL_CONTEXT_FLAG_DEBUG_BIT)) { + return; + } + } + + // Set the callback function + auto callback = [](GLenum source, GLenum type, GLuint id, + GLenum severity, GLsizei length, + const GLchar *message, + const GLvoid *userParam) { + Q_UNUSED(source) + Q_UNUSED(severity) + Q_UNUSED(userParam) + while (length && std::isspace(message[length - 1])) { + --length; + } + + switch (type) { + case GL_DEBUG_TYPE_ERROR: + case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR: + qCWarning(KWIN_OPENGL, "%#x: %.*s", id, length, message); + break; + + case GL_DEBUG_TYPE_OTHER: + case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR: + case GL_DEBUG_TYPE_PORTABILITY: + case GL_DEBUG_TYPE_PERFORMANCE: + default: + qCDebug(KWIN_OPENGL, "%#x: %.*s", id, length, message); + break; + } + }; + + glDebugMessageCallback(callback, nullptr); + + // This state exists only in GL_KHR_debug + if (have_KHR_debug) + glEnable(GL_DEBUG_OUTPUT); + +#if !defined(QT_NO_DEBUG) + // Enable all debug messages + glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, nullptr, GL_TRUE); +#else + // Enable error messages + glDebugMessageControl(GL_DONT_CARE, GL_DEBUG_TYPE_ERROR, GL_DONT_CARE, 0, nullptr, GL_TRUE); + glDebugMessageControl(GL_DONT_CARE, GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR, GL_DONT_CARE, 0, nullptr, GL_TRUE); +#endif + + // Insert a test message + const QByteArray message = QByteArrayLiteral("OpenGL debug output initialized"); + glDebugMessageInsert(GL_DEBUG_SOURCE_APPLICATION, GL_DEBUG_TYPE_OTHER, 0, + GL_DEBUG_SEVERITY_LOW, message.length(), message.constData()); +} + +SceneOpenGL *SceneOpenGL::createScene(QObject *parent) +{ + OpenGLBackend *backend = kwinApp()->platform()->createOpenGLBackend(); + if (!backend) { + return nullptr; + } + if (!backend->isFailed()) { + backend->init(); + } + if (backend->isFailed()) { + delete backend; + return nullptr; + } + SceneOpenGL *scene = nullptr; + // first let's try an OpenGL 2 scene + if (SceneOpenGL2::supported(backend)) { + scene = new SceneOpenGL2(backend, parent); + if (scene->initFailed()) { + delete scene; + scene = nullptr; + } else { + return scene; + } + } + if (!scene) { + if (GLPlatform::instance()->recommendedCompositor() == XRenderCompositing) { + qCCritical(KWIN_OPENGL) << "OpenGL driver recommends XRender based compositing. Falling back to XRender."; + qCCritical(KWIN_OPENGL) << "To overwrite the detection use the environment variable KWIN_COMPOSE"; + qCCritical(KWIN_OPENGL) << "For more information see https://community.kde.org/KWin/Environment_Variables#KWIN_COMPOSE"; + } + delete backend; + } + + return scene; +} + +OverlayWindow *SceneOpenGL::overlayWindow() const +{ + return m_backend->overlayWindow(); +} + +bool SceneOpenGL::syncsToVBlank() const +{ + return m_backend->syncsToVBlank(); +} + +bool SceneOpenGL::blocksForRetrace() const +{ + return m_backend->blocksForRetrace(); +} + +void SceneOpenGL::idle() +{ + m_backend->idle(); + Scene::idle(); +} + +bool SceneOpenGL::initFailed() const +{ + return !init_ok; +} + +void SceneOpenGL::handleGraphicsReset(GLenum status) +{ + switch (status) { + case GL_GUILTY_CONTEXT_RESET: + qCDebug(KWIN_OPENGL) << "A graphics reset attributable to the current GL context occurred."; + break; + + case GL_INNOCENT_CONTEXT_RESET: + qCDebug(KWIN_OPENGL) << "A graphics reset not attributable to the current GL context occurred."; + break; + + case GL_UNKNOWN_CONTEXT_RESET: + qCDebug(KWIN_OPENGL) << "A graphics reset of an unknown cause occurred."; + break; + + default: + break; + } + + QElapsedTimer timer; + timer.start(); + + // Wait until the reset is completed or max 10 seconds + while (timer.elapsed() < 10000 && glGetGraphicsResetStatus() != GL_NO_ERROR) + usleep(50); + + qCDebug(KWIN_OPENGL) << "Attempting to reset compositing."; + QMetaObject::invokeMethod(this, "resetCompositing", Qt::QueuedConnection); + + KNotification::event(QStringLiteral("graphicsreset"), i18n("Desktop effects were restarted due to a graphics reset")); +} + + +void SceneOpenGL::triggerFence() +{ + if (m_syncManager) { + m_currentFence = m_syncManager->nextFence(); + m_currentFence->trigger(); + } +} + +void SceneOpenGL::insertWait() +{ + if (m_currentFence && m_currentFence->state() != SyncObject::Waiting) { + m_currentFence->wait(); + } +} + +/** + * Render cursor texture in case hardware cursor is disabled. + * Useful for screen recording apps or backends that can't do planes. + */ +void SceneOpenGL2::paintCursor(const QRegion &rendered) +{ + Cursor* cursor = Cursors::self()->currentCursor(); + + // don't paint if we use hardware cursor or the cursor is hidden + if (!kwinApp()->platform()->usesSoftwareCursor() || + kwinApp()->platform()->isCursorHidden() || + cursor->image().isNull()) { + return; + } + + // figure out which part of the cursor needs to be repainted + const QPoint cursorPos = cursor->pos() - cursor->hotspot(); + const QRect cursorRect = cursor->rect(); + QRegion region; + for (const QRect &rect : rendered) { + region |= rect.translated(-cursorPos).intersected(cursorRect); + } + if (region.isEmpty()) { + return; + } + + // lazy init texture cursor only in case we need software rendering + if (!m_cursorTexture) { + auto updateCursorTexture = [this] { + // don't paint if no image for cursor is set + const QImage img = Cursors::self()->currentCursor()->image(); + if (img.isNull()) { + return; + } + m_cursorTexture.reset(new GLTexture(img)); + }; + + // init now + updateCursorTexture(); + + // handle shape update on case cursor image changed + connect(Cursors::self(), &Cursors::currentCursorChanged, this, updateCursorTexture); + } + + // get cursor position in projection coordinates + QMatrix4x4 mvp = m_projectionMatrix; + mvp.translate(cursorPos.x(), cursorPos.y()); + + // handle transparence + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + // paint texture in cursor offset + m_cursorTexture->bind(); + ShaderBinder binder(ShaderTrait::MapTexture); + binder.shader()->setUniform(GLShader::ModelViewProjectionMatrix, mvp); + m_cursorTexture->render(region, cursorRect); + m_cursorTexture->unbind(); + glDisable(GL_BLEND); +} + +void SceneOpenGL::aboutToStartPainting(const QRegion &damage) +{ + m_backend->aboutToStartPainting(damage); +} + +qint64 SceneOpenGL::paint(const QRegion &damage, const QList &toplevels) +{ + // actually paint the frame, flushed with the NEXT frame + createStackingOrder(toplevels); + + // After this call, updateRegion will contain the damaged region in the + // back buffer. This is the region that needs to be posted to repair + // the front buffer. It doesn't include the additional damage returned + // by prepareRenderingFrame(). validRegion is the region that has been + // repainted, and may be larger than updateRegion. + QRegion updateRegion, validRegion; + if (m_backend->perScreenRendering()) { + // trigger start render timer + m_backend->prepareRenderingFrame(); + for (int i = 0; i < screens()->count(); ++i) { + const QRect &geo = screens()->geometry(i); + const qreal scaling = screens()->scale(i); + QRegion update; + QRegion valid; + // prepare rendering makes context current on the output + QRegion repaint = m_backend->prepareRenderingForScreen(i); + GLVertexBuffer::setVirtualScreenGeometry(geo); + GLRenderTarget::setVirtualScreenGeometry(geo); + GLVertexBuffer::setVirtualScreenScale(scaling); + GLRenderTarget::setVirtualScreenScale(scaling); + + const GLenum status = glGetGraphicsResetStatus(); + if (status != GL_NO_ERROR) { + handleGraphicsReset(status); + return 0; + } + + int mask = 0; + updateProjectionMatrix(); + + paintScreen(&mask, damage.intersected(geo), repaint, &update, &valid, projectionMatrix(), geo, scaling); // call generic implementation + paintCursor(valid); + + GLVertexBuffer::streamingBuffer()->endOfFrame(); + + m_backend->endRenderingFrameForScreen(i, valid, update); + + GLVertexBuffer::streamingBuffer()->framePosted(); + } + } else { + m_backend->makeCurrent(); + QRegion repaint = m_backend->prepareRenderingFrame(); + + const GLenum status = glGetGraphicsResetStatus(); + if (status != GL_NO_ERROR) { + handleGraphicsReset(status); + return 0; + } + GLVertexBuffer::setVirtualScreenGeometry(screens()->geometry()); + GLRenderTarget::setVirtualScreenGeometry(screens()->geometry()); + GLVertexBuffer::setVirtualScreenScale(1); + GLRenderTarget::setVirtualScreenScale(1); + + int mask = 0; + updateProjectionMatrix(); + paintScreen(&mask, damage, repaint, &updateRegion, &validRegion, projectionMatrix()); // call generic implementation + + if (!GLPlatform::instance()->isGLES()) { + const QSize &screenSize = screens()->size(); + const QRegion displayRegion(0, 0, screenSize.width(), screenSize.height()); + + // copy dirty parts from front to backbuffer + if (!m_backend->supportsBufferAge() && + options->glPreferBufferSwap() == Options::CopyFrontBuffer && + validRegion != displayRegion) { + glReadBuffer(GL_FRONT); + m_backend->copyPixels(displayRegion - validRegion); + glReadBuffer(GL_BACK); + validRegion = displayRegion; + } + } + + GLVertexBuffer::streamingBuffer()->endOfFrame(); + + m_backend->endRenderingFrame(validRegion, updateRegion); + + GLVertexBuffer::streamingBuffer()->framePosted(); + } + + if (m_currentFence) { + if (!m_syncManager->updateFences()) { + qCDebug(KWIN_OPENGL) << "Aborting explicit synchronization with the X command stream."; + qCDebug(KWIN_OPENGL) << "Future frames will be rendered unsynchronized."; + delete m_syncManager; + m_syncManager = nullptr; + } + m_currentFence = nullptr; + } + + // do cleanup + clearStackingOrder(); + return m_backend->renderTime(); +} + +QMatrix4x4 SceneOpenGL::transformation(int mask, const ScreenPaintData &data) const +{ + QMatrix4x4 matrix; + + if (!(mask & PAINT_SCREEN_TRANSFORMED)) + return matrix; + + matrix.translate(data.translation()); + data.scale().applyTo(&matrix); + + if (data.rotationAngle() == 0.0) + return matrix; + + // Apply the rotation + // cannot use data.rotation->applyTo(&matrix) as QGraphicsRotation uses projectedRotate to map back to 2D + matrix.translate(data.rotationOrigin()); + const QVector3D axis = data.rotationAxis(); + matrix.rotate(data.rotationAngle(), axis.x(), axis.y(), axis.z()); + matrix.translate(-data.rotationOrigin()); + + return matrix; +} + +void SceneOpenGL::paintBackground(const QRegion ®ion) +{ + PaintClipper pc(region); + if (!PaintClipper::clip()) { + glClearColor(0, 0, 0, 1); + glClear(GL_COLOR_BUFFER_BIT); + return; + } + if (pc.clip() && pc.paintArea().isEmpty()) + return; // no background to paint + QVector verts; + for (PaintClipper::Iterator iterator; !iterator.isDone(); iterator.next()) { + QRect r = iterator.boundingRect(); + verts << r.x() + r.width() << r.y(); + verts << r.x() << r.y(); + verts << r.x() << r.y() + r.height(); + verts << r.x() << r.y() + r.height(); + verts << r.x() + r.width() << r.y() + r.height(); + verts << r.x() + r.width() << r.y(); + } + doPaintBackground(verts); +} + +void SceneOpenGL::extendPaintRegion(QRegion ®ion, bool opaqueFullscreen) +{ + if (m_backend->supportsBufferAge()) + return; + + const QSize &screenSize = screens()->size(); + if (options->glPreferBufferSwap() == Options::ExtendDamage) { // only Extend "large" repaints + const QRegion displayRegion(0, 0, screenSize.width(), screenSize.height()); + uint damagedPixels = 0; + const uint fullRepaintLimit = (opaqueFullscreen?0.49f:0.748f)*screenSize.width()*screenSize.height(); + // 16:9 is 75% of 4:3 and 2.55:1 is 49.01% of 5:4 + // (5:4 is the most square format and 2.55:1 is Cinemascope55 - the widest ever shot + // movie aspect - two times ;-) It's a Fox format, though, so maybe we want to restrict + // to 2.20:1 - Panavision - which has actually been used for interesting movies ...) + // would be 57% of 5/4 + for (const QRect &r : region) { +// damagedPixels += r.width() * r.height(); // combined window damage test + damagedPixels = r.width() * r.height(); // experimental single window damage testing + if (damagedPixels > fullRepaintLimit) { + region = displayRegion; + return; + } + } + } else if (options->glPreferBufferSwap() == Options::PaintFullScreen) { // forced full rePaint + region = QRegion(0, 0, screenSize.width(), screenSize.height()); + } +} + +SceneOpenGLTexture *SceneOpenGL::createTexture() +{ + return new SceneOpenGLTexture(m_backend); +} + +bool SceneOpenGL::viewportLimitsMatched(const QSize &size) const { + if (kwinApp()->operationMode() != Application::OperationModeX11) { + // TODO: On Wayland we can't suspend. Find a solution that works here as well! + return true; + } + GLint limit[2]; + glGetIntegerv(GL_MAX_VIEWPORT_DIMS, limit); + if (limit[0] < size.width() || limit[1] < size.height()) { + auto compositor = static_cast(Compositor::self()); + QMetaObject::invokeMethod(compositor, [compositor]() { + qCDebug(KWIN_OPENGL) << "Suspending compositing because viewport limits are not met"; + compositor->suspend(X11Compositor::AllReasonSuspend); + }, Qt::QueuedConnection); + return false; + } + return true; +} + +void SceneOpenGL::screenGeometryChanged(const QSize &size) +{ + if (!viewportLimitsMatched(size)) + return; + Scene::screenGeometryChanged(size); + glViewport(0,0, size.width(), size.height()); + m_backend->screenGeometryChanged(size); + GLRenderTarget::setVirtualScreenSize(size); +} + +void SceneOpenGL::paintDesktop(int desktop, int mask, const QRegion ®ion, ScreenPaintData &data) +{ + const QRect r = region.boundingRect(); + glEnable(GL_SCISSOR_TEST); + glScissor(r.x(), screens()->size().height() - r.y() - r.height(), r.width(), r.height()); + KWin::Scene::paintDesktop(desktop, mask, region, data); + glDisable(GL_SCISSOR_TEST); +} + +void SceneOpenGL::paintEffectQuickView(EffectQuickView *w) +{ + GLShader *shader = ShaderManager::instance()->pushShader(ShaderTrait::MapTexture); + const QRect rect = w->geometry(); + + GLTexture *t = w->bufferAsTexture(); + if (!t) { + return; + } + + QMatrix4x4 mvp(projectionMatrix()); + mvp.translate(rect.x(), rect.y()); + shader->setUniform(GLShader::ModelViewProjectionMatrix, mvp); + + glEnable(GL_BLEND); + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + t->bind(); + t->render(QRegion(infiniteRegion()), w->geometry()); + t->unbind(); + glDisable(GL_BLEND); + + ShaderManager::instance()->popShader(); +} + +bool SceneOpenGL::makeOpenGLContextCurrent() +{ + return m_backend->makeCurrent(); +} + +void SceneOpenGL::doneOpenGLContextCurrent() +{ + m_backend->doneCurrent(); +} + +bool SceneOpenGL::supportsSurfacelessContext() const +{ + return m_backend->supportsSurfacelessContext(); +} + +Scene::EffectFrame *SceneOpenGL::createEffectFrame(EffectFrameImpl *frame) +{ + return new SceneOpenGL::EffectFrame(frame, this); +} + +Shadow *SceneOpenGL::createShadow(Toplevel *toplevel) +{ + return new SceneOpenGLShadow(toplevel); +} + +Decoration::Renderer *SceneOpenGL::createDecorationRenderer(Decoration::DecoratedClientImpl *impl) +{ + return new SceneOpenGLDecorationRenderer(impl); +} + +bool SceneOpenGL::animationsSupported() const +{ + return !GLPlatform::instance()->isSoftwareEmulation(); +} + +QVector SceneOpenGL::openGLPlatformInterfaceExtensions() const +{ + return m_backend->extensions().toVector(); +} + +QSharedPointer SceneOpenGL::textureForOutput(AbstractOutput* output) const +{ + return m_backend->textureForOutput(output); +} + +//**************************************** +// SceneOpenGL2 +//**************************************** +bool SceneOpenGL2::supported(OpenGLBackend *backend) +{ + const QByteArray forceEnv = qgetenv("KWIN_COMPOSE"); + if (!forceEnv.isEmpty()) { + if (qstrcmp(forceEnv, "O2") == 0 || qstrcmp(forceEnv, "O2ES") == 0) { + qCDebug(KWIN_OPENGL) << "OpenGL 2 compositing enforced by environment variable"; + return true; + } else { + // OpenGL 2 disabled by environment variable + return false; + } + } + if (!backend->isDirectRendering()) { + return false; + } + if (GLPlatform::instance()->recommendedCompositor() < OpenGL2Compositing) { + qCDebug(KWIN_OPENGL) << "Driver does not recommend OpenGL 2 compositing"; + return false; + } + return true; +} + +SceneOpenGL2::SceneOpenGL2(OpenGLBackend *backend, QObject *parent) + : SceneOpenGL(backend, parent) + , m_lanczosFilter(nullptr) +{ + if (!init_ok) { + // base ctor already failed + return; + } + + // We only support the OpenGL 2+ shader API, not GL_ARB_shader_objects + if (!hasGLVersion(2, 0)) { + qCDebug(KWIN_OPENGL) << "OpenGL 2.0 is not supported"; + init_ok = false; + return; + } + + const QSize &s = screens()->size(); + GLRenderTarget::setVirtualScreenSize(s); + GLRenderTarget::setVirtualScreenGeometry(screens()->geometry()); + + // push one shader on the stack so that one is always bound + ShaderManager::instance()->pushShader(ShaderTrait::MapTexture); + if (checkGLError("Init")) { + qCCritical(KWIN_OPENGL) << "OpenGL 2 compositing setup failed"; + init_ok = false; + return; // error + } + + // It is not legal to not have a vertex array object bound in a core context + if (!GLPlatform::instance()->isGLES() && hasGLExtension(QByteArrayLiteral("GL_ARB_vertex_array_object"))) { + glGenVertexArrays(1, &vao); + glBindVertexArray(vao); + } + + if (!ShaderManager::instance()->selfTest()) { + qCCritical(KWIN_OPENGL) << "ShaderManager self test failed"; + init_ok = false; + return; + } + + qCDebug(KWIN_OPENGL) << "OpenGL 2 compositing successfully initialized"; + init_ok = true; +} + +SceneOpenGL2::~SceneOpenGL2() +{ + if (m_lanczosFilter) { + makeOpenGLContextCurrent(); + delete m_lanczosFilter; + m_lanczosFilter = nullptr; + } +} + +QMatrix4x4 SceneOpenGL2::createProjectionMatrix() const +{ + // Create a perspective projection with a 60° field-of-view, + // and an aspect ratio of 1.0. + const float fovY = 60.0f; + const float aspect = 1.0f; + const float zNear = 0.1f; + const float zFar = 100.0f; + + const float yMax = zNear * std::tan(fovY * M_PI / 360.0f); + const float yMin = -yMax; + const float xMin = yMin * aspect; + const float xMax = yMax * aspect; + + QMatrix4x4 projection; + projection.frustum(xMin, xMax, yMin, yMax, zNear, zFar); + + // Create a second matrix that transforms screen coordinates + // to world coordinates. + const float scaleFactor = 1.1 * std::tan(fovY * M_PI / 360.0f) / yMax; + const QSize size = screens()->size(); + + QMatrix4x4 matrix; + matrix.translate(xMin * scaleFactor, yMax * scaleFactor, -1.1); + matrix.scale( (xMax - xMin) * scaleFactor / size.width(), + -(yMax - yMin) * scaleFactor / size.height(), + 0.001); + + // Combine the matrices + return projection * matrix; +} + +void SceneOpenGL2::updateProjectionMatrix() +{ + m_projectionMatrix = createProjectionMatrix(); +} + +void SceneOpenGL2::paintSimpleScreen(int mask, const QRegion ®ion) +{ + m_screenProjectionMatrix = m_projectionMatrix; + + Scene::paintSimpleScreen(mask, region); +} + +void SceneOpenGL2::paintGenericScreen(int mask, const ScreenPaintData &data) +{ + const QMatrix4x4 screenMatrix = transformation(mask, data); + + m_screenProjectionMatrix = m_projectionMatrix * screenMatrix; + + Scene::paintGenericScreen(mask, data); +} + +void SceneOpenGL2::doPaintBackground(const QVector< float >& vertices) +{ + GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer(); + vbo->reset(); + vbo->setUseColor(true); + vbo->setData(vertices.count() / 2, 2, vertices.data(), nullptr); + + ShaderBinder binder(ShaderTrait::UniformColor); + binder.shader()->setUniform(GLShader::ModelViewProjectionMatrix, m_projectionMatrix); + + vbo->render(GL_TRIANGLES); +} + +Scene::Window *SceneOpenGL2::createWindow(Toplevel *t) +{ + return new OpenGLWindow(t, this); +} + +void SceneOpenGL2::finalDrawWindow(EffectWindowImpl* w, int mask, const QRegion ®ion, WindowPaintData& data) +{ + if (waylandServer() && waylandServer()->isScreenLocked() && !w->window()->isLockScreen() && !w->window()->isInputMethod()) { + return; + } + performPaintWindow(w, mask, region, data); +} + +void SceneOpenGL2::performPaintWindow(EffectWindowImpl* w, int mask, const QRegion ®ion, WindowPaintData& data) +{ + if (mask & PAINT_WINDOW_LANCZOS) { + if (!m_lanczosFilter) { + m_lanczosFilter = new LanczosFilter(this); + // reset the lanczos filter when the screen gets resized + // it will get created next paint + connect(screens(), &Screens::changed, this, [this]() { + makeOpenGLContextCurrent(); + delete m_lanczosFilter; + m_lanczosFilter = nullptr; + }); + } + m_lanczosFilter->performPaint(w, mask, region, data); + } else + w->sceneWindow()->performPaint(mask, region, data); +} + +//**************************************** +// OpenGLWindow +//**************************************** + +OpenGLWindow::OpenGLWindow(Toplevel *toplevel, SceneOpenGL *scene) + : Scene::Window(toplevel) + , m_scene(scene) +{ +} + +OpenGLWindow::~OpenGLWindow() +{ +} + +// Bind the window pixmap to an OpenGL texture. +bool OpenGLWindow::bindTexture() +{ + OpenGLWindowPixmap *pixmap = windowPixmap(); + if (!pixmap) { + return false; + } + if (pixmap->isDiscarded()) { + return !pixmap->texture()->isNull(); + } + + if (!window()->damage().isEmpty()) + m_scene->insertWait(); + + return pixmap->bind(); +} + +QMatrix4x4 OpenGLWindow::transformation(int mask, const WindowPaintData &data) const +{ + QMatrix4x4 matrix; + matrix.translate(x(), y()); + + if (!(mask & Scene::PAINT_WINDOW_TRANSFORMED)) + return matrix; + + matrix.translate(data.translation()); + data.scale().applyTo(&matrix); + + if (data.rotationAngle() == 0.0) + return matrix; + + // Apply the rotation + // cannot use data.rotation.applyTo(&matrix) as QGraphicsRotation uses projectedRotate to map back to 2D + matrix.translate(data.rotationOrigin()); + const QVector3D axis = data.rotationAxis(); + matrix.rotate(data.rotationAngle(), axis.x(), axis.y(), axis.z()); + matrix.translate(-data.rotationOrigin()); + + return matrix; +} + +bool OpenGLWindow::beginRenderWindow(int mask, const QRegion ®ion, WindowPaintData &data) +{ + if (region.isEmpty()) + return false; + + m_hardwareClipping = region != infiniteRegion() && (mask & Scene::PAINT_WINDOW_TRANSFORMED) && !(mask & Scene::PAINT_SCREEN_TRANSFORMED); + if (region != infiniteRegion() && !m_hardwareClipping) { + WindowQuadList quads; + quads.reserve(data.quads.count()); + + const QRegion filterRegion = region.translated(-x(), -y()); + // split all quads in bounding rect with the actual rects in the region + for (const WindowQuad &quad : qAsConst(data.quads)) { + for (const QRect &r : filterRegion) { + const QRectF rf(r); + const QRectF quadRect(QPointF(quad.left(), quad.top()), QPointF(quad.right(), quad.bottom())); + const QRectF &intersected = rf.intersected(quadRect); + if (intersected.isValid()) { + if (quadRect == intersected) { + // case 1: completely contains, include and do not check other rects + quads << quad; + break; + } + // case 2: intersection + quads << quad.makeSubQuad(intersected.left(), intersected.top(), intersected.right(), intersected.bottom()); + } + } + } + data.quads = quads; + } + + if (data.quads.isEmpty()) + return false; + + if (!bindTexture()) { + return false; + } + + if (m_hardwareClipping) { + glEnable(GL_SCISSOR_TEST); + } + + const GLVertexAttrib attribs[] = { + { VA_Position, 2, GL_FLOAT, offsetof(GLVertex2D, position) }, + { VA_TexCoord, 2, GL_FLOAT, offsetof(GLVertex2D, texcoord) }, + }; + + GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer(); + vbo->reset(); + vbo->setAttribLayout(attribs, 2, sizeof(GLVertex2D)); + + return true; +} + +void OpenGLWindow::endRenderWindow() +{ + if (m_hardwareClipping) { + glDisable(GL_SCISSOR_TEST); + } +} + +GLTexture *OpenGLWindow::getDecorationTexture() const +{ + if (AbstractClient *client = dynamic_cast(toplevel)) { + if (client->noBorder()) { + return nullptr; + } + + if (!client->isDecorated()) { + return nullptr; + } + if (SceneOpenGLDecorationRenderer *renderer = static_cast(client->decoratedClient()->renderer())) { + renderer->render(); + return renderer->texture(); + } + } else if (toplevel->isDeleted()) { + Deleted *deleted = static_cast(toplevel); + if (!deleted->wasClient() || deleted->noBorder()) { + return nullptr; + } + if (const SceneOpenGLDecorationRenderer *renderer = static_cast(deleted->decorationRenderer())) { + return renderer->texture(); + } + } + return nullptr; +} + +WindowPixmap *OpenGLWindow::createWindowPixmap() +{ + return new OpenGLWindowPixmap(this, m_scene); +} + +QVector4D OpenGLWindow::modulate(float opacity, float brightness) const +{ + const float a = opacity; + const float rgb = opacity * brightness; + + return QVector4D(rgb, rgb, rgb, a); +} + +void OpenGLWindow::setBlendEnabled(bool enabled) +{ + if (enabled && !m_blendingEnabled) + glEnable(GL_BLEND); + else if (!enabled && m_blendingEnabled) + glDisable(GL_BLEND); + + m_blendingEnabled = enabled; +} + +/** + * \internal + * + * Counts the total number of pixmaps in the tree with the given root \a windowPixmap. + */ +static int windowPixmapCount(WindowPixmap *windowPixmap) +{ + int count = 1; // 1 for the window pixmap itself. + + const QVector children = windowPixmap->children(); + for (WindowPixmap *child : children) + count += windowPixmapCount(child); + + return count; +} + +void OpenGLWindow::initializeRenderContext(RenderContext &context, const WindowPaintData &data) +{ + WindowPixmap *currentPixmap = windowPixmap(); + + context.shadowOffset = 0; + context.decorationOffset = 1; + context.contentOffset = 2; + context.previousContentOffset = windowPixmapCount(currentPixmap) + 2; + context.quadCount = data.quads.count(); + + const int nodeCount = context.previousContentOffset + 1; + + QVector &renderNodes = context.renderNodes; + renderNodes.resize(nodeCount); + + for (const WindowQuad &quad : data.quads) { + switch (quad.type()) { + case WindowQuadShadow: + renderNodes[context.shadowOffset].quads << quad; + break; + + case WindowQuadDecoration: + renderNodes[context.decorationOffset].quads << quad; + break; + + case WindowQuadContents: + renderNodes[context.contentOffset + quad.id()].quads << quad; + break; + + default: + // Ignore window quad generated by effects. + break; + } + } + + RenderNode &shadowRenderNode = renderNodes[context.shadowOffset]; + if (!shadowRenderNode.quads.isEmpty()) { + SceneOpenGLShadow *shadow = static_cast(m_shadow); + shadowRenderNode.texture = shadow->shadowTexture(); + shadowRenderNode.opacity = data.opacity(); + shadowRenderNode.hasAlpha = true; + shadowRenderNode.coordinateType = NormalizedCoordinates; + shadowRenderNode.leafType = ShadowLeaf; + } + + RenderNode &decorationRenderNode = renderNodes[context.decorationOffset]; + if (!decorationRenderNode.quads.isEmpty()) { + decorationRenderNode.texture = getDecorationTexture(); + decorationRenderNode.opacity = data.opacity(); + decorationRenderNode.hasAlpha = true; + decorationRenderNode.coordinateType = UnnormalizedCoordinates; + decorationRenderNode.leafType = DecorationLeaf; + } + + // FIXME: Cross-fading must be implemented in a shader. + float contentOpacity = data.opacity(); + if (data.crossFadeProgress() != 1.0 && (data.opacity() < 0.95 || toplevel->hasAlpha())) { + const float opacity = 1.0 - data.crossFadeProgress(); + contentOpacity *= 1 - pow(opacity, 1.0f + 2.0f * data.opacity()); + } + + // The main surface and all of its sub-surfaces form a tree. In order to initialize + // the render nodes for the window pixmaps we need to traverse the tree in the + // depth-first search manner. The id of content window quads corresponds to the time + // when we visited the corresponding window pixmap. The DFS traversal probably doesn't + // have a significant impact on performance. However, if that's the case, we could + // keep a cache of window pixmaps in the order in which they'll be rendered. + QStack stack; + stack.push(currentPixmap); + + int i = 0; + + while (!stack.isEmpty()) { + OpenGLWindowPixmap *windowPixmap = static_cast(stack.pop()); + + // If it's an unmapped sub-surface, don't render it and all of its children. + if (!windowPixmap->isValid()) + continue; + + RenderNode &contentRenderNode = renderNodes[context.contentOffset + i++]; + contentRenderNode.texture = windowPixmap->texture(); + contentRenderNode.hasAlpha = windowPixmap->hasAlphaChannel(); + contentRenderNode.opacity = contentOpacity; + contentRenderNode.coordinateType = UnnormalizedCoordinates; + contentRenderNode.leafType = ContentLeaf; + + const QVector children = windowPixmap->children(); + for (auto it = children.rbegin(); it != children.rend(); ++it) { + stack.push(*it); + } + } + + // Note that cross-fading is currently working properly only on X11. In order to make it + // work on Wayland, we have to render the current and the previous window pixmap trees in + // offscreen render targets, then use a cross-fading shader to blend those two layers. + if (data.crossFadeProgress() != 1.0) { + OpenGLWindowPixmap *previous = previousWindowPixmap(); + if (previous) { // TODO(vlad): Should cross-fading be disabled on Wayland? + const QRect &oldGeometry = previous->contentsRect(); + RenderNode &previousContentRenderNode = renderNodes[context.previousContentOffset]; + for (const WindowQuad &quad : qAsConst(renderNodes[context.contentOffset].quads)) { + // We need to create new window quads with normalized texture coordinates. + // Normal quads divide the x/y position by width/height. This would not work + // as the texture is larger than the visible content in case of a decorated + // Client resulting in garbage being shown. So we calculate the normalized + // texture coordinate in the Client's new content space and map it to the + // previous Client's content space. + WindowQuad newQuad(WindowQuadContents); + for (int i = 0; i < 4; ++i) { + const qreal xFactor = (quad[i].textureX() - toplevel->clientPos().x()) + / qreal(toplevel->clientSize().width()); + const qreal yFactor = (quad[i].textureY() - toplevel->clientPos().y()) + / qreal(toplevel->clientSize().height()); + const qreal u = (xFactor * oldGeometry.width() + oldGeometry.x()) + / qreal(previous->size().width()); + const qreal v = (yFactor * oldGeometry.height() + oldGeometry.y()) + / qreal(previous->size().height()); + newQuad[i] = WindowVertex(quad[i].x(), quad[i].y(), u, v); + } + previousContentRenderNode.quads.append(newQuad); + } + + previousContentRenderNode.texture = previous->texture(); + previousContentRenderNode.hasAlpha = previous->hasAlphaChannel(); + previousContentRenderNode.opacity = data.opacity() * (1.0 - data.crossFadeProgress()); + previousContentRenderNode.coordinateType = NormalizedCoordinates; + previousContentRenderNode.leafType = PreviousContentLeaf; + + context.quadCount += previousContentRenderNode.quads.count(); + } + } +} + +QMatrix4x4 OpenGLWindow::modelViewProjectionMatrix(int mask, const WindowPaintData &data) const +{ + SceneOpenGL2 *scene = static_cast(m_scene); + + const QMatrix4x4 pMatrix = data.projectionMatrix(); + const QMatrix4x4 mvMatrix = data.modelViewMatrix(); + + // An effect may want to override the default projection matrix in some cases, + // such as when it is rendering a window on a render target that doesn't have + // the same dimensions as the default framebuffer. + // + // Note that the screen transformation is not applied here. + if (!pMatrix.isIdentity()) + return pMatrix * mvMatrix; + + // If an effect has specified a model-view matrix, we multiply that matrix + // with the default projection matrix. If the effect hasn't specified a + // model-view matrix, mvMatrix will be the identity matrix. + if (mask & Scene::PAINT_SCREEN_TRANSFORMED) + return scene->screenProjectionMatrix() * mvMatrix; + + return scene->projectionMatrix() * mvMatrix; +} + +void OpenGLWindow::performPaint(int mask, const QRegion ®ion, const WindowPaintData &_data) +{ + WindowPaintData data = _data; + if (!beginRenderWindow(mask, region, data)) + return; + + QMatrix4x4 windowMatrix = transformation(mask, data); + const QMatrix4x4 modelViewProjection = modelViewProjectionMatrix(mask, data); + const QMatrix4x4 mvpMatrix = modelViewProjection * windowMatrix; + + bool useX11TextureClamp = false; + + GLShader *shader = data.shader; + GLenum filter; + + if (waylandServer()) { + filter = GL_LINEAR; + } else { + const bool isTransformed = mask & (Effect::PAINT_WINDOW_TRANSFORMED | + Effect::PAINT_SCREEN_TRANSFORMED); + useX11TextureClamp = isTransformed; + if (isTransformed && options->glSmoothScale() != 0) { + filter = GL_LINEAR; + } else { + filter = GL_NEAREST; + } + } + + if (!shader) { + ShaderTraits traits = ShaderTrait::MapTexture; + if (useX11TextureClamp) { + traits |= ShaderTrait::ClampTexture; + } + + if (data.opacity() != 1.0 || data.brightness() != 1.0 || data.crossFadeProgress() != 1.0) + traits |= ShaderTrait::Modulate; + + if (data.saturation() != 1.0) + traits |= ShaderTrait::AdjustSaturation; + + shader = ShaderManager::instance()->pushShader(traits); + } + shader->setUniform(GLShader::ModelViewProjectionMatrix, mvpMatrix); + + shader->setUniform(GLShader::Saturation, data.saturation()); + + RenderContext renderContext; + initializeRenderContext(renderContext, data); + + const bool indexedQuads = GLVertexBuffer::supportsIndexedQuads(); + const GLenum primitiveType = indexedQuads ? GL_QUADS : GL_TRIANGLES; + const int verticesPerQuad = indexedQuads ? 4 : 6; + + const size_t size = verticesPerQuad * renderContext.quadCount * sizeof(GLVertex2D); + + GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer(); + GLVertex2D *map = (GLVertex2D *) vbo->map(size); + + for (int i = 0, v = 0; i < renderContext.renderNodes.count(); i++) { + RenderNode &renderNode = renderContext.renderNodes[i]; + if (renderNode.quads.isEmpty() || !renderNode.texture) + continue; + + renderNode.firstVertex = v; + renderNode.vertexCount = renderNode.quads.count() * verticesPerQuad; + + const QMatrix4x4 matrix = renderNode.texture->matrix(renderNode.coordinateType); + + renderNode.quads.makeInterleavedArrays(primitiveType, &map[v], matrix); + v += renderNode.quads.count() * verticesPerQuad; + } + + vbo->unmap(); + vbo->bindArrays(); + + // Make sure the blend function is set up correctly in case we will be doing blending + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + + float opacity = -1.0; + + for (int i = 0; i < renderContext.renderNodes.count(); i++) { + const RenderNode &renderNode = renderContext.renderNodes[i]; + if (renderNode.vertexCount == 0) + continue; + + setBlendEnabled(renderNode.hasAlpha || renderNode.opacity < 1.0); + + if (opacity != renderNode.opacity) { + shader->setUniform(GLShader::ModulationConstant, + modulate(renderNode.opacity, data.brightness())); + opacity = renderNode.opacity; + } + + renderNode.texture->setFilter(filter); + renderNode.texture->setWrapMode(GL_CLAMP_TO_EDGE); + renderNode.texture->bind(); + + if (renderNode.leafType == ContentLeaf && useX11TextureClamp) { + // X11 windows are reparented to have their buffer in the middle of a larger texture + // holding the frame window. + // This code passes the texture geometry to the fragment shader + // any samples near the edge of the texture will be constrained to be + // at least half a pixel in bounds, meaning we don't bleed the transparent border + QRectF bufferContentRect = clientShape().boundingRect(); + bufferContentRect.adjust(0.5, 0.5, -0.5, -0.5); + const QRect bufferGeometry = toplevel->bufferGeometry(); + + float leftClamp = bufferContentRect.left() / bufferGeometry.width(); + float topClamp = bufferContentRect.top() / bufferGeometry.height(); + float rightClamp = bufferContentRect.right() / bufferGeometry.width(); + float bottomClamp = bufferContentRect.bottom() / bufferGeometry.height(); + shader->setUniform(GLShader::TextureClamp, QVector4D({leftClamp, topClamp, rightClamp, bottomClamp})); + } else { + shader->setUniform(GLShader::TextureClamp, QVector4D({0, 0, 1, 1})); + } + + vbo->draw(region, primitiveType, renderNode.firstVertex, + renderNode.vertexCount, m_hardwareClipping); + } + + vbo->unbindArrays(); + + setBlendEnabled(false); + + if (!data.shader) + ShaderManager::instance()->popShader(); + + endRenderWindow(); +} + +QSharedPointer OpenGLWindow::windowTexture() +{ + auto frame = windowPixmap(); + + if (frame && frame->children().isEmpty()) { + return QSharedPointer(new GLTexture(*frame->texture())); + } else { + auto effectWindow = window()->effectWindow(); + const QRect geo = window()->clientGeometry(); + QSharedPointer texture(new GLTexture(GL_RGBA8, geo.size())); + + QScopedPointer framebuffer(new KWin::GLRenderTarget(*texture)); + GLRenderTarget::pushRenderTarget(framebuffer.data()); + + auto renderVSG = GLRenderTarget::virtualScreenGeometry(); + GLVertexBuffer::setVirtualScreenGeometry(geo); + GLRenderTarget::setVirtualScreenGeometry(geo); + + QMatrix4x4 mvp; + mvp.ortho(geo.x(), geo.x() + geo.width(), geo.y(), geo.y() + geo.height(), -1, 1); + + WindowPaintData data(effectWindow); + data.setProjectionMatrix(mvp); + + performPaint(Scene::PAINT_WINDOW_TRANSFORMED | Scene::PAINT_WINDOW_LANCZOS, geo, data); + GLRenderTarget::popRenderTarget(); + GLVertexBuffer::setVirtualScreenGeometry(renderVSG); + GLRenderTarget::setVirtualScreenGeometry(renderVSG); + return texture; + } +} + +//**************************************** +// OpenGLWindowPixmap +//**************************************** + +OpenGLWindowPixmap::OpenGLWindowPixmap(Scene::Window *window, SceneOpenGL* scene) + : WindowPixmap(window) + , m_texture(scene->createTexture()) + , m_scene(scene) +{ +} + +OpenGLWindowPixmap::OpenGLWindowPixmap(const QPointer &subSurface, WindowPixmap *parent, SceneOpenGL *scene) + : WindowPixmap(subSurface, parent) + , m_texture(scene->createTexture()) + , m_scene(scene) +{ +} + +OpenGLWindowPixmap::~OpenGLWindowPixmap() +{ +} + +static bool needsPixmapUpdate(const OpenGLWindowPixmap *pixmap) +{ + // That's a regular Wayland client. + if (pixmap->surface()) { + return !pixmap->surface()->trackedDamage().isEmpty(); + } + + // That's an internal client with a raster buffer attached. + if (!pixmap->internalImage().isNull()) { + return !pixmap->toplevel()->damage().isEmpty(); + } + + // That's an internal client with an opengl framebuffer object attached. + if (!pixmap->fbo().isNull()) { + return !pixmap->toplevel()->damage().isEmpty(); + } + + // That's an X11 client. + return false; +} + +bool OpenGLWindowPixmap::bind() +{ + if (!m_texture->isNull()) { + if (needsPixmapUpdate(this)) { + m_texture->updateFromPixmap(this); + // mipmaps need to be updated + m_texture->setDirty(); + } + if (subSurface().isNull()) { + toplevel()->resetDamage(); + } + // also bind all children + for (auto it = children().constBegin(); it != children().constEnd(); ++it) { + static_cast(*it)->bind(); + } + return true; + } + for (auto it = children().constBegin(); it != children().constEnd(); ++it) { + static_cast(*it)->bind(); + } + if (!isValid()) { + return false; + } + + bool success = m_texture->load(this); + + if (success) { + if (subSurface().isNull()) { + toplevel()->resetDamage(); + } + } else + qCDebug(KWIN_OPENGL) << "Failed to bind window"; + return success; +} + +WindowPixmap *OpenGLWindowPixmap::createChild(const QPointer &subSurface) +{ + return new OpenGLWindowPixmap(subSurface, this, m_scene); +} + +bool OpenGLWindowPixmap::isValid() const +{ + if (!m_texture->isNull()) { + return true; + } + return WindowPixmap::isValid(); +} + +//**************************************** +// SceneOpenGL::EffectFrame +//**************************************** + +GLTexture* SceneOpenGL::EffectFrame::m_unstyledTexture = nullptr; +QPixmap* SceneOpenGL::EffectFrame::m_unstyledPixmap = nullptr; + +SceneOpenGL::EffectFrame::EffectFrame(EffectFrameImpl* frame, SceneOpenGL *scene) + : Scene::EffectFrame(frame) + , m_texture(nullptr) + , m_textTexture(nullptr) + , m_oldTextTexture(nullptr) + , m_textPixmap(nullptr) + , m_iconTexture(nullptr) + , m_oldIconTexture(nullptr) + , m_selectionTexture(nullptr) + , m_unstyledVBO(nullptr) + , m_scene(scene) +{ + if (m_effectFrame->style() == EffectFrameUnstyled && !m_unstyledTexture) { + updateUnstyledTexture(); + } +} + +SceneOpenGL::EffectFrame::~EffectFrame() +{ + delete m_texture; + delete m_textTexture; + delete m_textPixmap; + delete m_oldTextTexture; + delete m_iconTexture; + delete m_oldIconTexture; + delete m_selectionTexture; + delete m_unstyledVBO; +} + +void SceneOpenGL::EffectFrame::free() +{ + glFlush(); + delete m_texture; + m_texture = nullptr; + delete m_textTexture; + m_textTexture = nullptr; + delete m_textPixmap; + m_textPixmap = nullptr; + delete m_iconTexture; + m_iconTexture = nullptr; + delete m_selectionTexture; + m_selectionTexture = nullptr; + delete m_unstyledVBO; + m_unstyledVBO = nullptr; + delete m_oldIconTexture; + m_oldIconTexture = nullptr; + delete m_oldTextTexture; + m_oldTextTexture = nullptr; +} + +void SceneOpenGL::EffectFrame::freeIconFrame() +{ + delete m_iconTexture; + m_iconTexture = nullptr; +} + +void SceneOpenGL::EffectFrame::freeTextFrame() +{ + delete m_textTexture; + m_textTexture = nullptr; + delete m_textPixmap; + m_textPixmap = nullptr; +} + +void SceneOpenGL::EffectFrame::freeSelection() +{ + delete m_selectionTexture; + m_selectionTexture = nullptr; +} + +void SceneOpenGL::EffectFrame::crossFadeIcon() +{ + delete m_oldIconTexture; + m_oldIconTexture = m_iconTexture; + m_iconTexture = nullptr; +} + +void SceneOpenGL::EffectFrame::crossFadeText() +{ + delete m_oldTextTexture; + m_oldTextTexture = m_textTexture; + m_textTexture = nullptr; +} + +void SceneOpenGL::EffectFrame::render(const QRegion &_region, double opacity, double frameOpacity) +{ + if (m_effectFrame->geometry().isEmpty()) + return; // Nothing to display + + Q_UNUSED(_region); + const QRegion region = infiniteRegion(); // TODO: Old region doesn't seem to work with OpenGL + + GLShader* shader = m_effectFrame->shader(); + if (!shader) { + shader = ShaderManager::instance()->pushShader(ShaderTrait::MapTexture | ShaderTrait::Modulate); + } else if (shader) { + ShaderManager::instance()->pushShader(shader); + } + + if (shader) { + shader->setUniform(GLShader::ModulationConstant, QVector4D(1.0, 1.0, 1.0, 1.0)); + shader->setUniform(GLShader::Saturation, 1.0f); + } + const QMatrix4x4 projection = m_scene->projectionMatrix(); + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + // Render the actual frame + if (m_effectFrame->style() == EffectFrameUnstyled) { + if (!m_unstyledVBO) { + m_unstyledVBO = new GLVertexBuffer(GLVertexBuffer::Static); + QRect area = m_effectFrame->geometry(); + area.moveTo(0, 0); + area.adjust(-5, -5, 5, 5); + + const int roundness = 5; + QVector verts, texCoords; + verts.reserve(84); + texCoords.reserve(84); + + // top left + verts << area.left() << area.top(); + texCoords << 0.0f << 0.0f; + verts << area.left() << area.top() + roundness; + texCoords << 0.0f << 0.5f; + verts << area.left() + roundness << area.top(); + texCoords << 0.5f << 0.0f; + verts << area.left() + roundness << area.top() + roundness; + texCoords << 0.5f << 0.5f; + verts << area.left() << area.top() + roundness; + texCoords << 0.0f << 0.5f; + verts << area.left() + roundness << area.top(); + texCoords << 0.5f << 0.0f; + // top + verts << area.left() + roundness << area.top(); + texCoords << 0.5f << 0.0f; + verts << area.left() + roundness << area.top() + roundness; + texCoords << 0.5f << 0.5f; + verts << area.right() - roundness << area.top(); + texCoords << 0.5f << 0.0f; + verts << area.left() + roundness << area.top() + roundness; + texCoords << 0.5f << 0.5f; + verts << area.right() - roundness << area.top() + roundness; + texCoords << 0.5f << 0.5f; + verts << area.right() - roundness << area.top(); + texCoords << 0.5f << 0.0f; + // top right + verts << area.right() - roundness << area.top(); + texCoords << 0.5f << 0.0f; + verts << area.right() - roundness << area.top() + roundness; + texCoords << 0.5f << 0.5f; + verts << area.right() << area.top(); + texCoords << 1.0f << 0.0f; + verts << area.right() - roundness << area.top() + roundness; + texCoords << 0.5f << 0.5f; + verts << area.right() << area.top() + roundness; + texCoords << 1.0f << 0.5f; + verts << area.right() << area.top(); + texCoords << 1.0f << 0.0f; + // bottom left + verts << area.left() << area.bottom() - roundness; + texCoords << 0.0f << 0.5f; + verts << area.left() << area.bottom(); + texCoords << 0.0f << 1.0f; + verts << area.left() + roundness << area.bottom() - roundness; + texCoords << 0.5f << 0.5f; + verts << area.left() + roundness << area.bottom(); + texCoords << 0.5f << 1.0f; + verts << area.left() << area.bottom(); + texCoords << 0.0f << 1.0f; + verts << area.left() + roundness << area.bottom() - roundness; + texCoords << 0.5f << 0.5f; + // bottom + verts << area.left() + roundness << area.bottom() - roundness; + texCoords << 0.5f << 0.5f; + verts << area.left() + roundness << area.bottom(); + texCoords << 0.5f << 1.0f; + verts << area.right() - roundness << area.bottom() - roundness; + texCoords << 0.5f << 0.5f; + verts << area.left() + roundness << area.bottom(); + texCoords << 0.5f << 1.0f; + verts << area.right() - roundness << area.bottom(); + texCoords << 0.5f << 1.0f; + verts << area.right() - roundness << area.bottom() - roundness; + texCoords << 0.5f << 0.5f; + // bottom right + verts << area.right() - roundness << area.bottom() - roundness; + texCoords << 0.5f << 0.5f; + verts << area.right() - roundness << area.bottom(); + texCoords << 0.5f << 1.0f; + verts << area.right() << area.bottom() - roundness; + texCoords << 1.0f << 0.5f; + verts << area.right() - roundness << area.bottom(); + texCoords << 0.5f << 1.0f; + verts << area.right() << area.bottom(); + texCoords << 1.0f << 1.0f; + verts << area.right() << area.bottom() - roundness; + texCoords << 1.0f << 0.5f; + // center + verts << area.left() << area.top() + roundness; + texCoords << 0.0f << 0.5f; + verts << area.left() << area.bottom() - roundness; + texCoords << 0.0f << 0.5f; + verts << area.right() << area.top() + roundness; + texCoords << 1.0f << 0.5f; + verts << area.left() << area.bottom() - roundness; + texCoords << 0.0f << 0.5f; + verts << area.right() << area.bottom() - roundness; + texCoords << 1.0f << 0.5f; + verts << area.right() << area.top() + roundness; + texCoords << 1.0f << 0.5f; + + m_unstyledVBO->setData(verts.count() / 2, 2, verts.data(), texCoords.data()); + } + + if (shader) { + const float a = opacity * frameOpacity; + shader->setUniform(GLShader::ModulationConstant, QVector4D(a, a, a, a)); + } + + m_unstyledTexture->bind(); + const QPoint pt = m_effectFrame->geometry().topLeft(); + + QMatrix4x4 mvp(projection); + mvp.translate(pt.x(), pt.y()); + shader->setUniform(GLShader::ModelViewProjectionMatrix, mvp); + + m_unstyledVBO->render(region, GL_TRIANGLES); + m_unstyledTexture->unbind(); + } else if (m_effectFrame->style() == EffectFrameStyled) { + if (!m_texture) // Lazy creation + updateTexture(); + + if (shader) { + const float a = opacity * frameOpacity; + shader->setUniform(GLShader::ModulationConstant, QVector4D(a, a, a, a)); + } + m_texture->bind(); + qreal left, top, right, bottom; + m_effectFrame->frame().getMargins(left, top, right, bottom); // m_geometry is the inner geometry + const QRect rect = m_effectFrame->geometry().adjusted(-left, -top, right, bottom); + + QMatrix4x4 mvp(projection); + mvp.translate(rect.x(), rect.y()); + shader->setUniform(GLShader::ModelViewProjectionMatrix, mvp); + + m_texture->render(region, rect); + m_texture->unbind(); + + } + if (!m_effectFrame->selection().isNull()) { + if (!m_selectionTexture) { // Lazy creation + QPixmap pixmap = m_effectFrame->selectionFrame().framePixmap(); + if (!pixmap.isNull()) + m_selectionTexture = new GLTexture(pixmap); + } + if (m_selectionTexture) { + if (shader) { + const float a = opacity * frameOpacity; + shader->setUniform(GLShader::ModulationConstant, QVector4D(a, a, a, a)); + } + QMatrix4x4 mvp(projection); + mvp.translate(m_effectFrame->selection().x(), m_effectFrame->selection().y()); + shader->setUniform(GLShader::ModelViewProjectionMatrix, mvp); + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + m_selectionTexture->bind(); + m_selectionTexture->render(region, m_effectFrame->selection()); + m_selectionTexture->unbind(); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + } + } + + // Render icon + if (!m_effectFrame->icon().isNull() && !m_effectFrame->iconSize().isEmpty()) { + QPoint topLeft(m_effectFrame->geometry().x(), + m_effectFrame->geometry().center().y() - m_effectFrame->iconSize().height() / 2); + + QMatrix4x4 mvp(projection); + mvp.translate(topLeft.x(), topLeft.y()); + shader->setUniform(GLShader::ModelViewProjectionMatrix, mvp); + + if (m_effectFrame->isCrossFade() && m_oldIconTexture) { + if (shader) { + const float a = opacity * (1.0 - m_effectFrame->crossFadeProgress()); + shader->setUniform(GLShader::ModulationConstant, QVector4D(a, a, a, a)); + } + + m_oldIconTexture->bind(); + m_oldIconTexture->render(region, QRect(topLeft, m_effectFrame->iconSize())); + m_oldIconTexture->unbind(); + if (shader) { + const float a = opacity * m_effectFrame->crossFadeProgress(); + shader->setUniform(GLShader::ModulationConstant, QVector4D(a, a, a, a)); + } + } else { + if (shader) { + const QVector4D constant(opacity, opacity, opacity, opacity); + shader->setUniform(GLShader::ModulationConstant, constant); + } + } + + if (!m_iconTexture) { // lazy creation + m_iconTexture = new GLTexture(m_effectFrame->icon().pixmap(m_effectFrame->iconSize())); + } + m_iconTexture->bind(); + m_iconTexture->render(region, QRect(topLeft, m_effectFrame->iconSize())); + m_iconTexture->unbind(); + } + + // Render text + if (!m_effectFrame->text().isEmpty()) { + QMatrix4x4 mvp(projection); + mvp.translate(m_effectFrame->geometry().x(), m_effectFrame->geometry().y()); + shader->setUniform(GLShader::ModelViewProjectionMatrix, mvp); + if (m_effectFrame->isCrossFade() && m_oldTextTexture) { + if (shader) { + const float a = opacity * (1.0 - m_effectFrame->crossFadeProgress()); + shader->setUniform(GLShader::ModulationConstant, QVector4D(a, a, a, a)); + } + + m_oldTextTexture->bind(); + m_oldTextTexture->render(region, m_effectFrame->geometry()); + m_oldTextTexture->unbind(); + if (shader) { + const float a = opacity * m_effectFrame->crossFadeProgress(); + shader->setUniform(GLShader::ModulationConstant, QVector4D(a, a, a, a)); + } + } else { + if (shader) { + const QVector4D constant(opacity, opacity, opacity, opacity); + shader->setUniform(GLShader::ModulationConstant, constant); + } + } + if (!m_textTexture) // Lazy creation + updateTextTexture(); + + if (m_textTexture) { + m_textTexture->bind(); + m_textTexture->render(region, m_effectFrame->geometry()); + m_textTexture->unbind(); + } + } + + if (shader) { + ShaderManager::instance()->popShader(); + } + glDisable(GL_BLEND); +} + +void SceneOpenGL::EffectFrame::updateTexture() +{ + delete m_texture; + m_texture = nullptr; + if (m_effectFrame->style() == EffectFrameStyled) { + QPixmap pixmap = m_effectFrame->frame().framePixmap(); + m_texture = new GLTexture(pixmap); + } +} + +void SceneOpenGL::EffectFrame::updateTextTexture() +{ + delete m_textTexture; + m_textTexture = nullptr; + delete m_textPixmap; + m_textPixmap = nullptr; + + if (m_effectFrame->text().isEmpty()) + return; + + // Determine position on texture to paint text + QRect rect(QPoint(0, 0), m_effectFrame->geometry().size()); + if (!m_effectFrame->icon().isNull() && !m_effectFrame->iconSize().isEmpty()) + rect.setLeft(m_effectFrame->iconSize().width()); + + // If static size elide text as required + QString text = m_effectFrame->text(); + if (m_effectFrame->isStatic()) { + QFontMetrics metrics(m_effectFrame->font()); + text = metrics.elidedText(text, Qt::ElideRight, rect.width()); + } + + m_textPixmap = new QPixmap(m_effectFrame->geometry().size()); + m_textPixmap->fill(Qt::transparent); + QPainter p(m_textPixmap); + p.setFont(m_effectFrame->font()); + if (m_effectFrame->style() == EffectFrameStyled) + p.setPen(m_effectFrame->styledTextColor()); + else // TODO: What about no frame? Custom color setting required + p.setPen(Qt::white); + p.drawText(rect, m_effectFrame->alignment(), text); + p.end(); + m_textTexture = new GLTexture(*m_textPixmap); +} + +void SceneOpenGL::EffectFrame::updateUnstyledTexture() +{ + delete m_unstyledTexture; + m_unstyledTexture = nullptr; + delete m_unstyledPixmap; + m_unstyledPixmap = nullptr; + // Based off circle() from kwinxrenderutils.cpp +#define CS 8 + m_unstyledPixmap = new QPixmap(2 * CS, 2 * CS); + m_unstyledPixmap->fill(Qt::transparent); + QPainter p(m_unstyledPixmap); + p.setRenderHint(QPainter::Antialiasing); + p.setPen(Qt::NoPen); + p.setBrush(Qt::black); + p.drawEllipse(m_unstyledPixmap->rect()); + p.end(); +#undef CS + m_unstyledTexture = new GLTexture(*m_unstyledPixmap); +} + +void SceneOpenGL::EffectFrame::cleanup() +{ + delete m_unstyledTexture; + m_unstyledTexture = nullptr; + delete m_unstyledPixmap; + m_unstyledPixmap = nullptr; +} + +//**************************************** +// SceneOpenGL::Shadow +//**************************************** +class DecorationShadowTextureCache +{ +public: + ~DecorationShadowTextureCache(); + DecorationShadowTextureCache(const DecorationShadowTextureCache&) = delete; + static DecorationShadowTextureCache &instance(); + + void unregister(SceneOpenGLShadow *shadow); + QSharedPointer getTexture(SceneOpenGLShadow *shadow); + +private: + DecorationShadowTextureCache() = default; + struct Data { + QSharedPointer texture; + QVector shadows; + }; + QHash m_cache; +}; + +DecorationShadowTextureCache &DecorationShadowTextureCache::instance() +{ + static DecorationShadowTextureCache s_instance; + return s_instance; +} + +DecorationShadowTextureCache::~DecorationShadowTextureCache() +{ + Q_ASSERT(m_cache.isEmpty()); +} + +void DecorationShadowTextureCache::unregister(SceneOpenGLShadow *shadow) +{ + auto it = m_cache.begin(); + while (it != m_cache.end()) { + auto &d = it.value(); + // check whether the Vector of Shadows contains our shadow and remove all of them + auto glIt = d.shadows.begin(); + while (glIt != d.shadows.end()) { + if (*glIt == shadow) { + glIt = d.shadows.erase(glIt); + } else { + glIt++; + } + } + // if there are no shadows any more we can erase the cache entry + if (d.shadows.isEmpty()) { + it = m_cache.erase(it); + } else { + it++; + } + } +} + +QSharedPointer DecorationShadowTextureCache::getTexture(SceneOpenGLShadow *shadow) +{ + Q_ASSERT(shadow->hasDecorationShadow()); + unregister(shadow); + const auto &decoShadow = shadow->decorationShadow().toStrongRef(); + Q_ASSERT(!decoShadow.isNull()); + auto it = m_cache.find(decoShadow.data()); + if (it != m_cache.end()) { + Q_ASSERT(!it.value().shadows.contains(shadow)); + it.value().shadows << shadow; + return it.value().texture; + } + Data d; + d.shadows << shadow; + d.texture = QSharedPointer::create(shadow->decorationShadowImage()); + m_cache.insert(decoShadow.data(), d); + return d.texture; +} + +SceneOpenGLShadow::SceneOpenGLShadow(Toplevel *toplevel) + : Shadow(toplevel) +{ +} + +SceneOpenGLShadow::~SceneOpenGLShadow() +{ + Scene *scene = Compositor::self()->scene(); + if (scene) { + scene->makeOpenGLContextCurrent(); + DecorationShadowTextureCache::instance().unregister(this); + m_texture.reset(); + } +} + +static inline void distributeHorizontally(QRectF &leftRect, QRectF &rightRect) +{ + if (leftRect.right() > rightRect.left()) { + const qreal boundedRight = qMin(leftRect.right(), rightRect.right()); + const qreal boundedLeft = qMax(leftRect.left(), rightRect.left()); + const qreal halfOverlap = (boundedRight - boundedLeft) / 2.0; + leftRect.setRight(boundedRight - halfOverlap); + rightRect.setLeft(boundedLeft + halfOverlap); + } +} + +static inline void distributeVertically(QRectF &topRect, QRectF &bottomRect) +{ + if (topRect.bottom() > bottomRect.top()) { + const qreal boundedBottom = qMin(topRect.bottom(), bottomRect.bottom()); + const qreal boundedTop = qMax(topRect.top(), bottomRect.top()); + const qreal halfOverlap = (boundedBottom - boundedTop) / 2.0; + topRect.setBottom(boundedBottom - halfOverlap); + bottomRect.setTop(boundedTop + halfOverlap); + } +} + +void SceneOpenGLShadow::buildQuads() +{ + // Do not draw shadows if window width or window height is less than + // 5 px. 5 is an arbitrary choice. + if (topLevel()->width() < 5 || topLevel()->height() < 5) { + m_shadowQuads.clear(); + setShadowRegion(QRegion()); + return; + } + + const QSizeF top(elementSize(ShadowElementTop)); + const QSizeF topRight(elementSize(ShadowElementTopRight)); + const QSizeF right(elementSize(ShadowElementRight)); + const QSizeF bottomRight(elementSize(ShadowElementBottomRight)); + const QSizeF bottom(elementSize(ShadowElementBottom)); + const QSizeF bottomLeft(elementSize(ShadowElementBottomLeft)); + const QSizeF left(elementSize(ShadowElementLeft)); + const QSizeF topLeft(elementSize(ShadowElementTopLeft)); + + const QMarginsF shadowMargins( + std::max({topLeft.width(), left.width(), bottomLeft.width()}), + std::max({topLeft.height(), top.height(), topRight.height()}), + std::max({topRight.width(), right.width(), bottomRight.width()}), + std::max({bottomRight.height(), bottom.height(), bottomLeft.height()})); + + const QRectF outerRect(QPointF(-leftOffset(), -topOffset()), + QPointF(topLevel()->width() + rightOffset(), + topLevel()->height() + bottomOffset())); + + const int width = shadowMargins.left() + std::max(top.width(), bottom.width()) + shadowMargins.right(); + const int height = shadowMargins.top() + std::max(left.height(), right.height()) + shadowMargins.bottom(); + + QRectF topLeftRect; + if (!topLeft.isEmpty()) { + topLeftRect = QRectF(outerRect.topLeft(), topLeft); + } else { + topLeftRect = QRectF( + outerRect.left() + shadowMargins.left(), + outerRect.top() + shadowMargins.top(), + 0, 0); + } + + QRectF topRightRect; + if (!topRight.isEmpty()) { + topRightRect = QRectF( + outerRect.right() - topRight.width(), outerRect.top(), + topRight.width(), topRight.height()); + } else { + topRightRect = QRectF( + outerRect.right() - shadowMargins.right(), + outerRect.top() + shadowMargins.top(), + 0, 0); + } + + QRectF bottomRightRect; + if (!bottomRight.isEmpty()) { + bottomRightRect = QRectF( + outerRect.right() - bottomRight.width(), + outerRect.bottom() - bottomRight.height(), + bottomRight.width(), bottomRight.height()); + } else { + bottomRightRect = QRectF( + outerRect.right() - shadowMargins.right(), + outerRect.bottom() - shadowMargins.bottom(), + 0, 0); + } + + QRectF bottomLeftRect; + if (!bottomLeft.isEmpty()) { + bottomLeftRect = QRectF( + outerRect.left(), outerRect.bottom() - bottomLeft.height(), + bottomLeft.width(), bottomLeft.height()); + } else { + bottomLeftRect = QRectF( + outerRect.left() + shadowMargins.left(), + outerRect.bottom() - shadowMargins.bottom(), + 0, 0); + } + + // Re-distribute the corner tiles so no one of them is overlapping with others. + // By doing this, we assume that shadow's corner tiles are symmetric + // and it is OK to not draw top/right/bottom/left tile between corners. + // For example, let's say top-left and top-right tiles are overlapping. + // In that case, the right side of the top-left tile will be shifted to left, + // the left side of the top-right tile will shifted to right, and the top + // tile won't be rendered. + distributeHorizontally(topLeftRect, topRightRect); + distributeHorizontally(bottomLeftRect, bottomRightRect); + distributeVertically(topLeftRect, bottomLeftRect); + distributeVertically(topRightRect, bottomRightRect); + + qreal tx1 = 0.0, + tx2 = 0.0, + ty1 = 0.0, + ty2 = 0.0; + + m_shadowQuads.clear(); + + if (topLeftRect.isValid()) { + tx1 = 0.0; + ty1 = 0.0; + tx2 = topLeftRect.width() / width; + ty2 = topLeftRect.height() / height; + WindowQuad topLeftQuad(WindowQuadShadow); + topLeftQuad[0] = WindowVertex(topLeftRect.left(), topLeftRect.top(), tx1, ty1); + topLeftQuad[1] = WindowVertex(topLeftRect.right(), topLeftRect.top(), tx2, ty1); + topLeftQuad[2] = WindowVertex(topLeftRect.right(), topLeftRect.bottom(), tx2, ty2); + topLeftQuad[3] = WindowVertex(topLeftRect.left(), topLeftRect.bottom(), tx1, ty2); + m_shadowQuads.append(topLeftQuad); + } + + if (topRightRect.isValid()) { + tx1 = 1.0 - topRightRect.width() / width; + ty1 = 0.0; + tx2 = 1.0; + ty2 = topRightRect.height() / height; + WindowQuad topRightQuad(WindowQuadShadow); + topRightQuad[0] = WindowVertex(topRightRect.left(), topRightRect.top(), tx1, ty1); + topRightQuad[1] = WindowVertex(topRightRect.right(), topRightRect.top(), tx2, ty1); + topRightQuad[2] = WindowVertex(topRightRect.right(), topRightRect.bottom(), tx2, ty2); + topRightQuad[3] = WindowVertex(topRightRect.left(), topRightRect.bottom(), tx1, ty2); + m_shadowQuads.append(topRightQuad); + } + + if (bottomRightRect.isValid()) { + tx1 = 1.0 - bottomRightRect.width() / width; + tx2 = 1.0; + ty1 = 1.0 - bottomRightRect.height() / height; + ty2 = 1.0; + WindowQuad bottomRightQuad(WindowQuadShadow); + bottomRightQuad[0] = WindowVertex(bottomRightRect.left(), bottomRightRect.top(), tx1, ty1); + bottomRightQuad[1] = WindowVertex(bottomRightRect.right(), bottomRightRect.top(), tx2, ty1); + bottomRightQuad[2] = WindowVertex(bottomRightRect.right(), bottomRightRect.bottom(), tx2, ty2); + bottomRightQuad[3] = WindowVertex(bottomRightRect.left(), bottomRightRect.bottom(), tx1, ty2); + m_shadowQuads.append(bottomRightQuad); + } + + if (bottomLeftRect.isValid()) { + tx1 = 0.0; + tx2 = bottomLeftRect.width() / width; + ty1 = 1.0 - bottomLeftRect.height() / height; + ty2 = 1.0; + WindowQuad bottomLeftQuad(WindowQuadShadow); + bottomLeftQuad[0] = WindowVertex(bottomLeftRect.left(), bottomLeftRect.top(), tx1, ty1); + bottomLeftQuad[1] = WindowVertex(bottomLeftRect.right(), bottomLeftRect.top(), tx2, ty1); + bottomLeftQuad[2] = WindowVertex(bottomLeftRect.right(), bottomLeftRect.bottom(), tx2, ty2); + bottomLeftQuad[3] = WindowVertex(bottomLeftRect.left(), bottomLeftRect.bottom(), tx1, ty2); + m_shadowQuads.append(bottomLeftQuad); + } + + QRectF topRect( + QPointF(topLeftRect.right(), outerRect.top()), + QPointF(topRightRect.left(), outerRect.top() + top.height())); + + QRectF rightRect( + QPointF(outerRect.right() - right.width(), topRightRect.bottom()), + QPointF(outerRect.right(), bottomRightRect.top())); + + QRectF bottomRect( + QPointF(bottomLeftRect.right(), outerRect.bottom() - bottom.height()), + QPointF(bottomRightRect.left(), outerRect.bottom())); + + QRectF leftRect( + QPointF(outerRect.left(), topLeftRect.bottom()), + QPointF(outerRect.left() + left.width(), bottomLeftRect.top())); + + // Re-distribute left/right and top/bottom shadow tiles so they don't + // overlap when the window is too small. Please notice that we don't + // fix overlaps between left/top(left/bottom, right/top, and so on) + // corner tiles because corresponding counter parts won't be valid when + // the window is too small, which means they won't be rendered. + distributeHorizontally(leftRect, rightRect); + distributeVertically(topRect, bottomRect); + + if (topRect.isValid()) { + tx1 = shadowMargins.left() / width; + ty1 = 0.0; + tx2 = tx1 + top.width() / width; + ty2 = topRect.height() / height; + WindowQuad topQuad(WindowQuadShadow); + topQuad[0] = WindowVertex(topRect.left(), topRect.top(), tx1, ty1); + topQuad[1] = WindowVertex(topRect.right(), topRect.top(), tx2, ty1); + topQuad[2] = WindowVertex(topRect.right(), topRect.bottom(), tx2, ty2); + topQuad[3] = WindowVertex(topRect.left(), topRect.bottom(), tx1, ty2); + m_shadowQuads.append(topQuad); + } + + if (rightRect.isValid()) { + tx1 = 1.0 - rightRect.width() / width; + ty1 = shadowMargins.top() / height; + tx2 = 1.0; + ty2 = ty1 + right.height() / height; + WindowQuad rightQuad(WindowQuadShadow); + rightQuad[0] = WindowVertex(rightRect.left(), rightRect.top(), tx1, ty1); + rightQuad[1] = WindowVertex(rightRect.right(), rightRect.top(), tx2, ty1); + rightQuad[2] = WindowVertex(rightRect.right(), rightRect.bottom(), tx2, ty2); + rightQuad[3] = WindowVertex(rightRect.left(), rightRect.bottom(), tx1, ty2); + m_shadowQuads.append(rightQuad); + } + + if (bottomRect.isValid()) { + tx1 = shadowMargins.left() / width; + ty1 = 1.0 - bottomRect.height() / height; + tx2 = tx1 + bottom.width() / width; + ty2 = 1.0; + WindowQuad bottomQuad(WindowQuadShadow); + bottomQuad[0] = WindowVertex(bottomRect.left(), bottomRect.top(), tx1, ty1); + bottomQuad[1] = WindowVertex(bottomRect.right(), bottomRect.top(), tx2, ty1); + bottomQuad[2] = WindowVertex(bottomRect.right(), bottomRect.bottom(), tx2, ty2); + bottomQuad[3] = WindowVertex(bottomRect.left(), bottomRect.bottom(), tx1, ty2); + m_shadowQuads.append(bottomQuad); + } + + if (leftRect.isValid()) { + tx1 = 0.0; + ty1 = shadowMargins.top() / height; + tx2 = leftRect.width() / width; + ty2 = ty1 + left.height() / height; + WindowQuad leftQuad(WindowQuadShadow); + leftQuad[0] = WindowVertex(leftRect.left(), leftRect.top(), tx1, ty1); + leftQuad[1] = WindowVertex(leftRect.right(), leftRect.top(), tx2, ty1); + leftQuad[2] = WindowVertex(leftRect.right(), leftRect.bottom(), tx2, ty2); + leftQuad[3] = WindowVertex(leftRect.left(), leftRect.bottom(), tx1, ty2); + m_shadowQuads.append(leftQuad); + } +} + +bool SceneOpenGLShadow::prepareBackend() +{ + if (hasDecorationShadow()) { + // simplifies a lot by going directly to + Scene *scene = Compositor::self()->scene(); + scene->makeOpenGLContextCurrent(); + m_texture = DecorationShadowTextureCache::instance().getTexture(this); + + return true; + } + const QSize top(shadowPixmap(ShadowElementTop).size()); + const QSize topRight(shadowPixmap(ShadowElementTopRight).size()); + const QSize right(shadowPixmap(ShadowElementRight).size()); + const QSize bottom(shadowPixmap(ShadowElementBottom).size()); + const QSize bottomLeft(shadowPixmap(ShadowElementBottomLeft).size()); + const QSize left(shadowPixmap(ShadowElementLeft).size()); + const QSize topLeft(shadowPixmap(ShadowElementTopLeft).size()); + const QSize bottomRight(shadowPixmap(ShadowElementBottomRight).size()); + + const int width = std::max({topLeft.width(), left.width(), bottomLeft.width()}) + + std::max(top.width(), bottom.width()) + + std::max({topRight.width(), right.width(), bottomRight.width()}); + const int height = std::max({topLeft.height(), top.height(), topRight.height()}) + + std::max(left.height(), right.height()) + + std::max({bottomLeft.height(), bottom.height(), bottomRight.height()}); + + if (width == 0 || height == 0) { + return false; + } + + QImage image(width, height, QImage::Format_ARGB32); + image.fill(Qt::transparent); + + const int innerRectTop = std::max({topLeft.height(), top.height(), topRight.height()}); + const int innerRectLeft = std::max({topLeft.width(), left.width(), bottomLeft.width()}); + + QPainter p; + p.begin(&image); + + p.drawPixmap(0, 0, topLeft.width(), topLeft.height(), shadowPixmap(ShadowElementTopLeft)); + p.drawPixmap(innerRectLeft, 0, top.width(), top.height(), shadowPixmap(ShadowElementTop)); + p.drawPixmap(width - topRight.width(), 0, topRight.width(), topRight.height(), shadowPixmap(ShadowElementTopRight)); + + p.drawPixmap(0, innerRectTop, left.width(), left.height(), shadowPixmap(ShadowElementLeft)); + p.drawPixmap(width - right.width(), innerRectTop, right.width(), right.height(), shadowPixmap(ShadowElementRight)); + + p.drawPixmap(0, height - bottomLeft.height(), bottomLeft.width(), bottomLeft.height(), shadowPixmap(ShadowElementBottomLeft)); + p.drawPixmap(innerRectLeft, height - bottom.height(), bottom.width(), bottom.height(), shadowPixmap(ShadowElementBottom)); + p.drawPixmap(width - bottomRight.width(), height - bottomRight.height(), bottomRight.width(), bottomRight.height(), shadowPixmap(ShadowElementBottomRight)); + + p.end(); + + // Check if the image is alpha-only in practice, and if so convert it to an 8-bpp format + if (!GLPlatform::instance()->isGLES() && GLTexture::supportsSwizzle() && GLTexture::supportsFormatRG()) { + QImage alphaImage(image.size(), QImage::Format_Indexed8); // Change to Format_Alpha8 w/ Qt 5.5 + bool alphaOnly = true; + + for (ptrdiff_t y = 0; alphaOnly && y < image.height(); y++) { + const uint32_t * const src = reinterpret_cast(image.scanLine(y)); + uint8_t * const dst = reinterpret_cast(alphaImage.scanLine(y)); + + for (ptrdiff_t x = 0; x < image.width(); x++) { + if (src[x] & 0x00ffffff) + alphaOnly = false; + + dst[x] = qAlpha(src[x]); + } + } + + if (alphaOnly) { + image = alphaImage; + } + } + + Scene *scene = Compositor::self()->scene(); + scene->makeOpenGLContextCurrent(); + m_texture = QSharedPointer::create(image); + + if (m_texture->internalFormat() == GL_R8) { + // Swizzle red to alpha and all other channels to zero + m_texture->bind(); + m_texture->setSwizzle(GL_ZERO, GL_ZERO, GL_ZERO, GL_RED); + } + + return true; +} + +SceneOpenGLDecorationRenderer::SceneOpenGLDecorationRenderer(Decoration::DecoratedClientImpl *client) + : Renderer(client) + , m_texture() +{ + connect(this, &Renderer::renderScheduled, client->client(), static_cast(&AbstractClient::addRepaint)); +} + +SceneOpenGLDecorationRenderer::~SceneOpenGLDecorationRenderer() +{ + if (Scene *scene = Compositor::self()->scene()) { + scene->makeOpenGLContextCurrent(); + } +} + +// Rotates the given source rect 90° counter-clockwise, +// and flips it vertically +static QImage rotate(const QImage &srcImage, const QRect &srcRect) +{ + auto dpr = srcImage.devicePixelRatio(); + QImage image(srcRect.height() * dpr, srcRect.width() * dpr, srcImage.format()); + image.setDevicePixelRatio(dpr); + const QPoint srcPoint(srcRect.x() * dpr, srcRect.y() * dpr); + + const uint32_t *src = reinterpret_cast(srcImage.bits()); + uint32_t *dst = reinterpret_cast(image.bits()); + + for (int x = 0; x < image.width(); x++) { + const uint32_t *s = src + (srcPoint.y() + x) * srcImage.width() + srcPoint.x(); + uint32_t *d = dst + x; + + for (int y = 0; y < image.height(); y++) { + *d = s[y]; + d += image.width(); + } + } + + return image; +} + +static void clamp_row(int left, int width, int right, const uint32_t *src, uint32_t *dest) +{ + std::fill_n(dest, left, *src); + std::copy(src, src + width, dest + left); + std::fill_n(dest + left + width, right, *(src + width - 1)); +} + +static void clamp_sides(int left, int width, int right, const uint32_t *src, uint32_t *dest) +{ + std::fill_n(dest, left, *src); + std::fill_n(dest + left + width, right, *(src + width - 1)); +} + +static void clamp(QImage &image, const QRect &viewport) +{ + Q_ASSERT(image.depth() == 32); + + const QRect rect = image.rect(); + + const int left = viewport.left() - rect.left(); + const int top = viewport.top() - rect.top(); + const int right = rect.right() - viewport.right(); + const int bottom = rect.bottom() - viewport.bottom(); + + const int width = rect.width() - left - right; + const int height = rect.height() - top - bottom; + + const uint32_t *firstRow = reinterpret_cast(image.scanLine(top)); + const uint32_t *lastRow = reinterpret_cast(image.scanLine(top + height - 1)); + + for (int i = 0; i < top; ++i) { + uint32_t *dest = reinterpret_cast(image.scanLine(i)); + clamp_row(left, width, right, firstRow + left, dest); + } + + for (int i = 0; i < height; ++i) { + uint32_t *dest = reinterpret_cast(image.scanLine(top + i)); + clamp_sides(left, width, right, dest + left, dest); + } + + for (int i = 0; i < bottom; ++i) { + uint32_t *dest = reinterpret_cast(image.scanLine(top + height + i)); + clamp_row(left, width, right, lastRow + left, dest); + } +} + +void SceneOpenGLDecorationRenderer::render() +{ + const QRegion scheduled = getScheduled(); + if (scheduled.isEmpty()) { + return; + } + if (areImageSizesDirty()) { + resizeTexture(); + resetImageSizesDirty(); + } + + if (!m_texture) { + // for invalid sizes we get no texture, see BUG 361551 + return; + } + + QRect left, top, right, bottom; + client()->client()->layoutDecorationRects(left, top, right, bottom); + + // We pad each part in the decoration atlas in order to avoid texture bleeding. + const int padding = 1; + + auto renderPart = [=](const QRect &geo, const QRect &partRect, const QPoint &position, bool rotated = false) { + if (!geo.isValid()) { + return; + } + + QRect rect = geo; + + // We allow partial decoration updates and it might just so happen that the dirty region + // is completely contained inside the decoration part, i.e. the dirty region doesn't touch + // any of the decoration's edges. In that case, we should **not** pad the dirty region. + if (rect.left() == partRect.left()) { + rect.setLeft(rect.left() - padding); + } + if (rect.top() == partRect.top()) { + rect.setTop(rect.top() - padding); + } + if (rect.right() == partRect.right()) { + rect.setRight(rect.right() + padding); + } + if (rect.bottom() == partRect.bottom()) { + rect.setBottom(rect.bottom() + padding); + } + + QRect viewport = geo.translated(-rect.x(), -rect.y()); + const qreal devicePixelRatio = client()->client()->screenScale(); + + QImage image(rect.size() * devicePixelRatio, QImage::Format_ARGB32_Premultiplied); + image.setDevicePixelRatio(devicePixelRatio); + image.fill(Qt::transparent); + + QPainter painter(&image); + painter.setRenderHint(QPainter::Antialiasing); + painter.setViewport(QRect(viewport.topLeft(), viewport.size() * devicePixelRatio)); + painter.setWindow(QRect(geo.topLeft(), geo.size() * devicePixelRatio)); + painter.setClipRect(geo); + renderToPainter(&painter, geo); + painter.end(); + + clamp(image, QRect(viewport.topLeft(), viewport.size() * devicePixelRatio)); + + if (rotated) { + // TODO: get this done directly when rendering to the image + image = rotate(image, QRect(QPoint(), rect.size())); + viewport = QRect(viewport.y(), viewport.x(), viewport.height(), viewport.width()); + } + + const QPoint dirtyOffset = geo.topLeft() - partRect.topLeft(); + m_texture->update(image, (position + dirtyOffset - viewport.topLeft()) * image.devicePixelRatio()); + }; + + const QRect geometry = scheduled.boundingRect(); + + const QPoint topPosition(padding, padding); + const QPoint bottomPosition(padding, topPosition.y() + top.height() + 2 * padding); + const QPoint leftPosition(padding, bottomPosition.y() + bottom.height() + 2 * padding); + const QPoint rightPosition(padding, leftPosition.y() + left.width() + 2 * padding); + + renderPart(left.intersected(geometry), left, leftPosition, true); + renderPart(top.intersected(geometry), top, topPosition); + renderPart(right.intersected(geometry), right, rightPosition, true); + renderPart(bottom.intersected(geometry), bottom, bottomPosition); +} + +static int align(int value, int align) +{ + return (value + align - 1) & ~(align - 1); +} + +void SceneOpenGLDecorationRenderer::resizeTexture() +{ + QRect left, top, right, bottom; + client()->client()->layoutDecorationRects(left, top, right, bottom); + QSize size; + + size.rwidth() = qMax(qMax(top.width(), bottom.width()), + qMax(left.height(), right.height())); + size.rheight() = top.height() + bottom.height() + + left.width() + right.width(); + + // Reserve some space for padding. We pad decoration parts to avoid texture bleeding. + const int padding = 1; + size.rwidth() += 2 * padding; + size.rheight() += 4 * 2 * padding; + + size.rwidth() = align(size.width(), 128); + + size *= client()->client()->screenScale(); + if (m_texture && m_texture->size() == size) + return; + + if (!size.isEmpty()) { + m_texture.reset(new GLTexture(GL_RGBA8, size.width(), size.height())); + m_texture->setYInverted(true); + m_texture->setWrapMode(GL_CLAMP_TO_EDGE); + m_texture->clear(); + } else { + m_texture.reset(); + } +} + +void SceneOpenGLDecorationRenderer::reparent(Deleted *deleted) +{ + render(); + Renderer::reparent(deleted); +} + + +OpenGLFactory::OpenGLFactory(QObject *parent) + : SceneFactory(parent) +{ +} + +OpenGLFactory::~OpenGLFactory() = default; + +Scene *OpenGLFactory::create(QObject *parent) const +{ + qCDebug(KWIN_OPENGL) << "Initializing OpenGL compositing"; + + // Some broken drivers crash on glXQuery() so to prevent constant KWin crashes: + if (kwinApp()->platform()->openGLCompositingIsBroken()) { + qCWarning(KWIN_OPENGL) << "KWin has detected that your OpenGL library is unsafe to use"; + return nullptr; + } + kwinApp()->platform()->createOpenGLSafePoint(Platform::OpenGLSafePoint::PreInit); + auto s = SceneOpenGL::createScene(parent); + kwinApp()->platform()->createOpenGLSafePoint(Platform::OpenGLSafePoint::PostInit); + if (s && s->initFailed()) { + delete s; + return nullptr; + } + return s; +} + +} // namespace diff --git a/plugins/scenes/opengl/scene_opengl.h b/plugins/scenes/opengl/scene_opengl.h new file mode 100644 index 0000000..ff8b9d8 --- /dev/null +++ b/plugins/scenes/opengl/scene_opengl.h @@ -0,0 +1,339 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2009, 2010, 2011 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_SCENE_OPENGL_H +#define KWIN_SCENE_OPENGL_H + +#include "scene.h" +#include "shadow.h" + +#include "kwinglutils.h" + +#include "decorations/decorationrenderer.h" +#include "platformsupport/scenes/opengl/backend.h" + +namespace KWin +{ +class LanczosFilter; +class OpenGLBackend; +class SyncManager; +class SyncObject; + +class KWIN_EXPORT SceneOpenGL + : public Scene +{ + Q_OBJECT +public: + class EffectFrame; + ~SceneOpenGL() override; + bool initFailed() const override; + bool hasPendingFlush() const override; + qint64 paint(const QRegion &damage, const QList &windows) override; + Scene::EffectFrame *createEffectFrame(EffectFrameImpl *frame) override; + Shadow *createShadow(Toplevel *toplevel) override; + void screenGeometryChanged(const QSize &size) override; + OverlayWindow *overlayWindow() const override; + bool usesOverlayWindow() const override; + bool blocksForRetrace() const override; + bool syncsToVBlank() const override; + bool makeOpenGLContextCurrent() override; + void doneOpenGLContextCurrent() override; + bool supportsSurfacelessContext() const override; + Decoration::Renderer *createDecorationRenderer(Decoration::DecoratedClientImpl *impl) override; + void triggerFence() override; + virtual QMatrix4x4 projectionMatrix() const = 0; + bool animationsSupported() const override; + + void insertWait(); + + void idle() override; + + bool debug() const { return m_debug; } + void initDebugOutput(); + + /** + * @brief Factory method to create a backend specific texture. + * + * @return :SceneOpenGL::Texture* + */ + SceneOpenGLTexture *createTexture(); + + OpenGLBackend *backend() const { + return m_backend; + } + + QVector openGLPlatformInterfaceExtensions() const override; + QSharedPointer textureForOutput(AbstractOutput *output) const override; + + static SceneOpenGL *createScene(QObject *parent); + +protected: + SceneOpenGL(OpenGLBackend *backend, QObject *parent = nullptr); + void paintBackground(const QRegion ®ion) override; + void aboutToStartPainting(const QRegion &damage) override; + void extendPaintRegion(QRegion ®ion, bool opaqueFullscreen) override; + QMatrix4x4 transformation(int mask, const ScreenPaintData &data) const; + void paintDesktop(int desktop, int mask, const QRegion ®ion, ScreenPaintData &data) override; + void paintEffectQuickView(EffectQuickView *w) override; + + void handleGraphicsReset(GLenum status); + + virtual void doPaintBackground(const QVector &vertices) = 0; + virtual void updateProjectionMatrix() = 0; + +protected: + bool init_ok; +private: + bool viewportLimitsMatched(const QSize &size) const; + +private: + bool m_debug; + OpenGLBackend *m_backend; + SyncManager *m_syncManager; + SyncObject *m_currentFence; +}; + +class SceneOpenGL2 : public SceneOpenGL +{ + Q_OBJECT +public: + explicit SceneOpenGL2(OpenGLBackend *backend, QObject *parent = nullptr); + ~SceneOpenGL2() override; + CompositingType compositingType() const override { + return OpenGL2Compositing; + } + + static bool supported(OpenGLBackend *backend); + + QMatrix4x4 projectionMatrix() const override { return m_projectionMatrix; } + QMatrix4x4 screenProjectionMatrix() const override { return m_screenProjectionMatrix; } + +protected: + void paintSimpleScreen(int mask, const QRegion ®ion) override; + void paintGenericScreen(int mask, const ScreenPaintData &data) override; + void doPaintBackground(const QVector< float >& vertices) override; + Scene::Window *createWindow(Toplevel *t) override; + void finalDrawWindow(EffectWindowImpl* w, int mask, const QRegion ®ion, WindowPaintData& data) override; + void updateProjectionMatrix() override; + void paintCursor(const QRegion ®ion) override; + +private: + void performPaintWindow(EffectWindowImpl* w, int mask, const QRegion ®ion, WindowPaintData& data); + QMatrix4x4 createProjectionMatrix() const; + +private: + LanczosFilter *m_lanczosFilter; + QScopedPointer m_cursorTexture; + QMatrix4x4 m_projectionMatrix; + QMatrix4x4 m_screenProjectionMatrix; + GLuint vao; +}; + +class OpenGLWindowPixmap; + +class OpenGLWindow final : public Scene::Window +{ + Q_OBJECT + +public: + enum Leaf { ShadowLeaf, DecorationLeaf, ContentLeaf, PreviousContentLeaf }; + + struct RenderNode + { + RenderNode() + : texture(nullptr) + , firstVertex(0) + , vertexCount(0) + , opacity(1.0) + , hasAlpha(false) + , coordinateType(UnnormalizedCoordinates) + { + } + + GLTexture *texture; + WindowQuadList quads; + int firstVertex; + int vertexCount; + float opacity; + bool hasAlpha; + TextureCoordinateType coordinateType; + Leaf leafType; + }; + + struct RenderContext + { + QVector renderNodes; + int shadowOffset = 0; + int decorationOffset = 0; + int contentOffset = 0; + int previousContentOffset = 0; + int quadCount = 0; + }; + + OpenGLWindow(Toplevel *toplevel, SceneOpenGL *scene); + ~OpenGLWindow() override; + + WindowPixmap *createWindowPixmap() override; + void performPaint(int mask, const QRegion ®ion, const WindowPaintData &data) override; + QSharedPointer windowTexture() override; + +private: + QMatrix4x4 transformation(int mask, const WindowPaintData &data) const; + GLTexture *getDecorationTexture() const; + QMatrix4x4 modelViewProjectionMatrix(int mask, const WindowPaintData &data) const; + QVector4D modulate(float opacity, float brightness) const; + void setBlendEnabled(bool enabled); + void initializeRenderContext(RenderContext &context, const WindowPaintData &data); + bool beginRenderWindow(int mask, const QRegion ®ion, WindowPaintData &data); + void endRenderWindow(); + bool bindTexture(); + + SceneOpenGL *m_scene; + bool m_hardwareClipping = false; + bool m_blendingEnabled = false; +}; + +class OpenGLWindowPixmap : public WindowPixmap +{ +public: + explicit OpenGLWindowPixmap(Scene::Window *window, SceneOpenGL *scene); + ~OpenGLWindowPixmap() override; + SceneOpenGLTexture *texture() const; + bool bind(); + bool isValid() const override; +protected: + WindowPixmap *createChild(const QPointer &subSurface) override; +private: + explicit OpenGLWindowPixmap(const QPointer &subSurface, WindowPixmap *parent, SceneOpenGL *scene); + QScopedPointer m_texture; + SceneOpenGL *m_scene; +}; + +class SceneOpenGL::EffectFrame + : public Scene::EffectFrame +{ +public: + EffectFrame(EffectFrameImpl* frame, SceneOpenGL *scene); + ~EffectFrame() override; + + void free() override; + void freeIconFrame() override; + void freeTextFrame() override; + void freeSelection() override; + + void render(const QRegion ®ion, double opacity, double frameOpacity) override; + + void crossFadeIcon() override; + void crossFadeText() override; + + static void cleanup(); + +private: + void updateTexture(); + void updateTextTexture(); + + GLTexture *m_texture; + GLTexture *m_textTexture; + GLTexture *m_oldTextTexture; + QPixmap *m_textPixmap; // need to keep the pixmap around to workaround some driver problems + GLTexture *m_iconTexture; + GLTexture *m_oldIconTexture; + GLTexture *m_selectionTexture; + GLVertexBuffer *m_unstyledVBO; + SceneOpenGL *m_scene; + + static GLTexture* m_unstyledTexture; + static QPixmap* m_unstyledPixmap; // need to keep the pixmap around to workaround some driver problems + static void updateUnstyledTexture(); // Update OpenGL unstyled frame texture +}; + +/** + * @short OpenGL implementation of Shadow. + * + * This class extends Shadow by the Elements required for OpenGL rendering. + * @author Martin Gräßlin + */ +class SceneOpenGLShadow + : public Shadow +{ +public: + explicit SceneOpenGLShadow(Toplevel *toplevel); + ~SceneOpenGLShadow() override; + + GLTexture *shadowTexture() { + return m_texture.data(); + } +protected: + void buildQuads() override; + bool prepareBackend() override; +private: + QSharedPointer m_texture; +}; + +class SceneOpenGLDecorationRenderer : public Decoration::Renderer +{ + Q_OBJECT +public: + enum class DecorationPart : int { + Left, + Top, + Right, + Bottom, + Count + }; + explicit SceneOpenGLDecorationRenderer(Decoration::DecoratedClientImpl *client); + ~SceneOpenGLDecorationRenderer() override; + + void render() override; + void reparent(Deleted *deleted) override; + + GLTexture *texture() { + return m_texture.data(); + } + GLTexture *texture() const { + return m_texture.data(); + } + +private: + void resizeTexture(); + QScopedPointer m_texture; +}; + +inline bool SceneOpenGL::hasPendingFlush() const +{ + return m_backend->hasPendingFlush(); +} + +inline bool SceneOpenGL::usesOverlayWindow() const +{ + return m_backend->usesOverlayWindow(); +} + +inline SceneOpenGLTexture* OpenGLWindowPixmap::texture() const +{ + return m_texture.data(); +} + +class KWIN_EXPORT OpenGLFactory : public SceneFactory +{ + Q_OBJECT + Q_INTERFACES(KWin::SceneFactory) + Q_PLUGIN_METADATA(IID "org.kde.kwin.Scene" FILE "opengl.json") + +public: + explicit OpenGLFactory(QObject *parent = nullptr); + ~OpenGLFactory() override; + + Scene *create(QObject *parent = nullptr) const override; +}; + +} // namespace + +#endif diff --git a/plugins/scenes/opengl/shaders/1.10/lanczos-fragment.glsl b/plugins/scenes/opengl/shaders/1.10/lanczos-fragment.glsl new file mode 100644 index 0000000..b6b135c --- /dev/null +++ b/plugins/scenes/opengl/shaders/1.10/lanczos-fragment.glsl @@ -0,0 +1,16 @@ +uniform sampler2D sampler; +uniform vec2 offsets[16]; +uniform vec4 kernel[16]; + +varying vec2 texcoord0; + +void main(void) +{ + vec4 sum = texture2D(sampler, texcoord0.st) * kernel[0]; + for (int i = 1; i < 16; i++) { + sum += texture2D(sampler, texcoord0.st - offsets[i]) * kernel[i]; + sum += texture2D(sampler, texcoord0.st + offsets[i]) * kernel[i]; + } + gl_FragColor = sum; +} + diff --git a/plugins/scenes/opengl/shaders/1.40/lanczos-fragment.glsl b/plugins/scenes/opengl/shaders/1.40/lanczos-fragment.glsl new file mode 100644 index 0000000..a77d560 --- /dev/null +++ b/plugins/scenes/opengl/shaders/1.40/lanczos-fragment.glsl @@ -0,0 +1,19 @@ +#version 140 + +uniform sampler2D sampler; +uniform vec2 offsets[16]; +uniform vec4 kernel[16]; + +in vec2 texcoord0; +out vec4 fragColor; + +void main(void) +{ + vec4 sum = texture(sampler, texcoord0.st) * kernel[0]; + for (int i = 1; i < 16; i++) { + sum += texture(sampler, texcoord0.st - offsets[i]) * kernel[i]; + sum += texture(sampler, texcoord0.st + offsets[i]) * kernel[i]; + } + fragColor = sum; +} + diff --git a/plugins/scenes/qpainter/CMakeLists.txt b/plugins/scenes/qpainter/CMakeLists.txt new file mode 100644 index 0000000..24f0d9c --- /dev/null +++ b/plugins/scenes/qpainter/CMakeLists.txt @@ -0,0 +1,15 @@ +set(SCENE_QPAINTER_SRCS scene_qpainter.cpp) + +add_library(KWinSceneQPainter MODULE scene_qpainter.cpp) +set_target_properties(KWinSceneQPainter PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/org.kde.kwin.scenes/") +target_link_libraries(KWinSceneQPainter + kwin + SceneQPainterBackend +) + +install( + TARGETS + KWinSceneQPainter + DESTINATION + ${PLUGIN_INSTALL_DIR}/org.kde.kwin.scenes/ +) diff --git a/plugins/scenes/qpainter/qpainter.json b/plugins/scenes/qpainter/qpainter.json new file mode 100644 index 0000000..a140b20 --- /dev/null +++ b/plugins/scenes/qpainter/qpainter.json @@ -0,0 +1,82 @@ +{ + "CompositingType": 4, + "KPlugin": { + "Description": "KWin Compositor plugin rendering through QPainter", + "Description[az]": "QPainter vasitəsi ilə KWin birləşdirici əlavəsini formalaşdırmaq", + "Description[ca@valencia]": "Connector del Compositor de KWin que renderitza a través de QPainter", + "Description[ca]": "Connector del Compositor del KWin que renderitza a través del QPainter", + "Description[da]": "KWin-compositorplugin som renderer igennem QPainter", + "Description[de]": "KWin-Compositor-Modul zum Rendern mit QPainter", + "Description[el]": "Αποτύπωση πρσοθέτου συνθέτη KWin μέσω QPainter", + "Description[en_GB]": "KWin Compositor plugin rendering through QPainter", + "Description[es]": "Complemento compositor de KWin renderizando mediante QPainter", + "Description[et]": "KWini komposiitori plugin renderdamiseks QPainteri abil", + "Description[eu]": "Kwin konposatzailearen plugina QPainter bidez errendatzen", + "Description[fi]": "QPainterillä hahmontava KWin-koostajaliitännäinen", + "Description[fr]": "Module du compositeur KWin effectuant le rendu avec QPainter", + "Description[gl]": "Complemento de compositor de KWin que renderiza a través de QPainter.", + "Description[hu]": "KWin összeállító bővítmény QPainter leképezéssel", + "Description[id]": "Plugin KWin Compositor perenderan melalui QPainter", + "Description[it]": "Estensione del compositore di KWin per la resa tramite QPainter", + "Description[ko]": "QPainter로 렌더링하는 KWin 컴포지터 플러그인", + "Description[lt]": "KWin kompozitoriaus priedas atvaizdavimui per QPainter", + "Description[nl]": "KWin-compositor-plug-in rendering via QPainter", + "Description[nn]": "KWin-samansetjartillegg som brukar QPainter", + "Description[pl]": "Wtyczka kompozytora KWin wyświetlająca przez QPainter", + "Description[pt]": "'Plugin' de Composição do KWin com desenho via QPainter", + "Description[pt_BR]": "Plugin do compositor KWin renderizando pelo QPainter", + "Description[ru]": "Отрисовка подключаемым модулем компоновщика KWin через QPainter", + "Description[sk]": "Renderovací plugin kompozítora KWin cez QPainter", + "Description[sl]": "Izrisovanje vstavka upravljalnika skladnje KWin preko QPainter-ja", + "Description[sr@ijekavian]": "К‑винов прикључак слагача за рендеровање кроз QPainter", + "Description[sr@ijekavianlatin]": "KWinov priključak slagača za renderovanje kroz QPainter", + "Description[sr@latin]": "KWinov priključak slagača za renderovanje kroz QPainter", + "Description[sr]": "К‑винов прикључак слагача за рендеровање кроз QPainter", + "Description[sv]": "Kwin sammansättningsinsticksprogram Ã¥terger via QPainter", + "Description[tr]": "QPainter üzerinden KWin Dizgici eklentisi oluşturma", + "Description[uk]": "Додаток засобу композиції KWin для обробки з використанням QPainter", + "Description[x-test]": "xxKWin Compositor plugin rendering through QPainterxx", + "Description[zh_CN]": "使用 QPainter 渲染的 KWin 混成插件", + "Description[zh_TW]": "透過 QPainter 繪製 KWin 合成器附加元件", + "Id": "KWinSceneQPainter", + "Name": "SceneQPainter", + "Name[az]": "SceneQPainter", + "Name[ca@valencia]": "SceneQPainter", + "Name[ca]": "SceneQPainter", + "Name[cs]": "SceneQPainter", + "Name[da]": "SceneQPainter", + "Name[de]": "SceneQPainter", + "Name[el]": "SceneQPainter", + "Name[en_GB]": "SceneQPainter", + "Name[es]": "SceneQPainter", + "Name[et]": "SceneQPainter", + "Name[eu]": "SceneQPainter", + "Name[fi]": "SceneQPainter", + "Name[fr]": "SceneQPainter", + "Name[gl]": "SceneQPainter", + "Name[hu]": "SceneQPainter", + "Name[id]": "SceneQPainter", + "Name[it]": "SceneQPainter", + "Name[ko]": "SceneQPainter", + "Name[lt]": "SceneQPainter", + "Name[nl]": "SceneQPainter", + "Name[nn]": "SceneQPainter", + "Name[pl]": "QPainter sceny", + "Name[pt]": "SceneQPainter", + "Name[pt_BR]": "SceneQPainter", + "Name[ro]": "SceneQPainter", + "Name[ru]": "SceneQPainter", + "Name[sk]": "SceneQPainter", + "Name[sl]": "SceneQPainter", + "Name[sr@ijekavian]": "QPainter-сцена", + "Name[sr@ijekavianlatin]": "QPainter-scena", + "Name[sr@latin]": "QPainter-scena", + "Name[sr]": "QPainter-сцена", + "Name[sv]": "Scen QPainter", + "Name[tr]": "SceneQPainter", + "Name[uk]": "SceneQPainter", + "Name[x-test]": "xxSceneQPainterxx", + "Name[zh_CN]": "SceneQPainter", + "Name[zh_TW]": "SceneQPainter" + } +} diff --git a/plugins/scenes/qpainter/scene_qpainter.cpp b/plugins/scenes/qpainter/scene_qpainter.cpp new file mode 100644 index 0000000..953bd6a --- /dev/null +++ b/plugins/scenes/qpainter/scene_qpainter.cpp @@ -0,0 +1,877 @@ +/* + 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_qpainter.h" +// KWin +#include "abstract_client.h" +#include "composite.h" +#include "cursor.h" +#include "deleted.h" +#include "effects.h" +#include "main.h" +#include "screens.h" +#include "toplevel.h" +#include "platform.h" +#include "wayland_server.h" + +#include + +#include +#include +#include +#include "decorations/decoratedclient.h" +// Qt +#include +#include +#include + +#include + +namespace KWin +{ + +//**************************************** +// SceneQPainter +//**************************************** +SceneQPainter *SceneQPainter::createScene(QObject *parent) +{ + QScopedPointer backend(kwinApp()->platform()->createQPainterBackend()); + if (backend.isNull()) { + return nullptr; + } + if (backend->isFailed()) { + return nullptr; + } + return new SceneQPainter(backend.take(), parent); +} + +SceneQPainter::SceneQPainter(QPainterBackend *backend, QObject *parent) + : Scene(parent) + , m_backend(backend) + , m_painter(new QPainter()) +{ +} + +SceneQPainter::~SceneQPainter() +{ +} + +CompositingType SceneQPainter::compositingType() const +{ + return QPainterCompositing; +} + +bool SceneQPainter::initFailed() const +{ + return false; +} + +void SceneQPainter::paintGenericScreen(int mask, const ScreenPaintData &data) +{ + m_painter->save(); + m_painter->translate(data.xTranslation(), data.yTranslation()); + m_painter->scale(data.xScale(), data.yScale()); + Scene::paintGenericScreen(mask, data); + m_painter->restore(); +} + +qint64 SceneQPainter::paint(const QRegion &_damage, const QList &toplevels) +{ + QElapsedTimer renderTimer; + renderTimer.start(); + + createStackingOrder(toplevels); + QRegion damage = _damage; + + int mask = 0; + m_backend->prepareRenderingFrame(); + if (m_backend->perScreenRendering()) { + const bool needsFullRepaint = m_backend->needsFullRepaint(); + if (needsFullRepaint) { + mask |= Scene::PAINT_SCREEN_BACKGROUND_FIRST; + damage = screens()->geometry(); + } + QRegion overallUpdate; + for (int i = 0; i < screens()->count(); ++i) { + const QRect geometry = screens()->geometry(i); + QImage *buffer = m_backend->bufferForScreen(i); + if (!buffer || buffer->isNull()) { + continue; + } + m_painter->begin(buffer); + m_painter->save(); + m_painter->setWindow(geometry); + + QRegion updateRegion, validRegion; + paintScreen(&mask, damage.intersected(geometry), QRegion(), &updateRegion, &validRegion); + overallUpdate = overallUpdate.united(updateRegion); + paintCursor(updateRegion); + + m_painter->restore(); + m_painter->end(); + } + m_backend->showOverlay(); + m_backend->present(mask, overallUpdate); + } else { + m_painter->begin(m_backend->buffer()); + m_painter->setClipping(true); + m_painter->setClipRegion(damage); + if (m_backend->needsFullRepaint()) { + mask |= Scene::PAINT_SCREEN_BACKGROUND_FIRST; + damage = screens()->geometry(); + } + QRegion updateRegion, validRegion; + paintScreen(&mask, damage, QRegion(), &updateRegion, &validRegion); + + paintCursor(updateRegion); + m_backend->showOverlay(); + + m_painter->end(); + m_backend->present(mask, updateRegion); + } + + // do cleanup + clearStackingOrder(); + + return renderTimer.nsecsElapsed(); +} + +void SceneQPainter::paintBackground(const QRegion ®ion) +{ + m_painter->setBrush(Qt::black); + for (const QRect &rect : region) { + m_painter->drawRect(rect); + } +} + +void SceneQPainter::paintCursor(const QRegion &rendered) +{ + if (!kwinApp()->platform()->usesSoftwareCursor()) { + return; + } + + Cursor* cursor = Cursors::self()->currentCursor(); + const QImage img = cursor->image(); + if (img.isNull()) { + return; + } + + m_painter->save(); + m_painter->setClipRegion(rendered.intersected(cursor->geometry())); + m_painter->drawImage(cursor->geometry(), img); + m_painter->restore(); +} + +void SceneQPainter::paintEffectQuickView(EffectQuickView *w) +{ + QPainter *painter = effects->scenePainter(); + const QImage buffer = w->bufferAsImage(); + if (buffer.isNull()) { + return; + } + painter->drawImage(w->geometry(), buffer); +} + +Scene::Window *SceneQPainter::createWindow(Toplevel *toplevel) +{ + return new SceneQPainter::Window(this, toplevel); +} + +Scene::EffectFrame *SceneQPainter::createEffectFrame(EffectFrameImpl *frame) +{ + return new QPainterEffectFrame(frame, this); +} + +Shadow *SceneQPainter::createShadow(Toplevel *toplevel) +{ + return new SceneQPainterShadow(toplevel); +} + +void SceneQPainter::screenGeometryChanged(const QSize &size) +{ + Scene::screenGeometryChanged(size); + m_backend->screenGeometryChanged(size); +} + +QImage *SceneQPainter::qpainterRenderBuffer() const +{ + return m_backend->buffer(); +} + +//**************************************** +// SceneQPainter::Window +//**************************************** +SceneQPainter::Window::Window(SceneQPainter *scene, Toplevel *c) + : Scene::Window(c) + , m_scene(scene) +{ +} + +SceneQPainter::Window::~Window() +{ +} + +void SceneQPainter::Window::performPaint(int mask, const QRegion &_region, const WindowPaintData &data) +{ + QRegion region = _region; + if (!(mask & (PAINT_WINDOW_TRANSFORMED | PAINT_SCREEN_TRANSFORMED))) + region &= toplevel->visibleRect(); + + if (region.isEmpty()) + return; + QPainterWindowPixmap *pixmap = windowPixmap(); + if (!pixmap || !pixmap->isValid()) { + return; + } + toplevel->resetDamage(); + + QPainter *scenePainter = m_scene->scenePainter(); + QPainter *painter = scenePainter; + painter->save(); + painter->setClipRegion(region); + painter->setClipping(true); + + painter->translate(x(), y()); + if (mask & PAINT_WINDOW_TRANSFORMED) { + painter->translate(data.xTranslation(), data.yTranslation()); + painter->scale(data.xScale(), data.yScale()); + } + + const bool opaque = qFuzzyCompare(1.0, data.opacity()); + QImage tempImage; + QPainter tempPainter; + if (!opaque) { + // need a temp render target which we later on blit to the screen + tempImage = QImage(toplevel->visibleRect().size(), QImage::Format_ARGB32_Premultiplied); + tempImage.fill(Qt::transparent); + tempPainter.begin(&tempImage); + tempPainter.save(); + tempPainter.translate(toplevel->frameGeometry().topLeft() - toplevel->visibleRect().topLeft()); + painter = &tempPainter; + } + renderShadow(painter); + renderWindowDecorations(painter); + renderWindowPixmap(painter, pixmap); + + if (!opaque) { + tempPainter.restore(); + tempPainter.setCompositionMode(QPainter::CompositionMode_DestinationIn); + QColor translucent(Qt::transparent); + translucent.setAlphaF(data.opacity()); + tempPainter.fillRect(QRect(QPoint(0, 0), toplevel->visibleRect().size()), translucent); + tempPainter.end(); + painter = scenePainter; + painter->drawImage(toplevel->visibleRect().topLeft() - toplevel->frameGeometry().topLeft(), tempImage); + } + + painter->restore(); +} + +void SceneQPainter::Window::renderWindowPixmap(QPainter *painter, QPainterWindowPixmap *windowPixmap) +{ + const QRegion shape = windowPixmap->shape(); + for (const QRectF rect : shape) { + const QPointF windowTopLeft = windowPixmap->mapToWindow(rect.topLeft()); + const QPointF windowBottomRight = windowPixmap->mapToWindow(rect.bottomRight()); + + const QPointF bufferTopLeft = windowPixmap->mapToBuffer(rect.topLeft()); + const QPointF bufferBottomRight = windowPixmap->mapToBuffer(rect.bottomRight()); + + painter->drawImage(QRectF(windowTopLeft, windowBottomRight), + windowPixmap->image(), + QRectF(bufferTopLeft, bufferBottomRight)); + } + + const QVector children = windowPixmap->children(); + for (WindowPixmap *child : children) { + QPainterWindowPixmap *scenePixmap = static_cast(child); + if (scenePixmap->isValid()) { + renderWindowPixmap(painter, scenePixmap); + } + } +} + +void SceneQPainter::Window::renderShadow(QPainter* painter) +{ + if (!toplevel->shadow()) { + return; + } + SceneQPainterShadow *shadow = static_cast(toplevel->shadow()); + + const QImage &shadowTexture = shadow->shadowTexture(); + const WindowQuadList &shadowQuads = shadow->shadowQuads(); + + for (const auto &q : shadowQuads) { + auto topLeft = q[0]; + auto bottomRight = q[2]; + QRectF target(topLeft.x(), topLeft.y(), + bottomRight.x() - topLeft.x(), + bottomRight.y() - topLeft.y()); + QRectF source(topLeft.textureX(), topLeft.textureY(), + bottomRight.textureX() - topLeft.textureX(), + bottomRight.textureY() - topLeft.textureY()); + painter->drawImage(target, shadowTexture, source); + } +} + +void SceneQPainter::Window::renderWindowDecorations(QPainter *painter) +{ + // TODO: custom decoration opacity + AbstractClient *client = dynamic_cast(toplevel); + Deleted *deleted = dynamic_cast(toplevel); + if (!client && !deleted) { + return; + } + + bool noBorder = true; + const SceneQPainterDecorationRenderer *renderer = nullptr; + QRect dtr, dlr, drr, dbr; + if (client && !client->noBorder()) { + if (client->isDecorated()) { + if (SceneQPainterDecorationRenderer *r = static_cast(client->decoratedClient()->renderer())) { + r->render(); + renderer = r; + } + } + client->layoutDecorationRects(dlr, dtr, drr, dbr); + noBorder = false; + } else if (deleted && !deleted->noBorder()) { + noBorder = false; + deleted->layoutDecorationRects(dlr, dtr, drr, dbr); + renderer = static_cast(deleted->decorationRenderer()); + } + if (noBorder || !renderer) { + return; + } + + painter->drawImage(dtr, renderer->image(SceneQPainterDecorationRenderer::DecorationPart::Top)); + painter->drawImage(dlr, renderer->image(SceneQPainterDecorationRenderer::DecorationPart::Left)); + painter->drawImage(drr, renderer->image(SceneQPainterDecorationRenderer::DecorationPart::Right)); + painter->drawImage(dbr, renderer->image(SceneQPainterDecorationRenderer::DecorationPart::Bottom)); +} + +WindowPixmap *SceneQPainter::Window::createWindowPixmap() +{ + return new QPainterWindowPixmap(this); +} + +Decoration::Renderer *SceneQPainter::createDecorationRenderer(Decoration::DecoratedClientImpl *impl) +{ + return new SceneQPainterDecorationRenderer(impl); +} + +//**************************************** +// QPainterWindowPixmap +//**************************************** +QPainterWindowPixmap::QPainterWindowPixmap(Scene::Window *window) + : WindowPixmap(window) +{ +} + +QPainterWindowPixmap::QPainterWindowPixmap(const QPointer &subSurface, WindowPixmap *parent) + : WindowPixmap(subSurface, parent) +{ +} + +QPainterWindowPixmap::~QPainterWindowPixmap() +{ +} + +void QPainterWindowPixmap::create() +{ + if (isValid()) { + return; + } + KWin::WindowPixmap::create(); + if (!isValid()) { + return; + } + if (!surface()) { + // That's an internal client. + m_image = internalImage(); + return; + } + // performing deep copy, this could probably be improved + m_image = buffer()->data().copy(); + if (auto s = surface()) { + s->resetTrackedDamage(); + } +} + +WindowPixmap *QPainterWindowPixmap::createChild(const QPointer &subSurface) +{ + return new QPainterWindowPixmap(subSurface, this); +} + +void QPainterWindowPixmap::update() +{ + const auto oldBuffer = buffer(); + WindowPixmap::update(); + const auto &b = buffer(); + if (!surface()) { + // That's an internal client. + m_image = internalImage(); + return; + } + if (b.isNull()) { + m_image = QImage(); + return; + } + if (b == oldBuffer) { + return; + } + // perform deep copy + m_image = b->data().copy(); + if (auto s = surface()) { + s->resetTrackedDamage(); + } +} + +bool QPainterWindowPixmap::isValid() const +{ + if (!m_image.isNull()) { + return true; + } + return WindowPixmap::isValid(); +} + +QPainterEffectFrame::QPainterEffectFrame(EffectFrameImpl *frame, SceneQPainter *scene) + : Scene::EffectFrame(frame) + , m_scene(scene) +{ +} + +QPainterEffectFrame::~QPainterEffectFrame() +{ +} + +void QPainterEffectFrame::render(const QRegion ®ion, double opacity, double frameOpacity) +{ + Q_UNUSED(region) + Q_UNUSED(opacity) + // TODO: adjust opacity + if (m_effectFrame->geometry().isEmpty()) { + return; // Nothing to display + } + QPainter *painter = m_scene->scenePainter(); + + + // Render the actual frame + if (m_effectFrame->style() == EffectFrameUnstyled) { + painter->save(); + painter->setPen(Qt::NoPen); + QColor color(Qt::black); + color.setAlphaF(frameOpacity); + painter->setBrush(color); + painter->setRenderHint(QPainter::Antialiasing); + painter->drawRoundedRect(m_effectFrame->geometry().adjusted(-5, -5, 5, 5), 5.0, 5.0); + painter->restore(); + } else if (m_effectFrame->style() == EffectFrameStyled) { + qreal left, top, right, bottom; + m_effectFrame->frame().getMargins(left, top, right, bottom); // m_geometry is the inner geometry + QRect geom = m_effectFrame->geometry().adjusted(-left, -top, right, bottom); + painter->drawPixmap(geom, m_effectFrame->frame().framePixmap()); + } + if (!m_effectFrame->selection().isNull()) { + painter->drawPixmap(m_effectFrame->selection(), m_effectFrame->selectionFrame().framePixmap()); + } + + // Render icon + if (!m_effectFrame->icon().isNull() && !m_effectFrame->iconSize().isEmpty()) { + const QPoint topLeft(m_effectFrame->geometry().x(), + m_effectFrame->geometry().center().y() - m_effectFrame->iconSize().height() / 2); + + const QRect geom = QRect(topLeft, m_effectFrame->iconSize()); + painter->drawPixmap(geom, m_effectFrame->icon().pixmap(m_effectFrame->iconSize())); + } + + // Render text + if (!m_effectFrame->text().isEmpty()) { + // Determine position on texture to paint text + QRect rect(QPoint(0, 0), m_effectFrame->geometry().size()); + if (!m_effectFrame->icon().isNull() && !m_effectFrame->iconSize().isEmpty()) { + rect.setLeft(m_effectFrame->iconSize().width()); + } + + // If static size elide text as required + QString text = m_effectFrame->text(); + if (m_effectFrame->isStatic()) { + QFontMetrics metrics(m_effectFrame->text()); + text = metrics.elidedText(text, Qt::ElideRight, rect.width()); + } + + painter->save(); + painter->setFont(m_effectFrame->font()); + if (m_effectFrame->style() == EffectFrameStyled) { + painter->setPen(m_effectFrame->styledTextColor()); + } else { + // TODO: What about no frame? Custom color setting required + painter->setPen(Qt::white); + } + painter->drawText(rect.translated(m_effectFrame->geometry().topLeft()), m_effectFrame->alignment(), text); + painter->restore(); + } +} + +//**************************************** +// QPainterShadow +//**************************************** +SceneQPainterShadow::SceneQPainterShadow(Toplevel* toplevel) + : Shadow(toplevel) +{ +} + +SceneQPainterShadow::~SceneQPainterShadow() +{ +} + +void SceneQPainterShadow::buildQuads() +{ + // Do not draw shadows if window width or window height is less than + // 5 px. 5 is an arbitrary choice. + if (topLevel()->width() < 5 || topLevel()->height() < 5) { + m_shadowQuads.clear(); + setShadowRegion(QRegion()); + return; + } + + const QSizeF top(elementSize(ShadowElementTop)); + const QSizeF topRight(elementSize(ShadowElementTopRight)); + const QSizeF right(elementSize(ShadowElementRight)); + const QSizeF bottomRight(elementSize(ShadowElementBottomRight)); + const QSizeF bottom(elementSize(ShadowElementBottom)); + const QSizeF bottomLeft(elementSize(ShadowElementBottomLeft)); + const QSizeF left(elementSize(ShadowElementLeft)); + const QSizeF topLeft(elementSize(ShadowElementTopLeft)); + + const QRectF outerRect(QPointF(-leftOffset(), -topOffset()), + QPointF(topLevel()->width() + rightOffset(), + topLevel()->height() + bottomOffset())); + + const int width = std::max({topLeft.width(), left.width(), bottomLeft.width()}) + + std::max(top.width(), bottom.width()) + + std::max({topRight.width(), right.width(), bottomRight.width()}); + const int height = std::max({topLeft.height(), top.height(), topRight.height()}) + + std::max(left.height(), right.height()) + + std::max({bottomLeft.height(), bottom.height(), bottomRight.height()}); + + QRectF topLeftRect(outerRect.topLeft(), topLeft); + QRectF topRightRect(outerRect.topRight() - QPointF(topRight.width(), 0), topRight); + QRectF bottomRightRect( + outerRect.bottomRight() - QPointF(bottomRight.width(), bottomRight.height()), + bottomRight); + QRectF bottomLeftRect(outerRect.bottomLeft() - QPointF(0, bottomLeft.height()), bottomLeft); + + // Re-distribute the corner tiles so no one of them is overlapping with others. + // By doing this, we assume that shadow's corner tiles are symmetric + // and it is OK to not draw top/right/bottom/left tile between corners. + // For example, let's say top-left and top-right tiles are overlapping. + // In that case, the right side of the top-left tile will be shifted to left, + // the left side of the top-right tile will shifted to right, and the top + // tile won't be rendered. + bool drawTop = true; + if (topLeftRect.right() >= topRightRect.left()) { + const float halfOverlap = qAbs(topLeftRect.right() - topRightRect.left()) / 2; + topLeftRect.setRight(topLeftRect.right() - std::floor(halfOverlap)); + topRightRect.setLeft(topRightRect.left() + std::ceil(halfOverlap)); + drawTop = false; + } + + bool drawRight = true; + if (topRightRect.bottom() >= bottomRightRect.top()) { + const float halfOverlap = qAbs(topRightRect.bottom() - bottomRightRect.top()) / 2; + topRightRect.setBottom(topRightRect.bottom() - std::floor(halfOverlap)); + bottomRightRect.setTop(bottomRightRect.top() + std::ceil(halfOverlap)); + drawRight = false; + } + + bool drawBottom = true; + if (bottomLeftRect.right() >= bottomRightRect.left()) { + const float halfOverlap = qAbs(bottomLeftRect.right() - bottomRightRect.left()) / 2; + bottomLeftRect.setRight(bottomLeftRect.right() - std::floor(halfOverlap)); + bottomRightRect.setLeft(bottomRightRect.left() + std::ceil(halfOverlap)); + drawBottom = false; + } + + bool drawLeft = true; + if (topLeftRect.bottom() >= bottomLeftRect.top()) { + const float halfOverlap = qAbs(topLeftRect.bottom() - bottomLeftRect.top()) / 2; + topLeftRect.setBottom(topLeftRect.bottom() - std::floor(halfOverlap)); + bottomLeftRect.setTop(bottomLeftRect.top() + std::ceil(halfOverlap)); + drawLeft = false; + } + + qreal tx1 = 0.0, + tx2 = 0.0, + ty1 = 0.0, + ty2 = 0.0; + + m_shadowQuads.clear(); + + tx1 = 0.0; + ty1 = 0.0; + tx2 = topLeftRect.width(); + ty2 = topLeftRect.height(); + WindowQuad topLeftQuad(WindowQuadShadow); + topLeftQuad[0] = WindowVertex(topLeftRect.left(), topLeftRect.top(), tx1, ty1); + topLeftQuad[1] = WindowVertex(topLeftRect.right(), topLeftRect.top(), tx2, ty1); + topLeftQuad[2] = WindowVertex(topLeftRect.right(), topLeftRect.bottom(), tx2, ty2); + topLeftQuad[3] = WindowVertex(topLeftRect.left(), topLeftRect.bottom(), tx1, ty2); + m_shadowQuads.append(topLeftQuad); + + tx1 = width - topRightRect.width(); + ty1 = 0.0; + tx2 = width; + ty2 = topRightRect.height(); + WindowQuad topRightQuad(WindowQuadShadow); + topRightQuad[0] = WindowVertex(topRightRect.left(), topRightRect.top(), tx1, ty1); + topRightQuad[1] = WindowVertex(topRightRect.right(), topRightRect.top(), tx2, ty1); + topRightQuad[2] = WindowVertex(topRightRect.right(), topRightRect.bottom(), tx2, ty2); + topRightQuad[3] = WindowVertex(topRightRect.left(), topRightRect.bottom(), tx1, ty2); + m_shadowQuads.append(topRightQuad); + + tx1 = width - bottomRightRect.width(); + tx2 = width; + ty1 = height - bottomRightRect.height(); + ty2 = height; + WindowQuad bottomRightQuad(WindowQuadShadow); + bottomRightQuad[0] = WindowVertex(bottomRightRect.left(), bottomRightRect.top(), tx1, ty1); + bottomRightQuad[1] = WindowVertex(bottomRightRect.right(), bottomRightRect.top(), tx2, ty1); + bottomRightQuad[2] = WindowVertex(bottomRightRect.right(), bottomRightRect.bottom(), tx2, ty2); + bottomRightQuad[3] = WindowVertex(bottomRightRect.left(), bottomRightRect.bottom(), tx1, ty2); + m_shadowQuads.append(bottomRightQuad); + + tx1 = 0.0; + tx2 = bottomLeftRect.width(); + ty1 = height - bottomLeftRect.height(); + ty2 = height; + WindowQuad bottomLeftQuad(WindowQuadShadow); + bottomLeftQuad[0] = WindowVertex(bottomLeftRect.left(), bottomLeftRect.top(), tx1, ty1); + bottomLeftQuad[1] = WindowVertex(bottomLeftRect.right(), bottomLeftRect.top(), tx2, ty1); + bottomLeftQuad[2] = WindowVertex(bottomLeftRect.right(), bottomLeftRect.bottom(), tx2, ty2); + bottomLeftQuad[3] = WindowVertex(bottomLeftRect.left(), bottomLeftRect.bottom(), tx1, ty2); + m_shadowQuads.append(bottomLeftQuad); + + if (drawTop) { + QRectF topRect( + topLeftRect.topRight(), + topRightRect.bottomLeft()); + tx1 = topLeft.width(); + ty1 = 0.0; + tx2 = width - topRight.width(); + ty2 = topRect.height(); + WindowQuad topQuad(WindowQuadShadow); + topQuad[0] = WindowVertex(topRect.left(), topRect.top(), tx1, ty1); + topQuad[1] = WindowVertex(topRect.right(), topRect.top(), tx2, ty1); + topQuad[2] = WindowVertex(topRect.right(), topRect.bottom(), tx2, ty2); + topQuad[3] = WindowVertex(topRect.left(), topRect.bottom(), tx1, ty2); + m_shadowQuads.append(topQuad); + } + + if (drawRight) { + QRectF rightRect( + topRightRect.bottomLeft(), + bottomRightRect.topRight()); + tx1 = width - rightRect.width(); + ty1 = topRight.height(); + tx2 = width; + ty2 = height - bottomRight.height(); + WindowQuad rightQuad(WindowQuadShadow); + rightQuad[0] = WindowVertex(rightRect.left(), rightRect.top(), tx1, ty1); + rightQuad[1] = WindowVertex(rightRect.right(), rightRect.top(), tx2, ty1); + rightQuad[2] = WindowVertex(rightRect.right(), rightRect.bottom(), tx2, ty2); + rightQuad[3] = WindowVertex(rightRect.left(), rightRect.bottom(), tx1, ty2); + m_shadowQuads.append(rightQuad); + } + + if (drawBottom) { + QRectF bottomRect( + bottomLeftRect.topRight(), + bottomRightRect.bottomLeft()); + tx1 = bottomLeft.width(); + ty1 = height - bottomRect.height(); + tx2 = width - bottomRight.width(); + ty2 = height; + WindowQuad bottomQuad(WindowQuadShadow); + bottomQuad[0] = WindowVertex(bottomRect.left(), bottomRect.top(), tx1, ty1); + bottomQuad[1] = WindowVertex(bottomRect.right(), bottomRect.top(), tx2, ty1); + bottomQuad[2] = WindowVertex(bottomRect.right(), bottomRect.bottom(), tx2, ty2); + bottomQuad[3] = WindowVertex(bottomRect.left(), bottomRect.bottom(), tx1, ty2); + m_shadowQuads.append(bottomQuad); + } + + if (drawLeft) { + QRectF leftRect( + topLeftRect.bottomLeft(), + bottomLeftRect.topRight()); + tx1 = 0.0; + ty1 = topLeft.height(); + tx2 = leftRect.width(); + ty2 = height - bottomRight.height(); + WindowQuad leftQuad(WindowQuadShadow); + leftQuad[0] = WindowVertex(leftRect.left(), leftRect.top(), tx1, ty1); + leftQuad[1] = WindowVertex(leftRect.right(), leftRect.top(), tx2, ty1); + leftQuad[2] = WindowVertex(leftRect.right(), leftRect.bottom(), tx2, ty2); + leftQuad[3] = WindowVertex(leftRect.left(), leftRect.bottom(), tx1, ty2); + m_shadowQuads.append(leftQuad); + } +} + +bool SceneQPainterShadow::prepareBackend() +{ + if (hasDecorationShadow()) { + m_texture = decorationShadowImage(); + return true; + } + + const QPixmap &topLeft = shadowPixmap(ShadowElementTopLeft); + const QPixmap &top = shadowPixmap(ShadowElementTop); + const QPixmap &topRight = shadowPixmap(ShadowElementTopRight); + const QPixmap &bottomLeft = shadowPixmap(ShadowElementBottomLeft); + const QPixmap &bottom = shadowPixmap(ShadowElementBottom); + const QPixmap &bottomRight = shadowPixmap(ShadowElementBottomRight); + const QPixmap &left = shadowPixmap(ShadowElementLeft); + const QPixmap &right = shadowPixmap(ShadowElementRight); + + const int width = std::max({topLeft.width(), left.width(), bottomLeft.width()}) + + std::max(top.width(), bottom.width()) + + std::max({topRight.width(), right.width(), bottomRight.width()}); + const int height = std::max({topLeft.height(), top.height(), topRight.height()}) + + std::max(left.height(), right.height()) + + std::max({bottomLeft.height(), bottom.height(), bottomRight.height()}); + + if (width == 0 || height == 0) { + return false; + } + + QImage image(width, height, QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::transparent); + + QPainter painter; + painter.begin(&image); + painter.drawPixmap(0, 0, topLeft.width(), topLeft.height(), topLeft); + painter.drawPixmap(topLeft.width(), 0, top.width(), top.height(), top); + painter.drawPixmap(width - topRight.width(), 0, topRight.width(), topRight.height(), topRight); + painter.drawPixmap(0, height - bottomLeft.height(), bottomLeft.width(), bottomLeft.height(), bottomLeft); + painter.drawPixmap(bottomLeft.width(), height - bottom.height(), bottom.width(), bottom.height(), bottom); + painter.drawPixmap(width - bottomRight.width(), height - bottomRight.height(), bottomRight.width(), bottomRight.height(), bottomRight); + painter.drawPixmap(0, topLeft.height(), left.width(), left.height(), left); + painter.drawPixmap(width - right.width(), topRight.height(), right.width(), right.height(), right); + painter.end(); + + m_texture = image; + + return true; +} + +//**************************************** +// QPainterDecorationRenderer +//**************************************** +SceneQPainterDecorationRenderer::SceneQPainterDecorationRenderer(Decoration::DecoratedClientImpl *client) + : Renderer(client) +{ + connect(this, &Renderer::renderScheduled, client->client(), static_cast(&AbstractClient::addRepaint)); +} + +SceneQPainterDecorationRenderer::~SceneQPainterDecorationRenderer() = default; + +QImage SceneQPainterDecorationRenderer::image(SceneQPainterDecorationRenderer::DecorationPart part) const +{ + Q_ASSERT(part != DecorationPart::Count); + return m_images[int(part)]; +} + +void SceneQPainterDecorationRenderer::render() +{ + const QRegion scheduled = getScheduled(); + if (scheduled.isEmpty()) { + return; + } + if (areImageSizesDirty()) { + resizeImages(); + resetImageSizesDirty(); + } + + auto imageSize = [this](DecorationPart part) { + return m_images[int(part)].size() / m_images[int(part)].devicePixelRatio(); + }; + + const QRect top(QPoint(0, 0), imageSize(DecorationPart::Top)); + const QRect left(QPoint(0, top.height()), imageSize(DecorationPart::Left)); + const QRect right(QPoint(top.width() - imageSize(DecorationPart::Right).width(), top.height()), imageSize(DecorationPart::Right)); + const QRect bottom(QPoint(0, left.y() + left.height()), imageSize(DecorationPart::Bottom)); + + const QRect geometry = scheduled.boundingRect(); + auto renderPart = [this](const QRect &rect, const QRect &partRect, int index) { + if (rect.isEmpty()) { + return; + } + QPainter painter(&m_images[index]); + painter.setRenderHint(QPainter::Antialiasing); + painter.setWindow(QRect(partRect.topLeft(), partRect.size() * m_images[index].devicePixelRatio())); + painter.setClipRect(rect); + painter.save(); + // clear existing part + painter.setCompositionMode(QPainter::CompositionMode_Source); + painter.fillRect(rect, Qt::transparent); + painter.restore(); + client()->decoration()->paint(&painter, rect); + }; + + renderPart(left.intersected(geometry), left, int(DecorationPart::Left)); + renderPart(top.intersected(geometry), top, int(DecorationPart::Top)); + renderPart(right.intersected(geometry), right, int(DecorationPart::Right)); + renderPart(bottom.intersected(geometry), bottom, int(DecorationPart::Bottom)); +} + +void SceneQPainterDecorationRenderer::resizeImages() +{ + QRect left, top, right, bottom; + client()->client()->layoutDecorationRects(left, top, right, bottom); + + auto checkAndCreate = [this](int index, const QSize &size) { + auto dpr = client()->client()->screenScale(); + if (m_images[index].size() != size * dpr || + m_images[index].devicePixelRatio() != dpr) + { + m_images[index] = QImage(size * dpr, QImage::Format_ARGB32_Premultiplied); + m_images[index].setDevicePixelRatio(dpr); + m_images[index].fill(Qt::transparent); + } + }; + checkAndCreate(int(DecorationPart::Left), left.size()); + checkAndCreate(int(DecorationPart::Right), right.size()); + checkAndCreate(int(DecorationPart::Top), top.size()); + checkAndCreate(int(DecorationPart::Bottom), bottom.size()); +} + +void SceneQPainterDecorationRenderer::reparent(Deleted *deleted) +{ + render(); + Renderer::reparent(deleted); +} + + +QPainterFactory::QPainterFactory(QObject *parent) + : SceneFactory(parent) +{ +} + +QPainterFactory::~QPainterFactory() = default; + +Scene *QPainterFactory::create(QObject *parent) const +{ + auto s = SceneQPainter::createScene(parent); + if (s && s->initFailed()) { + delete s; + s = nullptr; + } + return s; +} + +} // KWin diff --git a/plugins/scenes/qpainter/scene_qpainter.h b/plugins/scenes/qpainter/scene_qpainter.h new file mode 100644 index 0000000..1e03571 --- /dev/null +++ b/plugins/scenes/qpainter/scene_qpainter.h @@ -0,0 +1,195 @@ +/* + 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 KWIN_SCENE_QPAINTER_H +#define KWIN_SCENE_QPAINTER_H + +#include "scene.h" +#include +#include "shadow.h" + +#include "decorations/decorationrenderer.h" + +namespace KWin { + +class KWIN_EXPORT SceneQPainter : public Scene +{ + Q_OBJECT + +public: + ~SceneQPainter() override; + bool usesOverlayWindow() const override; + OverlayWindow* overlayWindow() const override; + qint64 paint(const QRegion &damage, const QList &windows) override; + void paintGenericScreen(int mask, const ScreenPaintData &data) override; + CompositingType compositingType() const override; + bool initFailed() const override; + EffectFrame *createEffectFrame(EffectFrameImpl *frame) override; + Shadow *createShadow(Toplevel *toplevel) override; + Decoration::Renderer *createDecorationRenderer(Decoration::DecoratedClientImpl *impl) override; + void screenGeometryChanged(const QSize &size) override; + + bool animationsSupported() const override { + return false; + } + + QPainter *scenePainter() const override; + QImage *qpainterRenderBuffer() const override; + + QPainterBackend *backend() const { + return m_backend.data(); + } + + static SceneQPainter *createScene(QObject *parent); + +protected: + void paintBackground(const QRegion ®ion) override; + Scene::Window *createWindow(Toplevel *toplevel) override; + void paintCursor(const QRegion ®ion) override; + void paintEffectQuickView(EffectQuickView *w) override; + +private: + explicit SceneQPainter(QPainterBackend *backend, QObject *parent = nullptr); + QScopedPointer m_backend; + QScopedPointer m_painter; + class Window; +}; + +class QPainterWindowPixmap : public WindowPixmap +{ +public: + explicit QPainterWindowPixmap(Scene::Window *window); + ~QPainterWindowPixmap() override; + void create() override; + void update() override; + bool isValid() const override; + + const QImage &image(); + +protected: + WindowPixmap *createChild(const QPointer &subSurface) override; +private: + explicit QPainterWindowPixmap(const QPointer &subSurface, WindowPixmap *parent); + QImage m_image; +}; + +class SceneQPainter::Window : public Scene::Window +{ + Q_OBJECT + +public: + Window(SceneQPainter *scene, Toplevel *c); + ~Window() override; + void performPaint(int mask, const QRegion ®ion, const WindowPaintData &data) override; +protected: + WindowPixmap *createWindowPixmap() override; +private: + void renderWindowPixmap(QPainter *painter, QPainterWindowPixmap *windowPixmap); + void renderShadow(QPainter *painter); + void renderWindowDecorations(QPainter *painter); + SceneQPainter *m_scene; +}; + +class QPainterEffectFrame : public Scene::EffectFrame +{ +public: + QPainterEffectFrame(EffectFrameImpl *frame, SceneQPainter *scene); + ~QPainterEffectFrame() override; + void crossFadeIcon() override {} + void crossFadeText() override {} + void free() override {} + void freeIconFrame() override {} + void freeTextFrame() override {} + void freeSelection() override {} + void render(const QRegion ®ion, double opacity, double frameOpacity) override; +private: + SceneQPainter *m_scene; +}; + +class SceneQPainterShadow : public Shadow +{ +public: + SceneQPainterShadow(Toplevel* toplevel); + ~SceneQPainterShadow() override; + + QImage &shadowTexture() { + return m_texture; + } + +protected: + void buildQuads() override; + bool prepareBackend() override; + +private: + QImage m_texture; +}; + +class SceneQPainterDecorationRenderer : public Decoration::Renderer +{ + Q_OBJECT +public: + enum class DecorationPart : int { + Left, + Top, + Right, + Bottom, + Count + }; + explicit SceneQPainterDecorationRenderer(Decoration::DecoratedClientImpl *client); + ~SceneQPainterDecorationRenderer() override; + + void render() override; + void reparent(Deleted *deleted) override; + + QImage image(DecorationPart part) const; + +private: + void resizeImages(); + QImage m_images[int(DecorationPart::Count)]; +}; + +class KWIN_EXPORT QPainterFactory : public SceneFactory +{ + Q_OBJECT + Q_INTERFACES(KWin::SceneFactory) + Q_PLUGIN_METADATA(IID "org.kde.kwin.Scene" FILE "qpainter.json") + +public: + explicit QPainterFactory(QObject *parent = nullptr); + ~QPainterFactory() override; + + Scene *create(QObject *parent = nullptr) const override; +}; + +inline +bool SceneQPainter::usesOverlayWindow() const +{ + return m_backend->usesOverlayWindow(); +} + +inline +OverlayWindow* SceneQPainter::overlayWindow() const +{ + return m_backend->overlayWindow(); +} + +inline +QPainter* SceneQPainter::scenePainter() const +{ + return m_painter.data(); +} + +inline +const QImage &QPainterWindowPixmap::image() +{ + return m_image; +} + +} // KWin + +#endif // KWIN_SCENEQPAINTER_H diff --git a/plugins/scenes/xrender/CMakeLists.txt b/plugins/scenes/xrender/CMakeLists.txt new file mode 100644 index 0000000..2f16fc4 --- /dev/null +++ b/plugins/scenes/xrender/CMakeLists.txt @@ -0,0 +1,27 @@ +set(SCENE_XRENDER_SRCS scene_xrender.cpp) + +include(ECMQtDeclareLoggingCategory) +ecm_qt_declare_logging_category( + SCENE_XRENDER_SRCS HEADER + logging.h + IDENTIFIER + KWIN_XRENDER + CATEGORY_NAME + kwin_scene_xrender + DEFAULT_SEVERITY + Critical +) + +add_library(KWinSceneXRender MODULE ${SCENE_XRENDER_SRCS}) +set_target_properties(KWinSceneXRender PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/org.kde.kwin.scenes/") +target_link_libraries(KWinSceneXRender + kwin + kwinxrenderutils +) + +install( + TARGETS + KWinSceneXRender + DESTINATION + ${PLUGIN_INSTALL_DIR}/org.kde.kwin.scenes/ +) diff --git a/plugins/scenes/xrender/scene_xrender.cpp b/plugins/scenes/xrender/scene_xrender.cpp new file mode 100644 index 0000000..1243caa --- /dev/null +++ b/plugins/scenes/xrender/scene_xrender.cpp @@ -0,0 +1,1345 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2009 Fredrik Höglund + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "scene_xrender.h" + +#include "utils.h" + +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + +#include "logging.h" +#include "toplevel.h" +#include "x11client.h" +#include "composite.h" +#include "deleted.h" +#include "effects.h" +#include "main.h" +#include "overlaywindow.h" +#include "platform.h" +#include "screens.h" +#include "xcbutils.h" +#include "decorations/decoratedclient.h" + +#include +#include + +#include + +#include +#include +#include + +namespace KWin +{ + +ScreenPaintData SceneXrender::screen_paint; + +#define DOUBLE_TO_FIXED(d) ((xcb_render_fixed_t) ((d) * 65536)) +#define FIXED_TO_DOUBLE(f) ((double) ((f) / 65536.0)) + + +//**************************************** +// XRenderBackend +//**************************************** +XRenderBackend::XRenderBackend() + : m_buffer(XCB_RENDER_PICTURE_NONE) + , m_failed(false) +{ + if (!Xcb::Extensions::self()->isRenderAvailable()) { + setFailed("No XRender extension available"); + return; + } + if (!Xcb::Extensions::self()->isFixesRegionAvailable()) { + setFailed("No XFixes v3+ extension available"); + return; + } +} + +XRenderBackend::~XRenderBackend() +{ + if (m_buffer) { + xcb_render_free_picture(connection(), m_buffer); + } +} + +OverlayWindow* XRenderBackend::overlayWindow() +{ + return nullptr; +} + +void XRenderBackend::showOverlay() +{ +} + +void XRenderBackend::setBuffer(xcb_render_picture_t buffer) +{ + if (m_buffer != XCB_RENDER_PICTURE_NONE) { + xcb_render_free_picture(connection(), m_buffer); + } + m_buffer = buffer; +} + +void XRenderBackend::setFailed(const QString& reason) +{ + qCCritical(KWIN_XRENDER) << "Creating the XRender backend failed: " << reason; + m_failed = true; +} + +void XRenderBackend::screenGeometryChanged(const QSize &size) +{ + Q_UNUSED(size) +} + + +//**************************************** +// X11XRenderBackend +//**************************************** +X11XRenderBackend::X11XRenderBackend() + : XRenderBackend() + , m_overlayWindow(kwinApp()->platform()->createOverlayWindow()) + , m_front(XCB_RENDER_PICTURE_NONE) + , m_format(0) +{ + init(true); +} + +X11XRenderBackend::~X11XRenderBackend() +{ + if (m_front) { + xcb_render_free_picture(connection(), m_front); + } + m_overlayWindow->destroy(); +} + +OverlayWindow* X11XRenderBackend::overlayWindow() +{ + return m_overlayWindow.data(); +} + +void X11XRenderBackend::showOverlay() +{ + if (m_overlayWindow->window()) // show the window only after the first pass, since + m_overlayWindow->show(); // that pass may take long +} + +void X11XRenderBackend::init(bool createOverlay) +{ + if (m_front != XCB_RENDER_PICTURE_NONE) + xcb_render_free_picture(connection(), m_front); + bool haveOverlay = createOverlay ? m_overlayWindow->create() : (m_overlayWindow->window() != XCB_WINDOW_NONE); + if (haveOverlay) { + m_overlayWindow->setup(XCB_WINDOW_NONE); + ScopedCPointer attribs(xcb_get_window_attributes_reply(connection(), + xcb_get_window_attributes_unchecked(connection(), m_overlayWindow->window()), nullptr)); + if (!attribs) { + setFailed("Failed getting window attributes for overlay window"); + return; + } + m_format = XRenderUtils::findPictFormat(attribs->visual); + if (m_format == 0) { + setFailed("Failed to find XRender format for overlay window"); + return; + } + m_front = xcb_generate_id(connection()); + xcb_render_create_picture(connection(), m_front, m_overlayWindow->window(), m_format, 0, nullptr); + } else { + // create XRender picture for the root window + m_format = XRenderUtils::findPictFormat(kwinApp()->x11DefaultScreen()->root_visual); + if (m_format == 0) { + setFailed("Failed to find XRender format for root window"); + return; // error + } + m_front = xcb_generate_id(connection()); + const uint32_t values[] = {XCB_SUBWINDOW_MODE_INCLUDE_INFERIORS}; + xcb_render_create_picture(connection(), m_front, rootWindow(), m_format, XCB_RENDER_CP_SUBWINDOW_MODE, values); + } + createBuffer(); +} + +void X11XRenderBackend::createBuffer() +{ + xcb_pixmap_t pixmap = xcb_generate_id(connection()); + const auto displaySize = screens()->displaySize(); + xcb_create_pixmap(connection(), Xcb::defaultDepth(), pixmap, rootWindow(), displaySize.width(), displaySize.height()); + xcb_render_picture_t b = xcb_generate_id(connection()); + xcb_render_create_picture(connection(), b, pixmap, m_format, 0, nullptr); + xcb_free_pixmap(connection(), pixmap); // The picture owns the pixmap now + setBuffer(b); +} + +void X11XRenderBackend::present(int mask, const QRegion &damage) +{ + const auto displaySize = screens()->displaySize(); + if (mask & Scene::PAINT_SCREEN_REGION) { + // Use the damage region as the clip region for the root window + XFixesRegion frontRegion(damage); + xcb_xfixes_set_picture_clip_region(connection(), m_front, frontRegion, 0, 0); + // copy composed buffer to the root window + xcb_xfixes_set_picture_clip_region(connection(), buffer(), XCB_XFIXES_REGION_NONE, 0, 0); + xcb_render_composite(connection(), XCB_RENDER_PICT_OP_SRC, buffer(), XCB_RENDER_PICTURE_NONE, + m_front, 0, 0, 0, 0, 0, 0, displaySize.width(), displaySize.height()); + xcb_xfixes_set_picture_clip_region(connection(), m_front, XCB_XFIXES_REGION_NONE, 0, 0); + xcb_flush(connection()); + } else { + // copy composed buffer to the root window + xcb_render_composite(connection(), XCB_RENDER_PICT_OP_SRC, buffer(), XCB_RENDER_PICTURE_NONE, + m_front, 0, 0, 0, 0, 0, 0, displaySize.width(), displaySize.height()); + xcb_flush(connection()); + } +} + +void X11XRenderBackend::screenGeometryChanged(const QSize &size) +{ + Q_UNUSED(size) + init(false); +} + +bool X11XRenderBackend::usesOverlayWindow() const +{ + return true; +} + +//**************************************** +// SceneXrender +//**************************************** + +SceneXrender* SceneXrender::createScene(QObject *parent) +{ + QScopedPointer backend; + backend.reset(new X11XRenderBackend); + if (backend->isFailed()) { + return nullptr; + } + return new SceneXrender(backend.take(), parent); +} + +SceneXrender::SceneXrender(XRenderBackend *backend, QObject *parent) + : Scene(parent) + , m_backend(backend) +{ +} + +SceneXrender::~SceneXrender() +{ + SceneXrender::Window::cleanup(); + SceneXrender::EffectFrame::cleanup(); +} + +bool SceneXrender::initFailed() const +{ + return false; +} + +// the entry point for painting +qint64 SceneXrender::paint(const QRegion &damage, const QList &toplevels) +{ + QElapsedTimer renderTimer; + renderTimer.start(); + + createStackingOrder(toplevels); + + int mask = 0; + QRegion updateRegion, validRegion; + paintScreen(&mask, damage, QRegion(), &updateRegion, &validRegion); + + m_backend->showOverlay(); + + m_backend->present(mask, updateRegion); + // do cleanup + clearStackingOrder(); + + return renderTimer.nsecsElapsed(); +} + +void SceneXrender::paintGenericScreen(int mask, const ScreenPaintData &data) +{ + screen_paint = data; // save, transformations will be done when painting windows + Scene::paintGenericScreen(mask, data); +} + +void SceneXrender::paintDesktop(int desktop, int mask, const QRegion ®ion, ScreenPaintData &data) +{ + PaintClipper::push(region); + KWin::Scene::paintDesktop(desktop, mask, region, data); + PaintClipper::pop(region); +} + +// fill the screen background +void SceneXrender::paintBackground(const QRegion ®ion) +{ + xcb_render_color_t col = { 0, 0, 0, 0xffff }; // black + const QVector &rects = Xcb::regionToRects(region); + xcb_render_fill_rectangles(connection(), XCB_RENDER_PICT_OP_SRC, xrenderBufferPicture(), col, rects.count(), rects.data()); +} + +Scene::Window *SceneXrender::createWindow(Toplevel *toplevel) +{ + return new Window(toplevel, this); +} + +Scene::EffectFrame *SceneXrender::createEffectFrame(EffectFrameImpl *frame) +{ + return new SceneXrender::EffectFrame(frame); +} + +Shadow *SceneXrender::createShadow(Toplevel *toplevel) +{ + return new SceneXRenderShadow(toplevel); +} + +Decoration::Renderer *SceneXrender::createDecorationRenderer(Decoration::DecoratedClientImpl* client) +{ + return new SceneXRenderDecorationRenderer(client); +} + +//**************************************** +// SceneXrender::Window +//**************************************** + +XRenderPicture *SceneXrender::Window::s_tempPicture = nullptr; +QRect SceneXrender::Window::temp_visibleRect; +XRenderPicture *SceneXrender::Window::s_fadeAlphaPicture = nullptr; + +SceneXrender::Window::Window(Toplevel* c, SceneXrender *scene) + : Scene::Window(c) + , m_scene(scene) + , format(XRenderUtils::findPictFormat(c->visual())) +{ +} + +SceneXrender::Window::~Window() +{ +} + +void SceneXrender::Window::cleanup() +{ + delete s_tempPicture; + s_tempPicture = nullptr; + delete s_fadeAlphaPicture; + s_fadeAlphaPicture = nullptr; +} + +// Maps window coordinates to screen coordinates +QRect SceneXrender::Window::mapToScreen(int mask, const WindowPaintData &data, const QRect &rect) const +{ + QRect r = rect; + + if (mask & PAINT_WINDOW_TRANSFORMED) { + // Apply the window transformation + r.moveTo(r.x() * data.xScale() + data.xTranslation(), + r.y() * data.yScale() + data.yTranslation()); + r.setWidth(r.width() * data.xScale()); + r.setHeight(r.height() * data.yScale()); + } + + // Move the rectangle to the screen position + r.translate(x(), y()); + + if (mask & PAINT_SCREEN_TRANSFORMED) { + // Apply the screen transformation + r.moveTo(r.x() * screen_paint.xScale() + screen_paint.xTranslation(), + r.y() * screen_paint.yScale() + screen_paint.yTranslation()); + r.setWidth(r.width() * screen_paint.xScale()); + r.setHeight(r.height() * screen_paint.yScale()); + } + + return r; +} + +// Maps window coordinates to screen coordinates +QPoint SceneXrender::Window::mapToScreen(int mask, const WindowPaintData &data, const QPoint &point) const +{ + QPoint pt = point; + + if (mask & PAINT_WINDOW_TRANSFORMED) { + // Apply the window transformation + pt.rx() = pt.x() * data.xScale() + data.xTranslation(); + pt.ry() = pt.y() * data.yScale() + data.yTranslation(); + } + + // Move the point to the screen position + pt += QPoint(x(), y()); + + if (mask & PAINT_SCREEN_TRANSFORMED) { + // Apply the screen transformation + pt.rx() = pt.x() * screen_paint.xScale() + screen_paint.xTranslation(); + pt.ry() = pt.y() * screen_paint.yScale() + screen_paint.yTranslation(); + } + + return pt; +} + +QRect SceneXrender::Window::bufferToWindowRect(const QRect &rect) const +{ + return rect.translated(bufferOffset()); +} + +QRegion SceneXrender::Window::bufferToWindowRegion(const QRegion ®ion) const +{ + return region.translated(bufferOffset()); +} + +void SceneXrender::Window::prepareTempPixmap() +{ + const QSize oldSize = temp_visibleRect.size(); + temp_visibleRect = toplevel->visibleRect().translated(-toplevel->pos()); + if (s_tempPicture && (oldSize.width() < temp_visibleRect.width() || oldSize.height() < temp_visibleRect.height())) { + delete s_tempPicture; + s_tempPicture = nullptr; + scene_setXRenderOffscreenTarget(0); // invalidate, better crash than cause weird results for developers + } + if (!s_tempPicture) { + xcb_pixmap_t pix = xcb_generate_id(connection()); + xcb_create_pixmap(connection(), 32, pix, rootWindow(), temp_visibleRect.width(), temp_visibleRect.height()); + s_tempPicture = new XRenderPicture(pix, 32); + xcb_free_pixmap(connection(), pix); + } + const xcb_render_color_t transparent = {0, 0, 0, 0}; + const xcb_rectangle_t rect = {0, 0, uint16_t(temp_visibleRect.width()), uint16_t(temp_visibleRect.height())}; + xcb_render_fill_rectangles(connection(), XCB_RENDER_PICT_OP_SRC, *s_tempPicture, transparent, 1, &rect); +} + +// paint the window +void SceneXrender::Window::performPaint(int mask, const QRegion &_region, const WindowPaintData &data) +{ + QRegion region = _region; + setTransformedShape(QRegion()); // maybe nothing will be painted + // check if there is something to paint + bool opaque = isOpaque() && qFuzzyCompare(data.opacity(), 1.0); + /* HACK: It seems this causes painting glitches, disable temporarily + if (( mask & PAINT_WINDOW_OPAQUE ) ^ ( mask & PAINT_WINDOW_TRANSLUCENT )) + { // We are only painting either opaque OR translucent windows, not both + if ( mask & PAINT_WINDOW_OPAQUE && !opaque ) + return; // Only painting opaque and window is translucent + if ( mask & PAINT_WINDOW_TRANSLUCENT && opaque ) + return; // Only painting translucent and window is opaque + }*/ + // Intersect the clip region with the rectangle the window occupies on the screen + if (!(mask & (PAINT_WINDOW_TRANSFORMED | PAINT_SCREEN_TRANSFORMED))) + region &= toplevel->visibleRect(); + + if (region.isEmpty()) + return; + XRenderWindowPixmap *pixmap = windowPixmap(); + if (!pixmap || !pixmap->isValid()) { + return; + } + xcb_render_picture_t pic = pixmap->picture(); + if (pic == XCB_RENDER_PICTURE_NONE) // The render format can be null for GL and/or Xv visuals + return; + toplevel->resetDamage(); + // set picture filter + if (options->isXrenderSmoothScale()) { // only when forced, it's slow + if (mask & PAINT_WINDOW_TRANSFORMED) + filter = ImageFilterGood; + else if (mask & PAINT_SCREEN_TRANSFORMED) + filter = ImageFilterGood; + else + filter = ImageFilterFast; + } else + filter = ImageFilterFast; + // do required transformations + const QRect wr = mapToScreen(mask, data, QRect(0, 0, width(), height())); + QRect cr = QRect(toplevel->clientPos(), toplevel->clientSize()); // Content rect (in the buffer) + qreal xscale = 1; + qreal yscale = 1; + bool scaled = false; + + X11Client *client = dynamic_cast(toplevel); + Deleted *deleted = dynamic_cast(toplevel); + const QRect decorationRect = toplevel->rect(); + if (((client && !client->noBorder()) || (deleted && !deleted->noBorder())) && + true) { + // decorated client + transformed_shape = decorationRect; + if (toplevel->shape()) { + // "xeyes" + decoration + transformed_shape -= bufferToWindowRect(cr); + transformed_shape += bufferToWindowRegion(bufferShape()); + } + } else { + transformed_shape = bufferToWindowRegion(bufferShape()); + } + if (toplevel->shadow()) { + transformed_shape |= toplevel->shadow()->shadowRegion(); + } + + xcb_render_transform_t xform = { + DOUBLE_TO_FIXED(1), DOUBLE_TO_FIXED(0), DOUBLE_TO_FIXED(0), + DOUBLE_TO_FIXED(0), DOUBLE_TO_FIXED(1), DOUBLE_TO_FIXED(0), + DOUBLE_TO_FIXED(0), DOUBLE_TO_FIXED(0), DOUBLE_TO_FIXED(1) + }; + static const xcb_render_transform_t identity = { + DOUBLE_TO_FIXED(1), DOUBLE_TO_FIXED(0), DOUBLE_TO_FIXED(0), + DOUBLE_TO_FIXED(0), DOUBLE_TO_FIXED(1), DOUBLE_TO_FIXED(0), + DOUBLE_TO_FIXED(0), DOUBLE_TO_FIXED(0), DOUBLE_TO_FIXED(1) + }; + + if (mask & PAINT_WINDOW_TRANSFORMED) { + xscale = data.xScale(); + yscale = data.yScale(); + } + if (mask & PAINT_SCREEN_TRANSFORMED) { + xscale *= screen_paint.xScale(); + yscale *= screen_paint.yScale(); + } + if (!qFuzzyCompare(xscale, 1.0) || !qFuzzyCompare(yscale, 1.0)) { + scaled = true; + xform.matrix11 = DOUBLE_TO_FIXED(1.0 / xscale); + xform.matrix22 = DOUBLE_TO_FIXED(1.0 / yscale); + + // transform the shape for clipping in paintTransformedScreen() + QVector rects; + rects.reserve(transformed_shape.rectCount()); + for (const QRect &rect : transformed_shape) { + const QRect transformedRect( + qRound(rect.x() * xscale), + qRound(rect.y() * yscale), + qRound(rect.width() * xscale), + qRound(rect.height() * yscale) + ); + rects.append(transformedRect); + } + transformed_shape.setRects(rects.constData(), rects.count()); + } + + transformed_shape.translate(mapToScreen(mask, data, QPoint(0, 0))); + PaintClipper pcreg(region); // clip by the region to paint + PaintClipper pc(transformed_shape); // clip by window's shape + + const bool wantShadow = m_shadow && !m_shadow->shadowRegion().isEmpty(); + + // In order to obtain a pixel perfect rescaling + // we need to blit the window content togheter with + // decorations in a temporary pixmap and scale + // the temporary pixmap at the end. + // We should do this only if there is scaling and + // the window has border + // This solves a number of glitches and on top of this + // it optimizes painting quite a bit + const bool blitInTempPixmap = xRenderOffscreen() || (data.crossFadeProgress() < 1.0 && !opaque) || + (scaled && (wantShadow || (client && !client->noBorder()) || (deleted && !deleted->noBorder()))); + + xcb_render_picture_t renderTarget = m_scene->xrenderBufferPicture(); + if (blitInTempPixmap) { + if (scene_xRenderOffscreenTarget()) { + temp_visibleRect = toplevel->visibleRect().translated(-toplevel->pos()); + renderTarget = *scene_xRenderOffscreenTarget(); + } else { + prepareTempPixmap(); + renderTarget = *s_tempPicture; + } + } else { + xcb_render_set_picture_transform(connection(), pic, xform); + if (filter == ImageFilterGood) { + setPictureFilter(pic, KWin::Scene::ImageFilterGood); + } + + //BEGIN OF STUPID RADEON HACK + // This is needed to avoid hitting a fallback in the radeon driver. + // The Render specification states that sampling pixels outside the + // source picture results in alpha=0 pixels. This can be achieved by + // setting the border color to transparent black, but since the border + // color has the same format as the texture, it only works when the + // texture has an alpha channel. So the driver falls back to software + // when the repeat mode is RepeatNone, the picture has a non-identity + // transformation matrix, and doesn't have an alpha channel. + // Since we only scale the picture, we can work around this by setting + // the repeat mode to RepeatPad. + if (!window()->hasAlpha()) { + const uint32_t values[] = {XCB_RENDER_REPEAT_PAD}; + xcb_render_change_picture(connection(), pic, XCB_RENDER_CP_REPEAT, values); + } + //END OF STUPID RADEON HACK + } +#define MAP_RECT_TO_TARGET(_RECT_) \ + if (blitInTempPixmap) _RECT_.translate(-temp_visibleRect.topLeft()); else _RECT_ = mapToScreen(mask, data, _RECT_) + + //BEGIN deco preparations + bool noBorder = true; + xcb_render_picture_t left = XCB_RENDER_PICTURE_NONE; + xcb_render_picture_t top = XCB_RENDER_PICTURE_NONE; + xcb_render_picture_t right = XCB_RENDER_PICTURE_NONE; + xcb_render_picture_t bottom = XCB_RENDER_PICTURE_NONE; + QRect dtr, dlr, drr, dbr; + const SceneXRenderDecorationRenderer *renderer = nullptr; + if (client) { + if (client && !client->noBorder()) { + if (client->isDecorated()) { + SceneXRenderDecorationRenderer *r = static_cast(client->decoratedClient()->renderer()); + if (r) { + r->render(); + renderer = r; + } + } + noBorder = client->noBorder(); + client->layoutDecorationRects(dlr, dtr, drr, dbr); + } + } + if (deleted && !deleted->noBorder()) { + renderer = static_cast(deleted->decorationRenderer()); + noBorder = deleted->noBorder(); + deleted->layoutDecorationRects(dlr, dtr, drr, dbr); + } + if (renderer) { + left = renderer->picture(SceneXRenderDecorationRenderer::DecorationPart::Left); + top = renderer->picture(SceneXRenderDecorationRenderer::DecorationPart::Top); + right = renderer->picture(SceneXRenderDecorationRenderer::DecorationPart::Right); + bottom = renderer->picture(SceneXRenderDecorationRenderer::DecorationPart::Bottom); + } + if (!noBorder) { + MAP_RECT_TO_TARGET(dtr); + MAP_RECT_TO_TARGET(dlr); + MAP_RECT_TO_TARGET(drr); + MAP_RECT_TO_TARGET(dbr); + } + //END deco preparations + + //BEGIN shadow preparations + QRect stlr, str, strr, srr, sbrr, sbr, sblr, slr; + SceneXRenderShadow* m_xrenderShadow = static_cast(m_shadow); + + if (wantShadow) { + m_xrenderShadow->layoutShadowRects(str, strr, srr, sbrr, sbr, sblr, slr, stlr); + MAP_RECT_TO_TARGET(stlr); + MAP_RECT_TO_TARGET(str); + MAP_RECT_TO_TARGET(strr); + MAP_RECT_TO_TARGET(srr); + MAP_RECT_TO_TARGET(sbrr); + MAP_RECT_TO_TARGET(sbr); + MAP_RECT_TO_TARGET(sblr); + MAP_RECT_TO_TARGET(slr); + } + //BEGIN end preparations + + //BEGIN client preparations + QRect dr = cr; + if (blitInTempPixmap) { + dr.translate(-temp_visibleRect.topLeft()); + } else { + dr = mapToScreen(mask, data, bufferToWindowRect(dr)); // Destination rect + if (scaled) { + cr.moveLeft(cr.x() * xscale); + cr.moveTop(cr.y() * yscale); + } + } + + const int clientRenderOp = (opaque || blitInTempPixmap) ? XCB_RENDER_PICT_OP_SRC : XCB_RENDER_PICT_OP_OVER; + //END client preparations + +#undef MAP_RECT_TO_TARGET + + for (PaintClipper::Iterator iterator; !iterator.isDone(); iterator.next()) { + +#define RENDER_SHADOW_TILE(_TILE_, _RECT_) \ +xcb_render_composite(connection(), XCB_RENDER_PICT_OP_OVER, m_xrenderShadow->picture(SceneXRenderShadow::ShadowElement##_TILE_), \ + shadowAlpha, renderTarget, 0, 0, 0, 0, _RECT_.x(), _RECT_.y(), _RECT_.width(), _RECT_.height()) + + //shadow + if (wantShadow) { + xcb_render_picture_t shadowAlpha = XCB_RENDER_PICTURE_NONE; + if (!opaque) { + shadowAlpha = xRenderBlendPicture(data.opacity()); + } + RENDER_SHADOW_TILE(TopLeft, stlr); + RENDER_SHADOW_TILE(Top, str); + RENDER_SHADOW_TILE(TopRight, strr); + RENDER_SHADOW_TILE(Left, slr); + RENDER_SHADOW_TILE(Right, srr); + RENDER_SHADOW_TILE(BottomLeft, sblr); + RENDER_SHADOW_TILE(Bottom, sbr); + RENDER_SHADOW_TILE(BottomRight, sbrr); + } +#undef RENDER_SHADOW_TILE + + // Paint the window contents + if (!(client && client->isShade())) { + xcb_render_picture_t clientAlpha = XCB_RENDER_PICTURE_NONE; + if (!opaque) { + clientAlpha = xRenderBlendPicture(data.opacity()); + } + xcb_render_composite(connection(), clientRenderOp, pic, clientAlpha, renderTarget, + cr.x(), cr.y(), 0, 0, dr.x(), dr.y(), dr.width(), dr.height()); + if (data.crossFadeProgress() < 1.0 && data.crossFadeProgress() > 0.0) { + XRenderWindowPixmap *previous = previousWindowPixmap(); + if (previous && previous != pixmap) { + static xcb_render_color_t cFadeColor = {0, 0, 0, 0}; + cFadeColor.alpha = uint16_t((1.0 - data.crossFadeProgress()) * 0xffff); + if (!s_fadeAlphaPicture) { + s_fadeAlphaPicture = new XRenderPicture(xRenderFill(cFadeColor)); + } else { + xcb_rectangle_t rect = {0, 0, 1, 1}; + xcb_render_fill_rectangles(connection(), XCB_RENDER_PICT_OP_SRC, *s_fadeAlphaPicture, cFadeColor , 1, &rect); + } + if (previous->size() != pixmap->size()) { + xcb_render_transform_t xform2 = { + DOUBLE_TO_FIXED(FIXED_TO_DOUBLE(xform.matrix11) * previous->size().width() / pixmap->size().width()), DOUBLE_TO_FIXED(0), DOUBLE_TO_FIXED(0), + DOUBLE_TO_FIXED(0), DOUBLE_TO_FIXED(FIXED_TO_DOUBLE(xform.matrix22) * previous->size().height() / pixmap->size().height()), DOUBLE_TO_FIXED(0), + DOUBLE_TO_FIXED(0), DOUBLE_TO_FIXED(0), DOUBLE_TO_FIXED(1) + }; + xcb_render_set_picture_transform(connection(), previous->picture(), xform2); + } + + xcb_render_composite(connection(), opaque ? XCB_RENDER_PICT_OP_OVER : XCB_RENDER_PICT_OP_ATOP, + previous->picture(), *s_fadeAlphaPicture, renderTarget, + cr.x(), cr.y(), 0, 0, dr.x(), dr.y(), dr.width(), dr.height()); + + if (previous->size() != pixmap->size()) { + xcb_render_set_picture_transform(connection(), previous->picture(), identity); + } + } + } + if (!opaque) + transformed_shape = QRegion(); + } + + if (client || deleted) { + if (!noBorder) { + xcb_render_picture_t decorationAlpha = xRenderBlendPicture(data.opacity()); + auto renderDeco = [decorationAlpha, renderTarget](xcb_render_picture_t deco, const QRect &rect) { + if (deco == XCB_RENDER_PICTURE_NONE) { + return; + } + xcb_render_composite(connection(), XCB_RENDER_PICT_OP_OVER, deco, decorationAlpha, renderTarget, + 0, 0, 0, 0, rect.x(), rect.y(), rect.width(), rect.height()); + }; + renderDeco(top, dtr); + renderDeco(left, dlr); + renderDeco(right, drr); + renderDeco(bottom, dbr); + } + } + + if (data.brightness() != 1.0) { + // fake brightness change by overlaying black + const float alpha = (1 - data.brightness()) * data.opacity(); + xcb_rectangle_t rect; + if (blitInTempPixmap) { + rect.x = -temp_visibleRect.left(); + rect.y = -temp_visibleRect.top(); + rect.width = width(); + rect.height = height(); + } else { + rect.x = wr.x(); + rect.y = wr.y(); + rect.width = wr.width(); + rect.height = wr.height(); + } + xcb_render_fill_rectangles(connection(), XCB_RENDER_PICT_OP_OVER, renderTarget, + preMultiply(data.brightness() < 1.0 ? QColor(0,0,0,255*alpha) : QColor(255,255,255,-alpha*255)), + 1, &rect); + } + if (blitInTempPixmap) { + const QRect r = mapToScreen(mask, data, temp_visibleRect); + xcb_render_set_picture_transform(connection(), *s_tempPicture, xform); + setPictureFilter(*s_tempPicture, filter); + xcb_render_composite(connection(), XCB_RENDER_PICT_OP_OVER, *s_tempPicture, + XCB_RENDER_PICTURE_NONE, m_scene->xrenderBufferPicture(), + 0, 0, 0, 0, r.x(), r.y(), r.width(), r.height()); + xcb_render_set_picture_transform(connection(), *s_tempPicture, identity); + } + } + if (scaled && !blitInTempPixmap) { + xcb_render_set_picture_transform(connection(), pic, identity); + if (filter == ImageFilterGood) + setPictureFilter(pic, KWin::Scene::ImageFilterFast); + if (!window()->hasAlpha()) { + const uint32_t values[] = {XCB_RENDER_REPEAT_NONE}; + xcb_render_change_picture(connection(), pic, XCB_RENDER_CP_REPEAT, values); + } + } + if (xRenderOffscreen()) + scene_setXRenderOffscreenTarget(*s_tempPicture); +} + +void SceneXrender::Window::setPictureFilter(xcb_render_picture_t pic, Scene::ImageFilterType filter) +{ + QByteArray filterName; + switch (filter) { + case KWin::Scene::ImageFilterFast: + filterName = QByteArray("fast"); + break; + case KWin::Scene::ImageFilterGood: + filterName = QByteArray("good"); + break; + } + xcb_render_set_picture_filter(connection(), pic, filterName.length(), filterName.constData(), 0, nullptr); +} + +WindowPixmap* SceneXrender::Window::createWindowPixmap() +{ + return new XRenderWindowPixmap(this, format); +} + +void SceneXrender::screenGeometryChanged(const QSize &size) +{ + Scene::screenGeometryChanged(size); + m_backend->screenGeometryChanged(size); +} + +//**************************************** +// XRenderWindowPixmap +//**************************************** + +XRenderWindowPixmap::XRenderWindowPixmap(Scene::Window *window, xcb_render_pictformat_t format) + : WindowPixmap(window) + , m_picture(XCB_RENDER_PICTURE_NONE) + , m_format(format) +{ +} + +XRenderWindowPixmap::~XRenderWindowPixmap() +{ + if (m_picture != XCB_RENDER_PICTURE_NONE) { + xcb_render_free_picture(connection(), m_picture); + } +} + +void XRenderWindowPixmap::create() +{ + if (isValid()) { + return; + } + KWin::WindowPixmap::create(); + if (!isValid()) { + return; + } + m_picture = xcb_generate_id(connection()); + xcb_render_create_picture(connection(), m_picture, pixmap(), m_format, 0, nullptr); +} + +//**************************************** +// SceneXrender::EffectFrame +//**************************************** + +XRenderPicture *SceneXrender::EffectFrame::s_effectFrameCircle = nullptr; + +SceneXrender::EffectFrame::EffectFrame(EffectFrameImpl* frame) + : Scene::EffectFrame(frame) +{ + m_picture = nullptr; + m_textPicture = nullptr; + m_iconPicture = nullptr; + m_selectionPicture = nullptr; +} + +SceneXrender::EffectFrame::~EffectFrame() +{ + delete m_picture; + delete m_textPicture; + delete m_iconPicture; + delete m_selectionPicture; +} + +void SceneXrender::EffectFrame::cleanup() +{ + delete s_effectFrameCircle; + s_effectFrameCircle = nullptr; +} + +void SceneXrender::EffectFrame::free() +{ + delete m_picture; + m_picture = nullptr; + delete m_textPicture; + m_textPicture = nullptr; + delete m_iconPicture; + m_iconPicture = nullptr; + delete m_selectionPicture; + m_selectionPicture = nullptr; +} + +void SceneXrender::EffectFrame::freeIconFrame() +{ + delete m_iconPicture; + m_iconPicture = nullptr; +} + +void SceneXrender::EffectFrame::freeTextFrame() +{ + delete m_textPicture; + m_textPicture = nullptr; +} + +void SceneXrender::EffectFrame::freeSelection() +{ + delete m_selectionPicture; + m_selectionPicture = nullptr; +} + +void SceneXrender::EffectFrame::crossFadeIcon() +{ + // TODO: implement me +} + +void SceneXrender::EffectFrame::crossFadeText() +{ + // TODO: implement me +} + +void SceneXrender::EffectFrame::render(const QRegion ®ion, double opacity, double frameOpacity) +{ + Q_UNUSED(region) + if (m_effectFrame->geometry().isEmpty()) { + return; // Nothing to display + } + + // Render the actual frame + if (m_effectFrame->style() == EffectFrameUnstyled) { + renderUnstyled(effects->xrenderBufferPicture(), m_effectFrame->geometry(), opacity * frameOpacity); + } else if (m_effectFrame->style() == EffectFrameStyled) { + if (!m_picture) { // Lazy creation + updatePicture(); + } + if (m_picture) { + qreal left, top, right, bottom; + m_effectFrame->frame().getMargins(left, top, right, bottom); // m_geometry is the inner geometry + QRect geom = m_effectFrame->geometry().adjusted(-left, -top, right, bottom); + xcb_render_composite(connection(), XCB_RENDER_PICT_OP_OVER, *m_picture, + XCB_RENDER_PICTURE_NONE, effects->xrenderBufferPicture(), + 0, 0, 0, 0, geom.x(), geom.y(), geom.width(), geom.height()); + } + } + if (!m_effectFrame->selection().isNull()) { + if (!m_selectionPicture) { // Lazy creation + const QPixmap pix = m_effectFrame->selectionFrame().framePixmap(); + if (!pix.isNull()) // don't try if there's no content + m_selectionPicture = new XRenderPicture(m_effectFrame->selectionFrame().framePixmap().toImage()); + } + if (m_selectionPicture) { + const QRect geom = m_effectFrame->selection(); + xcb_render_composite(connection(), XCB_RENDER_PICT_OP_OVER, *m_selectionPicture, + XCB_RENDER_PICTURE_NONE, effects->xrenderBufferPicture(), + 0, 0, 0, 0, geom.x(), geom.y(), geom.width(), geom.height()); + } + } + + XRenderPicture fill = xRenderBlendPicture(opacity); + + // Render icon + if (!m_effectFrame->icon().isNull() && !m_effectFrame->iconSize().isEmpty()) { + QPoint topLeft(m_effectFrame->geometry().x(), m_effectFrame->geometry().center().y() - m_effectFrame->iconSize().height() / 2); + + if (!m_iconPicture) // lazy creation + m_iconPicture = new XRenderPicture(m_effectFrame->icon().pixmap(m_effectFrame->iconSize()).toImage()); + QRect geom = QRect(topLeft, m_effectFrame->iconSize()); + xcb_render_composite(connection(), XCB_RENDER_PICT_OP_OVER, *m_iconPicture, fill, + effects->xrenderBufferPicture(), + 0, 0, 0, 0, geom.x(), geom.y(), geom.width(), geom.height()); + } + + // Render text + if (!m_effectFrame->text().isEmpty()) { + if (!m_textPicture) { // Lazy creation + updateTextPicture(); + } + + if (m_textPicture) { + xcb_render_composite(connection(), XCB_RENDER_PICT_OP_OVER, *m_textPicture, fill, effects->xrenderBufferPicture(), + 0, 0, 0, 0, m_effectFrame->geometry().x(), m_effectFrame->geometry().y(), + m_effectFrame->geometry().width(), m_effectFrame->geometry().height()); + } + } +} + +void SceneXrender::EffectFrame::renderUnstyled(xcb_render_picture_t pict, const QRect &rect, qreal opacity) +{ + const int roundness = 5; + const QRect area = rect.adjusted(-roundness, -roundness, roundness, roundness); + xcb_rectangle_t rects[3]; + // center + rects[0].x = area.left(); + rects[0].y = area.top() + roundness; + rects[0].width = area.width(); + rects[0].height = area.height() - roundness * 2; + // top + rects[1].x = area.left() + roundness; + rects[1].y = area.top(); + rects[1].width = area.width() - roundness * 2; + rects[1].height = roundness; + // bottom + rects[2].x = area.left() + roundness; + rects[2].y = area.top() + area.height() - roundness; + rects[2].width = area.width() - roundness * 2; + rects[2].height = roundness; + xcb_render_color_t color = {0, 0, 0, uint16_t(opacity * 0xffff)}; + xcb_render_fill_rectangles(connection(), XCB_RENDER_PICT_OP_OVER, pict, color, 3, rects); + + if (!s_effectFrameCircle) { + // create the circle + const int diameter = roundness * 2; + xcb_pixmap_t pix = xcb_generate_id(connection()); + xcb_create_pixmap(connection(), 32, pix, rootWindow(), diameter, diameter); + s_effectFrameCircle = new XRenderPicture(pix, 32); + xcb_free_pixmap(connection(), pix); + + // clear it with transparent + xcb_rectangle_t xrect = {0, 0, diameter, diameter}; + xcb_render_color_t tranparent = {0, 0, 0, 0}; + xcb_render_fill_rectangles(connection(), XCB_RENDER_PICT_OP_SRC, *s_effectFrameCircle, tranparent, 1, &xrect); + + static const int num_segments = 80; + static const qreal theta = 2 * M_PI / qreal(num_segments); + static const qreal c = qCos(theta); //precalculate the sine and cosine + static const qreal s = qSin(theta); + qreal t; + + qreal x = roundness;//we start at angle = 0 + qreal y = 0; + + QVector points; + xcb_render_pointfix_t point; + point.x = DOUBLE_TO_FIXED(roundness); + point.y = DOUBLE_TO_FIXED(roundness); + points << point; + for (int ii = 0; ii <= num_segments; ++ii) { + point.x = DOUBLE_TO_FIXED(x + roundness); + point.y = DOUBLE_TO_FIXED(y + roundness); + points << point; + //apply the rotation matrix + t = x; + x = c * x - s * y; + y = s * t + c * y; + } + XRenderPicture fill = xRenderFill(Qt::black); + xcb_render_tri_fan(connection(), XCB_RENDER_PICT_OP_OVER, fill, *s_effectFrameCircle, + 0, 0, 0, points.count(), points.constData()); + } + // TODO: merge alpha mask with SceneXrender::Window::alphaMask + // alpha mask + xcb_pixmap_t pix = xcb_generate_id(connection()); + xcb_create_pixmap(connection(), 8, pix, rootWindow(), 1, 1); + XRenderPicture alphaMask(pix, 8); + xcb_free_pixmap(connection(), pix); + const uint32_t values[] = {true}; + xcb_render_change_picture(connection(), alphaMask, XCB_RENDER_CP_REPEAT, values); + color.alpha = int(opacity * 0xffff); + xcb_rectangle_t xrect = {0, 0, 1, 1}; + xcb_render_fill_rectangles(connection(), XCB_RENDER_PICT_OP_SRC, alphaMask, color, 1, &xrect); + + // TODO: replace by lambda +#define RENDER_CIRCLE(srcX, srcY, destX, destY) \ +xcb_render_composite(connection(), XCB_RENDER_PICT_OP_OVER, *s_effectFrameCircle, alphaMask, \ + pict, srcX, srcY, 0, 0, destX, destY, roundness, roundness) + + RENDER_CIRCLE(0, 0, area.left(), area.top()); + RENDER_CIRCLE(0, roundness, area.left(), area.top() + area.height() - roundness); + RENDER_CIRCLE(roundness, 0, area.left() + area.width() - roundness, area.top()); + RENDER_CIRCLE(roundness, roundness, + area.left() + area.width() - roundness, area.top() + area.height() - roundness); +#undef RENDER_CIRCLE +} + +void SceneXrender::EffectFrame::updatePicture() +{ + delete m_picture; + m_picture = nullptr; + if (m_effectFrame->style() == EffectFrameStyled) { + const QPixmap pix = m_effectFrame->frame().framePixmap(); + if (!pix.isNull()) + m_picture = new XRenderPicture(pix.toImage()); + } +} + +void SceneXrender::EffectFrame::updateTextPicture() +{ + // Mostly copied from SceneOpenGL::EffectFrame::updateTextTexture() above + delete m_textPicture; + m_textPicture = nullptr; + + if (m_effectFrame->text().isEmpty()) { + return; + } + + // Determine position on texture to paint text + QRect rect(QPoint(0, 0), m_effectFrame->geometry().size()); + if (!m_effectFrame->icon().isNull() && !m_effectFrame->iconSize().isEmpty()) { + rect.setLeft(m_effectFrame->iconSize().width()); + } + + // If static size elide text as required + QString text = m_effectFrame->text(); + if (m_effectFrame->isStatic()) { + QFontMetrics metrics(m_effectFrame->text()); + text = metrics.elidedText(text, Qt::ElideRight, rect.width()); + } + + QPixmap pixmap(m_effectFrame->geometry().size()); + pixmap.fill(Qt::transparent); + QPainter p(&pixmap); + p.setFont(m_effectFrame->font()); + if (m_effectFrame->style() == EffectFrameStyled) { + p.setPen(m_effectFrame->styledTextColor()); + } else { + // TODO: What about no frame? Custom color setting required + p.setPen(Qt::white); + } + p.drawText(rect, m_effectFrame->alignment(), text); + p.end(); + m_textPicture = new XRenderPicture(pixmap.toImage()); +} + +SceneXRenderShadow::SceneXRenderShadow(Toplevel *toplevel) + :Shadow(toplevel) +{ + for (int i=0; iclient(), static_cast(&AbstractClient::addRepaint)); + for (int i = 0; i < int(DecorationPart::Count); ++i) { + m_pixmaps[i] = XCB_PIXMAP_NONE; + m_pictures[i] = nullptr; + } +} + +SceneXRenderDecorationRenderer::~SceneXRenderDecorationRenderer() +{ + for (int i = 0; i < int(DecorationPart::Count); ++i) { + if (m_pixmaps[i] != XCB_PIXMAP_NONE) { + xcb_free_pixmap(connection(), m_pixmaps[i]); + } + delete m_pictures[i]; + } + if (m_gc != 0) { + xcb_free_gc(connection(), m_gc); + } +} + +void SceneXRenderDecorationRenderer::render() +{ + QRegion scheduled = getScheduled(); + if (scheduled.isEmpty()) { + return; + } + if (areImageSizesDirty()) { + resizePixmaps(); + resetImageSizesDirty(); + } + + const QRect top(QPoint(0, 0), m_sizes[int(DecorationPart::Top)]); + const QRect left(QPoint(0, top.height()), m_sizes[int(DecorationPart::Left)]); + const QRect right(QPoint(top.width() - m_sizes[int(DecorationPart::Right)].width(), top.height()), m_sizes[int(DecorationPart::Right)]); + const QRect bottom(QPoint(0, left.y() + left.height()), m_sizes[int(DecorationPart::Bottom)]); + + xcb_connection_t *c = connection(); + if (m_gc == 0) { + m_gc = xcb_generate_id(connection()); + xcb_create_gc(c, m_gc, m_pixmaps[int(DecorationPart::Top)], 0, nullptr); + } + auto renderPart = [this, c](const QRect &geo, const QPoint &offset, int index) { + if (!geo.isValid()) { + return; + } + QImage image = renderToImage(geo); + Q_ASSERT(image.devicePixelRatio() == 1); + xcb_put_image(c, XCB_IMAGE_FORMAT_Z_PIXMAP, m_pixmaps[index], m_gc, + image.width(), image.height(), geo.x() - offset.x(), geo.y() - offset.y(), 0, 32, + image.sizeInBytes(), image.constBits()); + }; + const QRect geometry = scheduled.boundingRect(); + renderPart(left.intersected(geometry), left.topLeft(), int(DecorationPart::Left)); + renderPart(top.intersected(geometry), top.topLeft(), int(DecorationPart::Top)); + renderPart(right.intersected(geometry), right.topLeft(), int(DecorationPart::Right)); + renderPart(bottom.intersected(geometry), bottom.topLeft(), int(DecorationPart::Bottom)); + xcb_flush(c); +} + +void SceneXRenderDecorationRenderer::resizePixmaps() +{ + QRect left, top, right, bottom; + client()->client()->layoutDecorationRects(left, top, right, bottom); + + xcb_connection_t *c = connection(); + auto checkAndCreate = [this, c](int border, const QRect &rect) { + const QSize size = rect.size(); + if (m_sizes[border] != size) { + m_sizes[border] = size; + if (m_pixmaps[border] != XCB_PIXMAP_NONE) { + xcb_free_pixmap(c, m_pixmaps[border]); + } + delete m_pictures[border]; + if (!size.isEmpty()) { + m_pixmaps[border] = xcb_generate_id(connection()); + xcb_create_pixmap(connection(), 32, m_pixmaps[border], rootWindow(), size.width(), size.height()); + m_pictures[border] = new XRenderPicture(m_pixmaps[border], 32); + } else { + m_pixmaps[border] = XCB_PIXMAP_NONE; + m_pictures[border] = nullptr; + } + } + if (!m_pictures[border]) { + return; + } + // fill transparent + xcb_rectangle_t r = {0, 0, uint16_t(size.width()), uint16_t(size.height())}; + xcb_render_fill_rectangles(connection(), XCB_RENDER_PICT_OP_SRC, *m_pictures[border], preMultiply(Qt::transparent), 1, &r); + }; + + checkAndCreate(int(DecorationPart::Left), left); + checkAndCreate(int(DecorationPart::Top), top); + checkAndCreate(int(DecorationPart::Right), right); + checkAndCreate(int(DecorationPart::Bottom), bottom); +} + +xcb_render_picture_t SceneXRenderDecorationRenderer::picture(SceneXRenderDecorationRenderer::DecorationPart part) const +{ + Q_ASSERT(part != DecorationPart::Count); + XRenderPicture *picture = m_pictures[int(part)]; + if (!picture) { + return XCB_RENDER_PICTURE_NONE; + } + return *picture; +} + +void SceneXRenderDecorationRenderer::reparent(Deleted *deleted) +{ + render(); + Renderer::reparent(deleted); +} + +#undef DOUBLE_TO_FIXED +#undef FIXED_TO_DOUBLE + +XRenderFactory::XRenderFactory(QObject *parent) + : SceneFactory(parent) +{ +} + +XRenderFactory::~XRenderFactory() = default; + +Scene *XRenderFactory::create(QObject *parent) const +{ + auto s = SceneXrender::createScene(parent); + if (s && s->initFailed()) { + delete s; + s = nullptr; + } + return s; +} + +} // namespace +#endif + + +void KWin::SceneXrender::paintCursor(const QRegion ®ion) +{ + Q_UNUSED(region) +} + +void KWin::SceneXrender::paintEffectQuickView(KWin::EffectQuickView *w) +{ + const QImage buffer = w->bufferAsImage(); + if (buffer.isNull()) { + return; + } + XRenderPicture picture(buffer); + xcb_render_composite(connection(), XCB_RENDER_PICT_OP_OVER, picture, XCB_RENDER_PICTURE_NONE, + effects->xrenderBufferPicture(), + 0, 0, 0, 0, w->geometry().x(), w->geometry().y(), + w->geometry().width(), w->geometry().height()); +} diff --git a/plugins/scenes/xrender/scene_xrender.h b/plugins/scenes/xrender/scene_xrender.h new file mode 100644 index 0000000..22f7cea --- /dev/null +++ b/plugins/scenes/xrender/scene_xrender.h @@ -0,0 +1,351 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KWIN_SCENE_XRENDER_H +#define KWIN_SCENE_XRENDER_H + +#include "scene.h" +#include "shadow.h" +#include "decorations/decorationrenderer.h" + +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + +namespace KWin +{ + +namespace Xcb +{ + class Shm; +} + +/** + * @brief Backend for the SceneXRender to hold the compositing buffer and take care of buffer + * swapping. + * + * This class is intended as a small abstraction to support multiple compositing backends in the + * SceneXRender. + */ +class XRenderBackend +{ +public: + virtual ~XRenderBackend(); + virtual void present(int mask, const QRegion &damage) = 0; + + /** + * @brief Returns the OverlayWindow used by the backend. + * + * A backend does not have to use an OverlayWindow, this is mostly for the X world. + * In case the backend does not use an OverlayWindow it is allowed to return @c null. + * It's the task of the caller to check whether it is @c null. + * + * @return :OverlayWindow* + */ + virtual OverlayWindow *overlayWindow(); + virtual bool usesOverlayWindow() const = 0; + /** + * @brief Shows the Overlay Window + * + * Default implementation does nothing. + */ + virtual void showOverlay(); + /** + * @brief React on screen geometry changes. + * + * Default implementation does nothing. Override if specific functionality is required. + * + * @param size The new screen size + */ + virtual void screenGeometryChanged(const QSize &size); + /** + * @brief The compositing buffer hold by this backend. + * + * The Scene composites the new frame into this buffer. + * + * @return xcb_render_picture_t + */ + xcb_render_picture_t buffer() const { + return m_buffer; + } + /** + * @brief Whether the creation of the Backend failed. + * + * The SceneXRender should test whether the Backend got constructed correctly. If this method + * returns @c true, the SceneXRender should not try to start the rendering. + * + * @return bool @c true if the creation of the Backend failed, @c false otherwise. + */ + bool isFailed() const { + return m_failed; + } + +protected: + XRenderBackend(); + /** + * @brief A subclass needs to call this method once it created the compositing back buffer. + * + * @param buffer The buffer to use for compositing + * @return void + */ + void setBuffer(xcb_render_picture_t buffer); + /** + * @brief Sets the backend initialization to failed. + * + * This method should be called by the concrete subclass in case the initialization failed. + * The given @p reason is logged as a warning. + * + * @param reason The reason why the initialization failed. + */ + void setFailed(const QString &reason); + +private: + // Create the compositing buffer. The root window is not double-buffered, + // so it is done manually using this buffer, + xcb_render_picture_t m_buffer; + bool m_failed; +}; + +/** + * @brief XRenderBackend using an X11 Overlay Window as compositing target. + */ +class X11XRenderBackend : public XRenderBackend +{ +public: + X11XRenderBackend(); + ~X11XRenderBackend() override; + + void present(int mask, const QRegion &damage) override; + OverlayWindow* overlayWindow() override; + void showOverlay() override; + void screenGeometryChanged(const QSize &size) override; + bool usesOverlayWindow() const override; +private: + void init(bool createOverlay); + void createBuffer(); + QScopedPointer m_overlayWindow; + xcb_render_picture_t m_front; + xcb_render_pictformat_t m_format; +}; + +class SceneXrender + : public Scene +{ + Q_OBJECT +public: + class EffectFrame; + ~SceneXrender() override; + bool initFailed() const override; + CompositingType compositingType() const override { + return XRenderCompositing; + } + qint64 paint(const QRegion &damage, const QList &windows) override; + Scene::EffectFrame *createEffectFrame(EffectFrameImpl *frame) override; + Shadow *createShadow(Toplevel *toplevel) override; + void screenGeometryChanged(const QSize &size) override; + xcb_render_picture_t xrenderBufferPicture() const override; + OverlayWindow *overlayWindow() const override { + return m_backend->overlayWindow(); + } + bool usesOverlayWindow() const override { + return m_backend->usesOverlayWindow(); + } + Decoration::Renderer *createDecorationRenderer(Decoration::DecoratedClientImpl *client) override; + + bool animationsSupported() const override { + return true; + } + + static SceneXrender *createScene(QObject *parent); +protected: + Scene::Window *createWindow(Toplevel *toplevel) override; + void paintBackground(const QRegion ®ion) override; + void paintGenericScreen(int mask, const ScreenPaintData &data) override; + void paintDesktop(int desktop, int mask, const QRegion ®ion, ScreenPaintData &data) override; + void paintCursor(const QRegion ®ion) override; + void paintEffectQuickView(EffectQuickView *w) override; +private: + explicit SceneXrender(XRenderBackend *backend, QObject *parent = nullptr); + static ScreenPaintData screen_paint; + class Window; + QScopedPointer m_backend; +}; + +class SceneXrender::Window : public Scene::Window +{ + Q_OBJECT + +public: + Window(Toplevel* c, SceneXrender *scene); + ~Window() override; + void performPaint(int mask, const QRegion ®ion, const WindowPaintData &data) override; + QRegion transformedShape() const; + void setTransformedShape(const QRegion& shape); + static void cleanup(); +protected: + WindowPixmap* createWindowPixmap() override; +private: + QRect mapToScreen(int mask, const WindowPaintData &data, const QRect &rect) const; + QPoint mapToScreen(int mask, const WindowPaintData &data, const QPoint &point) const; + QRect bufferToWindowRect(const QRect &rect) const; + QRegion bufferToWindowRegion(const QRegion ®ion) const; + void prepareTempPixmap(); + void setPictureFilter(xcb_render_picture_t pic, ImageFilterType filter); + SceneXrender *m_scene; + xcb_render_pictformat_t format; + QRegion transformed_shape; + static QRect temp_visibleRect; + static XRenderPicture *s_tempPicture; + static XRenderPicture *s_fadeAlphaPicture; +}; + +class XRenderWindowPixmap : public WindowPixmap +{ +public: + explicit XRenderWindowPixmap(Scene::Window *window, xcb_render_pictformat_t format); + ~XRenderWindowPixmap() override; + xcb_render_picture_t picture() const; + void create() override; +private: + xcb_render_picture_t m_picture; + xcb_render_pictformat_t m_format; +}; + +class SceneXrender::EffectFrame + : public Scene::EffectFrame +{ +public: + EffectFrame(EffectFrameImpl* frame); + ~EffectFrame() override; + + void free() override; + void freeIconFrame() override; + void freeTextFrame() override; + void freeSelection() override; + void crossFadeIcon() override; + void crossFadeText() override; + void render(const QRegion ®ion, double opacity, double frameOpacity) override; + static void cleanup(); + +private: + void updatePicture(); + void updateTextPicture(); + void renderUnstyled(xcb_render_picture_t pict, const QRect &rect, qreal opacity); + + XRenderPicture* m_picture; + XRenderPicture* m_textPicture; + XRenderPicture* m_iconPicture; + XRenderPicture* m_selectionPicture; + static XRenderPicture* s_effectFrameCircle; +}; + +inline +xcb_render_picture_t SceneXrender::xrenderBufferPicture() const +{ + return m_backend->buffer(); +} + +inline +QRegion SceneXrender::Window::transformedShape() const +{ + return transformed_shape; +} + +inline +void SceneXrender::Window::setTransformedShape(const QRegion& shape) +{ + transformed_shape = shape; +} + +inline +xcb_render_picture_t XRenderWindowPixmap::picture() const +{ + return m_picture; +} + +/** + * @short XRender implementation of Shadow. + * + * This class extends Shadow by the elements required for XRender rendering. + * @author Jacopo De Simoi + */ +class SceneXRenderShadow + : public Shadow +{ +public: + explicit SceneXRenderShadow(Toplevel *toplevel); + using Shadow::ShadowElements; + using Shadow::ShadowElementTop; + using Shadow::ShadowElementTopRight; + using Shadow::ShadowElementRight; + using Shadow::ShadowElementBottomRight; + using Shadow::ShadowElementBottom; + using Shadow::ShadowElementBottomLeft; + using Shadow::ShadowElementLeft; + using Shadow::ShadowElementTopLeft; + using Shadow::ShadowElementsCount; + using Shadow::shadowPixmap; + ~SceneXRenderShadow() override; + + void layoutShadowRects(QRect& top, QRect& topRight, + QRect& right, QRect& bottomRight, + QRect& bottom, QRect& bottomLeft, + QRect& Left, QRect& topLeft); + xcb_render_picture_t picture(ShadowElements element) const; + +protected: + void buildQuads() override; + bool prepareBackend() override; +private: + XRenderPicture* m_pictures[ShadowElementsCount]; +}; + +class SceneXRenderDecorationRenderer : public Decoration::Renderer +{ + Q_OBJECT +public: + enum class DecorationPart : int { + Left, + Top, + Right, + Bottom, + Count + }; + explicit SceneXRenderDecorationRenderer(Decoration::DecoratedClientImpl *client); + ~SceneXRenderDecorationRenderer() override; + + void render() override; + void reparent(Deleted *deleted) override; + + xcb_render_picture_t picture(DecorationPart part) const; + +private: + void resizePixmaps(); + QSize m_sizes[int(DecorationPart::Count)]; + xcb_pixmap_t m_pixmaps[int(DecorationPart::Count)]; + xcb_gcontext_t m_gc; + XRenderPicture* m_pictures[int(DecorationPart::Count)]; +}; + +class KWIN_EXPORT XRenderFactory : public SceneFactory +{ + Q_OBJECT + Q_INTERFACES(KWin::SceneFactory) + Q_PLUGIN_METADATA(IID "org.kde.kwin.Scene" FILE "xrender.json") + +public: + explicit XRenderFactory(QObject *parent = nullptr); + ~XRenderFactory() override; + + Scene *create(QObject *parent = nullptr) const override; +}; + +} // namespace + +#endif + +#endif diff --git a/plugins/scenes/xrender/xrender.json b/plugins/scenes/xrender/xrender.json new file mode 100644 index 0000000..f9ee43c --- /dev/null +++ b/plugins/scenes/xrender/xrender.json @@ -0,0 +1,82 @@ +{ + "CompositingType": 2, + "KPlugin": { + "Description": "KWin Compositor plugin rendering through XRender", + "Description[az]": "XRender avsitəsi ilə KWin birləşdirici əlavəsini formalaşdırmaq", + "Description[ca@valencia]": "Connector del Compositor de KWin que renderitza a través de XRender", + "Description[ca]": "Connector del Compositor del KWin que renderitza a través del XRender", + "Description[da]": "KWin-compositorplugin som renderer igennem XRender", + "Description[de]": "KWin-Compositor-Modul zum Rendern mit XRender", + "Description[el]": "Αποτύπωση πρσοθέτου συνθέτη KWin μέσω XRender", + "Description[en_GB]": "KWin Compositor plugin rendering through XRender", + "Description[es]": "Complemento compositor de KWin renderizando mediante XRender", + "Description[et]": "KWini komposiitori plugin renderdamiseks XRender'i abil", + "Description[eu]": "Kwin konposatzailearen plugina XRender bidez errendatzen", + "Description[fi]": "XRenderillä hahmontava KWin-koostajaliitännäinen", + "Description[fr]": "Module du compositeur KWin effectuant le rendu avec XRender", + "Description[gl]": "Complemento de compositor de KWin que renderiza a través de XRender.", + "Description[hu]": "KWin összeállító bővítmény XRender leképezéssel", + "Description[id]": "Plugin KWin Compositor perenderan melalui XRender", + "Description[it]": "Estensione del compositore di KWin per la resa tramite XRender", + "Description[ko]": "XRender로 렌더링하는 KWin 컴포지터 플러그인", + "Description[lt]": "KWin kompozitoriaus priedas atvaizdavimui per XRender", + "Description[nl]": "KWin-compositor-plug-in rendering via XRender", + "Description[nn]": "KWin-samansetjartillegg som brukar XRender", + "Description[pl]": "Wtyczka kompozytora KWin wyświetlająca przez XRender", + "Description[pt]": "'Plugin' de Composição do KWin com desenho via XRender", + "Description[pt_BR]": "Plugin do compositor KWin renderizando pelo XRender", + "Description[ru]": "Отрисовка подключаемым модулем компоновщика KWin через XRender", + "Description[sk]": "Renderovací plugin kompozítora KWin cez XRender", + "Description[sl]": "Izrisovanje vstavka upravljalnika skladnje KWin preko XRender-ja", + "Description[sr@ijekavian]": "К‑винов прикључак слагача за рендеровање кроз Икс‑рендер", + "Description[sr@ijekavianlatin]": "KWinov priključak slagača za renderovanje kroz XRender", + "Description[sr@latin]": "KWinov priključak slagača za renderovanje kroz XRender", + "Description[sr]": "К‑винов прикључак слагача за рендеровање кроз Икс‑рендер", + "Description[sv]": "Kwin sammansättningsinsticksprogram Ã¥terger via XRender", + "Description[tr]": "XRender üzerinden KWin Dizgici eklentisi oluşturma", + "Description[uk]": "Додаток засобу композиції KWin для обробки з використанням XRender", + "Description[x-test]": "xxKWin Compositor plugin rendering through XRenderxx", + "Description[zh_CN]": "使用 XRender 渲染的 KWin 混成插件", + "Description[zh_TW]": "透過 XRender 繪製 KWin 合成器附加元件", + "Id": "KWinSceneXRender", + "Name": "SceneXRender", + "Name[az]": "SceneXRender", + "Name[ca@valencia]": "SceneXRender", + "Name[ca]": "SceneXRender", + "Name[cs]": "SceneXRender", + "Name[da]": "SceneXRender", + "Name[de]": "SceneXRender", + "Name[el]": "SceneXRender", + "Name[en_GB]": "SceneXRender", + "Name[es]": "SceneXRender", + "Name[et]": "SceneXRender", + "Name[eu]": "SceneXRender", + "Name[fi]": "SceneXRender", + "Name[fr]": "SceneXRender", + "Name[gl]": "SceneXRender", + "Name[hu]": "SceneXRender", + "Name[id]": "SceneXRender", + "Name[it]": "SceneXRender", + "Name[ko]": "SceneXRender", + "Name[lt]": "SceneXRender", + "Name[nl]": "SceneXRender", + "Name[nn]": "SceneXRender", + "Name[pl]": "XRender sceny", + "Name[pt]": "SceneXRender", + "Name[pt_BR]": "SceneXRender", + "Name[ro]": "SceneXRender", + "Name[ru]": "SceneXRender", + "Name[sk]": "SceneXRender", + "Name[sl]": "SceneXRender", + "Name[sr@ijekavian]": "Икс‑рендер-сцена", + "Name[sr@ijekavianlatin]": "XRender-scena", + "Name[sr@latin]": "XRender-scena", + "Name[sr]": "Икс‑рендер-сцена", + "Name[sv]": "Scen XRender", + "Name[tr]": "SceneXRender", + "Name[uk]": "SceneXRender", + "Name[x-test]": "xxSceneXRenderxx", + "Name[zh_CN]": "SceneXRender", + "Name[zh_TW]": "SceneXRender" + } +} diff --git a/plugins/windowsystem/CMakeLists.txt b/plugins/windowsystem/CMakeLists.txt new file mode 100644 index 0000000..88ef5b8 --- /dev/null +++ b/plugins/windowsystem/CMakeLists.txt @@ -0,0 +1,17 @@ +set(kwindowsystem_plugin_SRCS + plugin.cpp + windoweffects.cpp + windowshadow.cpp + windowsystem.cpp +) + +add_library(KF5WindowSystemKWinPrivatePlugin MODULE ${kwindowsystem_plugin_SRCS}) +set_target_properties(KF5WindowSystemKWinPrivatePlugin PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/kf5/kwindowsystem/") +target_link_libraries(KF5WindowSystemKWinPrivatePlugin kwin) + +install( + TARGETS + KF5WindowSystemKWinPrivatePlugin + DESTINATION + ${PLUGIN_INSTALL_DIR}/kf5/kwindowsystem/ +) diff --git a/plugins/windowsystem/kwindowsystem.json b/plugins/windowsystem/kwindowsystem.json new file mode 100644 index 0000000..aaf6fd0 --- /dev/null +++ b/plugins/windowsystem/kwindowsystem.json @@ -0,0 +1,3 @@ +{ + "platforms": ["wayland-org.kde.kwin.qpa"] +} diff --git a/plugins/windowsystem/plugin.cpp b/plugins/windowsystem/plugin.cpp new file mode 100644 index 0000000..07d0df1 --- /dev/null +++ b/plugins/windowsystem/plugin.cpp @@ -0,0 +1,38 @@ +/* + SPDX-FileCopyrightText: 2019 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#include "plugin.h" +#include "windoweffects.h" +#include "windowshadow.h" +#include "windowsystem.h" + +KWindowSystemKWinPlugin::KWindowSystemKWinPlugin(QObject *parent) + : KWindowSystemPluginInterface(parent) +{ +} + +KWindowSystemKWinPlugin::~KWindowSystemKWinPlugin() +{ +} + +KWindowEffectsPrivate *KWindowSystemKWinPlugin::createEffects() +{ + return new KWin::WindowEffects(); +} + +KWindowSystemPrivate *KWindowSystemKWinPlugin::createWindowSystem() +{ + return new KWin::WindowSystem(); +} + +KWindowShadowTilePrivate *KWindowSystemKWinPlugin::createWindowShadowTile() +{ + return new KWin::WindowShadowTile(); +} + +KWindowShadowPrivate *KWindowSystemKWinPlugin::createWindowShadow() +{ + return new KWin::WindowShadow(); +} diff --git a/plugins/windowsystem/plugin.h b/plugins/windowsystem/plugin.h new file mode 100644 index 0000000..4f1f311 --- /dev/null +++ b/plugins/windowsystem/plugin.h @@ -0,0 +1,24 @@ +/* + SPDX-FileCopyrightText: 2019 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#pragma once + +#include + +class KWindowSystemKWinPlugin : public KWindowSystemPluginInterface +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.kde.kwindowsystem.KWindowSystemPluginInterface" FILE "kwindowsystem.json") + Q_INTERFACES(KWindowSystemPluginInterface) + +public: + explicit KWindowSystemKWinPlugin(QObject *parent = nullptr); + ~KWindowSystemKWinPlugin() override; + + KWindowEffectsPrivate *createEffects() override; + KWindowSystemPrivate *createWindowSystem() override; + KWindowShadowTilePrivate *createWindowShadowTile() override; + KWindowShadowPrivate *createWindowShadow() override; +}; diff --git a/plugins/windowsystem/windoweffects.cpp b/plugins/windowsystem/windoweffects.cpp new file mode 100644 index 0000000..bee1f42 --- /dev/null +++ b/plugins/windowsystem/windoweffects.cpp @@ -0,0 +1,140 @@ +/* + SPDX-FileCopyrightText: 2019 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#include "windoweffects.h" +#include "effect_builtins.h" +#include "../../effects.h" + +#include +#include +#include + +Q_DECLARE_METATYPE(KWindowEffects::SlideFromLocation) + +namespace KWin +{ + +WindowEffects::WindowEffects() + : QObject(), + KWindowEffectsPrivate() +{ +} + +WindowEffects::~WindowEffects() +{} + +namespace +{ +QWindow *findWindow(WId win) +{ + const auto windows = qApp->allWindows(); + auto it = std::find_if(windows.begin(), windows.end(), [win] (QWindow *w) { return w->winId() == win; }); + if (it == windows.end()) { + return nullptr; + } + return *it; +} +} + +bool WindowEffects::isEffectAvailable(KWindowEffects::Effect effect) +{ + if (!effects) { + return false; + } + auto e = static_cast(effects); + switch (effect) { + case KWindowEffects::BackgroundContrast: + return e->isEffectLoaded(BuiltInEffects::nameForEffect(BuiltInEffect::Contrast)); + case KWindowEffects::BlurBehind: + return e->isEffectLoaded(BuiltInEffects::nameForEffect(BuiltInEffect::Blur)); + case KWindowEffects::Slide: + return e->isEffectLoaded(BuiltInEffects::nameForEffect(BuiltInEffect::SlidingPopups)); + default: + // plugin does not provide integration for other effects + return false; + } +} + +void WindowEffects::slideWindow(WId id, KWindowEffects::SlideFromLocation location, int offset) +{ + auto w = findWindow(id); + if (!w) { + return; + } + w->setProperty("kwin_slide", QVariant::fromValue(location)); + w->setProperty("kwin_slide_offset", offset); +} + +#if KWINDOWSYSTEM_VERSION <= QT_VERSION_CHECK(5, 61, 0) +void WindowEffects::slideWindow(QWidget *widget, KWindowEffects::SlideFromLocation location) +{ + slideWindow(widget->winId(), location, 0); +} +#endif + +QList WindowEffects::windowSizes(const QList &ids) +{ + Q_UNUSED(ids) + return {}; +} + +void WindowEffects::presentWindows(WId controller, const QList &ids) +{ + Q_UNUSED(controller) + Q_UNUSED(ids) +} + +void WindowEffects::presentWindows(WId controller, int desktop) +{ + Q_UNUSED(controller) + Q_UNUSED(desktop) +} + +void WindowEffects::highlightWindows(WId controller, const QList &ids) +{ + Q_UNUSED(controller) + Q_UNUSED(ids) +} + +void WindowEffects::enableBlurBehind(WId window, bool enable, const QRegion ®ion) +{ + auto w = findWindow(window); + if (!w) { + return; + } + if (enable) { + w->setProperty("kwin_blur", region); + } else { + w->setProperty("kwin_blur", {}); + } +} + +void WindowEffects::enableBackgroundContrast(WId window, bool enable, qreal contrast, qreal intensity, qreal saturation, const QRegion ®ion) +{ + auto w = findWindow(window); + if (!w) { + return; + } + if (enable) { + w->setProperty("kwin_background_region", region); + w->setProperty("kwin_background_contrast", contrast); + w->setProperty("kwin_background_intensity", intensity); + w->setProperty("kwin_background_saturation", saturation); + } else { + w->setProperty("kwin_background_region", {}); + w->setProperty("kwin_background_contrast", {}); + w->setProperty("kwin_background_intensity", {}); + w->setProperty("kwin_background_saturation", {}); + } +} + +#if KWINDOWSYSTEM_BUILD_DEPRECATED_SINCE(5, 67) +void WindowEffects::markAsDashboard(WId window) +{ + Q_UNUSED(window) +} +#endif + +} diff --git a/plugins/windowsystem/windoweffects.h b/plugins/windowsystem/windoweffects.h new file mode 100644 index 0000000..a70dd91 --- /dev/null +++ b/plugins/windowsystem/windoweffects.h @@ -0,0 +1,35 @@ +/* + SPDX-FileCopyrightText: 2019 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#pragma once +#include +#include + +namespace KWin +{ + +class WindowEffects : public QObject, public KWindowEffectsPrivate +{ +public: + WindowEffects(); + ~WindowEffects() override; + + bool isEffectAvailable(KWindowEffects::Effect effect) override; + void slideWindow(WId id, KWindowEffects::SlideFromLocation location, int offset) override; +#if KWINDOWSYSTEM_VERSION <= QT_VERSION_CHECK(5, 61, 0) + void slideWindow(QWidget *widget, KWindowEffects::SlideFromLocation location) override; +#endif + QList windowSizes(const QList &ids) override; + void presentWindows(WId controller, const QList &ids) override; + void presentWindows(WId controller, int desktop = NET::OnAllDesktops) override; + void highlightWindows(WId controller, const QList &ids) override; + void enableBlurBehind(WId window, bool enable = true, const QRegion ®ion = QRegion()) override; + void enableBackgroundContrast(WId window, bool enable = true, qreal contrast = 1, qreal intensity = 1, qreal saturation = 1, const QRegion ®ion = QRegion()) override; +#if KWINDOWSYSTEM_BUILD_DEPRECATED_SINCE(5, 67) + void markAsDashboard(WId window) override; +#endif +}; + +} diff --git a/plugins/windowsystem/windowshadow.cpp b/plugins/windowsystem/windowshadow.cpp new file mode 100644 index 0000000..2db94f0 --- /dev/null +++ b/plugins/windowsystem/windowshadow.cpp @@ -0,0 +1,81 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "windowshadow.h" + +#include + +Q_DECLARE_METATYPE(QMargins) + +namespace KWin +{ + +bool WindowShadowTile::create() +{ + return true; +} + +void WindowShadowTile::destroy() +{ +} + +bool WindowShadow::create() +{ + // TODO: Perhaps we set way too many properties here. Alternatively we could put all shadow tiles + // in one big image and attach it rather than 8 separate images. + if (leftTile) { + window->setProperty("kwin_shadow_left_tile", QVariant::fromValue(leftTile->image())); + } + if (topLeftTile) { + window->setProperty("kwin_shadow_top_left_tile", QVariant::fromValue(topLeftTile->image())); + } + if (topTile) { + window->setProperty("kwin_shadow_top_tile", QVariant::fromValue(topTile->image())); + } + if (topRightTile) { + window->setProperty("kwin_shadow_top_right_tile", QVariant::fromValue(topRightTile->image())); + } + if (rightTile) { + window->setProperty("kwin_shadow_right_tile", QVariant::fromValue(rightTile->image())); + } + if (bottomRightTile) { + window->setProperty("kwin_shadow_bottom_right_tile", QVariant::fromValue(bottomRightTile->image())); + } + if (bottomTile) { + window->setProperty("kwin_shadow_bottom_tile", QVariant::fromValue(bottomTile->image())); + } + if (bottomLeftTile) { + window->setProperty("kwin_shadow_bottom_left_tile", QVariant::fromValue(bottomLeftTile->image())); + } + window->setProperty("kwin_shadow_padding", QVariant::fromValue(padding)); + + // Notice that the enabled property must be set last. + window->setProperty("kwin_shadow_enabled", QVariant::fromValue(true)); + + return true; +} + +void WindowShadow::destroy() +{ + // Attempting to uninstall the shadow after the decorated window has been destroyed. It's doomed. + if (!window) { + return; + } + + // Remove relevant shadow properties. + window->setProperty("kwin_shadow_left_tile", {}); + window->setProperty("kwin_shadow_top_left_tile", {}); + window->setProperty("kwin_shadow_top_tile", {}); + window->setProperty("kwin_shadow_top_right_tile", {}); + window->setProperty("kwin_shadow_right_tile", {}); + window->setProperty("kwin_shadow_bottom_right_tile", {}); + window->setProperty("kwin_shadow_bottom_tile", {}); + window->setProperty("kwin_shadow_bottom_left_tile", {}); + window->setProperty("kwin_shadow_padding", {}); + window->setProperty("kwin_shadow_enabled", {}); +} + +} // namespace KWin diff --git a/plugins/windowsystem/windowshadow.h b/plugins/windowsystem/windowshadow.h new file mode 100644 index 0000000..0980760 --- /dev/null +++ b/plugins/windowsystem/windowshadow.h @@ -0,0 +1,28 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include + +namespace KWin +{ + +class WindowShadowTile final : public KWindowShadowTilePrivate +{ +public: + bool create() override; + void destroy() override; +}; + +class WindowShadow final : public KWindowShadowPrivate +{ +public: + bool create() override; + void destroy() override; +}; + +} // namespace KWin diff --git a/plugins/windowsystem/windowsystem.cpp b/plugins/windowsystem/windowsystem.cpp new file mode 100644 index 0000000..faea561 --- /dev/null +++ b/plugins/windowsystem/windowsystem.cpp @@ -0,0 +1,314 @@ +/* + SPDX-FileCopyrightText: 2019 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#include "windowsystem.h" + +#include + +#include +#include + +Q_DECLARE_METATYPE(NET::WindowType) + +namespace KWin +{ + +WindowSystem::WindowSystem() + : QObject() + , KWindowSystemPrivate() +{ +} + +void WindowSystem::activateWindow(WId win, long int time) +{ + Q_UNUSED(win) + Q_UNUSED(time) + // KWin cannot activate own windows +} + +void WindowSystem::forceActiveWindow(WId win, long int time) +{ + Q_UNUSED(win) + Q_UNUSED(time) + // KWin cannot activate own windows +} + +WId WindowSystem::activeWindow() +{ + // KWin internal should not use KWindowSystem to find active window + return 0; +} + +bool WindowSystem::allowedActionsSupported() +{ + return false; +} + +void WindowSystem::allowExternalProcessWindowActivation(int pid) +{ + Q_UNUSED(pid) +} + +bool WindowSystem::compositingActive() +{ + // wayland is always composited + return true; +} + +void WindowSystem::connectNotify(const QMetaMethod &signal) +{ + Q_UNUSED(signal) +} + +QPoint WindowSystem::constrainViewportRelativePosition(const QPoint &pos) +{ + Q_UNUSED(pos) + return QPoint(); +} + +int WindowSystem::currentDesktop() +{ + // KWin internal should not use KWindowSystem to find current desktop + return 0; +} + +void WindowSystem::demandAttention(WId win, bool set) +{ + Q_UNUSED(win) + Q_UNUSED(set) +} + +QString WindowSystem::desktopName(int desktop) +{ + Q_UNUSED(desktop) + return QString(); +} + +QPoint WindowSystem::desktopToViewport(int desktop, bool absolute) +{ + Q_UNUSED(desktop) + Q_UNUSED(absolute) + return QPoint(); +} + +#if KWINDOWSYSTEM_BUILD_DEPRECATED_SINCE(5, 0) +WId WindowSystem::groupLeader(WId window) +{ + Q_UNUSED(window) + return 0; +} +#endif + +bool WindowSystem::icccmCompliantMappingState() +{ + return false; +} + +QPixmap WindowSystem::icon(WId win, int width, int height, bool scale, int flags) +{ + Q_UNUSED(win) + Q_UNUSED(width) + Q_UNUSED(height) + Q_UNUSED(scale) + Q_UNUSED(flags) + return QPixmap(); +} + +void WindowSystem::lowerWindow(WId win) +{ + Q_UNUSED(win) +} + +bool WindowSystem::mapViewport() +{ + return false; +} + +void WindowSystem::minimizeWindow(WId win) +{ + Q_UNUSED(win) +} + +void WindowSystem::unminimizeWindow(WId win) +{ + Q_UNUSED(win) +} + +int WindowSystem::numberOfDesktops() +{ + // KWin internal should not use KWindowSystem to find number of desktops + return 1; +} + +void WindowSystem::raiseWindow(WId win) +{ + Q_UNUSED(win) +} + +QString WindowSystem::readNameProperty(WId window, long unsigned int atom) +{ + Q_UNUSED(window) + Q_UNUSED(atom) + return QString(); +} + +void WindowSystem::setBlockingCompositing(WId window, bool active) +{ + Q_UNUSED(window) + Q_UNUSED(active) +} + +void WindowSystem::setCurrentDesktop(int desktop) +{ + Q_UNUSED(desktop) + // KWin internal should not use KWindowSystem to set current desktop +} + +void WindowSystem::setDesktopName(int desktop, const QString &name) +{ + Q_UNUSED(desktop) + Q_UNUSED(name) + // KWin internal should not use KWindowSystem to set desktop name +} + +void WindowSystem::setExtendedStrut(WId win, int left_width, int left_start, int left_end, int right_width, int right_start, int right_end, int top_width, int top_start, int top_end, int bottom_width, int bottom_start, int bottom_end) +{ + Q_UNUSED(win) + Q_UNUSED(left_width) + Q_UNUSED(left_start) + Q_UNUSED(left_end) + Q_UNUSED(right_width) + Q_UNUSED(right_start) + Q_UNUSED(right_end) + Q_UNUSED(top_width) + Q_UNUSED(top_start) + Q_UNUSED(top_end) + Q_UNUSED(bottom_width) + Q_UNUSED(bottom_start) + Q_UNUSED(bottom_end) +} + +void WindowSystem::setStrut(WId win, int left, int right, int top, int bottom) +{ + Q_UNUSED(win) + Q_UNUSED(left) + Q_UNUSED(right) + Q_UNUSED(top) + Q_UNUSED(bottom) +} + +void WindowSystem::setIcons(WId win, const QPixmap &icon, const QPixmap &miniIcon) +{ + Q_UNUSED(win) + Q_UNUSED(icon) + Q_UNUSED(miniIcon) +} + +void WindowSystem::setOnActivities(WId win, const QStringList &activities) +{ + Q_UNUSED(win) + Q_UNUSED(activities) +} + +void WindowSystem::setOnAllDesktops(WId win, bool b) +{ + Q_UNUSED(win) + Q_UNUSED(b) +} + +void WindowSystem::setOnDesktop(WId win, int desktop) +{ + Q_UNUSED(win) + Q_UNUSED(desktop) +} + +void WindowSystem::setShowingDesktop(bool showing) +{ + Q_UNUSED(showing) + // KWin should not use KWindowSystem to set showing desktop state +} + +void WindowSystem::clearState(WId win, NET::States state) +{ + // KWin's windows don't support state + Q_UNUSED(win) + Q_UNUSED(state) +} + +void WindowSystem::setState(WId win, NET::States state) +{ + // KWin's windows don't support state + Q_UNUSED(win) + Q_UNUSED(state) +} + +void WindowSystem::setType(WId win, NET::WindowType windowType) +{ + const auto windows = qApp->allWindows(); + auto it = std::find_if(windows.begin(), windows.end(), [win] (QWindow *w) { return w->winId() == win; }); + if (it == windows.end()) { + return; + } + + (*it)->setProperty("kwin_windowType", QVariant::fromValue(windowType)); +} + +void WindowSystem::setUserTime(WId win, long int time) +{ + Q_UNUSED(win) + Q_UNUSED(time) +} + +bool WindowSystem::showingDesktop() +{ + // KWin should not use KWindowSystem for showing desktop state + return false; +} + +QList< WId > WindowSystem::stackingOrder() +{ + // KWin should not use KWindowSystem for stacking order + return {}; +} + +#if KWINDOWSYSTEM_BUILD_DEPRECATED_SINCE(5, 0) +WId WindowSystem::transientFor(WId window) +{ + Q_UNUSED(window) + return 0; +} +#endif + +int WindowSystem::viewportToDesktop(const QPoint &pos) +{ + Q_UNUSED(pos) + return 0; +} + +int WindowSystem::viewportWindowToDesktop(const QRect &r) +{ + Q_UNUSED(r) + return 0; +} + +QList< WId > WindowSystem::windows() +{ + return {}; +} + +QRect WindowSystem::workArea(const QList< WId > &excludes, int desktop) +{ + Q_UNUSED(excludes) + Q_UNUSED(desktop) + return {}; +} + +QRect WindowSystem::workArea(int desktop) +{ + Q_UNUSED(desktop) + return {}; +} + +} diff --git a/plugins/windowsystem/windowsystem.h b/plugins/windowsystem/windowsystem.h new file mode 100644 index 0000000..3aa7b8a --- /dev/null +++ b/plugins/windowsystem/windowsystem.h @@ -0,0 +1,71 @@ +/* + SPDX-FileCopyrightText: 2019 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#pragma once + +#include + +#include + +namespace KWin +{ + +class WindowSystem : public QObject, public KWindowSystemPrivate +{ + Q_OBJECT +public: + WindowSystem(); + QList windows() override; + QList stackingOrder() override; + WId activeWindow() override; + void activateWindow(WId win, long time) override; + void forceActiveWindow(WId win, long time) override; + void demandAttention(WId win, bool set) override; + bool compositingActive() override; + int currentDesktop() override; + int numberOfDesktops() override; + void setCurrentDesktop(int desktop) override; + void setOnAllDesktops(WId win, bool b) override; + void setOnDesktop(WId win, int desktop) override; + void setOnActivities(WId win, const QStringList &activities) override; +#if KWINDOWSYSTEM_BUILD_DEPRECATED_SINCE(5, 0) + WId transientFor(WId window) override; + WId groupLeader(WId window) override; +#endif + QPixmap icon(WId win, int width, int height, bool scale, int flags) override; + void setIcons(WId win, const QPixmap &icon, const QPixmap &miniIcon) override; + void setType(WId win, NET::WindowType windowType) override; + void setState(WId win, NET::States state) override; + void clearState(WId win, NET::States state) override; + void minimizeWindow(WId win) override; + void unminimizeWindow(WId win) override; + void raiseWindow(WId win) override; + void lowerWindow(WId win) override; + bool icccmCompliantMappingState() override; + QRect workArea(int desktop) override; + QRect workArea(const QList &excludes, int desktop) override; + QString desktopName(int desktop) override; + void setDesktopName(int desktop, const QString &name) override; + bool showingDesktop() override; + void setShowingDesktop(bool showing) override; + void setUserTime(WId win, long time) override; + void setExtendedStrut(WId win, int left_width, int left_start, int left_end, + int right_width, int right_start, int right_end, int top_width, int top_start, int top_end, + int bottom_width, int bottom_start, int bottom_end) override; + void setStrut(WId win, int left, int right, int top, int bottom) override; + bool allowedActionsSupported() override; + QString readNameProperty(WId window, unsigned long atom) override; + void allowExternalProcessWindowActivation(int pid) override; + void setBlockingCompositing(WId window, bool active) override; + bool mapViewport() override; + int viewportToDesktop(const QPoint &pos) override; + int viewportWindowToDesktop(const QRect &r) override; + QPoint desktopToViewport(int desktop, bool absolute) override; + QPoint constrainViewportRelativePosition(const QPoint &pos) override; + + void connectNotify(const QMetaMethod &signal) override; +}; + +} diff --git a/po/af/kcm_kwindecoration.po b/po/af/kcm_kwindecoration.po new file mode 100644 index 0000000..89ce29a --- /dev/null +++ b/po/af/kcm_kwindecoration.po @@ -0,0 +1,227 @@ +# UTF-8 test:äëïöü +# Copyright (C) 2001 Free Software Foundation, Inc. +# Juanita Franz , 2005. +msgid "" +msgstr "" +"Project-Id-Version: kcmkwindecoration stable\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-11-17 08:20+0100\n" +"PO-Revision-Date: 2005-06-18 11:28+0200\n" +"Last-Translator: JUANITA FRANZ \n" +"Language-Team: AFRIKAANS \n" +"Language: af\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 "WEB-Vertaler (http://kde.af.org.za),Juanita Franz" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "frix@expertron.co.za,juanita.franz@vr-web.de" + +#: declarative-plugin/buttonsmodel.cpp:54 +#, kde-format +msgid "Menu" +msgstr "Kieslys" + +#: declarative-plugin/buttonsmodel.cpp:56 +#, kde-format +msgid "Application menu" +msgstr "" + +#: declarative-plugin/buttonsmodel.cpp:58 +#, fuzzy, kde-format +#| msgid "On All Desktops" +msgid "On all desktops" +msgstr "Op alle werkskerms" + +#: declarative-plugin/buttonsmodel.cpp:60 +#, kde-format +msgid "Minimize" +msgstr "Minimeer" + +#: declarative-plugin/buttonsmodel.cpp:62 +#, kde-format +msgid "Maximize" +msgstr "Maksimeer" + +#: declarative-plugin/buttonsmodel.cpp:64 +#, kde-format +msgid "Close" +msgstr "" + +#: declarative-plugin/buttonsmodel.cpp:66 +#, kde-format +msgid "Context help" +msgstr "" + +#: declarative-plugin/buttonsmodel.cpp:68 +#, kde-format +msgid "Shade" +msgstr "Verskadu" + +#: declarative-plugin/buttonsmodel.cpp:70 +#, fuzzy, kde-format +#| msgid "Keep Below Others" +msgid "Keep below" +msgstr "Hou Onder ander" + +#: declarative-plugin/buttonsmodel.cpp:72 +#, fuzzy, kde-format +#| msgid "Keep Above Others" +msgid "Keep above" +msgstr "Hou Bo ander" + +#: kcm.cpp:50 +#, fuzzy, kde-format +#| msgid "&Window Decoration" +msgid "Window Decorations" +msgstr "Venster Versiering" + +#: kcm.cpp:54 +#, kde-format +msgid "Valerio Pilo" +msgstr "" + +#: kcm.cpp:55 +#, kde-format +msgid "Author" +msgstr "" + +#: kcm.cpp:104 +#, fuzzy, kde-format +#| msgid "&Window Decoration" +msgid "Download New Window Decorations" +msgstr "Venster Versiering" + +#: package/contents/ui/Buttons.qml:73 +#, kde-format +msgid "Titlebar" +msgstr "" + +#: package/contents/ui/Buttons.qml:214 +#, kde-format +msgid "Drop button here to remove it" +msgstr "" + +#: package/contents/ui/Buttons.qml:232 +#, kde-format +msgid "Drag buttons between here and the titlebar" +msgstr "" + +#: package/contents/ui/main.qml:15 +#, kde-format +msgid "This module lets you configure the window decorations." +msgstr "" + +#: package/contents/ui/main.qml:49 +#, kde-format +msgctxt "tab label" +msgid "Theme" +msgstr "" + +#: package/contents/ui/main.qml:53 +#, fuzzy, kde-format +#| msgid "Buttons" +msgctxt "tab label" +msgid "Titlebar Buttons" +msgstr "Knoppies" + +#: package/contents/ui/main.qml:78 +#, kde-format +msgctxt "checkbox label" +msgid "Use theme's default window border size" +msgstr "" + +#: package/contents/ui/main.qml:109 +#, fuzzy, kde-format +#| msgid "&Window Decoration" +msgctxt "button text" +msgid "Get New Window Decorations..." +msgstr "Venster Versiering" + +#: package/contents/ui/main.qml:126 +#, kde-format +msgctxt "checkbox label" +msgid "Close windows by double clicking the menu button" +msgstr "" + +#: package/contents/ui/main.qml:139 +#, kde-format +msgctxt "popup tip" +msgid "" +"Close by double clicking: Keep the window's Menu button pressed until it " +"appears." +msgstr "" + +#: package/contents/ui/main.qml:146 +#, fuzzy, kde-format +#| msgid "&Show window button tooltips" +msgctxt "checkbox label" +msgid "Show titlebar button tooltips" +msgstr "Vertoon venster knoppie sleutel-leidraad" + +#: package/contents/ui/Themes.qml:89 +#, kde-format +msgid "Edit %1 Theme" +msgstr "" + +#: utils.cpp:26 +#, fuzzy, kde-format +#| msgid "B&order size:" +msgid "No Borders" +msgstr "Rant grootte:" + +#: utils.cpp:27 +#, fuzzy, kde-format +#| msgid "B&order size:" +msgid "No Side Borders" +msgstr "Rant grootte:" + +#: utils.cpp:28 +#, fuzzy, kde-format +#| msgid "Tiny" +msgid "Tiny" +msgstr "Klein" + +#: utils.cpp:29 +#, fuzzy, kde-format +#| msgid "Normal" +msgid "Normal" +msgstr "Normaal" + +#: utils.cpp:30 +#, fuzzy, kde-format +#| msgid "Large" +msgid "Large" +msgstr "Groot" + +#: utils.cpp:31 +#, fuzzy, kde-format +#| msgid "Very Large" +msgid "Very Large" +msgstr "Baie Groot" + +#: utils.cpp:32 +#, fuzzy, kde-format +#| msgid "Huge" +msgid "Huge" +msgstr "Groot" + +#: utils.cpp:33 +#, fuzzy, kde-format +#| msgid "Very Huge" +msgid "Very Huge" +msgstr "Baie groot" + +#: utils.cpp:34 +#, fuzzy, kde-format +#| msgid "Oversized" +msgid "Oversized" +msgstr "Oorgrote" \ No newline at end of file diff --git a/po/af/kcm_kwinrules.po b/po/af/kcm_kwinrules.po new file mode 100644 index 0000000..baf10ce --- /dev/null +++ b/po/af/kcm_kwinrules.po @@ -0,0 +1,875 @@ +# UTF-8 test:äëïöü +msgid "" +msgstr "" +"Project-Id-Version: kcmkwinrules stable\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-11-03 08:14+0100\n" +"PO-Revision-Date: 2005-11-26 16:33+0200\n" +"Last-Translator: Ilze Thirion \n" +"Language-Team: AFRIKAANS \n" +"Language: af\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 "Frikkie Thirion" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "frix@expertron.co.za" + +#: kcmrules.cpp:28 +#, fuzzy, kde-format +#| msgid "Window &role:" +msgid "Window Rules" +msgstr "Venster rol:" + +#: kcmrules.cpp:32 +#, kde-format +msgid "Ismael Asensio" +msgstr "" + +#: kcmrules.cpp:33 +#, kde-format +msgid "Author" +msgstr "" + +#: kcmrules.cpp:37 +#, fuzzy, kde-format +#| msgid "" +#| "

Window-specific Settings

Here you can customize window settings " +#| "specifically only for some windows.

Please note that this " +#| "configuration will not take effect if you do not use KWin as your window " +#| "manager. If you do use a different window manager, please refer to its " +#| "documentation for how to customize window behavior." +msgid "" +"

Window-specific Settings

Here you can customize window settings " +"specifically only for some windows.

Please note that this " +"configuration will not take effect if you do not use KWin as your window " +"manager. If you do use a different window manager, please refer to its " +"documentation for how to customize window behavior.

" +msgstr "" +"Venster-spesifieke Opset

zDA2QkY1u@K)#@eR5gHjk#khf7w9@Uafs5${q)@o|CR)yHNK`vt{!M=z zZEmAzkZ)eE?%-k2+MlParX-Ik?{HUbGa}`$xa^&8Cf(QJ3`wCE%0TrdbD!GE6_j zGP)3&pOw_ny!}cAsO+jzIz{rqFKhkrCm9MJcH?~RmEXqB7J3r+d?Ar^OI{8(d% zN!tx22NsQ@N7{NECJYM=g9wciwRkcoP#!CDR3Q$QR_~|w#92w-#{9%nIn)NNQCq#8 zhE#Tf*6h@Mb%Rry%X0n2h?Sd8nBsau){jOkfB(^_O3b!!##YRJ%ZIStGDMt=>cM!# zeKVYi2B-f65@;%HpHL!>t!9@Qti|R33w)u~kd9iU-sRwy^0=Of4FRW`DdA~^gaZJ% z+~|*HF)``mxqfd{9LG7|o=JV&41oO2T>zHouMXoF1o@hybPY#tWK8UE4E> z07@AiOa+z*YGt7kEdye6iufHq&|8B}yZ<0&Mt_d2N4pV&Rmz#IB^%#&(u`4F8cJu^ zA#~KgZu3-*4?6v0Bu^?*>~C=S8rcb|6cg0F`b=0&{{h|@Fa${aGI zfs*~vy&JW%lT~2wYQrZ?3(P4rxx~>lQWu-a}${j59cXmz(CxP&>l|sE=Kl|$8jTM&|VpL2+$)0bcWvG?j zkkaZ>_O#ncLRdaPKhdkmSmF*AF)jEfmCiKLQt?~g+K%ez_pxp${y|C!F}5bN?$+L~ zVKniBNv0utEO97_kkt}4$Xf77{hiU<4m~-u?LGX-boQY1vx9FD2wfi*-u)SvILtIL zx^cW;pt5H6_QhAEw0U?3$IG#}UBnAPn^^<^_K>Rt~HTs4E(Zv12Cx z^BqBLygnbje^<0TBHahZnb;azWh5<&qQ4Evevft&?N!c*aI@f?VH zft-@i9JEdjwgjPD#UfS3QtURJ5@&90hL_qhzbI60`7>;KV7|t5gmp2lrLS?UcFhUH zYlZ^7vKBE(?XL*AK_t;mE5C7tb~|+t(IwDK0f%SK(4&X>VES253~RH$HDEM)&{|XC z{_7qoWXGbMc%Y}IsFCx{LXB-)EqF90O^%CgS$eeBwLk3&QbBg{m7Ljy=7#lLCylT8 zNR}(6TP?)KRZz=^>c4vfAk)Y9@7+FO?hp`;7?G~*lORq!cMEsb)(9mGE9$`1sn8B( z&@3?^)7=>DGvZ@JyL_PvmxLsbch62ZK;>Y>*jA)nmSTPKzd2de_WmwGm<3}#gyzJD zWtK_EU9b0U;P(g@-ORdgzXXONm9V16wO@Dcbb1@lh~oQQOn%=$R#>bhE=?_v7}5=$*_Ll_%Di$h14fi{cOWAYht!q-}L?VSToxgj9omGXOXxCH};LafM}? zw<5}k|NhL+nar>1STN4(o!CLB%g12-nUDnO#Sd;WYxU*rgFp0tkxbrT2*OK6DHeZL zF0f_VldrYcdMBHM%#g?bP7O(mP6*OdlQ-j5^yBQ$x}-Y#b*m0e{hfz{Xq6Z`R{G!# zg)zW>zY5Ge`Nit2&JjgBHS%<>pVu517`KJm%6P(#KO-AKJgBhG$u_U|0sB~U1>+wC zhs*jyNyKDg**E04LQmg7+`jYa2q}QL_#qi9XIBJ?FVw>)q377xyThGe2!iQkHbT3T z3I6mloaV~vvqj&sPCDBLyv$``s0kX&QR0nkb!~oYis8STlQRxJtQIL3Z7RpLvMO#m z^r*5i6>t8whQ$F_$T8EgDVn*ssI8qH8hZP<7=kAoa9@UM8F=lINRCRiPeIGIxGUR` z_1t#H9Kz!2FA^Z=@_@UX1lM#jss38QR~wyWPH&6+*W!p7sE_yJIarQ46HSA;s{C1` z{F$qK)Z~6Xs+`00>W`Z@M3Ve^ZDbrY>*N13ct+6-=kPKwGBoMLh=hmGy^=jQ^eoLH zeXJfAGCOm}!d9^f)<`~qHSxE9W=!21!HLDrIZ#3fYz@&gv ziRC!ySdG6nCe~M7ZupT;W#2*>om#pa*LJ-BqfZQP(Zsgq@9$Dhl*e1Mv8yiY#LU?S zyam-|CW5yR3biX?HmkaCKzW&&pkjuJp}IR>Ez`f56rv7Sb+Ri_Ea0LoyP}W=aAY6H zoV;p(qWy&8!pSqgi4KAJ+@z8ehrxe7kj?lY=BJ-bnZn#n`XfUm~$5eh0=tog;tt{L2 z!SDTF6pJCLD=`xD6=%jn)wjJil zIR3B6{99L3*RLu_|0%{Bk#8hsYxM3Jw|j)<#*qS^nff7B%X@*n6gebAP|HkGQjs!e#Jr$3Qf(=2@m1ZHL>%kZ-ru*W?!#GRFy7 zt<^+ER9JCoT-}Z9GA*Vg(Fw7>EGxH}7d;N|D*n1sAn;>S3RDlAVkppc+2a3Pc;n?R zg}T1_ZCog}D6aUzZRC$PV()mjF_;O=12Yc{cSfG*g`x_(b!pFh& zIrelCjs2A(1hy@OV!12MDE|Y;%{35i-%0=6r>h7XVfzQ)+l??Y<5H{?>u;Y4Dsy|CE5s0B;!QHK4!X7GGlYyVjRO8I{XlDsJfODCk*;qPXZ3_L*OfE31!s;O{Ji8-T>HX8oi{*5q_;i=3ODdsou$EiYk0 zb4})le!I!8T`X^XAU1D6B;jN*^Cav$C|jL-yH&(8(oSZp6Z(@RN4*vB)7shW0Bayq zv#TzG#2kKWt4lH4Qd$<@|Cir#*r04xtde5iK%Vqt4i(W`at51=RBHS|AeJyy`KgYM zg%*Z{nWgr|TIO73<7i?!-1UqF?^+D74qxp-Q8+r~+?S>AEan{tnk7M2`s%~X`tEtN zr$YlCl@(I>#Si*2yB=$^OBBGxnl6A>Aj&fP2g_R2UaQZiZ`qsRiyQ(*JrzK2YnaX9 zc+!D~BD=S(K|lS7&O+@_E;<&t+n{r0)mZ+Mh7ju}^ZV`uiztOalauA+8g4cepo)4F z5>C!fspuVyx>#ieYeY~}KB8x5CfJqsV!ve@veEd4RrEe`IfPrWjED-C<>6}RD3#O* z=i+&1Y%D40W0II$3Jb4DnGRaFv!&rXBv3B zKT8)A7W%oG7yk#@V2PS&Qpjcb2S#6AAVWw#!P%i$x99pzcxL6l}xZ zCMkXJ{Wq#l2`2=XD<|!}A@l7hd?n%bWyjZoMK8_@&nvDj0N@%EaM6*$* z1;PBXM&u|r(*4r*&l3bKd(_qP_wbaCwK)ASeoa*5C&ooyEbK(xpAzuhR+3~;tD}sz zzwbV&*u3DEXpEIRal7r}8v+FmgFkCo{C1w>3*}jnXLla|azAXC!^J>eD*$qg1-)g* zmFG+)e1;=|hg*$K?~9gnn8$G`YF=QPZ>0)5Au-g^CM~Xox9?LtKDNdvvucGUA5MAZ z*r%h8$9_4AWouF5y9e*Prr$XZvBG4S|Dg&8g&m(TivvMwzNY-+5gXZ9;)L{2-)d4q z=-@pzDEz4Wb0@w^i|&cn8w=dkAiR2NfN5x$UIvpB#bL+M0f0S<6ZB)p1=4Pe53)j_=>16BXPd#$5XQu%ti@^ z2G(l$W8FR6+(zd^Z>dWaK4Yrq+1#Rt3k~9eT!jz}HKJJlWX zk-*EJ#P>Jw*phHgT)`5Pz-ugdKBeoA1(Qoo_4RhKb|2D;`V(+9H5@9;!=P}vRrWYP zSEk!<=mn#f4b9Pwa*9dGF2oc21p58`Z$2Lf|FTX^(t?DW1hK_@LEJorXUA zbHMmHGY%)>%nt=JKO~{>`P0PM;Ly!=YSRuJChPW7*d_Spm0&lfy4(Zs40wEeNQSPU`s_8Hh!Gf7UCj@;_IKw;*HR}pLhRXci$P*WYjboq=^Wq zfFNBF5ot8t?i1Aa z-uY&}`{Vm_XYMmYCY+qIXV31Q-92ab`B8ViHHlQ=mvR2I%_~Fb z1c=&L5@r*WiXC79S!b)5?hStoJ)coOca#ZOOe7+W2?g1=_$q%V%?k4*tqpSe8J&K` z>Svj3cj{0;hlGZR20G^F3d{h+6Azd)flk+~C=mebwBHpeBX#c5eQsxbZtGEwEv^~t z9RzM-@yXvjns~GbBT_w^u(L8IudiHP+B*3vD1v>r@2MgGf)^LOS6yar?#?Nx5O3h> z$cNoxpGGIrDEZoTgQd2r5O8t|%z|-Tw#mQ>EC?Y=pqmH-7IEDyVl-4S6arDCF`J{o z-;xaJbJZ&gdXm5HpWp*%cBWJ41F49bPC#Zz0|T5s)^5jz<^cCVG>{U|z389JChc=a z5kUM{!G$6*tR>xXAS&a2pP3=D)ri_wB@m5uHH+)6z8Il|_<9Q;UmNg1CA`!9>b?>^ z`3Uf)ZvhHG6fg@jk}J~L{CezR*If?zQG%hkrrTE*gtByYH@;H*zP$_gk4NISf!>zv zfcLqQ0kbLq6A$RY0!Gc&qh!t2A4Y*jgJ>EH&)u;I)>P?*e%uYt7P$i)DdY}BKdwDq z!m!cn&3ewVddd}fA|<5H;mrO^_CbO`u-L~ZNvoePXTPS3pv>t`RQxp9dosG@7+^!=Vkd_Zq|q@kWG6>ESpg}l~U4TB9$&Q6Nw z_PFE0A-mKKWvR_pXe zF@S#GJWO8~mr(jqS(keH! zl|K{fNqKITE+3nsCAMxxtbSd4IA0&~Qjnbqv{uxT7^KiYV3#ZS@b>coF?VZTL!~sH z>m|o%uOe-sT-#43SnIx(5cm$%o{v~ zR_yR&C|fE8cR94#7SiB-j}3Ry-t`TcDS`HHd`?Cl$;;M=wTV7D=bWpko0*%9a&KTCoiv#yyA3F@B(`uQO=4Bgq|H#;Km|c6|ES}61-RUlG z7na^$9Npag=868PeNyUYL}*j#D9cri8U2G% zGk!7#aovW|8&dA<%rQ^N6B~xn9jqRNs0*+&@$qhLpsKFy<94OC_vaffkHm_J(vh+c z&4iEJec9iW+iQ3oVm8M^{nc}%E4%h64b?f52jMGDd$_z%+j|b@q?Lv0@|}HCs}@zO z2-Z2OZ<%Ul1CGy58a65rD6A2Zn;GhD*Fn3vVZsNyq|ls**`?r%4qB!iW*m#TOP1yA zO_VMAMJNpPGkN)-q$?dAYE4?f=3I;W!pJCdPiX3BTMup6=hPd+y}!N|`LoI5)_`MW z6Hdu25F`BsiJ074kFs25sLpS;g6UACV~zlPG)BWmb zvLfvVaO*$*Y0V=SlJ$>6=AkdKIosawrmKd6(IhF`7|e9A!=lH=>kytXmonUod0B*N z^C=J?Q~Kyg4|ranzfRC1oNop8VQ*y#b^uF`oqEkQ8!Aa7w>K9N8Z{Rht>Ay2^I-Oe zj`p&&mT~@IV$QY~ywgZqfL|eUd98M7JBsb58g3gM^OXt$ym~2pO(R%VG+R&o=N4}; zTN8ei0SGDMa$xd=@;PpKpgE(qyo_HLTi$p5-e$M4Du>*=i3zFUVpy5s>AMm2L}ZNQ zq*dBFG(ruKI|8*7y-7;k5s+cy*e^#C76gk!{i~mS=Z^tilj;rc4WLYyX^&T3iT3$W zKD;p6tm`mygK_eV&LxoF>|jPzk(^N0uV#6U{)ztJwD>;p`y2h?YL_4e>z4zqp5+FJ zYdCDWy$R+T91JO{m5j|HN8ux$`Sg}SCr#A z;$vN8$9oUh0I~2|R7RsFKFTFyE*#(*3Xl#$U64C)E&;+LNx_8*6-<}!8CVW~Lt3*^ zcytQFCgT``fp*?oNYy*Hcn_(>^n-Jg1cOuO)*v1yxj=2z;3C^*Bt1dfZ2)8{nb46 z(+mc1f1SKiKJVR4=~tPoF=uP}17T^ov|@J`t7n7`V`Y(hRXF!^OpYLKGUi6f5|*u9P@vxcH9L|>CUxQpqtkXJ}G!3RO|9Uu@FyD*S90f=Zl`0*R1PO<_E;SBUk`;i!x16$R?#aA1k z@dP*;;M%>}S%?)4j;rgXSS6-&;Z3vNa0=kb2G-fHK9MxhOPMYiTYR*jh?Z7o!bXt7 zR!TU2O@^MBlg0Z%KkY5ESTr+i&FbzVIl*O@`fjnscajC`W|M%b`PfDeDz5Kn!L~{3 zop9tKPNA=TOdb33hVqn~K)PagzulGnR0X`=H>n=?e%U6HO5+2~rCw8_Vh@zREPEiOT-j!gr-B_Ow!a-E+^BtX^>-|`%aZ@#H@Yn7 z^NRyxGE1a+eLZ8SBq#hG#cbSV1iR-~sI)?V;?lT8%a7M_D5fnR_{!_r`%~dc9=H9T z_viB5fpN~CI3BPIV&OkIifUr8Br17?mE)VVkhF#`a6VOM#W`nFOKGZbwsFNm{9x+k z4QP~~lYZV38y?7#3fz|R43cUbKnsW*K%;0V;=BnQ;EF5w;+u`jchtPKBQBfE;`^Hg z_vC`!3Pu+lwmiUQ_YY*;h259d3j_o_q*Ep!2AP=_|F8>TeAPqgWS+xEe|JyRkvW^A zIejs&b#!el>GMWLrz~M0?@+9H_o8^ASh&}QB-rwZ73lCRcPHwn^oLyjuy4G64MxO)V$@b|q||X(x^)e(_w3Ck zAn1fP4cPCz6Se}^2@x_Y8w*ZeV20OTGTdvpZa9B$T^jZY!&CS{g$HB*5x3csaMN8~ z7zEt53kbAEAT0p+-Q!*hZDyA1dX@GsjqrDjvp5Dt+USEMico_+jWY^?kJECe0?qwI zic~^;z0FbL)#~3)0dliN3v5RGT6#{-<*IY;1K;6GFh~7Dw#tT7bBD70!~qsYb0v$t z!Rv;0JU<+#b&_>hsfAcIC;a8m&B6=cPMra${Fy%y2;Bd37XV~DPXLswKo1D>HI{0e z0fx28vSaAn60t_TJk9KL@V6}VKI@Y?}-#ouI#_g9ouO*FE!*+2DWYi|oh(K(U# zC&n$Y@UCZNL1j^4f`H)QbG-6_DI=x(b$v?tiM7iTcwqOi0eYzf6NJkR7~1XVbtg6S z0_5c!%W`2za3sfM0rbl!&>E_uaS=ZxraFmb85SJWIot6rD>SoQCF7c)Rf|VJ!QeW?fAdxj%Mty4(XQmcx z`%&s~&;srSuu;nMXUD5cAgLwLH*CtWt@d7@drIXku?`;!Y%q}fS3S^3e;GL+6ejL$ z#<*llf7YW=BqYbFNMdhrMo2pLPe0>ZP`|xl+HwniVWPbUHW4ud$QsO4W?(T;x&J)%+mv zFB~M^peRcgEiI&u?{5Ew%ah6vY3AE6^3(3&4Q}a)Jur)0Sh}{}P|DaM5gY509*?*1 z={S_NdI9wd6 zw=c)hGtjdTRP=J&UFuUbCIrm2%d!vY95ir-?=lPNcOS=lb;~}5?oBY!_#eqI7Snj7 z53!wYDAB)%(f2CV*GKku(qM*dF*Twq7@CR-lm7XZo)NGD>|rbt86e9sq_;TG*7It- zDC(+rUsW#hVq?^s6Jf=B5S|Zlhq%jq9c`L`Qj(|2ej;n>V1!*eH|obWQfjp1dO`< z?LqM+=s`GY8pYq0__QmVSt(|oOLJbEv^7gGqUZP}b{xHuXXP)0^LF6N)RfjaqfmJe z{S;agho79L43>xZ7=a=o|ZWzCy20UDobtS;7A^G)geN%dvwom^{g zKhkW>R7(|5-k%Iou(xbIzVcJj z@xAu0aMe79AfTNCZ`2byu+hxRVbP|9M}-QGxjCq)J!FF1|GAfOR5QX(!zWp*@O_}W zw)#TP?xHEyRxFydWvB}Na3g8JQo5iH^N8-mRlbRG6n3r?>_c}Cz^TtK3u-vk0NTL6qSYs z%5K!grKwZQRu5`$3R@mLF-E1>a8GRhJPZO1O2EZEm0{)qvhHTe&?eUE)B%1v5;_IdPE zJ&%)3&BQNe!pXZ#GukCQ(Y~BRsYU-R*dgX(`8|WKq_Y6^yI*`a|F=qBr#7HWdl4+iy8L$$q*9HWj4d8L54MOq<- z0A{W%qJC#~Si|Dc8HqCxMoVf?jPh1VpQMe~=GExAJ2E0a7z~aM984xmIbe2+>xZTo zPAtzL7p8XD-rm)a(qHbkVu+K-ZvlO2mLZ?n6kY+%GG=Mty*{!qXi z{=fkWdomPd)V1~m7$Mw<0tA01FL347ZVlAfDNiIUu!U8epj$8tYq_dlGjB{(f3E;G>Jw2W(d^p-9icm$3TEq*i2g4OWOy(c=@=i6ouo2%@BeYG`S zsOBMbmuzC?j;Hi1tY}iwRKr(C#;%&3D-ap02~NHQ#ku8S?f^RRlTW#b zdJ#lKsl(dh(L$<5@aQ?rmCmTTf(6y-NmzRHHdoF3ot9SLMY+3 zzgzAD=d|?TE_c9BBHuqi+q55w@O{^G6EiZOvvnr&2vGk(YhrGw5o0<~`Pxo;`K3PB zn@tYw(|!rU#976tA_c-gz{E^`uy*aK6SNrV1cQ~Rt;+t`j3OzAwvgfP6P2P1p1D*U zJ<)9Ehm#K`WW}+@KUj_dX1udZPhmu-^Hw*OgrCz;vN#WOzT*P+7G#mdIaOnNqqe2r zXWq#KO`7+W8P(K^5+(>x|MJdjY;8Fun$U^V=_P*XzSf>uuZq~=T52-k?i+HN`Y&JU zxrnr(AnHaVnBI$?wd7F$ywR)4yq^NW#2H}qe4PU7A#YAhu37T0d6i28?fe048MNQ(rGqZ%nZKIeB(LzI>r&M5*wz^1f!42CMQUI%0;oH)Q) zgOeukn_AY4>#*zP(}#V+9rlrJ+|VGu2=QZ#-k)5frvN-fZS_!LZ4n$Z-#Eh#Ye{3wf$6t; z45Jb66egs~j1Sy`OoEZW$S*R5-#_UF)!g?6y+My=N`Y(T@M9SM@`e}S^Uo=Xaj_YS z3Z1>~P~2k5WOL*wYm+W4vZu||s$-Y~Fxz^sXvzhAz!cwOBD+f&D}BeSn^!ZE36xU5 zkJU~dSBsNw7cN5XzlkXOAVmlkJ5~O+nOG-Nl#6U@3h-Yu(njuAZ4O;RaGf!Qxgz_g z@fzALueZ#~S$%fu7KvK7v^TxJ($xa*Ir#B(L%Whp3J}AwS8=P?HoT1KE<%bv#Ci8~ zj8)3?1N_LJLwhj&V1IL}v)q!*6gH(=%n9JtGk!C-fwMW+U%%JwA3+sZwbNibF+0HePL>u#WLN#{|4Uc(za^M51cB32Y5JqHjob=n~6} z)bvYks5r$av7qQ&z(`SNf+wP0LTK29R|+Ie^leGMw~ainMVRFI$=&-FTN@OSE1W~M zXx3cVk4ii%76KMg3kAtaa0geRs9ovB#ci~2Ieto|EQ(5wN zo(4`gKr-5V;nI$1vV1SO-uIhB4&hUo9U_IU!}LO{8~^y)7)SJ=(Z&Yrh>R{S6Eh|K#0`NB{foKSD@ae z*;X;=kDm6ea!yo#X?hFu!eH-5lF`w&ua>y$YZiA-ALEVSo!@1)4}Md|%t}&0RzpE? zgN!jx^g9fgicW4}7)ny#a{hP-I%8Ur!0u|S>>e*^P-$eoZ<*JX)P%EjP=Rhwsjn_i z2iwZ-ki#VqXR;7LR(N$8NgGy4XdTtB#_3W_?ZaJfm#v>+m=?rLA5hpNvJS2?!7pdbYV zT5&Kfe1w^C=~P=X#VYQ502`py!M;$)%bwBrXzAmwETq|79;>Tg(2LF`E#owZZ*OG(B40|F6z!=! z^Z3d|NOM!0hB$EbHlo2`My?9QiBXxdhz)y(Meyju=) zEltl1CmgnPxv8~l^G~6RtIsaJ(?&mR_0D8uJ*w=wx1?-PPqu(Jc`tsMxFB3p3ns9Z z>f!vU&Mo8n+Z;@pIQ!cZ0y zz!Q6#Gc|}I=xh_2?T~5SFBdm1oZt&?h^ws=iQ^|S3({-BviqC~xKa@>ZqP8SPW$VbA*+@R0~P3n12?sfGA0sj zYN+o2#IBGthc;EFFaF(p^s_y}HuBK^7wx1@4{+0E=FDW0O)QVe*=wE8{hpQJ3qBg% zQb@rw(DT0uuC@`31kvj{p#@l!W2EW`i>UoTDQ2fC5QY z!3l-8GZ(*%t79x`(+KFU9O>NQH+bHoS6AF#d|hou|v>5i6x#McQ)D{p?T z-cH*yz*NoWN@LO{6cF|G=RsK@6@;k=TpnU-z4a}}NXeW7=E-TJwCx%g`QTbyTy@W> z)+iGs7}iALy5msOKRu7f9olOrYt!}BL08x4d`o|2{*g%#s4Bv2k?wI}*^}a)Vm9>F zZR!_Fj81B$`hl4Kvo%CY9gUj?I;Wo;=>kgjo{^`|g}?xRK&02Ct-Mg(VB!OkFN0WK zt@Bn;TK~kiEZSlw#5I_LR_hTwbvh@>MP|@JB{CMp2sLt-W?f%Yt^bO!_-M z?u`|TpXf&zbt3srv1!rRTm7hEfVUA) zu1tStHetKf{YFE{oHPH0A0uP^OA`!1XYuUy^DiDV?;p9JEJRH>MQ)j;^mOF@@?Vp& z2ucb(

;(=9WxI#+>Ytao^Sz!w; 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—!CHLPY)Kzu5_h6cSyzr8@>?4}H{DvmUig}qm3A9m;#NLO z7BB3Z8TZ_LnY;<+#+79kG*dbD4uC5+QvUqKG`uqpksXE$m*aqFiMX6RsZ_c0|T&zb0z}M0RUH>1pn*TjZ?ifGIob*tZA#L z8eGItT_-iqOR_L`1iXDT*>>@};l7cPohl~5%0bx(=PmPnmKjNCJ6+{eDYp3MbLI4b zj-W6E(75Lpz6Gqg5tmr4ltf{{$Wf;rROK^lQUfFDMm-XsE5iu_K4D#KT>? zPC;qKl0_&j$wzIK3j6(hwQCfrRxMvbeb%h`Y~6&P_wCiGEulvoq!?q2>2Hu~ZBmOi z$72^ZAC1HA@MQcDng}N^e{9(k0CSi1So2dP_MEt)S@Q|Hgei_2k)C;5RNd*uMrc%5 z>CDl1hOv=$Ob>6P?jXt-O1rZPE7EX>ffdNCU8 z>`>)sgXrNyp%j8klHo{PoHUm1kZnQHrlb~RaIm-Y^`Qf z0H8Uk#0DuzHO~p5D0YCii6^+I5ZGWZgu!Z~PPX}|&88T%I`2z%)sF+DjQ@yiF-9qnv!HaZgbRB|+` z@6nZd)U{uv+d29~DmN>w+uHLS>>)4A(T+b*M=TCfj4{UaHb~J!Sh8j7ePQb6iPipb z*m^7tKF4okrQa!hYvqc>iVUC2j3#ckrATj;|jcH!HhaTQ^B^P2&Yre_) zW@<8x#S$B&B-Kn=y$bO{IEoweIqr-fhEgF!)qXVyCxs9t4-L}nQJ3UFZafK@gFi>o z$U$^Z&ppYIX?|+&QtUZYSKLKW-fisIwpqJwZ8;0^v1d_Rr4-h(u)*7ldemLNx}6I= zd=SePFVx&ysVsq??=J0HSV$mNE?tbgJ84i?tB{>}lR|CRI%08Z z=YPFS^Zl9FASJnOeq=a82Y-Q#Zw;UnLX>j!VB{JOL+LkvMbm^4XddxIT6Wtqb zKNzHF?NM9rds1DJkBh5T;+V-a93J&Gjv9_Z@N^TJqqV;3NB;;&()-RqiZRBRekhk0 zA^rMQq^2a}WydqLXoG`(YHqB>s~694?Mf1zo6i5yo^-r;Dg=N(+7lJ&Y2SeFdr#UQ zws4U8Z+myw(l`)>aXg<{@M3QS1^1>`z7!?9ve06ut)*Js#aeKEPt&w9u}BsDFh@_o z!4-wMS(MCw;Kz{Mod*IBnLq#lUr&FpE1T;p5^hu_j;`e4{;u#?!aa^IWTU#O^XlAW z^1t_wbEilF0LtNXa3}|zwuIF+`M$Uy-{$AaO6Su#bzYsDOg<-fiWC5#d}V5|zbjp{ zEsbAWWu^1!oH{R=d|vJpDF8q@oed84`<1fN`N-t!=I&mkX4BmJhZO(-z}4&~6I-Ni z^VIu500017-Dc^lB9&3300017P1CV0QqLEO_kjQa0JzG>Be6v)Ph#%_0RRASm5qjC zi_~=-c^?P>0Dvp)PPRx*qD$`s0RRASHHj|77OCX&%=nUe->lDZOsT`=9^- z0IYw@lRehEo{Xi_ZrLJb%T&u4>HqG@-~J!(g988ncqvb_X)eWao@v+;_3ru^hEl0i j0002+%8xLV;LUmhL_}E7+o+tu00000NkvXXu0mjfy~|R3 literal 0 HcmV?d00001 diff --git a/doc/windowspecific/pager-4-desktops.png b/doc/windowspecific/pager-4-desktops.png new file mode 100644 index 0000000000000000000000000000000000000000..4f2b28324fd0ac1456c2a77b4be55b1f8d57d5d3 GIT binary patch literal 11817 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�(z_~t`0O{{>vzi1Z9zjt1IFLfBmVZnI5qDc&m`| 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 literal 0 HcmV?d00001 diff --git a/doc/windowspecific/tbird-main-info.png b/doc/windowspecific/tbird-main-info.png new file mode 100644 index 0000000000000000000000000000000000000000..c151bb7b6fb9063d65174dba159542c98b897de8 GIT binary patch literal 36343 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 Ai2wiq literal 0 HcmV?d00001 diff --git a/doc/windowspecific/tbird-reminder-info.png b/doc/windowspecific/tbird-reminder-info.png new file mode 100644 index 0000000000000000000000000000000000000000..7b764cd5e6a04e44644b853e5dc70565063ad8b1 GIT binary patch literal 36154 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 literal 0 HcmV?d00001 diff --git a/doc/windowspecific/window-matching-emacs.png b/doc/windowspecific/window-matching-emacs.png new file mode 100644 index 0000000000000000000000000000000000000000..e0f1ad76c31a0b9f6256600b0079e3bbb47c0bbe GIT binary patch literal 55919 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

Hier kan jy venster instellings " +"pasmaakspesifiek net vir skeer vensters.

Neem asseblief kennis dat hierdie " +"konfigurasie nie in werking sal tree as jy nieKWIn as jou venster bestuurder " +"gebruik nie. As jy wel 'n ander venster bestuurder gebruik,verwys asseblief " +"na hierdie dokumentasie hoe om venster gedrag te pasmaak" + +#: main.cpp:91 +#, kde-format +msgid "Application settings for %1" +msgstr "Toepassings instellings vir %1" + +#: main.cpp:111 rulesmodel.cpp:216 +#, kde-format +msgid "Window settings for %1" +msgstr "Venster instellings vir %1" + +#: main.cpp:163 +#, fuzzy, kde-format +#| msgid "Edit Window-Specific Settings" +msgctxt "Window caption for the application wide rules dialog" +msgid "Edit Application-Specific Settings" +msgstr "Redigeer Venster-Spesifieke Instellings" + +#: main.cpp:197 +#, kde-format +msgid "KWin" +msgstr "KWin" + +#: main.cpp:204 +#, kde-format +msgid "KWin helper utility" +msgstr "KWin helper nutsprogramme" + +#: main.cpp:205 +#, kde-format +msgid "KWin id of the window for special window settings." +msgstr "" + +#: main.cpp:206 +#, kde-format +msgid "Whether the settings should affect all windows of the application." +msgstr "" + +#: main.cpp:215 +#, kde-format +msgid "This helper utility is not supposed to be called directly." +msgstr "" +"Hierdie helper nutsprogramme is nie veronderstel om direk geroep te word nie" + +#: optionsmodel.cpp:145 +#, kde-format +msgid "Unimportant" +msgstr "Onbelangrik" + +#: optionsmodel.cpp:146 +#, kde-format +msgid "Exact Match" +msgstr "Presiese Pasmaat" + +#: optionsmodel.cpp:147 +#, kde-format +msgid "Substring Match" +msgstr "Substring Pasmaat" + +#: optionsmodel.cpp:148 +#, kde-format +msgid "Regular Expression" +msgstr "Gewone Uitdrukking" + +#: optionsmodel.cpp:153 +#, kde-format +msgid "Do Not Affect" +msgstr "Het Geen Effek" + +#: optionsmodel.cpp:154 +#, kde-format +msgid "" +"The window property will not be affected and therefore the default handling " +"for it will be used.\n" +"Specifying this will block more generic window settings from taking effect." +msgstr "" + +#: optionsmodel.cpp:157 +#, kde-format +msgid "Apply Initially" +msgstr "Pas Aanvanklik Toe" + +#: optionsmodel.cpp:158 +#, kde-format +msgid "" +"The window property will be only set to the given value after the window is " +"created.\n" +"No further changes will be affected." +msgstr "" + +#: optionsmodel.cpp:161 +#, kde-format +msgid "Remember" +msgstr "Herroep" + +#: optionsmodel.cpp:162 +#, kde-format +msgid "" +"The value of the window property will be remembered and, every time the " +"window is created, the last remembered value will be applied." +msgstr "" + +#: optionsmodel.cpp:165 +#, kde-format +msgid "Force" +msgstr "Forseer" + +#: optionsmodel.cpp:166 +#, kde-format +msgid "The window property will be always forced to the given value." +msgstr "" + +#: optionsmodel.cpp:168 +#, kde-format +msgid "Apply Now" +msgstr "Pas Nou Toe" + +#: optionsmodel.cpp:169 +#, kde-format +msgid "" +"The window property will be set to the given value immediately and will not " +"be affected later\n" +"(this action will be deleted afterwards)." +msgstr "" + +#: optionsmodel.cpp:172 +#, kde-format +msgid "Force Temporarily" +msgstr "Forseer Tydelik" + +#: optionsmodel.cpp:173 +#, kde-format +msgid "" +"The window property will be forced to the given value until it is hidden\n" +"(this action will be deleted after the window is hidden)." +msgstr "" + +#: package/contents/ui/FileDialogLoader.qml:14 +#, kde-format +msgid "Select File" +msgstr "" + +#: package/contents/ui/FileDialogLoader.qml:26 +#, kde-format +msgid "KWin Rules (*.kwinrule)" +msgstr "" + +#: package/contents/ui/OptionsComboBox.qml:32 +#, kde-format +msgid "None selected" +msgstr "" + +#: package/contents/ui/OptionsComboBox.qml:37 +#, kde-format +msgid "All selected" +msgstr "" + +#: package/contents/ui/OptionsComboBox.qml:39 +#, kde-format +msgid "%1 selected" +msgid_plural "%1 selected" +msgstr[0] "" +msgstr[1] "" + +#: package/contents/ui/RulesEditor.qml:48 +#: package/contents/ui/RulesEditor.qml:67 +#, kde-format +msgid "Add Properties..." +msgstr "" + +#: package/contents/ui/RulesEditor.qml:67 +#, fuzzy, kde-format +#| msgid "&Closeable" +msgid "Close" +msgstr "Toemaakbaar" + +#: package/contents/ui/RulesEditor.qml:80 +#, fuzzy, kde-format +#| msgid "Detect Window Properties" +msgid "Detect Window Properties" +msgstr "Ondersoek Venster eienskappe" + +#: package/contents/ui/RulesEditor.qml:93 +#, kde-format +msgid "Instantly" +msgstr "" + +#: package/contents/ui/RulesEditor.qml:94 +#, kde-format +msgid "After %1 second" +msgid_plural "After %1 seconds" +msgstr[0] "" +msgstr[1] "" + +#: package/contents/ui/RulesEditor.qml:113 +#, fuzzy, kde-format +#| msgid "Detect Window Properties" +msgid "Select properties" +msgstr "Ondersoek Venster eienskappe" + +#: package/contents/ui/RulesEditor.qml:205 +#: package/contents/ui/ValueEditor.qml:53 +#, kde-format +msgid "Yes" +msgstr "" + +#: package/contents/ui/RulesEditor.qml:205 +#: package/contents/ui/ValueEditor.qml:59 +#, fuzzy, kde-format +#| msgid "None" +msgid "No" +msgstr "Geen" + +#: package/contents/ui/RulesEditor.qml:207 +#: package/contents/ui/ValueEditor.qml:127 +#: package/contents/ui/ValueEditor.qml:134 +#, kde-format +msgid "%1 %" +msgstr "" + +#: package/contents/ui/RulesEditor.qml:209 +#, kde-format +msgctxt "Coordinates (x, y)" +msgid "(%1, %2)" +msgstr "" + +#: package/contents/ui/RulesEditor.qml:211 +#, kde-format +msgctxt "Size (width, height)" +msgid "(%1, %2)" +msgstr "" + +#: package/contents/ui/RulesList.qml:61 +#, kde-format +msgid "No rules for specific windows are currently set" +msgstr "" + +#: package/contents/ui/RulesList.qml:69 +#, kde-format +msgid "Select the rules to export" +msgstr "" + +#: package/contents/ui/RulesList.qml:73 +#, kde-format +msgid "Unselect All" +msgstr "" + +#: package/contents/ui/RulesList.qml:73 +#, kde-format +msgid "Select All" +msgstr "" + +#: package/contents/ui/RulesList.qml:87 +#, kde-format +msgid "Save Rules" +msgstr "" + +#: package/contents/ui/RulesList.qml:98 +#, fuzzy, kde-format +#| msgid "&New..." +msgid "Add New..." +msgstr "Nuwe..." + +#: package/contents/ui/RulesList.qml:109 +#, kde-format +msgid "Import..." +msgstr "" + +#: package/contents/ui/RulesList.qml:117 +#, kde-format +msgid "Cancel Export" +msgstr "" + +#: package/contents/ui/RulesList.qml:117 +#, fuzzy, kde-format +#| msgid "Edit..." +msgid "Export..." +msgstr "Redigeer..." + +#: package/contents/ui/RulesList.qml:198 +#, fuzzy, kde-format +msgid "Edit" +msgstr "Redigeer..." + +#: package/contents/ui/RulesList.qml:207 +#, fuzzy, kde-format +msgid "Delete" +msgstr "Ondersoek" + +#: package/contents/ui/RulesList.qml:220 +#, kde-format +msgid "Import Rules" +msgstr "" + +#: package/contents/ui/RulesList.qml:232 +#, kde-format +msgid "Export Rules" +msgstr "" + +#: package/contents/ui/ValueEditor.qml:162 +#, kde-format +msgctxt "(x, y) coordinates separator in size/position" +msgid "x" +msgstr "" + +#: rulesdialog.cpp:28 +#, kde-format +msgid "Edit Window-Specific Settings" +msgstr "Redigeer Venster-Spesifieke Instellings" + +#: rulesmodel.cpp:219 +#, kde-format +msgid "Settings for %1" +msgstr "Instellings vir %1" + +#: rulesmodel.cpp:222 +#, fuzzy, kde-format +#| msgid "Window settings for %1" +msgid "New window settings" +msgstr "Venster instellings vir %1" + +#: rulesmodel.cpp:236 +#, kde-format +msgid "" +"You have specified the window class as unimportant.\n" +"This means the settings will possibly apply to windows from all " +"applications. If you really want to create a generic setting, it is " +"recommended you at least limit the window types to avoid special window " +"types." +msgstr "" +"Jy het die venster klas as onbelangrik gespesifiseer.\n" +"Dit beteken dat die instellings moontlik op alle toepassings van toepassing " +"sal wees.as jy regtig 'n generiese instelling wil skep, word dit aanbeveel " +"dat jy ten minste die venster tipes beperk om sodoende spesiale venster " +"tipes te voorkom." + +#: rulesmodel.cpp:366 +#, fuzzy, kde-format +#| msgid "De&scription:" +msgid "Description" +msgstr "Beskrywing" + +#: rulesmodel.cpp:366 rulesmodel.cpp:374 rulesmodel.cpp:382 rulesmodel.cpp:389 +#: rulesmodel.cpp:395 rulesmodel.cpp:403 rulesmodel.cpp:408 rulesmodel.cpp:414 +#, fuzzy, kde-format +#| msgid "&Window" +msgid "Window matching" +msgstr "Venster" + +#: rulesmodel.cpp:374 +#, fuzzy, kde-format +#| msgid "Window &class (application type):" +msgid "Window class (application)" +msgstr "Venster klas (toepassing tipe):" + +#: rulesmodel.cpp:382 +#, fuzzy, kde-format +#| msgid "Match w&hole window class" +msgid "Match whole window class" +msgstr "Pas volldegie venster klas" + +#: rulesmodel.cpp:389 +#, fuzzy, kde-format +#| msgid "Match w&hole window class" +msgid "Whole window class" +msgstr "Pas volldegie venster klas" + +#: rulesmodel.cpp:395 +#, fuzzy, kde-format +#| msgid "Window &types:" +msgid "Window types" +msgstr "Venster Tipes:" + +#: rulesmodel.cpp:403 +#, fuzzy, kde-format +#| msgid "Window &role:" +msgid "Window role" +msgstr "Venster rol:" + +#: rulesmodel.cpp:408 +#, fuzzy, kde-format +#| msgid "Window t&itle:" +msgid "Window title" +msgstr "Venster titel:" + +#: rulesmodel.cpp:414 +#, fuzzy, kde-format +#| msgid "&Machine (hostname):" +msgid "Machine (hostname)" +msgstr "Masjien (gasheer-naam):" + +#: rulesmodel.cpp:420 +#, fuzzy, kde-format +#| msgid "&Position" +msgid "Position" +msgstr "Posisie" + +#: rulesmodel.cpp:420 rulesmodel.cpp:425 rulesmodel.cpp:430 rulesmodel.cpp:435 +#: rulesmodel.cpp:440 rulesmodel.cpp:453 rulesmodel.cpp:467 rulesmodel.cpp:472 +#: rulesmodel.cpp:477 rulesmodel.cpp:482 rulesmodel.cpp:487 rulesmodel.cpp:493 +#: rulesmodel.cpp:502 rulesmodel.cpp:507 rulesmodel.cpp:512 +#, fuzzy, kde-format +#| msgid "&Position" +msgid "Size & Position" +msgstr "Posisie" + +#: rulesmodel.cpp:425 +#, fuzzy, kde-format +#| msgid "&Size" +msgid "Size" +msgstr "Grootte" + +#: rulesmodel.cpp:430 +#, fuzzy, kde-format +#| msgid "Maximized &horizontally" +msgid "Maximized horizontally" +msgstr "Horisontaal Gemaksimeer" + +#: rulesmodel.cpp:435 +#, fuzzy, kde-format +#| msgid "Maximized &vertically" +msgid "Maximized vertically" +msgstr "Vertikaal Gemaksimeer" + +#: rulesmodel.cpp:440 +#, fuzzy, kde-format +#| msgid "All Desktops" +msgid "Virtual Desktop" +msgstr "Alle Werkskerms" + +#: rulesmodel.cpp:453 +#, fuzzy, kde-format +#| msgid "A&ctive opacity in %" +msgid "Activity" +msgstr "Aktiewe opacity in %" + +#: rulesmodel.cpp:467 +#, fuzzy, kde-format +#| msgid "Splash Screen" +msgid "Screen" +msgstr "Spat Skerm" + +#: rulesmodel.cpp:472 +#, fuzzy, kde-format +#| msgid "&Fullscreen" +msgid "Fullscreen" +msgstr "Volskerm" + +#: rulesmodel.cpp:477 +#, fuzzy, kde-format +#| msgid "M&inimized" +msgid "Minimized" +msgstr "Geminimiseer" + +#: rulesmodel.cpp:482 +#, fuzzy, kde-format +#| msgid "Sh&aded" +msgid "Shaded" +msgstr "Geskakeer" + +#: rulesmodel.cpp:487 +#, fuzzy, kde-format +#| msgid "P&lacement" +msgid "Initial placement" +msgstr "Plasing" + +#: rulesmodel.cpp:493 +#, fuzzy, kde-format +#| msgid "Ignore requested &geometry" +msgid "Ignore requested geometry" +msgstr "Ignoreer aangevraagde afmeting" + +#: rulesmodel.cpp:495 +#, kde-format +msgid "" +"Windows can ask to appear in a certain position.\n" +"By default this overrides the placement strategy\n" +"what might be nasty if the client abuses the feature\n" +"to unconditionally popup in the middle of your screen." +msgstr "" + +#: rulesmodel.cpp:502 +#, fuzzy, kde-format +#| msgid "M&inimum size" +msgid "Minimum Size" +msgstr "Minimum grootte" + +#: rulesmodel.cpp:507 +#, fuzzy, kde-format +#| msgid "M&aximum size" +msgid "Maximum Size" +msgstr "Maksimum grootte" + +#: rulesmodel.cpp:512 +#, kde-format +msgid "Obey geometry restrictions" +msgstr "" + +#: rulesmodel.cpp:514 +#, kde-format +msgid "" +"Eg. terminals or video players can ask to keep a certain aspect ratio\n" +"or only grow by values larger than one\n" +"(eg. by the dimensions of one character).\n" +"This may be pointless and the restriction prevents arbitrary dimensions\n" +"like your complete screen area." +msgstr "" + +#: rulesmodel.cpp:523 +#, fuzzy, kde-format +#| msgid "Keep &above" +msgid "Keep above" +msgstr "hou bo" + +#: rulesmodel.cpp:523 rulesmodel.cpp:528 rulesmodel.cpp:533 rulesmodel.cpp:539 +#: rulesmodel.cpp:545 rulesmodel.cpp:551 +#, kde-format +msgid "Arrangement & Access" +msgstr "" + +#: rulesmodel.cpp:528 +#, fuzzy, kde-format +#| msgid "Keep &below" +msgid "Keep below" +msgstr "Hou onder" + +#: rulesmodel.cpp:533 +#, fuzzy, kde-format +#| msgid "Skip &taskbar" +msgid "Skip taskbar" +msgstr "Slaan taakbalk oor" + +#: rulesmodel.cpp:535 +#, kde-format +msgid "Window shall (not) appear in the taskbar." +msgstr "" + +#: rulesmodel.cpp:539 +#, fuzzy, kde-format +#| msgid "Skip pa&ger" +msgid "Skip pager" +msgstr "Slaan blaaier om" + +#: rulesmodel.cpp:541 +#, kde-format +msgid "Window shall (not) appear in the manager for virtual desktops" +msgstr "" + +#: rulesmodel.cpp:545 +#, fuzzy, kde-format +#| msgid "Skip pa&ger" +msgid "Skip switcher" +msgstr "Slaan blaaier om" + +#: rulesmodel.cpp:547 +#, kde-format +msgid "Window shall (not) appear in the Alt+Tab list" +msgstr "" + +#: rulesmodel.cpp:551 +#, kde-format +msgid "Shortcut" +msgstr "Kortpad" + +#: rulesmodel.cpp:557 +#, kde-format +msgid "No titlebar and frame" +msgstr "" + +#: rulesmodel.cpp:557 rulesmodel.cpp:562 rulesmodel.cpp:568 rulesmodel.cpp:573 +#: rulesmodel.cpp:578 rulesmodel.cpp:589 rulesmodel.cpp:600 rulesmodel.cpp:608 +#: rulesmodel.cpp:621 rulesmodel.cpp:626 rulesmodel.cpp:632 rulesmodel.cpp:637 +#, kde-format +msgid "Appearance & Fixes" +msgstr "" + +#: rulesmodel.cpp:562 +#, kde-format +msgid "Titlebar color scheme" +msgstr "" + +#: rulesmodel.cpp:568 +#, fuzzy, kde-format +#| msgid "A&ctive opacity in %" +msgid "Active opacity" +msgstr "Aktiewe opacity in %" + +#: rulesmodel.cpp:573 +#, fuzzy, kde-format +#| msgid "I&nactive opacity in %" +msgid "Inactive opacity" +msgstr "Onaktiewe opacity in %" + +#: rulesmodel.cpp:578 +#, fuzzy, kde-format +#| msgid "&Focus stealing prevention" +msgid "Focus stealing prevention" +msgstr "Fokus diefstal voorkoming" + +#: rulesmodel.cpp:580 +#, kde-format +msgid "" +"KWin tries to prevent windows from taking the focus\n" +"(\"activate\") while you're working in another window,\n" +"but this may sometimes fail or superact.\n" +"\"None\" will unconditionally allow this window to get the focus while\n" +"\"Extreme\" will completely prevent it from taking the focus." +msgstr "" + +#: rulesmodel.cpp:589 +#, fuzzy, kde-format +#| msgid "&Focus stealing prevention" +msgid "Focus protection" +msgstr "Fokus diefstal voorkoming" + +#: rulesmodel.cpp:591 +#, kde-format +msgid "" +"This controls the focus protection of the currently active window.\n" +"None will always give the focus away,\n" +"Extreme will keep it.\n" +"Otherwise it's interleaved with the stealing prevention\n" +"assigned to the window that wants the focus." +msgstr "" + +#: rulesmodel.cpp:600 +#, fuzzy, kde-format +#| msgid "Accept &focus" +msgid "Accept focus" +msgstr "Aanvaar fokus" + +#: rulesmodel.cpp:602 +#, kde-format +msgid "" +"Windows may prevent to get the focus (activate) when being clicked.\n" +"On the other hand you might wish to prevent a window\n" +"from getting focused on a mouse click." +msgstr "" + +#: rulesmodel.cpp:608 +#, fuzzy, kde-format +#| msgid "Block global shortcuts" +msgid "Ignore global shortcuts" +msgstr "Blokeer globale kortpaaie" + +#: rulesmodel.cpp:610 +#, kde-format +msgid "" +"When used, a window will receive\n" +"all keyboard inputs while it is active, including Alt+Tab etc.\n" +"This is especially interesting for emulators or virtual machines.\n" +"\n" +"Be warned:\n" +"you won't be able to Alt+Tab out of the window\n" +"nor use any other global shortcut (such as Alt+F2 to show KRunner)\n" +"while it's active!" +msgstr "" + +#: rulesmodel.cpp:621 +#, fuzzy, kde-format +#| msgid "&Closeable" +msgid "Closeable" +msgstr "Toemaakbaar" + +#: rulesmodel.cpp:626 +#, fuzzy, kde-format +#| msgid "Window &type" +msgid "Set window type" +msgstr "Venster tipe" + +#: rulesmodel.cpp:632 +#, kde-format +msgid "Desktop file name" +msgstr "" + +#: rulesmodel.cpp:637 +#, kde-format +msgid "Block compositing" +msgstr "" + +#: rulesmodel.cpp:717 +#, kde-format +msgid "Normal Window" +msgstr "Normale Venster" + +#: rulesmodel.cpp:718 +#, kde-format +msgid "Dialog Window" +msgstr "Dialoog Venster" + +#: rulesmodel.cpp:719 +#, kde-format +msgid "Utility Window" +msgstr "Nuts Venster" + +#: rulesmodel.cpp:720 +#, kde-format +msgid "Dock (panel)" +msgstr "Meer vas (paneel)" + +#: rulesmodel.cpp:721 +#, kde-format +msgid "Toolbar" +msgstr "Nutsbalk" + +#: rulesmodel.cpp:722 +#, kde-format +msgid "Torn-Off Menu" +msgstr "Afskeur kieslys" + +#: rulesmodel.cpp:723 +#, kde-format +msgid "Splash Screen" +msgstr "Spat Skerm" + +#: rulesmodel.cpp:724 +#, kde-format +msgid "Desktop" +msgstr "Werkskerm" + +#. i18n("Unmanaged Window") }, deprecated +#: rulesmodel.cpp:726 +#, kde-format +msgid "Standalone Menubar" +msgstr "Alleenstaande Kiesbalk" + +#: rulesmodel.cpp:741 +#, kde-format +msgid "All Desktops" +msgstr "Alle Werkskerms" + +#: rulesmodel.cpp:754 +#, kde-format +msgid "All Activities" +msgstr "" + +#: rulesmodel.cpp:775 +#, kde-format +msgid "Default" +msgstr "" + +#: rulesmodel.cpp:776 +#, kde-format +msgid "No Placement" +msgstr "Geen Plasing" + +#: rulesmodel.cpp:777 +#, kde-format +msgid "Minimal Overlapping" +msgstr "" + +#: rulesmodel.cpp:778 +#, fuzzy, kde-format +#| msgid "Maximizing" +msgid "Maximized" +msgstr "Maksimerend" + +#: rulesmodel.cpp:779 +#, fuzzy, kde-format +#| msgid "Cascade" +msgid "Cascaded" +msgstr "Kaskade" + +#: rulesmodel.cpp:780 +#, kde-format +msgid "Centered" +msgstr "In die middel" + +#: rulesmodel.cpp:781 +#, kde-format +msgid "Random" +msgstr "Lukraak" + +#: rulesmodel.cpp:782 +#, fuzzy, kde-format +#| msgid "Top-Left Corner" +msgid "In Top-Left Corner" +msgstr "Boonste-linker hoek" + +#: rulesmodel.cpp:783 +#, kde-format +msgid "Under Mouse" +msgstr "Onder Muis" + +#: rulesmodel.cpp:784 +#, kde-format +msgid "On Main Window" +msgstr "Op Hoof Venster" + +#: rulesmodel.cpp:792 +#, fuzzy, kde-format +#| msgid "None" +msgid "None" +msgstr "Geen" + +#: rulesmodel.cpp:793 +#, kde-format +msgid "Low" +msgstr "Laag" + +#: rulesmodel.cpp:794 +#, kde-format +msgid "Normal" +msgstr "Normaal" + +#: rulesmodel.cpp:795 +#, kde-format +msgid "High" +msgstr "Hoog" + +#: rulesmodel.cpp:796 +#, kde-format +msgid "Extreme" +msgstr "Buitensporige" \ No newline at end of file diff --git a/po/af/kcmkwm.po b/po/af/kcmkwm.po new file mode 100644 index 0000000..8a26a4f --- /dev/null +++ b/po/af/kcmkwm.po @@ -0,0 +1,1427 @@ +# UTF-8 test:äëïöü +# Copyright (C) 2001 Free Software Foundation, Inc. +# Kobus Venter , 2005. +# +msgid "" +msgstr "" +"Project-Id-Version: kcmkwm stable\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-08-29 02:26+0200\n" +"PO-Revision-Date: 2005-11-25 19:56+0200\n" +"Last-Translator: Kobus Venter \n" +"Language-Team: AFRIKAANS \n" +"Language: af\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 "Kobus Venter" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "kabousv@therugby.co.za" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_1) +#: actions.ui:17 +#, fuzzy, kde-format +#| msgid "Inactive Inner Window" +msgid "Inactive Inner Window Actions" +msgstr "Onaktiewe Binneste Venster" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#. i18n: ectx: property (text), widget (QLabel, label_1) +#: actions.ui:26 mouse.ui:177 +#, fuzzy, kde-format +#| msgid "&Titlebar double-click:" +msgid "&Left click:" +msgstr "Titelbalk dubbel-kliek:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandWindow1) +#: actions.ui:39 +#, kde-format +msgid "" +"In this row you can customize left click behavior when clicking into an " +"inactive inner window ('inner' means: not titlebar, not frame)." +msgstr "" +"In hierdie ry kan jy die linker kliek gedrag pasmaak vir wanneer jy kliek " +"binne 'n onaktiewe binneste venster ('binneste' beteken: nie titelbalk, nie " +"raam)." + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow3) +#: actions.ui:43 actions.ui:83 actions.ui:123 +#, fuzzy, kde-format +#| msgid "Activate, Raise & Pass Click" +msgid "Activate, raise and pass click" +msgstr "Aktiveer, Lig & Herhaling Kliek" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow3) +#: actions.ui:48 actions.ui:88 actions.ui:128 +#, fuzzy, kde-format +#| msgid "Activate & Pass Click" +msgid "Activate and pass click" +msgstr "Aktiveer & Herhaling Kliek" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow3) +#: actions.ui:53 actions.ui:93 actions.ui:133 mouse.ui:293 mouse.ui:408 +#: mouse.ui:523 +#, kde-format +msgid "Activate" +msgstr "Aktiveer" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow3) +#: actions.ui:58 actions.ui:98 actions.ui:138 mouse.ui:283 mouse.ui:398 +#: mouse.ui:513 +#, fuzzy, kde-format +#| msgid "Activate & Raise" +msgid "Activate and raise" +msgstr "Aktiveer & Lig" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: actions.ui:66 mouse.ui:200 +#, fuzzy, kde-format +#| msgid "&Titlebar double-click:" +msgid "&Middle click:" +msgstr "Titelbalk dubbel-kliek:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandWindow2) +#: actions.ui:79 +#, kde-format +msgid "" +"In this row you can customize middle click behavior when clicking into an " +"inactive inner window ('inner' means: not titlebar, not frame)." +msgstr "" +"In hierdie ry kan jy die middelste kliek gedrag pasmaak vir wanneer jy kliek " +"binne 'n onaktiewe binneste venster ('binneste' beteken: nie titelbalk, nie " +"raam)." + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: actions.ui:106 mouse.ui:213 +#, kde-format +msgid "&Right click:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandWindow3) +#: actions.ui:119 +#, kde-format +msgid "" +"In this row you can customize right click behavior when clicking into an " +"inactive inner window ('inner' means: not titlebar, not frame)." +msgstr "" +"In hierdie ry kan jy die regter kliek gedrag pasmaak vir wanneer jy kliek " +"binne 'n onaktiewe binneste venster ('binneste' beteken: nie titelbalk, nie " +"raam)." + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: actions.ui:146 mouse.ui:88 +#, fuzzy, kde-format +#| msgid "Modifier key + mouse wheel:" +msgid "Mouse &wheel:" +msgstr "Modifikasie sleutel + muis wiel:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandWindowWheel) +#: actions.ui:159 +#, fuzzy, kde-format +#| msgid "" +#| "In this row you can customize left click behavior when clicking into an " +#| "inactive inner window ('inner' means: not titlebar, not frame)." +msgid "" +"In this row you can customize behavior when scrolling into an inactive inner " +"window ('inner' means: not titlebar, not frame)." +msgstr "" +"In hierdie ry kan jy die linker kliek gedrag pasmaak vir wanneer jy kliek " +"binne 'n onaktiewe binneste venster ('binneste' beteken: nie titelbalk, nie " +"raam)." + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindowWheel) +#: actions.ui:163 +#, kde-format +msgid "Scroll" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindowWheel) +#: actions.ui:168 +#, fuzzy, kde-format +#| msgid "Activate & Lower" +msgid "Activate and scroll" +msgstr "Aktiveer & Verlaag" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindowWheel) +#: actions.ui:173 +#, fuzzy, kde-format +#| msgid "Activate, Raise and Move" +msgid "Activate, raise and scroll" +msgstr "Aktiveer, Lig & Skuif" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#: actions.ui:184 +#, fuzzy, kde-format +#| msgid "Inner Window, Titlebar && Frame" +msgid "Inner Window, Titlebar and Frame Actions" +msgstr "Binneste Venster, Titelbalk && Raam" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: actions.ui:195 +#, fuzzy, kde-format +#| msgid "Modifier key:" +msgid "Mo&difier key:" +msgstr "Verander sleutel:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandAllKey) +#: actions.ui:205 +#, kde-format +msgid "" +"Here you select whether holding the Meta key or Alt key will allow you to " +"perform the following actions." +msgstr "" +"Hier kan jy kies of die inhou van die Meta sleutel of Alt sleutel jou sal " +"toelaat om die volgende aksies uit te voer." + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllKey) +#: actions.ui:209 +#, kde-format +msgid "Meta" +msgstr "Meta" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllKey) +#: actions.ui:214 +#, kde-format +msgid "Alt" +msgstr "Alt" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: actions.ui:236 +#, kde-format +msgid " + " +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_8) +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: actions.ui:248 mouse.ui:601 +#, fuzzy, kde-format +#| msgid "&Titlebar double-click:" +msgid "L&eft click:" +msgstr "Titelbalk dubbel-kliek:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandAll1) +#: actions.ui:261 +#, kde-format +msgid "" +"In this row you can customize left click behavior when clicking into the " +"titlebar or the frame." +msgstr "" +"In hierdie ry jy kan linkskliek gedrag pasmaak wanneer gekliek word binne " +"die titelbalk of die raam." + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:265 actions.ui:335 actions.ui:405 +#, kde-format +msgid "Move" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:270 actions.ui:340 actions.ui:410 +#, fuzzy, kde-format +#| msgid "Activate, Raise and Move" +msgid "Activate, raise and move" +msgstr "Aktiveer, Lig & Skuif" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:275 actions.ui:345 actions.ui:415 mouse.ui:246 mouse.ui:308 +#: mouse.ui:361 mouse.ui:423 mouse.ui:476 mouse.ui:538 +#, fuzzy, kde-format +#| msgid "Toggle Raise & Lower" +msgid "Toggle raise and lower" +msgstr "Wissel Lig & Verlaag" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:280 actions.ui:350 actions.ui:420 +#, kde-format +msgid "Resize" +msgstr "Hervergroot" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:285 actions.ui:355 actions.ui:425 mouse.ui:236 mouse.ui:298 +#: mouse.ui:351 mouse.ui:413 mouse.ui:466 mouse.ui:528 +#, kde-format +msgid "Raise" +msgstr "Lig" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:290 actions.ui:360 actions.ui:430 mouse.ui:65 mouse.ui:241 +#: mouse.ui:303 mouse.ui:356 mouse.ui:418 mouse.ui:471 mouse.ui:533 +#, kde-format +msgid "Lower" +msgstr "Verlaag" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:295 actions.ui:365 actions.ui:435 mouse.ui:55 mouse.ui:251 +#: mouse.ui:313 mouse.ui:366 mouse.ui:428 mouse.ui:481 mouse.ui:543 +#, kde-format +msgid "Minimize" +msgstr "Verklein" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:300 actions.ui:370 actions.ui:440 +#, fuzzy, kde-format +#| msgid "Change Opacity" +msgid "Decrease opacity" +msgstr "Verander Opacity" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:305 actions.ui:375 actions.ui:445 +#, fuzzy, kde-format +#| msgid "Change Opacity" +msgid "Increase opacity" +msgstr "Verander Opacity" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:310 actions.ui:380 actions.ui:450 actions.ui:505 mouse.ui:80 +#: mouse.ui:132 mouse.ui:271 mouse.ui:333 mouse.ui:386 mouse.ui:448 +#: mouse.ui:501 mouse.ui:563 +#, fuzzy, kde-format +#| msgid "Nothing" +msgid "Do nothing" +msgstr "Niks" + +#. i18n: ectx: property (text), widget (QLabel, label_8) +#: actions.ui:318 +#, fuzzy, kde-format +#| msgid "Middle button:" +msgid "Middle &click:" +msgstr "Middelste knoppie:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandAll2) +#: actions.ui:331 +#, kde-format +msgid "" +"In this row you can customize middle click behavior when clicking into the " +"titlebar or the frame." +msgstr "" +"In hierdie ry jy kan middelste kliek gedrag pasmaak wanneer gekliek word " +"binne die titelbalk of die raam." + +#. i18n: ectx: property (text), widget (QLabel, label_10) +#. i18n: ectx: property (text), widget (QLabel, label_9) +#: actions.ui:388 mouse.ui:671 +#, kde-format +msgid "Right clic&k:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:401 +#, kde-format +msgid "" +"In this row you can customize right click behavior when clicking into the " +"titlebar or the frame." +msgstr "" +"In hierdie ry jy kan regterkant kliek gedrag pasmaak wanneer gekliek word " +"binne die titelbalk of die raam." + +#. i18n: ectx: property (text), widget (QLabel, label_10) +#: actions.ui:458 +#, fuzzy, kde-format +#| msgid "Modifier key + mouse wheel:" +msgid "Mo&use wheel:" +msgstr "Modifikasie sleutel + muis wiel:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:471 +#, fuzzy, kde-format +#| msgid "" +#| "Here you can customize KDE's behavior when scrolling with the mouse " +#| "wheel in a window while pressing the modifier key." +msgid "" +"Here you can customize KDE's behavior when scrolling with the mouse wheel in " +"a window while pressing the modifier key." +msgstr "" +"Jy kan KDE se gedrag hier pasmaak vir wanneer die muis wiel binne 'n venster " +"gedraai word, terwyl 'n modifikasie sleutel gedruk word." + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:475 mouse.ui:102 +#, fuzzy, kde-format +#| msgid "Raise/Lower" +msgid "Raise/lower" +msgstr "Lig && Verlaag" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:480 mouse.ui:107 +#, fuzzy, kde-format +#| msgid "Shade/Unshade" +msgid "Shade/unshade" +msgstr "Skadu/Ontskadu" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:485 mouse.ui:112 +#, fuzzy, kde-format +#| msgid "Maximize/Restore" +msgid "Maximize/restore" +msgstr "Maksimeer/Herstel" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:490 mouse.ui:117 +#, fuzzy, kde-format +#| msgid "Keep Above/Below" +msgid "Keep above/below" +msgstr "Ho bo/onder" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:495 mouse.ui:122 +#, fuzzy, kde-format +#| msgid "Move to Previous/Next Desktop" +msgid "Move to previous/next desktop" +msgstr "Gaan na Vorige/Volgende Werkskerm" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:500 mouse.ui:127 +#, fuzzy, kde-format +#| msgid "Change Opacity" +msgid "Change opacity" +msgstr "Verander Opacity" + +#. i18n: ectx: property (text), widget (QLabel, shadeHoverLabel) +#: advanced.ui:20 +#, fuzzy, kde-format +#| msgid "Window Actio&ns" +msgid "Window &unshading:" +msgstr "Venster Aksies" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_ShadeHover) +#: advanced.ui:32 +#, fuzzy, kde-format +#| msgid "" +#| "If Shade Hover is enabled, a shaded window will un-shade automatically " +#| "when the mouse pointer has been over the title bar for some time." +msgid "" +"

If this option is enabled, a shaded window will " +"unshade automatically when the mouse pointer has been over the titlebar for " +"some time.

" +msgstr "" +"As Skadu Sweef geaktiveer is, sal 'n beskadude venster automaties ontskadu " +"wanneer die muis wyser 'n geruime tyd oor die titel balk is." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ShadeHover) +#: advanced.ui:35 +#, kde-format +msgid "On titlebar hover after:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QSpinBox, kcfg_ShadeHoverInterval) +#: advanced.ui:42 +#, kde-format +msgid "" +"Sets the time in milliseconds before the window unshades when the mouse " +"pointer goes over the shaded window." +msgstr "" +"Verstel die tyd in millisekondes voordat die venster ontskadu wanneer die " +"muis wyser bo-oor die beskadude venster gaan." + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_ShadeHoverInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_DelayFocusInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_AutoRaiseInterval) +#: advanced.ui:45 focus.ui:85 focus.ui:178 +#, fuzzy, kde-format +#| msgid " msec" +msgid " ms" +msgstr " msek" + +#. i18n: ectx: property (text), widget (QLabel, windowPlacementLabel) +#: advanced.ui:66 +#, fuzzy, kde-format +#| msgid "&Placement:" +msgid "Window &placement:" +msgstr "Plasing:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_Placement) +#: advanced.ui:76 +#, kde-format +msgid "" +"

The placement policy determines where a new window " +"will appear on the desktop.

  • Smart will try to achieve a minimum overlap of windows
  • Maximizing will try to maximize every window to fill the " +"whole screen. It might be useful to selectively affect placement of some " +"windows using the window-specific settings.
  • Cascade will " +"cascade the windows
  • Random will use a random " +"position
  • Centered will place the window " +"centered
  • Zero-cornered will place the window in " +"the top-left corner
  • Under mouse will place the " +"window under the pointer
" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:80 +#, fuzzy, kde-format +#| msgid "Snap windows onl&y when overlapping" +msgid "Minimal Overlapping" +msgstr "Klamp vensters slegs wanneer oorvleuel" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:85 +#, fuzzy, kde-format +#| msgid "Maximize" +msgid "Maximized" +msgstr "Vergroot" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:90 +#, fuzzy, kde-format +#| msgid "Cascade" +msgid "Cascaded" +msgstr "Kaskade" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:95 +#, kde-format +msgid "Random" +msgstr "Lukrake" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:100 +#, kde-format +msgid "Centered" +msgstr "Gesentreer" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:105 +#, kde-format +msgid "In Top-Left Corner" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:110 +#, fuzzy, kde-format +#| msgid "Focus Under Mouse" +msgid "Under mouse" +msgstr "Fokus Onder Muis" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_AllowKDEAppsToRememberWindowPositions) +#: advanced.ui:118 +#, kde-format +msgid "" +"When turned on, KDE apps which are able to remember the positions of their " +"windows are allowed to do so. This will override the window placement mode " +"defined above." +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AllowKDEAppsToRememberWindowPositions) +#: advanced.ui:121 +#, kde-format +msgid "Allow KDE apps to remember the positions of their own windows" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, specialWindowsLabel) +#: advanced.ui:128 +#, fuzzy, kde-format +#| msgid "Windows" +msgid "&Special windows:" +msgstr "Vensters" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_HideUtilityWindowsForInactive) +#: advanced.ui:138 +#, kde-format +msgid "" +"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." +msgstr "" +"Wanneer hierdie aktief is, sal nutsvensters (gereedskap vensters, afskeur " +"kieslyste,...) van onaktiewe programme weg gesteek word. Hulle sal net gewys " +"word wanneer die program aktief word. Die program moet die vensters met die " +"regte tipe merk vir hierdie eienskap om te werk." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_HideUtilityWindowsForInactive) +#: advanced.ui:141 +#, kde-format +msgid "Hide utility windows for inactive applications" +msgstr "Steek nutsvensters van onaktiewe programme weg" + +#. i18n: ectx: property (text), widget (QLabel, windowFocusPolicyLabel) +#: focus.ui:22 +#, kde-format +msgid "Window &activation policy:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QComboBox, windowFocusPolicy) +#: focus.ui:32 +#, fuzzy, kde-format +#| msgid "" +#| "Enable this option if you want an animation shown when windows are " +#| "minimized or restored." +msgid "With this option you can specify how and when windows will be focused." +msgstr "" +"Aktiveer hierdie opsie as jy 'n animasie vertoon wil hê wanneer vensters " +"verklein herstel word." + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:36 +#, fuzzy, kde-format +#| msgid "Click to Focus" +msgctxt "sassa asas" +msgid "Click to focus" +msgstr "Kliek om te Fokus" + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:41 +#, kde-format +msgid "Click to focus (mouse precedence)" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:46 +#, fuzzy, kde-format +#| msgid "Focus Follows Mouse" +msgid "Focus follows mouse" +msgstr "Fokus Volg Muis" + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:51 +#, kde-format +msgid "Focus follows mouse (mouse precedence)" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:56 +#, fuzzy, kde-format +#| msgid "Focus Under Mouse" +msgid "Focus under mouse" +msgstr "Fokus Onder Muis" + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:61 +#, fuzzy, kde-format +#| msgid "Focus Strictly Under Mouse" +msgid "Focus strictly under mouse" +msgstr "Fokus Streng Onder Muis" + +#. i18n: ectx: property (text), widget (QLabel, delayFocusOnLabel) +#: focus.ui:69 +#, fuzzy, kde-format +#| msgid "Delay focus" +msgid "&Delay focus by:" +msgstr "Vertraag Fokus" + +#. i18n: ectx: property (whatsThis), widget (QSpinBox, kcfg_DelayFocusInterval) +#: focus.ui:82 +#, kde-format +msgid "" +"This is the delay after which the window the mouse pointer is over will " +"automatically receive focus." +msgstr "" +"Hierdie is die vertraging waarna die venster waaroor die muis wyser is " +"automaties op gefokus sal word." + +#. i18n: ectx: property (text), widget (QLabel, focusStealingLabel) +#: focus.ui:101 +#, fuzzy, kde-format +#| msgid "Focus stealing prevention level:" +msgid "Focus &stealing prevention:" +msgstr "Fokus steel voorkoming vlak" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_FocusStealingPreventionLevel) +#: focus.ui:114 +#, fuzzy, kde-format +#| msgid "" +#| "

This option specifies how much KWin will try to prevent unwanted focus " +#| "stealing caused by unexpected activation of new windows. (Note: This " +#| "feature does not work with the Focus Under Mouse or Focus Strictly Under " +#| "Mouse focus policies.)

  • 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 normal level, depending on the applications.
  • Normal: 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.

" +msgid "" +"

This option specifies how much KWin will try to " +"prevent unwanted focus stealing caused by unexpected activation of new " +"windows. (Note: This feature does not work with the Focus under mouse or Focus strictly under mouse focus policies.)

  • 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.

" +msgstr "" +"

Hiedie opsie spesifiseer hoeveel KWin probeer om te keer dat nuwe " +"vensters die fokus steel.Hierdie opsie werk nie vir 'Fokus onder Muis' en " +"'Fokus streng onder Muis' beleid nie.

  • Geen:Voorkoming word " +"af gesit en nuwe vensters word altyd aktief
  • Laag:Voorkoming " +"is aktief. Vir vensters wat nie die onderliggende meganisme ondersteun nie " +"sal dit aktief gemaak word
  • Normaal:Voorkoming is aktief.
  • Hoog:Nuwe vensters sal geaktiveer word as dit aan die " +"huidige aktiewe program behoort. Dit sal nie geaktiveer word as 'n ander " +"venster aktief is nie. Hierdie opsie maak meeste sin saam met 'n muis fokus " +"beleid.
  • Ekstreem: Alle vensters moet spesifiek deur die " +"gebruiker geaktiveer word

Vensters wat verhoed word om die " +"fokus te steel sal aandui dat hulle aandag benodig. Gewoonlik beteken dit " +"dat hulle taakbalk inskrywing verlig is. Dit kan in die Inkennisstelling " +"beheer module verander word.

" + +#. i18n: Focus Stealing Prevention Level +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_FocusStealingPreventionLevel) +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_BorderSnapZone) +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_WindowSnapZone) +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_CenterSnapZone) +#: focus.ui:118 moving.ui:53 moving.ui:75 moving.ui:97 +#, fuzzy, kde-format +#| msgctxt "Focus Stealing Prevention Level" +#| msgid "None" +msgid "None" +msgstr "Geen" + +#. i18n: Focus Stealing Prevention Level +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_FocusStealingPreventionLevel) +#: focus.ui:123 +#, fuzzy, kde-format +#| msgctxt "Focus Stealing Prevention Level" +#| msgid "Low" +msgid "Low" +msgstr "Laag" + +#. i18n: Focus Stealing Prevention Level +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_FocusStealingPreventionLevel) +#: focus.ui:128 +#, kde-format +msgid "Medium" +msgstr "" + +#. i18n: Focus Stealing Prevention Level +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_FocusStealingPreventionLevel) +#: focus.ui:133 +#, fuzzy, kde-format +#| msgctxt "Focus Stealing Prevention Level" +#| msgid "High" +msgid "High" +msgstr "Hoog" + +#. i18n: Focus Stealing Prevention Level +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_FocusStealingPreventionLevel) +#: focus.ui:138 +#, fuzzy, kde-format +#| msgctxt "Focus Stealing Prevention Level" +#| msgid "Extreme" +msgid "Extreme" +msgstr "Uiters" + +#. i18n: ectx: property (text), widget (QLabel, raisingWindowsLabel) +#: focus.ui:146 +#, fuzzy, kde-format +#| msgid "Moving windows:" +msgid "Raising windows:" +msgstr "Skuif vensters" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_ClickRaise) +#: focus.ui:153 +#, kde-format +msgid "" +"When this option is enabled, the active window will be brought to the front " +"when you click somewhere into the window contents. To change it for inactive " +"windows, you need to change the settings in the Actions tab." +msgstr "" +"Wanneer geaktiveer, sal die aktiewe venster na vore gebring word wanneer jy " +"êrens binne die venster inhoud kliek. Om dit verander na onaktiewe vensters, " +"moet jy dit stel in die Aksie oortjie." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ClickRaise) +#: focus.ui:156 +#, fuzzy, kde-format +#| msgid "C&lick raise active window" +msgid "&Click raises active window" +msgstr "Kliek lig aktiewe venster" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_AutoRaise) +#: focus.ui:165 +#, kde-format +msgid "" +"When this option is enabled, a window in the background will automatically " +"come to the front when the mouse pointer has been over it for some time." +msgstr "" +"Wanneer hierdie opsie geaktiveer is, sal 'n venster in die agtergrond " +"automaties na vore kom wanneer die muis wyser bo-oor dit is vir 'n geruime " +"tyd." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AutoRaise) +#: focus.ui:168 +#, kde-format +msgid "&Raise on hover, delayed by:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QSpinBox, kcfg_AutoRaiseInterval) +#: focus.ui:175 +#, kde-format +msgid "" +"This is the delay after which the window that the mouse pointer is over will " +"automatically come to the front." +msgstr "" +"Hierdie is die vertraging waarna die venster waaroor die muis wyser is " +"automaties na vore sal kom." + +#. i18n: ectx: property (text), widget (QLabel, multiscreenBehaviorLabel) +#: focus.ui:196 +#, kde-format +msgid "Multiscreen behavior:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_ActiveMouseScreen) +#: focus.ui:203 +#, kde-format +msgid "" +"When this option is enabled, the active Xinerama screen (where new windows " +"appear, for example) is the screen containing the mouse pointer. When " +"disabled, the active Xinerama screen is the screen containing the focused " +"window. By default this option is disabled for Click to focus and enabled " +"for other focus policies." +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ActiveMouseScreen) +#: focus.ui:206 +#, kde-format +msgid "Active screen follows &mouse" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_SeparateScreenFocus) +#: focus.ui:213 +#, kde-format +msgid "" +"When this option is enabled, focus operations are limited only to the active " +"Xinerama screen" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_SeparateScreenFocus) +#: focus.ui:216 +#, kde-format +msgid "&Separate screen focus" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, windowFocusPolicyDescriptionLabel) +#: focus.ui:229 +#, kde-format +msgid "Window activation policy description" +msgstr "" + +#: main.cpp:73 +#, kde-format +msgid "&Focus" +msgstr "Fokus" + +#: main.cpp:84 +#, fuzzy, kde-format +#| msgid "&Titlebar Actions" +msgid "Titlebar A&ctions" +msgstr "Titelbalk Aksies" + +#: main.cpp:91 +#, fuzzy, kde-format +#| msgid "Window Actio&ns" +msgid "W&indow Actions" +msgstr "Venster Aksies" + +#: main.cpp:98 +#, fuzzy, kde-format +#| msgid "&Placement:" +msgid "Mo&vement" +msgstr "Plasing:" + +#: main.cpp:105 +#, fuzzy, kde-format +#| msgid "Ad&vanced" +msgid "Adva&nced" +msgstr "Gevorderde" + +#: main.cpp:110 +#, kde-format +msgid "Window Behavior Configuration Module" +msgstr "Venster Gedrag Opstelling Module" + +#: main.cpp:112 +#, kde-format +msgid "(c) 1997 - 2002 KWin and KControl Authors" +msgstr "(c) 1997 - 2002 Kwin en KControl Outeure" + +#: main.cpp:114 +#, kde-format +msgid "Matthias Ettrich" +msgstr "" + +#: main.cpp:115 +#, kde-format +msgid "Waldo Bastian" +msgstr "" + +#: main.cpp:116 +#, kde-format +msgid "Cristian Tibirna" +msgstr "" + +#: main.cpp:117 +#, kde-format +msgid "Matthias Kalle Dalheimer" +msgstr "" + +#: main.cpp:118 +#, kde-format +msgid "Daniel Molkentin" +msgstr "" + +#: main.cpp:119 +#, kde-format +msgid "Wynn Wilkes" +msgstr "" + +#: main.cpp:120 +#, kde-format +msgid "Pat Dowler" +msgstr "" + +#: main.cpp:121 +#, kde-format +msgid "Bernd Wuebben" +msgstr "" + +#: main.cpp:122 +#, kde-format +msgid "Matthias Hoelzer-Kluepfel" +msgstr "" + +#: main.cpp:167 +#, fuzzy, kde-format +#| msgid "" +#| "

Window Behavior

Here you can customize the way windows behave " +#| "when being moved, resized or clicked on. You can also specify a focus " +#| "policy as well as a placement policy for new windows.

Please note that " +#| "this configuration will not take effect if you do not use KWin as your " +#| "window manager. If you do use a different window manager, please refer to " +#| "its documentation for how to customize window behavior." +msgid "" +"

Window Behavior

Here you can customize the way windows behave " +"when being moved, resized or clicked on. You can also specify a focus policy " +"as well as a placement policy for new windows.

Please note that this " +"configuration will not take effect if you do not use KWin as your window " +"manager. If you do use a different window manager, please refer to its " +"documentation for how to customize window behavior.

" +msgstr "" +"

Venster Gedrag

Hier kan jy die wyse waarop vensters hulle gedra " +"pasmaak wanneer hulle verskuif, hervergroot of daarop gekliek word. Jy kan " +"asook 'n fokus beleid spesifiseer, asook 'n plasing beleid vir nuwe " +"vensters.

Let daarop dat hierdie opstelling geen effek sal hê as jy nie " +"Kwin gebruik as jou Venster Bestuurder nie. As jy 'n ander Venster " +"Bestuurder gebruik, verwys na sy dokumentasie hoe om venster gedrag te " +"pasmaak." + +#: main.cpp:187 +#, kde-format +msgid "&Titlebar Actions" +msgstr "Titelbalk Aksies" + +#: main.cpp:193 +#, kde-format +msgid "Window Actio&ns" +msgstr "Venster Aksies" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_1) +#: mouse.ui:17 +#, fuzzy, kde-format +#| msgid "&Titlebar Actions" +msgid "Titlebar Actions" +msgstr "Titelbalk Aksies" + +#. i18n: ectx: property (text), widget (QLabel, label_1) +#: mouse.ui:26 +#, fuzzy, kde-format +#| msgid "&Titlebar double-click:" +msgid "&Double-click:" +msgstr "Titelbalk dubbel-kliek:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#: mouse.ui:36 +#, kde-format +msgid "Behavior on double click into the titlebar." +msgstr "Gedrag vir dubbel kliek binne die titelbalk." + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonLeftClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonMiddleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonRightClickCommand) +#: mouse.ui:40 mouse.ui:615 mouse.ui:650 mouse.ui:685 +#, fuzzy, kde-format +#| msgid "Maximize" +msgid "Maximize" +msgstr "Vergroot" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonLeftClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonMiddleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonRightClickCommand) +#: mouse.ui:45 mouse.ui:620 mouse.ui:655 mouse.ui:690 +#, kde-format +msgid "Vertically maximize" +msgstr "" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonLeftClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonMiddleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonRightClickCommand) +#: mouse.ui:50 mouse.ui:625 mouse.ui:660 mouse.ui:695 +#, fuzzy, kde-format +#| msgid "Horizontal offset:" +msgid "Horizontally maximize" +msgstr "Horisontale verskil:" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#: mouse.ui:60 mouse.ui:256 mouse.ui:318 mouse.ui:371 mouse.ui:433 mouse.ui:486 +#: mouse.ui:548 +#, kde-format +msgid "Shade" +msgstr "Skadu" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#: mouse.ui:70 mouse.ui:261 mouse.ui:323 mouse.ui:376 mouse.ui:438 mouse.ui:491 +#: mouse.ui:553 +#, kde-format +msgid "Close" +msgstr "" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#: mouse.ui:75 +#, fuzzy, kde-format +#| msgid "On All Desktops" +msgid "Show on all desktops" +msgstr "Op Alle Werkskerms" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandTitlebarWheel) +#: mouse.ui:98 +#, fuzzy, kde-format +#| msgid "Behavior on double click into the titlebar." +msgid "Behavior on mouse wheel scroll over the titlebar." +msgstr "Gedrag vir dubbel kliek binne die titelbalk." + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#: mouse.ui:143 +#, fuzzy, kde-format +#| msgid "&Titlebar Actions" +msgid "Titlebar and Frame Actions" +msgstr "Titelbalk Aksies" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: mouse.ui:167 +#, kde-format +msgid "Active" +msgstr "Aktief" + +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: mouse.ui:190 +#, kde-format +msgid "Inactive" +msgstr "Onaktief" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandActiveTitlebar3) +#: mouse.ui:232 mouse.ui:347 mouse.ui:462 +#, kde-format +msgid "" +"Behavior on left click into the titlebar or frame of an active window." +msgstr "" +"Gedrag vir links kliek binne die titelbalk of raam van 'n " +"aktiewe venster." + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#: mouse.ui:266 mouse.ui:328 mouse.ui:381 mouse.ui:443 mouse.ui:496 +#: mouse.ui:558 +#, fuzzy, kde-format +#| msgid "Operations Menu" +msgid "Show actions menu" +msgstr "Operasies Kieslys" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#: mouse.ui:279 mouse.ui:394 mouse.ui:509 +#, kde-format +msgid "" +"Behavior on left click into the titlebar or frame of an " +"inactive window." +msgstr "" +"Gedrag vir links kliek binne die titelbalk of raam van 'n " +"onaktiewe venster." + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#: mouse.ui:288 mouse.ui:403 mouse.ui:518 +#, fuzzy, kde-format +#| msgid "Activate & Lower" +msgid "Activate and lower" +msgstr "Aktiveer & Verlaag" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_3) +#: mouse.ui:589 +#, fuzzy, kde-format +#| msgid "Maximize Button" +msgid "Maximize Button Actions" +msgstr "Vergroot Knoppie" + +#. i18n: ectx: property (whatsThis), widget (QLabel, label_8) +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_MaximizeButtonLeftClickCommand) +#: mouse.ui:598 mouse.ui:611 +#, kde-format +msgid "Behavior on left click onto the maximize button." +msgstr "Gedrag vir links kliek op die vergroot knoppie." + +#. i18n: ectx: property (whatsThis), widget (QLabel, label_9) +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_MaximizeButtonMiddleClickCommand) +#: mouse.ui:633 mouse.ui:646 +#, kde-format +msgid "Behavior on middle click onto the maximize button." +msgstr "Gedrag vir middel kliek op die vergroot knoppie." + +#. i18n: ectx: property (text), widget (QLabel, label_9) +#: mouse.ui:636 +#, fuzzy, kde-format +#| msgid "Middle button:" +msgid "Middle c&lick:" +msgstr "Middelste knoppie:" + +#. i18n: ectx: property (whatsThis), widget (QLabel, label_10) +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_MaximizeButtonRightClickCommand) +#: mouse.ui:668 mouse.ui:681 +#, kde-format +msgid "Behavior on right click onto the maximize button." +msgstr "Gedrag vir regs kliek op die vergroot knoppie." + +#. i18n: ectx: property (text), widget (QLabel, geometryTipLabel) +#: moving.ui:20 +#, kde-format +msgid "Window &geometry:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_GeometryTip) +#: moving.ui:30 +#, kde-format +msgid "" +"Enable this option if you want a window's geometry to be displayed while it " +"is being moved or resized. The window position relative to the top-left " +"corner of the screen is displayed together with its size." +msgstr "" +"Aktiveer hierdie opsie as jy wil hê die vesnter se geometrie moet vertoon " +"word wanneer dit geskuif hervergroot word. Die venster posissie relatief tot " +"die linker-bo hoek van die skerm word vertoon saam met sy groote." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_GeometryTip) +#: moving.ui:33 +#, fuzzy, kde-format +#| msgid "Display window &geometry when moving or resizing" +msgid "Display when moving or resizing" +msgstr "Verstoon venster geometrie wanneer 'n venster skuif of hervergroot" + +#. i18n: ectx: property (text), widget (QLabel, borderSnapLabel) +#: moving.ui:40 +#, fuzzy, kde-format +#| msgid "&Border snap zone:" +msgid "Screen &edge snap zone:" +msgstr "Grens klamp sone:" + +#. i18n: ectx: property (whatsThis), widget (QSpinBox, kcfg_BorderSnapZone) +#: moving.ui:50 +#, fuzzy, kde-format +#| msgid "" +#| "Here you can set the snap zone for screen borders, i.e. the 'strength' of " +#| "the magnetic field which will make windows snap to the border when moved " +#| "near it." +msgid "" +"Here you can set the snap zone for screen edges, i.e. the 'strength' of the " +"magnetic field which will make windows snap to the border when moved near it." +msgstr "" +"Hier kan jy die klamp sone van die skerm rame stel, byvoorbeeld die " +"'sterkte' van die magnetiese veld wat sal veroorsaak dat vensters aan die " +"raam klamp wanneer hulle naby daaraan gekuif word." + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_BorderSnapZone) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_WindowSnapZone) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_CenterSnapZone) +#: moving.ui:56 moving.ui:78 moving.ui:100 +#, kde-format +msgid " px" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QSpinBox, kcfg_WindowSnapZone) +#: moving.ui:72 +#, fuzzy, kde-format +#| msgid "" +#| "Here you can set the snap zone for windows, i.e. the 'strength' of the " +#| "magnetic field which will make windows snap to each other when they're " +#| "moved near another window." +msgid "" +"Here you can set the snap zone for windows, i.e. the 'strength' of the " +"magnetic field which will make windows snap to each other when they are " +"moved near another window." +msgstr "" +"Hier kan jy die klamp sone van die vensters stel, byvoorbeeld die 'sterkte' " +"van die magnetiese veld wat sal veroorsaak dat vensters aan mekaar klamp " +"wanneer hulle naby aan 'n ander venster gekuif word." + +#. i18n: ectx: property (whatsThis), widget (QSpinBox, kcfg_CenterSnapZone) +#: moving.ui:94 +#, fuzzy, kde-format +#| msgid "" +#| "Here you can set the snap zone for screen borders, i.e. the 'strength' of " +#| "the magnetic field which will make windows snap to the border when moved " +#| "near it." +msgid "" +"Here you can set the snap zone for the screen center, i.e. the 'strength' of " +"the magnetic field which will make windows snap to the center of the screen " +"when moved near it." +msgstr "" +"Hier kan jy die klamp sone van die skerm rame stel, byvoorbeeld die " +"'sterkte' van die magnetiese veld wat sal veroorsaak dat vensters aan die " +"raam klamp wanneer hulle naby daaraan gekuif word." + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_SnapOnlyWhenOverlapping) +#: moving.ui:113 +#, kde-format +msgid "" +"Here you can set that windows will be only snapped if you try to overlap " +"them, i.e. they will not be snapped if the windows comes only near another " +"window or border." +msgstr "" +"Hier kan jy kies dat vensters net klamp as jy probeer om dit oor mekaar te " +"plaas, hulle sal byvoorbeeld nie klamp wanneer 'n venster naby 'n ander " +"venster of raam kom nie." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_SnapOnlyWhenOverlapping) +#: moving.ui:116 +#, fuzzy, kde-format +#| msgid "Snap windows onl&y when overlapping" +msgid "Only when overlapping" +msgstr "Klamp vensters slegs wanneer oorvleuel" + +#. i18n: ectx: property (text), widget (QLabel, windowSnapLabel) +#: moving.ui:123 +#, kde-format +msgid "&Window snap zone:" +msgstr "Venster klamp sone:" + +#. i18n: ectx: property (text), widget (QLabel, centerSnaplabel) +#: moving.ui:133 +#, fuzzy, kde-format +#| msgid "&Border snap zone:" +msgid "&Center snap zone:" +msgstr "Grens klamp sone:" + +#. i18n: ectx: property (text), widget (QLabel, OverlapSnapLabel) +#: moving.ui:143 +#, fuzzy, kde-format +#| msgid "Inactive windows:" +msgid "&Snap windows:" +msgstr "Onaktiewe venster" + +#: windows.cpp:87 +#, kde-format +msgid "" +"Click to focus: A window becomes active when you click into it. " +"This behavior is common on other operating systems and likely what you want." +msgstr "" + +#: windows.cpp:91 +#, kde-format +msgid "" +"Click to focus (mouse precedence): Mostly the same as Click to " +"focus. If an active window has to be chosen by the system (eg. because " +"the currently active one was closed) the window under the mouse is the " +"preferred candidate. Unusual, but possible variant of Click to focus." +msgstr "" + +#: windows.cpp:96 +#, kde-format +msgid "" +"Focus follows mouse: Moving the mouse onto a window will activate " +"it. Eg. windows randomly appearing under the mouse will not gain the focus. " +"Focus stealing prevention takes place as usual. Think as Click " +"to focus just without having to actually click." +msgstr "" + +#: windows.cpp:100 +#, kde-format +msgid "" +"This is mostly the same as Focus follows mouse. If an active window " +"has to be chosen by the system (eg. because the currently active one was " +"closed) the window under the mouse is the preferred candidate. Choose this, " +"if you want a hover controlled focus." +msgstr "" + +#: windows.cpp:105 +#, kde-format +msgid "" +"Focus under mouse: The focus always remains on the window under the " +"mouse.
Warning: Focus stealing prevention and " +"the tabbox ('Alt+Tab') contradict the activation policy and will " +"not work. You very likely want to use Focus follows mouse (mouse " +"precedence) instead!" +msgstr "" + +#: windows.cpp:109 +#, kde-format +msgid "" +"Focus strictly under mouse: The focus is always on the window under " +"the mouse (in doubt nowhere) very much like the focus behavior in an " +"unmanaged legacy X11 environment.
Warning: Focus " +"stealing prevention and the tabbox ('Alt+Tab') contradict the " +"activation policy and will not work. You very likely want to use Focus " +"follows mouse (mouse precedence) instead!" +msgstr "" \ No newline at end of file diff --git a/po/af/kwin.po b/po/af/kwin.po new file mode 100644 index 0000000..c654742 --- /dev/null +++ b/po/af/kwin.po @@ -0,0 +1,2624 @@ +# UTF-8 test:äëïöü +# Copyright (C) 2001, 2005 Free Software Foundation, Inc. +# Frikkie Thirion , 2001,2002. +# Kobus Venter , 2005. +# +msgid "" +msgstr "" +"Project-Id-Version: kwin stable\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-11-25 08:45+0100\n" +"PO-Revision-Date: 2005-11-28 12:28+0200\n" +"Last-Translator: Kobus Venter \n" +"Language-Team: AFRIKAANS \n" +"Language: af\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: KBabel 1.10\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "WEB-Vertaler (http://kde.af.org.za), Kobus Venter" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "frix@expertron.co.za, kabousv@therugby.co.za" + +#: abstract_client.cpp:2729 +#, kde-format +msgctxt "Application is not responding, appended to window title" +msgid "(Not Responding)" +msgstr "" + +#: abstract_wayland_output.cpp:252 +#, kde-format +msgid "unknown" +msgstr "" + +#: colorcorrection/manager.cpp:58 +#, kde-format +msgctxt "Night Color was disabled" +msgid "Night Color Off" +msgstr "" + +#: colorcorrection/manager.cpp:59 +#, kde-format +msgctxt "Night Color was enabled" +msgid "Night Color On" +msgstr "" + +#: colorcorrection/manager.cpp:228 colorcorrection/manager.cpp:231 +#: colorcorrection/manager.cpp:238 +#, kde-format +msgid "Toggle Night Color" +msgstr "" + +#: composite.cpp:926 +#, kde-format +msgid "" +"Desktop effects have been suspended by another application.
You can " +"resume using the '%1' shortcut." +msgstr "" + +#: debug_console.cpp:65 +#, kde-format +msgid "Timestamp" +msgstr "" + +#: debug_console.cpp:70 +#, kde-format +msgid "Timestamp (µsec)" +msgstr "" + +#: debug_console.cpp:77 +#, kde-format +msgctxt "A mouse button" +msgid "Left" +msgstr "" + +#: debug_console.cpp:79 +#, kde-format +msgctxt "A mouse button" +msgid "Right" +msgstr "" + +#: debug_console.cpp:81 +#, kde-format +msgctxt "A mouse button" +msgid "Middle" +msgstr "" + +#: debug_console.cpp:83 +#, kde-format +msgctxt "A mouse button" +msgid "Back" +msgstr "" + +#: debug_console.cpp:85 +#, kde-format +msgctxt "A mouse button" +msgid "Forward" +msgstr "" + +#: debug_console.cpp:87 +#, kde-format +msgctxt "A mouse button" +msgid "Task" +msgstr "" + +#: debug_console.cpp:89 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 4" +msgstr "" + +#: debug_console.cpp:91 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 5" +msgstr "" + +#: debug_console.cpp:93 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 6" +msgstr "" + +#: debug_console.cpp:95 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 7" +msgstr "" + +#: debug_console.cpp:97 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 8" +msgstr "" + +#: debug_console.cpp:99 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 9" +msgstr "" + +#: debug_console.cpp:101 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 10" +msgstr "" + +#: debug_console.cpp:103 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 11" +msgstr "" + +#: debug_console.cpp:105 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 12" +msgstr "" + +#: debug_console.cpp:107 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 13" +msgstr "" + +#: debug_console.cpp:109 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 14" +msgstr "" + +#: debug_console.cpp:111 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 15" +msgstr "" + +#: debug_console.cpp:113 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 16" +msgstr "" + +#: debug_console.cpp:115 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 17" +msgstr "" + +#: debug_console.cpp:117 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 18" +msgstr "" + +#: debug_console.cpp:119 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 19" +msgstr "" + +#: debug_console.cpp:121 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 20" +msgstr "" + +#: debug_console.cpp:123 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 21" +msgstr "" + +#: debug_console.cpp:125 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 22" +msgstr "" + +#: debug_console.cpp:127 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 23" +msgstr "" + +#: debug_console.cpp:129 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 24" +msgstr "" + +#: debug_console.cpp:138 debug_console.cpp:140 +#, kde-format +msgid "Input Device" +msgstr "" + +#: debug_console.cpp:138 +#, kde-format +msgctxt "The input device of the event is not known" +msgid "Unknown" +msgstr "" + +#: debug_console.cpp:175 +#, kde-format +msgctxt "A mouse pointer motion event" +msgid "Pointer Motion" +msgstr "" + +#: debug_console.cpp:182 +#, kde-format +msgctxt "The relative mouse movement" +msgid "Delta" +msgstr "" + +#: debug_console.cpp:186 +#, kde-format +msgctxt "The relative mouse movement" +msgid "Delta (not accelerated)" +msgstr "" + +#: debug_console.cpp:189 +#, kde-format +msgctxt "The global mouse pointer position" +msgid "Global Position" +msgstr "" + +#: debug_console.cpp:193 +#, kde-format +msgctxt "A mouse pointer button press event" +msgid "Pointer Button Press" +msgstr "" + +#: debug_console.cpp:196 debug_console.cpp:204 +#, kde-format +msgctxt "A button in a mouse press/release event" +msgid "Button" +msgstr "" + +#: debug_console.cpp:197 debug_console.cpp:205 +#, kde-format +msgctxt "A button in a mouse press/release event" +msgid "Native Button code" +msgstr "" + +#: debug_console.cpp:198 debug_console.cpp:206 +#, kde-format +msgctxt "All currently pressed buttons in a mouse press/release event" +msgid "Pressed Buttons" +msgstr "" + +#: debug_console.cpp:201 +#, kde-format +msgctxt "A mouse pointer button release event" +msgid "Pointer Button Release" +msgstr "" + +#: debug_console.cpp:221 +#, kde-format +msgctxt "A mouse pointer axis (wheel) event" +msgid "Pointer Axis" +msgstr "" + +#: debug_console.cpp:225 +#, kde-format +msgctxt "The orientation of a pointer axis event" +msgid "Orientation" +msgstr "" + +#: debug_console.cpp:226 +#, kde-format +msgctxt "An orientation of a pointer axis event" +msgid "Horizontal" +msgstr "" + +#: debug_console.cpp:227 +#, kde-format +msgctxt "An orientation of a pointer axis event" +msgid "Vertical" +msgstr "" + +#: debug_console.cpp:228 +#, kde-format +msgctxt "The angle delta of a pointer axis event" +msgid "Delta" +msgstr "" + +#: debug_console.cpp:243 +#, kde-format +msgctxt "A key press event" +msgid "Key Press" +msgstr "" + +#: debug_console.cpp:246 +#, kde-format +msgctxt "A key release event" +msgid "Key Release" +msgstr "" + +#: debug_console.cpp:255 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Shift" +msgstr "" + +#: debug_console.cpp:259 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Control" +msgstr "" + +#: debug_console.cpp:263 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Alt" +msgstr "" + +#: debug_console.cpp:267 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Meta" +msgstr "" + +#: debug_console.cpp:271 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Keypad" +msgstr "" + +#: debug_console.cpp:275 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Group-switch" +msgstr "" + +#: debug_console.cpp:281 +#, kde-format +msgctxt "Whether the event is an automatic key repeat" +msgid "Repeat" +msgstr "" + +#: debug_console.cpp:285 +#, kde-format +msgctxt "The code as read from the input device" +msgid "Scan code" +msgstr "" + +#: debug_console.cpp:286 +#, kde-format +msgctxt "Key according to Qt" +msgid "Qt::Key code" +msgstr "" + +#: debug_console.cpp:288 +#, kde-format +msgctxt "The translated code to an Xkb symbol" +msgid "Xkb symbol" +msgstr "" + +#: debug_console.cpp:289 +#, kde-format +msgctxt "The translated code interpreted as text" +msgid "Utf8" +msgstr "" + +#: debug_console.cpp:290 +#, kde-format +msgctxt "The currently active modifiers" +msgid "Modifiers" +msgstr "" + +#: debug_console.cpp:302 +#, kde-format +msgctxt "A touch down event" +msgid "Touch down" +msgstr "" + +#: debug_console.cpp:304 debug_console.cpp:319 debug_console.cpp:334 +#, kde-format +msgctxt "The id of the touch point in the touch event" +msgid "Point identifier" +msgstr "" + +#: debug_console.cpp:305 debug_console.cpp:320 +#, kde-format +msgctxt "The global position of the touch point" +msgid "Global position" +msgstr "" + +#: debug_console.cpp:317 +#, kde-format +msgctxt "A touch motion event" +msgid "Touch Motion" +msgstr "" + +#: debug_console.cpp:332 +#, kde-format +msgctxt "A touch up event" +msgid "Touch Up" +msgstr "" + +#: debug_console.cpp:345 +#, kde-format +msgctxt "A pinch gesture is started" +msgid "Pinch start" +msgstr "" + +#: debug_console.cpp:347 +#, kde-format +msgctxt "Number of fingers in this pinch gesture" +msgid "Finger count" +msgstr "" + +#: debug_console.cpp:358 +#, kde-format +msgctxt "A pinch gesture is updated" +msgid "Pinch update" +msgstr "" + +#: debug_console.cpp:360 +#, kde-format +msgctxt "Current scale in pinch gesture" +msgid "Scale" +msgstr "" + +#: debug_console.cpp:361 +#, kde-format +msgctxt "Current angle in pinch gesture" +msgid "Angle delta" +msgstr "" + +#: debug_console.cpp:362 +#, kde-format +msgctxt "Current delta in pinch gesture" +msgid "Delta x" +msgstr "" + +#: debug_console.cpp:363 +#, kde-format +msgctxt "Current delta in pinch gesture" +msgid "Delta y" +msgstr "" + +#: debug_console.cpp:374 +#, kde-format +msgctxt "A pinch gesture ended" +msgid "Pinch end" +msgstr "" + +#: debug_console.cpp:386 +#, kde-format +msgctxt "A pinch gesture got cancelled" +msgid "Pinch cancelled" +msgstr "" + +#: debug_console.cpp:398 +#, kde-format +msgctxt "A swipe gesture is started" +msgid "Swipe start" +msgstr "" + +#: debug_console.cpp:400 +#, kde-format +msgctxt "Number of fingers in this swipe gesture" +msgid "Finger count" +msgstr "" + +#: debug_console.cpp:411 +#, kde-format +msgctxt "A swipe gesture is updated" +msgid "Swipe update" +msgstr "" + +#: debug_console.cpp:413 +#, kde-format +msgctxt "Current delta in swipe gesture" +msgid "Delta x" +msgstr "" + +#: debug_console.cpp:414 +#, kde-format +msgctxt "Current delta in swipe gesture" +msgid "Delta y" +msgstr "" + +#: debug_console.cpp:425 +#, kde-format +msgctxt "A swipe gesture ended" +msgid "Swipe end" +msgstr "" + +#: debug_console.cpp:437 +#, kde-format +msgctxt "A swipe gesture got cancelled" +msgid "Swipe cancelled" +msgstr "" + +#: debug_console.cpp:449 +#, fuzzy, kde-format +#| msgid "Switch to Desktop 10" +msgctxt "A hardware switch (e.g. notebook lid) got toggled" +msgid "Switch toggled" +msgstr "Wissel na Werkskerm 10" + +#: debug_console.cpp:457 +#, kde-format +msgctxt "Name of a hardware switch" +msgid "Notebook lid" +msgstr "" + +#: debug_console.cpp:459 +#, kde-format +msgctxt "Name of a hardware switch" +msgid "Tablet mode" +msgstr "" + +#: debug_console.cpp:461 +#, fuzzy, kde-format +#| msgid "Switch to Desktop 10" +msgctxt "A hardware switch" +msgid "Switch" +msgstr "Wissel na Werkskerm 10" + +#: debug_console.cpp:465 +#, kde-format +msgctxt "The hardware switch got turned off" +msgid "Off" +msgstr "" + +#: debug_console.cpp:468 +#, kde-format +msgctxt "The hardware switch got turned on" +msgid "On" +msgstr "" + +#: debug_console.cpp:473 +#, kde-format +msgctxt "State of a hardware switch (on/off)" +msgid "State" +msgstr "" + +#: debug_console.cpp:488 +#, kde-format +msgid "Tablet Tool" +msgstr "" + +#: debug_console.cpp:489 +#, kde-format +msgid "EventType" +msgstr "" + +#: debug_console.cpp:490 debug_console.cpp:537 debug_console.cpp:549 +#, kde-format +msgid "Position" +msgstr "" + +#: debug_console.cpp:492 +#, kde-format +msgid "Tilt" +msgstr "" + +#: debug_console.cpp:494 +#, kde-format +msgid "Rotation" +msgstr "" + +#: debug_console.cpp:495 +#, kde-format +msgid "Pressure" +msgstr "" + +#: debug_console.cpp:496 +#, fuzzy, kde-format +#| msgid "Mouse Emulation" +msgid "Buttons" +msgstr "Muis Emulering" + +#. i18n: ectx: property (title), widget (QGroupBox, modifiersBox) +#: debug_console.cpp:497 debug_console.ui:356 +#, kde-format +msgid "Modifiers" +msgstr "" + +#: debug_console.cpp:510 +#, kde-format +msgid "Tablet Tool Button" +msgstr "" + +#: debug_console.cpp:511 debug_console.cpp:526 +#, fuzzy, kde-format +#| msgid "Mouse Emulation" +msgid "Pressed Buttons" +msgstr "Muis Emulering" + +#: debug_console.cpp:525 +#, kde-format +msgid "Tablet Pad Button" +msgstr "" + +#: debug_console.cpp:535 +#, kde-format +msgid "Tablet Pad Strip" +msgstr "" + +#: debug_console.cpp:536 debug_console.cpp:548 +#, kde-format +msgid "Number" +msgstr "" + +#: debug_console.cpp:538 debug_console.cpp:550 +#, kde-format +msgid "isFinger" +msgstr "" + +#: debug_console.cpp:547 +#, kde-format +msgid "Tablet Pad Ring" +msgstr "" + +#: debug_console.cpp:735 +#, fuzzy, kde-format +#| msgid "Mouse Emulation" +msgid "No Mouse Buttons" +msgstr "Muis Emulering" + +#: debug_console.cpp:739 +#, kde-format +msgctxt "Mouse Button" +msgid "left" +msgstr "" + +#: debug_console.cpp:742 +#, kde-format +msgctxt "Mouse Button" +msgid "right" +msgstr "" + +#: debug_console.cpp:745 +#, kde-format +msgctxt "Mouse Button" +msgid "middle" +msgstr "" + +#: debug_console.cpp:748 +#, kde-format +msgctxt "Mouse Button" +msgid "back" +msgstr "" + +#: debug_console.cpp:751 +#, kde-format +msgctxt "Mouse Button" +msgid "forward" +msgstr "" + +#: debug_console.cpp:754 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 1" +msgstr "" + +#: debug_console.cpp:757 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 2" +msgstr "" + +#: debug_console.cpp:760 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 3" +msgstr "" + +#: debug_console.cpp:763 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 4" +msgstr "" + +#: debug_console.cpp:766 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 5" +msgstr "" + +#: debug_console.cpp:769 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 6" +msgstr "" + +#: debug_console.cpp:772 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 7" +msgstr "" + +#: debug_console.cpp:775 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 8" +msgstr "" + +#: debug_console.cpp:778 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 9" +msgstr "" + +#: debug_console.cpp:781 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 10" +msgstr "" + +#: debug_console.cpp:784 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 11" +msgstr "" + +#: debug_console.cpp:787 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 12" +msgstr "" + +#: debug_console.cpp:790 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 13" +msgstr "" + +#: debug_console.cpp:793 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 14" +msgstr "" + +#: debug_console.cpp:796 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 15" +msgstr "" + +#: debug_console.cpp:799 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 16" +msgstr "" + +#: debug_console.cpp:802 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 17" +msgstr "" + +#: debug_console.cpp:805 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 18" +msgstr "" + +#: debug_console.cpp:808 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 19" +msgstr "" + +#: debug_console.cpp:811 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 20" +msgstr "" + +#: debug_console.cpp:814 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 21" +msgstr "" + +#: debug_console.cpp:817 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 22" +msgstr "" + +#: debug_console.cpp:820 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 23" +msgstr "" + +#: debug_console.cpp:823 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 24" +msgstr "" + +#: debug_console.cpp:826 +#, kde-format +msgctxt "Mouse Button" +msgid "task" +msgstr "" + +#: debug_console.cpp:1176 +#, fuzzy, kde-format +#| msgid "Close Window" +msgid "X11 Client Windows" +msgstr "Sluit Venster" + +#: debug_console.cpp:1178 +#, kde-format +msgid "X11 Unmanaged Windows" +msgstr "" + +#: debug_console.cpp:1180 +#, fuzzy, kde-format +#| msgid "Shade Window" +msgid "Wayland Windows" +msgstr "Verskadu Vensters" + +#: debug_console.cpp:1182 +#, fuzzy, kde-format +#| msgid "Raise Window" +msgid "Internal Windows" +msgstr "Lig Venster" + +#. i18n: ectx: property (text), widget (QPushButton, quitButton) +#: debug_console.ui:32 +#, kde-format +msgid "Quit Debug Console" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, windows) +#: debug_console.ui:45 +#, kde-format +msgid "Windows" +msgstr "Vensters" + +#. i18n: ectx: attribute (title), widget (QWidget, surfaces) +#: debug_console.ui:59 +#, kde-format +msgid "Surfaces" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, input) +#: debug_console.ui:69 +#, kde-format +msgid "Input Events" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, inputDevices) +#: debug_console.ui:86 +#, kde-format +msgid "Input Devices" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, tab) +#: debug_console.ui:96 +#, kde-format +msgid "OpenGL" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, noOpenGLLabel) +#: debug_console.ui:102 +#, kde-format +msgid "No OpenGL compositor running" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, driverInfoBox) +#: debug_console.ui:130 +#, kde-format +msgid "OpenGL (ES) driver information" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: debug_console.ui:136 +#, kde-format +msgid "Vendor:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: debug_console.ui:143 +#, kde-format +msgid "Renderer:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: debug_console.ui:150 +#, kde-format +msgid "Version:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: debug_console.ui:157 +#, kde-format +msgid "Shading Language Version:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: debug_console.ui:164 +#, kde-format +msgid "Driver:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: debug_console.ui:171 +#, kde-format +msgid "GPU class:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: debug_console.ui:178 +#, kde-format +msgid "OpenGL Version:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_8) +#: debug_console.ui:185 +#, kde-format +msgid "GLSL Version:" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, platformExtensionsBox) +#: debug_console.ui:251 +#, kde-format +msgid "Platform Extensions" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, glExtensionsBox) +#: debug_console.ui:267 +#, kde-format +msgid "OpenGL (ES) Extensions" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, keyboard) +#: debug_console.ui:288 +#, kde-format +msgid "Keyboard" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, layoutBox) +#: debug_console.ui:315 +#, kde-format +msgid "Keymap Layouts" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_9) +#: debug_console.ui:337 +#, fuzzy, kde-format +#| msgid "Configur&e Window Behavior..." +msgid "Current Layout:" +msgstr "Konfigureer Venster Gedrag..." + +#. i18n: ectx: property (title), widget (QGroupBox, activeModifiersBox) +#: debug_console.ui:372 +#, kde-format +msgid "Active Modifiers" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, ledsBox) +#: debug_console.ui:388 +#, kde-format +msgid "LEDs" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, activeLedsBox) +#: debug_console.ui:404 +#, kde-format +msgid "Active LEDs" +msgstr "" + +#: helpers/killer/killer.cpp:30 +#, fuzzy, kde-format +#| msgid "KDE window manager" +msgid "Window Manager" +msgstr "Kde venster bestuurder." + +#: helpers/killer/killer.cpp:35 +#, kde-format +msgid "PID of the application to terminate" +msgstr "" + +#: helpers/killer/killer.cpp:35 +#, kde-format +msgid "pid" +msgstr "" + +#: helpers/killer/killer.cpp:37 +#, kde-format +msgid "Hostname on which the application is running" +msgstr "" + +#: helpers/killer/killer.cpp:37 +#, kde-format +msgid "hostname" +msgstr "" + +#: helpers/killer/killer.cpp:39 +#, kde-format +msgid "Caption of the window to be terminated" +msgstr "" + +#: helpers/killer/killer.cpp:39 +#, kde-format +msgid "caption" +msgstr "" + +#: helpers/killer/killer.cpp:41 +#, kde-format +msgid "Name of the application to be terminated" +msgstr "" + +#: helpers/killer/killer.cpp:41 +#, kde-format +msgid "name" +msgstr "" + +#: helpers/killer/killer.cpp:43 +#, kde-format +msgid "ID of resource belonging to the application" +msgstr "" + +#: helpers/killer/killer.cpp:43 +#, kde-format +msgid "id" +msgstr "" + +#: helpers/killer/killer.cpp:45 +#, kde-format +msgid "Time of user action causing termination" +msgstr "" + +#: helpers/killer/killer.cpp:45 +#, kde-format +msgid "time" +msgstr "" + +#: helpers/killer/killer.cpp:47 +#, kde-format +msgid "KWin helper utility" +msgstr "Kwin hulp program" + +#: helpers/killer/killer.cpp:71 +#, kde-format +msgid "This helper utility is not supposed to be called directly." +msgstr "Hierdie hulp program is nie veronderstel om direk geroep te word nie." + +#: helpers/killer/killer.cpp:81 +#, kde-format +msgctxt "@info" +msgid "Application \"%1\" is not responding" +msgstr "" + +#: helpers/killer/killer.cpp:83 +#, kde-kuit-format +msgctxt "@info" +msgid "" +"

You tried to close window \"%1\" from application \"%2\" (Process ID: %3) " +"but the application is not responding.

" +msgstr "" + +#: helpers/killer/killer.cpp:85 +#, kde-kuit-format +msgctxt "@info" +msgid "" +"

You tried to close window \"%1\" from application \"%2\" (Process ID: " +"%3), running on host \"%4\", but the application is not responding.

" +msgstr "" + +#: helpers/killer/killer.cpp:88 +#, kde-kuit-format +msgctxt "@info" +msgid "" +"

Do you want to terminate this application?

Terminating the " +"application will close all of its child windows. Any unsaved data will be " +"lost.

" +msgstr "" + +#: helpers/killer/killer.cpp:92 +#, kde-format +msgid "&Terminate Application %1" +msgstr "" + +#: helpers/killer/killer.cpp:93 +#, kde-format +msgid "Wait Longer" +msgstr "" + +#: keyboard_layout.cpp:110 keyboard_layout.cpp:111 +#, kde-format +msgctxt "tooltip title" +msgid "Keyboard Layout" +msgstr "" + +#: keyboard_layout.cpp:258 +#, fuzzy, kde-format +#| msgid "Configur&e Window Behavior..." +msgid "Configure Layouts..." +msgstr "Konfigureer Venster Gedrag..." + +#: killwindow.cpp:33 +#, kde-format +msgid "" +"Select window to force close with left click or enter.\n" +"Escape or right click to cancel." +msgstr "" + +#: kwinbindings.cpp:38 +#, kde-format +msgid "Window Operations Menu" +msgstr "Venster Operasies Kieslys" + +#: kwinbindings.cpp:40 +#, kde-format +msgid "Close Window" +msgstr "Sluit Venster" + +#: kwinbindings.cpp:42 +#, kde-format +msgid "Maximize Window" +msgstr "Vergoot Venster" + +#: kwinbindings.cpp:44 +#, kde-format +msgid "Maximize Window Vertically" +msgstr "Vergroot Venster Vertikaal" + +#: kwinbindings.cpp:46 +#, kde-format +msgid "Maximize Window Horizontally" +msgstr "Vergroot Venster Horisontaal" + +#: kwinbindings.cpp:48 +#, kde-format +msgid "Minimize Window" +msgstr "Verklein Venster" + +#: kwinbindings.cpp:50 +#, kde-format +msgid "Shade Window" +msgstr "Verskadu Vensters" + +#: kwinbindings.cpp:52 +#, kde-format +msgid "Move Window" +msgstr "Skuif Vensters" + +#: kwinbindings.cpp:54 +#, kde-format +msgid "Resize Window" +msgstr "Hervergroot Venster" + +#: kwinbindings.cpp:56 +#, kde-format +msgid "Raise Window" +msgstr "Lig Venster" + +#: kwinbindings.cpp:58 +#, kde-format +msgid "Lower Window" +msgstr "Verlaag Vensters" + +#: kwinbindings.cpp:60 +#, kde-format +msgid "Toggle Window Raise/Lower" +msgstr "Wissel Venster Lig/Sagter" + +#: kwinbindings.cpp:62 +#, kde-format +msgid "Make Window Fullscreen" +msgstr "Maak Venster Volskerm" + +#: kwinbindings.cpp:64 +#, kde-format +msgid "Hide Window Border" +msgstr "Versteek Venster Raam" + +#: kwinbindings.cpp:66 +#, kde-format +msgid "Keep Window Above Others" +msgstr "Hou Venster Bo-Op Ander" + +#: kwinbindings.cpp:68 +#, kde-format +msgid "Keep Window Below Others" +msgstr "Hou Venster Onder Ander" + +#: kwinbindings.cpp:70 +#, kde-format +msgid "Activate Window Demanding Attention" +msgstr "Aktiveer Venster Versoek Aandag" + +#: kwinbindings.cpp:72 +#, kde-format +msgid "Setup Window Shortcut" +msgstr "Stel Venster Kortpad" + +#: kwinbindings.cpp:74 +#, kde-format +msgid "Pack Window to the Right" +msgstr "Verpak Venster na Regs" + +#: kwinbindings.cpp:76 +#, kde-format +msgid "Pack Window to the Left" +msgstr "Verpak Venster na Links" + +#: kwinbindings.cpp:78 +#, kde-format +msgid "Pack Window Up" +msgstr "Verpak Venster na Bo" + +#: kwinbindings.cpp:80 +#, kde-format +msgid "Pack Window Down" +msgstr "Verpak Venster Af" + +#: kwinbindings.cpp:82 +#, kde-format +msgid "Pack Grow Window Horizontally" +msgstr "Verpak Groei Venster Horisontaal" + +#: kwinbindings.cpp:84 +#, kde-format +msgid "Pack Grow Window Vertically" +msgstr "Verpak Groei Venster Vertikaal" + +#: kwinbindings.cpp:86 +#, kde-format +msgid "Pack Shrink Window Horizontally" +msgstr "Verpak Verklein Venster Horisontaal" + +#: kwinbindings.cpp:88 +#, kde-format +msgid "Pack Shrink Window Vertically" +msgstr "Verpak Verklein Venster Vertikaal" + +#: kwinbindings.cpp:90 +#, fuzzy, kde-format +#| msgid "Pack Window to the Left" +msgid "Quick Tile Window to the Left" +msgstr "Verpak Venster na Links" + +#: kwinbindings.cpp:92 +#, fuzzy, kde-format +#| msgid "Pack Window to the Right" +msgid "Quick Tile Window to the Right" +msgstr "Verpak Venster na Regs" + +#: kwinbindings.cpp:94 +#, fuzzy, kde-format +#| msgid "Pack Window to the Left" +msgid "Quick Tile Window to the Top" +msgstr "Verpak Venster na Links" + +#: kwinbindings.cpp:96 +#, fuzzy, kde-format +#| msgid "Pack Window to the Left" +msgid "Quick Tile Window to the Bottom" +msgstr "Verpak Venster na Links" + +#: kwinbindings.cpp:98 +#, fuzzy, kde-format +#| msgid "Pack Window to the Left" +msgid "Quick Tile Window to the Top Left" +msgstr "Verpak Venster na Links" + +#: kwinbindings.cpp:100 +#, fuzzy, kde-format +#| msgid "Pack Window to the Left" +msgid "Quick Tile Window to the Bottom Left" +msgstr "Verpak Venster na Links" + +#: kwinbindings.cpp:102 +#, fuzzy, kde-format +#| msgid "Pack Window to the Right" +msgid "Quick Tile Window to the Top Right" +msgstr "Verpak Venster na Regs" + +#: kwinbindings.cpp:104 +#, fuzzy, kde-format +#| msgid "Pack Window to the Right" +msgid "Quick Tile Window to the Bottom Right" +msgstr "Verpak Venster na Regs" + +#: kwinbindings.cpp:106 +#, fuzzy, kde-format +#| msgid "Switch to Desktop 10" +msgid "Switch to Window Above" +msgstr "Wissel na Werkskerm 10" + +#: kwinbindings.cpp:108 +#, fuzzy, kde-format +#| msgid "Switch to Previous Desktop" +msgid "Switch to Window Below" +msgstr "Wissel na Vorige Werkskerm" + +#: kwinbindings.cpp:110 +#, fuzzy, kde-format +#| msgid "Pack Window to the Right" +msgid "Switch to Window to the Right" +msgstr "Verpak Venster na Regs" + +#: kwinbindings.cpp:112 +#, fuzzy, kde-format +#| msgid "Pack Window to the Left" +msgid "Switch to Window to the Left" +msgstr "Verpak Venster na Links" + +#: kwinbindings.cpp:114 +#, kde-format +msgid "Increase Opacity of Active Window by 5 %" +msgstr "" + +#: kwinbindings.cpp:116 +#, kde-format +msgid "Decrease Opacity of Active Window by 5 %" +msgstr "" + +#: kwinbindings.cpp:119 +#, kde-format +msgid "Keep Window on All Desktops" +msgstr "Hou Venster op Alle Werkskerm" + +#: kwinbindings.cpp:123 +#, fuzzy, kde-format +#| msgid "Window to Desktop 1" +msgid "Window to Desktop %1" +msgstr "Venster na Werkskerm 1" + +#: kwinbindings.cpp:125 +#, kde-format +msgid "Window to Next Desktop" +msgstr "Venster na Volgende Werkskerm" + +#: kwinbindings.cpp:126 +#, kde-format +msgid "Window to Previous Desktop" +msgstr "Venster na Vorige Werkskerm" + +#: kwinbindings.cpp:127 +#, kde-format +msgid "Window One Desktop to the Right" +msgstr "Vesnter Een Werkskerm na Regs" + +#: kwinbindings.cpp:128 +#, kde-format +msgid "Window One Desktop to the Left" +msgstr "Venster Een Werkskerm na Links" + +#: kwinbindings.cpp:129 +#, kde-format +msgid "Window One Desktop Up" +msgstr "Venster Een Werkskerm Op" + +#: kwinbindings.cpp:130 +#, kde-format +msgid "Window One Desktop Down" +msgstr "Venster Een Werkskerm Af" + +#: kwinbindings.cpp:133 +#, fuzzy, kde-format +#| msgid "Window to Desktop 1" +msgid "Window to Screen %1" +msgstr "Venster na Werkskerm 1" + +#: kwinbindings.cpp:135 +#, fuzzy, kde-format +#| msgid "Window to Next Desktop" +msgid "Window to Next Screen" +msgstr "Venster na Volgende Werkskerm" + +#: kwinbindings.cpp:136 +#, fuzzy, kde-format +#| msgid "Window to Previous Desktop" +msgid "Window to Previous Screen" +msgstr "Venster na Vorige Werkskerm" + +#: kwinbindings.cpp:137 +#, fuzzy, kde-format +#| msgid "Switch to Desktop 1" +msgid "Show Desktop" +msgstr "Wissel na Werkskerm 1" + +#: kwinbindings.cpp:140 +#, fuzzy, kde-format +#| msgid "Switch to Desktop 1" +msgid "Switch to Screen %1" +msgstr "Wissel na Werkskerm 1" + +#: kwinbindings.cpp:143 +#, fuzzy, kde-format +#| msgid "Switch to Next Desktop" +msgid "Switch to Next Screen" +msgstr "Wissel na Volgende Werkskerm" + +#: kwinbindings.cpp:144 +#, fuzzy, kde-format +#| msgid "Switch to Previous Desktop" +msgid "Switch to Previous Screen" +msgstr "Wissel na Vorige Werkskerm" + +#: kwinbindings.cpp:146 +#, kde-format +msgid "Kill Window" +msgstr "Stop Venster" + +#: kwinbindings.cpp:147 +#, kde-format +msgid "Suspend Compositing" +msgstr "" + +#: kwinbindings.cpp:148 +#, kde-format +msgid "Invert Screen Colors" +msgstr "" + +#: main.cpp:184 main.cpp:214 +#, kde-format +msgid "KDE window manager" +msgstr "Kde venster bestuurder." + +#: main.cpp:189 +#, kde-format +msgid "KWin" +msgstr "Kwin" + +#: main.cpp:193 +#, fuzzy, kde-format +#| msgid "(c) 1999-2005, The KDE Developers" +msgid "(c) 1999-2019, The KDE Developers" +msgstr "(c) 1999-2005, Die Kde Ontwikkelaars" + +#: main.cpp:195 +#, kde-format +msgid "Matthias Ettrich" +msgstr "" + +#: main.cpp:196 +#, kde-format +msgid "Cristian Tibirna" +msgstr "" + +#: main.cpp:197 +#, kde-format +msgid "Daniel M. Duley" +msgstr "" + +#: main.cpp:198 +#, kde-format +msgid "Luboš Luňák" +msgstr "" + +#: main.cpp:199 +#, kde-format +msgid "Martin Flöser" +msgstr "" + +#: main.cpp:200 +#, kde-format +msgid "David Edmundson" +msgstr "" + +#: main.cpp:201 +#, kde-format +msgid "Roman Gilg" +msgstr "" + +#: main.cpp:202 +#, kde-format +msgid "Vlad Zahorodnii" +msgstr "" + +#: main.cpp:211 +#, kde-format +msgid "Disable configuration options" +msgstr "Skakel konfigusrie opsies af" + +#: main.cpp:212 +#, kde-format +msgid "Indicate that KWin has recently crashed n times" +msgstr "" + +#: main_wayland.cpp:459 +#, kde-format +msgid "Start a rootless Xwayland server." +msgstr "" + +#: main_wayland.cpp:461 +#, kde-format +msgid "" +"Name of the Wayland socket to listen on. If not set \"wayland-0\" is used." +msgstr "" + +#: main_wayland.cpp:464 +#, kde-format +msgid "Render to framebuffer." +msgstr "" + +#: main_wayland.cpp:466 +#, kde-format +msgid "The framebuffer device to render to." +msgstr "" + +#: main_wayland.cpp:469 +#, kde-format +msgid "The X11 Display to use in windowed mode on platform X11." +msgstr "" + +#: main_wayland.cpp:472 +#, kde-format +msgid "The Wayland Display to use in windowed mode on platform Wayland." +msgstr "" + +#: main_wayland.cpp:474 +#, kde-format +msgid "Render to a virtual framebuffer." +msgstr "" + +#: main_wayland.cpp:476 +#, kde-format +msgid "The width for windowed mode. Default width is 1024." +msgstr "" + +#: main_wayland.cpp:480 +#, kde-format +msgid "The height for windowed mode. Default height is 768." +msgstr "" + +#: main_wayland.cpp:485 +#, kde-format +msgid "The scale for windowed mode. Default value is 1." +msgstr "" + +#: main_wayland.cpp:490 +#, kde-format +msgid "" +"The number of windows to open as outputs in windowed mode. Default value is 1" +msgstr "" + +#: main_wayland.cpp:520 +#, kde-format +msgid "Use libhybris hwcomposer" +msgstr "" + +#: main_wayland.cpp:526 +#, kde-format +msgid "" +"Enable libinput support for input events processing. Note: never use in a " +"nested session.\t(deprecated)" +msgstr "" + +#: main_wayland.cpp:529 +#, kde-format +msgid "Render through drm node." +msgstr "" + +#: main_wayland.cpp:536 +#, kde-format +msgid "Input method that KWin starts." +msgstr "" + +#: main_wayland.cpp:541 +#, kde-format +msgid "List all available backends and quit." +msgstr "" + +#: main_wayland.cpp:545 +#, kde-format +msgid "Starts the session in locked mode." +msgstr "" + +#: main_wayland.cpp:549 +#, kde-format +msgid "Starts the session without lock screen support." +msgstr "" + +#: main_wayland.cpp:553 +#, kde-format +msgid "Starts the session without global shortcuts support." +msgstr "" + +#: main_wayland.cpp:557 +#, kde-format +msgid "Exit after the session application, which is started by KWin, closed." +msgstr "" + +#: main_wayland.cpp:562 +#, kde-format +msgid "Applications to start once Wayland and Xwayland server are started" +msgstr "" + +#: main_x11.cpp:65 +#, kde-format +msgid "" +"KWin is unstable.\n" +"It seems to have crashed several times in a row.\n" +"You can select another window manager to run:" +msgstr "" + +#: main_x11.cpp:224 +#, kde-format +msgid "" +"kwin: unable to claim manager selection, another wm running? (try using --" +"replace)\n" +msgstr "" +"kwin: kon nie bestuurder seleksie vasmaak, is daar 'n ander wm wat loop? " +"(probeer --replace)\n" + +#: main_x11.cpp:241 +#, fuzzy, kde-format +#| msgid "" +#| "kwin: unable to claim manager selection, another wm running? (try using --" +#| "replace)\n" +msgid "kwin: another window manager is running (try using --replace)\n" +msgstr "" +"kwin: kon nie bestuurder seleksie vasmaak, is daar 'n ander wm wat loop? " +"(probeer --replace)\n" + +#: main_x11.cpp:437 +#, kde-format +msgid "Replace already-running ICCCM2.0-compliant window manager" +msgstr "Vervang ICCCM2.0 aanpasbare venster beheerder wat reeds loop" + +#: main_x11.cpp:444 +#, kde-format +msgid "Disable KActivities integration." +msgstr "" + +#: plugins/scenes/opengl/scene_opengl.cpp:535 +#, kde-format +msgid "Desktop effects were restarted due to a graphics reset" +msgstr "" + +#. i18n: ectx: label, entry (count), group (General) +#: rulebooksettingsbase.kcfg:9 +#, kde-format +msgid "Total rules count" +msgstr "" + +#. i18n: ectx: label, entry (description), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:10 +#, kde-format +msgid "Rule description" +msgstr "" + +#. i18n: ectx: label, entry (descriptionLegacy), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:13 +#, kde-format +msgid "Rule description (legacy)" +msgstr "" + +#. i18n: ectx: label, entry (DeleteRule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:16 +#, kde-format +msgid "Delete this rule (for use in imports)" +msgstr "" + +#. i18n: ectx: label, entry (wmclass), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:20 +#, kde-format +msgid "Window class (application)" +msgstr "" + +#. i18n: ectx: label, entry (wmclassmatch), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:23 +#, kde-format +msgid "Window class string match type" +msgstr "" + +#. i18n: ectx: label, entry (wmclasscomplete), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:29 +#, kde-format +msgid "Match whole window class" +msgstr "" + +#. i18n: ectx: label, entry (windowrole), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:34 +#, fuzzy, kde-format +#| msgid "Windows" +msgid "Window role" +msgstr "Vensters" + +#. i18n: ectx: label, entry (windowrolematch), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:37 +#, kde-format +msgid "Window role string match type" +msgstr "" + +#. i18n: ectx: label, entry (title), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:44 +#, fuzzy, kde-format +#| msgid "Windows" +msgid "Window title" +msgstr "Vensters" + +#. i18n: ectx: label, entry (titlematch), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:47 +#, kde-format +msgid "Window title string match type" +msgstr "" + +#. i18n: ectx: label, entry (clientmachine), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:54 +#, kde-format +msgid "Machine (hostname)" +msgstr "" + +#. i18n: ectx: label, entry (clientmachinematch), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:57 +#, kde-format +msgid "Machine string match type" +msgstr "" + +#. i18n: ectx: label, entry (types), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:64 +#, kde-format +msgid "Window types that match" +msgstr "" + +#. i18n: ectx: label, entry (placement), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:69 +#, kde-format +msgid "Initial placement" +msgstr "" + +#. i18n: ectx: label, entry (placementrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:74 +#, fuzzy, kde-format +#| msgid "&Fullscreen" +msgid "Initial placement rule type" +msgstr "Volskerm" + +#. i18n: ectx: label, entry (position), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:79 +#, fuzzy, kde-format +#| msgid "Window Operations Menu" +msgid "Window position" +msgstr "Venster Operasies Kieslys" + +#. i18n: ectx: label, entry (positionrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:83 +#, fuzzy, kde-format +#| msgid "Window to Desktop 10" +msgid "Window position rule type" +msgstr "Venster na Werkskerm 10" + +#. i18n: ectx: label, entry (size), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:90 +#, fuzzy, kde-format +#| msgid "Windows" +msgid "Window size" +msgstr "Vensters" + +#. i18n: ectx: label, entry (sizerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:93 +#, kde-format +msgid "Window size rule type" +msgstr "" + +#. i18n: ectx: label, entry (minsize), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:100 +#, kde-format +msgid "Window minimum size" +msgstr "" + +#. i18n: ectx: label, entry (minsizerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:104 +#, kde-format +msgid "Window minimum size rule type" +msgstr "" + +#. i18n: ectx: label, entry (maxsize), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:109 +#, kde-format +msgid "Window maximum size" +msgstr "" + +#. i18n: ectx: label, entry (maxsizerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:113 +#, kde-format +msgid "Window maximum size rule type" +msgstr "" + +#. i18n: ectx: label, entry (opacityactive), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:118 +#, kde-format +msgid "Active opacity" +msgstr "" + +#. i18n: ectx: label, entry (opacityactiverule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:124 +#, kde-format +msgid "Active opacity rule type" +msgstr "" + +#. i18n: ectx: label, entry (opacityinactive), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:129 +#, kde-format +msgid "Inactive opacity" +msgstr "" + +#. i18n: ectx: label, entry (opacityinactiverule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:135 +#, kde-format +msgid "Inactive opacity rule type" +msgstr "" + +#. i18n: ectx: label, entry (ignoregeometry), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:140 +#, kde-format +msgid "Ignore requested geometry" +msgstr "" + +#. i18n: ectx: label, entry (ignoregeometryrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:144 +#, kde-format +msgid "Ignore requested geometry rule type" +msgstr "" + +#. i18n: ectx: label, entry (desktop), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:151 +#, fuzzy, kde-format +#| msgid "Desktop %1" +msgid "Desktop number" +msgstr "Werkskerm %1" + +#. i18n: ectx: label, entry (desktoprule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:155 +#, fuzzy, kde-format +#| msgid "Desktop %1" +msgid "Desktop number rule type" +msgstr "Werkskerm %1" + +#. i18n: ectx: label, entry (screen), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:162 +#, kde-format +msgid "Screen number" +msgstr "" + +#. i18n: ectx: label, entry (screenrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:166 +#, kde-format +msgid "Screen number rule type" +msgstr "" + +#. i18n: ectx: label, entry (activity), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:173 +#, kde-format +msgid "Activity" +msgstr "" + +#. i18n: ectx: label, entry (activityrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:176 +#, kde-format +msgid "Activity rule type" +msgstr "" + +#. i18n: ectx: label, entry (type), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:183 +#, fuzzy, kde-format +#| msgid "Setup Window Shortcut" +msgid "Set window type to" +msgstr "Stel Venster Kortpad" + +#. i18n: ectx: label, entry (typerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:189 +#, kde-format +msgid "Set window type rule type" +msgstr "" + +#. i18n: ectx: label, entry (maximizevert), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:194 +#, fuzzy, kde-format +#| msgid "Maximize Window Vertically" +msgid "Maximized vertically" +msgstr "Vergroot Venster Vertikaal" + +#. i18n: ectx: label, entry (maximizevertrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:198 +#, fuzzy, kde-format +#| msgid "Maximize Window Vertically" +msgid "Maximized vertically rule type" +msgstr "Vergroot Venster Vertikaal" + +#. i18n: ectx: label, entry (maximizehoriz), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:205 +#, fuzzy, kde-format +#| msgid "Maximize Window Horizontally" +msgid "Maximized horizontally" +msgstr "Vergroot Venster Horisontaal" + +#. i18n: ectx: label, entry (maximizehorizrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:209 +#, fuzzy, kde-format +#| msgid "Maximize Window Horizontally" +msgid "Maximized horizontally rule type" +msgstr "Vergroot Venster Horisontaal" + +#. i18n: ectx: label, entry (minimize), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:216 +#, fuzzy, kde-format +#| msgid "Minimize" +msgid "Minimized" +msgstr "Verklein" + +#. i18n: ectx: label, entry (minimizerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:220 +#, kde-format +msgid "Minimized rule type" +msgstr "" + +#. i18n: ectx: label, entry (shade), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:227 +#, fuzzy, kde-format +#| msgid "Shade" +msgid "Shaded" +msgstr "Beskadu" + +#. i18n: ectx: label, entry (shaderule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:231 +#, kde-format +msgid "Shaded rule type" +msgstr "" + +#. i18n: ectx: label, entry (skiptaskbar), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:238 +#, kde-format +msgid "Skip taskbar" +msgstr "" + +#. i18n: ectx: label, entry (skiptaskbarrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:242 +#, kde-format +msgid "Skip taskbar rule type" +msgstr "" + +#. i18n: ectx: label, entry (skippager), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:249 +#, kde-format +msgid "Skip pager" +msgstr "" + +#. i18n: ectx: label, entry (skippagerrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:253 +#, kde-format +msgid "Skip pager rule type" +msgstr "" + +#. i18n: ectx: label, entry (skipswitcher), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:260 +#, fuzzy, kde-format +#| msgid "Switch to Desktop 10" +msgid "Skip switcher" +msgstr "Wissel na Werkskerm 10" + +#. i18n: ectx: label, entry (skipswitcherrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:264 +#, kde-format +msgid "Skip switcher rule type" +msgstr "" + +#. i18n: ectx: label, entry (above), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:271 +#, fuzzy, kde-format +#| msgid "Keep above others" +msgid "Keep above" +msgstr "Hou bo-op ander" + +#. i18n: ectx: label, entry (aboverule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:275 +#, fuzzy, kde-format +#| msgid "Keep above others" +msgid "Keep above rule type" +msgstr "Hou bo-op ander" + +#. i18n: ectx: label, entry (below), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:282 +#, fuzzy, kde-format +#| msgid "Keep below others" +msgid "Keep below" +msgstr "Hou onder ander" + +#. i18n: ectx: label, entry (belowrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:286 +#, fuzzy, kde-format +#| msgid "Keep below others" +msgid "Keep below rule type" +msgstr "Hou onder ander" + +#. i18n: ectx: label, entry (fullscreen), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:293 +#, fuzzy, kde-format +#| msgid "&Fullscreen" +msgid "Fullscreen" +msgstr "Volskerm" + +#. i18n: ectx: label, entry (fullscreenrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:297 +#, fuzzy, kde-format +#| msgid "&Fullscreen" +msgid "Fullscreen rule type" +msgstr "Volskerm" + +#. i18n: ectx: label, entry (noborder), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:304 +#, kde-format +msgid "No titlebar and frame" +msgstr "" + +#. i18n: ectx: label, entry (noborderrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:308 +#, kde-format +msgid "No titlebar rule type" +msgstr "" + +#. i18n: ectx: label, entry (decocolor), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:315 +#, kde-format +msgid "Titlebar color and scheme" +msgstr "" + +#. i18n: ectx: label, entry (decocolorrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:318 +#, kde-format +msgid "Titlebar color rule type" +msgstr "" + +#. i18n: ectx: label, entry (blockcompositing), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:323 +#, kde-format +msgid "Block Compositing" +msgstr "" + +#. i18n: ectx: label, entry (blockcompositingrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:327 +#, kde-format +msgid "Block Compositing rule type" +msgstr "" + +#. i18n: ectx: label, entry (fsplevel), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:332 +#, kde-format +msgid "Focus stealing prevention" +msgstr "" + +#. i18n: ectx: label, entry (fsplevelrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:338 +#, kde-format +msgid "Focus stealing prevention rule type" +msgstr "" + +#. i18n: ectx: label, entry (fpplevel), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:343 +#, kde-format +msgid "Focus protection" +msgstr "" + +#. i18n: ectx: label, entry (fpplevelrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:349 +#, kde-format +msgid "Focus protection rule type" +msgstr "" + +#. i18n: ectx: label, entry (acceptfocus), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:354 +#, kde-format +msgid "Accept Focus" +msgstr "" + +#. i18n: ectx: label, entry (acceptfocusrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:358 +#, kde-format +msgid "Accept Focus rule type" +msgstr "" + +#. i18n: ectx: label, entry (closeable), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:363 +#, kde-format +msgid "Closeable" +msgstr "" + +#. i18n: ectx: label, entry (closeablerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:367 +#, kde-format +msgid "Closeable rule type" +msgstr "" + +#. i18n: ectx: label, entry (autogroup), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:372 +#, kde-format +msgid "Autogroup with identical" +msgstr "" + +#. i18n: ectx: label, entry (autogrouprule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:376 +#, kde-format +msgid "Autogroup with identical rule type" +msgstr "" + +#. i18n: ectx: label, entry (autogroupfg), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:381 +#, kde-format +msgid "Autogroup in foreground" +msgstr "" + +#. i18n: ectx: label, entry (autogroupfgrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:385 +#, kde-format +msgid "Autogroup in foreground rule type" +msgstr "" + +#. i18n: ectx: label, entry (autogroupid), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:390 +#, kde-format +msgid "Autogroup by ID" +msgstr "" + +#. i18n: ectx: label, entry (autogroupidrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:393 +#, kde-format +msgid "Autogroup by ID rule type" +msgstr "" + +#. i18n: ectx: label, entry (strictgeometry), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:398 +#, kde-format +msgid "Obey geometry restrictions" +msgstr "" + +#. i18n: ectx: label, entry (strictgeometryrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:402 +#, kde-format +msgid "Obey geometry restrictions rule type" +msgstr "" + +#. i18n: ectx: label, entry (shortcut), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:407 +#, kde-format +msgid "Shortcut" +msgstr "" + +#. i18n: ectx: label, entry (shortcutrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:410 +#, kde-format +msgid "Shortcut rule type" +msgstr "" + +#. i18n: ectx: label, entry (disableglobalshortcuts), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:417 +#, fuzzy, kde-format +#| msgid "Block Global Shortcuts" +msgid "Ignore global shortcuts" +msgstr "Blok Globale kortpaaie" + +#. i18n: ectx: label, entry (disableglobalshortcutsrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:421 +#, kde-format +msgid "Ignore global shortcuts rule type" +msgstr "" + +#. i18n: ectx: label, entry (desktopfile), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:426 +#, fuzzy, kde-format +#| msgid "Desktop %1" +msgid "Desktop file name" +msgstr "Werkskerm %1" + +#. i18n: ectx: label, entry (desktopfilerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:429 +#, kde-format +msgid "Desktop file name rule type" +msgstr "" + +#: scripting/genericscriptedconfig.cpp:70 +#, kde-format +msgctxt "Error message" +msgid "Plugin does not provide configuration file in expected location" +msgstr "" + +#: scripting/scripting.cpp:117 +#, kde-format +msgctxt "Assertion failed in KWin script with given value" +msgid "Assertion failed: %1 is not null" +msgstr "" + +#: scripting/scripting.cpp:135 +#, kde-format +msgctxt "Assertion failed in KWin script" +msgid "Assertion failed: argument is null" +msgstr "" + +#: scripting/scripting.cpp:177 +#, kde-format +msgctxt "Error in KWin Script" +msgid "" +"Invalid number of arguments. At least service, path, interface and method " +"need to be provided" +msgstr "" + +#: scripting/scripting.cpp:183 +#, kde-format +msgctxt "Error in KWin Script" +msgid "" +"Invalid type. Service, path, interface and method need to be string values" +msgstr "" + +#: scripting/scriptingutils.cpp:17 +#, kde-format +msgctxt "syntax error in KWin script" +msgid "Invalid number of arguments" +msgstr "" + +#: scripting/scriptingutils.cpp:30 +#, kde-format +msgctxt "KWin Scripting function received incorrect value for an expected type" +msgid "%1 is not a variant type" +msgstr "" + +#. i18n: ectx: property (windowTitle), widget (QDialog, ShortcutDialog) +#: shortcutdialog.ui:14 +#, kde-format +msgid "Dialog" +msgstr "" + +#. i18n: ectx: property (text), widget (QToolButton, clearButton) +#: shortcutdialog.ui:25 +#, kde-format +msgid "..." +msgstr "" + +#: tabbox/tabbox.cpp:372 +#, fuzzy, kde-format +#| msgid "Switch to Desktop 1" +msgctxt "Special entry in alt+tab list for minimizing all windows" +msgid "Show Desktop" +msgstr "Wissel na Werkskerm 1" + +#: tabbox/tabbox.cpp:521 +#, kde-format +msgid "Walk Through Windows" +msgstr "Stap Deur Vensters" + +#: tabbox/tabbox.cpp:522 +#, kde-format +msgid "Walk Through Windows (Reverse)" +msgstr "Stap Deur Vensters (Omgekeerde)" + +#: tabbox/tabbox.cpp:523 +#, fuzzy, kde-format +#| msgid "Walk Through Windows (Reverse)" +msgid "Walk Through Windows Alternative" +msgstr "Stap Deur Vensters (Omgekeerde)" + +#: tabbox/tabbox.cpp:524 +#, fuzzy, kde-format +#| msgid "Walk Through Windows (Reverse)" +msgid "Walk Through Windows Alternative (Reverse)" +msgstr "Stap Deur Vensters (Omgekeerde)" + +#: tabbox/tabbox.cpp:525 +#, fuzzy, kde-format +#| msgid "Walk Through Windows (Reverse)" +msgid "Walk Through Windows of Current Application" +msgstr "Stap Deur Vensters (Omgekeerde)" + +#: tabbox/tabbox.cpp:526 +#, fuzzy, kde-format +#| msgid "Walk Through Windows (Reverse)" +msgid "Walk Through Windows of Current Application (Reverse)" +msgstr "Stap Deur Vensters (Omgekeerde)" + +#: tabbox/tabbox.cpp:527 +#, fuzzy, kde-format +#| msgid "Walk Through Windows (Reverse)" +msgid "Walk Through Windows of Current Application Alternative" +msgstr "Stap Deur Vensters (Omgekeerde)" + +#: tabbox/tabbox.cpp:528 +#, fuzzy, kde-format +#| msgid "Walk Through Windows (Reverse)" +msgid "Walk Through Windows of Current Application Alternative (Reverse)" +msgstr "Stap Deur Vensters (Omgekeerde)" + +#: tabbox/tabbox.cpp:529 +#, kde-format +msgid "Walk Through Desktops" +msgstr "Stap Deur Werkskerms" + +#: tabbox/tabbox.cpp:530 +#, kde-format +msgid "Walk Through Desktops (Reverse)" +msgstr "Stap Deur Werkskerms (Omgekeerde)" + +#: tabbox/tabbox.cpp:531 +#, kde-format +msgid "Walk Through Desktop List" +msgstr "Stap Deur Werkskerm Lys" + +#: tabbox/tabbox.cpp:532 +#, kde-format +msgid "Walk Through Desktop List (Reverse)" +msgstr "Stap Deur Werkskerm Lys (Omgekeerde)" + +#: tabbox/tabboxhandler.cpp:272 +#, kde-format +msgid "" +"The Window Switcher installation is broken, resources are missing.\n" +"Contact your distribution about this." +msgstr "" + +#: useractions.cpp:167 +#, kde-format +msgid "" +"You have selected to show a window without its border.\n" +"Without the border, you will not be able to enable the border again using " +"the mouse: use the window operations menu instead, activated using the %1 " +"keyboard shortcut." +msgstr "" +"Jy het gekies om 'n venster sonder sy raam te vertoon.\n" +"Sonder die raam, sal jy nie in staat wees om weer die raam aan te skakel " +"deur gebruik te maak van die muis nie: gebruik die venster beheer kieslys " +"daarvoor, geaktiveer gebruik die %1 sleutelbord kortpad." + +#: useractions.cpp:175 +#, kde-format +msgid "" +"You have selected to show a window in fullscreen mode.\n" +"If the application itself does not have an option to turn the fullscreen " +"mode off you will not be able to disable it again using the mouse: use the " +"window operations menu instead, activated using the %1 keyboard shortcut." +msgstr "" +"Jy het gekies om 'n venster te vertoon in vol-skerm modus.\n" +"Indien die program nie 'n opsie het om die volskerm af te sit nie, sal jy " +"nie in staat wees om dit weer af te skakel deur gebruik te maak van die muis " +"nie: gebruik die venster beheer kieslys daarvoor, geaktiveer gebruik die %1 " +"sleutelbord kortpad." + +#: useractions.cpp:240 +#, kde-format +msgid "&Move" +msgstr "Beweeg" + +#: useractions.cpp:245 +#, fuzzy, kde-format +#| msgid "Re&size" +msgid "&Resize" +msgstr "Hervergroot" + +#: useractions.cpp:250 +#, kde-format +msgid "Keep &Above Others" +msgstr "Hou B-Op Ander" + +#: useractions.cpp:256 +#, kde-format +msgid "Keep &Below Others" +msgstr "Hou Onder Ander" + +#: useractions.cpp:262 +#, kde-format +msgid "&Fullscreen" +msgstr "Volskerm" + +#: useractions.cpp:268 +#, fuzzy, kde-format +#| msgid "Shade" +msgid "&Shade" +msgstr "Beskadu" + +#: useractions.cpp:274 +#, kde-format +msgid "&No Border" +msgstr "Geen Raam" + +#: useractions.cpp:282 +#, fuzzy, kde-format +#| msgid "Window &Shortcut..." +msgid "Set Window Short&cut..." +msgstr "Venster &Kortpad..." + +#: useractions.cpp:287 +#, fuzzy, kde-format +#| msgid "&Special Window Settings..." +msgid "Configure Special &Window Settings..." +msgstr "Spesiale Venster Instellings..." + +#: useractions.cpp:292 +#, fuzzy, kde-format +#| msgid "&Special Application Settings..." +msgid "Configure S&pecial Application Settings..." +msgstr "Spesiale Program Instellings..." + +#: useractions.cpp:300 +#, fuzzy, kde-format +#| msgid "KDE window manager" +msgctxt "" +"Entry in context menu of window decoration to open the configuration module " +"of KWin" +msgid "Configure W&indow Manager..." +msgstr "Kde venster bestuurder." + +#: useractions.cpp:328 +#, kde-format +msgid "Ma&ximize" +msgstr "Maksimeer" + +#: useractions.cpp:334 +#, kde-format +msgid "Mi&nimize" +msgstr "Minimeer" + +#: useractions.cpp:340 +#, kde-format +msgid "&More Actions" +msgstr "" + +#: useractions.cpp:343 +#, kde-format +msgid "&Close" +msgstr "" + +#: useractions.cpp:410 +#, kde-format +msgid "&Extensions" +msgstr "" + +#: useractions.cpp:451 +#, fuzzy, kde-format +#| msgid "&All Desktops" +msgid "&Desktops" +msgstr "Alle Werkskerms" + +#: useractions.cpp:465 +#, fuzzy, kde-format +#| msgid "To &Desktop" +msgid "Move to &Desktop" +msgstr "Na Werkskerm" + +#: useractions.cpp:483 +#, fuzzy, kde-format +#| msgid "To &Desktop" +msgid "Move to &Screen" +msgstr "Na Werkskerm" + +#: useractions.cpp:499 +#, kde-format +msgid "Show in &Activities" +msgstr "" + +#: useractions.cpp:514 useractions.cpp:559 +#, kde-format +msgid "&All Desktops" +msgstr "Alle Werkskerms" + +#: useractions.cpp:542 useractions.cpp:595 +#, fuzzy, kde-format +#| msgid "Switch to Desktop 1" +msgctxt "Create a new desktop and move there the window" +msgid "&New Desktop" +msgstr "Wissel na Werkskerm 1" + +#: useractions.cpp:618 +#, fuzzy, kde-format +#| msgid "Window to Desktop 1" +msgctxt "" +"@item:inmenu List of all Screens to send a window to. First argument is a " +"number, second the output identifier. E.g. Screen 1 (HDMI1)" +msgid "Screen &%1 (%2)" +msgstr "Venster na Werkskerm 1" + +#: useractions.cpp:641 +#, kde-format +msgid "&All Activities" +msgstr "" + +#: useractions.cpp:887 +#, kde-format +msgctxt "'%1' is a keyboard shortcut like 'ctrl+w'" +msgid "%1 is already in use" +msgstr "" + +#: useractions.cpp:889 +#, kde-format +msgctxt "keyboard shortcut '%1' is used by action '%2' in application '%3'" +msgid "%1 is used by %2 in %3" +msgstr "" + +#: useractions.cpp:1021 +#, kde-format +msgid "Activate Window (%1)" +msgstr "" + +#: useractions.cpp:1163 +#, kde-format +msgid "" +"The window manager is configured to consider the screen with the mouse on it " +"as active one.\n" +"Therefore it is not possible to switch to a screen explicitly." +msgstr "" + +#: virtualdesktops.cpp:698 virtualdesktops.cpp:767 +#, kde-format +msgid "Desktop %1" +msgstr "Werkskerm %1" + +#: virtualdesktops.cpp:802 +#, kde-format +msgid "Switch to Next Desktop" +msgstr "Wissel na Volgende Werkskerm" + +#: virtualdesktops.cpp:804 +#, kde-format +msgid "Switch to Previous Desktop" +msgstr "Wissel na Vorige Werkskerm" + +#: virtualdesktops.cpp:806 +#, kde-format +msgid "Switch One Desktop to the Right" +msgstr "Wissel Een Werkskerm na die Regterkant" + +#: virtualdesktops.cpp:808 +#, kde-format +msgid "Switch One Desktop to the Left" +msgstr "Wissel Een Werkskerm na die Links" + +#: virtualdesktops.cpp:810 +#, kde-format +msgid "Switch One Desktop Up" +msgstr "Wissel Een Werkskerm Begin" + +#: virtualdesktops.cpp:812 +#, kde-format +msgid "Switch One Desktop Down" +msgstr "Wissel Een Werkskerm Ondertoe" + +#: virtualdesktops.cpp:825 +#, fuzzy, kde-format +#| msgid "Switch to Desktop 1" +msgid "Switch to Desktop %1" +msgstr "Wissel na Werkskerm 1" + +#: virtualkeyboard.cpp:84 +#, kde-format +msgid "Virtual Keyboard" +msgstr "" + +#: virtualkeyboard.cpp:350 +#, kde-format +msgid "Virtual Keyboard: enabled" +msgstr "" + +#: virtualkeyboard.cpp:353 +#, kde-format +msgid "Virtual Keyboard: disabled" +msgstr "" + +#: virtualkeyboard.cpp:355 +#, kde-format +msgid "Whether to show the virtual keyboard on demand." +msgstr "" + +#: workspace.cpp:1363 +#, kde-format +msgctxt "Introductory text shown in the support information." +msgid "" +"KWin Support Information:\n" +"The following information should be used when requesting support on e.g. " +"https://forum.kde.org.\n" +"It provides information about the currently running instance, which options " +"are used,\n" +"what OpenGL driver and which effects are running.\n" +"Please post the information provided underneath this introductory text to a " +"paste bin service\n" +"like https://paste.kde.org instead of pasting into support threads.\n" +msgstr "" \ No newline at end of file diff --git a/po/af/kwin_clients.po b/po/af/kwin_clients.po new file mode 100644 index 0000000..e5a915d --- /dev/null +++ b/po/af/kwin_clients.po @@ -0,0 +1,132 @@ +# UTF-8 test:äëïöü +# Copyright (C) 2001, 2005 Free Software Foundation, Inc. +# Kobus , 2005. +# +msgid "" +msgstr "" +"Project-Id-Version: kwin_clients stable\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-08-08 02:22+0200\n" +"PO-Revision-Date: 2005-11-25 22:32+0200\n" +"Last-Translator: Kobus \n" +"Language-Team: AFRIKAANS \n" +"Language: af\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: KBabel 1.10.2\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#: aurorae/src/aurorae.cpp:683 +#, kde-format +msgctxt "@item:inlistbox Button size:" +msgid "Tiny" +msgstr "" + +#: aurorae/src/aurorae.cpp:684 +#, kde-format +msgctxt "@item:inlistbox Button size:" +msgid "Normal" +msgstr "" + +#: aurorae/src/aurorae.cpp:685 +#, fuzzy, kde-format +#| msgid "Large" +msgctxt "@item:inlistbox Button size:" +msgid "Large" +msgstr "Groot" + +#: aurorae/src/aurorae.cpp:686 +#, fuzzy, kde-format +#| msgid "Large" +msgctxt "@item:inlistbox Button size:" +msgid "Very Large" +msgstr "Groot" + +#: aurorae/src/aurorae.cpp:687 +#, fuzzy, kde-format +#| msgid "Large" +msgctxt "@item:inlistbox Button size:" +msgid "Huge" +msgstr "Groot" + +#: aurorae/src/aurorae.cpp:688 +#, fuzzy, kde-format +#| msgid "Large" +msgctxt "@item:inlistbox Button size:" +msgid "Very Huge" +msgstr "Groot" + +#: aurorae/src/aurorae.cpp:689 +#, fuzzy, kde-format +#| msgid "Resize" +msgctxt "@item:inlistbox Button size:" +msgid "Oversized" +msgstr "Vergroot" + +#: aurorae/src/aurorae.cpp:692 +#, kde-format +msgid "Button size:" +msgstr "" + +#. i18n: ectx: property (windowTitle), widget (QWidget, PlastikConfigDialog) +#: aurorae/themes/plastik/package/contents/ui/config.ui:14 +#, kde-format +msgid "Config Dialog" +msgstr "Konfigurasie Dialoog" + +#. i18n: ectx: property (title), widget (KButtonGroup, titleAlign) +#: aurorae/themes/plastik/package/contents/ui/config.ui:23 +#, kde-format +msgid "Title &Alignment" +msgstr "Titel Belyning" + +#. i18n: ectx: property (text), widget (QRadioButton, kcfg_titleAlignLeft) +#: aurorae/themes/plastik/package/contents/ui/config.ui:29 +#, kde-format +msgid "Left" +msgstr "" + +#. i18n: ectx: property (text), widget (QRadioButton, kcfg_titleAlignCenter) +#: aurorae/themes/plastik/package/contents/ui/config.ui:36 +#, kde-format +msgid "Center" +msgstr "" + +#. i18n: ectx: property (text), widget (QRadioButton, kcfg_titleAlignRight) +#: aurorae/themes/plastik/package/contents/ui/config.ui:43 +#, kde-format +msgid "Right" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_coloredBorder) +#: aurorae/themes/plastik/package/contents/ui/config.ui:53 +#, kde-format +msgid "" +"Check this option if the window border should be painted in the titlebar " +"color. Otherwise it will be painted in the background color." +msgstr "" +"Kies hierdie opsie indien die venster rand dieslefde kleur as die titelbalk " +"moet wees. Andersins sal dit volgens die agtergrond kleur ingekleur word." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_coloredBorder) +#: aurorae/themes/plastik/package/contents/ui/config.ui:56 +#, fuzzy, kde-format +msgid "Colored window border" +msgstr "Gekleurde venster rand" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_animateButtons) +#: aurorae/themes/plastik/package/contents/ui/config.ui:66 +#, kde-format +msgid "" +"Check this option if you want the buttons to fade in when the mouse pointer " +"hovers over them and fade out again when it moves away." +msgstr "" +"Kies hierdie opsie indien jy wil hê dat die knoppies moet verhelder wanneer " +"die muis wyser daaroor huiwer, en weer verdof wanneer dit weg beweeg." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_animateButtons) +#: aurorae/themes/plastik/package/contents/ui/config.ui:69 +#, kde-format +msgid "Animate buttons" +msgstr "Animeer knoppies" \ No newline at end of file diff --git a/po/ar/kcm-kwin-scripts.po b/po/ar/kcm-kwin-scripts.po new file mode 100644 index 0000000..24c0685 --- /dev/null +++ b/po/ar/kcm-kwin-scripts.po @@ -0,0 +1,91 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# Safa Alfulaij , 2015. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-09-01 02:29+0200\n" +"PO-Revision-Date: 2015-01-24 23:26+0300\n" +"Last-Translator: Safa Alfulaij \n" +"Language-Team: Arabic \n" +"Language: ar\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " +"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" +"X-Generator: Lokalize 1.5\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "صفا الفليج" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "safa1996alfulaij@gmail.com" + +#: module.cpp:38 +#, kde-format +msgid "KWin Scripts" +msgstr "سكرِبتات نوافذك" + +#: module.cpp:40 +#, kde-format +msgid "Configure KWin scripts" +msgstr "اضبط سكرِبتات نوافذك" + +#: module.cpp:43 +#, kde-format +msgid "Tamás Krutki" +msgstr "" + +#: module.cpp:79 +#, kde-format +msgid "Error when uninstalling KWin Script: %1" +msgstr "" + +#: module.cpp:100 +#, kde-format +msgid "Import KWin Script" +msgstr "استورد سكرِبت نوافذك" + +#: module.cpp:101 +#, kde-format +msgid "*.kwinscript|KWin scripts (*.kwinscript)" +msgstr "*.kwinscript|سكرِبتات نوافذك (*.kwinscript)" + +#: module.cpp:120 +#, kde-format +msgctxt "Placeholder is error message returned from the install service" +msgid "" +"Cannot import selected script.\n" +"%1" +msgstr "" + +#: module.cpp:134 +#, kde-format +msgctxt "Placeholder is name of the script that was imported" +msgid "The script \"%1\" was successfully imported." +msgstr "" + +#. i18n: ectx: property (windowTitle), widget (QWidget, Module) +#: module.ui:14 +#, kde-format +msgid "KWin script configuration" +msgstr "إعداد سكرِبت نوافذك" + +#. i18n: ectx: property (text), widget (QPushButton, importScriptButton) +#: module.ui:55 +#, kde-format +msgid "Install from File..." +msgstr "" + +#. i18n: ectx: property (text), widget (KNS3::Button, ghnsButton) +#: module.ui:67 +#, fuzzy, kde-format +#| msgid "Get New Script..." +msgid "Get New Scripts..." +msgstr "احصل على سكرِبت جديد..." \ No newline at end of file diff --git a/po/ar/kcm_kwin_virtualdesktops.po b/po/ar/kcm_kwin_virtualdesktops.po new file mode 100644 index 0000000..cf967ad --- /dev/null +++ b/po/ar/kcm_kwin_virtualdesktops.po @@ -0,0 +1,135 @@ +# translation of kcm_kwindesktop.po to +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# محمد ابراهيم الحرقان , 2008. +# Safa Alfulaij , ٢٠١٥. +msgid "" +msgstr "" +"Project-Id-Version: kcm_kwindesktop\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-08-08 02:22+0200\n" +"PO-Revision-Date: ٢٠١٥-٠٨-١٨ ١٢:٤٧+0300\n" +"Last-Translator: Safa Alfulaij \n" +"Language-Team: Arabic \n" +"Language: ar\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " +"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" +"X-Generator: Lokalize 2.0\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "صفا الفليج" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "safa1996alfulaij@gmail.com" + +#: desktopsmodel.cpp:455 +#, kde-format +msgid "There was an error connecting to the compositor." +msgstr "" + +#: desktopsmodel.cpp:657 +#, kde-format +msgid "There was an error saving the settings to the compositor." +msgstr "" + +#: desktopsmodel.cpp:660 +#, kde-format +msgid "There was an error requesting information from the compositor." +msgstr "" + +#: package/contents/ui/main.qml:18 +#, kde-format +msgid "" +"This module lets you configure the navigation, number and layout of virtual " +"desktops." +msgstr "" + +#: package/contents/ui/main.qml:68 +#, kde-format +msgctxt "@info:tooltip" +msgid "Rename" +msgstr "" + +#: package/contents/ui/main.qml:78 +#, kde-format +msgctxt "@info:tooltip" +msgid "Remove" +msgstr "" + +#: package/contents/ui/main.qml:104 +#, kde-format +msgid "" +"Virtual desktops have been changed outside this settings application. Saving " +"now will overwrite the changes." +msgstr "" + +#: package/contents/ui/main.qml:118 +#, kde-format +msgid "Row %1" +msgstr "" + +#: package/contents/ui/main.qml:131 +#, kde-format +msgctxt "@action:button" +msgid "Add" +msgstr "" + +#: package/contents/ui/main.qml:134 +#, fuzzy, kde-format +#| msgid "Desktops" +msgid "New Desktop" +msgstr "أسطح المكتب" + +#: package/contents/ui/main.qml:148 +#, kde-format +msgid "1 Row" +msgid_plural "%1 Rows" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" +msgstr[4] "" +msgstr[5] "" + +#: package/contents/ui/main.qml:160 +#, kde-format +msgid "Options:" +msgstr "" + +#: package/contents/ui/main.qml:162 +#, kde-format +msgid "Navigation wraps around" +msgstr "" + +#: package/contents/ui/main.qml:177 +#, kde-format +msgid "Show animation when switching:" +msgstr "" + +#: package/contents/ui/main.qml:220 +#, kde-format +msgid "Show on-screen display when switching:" +msgstr "" + +#: package/contents/ui/main.qml:238 +#, kde-format +msgid "%1 ms" +msgstr "" + +#: package/contents/ui/main.qml:256 +#, kde-format +msgid "Show desktop layout indicators" +msgstr "" + +#: virtualdesktops.cpp:30 +#, fuzzy, kde-format +#| msgid "Desktops" +msgid "Virtual Desktops" +msgstr "أسطح المكتب" \ No newline at end of file diff --git a/po/ar/kcm_kwindecoration.po b/po/ar/kcm_kwindecoration.po new file mode 100644 index 0000000..f18f732 --- /dev/null +++ b/po/ar/kcm_kwindecoration.po @@ -0,0 +1,250 @@ +# translation of kcmkwindecoration.po to Arabic +# translation of kcmkwindecoration.po to +# Copyright (C) 2001,2002, 2004, 2006, 2007, 2008 Free Software Foundation, Inc. +# Mohammed Gamal , 2001. +# Isam Bayazidi , 2002. +# Ossama Khayat , 2004. +# محمد سعد Mohamed SAAD , 2006. +# AbdulAziz AlSharif , 2007. +# Youssef Chahibi , 2007. +# zayed , 2008. +# Zayed Al-Saidi , 2010. +# Safa Alfulaij , ٢٠١٦. +msgid "" +msgstr "" +"Project-Id-Version: kcmkwindecoration\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-11-17 08:20+0100\n" +"PO-Revision-Date: ٢٠١٦-٠٩-٠٧ ١٢:٠٤+0300\n" +"Last-Translator: Safa Alfulaij \n" +"Language-Team: Arabic \n" +"Language: ar\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " +"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" +"X-Generator: Lokalize 2.0\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Mohamed SAAD محمد سعد,زايد السعيدي" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "metehyi@free.fr ,zayed.alsaidi@gmail.com" + +#: declarative-plugin/buttonsmodel.cpp:54 +#, kde-format +msgid "Menu" +msgstr "قائمة" + +#: declarative-plugin/buttonsmodel.cpp:56 +#, kde-format +msgid "Application menu" +msgstr "قائمة التّطبيق" + +#: declarative-plugin/buttonsmodel.cpp:58 +#, kde-format +msgid "On all desktops" +msgstr "على كلّ أسطح المكتب" + +#: declarative-plugin/buttonsmodel.cpp:60 +#, kde-format +msgid "Minimize" +msgstr "صغّر" + +#: declarative-plugin/buttonsmodel.cpp:62 +#, kde-format +msgid "Maximize" +msgstr "كبّر" + +#: declarative-plugin/buttonsmodel.cpp:64 +#, kde-format +msgid "Close" +msgstr "أغلق" + +#: declarative-plugin/buttonsmodel.cpp:66 +#, kde-format +msgid "Context help" +msgstr "مساعدة سياقيّة" + +#: declarative-plugin/buttonsmodel.cpp:68 +#, kde-format +msgid "Shade" +msgstr "ظلّل" + +#: declarative-plugin/buttonsmodel.cpp:70 +#, kde-format +msgid "Keep below" +msgstr "أبقها بالأسفل" + +#: declarative-plugin/buttonsmodel.cpp:72 +#, kde-format +msgid "Keep above" +msgstr "أبقها بالأعلى" + +#: kcm.cpp:50 +#, fuzzy, kde-format +#| msgid "Get New Decorations..." +msgid "Window Decorations" +msgstr "اجلب زخرفة جديدة..." + +#: kcm.cpp:54 +#, kde-format +msgid "Valerio Pilo" +msgstr "" + +#: kcm.cpp:55 +#, kde-format +msgid "Author" +msgstr "" + +#: kcm.cpp:104 +#, fuzzy, kde-format +#| msgid "Get New Decorations..." +msgid "Download New Window Decorations" +msgstr "اجلب زخرفة جديدة..." + +#: package/contents/ui/Buttons.qml:73 +#, kde-format +msgid "Titlebar" +msgstr "شريط العنوان" + +#: package/contents/ui/Buttons.qml:214 +#, fuzzy, kde-format +#| msgid "Drop here to remove button" +msgid "Drop button here to remove it" +msgstr "أفلت هنا لإزالة الزّرّ" + +#: package/contents/ui/Buttons.qml:232 +#, kde-format +msgid "Drag buttons between here and the titlebar" +msgstr "اسحب الأزرار من وإلا شريط العنوان" + +#: package/contents/ui/main.qml:15 +#, kde-format +msgid "This module lets you configure the window decorations." +msgstr "" + +#: package/contents/ui/main.qml:49 +#, fuzzy, kde-format +#| msgid "Theme" +msgctxt "tab label" +msgid "Theme" +msgstr "السّمة" + +#: package/contents/ui/main.qml:53 +#, fuzzy, kde-format +#| msgid "Titlebar" +msgctxt "tab label" +msgid "Titlebar Buttons" +msgstr "شريط العنوان" + +#: package/contents/ui/main.qml:78 +#, kde-format +msgctxt "checkbox label" +msgid "Use theme's default window border size" +msgstr "" + +#: package/contents/ui/main.qml:109 +#, fuzzy, kde-format +#| msgid "Get New Decorations..." +msgctxt "button text" +msgid "Get New Window Decorations..." +msgstr "اجلب زخرفة جديدة..." + +#: package/contents/ui/main.qml:126 +#, fuzzy, kde-format +#| msgid "Close windows by double clicking &the menu button" +msgctxt "checkbox label" +msgid "Close windows by double clicking the menu button" +msgstr "أغلق النّوافذ بنقر زرّ ال&قائمة مزدوجًا" + +#: package/contents/ui/main.qml:139 +#, fuzzy, kde-format +#| msgid "" +#| "Close by double clicking:\n" +#| " To open the menu, keep the button pressed until it appears." +msgctxt "popup tip" +msgid "" +"Close by double clicking: Keep the window's Menu button pressed until it " +"appears." +msgstr "أغلق بالنّقر المزدوج: لفتح القائمة، أبقِ الزّرّ منقورًا حتّى تظهر." + +#: package/contents/ui/main.qml:146 +#, fuzzy, kde-format +#| msgid "&Show window button tooltips" +msgctxt "checkbox label" +msgid "Show titlebar button tooltips" +msgstr "أ&ظهر تلميحات زر النافذة" + +#: package/contents/ui/Themes.qml:89 +#, kde-format +msgid "Edit %1 Theme" +msgstr "" + +#: utils.cpp:26 +#, fuzzy, kde-format +#| msgctxt "@item:inlistbox Border size:" +#| msgid "No Borders" +msgid "No Borders" +msgstr "لا حدود" + +#: utils.cpp:27 +#, fuzzy, kde-format +#| msgctxt "@item:inlistbox Border size:" +#| msgid "No Side Borders" +msgid "No Side Borders" +msgstr "لا حدود جانبيّة" + +#: utils.cpp:28 +#, fuzzy, kde-format +#| msgctxt "@item:inlistbox Border size:" +#| msgid "Tiny" +msgid "Tiny" +msgstr "ضئيل" + +#: utils.cpp:29 +#, fuzzy, kde-format +#| msgctxt "@item:inlistbox Border size:" +#| msgid "Normal" +msgid "Normal" +msgstr "عادي" + +#: utils.cpp:30 +#, fuzzy, kde-format +#| msgctxt "@item:inlistbox Border size:" +#| msgid "Large" +msgid "Large" +msgstr "كبير" + +#: utils.cpp:31 +#, fuzzy, kde-format +#| msgctxt "@item:inlistbox Border size:" +#| msgid "Very Large" +msgid "Very Large" +msgstr "كبير جدًّا" + +#: utils.cpp:32 +#, fuzzy, kde-format +#| msgctxt "@item:inlistbox Border size:" +#| msgid "Huge" +msgid "Huge" +msgstr "ضخم" + +#: utils.cpp:33 +#, fuzzy, kde-format +#| msgctxt "@item:inlistbox Border size:" +#| msgid "Very Huge" +msgid "Very Huge" +msgstr "ضخم جدًّا" + +#: utils.cpp:34 +#, fuzzy, kde-format +#| msgctxt "@item:inlistbox Border size:" +#| msgid "Oversized" +msgid "Oversized" +msgstr "مضخّم الحجم" \ No newline at end of file diff --git a/po/ar/kcm_kwinrules.po b/po/ar/kcm_kwinrules.po new file mode 100644 index 0000000..cea3a5d --- /dev/null +++ b/po/ar/kcm_kwinrules.po @@ -0,0 +1,869 @@ +# translation of kcmkwinrules.po to Arabic +# translation of kcmkwinrules.po to +# محمد سعد Mohamed SAAD , 2006. +# AbdulAziz AlSharif , 2007. +# Youssef Chahibi , 2007. +# zayed , 2008. +msgid "" +msgstr "" +"Project-Id-Version: kcmkwinrules\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-11-03 08:14+0100\n" +"PO-Revision-Date: 2008-12-01 22:45+0400\n" +"Last-Translator: zayed \n" +"Language-Team: Arabic \n" +"Language: ar\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " +"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" +"X-Generator: KBabel 1.11.4\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Mohamed SAAD محمد سعد,زايد السعيدي" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "metehyi@free.fr,zayed.alsaidi@gmail.com" + +#: kcmrules.cpp:28 +#, fuzzy, kde-format +#| msgid "Window &role:" +msgid "Window Rules" +msgstr "&دور النافذة:" + +#: kcmrules.cpp:32 +#, kde-format +msgid "Ismael Asensio" +msgstr "" + +#: kcmrules.cpp:33 +#, kde-format +msgid "Author" +msgstr "" + +#: kcmrules.cpp:37 +#, kde-format +msgid "" +"

Window-specific Settings

Here you can customize window settings " +"specifically only for some windows.

Please note that this " +"configuration will not take effect if you do not use KWin as your window " +"manager. If you do use a different window manager, please refer to its " +"documentation for how to customize window behavior.

" +msgstr "" +"

إعدادات خاصة بالنافذة

هنا يمكنك تخصيص إعدادات النافذة خصوصا لبعض " +"النوافذ فقط.

الرجاء ملاحظة أن هذا الضبط لن يفعل إذا اختر غير كوِن " +"كمدير للنوافذ.إذا كنت تستخدم مدير نوافذ مختلف ، فالرجاء الرجوع إلى " +"وثائقهلكيفية تخصيص سلوك النافذة.

" + +#: main.cpp:91 +#, kde-format +msgid "Application settings for %1" +msgstr "إعدادات التطبيق لـِ %1" + +#: main.cpp:111 rulesmodel.cpp:216 +#, kde-format +msgid "Window settings for %1" +msgstr "إعدادات النافذة لـِ %1" + +#: main.cpp:163 +#, fuzzy, kde-format +msgctxt "Window caption for the application wide rules dialog" +msgid "Edit Application-Specific Settings" +msgstr "حرّر الإعدادات الخاصة بالنافذة" + +#: main.cpp:197 +#, kde-format +msgid "KWin" +msgstr "كِون" + +#: main.cpp:204 +#, kde-format +msgid "KWin helper utility" +msgstr "أداة مساعدة كِون" + +#: main.cpp:205 +#, fuzzy, kde-format +#| msgid "WId of the window for special window settings." +msgid "KWin id of the window for special window settings." +msgstr "رقم النافذة لإعدادات الخاصة بالنافذة." + +#: main.cpp:206 +#, kde-format +msgid "Whether the settings should affect all windows of the application." +msgstr "إذا كانت الإعدادات ستوثر على كلّ نوافذ التطبيق" + +#: main.cpp:215 +#, kde-format +msgid "This helper utility is not supposed to be called directly." +msgstr "أداة المساعدة هذه لا تدعم الاستدعاء مباشرة" + +#: optionsmodel.cpp:145 +#, kde-format +msgid "Unimportant" +msgstr "غير مهم" + +#: optionsmodel.cpp:146 +#, kde-format +msgid "Exact Match" +msgstr "تطابق تام" + +#: optionsmodel.cpp:147 +#, kde-format +msgid "Substring Match" +msgstr "تطابق السلسلة الفرعية" + +#: optionsmodel.cpp:148 +#, kde-format +msgid "Regular Expression" +msgstr "التعبير النمطي" + +#: optionsmodel.cpp:153 +#, kde-format +msgid "Do Not Affect" +msgstr "لا يتأثر" + +#: optionsmodel.cpp:154 +#, kde-format +msgid "" +"The window property will not be affected and therefore the default handling " +"for it will be used.\n" +"Specifying this will block more generic window settings from taking effect." +msgstr "" + +#: optionsmodel.cpp:157 +#, kde-format +msgid "Apply Initially" +msgstr "طبّق مبدئياً" + +#: optionsmodel.cpp:158 +#, kde-format +msgid "" +"The window property will be only set to the given value after the window is " +"created.\n" +"No further changes will be affected." +msgstr "" + +#: optionsmodel.cpp:161 +#, kde-format +msgid "Remember" +msgstr "تذكّر" + +#: optionsmodel.cpp:162 +#, kde-format +msgid "" +"The value of the window property will be remembered and, every time the " +"window is created, the last remembered value will be applied." +msgstr "" + +#: optionsmodel.cpp:165 +#, kde-format +msgid "Force" +msgstr "أجبر" + +#: optionsmodel.cpp:166 +#, kde-format +msgid "The window property will be always forced to the given value." +msgstr "" + +#: optionsmodel.cpp:168 +#, kde-format +msgid "Apply Now" +msgstr "طبّق الآن" + +#: optionsmodel.cpp:169 +#, kde-format +msgid "" +"The window property will be set to the given value immediately and will not " +"be affected later\n" +"(this action will be deleted afterwards)." +msgstr "" + +#: optionsmodel.cpp:172 +#, kde-format +msgid "Force Temporarily" +msgstr "أجبر بشكل مؤقت" + +#: optionsmodel.cpp:173 +#, kde-format +msgid "" +"The window property will be forced to the given value until it is hidden\n" +"(this action will be deleted after the window is hidden)." +msgstr "" + +#: package/contents/ui/FileDialogLoader.qml:14 +#, kde-format +msgid "Select File" +msgstr "" + +#: package/contents/ui/FileDialogLoader.qml:26 +#, kde-format +msgid "KWin Rules (*.kwinrule)" +msgstr "" + +#: package/contents/ui/OptionsComboBox.qml:32 +#, kde-format +msgid "None selected" +msgstr "" + +#: package/contents/ui/OptionsComboBox.qml:37 +#, kde-format +msgid "All selected" +msgstr "" + +#: package/contents/ui/OptionsComboBox.qml:39 +#, kde-format +msgid "%1 selected" +msgid_plural "%1 selected" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" +msgstr[4] "" +msgstr[5] "" + +#: package/contents/ui/RulesEditor.qml:48 +#: package/contents/ui/RulesEditor.qml:67 +#, kde-format +msgid "Add Properties..." +msgstr "" + +#: package/contents/ui/RulesEditor.qml:67 +#, fuzzy, kde-format +#| msgid "&Closeable" +msgid "Close" +msgstr "قابلة للا&غلاق" + +#: package/contents/ui/RulesEditor.qml:80 +#, fuzzy, kde-format +#| msgid "&Detect Window Properties" +msgid "Detect Window Properties" +msgstr "ا&كشف خصائص النافذة" + +#: package/contents/ui/RulesEditor.qml:93 +#, kde-format +msgid "Instantly" +msgstr "" + +#: package/contents/ui/RulesEditor.qml:94 +#, kde-format +msgid "After %1 second" +msgid_plural "After %1 seconds" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" +msgstr[4] "" +msgstr[5] "" + +#: package/contents/ui/RulesEditor.qml:113 +#, fuzzy, kde-format +#| msgid "&Detect Window Properties" +msgid "Select properties" +msgstr "ا&كشف خصائص النافذة" + +#: package/contents/ui/RulesEditor.qml:205 +#: package/contents/ui/ValueEditor.qml:53 +#, kde-format +msgid "Yes" +msgstr "" + +#: package/contents/ui/RulesEditor.qml:205 +#: package/contents/ui/ValueEditor.qml:59 +#, fuzzy, kde-format +msgid "No" +msgstr "بدون" + +#: package/contents/ui/RulesEditor.qml:207 +#: package/contents/ui/ValueEditor.qml:127 +#: package/contents/ui/ValueEditor.qml:134 +#, kde-format +msgid "%1 %" +msgstr "" + +#: package/contents/ui/RulesEditor.qml:209 +#, kde-format +msgctxt "Coordinates (x, y)" +msgid "(%1, %2)" +msgstr "" + +#: package/contents/ui/RulesEditor.qml:211 +#, kde-format +msgctxt "Size (width, height)" +msgid "(%1, %2)" +msgstr "" + +#: package/contents/ui/RulesList.qml:61 +#, kde-format +msgid "No rules for specific windows are currently set" +msgstr "" + +#: package/contents/ui/RulesList.qml:69 +#, kde-format +msgid "Select the rules to export" +msgstr "" + +#: package/contents/ui/RulesList.qml:73 +#, kde-format +msgid "Unselect All" +msgstr "" + +#: package/contents/ui/RulesList.qml:73 +#, kde-format +msgid "Select All" +msgstr "" + +#: package/contents/ui/RulesList.qml:87 +#, kde-format +msgid "Save Rules" +msgstr "" + +#: package/contents/ui/RulesList.qml:98 +#, fuzzy, kde-format +#| msgid "&New..." +msgid "Add New..." +msgstr "&جديد..." + +#: package/contents/ui/RulesList.qml:109 +#, kde-format +msgid "Import..." +msgstr "" + +#: package/contents/ui/RulesList.qml:117 +#, kde-format +msgid "Cancel Export" +msgstr "" + +#: package/contents/ui/RulesList.qml:117 +#, fuzzy, kde-format +#| msgid "Edit..." +msgid "Export..." +msgstr "حرّر..." + +#: package/contents/ui/RulesList.qml:198 +#, kde-format +msgid "Edit" +msgstr "حرّر" + +#: package/contents/ui/RulesList.qml:207 +#, kde-format +msgid "Delete" +msgstr "احذف" + +#: package/contents/ui/RulesList.qml:220 +#, kde-format +msgid "Import Rules" +msgstr "" + +#: package/contents/ui/RulesList.qml:232 +#, kde-format +msgid "Export Rules" +msgstr "" + +#: package/contents/ui/ValueEditor.qml:162 +#, kde-format +msgctxt "(x, y) coordinates separator in size/position" +msgid "x" +msgstr "" + +#: rulesdialog.cpp:28 +#, kde-format +msgid "Edit Window-Specific Settings" +msgstr "حرّر الإعدادات الخاصة بالنافذة" + +#: rulesmodel.cpp:219 +#, kde-format +msgid "Settings for %1" +msgstr "الإعدادات لِــ %1" + +#: rulesmodel.cpp:222 +#, fuzzy, kde-format +#| msgid "Window settings for %1" +msgid "New window settings" +msgstr "إعدادات النافذة لـِ %1" + +#: rulesmodel.cpp:236 +#, kde-format +msgid "" +"You have specified the window class as unimportant.\n" +"This means the settings will possibly apply to windows from all " +"applications. If you really want to create a generic setting, it is " +"recommended you at least limit the window types to avoid special window " +"types." +msgstr "" +"لقد حددت أن صنف النافذة غير مهم.\n" +"هذا يعني أنه من الممكن أن تطبق الإعدادات على نوافذ من جميع التطبيقات. إذا " +"كنت تريد فعلا إنشاء إعداد عام ، فإنه من الموصى أن تحدد أنواع النوافذ لمنع " +"أنواع النوافذ الخاصة." + +#: rulesmodel.cpp:366 +#, fuzzy, kde-format +#| msgid "De&scription:" +msgid "Description" +msgstr "ال&وصف:" + +#: rulesmodel.cpp:366 rulesmodel.cpp:374 rulesmodel.cpp:382 rulesmodel.cpp:389 +#: rulesmodel.cpp:395 rulesmodel.cpp:403 rulesmodel.cpp:408 rulesmodel.cpp:414 +#, fuzzy, kde-format +msgid "Window matching" +msgstr "ال&نافذة" + +#: rulesmodel.cpp:374 +#, fuzzy, kde-format +msgid "Window class (application)" +msgstr "&صنف النافذة ( نوع التطبيق ):" + +#: rulesmodel.cpp:382 +#, fuzzy, kde-format +#| msgid "Match w&hole window class" +msgid "Match whole window class" +msgstr "طابق صنف النافذة &كلها" + +#: rulesmodel.cpp:389 +#, fuzzy, kde-format +#| msgid "Match w&hole window class" +msgid "Whole window class" +msgstr "طابق صنف النافذة &كلها" + +#: rulesmodel.cpp:395 +#, fuzzy, kde-format +#| msgid "Window &types:" +msgid "Window types" +msgstr "أن&واع النافذة:" + +#: rulesmodel.cpp:403 +#, fuzzy, kde-format +#| msgid "Window &role:" +msgid "Window role" +msgstr "&دور النافذة:" + +#: rulesmodel.cpp:408 +#, fuzzy, kde-format +#| msgid "Window t&itle:" +msgid "Window title" +msgstr "عنوا&ن النافذة:" + +#: rulesmodel.cpp:414 +#, fuzzy, kde-format +#| msgid "&Machine (hostname):" +msgid "Machine (hostname)" +msgstr "الآ&لة (اسم المضيف):" + +#: rulesmodel.cpp:420 +#, fuzzy, kde-format +#| msgid "&Position" +msgid "Position" +msgstr "ال&موضع" + +#: rulesmodel.cpp:420 rulesmodel.cpp:425 rulesmodel.cpp:430 rulesmodel.cpp:435 +#: rulesmodel.cpp:440 rulesmodel.cpp:453 rulesmodel.cpp:467 rulesmodel.cpp:472 +#: rulesmodel.cpp:477 rulesmodel.cpp:482 rulesmodel.cpp:487 rulesmodel.cpp:493 +#: rulesmodel.cpp:502 rulesmodel.cpp:507 rulesmodel.cpp:512 +#, fuzzy, kde-format +msgid "Size & Position" +msgstr "ال&موضع" + +#: rulesmodel.cpp:425 +#, fuzzy, kde-format +#| msgid "&Size" +msgid "Size" +msgstr "ال&حجم" + +#: rulesmodel.cpp:430 +#, fuzzy, kde-format +#| msgid "Maximized &horizontally" +msgid "Maximized horizontally" +msgstr "مكبّرة &أفقياً" + +#: rulesmodel.cpp:435 +#, fuzzy, kde-format +#| msgid "Maximized &vertically" +msgid "Maximized vertically" +msgstr "مكبّرة &عامودياً" + +#: rulesmodel.cpp:440 +#, fuzzy, kde-format +#| msgid "All Desktops" +msgid "Virtual Desktop" +msgstr "كلّ أسطح المكتب" + +#: rulesmodel.cpp:453 +#, fuzzy, kde-format +msgid "Activity" +msgstr "ن&شّط العتمة في %" + +#: rulesmodel.cpp:467 +#, fuzzy, kde-format +msgid "Screen" +msgstr "شاشة البداية" + +#: rulesmodel.cpp:472 +#, fuzzy, kde-format +#| msgid "&Fullscreen" +msgid "Fullscreen" +msgstr "&ملء الشاشة" + +#: rulesmodel.cpp:477 +#, fuzzy, kde-format +#| msgid "M&inimized" +msgid "Minimized" +msgstr "م&صغرة" + +#: rulesmodel.cpp:482 +#, fuzzy, kde-format +#| msgid "Sh&aded" +msgid "Shaded" +msgstr "مظ&للة" + +#: rulesmodel.cpp:487 +#, fuzzy, kde-format +msgid "Initial placement" +msgstr "الم&وضع" + +#: rulesmodel.cpp:493 +#, fuzzy, kde-format +#| msgid "Ignore requested &geometry" +msgid "Ignore requested geometry" +msgstr "تجاهل طلب الأب&عاد" + +#: rulesmodel.cpp:495 +#, kde-format +msgid "" +"Windows can ask to appear in a certain position.\n" +"By default this overrides the placement strategy\n" +"what might be nasty if the client abuses the feature\n" +"to unconditionally popup in the middle of your screen." +msgstr "" + +#: rulesmodel.cpp:502 +#, fuzzy, kde-format +#| msgid "M&inimum size" +msgid "Minimum Size" +msgstr "الحجم الأ&صغر" + +#: rulesmodel.cpp:507 +#, fuzzy, kde-format +#| msgid "M&aximum size" +msgid "Maximum Size" +msgstr "الحجم ال&أكبر" + +#: rulesmodel.cpp:512 +#, kde-format +msgid "Obey geometry restrictions" +msgstr "" + +#: rulesmodel.cpp:514 +#, kde-format +msgid "" +"Eg. terminals or video players can ask to keep a certain aspect ratio\n" +"or only grow by values larger than one\n" +"(eg. by the dimensions of one character).\n" +"This may be pointless and the restriction prevents arbitrary dimensions\n" +"like your complete screen area." +msgstr "" + +#: rulesmodel.cpp:523 +#, fuzzy, kde-format +#| msgid "Keep &above" +msgid "Keep above" +msgstr "أبقهِ أ&على" + +#: rulesmodel.cpp:523 rulesmodel.cpp:528 rulesmodel.cpp:533 rulesmodel.cpp:539 +#: rulesmodel.cpp:545 rulesmodel.cpp:551 +#, kde-format +msgid "Arrangement & Access" +msgstr "" + +#: rulesmodel.cpp:528 +#, fuzzy, kde-format +#| msgid "Keep &below" +msgid "Keep below" +msgstr "أبقهِ أ&سفل" + +#: rulesmodel.cpp:533 +#, fuzzy, kde-format +#| msgid "Skip &taskbar" +msgid "Skip taskbar" +msgstr "تخطى ش&ريط المهام" + +#: rulesmodel.cpp:535 +#, kde-format +msgid "Window shall (not) appear in the taskbar." +msgstr "" + +#: rulesmodel.cpp:539 +#, fuzzy, kde-format +#| msgid "Skip pa&ger" +msgid "Skip pager" +msgstr "تخطّى ال&منادي" + +#: rulesmodel.cpp:541 +#, kde-format +msgid "Window shall (not) appear in the manager for virtual desktops" +msgstr "" + +#: rulesmodel.cpp:545 +#, fuzzy, kde-format +msgid "Skip switcher" +msgstr "تخطّى ال&منادي" + +#: rulesmodel.cpp:547 +#, kde-format +msgid "Window shall (not) appear in the Alt+Tab list" +msgstr "" + +#: rulesmodel.cpp:551 +#, kde-format +msgid "Shortcut" +msgstr "الاختصار" + +#: rulesmodel.cpp:557 +#, kde-format +msgid "No titlebar and frame" +msgstr "" + +#: rulesmodel.cpp:557 rulesmodel.cpp:562 rulesmodel.cpp:568 rulesmodel.cpp:573 +#: rulesmodel.cpp:578 rulesmodel.cpp:589 rulesmodel.cpp:600 rulesmodel.cpp:608 +#: rulesmodel.cpp:621 rulesmodel.cpp:626 rulesmodel.cpp:632 rulesmodel.cpp:637 +#, kde-format +msgid "Appearance & Fixes" +msgstr "" + +#: rulesmodel.cpp:562 +#, kde-format +msgid "Titlebar color scheme" +msgstr "" + +#: rulesmodel.cpp:568 +#, fuzzy, kde-format +msgid "Active opacity" +msgstr "ن&شّط العتمة في %" + +#: rulesmodel.cpp:573 +#, fuzzy, kde-format +msgid "Inactive opacity" +msgstr "ع&طّل العتمة في %" + +#: rulesmodel.cpp:578 +#, fuzzy, kde-format +#| msgid "&Focus stealing prevention" +msgid "Focus stealing prevention" +msgstr "منع &سرقة التركيز" + +#: rulesmodel.cpp:580 +#, kde-format +msgid "" +"KWin tries to prevent windows from taking the focus\n" +"(\"activate\") while you're working in another window,\n" +"but this may sometimes fail or superact.\n" +"\"None\" will unconditionally allow this window to get the focus while\n" +"\"Extreme\" will completely prevent it from taking the focus." +msgstr "" + +#: rulesmodel.cpp:589 +#, fuzzy, kde-format +#| msgid "&Focus stealing prevention" +msgid "Focus protection" +msgstr "منع &سرقة التركيز" + +#: rulesmodel.cpp:591 +#, kde-format +msgid "" +"This controls the focus protection of the currently active window.\n" +"None will always give the focus away,\n" +"Extreme will keep it.\n" +"Otherwise it's interleaved with the stealing prevention\n" +"assigned to the window that wants the focus." +msgstr "" + +#: rulesmodel.cpp:600 +#, fuzzy, kde-format +#| msgid "Accept &focus" +msgid "Accept focus" +msgstr "أقبل ال&تركيز" + +#: rulesmodel.cpp:602 +#, kde-format +msgid "" +"Windows may prevent to get the focus (activate) when being clicked.\n" +"On the other hand you might wish to prevent a window\n" +"from getting focused on a mouse click." +msgstr "" + +#: rulesmodel.cpp:608 +#, fuzzy, kde-format +msgid "Ignore global shortcuts" +msgstr "احظر الاختصارات الشمولية" + +#: rulesmodel.cpp:610 +#, kde-format +msgid "" +"When used, a window will receive\n" +"all keyboard inputs while it is active, including Alt+Tab etc.\n" +"This is especially interesting for emulators or virtual machines.\n" +"\n" +"Be warned:\n" +"you won't be able to Alt+Tab out of the window\n" +"nor use any other global shortcut (such as Alt+F2 to show KRunner)\n" +"while it's active!" +msgstr "" + +#: rulesmodel.cpp:621 +#, fuzzy, kde-format +#| msgid "&Closeable" +msgid "Closeable" +msgstr "قابلة للا&غلاق" + +#: rulesmodel.cpp:626 +#, fuzzy, kde-format +#| msgid "Window &type" +msgid "Set window type" +msgstr "&نوع النافذة" + +#: rulesmodel.cpp:632 +#, kde-format +msgid "Desktop file name" +msgstr "" + +#: rulesmodel.cpp:637 +#, kde-format +msgid "Block compositing" +msgstr "" + +#: rulesmodel.cpp:717 +#, kde-format +msgid "Normal Window" +msgstr "نافذة عادية" + +#: rulesmodel.cpp:718 +#, kde-format +msgid "Dialog Window" +msgstr "نافذة الحوار" + +#: rulesmodel.cpp:719 +#, kde-format +msgid "Utility Window" +msgstr "أداة النافذة" + +#: rulesmodel.cpp:720 +#, kde-format +msgid "Dock (panel)" +msgstr "إرساء (اللوحة)" + +#: rulesmodel.cpp:721 +#, kde-format +msgid "Toolbar" +msgstr "شريط الأدوات" + +#: rulesmodel.cpp:722 +#, kde-format +msgid "Torn-Off Menu" +msgstr "قائمة ممزقة" + +#: rulesmodel.cpp:723 +#, kde-format +msgid "Splash Screen" +msgstr "شاشة البداية" + +#: rulesmodel.cpp:724 +#, kde-format +msgid "Desktop" +msgstr "سطح المكتب" + +#. i18n("Unmanaged Window") }, deprecated +#: rulesmodel.cpp:726 +#, kde-format +msgid "Standalone Menubar" +msgstr "شريط أدوات مستقل" + +#: rulesmodel.cpp:741 +#, kde-format +msgid "All Desktops" +msgstr "كلّ أسطح المكتب" + +#: rulesmodel.cpp:754 +#, kde-format +msgid "All Activities" +msgstr "" + +#: rulesmodel.cpp:775 +#, kde-format +msgid "Default" +msgstr "افتراضي" + +#: rulesmodel.cpp:776 +#, kde-format +msgid "No Placement" +msgstr "موضع غير محدد" + +#: rulesmodel.cpp:777 +#, kde-format +msgid "Minimal Overlapping" +msgstr "" + +#: rulesmodel.cpp:778 +#, fuzzy, kde-format +#| msgid "Maximizing" +msgid "Maximized" +msgstr "تكبير" + +#: rulesmodel.cpp:779 +#, fuzzy, kde-format +#| msgid "Cascade" +msgid "Cascaded" +msgstr "متتالي" + +#: rulesmodel.cpp:780 +#, kde-format +msgid "Centered" +msgstr "مركز" + +#: rulesmodel.cpp:781 +#, kde-format +msgid "Random" +msgstr "عشوائي" + +#: rulesmodel.cpp:782 +#, fuzzy, kde-format +#| msgid "Top-Left Corner" +msgid "In Top-Left Corner" +msgstr "الزاوِيَة أعلى اليسار" + +#: rulesmodel.cpp:783 +#, kde-format +msgid "Under Mouse" +msgstr "تحت الفأرة" + +#: rulesmodel.cpp:784 +#, kde-format +msgid "On Main Window" +msgstr "على النافذة الرئيسية" + +#: rulesmodel.cpp:792 +#, fuzzy, kde-format +msgid "None" +msgstr "بدون" + +#: rulesmodel.cpp:793 +#, kde-format +msgid "Low" +msgstr "منخفض" + +#: rulesmodel.cpp:794 +#, kde-format +msgid "Normal" +msgstr "عادي" + +#: rulesmodel.cpp:795 +#, kde-format +msgid "High" +msgstr "عالي" + +#: rulesmodel.cpp:796 +#, kde-format +msgid "Extreme" +msgstr "عالي جداً" \ No newline at end of file diff --git a/po/ar/kcm_kwintabbox.po b/po/ar/kcm_kwintabbox.po new file mode 100644 index 0000000..081d068 --- /dev/null +++ b/po/ar/kcm_kwintabbox.po @@ -0,0 +1,223 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# Safa Alfulaij , 2015. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-08-15 02:23+0200\n" +"PO-Revision-Date: 2015-01-27 01:31+0300\n" +"Last-Translator: Safa Alfulaij \n" +"Language-Team: Arabic \n" +"Language: ar\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " +"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" +"X-Generator: Lokalize 1.5\n" + +#: kwintabboxconfigform.cpp:77 +#, kde-format +msgid "KWin" +msgstr "" + +#: layoutpreview.cpp:147 +#, kde-format +msgctxt "An example Desktop Name" +msgid "Desktop 1" +msgstr "سطح المكتب 1" + +#: main.cpp:65 +#, kde-format +msgid "Main" +msgstr "الرّئيسيّ" + +#: main.cpp:66 +#, kde-format +msgid "Alternative" +msgstr "البديل" + +#: main.cpp:68 +#, fuzzy, kde-format +#| msgid "Get New Window Switcher Layout" +msgid "Get New Task Switchers..." +msgstr "احصل على تخطيط مُبدِّل نوافذ جديد" + +#: main.cpp:78 +#, kde-format +msgid "" +"Focus policy settings limit the functionality of navigating through windows." +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_3) +#: main.ui:32 +#, kde-format +msgid "Content" +msgstr "المحتوى" + +#. i18n: ectx: property (text), widget (QCheckBox, showDesktop) +#: main.ui:41 +#, kde-format +msgid "Include \"Show Desktop\" icon" +msgstr "ضمّن أيقونة \"أظهر سطح المكتب\"" + +#. i18n: ectx: property (text), item, widget (KComboBox, switchingModeCombo) +#: main.ui:55 +#, kde-format +msgid "Recently used" +msgstr "المستخدَم حديثًا" + +#. i18n: ectx: property (text), item, widget (KComboBox, switchingModeCombo) +#: main.ui:60 +#, kde-format +msgid "Stacking order" +msgstr "ترتيب التّكديس" + +#. i18n: ectx: property (text), widget (QCheckBox, oneAppWindow) +#: main.ui:68 +#, kde-format +msgid "Only one window per application" +msgstr "نافذة لكلّ تطبيق" + +#. i18n: ectx: property (text), widget (QLabel, label_8) +#: main.ui:78 +#, kde-format +msgid "Sort order:" +msgstr "ترتيب الفرز:" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox) +#: main.ui:104 +#, kde-format +msgid "Filter windows by" +msgstr "رشّح النّوافذ حسب" + +#. i18n: ectx: property (text), widget (QCheckBox, filterDesktops) +#: main.ui:113 +#, kde-format +msgid "Virtual desktops" +msgstr "أسطح المكتب الوهميّة" + +#. i18n: ectx: property (text), widget (QRadioButton, currentDesktop) +#: main.ui:157 +#, kde-format +msgid "Current desktop" +msgstr "سطح المكتب الحاليّ" + +#. i18n: ectx: property (text), widget (QRadioButton, otherDesktops) +#: main.ui:164 +#, kde-format +msgid "All other desktops" +msgstr "كلّ أسطح المكتب الأخرى" + +#. i18n: ectx: property (text), widget (QCheckBox, filterActivities) +#: main.ui:174 +#, kde-format +msgid "Activities" +msgstr "الأنشطة" + +#. i18n: ectx: property (text), widget (QRadioButton, currentActivity) +#: main.ui:218 +#, kde-format +msgid "Current activity" +msgstr "النّشاط الحاليّ" + +#. i18n: ectx: property (text), widget (QRadioButton, otherActivities) +#: main.ui:225 +#, kde-format +msgid "All other activities" +msgstr "كلّ الأنشطة الأخرى" + +#. i18n: ectx: property (text), widget (QCheckBox, filterScreens) +#: main.ui:235 +#, kde-format +msgid "Screens" +msgstr "الشّاشات" + +#. i18n: ectx: property (text), widget (QRadioButton, currentScreen) +#: main.ui:279 +#, kde-format +msgid "Current screen" +msgstr "الشّاشة الحاليّة" + +#. i18n: ectx: property (text), widget (QRadioButton, otherScreens) +#: main.ui:286 +#, kde-format +msgid "All other screens" +msgstr "كلّ الشّاشات الأخرى" + +#. i18n: ectx: property (text), widget (QCheckBox, filterMinimization) +#: main.ui:296 +#, kde-format +msgid "Minimization" +msgstr "التّصغير" + +#. i18n: ectx: property (text), widget (QRadioButton, visibleWindows) +#: main.ui:340 +#, kde-format +msgid "Visible windows" +msgstr "" + +#. i18n: ectx: property (text), widget (QRadioButton, hiddenWindows) +#: main.ui:347 +#, kde-format +msgid "Hidden windows" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_4) +#: main.ui:386 +#, kde-format +msgid "Shortcuts" +msgstr "الاختصارات" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: main.ui:395 main.ui:438 +#, kde-format +msgid "Forward" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: main.ui:418 +#, kde-format +msgid "All windows" +msgstr "كلّ النّوافذ" + +#. i18n: ectx: property (text), widget (QLabel, label_4) +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: main.ui:428 main.ui:448 +#, kde-format +msgid "Reverse" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: main.ui:470 +#, kde-format +msgid "Current application" +msgstr "التّطبيق الحاليّ" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#: main.ui:489 +#, kde-format +msgid "Visualization" +msgstr "" + +#. i18n: ectx: property (toolTip), widget (QComboBox, effectCombo) +#: main.ui:519 +#, kde-format +msgid "The effect to replace the list window when desktop effects are active." +msgstr "" + +#. i18n: ectx: property (toolTip), widget (QCheckBox, kcfg_HighlightWindows) +#: main.ui:549 +#, kde-format +msgid "" +"The currently selected window will be highlighted by fading out all other " +"windows. This option requires desktop effects to be active." +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_HighlightWindows) +#: main.ui:552 +#, kde-format +msgid "Show selected window" +msgstr "" \ No newline at end of file diff --git a/po/ar/kcmkwincompositing.po b/po/ar/kcmkwincompositing.po new file mode 100644 index 0000000..64a522f --- /dev/null +++ b/po/ar/kcmkwincompositing.po @@ -0,0 +1,230 @@ +# translation of kcmkwincompositing.po to Arabic +# translation of kcmkwincompositing.po to +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Youssef Chahibi , 2007. +# Abdulaziz AlSharif , 2007. +# zayed , 2008. +# Zayed Al-Saidi , 2010. +# Abdalrahim G. Fakhouri , 2014. +msgid "" +msgstr "" +"Project-Id-Version: kcmkwincompositing\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-08-08 02:22+0200\n" +"PO-Revision-Date: 2014-07-04 19:16+0300\n" +"Last-Translator: Abdalrahim G. Fakhouri \n" +"Language-Team: Arabic >\n" +"Language: ar\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 1.5\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 && n%100<=99 ? 4 : 5;\n" + +#. i18n: ectx: property (text), widget (KMessageWidget, glCrashedWarning) +#: compositing.ui:32 +#, kde-format +msgid "" +"OpenGL compositing (the default) has crashed KWin in the past.\n" +"This was most likely due to a driver bug.\n" +"If you think that you have meanwhile upgraded to a stable driver,\n" +"you can reset this protection but be aware that this might result in an " +"immediate crash!\n" +"Alternatively, you might want to use the XRender backend instead." +msgstr "" + +#. i18n: ectx: property (text), widget (KMessageWidget, scaleWarning) +#: compositing.ui:45 +#, kde-format +msgid "" +"Scale method \"Accurate\" is not supported by all hardware and can cause " +"performance regressions and rendering artifacts." +msgstr "" + +#. i18n: ectx: property (text), widget (KMessageWidget, windowThumbnailWarning) +#: compositing.ui:68 +#, kde-format +msgid "" +"Keeping the window thumbnail always interferes with the minimized state of " +"windows. This can result in windows not suspending their work when minimized." +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Enabled) +#: compositing.ui:80 +#, kde-format +msgid "Enable compositor on startup" +msgstr "مكّن تأثيرات سطح المكتب عند بدء التشغيل" + +#. i18n: ectx: property (text), widget (QLabel, animationSpeedLabel) +#: compositing.ui:87 +#, fuzzy, kde-format +#| msgid "Animation Speed:" +msgid "Animation speed:" +msgstr "سرعة التحريك:" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: compositing.ui:124 +#, fuzzy, kde-format +#| msgid "Very Slow" +msgid "Very slow" +msgstr "بطيء جدا" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: compositing.ui:144 +#, kde-format +msgid "Instant" +msgstr "لحظي" + +#. i18n: ectx: property (text), widget (QLabel, scaleMethodLabel) +#: compositing.ui:156 +#, fuzzy, kde-format +#| msgid "Scale Method:" +msgid "Scale method:" +msgstr "طريقة التحجيم:" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_XRenderSmoothScale) +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glTextureFilter) +#: compositing.ui:166 compositing.ui:180 +#, kde-format +msgid "Crisp" +msgstr "الحدة" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_XRenderSmoothScale) +#: compositing.ui:171 +#, kde-format +msgid "Smooth (slower)" +msgstr "سَلِس (بطيء)" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glTextureFilter) +#: compositing.ui:185 +#, kde-format +msgid "Smooth" +msgstr "سَلِس" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glTextureFilter) +#: compositing.ui:190 +#, kde-format +msgid "Accurate" +msgstr "دقيق" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: compositing.ui:207 +#, kde-format +msgid "Rendering backend:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: compositing.ui:224 +#, fuzzy, kde-format +#| msgid "Tearing Prevention (VSync):" +msgid "Tearing prevention (\"vsync\"):" +msgstr "منع التكسير (المزامنة العمودية):" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glPreferBufferSwap) +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_HiddenPreviews) +#: compositing.ui:232 compositing.ui:268 +#, fuzzy, kde-format +#| msgid "Never" +msgid "Never" +msgstr "لا مطلقا" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glPreferBufferSwap) +#: compositing.ui:237 +#, kde-format +msgid "Automatic" +msgstr "آليّ" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glPreferBufferSwap) +#: compositing.ui:242 +#, kde-format +msgid "Only when cheap" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glPreferBufferSwap) +#: compositing.ui:247 +#, kde-format +msgid "Full screen repaints" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glPreferBufferSwap) +#: compositing.ui:252 +#, kde-format +msgid "Re-use screen content" +msgstr "استخدام مسبق لمحتوى الشاشة" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: compositing.ui:260 +#, fuzzy, kde-format +#| msgid "Keep Window Thumbnails:" +msgid "Keep window thumbnails:" +msgstr "حافظ على مصغرات النافذة:" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_HiddenPreviews) +#: compositing.ui:273 +#, kde-format +msgid "Only for Shown Windows" +msgstr "فقط للنوافذ المعروضة" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_HiddenPreviews) +#: compositing.ui:278 +#, kde-format +msgid "Always" +msgstr "دائما" + +#. i18n: ectx: property (toolTip), widget (QCheckBox, kcfg_WindowsBlockCompositing) +#: compositing.ui:288 +#, kde-format +msgid "" +"Applications can set a hint to block compositing when the window is open.\n" +" This brings performance improvements for e.g. games.\n" +" The setting can be overruled by window-specific rules." +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_WindowsBlockCompositing) +#: compositing.ui:291 +#, kde-format +msgid "Allow applications to block compositing" +msgstr "" + +#: main.cpp:80 +#, kde-format +msgid "Re-enable OpenGL detection" +msgstr "أعد تفعيل اكتشاف OpenGL" + +#: main.cpp:132 +#, kde-format +msgid "" +"\"Only when cheap\" only prevents tearing for full screen changes like a " +"video." +msgstr "" + +#: main.cpp:136 +#, kde-format +msgid "\"Full screen repaints\" can cause performance problems." +msgstr "" + +#: main.cpp:140 +#, kde-format +msgid "" +"\"Re-use screen content\" causes severe performance problems on MESA drivers." +msgstr "" + +#: main.cpp:160 +#, fuzzy, kde-format +#| msgid "OpenGL" +msgid "OpenGL 3.1" +msgstr "OpenGL" + +#: main.cpp:161 +#, fuzzy, kde-format +#| msgid "OpenGL" +msgid "OpenGL 2.0" +msgstr "OpenGL" + +#: main.cpp:162 +#, kde-format +msgid "XRender" +msgstr "XRender" \ No newline at end of file diff --git a/po/ar/kcmkwinscreenedges.po b/po/ar/kcmkwinscreenedges.po new file mode 100644 index 0000000..f882f36 --- /dev/null +++ b/po/ar/kcmkwinscreenedges.po @@ -0,0 +1,238 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# Zayed Al-Saidi , 2009. +# Safa Alfulaij , ٢٠١٦. +msgid "" +msgstr "" +"Project-Id-Version: kcmkwinscreenedges\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-08-08 02:22+0200\n" +"PO-Revision-Date: ٢٠١٦-٠٩-٠٤ ٢٣:١٠+0300\n" +"Last-Translator: Safa Alfulaij \n" +"Language-Team: Arabic \n" +"Language: ar\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " +"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" +"X-Generator: Lokalize 2.0\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "صفا الفليج" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "safa1996alfulaij@gmail.com" + +#: main.cpp:123 touch.cpp:122 +#, kde-format +msgid "No Action" +msgstr "لا إجراء" + +#: main.cpp:124 touch.cpp:123 +#, kde-format +msgid "Show Desktop" +msgstr "أظهر سطح المكتب" + +#: main.cpp:125 touch.cpp:124 +#, kde-format +msgid "Lock Screen" +msgstr "اقفل الشّاشة" + +#: main.cpp:126 touch.cpp:125 +#, kde-format +msgid "Show KRunner" +msgstr "" + +#: main.cpp:127 touch.cpp:126 +#, kde-format +msgid "Activity Manager" +msgstr "مدير الأنشطة" + +#: main.cpp:128 touch.cpp:127 +#, kde-format +msgid "Application Launcher" +msgstr "مُطلق التّطبيقات" + +#: main.cpp:132 touch.cpp:131 +#, kde-format +msgid "%1 - All Desktops" +msgstr "%1 - كلّ أسطح المكتب" + +#: main.cpp:133 touch.cpp:132 +#, kde-format +msgid "%1 - Current Desktop" +msgstr "%1 - سطح المكتب الحاليّ" + +#: main.cpp:134 touch.cpp:133 +#, kde-format +msgid "%1 - Current Application" +msgstr "%1 - التّطبيق الحاليّ" + +#: main.cpp:137 touch.cpp:136 +#, kde-format +msgid "%1 - Cube" +msgstr "%1 - معكّب" + +#: main.cpp:138 touch.cpp:137 +#, kde-format +msgid "%1 - Cylinder" +msgstr "%1 - أسطوانة" + +#: main.cpp:139 touch.cpp:138 +#, kde-format +msgid "%1 - Sphere" +msgstr "%1 - كرة" + +#: main.cpp:141 touch.cpp:140 +#, kde-format +msgid "Toggle window switching" +msgstr "" + +#: main.cpp:142 touch.cpp:141 +#, kde-format +msgid "Toggle alternative window switching" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, infoLabel) +#: main.ui:23 +#, fuzzy, kde-format +#| msgid "" +#| "To trigger an action push your mouse cursor against the edge of the " +#| "screen in the action's direction." +msgid "" +"You can trigger an action by pushing the mouse cursor against the " +"corresponding screen edge or corner." +msgstr "لتحفيز إجراء ادفع بمؤشّر الفأرة إلى جانب الشّاشة باتّجاه الإجراء." + +#. i18n: ectx: property (text), widget (QLabel, quickMaximizeLabel) +#: main.ui:67 +#, kde-format +msgid "&Maximize:" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ElectricBorderMaximize) +#: main.ui:77 +#, kde-format +msgid "Windows dragged to top edge" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, quickTileLabel) +#: main.ui:84 +#, kde-format +msgid "&Tile:" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ElectricBorderTiling) +#: main.ui:94 +#, fuzzy, kde-format +#| msgid "Maximize windows by dragging them to the top of the screen" +msgid "Windows dragged to left or right edge" +msgstr "كبّر النّوافذ بسحبها أعلى الشّاشة" + +#. i18n: ectx: property (text), widget (QLabel, electricBorderCornerRatioLabel) +#: main.ui:101 +#, kde-format +msgid "Trigger &quarter tiling in:" +msgstr "" + +#. i18n: ectx: property (suffix), widget (QSpinBox, electricBorderCornerRatioSpin) +#: main.ui:116 +#, no-c-format, kde-format +msgid "%" +msgstr "%" + +#. i18n: ectx: property (prefix), widget (QSpinBox, electricBorderCornerRatioSpin) +#: main.ui:119 +#, kde-format +msgid "Outer " +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_1) +#: main.ui:135 +#, kde-format +msgid "of the screen" +msgstr "من الشّاشة" + +#. i18n: ectx: property (toolTip), widget (QLabel, desktopSwitchLabel) +#: main.ui:144 +#, kde-format +msgid "" +"Change desktop when the mouse cursor is pushed against the edge of the screen" +msgstr "غيّر سطح المكتب عند الدّفع بمؤشّر الفأرة إلى حافّة الشّاشة" + +#. i18n: ectx: property (text), widget (QLabel, desktopSwitchLabel) +#: main.ui:147 +#, kde-format +msgid "&Switch desktop on edge:" +msgstr "&بدّل سطح المكتب عند الحافّة:" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_ElectricBorders) +#: main.ui:158 +#, kde-format +msgctxt "Switch desktop on edge" +msgid "Disabled" +msgstr "معطّل" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_ElectricBorders) +#: main.ui:163 +#, kde-format +msgid "Only When Moving Windows" +msgstr "عند تحريك النّوافذ فقط" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_ElectricBorders) +#: main.ui:168 +#, kde-format +msgid "Always Enabled" +msgstr "مفعّل دائمًا" + +#. i18n: ectx: property (toolTip), widget (QLabel, activationDelayLabel) +#: main.ui:176 +#, kde-format +msgid "" +"Amount of time required for the mouse cursor to be pushed against the edge " +"of the screen before the action is triggered" +msgstr "" +"كمّ الوقت اللازم قبل تحفيز الإجراء بعد الدّفع بمؤشّر الفأرة إلى حافّة الشّاشة" + +#. i18n: ectx: property (text), widget (QLabel, activationDelayLabel) +#: main.ui:179 +#, kde-format +msgid "Activation &delay:" +msgstr "&مهلة التّنشيط:" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_ElectricBorderDelay) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_ElectricBorderCooldown) +#: main.ui:189 main.ui:224 +#, kde-format +msgid " ms" +msgstr " م‌ث" + +#. i18n: ectx: property (toolTip), widget (QLabel, triggerCooldownLabel) +#: main.ui:208 +#, kde-format +msgid "" +"Amount of time required after triggering an action until the next trigger " +"can occur" +msgstr "كمّ الوقت اللازم حتّى تحفيز الإجراء التّالي بعد انتهاء الأوّل" + +#. i18n: ectx: property (text), widget (QLabel, triggerCooldownLabel) +#: main.ui:211 +#, kde-format +msgid "&Reactivation delay:" +msgstr "مهلة إ&عادة التّنشيط:" + +#. i18n: ectx: property (text), widget (QLabel, label_1) +#: touch.ui:17 +#, fuzzy, kde-format +#| msgid "" +#| "To trigger an action push your mouse cursor against the edge of the " +#| "screen in the action's direction." +msgid "" +"You can trigger an action by swiping from the screen edge towards the center " +"of the screen." +msgstr "لتحفيز إجراء ادفع بمؤشّر الفأرة إلى جانب الشّاشة باتّجاه الإجراء." \ No newline at end of file diff --git a/po/ar/kcmkwm.po b/po/ar/kcmkwm.po new file mode 100644 index 0000000..562c737 --- /dev/null +++ b/po/ar/kcmkwm.po @@ -0,0 +1,1331 @@ +# translation of kcmkwm.po to Arabic +# translation of kcmkwm.po to +# Copyright (C) 2002, 2004, 2006, 2007, 2008 Free Software Foundation, Inc. +# Isam Bayazidi , 2001,2002. +# Ammar Tabbaa , 2004. +# محمد سعد Mohamed SAAD , 2006. +# AbdulAziz AlSharif , 2007. +# Youssef Chahibi , 2007. +# zayed , 2008. +# Zayed Al-Saidi , 2010. +msgid "" +msgstr "" +"Project-Id-Version: kcmkwm\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-08-29 02:26+0200\n" +"PO-Revision-Date: 2010-07-19 07:18+0400\n" +"Last-Translator: Zayed Al-Saidi \n" +"Language-Team: Arabic \n" +"Language: ar\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " +"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" +"X-Generator: Lokalize 1.0\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Mohamed SAAD محمد سعد,زايد السعيدي" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "metehyi@free.fr,zayed.alsaidi@gmail.com" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_1) +#: actions.ui:17 +#, fuzzy, kde-format +#| msgid "Inactive Inner Window" +msgid "Inactive Inner Window Actions" +msgstr "نافذة داخلية خاملة" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#. i18n: ectx: property (text), widget (QLabel, label_1) +#: actions.ui:26 mouse.ui:177 +#, fuzzy, kde-format +msgid "&Left click:" +msgstr "النقر ال&مزدوج على شريط العنوان:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandWindow1) +#: actions.ui:39 +#, kde-format +msgid "" +"In this row you can customize left click behavior when clicking into an " +"inactive inner window ('inner' means: not titlebar, not frame)." +msgstr "" +"في هذا الصف، يمكنك تخصيص سلوك النقر بالزر الأيسر عند النقر في نافذة داخلية " +"خاملة ('داخلية' تعني: ليس إطار النافذة ولا شريط العنوان)." + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow3) +#: actions.ui:43 actions.ui:83 actions.ui:123 +#, fuzzy, kde-format +#| msgid "Activate, Raise & Pass Click" +msgid "Activate, raise and pass click" +msgstr "نشّط، ارفع و مرّر النقرة" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow3) +#: actions.ui:48 actions.ui:88 actions.ui:128 +#, fuzzy, kde-format +#| msgid "Activate & Pass Click" +msgid "Activate and pass click" +msgstr "نشّط و مرّر النقرة" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow3) +#: actions.ui:53 actions.ui:93 actions.ui:133 mouse.ui:293 mouse.ui:408 +#: mouse.ui:523 +#, kde-format +msgid "Activate" +msgstr "نشّط" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow3) +#: actions.ui:58 actions.ui:98 actions.ui:138 mouse.ui:283 mouse.ui:398 +#: mouse.ui:513 +#, fuzzy, kde-format +#| msgid "Activate & Raise" +msgid "Activate and raise" +msgstr "نشّط و ارفع" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: actions.ui:66 mouse.ui:200 +#, fuzzy, kde-format +msgid "&Middle click:" +msgstr "النقر ال&مزدوج على شريط العنوان:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandWindow2) +#: actions.ui:79 +#, kde-format +msgid "" +"In this row you can customize middle click behavior when clicking into an " +"inactive inner window ('inner' means: not titlebar, not frame)." +msgstr "" +"في هذا الصف، يمكنك تخصيص سلوك النقر بالزر الأوسط عند النقر في نافذة داخلية " +"خاملة ('داخلية' تعني: ليس إطار النافذة ولا شريط العنوان)." + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: actions.ui:106 mouse.ui:213 +#, kde-format +msgid "&Right click:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandWindow3) +#: actions.ui:119 +#, kde-format +msgid "" +"In this row you can customize right click behavior when clicking into an " +"inactive inner window ('inner' means: not titlebar, not frame)." +msgstr "" +"في هذا الصف، يمكنك تخصيص سلوك النقر بالزر الأيمن عند النقر في نافذة داخلية " +"خاملة ('داخلية' تعني: ليس إطار النافذة ولا شريط العنوان)." + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: actions.ui:146 mouse.ui:88 +#, fuzzy, kde-format +msgid "Mouse &wheel:" +msgstr "عجلة الفأرة:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandWindowWheel) +#: actions.ui:159 +#, kde-format +msgid "" +"In this row you can customize behavior when scrolling into an inactive inner " +"window ('inner' means: not titlebar, not frame)." +msgstr "" +"في هذا الصف، يمكنك تخصيص سلوك التمرير في نافذة داخلية خاملة ('داخلية' تعني: " +"ليس إطار النافذة ولا شريط العنوان)." + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindowWheel) +#: actions.ui:163 +#, kde-format +msgid "Scroll" +msgstr "التمرير" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindowWheel) +#: actions.ui:168 +#, fuzzy, kde-format +#| msgid "Activate & Scroll" +msgid "Activate and scroll" +msgstr "نشّط و مرر" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindowWheel) +#: actions.ui:173 +#, fuzzy, kde-format +#| msgid "Activate, Raise & Scroll" +msgid "Activate, raise and scroll" +msgstr "نشّط و ارفع و مرر" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#: actions.ui:184 +#, fuzzy, kde-format +msgid "Inner Window, Titlebar and Frame Actions" +msgstr "النافذة الداخلية، شريط العنوان و الإ&طار" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: actions.ui:195 +#, fuzzy, kde-format +msgid "Mo&difier key:" +msgstr "مفتاح مغيير:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandAllKey) +#: actions.ui:205 +#, kde-format +msgid "" +"Here you select whether holding the Meta key or Alt key will allow you to " +"perform the following actions." +msgstr "" +"هنا يمكنك انتقاء ما إذا كان الضغط على مفتاح Meta أو Alt سوف يسمح لك بالقيام " +"بالإجراءات التالية." + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllKey) +#: actions.ui:209 +#, kde-format +msgid "Meta" +msgstr "Meta" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllKey) +#: actions.ui:214 +#, kde-format +msgid "Alt" +msgstr "Alt" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: actions.ui:236 +#, kde-format +msgid " + " +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_8) +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: actions.ui:248 mouse.ui:601 +#, fuzzy, kde-format +msgid "L&eft click:" +msgstr "النقر ال&مزدوج على شريط العنوان:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandAll1) +#: actions.ui:261 +#, kde-format +msgid "" +"In this row you can customize left click behavior when clicking into the " +"titlebar or the frame." +msgstr "" +"في هذا الصف، يمكنك تخصيص سلوك النقر بالزر الأيسر عند النقر في شريط العنوان " +"أو الإطار." + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:265 actions.ui:335 actions.ui:405 +#, kde-format +msgid "Move" +msgstr "انقل" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:270 actions.ui:340 actions.ui:410 +#, fuzzy, kde-format +msgid "Activate, raise and move" +msgstr "نشّط و ارفع وحرّك" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:275 actions.ui:345 actions.ui:415 mouse.ui:246 mouse.ui:308 +#: mouse.ui:361 mouse.ui:423 mouse.ui:476 mouse.ui:538 +#, fuzzy, kde-format +#| msgid "Toggle Raise & Lower" +msgid "Toggle raise and lower" +msgstr "بدّل الرفع و الخفض" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:280 actions.ui:350 actions.ui:420 +#, kde-format +msgid "Resize" +msgstr "أعد التحجيم" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:285 actions.ui:355 actions.ui:425 mouse.ui:236 mouse.ui:298 +#: mouse.ui:351 mouse.ui:413 mouse.ui:466 mouse.ui:528 +#, kde-format +msgid "Raise" +msgstr "ارفع" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:290 actions.ui:360 actions.ui:430 mouse.ui:65 mouse.ui:241 +#: mouse.ui:303 mouse.ui:356 mouse.ui:418 mouse.ui:471 mouse.ui:533 +#, kde-format +msgid "Lower" +msgstr "أقل" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:295 actions.ui:365 actions.ui:435 mouse.ui:55 mouse.ui:251 +#: mouse.ui:313 mouse.ui:366 mouse.ui:428 mouse.ui:481 mouse.ui:543 +#, kde-format +msgid "Minimize" +msgstr "صغّر" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:300 actions.ui:370 actions.ui:440 +#, fuzzy, kde-format +msgid "Decrease opacity" +msgstr "غيّر العتمة" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:305 actions.ui:375 actions.ui:445 +#, fuzzy, kde-format +msgid "Increase opacity" +msgstr "غيّر العتمة" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:310 actions.ui:380 actions.ui:450 actions.ui:505 mouse.ui:80 +#: mouse.ui:132 mouse.ui:271 mouse.ui:333 mouse.ui:386 mouse.ui:448 +#: mouse.ui:501 mouse.ui:563 +#, fuzzy, kde-format +#| msgid "Nothing" +msgid "Do nothing" +msgstr "لا شيء " + +#. i18n: ectx: property (text), widget (QLabel, label_8) +#: actions.ui:318 +#, fuzzy, kde-format +msgid "Middle &click:" +msgstr "الزر الأوسط:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandAll2) +#: actions.ui:331 +#, kde-format +msgid "" +"In this row you can customize middle click behavior when clicking into the " +"titlebar or the frame." +msgstr "" +"في هذا الصف، يمكنك تخصيص سلوك النقر بالزر الأوسط عند النقر في شريط العنوان " +"أو الإطار." + +#. i18n: ectx: property (text), widget (QLabel, label_10) +#. i18n: ectx: property (text), widget (QLabel, label_9) +#: actions.ui:388 mouse.ui:671 +#, kde-format +msgid "Right clic&k:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:401 +#, kde-format +msgid "" +"In this row you can customize right click behavior when clicking into the " +"titlebar or the frame." +msgstr "" +"في هذا الصف، يمكنك تخصيص سلوك النقر بالزر الأيمن عند النقر في شريط العنوان " +"أو الإطار." + +#. i18n: ectx: property (text), widget (QLabel, label_10) +#: actions.ui:458 +#, fuzzy, kde-format +msgid "Mo&use wheel:" +msgstr "عجلة الفأرة:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:471 +#, kde-format +msgid "" +"Here you can customize KDE's behavior when scrolling with the mouse wheel in " +"a window while pressing the modifier key." +msgstr "" +"هنا يمكنك تخصيص سلوك الكيدي عند التمرير بواسطة عجلة الفأرة في النافذة أثناء " +"الضغط على مفتاح مغيير." + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:475 mouse.ui:102 +#, fuzzy, kde-format +#| msgid "Raise/Lower" +msgid "Raise/lower" +msgstr "ارفع/اخفض" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:480 mouse.ui:107 +#, fuzzy, kde-format +#| msgid "Shade/Unshade" +msgid "Shade/unshade" +msgstr "ظلّل/لا تظلّل" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:485 mouse.ui:112 +#, fuzzy, kde-format +#| msgid "Maximize/Restore" +msgid "Maximize/restore" +msgstr "كبّر/استعد" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:490 mouse.ui:117 +#, fuzzy, kde-format +#| msgid "Keep Above/Below" +msgid "Keep above/below" +msgstr "ابقِ أعلى/أسفل" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:495 mouse.ui:122 +#, fuzzy, kde-format +#| msgid "Move to Previous/Next Desktop" +msgid "Move to previous/next desktop" +msgstr "انقل للسابق/سطح المكتب التالي" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:500 mouse.ui:127 +#, fuzzy, kde-format +#| msgid "Change Opacity" +msgid "Change opacity" +msgstr "غيّر العتمة" + +#. i18n: ectx: property (text), widget (QLabel, shadeHoverLabel) +#: advanced.ui:20 +#, fuzzy, kde-format +#| msgid "Window Tabbing" +msgid "Window &unshading:" +msgstr "ألسنة النوافذ" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_ShadeHover) +#: advanced.ui:32 +#, fuzzy, kde-format +#| msgid "" +#| "If Shade Hover is enabled, a shaded window will un-shade automatically " +#| "when the mouse pointer has been over the title bar for some time." +msgid "" +"

If this option is enabled, a shaded window will " +"unshade automatically when the mouse pointer has been over the titlebar for " +"some time.

" +msgstr "" +"إذا كان تمرير الظل مفعل ، فإن أي نافذة يوضع فوقها مؤشر الفأرة لفترة من الزمن " +"سوف يزال تظليلها." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ShadeHover) +#: advanced.ui:35 +#, kde-format +msgid "On titlebar hover after:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QSpinBox, kcfg_ShadeHoverInterval) +#: advanced.ui:42 +#, kde-format +msgid "" +"Sets the time in milliseconds before the window unshades when the mouse " +"pointer goes over the shaded window." +msgstr "" +"يعيّن الوقت الذي يمر بالجزء من الألف من الثانية قبل إزالة التظليل عن نافذة " +"عند وضع مؤشر الفأرة فوقها." + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_ShadeHoverInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_DelayFocusInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_AutoRaiseInterval) +#: advanced.ui:45 focus.ui:85 focus.ui:178 +#, kde-format +msgid " ms" +msgstr "م.ث" + +#. i18n: ectx: property (text), widget (QLabel, windowPlacementLabel) +#: advanced.ui:66 +#, fuzzy, kde-format +#| msgid "&Placement:" +msgid "Window &placement:" +msgstr "مكان الوض&ع:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_Placement) +#: advanced.ui:76 +#, kde-format +msgid "" +"

The placement policy determines where a new window " +"will appear on the desktop.

  • Smart will try to achieve a minimum overlap of windows
  • Maximizing will try to maximize every window to fill the " +"whole screen. It might be useful to selectively affect placement of some " +"windows using the window-specific settings.
  • Cascade will " +"cascade the windows
  • Random will use a random " +"position
  • Centered will place the window " +"centered
  • Zero-cornered will place the window in " +"the top-left corner
  • Under mouse will place the " +"window under the pointer
" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:80 +#, fuzzy, kde-format +#| msgid "Snap windows onl&y when overlapping" +msgid "Minimal Overlapping" +msgstr "اج&ذب النوافذ فقط عندما تكون تغطي بعضها" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:85 +#, fuzzy, kde-format +msgid "Maximized" +msgstr "كبّر" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:90 +#, fuzzy, kde-format +#| msgid "Cascade" +msgid "Cascaded" +msgstr "ترتيب متسلسل" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:95 +#, kde-format +msgid "Random" +msgstr "عشوائي" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:100 +#, kde-format +msgid "Centered" +msgstr "في الوسط" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:105 +#, kde-format +msgid "In Top-Left Corner" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:110 +#, fuzzy, kde-format +msgid "Under mouse" +msgstr "التركيز تحت الفأرة" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_AllowKDEAppsToRememberWindowPositions) +#: advanced.ui:118 +#, kde-format +msgid "" +"When turned on, KDE apps which are able to remember the positions of their " +"windows are allowed to do so. This will override the window placement mode " +"defined above." +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AllowKDEAppsToRememberWindowPositions) +#: advanced.ui:121 +#, kde-format +msgid "Allow KDE apps to remember the positions of their own windows" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, specialWindowsLabel) +#: advanced.ui:128 +#, fuzzy, kde-format +msgid "&Special windows:" +msgstr "النوافذ" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_HideUtilityWindowsForInactive) +#: advanced.ui:138 +#, kde-format +msgid "" +"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." +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_HideUtilityWindowsForInactive) +#: advanced.ui:141 +#, kde-format +msgid "Hide utility windows for inactive applications" +msgstr "اخفِ نوافذ المساعدة للتطبيقات الخاملة" + +#. i18n: ectx: property (text), widget (QLabel, windowFocusPolicyLabel) +#: focus.ui:22 +#, kde-format +msgid "Window &activation policy:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QComboBox, windowFocusPolicy) +#: focus.ui:32 +#, kde-format +msgid "With this option you can specify how and when windows will be focused." +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:36 +#, fuzzy, kde-format +#| msgid "Click to Focus" +msgctxt "sassa asas" +msgid "Click to focus" +msgstr "انقر للتركيز" + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:41 +#, kde-format +msgid "Click to focus (mouse precedence)" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:46 +#, fuzzy, kde-format +#| msgid "Focus Follows Mouse" +msgid "Focus follows mouse" +msgstr "التركيز يتبع الفأرة" + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:51 +#, kde-format +msgid "Focus follows mouse (mouse precedence)" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:56 +#, fuzzy, kde-format +#| msgid "Focus Under Mouse" +msgid "Focus under mouse" +msgstr "التركيز تحت الفأرة" + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:61 +#, fuzzy, kde-format +#| msgid "Focus Strictly Under Mouse" +msgid "Focus strictly under mouse" +msgstr "التركيز تحت الفأرة فقط" + +#. i18n: ectx: property (text), widget (QLabel, delayFocusOnLabel) +#: focus.ui:69 +#, fuzzy, kde-format +msgid "&Delay focus by:" +msgstr "تأخير التركيز بـ:" + +#. i18n: ectx: property (whatsThis), widget (QSpinBox, kcfg_DelayFocusInterval) +#: focus.ui:82 +#, kde-format +msgid "" +"This is the delay after which the window the mouse pointer is over will " +"automatically receive focus." +msgstr "" +" هذا هو التأخير الذي ستستقبل بعده النافذة التي يوجد فوقها مؤشر الفأرة التركيز" + +#. i18n: ectx: property (text), widget (QLabel, focusStealingLabel) +#: focus.ui:101 +#, fuzzy, kde-format +msgid "Focus &stealing prevention:" +msgstr "مستوى الوقاية من غياب التركيز." + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_FocusStealingPreventionLevel) +#: focus.ui:114 +#, kde-format +msgid "" +"

This option specifies how much KWin will try to " +"prevent unwanted focus stealing caused by unexpected activation of new " +"windows. (Note: This feature does not work with the Focus under mouse or Focus strictly under mouse focus policies.)

  • 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.

" +msgstr "" + +#. i18n: Focus Stealing Prevention Level +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_FocusStealingPreventionLevel) +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_BorderSnapZone) +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_WindowSnapZone) +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_CenterSnapZone) +#: focus.ui:118 moving.ui:53 moving.ui:75 moving.ui:97 +#, fuzzy, kde-format +msgid "None" +msgstr "بدون" + +#. i18n: Focus Stealing Prevention Level +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_FocusStealingPreventionLevel) +#: focus.ui:123 +#, fuzzy, kde-format +msgid "Low" +msgstr "منخفض" + +#. i18n: Focus Stealing Prevention Level +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_FocusStealingPreventionLevel) +#: focus.ui:128 +#, fuzzy, kde-format +msgid "Medium" +msgstr "متوسط" + +#. i18n: Focus Stealing Prevention Level +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_FocusStealingPreventionLevel) +#: focus.ui:133 +#, fuzzy, kde-format +msgid "High" +msgstr "عالي" + +#. i18n: Focus Stealing Prevention Level +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_FocusStealingPreventionLevel) +#: focus.ui:138 +#, fuzzy, kde-format +msgid "Extreme" +msgstr "عالي جداً" + +#. i18n: ectx: property (text), widget (QLabel, raisingWindowsLabel) +#: focus.ui:146 +#, fuzzy, kde-format +msgid "Raising windows:" +msgstr "ارفع/اخفض جميع النوافذ العائمة" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_ClickRaise) +#: focus.ui:153 +#, kde-format +msgid "" +"When this option is enabled, the active window will be brought to the front " +"when you click somewhere into the window contents. To change it for inactive " +"windows, you need to change the settings in the Actions tab." +msgstr "" +"عندما يكون هذا الخيار ممكّن، فإن النافذة النشطة سوف تنقل إلى الأمام عندما " +"تنقر على مكان ما داخل محتويات النافذة. لتغييرها للنوافذ الخاملة تحتاج " +"لتغيير الإعدادات في لسان الإجراءات." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ClickRaise) +#: focus.ui:156 +#, fuzzy, kde-format +#| msgid "C&lick raises active window" +msgid "&Click raises active window" +msgstr "النقر ير&فع النافذة النشطة" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_AutoRaise) +#: focus.ui:165 +#, kde-format +msgid "" +"When this option is enabled, a window in the background will automatically " +"come to the front when the mouse pointer has been over it for some time." +msgstr "" +"إذا كان هذا الخيار ممكّن، فإن أية نافذة في الخلفية سوف تنقل تلقائياً إلى " +"الأمام عندما تضع مؤشر الفأرة فوقها لفترة من الزمن." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AutoRaise) +#: focus.ui:168 +#, kde-format +msgid "&Raise on hover, delayed by:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QSpinBox, kcfg_AutoRaiseInterval) +#: focus.ui:175 +#, kde-format +msgid "" +"This is the delay after which the window that the mouse pointer is over will " +"automatically come to the front." +msgstr "" +"هذا هو التأخير الذي يتم بعده نقل أي نافذة التي يوجد فوقها مؤشر الفأرة " +"تلقائياً إلى الأمام." + +#. i18n: ectx: property (text), widget (QLabel, multiscreenBehaviorLabel) +#: focus.ui:196 +#, kde-format +msgid "Multiscreen behavior:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_ActiveMouseScreen) +#: focus.ui:203 +#, kde-format +msgid "" +"When this option is enabled, the active Xinerama screen (where new windows " +"appear, for example) is the screen containing the mouse pointer. When " +"disabled, the active Xinerama screen is the screen containing the focused " +"window. By default this option is disabled for Click to focus and enabled " +"for other focus policies." +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ActiveMouseScreen) +#: focus.ui:206 +#, kde-format +msgid "Active screen follows &mouse" +msgstr "الشاشة النشطة تتبع ال&فأرة" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_SeparateScreenFocus) +#: focus.ui:213 +#, kde-format +msgid "" +"When this option is enabled, focus operations are limited only to the active " +"Xinerama screen" +msgstr "" +"إذا تم تمكين هذا الخيار ، عمليات التركيز ستكون محدودة فقط في شاشة Xinerama " +"النشطة" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_SeparateScreenFocus) +#: focus.ui:216 +#, fuzzy, kde-format +#| msgid "S&eparate screen focus" +msgid "&Separate screen focus" +msgstr "اف&صل تركيز الشاشة" + +#. i18n: ectx: property (text), widget (QLabel, windowFocusPolicyDescriptionLabel) +#: focus.ui:229 +#, kde-format +msgid "Window activation policy description" +msgstr "" + +#: main.cpp:73 +#, kde-format +msgid "&Focus" +msgstr "التر&كيز" + +#: main.cpp:84 +#, fuzzy, kde-format +#| msgid "&Titlebar Actions" +msgid "Titlebar A&ctions" +msgstr "إج&راءات شريط العنوان" + +#: main.cpp:91 +#, fuzzy, kde-format +#| msgid "Window Actio&ns" +msgid "W&indow Actions" +msgstr "اج&راءات النوافذ" + +#: main.cpp:98 +#, fuzzy, kde-format +#| msgid "&Placement:" +msgid "Mo&vement" +msgstr "مكان الوض&ع:" + +#: main.cpp:105 +#, fuzzy, kde-format +#| msgid "Ad&vanced" +msgid "Adva&nced" +msgstr "م&تقدم" + +#: main.cpp:110 +#, kde-format +msgid "Window Behavior Configuration Module" +msgstr "وحدة تشكيل سلوك النوافذ" + +#: main.cpp:112 +#, kde-format +msgid "(c) 1997 - 2002 KWin and KControl Authors" +msgstr "(c) 1997 - 2002 KWin and KControl مؤلفو" + +#: main.cpp:114 +#, kde-format +msgid "Matthias Ettrich" +msgstr "Matthias Ettrich" + +#: main.cpp:115 +#, kde-format +msgid "Waldo Bastian" +msgstr "Waldo Bastian" + +#: main.cpp:116 +#, kde-format +msgid "Cristian Tibirna" +msgstr "Cristian Tibirna" + +#: main.cpp:117 +#, kde-format +msgid "Matthias Kalle Dalheimer" +msgstr "Matthias Kalle Dalheimer" + +#: main.cpp:118 +#, kde-format +msgid "Daniel Molkentin" +msgstr "Daniel Molkentin" + +#: main.cpp:119 +#, kde-format +msgid "Wynn Wilkes" +msgstr "Wynn Wilkes" + +#: main.cpp:120 +#, kde-format +msgid "Pat Dowler" +msgstr "Pat Dowler" + +#: main.cpp:121 +#, kde-format +msgid "Bernd Wuebben" +msgstr "Bernd Wuebben" + +#: main.cpp:122 +#, kde-format +msgid "Matthias Hoelzer-Kluepfel" +msgstr "Matthias Hoelzer-Kluepfel" + +#: main.cpp:167 +#, kde-format +msgid "" +"

Window Behavior

Here you can customize the way windows behave " +"when being moved, resized or clicked on. You can also specify a focus policy " +"as well as a placement policy for new windows.

Please note that this " +"configuration will not take effect if you do not use KWin as your window " +"manager. If you do use a different window manager, please refer to its " +"documentation for how to customize window behavior.

" +msgstr "" +"

سلوك النافذة

هنا يمكنك تخصيص كيف تتصرف النافذة عندما تحرك أو تحجم " +"أو ينقر عليها ، كذلك تستطيع أن تحدد سياسة التركيز و سياسة مكان النوافذ " +"الجديدة.

الرجاء ملاحظة أن هذه الإعدادات لن تدخل في حيز التنفيذ إذا لم " +"تستعمل كوِن كمدير نوافذك ، إذا كنت تستخدم مدير نوافذ مختلف فراجع وثائقه " +"لتعريف كيف يمكن تخصيصه.

" + +#: main.cpp:187 +#, kde-format +msgid "&Titlebar Actions" +msgstr "إج&راءات شريط العنوان" + +#: main.cpp:193 +#, kde-format +msgid "Window Actio&ns" +msgstr "اج&راءات النوافذ" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_1) +#: mouse.ui:17 +#, fuzzy, kde-format +#| msgid "&Titlebar Actions" +msgid "Titlebar Actions" +msgstr "إج&راءات شريط العنوان" + +#. i18n: ectx: property (text), widget (QLabel, label_1) +#: mouse.ui:26 +#, fuzzy, kde-format +msgid "&Double-click:" +msgstr "النقر ال&مزدوج على شريط العنوان:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#: mouse.ui:36 +#, kde-format +msgid "Behavior on double click into the titlebar." +msgstr "السلوك عند النقر المزدوج في شريط العنوان." + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonLeftClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonMiddleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonRightClickCommand) +#: mouse.ui:40 mouse.ui:615 mouse.ui:650 mouse.ui:685 +#, fuzzy, kde-format +msgid "Maximize" +msgstr "كبّر" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonLeftClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonMiddleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonRightClickCommand) +#: mouse.ui:45 mouse.ui:620 mouse.ui:655 mouse.ui:690 +#, kde-format +msgid "Vertically maximize" +msgstr "" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonLeftClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonMiddleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonRightClickCommand) +#: mouse.ui:50 mouse.ui:625 mouse.ui:660 mouse.ui:695 +#, kde-format +msgid "Horizontally maximize" +msgstr "" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#: mouse.ui:60 mouse.ui:256 mouse.ui:318 mouse.ui:371 mouse.ui:433 mouse.ui:486 +#: mouse.ui:548 +#, kde-format +msgid "Shade" +msgstr "ظلّل" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#: mouse.ui:70 mouse.ui:261 mouse.ui:323 mouse.ui:376 mouse.ui:438 mouse.ui:491 +#: mouse.ui:553 +#, kde-format +msgid "Close" +msgstr "أغلق" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#: mouse.ui:75 +#, fuzzy, kde-format +msgid "Show on all desktops" +msgstr "على كلّ أسطح المكتب" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandTitlebarWheel) +#: mouse.ui:98 +#, fuzzy, kde-format +#| msgid "Behavior on double click into the titlebar." +msgid "Behavior on mouse wheel scroll over the titlebar." +msgstr "السلوك عند النقر المزدوج في شريط العنوان." + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#: mouse.ui:143 +#, fuzzy, kde-format +#| msgid "&Titlebar Actions" +msgid "Titlebar and Frame Actions" +msgstr "إج&راءات شريط العنوان" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: mouse.ui:167 +#, kde-format +msgid "Active" +msgstr "نشِط" + +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: mouse.ui:190 +#, kde-format +msgid "Inactive" +msgstr "خامل" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandActiveTitlebar3) +#: mouse.ui:232 mouse.ui:347 mouse.ui:462 +#, kde-format +msgid "" +"Behavior on left click into the titlebar or frame of an active window." +msgstr "" +"السلوك عند النقر بالزر الأيسر في شريط العنوان أو إطار نافذة " +"نشِطة." + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#: mouse.ui:266 mouse.ui:328 mouse.ui:381 mouse.ui:443 mouse.ui:496 +#: mouse.ui:558 +#, fuzzy, kde-format +#| msgid "Operations Menu" +msgid "Show actions menu" +msgstr "قائمة العمليات" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#: mouse.ui:279 mouse.ui:394 mouse.ui:509 +#, kde-format +msgid "" +"Behavior on left click into the titlebar or frame of an " +"inactive window." +msgstr "" +"السلوك عند النقر بالزر الأيسر في شريط العنوان أو إطار نافذة " +"خاملة." + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#: mouse.ui:288 mouse.ui:403 mouse.ui:518 +#, fuzzy, kde-format +#| msgid "Activate & Lower" +msgid "Activate and lower" +msgstr "نشّط و اخفض" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_3) +#: mouse.ui:589 +#, fuzzy, kde-format +#| msgid "Maximize Button" +msgid "Maximize Button Actions" +msgstr "زر التكبير" + +#. i18n: ectx: property (whatsThis), widget (QLabel, label_8) +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_MaximizeButtonLeftClickCommand) +#: mouse.ui:598 mouse.ui:611 +#, kde-format +msgid "Behavior on left click onto the maximize button." +msgstr "السلوك عند النقر الأيسر في زر التكبير." + +#. i18n: ectx: property (whatsThis), widget (QLabel, label_9) +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_MaximizeButtonMiddleClickCommand) +#: mouse.ui:633 mouse.ui:646 +#, kde-format +msgid "Behavior on middle click onto the maximize button." +msgstr "السلوك عند النقر الأوسط في زر التكبير." + +#. i18n: ectx: property (text), widget (QLabel, label_9) +#: mouse.ui:636 +#, fuzzy, kde-format +msgid "Middle c&lick:" +msgstr "الزر الأوسط:" + +#. i18n: ectx: property (whatsThis), widget (QLabel, label_10) +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_MaximizeButtonRightClickCommand) +#: mouse.ui:668 mouse.ui:681 +#, kde-format +msgid "Behavior on right click onto the maximize button." +msgstr "السلوك عند النقر الأيمن في زر التكبير." + +#. i18n: ectx: property (text), widget (QLabel, geometryTipLabel) +#: moving.ui:20 +#, kde-format +msgid "Window &geometry:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_GeometryTip) +#: moving.ui:30 +#, kde-format +msgid "" +"Enable this option if you want a window's geometry to be displayed while it " +"is being moved or resized. The window position relative to the top-left " +"corner of the screen is displayed together with its size." +msgstr "" +"مكّن هذا الخيار إذا كنت تريد عرض إحداثيات النافذة أثناء تحريكها أو إعادة " +"تحجيمها. يعرض وضعُ النافذة نسبة إلى الزاوِيَة اليسرى إلى الأعلى من الشاشة مع " +"حجم النافذة." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_GeometryTip) +#: moving.ui:33 +#, fuzzy, kde-format +#| msgid "Display window &geometry when moving or resizing" +msgid "Display when moving or resizing" +msgstr "اعرض إ&حداثيات النوافذ عند التحريك أو إعادة التحجيم" + +#. i18n: ectx: property (text), widget (QLabel, borderSnapLabel) +#: moving.ui:40 +#, fuzzy, kde-format +#| msgid "&Border snap zone:" +msgid "Screen &edge snap zone:" +msgstr "منطقة ال&جذب للحدود:" + +#. i18n: ectx: property (whatsThis), widget (QSpinBox, kcfg_BorderSnapZone) +#: moving.ui:50 +#, fuzzy, kde-format +#| msgid "" +#| "Here you can set the snap zone for screen borders, i.e. the 'strength' of " +#| "the magnetic field which will make windows snap to the border when moved " +#| "near it." +msgid "" +"Here you can set the snap zone for screen edges, i.e. the 'strength' of the " +"magnetic field which will make windows snap to the border when moved near it." +msgstr "" +"هنا يمكنك إعداد مناطق الجذب لحدود الشاشة، أو بمعنى آخر، 'قوة' الحقل " +"الميغناطيسي التي تجعل النوافذ تلتصق بالحدود عندما يتم تحريكها جوارها." + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_BorderSnapZone) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_WindowSnapZone) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_CenterSnapZone) +#: moving.ui:56 moving.ui:78 moving.ui:100 +#, fuzzy, kde-format +msgid " px" +msgstr "صفر بكسل" + +#. i18n: ectx: property (whatsThis), widget (QSpinBox, kcfg_WindowSnapZone) +#: moving.ui:72 +#, kde-format +msgid "" +"Here you can set the snap zone for windows, i.e. the 'strength' of the " +"magnetic field which will make windows snap to each other when they are " +"moved near another window." +msgstr "" +"هنا يمكنك إعداد مناطق الجذب للنوافذ، أو بمعنى آخر، 'قوة' الحقل المغناطيسي " +"التي تجعل النوافذ تنجذب إلى بعضها البعض عندما يتم تحريكها بجوار نافذة أخرى." + +#. i18n: ectx: property (whatsThis), widget (QSpinBox, kcfg_CenterSnapZone) +#: moving.ui:94 +#, kde-format +msgid "" +"Here you can set the snap zone for the screen center, i.e. the 'strength' of " +"the magnetic field which will make windows snap to the center of the screen " +"when moved near it." +msgstr "" +"هنا يمكنك إعداد مناطق الجذب لوسط الشاشة، أو بمعنى آخر، 'قوة' الحقل " +"المغناطيسي التي تجعل النوافذ تنجذب إلى وسط الشاشة عندما يتم تحريكها بجوارها." + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_SnapOnlyWhenOverlapping) +#: moving.ui:113 +#, kde-format +msgid "" +"Here you can set that windows will be only snapped if you try to overlap " +"them, i.e. they will not be snapped if the windows comes only near another " +"window or border." +msgstr "" +"هنا يمكن تحديد أن نوافذ ستنجذب فقط إذا حاولت أن تطابق بينها ، بمعنى أنها لن " +"تنجذب حتى تكون النوافذ قريبة من النوافذ الأخرى أو قريبة من حدودها." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_SnapOnlyWhenOverlapping) +#: moving.ui:116 +#, fuzzy, kde-format +#| msgid "Snap windows onl&y when overlapping" +msgid "Only when overlapping" +msgstr "اج&ذب النوافذ فقط عندما تكون تغطي بعضها" + +#. i18n: ectx: property (text), widget (QLabel, windowSnapLabel) +#: moving.ui:123 +#, kde-format +msgid "&Window snap zone:" +msgstr "منطقة جذب للن&افذة:" + +#. i18n: ectx: property (text), widget (QLabel, centerSnaplabel) +#: moving.ui:133 +#, kde-format +msgid "&Center snap zone:" +msgstr "منطقة ال&جذب للوسط:" + +#. i18n: ectx: property (text), widget (QLabel, OverlapSnapLabel) +#: moving.ui:143 +#, fuzzy, kde-format +msgid "&Snap windows:" +msgstr "ارفع/اخفض جميع النوافذ العائمة" + +#: windows.cpp:87 +#, kde-format +msgid "" +"Click to focus: A window becomes active when you click into it. " +"This behavior is common on other operating systems and likely what you want." +msgstr "" + +#: windows.cpp:91 +#, kde-format +msgid "" +"Click to focus (mouse precedence): Mostly the same as Click to " +"focus. If an active window has to be chosen by the system (eg. because " +"the currently active one was closed) the window under the mouse is the " +"preferred candidate. Unusual, but possible variant of Click to focus." +msgstr "" + +#: windows.cpp:96 +#, kde-format +msgid "" +"Focus follows mouse: Moving the mouse onto a window will activate " +"it. Eg. windows randomly appearing under the mouse will not gain the focus. " +"Focus stealing prevention takes place as usual. Think as Click " +"to focus just without having to actually click." +msgstr "" + +#: windows.cpp:100 +#, kde-format +msgid "" +"This is mostly the same as Focus follows mouse. If an active window " +"has to be chosen by the system (eg. because the currently active one was " +"closed) the window under the mouse is the preferred candidate. Choose this, " +"if you want a hover controlled focus." +msgstr "" + +#: windows.cpp:105 +#, kde-format +msgid "" +"Focus under mouse: The focus always remains on the window under the " +"mouse.
Warning: Focus stealing prevention and " +"the tabbox ('Alt+Tab') contradict the activation policy and will " +"not work. You very likely want to use Focus follows mouse (mouse " +"precedence) instead!" +msgstr "" + +#: windows.cpp:109 +#, kde-format +msgid "" +"Focus strictly under mouse: The focus is always on the window under " +"the mouse (in doubt nowhere) very much like the focus behavior in an " +"unmanaged legacy X11 environment.
Warning: Focus " +"stealing prevention and the tabbox ('Alt+Tab') contradict the " +"activation policy and will not work. You very likely want to use Focus " +"follows mouse (mouse precedence) instead!" +msgstr "" \ No newline at end of file diff --git a/po/ar/kwin.po b/po/ar/kwin.po new file mode 100644 index 0000000..d85abda --- /dev/null +++ b/po/ar/kwin.po @@ -0,0 +1,2639 @@ +# translation of kwin.po to Arabic +# translation of kwin.po to +# Copyright (C) 2001,2002, 2004, 2006, 2007, 2008 Free Software Foundation, Inc. +# Mohammed Gamal , 2001. +# Isam Bayazidi , 2002. +# Ossama Khayat , 2004. +# Munzir Taha , 2004. +# محمد سعد Mohamed SAAD , 2006. +# AbdulAziz AlSharif , 2007. +# Youssef Chahibi , 2007. +# zayed , 2008. +# Zayed Al-Saidi , 2010. +# Safa Alfulaij , 2015. +msgid "" +msgstr "" +"Project-Id-Version: kwin\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-11-25 08:45+0100\n" +"PO-Revision-Date: 2015-01-22 02:22+0300\n" +"Last-Translator: Safa Alfulaij \n" +"Language-Team: Arabic \n" +"Language: ar\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " +"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" +"X-Generator: Lokalize 1.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,safa1996alfulaij@gmail.com" + +#: abstract_client.cpp:2729 +#, kde-format +msgctxt "Application is not responding, appended to window title" +msgid "(Not Responding)" +msgstr "" + +#: abstract_wayland_output.cpp:252 +#, kde-format +msgid "unknown" +msgstr "" + +#: colorcorrection/manager.cpp:58 +#, kde-format +msgctxt "Night Color was disabled" +msgid "Night Color Off" +msgstr "" + +#: colorcorrection/manager.cpp:59 +#, kde-format +msgctxt "Night Color was enabled" +msgid "Night Color On" +msgstr "" + +#: colorcorrection/manager.cpp:228 colorcorrection/manager.cpp:231 +#: colorcorrection/manager.cpp:238 +#, kde-format +msgid "Toggle Night Color" +msgstr "" + +#: composite.cpp:926 +#, kde-format +msgid "" +"Desktop effects have been suspended by another application.
You can " +"resume using the '%1' shortcut." +msgstr "" +"علّقت تطبيقات أخرى تأثيرات سطح المكتب.
يمكنك استئنافها باستخدام الاختصار " +"'%1'." + +#: debug_console.cpp:65 +#, kde-format +msgid "Timestamp" +msgstr "" + +#: debug_console.cpp:70 +#, kde-format +msgid "Timestamp (µsec)" +msgstr "" + +#: debug_console.cpp:77 +#, fuzzy, kde-format +#| msgid "Top-Left" +msgctxt "A mouse button" +msgid "Left" +msgstr "أعلى اليسار" + +#: debug_console.cpp:79 +#, fuzzy, kde-format +#| msgid "Top-Right" +msgctxt "A mouse button" +msgid "Right" +msgstr "أعلى اليمين" + +#: debug_console.cpp:81 +#, kde-format +msgctxt "A mouse button" +msgid "Middle" +msgstr "" + +#: debug_console.cpp:83 +#, kde-format +msgctxt "A mouse button" +msgid "Back" +msgstr "" + +#: debug_console.cpp:85 +#, kde-format +msgctxt "A mouse button" +msgid "Forward" +msgstr "" + +#: debug_console.cpp:87 +#, kde-format +msgctxt "A mouse button" +msgid "Task" +msgstr "" + +#: debug_console.cpp:89 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 4" +msgstr "" + +#: debug_console.cpp:91 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 5" +msgstr "" + +#: debug_console.cpp:93 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 6" +msgstr "" + +#: debug_console.cpp:95 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 7" +msgstr "" + +#: debug_console.cpp:97 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 8" +msgstr "" + +#: debug_console.cpp:99 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 9" +msgstr "" + +#: debug_console.cpp:101 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 10" +msgstr "" + +#: debug_console.cpp:103 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 11" +msgstr "" + +#: debug_console.cpp:105 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 12" +msgstr "" + +#: debug_console.cpp:107 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 13" +msgstr "" + +#: debug_console.cpp:109 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 14" +msgstr "" + +#: debug_console.cpp:111 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 15" +msgstr "" + +#: debug_console.cpp:113 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 16" +msgstr "" + +#: debug_console.cpp:115 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 17" +msgstr "" + +#: debug_console.cpp:117 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 18" +msgstr "" + +#: debug_console.cpp:119 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 19" +msgstr "" + +#: debug_console.cpp:121 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 20" +msgstr "" + +#: debug_console.cpp:123 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 21" +msgstr "" + +#: debug_console.cpp:125 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 22" +msgstr "" + +#: debug_console.cpp:127 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 23" +msgstr "" + +#: debug_console.cpp:129 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 24" +msgstr "" + +#: debug_console.cpp:138 debug_console.cpp:140 +#, kde-format +msgid "Input Device" +msgstr "" + +#: debug_console.cpp:138 +#, kde-format +msgctxt "The input device of the event is not known" +msgid "Unknown" +msgstr "" + +#: debug_console.cpp:175 +#, kde-format +msgctxt "A mouse pointer motion event" +msgid "Pointer Motion" +msgstr "" + +#: debug_console.cpp:182 +#, kde-format +msgctxt "The relative mouse movement" +msgid "Delta" +msgstr "" + +#: debug_console.cpp:186 +#, kde-format +msgctxt "The relative mouse movement" +msgid "Delta (not accelerated)" +msgstr "" + +#: debug_console.cpp:189 +#, kde-format +msgctxt "The global mouse pointer position" +msgid "Global Position" +msgstr "" + +#: debug_console.cpp:193 +#, kde-format +msgctxt "A mouse pointer button press event" +msgid "Pointer Button Press" +msgstr "" + +#: debug_console.cpp:196 debug_console.cpp:204 +#, kde-format +msgctxt "A button in a mouse press/release event" +msgid "Button" +msgstr "" + +#: debug_console.cpp:197 debug_console.cpp:205 +#, kde-format +msgctxt "A button in a mouse press/release event" +msgid "Native Button code" +msgstr "" + +#: debug_console.cpp:198 debug_console.cpp:206 +#, kde-format +msgctxt "All currently pressed buttons in a mouse press/release event" +msgid "Pressed Buttons" +msgstr "" + +#: debug_console.cpp:201 +#, kde-format +msgctxt "A mouse pointer button release event" +msgid "Pointer Button Release" +msgstr "" + +#: debug_console.cpp:221 +#, kde-format +msgctxt "A mouse pointer axis (wheel) event" +msgid "Pointer Axis" +msgstr "" + +#: debug_console.cpp:225 +#, kde-format +msgctxt "The orientation of a pointer axis event" +msgid "Orientation" +msgstr "" + +#: debug_console.cpp:226 +#, kde-format +msgctxt "An orientation of a pointer axis event" +msgid "Horizontal" +msgstr "" + +#: debug_console.cpp:227 +#, kde-format +msgctxt "An orientation of a pointer axis event" +msgid "Vertical" +msgstr "" + +#: debug_console.cpp:228 +#, kde-format +msgctxt "The angle delta of a pointer axis event" +msgid "Delta" +msgstr "" + +#: debug_console.cpp:243 +#, kde-format +msgctxt "A key press event" +msgid "Key Press" +msgstr "" + +#: debug_console.cpp:246 +#, kde-format +msgctxt "A key release event" +msgid "Key Release" +msgstr "" + +#: debug_console.cpp:255 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Shift" +msgstr "" + +#: debug_console.cpp:259 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Control" +msgstr "" + +#: debug_console.cpp:263 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Alt" +msgstr "" + +#: debug_console.cpp:267 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Meta" +msgstr "" + +#: debug_console.cpp:271 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Keypad" +msgstr "" + +#: debug_console.cpp:275 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Group-switch" +msgstr "" + +#: debug_console.cpp:281 +#, kde-format +msgctxt "Whether the event is an automatic key repeat" +msgid "Repeat" +msgstr "" + +#: debug_console.cpp:285 +#, kde-format +msgctxt "The code as read from the input device" +msgid "Scan code" +msgstr "" + +#: debug_console.cpp:286 +#, kde-format +msgctxt "Key according to Qt" +msgid "Qt::Key code" +msgstr "" + +#: debug_console.cpp:288 +#, kde-format +msgctxt "The translated code to an Xkb symbol" +msgid "Xkb symbol" +msgstr "" + +#: debug_console.cpp:289 +#, kde-format +msgctxt "The translated code interpreted as text" +msgid "Utf8" +msgstr "" + +#: debug_console.cpp:290 +#, kde-format +msgctxt "The currently active modifiers" +msgid "Modifiers" +msgstr "" + +#: debug_console.cpp:302 +#, kde-format +msgctxt "A touch down event" +msgid "Touch down" +msgstr "" + +#: debug_console.cpp:304 debug_console.cpp:319 debug_console.cpp:334 +#, kde-format +msgctxt "The id of the touch point in the touch event" +msgid "Point identifier" +msgstr "" + +#: debug_console.cpp:305 debug_console.cpp:320 +#, kde-format +msgctxt "The global position of the touch point" +msgid "Global position" +msgstr "" + +#: debug_console.cpp:317 +#, kde-format +msgctxt "A touch motion event" +msgid "Touch Motion" +msgstr "" + +#: debug_console.cpp:332 +#, kde-format +msgctxt "A touch up event" +msgid "Touch Up" +msgstr "" + +#: debug_console.cpp:345 +#, kde-format +msgctxt "A pinch gesture is started" +msgid "Pinch start" +msgstr "" + +#: debug_console.cpp:347 +#, kde-format +msgctxt "Number of fingers in this pinch gesture" +msgid "Finger count" +msgstr "" + +#: debug_console.cpp:358 +#, kde-format +msgctxt "A pinch gesture is updated" +msgid "Pinch update" +msgstr "" + +#: debug_console.cpp:360 +#, kde-format +msgctxt "Current scale in pinch gesture" +msgid "Scale" +msgstr "" + +#: debug_console.cpp:361 +#, kde-format +msgctxt "Current angle in pinch gesture" +msgid "Angle delta" +msgstr "" + +#: debug_console.cpp:362 +#, kde-format +msgctxt "Current delta in pinch gesture" +msgid "Delta x" +msgstr "" + +#: debug_console.cpp:363 +#, kde-format +msgctxt "Current delta in pinch gesture" +msgid "Delta y" +msgstr "" + +#: debug_console.cpp:374 +#, kde-format +msgctxt "A pinch gesture ended" +msgid "Pinch end" +msgstr "" + +#: debug_console.cpp:386 +#, kde-format +msgctxt "A pinch gesture got cancelled" +msgid "Pinch cancelled" +msgstr "" + +#: debug_console.cpp:398 +#, kde-format +msgctxt "A swipe gesture is started" +msgid "Swipe start" +msgstr "" + +#: debug_console.cpp:400 +#, kde-format +msgctxt "Number of fingers in this swipe gesture" +msgid "Finger count" +msgstr "" + +#: debug_console.cpp:411 +#, kde-format +msgctxt "A swipe gesture is updated" +msgid "Swipe update" +msgstr "" + +#: debug_console.cpp:413 +#, kde-format +msgctxt "Current delta in swipe gesture" +msgid "Delta x" +msgstr "" + +#: debug_console.cpp:414 +#, kde-format +msgctxt "Current delta in swipe gesture" +msgid "Delta y" +msgstr "" + +#: debug_console.cpp:425 +#, kde-format +msgctxt "A swipe gesture ended" +msgid "Swipe end" +msgstr "" + +#: debug_console.cpp:437 +#, kde-format +msgctxt "A swipe gesture got cancelled" +msgid "Swipe cancelled" +msgstr "" + +#: debug_console.cpp:449 +#, fuzzy, kde-format +#| msgid "Switch to Tab" +msgctxt "A hardware switch (e.g. notebook lid) got toggled" +msgid "Switch toggled" +msgstr "بدّل إلى اللسان" + +#: debug_console.cpp:457 +#, kde-format +msgctxt "Name of a hardware switch" +msgid "Notebook lid" +msgstr "" + +#: debug_console.cpp:459 +#, kde-format +msgctxt "Name of a hardware switch" +msgid "Tablet mode" +msgstr "" + +#: debug_console.cpp:461 +#, fuzzy, kde-format +#| msgid "Switch to Tab" +msgctxt "A hardware switch" +msgid "Switch" +msgstr "بدّل إلى اللسان" + +#: debug_console.cpp:465 +#, kde-format +msgctxt "The hardware switch got turned off" +msgid "Off" +msgstr "" + +#: debug_console.cpp:468 +#, kde-format +msgctxt "The hardware switch got turned on" +msgid "On" +msgstr "" + +#: debug_console.cpp:473 +#, kde-format +msgctxt "State of a hardware switch (on/off)" +msgid "State" +msgstr "" + +#: debug_console.cpp:488 +#, kde-format +msgid "Tablet Tool" +msgstr "" + +#: debug_console.cpp:489 +#, kde-format +msgid "EventType" +msgstr "" + +#: debug_console.cpp:490 debug_console.cpp:537 debug_console.cpp:549 +#, kde-format +msgid "Position" +msgstr "" + +#: debug_console.cpp:492 +#, kde-format +msgid "Tilt" +msgstr "" + +#: debug_console.cpp:494 +#, fuzzy, kde-format +#| msgid "caption" +msgid "Rotation" +msgstr "الواصفة" + +#: debug_console.cpp:495 +#, kde-format +msgid "Pressure" +msgstr "" + +#: debug_console.cpp:496 +#, fuzzy, kde-format +#| msgid "Mouse Emulation" +msgid "Buttons" +msgstr "محاكاة الفأرة" + +#. i18n: ectx: property (title), widget (QGroupBox, modifiersBox) +#: debug_console.cpp:497 debug_console.ui:356 +#, kde-format +msgid "Modifiers" +msgstr "" + +#: debug_console.cpp:510 +#, kde-format +msgid "Tablet Tool Button" +msgstr "" + +#: debug_console.cpp:511 debug_console.cpp:526 +#, fuzzy, kde-format +#| msgid "Mouse Emulation" +msgid "Pressed Buttons" +msgstr "محاكاة الفأرة" + +#: debug_console.cpp:525 +#, kde-format +msgid "Tablet Pad Button" +msgstr "" + +#: debug_console.cpp:535 +#, kde-format +msgid "Tablet Pad Strip" +msgstr "" + +#: debug_console.cpp:536 debug_console.cpp:548 +#, kde-format +msgid "Number" +msgstr "" + +#: debug_console.cpp:538 debug_console.cpp:550 +#, kde-format +msgid "isFinger" +msgstr "" + +#: debug_console.cpp:547 +#, kde-format +msgid "Tablet Pad Ring" +msgstr "" + +#: debug_console.cpp:735 +#, fuzzy, kde-format +#| msgid "Mouse Emulation" +msgid "No Mouse Buttons" +msgstr "محاكاة الفأرة" + +#: debug_console.cpp:739 +#, kde-format +msgctxt "Mouse Button" +msgid "left" +msgstr "" + +#: debug_console.cpp:742 +#, fuzzy, kde-format +#| msgid "Top-Right" +msgctxt "Mouse Button" +msgid "right" +msgstr "أعلى اليمين" + +#: debug_console.cpp:745 +#, kde-format +msgctxt "Mouse Button" +msgid "middle" +msgstr "" + +#: debug_console.cpp:748 +#, kde-format +msgctxt "Mouse Button" +msgid "back" +msgstr "" + +#: debug_console.cpp:751 +#, kde-format +msgctxt "Mouse Button" +msgid "forward" +msgstr "" + +#: debug_console.cpp:754 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 1" +msgstr "" + +#: debug_console.cpp:757 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 2" +msgstr "" + +#: debug_console.cpp:760 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 3" +msgstr "" + +#: debug_console.cpp:763 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 4" +msgstr "" + +#: debug_console.cpp:766 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 5" +msgstr "" + +#: debug_console.cpp:769 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 6" +msgstr "" + +#: debug_console.cpp:772 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 7" +msgstr "" + +#: debug_console.cpp:775 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 8" +msgstr "" + +#: debug_console.cpp:778 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 9" +msgstr "" + +#: debug_console.cpp:781 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 10" +msgstr "" + +#: debug_console.cpp:784 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 11" +msgstr "" + +#: debug_console.cpp:787 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 12" +msgstr "" + +#: debug_console.cpp:790 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 13" +msgstr "" + +#: debug_console.cpp:793 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 14" +msgstr "" + +#: debug_console.cpp:796 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 15" +msgstr "" + +#: debug_console.cpp:799 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 16" +msgstr "" + +#: debug_console.cpp:802 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 17" +msgstr "" + +#: debug_console.cpp:805 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 18" +msgstr "" + +#: debug_console.cpp:808 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 19" +msgstr "" + +#: debug_console.cpp:811 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 20" +msgstr "" + +#: debug_console.cpp:814 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 21" +msgstr "" + +#: debug_console.cpp:817 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 22" +msgstr "" + +#: debug_console.cpp:820 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 23" +msgstr "" + +#: debug_console.cpp:823 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 24" +msgstr "" + +#: debug_console.cpp:826 +#, kde-format +msgctxt "Mouse Button" +msgid "task" +msgstr "" + +#: debug_console.cpp:1176 +#, fuzzy, kde-format +#| msgid "Close Window" +msgid "X11 Client Windows" +msgstr "أغلق النّافذة" + +#: debug_console.cpp:1178 +#, kde-format +msgid "X11 Unmanaged Windows" +msgstr "" + +#: debug_console.cpp:1180 +#, fuzzy, kde-format +#| msgid "Shade Window" +msgid "Wayland Windows" +msgstr "ظلّل النّافذة" + +#: debug_console.cpp:1182 +#, fuzzy, kde-format +#| msgid "Lower Window" +msgid "Internal Windows" +msgstr "اخفض النّافذة" + +#. i18n: ectx: property (text), widget (QPushButton, quitButton) +#: debug_console.ui:32 +#, kde-format +msgid "Quit Debug Console" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, windows) +#: debug_console.ui:45 +#, kde-format +msgid "Windows" +msgstr "النوافذ" + +#. i18n: ectx: attribute (title), widget (QWidget, surfaces) +#: debug_console.ui:59 +#, kde-format +msgid "Surfaces" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, input) +#: debug_console.ui:69 +#, kde-format +msgid "Input Events" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, inputDevices) +#: debug_console.ui:86 +#, kde-format +msgid "Input Devices" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, tab) +#: debug_console.ui:96 +#, kde-format +msgid "OpenGL" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, noOpenGLLabel) +#: debug_console.ui:102 +#, kde-format +msgid "No OpenGL compositor running" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, driverInfoBox) +#: debug_console.ui:130 +#, kde-format +msgid "OpenGL (ES) driver information" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: debug_console.ui:136 +#, kde-format +msgid "Vendor:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: debug_console.ui:143 +#, kde-format +msgid "Renderer:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: debug_console.ui:150 +#, kde-format +msgid "Version:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: debug_console.ui:157 +#, kde-format +msgid "Shading Language Version:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: debug_console.ui:164 +#, kde-format +msgid "Driver:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: debug_console.ui:171 +#, kde-format +msgid "GPU class:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: debug_console.ui:178 +#, kde-format +msgid "OpenGL Version:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_8) +#: debug_console.ui:185 +#, kde-format +msgid "GLSL Version:" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, platformExtensionsBox) +#: debug_console.ui:251 +#, fuzzy, kde-format +#| msgid "&Extensions" +msgid "Platform Extensions" +msgstr "الامت&دادات" + +#. i18n: ectx: property (title), widget (QGroupBox, glExtensionsBox) +#: debug_console.ui:267 +#, fuzzy, kde-format +#| msgid "&Extensions" +msgid "OpenGL (ES) Extensions" +msgstr "الامت&دادات" + +#. i18n: ectx: attribute (title), widget (QWidget, keyboard) +#: debug_console.ui:288 +#, fuzzy, kde-format +#| msgid "Next Layout" +msgid "Keyboard" +msgstr "التخطيط التالي" + +#. i18n: ectx: property (title), widget (QGroupBox, layoutBox) +#: debug_console.ui:315 +#, fuzzy, kde-format +#| msgid "Next Layout" +msgid "Keymap Layouts" +msgstr "التخطيط التالي" + +#. i18n: ectx: property (text), widget (QLabel, label_9) +#: debug_console.ui:337 +#, fuzzy, kde-format +#| msgid "Configur&e Window Behavior..." +msgid "Current Layout:" +msgstr "&اضبط سلوك النوافذ..." + +#. i18n: ectx: property (title), widget (QGroupBox, activeModifiersBox) +#: debug_console.ui:372 +#, kde-format +msgid "Active Modifiers" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, ledsBox) +#: debug_console.ui:388 +#, kde-format +msgid "LEDs" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, activeLedsBox) +#: debug_console.ui:404 +#, kde-format +msgid "Active LEDs" +msgstr "" + +#: helpers/killer/killer.cpp:30 +#, kde-format +msgid "Window Manager" +msgstr "مدير نوافذ" + +#: helpers/killer/killer.cpp:35 +#, kde-format +msgid "PID of the application to terminate" +msgstr "معرّف عمليّة التّطبيق لإنهائه." + +#: helpers/killer/killer.cpp:35 +#, kde-format +msgid "pid" +msgstr "معرّف_العمليّة" + +#: helpers/killer/killer.cpp:37 +#, kde-format +msgid "Hostname on which the application is running" +msgstr "اسم المضيف الذي يعمل فيه التّطبيق" + +#: helpers/killer/killer.cpp:37 +#, kde-format +msgid "hostname" +msgstr "اسم_المضيف" + +#: helpers/killer/killer.cpp:39 +#, kde-format +msgid "Caption of the window to be terminated" +msgstr "واصفة النّافذة لإنهائها" + +#: helpers/killer/killer.cpp:39 +#, kde-format +msgid "caption" +msgstr "الواصفة" + +#: helpers/killer/killer.cpp:41 +#, kde-format +msgid "Name of the application to be terminated" +msgstr "اسم التّطبيق لإنهائه." + +#: helpers/killer/killer.cpp:41 +#, kde-format +msgid "name" +msgstr "الاسم" + +#: helpers/killer/killer.cpp:43 +#, kde-format +msgid "ID of resource belonging to the application" +msgstr "معرّف المورد التّابع للتّطبيق" + +#: helpers/killer/killer.cpp:43 +#, kde-format +msgid "id" +msgstr "المعرّف" + +#: helpers/killer/killer.cpp:45 +#, kde-format +msgid "Time of user action causing termination" +msgstr "وقت إجراء المستخدم المسبّب للإنهاء" + +#: helpers/killer/killer.cpp:45 +#, kde-format +msgid "time" +msgstr "الوقت" + +#: helpers/killer/killer.cpp:47 +#, kde-format +msgid "KWin helper utility" +msgstr "أداة نوافذك للمساعدة" + +#: helpers/killer/killer.cpp:71 +#, kde-format +msgid "This helper utility is not supposed to be called directly." +msgstr "لا يفترض طلب الأداة المساعدة هذه مباشرةً." + +#: helpers/killer/killer.cpp:81 +#, kde-format +msgctxt "@info" +msgid "Application \"%1\" is not responding" +msgstr "التّطبيق \"%1\" لا يستجيب" + +#: helpers/killer/killer.cpp:83 +#, fuzzy, kde-kuit-format +#| msgctxt "@info" +#| msgid "" +#| "You tried to close window \"%1\" from application \"%2\" (Process " +#| "ID: %3) but the application is not responding." +msgctxt "@info" +msgid "" +"

You tried to close window \"%1\" from application \"%2\" (Process ID: %3) " +"but the application is not responding.

" +msgstr "" +"حاولت إغلاق النّافذة \"%1\" من التّطبيق \"%2\" (معرّف العمليّة: %3) لكنّ " +"التّطبيق لا يستجيب." + +#: helpers/killer/killer.cpp:85 +#, fuzzy, kde-kuit-format +#| msgctxt "@info" +#| msgid "" +#| "You tried to close window \"%1\" from application \"%2\" (Process " +#| "ID: %3), running on host \"%4\", but the application is not responding." +msgctxt "@info" +msgid "" +"

You tried to close window \"%1\" from application \"%2\" (Process ID: " +"%3), running on host \"%4\", but the application is not responding.

" +msgstr "" +"حاولت إغلاق النّافذة \"%1\" من التّطبيق \"%2\" (معرّف العمليّة: %3)، والّتي " +"تعمل على المضيف \"%4، لكنّ التّطبيق لا يستجيب." + +#: helpers/killer/killer.cpp:88 +#, fuzzy, kde-kuit-format +#| msgctxt "@info" +#| msgid "" +#| "Do you want to terminate this application?Terminating the application will close all of its " +#| "child windows. Any unsaved data will be lost." +msgctxt "@info" +msgid "" +"

Do you want to terminate this application?

Terminating the " +"application will close all of its child windows. Any unsaved data will be " +"lost.

" +msgstr "" +"أتريد إنهاء هذا التّطبيق؟إنهاء التّطبيق سيغلق كلّ " +"نوافذه الابنة. أيّة بيانات غير محفوظة ستُفقد." + +#: helpers/killer/killer.cpp:92 +#, kde-format +msgid "&Terminate Application %1" +msgstr "أ&نهِ التّطبيق %1" + +#: helpers/killer/killer.cpp:93 +#, kde-format +msgid "Wait Longer" +msgstr "انتظر أكثر" + +#: keyboard_layout.cpp:110 keyboard_layout.cpp:111 +#, fuzzy, kde-format +#| msgid "Next Layout" +msgctxt "tooltip title" +msgid "Keyboard Layout" +msgstr "التخطيط التالي" + +#: keyboard_layout.cpp:258 +#, fuzzy, kde-format +#| msgid "Configur&e Window Behavior..." +msgid "Configure Layouts..." +msgstr "&اضبط سلوك النوافذ..." + +#: killwindow.cpp:33 +#, kde-format +msgid "" +"Select window to force close with left click or enter.\n" +"Escape or right click to cancel." +msgstr "" + +#: kwinbindings.cpp:38 +#, kde-format +msgid "Window Operations Menu" +msgstr "قائمة عمليّات النّافذة" + +#: kwinbindings.cpp:40 +#, kde-format +msgid "Close Window" +msgstr "أغلق النّافذة" + +#: kwinbindings.cpp:42 +#, kde-format +msgid "Maximize Window" +msgstr "كبّر النّافذة" + +#: kwinbindings.cpp:44 +#, kde-format +msgid "Maximize Window Vertically" +msgstr "كبّر النّافذة رأسيًّا" + +#: kwinbindings.cpp:46 +#, kde-format +msgid "Maximize Window Horizontally" +msgstr "كبّر النّافذة أفقيًّا" + +#: kwinbindings.cpp:48 +#, kde-format +msgid "Minimize Window" +msgstr "صغّر النّافذة" + +#: kwinbindings.cpp:50 +#, kde-format +msgid "Shade Window" +msgstr "ظلّل النّافذة" + +#: kwinbindings.cpp:52 +#, kde-format +msgid "Move Window" +msgstr "انقل النّافذة" + +#: kwinbindings.cpp:54 +#, kde-format +msgid "Resize Window" +msgstr "غيّر حجم النّافذة" + +#: kwinbindings.cpp:56 +#, kde-format +msgid "Raise Window" +msgstr "ارفع النّافذة" + +#: kwinbindings.cpp:58 +#, kde-format +msgid "Lower Window" +msgstr "اخفض النّافذة" + +#: kwinbindings.cpp:60 +#, kde-format +msgid "Toggle Window Raise/Lower" +msgstr "بدّل رفع/خفض النّافذة" + +#: kwinbindings.cpp:62 +#, kde-format +msgid "Make Window Fullscreen" +msgstr "اجعل النّافذة بملء الشّاشة" + +#: kwinbindings.cpp:64 +#, kde-format +msgid "Hide Window Border" +msgstr "أخفِ حدّ النّافذة" + +#: kwinbindings.cpp:66 +#, kde-format +msgid "Keep Window Above Others" +msgstr "أبقِ النّافذة فوق غيرها" + +#: kwinbindings.cpp:68 +#, kde-format +msgid "Keep Window Below Others" +msgstr "أبقِ النّافذة تحت غيرها" + +#: kwinbindings.cpp:70 +#, kde-format +msgid "Activate Window Demanding Attention" +msgstr "نشّط النّافذة الطّالبة الانتباه" + +#: kwinbindings.cpp:72 +#, kde-format +msgid "Setup Window Shortcut" +msgstr "أعدّ اختصار النّافذة" + +#: kwinbindings.cpp:74 +#, kde-format +msgid "Pack Window to the Right" +msgstr "احزم النّافذة إلى اليمين" + +#: kwinbindings.cpp:76 +#, kde-format +msgid "Pack Window to the Left" +msgstr "احزم النّافذة إلى اليسار" + +#: kwinbindings.cpp:78 +#, kde-format +msgid "Pack Window Up" +msgstr "احزم النّافذة لأعلى" + +#: kwinbindings.cpp:80 +#, kde-format +msgid "Pack Window Down" +msgstr "احزم النّافذة لأسفل" + +#: kwinbindings.cpp:82 +#, kde-format +msgid "Pack Grow Window Horizontally" +msgstr "احزم النّافذة نامية أفقيًّا" + +#: kwinbindings.cpp:84 +#, kde-format +msgid "Pack Grow Window Vertically" +msgstr "احزم النّافذة بإنمائها رأسيًّا" + +#: kwinbindings.cpp:86 +#, kde-format +msgid "Pack Shrink Window Horizontally" +msgstr "احزم النّافذة بتقليصها أفقيًّا" + +#: kwinbindings.cpp:88 +#, kde-format +msgid "Pack Shrink Window Vertically" +msgstr "احزم النّافذة بتقليصها رأسيًّا" + +#: kwinbindings.cpp:90 +#, kde-format +msgid "Quick Tile Window to the Left" +msgstr "" + +#: kwinbindings.cpp:92 +#, kde-format +msgid "Quick Tile Window to the Right" +msgstr "" + +#: kwinbindings.cpp:94 +#, fuzzy, kde-format +#| msgid "Pack Window to the Left" +msgid "Quick Tile Window to the Top" +msgstr "احزم النّافذة إلى اليسار" + +#: kwinbindings.cpp:96 +#, fuzzy, kde-format +#| msgid "Pack Window to the Left" +msgid "Quick Tile Window to the Bottom" +msgstr "احزم النّافذة إلى اليسار" + +#: kwinbindings.cpp:98 +#, kde-format +msgid "Quick Tile Window to the Top Left" +msgstr "" + +#: kwinbindings.cpp:100 +#, kde-format +msgid "Quick Tile Window to the Bottom Left" +msgstr "" + +#: kwinbindings.cpp:102 +#, kde-format +msgid "Quick Tile Window to the Top Right" +msgstr "" + +#: kwinbindings.cpp:104 +#, kde-format +msgid "Quick Tile Window to the Bottom Right" +msgstr "" + +#: kwinbindings.cpp:106 +#, kde-format +msgid "Switch to Window Above" +msgstr "بدّل إلى النّافذة العليا" + +#: kwinbindings.cpp:108 +#, kde-format +msgid "Switch to Window Below" +msgstr "بدّل إلى النّافذة السفلى" + +#: kwinbindings.cpp:110 +#, kde-format +msgid "Switch to Window to the Right" +msgstr "بدّل إلى النّافذة اليمنى" + +#: kwinbindings.cpp:112 +#, kde-format +msgid "Switch to Window to the Left" +msgstr "بدّل إلى النّافذة اليسرى" + +#: kwinbindings.cpp:114 +#, kde-format +msgid "Increase Opacity of Active Window by 5 %" +msgstr "زِد عتمة النّافذة النّشطة 5%" + +#: kwinbindings.cpp:116 +#, kde-format +msgid "Decrease Opacity of Active Window by 5 %" +msgstr "أنقص عتمة النّافذة النّشطة 5%" + +#: kwinbindings.cpp:119 +#, kde-format +msgid "Keep Window on All Desktops" +msgstr "أبقِ النّافذة في كلّ أسطح المكتب" + +#: kwinbindings.cpp:123 +#, kde-format +msgid "Window to Desktop %1" +msgstr "النّافذة إلى سطح المكتب %1" + +#: kwinbindings.cpp:125 +#, kde-format +msgid "Window to Next Desktop" +msgstr "النّافذة إلى سطح المكتب التّالي" + +#: kwinbindings.cpp:126 +#, kde-format +msgid "Window to Previous Desktop" +msgstr "النّافذة إلى سطح المكتب السّابق" + +#: kwinbindings.cpp:127 +#, kde-format +msgid "Window One Desktop to the Right" +msgstr "" + +#: kwinbindings.cpp:128 +#, kde-format +msgid "Window One Desktop to the Left" +msgstr "" + +#: kwinbindings.cpp:129 +#, kde-format +msgid "Window One Desktop Up" +msgstr "" + +#: kwinbindings.cpp:130 +#, kde-format +msgid "Window One Desktop Down" +msgstr "" + +#: kwinbindings.cpp:133 +#, kde-format +msgid "Window to Screen %1" +msgstr "" + +#: kwinbindings.cpp:135 +#, kde-format +msgid "Window to Next Screen" +msgstr "" + +#: kwinbindings.cpp:136 +#, kde-format +msgid "Window to Previous Screen" +msgstr "" + +#: kwinbindings.cpp:137 +#, kde-format +msgid "Show Desktop" +msgstr "أظهر سطح المكتب" + +#: kwinbindings.cpp:140 +#, kde-format +msgid "Switch to Screen %1" +msgstr "بدّل إلى الشّاشة %1" + +#: kwinbindings.cpp:143 +#, kde-format +msgid "Switch to Next Screen" +msgstr "بدّل إلى الشّاشة التّالية" + +#: kwinbindings.cpp:144 +#, kde-format +msgid "Switch to Previous Screen" +msgstr "بدّل إلى الشّاشة السّابقة" + +#: kwinbindings.cpp:146 +#, kde-format +msgid "Kill Window" +msgstr "اقتل النّافذة" + +#: kwinbindings.cpp:147 +#, kde-format +msgid "Suspend Compositing" +msgstr "علّق التّركيب" + +#: kwinbindings.cpp:148 +#, kde-format +msgid "Invert Screen Colors" +msgstr "اعكس ألوان الشّاشة" + +#: main.cpp:184 main.cpp:214 +#, kde-format +msgid "KDE window manager" +msgstr "مدير كدي للنّوافذ" + +#: main.cpp:189 +#, kde-format +msgid "KWin" +msgstr "نوافذك" + +#: main.cpp:193 +#, kde-format +msgid "(c) 1999-2019, The KDE Developers" +msgstr "" + +#: main.cpp:195 +#, kde-format +msgid "Matthias Ettrich" +msgstr "" + +#: main.cpp:196 +#, kde-format +msgid "Cristian Tibirna" +msgstr "" + +#: main.cpp:197 +#, kde-format +msgid "Daniel M. Duley" +msgstr "" + +#: main.cpp:198 +#, kde-format +msgid "Luboš Luňák" +msgstr "" + +#: main.cpp:199 +#, kde-format +msgid "Martin Flöser" +msgstr "" + +#: main.cpp:200 +#, kde-format +msgid "David Edmundson" +msgstr "" + +#: main.cpp:201 +#, kde-format +msgid "Roman Gilg" +msgstr "" + +#: main.cpp:202 +#, kde-format +msgid "Vlad Zahorodnii" +msgstr "" + +#: main.cpp:211 +#, kde-format +msgid "Disable configuration options" +msgstr "عطّل خيارات الضّبط" + +#: main.cpp:212 +#, kde-format +msgid "Indicate that KWin has recently crashed n times" +msgstr "يشير إلى أنّ نوافذك انهار ن مرّة حديثًا" + +#: main_wayland.cpp:459 +#, kde-format +msgid "Start a rootless Xwayland server." +msgstr "" + +#: main_wayland.cpp:461 +#, kde-format +msgid "" +"Name of the Wayland socket to listen on. If not set \"wayland-0\" is used." +msgstr "" + +#: main_wayland.cpp:464 +#, kde-format +msgid "Render to framebuffer." +msgstr "" + +#: main_wayland.cpp:466 +#, kde-format +msgid "The framebuffer device to render to." +msgstr "" + +#: main_wayland.cpp:469 +#, kde-format +msgid "The X11 Display to use in windowed mode on platform X11." +msgstr "" + +#: main_wayland.cpp:472 +#, kde-format +msgid "The Wayland Display to use in windowed mode on platform Wayland." +msgstr "" + +#: main_wayland.cpp:474 +#, kde-format +msgid "Render to a virtual framebuffer." +msgstr "" + +#: main_wayland.cpp:476 +#, kde-format +msgid "The width for windowed mode. Default width is 1024." +msgstr "" + +#: main_wayland.cpp:480 +#, kde-format +msgid "The height for windowed mode. Default height is 768." +msgstr "" + +#: main_wayland.cpp:485 +#, kde-format +msgid "The scale for windowed mode. Default value is 1." +msgstr "" + +#: main_wayland.cpp:490 +#, kde-format +msgid "" +"The number of windows to open as outputs in windowed mode. Default value is 1" +msgstr "" + +#: main_wayland.cpp:520 +#, kde-format +msgid "Use libhybris hwcomposer" +msgstr "" + +#: main_wayland.cpp:526 +#, fuzzy, kde-format +msgid "" +"Enable libinput support for input events processing. Note: never use in a " +"nested session.\t(deprecated)" +msgstr "مكّن دعم libinput لمعالجة أحداث الدَّخل. لاحظ: أبدًا لا تستخدمه في جلسة" + +#: main_wayland.cpp:529 +#, kde-format +msgid "Render through drm node." +msgstr "" + +#: main_wayland.cpp:536 +#, kde-format +msgid "Input method that KWin starts." +msgstr "" + +#: main_wayland.cpp:541 +#, kde-format +msgid "List all available backends and quit." +msgstr "" + +#: main_wayland.cpp:545 +#, kde-format +msgid "Starts the session in locked mode." +msgstr "" + +#: main_wayland.cpp:549 +#, kde-format +msgid "Starts the session without lock screen support." +msgstr "" + +#: main_wayland.cpp:553 +#, kde-format +msgid "Starts the session without global shortcuts support." +msgstr "" + +#: main_wayland.cpp:557 +#, kde-format +msgid "Exit after the session application, which is started by KWin, closed." +msgstr "" + +#: main_wayland.cpp:562 +#, kde-format +msgid "Applications to start once Wayland and Xwayland server are started" +msgstr "" + +#: main_x11.cpp:65 +#, kde-format +msgid "" +"KWin is unstable.\n" +"It seems to have crashed several times in a row.\n" +"You can select another window manager to run:" +msgstr "" +"نوافذك غير مستقرّ.\n" +"يبدو أنّه انهار عدّة مرّات متتالية.\n" +"يمكنك اختيار مدير نوافذ آخر لتشغيله:" + +#: main_x11.cpp:224 +#, kde-format +msgid "" +"kwin: unable to claim manager selection, another wm running? (try using --" +"replace)\n" +msgstr "" + +#: main_x11.cpp:241 +#, kde-format +msgid "kwin: another window manager is running (try using --replace)\n" +msgstr "نوافذك: مدير نوافذ آخر يعمل (جرّب استخدام ‎--replace)\n" + +#: main_x11.cpp:437 +#, kde-format +msgid "Replace already-running ICCCM2.0-compliant window manager" +msgstr "استبدل مدير النّوافذ المتوافق مع ICCCM2.0 المشغّل بالفعل" + +#: main_x11.cpp:444 +#, kde-format +msgid "Disable KActivities integration." +msgstr "" + +#: plugins/scenes/opengl/scene_opengl.cpp:535 +#, kde-format +msgid "Desktop effects were restarted due to a graphics reset" +msgstr "أُعيد تشغيل تأثيرات سطح المكتب بسبب تصفير الرّسوميّات" + +#. i18n: ectx: label, entry (count), group (General) +#: rulebooksettingsbase.kcfg:9 +#, kde-format +msgid "Total rules count" +msgstr "" + +#. i18n: ectx: label, entry (description), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:10 +#, kde-format +msgid "Rule description" +msgstr "" + +#. i18n: ectx: label, entry (descriptionLegacy), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:13 +#, kde-format +msgid "Rule description (legacy)" +msgstr "" + +#. i18n: ectx: label, entry (DeleteRule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:16 +#, kde-format +msgid "Delete this rule (for use in imports)" +msgstr "" + +#. i18n: ectx: label, entry (wmclass), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:20 +#, kde-format +msgid "Window class (application)" +msgstr "" + +#. i18n: ectx: label, entry (wmclassmatch), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:23 +#, kde-format +msgid "Window class string match type" +msgstr "" + +#. i18n: ectx: label, entry (wmclasscomplete), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:29 +#, kde-format +msgid "Match whole window class" +msgstr "" + +#. i18n: ectx: label, entry (windowrole), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:34 +#, fuzzy, kde-format +#| msgid "Windows" +msgid "Window role" +msgstr "النوافذ" + +#. i18n: ectx: label, entry (windowrolematch), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:37 +#, kde-format +msgid "Window role string match type" +msgstr "" + +#. i18n: ectx: label, entry (title), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:44 +#, fuzzy, kde-format +#| msgid "Windows" +msgid "Window title" +msgstr "النوافذ" + +#. i18n: ectx: label, entry (titlematch), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:47 +#, kde-format +msgid "Window title string match type" +msgstr "" + +#. i18n: ectx: label, entry (clientmachine), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:54 +#, fuzzy, kde-format +#| msgid "hostname" +msgid "Machine (hostname)" +msgstr "اسم_المضيف" + +#. i18n: ectx: label, entry (clientmachinematch), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:57 +#, kde-format +msgid "Machine string match type" +msgstr "" + +#. i18n: ectx: label, entry (types), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:64 +#, kde-format +msgid "Window types that match" +msgstr "" + +#. i18n: ectx: label, entry (placement), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:69 +#, kde-format +msgid "Initial placement" +msgstr "" + +#. i18n: ectx: label, entry (placementrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:74 +#, fuzzy, kde-format +#| msgid "&Fullscreen" +msgid "Initial placement rule type" +msgstr "&ملء الشّاشة" + +#. i18n: ectx: label, entry (position), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:79 +#, fuzzy, kde-format +#| msgid "Window Operations Menu" +msgid "Window position" +msgstr "قائمة عمليّات النّافذة" + +#. i18n: ectx: label, entry (positionrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:83 +#, fuzzy, kde-format +#| msgid "Window to Screen 0" +msgid "Window position rule type" +msgstr "النافذة للشاشة 0" + +#. i18n: ectx: label, entry (size), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:90 +#, fuzzy, kde-format +#| msgid "Windows" +msgid "Window size" +msgstr "النوافذ" + +#. i18n: ectx: label, entry (sizerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:93 +#, kde-format +msgid "Window size rule type" +msgstr "" + +#. i18n: ectx: label, entry (minsize), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:100 +#, kde-format +msgid "Window minimum size" +msgstr "" + +#. i18n: ectx: label, entry (minsizerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:104 +#, kde-format +msgid "Window minimum size rule type" +msgstr "" + +#. i18n: ectx: label, entry (maxsize), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:109 +#, kde-format +msgid "Window maximum size" +msgstr "" + +#. i18n: ectx: label, entry (maxsizerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:113 +#, kde-format +msgid "Window maximum size rule type" +msgstr "" + +#. i18n: ectx: label, entry (opacityactive), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:118 +#, kde-format +msgid "Active opacity" +msgstr "" + +#. i18n: ectx: label, entry (opacityactiverule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:124 +#, kde-format +msgid "Active opacity rule type" +msgstr "" + +#. i18n: ectx: label, entry (opacityinactive), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:129 +#, kde-format +msgid "Inactive opacity" +msgstr "" + +#. i18n: ectx: label, entry (opacityinactiverule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:135 +#, kde-format +msgid "Inactive opacity rule type" +msgstr "" + +#. i18n: ectx: label, entry (ignoregeometry), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:140 +#, kde-format +msgid "Ignore requested geometry" +msgstr "" + +#. i18n: ectx: label, entry (ignoregeometryrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:144 +#, kde-format +msgid "Ignore requested geometry rule type" +msgstr "" + +#. i18n: ectx: label, entry (desktop), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:151 +#, fuzzy, kde-format +#| msgid "Desktop Cube" +msgid "Desktop number" +msgstr "مكعب سطح المكتب" + +#. i18n: ectx: label, entry (desktoprule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:155 +#, fuzzy, kde-format +#| msgid "Desktop Cube" +msgid "Desktop number rule type" +msgstr "مكعب سطح المكتب" + +#. i18n: ectx: label, entry (screen), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:162 +#, kde-format +msgid "Screen number" +msgstr "" + +#. i18n: ectx: label, entry (screenrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:166 +#, kde-format +msgid "Screen number rule type" +msgstr "" + +#. i18n: ectx: label, entry (activity), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:173 +#, fuzzy, kde-format +#| msgid "&All Activities" +msgid "Activity" +msgstr "&كلّ الأنشطة" + +#. i18n: ectx: label, entry (activityrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:176 +#, kde-format +msgid "Activity rule type" +msgstr "" + +#. i18n: ectx: label, entry (type), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:183 +#, fuzzy, kde-format +#| msgid "Setup Window Shortcut" +msgid "Set window type to" +msgstr "أعدّ اختصار النّافذة" + +#. i18n: ectx: label, entry (typerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:189 +#, kde-format +msgid "Set window type rule type" +msgstr "" + +#. i18n: ectx: label, entry (maximizevert), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:194 +#, fuzzy, kde-format +#| msgid "Maximize Window Vertically" +msgid "Maximized vertically" +msgstr "كبّر النّافذة رأسيًّا" + +#. i18n: ectx: label, entry (maximizevertrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:198 +#, fuzzy, kde-format +#| msgid "Maximize Window Vertically" +msgid "Maximized vertically rule type" +msgstr "كبّر النّافذة رأسيًّا" + +#. i18n: ectx: label, entry (maximizehoriz), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:205 +#, fuzzy, kde-format +#| msgid "Maximize Window Horizontally" +msgid "Maximized horizontally" +msgstr "كبّر النّافذة أفقيًّا" + +#. i18n: ectx: label, entry (maximizehorizrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:209 +#, fuzzy, kde-format +#| msgid "Maximize Window Horizontally" +msgid "Maximized horizontally rule type" +msgstr "كبّر النّافذة أفقيًّا" + +#. i18n: ectx: label, entry (minimize), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:216 +#, fuzzy, kde-format +#| msgid "Minimize" +msgid "Minimized" +msgstr "صغّر" + +#. i18n: ectx: label, entry (minimizerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:220 +#, kde-format +msgid "Minimized rule type" +msgstr "" + +#. i18n: ectx: label, entry (shade), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:227 +#, fuzzy, kde-format +#| msgid "Shade" +msgid "Shaded" +msgstr "ظلّل" + +#. i18n: ectx: label, entry (shaderule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:231 +#, kde-format +msgid "Shaded rule type" +msgstr "" + +#. i18n: ectx: label, entry (skiptaskbar), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:238 +#, kde-format +msgid "Skip taskbar" +msgstr "" + +#. i18n: ectx: label, entry (skiptaskbarrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:242 +#, kde-format +msgid "Skip taskbar rule type" +msgstr "" + +#. i18n: ectx: label, entry (skippager), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:249 +#, kde-format +msgid "Skip pager" +msgstr "" + +#. i18n: ectx: label, entry (skippagerrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:253 +#, kde-format +msgid "Skip pager rule type" +msgstr "" + +#. i18n: ectx: label, entry (skipswitcher), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:260 +#, fuzzy, kde-format +#| msgid "Switch to Tab" +msgid "Skip switcher" +msgstr "بدّل إلى اللسان" + +#. i18n: ectx: label, entry (skipswitcherrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:264 +#, kde-format +msgid "Skip switcher rule type" +msgstr "" + +#. i18n: ectx: label, entry (above), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:271 +#, fuzzy, kde-format +#| msgid "Keep above others" +msgid "Keep above" +msgstr "أبقهِ فوق الآخرين" + +#. i18n: ectx: label, entry (aboverule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:275 +#, fuzzy, kde-format +#| msgid "Keep above others" +msgid "Keep above rule type" +msgstr "أبقهِ فوق الآخرين" + +#. i18n: ectx: label, entry (below), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:282 +#, fuzzy, kde-format +#| msgid "Keep below others" +msgid "Keep below" +msgstr "أبقهِ تحت الآخرين" + +#. i18n: ectx: label, entry (belowrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:286 +#, fuzzy, kde-format +#| msgid "Keep below others" +msgid "Keep below rule type" +msgstr "أبقهِ تحت الآخرين" + +#. i18n: ectx: label, entry (fullscreen), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:293 +#, fuzzy, kde-format +#| msgid "&Fullscreen" +msgid "Fullscreen" +msgstr "&ملء الشّاشة" + +#. i18n: ectx: label, entry (fullscreenrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:297 +#, fuzzy, kde-format +#| msgid "&Fullscreen" +msgid "Fullscreen rule type" +msgstr "&ملء الشّاشة" + +#. i18n: ectx: label, entry (noborder), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:304 +#, kde-format +msgid "No titlebar and frame" +msgstr "" + +#. i18n: ectx: label, entry (noborderrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:308 +#, kde-format +msgid "No titlebar rule type" +msgstr "" + +#. i18n: ectx: label, entry (decocolor), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:315 +#, kde-format +msgid "Titlebar color and scheme" +msgstr "" + +#. i18n: ectx: label, entry (decocolorrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:318 +#, kde-format +msgid "Titlebar color rule type" +msgstr "" + +#. i18n: ectx: label, entry (blockcompositing), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:323 +#, fuzzy, kde-format +#| msgid "Suspend Compositing" +msgid "Block Compositing" +msgstr "علّق التّركيب" + +#. i18n: ectx: label, entry (blockcompositingrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:327 +#, kde-format +msgid "Block Compositing rule type" +msgstr "" + +#. i18n: ectx: label, entry (fsplevel), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:332 +#, kde-format +msgid "Focus stealing prevention" +msgstr "" + +#. i18n: ectx: label, entry (fsplevelrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:338 +#, kde-format +msgid "Focus stealing prevention rule type" +msgstr "" + +#. i18n: ectx: label, entry (fpplevel), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:343 +#, kde-format +msgid "Focus protection" +msgstr "" + +#. i18n: ectx: label, entry (fpplevelrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:349 +#, kde-format +msgid "Focus protection rule type" +msgstr "" + +#. i18n: ectx: label, entry (acceptfocus), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:354 +#, kde-format +msgid "Accept Focus" +msgstr "" + +#. i18n: ectx: label, entry (acceptfocusrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:358 +#, kde-format +msgid "Accept Focus rule type" +msgstr "" + +#. i18n: ectx: label, entry (closeable), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:363 +#, fuzzy, kde-format +#| msgid "Close" +msgid "Closeable" +msgstr "أغلق" + +#. i18n: ectx: label, entry (closeablerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:367 +#, kde-format +msgid "Closeable rule type" +msgstr "" + +#. i18n: ectx: label, entry (autogroup), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:372 +#, kde-format +msgid "Autogroup with identical" +msgstr "" + +#. i18n: ectx: label, entry (autogrouprule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:376 +#, kde-format +msgid "Autogroup with identical rule type" +msgstr "" + +#. i18n: ectx: label, entry (autogroupfg), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:381 +#, kde-format +msgid "Autogroup in foreground" +msgstr "" + +#. i18n: ectx: label, entry (autogroupfgrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:385 +#, kde-format +msgid "Autogroup in foreground rule type" +msgstr "" + +#. i18n: ectx: label, entry (autogroupid), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:390 +#, kde-format +msgid "Autogroup by ID" +msgstr "" + +#. i18n: ectx: label, entry (autogroupidrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:393 +#, kde-format +msgid "Autogroup by ID rule type" +msgstr "" + +#. i18n: ectx: label, entry (strictgeometry), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:398 +#, kde-format +msgid "Obey geometry restrictions" +msgstr "" + +#. i18n: ectx: label, entry (strictgeometryrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:402 +#, kde-format +msgid "Obey geometry restrictions rule type" +msgstr "" + +#. i18n: ectx: label, entry (shortcut), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:407 +#, kde-format +msgid "Shortcut" +msgstr "" + +#. i18n: ectx: label, entry (shortcutrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:410 +#, kde-format +msgid "Shortcut rule type" +msgstr "" + +#. i18n: ectx: label, entry (disableglobalshortcuts), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:417 +#, fuzzy, kde-format +#| msgid "Block Global Shortcuts" +msgid "Ignore global shortcuts" +msgstr "امنع الاختصارات العامة" + +#. i18n: ectx: label, entry (disableglobalshortcutsrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:421 +#, kde-format +msgid "Ignore global shortcuts rule type" +msgstr "" + +#. i18n: ectx: label, entry (desktopfile), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:426 +#, fuzzy, kde-format +#| msgid "Desktop Cube" +msgid "Desktop file name" +msgstr "مكعب سطح المكتب" + +#. i18n: ectx: label, entry (desktopfilerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:429 +#, kde-format +msgid "Desktop file name rule type" +msgstr "" + +#: scripting/genericscriptedconfig.cpp:70 +#, kde-format +msgctxt "Error message" +msgid "Plugin does not provide configuration file in expected location" +msgstr "لم توفّر الملحقة ملفّ ضبط في المكان المتوقّع" + +#: scripting/scripting.cpp:117 +#, kde-format +msgctxt "Assertion failed in KWin script with given value" +msgid "Assertion failed: %1 is not null" +msgstr "فشل توكيد: %1 ليس خاليًا" + +#: scripting/scripting.cpp:135 +#, kde-format +msgctxt "Assertion failed in KWin script" +msgid "Assertion failed: argument is null" +msgstr "فشل توكيد: المعطى خالٍ" + +#: scripting/scripting.cpp:177 +#, kde-format +msgctxt "Error in KWin Script" +msgid "" +"Invalid number of arguments. At least service, path, interface and method " +"need to be provided" +msgstr "" + +#: scripting/scripting.cpp:183 +#, kde-format +msgctxt "Error in KWin Script" +msgid "" +"Invalid type. Service, path, interface and method need to be string values" +msgstr "" + +#: scripting/scriptingutils.cpp:17 +#, kde-format +msgctxt "syntax error in KWin script" +msgid "Invalid number of arguments" +msgstr "عدد معطيات غير صالح" + +#: scripting/scriptingutils.cpp:30 +#, kde-format +msgctxt "KWin Scripting function received incorrect value for an expected type" +msgid "%1 is not a variant type" +msgstr "%1 ليس نوع تنويعة" + +#. i18n: ectx: property (windowTitle), widget (QDialog, ShortcutDialog) +#: shortcutdialog.ui:14 +#, kde-format +msgid "Dialog" +msgstr "" + +#. i18n: ectx: property (text), widget (QToolButton, clearButton) +#: shortcutdialog.ui:25 +#, kde-format +msgid "..." +msgstr "" + +#: tabbox/tabbox.cpp:372 +#, kde-format +msgctxt "Special entry in alt+tab list for minimizing all windows" +msgid "Show Desktop" +msgstr "أظهر سطح المكتب" + +#: tabbox/tabbox.cpp:521 +#, kde-format +msgid "Walk Through Windows" +msgstr "تنقّل بين النّوافذ" + +#: tabbox/tabbox.cpp:522 +#, kde-format +msgid "Walk Through Windows (Reverse)" +msgstr "تنقّل بين النّوافذ (بالعكس)" + +#: tabbox/tabbox.cpp:523 +#, kde-format +msgid "Walk Through Windows Alternative" +msgstr "تنقّل بين النّوافذ البديلة" + +#: tabbox/tabbox.cpp:524 +#, kde-format +msgid "Walk Through Windows Alternative (Reverse)" +msgstr "تنقّل بين النّوافذ البديلة (بالعكس)" + +#: tabbox/tabbox.cpp:525 +#, kde-format +msgid "Walk Through Windows of Current Application" +msgstr "تنقّل بين نوافذ التّطبيق الحاليّ" + +#: tabbox/tabbox.cpp:526 +#, kde-format +msgid "Walk Through Windows of Current Application (Reverse)" +msgstr "تنقّل بين نوافذ التّطبيق الحاليّ (بالعكس)" + +#: tabbox/tabbox.cpp:527 +#, kde-format +msgid "Walk Through Windows of Current Application Alternative" +msgstr "تنقّل بين نوافذ التّطبيق الحاليّ البديلة" + +#: tabbox/tabbox.cpp:528 +#, kde-format +msgid "Walk Through Windows of Current Application Alternative (Reverse)" +msgstr "تنقّل بين نوافذ التّطبيق الحاليّ البديلة (بالعكس)" + +#: tabbox/tabbox.cpp:529 +#, kde-format +msgid "Walk Through Desktops" +msgstr "تنقّل بين أسطح المكتب" + +#: tabbox/tabbox.cpp:530 +#, kde-format +msgid "Walk Through Desktops (Reverse)" +msgstr "تنقّل بين أسطح المكتب (بالعكس)" + +#: tabbox/tabbox.cpp:531 +#, kde-format +msgid "Walk Through Desktop List" +msgstr "تنقّل بين قائمة أسطح المكتب" + +#: tabbox/tabbox.cpp:532 +#, kde-format +msgid "Walk Through Desktop List (Reverse)" +msgstr "تنقّل بين قائمة أسطح المكتب (بالعكس)" + +#: tabbox/tabboxhandler.cpp:272 +#, kde-format +msgid "" +"The Window Switcher installation is broken, resources are missing.\n" +"Contact your distribution about this." +msgstr "" +"تثبيت مبدّل النّوافذ معطوب، الموارد ناقصة.\n" +"تواصل مع توزيعتك حول هذا." + +#: useractions.cpp:167 +#, kde-format +msgid "" +"You have selected to show a window without its border.\n" +"Without the border, you will not be able to enable the border again using " +"the mouse: use the window operations menu instead, activated using the %1 " +"keyboard shortcut." +msgstr "" +"لقد اخترت إظهار نافذة بلا حدّ.\n" +"بدون الحدود لن تستطيع تمكين الحدّ مجدّدًا باستخدام الفأرة: استخدم قائمة عمليّات " +"النّافذة بدل ذلك، لتنشيطها استخدم اختصار لوحة المفاتيح %1." + +#: useractions.cpp:175 +#, kde-format +msgid "" +"You have selected to show a window in fullscreen mode.\n" +"If the application itself does not have an option to turn the fullscreen " +"mode off you will not be able to disable it again using the mouse: use the " +"window operations menu instead, activated using the %1 keyboard shortcut." +msgstr "" +"لقد اخترت إظهار نافذة بوضع ملء الشّاشة.\n" +"إن لم يحوي التّطبيق نفسه خيارًا لإطفاء وقت ملء الشّاشة لن تستطيع تعطيله مجدّدًا " +"باستخدام الفأرة: استخدم قائمة عمليّات النّافذة بدل ذلك، لتنشيطها استخدم اختصار " +"لوحة المفاتيح %1." + +#: useractions.cpp:240 +#, kde-format +msgid "&Move" +msgstr "ا&نقل" + +#: useractions.cpp:245 +#, fuzzy, kde-format +#| msgid "Re&size" +msgid "&Resize" +msgstr "&غيّر الحجم" + +#: useractions.cpp:250 +#, kde-format +msgid "Keep &Above Others" +msgstr "أبقِ فو&ق غيرها" + +#: useractions.cpp:256 +#, kde-format +msgid "Keep &Below Others" +msgstr "أبقِ ت&حت غيرها" + +#: useractions.cpp:262 +#, kde-format +msgid "&Fullscreen" +msgstr "&ملء الشّاشة" + +#: useractions.cpp:268 +#, fuzzy, kde-format +#| msgid "Shade" +msgid "&Shade" +msgstr "ظلّل" + +#: useractions.cpp:274 +#, kde-format +msgid "&No Border" +msgstr "&بلا حدّ" + +#: useractions.cpp:282 +#, fuzzy, kde-format +#| msgid "Window &Shortcut..." +msgid "Set Window Short&cut..." +msgstr "اختصار ال&نّافذة..." + +#: useractions.cpp:287 +#, fuzzy, kde-format +#| msgid "&Special Window Settings..." +msgid "Configure Special &Window Settings..." +msgstr "إعدادات &خاصّة للنّافذة..." + +#: useractions.cpp:292 +#, fuzzy, kde-format +#| msgid "S&pecial Application Settings..." +msgid "Configure S&pecial Application Settings..." +msgstr "إعدادات خا&صّة للتّطبيق..." + +#: useractions.cpp:300 +#, fuzzy, kde-format +#| msgid "Window Manager" +msgctxt "" +"Entry in context menu of window decoration to open the configuration module " +"of KWin" +msgid "Configure W&indow Manager..." +msgstr "مدير نوافذ" + +#: useractions.cpp:328 +#, kde-format +msgid "Ma&ximize" +msgstr "&كبّر" + +#: useractions.cpp:334 +#, kde-format +msgid "Mi&nimize" +msgstr "&صغّر" + +#: useractions.cpp:340 +#, kde-format +msgid "&More Actions" +msgstr "إ&جراءات أكثر" + +#: useractions.cpp:343 +#, kde-format +msgid "&Close" +msgstr "أ&غلق" + +#: useractions.cpp:410 +#, kde-format +msgid "&Extensions" +msgstr "الامت&دادات" + +#: useractions.cpp:451 +#, fuzzy, kde-format +#| msgid "&All Desktops" +msgid "&Desktops" +msgstr "كلّ أ&سطح المكتب" + +#: useractions.cpp:465 +#, fuzzy, kde-format +#| msgid "Move To &Desktop" +msgid "Move to &Desktop" +msgstr "انقل إلى سطح المك&تب" + +#: useractions.cpp:483 +#, fuzzy, kde-format +#| msgid "Move To &Screen" +msgid "Move to &Screen" +msgstr "انقل إلى ال&شّاشة" + +#: useractions.cpp:499 +#, fuzzy, kde-format +#| msgid "Ac&tivities" +msgid "Show in &Activities" +msgstr "الأ&نشطة" + +#: useractions.cpp:514 useractions.cpp:559 +#, kde-format +msgid "&All Desktops" +msgstr "كلّ أ&سطح المكتب" + +#: useractions.cpp:542 useractions.cpp:595 +#, kde-format +msgctxt "Create a new desktop and move there the window" +msgid "&New Desktop" +msgstr "سطح مكتب &جديد" + +#: useractions.cpp:618 +#, fuzzy, kde-format +#| msgctxt "@item:inmenu List of all Screens to send a window to" +#| msgid "Screen &%1" +msgctxt "" +"@item:inmenu List of all Screens to send a window to. First argument is a " +"number, second the output identifier. E.g. Screen 1 (HDMI1)" +msgid "Screen &%1 (%2)" +msgstr "الشّاشة &%1" + +#: useractions.cpp:641 +#, kde-format +msgid "&All Activities" +msgstr "&كلّ الأنشطة" + +#: useractions.cpp:887 +#, kde-format +msgctxt "'%1' is a keyboard shortcut like 'ctrl+w'" +msgid "%1 is already in use" +msgstr "%1 مستخدم بالفعل" + +#: useractions.cpp:889 +#, kde-format +msgctxt "keyboard shortcut '%1' is used by action '%2' in application '%3'" +msgid "%1 is used by %2 in %3" +msgstr "%1 يستخدمه %2 في %3" + +#: useractions.cpp:1021 +#, kde-format +msgid "Activate Window (%1)" +msgstr "فعّل النّافذة (%1)" + +#: useractions.cpp:1163 +#, kde-format +msgid "" +"The window manager is configured to consider the screen with the mouse on it " +"as active one.\n" +"Therefore it is not possible to switch to a screen explicitly." +msgstr "" +"مدير النّوافذ مضبوط ليعتبر الشّاشة بالفأرة عليها هي الشّاشة النّشطة.\n" +"لذلك لا يمكن التّبديل إلى شاشة تبديلًا صريحًا." + +#: virtualdesktops.cpp:698 virtualdesktops.cpp:767 +#, kde-format +msgid "Desktop %1" +msgstr "سطح المكتب %1" + +#: virtualdesktops.cpp:802 +#, kde-format +msgid "Switch to Next Desktop" +msgstr "بدّل إلى سطح المكتب التّالي" + +#: virtualdesktops.cpp:804 +#, kde-format +msgid "Switch to Previous Desktop" +msgstr "بدّل إلى سطح المكتب السّابق" + +#: virtualdesktops.cpp:806 +#, kde-format +msgid "Switch One Desktop to the Right" +msgstr "" + +#: virtualdesktops.cpp:808 +#, kde-format +msgid "Switch One Desktop to the Left" +msgstr "" + +#: virtualdesktops.cpp:810 +#, kde-format +msgid "Switch One Desktop Up" +msgstr "" + +#: virtualdesktops.cpp:812 +#, kde-format +msgid "Switch One Desktop Down" +msgstr "" + +#: virtualdesktops.cpp:825 +#, kde-format +msgid "Switch to Desktop %1" +msgstr "بدّل إلى سطح المكتب %1" + +#: virtualkeyboard.cpp:84 +#, kde-format +msgid "Virtual Keyboard" +msgstr "" + +#: virtualkeyboard.cpp:350 +#, kde-format +msgid "Virtual Keyboard: enabled" +msgstr "" + +#: virtualkeyboard.cpp:353 +#, kde-format +msgid "Virtual Keyboard: disabled" +msgstr "" + +#: virtualkeyboard.cpp:355 +#, kde-format +msgid "Whether to show the virtual keyboard on demand." +msgstr "" + +#: workspace.cpp:1363 +#, kde-format +msgctxt "Introductory text shown in the support information." +msgid "" +"KWin Support Information:\n" +"The following information should be used when requesting support on e.g. " +"https://forum.kde.org.\n" +"It provides information about the currently running instance, which options " +"are used,\n" +"what OpenGL driver and which effects are running.\n" +"Please post the information provided underneath this introductory text to a " +"paste bin service\n" +"like https://paste.kde.org instead of pasting into support threads.\n" +msgstr "" \ No newline at end of file diff --git a/po/ar/kwin_clients.po b/po/ar/kwin_clients.po new file mode 100644 index 0000000..301b194 --- /dev/null +++ b/po/ar/kwin_clients.po @@ -0,0 +1,130 @@ +# translation of kwin_clients.po to Arabic +# translation of kwin_clients.po to +# محمد سعد Mohamed SAAD , 2006. +# AbdulAziz AlSharif , 2007. +# Youssef Chahibi , 2007. +# zayed , 2008. +msgid "" +msgstr "" +"Project-Id-Version: kwin_clients\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-08-08 02:22+0200\n" +"PO-Revision-Date: 2008-12-17 12:00+0400\n" +"Last-Translator: zayed \n" +"Language-Team: Arabic \n" +"Language: ar\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " +"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" +"X-Generator: KBabel 1.11.4\n" + +#: aurorae/src/aurorae.cpp:683 +#, kde-format +msgctxt "@item:inlistbox Button size:" +msgid "Tiny" +msgstr "" + +#: aurorae/src/aurorae.cpp:684 +#, kde-format +msgctxt "@item:inlistbox Button size:" +msgid "Normal" +msgstr "" + +#: aurorae/src/aurorae.cpp:685 +#, fuzzy, kde-format +msgctxt "@item:inlistbox Button size:" +msgid "Large" +msgstr "كبير" + +#: aurorae/src/aurorae.cpp:686 +#, fuzzy, kde-format +msgctxt "@item:inlistbox Button size:" +msgid "Very Large" +msgstr "كبير" + +#: aurorae/src/aurorae.cpp:687 +#, fuzzy, kde-format +msgctxt "@item:inlistbox Button size:" +msgid "Huge" +msgstr "كبير" + +#: aurorae/src/aurorae.cpp:688 +#, fuzzy, kde-format +msgctxt "@item:inlistbox Button size:" +msgid "Very Huge" +msgstr "كبير" + +#: aurorae/src/aurorae.cpp:689 +#, fuzzy, kde-format +msgctxt "@item:inlistbox Button size:" +msgid "Oversized" +msgstr "أعد التحجيم" + +#: aurorae/src/aurorae.cpp:692 +#, kde-format +msgid "Button size:" +msgstr "" + +#. i18n: ectx: property (windowTitle), widget (QWidget, PlastikConfigDialog) +#: aurorae/themes/plastik/package/contents/ui/config.ui:14 +#, kde-format +msgid "Config Dialog" +msgstr "حوار الضبط" + +#. i18n: ectx: property (title), widget (KButtonGroup, titleAlign) +#: aurorae/themes/plastik/package/contents/ui/config.ui:23 +#, kde-format +msgid "Title &Alignment" +msgstr "م&حاذاة العنوان" + +#. i18n: ectx: property (text), widget (QRadioButton, kcfg_titleAlignLeft) +#: aurorae/themes/plastik/package/contents/ui/config.ui:29 +#, kde-format +msgid "Left" +msgstr "يسار" + +#. i18n: ectx: property (text), widget (QRadioButton, kcfg_titleAlignCenter) +#: aurorae/themes/plastik/package/contents/ui/config.ui:36 +#, kde-format +msgid "Center" +msgstr "وسط" + +#. i18n: ectx: property (text), widget (QRadioButton, kcfg_titleAlignRight) +#: aurorae/themes/plastik/package/contents/ui/config.ui:43 +#, kde-format +msgid "Right" +msgstr "يمين" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_coloredBorder) +#: aurorae/themes/plastik/package/contents/ui/config.ui:53 +#, kde-format +msgid "" +"Check this option if the window border should be painted in the titlebar " +"color. Otherwise it will be painted in the background color." +msgstr "" +"حدّد هذا الخيار لجعل حد النافذة بنفس لون شريط العنوان. ماعدا ذلك ستكون بنفس " +"لون الخلفية." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_coloredBorder) +#: aurorae/themes/plastik/package/contents/ui/config.ui:56 +#, kde-format +msgid "Colored window border" +msgstr "حد النافذة ملوّن" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_animateButtons) +#: aurorae/themes/plastik/package/contents/ui/config.ui:66 +#, kde-format +msgid "" +"Check this option if you want the buttons to fade in when the mouse pointer " +"hovers over them and fade out again when it moves away." +msgstr "" +"حدّد هذا الخيار لكي تنصع الأزرار عند تمرير مؤشر الفأرة عليها و تتضاءل مرة " +"ثانية عند الابتعاد عنها" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_animateButtons) +#: aurorae/themes/plastik/package/contents/ui/config.ui:69 +#, kde-format +msgid "Animate buttons" +msgstr "أزرار التحريك" \ No newline at end of file diff --git a/po/ar/kwin_effects.po b/po/ar/kwin_effects.po new file mode 100644 index 0000000..3997988 --- /dev/null +++ b/po/ar/kwin_effects.po @@ -0,0 +1,2185 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# Safa Alfulaij , 2014. +# Abdalrahim Fakhouri , 2014. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-10-23 08:49+0200\n" +"PO-Revision-Date: 2014-07-05 19:42+0300\n" +"Last-Translator: Abdalrahim Fakhouri \n" +"Language-Team: Arabic =3 " +"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" +"X-Generator: Virtaal 0.7.1\n" +"X-Project-Style: kde\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "صفا الفليج" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "safa1996alfulaij@gmail.com" + +#. i18n: ectx: property (text), widget (QLabel, labelConstantBlurDescription) +#: blur/blur_config.ui:17 +#, fuzzy, kde-format +#| msgid "&Strength:" +msgid "Blur strength:" +msgstr "ال&قوّة:" + +#. i18n: ectx: property (text), widget (QLabel, labelConstantBlurLight) +#. i18n: ectx: property (text), widget (QLabel, labelConstantNoiseLight) +#: blur/blur_config.ui:42 blur/blur_config.ui:108 +#, kde-format +msgid "Light" +msgstr "خفيف" + +#. i18n: ectx: property (text), widget (QLabel, labelConstantBlurStrong) +#. i18n: ectx: property (text), widget (QLabel, labelConstantNoiseStrong) +#: blur/blur_config.ui:74 blur/blur_config.ui:137 +#, kde-format +msgid "Strong" +msgstr "قوي" + +#. i18n: ectx: property (text), widget (QLabel, labelConstantNoiseDescription) +#: blur/blur_config.ui:83 +#, fuzzy, kde-format +#| msgid "&Strength:" +msgid "Noise strength:" +msgstr "ال&قوّة:" + +#: colorpicker/colorpicker.cpp:107 +#, kde-format +msgid "" +"Select a position for color picking with left click or enter.\n" +"Escape or right click to cancel." +msgstr "" + +#: coverswitch/coverswitch.cpp:945 flipswitch/flipswitch.cpp:925 +#, kde-format +msgctxt "Special entry in alt+tab list for minimizing all windows" +msgid "Show Desktop" +msgstr "أظهر سطح المكتب" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DrawWindowCaptions) +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_WindowTitle) +#: coverswitch/coverswitch_config.ui:17 flipswitch/flipswitch_config.ui:191 +#: presentwindows/presentwindows_config.ui:406 +#, kde-format +msgid "Display window &titles" +msgstr "اعرض &عناوين النوافذ" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: coverswitch/coverswitch_config.ui:29 cube/cube_config.ui:344 +#, kde-format +msgid "Zoom" +msgstr "التقريب" + +#. i18n: ectx: property (toolTip), widget (QSlider, kcfg_zPosition) +#: coverswitch/coverswitch_config.ui:39 +#, kde-format +msgid "Define how far away the windows should appear" +msgstr "عرّف عند أي بُعد ستظهر النوافذ" + +#. i18n: ectx: property (text), widget (QLabel, label_8) +#: coverswitch/coverswitch_config.ui:66 cube/cube_config.ui:350 +#, kde-format +msgid "Near" +msgstr "قريب" + +#. i18n: ectx: property (text), widget (QLabel, label_9) +#: coverswitch/coverswitch_config.ui:86 cube/cube_config.ui:357 +#, kde-format +msgid "Far" +msgstr "بعيد" + +#. i18n: ectx: attribute (title), widget (QWidget, tab) +#: coverswitch/coverswitch_config.ui:110 +#, kde-format +msgid "Animation" +msgstr "ال&حركة" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AnimateSwitch) +#: coverswitch/coverswitch_config.ui:116 +#, kde-format +msgid "Animate switch" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AnimateStart) +#: coverswitch/coverswitch_config.ui:123 +#, kde-format +msgid "Animation on tab box open" +msgstr "حركة عند فتح صندوق اللسان" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AnimateStop) +#: coverswitch/coverswitch_config.ui:130 +#, kde-format +msgid "Animation on tab box close" +msgstr "حركة عند إغلاق صندوق اللسان" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: coverswitch/coverswitch_config.ui:139 magiclamp/magiclamp_config.ui:17 +#, kde-format +msgid "Animation duration:" +msgstr "مدّة الحركة:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_RotationDuration) +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_Duration) +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_AnimationDuration) +#: coverswitch/coverswitch_config.ui:158 cube/cube_config.ui:149 +#: cubeslide/cubeslide_config.ui:49 magiclamp/magiclamp_config.ui:36 +#, kde-format +msgctxt "Duration of rotation" +msgid "Default" +msgstr "الافتراضية" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_Duration) +#: coverswitch/coverswitch_config.ui:161 glide/glide_config.ui:35 +#: scale/package/contents/ui/config.ui:33 slide/slide_config.ui:35 +#, kde-format +msgid " milliseconds" +msgstr " م.ث" + +#. i18n: ectx: attribute (title), widget (QWidget, tab_3) +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Reflection) +#: coverswitch/coverswitch_config.ui:177 coverswitch/coverswitch_config.ui:183 +#, kde-format +msgid "Reflections" +msgstr "الانعكاس" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: coverswitch/coverswitch_config.ui:195 +#, kde-format +msgid "Rear color" +msgstr "اللون الخلفي" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: coverswitch/coverswitch_config.ui:205 +#, kde-format +msgid "Front color" +msgstr "اللون الأمامي" + +#: cube/cube.cpp:186 cube/cube_config.cpp:60 +#, kde-format +msgid "Desktop Cube" +msgstr "سطح المكتب المكعّب" + +#: cube/cube.cpp:194 cube/cube_config.cpp:65 +#, kde-format +msgid "Desktop Cylinder" +msgstr "سطح المكتب الأسطواني" + +#: cube/cube.cpp:200 cube/cube_config.cpp:69 +#, kde-format +msgid "Desktop Sphere" +msgstr "سطح المكتب الكروي" + +#: cube/cube_config.cpp:49 +#, kde-format +msgctxt "@title:tab Basic Settings" +msgid "Basic" +msgstr "أساسي" + +#: cube/cube_config.cpp:50 +#, kde-format +msgctxt "@title:tab Advanced Settings" +msgid "Advanced" +msgstr "متقدّم" + +#: cube/cube_config.cpp:54 desktopgrid/desktopgrid_config.cpp:52 +#: flipswitch/flipswitch_config.cpp:56 invert/invert_config.cpp:38 +#: lookingglass/lookingglass_config.cpp:58 magnifier/magnifier_config.cpp:58 +#: mouseclick/mouseclick_config.cpp:50 mousemark/mousemark_config.cpp:56 +#: presentwindows/presentwindows_config.cpp:51 +#: showpaint/showpaint_config.cpp:36 +#: thumbnailaside/thumbnailaside_config.cpp:57 +#: trackmouse/trackmouse_config.cpp:54 +#: windowgeometry/windowgeometry_config.cpp:45 zoom/zoom_config.cpp:59 +#, kde-format +msgid "KWin" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, tab) +#: cube/cube_config.ui:21 +#, kde-format +msgid "Tab 1" +msgstr "اللسان رقم 1" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_5) +#: cube/cube_config.ui:27 +#, kde-format +msgid "Background" +msgstr "الخلفية" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: cube/cube_config.ui:33 +#, kde-format +msgid "Background color:" +msgstr "لون الخلفية:" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: cube/cube_config.ui:56 +#, kde-format +msgid "Wallpaper:" +msgstr "الخلفية:" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_8) +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#: cube/cube_config.ui:82 desktopgrid/desktopgrid_config.ui:207 +#: flipswitch/flipswitch_config.ui:204 +#: presentwindows/presentwindows_config.ui:17 +#, kde-format +msgid "Activation" +msgstr "التفعيل" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_7) +#. i18n: ectx: property (title), widget (QGroupBox, groupBox) +#: cube/cube_config.ui:104 desktopgrid/desktopgrid_config.ui:17 +#: flipswitch/flipswitch_config.ui:17 mousemark/mousemark_config.ui:17 +#: presentwindows/presentwindows_config.ui:387 +#: thumbnailaside/thumbnailaside_config.ui:17 +#, kde-format +msgid "Appearance" +msgstr "المظهر" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DisplayDesktopName) +#: cube/cube_config.ui:110 +#, kde-format +msgid "Display desktop name" +msgstr "اعرض اسم سطح المكتب" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Reflection) +#: cube/cube_config.ui:117 +#, kde-format +msgid "Reflection" +msgstr "الانعكاس" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: cube/cube_config.ui:124 cubeslide/cubeslide_config.ui:72 +#, kde-format +msgid "Rotation duration:" +msgstr "مدّة الدوران" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ZOrdering) +#: cube/cube_config.ui:175 +#, kde-format +msgid "Windows hover above cube" +msgstr "النوافذ تطفو على المكعب" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_4) +#: cube/cube_config.ui:185 +#, kde-format +msgid "Opacity" +msgstr "الإعتام" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_OpacitySpin) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_Opacity) +#: cube/cube_config.ui:225 thumbnailaside/thumbnailaside_config.ui:87 +#, no-c-format, kde-format +msgid " %" +msgstr " %" + +#. i18n: ectx: property (text), widget (QLabel, label_4) +#. i18n: ectx: property (text), widget (QLabel, label_2) +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: cube/cube_config.ui:238 translucency/package/contents/ui/config.ui:156 +#: translucency/package/contents/ui/config.ui:418 +#, kde-format +msgid "Transparent" +msgstr "شفاف" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#. i18n: ectx: property (text), widget (QLabel, label_3) +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: cube/cube_config.ui:245 translucency/package/contents/ui/config.ui:121 +#: translucency/package/contents/ui/config.ui:431 +#, kde-format +msgid "Opaque" +msgstr "معتم" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_OpacityDesktopOnly) +#: cube/cube_config.ui:255 +#, kde-format +msgid "Do not change opacity of windows" +msgstr "لا تغيّر إعتام النوافذ" + +#. i18n: ectx: attribute (title), widget (QWidget, tab_2) +#: cube/cube_config.ui:279 +#, kde-format +msgid "Tab 2" +msgstr "اللسان 2" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_3) +#: cube/cube_config.ui:285 +#, kde-format +msgid "Caps" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Caps) +#: cube/cube_config.ui:291 +#, kde-format +msgid "Show caps" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, capColorLabel) +#: cube/cube_config.ui:298 +#, kde-format +msgid "Cap color:" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_TexturedCaps) +#: cube/cube_config.ui:321 +#, kde-format +msgid "Display image on caps" +msgstr "" + +#. i18n: ectx: property (toolTip), widget (QSlider, kcfg_ZPosition) +#: cube/cube_config.ui:367 +#, kde-format +msgid "Define how far away the object should appear" +msgstr "حدِّد البُعد الذي سيظهر فيه الكائن" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_9) +#: cube/cube_config.ui:408 +#, kde-format +msgid "Additional Options" +msgstr "خيارات إضافية" + +#. i18n: ectx: property (toolTip), widget (QCheckBox, kcfg_CloseOnMouseRelease) +#: cube/cube_config.ui:415 +#, kde-format +msgid "" +"If enabled the effect will be deactivated after rotating the cube with the " +"mouse,\n" +"otherwise it will remain active" +msgstr "" +"إن مُكّن، سيُعطّل التأثير بعد تدوير المكعّب بالفأرة،\n" +"وإلّا سيبقى نشطًا" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_CloseOnMouseRelease) +#: cube/cube_config.ui:418 +#, kde-format +msgid "Close after mouse dragging" +msgstr "أغلق بعد سحب الفأرة" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_TabBox) +#: cube/cube_config.ui:425 +#, kde-format +msgid "Use this effect for walking through the desktops" +msgstr "استخدم هذا التأثير للعبور بين أسطح المكتب" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_InvertKeys) +#: cube/cube_config.ui:432 +#, kde-format +msgid "Invert cursor keys" +msgstr "اعكس مفاتيح المؤشّر" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_InvertMouse) +#: cube/cube_config.ui:439 +#, kde-format +msgid "Invert mouse" +msgstr "اعكس الفأرة" + +#. i18n: ectx: property (title), widget (QGroupBox, capDeformationGroupBox) +#: cube/cube_config.ui:449 +#, kde-format +msgid "Sphere Cap Deformation" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, capDeformationSphereLabel) +#: cube/cube_config.ui:471 +#, kde-format +msgid "Sphere" +msgstr "كروي" + +#. i18n: ectx: property (text), widget (QLabel, capDeformationPlaneLabel) +#: cube/cube_config.ui:478 +#, kde-format +msgid "Plane" +msgstr "مسطّح" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DontSlideStickyWindows) +#: cubeslide/cubeslide_config.ui:17 +#, kde-format +msgid "Do not animate windows on all desktops" +msgstr "لا تحرّك النوافذ على كل أسطح المكتب" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_RingLife) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_RotationDuration) +#: cubeslide/cubeslide_config.ui:52 mouseclick/mouseclick_config.ui:132 +#, kde-format +msgid " msec" +msgstr " م.ث" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DontSlidePanels) +#: cubeslide/cubeslide_config.ui:65 +#, kde-format +msgid "Do not animate panels" +msgstr "لا تحرِّك الألواح" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_UsePagerLayout) +#: cubeslide/cubeslide_config.ui:85 +#, kde-format +msgid "Use pager layout for animation" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_UseWindowMoving) +#: cubeslide/cubeslide_config.ui:92 +#, kde-format +msgid "Start animation when moving windows towards screen edges" +msgstr "ابدأ الحركة عند تحريك النافذة إلى حافة الشاشة" + +#: desktopgrid/desktopgrid.cpp:65 desktopgrid/desktopgrid_config.cpp:57 +#, kde-format +msgid "Show Desktop Grid" +msgstr "أظهر شبكة سطح المكتب" + +#: desktopgrid/desktopgrid_config.cpp:65 +#, kde-format +msgctxt "Desktop name alignment:" +msgid "Disabled" +msgstr "معطّلة" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_InRotationEdge) +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_OutRotationEdge) +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: desktopgrid/desktopgrid_config.cpp:66 flipswitch/flipswitch_config.ui:160 +#: glide/glide_config.ui:70 glide/glide_config.ui:168 +#, kde-format +msgid "Top" +msgstr "أعلى" + +#: desktopgrid/desktopgrid_config.cpp:67 +#, kde-format +msgid "Top-Right" +msgstr "أعلى اليمين" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_InRotationEdge) +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_OutRotationEdge) +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: desktopgrid/desktopgrid_config.cpp:68 flipswitch/flipswitch_config.ui:125 +#: glide/glide_config.ui:75 glide/glide_config.ui:173 +#, kde-format +msgid "Right" +msgstr "يمين" + +#: desktopgrid/desktopgrid_config.cpp:69 +#, kde-format +msgid "Bottom-Right" +msgstr "أدنى اليمين" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_InRotationEdge) +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_OutRotationEdge) +#. i18n: ectx: property (text), widget (QLabel, label_8) +#: desktopgrid/desktopgrid_config.cpp:70 flipswitch/flipswitch_config.ui:180 +#: glide/glide_config.ui:80 glide/glide_config.ui:178 +#, kde-format +msgid "Bottom" +msgstr "أسفل" + +#: desktopgrid/desktopgrid_config.cpp:71 +#, kde-format +msgid "Bottom-Left" +msgstr "أدنى اليسار" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_InRotationEdge) +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_OutRotationEdge) +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: desktopgrid/desktopgrid_config.cpp:72 flipswitch/flipswitch_config.ui:105 +#: glide/glide_config.ui:85 glide/glide_config.ui:183 +#, kde-format +msgid "Left" +msgstr "يسار" + +#: desktopgrid/desktopgrid_config.cpp:73 +#, kde-format +msgid "Top-Left" +msgstr "أعلى اليسار" + +#: desktopgrid/desktopgrid_config.cpp:74 +#, kde-format +msgid "Center" +msgstr "وسط" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: desktopgrid/desktopgrid_config.ui:23 +#, kde-format +msgid "Zoom &duration:" +msgstr "" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_ZoomDuration) +#: desktopgrid/desktopgrid_config.ui:42 +#, kde-format +msgctxt "Duration of zoom" +msgid "Default" +msgstr "الافتراضية" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: desktopgrid/desktopgrid_config.ui:55 +#, fuzzy, kde-format +#| msgid "&Border width:" +msgid "Border wid&th:" +msgstr "عرض الح&د:" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: desktopgrid/desktopgrid_config.ui:84 +#, kde-format +msgid "Desktop &name alignment:" +msgstr "محاذاة ا&سم سطح المكتب:" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: desktopgrid/desktopgrid_config.ui:107 +#, kde-format +msgid "&Layout mode:" +msgstr "وضع الت&صميم:" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LayoutMode) +#: desktopgrid/desktopgrid_config.ui:127 +#, kde-format +msgid "Pager" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LayoutMode) +#: desktopgrid/desktopgrid_config.ui:132 +#, kde-format +msgid "Automatic" +msgstr "آلي" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LayoutMode) +#: desktopgrid/desktopgrid_config.ui:137 +#, kde-format +msgid "Custom" +msgstr "مخصّص" + +#. i18n: ectx: property (text), widget (QLabel, layoutRowsLabel) +#: desktopgrid/desktopgrid_config.ui:145 +#, fuzzy, kde-format +#| msgid "Number of &rows:" +msgid "N&umber of rows:" +msgstr "عدد ال&صفوف:" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_PresentWindows) +#: desktopgrid/desktopgrid_config.ui:190 +#, kde-format +msgid "Use Present Windows effect to layout the windows" +msgstr "استخدم تأثير النوافذ الحالي لتصميم النوافذ" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ShowAddRemove) +#: desktopgrid/desktopgrid_config.ui:197 +#, kde-format +msgid "Show buttons to alter count of virtual desktops" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_Strength) +#: diminactive/diminactive_config.ui:17 +#, fuzzy, kde-format +#| msgid "&Strength:" +msgid "Strength:" +msgstr "ال&قوّة:" + +#. i18n: ectx: property (text), widget (QLabel, label_Dim) +#: diminactive/diminactive_config.ui:40 +#, kde-format +msgid "Dim:" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DimPanels) +#: diminactive/diminactive_config.ui:47 +#, fuzzy, kde-format +#| msgid "Do not animate panels" +msgid "Docks and panels" +msgstr "لا تحرِّك الألواح" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DimDesktop) +#: diminactive/diminactive_config.ui:54 +#, fuzzy, kde-format +#| msgctxt "@title:group actions when clicking on desktop" +#| msgid "Desktop" +msgid "Desktop" +msgstr "سطح المكتب" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DimKeepAbove) +#: diminactive/diminactive_config.ui:61 +#, fuzzy, kde-format +#| msgctxt "Comment describing the KWin Effect" +#| msgid "Darken inactive windows" +msgid "Keep above windows" +msgstr "أعتِم النوافذ غير النشطة" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DimByGroup) +#: diminactive/diminactive_config.ui:68 +#, kde-format +msgid "By window group" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DimFullScreen) +#: diminactive/diminactive_config.ui:75 +#, fuzzy, kde-format +#| msgctxt "Name of a KWin Effect" +#| msgid "Present Windows" +msgid "Fullscreen windows" +msgstr "النوافذ الحاليّة" + +#: effect_builtins.cpp:91 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Blur" +msgstr "غشاوة" + +#: effect_builtins.cpp:92 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Blurs the background behind semi-transparent windows" +msgstr "" + +#: effect_builtins.cpp:106 +#, fuzzy, kde-format +#| msgctxt "High saturation" +#| msgid "Colored" +msgctxt "Name of a KWin Effect" +msgid "Color Picker" +msgstr "ملوّنة" + +#: effect_builtins.cpp:107 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Supports picking a color" +msgstr "" + +#: effect_builtins.cpp:121 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Background contrast" +msgstr "تباين الخلفية" + +#: effect_builtins.cpp:122 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Improve contrast and readability behind semi-transparent windows" +msgstr "حسِّن التباين وقابلية القراءة خلف النوافذ الظاهرة جزئيًّا" + +#: effect_builtins.cpp:136 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Cover Switch" +msgstr "غطِّ مفتاح التبديل" + +#: effect_builtins.cpp:137 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Display a Cover Flow effect for the alt+tab window switcher" +msgstr "" + +#: effect_builtins.cpp:151 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Desktop Cube" +msgstr "سطح المكتب المكعّب" + +#: effect_builtins.cpp:152 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Display each virtual desktop on a side of a cube" +msgstr "اعرض كلّ سطح مكتب وهمي على جانب من مكعّب" + +#: effect_builtins.cpp:166 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Desktop Cube Animation" +msgstr "حركة سطح المكتب المكعّب" + +#: effect_builtins.cpp:167 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Animate desktop switching with a cube" +msgstr "حرّك التبديل بين أسطح المكتب كمكعّب" + +#: effect_builtins.cpp:181 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Desktop Grid" +msgstr "شبكة سطح المكتب" + +#: effect_builtins.cpp:182 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Zoom out so all desktops are displayed side-by-side in a grid" +msgstr "بعّد لتُعرَض كلّ أسطح المكتب واحدة بجانب الأخرى في شبكة" + +#: effect_builtins.cpp:196 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Dim Inactive" +msgstr "إخفات غير النشط" + +#: effect_builtins.cpp:197 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Darken inactive windows" +msgstr "أعتِم النوافذ غير النشطة" + +#: effect_builtins.cpp:211 +#, fuzzy, kde-format +msgctxt "Name of a KWin Effect" +msgid "Fall Apart" +msgstr "انهيار" + +#: effect_builtins.cpp:212 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Closed windows fall into pieces" +msgstr "النوافذ المغلقة تسقط قطعًا" + +#: effect_builtins.cpp:226 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Flip Switch" +msgstr "" + +#: effect_builtins.cpp:227 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "" +"Flip through windows that are in a stack for the alt+tab window switcher" +msgstr "" + +#: effect_builtins.cpp:241 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Glide" +msgstr "انزلق" + +#: effect_builtins.cpp:242 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Glide windows as they appear or disappear" +msgstr "" + +#: effect_builtins.cpp:256 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Highlight Window" +msgstr "إبراز النوافذ" + +#: effect_builtins.cpp:257 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Highlight the appropriate window when hovering over taskbar entries" +msgstr "إبراز النافذة المناسبة عند المرور فوق مُدخلات شريط المهام" + +#: effect_builtins.cpp:271 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Invert" +msgstr "عكس" + +#: effect_builtins.cpp:272 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Inverts the color of the desktop and windows" +msgstr "يعكس ألوان سطح المكتب والنوافذ" + +#: effect_builtins.cpp:286 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Kscreen" +msgstr "شاشة.ك" + +#: effect_builtins.cpp:287 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Helper Effect for KScreen" +msgstr "تأثير مساعدة لـشاشة.ك" + +#: effect_builtins.cpp:301 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Looking Glass" +msgstr "عدسة مطالعة" + +#: effect_builtins.cpp:302 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "A screen magnifier that looks like a fisheye lens" +msgstr "مكبّرة شاشة تطالع كما عدسة عينا السمكة" + +#: effect_builtins.cpp:316 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Magic Lamp" +msgstr "مصباح سحريّ" + +#: effect_builtins.cpp:317 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Simulate a magic lamp when minimizing windows" +msgstr "حاكِ مصباحًا سحريًّا عند تصغير النوافذ" + +#: effect_builtins.cpp:331 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Magnifier" +msgstr "مكبّرة" + +#: effect_builtins.cpp:332 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Magnify the section of the screen that is near the mouse cursor" +msgstr "كبّر قسم الشاشة القريب من مؤشّر الفأرة" + +#: effect_builtins.cpp:346 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Mouse Click Animation" +msgstr "حركة نقرة الفأرة" + +#: effect_builtins.cpp:347 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "" +"Creates an animation whenever a mouse button is clicked. This is useful for " +"screenrecordings/presentations" +msgstr "" +"ينشئ حركة عندما يُنقَر زر الفأرة. هذا مفيد لتسجيلات الشاشة/العروض التقديمية" + +#: effect_builtins.cpp:361 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Mouse Mark" +msgstr "علامة الفأرة" + +#: effect_builtins.cpp:362 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Allows you to draw lines on the desktop" +msgstr "يسمح لك برسم خطوط على سطح المكتب" + +#: effect_builtins.cpp:376 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Present Windows" +msgstr "النوافذ الحاليّة" + +#: effect_builtins.cpp:377 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Zoom out until all opened windows can be displayed side-by-side" +msgstr "بعِّد إلى أن يصير بالإمكان عرض كل النوافذ المفتوحة جبناً إلى جنب" + +#: effect_builtins.cpp:391 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Resize Window" +msgstr "تغيير حجم النافذة" + +#: effect_builtins.cpp:392 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Resizes windows with a fast texture scale instead of updating contents" +msgstr "يغيّر حجم النوافذ بتحجيم نقش سريع بدلًا من تحديث المحتويات" + +#: effect_builtins.cpp:406 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Screen Edge" +msgstr "حافة الشاشة" + +#: effect_builtins.cpp:407 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Highlights a screen edge when approaching" +msgstr "يُبرِز حافة الشاشة عند الاقتراب منها" + +#: effect_builtins.cpp:421 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Screenshot" +msgstr "لقطة شاشة" + +#: effect_builtins.cpp:422 +#, fuzzy, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Helper effect for screenshot tools" +msgstr "تأثير مساعدة لـ" + +#: effect_builtins.cpp:436 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Sheet" +msgstr "ورقة" + +#: effect_builtins.cpp:437 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "" +"Make modal dialogs smoothly fly in and out when they are shown or hidden" +msgstr "اجعل الحوارات الـ... تطير داخلةً وخارجةً عند إظهارها أو إخفائها" + +#: effect_builtins.cpp:451 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Show FPS" +msgstr "إظهار إطار/ث" + +#: effect_builtins.cpp:452 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Display KWin's performance in the corner of the screen" +msgstr "اعرض أداء نوافذ.ك في زاوية من زوايا الشاشة" + +#: effect_builtins.cpp:466 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Show Paint" +msgstr "إظهار المرسوم" + +#: effect_builtins.cpp:467 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Highlight areas of the desktop that have been recently updated" +msgstr "أبرِز مناطق سطح المكتب التي حُدِّثت مؤخّرًا" + +#: effect_builtins.cpp:481 +#, fuzzy, kde-format +msgctxt "Name of a KWin Effect" +msgid "Slide" +msgstr "انزلق" + +#: effect_builtins.cpp:482 +#, fuzzy, kde-format +#| msgctxt "Comment describing the KWin Effect" +#| msgid "Animate desktop switching with a cube" +msgctxt "Comment describing the KWin Effect" +msgid "Slide desktops when switching virtual desktops" +msgstr "حرّك التبديل بين أسطح المكتب كمكعّب" + +#: effect_builtins.cpp:496 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Slide Back" +msgstr "" + +#: effect_builtins.cpp:497 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Slide back windows when another window is raised" +msgstr "" + +#: effect_builtins.cpp:511 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Sliding popups" +msgstr "" + +#: effect_builtins.cpp:512 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Sliding animation for Plasma popups" +msgstr "" + +#: effect_builtins.cpp:526 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Snap Helper" +msgstr "مساعد الجذب" + +#: effect_builtins.cpp:527 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Help you locate the center of the screen when moving a window" +msgstr "يساعدك في معرفة مركز الشاشة عند تحريك نافذة ما" + +#: effect_builtins.cpp:541 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Startup Feedback" +msgstr "" + +#: effect_builtins.cpp:542 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Helper effect for startup feedback" +msgstr "" + +#: effect_builtins.cpp:556 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Thumbnail Aside" +msgstr "" + +#: effect_builtins.cpp:557 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Display window thumbnails on the edge of the screen" +msgstr "اعرض مصغّرات النوافذ في حافة الشاشة" + +#: effect_builtins.cpp:571 +#, fuzzy, kde-format +#| msgid "Mouse Pointer:" +msgctxt "Name of a KWin Effect" +msgid "Touch Points" +msgstr "مؤشّر الفأرة:" + +#: effect_builtins.cpp:572 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Visualize touch points" +msgstr "" + +#: effect_builtins.cpp:586 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Track Mouse" +msgstr "تتبّع الفأرة" + +#: effect_builtins.cpp:587 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Display a mouse cursor locating effect when activated" +msgstr "" + +#: effect_builtins.cpp:601 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Window Geometry" +msgstr "" + +#: effect_builtins.cpp:602 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Display window geometries on move/resize" +msgstr "" + +#: effect_builtins.cpp:616 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Wobbly Windows" +msgstr "نوافذ متذبذبة" + +#: effect_builtins.cpp:617 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Deform windows while they are moving" +msgstr "" + +#: effect_builtins.cpp:631 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Zoom" +msgstr "" + +#: effect_builtins.cpp:632 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Magnify the entire desktop" +msgstr "كبّر كلّ سطح المكتب" + +#: flipswitch/flipswitch.cpp:48 flipswitch/flipswitch_config.cpp:50 +#, kde-format +msgid "Toggle Flip Switch (Current desktop)" +msgstr "" + +#: flipswitch/flipswitch.cpp:55 flipswitch/flipswitch_config.cpp:53 +#, kde-format +msgid "Toggle Flip Switch (All desktops)" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: flipswitch/flipswitch_config.ui:23 +#, kde-format +msgid "Flip animation duration:" +msgstr "مدة حركة القلب:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_Duration) +#: flipswitch/flipswitch_config.ui:42 +#, kde-format +msgctxt "Duration of flip animation" +msgid "Default" +msgstr "الافتراضية" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: flipswitch/flipswitch_config.ui:55 +#, kde-format +msgid "Angle:" +msgstr "الزاوية:" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_Angle) +#: flipswitch/flipswitch_config.ui:71 +#, kde-format +msgid " °" +msgstr " °" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: flipswitch/flipswitch_config.ui:81 +#, kde-format +msgid "Horizontal position of front:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: flipswitch/flipswitch_config.ui:136 +#, kde-format +msgid "Vertical position of front:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_Duration) +#: glide/glide_config.ui:19 scale/package/contents/ui/config.ui:17 +#: slide/slide_config.ui:19 +#, fuzzy, kde-format +#| msgid "Ring Duration:" +msgid "Duration:" +msgstr "مدّة الحلقة:" + +#. i18n: Duration of the slide animation. +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_Duration) +#: glide/glide_config.ui:32 scale/package/contents/ui/config.ui:30 +#: slide/slide_config.ui:32 +#, fuzzy, kde-format +#| msgctxt "Duration of rotation" +#| msgid "Default" +msgid "Default" +msgstr "الافتراضية" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_InAnimation) +#: glide/glide_config.ui:50 +#, fuzzy, kde-format +#| msgctxt "Name of a KWin Effect" +#| msgid "Minimize Animation" +msgid "Window Open Animation" +msgstr "حركة التصغير" + +#. i18n: ectx: property (text), widget (QLabel, label_InRotationEdge) +#. i18n: ectx: property (text), widget (QLabel, label_OutRotationEdge) +#: glide/glide_config.ui:56 glide/glide_config.ui:154 +#, fuzzy, kde-format +#| msgid "Rotation duration:" +msgid "Rotation edge:" +msgstr "مدّة الدوران" + +#. i18n: ectx: property (text), widget (QLabel, label_InRotationAngle) +#. i18n: ectx: property (text), widget (QLabel, label_OutRotationAngle) +#: glide/glide_config.ui:93 glide/glide_config.ui:191 +#, fuzzy, kde-format +#| msgid "Rotation duration:" +msgid "Rotation angle:" +msgstr "مدّة الدوران" + +#. i18n: ectx: property (text), widget (QLabel, label_InDistance) +#. i18n: ectx: property (text), widget (QLabel, label_OutDistance) +#: glide/glide_config.ui:119 glide/glide_config.ui:198 +#, kde-format +msgid "Distance:" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_OutAnimation) +#: glide/glide_config.ui:148 +#, fuzzy, kde-format +#| msgctxt "Name of a KWin Effect" +#| msgid "Mouse Click Animation" +msgid "Window Close Animation" +msgstr "حركة نقرة الفأرة" + +#: invert/invert.cpp:34 invert/invert_config.cpp:41 +#, kde-format +msgid "Toggle Invert Effect" +msgstr "بدّل بين تأثير العكس وعدمه" + +#: invert/invert.cpp:42 invert/invert_config.cpp:47 +#, kde-format +msgid "Toggle Invert Effect on Window" +msgstr "بدّل بين تأثير العكس للنافذة وعدمه" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_FadeToBlack) +#: login/package/contents/ui/config.ui:17 +#, kde-format +msgid "Fade to black (fullscreen splash screens only)" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: lookingglass/lookingglass_config.ui:24 +#, kde-format +msgid "&Radius:" +msgstr "&نصف القطر:" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_AnimationDuration) +#: magiclamp/magiclamp_config.ui:39 +#, kde-format +msgid "milliseconds" +msgstr "مث" + +#. i18n: ectx: property (title), widget (QGroupBox, groupSize) +#: magnifier/magnifier_config.ui:17 +#, kde-format +msgid "Size" +msgstr "الحجم" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: magnifier/magnifier_config.ui:23 +#, kde-format +msgid "&Width:" +msgstr "ال&عرض:" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_Width) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_Height) +#: magnifier/magnifier_config.ui:42 magnifier/magnifier_config.ui:74 +#, kde-format +msgid " px" +msgstr " بكسل" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: magnifier/magnifier_config.ui:55 +#, kde-format +msgid "&Height:" +msgstr "الا&رتفاع:" + +#: mouseclick/mouseclick.cpp:40 mouseclick/mouseclick_config.cpp:53 +#, fuzzy, kde-format +#| msgid "Toggle Effect" +msgid "Toggle Mouse Click Effect" +msgstr "بدّل بين التأثير وعدمه" + +#: mouseclick/mouseclick.cpp:48 +#, fuzzy, kde-format +#| msgid "Left" +msgctxt "Left mouse button" +msgid "Left" +msgstr "يسار" + +#: mouseclick/mouseclick.cpp:49 +#, fuzzy, kde-format +#| msgid "Middle button:" +msgctxt "Middle mouse button" +msgid "Middle" +msgstr "الزر الأوسط:" + +#: mouseclick/mouseclick.cpp:50 +#, fuzzy, kde-format +#| msgid "Right" +msgctxt "Right mouse button" +msgid "Right" +msgstr "يمين" + +#: mouseclick/mouseclick.h:63 +#, kde-format +msgid "↓" +msgstr "" + +#: mouseclick/mouseclick.h:64 +#, kde-format +msgid "↑" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, basic_tab) +#: mouseclick/mouseclick_config.ui:21 +#, kde-format +msgid "Basic Settings" +msgstr "أساسي" + +#. i18n: ectx: property (text), widget (QLabel, button1_label) +#: mouseclick/mouseclick_config.ui:37 +#, kde-format +msgid "Left Mouse Button Color:" +msgstr "لون زر الفأرة الأيسر:" + +#. i18n: ectx: property (text), widget (QLabel, button2_label) +#: mouseclick/mouseclick_config.ui:50 +#, kde-format +msgid "Middle Mouse Button Color:" +msgstr "لون زر الفأرة الأوسط:" + +#. i18n: ectx: property (text), widget (QLabel, button3_label) +#: mouseclick/mouseclick_config.ui:70 +#, kde-format +msgid "Right Mouse Button Color:" +msgstr "لون زر الفأرة الأيمن:" + +#. i18n: ectx: attribute (title), widget (QWidget, advanced_tab) +#: mouseclick/mouseclick_config.ui:91 +#, kde-format +msgid "Advanced Settings" +msgstr "متقدّم" + +#. i18n: ectx: property (title), widget (QGroupBox, rings) +#: mouseclick/mouseclick_config.ui:97 +#, kde-format +msgid "Rings" +msgstr "الحلقات" + +#. i18n: ectx: property (text), widget (QLabel, ring_line_width_label) +#: mouseclick/mouseclick_config.ui:103 +#, kde-format +msgid "Line Width:" +msgstr "عرض السطر:" + +#. i18n: ectx: property (suffix), widget (QDoubleSpinBox, kcfg_LineWidth) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_RingSize) +#: mouseclick/mouseclick_config.ui:119 mouseclick/mouseclick_config.ui:171 +#: mousemark/mousemark_config.cpp:45 +#, kde-format +msgid " pixel" +msgid_plural " pixels" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" +msgstr[4] "" +msgstr[5] "" + +#. i18n: ectx: property (text), widget (QLabel, ring_duration_label) +#: mouseclick/mouseclick_config.ui:145 +#, kde-format +msgid "Ring Duration:" +msgstr "مدّة الحلقة:" + +#. i18n: ectx: property (text), widget (QLabel, ring_radius_label) +#: mouseclick/mouseclick_config.ui:155 +#, kde-format +msgid "Ring Radius:" +msgstr "نصف قطر الحلقة:" + +#. i18n: ectx: property (text), widget (QLabel, ring_count_label) +#: mouseclick/mouseclick_config.ui:184 +#, kde-format +msgid "Ring Count:" +msgstr "عدد الحلقات:" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox) +#. i18n: ectx: property (title), widget (QGroupBox, font) +#: mouseclick/mouseclick_config.ui:210 showfps/showfps_config.ui:17 +#, kde-format +msgid "Text" +msgstr "النصّ" + +#. i18n: ectx: property (text), widget (QLabel, font_label) +#: mouseclick/mouseclick_config.ui:216 +#, kde-format +msgid "Font:" +msgstr "الخط:" + +#. i18n: ectx: property (text), widget (QLabel, showtext_label) +#: mouseclick/mouseclick_config.ui:233 +#, kde-format +msgid "Show Text:" +msgstr "أظهر النصّ:" + +#: mousemark/mousemark.cpp:41 +#, kde-format +msgid "Clear All Mouse Marks" +msgstr "امحُ كل علامات الفأرة" + +#: mousemark/mousemark.cpp:48 mousemark/mousemark_config.cpp:65 +#, kde-format +msgid "Clear Last Mouse Mark" +msgstr "امحُ آخر علامة للفأرة" + +#: mousemark/mousemark_config.cpp:59 +#, kde-format +msgid "Clear Mouse Marks" +msgstr "امحُ علامات الفأرة" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: mousemark/mousemark_config.ui:23 +#, kde-format +msgid "Wid&th:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: mousemark/mousemark_config.ui:36 +#, kde-format +msgid "&Color:" +msgstr "اللو&ن:" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: mousemark/mousemark_config.ui:78 +#, kde-format +msgid "Draw with the mouse by holding Shift+Meta keys and moving the mouse." +msgstr "اسحب بالفأرة بالضغط على المفاتيح Shift+Meta وتحريك الفأرة." + +#: presentwindows/presentwindows.cpp:66 +#: presentwindows/presentwindows_config.cpp:62 +#, kde-format +msgid "Toggle Present Windows (Current desktop)" +msgstr "" + +#: presentwindows/presentwindows.cpp:75 +#: presentwindows/presentwindows_config.cpp:56 +#, kde-format +msgid "Toggle Present Windows (All desktops)" +msgstr "" + +#: presentwindows/presentwindows.cpp:85 +#: presentwindows/presentwindows_config.cpp:68 +#, kde-format +msgid "Toggle Present Windows (Window class)" +msgstr "" + +#: presentwindows/presentwindows.cpp:1666 +#, kde-format +msgid "" +"Filter:\n" +"%1" +msgstr "" +"المرشّح:\n" +"%1" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_3) +#: presentwindows/presentwindows_config.ui:39 +#, kde-format +msgid "Natural Layout Settings" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_FillGaps) +#: presentwindows/presentwindows_config.ui:45 +#, kde-format +msgid "Fill &gaps" +msgstr "املأ ال&فراغات" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: presentwindows/presentwindows_config.ui:65 +#, kde-format +msgid "Faster" +msgstr "أسرع" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: presentwindows/presentwindows_config.ui:112 +#, kde-format +msgid "Nicer" +msgstr "أجمل" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_4) +#: presentwindows/presentwindows_config.ui:122 +#, kde-format +msgid "Windows" +msgstr "النوافذ" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#. i18n: ectx: property (text), widget (QLabel, label_8) +#: presentwindows/presentwindows_config.ui:128 +#: presentwindows/presentwindows_config.ui:282 +#, kde-format +msgid "Left button:" +msgstr "الزر الأيسر:" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonDesktop) +#: presentwindows/presentwindows_config.ui:139 +#: presentwindows/presentwindows_config.ui:183 +#: presentwindows/presentwindows_config.ui:232 +#: presentwindows/presentwindows_config.ui:293 +#: presentwindows/presentwindows_config.ui:327 +#: presentwindows/presentwindows_config.ui:361 +#, kde-format +msgid "No action" +msgstr "لا إجراء" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonDesktop) +#: presentwindows/presentwindows_config.ui:144 +#: presentwindows/presentwindows_config.ui:188 +#: presentwindows/presentwindows_config.ui:237 +#: presentwindows/presentwindows_config.ui:298 +#: presentwindows/presentwindows_config.ui:332 +#: presentwindows/presentwindows_config.ui:366 +#, kde-format +msgid "Activate window" +msgstr "فعّل النافذة" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonDesktop) +#: presentwindows/presentwindows_config.ui:149 +#: presentwindows/presentwindows_config.ui:193 +#: presentwindows/presentwindows_config.ui:242 +#: presentwindows/presentwindows_config.ui:303 +#: presentwindows/presentwindows_config.ui:337 +#: presentwindows/presentwindows_config.ui:371 +#, kde-format +msgid "End effect" +msgstr "أنهِ التأثير" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonWindow) +#: presentwindows/presentwindows_config.ui:154 +#: presentwindows/presentwindows_config.ui:198 +#: presentwindows/presentwindows_config.ui:247 +#, kde-format +msgid "Bring window to current desktop" +msgstr "اجلب النافذة إلى سطح المكتب الحالي" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonWindow) +#: presentwindows/presentwindows_config.ui:159 +#: presentwindows/presentwindows_config.ui:203 +#: presentwindows/presentwindows_config.ui:252 +#, kde-format +msgid "Send window to all desktops" +msgstr "أرسل النافذة إلى كلّ أسطح المكتب" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonWindow) +#: presentwindows/presentwindows_config.ui:164 +#: presentwindows/presentwindows_config.ui:208 +#: presentwindows/presentwindows_config.ui:257 +#, kde-format +msgid "(Un-)Minimize window" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_4) +#. i18n: ectx: property (text), widget (QLabel, label_9) +#: presentwindows/presentwindows_config.ui:172 +#: presentwindows/presentwindows_config.ui:316 +#, kde-format +msgid "Middle button:" +msgstr "الزر الأوسط:" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonWindow) +#: presentwindows/presentwindows_config.ui:213 +#: presentwindows/presentwindows_config.ui:262 +#, fuzzy, kde-format +#| msgid "Scale window" +msgid "Close window" +msgstr "حجّم النافذة" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#. i18n: ectx: property (text), widget (QLabel, label_10) +#: presentwindows/presentwindows_config.ui:221 +#: presentwindows/presentwindows_config.ui:350 +#, kde-format +msgid "Right button:" +msgstr "الزر الأيمن:" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_5) +#: presentwindows/presentwindows_config.ui:273 +#, kde-format +msgctxt "@title:group actions when clicking on desktop" +msgid "Desktop" +msgstr "سطح المكتب" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonDesktop) +#: presentwindows/presentwindows_config.ui:308 +#: presentwindows/presentwindows_config.ui:342 +#: presentwindows/presentwindows_config.ui:376 +#, kde-format +msgid "Show desktop" +msgstr "أظهر سطح المكتب" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: presentwindows/presentwindows_config.ui:393 +#, kde-format +msgid "Layout mode:" +msgstr "وضع التصميم:" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DrawWindowIcons) +#: presentwindows/presentwindows_config.ui:413 +#, kde-format +msgid "Display window &icons" +msgstr "اعرض أي&قونات النوافذ" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_IgnoreMinimized) +#: presentwindows/presentwindows_config.ui:420 +#, kde-format +msgid "Ignore &minimized windows" +msgstr "تجاهل النوافذ المص&غّرة" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ShowPanel) +#: presentwindows/presentwindows_config.ui:427 +#, kde-format +msgid "Show &panels" +msgstr "أظهر اللو&حات" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LayoutMode) +#: presentwindows/presentwindows_config.ui:441 +#, kde-format +msgid "Natural" +msgstr "طبيعي" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LayoutMode) +#: presentwindows/presentwindows_config.ui:446 +#, kde-format +msgid "Regular Grid" +msgstr "شبكة عادية" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LayoutMode) +#: presentwindows/presentwindows_config.ui:451 +#, kde-format +msgid "Flexible Grid" +msgstr "شبكة مرنة" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AllowClosingWindows) +#: presentwindows/presentwindows_config.ui:459 +#, kde-format +msgid "Provide buttons to close the windows" +msgstr "وفّر أزرارًا لإغلاق النوافذ" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_TextureScale) +#: resize/resize_config.ui:17 +#, kde-format +msgid "Scale window" +msgstr "حجّم النافذة" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Outline) +#: resize/resize_config.ui:24 +#, kde-format +msgid "Show outline" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_InScale) +#: scale/package/contents/ui/config.ui:46 +#, fuzzy, kde-format +#| msgctxt "Name of a KWin Effect" +#| msgid "Minimize Animation" +msgid "Window open scale:" +msgstr "حركة التصغير" + +#. i18n: ectx: property (text), widget (QLabel, label_OutScale) +#: scale/package/contents/ui/config.ui:53 +#, fuzzy, kde-format +#| msgctxt "Name of a KWin Effect" +#| msgid "Mouse Click Animation" +msgid "Window close scale:" +msgstr "حركة نقرة الفأرة" + +#: screenshot/screenshot.cpp:440 +#, fuzzy, kde-format +#| msgctxt "Name of a KWin Effect" +#| msgid "Screenshot" +msgctxt "Notification caption that a screenshot got saved to file" +msgid "Screenshot" +msgstr "لقطة شاشة" + +#: screenshot/screenshot.cpp:441 +#, fuzzy, kde-format +#| msgctxt "Name of a KWin Effect" +#| msgid "Screenshot" +msgctxt "Notification with path to screenshot file" +msgid "Screenshot saved to %1" +msgstr "لقطة شاشة" + +#: screenshot/screenshot.cpp:578 +#, kde-format +msgid "" +"Select window to screen shot with left click or enter.\n" +"Escape or right click to cancel." +msgstr "" + +#: screenshot/screenshot.cpp:581 +#, kde-format +msgid "" +"Create screen shot with left click or enter.\n" +"Escape or right click to cancel." +msgstr "" + +#: showfps/showfps.cpp:54 +#, kde-format +msgid "This effect is not a benchmark" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: showfps/showfps_config.ui:23 +#, kde-format +msgid "Text position:" +msgstr "موقع النصّ:" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TextPosition) +#: showfps/showfps_config.ui:43 +#, kde-format +msgid "Inside Graph" +msgstr "داخل الرسومي" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TextPosition) +#: showfps/showfps_config.ui:48 +#, fuzzy, kde-format +msgid "Nowhere" +msgstr "في اللامكان" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TextPosition) +#: showfps/showfps_config.ui:53 +#, kde-format +msgid "Top Left" +msgstr "أعلى اليسار" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TextPosition) +#: showfps/showfps_config.ui:58 +#, kde-format +msgid "Top Right" +msgstr "أعلى اليمين" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TextPosition) +#: showfps/showfps_config.ui:63 +#, kde-format +msgid "Bottom Left" +msgstr "أدنى اليسار" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TextPosition) +#: showfps/showfps_config.ui:68 +#, kde-format +msgid "Bottom Right" +msgstr "أدنى اليمين" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: showfps/showfps_config.ui:76 +#, kde-format +msgid "Text font:" +msgstr "خط النصّ:" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: showfps/showfps_config.ui:96 +#, kde-format +msgid "Text color:" +msgstr "لون النصّ:" + +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: showfps/showfps_config.ui:119 +#, kde-format +msgid "Text alpha:" +msgstr "ألفا للنصّ:" + +#: showpaint/showpaint.cpp:42 showpaint/showpaint_config.cpp:41 +#, fuzzy, kde-format +#| msgctxt "Name of a KWin Effect" +#| msgid "Show Paint" +msgid "Toggle Show Paint" +msgstr "إظهار المرسوم" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_Gaps) +#: slide/slide_config.ui:50 +#, fuzzy, kde-format +#| msgctxt "Comment describing the KWin Effect" +#| msgid "Magnify the entire desktop" +msgid "Gap between desktops" +msgstr "كبّر كلّ سطح المكتب" + +#. i18n: ectx: property (text), widget (QLabel, label_HorizontalGap) +#: slide/slide_config.ui:56 +#, kde-format +msgid "Horizontal:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_VerticalGap) +#: slide/slide_config.ui:79 +#, kde-format +msgid "Vertical:" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_SlideDocks) +#: slide/slide_config.ui:105 +#, fuzzy, kde-format +msgid "Slide docks" +msgstr "انزلق" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_SlideBackground) +#: slide/slide_config.ui:112 +#, kde-format +msgid "Slide desktop background" +msgstr "" + +#: thumbnailaside/thumbnailaside.cpp:29 +#: thumbnailaside/thumbnailaside_config.cpp:62 +#, kde-format +msgid "Toggle Thumbnail for Current Window" +msgstr "بدّل بين مصغّرات النافذة الحالية وبدونها" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: thumbnailaside/thumbnailaside_config.ui:23 +#, kde-format +msgid "Maximum &width:" +msgstr "أقصى &عرض:" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: thumbnailaside/thumbnailaside_config.ui:36 +#, kde-format +msgid "&Spacing:" +msgstr "ال&تباعد:" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_Spacing) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_MaxWidth) +#: thumbnailaside/thumbnailaside_config.ui:55 +#: thumbnailaside/thumbnailaside_config.ui:106 +#, kde-format +msgid " pixels" +msgstr " بكسل" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: thumbnailaside/thumbnailaside_config.ui:68 +#, kde-format +msgid "&Opacity:" +msgstr "الإ&عتام:" + +#: trackmouse/trackmouse.cpp:50 trackmouse/trackmouse_config.cpp:59 +#, kde-format +msgid "Track mouse" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: trackmouse/trackmouse_config.ui:26 +#, kde-format +msgid "Trigger effect with:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_KeyboardShortcut) +#: trackmouse/trackmouse_config.ui:33 +#, kde-format +msgid "Keyboard shortcut:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_ModifierKeys) +#: trackmouse/trackmouse_config.ui:43 +#, fuzzy, kde-format +#| msgid "Modifiers" +msgid "Modifier keys:" +msgstr "المُعدِّلات" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Alt) +#: trackmouse/trackmouse_config.ui:65 +#, kde-format +msgid "Alt" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Control) +#: trackmouse/trackmouse_config.ui:72 +#, kde-format +msgid "Ctrl" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Shift) +#: trackmouse/trackmouse_config.ui:79 +#, kde-format +msgid "Shift" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Meta) +#: trackmouse/trackmouse_config.ui:86 +#, kde-format +msgid "Meta" +msgstr "" + +#. i18n: ectx: property (windowTitle), widget (QWidget, KWin::TranslucencyEffectConfigForm) +#: translucency/package/contents/ui/config.ui:14 +#, kde-format +msgid "Translucency" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, m_opacityGroupBox) +#: translucency/package/contents/ui/config.ui:20 +#, kde-format +msgid "General Translucency Settings" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, comboboxpopup_label) +#: translucency/package/contents/ui/config.ui:64 +#, kde-format +msgid "Combobox popups:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, dialogs_label) +#: translucency/package/contents/ui/config.ui:137 +#, kde-format +msgid "Dialogs:" +msgstr "الحواريات:" + +#. i18n: ectx: property (text), widget (QLabel, menus_label) +#: translucency/package/contents/ui/config.ui:188 +#, kde-format +msgid "Menus:" +msgstr "القوائم:" + +#. i18n: ectx: property (text), widget (QLabel, moveresize_label) +#: translucency/package/contents/ui/config.ui:207 +#, kde-format +msgid "Moving windows:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, inactive_label) +#: translucency/package/contents/ui/config.ui:226 +#, kde-format +msgid "Inactive windows:" +msgstr "النوافذ غير النشطة:" + +#. i18n: ectx: property (title), widget (QGroupBox, kcfg_IndividualMenuConfig) +#: translucency/package/contents/ui/config.ui:267 +#, kde-format +msgid "Set menu translucency independently" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, dropdownmenus_label) +#: translucency/package/contents/ui/config.ui:285 +#, kde-format +msgid "Dropdown menus:" +msgstr "القوائم المنسدلة:" + +#. i18n: ectx: property (text), widget (QLabel, popupmenus_label) +#: translucency/package/contents/ui/config.ui:329 +#, kde-format +msgid "Popup menus:" +msgstr "القوائم المنبثقة:" + +#. i18n: ectx: property (text), widget (QLabel, tornoffmenus_label) +#: translucency/package/contents/ui/config.ui:367 +#, kde-format +msgid "Torn-off menus:" +msgstr "القوائم المفصولة:" + +#: windowgeometry/windowgeometry.cpp:43 +#, kde-format +msgid "Toggle window geometry display (effect only)" +msgstr "" + +#: windowgeometry/windowgeometry_config.cpp:47 +#, kde-format +msgid "Toggle KWin composited geometry display" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Move) +#: windowgeometry/windowgeometry_config.ui:17 +#, kde-format +msgid "Display for moving windows" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Resize) +#: windowgeometry/windowgeometry_config.ui:24 +#, kde-format +msgid "Display for resizing windows" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, advancedGroup) +#: wobblywindows/wobblywindows_config.ui:20 +#, kde-format +msgid "Advanced" +msgstr "متقدّم" + +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: wobblywindows/wobblywindows_config.ui:26 +#, kde-format +msgid "&Stiffness:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: wobblywindows/wobblywindows_config.ui:68 +#, kde-format +msgid "Dra&g:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: wobblywindows/wobblywindows_config.ui:81 +#, kde-format +msgid "&Move factor:" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_MoveWobble) +#: wobblywindows/wobblywindows_config.ui:155 +#, kde-format +msgid "Wo&bble when moving" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ResizeWobble) +#: wobblywindows/wobblywindows_config.ui:162 +#, kde-format +msgid "Wobble when &resizing" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AdvancedMode) +#: wobblywindows/wobblywindows_config.ui:182 +#, kde-format +msgid "Enable &advanced mode" +msgstr "مكّن الوضع المت&قدّم" + +#. i18n: ectx: property (title), widget (QGroupBox, basicGroup) +#: wobblywindows/wobblywindows_config.ui:192 +#, kde-format +msgid "&Wobbliness" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: wobblywindows/wobblywindows_config.ui:201 +#, kde-format +msgid "Less" +msgstr "أقل" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: wobblywindows/wobblywindows_config.ui:224 +#, kde-format +msgid "More" +msgstr "أكثر" + +#: zoom/zoom.cpp:74 +#, kde-format +msgid "Move Zoomed Area to Left" +msgstr "" + +#: zoom/zoom.cpp:82 +#, kde-format +msgid "Move Zoomed Area to Right" +msgstr "" + +#: zoom/zoom.cpp:90 +#, kde-format +msgid "Move Zoomed Area Upwards" +msgstr "" + +#: zoom/zoom.cpp:98 +#, kde-format +msgid "Move Zoomed Area Downwards" +msgstr "" + +#: zoom/zoom.cpp:107 zoom/zoom_config.cpp:109 +#, kde-format +msgid "Move Mouse to Focus" +msgstr "" + +#: zoom/zoom.cpp:115 zoom/zoom_config.cpp:116 +#, kde-format +msgid "Move Mouse to Center" +msgstr "" + +#: zoom/zoom_config.cpp:81 +#, kde-format +msgid "Move Left" +msgstr "" + +#: zoom/zoom_config.cpp:88 +#, kde-format +msgid "Move Right" +msgstr "" + +#: zoom/zoom_config.cpp:95 +#, kde-format +msgid "Move Up" +msgstr "" + +#: zoom/zoom_config.cpp:102 +#, kde-format +msgid "Move Down" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QLabel, label) +#. i18n: ectx: property (whatsThis), widget (QDoubleSpinBox, kcfg_ZoomFactor) +#: zoom/zoom_config.ui:25 zoom/zoom_config.ui:41 +#, kde-format +msgid "On zoom-in and zoom-out change the zoom by the defined zoom-factor." +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: zoom/zoom_config.ui:28 +#, kde-format +msgid "Zoom Factor:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_EnableFocusTracking) +#: zoom/zoom_config.ui:66 +#, kde-format +msgid "" +"Enable tracking of the focused location. This needs QAccessible to be " +"enabled per application (\"export QT_LINUX_ACCESSIBILITY_ALWAYS_ON=1\")." +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_EnableFocusTracking) +#: zoom/zoom_config.ui:69 +#, kde-format +msgid "Enable Focus Tracking" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_EnableTextCaretTracking) +#: zoom/zoom_config.ui:76 +#, kde-format +msgid "" +"Enable tracking of the text cursor. This needs QAccessible to be enabled per " +"application (\"export QT_LINUX_ACCESSIBILITY_ALWAYS_ON=1\")." +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_EnableTextCaretTracking) +#: zoom/zoom_config.ui:79 +#, kde-format +msgid "Enable Text Cursor Tracking" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: zoom/zoom_config.ui:86 +#, kde-format +msgid "Mouse Pointer:" +msgstr "مؤشّر الفأرة:" + +#. i18n: ectx: property (whatsThis), widget (QComboBox, kcfg_MousePointer) +#: zoom/zoom_config.ui:99 +#, kde-format +msgid "Visibility of the mouse-pointer." +msgstr "ظهور مؤشّر الفأرة." + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_MousePointer) +#: zoom/zoom_config.ui:103 +#, kde-format +msgid "Scale" +msgstr "حجّم" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_MousePointer) +#: zoom/zoom_config.ui:108 +#, kde-format +msgid "Keep" +msgstr "أبقِ" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_MousePointer) +#: zoom/zoom_config.ui:113 +#, kde-format +msgid "Hide" +msgstr "أخفِ" + +#. i18n: ectx: property (whatsThis), widget (QComboBox, kcfg_MouseTracking) +#: zoom/zoom_config.ui:121 +#, kde-format +msgid "Track moving of the mouse." +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_MouseTracking) +#: zoom/zoom_config.ui:125 +#, kde-format +msgid "Proportional" +msgstr "تناسبيّ" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_MouseTracking) +#: zoom/zoom_config.ui:130 +#, kde-format +msgid "Centered" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_MouseTracking) +#: zoom/zoom_config.ui:135 +#, kde-format +msgid "Push" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_MouseTracking) +#: zoom/zoom_config.ui:140 +#, kde-format +msgid "Disabled" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: zoom/zoom_config.ui:148 +#, kde-format +msgid "Mouse Tracking:" +msgstr "تتبّع الفأرة:" \ No newline at end of file diff --git a/po/ar/kwin_scripting.po b/po/ar/kwin_scripting.po new file mode 100644 index 0000000..2cbdcf3 --- /dev/null +++ b/po/ar/kwin_scripting.po @@ -0,0 +1,114 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Safa Alfulaij , 2015. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-08-08 02:22+0200\n" +"PO-Revision-Date: 2015-01-25 18:08+0300\n" +"Last-Translator: Safa Alfulaij \n" +"Language-Team: Arabic \n" +"Language: ar\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " +"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" +"X-Generator: Lokalize 1.5\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "صفا الفليج" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "safa1996alfulaij@gmail.com" + +#: genericscriptedconfig.cpp:70 +#, kde-format +msgctxt "Error message" +msgid "Plugin does not provide configuration file in expected location" +msgstr "لم توفّر الملحقة ملفّ ضبط في المكان المتوقّع" + +#: scripting.cpp:117 +#, kde-format +msgctxt "Assertion failed in KWin script with given value" +msgid "Assertion failed: %1 is not null" +msgstr "فشل التّوكيد: %1 ليس خاليًا" + +#: scripting.cpp:135 +#, kde-format +msgctxt "Assertion failed in KWin script" +msgid "Assertion failed: argument is null" +msgstr "فشل التّوكيد: المعطى خالٍ" + +#: scripting.cpp:177 +#, kde-format +msgctxt "Error in KWin Script" +msgid "" +"Invalid number of arguments. At least service, path, interface and method " +"need to be provided" +msgstr "" +"عدد معطيات غير صالح. يجب توفير الخدمة، والمسار، والواجهة والطريقة على الأقل" + +#: scripting.cpp:183 +#, kde-format +msgctxt "Error in KWin Script" +msgid "" +"Invalid type. Service, path, interface and method need to be string values" +msgstr "" +"نوع غير صالح. الخدمة، والمسار، والواجهة والطريقة يجب أن يكونوا قيم سلاسل" + +#: scriptingutils.cpp:17 +#, kde-format +msgctxt "syntax error in KWin script" +msgid "Invalid number of arguments" +msgstr "عدد معطيات غير صالح" + +#: scriptingutils.cpp:30 +#, kde-format +msgctxt "KWin Scripting function received incorrect value for an expected type" +msgid "%1 is not a variant type" +msgstr "%1 ليس نوع تنويعة" + +#: scriptingutils.h:40 +#, kde-format +msgctxt "KWin Scripting function received incorrect value for an expected type" +msgid "%1 is not of required type" +msgstr "%1 ليس من الأنواع المطلوبة" + +#: scriptingutils.h:147 +#, kde-format +msgctxt "KWin Scripting error thrown due to incorrect argument" +msgid "Second argument to registerScreenEdge needs to be a callback" +msgstr "المعطى الثّاني إلى registerScreenEdge يجب أن يكون ردّ نداء" + +#: scriptingutils.h:202 +#, fuzzy, kde-format +#| msgctxt "KWin Scripting error thrown due to incorrect argument" +#| msgid "Second argument to registerScreenEdge needs to be a callback" +msgctxt "KWin Scripting error thrown due to incorrect argument" +msgid "Second argument to registerTouchScreenEdge needs to be a callback" +msgstr "المعطى الثّاني إلى registerScreenEdge يجب أن يكون ردّ نداء" + +#: scriptingutils.h:239 +#, kde-format +msgctxt "KWin Scripting error thrown due to incorrect argument" +msgid "Argument for registerUserActionsMenu needs to be a callback" +msgstr "معطى registerUserActionsMenu يجب أن يكون ردّ نداء" + +#: scriptingutils.h:294 +#, kde-format +msgctxt "Assertion failed in KWin script with given value" +msgid "Assertion failed: %1" +msgstr "فشل التّوكيد: %1" + +#: scriptingutils.h:305 +#, kde-format +msgctxt "Assertion failed in KWin script with expected value and actual value" +msgid "Assertion failed: Expected %1, got %2" +msgstr "فشل التّوكيد: توقّعت %1، حصلت على %2" \ No newline at end of file diff --git a/po/as/kwin.po b/po/as/kwin.po new file mode 100644 index 0000000..d5319d1 --- /dev/null +++ b/po/as/kwin.po @@ -0,0 +1,2621 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Amitakhya Phukan <অমিতাক্ষ ফুকন>, 2009. +msgid "" +msgstr "" +"Project-Id-Version: kwin\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-11-25 08:45+0100\n" +"PO-Revision-Date: 2009-01-16 16:49+0530\n" +"Last-Translator: Amitakhya Phukan <অমিতাক্ষ ফুকন>\n" +"Language-Team: Assamese \n" +"Language: as\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 0.2\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 "aphukan@fedoraproject.org" + +#: abstract_client.cpp:2729 +#, kde-format +msgctxt "Application is not responding, appended to window title" +msgid "(Not Responding)" +msgstr "" + +#: abstract_wayland_output.cpp:252 +#, kde-format +msgid "unknown" +msgstr "" + +#: colorcorrection/manager.cpp:58 +#, kde-format +msgctxt "Night Color was disabled" +msgid "Night Color Off" +msgstr "" + +#: colorcorrection/manager.cpp:59 +#, kde-format +msgctxt "Night Color was enabled" +msgid "Night Color On" +msgstr "" + +#: colorcorrection/manager.cpp:228 colorcorrection/manager.cpp:231 +#: colorcorrection/manager.cpp:238 +#, kde-format +msgid "Toggle Night Color" +msgstr "" + +#: composite.cpp:926 +#, kde-format +msgid "" +"Desktop effects have been suspended by another application.
You can " +"resume using the '%1' shortcut." +msgstr "" + +#: debug_console.cpp:65 +#, kde-format +msgid "Timestamp" +msgstr "" + +#: debug_console.cpp:70 +#, kde-format +msgid "Timestamp (µsec)" +msgstr "" + +#: debug_console.cpp:77 +#, fuzzy, kde-format +#| msgid "Top-Left" +msgctxt "A mouse button" +msgid "Left" +msgstr "ওপৰত বাওঁফালে" + +#: debug_console.cpp:79 +#, fuzzy, kde-format +#| msgid "Top-Right" +msgctxt "A mouse button" +msgid "Right" +msgstr "ওপৰত সোঁফালে" + +#: debug_console.cpp:81 +#, kde-format +msgctxt "A mouse button" +msgid "Middle" +msgstr "" + +#: debug_console.cpp:83 +#, kde-format +msgctxt "A mouse button" +msgid "Back" +msgstr "" + +#: debug_console.cpp:85 +#, kde-format +msgctxt "A mouse button" +msgid "Forward" +msgstr "" + +#: debug_console.cpp:87 +#, kde-format +msgctxt "A mouse button" +msgid "Task" +msgstr "" + +#: debug_console.cpp:89 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 4" +msgstr "" + +#: debug_console.cpp:91 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 5" +msgstr "" + +#: debug_console.cpp:93 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 6" +msgstr "" + +#: debug_console.cpp:95 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 7" +msgstr "" + +#: debug_console.cpp:97 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 8" +msgstr "" + +#: debug_console.cpp:99 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 9" +msgstr "" + +#: debug_console.cpp:101 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 10" +msgstr "" + +#: debug_console.cpp:103 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 11" +msgstr "" + +#: debug_console.cpp:105 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 12" +msgstr "" + +#: debug_console.cpp:107 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 13" +msgstr "" + +#: debug_console.cpp:109 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 14" +msgstr "" + +#: debug_console.cpp:111 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 15" +msgstr "" + +#: debug_console.cpp:113 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 16" +msgstr "" + +#: debug_console.cpp:115 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 17" +msgstr "" + +#: debug_console.cpp:117 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 18" +msgstr "" + +#: debug_console.cpp:119 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 19" +msgstr "" + +#: debug_console.cpp:121 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 20" +msgstr "" + +#: debug_console.cpp:123 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 21" +msgstr "" + +#: debug_console.cpp:125 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 22" +msgstr "" + +#: debug_console.cpp:127 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 23" +msgstr "" + +#: debug_console.cpp:129 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 24" +msgstr "" + +#: debug_console.cpp:138 debug_console.cpp:140 +#, kde-format +msgid "Input Device" +msgstr "" + +#: debug_console.cpp:138 +#, kde-format +msgctxt "The input device of the event is not known" +msgid "Unknown" +msgstr "" + +#: debug_console.cpp:175 +#, kde-format +msgctxt "A mouse pointer motion event" +msgid "Pointer Motion" +msgstr "" + +#: debug_console.cpp:182 +#, kde-format +msgctxt "The relative mouse movement" +msgid "Delta" +msgstr "" + +#: debug_console.cpp:186 +#, kde-format +msgctxt "The relative mouse movement" +msgid "Delta (not accelerated)" +msgstr "" + +#: debug_console.cpp:189 +#, kde-format +msgctxt "The global mouse pointer position" +msgid "Global Position" +msgstr "" + +#: debug_console.cpp:193 +#, kde-format +msgctxt "A mouse pointer button press event" +msgid "Pointer Button Press" +msgstr "" + +#: debug_console.cpp:196 debug_console.cpp:204 +#, kde-format +msgctxt "A button in a mouse press/release event" +msgid "Button" +msgstr "" + +#: debug_console.cpp:197 debug_console.cpp:205 +#, kde-format +msgctxt "A button in a mouse press/release event" +msgid "Native Button code" +msgstr "" + +#: debug_console.cpp:198 debug_console.cpp:206 +#, kde-format +msgctxt "All currently pressed buttons in a mouse press/release event" +msgid "Pressed Buttons" +msgstr "" + +#: debug_console.cpp:201 +#, kde-format +msgctxt "A mouse pointer button release event" +msgid "Pointer Button Release" +msgstr "" + +#: debug_console.cpp:221 +#, kde-format +msgctxt "A mouse pointer axis (wheel) event" +msgid "Pointer Axis" +msgstr "" + +#: debug_console.cpp:225 +#, kde-format +msgctxt "The orientation of a pointer axis event" +msgid "Orientation" +msgstr "" + +#: debug_console.cpp:226 +#, kde-format +msgctxt "An orientation of a pointer axis event" +msgid "Horizontal" +msgstr "" + +#: debug_console.cpp:227 +#, kde-format +msgctxt "An orientation of a pointer axis event" +msgid "Vertical" +msgstr "" + +#: debug_console.cpp:228 +#, kde-format +msgctxt "The angle delta of a pointer axis event" +msgid "Delta" +msgstr "" + +#: debug_console.cpp:243 +#, kde-format +msgctxt "A key press event" +msgid "Key Press" +msgstr "" + +#: debug_console.cpp:246 +#, kde-format +msgctxt "A key release event" +msgid "Key Release" +msgstr "" + +#: debug_console.cpp:255 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Shift" +msgstr "" + +#: debug_console.cpp:259 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Control" +msgstr "" + +#: debug_console.cpp:263 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Alt" +msgstr "" + +#: debug_console.cpp:267 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Meta" +msgstr "" + +#: debug_console.cpp:271 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Keypad" +msgstr "" + +#: debug_console.cpp:275 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Group-switch" +msgstr "" + +#: debug_console.cpp:281 +#, kde-format +msgctxt "Whether the event is an automatic key repeat" +msgid "Repeat" +msgstr "" + +#: debug_console.cpp:285 +#, kde-format +msgctxt "The code as read from the input device" +msgid "Scan code" +msgstr "" + +#: debug_console.cpp:286 +#, kde-format +msgctxt "Key according to Qt" +msgid "Qt::Key code" +msgstr "" + +#: debug_console.cpp:288 +#, kde-format +msgctxt "The translated code to an Xkb symbol" +msgid "Xkb symbol" +msgstr "" + +#: debug_console.cpp:289 +#, kde-format +msgctxt "The translated code interpreted as text" +msgid "Utf8" +msgstr "" + +#: debug_console.cpp:290 +#, kde-format +msgctxt "The currently active modifiers" +msgid "Modifiers" +msgstr "" + +#: debug_console.cpp:302 +#, kde-format +msgctxt "A touch down event" +msgid "Touch down" +msgstr "" + +#: debug_console.cpp:304 debug_console.cpp:319 debug_console.cpp:334 +#, kde-format +msgctxt "The id of the touch point in the touch event" +msgid "Point identifier" +msgstr "" + +#: debug_console.cpp:305 debug_console.cpp:320 +#, kde-format +msgctxt "The global position of the touch point" +msgid "Global position" +msgstr "" + +#: debug_console.cpp:317 +#, kde-format +msgctxt "A touch motion event" +msgid "Touch Motion" +msgstr "" + +#: debug_console.cpp:332 +#, kde-format +msgctxt "A touch up event" +msgid "Touch Up" +msgstr "" + +#: debug_console.cpp:345 +#, kde-format +msgctxt "A pinch gesture is started" +msgid "Pinch start" +msgstr "" + +#: debug_console.cpp:347 +#, kde-format +msgctxt "Number of fingers in this pinch gesture" +msgid "Finger count" +msgstr "" + +#: debug_console.cpp:358 +#, kde-format +msgctxt "A pinch gesture is updated" +msgid "Pinch update" +msgstr "" + +#: debug_console.cpp:360 +#, kde-format +msgctxt "Current scale in pinch gesture" +msgid "Scale" +msgstr "" + +#: debug_console.cpp:361 +#, kde-format +msgctxt "Current angle in pinch gesture" +msgid "Angle delta" +msgstr "" + +#: debug_console.cpp:362 +#, kde-format +msgctxt "Current delta in pinch gesture" +msgid "Delta x" +msgstr "" + +#: debug_console.cpp:363 +#, kde-format +msgctxt "Current delta in pinch gesture" +msgid "Delta y" +msgstr "" + +#: debug_console.cpp:374 +#, kde-format +msgctxt "A pinch gesture ended" +msgid "Pinch end" +msgstr "" + +#: debug_console.cpp:386 +#, kde-format +msgctxt "A pinch gesture got cancelled" +msgid "Pinch cancelled" +msgstr "" + +#: debug_console.cpp:398 +#, kde-format +msgctxt "A swipe gesture is started" +msgid "Swipe start" +msgstr "" + +#: debug_console.cpp:400 +#, kde-format +msgctxt "Number of fingers in this swipe gesture" +msgid "Finger count" +msgstr "" + +#: debug_console.cpp:411 +#, kde-format +msgctxt "A swipe gesture is updated" +msgid "Swipe update" +msgstr "" + +#: debug_console.cpp:413 +#, kde-format +msgctxt "Current delta in swipe gesture" +msgid "Delta x" +msgstr "" + +#: debug_console.cpp:414 +#, kde-format +msgctxt "Current delta in swipe gesture" +msgid "Delta y" +msgstr "" + +#: debug_console.cpp:425 +#, kde-format +msgctxt "A swipe gesture ended" +msgid "Swipe end" +msgstr "" + +#: debug_console.cpp:437 +#, kde-format +msgctxt "A swipe gesture got cancelled" +msgid "Swipe cancelled" +msgstr "" + +#: debug_console.cpp:449 +#, fuzzy, kde-format +#| msgid "Switch to Screen 0" +msgctxt "A hardware switch (e.g. notebook lid) got toggled" +msgid "Switch toggled" +msgstr "পৰ্দ্দা ০ লৈ পৰিবৰ্তন কৰক" + +#: debug_console.cpp:457 +#, kde-format +msgctxt "Name of a hardware switch" +msgid "Notebook lid" +msgstr "" + +#: debug_console.cpp:459 +#, kde-format +msgctxt "Name of a hardware switch" +msgid "Tablet mode" +msgstr "" + +#: debug_console.cpp:461 +#, fuzzy, kde-format +#| msgid "Switch to Screen 0" +msgctxt "A hardware switch" +msgid "Switch" +msgstr "পৰ্দ্দা ০ লৈ পৰিবৰ্তন কৰক" + +#: debug_console.cpp:465 +#, kde-format +msgctxt "The hardware switch got turned off" +msgid "Off" +msgstr "" + +#: debug_console.cpp:468 +#, kde-format +msgctxt "The hardware switch got turned on" +msgid "On" +msgstr "" + +#: debug_console.cpp:473 +#, kde-format +msgctxt "State of a hardware switch (on/off)" +msgid "State" +msgstr "" + +#: debug_console.cpp:488 +#, kde-format +msgid "Tablet Tool" +msgstr "" + +#: debug_console.cpp:489 +#, kde-format +msgid "EventType" +msgstr "" + +#: debug_console.cpp:490 debug_console.cpp:537 debug_console.cpp:549 +#, kde-format +msgid "Position" +msgstr "" + +#: debug_console.cpp:492 +#, kde-format +msgid "Tilt" +msgstr "" + +#: debug_console.cpp:494 +#, kde-format +msgid "Rotation" +msgstr "" + +#: debug_console.cpp:495 +#, kde-format +msgid "Pressure" +msgstr "" + +#: debug_console.cpp:496 +#, fuzzy, kde-format +#| msgid "Mouse Emulation" +msgid "Buttons" +msgstr "মাউছ এমুলেছন" + +#. i18n: ectx: property (title), widget (QGroupBox, modifiersBox) +#: debug_console.cpp:497 debug_console.ui:356 +#, kde-format +msgid "Modifiers" +msgstr "" + +#: debug_console.cpp:510 +#, kde-format +msgid "Tablet Tool Button" +msgstr "" + +#: debug_console.cpp:511 debug_console.cpp:526 +#, fuzzy, kde-format +#| msgid "Mouse Emulation" +msgid "Pressed Buttons" +msgstr "মাউছ এমুলেছন" + +#: debug_console.cpp:525 +#, kde-format +msgid "Tablet Pad Button" +msgstr "" + +#: debug_console.cpp:535 +#, kde-format +msgid "Tablet Pad Strip" +msgstr "" + +#: debug_console.cpp:536 debug_console.cpp:548 +#, kde-format +msgid "Number" +msgstr "" + +#: debug_console.cpp:538 debug_console.cpp:550 +#, kde-format +msgid "isFinger" +msgstr "" + +#: debug_console.cpp:547 +#, kde-format +msgid "Tablet Pad Ring" +msgstr "" + +#: debug_console.cpp:735 +#, fuzzy, kde-format +#| msgid "Mouse Emulation" +msgid "No Mouse Buttons" +msgstr "মাউছ এমুলেছন" + +#: debug_console.cpp:739 +#, kde-format +msgctxt "Mouse Button" +msgid "left" +msgstr "" + +#: debug_console.cpp:742 +#, fuzzy, kde-format +#| msgid "Top-Right" +msgctxt "Mouse Button" +msgid "right" +msgstr "ওপৰত সোঁফালে" + +#: debug_console.cpp:745 +#, kde-format +msgctxt "Mouse Button" +msgid "middle" +msgstr "" + +#: debug_console.cpp:748 +#, kde-format +msgctxt "Mouse Button" +msgid "back" +msgstr "" + +#: debug_console.cpp:751 +#, kde-format +msgctxt "Mouse Button" +msgid "forward" +msgstr "" + +#: debug_console.cpp:754 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 1" +msgstr "" + +#: debug_console.cpp:757 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 2" +msgstr "" + +#: debug_console.cpp:760 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 3" +msgstr "" + +#: debug_console.cpp:763 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 4" +msgstr "" + +#: debug_console.cpp:766 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 5" +msgstr "" + +#: debug_console.cpp:769 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 6" +msgstr "" + +#: debug_console.cpp:772 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 7" +msgstr "" + +#: debug_console.cpp:775 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 8" +msgstr "" + +#: debug_console.cpp:778 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 9" +msgstr "" + +#: debug_console.cpp:781 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 10" +msgstr "" + +#: debug_console.cpp:784 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 11" +msgstr "" + +#: debug_console.cpp:787 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 12" +msgstr "" + +#: debug_console.cpp:790 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 13" +msgstr "" + +#: debug_console.cpp:793 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 14" +msgstr "" + +#: debug_console.cpp:796 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 15" +msgstr "" + +#: debug_console.cpp:799 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 16" +msgstr "" + +#: debug_console.cpp:802 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 17" +msgstr "" + +#: debug_console.cpp:805 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 18" +msgstr "" + +#: debug_console.cpp:808 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 19" +msgstr "" + +#: debug_console.cpp:811 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 20" +msgstr "" + +#: debug_console.cpp:814 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 21" +msgstr "" + +#: debug_console.cpp:817 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 22" +msgstr "" + +#: debug_console.cpp:820 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 23" +msgstr "" + +#: debug_console.cpp:823 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 24" +msgstr "" + +#: debug_console.cpp:826 +#, kde-format +msgctxt "Mouse Button" +msgid "task" +msgstr "" + +#: debug_console.cpp:1176 +#, fuzzy, kde-format +#| msgid "Close Window" +msgid "X11 Client Windows" +msgstr "সংযোগক্ষেত্ৰ বন্ধ কৰক" + +#: debug_console.cpp:1178 +#, kde-format +msgid "X11 Unmanaged Windows" +msgstr "" + +#: debug_console.cpp:1180 +#, fuzzy, kde-format +#| msgid "Shade Window" +msgid "Wayland Windows" +msgstr "ছায়াবৃত সংযোগক্ষেত্ৰ" + +#: debug_console.cpp:1182 +#, fuzzy, kde-format +#| msgid "Lower Window" +msgid "Internal Windows" +msgstr "সংযোগক্ষেত্ৰ নমাওক" + +#. i18n: ectx: property (text), widget (QPushButton, quitButton) +#: debug_console.ui:32 +#, kde-format +msgid "Quit Debug Console" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, windows) +#: debug_console.ui:45 +#, kde-format +msgid "Windows" +msgstr "সংযোগক্ষেত্ৰ" + +#. i18n: ectx: attribute (title), widget (QWidget, surfaces) +#: debug_console.ui:59 +#, kde-format +msgid "Surfaces" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, input) +#: debug_console.ui:69 +#, kde-format +msgid "Input Events" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, inputDevices) +#: debug_console.ui:86 +#, kde-format +msgid "Input Devices" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, tab) +#: debug_console.ui:96 +#, kde-format +msgid "OpenGL" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, noOpenGLLabel) +#: debug_console.ui:102 +#, kde-format +msgid "No OpenGL compositor running" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, driverInfoBox) +#: debug_console.ui:130 +#, kde-format +msgid "OpenGL (ES) driver information" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: debug_console.ui:136 +#, kde-format +msgid "Vendor:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: debug_console.ui:143 +#, kde-format +msgid "Renderer:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: debug_console.ui:150 +#, kde-format +msgid "Version:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: debug_console.ui:157 +#, kde-format +msgid "Shading Language Version:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: debug_console.ui:164 +#, kde-format +msgid "Driver:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: debug_console.ui:171 +#, kde-format +msgid "GPU class:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: debug_console.ui:178 +#, kde-format +msgid "OpenGL Version:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_8) +#: debug_console.ui:185 +#, kde-format +msgid "GLSL Version:" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, platformExtensionsBox) +#: debug_console.ui:251 +#, kde-format +msgid "Platform Extensions" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, glExtensionsBox) +#: debug_console.ui:267 +#, kde-format +msgid "OpenGL (ES) Extensions" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, keyboard) +#: debug_console.ui:288 +#, kde-format +msgid "Keyboard" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, layoutBox) +#: debug_console.ui:315 +#, kde-format +msgid "Keymap Layouts" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_9) +#: debug_console.ui:337 +#, fuzzy, kde-format +#| msgid "Configur&e Window Behavior..." +msgid "Current Layout:" +msgstr "সংযোগক্ষেত্ৰৰ আচৰণ বিন্যাস কৰক...(&e)" + +#. i18n: ectx: property (title), widget (QGroupBox, activeModifiersBox) +#: debug_console.ui:372 +#, kde-format +msgid "Active Modifiers" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, ledsBox) +#: debug_console.ui:388 +#, kde-format +msgid "LEDs" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, activeLedsBox) +#: debug_console.ui:404 +#, kde-format +msgid "Active LEDs" +msgstr "" + +#: helpers/killer/killer.cpp:30 +#, fuzzy, kde-format +#| msgid "KDE window manager" +msgid "Window Manager" +msgstr "KDE সংযোগক্ষেত্ৰ পৰিচালন ব্যৱস্থা" + +#: helpers/killer/killer.cpp:35 +#, kde-format +msgid "PID of the application to terminate" +msgstr "বন্ধ কৰাৰ বাবে চিহ্নিত অনুপ্ৰয়োগৰ PID" + +#: helpers/killer/killer.cpp:35 +#, kde-format +msgid "pid" +msgstr "" + +#: helpers/killer/killer.cpp:37 +#, kde-format +msgid "Hostname on which the application is running" +msgstr "অনুপ্ৰয়োগ চালনকাৰী যন্ত্ৰৰ গৃহস্থৰ নাম" + +#: helpers/killer/killer.cpp:37 +#, kde-format +msgid "hostname" +msgstr "" + +#: helpers/killer/killer.cpp:39 +#, kde-format +msgid "Caption of the window to be terminated" +msgstr "বন্ধ কৰাৰ বাবে চিহ্নিত সংযোগক্ষেত্ৰৰ শিৰোনাম" + +#: helpers/killer/killer.cpp:39 +#, kde-format +msgid "caption" +msgstr "" + +#: helpers/killer/killer.cpp:41 +#, kde-format +msgid "Name of the application to be terminated" +msgstr "বন্ধ কৰাৰ বাবে চিহ্নিত অনুপ্ৰয়োগৰ নাম" + +#: helpers/killer/killer.cpp:41 +#, kde-format +msgid "name" +msgstr "" + +#: helpers/killer/killer.cpp:43 +#, kde-format +msgid "ID of resource belonging to the application" +msgstr "অনুপ্ৰয়োগৰ সম্পদৰ ID" + +#: helpers/killer/killer.cpp:43 +#, kde-format +msgid "id" +msgstr "" + +#: helpers/killer/killer.cpp:45 +#, kde-format +msgid "Time of user action causing termination" +msgstr "" + +#: helpers/killer/killer.cpp:45 +#, kde-format +msgid "time" +msgstr "" + +#: helpers/killer/killer.cpp:47 +#, kde-format +msgid "KWin helper utility" +msgstr "KWin সহায়ক সামগ্ৰী" + +#: helpers/killer/killer.cpp:71 +#, kde-format +msgid "This helper utility is not supposed to be called directly." +msgstr "সহায়ক সামগ্ৰীক পোনেপোনে মাতিব নালাগে ।" + +#: helpers/killer/killer.cpp:81 +#, kde-format +msgctxt "@info" +msgid "Application \"%1\" is not responding" +msgstr "" + +#: helpers/killer/killer.cpp:83 +#, kde-kuit-format +msgctxt "@info" +msgid "" +"

You tried to close window \"%1\" from application \"%2\" (Process ID: %3) " +"but the application is not responding.

" +msgstr "" + +#: helpers/killer/killer.cpp:85 +#, kde-kuit-format +msgctxt "@info" +msgid "" +"

You tried to close window \"%1\" from application \"%2\" (Process ID: " +"%3), running on host \"%4\", but the application is not responding.

" +msgstr "" + +#: helpers/killer/killer.cpp:88 +#, kde-kuit-format +msgctxt "@info" +msgid "" +"

Do you want to terminate this application?

Terminating the " +"application will close all of its child windows. Any unsaved data will be " +"lost.

" +msgstr "" + +#: helpers/killer/killer.cpp:92 +#, kde-format +msgid "&Terminate Application %1" +msgstr "" + +#: helpers/killer/killer.cpp:93 +#, kde-format +msgid "Wait Longer" +msgstr "" + +#: keyboard_layout.cpp:110 keyboard_layout.cpp:111 +#, kde-format +msgctxt "tooltip title" +msgid "Keyboard Layout" +msgstr "" + +#: keyboard_layout.cpp:258 +#, fuzzy, kde-format +#| msgid "Configur&e Window Behavior..." +msgid "Configure Layouts..." +msgstr "সংযোগক্ষেত্ৰৰ আচৰণ বিন্যাস কৰক...(&e)" + +#: killwindow.cpp:33 +#, kde-format +msgid "" +"Select window to force close with left click or enter.\n" +"Escape or right click to cancel." +msgstr "" + +#: kwinbindings.cpp:38 +#, kde-format +msgid "Window Operations Menu" +msgstr "সংযোগক্ষেত্ৰ পৰিচালনাৰ তালিকা" + +#: kwinbindings.cpp:40 +#, kde-format +msgid "Close Window" +msgstr "সংযোগক্ষেত্ৰ বন্ধ কৰক" + +#: kwinbindings.cpp:42 +#, kde-format +msgid "Maximize Window" +msgstr "সংযোগক্ষেত্ৰ ডাঙৰ কৰক" + +#: kwinbindings.cpp:44 +#, kde-format +msgid "Maximize Window Vertically" +msgstr "উলম্ব দিশত সংযোগক্ষেত্ৰ ডাঙৰ কৰক" + +#: kwinbindings.cpp:46 +#, kde-format +msgid "Maximize Window Horizontally" +msgstr "অনুভূমিক দিশত সংযোগক্ষেত্ৰ ডাঙৰ কৰক" + +#: kwinbindings.cpp:48 +#, kde-format +msgid "Minimize Window" +msgstr "সংযোগক্ষেত্ৰ সৰু কৰক" + +#: kwinbindings.cpp:50 +#, kde-format +msgid "Shade Window" +msgstr "ছায়াবৃত সংযোগক্ষেত্ৰ" + +#: kwinbindings.cpp:52 +#, kde-format +msgid "Move Window" +msgstr "সংযোগক্ষেত্ৰ স্থানান্তৰণ" + +#: kwinbindings.cpp:54 +#, kde-format +msgid "Resize Window" +msgstr "সংযোগক্ষেত্ৰৰ মাপ পৰিবৰ্তন" + +#: kwinbindings.cpp:56 +#, kde-format +msgid "Raise Window" +msgstr "সংযোগক্ষেত্ৰ উঠাওক" + +#: kwinbindings.cpp:58 +#, kde-format +msgid "Lower Window" +msgstr "সংযোগক্ষেত্ৰ নমাওক" + +#: kwinbindings.cpp:60 +#, kde-format +msgid "Toggle Window Raise/Lower" +msgstr "" + +#: kwinbindings.cpp:62 +#, kde-format +msgid "Make Window Fullscreen" +msgstr "সম্পূৰ্ণ পৰ্দ্দাত সংযোগক্ষেত্ৰ প্ৰদৰ্শন" + +#: kwinbindings.cpp:64 +#, kde-format +msgid "Hide Window Border" +msgstr "সংযোগক্ষেত্ৰৰ প্ৰান্ত লুকাওক কৰক" + +#: kwinbindings.cpp:66 +#, kde-format +msgid "Keep Window Above Others" +msgstr "সকলো সংযোগক্ষেত্ৰৰ শীৰ্ষত এই সংযোগক্ষেত্ৰ স্থাপন কৰক" + +#: kwinbindings.cpp:68 +#, kde-format +msgid "Keep Window Below Others" +msgstr "অন্য সংযোগক্ষেত্ৰৰ তলত এই সংযোগক্ষেত্ৰ স্থাপন কৰক" + +#: kwinbindings.cpp:70 +#, kde-format +msgid "Activate Window Demanding Attention" +msgstr "" + +#: kwinbindings.cpp:72 +#, kde-format +msgid "Setup Window Shortcut" +msgstr "সংযোগক্ষেত্ৰৰ শৰ্ট কাট নিৰ্ধাৰণ কৰক" + +#: kwinbindings.cpp:74 +#, kde-format +msgid "Pack Window to the Right" +msgstr "সোঁফালে সংযোগক্ষেত্ৰ একত্ৰিত কৰক" + +#: kwinbindings.cpp:76 +#, kde-format +msgid "Pack Window to the Left" +msgstr "বাওঁফালে সংযোগক্ষেত্ৰ একত্ৰিত কৰক" + +#: kwinbindings.cpp:78 +#, kde-format +msgid "Pack Window Up" +msgstr "ওপৰত সংযোগক্ষেত্ৰ একত্ৰিত কৰক" + +#: kwinbindings.cpp:80 +#, kde-format +msgid "Pack Window Down" +msgstr "তলত সংযোগক্ষেত্ৰ একত্ৰিত কৰক" + +#: kwinbindings.cpp:82 +#, kde-format +msgid "Pack Grow Window Horizontally" +msgstr "" + +#: kwinbindings.cpp:84 +#, kde-format +msgid "Pack Grow Window Vertically" +msgstr "" + +#: kwinbindings.cpp:86 +#, kde-format +msgid "Pack Shrink Window Horizontally" +msgstr "" + +#: kwinbindings.cpp:88 +#, kde-format +msgid "Pack Shrink Window Vertically" +msgstr "" + +#: kwinbindings.cpp:90 +#, fuzzy, kde-format +#| msgid "Pack Window to the Left" +msgid "Quick Tile Window to the Left" +msgstr "বাওঁফালে সংযোগক্ষেত্ৰ একত্ৰিত কৰক" + +#: kwinbindings.cpp:92 +#, fuzzy, kde-format +#| msgid "Pack Window to the Right" +msgid "Quick Tile Window to the Right" +msgstr "সোঁফালে সংযোগক্ষেত্ৰ একত্ৰিত কৰক" + +#: kwinbindings.cpp:94 +#, fuzzy, kde-format +#| msgid "Pack Window to the Left" +msgid "Quick Tile Window to the Top" +msgstr "বাওঁফালে সংযোগক্ষেত্ৰ একত্ৰিত কৰক" + +#: kwinbindings.cpp:96 +#, fuzzy, kde-format +#| msgid "Pack Window to the Left" +msgid "Quick Tile Window to the Bottom" +msgstr "বাওঁফালে সংযোগক্ষেত্ৰ একত্ৰিত কৰক" + +#: kwinbindings.cpp:98 +#, fuzzy, kde-format +#| msgid "Pack Window to the Left" +msgid "Quick Tile Window to the Top Left" +msgstr "বাওঁফালে সংযোগক্ষেত্ৰ একত্ৰিত কৰক" + +#: kwinbindings.cpp:100 +#, fuzzy, kde-format +#| msgid "Pack Window to the Left" +msgid "Quick Tile Window to the Bottom Left" +msgstr "বাওঁফালে সংযোগক্ষেত্ৰ একত্ৰিত কৰক" + +#: kwinbindings.cpp:102 +#, fuzzy, kde-format +#| msgid "Pack Window to the Right" +msgid "Quick Tile Window to the Top Right" +msgstr "সোঁফালে সংযোগক্ষেত্ৰ একত্ৰিত কৰক" + +#: kwinbindings.cpp:104 +#, fuzzy, kde-format +#| msgid "Pack Window to the Right" +msgid "Quick Tile Window to the Bottom Right" +msgstr "সোঁফালে সংযোগক্ষেত্ৰ একত্ৰিত কৰক" + +#: kwinbindings.cpp:106 +#, fuzzy, kde-format +#| msgid "Switch to Screen 0" +msgid "Switch to Window Above" +msgstr "পৰ্দ্দা ০ লৈ পৰিবৰ্তন কৰক" + +#: kwinbindings.cpp:108 +#, fuzzy, kde-format +#| msgid "Switch to Previous Desktop" +msgid "Switch to Window Below" +msgstr "পূৰ্ববৰ্তী ডেষ্কট'পলৈ পৰিবৰ্তন কৰক" + +#: kwinbindings.cpp:110 +#, fuzzy, kde-format +#| msgid "Pack Window to the Right" +msgid "Switch to Window to the Right" +msgstr "সোঁফালে সংযোগক্ষেত্ৰ একত্ৰিত কৰক" + +#: kwinbindings.cpp:112 +#, fuzzy, kde-format +#| msgid "Pack Window to the Left" +msgid "Switch to Window to the Left" +msgstr "বাওঁফালে সংযোগক্ষেত্ৰ একত্ৰিত কৰক" + +#: kwinbindings.cpp:114 +#, kde-format +msgid "Increase Opacity of Active Window by 5 %" +msgstr "" + +#: kwinbindings.cpp:116 +#, kde-format +msgid "Decrease Opacity of Active Window by 5 %" +msgstr "" + +#: kwinbindings.cpp:119 +#, kde-format +msgid "Keep Window on All Desktops" +msgstr "সকলো ডেষ্কট'পত সংযোগক্ষেত্ৰ প্ৰদৰ্শিত হ'ব" + +#: kwinbindings.cpp:123 +#, fuzzy, kde-format +#| msgid "Window to Desktop 1" +msgid "Window to Desktop %1" +msgstr "ডেষ্কট'প ১ লৈ সংযোগক্ষেত্ৰ স্থানান্তৰ কৰক" + +#: kwinbindings.cpp:125 +#, kde-format +msgid "Window to Next Desktop" +msgstr "পৰবৰ্তি ডেষ্কট'পলৈ সংযোগক্ষেত্ৰ স্থানান্তৰ কৰক" + +#: kwinbindings.cpp:126 +#, kde-format +msgid "Window to Previous Desktop" +msgstr "পূৰ্ববৰ্তী ডেষ্কট'পলৈ সংযোগক্ষেত্ৰ স্থানান্তৰ কৰক" + +#: kwinbindings.cpp:127 +#, kde-format +msgid "Window One Desktop to the Right" +msgstr "সোঁফালৰ ডেষ্কট'পলৈ সংযোগক্ষেত্ৰ স্থানান্তৰ কৰক" + +#: kwinbindings.cpp:128 +#, kde-format +msgid "Window One Desktop to the Left" +msgstr "বাওঁফালৰ ডেষ্কট'পলৈ সংযোগক্ষেত্ৰ স্থানান্তৰ কৰক" + +#: kwinbindings.cpp:129 +#, kde-format +msgid "Window One Desktop Up" +msgstr "ওপৰৰ ডেষ্কট'পলৈ সংযোগক্ষেত্ৰ স্থানান্তৰ কৰক" + +#: kwinbindings.cpp:130 +#, kde-format +msgid "Window One Desktop Down" +msgstr "তলৰ ডেষ্কট'পলৈ সংযোগক্ষেত্ৰ স্থানান্তৰ কৰক" + +#: kwinbindings.cpp:133 +#, fuzzy, kde-format +#| msgid "Window to Screen 1" +msgid "Window to Screen %1" +msgstr "পৰ্দ্দা ১ লৈ সংযোগক্ষেত্ৰ স্থানান্তৰ কৰক" + +#: kwinbindings.cpp:135 +#, kde-format +msgid "Window to Next Screen" +msgstr "পৰবৰ্তী পৰ্দ্দালৈ সংযোগক্ষেত্ৰ স্থানান্তৰ কৰক" + +#: kwinbindings.cpp:136 +#, fuzzy, kde-format +#| msgid "Window to Previous Desktop" +msgid "Window to Previous Screen" +msgstr "পূৰ্ববৰ্তী ডেষ্কট'পলৈ সংযোগক্ষেত্ৰ স্থানান্তৰ কৰক" + +#: kwinbindings.cpp:137 +#, fuzzy, kde-format +#| msgid "Show Desktop Grid" +msgid "Show Desktop" +msgstr "Desktop Grid দেখুৱাওক" + +#: kwinbindings.cpp:140 +#, fuzzy, kde-format +#| msgid "Switch to Screen 1" +msgid "Switch to Screen %1" +msgstr "পৰ্দ্দা ১ লৈ পৰিবৰ্তন কৰক" + +#: kwinbindings.cpp:143 +#, kde-format +msgid "Switch to Next Screen" +msgstr "পৰবৰ্তী পৰ্দ্দালৈ পৰিবৰ্তন কৰক" + +#: kwinbindings.cpp:144 +#, fuzzy, kde-format +#| msgid "Switch to Previous Desktop" +msgid "Switch to Previous Screen" +msgstr "পূৰ্ববৰ্তী ডেষ্কট'পলৈ পৰিবৰ্তন কৰক" + +#: kwinbindings.cpp:146 +#, kde-format +msgid "Kill Window" +msgstr "সংযোগক্ষেত্ৰ Kill কৰক" + +#: kwinbindings.cpp:147 +#, kde-format +msgid "Suspend Compositing" +msgstr "" + +#: kwinbindings.cpp:148 +#, kde-format +msgid "Invert Screen Colors" +msgstr "" + +#: main.cpp:184 main.cpp:214 +#, kde-format +msgid "KDE window manager" +msgstr "KDE সংযোগক্ষেত্ৰ পৰিচালন ব্যৱস্থা" + +#: main.cpp:189 +#, kde-format +msgid "KWin" +msgstr "KWin" + +#: main.cpp:193 +#, fuzzy, kde-format +#| msgid "(c) 1999-2008, The KDE Developers" +msgid "(c) 1999-2019, The KDE Developers" +msgstr "(c) 1999-2008, The KDE Developers" + +#: main.cpp:195 +#, kde-format +msgid "Matthias Ettrich" +msgstr "Matthias Ettrich" + +#: main.cpp:196 +#, kde-format +msgid "Cristian Tibirna" +msgstr "Cristian Tibirna" + +#: main.cpp:197 +#, kde-format +msgid "Daniel M. Duley" +msgstr "Daniel M. Duley" + +#: main.cpp:198 +#, kde-format +msgid "Luboš Luňák" +msgstr "Luboš Luňák" + +#: main.cpp:199 +#, kde-format +msgid "Martin Flöser" +msgstr "" + +#: main.cpp:200 +#, kde-format +msgid "David Edmundson" +msgstr "" + +#: main.cpp:201 +#, kde-format +msgid "Roman Gilg" +msgstr "" + +#: main.cpp:202 +#, kde-format +msgid "Vlad Zahorodnii" +msgstr "" + +#: main.cpp:211 +#, kde-format +msgid "Disable configuration options" +msgstr "বিন্যাসৰ বিকল্প নিষ্ক্ৰিয় কৰা হ'ব" + +#: main.cpp:212 +#, kde-format +msgid "Indicate that KWin has recently crashed n times" +msgstr "KWin এ n সংখ্যক বাৰ বিপৰ্যস্ত হোৱাৰ ইঙ্গিত প্ৰদৰ্শন কৰা হ'ব" + +#: main_wayland.cpp:459 +#, kde-format +msgid "Start a rootless Xwayland server." +msgstr "" + +#: main_wayland.cpp:461 +#, kde-format +msgid "" +"Name of the Wayland socket to listen on. If not set \"wayland-0\" is used." +msgstr "" + +#: main_wayland.cpp:464 +#, kde-format +msgid "Render to framebuffer." +msgstr "" + +#: main_wayland.cpp:466 +#, kde-format +msgid "The framebuffer device to render to." +msgstr "" + +#: main_wayland.cpp:469 +#, kde-format +msgid "The X11 Display to use in windowed mode on platform X11." +msgstr "" + +#: main_wayland.cpp:472 +#, kde-format +msgid "The Wayland Display to use in windowed mode on platform Wayland." +msgstr "" + +#: main_wayland.cpp:474 +#, kde-format +msgid "Render to a virtual framebuffer." +msgstr "" + +#: main_wayland.cpp:476 +#, kde-format +msgid "The width for windowed mode. Default width is 1024." +msgstr "" + +#: main_wayland.cpp:480 +#, kde-format +msgid "The height for windowed mode. Default height is 768." +msgstr "" + +#: main_wayland.cpp:485 +#, kde-format +msgid "The scale for windowed mode. Default value is 1." +msgstr "" + +#: main_wayland.cpp:490 +#, kde-format +msgid "" +"The number of windows to open as outputs in windowed mode. Default value is 1" +msgstr "" + +#: main_wayland.cpp:520 +#, kde-format +msgid "Use libhybris hwcomposer" +msgstr "" + +#: main_wayland.cpp:526 +#, kde-format +msgid "" +"Enable libinput support for input events processing. Note: never use in a " +"nested session.\t(deprecated)" +msgstr "" + +#: main_wayland.cpp:529 +#, kde-format +msgid "Render through drm node." +msgstr "" + +#: main_wayland.cpp:536 +#, kde-format +msgid "Input method that KWin starts." +msgstr "" + +#: main_wayland.cpp:541 +#, kde-format +msgid "List all available backends and quit." +msgstr "" + +#: main_wayland.cpp:545 +#, kde-format +msgid "Starts the session in locked mode." +msgstr "" + +#: main_wayland.cpp:549 +#, kde-format +msgid "Starts the session without lock screen support." +msgstr "" + +#: main_wayland.cpp:553 +#, kde-format +msgid "Starts the session without global shortcuts support." +msgstr "" + +#: main_wayland.cpp:557 +#, kde-format +msgid "Exit after the session application, which is started by KWin, closed." +msgstr "" + +#: main_wayland.cpp:562 +#, kde-format +msgid "Applications to start once Wayland and Xwayland server are started" +msgstr "" + +#: main_x11.cpp:65 +#, kde-format +msgid "" +"KWin is unstable.\n" +"It seems to have crashed several times in a row.\n" +"You can select another window manager to run:" +msgstr "" +"KWin ত সমস্যা দেখা দিছে ।\n" +"একাধিক বাৰ বিপৰ্যস্ত হৈছে ।\n" +"আপুনি কোনো পৃথক সংযোগক্ষেত্ৰ পৰিচালন ব্যৱস্থা নিৰ্ব্বাচন কৰিব পাৰে:" + +#: main_x11.cpp:224 +#, kde-format +msgid "" +"kwin: unable to claim manager selection, another wm running? (try using --" +"replace)\n" +msgstr "" +"kwin: পৰিচালন ব্যৱস্থা ধাৰ্য কৰোঁতে ব্যৰ্থ, এটা পৃথক সংযোগক্ষেত্ৰ পৰিচালন ব্যৱস্থা " +"বৰ্তমানে চলিছে নেকি ? ( --replace ৰ প্ৰয়োগ চেষ্টা কৰক)\n" + +#: main_x11.cpp:241 +#, fuzzy, kde-format +#| msgid "" +#| "kwin: unable to claim manager selection, another wm running? (try using --" +#| "replace)\n" +msgid "kwin: another window manager is running (try using --replace)\n" +msgstr "" +"kwin: পৰিচালন ব্যৱস্থা ধাৰ্য কৰোঁতে ব্যৰ্থ, এটা পৃথক সংযোগক্ষেত্ৰ পৰিচালন ব্যৱস্থা " +"বৰ্তমানে চলিছে নেকি ? ( --replace ৰ প্ৰয়োগ চেষ্টা কৰক)\n" + +#: main_x11.cpp:437 +#, kde-format +msgid "Replace already-running ICCCM2.0-compliant window manager" +msgstr "" +"ইতিমধ্যে চলি থকা ICCCM2.0 ৰ সৈতে সুসঙ্গত সংযোগক্ষেত্ৰ পৰিচালন ব্যৱস্থাক প্ৰতিস্থাপন " +"কৰক" + +#: main_x11.cpp:444 +#, kde-format +msgid "Disable KActivities integration." +msgstr "" + +#: plugins/scenes/opengl/scene_opengl.cpp:535 +#, kde-format +msgid "Desktop effects were restarted due to a graphics reset" +msgstr "" + +#. i18n: ectx: label, entry (count), group (General) +#: rulebooksettingsbase.kcfg:9 +#, kde-format +msgid "Total rules count" +msgstr "" + +#. i18n: ectx: label, entry (description), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:10 +#, kde-format +msgid "Rule description" +msgstr "" + +#. i18n: ectx: label, entry (descriptionLegacy), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:13 +#, kde-format +msgid "Rule description (legacy)" +msgstr "" + +#. i18n: ectx: label, entry (DeleteRule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:16 +#, kde-format +msgid "Delete this rule (for use in imports)" +msgstr "" + +#. i18n: ectx: label, entry (wmclass), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:20 +#, kde-format +msgid "Window class (application)" +msgstr "" + +#. i18n: ectx: label, entry (wmclassmatch), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:23 +#, kde-format +msgid "Window class string match type" +msgstr "" + +#. i18n: ectx: label, entry (wmclasscomplete), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:29 +#, kde-format +msgid "Match whole window class" +msgstr "" + +#. i18n: ectx: label, entry (windowrole), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:34 +#, fuzzy, kde-format +#| msgid "Windows" +msgid "Window role" +msgstr "সংযোগক্ষেত্ৰ" + +#. i18n: ectx: label, entry (windowrolematch), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:37 +#, kde-format +msgid "Window role string match type" +msgstr "" + +#. i18n: ectx: label, entry (title), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:44 +#, fuzzy, kde-format +#| msgid "Windows" +msgid "Window title" +msgstr "সংযোগক্ষেত্ৰ" + +#. i18n: ectx: label, entry (titlematch), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:47 +#, kde-format +msgid "Window title string match type" +msgstr "" + +#. i18n: ectx: label, entry (clientmachine), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:54 +#, kde-format +msgid "Machine (hostname)" +msgstr "" + +#. i18n: ectx: label, entry (clientmachinematch), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:57 +#, kde-format +msgid "Machine string match type" +msgstr "" + +#. i18n: ectx: label, entry (types), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:64 +#, kde-format +msgid "Window types that match" +msgstr "" + +#. i18n: ectx: label, entry (placement), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:69 +#, kde-format +msgid "Initial placement" +msgstr "" + +#. i18n: ectx: label, entry (placementrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:74 +#, fuzzy, kde-format +#| msgid "&Fullscreen" +msgid "Initial placement rule type" +msgstr "সম্পূৰ্ণ পৰ্দ্দাত প্ৰদৰ্শন (&F)" + +#. i18n: ectx: label, entry (position), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:79 +#, fuzzy, kde-format +#| msgid "Window Operations Menu" +msgid "Window position" +msgstr "সংযোগক্ষেত্ৰ পৰিচালনাৰ তালিকা" + +#. i18n: ectx: label, entry (positionrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:83 +#, fuzzy, kde-format +#| msgid "Window to Screen 0" +msgid "Window position rule type" +msgstr "পৰ্দ্দা ০ লৈ সংযোগক্ষেত্ৰ স্থানান্তৰ কৰক" + +#. i18n: ectx: label, entry (size), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:90 +#, fuzzy, kde-format +#| msgid "Windows" +msgid "Window size" +msgstr "সংযোগক্ষেত্ৰ" + +#. i18n: ectx: label, entry (sizerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:93 +#, kde-format +msgid "Window size rule type" +msgstr "" + +#. i18n: ectx: label, entry (minsize), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:100 +#, kde-format +msgid "Window minimum size" +msgstr "" + +#. i18n: ectx: label, entry (minsizerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:104 +#, kde-format +msgid "Window minimum size rule type" +msgstr "" + +#. i18n: ectx: label, entry (maxsize), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:109 +#, kde-format +msgid "Window maximum size" +msgstr "" + +#. i18n: ectx: label, entry (maxsizerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:113 +#, kde-format +msgid "Window maximum size rule type" +msgstr "" + +#. i18n: ectx: label, entry (opacityactive), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:118 +#, kde-format +msgid "Active opacity" +msgstr "" + +#. i18n: ectx: label, entry (opacityactiverule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:124 +#, kde-format +msgid "Active opacity rule type" +msgstr "" + +#. i18n: ectx: label, entry (opacityinactive), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:129 +#, kde-format +msgid "Inactive opacity" +msgstr "" + +#. i18n: ectx: label, entry (opacityinactiverule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:135 +#, kde-format +msgid "Inactive opacity rule type" +msgstr "" + +#. i18n: ectx: label, entry (ignoregeometry), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:140 +#, kde-format +msgid "Ignore requested geometry" +msgstr "" + +#. i18n: ectx: label, entry (ignoregeometryrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:144 +#, kde-format +msgid "Ignore requested geometry rule type" +msgstr "" + +#. i18n: ectx: label, entry (desktop), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:151 +#, fuzzy, kde-format +#| msgid "Desktop Cube" +msgid "Desktop number" +msgstr "Desktop Cube" + +#. i18n: ectx: label, entry (desktoprule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:155 +#, fuzzy, kde-format +#| msgid "Desktop Cube" +msgid "Desktop number rule type" +msgstr "Desktop Cube" + +#. i18n: ectx: label, entry (screen), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:162 +#, kde-format +msgid "Screen number" +msgstr "" + +#. i18n: ectx: label, entry (screenrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:166 +#, kde-format +msgid "Screen number rule type" +msgstr "" + +#. i18n: ectx: label, entry (activity), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:173 +#, kde-format +msgid "Activity" +msgstr "" + +#. i18n: ectx: label, entry (activityrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:176 +#, kde-format +msgid "Activity rule type" +msgstr "" + +#. i18n: ectx: label, entry (type), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:183 +#, fuzzy, kde-format +#| msgid "Setup Window Shortcut" +msgid "Set window type to" +msgstr "সংযোগক্ষেত্ৰৰ শৰ্ট কাট নিৰ্ধাৰণ কৰক" + +#. i18n: ectx: label, entry (typerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:189 +#, kde-format +msgid "Set window type rule type" +msgstr "" + +#. i18n: ectx: label, entry (maximizevert), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:194 +#, fuzzy, kde-format +#| msgid "Maximize Window Vertically" +msgid "Maximized vertically" +msgstr "উলম্ব দিশত সংযোগক্ষেত্ৰ ডাঙৰ কৰক" + +#. i18n: ectx: label, entry (maximizevertrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:198 +#, fuzzy, kde-format +#| msgid "Maximize Window Vertically" +msgid "Maximized vertically rule type" +msgstr "উলম্ব দিশত সংযোগক্ষেত্ৰ ডাঙৰ কৰক" + +#. i18n: ectx: label, entry (maximizehoriz), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:205 +#, fuzzy, kde-format +#| msgid "Maximize Window Horizontally" +msgid "Maximized horizontally" +msgstr "অনুভূমিক দিশত সংযোগক্ষেত্ৰ ডাঙৰ কৰক" + +#. i18n: ectx: label, entry (maximizehorizrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:209 +#, fuzzy, kde-format +#| msgid "Maximize Window Horizontally" +msgid "Maximized horizontally rule type" +msgstr "অনুভূমিক দিশত সংযোগক্ষেত্ৰ ডাঙৰ কৰক" + +#. i18n: ectx: label, entry (minimize), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:216 +#, fuzzy, kde-format +#| msgid "Minimize" +msgid "Minimized" +msgstr "সৰু কৰক" + +#. i18n: ectx: label, entry (minimizerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:220 +#, kde-format +msgid "Minimized rule type" +msgstr "" + +#. i18n: ectx: label, entry (shade), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:227 +#, fuzzy, kde-format +#| msgid "Shade" +msgid "Shaded" +msgstr "ছায়াবৃত" + +#. i18n: ectx: label, entry (shaderule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:231 +#, kde-format +msgid "Shaded rule type" +msgstr "" + +#. i18n: ectx: label, entry (skiptaskbar), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:238 +#, kde-format +msgid "Skip taskbar" +msgstr "" + +#. i18n: ectx: label, entry (skiptaskbarrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:242 +#, kde-format +msgid "Skip taskbar rule type" +msgstr "" + +#. i18n: ectx: label, entry (skippager), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:249 +#, kde-format +msgid "Skip pager" +msgstr "" + +#. i18n: ectx: label, entry (skippagerrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:253 +#, kde-format +msgid "Skip pager rule type" +msgstr "" + +#. i18n: ectx: label, entry (skipswitcher), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:260 +#, fuzzy, kde-format +#| msgid "Switch to Screen 0" +msgid "Skip switcher" +msgstr "পৰ্দ্দা ০ লৈ পৰিবৰ্তন কৰক" + +#. i18n: ectx: label, entry (skipswitcherrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:264 +#, kde-format +msgid "Skip switcher rule type" +msgstr "" + +#. i18n: ectx: label, entry (above), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:271 +#, fuzzy, kde-format +#| msgid "Keep above others" +msgid "Keep above" +msgstr "সকলো সংযোগক্ষেত্ৰৰ ওপৰত" + +#. i18n: ectx: label, entry (aboverule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:275 +#, fuzzy, kde-format +#| msgid "Keep above others" +msgid "Keep above rule type" +msgstr "সকলো সংযোগক্ষেত্ৰৰ ওপৰত" + +#. i18n: ectx: label, entry (below), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:282 +#, fuzzy, kde-format +#| msgid "Keep below others" +msgid "Keep below" +msgstr "অন্যান্য সংযোগক্ষেত্ৰৰ তলত" + +#. i18n: ectx: label, entry (belowrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:286 +#, fuzzy, kde-format +#| msgid "Keep below others" +msgid "Keep below rule type" +msgstr "অন্যান্য সংযোগক্ষেত্ৰৰ তলত" + +#. i18n: ectx: label, entry (fullscreen), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:293 +#, fuzzy, kde-format +#| msgid "&Fullscreen" +msgid "Fullscreen" +msgstr "সম্পূৰ্ণ পৰ্দ্দাত প্ৰদৰ্শন (&F)" + +#. i18n: ectx: label, entry (fullscreenrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:297 +#, fuzzy, kde-format +#| msgid "&Fullscreen" +msgid "Fullscreen rule type" +msgstr "সম্পূৰ্ণ পৰ্দ্দাত প্ৰদৰ্শন (&F)" + +#. i18n: ectx: label, entry (noborder), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:304 +#, kde-format +msgid "No titlebar and frame" +msgstr "" + +#. i18n: ectx: label, entry (noborderrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:308 +#, kde-format +msgid "No titlebar rule type" +msgstr "" + +#. i18n: ectx: label, entry (decocolor), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:315 +#, kde-format +msgid "Titlebar color and scheme" +msgstr "" + +#. i18n: ectx: label, entry (decocolorrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:318 +#, kde-format +msgid "Titlebar color rule type" +msgstr "" + +#. i18n: ectx: label, entry (blockcompositing), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:323 +#, kde-format +msgid "Block Compositing" +msgstr "" + +#. i18n: ectx: label, entry (blockcompositingrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:327 +#, kde-format +msgid "Block Compositing rule type" +msgstr "" + +#. i18n: ectx: label, entry (fsplevel), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:332 +#, kde-format +msgid "Focus stealing prevention" +msgstr "" + +#. i18n: ectx: label, entry (fsplevelrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:338 +#, kde-format +msgid "Focus stealing prevention rule type" +msgstr "" + +#. i18n: ectx: label, entry (fpplevel), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:343 +#, kde-format +msgid "Focus protection" +msgstr "" + +#. i18n: ectx: label, entry (fpplevelrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:349 +#, kde-format +msgid "Focus protection rule type" +msgstr "" + +#. i18n: ectx: label, entry (acceptfocus), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:354 +#, kde-format +msgid "Accept Focus" +msgstr "" + +#. i18n: ectx: label, entry (acceptfocusrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:358 +#, kde-format +msgid "Accept Focus rule type" +msgstr "" + +#. i18n: ectx: label, entry (closeable), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:363 +#, fuzzy, kde-format +#| msgid "Close" +msgid "Closeable" +msgstr "বন্ধ কৰক" + +#. i18n: ectx: label, entry (closeablerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:367 +#, kde-format +msgid "Closeable rule type" +msgstr "" + +#. i18n: ectx: label, entry (autogroup), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:372 +#, kde-format +msgid "Autogroup with identical" +msgstr "" + +#. i18n: ectx: label, entry (autogrouprule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:376 +#, kde-format +msgid "Autogroup with identical rule type" +msgstr "" + +#. i18n: ectx: label, entry (autogroupfg), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:381 +#, kde-format +msgid "Autogroup in foreground" +msgstr "" + +#. i18n: ectx: label, entry (autogroupfgrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:385 +#, kde-format +msgid "Autogroup in foreground rule type" +msgstr "" + +#. i18n: ectx: label, entry (autogroupid), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:390 +#, kde-format +msgid "Autogroup by ID" +msgstr "" + +#. i18n: ectx: label, entry (autogroupidrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:393 +#, kde-format +msgid "Autogroup by ID rule type" +msgstr "" + +#. i18n: ectx: label, entry (strictgeometry), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:398 +#, kde-format +msgid "Obey geometry restrictions" +msgstr "" + +#. i18n: ectx: label, entry (strictgeometryrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:402 +#, kde-format +msgid "Obey geometry restrictions rule type" +msgstr "" + +#. i18n: ectx: label, entry (shortcut), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:407 +#, kde-format +msgid "Shortcut" +msgstr "" + +#. i18n: ectx: label, entry (shortcutrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:410 +#, kde-format +msgid "Shortcut rule type" +msgstr "" + +#. i18n: ectx: label, entry (disableglobalshortcuts), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:417 +#, fuzzy, kde-format +#| msgid "Block Global Shortcuts" +msgid "Ignore global shortcuts" +msgstr "সাৰ্বজনীন শৰ্ট বাট ৰোধ কৰা হ'ব" + +#. i18n: ectx: label, entry (disableglobalshortcutsrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:421 +#, kde-format +msgid "Ignore global shortcuts rule type" +msgstr "" + +#. i18n: ectx: label, entry (desktopfile), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:426 +#, fuzzy, kde-format +#| msgid "Desktop Cube" +msgid "Desktop file name" +msgstr "Desktop Cube" + +#. i18n: ectx: label, entry (desktopfilerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:429 +#, kde-format +msgid "Desktop file name rule type" +msgstr "" + +#: scripting/genericscriptedconfig.cpp:70 +#, kde-format +msgctxt "Error message" +msgid "Plugin does not provide configuration file in expected location" +msgstr "" + +#: scripting/scripting.cpp:117 +#, kde-format +msgctxt "Assertion failed in KWin script with given value" +msgid "Assertion failed: %1 is not null" +msgstr "" + +#: scripting/scripting.cpp:135 +#, kde-format +msgctxt "Assertion failed in KWin script" +msgid "Assertion failed: argument is null" +msgstr "" + +#: scripting/scripting.cpp:177 +#, kde-format +msgctxt "Error in KWin Script" +msgid "" +"Invalid number of arguments. At least service, path, interface and method " +"need to be provided" +msgstr "" + +#: scripting/scripting.cpp:183 +#, kde-format +msgctxt "Error in KWin Script" +msgid "" +"Invalid type. Service, path, interface and method need to be string values" +msgstr "" + +#: scripting/scriptingutils.cpp:17 +#, kde-format +msgctxt "syntax error in KWin script" +msgid "Invalid number of arguments" +msgstr "" + +#: scripting/scriptingutils.cpp:30 +#, kde-format +msgctxt "KWin Scripting function received incorrect value for an expected type" +msgid "%1 is not a variant type" +msgstr "" + +#. i18n: ectx: property (windowTitle), widget (QDialog, ShortcutDialog) +#: shortcutdialog.ui:14 +#, kde-format +msgid "Dialog" +msgstr "" + +#. i18n: ectx: property (text), widget (QToolButton, clearButton) +#: shortcutdialog.ui:25 +#, kde-format +msgid "..." +msgstr "" + +#: tabbox/tabbox.cpp:372 +#, fuzzy, kde-format +#| msgid "Show Desktop Grid" +msgctxt "Special entry in alt+tab list for minimizing all windows" +msgid "Show Desktop" +msgstr "Desktop Grid দেখুৱাওক" + +#: tabbox/tabbox.cpp:521 +#, kde-format +msgid "Walk Through Windows" +msgstr "সংযোগক্ষেত্ৰসমূহৰ পৰিদৰ্শন কৰক" + +#: tabbox/tabbox.cpp:522 +#, kde-format +msgid "Walk Through Windows (Reverse)" +msgstr "সংযোগক্ষেত্ৰসমূহ পৰিদৰ্শন কৰক (বিপৰীত দিশে)" + +#: tabbox/tabbox.cpp:523 +#, fuzzy, kde-format +#| msgid "Walk Through Windows (Reverse)" +msgid "Walk Through Windows Alternative" +msgstr "সংযোগক্ষেত্ৰসমূহ পৰিদৰ্শন কৰক (বিপৰীত দিশে)" + +#: tabbox/tabbox.cpp:524 +#, fuzzy, kde-format +#| msgid "Walk Through Windows (Reverse)" +msgid "Walk Through Windows Alternative (Reverse)" +msgstr "সংযোগক্ষেত্ৰসমূহ পৰিদৰ্শন কৰক (বিপৰীত দিশে)" + +#: tabbox/tabbox.cpp:525 +#, fuzzy, kde-format +#| msgid "Walk Through Windows (Reverse)" +msgid "Walk Through Windows of Current Application" +msgstr "সংযোগক্ষেত্ৰসমূহ পৰিদৰ্শন কৰক (বিপৰীত দিশে)" + +#: tabbox/tabbox.cpp:526 +#, fuzzy, kde-format +#| msgid "Walk Through Windows (Reverse)" +msgid "Walk Through Windows of Current Application (Reverse)" +msgstr "সংযোগক্ষেত্ৰসমূহ পৰিদৰ্শন কৰক (বিপৰীত দিশে)" + +#: tabbox/tabbox.cpp:527 +#, fuzzy, kde-format +#| msgid "Walk Through Windows (Reverse)" +msgid "Walk Through Windows of Current Application Alternative" +msgstr "সংযোগক্ষেত্ৰসমূহ পৰিদৰ্শন কৰক (বিপৰীত দিশে)" + +#: tabbox/tabbox.cpp:528 +#, fuzzy, kde-format +#| msgid "Walk Through Windows (Reverse)" +msgid "Walk Through Windows of Current Application Alternative (Reverse)" +msgstr "সংযোগক্ষেত্ৰসমূহ পৰিদৰ্শন কৰক (বিপৰীত দিশে)" + +#: tabbox/tabbox.cpp:529 +#, kde-format +msgid "Walk Through Desktops" +msgstr "ডেস্কট'পসমূহ পৰিদৰ্শন কৰক" + +#: tabbox/tabbox.cpp:530 +#, kde-format +msgid "Walk Through Desktops (Reverse)" +msgstr "ডেষ্কট'পসমূহ পৰিদৰ্শন কৰক (বিপৰীত দিশে)" + +#: tabbox/tabbox.cpp:531 +#, kde-format +msgid "Walk Through Desktop List" +msgstr "ডেষ্কট'প তালিকা পৰিদৰ্শন কৰক" + +#: tabbox/tabbox.cpp:532 +#, kde-format +msgid "Walk Through Desktop List (Reverse)" +msgstr "ডেষ্কট'প তালিকা পৰিদৰ্শন কৰক (বিপৰীত দিশে)" + +#: tabbox/tabboxhandler.cpp:272 +#, kde-format +msgid "" +"The Window Switcher installation is broken, resources are missing.\n" +"Contact your distribution about this." +msgstr "" + +#: useractions.cpp:167 +#, kde-format +msgid "" +"You have selected to show a window without its border.\n" +"Without the border, you will not be able to enable the border again using " +"the mouse: use the window operations menu instead, activated using the %1 " +"keyboard shortcut." +msgstr "" + +#: useractions.cpp:175 +#, kde-format +msgid "" +"You have selected to show a window in fullscreen mode.\n" +"If the application itself does not have an option to turn the fullscreen " +"mode off you will not be able to disable it again using the mouse: use the " +"window operations menu instead, activated using the %1 keyboard shortcut." +msgstr "" + +#: useractions.cpp:240 +#, kde-format +msgid "&Move" +msgstr "লৰাওক (&M)" + +#: useractions.cpp:245 +#, fuzzy, kde-format +#| msgid "Re&size" +msgid "&Resize" +msgstr "মাপ পৰিবৰ্তন (&s)" + +#: useractions.cpp:250 +#, kde-format +msgid "Keep &Above Others" +msgstr "সকলো সংযোগক্ষেত্ৰৰ ওপৰত স্থাপন (&A)" + +#: useractions.cpp:256 +#, kde-format +msgid "Keep &Below Others" +msgstr "সকল সংযোগক্ষেত্ৰৰ তলত স্থাপন (&B)" + +#: useractions.cpp:262 +#, kde-format +msgid "&Fullscreen" +msgstr "সম্পূৰ্ণ পৰ্দ্দাত প্ৰদৰ্শন (&F)" + +#: useractions.cpp:268 +#, fuzzy, kde-format +#| msgid "Shade" +msgid "&Shade" +msgstr "ছায়াবৃত" + +#: useractions.cpp:274 +#, kde-format +msgid "&No Border" +msgstr "প্ৰান্ত নোহোৱা (&N)" + +#: useractions.cpp:282 +#, fuzzy, kde-format +#| msgid "Window &Shortcut..." +msgid "Set Window Short&cut..." +msgstr "সংযোগক্ষেত্ৰৰ শ'ৰ্ট কাট... (&S)" + +#: useractions.cpp:287 +#, fuzzy, kde-format +#| msgid "&Special Window Settings..." +msgid "Configure Special &Window Settings..." +msgstr "সংযোগক্ষেত্ৰৰ বিশেষ বৈশিষ্ট্য...(&S)" + +#: useractions.cpp:292 +#, fuzzy, kde-format +#| msgid "&Special Application Settings..." +msgid "Configure S&pecial Application Settings..." +msgstr "অনুপ্ৰয়োগ সংক্ৰান্ত বিশেষ বৈশিষ্ট্য...(&S)" + +#: useractions.cpp:300 +#, fuzzy, kde-format +#| msgid "KDE window manager" +msgctxt "" +"Entry in context menu of window decoration to open the configuration module " +"of KWin" +msgid "Configure W&indow Manager..." +msgstr "KDE সংযোগক্ষেত্ৰ পৰিচালন ব্যৱস্থা" + +#: useractions.cpp:328 +#, kde-format +msgid "Ma&ximize" +msgstr "ডাঙৰ কৰক (&x)" + +#: useractions.cpp:334 +#, kde-format +msgid "Mi&nimize" +msgstr "সৰু কৰক (&n)" + +#: useractions.cpp:340 +#, kde-format +msgid "&More Actions" +msgstr "" + +#: useractions.cpp:343 +#, kde-format +msgid "&Close" +msgstr "বন্ধ কৰক (&C)" + +#: useractions.cpp:410 +#, kde-format +msgid "&Extensions" +msgstr "" + +#: useractions.cpp:451 +#, fuzzy, kde-format +#| msgid "&All Desktops" +msgid "&Desktops" +msgstr "সকলো ডেষ্কট'পলৈ (&A)" + +#: useractions.cpp:465 +#, fuzzy, kde-format +#| msgid "To &Desktop" +msgid "Move to &Desktop" +msgstr "চিহ্নিত ডেষ্কট'পলৈ স্থানান্তৰ (&D)" + +#: useractions.cpp:483 +#, fuzzy, kde-format +#| msgid "To &Desktop" +msgid "Move to &Screen" +msgstr "চিহ্নিত ডেষ্কট'পলৈ স্থানান্তৰ (&D)" + +#: useractions.cpp:499 +#, kde-format +msgid "Show in &Activities" +msgstr "" + +#: useractions.cpp:514 useractions.cpp:559 +#, kde-format +msgid "&All Desktops" +msgstr "সকলো ডেষ্কট'পলৈ (&A)" + +#: useractions.cpp:542 useractions.cpp:595 +#, fuzzy, kde-format +#| msgid "Show Desktop Grid" +msgctxt "Create a new desktop and move there the window" +msgid "&New Desktop" +msgstr "Desktop Grid দেখুৱাওক" + +#: useractions.cpp:618 +#, fuzzy, kde-format +#| msgid "Window to Screen 1" +msgctxt "" +"@item:inmenu List of all Screens to send a window to. First argument is a " +"number, second the output identifier. E.g. Screen 1 (HDMI1)" +msgid "Screen &%1 (%2)" +msgstr "পৰ্দ্দা ১ লৈ সংযোগক্ষেত্ৰ স্থানান্তৰ কৰক" + +#: useractions.cpp:641 +#, kde-format +msgid "&All Activities" +msgstr "" + +#: useractions.cpp:887 +#, kde-format +msgctxt "'%1' is a keyboard shortcut like 'ctrl+w'" +msgid "%1 is already in use" +msgstr "" + +#: useractions.cpp:889 +#, kde-format +msgctxt "keyboard shortcut '%1' is used by action '%2' in application '%3'" +msgid "%1 is used by %2 in %3" +msgstr "" + +#: useractions.cpp:1021 +#, kde-format +msgid "Activate Window (%1)" +msgstr "সংযোগক্ষেত্ৰ সক্ৰিয় কৰক (%1)" + +#: useractions.cpp:1163 +#, kde-format +msgid "" +"The window manager is configured to consider the screen with the mouse on it " +"as active one.\n" +"Therefore it is not possible to switch to a screen explicitly." +msgstr "" + +#: virtualdesktops.cpp:698 virtualdesktops.cpp:767 +#, kde-format +msgid "Desktop %1" +msgstr "ডেস্কট'প %1" + +#: virtualdesktops.cpp:802 +#, kde-format +msgid "Switch to Next Desktop" +msgstr "পৰবৰ্তী ডেষ্কট'পলৈ পৰিবৰ্তন কৰক" + +#: virtualdesktops.cpp:804 +#, kde-format +msgid "Switch to Previous Desktop" +msgstr "পূৰ্ববৰ্তী ডেষ্কট'পলৈ পৰিবৰ্তন কৰক" + +#: virtualdesktops.cpp:806 +#, kde-format +msgid "Switch One Desktop to the Right" +msgstr "সোঁফালৰ ডেষ্কট'পলৈ পৰিবৰ্তন কৰক" + +#: virtualdesktops.cpp:808 +#, kde-format +msgid "Switch One Desktop to the Left" +msgstr "বাওঁফালৰ ডেষ্কট'পলৈ পৰিবৰ্তন কৰক" + +#: virtualdesktops.cpp:810 +#, kde-format +msgid "Switch One Desktop Up" +msgstr "উপৰৰ ডেষ্কট'পলৈ পৰিবৰ্তন কৰক" + +#: virtualdesktops.cpp:812 +#, kde-format +msgid "Switch One Desktop Down" +msgstr "তলৰ ডেষ্কট'পলৈ পৰিবৰ্তন কৰক" + +#: virtualdesktops.cpp:825 +#, fuzzy, kde-format +#| msgid "Switch to Desktop 1" +msgid "Switch to Desktop %1" +msgstr "ডেষ্কট'প ১ লৈ পৰিবৰ্তন কৰক" + +#: virtualkeyboard.cpp:84 +#, kde-format +msgid "Virtual Keyboard" +msgstr "" + +#: virtualkeyboard.cpp:350 +#, kde-format +msgid "Virtual Keyboard: enabled" +msgstr "" + +#: virtualkeyboard.cpp:353 +#, kde-format +msgid "Virtual Keyboard: disabled" +msgstr "" + +#: virtualkeyboard.cpp:355 +#, kde-format +msgid "Whether to show the virtual keyboard on demand." +msgstr "" + +#: workspace.cpp:1363 +#, kde-format +msgctxt "Introductory text shown in the support information." +msgid "" +"KWin Support Information:\n" +"The following information should be used when requesting support on e.g. " +"https://forum.kde.org.\n" +"It provides information about the currently running instance, which options " +"are used,\n" +"what OpenGL driver and which effects are running.\n" +"Please post the information provided underneath this introductory text to a " +"paste bin service\n" +"like https://paste.kde.org instead of pasting into support threads.\n" +msgstr "" \ No newline at end of file diff --git a/po/ast/kcm-kwin-scripts.po b/po/ast/kcm-kwin-scripts.po new file mode 100644 index 0000000..369c936 --- /dev/null +++ b/po/ast/kcm-kwin-scripts.po @@ -0,0 +1,92 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the kwin package. +# +# enolp , 2019. +msgid "" +msgstr "" +"Project-Id-Version: kwin\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-09-01 02:29+0200\n" +"PO-Revision-Date: 2019-10-22 02:06+0200\n" +"Last-Translator: enolp \n" +"Language-Team: Asturian\n" +"Language: ast\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 19.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" + +#: module.cpp:38 +#, kde-format +msgid "KWin Scripts" +msgstr "Scripts pa KWin" + +#: module.cpp:40 +#, kde-format +msgid "Configure KWin scripts" +msgstr "Configura los scripts pa KWin" + +#: module.cpp:43 +#, kde-format +msgid "Tamás Krutki" +msgstr "Tamás Krutki" + +#: module.cpp:79 +#, kde-format +msgid "Error when uninstalling KWin Script: %1" +msgstr "" + +#: module.cpp:100 +#, kde-format +msgid "Import KWin Script" +msgstr "Importación d'un script pa KWin" + +#: module.cpp:101 +#, kde-format +msgid "*.kwinscript|KWin scripts (*.kwinscript)" +msgstr "*.kwinscript|Scripts pa KWin (*.kwinscript)" + +#: module.cpp:120 +#, kde-format +msgctxt "Placeholder is error message returned from the install service" +msgid "" +"Cannot import selected script.\n" +"%1" +msgstr "" +"Nun pue importase'l script esbilláu.\n" +"%1" + +#: module.cpp:134 +#, kde-format +msgctxt "Placeholder is name of the script that was imported" +msgid "The script \"%1\" was successfully imported." +msgstr "El scripts «%1» importóse con ésitu." + +#. i18n: ectx: property (windowTitle), widget (QWidget, Module) +#: module.ui:14 +#, kde-format +msgid "KWin script configuration" +msgstr "Configuración del script pa KWin" + +#. i18n: ectx: property (text), widget (QPushButton, importScriptButton) +#: module.ui:55 +#, kde-format +msgid "Install from File..." +msgstr "Instalar dende un ficheru…" + +#. i18n: ectx: property (text), widget (KNS3::Button, ghnsButton) +#: module.ui:67 +#, kde-format +msgid "Get New Scripts..." +msgstr "Consiguir scripts nuevos…" \ No newline at end of file diff --git a/po/ast/kcm_kwin_effects.po b/po/ast/kcm_kwin_effects.po new file mode 100644 index 0000000..4007377 --- /dev/null +++ b/po/ast/kcm_kwin_effects.po @@ -0,0 +1,98 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the kwin package. +# +# enolp , 2019, 2020. +msgid "" +msgstr "" +"Project-Id-Version: kwin\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-08-08 02:22+0200\n" +"PO-Revision-Date: 2020-02-19 16:11+0100\n" +"Last-Translator: enolp \n" +"Language-Team: Asturian \n" +"Language: ast\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 19.12.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" + +#: kcm.cpp:33 +#, kde-format +msgid "Desktop Effects" +msgstr "Efeutos pal escritoriu" + +#: kcm.cpp:38 +#, kde-format +msgid "Vlad Zahorodnii" +msgstr "Vlad Zahorodnii" + +#: kcm.cpp:77 +#, kde-format +msgid "Download New Desktop Effects" +msgstr "Descarga d'efeutos nuevos" + +#: package/contents/ui/Effect.qml:77 +#, kde-format +msgid "" +"Author: %1\n" +"License: %2" +msgstr "" +"Autor: %1\n" +"Llicencia: %2" + +#: package/contents/ui/Effect.qml:106 +#, kde-format +msgctxt "@info:tooltip" +msgid "Show/Hide Video" +msgstr "Amosar/anubrir el videu" + +#: package/contents/ui/Effect.qml:113 +#, kde-format +msgctxt "@info:tooltip" +msgid "Configure..." +msgstr "Configurar…" + +#: package/contents/ui/main.qml:18 +#, kde-format +msgid "This module lets you configure desktop effects." +msgstr "Esti módulu déxate configurar los efeutos pal escritoriu." + +#: package/contents/ui/main.qml:25 +#, kde-format +msgid "" +"Hint: To find out or configure how to activate an effect, look at the " +"effect's settings." +msgstr "" +"Pista: Pa descubrir o configurar cómo activar efeutos, mira los axutes de " +"los efeutos" + +#: package/contents/ui/main.qml:45 +#, kde-format +msgid "Configure Filter" +msgstr "Configurar la peñera" + +#: package/contents/ui/main.qml:57 +#, kde-format +msgid "Exclude unsupported effects" +msgstr "Escluyir los efeutos que nun se sofiten" + +#: package/contents/ui/main.qml:65 +#, kde-format +msgid "Exclude internal effects" +msgstr "Escluyir los efeutos internos" + +#: package/contents/ui/main.qml:124 +#, kde-format +msgid "Get New Desktop Effects..." +msgstr "Consiguir efeutos nuevos…" \ No newline at end of file diff --git a/po/ast/kcm_kwin_virtualdesktops.po b/po/ast/kcm_kwin_virtualdesktops.po new file mode 100644 index 0000000..1cb8494 --- /dev/null +++ b/po/ast/kcm_kwin_virtualdesktops.po @@ -0,0 +1,131 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the kwin package. +# +# enolp , 2019. +msgid "" +msgstr "" +"Project-Id-Version: kwin\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-08-08 02:22+0200\n" +"PO-Revision-Date: 2019-08-10 20:40+0200\n" +"Last-Translator: enolp \n" +"Language-Team: Asturian \n" +"Language: ast\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 19.04.3\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" + +#: desktopsmodel.cpp:455 +#, kde-format +msgid "There was an error connecting to the compositor." +msgstr "Hebo un fallu coneutando col compositor." + +#: desktopsmodel.cpp:657 +#, kde-format +msgid "There was an error saving the settings to the compositor." +msgstr "Hebo un fallu al guardar los axustes nel compositor." + +#: desktopsmodel.cpp:660 +#, kde-format +msgid "There was an error requesting information from the compositor." +msgstr "" + +#: package/contents/ui/main.qml:18 +#, kde-format +msgid "" +"This module lets you configure the navigation, number and layout of virtual " +"desktops." +msgstr "" +"Esti módulu déxate configurar la navegación, distribución y el númberu " +"d'escritorios virtuales" + +#: package/contents/ui/main.qml:68 +#, kde-format +msgctxt "@info:tooltip" +msgid "Rename" +msgstr "Renomar" + +#: package/contents/ui/main.qml:78 +#, kde-format +msgctxt "@info:tooltip" +msgid "Remove" +msgstr "Desaniciar" + +#: package/contents/ui/main.qml:104 +#, kde-format +msgid "" +"Virtual desktops have been changed outside this settings application. Saving " +"now will overwrite the changes." +msgstr "" +"Los escritorios virtuales camudaron fuera d'esta aplicación d'axustes. " +"Agora'l guardáu va sobrescribir los cambeos." + +#: package/contents/ui/main.qml:118 +#, kde-format +msgid "Row %1" +msgstr "Filera %1" + +#: package/contents/ui/main.qml:131 +#, kde-format +msgctxt "@action:button" +msgid "Add" +msgstr "Amestar" + +#: package/contents/ui/main.qml:134 +#, kde-format +msgid "New Desktop" +msgstr "Escritoriu nuevu" + +#: package/contents/ui/main.qml:148 +#, kde-format +msgid "1 Row" +msgid_plural "%1 Rows" +msgstr[0] "1 filera" +msgstr[1] "%1 fileres" + +#: package/contents/ui/main.qml:160 +#, kde-format +msgid "Options:" +msgstr "Opciones:" + +#: package/contents/ui/main.qml:162 +#, kde-format +msgid "Navigation wraps around" +msgstr "" + +#: package/contents/ui/main.qml:177 +#, kde-format +msgid "Show animation when switching:" +msgstr "Amosar una animación al cambiar:" + +#: package/contents/ui/main.qml:220 +#, kde-format +msgid "Show on-screen display when switching:" +msgstr "Amosar un indicador na pantalla al cambiar:" + +#: package/contents/ui/main.qml:238 +#, kde-format +msgid "%1 ms" +msgstr "%1 ms" + +#: package/contents/ui/main.qml:256 +#, kde-format +msgid "Show desktop layout indicators" +msgstr "Amosar la distribución de los escritorios" + +#: virtualdesktops.cpp:30 +#, kde-format +msgid "Virtual Desktops" +msgstr "Escritorios virtuales" \ No newline at end of file diff --git a/po/ast/kcmkwincompositing.po b/po/ast/kcmkwincompositing.po new file mode 100644 index 0000000..605fb9c --- /dev/null +++ b/po/ast/kcmkwincompositing.po @@ -0,0 +1,221 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the kwin package. +# +# enolp , 2019, 2020. +msgid "" +msgstr "" +"Project-Id-Version: kwin\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-08-08 02:22+0200\n" +"PO-Revision-Date: 2020-02-25 16:25+0100\n" +"Last-Translator: enolp \n" +"Language-Team: Asturian \n" +"Language: ast\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 19.12.2\n" + +#. i18n: ectx: property (text), widget (KMessageWidget, glCrashedWarning) +#: compositing.ui:32 +#, kde-format +msgid "" +"OpenGL compositing (the default) has crashed KWin in the past.\n" +"This was most likely due to a driver bug.\n" +"If you think that you have meanwhile upgraded to a stable driver,\n" +"you can reset this protection but be aware that this might result in an " +"immediate crash!\n" +"Alternatively, you might want to use the XRender backend instead." +msgstr "" + +#. i18n: ectx: property (text), widget (KMessageWidget, scaleWarning) +#: compositing.ui:45 +#, kde-format +msgid "" +"Scale method \"Accurate\" is not supported by all hardware and can cause " +"performance regressions and rendering artifacts." +msgstr "" +"El métodu d'escaláu «Precisu» nun lu sofita tol hardware y pue causar " +"regresiones nel rindimientu y fallos de renderizáu" + +#. i18n: ectx: property (text), widget (KMessageWidget, windowThumbnailWarning) +#: compositing.ui:68 +#, kde-format +msgid "" +"Keeping the window thumbnail always interferes with the minimized state of " +"windows. This can result in windows not suspending their work when minimized." +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Enabled) +#: compositing.ui:80 +#, kde-format +msgid "Enable compositor on startup" +msgstr "Activar el compositor nel aniciu" + +#. i18n: ectx: property (text), widget (QLabel, animationSpeedLabel) +#: compositing.ui:87 +#, kde-format +msgid "Animation speed:" +msgstr "Velocidá d'animación:" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: compositing.ui:124 +#, kde-format +msgid "Very slow" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: compositing.ui:144 +#, kde-format +msgid "Instant" +msgstr "Nel intre" + +#. i18n: ectx: property (text), widget (QLabel, scaleMethodLabel) +#: compositing.ui:156 +#, kde-format +msgid "Scale method:" +msgstr "Métodu d'escaláu:" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_XRenderSmoothScale) +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glTextureFilter) +#: compositing.ui:166 compositing.ui:180 +#, kde-format +msgid "Crisp" +msgstr "Remarcáu" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_XRenderSmoothScale) +#: compositing.ui:171 +#, kde-format +msgid "Smooth (slower)" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glTextureFilter) +#: compositing.ui:185 +#, kde-format +msgid "Smooth" +msgstr "Suave" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glTextureFilter) +#: compositing.ui:190 +#, kde-format +msgid "Accurate" +msgstr "Precisu" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: compositing.ui:207 +#, kde-format +msgid "Rendering backend:" +msgstr "Backend de renderización:" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: compositing.ui:224 +#, kde-format +msgid "Tearing prevention (\"vsync\"):" +msgstr "Sincronización vertical:" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glPreferBufferSwap) +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_HiddenPreviews) +#: compositing.ui:232 compositing.ui:268 +#, kde-format +msgid "Never" +msgstr "Enxamás" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glPreferBufferSwap) +#: compositing.ui:237 +#, kde-format +msgid "Automatic" +msgstr "Automática" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glPreferBufferSwap) +#: compositing.ui:242 +#, kde-format +msgid "Only when cheap" +msgstr "Namás cuando seya económico" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glPreferBufferSwap) +#: compositing.ui:247 +#, kde-format +msgid "Full screen repaints" +msgstr "Repintar tola pantalla" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glPreferBufferSwap) +#: compositing.ui:252 +#, kde-format +msgid "Re-use screen content" +msgstr "Reusar el conteníu de la pantalla" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: compositing.ui:260 +#, kde-format +msgid "Keep window thumbnails:" +msgstr "Caltener les miniatures de les ventanes:" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_HiddenPreviews) +#: compositing.ui:273 +#, kde-format +msgid "Only for Shown Windows" +msgstr "Namás pa les ventanes que s'amuesen" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_HiddenPreviews) +#: compositing.ui:278 +#, kde-format +msgid "Always" +msgstr "Siempres" + +#. i18n: ectx: property (toolTip), widget (QCheckBox, kcfg_WindowsBlockCompositing) +#: compositing.ui:288 +#, kde-format +msgid "" +"Applications can set a hint to block compositing when the window is open.\n" +" This brings performance improvements for e.g. games.\n" +" The setting can be overruled by window-specific rules." +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_WindowsBlockCompositing) +#: compositing.ui:291 +#, kde-format +msgid "Allow applications to block compositing" +msgstr "Permitir que les aplicaciones bloquien la composición" + +#: main.cpp:80 +#, kde-format +msgid "Re-enable OpenGL detection" +msgstr "" + +#: main.cpp:132 +#, kde-format +msgid "" +"\"Only when cheap\" only prevents tearing for full screen changes like a " +"video." +msgstr "" +"«Namás cuando seya económico» Namás evita'l llariméu en cambeos a pantalla " +"completa como nun videu." + +#: main.cpp:136 +#, kde-format +msgid "\"Full screen repaints\" can cause performance problems." +msgstr "«Repintar tola pantalla» pue causar problemes de rindimientu." + +#: main.cpp:140 +#, kde-format +msgid "" +"\"Re-use screen content\" causes severe performance problems on MESA drivers." +msgstr "" +"«Reusar el conteníu de la pantalla» causa problemes de rindimientu graves en " +"controladores de MESA." + +#: main.cpp:160 +#, kde-format +msgid "OpenGL 3.1" +msgstr "OpenGL 3.1" + +#: main.cpp:161 +#, kde-format +msgid "OpenGL 2.0" +msgstr "OpenGL 2.0" + +#: main.cpp:162 +#, kde-format +msgid "XRender" +msgstr "XRender" \ No newline at end of file diff --git a/po/ast/kwin_effects.po b/po/ast/kwin_effects.po new file mode 100644 index 0000000..8908267 --- /dev/null +++ b/po/ast/kwin_effects.po @@ -0,0 +1,2133 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the kwin package. +# +# enolp , 2020. +msgid "" +msgstr "" +"Project-Id-Version: kwin\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-10-23 08:49+0200\n" +"PO-Revision-Date: 2020-02-25 00:36+0100\n" +"Last-Translator: enolp \n" +"Language-Team: Asturian \n" +"Language: ast\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 19.12.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" + +#. i18n: ectx: property (text), widget (QLabel, labelConstantBlurDescription) +#: blur/blur_config.ui:17 +#, kde-format +msgid "Blur strength:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, labelConstantBlurLight) +#. i18n: ectx: property (text), widget (QLabel, labelConstantNoiseLight) +#: blur/blur_config.ui:42 blur/blur_config.ui:108 +#, kde-format +msgid "Light" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, labelConstantBlurStrong) +#. i18n: ectx: property (text), widget (QLabel, labelConstantNoiseStrong) +#: blur/blur_config.ui:74 blur/blur_config.ui:137 +#, kde-format +msgid "Strong" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, labelConstantNoiseDescription) +#: blur/blur_config.ui:83 +#, kde-format +msgid "Noise strength:" +msgstr "Fuercia del ruíu:" + +#: colorpicker/colorpicker.cpp:107 +#, kde-format +msgid "" +"Select a position for color picking with left click or enter.\n" +"Escape or right click to cancel." +msgstr "" + +#: coverswitch/coverswitch.cpp:945 flipswitch/flipswitch.cpp:925 +#, kde-format +msgctxt "Special entry in alt+tab list for minimizing all windows" +msgid "Show Desktop" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DrawWindowCaptions) +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_WindowTitle) +#: coverswitch/coverswitch_config.ui:17 flipswitch/flipswitch_config.ui:191 +#: presentwindows/presentwindows_config.ui:406 +#, kde-format +msgid "Display window &titles" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: coverswitch/coverswitch_config.ui:29 cube/cube_config.ui:344 +#, kde-format +msgid "Zoom" +msgstr "Zoom" + +#. i18n: ectx: property (toolTip), widget (QSlider, kcfg_zPosition) +#: coverswitch/coverswitch_config.ui:39 +#, kde-format +msgid "Define how far away the windows should appear" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_8) +#: coverswitch/coverswitch_config.ui:66 cube/cube_config.ui:350 +#, kde-format +msgid "Near" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_9) +#: coverswitch/coverswitch_config.ui:86 cube/cube_config.ui:357 +#, kde-format +msgid "Far" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, tab) +#: coverswitch/coverswitch_config.ui:110 +#, kde-format +msgid "Animation" +msgstr "Animación" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AnimateSwitch) +#: coverswitch/coverswitch_config.ui:116 +#, kde-format +msgid "Animate switch" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AnimateStart) +#: coverswitch/coverswitch_config.ui:123 +#, kde-format +msgid "Animation on tab box open" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AnimateStop) +#: coverswitch/coverswitch_config.ui:130 +#, kde-format +msgid "Animation on tab box close" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: coverswitch/coverswitch_config.ui:139 magiclamp/magiclamp_config.ui:17 +#, kde-format +msgid "Animation duration:" +msgstr "Duración de l'animación:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_RotationDuration) +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_Duration) +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_AnimationDuration) +#: coverswitch/coverswitch_config.ui:158 cube/cube_config.ui:149 +#: cubeslide/cubeslide_config.ui:49 magiclamp/magiclamp_config.ui:36 +#, kde-format +msgctxt "Duration of rotation" +msgid "Default" +msgstr "Por defeutu" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_Duration) +#: coverswitch/coverswitch_config.ui:161 glide/glide_config.ui:35 +#: scale/package/contents/ui/config.ui:33 slide/slide_config.ui:35 +#, kde-format +msgid " milliseconds" +msgstr " milisegundos" + +#. i18n: ectx: attribute (title), widget (QWidget, tab_3) +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Reflection) +#: coverswitch/coverswitch_config.ui:177 coverswitch/coverswitch_config.ui:183 +#, kde-format +msgid "Reflections" +msgstr "Reflexos" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: coverswitch/coverswitch_config.ui:195 +#, kde-format +msgid "Rear color" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: coverswitch/coverswitch_config.ui:205 +#, kde-format +msgid "Front color" +msgstr "" + +#: cube/cube.cpp:186 cube/cube_config.cpp:60 +#, kde-format +msgid "Desktop Cube" +msgstr "" + +#: cube/cube.cpp:194 cube/cube_config.cpp:65 +#, kde-format +msgid "Desktop Cylinder" +msgstr "" + +#: cube/cube.cpp:200 cube/cube_config.cpp:69 +#, kde-format +msgid "Desktop Sphere" +msgstr "" + +#: cube/cube_config.cpp:49 +#, kde-format +msgctxt "@title:tab Basic Settings" +msgid "Basic" +msgstr "" + +#: cube/cube_config.cpp:50 +#, kde-format +msgctxt "@title:tab Advanced Settings" +msgid "Advanced" +msgstr "" + +#: cube/cube_config.cpp:54 desktopgrid/desktopgrid_config.cpp:52 +#: flipswitch/flipswitch_config.cpp:56 invert/invert_config.cpp:38 +#: lookingglass/lookingglass_config.cpp:58 magnifier/magnifier_config.cpp:58 +#: mouseclick/mouseclick_config.cpp:50 mousemark/mousemark_config.cpp:56 +#: presentwindows/presentwindows_config.cpp:51 +#: showpaint/showpaint_config.cpp:36 +#: thumbnailaside/thumbnailaside_config.cpp:57 +#: trackmouse/trackmouse_config.cpp:54 +#: windowgeometry/windowgeometry_config.cpp:45 zoom/zoom_config.cpp:59 +#, kde-format +msgid "KWin" +msgstr "KWin" + +#. i18n: ectx: attribute (title), widget (QWidget, tab) +#: cube/cube_config.ui:21 +#, kde-format +msgid "Tab 1" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_5) +#: cube/cube_config.ui:27 +#, kde-format +msgid "Background" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: cube/cube_config.ui:33 +#, kde-format +msgid "Background color:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: cube/cube_config.ui:56 +#, kde-format +msgid "Wallpaper:" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_8) +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#: cube/cube_config.ui:82 desktopgrid/desktopgrid_config.ui:207 +#: flipswitch/flipswitch_config.ui:204 +#: presentwindows/presentwindows_config.ui:17 +#, kde-format +msgid "Activation" +msgstr "Activación" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_7) +#. i18n: ectx: property (title), widget (QGroupBox, groupBox) +#: cube/cube_config.ui:104 desktopgrid/desktopgrid_config.ui:17 +#: flipswitch/flipswitch_config.ui:17 mousemark/mousemark_config.ui:17 +#: presentwindows/presentwindows_config.ui:387 +#: thumbnailaside/thumbnailaside_config.ui:17 +#, kde-format +msgid "Appearance" +msgstr "Aspeutu" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DisplayDesktopName) +#: cube/cube_config.ui:110 +#, kde-format +msgid "Display desktop name" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Reflection) +#: cube/cube_config.ui:117 +#, kde-format +msgid "Reflection" +msgstr "Reflexu" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: cube/cube_config.ui:124 cubeslide/cubeslide_config.ui:72 +#, kde-format +msgid "Rotation duration:" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ZOrdering) +#: cube/cube_config.ui:175 +#, kde-format +msgid "Windows hover above cube" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_4) +#: cube/cube_config.ui:185 +#, kde-format +msgid "Opacity" +msgstr "Opacidá" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_OpacitySpin) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_Opacity) +#: cube/cube_config.ui:225 thumbnailaside/thumbnailaside_config.ui:87 +#, no-c-format, kde-format +msgid " %" +msgstr " %" + +#. i18n: ectx: property (text), widget (QLabel, label_4) +#. i18n: ectx: property (text), widget (QLabel, label_2) +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: cube/cube_config.ui:238 translucency/package/contents/ui/config.ui:156 +#: translucency/package/contents/ui/config.ui:418 +#, kde-format +msgid "Transparent" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#. i18n: ectx: property (text), widget (QLabel, label_3) +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: cube/cube_config.ui:245 translucency/package/contents/ui/config.ui:121 +#: translucency/package/contents/ui/config.ui:431 +#, kde-format +msgid "Opaque" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_OpacityDesktopOnly) +#: cube/cube_config.ui:255 +#, kde-format +msgid "Do not change opacity of windows" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, tab_2) +#: cube/cube_config.ui:279 +#, kde-format +msgid "Tab 2" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_3) +#: cube/cube_config.ui:285 +#, kde-format +msgid "Caps" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Caps) +#: cube/cube_config.ui:291 +#, kde-format +msgid "Show caps" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, capColorLabel) +#: cube/cube_config.ui:298 +#, kde-format +msgid "Cap color:" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_TexturedCaps) +#: cube/cube_config.ui:321 +#, kde-format +msgid "Display image on caps" +msgstr "" + +#. i18n: ectx: property (toolTip), widget (QSlider, kcfg_ZPosition) +#: cube/cube_config.ui:367 +#, kde-format +msgid "Define how far away the object should appear" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_9) +#: cube/cube_config.ui:408 +#, kde-format +msgid "Additional Options" +msgstr "Opciones adicionales" + +#. i18n: ectx: property (toolTip), widget (QCheckBox, kcfg_CloseOnMouseRelease) +#: cube/cube_config.ui:415 +#, kde-format +msgid "" +"If enabled the effect will be deactivated after rotating the cube with the " +"mouse,\n" +"otherwise it will remain active" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_CloseOnMouseRelease) +#: cube/cube_config.ui:418 +#, kde-format +msgid "Close after mouse dragging" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_TabBox) +#: cube/cube_config.ui:425 +#, kde-format +msgid "Use this effect for walking through the desktops" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_InvertKeys) +#: cube/cube_config.ui:432 +#, kde-format +msgid "Invert cursor keys" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_InvertMouse) +#: cube/cube_config.ui:439 +#, kde-format +msgid "Invert mouse" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, capDeformationGroupBox) +#: cube/cube_config.ui:449 +#, kde-format +msgid "Sphere Cap Deformation" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, capDeformationSphereLabel) +#: cube/cube_config.ui:471 +#, kde-format +msgid "Sphere" +msgstr "Esfera" + +#. i18n: ectx: property (text), widget (QLabel, capDeformationPlaneLabel) +#: cube/cube_config.ui:478 +#, kde-format +msgid "Plane" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DontSlideStickyWindows) +#: cubeslide/cubeslide_config.ui:17 +#, kde-format +msgid "Do not animate windows on all desktops" +msgstr "" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_RingLife) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_RotationDuration) +#: cubeslide/cubeslide_config.ui:52 mouseclick/mouseclick_config.ui:132 +#, kde-format +msgid " msec" +msgstr " ms" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DontSlidePanels) +#: cubeslide/cubeslide_config.ui:65 +#, kde-format +msgid "Do not animate panels" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_UsePagerLayout) +#: cubeslide/cubeslide_config.ui:85 +#, kde-format +msgid "Use pager layout for animation" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_UseWindowMoving) +#: cubeslide/cubeslide_config.ui:92 +#, kde-format +msgid "Start animation when moving windows towards screen edges" +msgstr "" + +#: desktopgrid/desktopgrid.cpp:65 desktopgrid/desktopgrid_config.cpp:57 +#, kde-format +msgid "Show Desktop Grid" +msgstr "" + +#: desktopgrid/desktopgrid_config.cpp:65 +#, kde-format +msgctxt "Desktop name alignment:" +msgid "Disabled" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_InRotationEdge) +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_OutRotationEdge) +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: desktopgrid/desktopgrid_config.cpp:66 flipswitch/flipswitch_config.ui:160 +#: glide/glide_config.ui:70 glide/glide_config.ui:168 +#, kde-format +msgid "Top" +msgstr "" + +#: desktopgrid/desktopgrid_config.cpp:67 +#, kde-format +msgid "Top-Right" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_InRotationEdge) +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_OutRotationEdge) +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: desktopgrid/desktopgrid_config.cpp:68 flipswitch/flipswitch_config.ui:125 +#: glide/glide_config.ui:75 glide/glide_config.ui:173 +#, kde-format +msgid "Right" +msgstr "" + +#: desktopgrid/desktopgrid_config.cpp:69 +#, kde-format +msgid "Bottom-Right" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_InRotationEdge) +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_OutRotationEdge) +#. i18n: ectx: property (text), widget (QLabel, label_8) +#: desktopgrid/desktopgrid_config.cpp:70 flipswitch/flipswitch_config.ui:180 +#: glide/glide_config.ui:80 glide/glide_config.ui:178 +#, kde-format +msgid "Bottom" +msgstr "" + +#: desktopgrid/desktopgrid_config.cpp:71 +#, kde-format +msgid "Bottom-Left" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_InRotationEdge) +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_OutRotationEdge) +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: desktopgrid/desktopgrid_config.cpp:72 flipswitch/flipswitch_config.ui:105 +#: glide/glide_config.ui:85 glide/glide_config.ui:183 +#, kde-format +msgid "Left" +msgstr "" + +#: desktopgrid/desktopgrid_config.cpp:73 +#, kde-format +msgid "Top-Left" +msgstr "" + +#: desktopgrid/desktopgrid_config.cpp:74 +#, kde-format +msgid "Center" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: desktopgrid/desktopgrid_config.ui:23 +#, kde-format +msgid "Zoom &duration:" +msgstr "" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_ZoomDuration) +#: desktopgrid/desktopgrid_config.ui:42 +#, kde-format +msgctxt "Duration of zoom" +msgid "Default" +msgstr "Por defeutu" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: desktopgrid/desktopgrid_config.ui:55 +#, kde-format +msgid "Border wid&th:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: desktopgrid/desktopgrid_config.ui:84 +#, kde-format +msgid "Desktop &name alignment:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: desktopgrid/desktopgrid_config.ui:107 +#, kde-format +msgid "&Layout mode:" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LayoutMode) +#: desktopgrid/desktopgrid_config.ui:127 +#, kde-format +msgid "Pager" +msgstr "Paxinador" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LayoutMode) +#: desktopgrid/desktopgrid_config.ui:132 +#, kde-format +msgid "Automatic" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LayoutMode) +#: desktopgrid/desktopgrid_config.ui:137 +#, kde-format +msgid "Custom" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, layoutRowsLabel) +#: desktopgrid/desktopgrid_config.ui:145 +#, kde-format +msgid "N&umber of rows:" +msgstr "N&úmberu de fileres:" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_PresentWindows) +#: desktopgrid/desktopgrid_config.ui:190 +#, kde-format +msgid "Use Present Windows effect to layout the windows" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ShowAddRemove) +#: desktopgrid/desktopgrid_config.ui:197 +#, kde-format +msgid "Show buttons to alter count of virtual desktops" +msgstr "Amuesa botones p'alteriar el númberu d'escritorios virtuales" + +#. i18n: ectx: property (text), widget (QLabel, label_Strength) +#: diminactive/diminactive_config.ui:17 +#, kde-format +msgid "Strength:" +msgstr "Fuercia:" + +#. i18n: ectx: property (text), widget (QLabel, label_Dim) +#: diminactive/diminactive_config.ui:40 +#, kde-format +msgid "Dim:" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DimPanels) +#: diminactive/diminactive_config.ui:47 +#, kde-format +msgid "Docks and panels" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DimDesktop) +#: diminactive/diminactive_config.ui:54 +#, kde-format +msgid "Desktop" +msgstr "Escritoriu" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DimKeepAbove) +#: diminactive/diminactive_config.ui:61 +#, kde-format +msgid "Keep above windows" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DimByGroup) +#: diminactive/diminactive_config.ui:68 +#, kde-format +msgid "By window group" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DimFullScreen) +#: diminactive/diminactive_config.ui:75 +#, kde-format +msgid "Fullscreen windows" +msgstr "" + +#: effect_builtins.cpp:91 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Blur" +msgstr "" + +#: effect_builtins.cpp:92 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Blurs the background behind semi-transparent windows" +msgstr "" + +#: effect_builtins.cpp:106 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Color Picker" +msgstr "" + +#: effect_builtins.cpp:107 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Supports picking a color" +msgstr "Sofita la escoyeta d'un color" + +#: effect_builtins.cpp:121 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Background contrast" +msgstr "Contraste del fondu" + +#: effect_builtins.cpp:122 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Improve contrast and readability behind semi-transparent windows" +msgstr "" +"Ameyora'l contraste y la llexibilidá darrera de les ventanes semitresparentes" + +#: effect_builtins.cpp:136 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Cover Switch" +msgstr "" + +#: effect_builtins.cpp:137 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Display a Cover Flow effect for the alt+tab window switcher" +msgstr "" + +#: effect_builtins.cpp:151 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Desktop Cube" +msgstr "" + +#: effect_builtins.cpp:152 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Display each virtual desktop on a side of a cube" +msgstr "" + +#: effect_builtins.cpp:166 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Desktop Cube Animation" +msgstr "" + +#: effect_builtins.cpp:167 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Animate desktop switching with a cube" +msgstr "Anima'l cambéu d'escritorios con un cubu" + +#: effect_builtins.cpp:181 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Desktop Grid" +msgstr "" + +#: effect_builtins.cpp:182 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Zoom out so all desktops are displayed side-by-side in a grid" +msgstr "" + +#: effect_builtins.cpp:196 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Dim Inactive" +msgstr "" + +#: effect_builtins.cpp:197 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Darken inactive windows" +msgstr "Escurez les ventanes inactives" + +#: effect_builtins.cpp:211 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Fall Apart" +msgstr "" + +#: effect_builtins.cpp:212 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Closed windows fall into pieces" +msgstr "" + +#: effect_builtins.cpp:226 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Flip Switch" +msgstr "" + +#: effect_builtins.cpp:227 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "" +"Flip through windows that are in a stack for the alt+tab window switcher" +msgstr "" + +#: effect_builtins.cpp:241 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Glide" +msgstr "" + +#: effect_builtins.cpp:242 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Glide windows as they appear or disappear" +msgstr "" + +#: effect_builtins.cpp:256 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Highlight Window" +msgstr "" + +#: effect_builtins.cpp:257 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Highlight the appropriate window when hovering over taskbar entries" +msgstr "" + +#: effect_builtins.cpp:271 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Invert" +msgstr "Inversión" + +#: effect_builtins.cpp:272 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Inverts the color of the desktop and windows" +msgstr "Invierte'l color del escritoriu y les ventanes" + +#: effect_builtins.cpp:286 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Kscreen" +msgstr "Kscreen" + +#: effect_builtins.cpp:287 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Helper Effect for KScreen" +msgstr "" + +#: effect_builtins.cpp:301 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Looking Glass" +msgstr "Güeyu de pexe" + +#: effect_builtins.cpp:302 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "A screen magnifier that looks like a fisheye lens" +msgstr "" + +#: effect_builtins.cpp:316 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Magic Lamp" +msgstr "Llámpara máxica" + +#: effect_builtins.cpp:317 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Simulate a magic lamp when minimizing windows" +msgstr "Simula una llámpara máxica al minimizar ventanes" + +#: effect_builtins.cpp:331 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Magnifier" +msgstr "" + +#: effect_builtins.cpp:332 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Magnify the section of the screen that is near the mouse cursor" +msgstr "Aumenta una seición de la pantalla que tea cierca del cursor" + +#: effect_builtins.cpp:346 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Mouse Click Animation" +msgstr "" + +#: effect_builtins.cpp:347 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "" +"Creates an animation whenever a mouse button is clicked. This is useful for " +"screenrecordings/presentations" +msgstr "" +"Crea una animación siempres que se calque un botón del mur. Esto ye útil pa " +"presentaciones o grabaciones de pantalla" + +#: effect_builtins.cpp:361 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Mouse Mark" +msgstr "" + +#: effect_builtins.cpp:362 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Allows you to draw lines on the desktop" +msgstr "Permítete dibuxar llinies nel escritoriu" + +#: effect_builtins.cpp:376 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Present Windows" +msgstr "Presentación de ventanes" + +#: effect_builtins.cpp:377 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Zoom out until all opened windows can be displayed side-by-side" +msgstr "" + +#: effect_builtins.cpp:391 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Resize Window" +msgstr "Redimensión de ventanes" + +#: effect_builtins.cpp:392 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Resizes windows with a fast texture scale instead of updating contents" +msgstr "" + +#: effect_builtins.cpp:406 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Screen Edge" +msgstr "" + +#: effect_builtins.cpp:407 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Highlights a screen edge when approaching" +msgstr "" + +#: effect_builtins.cpp:421 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Screenshot" +msgstr "" + +#: effect_builtins.cpp:422 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Helper effect for screenshot tools" +msgstr "" + +#: effect_builtins.cpp:436 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Sheet" +msgstr "" + +#: effect_builtins.cpp:437 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "" +"Make modal dialogs smoothly fly in and out when they are shown or hidden" +msgstr "" + +#: effect_builtins.cpp:451 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Show FPS" +msgstr "" + +#: effect_builtins.cpp:452 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Display KWin's performance in the corner of the screen" +msgstr "" + +#: effect_builtins.cpp:466 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Show Paint" +msgstr "" + +#: effect_builtins.cpp:467 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Highlight areas of the desktop that have been recently updated" +msgstr "Rescampla les árees del escritoriu que s'anovaren apocayá" + +#: effect_builtins.cpp:481 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Slide" +msgstr "" + +#: effect_builtins.cpp:482 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Slide desktops when switching virtual desktops" +msgstr "" + +#: effect_builtins.cpp:496 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Slide Back" +msgstr "" + +#: effect_builtins.cpp:497 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Slide back windows when another window is raised" +msgstr "" + +#: effect_builtins.cpp:511 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Sliding popups" +msgstr "" + +#: effect_builtins.cpp:512 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Sliding animation for Plasma popups" +msgstr "" + +#: effect_builtins.cpp:526 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Snap Helper" +msgstr "" + +#: effect_builtins.cpp:527 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Help you locate the center of the screen when moving a window" +msgstr "" + +#: effect_builtins.cpp:541 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Startup Feedback" +msgstr "" + +#: effect_builtins.cpp:542 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Helper effect for startup feedback" +msgstr "" + +#: effect_builtins.cpp:556 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Thumbnail Aside" +msgstr "" + +#: effect_builtins.cpp:557 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Display window thumbnails on the edge of the screen" +msgstr "" + +#: effect_builtins.cpp:571 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Touch Points" +msgstr "Puntos de toque" + +#: effect_builtins.cpp:572 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Visualize touch points" +msgstr "Visualiza los puntos de toque" + +#: effect_builtins.cpp:586 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Track Mouse" +msgstr "" + +#: effect_builtins.cpp:587 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Display a mouse cursor locating effect when activated" +msgstr "" + +#: effect_builtins.cpp:601 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Window Geometry" +msgstr "" + +#: effect_builtins.cpp:602 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Display window geometries on move/resize" +msgstr "" + +#: effect_builtins.cpp:616 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Wobbly Windows" +msgstr "Ventanes cimblantes" + +#: effect_builtins.cpp:617 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Deform windows while they are moving" +msgstr "Deforma les ventanes mentanto se mueven" + +#: effect_builtins.cpp:631 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Zoom" +msgstr "Zoom" + +#: effect_builtins.cpp:632 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Magnify the entire desktop" +msgstr "Aumenta tol escritoriu" + +#: flipswitch/flipswitch.cpp:48 flipswitch/flipswitch_config.cpp:50 +#, kde-format +msgid "Toggle Flip Switch (Current desktop)" +msgstr "" + +#: flipswitch/flipswitch.cpp:55 flipswitch/flipswitch_config.cpp:53 +#, kde-format +msgid "Toggle Flip Switch (All desktops)" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: flipswitch/flipswitch_config.ui:23 +#, kde-format +msgid "Flip animation duration:" +msgstr "" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_Duration) +#: flipswitch/flipswitch_config.ui:42 +#, kde-format +msgctxt "Duration of flip animation" +msgid "Default" +msgstr "Por defeutu" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: flipswitch/flipswitch_config.ui:55 +#, kde-format +msgid "Angle:" +msgstr "Ángulu:" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_Angle) +#: flipswitch/flipswitch_config.ui:71 +#, kde-format +msgid " °" +msgstr " °" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: flipswitch/flipswitch_config.ui:81 +#, kde-format +msgid "Horizontal position of front:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: flipswitch/flipswitch_config.ui:136 +#, kde-format +msgid "Vertical position of front:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_Duration) +#: glide/glide_config.ui:19 scale/package/contents/ui/config.ui:17 +#: slide/slide_config.ui:19 +#, kde-format +msgid "Duration:" +msgstr "Duración:" + +#. i18n: Duration of the slide animation. +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_Duration) +#: glide/glide_config.ui:32 scale/package/contents/ui/config.ui:30 +#: slide/slide_config.ui:32 +#, kde-format +msgid "Default" +msgstr "Por defeutu" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_InAnimation) +#: glide/glide_config.ui:50 +#, kde-format +msgid "Window Open Animation" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_InRotationEdge) +#. i18n: ectx: property (text), widget (QLabel, label_OutRotationEdge) +#: glide/glide_config.ui:56 glide/glide_config.ui:154 +#, kde-format +msgid "Rotation edge:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_InRotationAngle) +#. i18n: ectx: property (text), widget (QLabel, label_OutRotationAngle) +#: glide/glide_config.ui:93 glide/glide_config.ui:191 +#, kde-format +msgid "Rotation angle:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_InDistance) +#. i18n: ectx: property (text), widget (QLabel, label_OutDistance) +#: glide/glide_config.ui:119 glide/glide_config.ui:198 +#, kde-format +msgid "Distance:" +msgstr "Distancia:" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_OutAnimation) +#: glide/glide_config.ui:148 +#, kde-format +msgid "Window Close Animation" +msgstr "" + +#: invert/invert.cpp:34 invert/invert_config.cpp:41 +#, kde-format +msgid "Toggle Invert Effect" +msgstr "" + +#: invert/invert.cpp:42 invert/invert_config.cpp:47 +#, kde-format +msgid "Toggle Invert Effect on Window" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_FadeToBlack) +#: login/package/contents/ui/config.ui:17 +#, kde-format +msgid "Fade to black (fullscreen splash screens only)" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: lookingglass/lookingglass_config.ui:24 +#, kde-format +msgid "&Radius:" +msgstr "&Radiu:" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_AnimationDuration) +#: magiclamp/magiclamp_config.ui:39 +#, kde-format +msgid "milliseconds" +msgstr "milisegundos" + +#. i18n: ectx: property (title), widget (QGroupBox, groupSize) +#: magnifier/magnifier_config.ui:17 +#, kde-format +msgid "Size" +msgstr "Tamañu" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: magnifier/magnifier_config.ui:23 +#, kde-format +msgid "&Width:" +msgstr "A&nchor:" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_Width) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_Height) +#: magnifier/magnifier_config.ui:42 magnifier/magnifier_config.ui:74 +#, kde-format +msgid " px" +msgstr " px" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: magnifier/magnifier_config.ui:55 +#, kde-format +msgid "&Height:" +msgstr "A<or:" + +#: mouseclick/mouseclick.cpp:40 mouseclick/mouseclick_config.cpp:53 +#, kde-format +msgid "Toggle Mouse Click Effect" +msgstr "" + +#: mouseclick/mouseclick.cpp:48 +#, kde-format +msgctxt "Left mouse button" +msgid "Left" +msgstr "" + +#: mouseclick/mouseclick.cpp:49 +#, kde-format +msgctxt "Middle mouse button" +msgid "Middle" +msgstr "" + +#: mouseclick/mouseclick.cpp:50 +#, kde-format +msgctxt "Right mouse button" +msgid "Right" +msgstr "" + +#: mouseclick/mouseclick.h:63 +#, kde-format +msgid "↓" +msgstr "" + +#: mouseclick/mouseclick.h:64 +#, kde-format +msgid "↑" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, basic_tab) +#: mouseclick/mouseclick_config.ui:21 +#, kde-format +msgid "Basic Settings" +msgstr "Axustes básicos" + +#. i18n: ectx: property (text), widget (QLabel, button1_label) +#: mouseclick/mouseclick_config.ui:37 +#, kde-format +msgid "Left Mouse Button Color:" +msgstr "Color del botón esquierdu del mur:" + +#. i18n: ectx: property (text), widget (QLabel, button2_label) +#: mouseclick/mouseclick_config.ui:50 +#, kde-format +msgid "Middle Mouse Button Color:" +msgstr "Color del botón d'en mediu del mur:" + +#. i18n: ectx: property (text), widget (QLabel, button3_label) +#: mouseclick/mouseclick_config.ui:70 +#, kde-format +msgid "Right Mouse Button Color:" +msgstr "Color del botón drechu del mur:" + +#. i18n: ectx: attribute (title), widget (QWidget, advanced_tab) +#: mouseclick/mouseclick_config.ui:91 +#, kde-format +msgid "Advanced Settings" +msgstr "Axustes avanzaos" + +#. i18n: ectx: property (title), widget (QGroupBox, rings) +#: mouseclick/mouseclick_config.ui:97 +#, kde-format +msgid "Rings" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, ring_line_width_label) +#: mouseclick/mouseclick_config.ui:103 +#, kde-format +msgid "Line Width:" +msgstr "" + +#. i18n: ectx: property (suffix), widget (QDoubleSpinBox, kcfg_LineWidth) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_RingSize) +#: mouseclick/mouseclick_config.ui:119 mouseclick/mouseclick_config.ui:171 +#: mousemark/mousemark_config.cpp:45 +#, kde-format +msgid " pixel" +msgid_plural " pixels" +msgstr[0] " píxel" +msgstr[1] " píxeles" + +#. i18n: ectx: property (text), widget (QLabel, ring_duration_label) +#: mouseclick/mouseclick_config.ui:145 +#, kde-format +msgid "Ring Duration:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, ring_radius_label) +#: mouseclick/mouseclick_config.ui:155 +#, kde-format +msgid "Ring Radius:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, ring_count_label) +#: mouseclick/mouseclick_config.ui:184 +#, kde-format +msgid "Ring Count:" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox) +#. i18n: ectx: property (title), widget (QGroupBox, font) +#: mouseclick/mouseclick_config.ui:210 showfps/showfps_config.ui:17 +#, kde-format +msgid "Text" +msgstr "Testu" + +#. i18n: ectx: property (text), widget (QLabel, font_label) +#: mouseclick/mouseclick_config.ui:216 +#, kde-format +msgid "Font:" +msgstr "Fonte:" + +#. i18n: ectx: property (text), widget (QLabel, showtext_label) +#: mouseclick/mouseclick_config.ui:233 +#, kde-format +msgid "Show Text:" +msgstr "Amosar:" + +#: mousemark/mousemark.cpp:41 +#, kde-format +msgid "Clear All Mouse Marks" +msgstr "" + +#: mousemark/mousemark.cpp:48 mousemark/mousemark_config.cpp:65 +#, kde-format +msgid "Clear Last Mouse Mark" +msgstr "" + +#: mousemark/mousemark_config.cpp:59 +#, kde-format +msgid "Clear Mouse Marks" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: mousemark/mousemark_config.ui:23 +#, kde-format +msgid "Wid&th:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: mousemark/mousemark_config.ui:36 +#, kde-format +msgid "&Color:" +msgstr "&Color:" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: mousemark/mousemark_config.ui:78 +#, kde-format +msgid "Draw with the mouse by holding Shift+Meta keys and moving the mouse." +msgstr "" + +#: presentwindows/presentwindows.cpp:66 +#: presentwindows/presentwindows_config.cpp:62 +#, kde-format +msgid "Toggle Present Windows (Current desktop)" +msgstr "" + +#: presentwindows/presentwindows.cpp:75 +#: presentwindows/presentwindows_config.cpp:56 +#, kde-format +msgid "Toggle Present Windows (All desktops)" +msgstr "" + +#: presentwindows/presentwindows.cpp:85 +#: presentwindows/presentwindows_config.cpp:68 +#, kde-format +msgid "Toggle Present Windows (Window class)" +msgstr "" + +#: presentwindows/presentwindows.cpp:1666 +#, kde-format +msgid "" +"Filter:\n" +"%1" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_3) +#: presentwindows/presentwindows_config.ui:39 +#, kde-format +msgid "Natural Layout Settings" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_FillGaps) +#: presentwindows/presentwindows_config.ui:45 +#, kde-format +msgid "Fill &gaps" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: presentwindows/presentwindows_config.ui:65 +#, kde-format +msgid "Faster" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: presentwindows/presentwindows_config.ui:112 +#, kde-format +msgid "Nicer" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_4) +#: presentwindows/presentwindows_config.ui:122 +#, kde-format +msgid "Windows" +msgstr "Ventanes" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#. i18n: ectx: property (text), widget (QLabel, label_8) +#: presentwindows/presentwindows_config.ui:128 +#: presentwindows/presentwindows_config.ui:282 +#, kde-format +msgid "Left button:" +msgstr "Botón esquierdu:" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonDesktop) +#: presentwindows/presentwindows_config.ui:139 +#: presentwindows/presentwindows_config.ui:183 +#: presentwindows/presentwindows_config.ui:232 +#: presentwindows/presentwindows_config.ui:293 +#: presentwindows/presentwindows_config.ui:327 +#: presentwindows/presentwindows_config.ui:361 +#, kde-format +msgid "No action" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonDesktop) +#: presentwindows/presentwindows_config.ui:144 +#: presentwindows/presentwindows_config.ui:188 +#: presentwindows/presentwindows_config.ui:237 +#: presentwindows/presentwindows_config.ui:298 +#: presentwindows/presentwindows_config.ui:332 +#: presentwindows/presentwindows_config.ui:366 +#, kde-format +msgid "Activate window" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonDesktop) +#: presentwindows/presentwindows_config.ui:149 +#: presentwindows/presentwindows_config.ui:193 +#: presentwindows/presentwindows_config.ui:242 +#: presentwindows/presentwindows_config.ui:303 +#: presentwindows/presentwindows_config.ui:337 +#: presentwindows/presentwindows_config.ui:371 +#, kde-format +msgid "End effect" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonWindow) +#: presentwindows/presentwindows_config.ui:154 +#: presentwindows/presentwindows_config.ui:198 +#: presentwindows/presentwindows_config.ui:247 +#, kde-format +msgid "Bring window to current desktop" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonWindow) +#: presentwindows/presentwindows_config.ui:159 +#: presentwindows/presentwindows_config.ui:203 +#: presentwindows/presentwindows_config.ui:252 +#, kde-format +msgid "Send window to all desktops" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonWindow) +#: presentwindows/presentwindows_config.ui:164 +#: presentwindows/presentwindows_config.ui:208 +#: presentwindows/presentwindows_config.ui:257 +#, kde-format +msgid "(Un-)Minimize window" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_4) +#. i18n: ectx: property (text), widget (QLabel, label_9) +#: presentwindows/presentwindows_config.ui:172 +#: presentwindows/presentwindows_config.ui:316 +#, kde-format +msgid "Middle button:" +msgstr "Botón d'en mediu:" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonWindow) +#: presentwindows/presentwindows_config.ui:213 +#: presentwindows/presentwindows_config.ui:262 +#, kde-format +msgid "Close window" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#. i18n: ectx: property (text), widget (QLabel, label_10) +#: presentwindows/presentwindows_config.ui:221 +#: presentwindows/presentwindows_config.ui:350 +#, kde-format +msgid "Right button:" +msgstr "Botón drechu:" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_5) +#: presentwindows/presentwindows_config.ui:273 +#, kde-format +msgctxt "@title:group actions when clicking on desktop" +msgid "Desktop" +msgstr "Escritoriu" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonDesktop) +#: presentwindows/presentwindows_config.ui:308 +#: presentwindows/presentwindows_config.ui:342 +#: presentwindows/presentwindows_config.ui:376 +#, kde-format +msgid "Show desktop" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: presentwindows/presentwindows_config.ui:393 +#, kde-format +msgid "Layout mode:" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DrawWindowIcons) +#: presentwindows/presentwindows_config.ui:413 +#, kde-format +msgid "Display window &icons" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_IgnoreMinimized) +#: presentwindows/presentwindows_config.ui:420 +#, kde-format +msgid "Ignore &minimized windows" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ShowPanel) +#: presentwindows/presentwindows_config.ui:427 +#, kde-format +msgid "Show &panels" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LayoutMode) +#: presentwindows/presentwindows_config.ui:441 +#, kde-format +msgid "Natural" +msgstr "Natural" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LayoutMode) +#: presentwindows/presentwindows_config.ui:446 +#, kde-format +msgid "Regular Grid" +msgstr "Rexáu regular" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LayoutMode) +#: presentwindows/presentwindows_config.ui:451 +#, kde-format +msgid "Flexible Grid" +msgstr "Rexáu flexible" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AllowClosingWindows) +#: presentwindows/presentwindows_config.ui:459 +#, kde-format +msgid "Provide buttons to close the windows" +msgstr "Apurre botones pa zarrar les ventanes" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_TextureScale) +#: resize/resize_config.ui:17 +#, kde-format +msgid "Scale window" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Outline) +#: resize/resize_config.ui:24 +#, kde-format +msgid "Show outline" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_InScale) +#: scale/package/contents/ui/config.ui:46 +#, kde-format +msgid "Window open scale:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_OutScale) +#: scale/package/contents/ui/config.ui:53 +#, kde-format +msgid "Window close scale:" +msgstr "" + +#: screenshot/screenshot.cpp:440 +#, kde-format +msgctxt "Notification caption that a screenshot got saved to file" +msgid "Screenshot" +msgstr "" + +#: screenshot/screenshot.cpp:441 +#, kde-format +msgctxt "Notification with path to screenshot file" +msgid "Screenshot saved to %1" +msgstr "" + +#: screenshot/screenshot.cpp:578 +#, kde-format +msgid "" +"Select window to screen shot with left click or enter.\n" +"Escape or right click to cancel." +msgstr "" + +#: screenshot/screenshot.cpp:581 +#, kde-format +msgid "" +"Create screen shot with left click or enter.\n" +"Escape or right click to cancel." +msgstr "" + +#: showfps/showfps.cpp:54 +#, kde-format +msgid "This effect is not a benchmark" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: showfps/showfps_config.ui:23 +#, kde-format +msgid "Text position:" +msgstr "Posición:" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TextPosition) +#: showfps/showfps_config.ui:43 +#, kde-format +msgid "Inside Graph" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TextPosition) +#: showfps/showfps_config.ui:48 +#, kde-format +msgid "Nowhere" +msgstr "Nenyures" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TextPosition) +#: showfps/showfps_config.ui:53 +#, kde-format +msgid "Top Left" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TextPosition) +#: showfps/showfps_config.ui:58 +#, kde-format +msgid "Top Right" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TextPosition) +#: showfps/showfps_config.ui:63 +#, kde-format +msgid "Bottom Left" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TextPosition) +#: showfps/showfps_config.ui:68 +#, kde-format +msgid "Bottom Right" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: showfps/showfps_config.ui:76 +#, kde-format +msgid "Text font:" +msgstr "Fonte:" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: showfps/showfps_config.ui:96 +#, kde-format +msgid "Text color:" +msgstr "Color:" + +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: showfps/showfps_config.ui:119 +#, kde-format +msgid "Text alpha:" +msgstr "" + +#: showpaint/showpaint.cpp:42 showpaint/showpaint_config.cpp:41 +#, kde-format +msgid "Toggle Show Paint" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_Gaps) +#: slide/slide_config.ui:50 +#, kde-format +msgid "Gap between desktops" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_HorizontalGap) +#: slide/slide_config.ui:56 +#, kde-format +msgid "Horizontal:" +msgstr "Horizontal:" + +#. i18n: ectx: property (text), widget (QLabel, label_VerticalGap) +#: slide/slide_config.ui:79 +#, kde-format +msgid "Vertical:" +msgstr "Vertical:" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_SlideDocks) +#: slide/slide_config.ui:105 +#, kde-format +msgid "Slide docks" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_SlideBackground) +#: slide/slide_config.ui:112 +#, kde-format +msgid "Slide desktop background" +msgstr "" + +#: thumbnailaside/thumbnailaside.cpp:29 +#: thumbnailaside/thumbnailaside_config.cpp:62 +#, kde-format +msgid "Toggle Thumbnail for Current Window" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: thumbnailaside/thumbnailaside_config.ui:23 +#, kde-format +msgid "Maximum &width:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: thumbnailaside/thumbnailaside_config.ui:36 +#, kde-format +msgid "&Spacing:" +msgstr "" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_Spacing) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_MaxWidth) +#: thumbnailaside/thumbnailaside_config.ui:55 +#: thumbnailaside/thumbnailaside_config.ui:106 +#, kde-format +msgid " pixels" +msgstr " píxeles" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: thumbnailaside/thumbnailaside_config.ui:68 +#, kde-format +msgid "&Opacity:" +msgstr "&Opacidá:" + +#: trackmouse/trackmouse.cpp:50 trackmouse/trackmouse_config.cpp:59 +#, kde-format +msgid "Track mouse" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: trackmouse/trackmouse_config.ui:26 +#, kde-format +msgid "Trigger effect with:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_KeyboardShortcut) +#: trackmouse/trackmouse_config.ui:33 +#, kde-format +msgid "Keyboard shortcut:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_ModifierKeys) +#: trackmouse/trackmouse_config.ui:43 +#, kde-format +msgid "Modifier keys:" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Alt) +#: trackmouse/trackmouse_config.ui:65 +#, kde-format +msgid "Alt" +msgstr "Alt" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Control) +#: trackmouse/trackmouse_config.ui:72 +#, kde-format +msgid "Ctrl" +msgstr "Ctrl" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Shift) +#: trackmouse/trackmouse_config.ui:79 +#, kde-format +msgid "Shift" +msgstr "Mayús" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Meta) +#: trackmouse/trackmouse_config.ui:86 +#, kde-format +msgid "Meta" +msgstr "" + +#. i18n: ectx: property (windowTitle), widget (QWidget, KWin::TranslucencyEffectConfigForm) +#: translucency/package/contents/ui/config.ui:14 +#, kde-format +msgid "Translucency" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, m_opacityGroupBox) +#: translucency/package/contents/ui/config.ui:20 +#, kde-format +msgid "General Translucency Settings" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, comboboxpopup_label) +#: translucency/package/contents/ui/config.ui:64 +#, kde-format +msgid "Combobox popups:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, dialogs_label) +#: translucency/package/contents/ui/config.ui:137 +#, kde-format +msgid "Dialogs:" +msgstr "Diálogos:" + +#. i18n: ectx: property (text), widget (QLabel, menus_label) +#: translucency/package/contents/ui/config.ui:188 +#, kde-format +msgid "Menus:" +msgstr "Menús:" + +#. i18n: ectx: property (text), widget (QLabel, moveresize_label) +#: translucency/package/contents/ui/config.ui:207 +#, kde-format +msgid "Moving windows:" +msgstr "Ventanes que se mueven:" + +#. i18n: ectx: property (text), widget (QLabel, inactive_label) +#: translucency/package/contents/ui/config.ui:226 +#, kde-format +msgid "Inactive windows:" +msgstr "Ventanes inactives:" + +#. i18n: ectx: property (title), widget (QGroupBox, kcfg_IndividualMenuConfig) +#: translucency/package/contents/ui/config.ui:267 +#, kde-format +msgid "Set menu translucency independently" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, dropdownmenus_label) +#: translucency/package/contents/ui/config.ui:285 +#, kde-format +msgid "Dropdown menus:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, popupmenus_label) +#: translucency/package/contents/ui/config.ui:329 +#, kde-format +msgid "Popup menus:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, tornoffmenus_label) +#: translucency/package/contents/ui/config.ui:367 +#, kde-format +msgid "Torn-off menus:" +msgstr "" + +#: windowgeometry/windowgeometry.cpp:43 +#, kde-format +msgid "Toggle window geometry display (effect only)" +msgstr "" + +#: windowgeometry/windowgeometry_config.cpp:47 +#, kde-format +msgid "Toggle KWin composited geometry display" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Move) +#: windowgeometry/windowgeometry_config.ui:17 +#, kde-format +msgid "Display for moving windows" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Resize) +#: windowgeometry/windowgeometry_config.ui:24 +#, kde-format +msgid "Display for resizing windows" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, advancedGroup) +#: wobblywindows/wobblywindows_config.ui:20 +#, kde-format +msgid "Advanced" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: wobblywindows/wobblywindows_config.ui:26 +#, kde-format +msgid "&Stiffness:" +msgstr "&Rixidez" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: wobblywindows/wobblywindows_config.ui:68 +#, kde-format +msgid "Dra&g:" +msgstr "&Arrastre:" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: wobblywindows/wobblywindows_config.ui:81 +#, kde-format +msgid "&Move factor:" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_MoveWobble) +#: wobblywindows/wobblywindows_config.ui:155 +#, kde-format +msgid "Wo&bble when moving" +msgstr "Cimblar al &mover" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ResizeWobble) +#: wobblywindows/wobblywindows_config.ui:162 +#, kde-format +msgid "Wobble when &resizing" +msgstr "Cimblar al &redimensionar" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AdvancedMode) +#: wobblywindows/wobblywindows_config.ui:182 +#, kde-format +msgid "Enable &advanced mode" +msgstr "Activar el mou &avanzáu" + +#. i18n: ectx: property (title), widget (QGroupBox, basicGroup) +#: wobblywindows/wobblywindows_config.ui:192 +#, kde-format +msgid "&Wobbliness" +msgstr "&Cimblíu" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: wobblywindows/wobblywindows_config.ui:201 +#, kde-format +msgid "Less" +msgstr "Menos" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: wobblywindows/wobblywindows_config.ui:224 +#, kde-format +msgid "More" +msgstr "Más" + +#: zoom/zoom.cpp:74 +#, kde-format +msgid "Move Zoomed Area to Left" +msgstr "" + +#: zoom/zoom.cpp:82 +#, kde-format +msgid "Move Zoomed Area to Right" +msgstr "" + +#: zoom/zoom.cpp:90 +#, kde-format +msgid "Move Zoomed Area Upwards" +msgstr "" + +#: zoom/zoom.cpp:98 +#, kde-format +msgid "Move Zoomed Area Downwards" +msgstr "" + +#: zoom/zoom.cpp:107 zoom/zoom_config.cpp:109 +#, kde-format +msgid "Move Mouse to Focus" +msgstr "" + +#: zoom/zoom.cpp:115 zoom/zoom_config.cpp:116 +#, kde-format +msgid "Move Mouse to Center" +msgstr "" + +#: zoom/zoom_config.cpp:81 +#, kde-format +msgid "Move Left" +msgstr "" + +#: zoom/zoom_config.cpp:88 +#, kde-format +msgid "Move Right" +msgstr "" + +#: zoom/zoom_config.cpp:95 +#, kde-format +msgid "Move Up" +msgstr "" + +#: zoom/zoom_config.cpp:102 +#, kde-format +msgid "Move Down" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QLabel, label) +#. i18n: ectx: property (whatsThis), widget (QDoubleSpinBox, kcfg_ZoomFactor) +#: zoom/zoom_config.ui:25 zoom/zoom_config.ui:41 +#, kde-format +msgid "On zoom-in and zoom-out change the zoom by the defined zoom-factor." +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: zoom/zoom_config.ui:28 +#, kde-format +msgid "Zoom Factor:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_EnableFocusTracking) +#: zoom/zoom_config.ui:66 +#, kde-format +msgid "" +"Enable tracking of the focused location. This needs QAccessible to be " +"enabled per application (\"export QT_LINUX_ACCESSIBILITY_ALWAYS_ON=1\")." +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_EnableFocusTracking) +#: zoom/zoom_config.ui:69 +#, kde-format +msgid "Enable Focus Tracking" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_EnableTextCaretTracking) +#: zoom/zoom_config.ui:76 +#, kde-format +msgid "" +"Enable tracking of the text cursor. This needs QAccessible to be enabled per " +"application (\"export QT_LINUX_ACCESSIBILITY_ALWAYS_ON=1\")." +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_EnableTextCaretTracking) +#: zoom/zoom_config.ui:79 +#, kde-format +msgid "Enable Text Cursor Tracking" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: zoom/zoom_config.ui:86 +#, kde-format +msgid "Mouse Pointer:" +msgstr "Punteru del mur:" + +#. i18n: ectx: property (whatsThis), widget (QComboBox, kcfg_MousePointer) +#: zoom/zoom_config.ui:99 +#, kde-format +msgid "Visibility of the mouse-pointer." +msgstr "Visibilidá del punteru del mur." + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_MousePointer) +#: zoom/zoom_config.ui:103 +#, kde-format +msgid "Scale" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_MousePointer) +#: zoom/zoom_config.ui:108 +#, kde-format +msgid "Keep" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_MousePointer) +#: zoom/zoom_config.ui:113 +#, kde-format +msgid "Hide" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QComboBox, kcfg_MouseTracking) +#: zoom/zoom_config.ui:121 +#, kde-format +msgid "Track moving of the mouse." +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_MouseTracking) +#: zoom/zoom_config.ui:125 +#, kde-format +msgid "Proportional" +msgstr "Proporcional" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_MouseTracking) +#: zoom/zoom_config.ui:130 +#, kde-format +msgid "Centered" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_MouseTracking) +#: zoom/zoom_config.ui:135 +#, kde-format +msgid "Push" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_MouseTracking) +#: zoom/zoom_config.ui:140 +#, kde-format +msgid "Disabled" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: zoom/zoom_config.ui:148 +#, kde-format +msgid "Mouse Tracking:" +msgstr "" \ No newline at end of file diff --git a/po/az/kcm-kwin-scripts.po b/po/az/kcm-kwin-scripts.po new file mode 100644 index 0000000..3ed5761 --- /dev/null +++ b/po/az/kcm-kwin-scripts.po @@ -0,0 +1,92 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the kwin package. +# +# Xəyyam Qocayev , 2020. +msgid "" +msgstr "" +"Project-Id-Version: kwin\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-09-01 02:29+0200\n" +"PO-Revision-Date: 2020-09-03 00:05+0400\n" +"Last-Translator: Kheyyam Gojayev \n" +"Language-Team: Azerbaijani \n" +"Language: az\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Lokalize 20.08.0\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Xəyyam" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "xxmn77@gmail.com" + +#: module.cpp:38 +#, kde-format +msgid "KWin Scripts" +msgstr "KWin Skriptləri" + +#: module.cpp:40 +#, kde-format +msgid "Configure KWin scripts" +msgstr "KWin skriptlərini tənzimləyin" + +#: module.cpp:43 +#, kde-format +msgid "Tamás Krutki" +msgstr "Tamas Krutki" + +#: module.cpp:79 +#, kde-format +msgid "Error when uninstalling KWin Script: %1" +msgstr "Bu KWin Script'in silinməsi zamanı xəta: %1" + +#: module.cpp:100 +#, kde-format +msgid "Import KWin Script" +msgstr "KWin Skriptinin İdxalı" + +#: module.cpp:101 +#, kde-format +msgid "*.kwinscript|KWin scripts (*.kwinscript)" +msgstr "*.kwinscript|KWin skriptləri (*.kwinscript)" + +#: module.cpp:120 +#, kde-format +msgctxt "Placeholder is error message returned from the install service" +msgid "" +"Cannot import selected script.\n" +"%1" +msgstr "" +"Seçilmiş skript idxal edilə bilmir.\n" +"%1" + +#: module.cpp:134 +#, kde-format +msgctxt "Placeholder is name of the script that was imported" +msgid "The script \"%1\" was successfully imported." +msgstr "\"%1\" skripti uğurla idxal olundu." + +#. i18n: ectx: property (windowTitle), widget (QWidget, Module) +#: module.ui:14 +#, kde-format +msgid "KWin script configuration" +msgstr "KWin skripti tənzimləmələri" + +#. i18n: ectx: property (text), widget (QPushButton, importScriptButton) +#: module.ui:55 +#, kde-format +msgid "Install from File..." +msgstr "Bu fayldan quraşdır..." + +#. i18n: ectx: property (text), widget (KNS3::Button, ghnsButton) +#: module.ui:67 +#, kde-format +msgid "Get New Scripts..." +msgstr "Yeni Skriptlər Yükləmək..." \ No newline at end of file diff --git a/po/az/kcm_kwin_effects.po b/po/az/kcm_kwin_effects.po new file mode 100644 index 0000000..a2ecf4f --- /dev/null +++ b/po/az/kcm_kwin_effects.po @@ -0,0 +1,98 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the kwin package. +# +# Xəyyam Qocayev , 2020. +msgid "" +msgstr "" +"Project-Id-Version: kwin\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-08-08 02:22+0200\n" +"PO-Revision-Date: 2020-07-22 16:45+0400\n" +"Last-Translator: Xəyyam Qocayev \n" +"Language-Team: Azerbaijani \n" +"Language: az\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Lokalize 20.04.3\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Xəyyam Qocayev" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "xxmn77@gmail.com" + +#: kcm.cpp:33 +#, kde-format +msgid "Desktop Effects" +msgstr "İş Masası effektləri" + +#: kcm.cpp:38 +#, kde-format +msgid "Vlad Zahorodnii" +msgstr "Vlad Zahorodnii" + +#: kcm.cpp:77 +#, kde-format +msgid "Download New Desktop Effects" +msgstr "Yeni İş Masası Effektlərini Yükləmək" + +#: package/contents/ui/Effect.qml:77 +#, kde-format +msgid "" +"Author: %1\n" +"License: %2" +msgstr "" +"Müəllif: %1\n" +"Lisenziya: %2" + +#: package/contents/ui/Effect.qml:106 +#, kde-format +msgctxt "@info:tooltip" +msgid "Show/Hide Video" +msgstr "Videonu Göstərmək/Gizlətmək" + +#: package/contents/ui/Effect.qml:113 +#, kde-format +msgctxt "@info:tooltip" +msgid "Configure..." +msgstr "Tənzimləmə..." + +#: package/contents/ui/main.qml:18 +#, kde-format +msgid "This module lets you configure desktop effects." +msgstr "Bu modul iş masası effektlərinizi tənzimləmənizə imkan verir." + +#: package/contents/ui/main.qml:25 +#, kde-format +msgid "" +"Hint: To find out or configure how to activate an effect, look at the " +"effect's settings." +msgstr "" +"Qeyd: Effekti necə aktivləşdirəcəyinizi tapmaq və ya tənzimləmək üçün " +"effektin ayarlarına baxın." + +#: package/contents/ui/main.qml:45 +#, kde-format +msgid "Configure Filter" +msgstr "Çeşidləmə qaydaları" + +#: package/contents/ui/main.qml:57 +#, kde-format +msgid "Exclude unsupported effects" +msgstr "Dəstəklənməyən effektləri gizlətmək" + +#: package/contents/ui/main.qml:65 +#, kde-format +msgid "Exclude internal effects" +msgstr "Daxili effektləri gizlətmək" + +#: package/contents/ui/main.qml:124 +#, kde-format +msgid "Get New Desktop Effects..." +msgstr "Yeni İş Masası Effektləri Almaq..." \ No newline at end of file diff --git a/po/az/kcm_kwin_virtualdesktops.po b/po/az/kcm_kwin_virtualdesktops.po new file mode 100644 index 0000000..fbb4d7b --- /dev/null +++ b/po/az/kcm_kwin_virtualdesktops.po @@ -0,0 +1,131 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the kwin package. +# +# Xəyyam Qocayev , 2020. +msgid "" +msgstr "" +"Project-Id-Version: kwin\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-08-08 02:22+0200\n" +"PO-Revision-Date: 2020-07-22 15:52+0400\n" +"Last-Translator: Xəyyam Qocayev \n" +"Language-Team: Azerbaijani \n" +"Language: az\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Lokalize 20.04.3\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Xəyyam Qocayev" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "xxmn77@gmail.com" + +#: desktopsmodel.cpp:455 +#, kde-format +msgid "There was an error connecting to the compositor." +msgstr "Bəstələyiciyə qoşulmada xəta baş verdi." + +#: desktopsmodel.cpp:657 +#, kde-format +msgid "There was an error saving the settings to the compositor." +msgstr "Bəstələyicinin ayarlarının saxlanmasında xəta baş verdi." + +#: desktopsmodel.cpp:660 +#, kde-format +msgid "There was an error requesting information from the compositor." +msgstr "Bəstələyici haqqında məlumatlar sorğusunda xəta baş verdi." + +#: package/contents/ui/main.qml:18 +#, kde-format +msgid "" +"This module lets you configure the navigation, number and layout of virtual " +"desktops." +msgstr "" +"Bu modul Virtual İş Masasının nömrəsi, qatı və istiqamətinin tənzimlənməsinə " +"imkan verir." + +#: package/contents/ui/main.qml:68 +#, kde-format +msgctxt "@info:tooltip" +msgid "Rename" +msgstr "Adını Dəyişmək" + +#: package/contents/ui/main.qml:78 +#, kde-format +msgctxt "@info:tooltip" +msgid "Remove" +msgstr "Silmək" + +#: package/contents/ui/main.qml:104 +#, kde-format +msgid "" +"Virtual desktops have been changed outside this settings application. Saving " +"now will overwrite the changes." +msgstr "" +"Virtual İş Masaları başqa tətbiq tərəfindən dəyişdirildi. Bu pəncərədə " +"ayarların saxlanılması kənar dəyişikləri inkar edəcək." + +#: package/contents/ui/main.qml:118 +#, kde-format +msgid "Row %1" +msgstr "Sətir %1" + +#: package/contents/ui/main.qml:131 +#, kde-format +msgctxt "@action:button" +msgid "Add" +msgstr "Əlavə etmək" + +#: package/contents/ui/main.qml:134 +#, kde-format +msgid "New Desktop" +msgstr "Yeni İş Masası" + +#: package/contents/ui/main.qml:148 +#, kde-format +msgid "1 Row" +msgid_plural "%1 Rows" +msgstr[0] "%1 Sətir" +msgstr[1] "%1 sətir" + +#: package/contents/ui/main.qml:160 +#, kde-format +msgid "Options:" +msgstr "Seçimlər:" + +#: package/contents/ui/main.qml:162 +#, kde-format +msgid "Navigation wraps around" +msgstr "Dairəvi naviqasiya" + +#: package/contents/ui/main.qml:177 +#, kde-format +msgid "Show animation when switching:" +msgstr "Dəyişdirmə zamanı animasiyaları göstərmək" + +#: package/contents/ui/main.qml:220 +#, kde-format +msgid "Show on-screen display when switching:" +msgstr "Dəyişdirilmə zamanı bildirişi ekranda göstərmək:" + +#: package/contents/ui/main.qml:238 +#, kde-format +msgid "%1 ms" +msgstr "%1 msan" + +#: package/contents/ui/main.qml:256 +#, kde-format +msgid "Show desktop layout indicators" +msgstr "Bütün İş Masalarının sxematik görünüşü" + +#: virtualdesktops.cpp:30 +#, kde-format +msgid "Virtual Desktops" +msgstr "Virtual İş Masaları" \ No newline at end of file diff --git a/po/az/kcm_kwindecoration.po b/po/az/kcm_kwindecoration.po new file mode 100644 index 0000000..f7b0cef --- /dev/null +++ b/po/az/kcm_kwindecoration.po @@ -0,0 +1,214 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the kwin package. +# +# Xəyyam Qocayev , 2020. +msgid "" +msgstr "" +"Project-Id-Version: kwin\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-11-17 08:20+0100\n" +"PO-Revision-Date: 2020-07-22 12:20+0400\n" +"Last-Translator: Xəyyam Qocayev \n" +"Language-Team: Azerbaijani \n" +"Language: az\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Lokalize 20.04.3\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Xəyyam Qocayev" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "xxmn77@gmail.com" + +#: declarative-plugin/buttonsmodel.cpp:54 +#, kde-format +msgid "Menu" +msgstr "Menyu" + +#: declarative-plugin/buttonsmodel.cpp:56 +#, kde-format +msgid "Application menu" +msgstr "Tətbiq menyusu" + +#: declarative-plugin/buttonsmodel.cpp:58 +#, kde-format +msgid "On all desktops" +msgstr "Bütün İş Masalarında" + +#: declarative-plugin/buttonsmodel.cpp:60 +#, kde-format +msgid "Minimize" +msgstr "Yığmaq" + +#: declarative-plugin/buttonsmodel.cpp:62 +#, kde-format +msgid "Maximize" +msgstr "Genişləndirmək" + +#: declarative-plugin/buttonsmodel.cpp:64 +#, kde-format +msgid "Close" +msgstr "Bağlamaq" + +#: declarative-plugin/buttonsmodel.cpp:66 +#, kde-format +msgid "Context help" +msgstr "Kontekst arayış" + +#: declarative-plugin/buttonsmodel.cpp:68 +#, kde-format +msgid "Shade" +msgstr "Başlığa yığmaq" + +#: declarative-plugin/buttonsmodel.cpp:70 +#, kde-format +msgid "Keep below" +msgstr "Altında tutmaq" + +#: declarative-plugin/buttonsmodel.cpp:72 +#, kde-format +msgid "Keep above" +msgstr "Üstündə tutmaq" + +#: kcm.cpp:50 +#, kde-format +msgid "Window Decorations" +msgstr "Pəncərə dekorasiyası" + +#: kcm.cpp:54 +#, kde-format +msgid "Valerio Pilo" +msgstr "Valerio Pilo" + +#: kcm.cpp:55 +#, kde-format +msgid "Author" +msgstr "Müəllif" + +#: kcm.cpp:104 +#, kde-format +msgid "Download New Window Decorations" +msgstr "Yeni Pəncərə dekorasiyalarını Yükləmək" + +#: package/contents/ui/Buttons.qml:73 +#, kde-format +msgid "Titlebar" +msgstr "Başlıq paneli" + +#: package/contents/ui/Buttons.qml:214 +#, kde-format +msgid "Drop button here to remove it" +msgstr "Silmək üçün düyməni buraya köçürün" + +#: package/contents/ui/Buttons.qml:232 +#, kde-format +msgid "Drag buttons between here and the titlebar" +msgstr "Düymələri bu sahə və başlıq paneli arasına daşıyın" + +#: package/contents/ui/main.qml:15 +#, kde-format +msgid "This module lets you configure the window decorations." +msgstr "Bu modul pəncərə dekorasiyasını ayarlamağa imkan verir" + +#: package/contents/ui/main.qml:49 +#, kde-format +msgctxt "tab label" +msgid "Theme" +msgstr "Mövzu" + +#: package/contents/ui/main.qml:53 +#, kde-format +msgctxt "tab label" +msgid "Titlebar Buttons" +msgstr "Başlıq paneli düymələri" + +#: package/contents/ui/main.qml:78 +#, kde-format +msgctxt "checkbox label" +msgid "Use theme's default window border size" +msgstr "Mövzunun standart pəncərə sərhəddi ölçüsünü istifadə etmək" + +#: package/contents/ui/main.qml:109 +#, kde-format +msgctxt "button text" +msgid "Get New Window Decorations..." +msgstr "Yeni Pəncərə Dekorasiyaları Almaq..." + +#: package/contents/ui/main.qml:126 +#, kde-format +msgctxt "checkbox label" +msgid "Close windows by double clicking the menu button" +msgstr "Menyu düyməsinə iki dəfə vuraraq pəncərəni bağlamaq" + +#: package/contents/ui/main.qml:139 +#, kde-format +msgctxt "popup tip" +msgid "" +"Close by double clicking: Keep the window's Menu button pressed until it " +"appears." +msgstr "" +"İki dəfə vurmaqla bağlamaq: Görünənə qədər pəncərənin Menyu düyməsini basılı " +"saxlamaq." + +#: package/contents/ui/main.qml:146 +#, kde-format +msgctxt "checkbox label" +msgid "Show titlebar button tooltips" +msgstr "Başlıq düyməsi köməkçi bildirişlərini göstərmək" + +#: package/contents/ui/Themes.qml:89 +#, kde-format +msgid "Edit %1 Theme" +msgstr "%1 mövzusuna düzəliş etmək" + +#: utils.cpp:26 +#, kde-format +msgid "No Borders" +msgstr "Kənar çərçivələrsiz" + +#: utils.cpp:27 +#, kde-format +msgid "No Side Borders" +msgstr "Kənar çərçivələri yoxdur" + +#: utils.cpp:28 +#, kde-format +msgid "Tiny" +msgstr "İncə" + +#: utils.cpp:29 +#, kde-format +msgid "Normal" +msgstr "Normal" + +#: utils.cpp:30 +#, kde-format +msgid "Large" +msgstr "Geniş" + +#: utils.cpp:31 +#, kde-format +msgid "Very Large" +msgstr "Çox geniş" + +#: utils.cpp:32 +#, kde-format +msgid "Huge" +msgstr "Nəhəng" + +#: utils.cpp:33 +#, kde-format +msgid "Very Huge" +msgstr "Çox nəhəng" + +#: utils.cpp:34 +#, kde-format +msgid "Oversized" +msgstr "Ən geniş" \ No newline at end of file diff --git a/po/az/kcm_kwinrules.po b/po/az/kcm_kwinrules.po new file mode 100644 index 0000000..e9754bb --- /dev/null +++ b/po/az/kcm_kwinrules.po @@ -0,0 +1,873 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the kwin package. +# +# Xəyyam Qocayev , 2020. +msgid "" +msgstr "" +"Project-Id-Version: kwin\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-11-03 08:14+0100\n" +"PO-Revision-Date: 2020-07-21 18:35+0400\n" +"Last-Translator: Xəyyam Qocayev \n" +"Language-Team: Azerbaijani \n" +"Language: az\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Lokalize 20.04.3\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Xəyyam Qocayev" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "xxmn77@gmail.com" + +#: kcmrules.cpp:28 +#, kde-format +msgid "Window Rules" +msgstr "Xüsusi Pəncərə Qaydaları" + +#: kcmrules.cpp:32 +#, kde-format +msgid "Ismael Asensio" +msgstr "Ismael Asensio" + +#: kcmrules.cpp:33 +#, kde-format +msgid "Author" +msgstr "Müəllif" + +#: kcmrules.cpp:37 +#, kde-format +msgid "" +"

Window-specific Settings

Here you can customize window settings " +"specifically only for some windows.

Please note that this " +"configuration will not take effect if you do not use KWin as your window " +"manager. If you do use a different window manager, please refer to its " +"documentation for how to customize window behavior.

" +msgstr "" +"

Xüsusi pəncərə qaydaları

Burada siz ayrı-ayrı pəncərə " +"parametrlərini yalnız bəzi pəncərələr üçün ayarlaya bilərsiniz.

Nəzərə " +"alın ki, bu tənzimləmər yalnız KWin-i pəncərə meneceri kimi istifadə etdikdə " +"qüvvədə olacaqdır. Digər pəncərə menecerlərindən istifadə etmək istəsəniz " +"onların necə tənzimlənməsi haqqında təlimatlarla tanış olun.

" + +#: main.cpp:91 +#, kde-format +msgid "Application settings for %1" +msgstr "%1 üçün tətbiq ayarları" + +#: main.cpp:111 rulesmodel.cpp:216 +#, kde-format +msgid "Window settings for %1" +msgstr "%1 üçün pəncərə ayarları" + +#: main.cpp:163 +#, kde-format +msgctxt "Window caption for the application wide rules dialog" +msgid "Edit Application-Specific Settings" +msgstr "Göstərilən tətbiqlər üçün parametrləri ayarlamaq" + +#: main.cpp:197 +#, kde-format +msgid "KWin" +msgstr "KWin" + +#: main.cpp:204 +#, kde-format +msgid "KWin helper utility" +msgstr "KWin köməkçi vasitəsi" + +#: main.cpp:205 +#, kde-format +msgid "KWin id of the window for special window settings." +msgstr "Xüsusi pəncərə ayarları üçün pəncərənin KWin İD-si" + +#: main.cpp:206 +#, kde-format +msgid "Whether the settings should affect all windows of the application." +msgstr "Ayarların tətbiqin bütün pəncərələrinə təsir edib etməməsi." + +#: main.cpp:215 +#, kde-format +msgid "This helper utility is not supposed to be called directly." +msgstr "Bu köməkçi vasitə müstəqil proqram kimi başladıla bilməz." + +#: optionsmodel.cpp:145 +#, kde-format +msgid "Unimportant" +msgstr "Əhəmiyyəti yoxdur" + +#: optionsmodel.cpp:146 +#, kde-format +msgid "Exact Match" +msgstr "Tam uyğunluq" + +#: optionsmodel.cpp:147 +#, kde-format +msgid "Substring Match" +msgstr "Qurulmuş oxşarlıq" + +#: optionsmodel.cpp:148 +#, kde-format +msgid "Regular Expression" +msgstr "Müntəzəm ifadə" + +#: optionsmodel.cpp:153 +#, kde-format +msgid "Do Not Affect" +msgstr "Tətbiq etməmək" + +#: optionsmodel.cpp:154 +#, kde-format +msgid "" +"The window property will not be affected and therefore the default handling " +"for it will be used.\n" +"Specifying this will block more generic window settings from taking effect." +msgstr "" +"Bu pəncərə xüsusiyyətləri tətbiq olunmayacaq və bu səbəbdən standart " +"əməllərdən istifadə olunacaq.\n" +"Bu seçim adi pəncərə ayarlarının qüvvəyə minməsinə mane olacaq." + +#: optionsmodel.cpp:157 +#, kde-format +msgid "Apply Initially" +msgstr "Başlanğıcda tətbiq etmək" + +#: optionsmodel.cpp:158 +#, kde-format +msgid "" +"The window property will be only set to the given value after the window is " +"created.\n" +"No further changes will be affected." +msgstr "" +"Pəncərə xüsusiyyəti yalnız pəncərə yaradıldıqdan sonra göstərilən dəyər görə " +"ayarlanacaq.\n" +"Əlavə təsirlər tətbiq edilməyəcək." + +#: optionsmodel.cpp:161 +#, kde-format +msgid "Remember" +msgstr "Xatırlatmaq" + +#: optionsmodel.cpp:162 +#, kde-format +msgid "" +"The value of the window property will be remembered and, every time the " +"window is created, the last remembered value will be applied." +msgstr "" +"Pəncərə xüsusiyyətinin dəyəri xatırlanacaq və hər dəfə pəncərə yaradıldıqda " +"son yadda qalan dəyər tətbiq ediləcəkdir." + +#: optionsmodel.cpp:165 +#, kde-format +msgid "Force" +msgstr "Məcburi" + +#: optionsmodel.cpp:166 +#, kde-format +msgid "The window property will be always forced to the given value." +msgstr "Pəncərə xüsusiyyətinin verilmiş dəyəri məcburi tətbiq ediləcəkdir." + +#: optionsmodel.cpp:168 +#, kde-format +msgid "Apply Now" +msgstr "Həmişə tətbiq etmək" + +#: optionsmodel.cpp:169 +#, kde-format +msgid "" +"The window property will be set to the given value immediately and will not " +"be affected later\n" +"(this action will be deleted afterwards)." +msgstr "" +"Pəncərə xüsusiyyətləriti dərhal göstərilən dəyərə ayarlanacaq və sonradan " +"tətbiq edilməyəcək\n" +"(və fəaliyyət silinəcəkdir)." + +#: optionsmodel.cpp:172 +#, kde-format +msgid "Force Temporarily" +msgstr "Müvəqqəti məcbur etmək" + +#: optionsmodel.cpp:173 +#, kde-format +msgid "" +"The window property will be forced to the given value until it is hidden\n" +"(this action will be deleted after the window is hidden)." +msgstr "" +"Verilmiş dəyər pəncərə gizlədilənə qədər ona məcburi tətbiq ediləcəkdir\n" +"(bu fəaliyyət pəncərə gizlədildikdən sonra silinəcəkdir)" + +#: package/contents/ui/FileDialogLoader.qml:14 +#, kde-format +msgid "Select File" +msgstr "Faylı seçmək" + +#: package/contents/ui/FileDialogLoader.qml:26 +#, kde-format +msgid "KWin Rules (*.kwinrule)" +msgstr "KWin qaydaları (*.kwinrule)" + +#: package/contents/ui/OptionsComboBox.qml:32 +#, kde-format +msgid "None selected" +msgstr "Heç nə seçilməyib" + +#: package/contents/ui/OptionsComboBox.qml:37 +#, kde-format +msgid "All selected" +msgstr "Hamısı seçilib" + +#: package/contents/ui/OptionsComboBox.qml:39 +#, kde-format +msgid "%1 selected" +msgid_plural "%1 selected" +msgstr[0] "%1 seçildi" +msgstr[1] "%1 seçildi" + +#: package/contents/ui/RulesEditor.qml:48 +#: package/contents/ui/RulesEditor.qml:67 +#, kde-format +msgid "Add Properties..." +msgstr "Xüsusiyyətlər əlavə etmək..." + +#: package/contents/ui/RulesEditor.qml:67 +#, kde-format +msgid "Close" +msgstr "Bağlamaq" + +#: package/contents/ui/RulesEditor.qml:80 +#, kde-format +msgid "Detect Window Properties" +msgstr "Pəncərə xüsusiyyətlərini aşkar etmək" + +#: package/contents/ui/RulesEditor.qml:93 +#, kde-format +msgid "Instantly" +msgstr "Dərhal" + +#: package/contents/ui/RulesEditor.qml:94 +#, kde-format +msgid "After %1 second" +msgid_plural "After %1 seconds" +msgstr[0] "%1 saniyədən sonra" +msgstr[1] "%1 saniyədən sonra" + +#: package/contents/ui/RulesEditor.qml:113 +#, kde-format +msgid "Select properties" +msgstr "Xüsusiyyətləri seçmək" + +#: package/contents/ui/RulesEditor.qml:205 +#: package/contents/ui/ValueEditor.qml:53 +#, kde-format +msgid "Yes" +msgstr "Bəli" + +#: package/contents/ui/RulesEditor.qml:205 +#: package/contents/ui/ValueEditor.qml:59 +#, kde-format +msgid "No" +msgstr "1" + +#: package/contents/ui/RulesEditor.qml:207 +#: package/contents/ui/ValueEditor.qml:127 +#: package/contents/ui/ValueEditor.qml:134 +#, kde-format +msgid "%1 %" +msgstr "%1 %" + +#: package/contents/ui/RulesEditor.qml:209 +#, kde-format +msgctxt "Coordinates (x, y)" +msgid "(%1, %2)" +msgstr "(%1, %2)" + +#: package/contents/ui/RulesEditor.qml:211 +#, kde-format +msgctxt "Size (width, height)" +msgid "(%1, %2)" +msgstr "(%1, %2)" + +#: package/contents/ui/RulesList.qml:61 +#, kde-format +msgid "No rules for specific windows are currently set" +msgstr "Hal-hazırda xüsusi pəncərələr üçün qaydalar təyin edilməyib" + +#: package/contents/ui/RulesList.qml:69 +#, kde-format +msgid "Select the rules to export" +msgstr "İxrac etmək üçün qaydaları seçmək" + +#: package/contents/ui/RulesList.qml:73 +#, kde-format +msgid "Unselect All" +msgstr "Bütün seçimləri ləğv etmək" + +#: package/contents/ui/RulesList.qml:73 +#, kde-format +msgid "Select All" +msgstr "Hamısını seçmək" + +#: package/contents/ui/RulesList.qml:87 +#, kde-format +msgid "Save Rules" +msgstr "Qaydaları saxlamaq" + +#: package/contents/ui/RulesList.qml:98 +#, kde-format +msgid "Add New..." +msgstr "Yenisini əlavə etmək..." + +#: package/contents/ui/RulesList.qml:109 +#, kde-format +msgid "Import..." +msgstr "İdxal..." + +#: package/contents/ui/RulesList.qml:117 +#, kde-format +msgid "Cancel Export" +msgstr "İxracı dayandırmaq..." + +#: package/contents/ui/RulesList.qml:117 +#, kde-format +msgid "Export..." +msgstr "İxrac..." + +#: package/contents/ui/RulesList.qml:198 +#, kde-format +msgid "Edit" +msgstr "Düzəliş etmək" + +#: package/contents/ui/RulesList.qml:207 +#, kde-format +msgid "Delete" +msgstr "Silmək" + +#: package/contents/ui/RulesList.qml:220 +#, kde-format +msgid "Import Rules" +msgstr "Qaydaları idxal etmək" + +#: package/contents/ui/RulesList.qml:232 +#, kde-format +msgid "Export Rules" +msgstr "Qaydaları ixrac etmək" + +#: package/contents/ui/ValueEditor.qml:162 +#, kde-format +msgctxt "(x, y) coordinates separator in size/position" +msgid "x" +msgstr "x" + +#: rulesdialog.cpp:28 +#, kde-format +msgid "Edit Window-Specific Settings" +msgstr "Pəncərə xüsusi ayarlarına düzəliş etmək" + +#: rulesmodel.cpp:219 +#, kde-format +msgid "Settings for %1" +msgstr "%1 üçün ayarlar" + +#: rulesmodel.cpp:222 +#, kde-format +msgid "New window settings" +msgstr "Yeni pəncərər ayarları" + +#: rulesmodel.cpp:236 +#, kde-format +msgid "" +"You have specified the window class as unimportant.\n" +"This means the settings will possibly apply to windows from all " +"applications. If you really want to create a generic setting, it is " +"recommended you at least limit the window types to avoid special window " +"types." +msgstr "" +"Pəncərə sinifini əhəmiyyətsiz kimi göstərdiniz.\n" +"Bu o deməkdir ki, dəyişikliklər bütün istənilən pəncərəyə aid ediləcək. Əgər " +"siz ümumi xüsusiyyət yaratmaq istəyirsinizsə xüsusi sinifləri istisna " +"etməklə ən azı bir pəncərə sinifini göstərmənizi tövsiyə edirik." + +#: rulesmodel.cpp:366 +#, kde-format +msgid "Description" +msgstr "Təsviri" + +#: rulesmodel.cpp:366 rulesmodel.cpp:374 rulesmodel.cpp:382 rulesmodel.cpp:389 +#: rulesmodel.cpp:395 rulesmodel.cpp:403 rulesmodel.cpp:408 rulesmodel.cpp:414 +#, kde-format +msgid "Window matching" +msgstr "Pəncərə uyğunluğu" + +#: rulesmodel.cpp:374 +#, kde-format +msgid "Window class (application)" +msgstr "Pəncərə sinifi (tətbiq)" + +#: rulesmodel.cpp:382 +#, kde-format +msgid "Match whole window class" +msgstr "Bütün pəncərə sinifinə oxşar" + +#: rulesmodel.cpp:389 +#, kde-format +msgid "Whole window class" +msgstr "Bütün pəncərə sinifi" + +#: rulesmodel.cpp:395 +#, kde-format +msgid "Window types" +msgstr "Pəncərə növü" + +#: rulesmodel.cpp:403 +#, kde-format +msgid "Window role" +msgstr "Pəncərə rolu" + +#: rulesmodel.cpp:408 +#, kde-format +msgid "Window title" +msgstr "Pəncərə başlığı" + +#: rulesmodel.cpp:414 +#, kde-format +msgid "Machine (hostname)" +msgstr "Sistem (host_adı)" + +#: rulesmodel.cpp:420 +#, kde-format +msgid "Position" +msgstr "Mövqe" + +#: rulesmodel.cpp:420 rulesmodel.cpp:425 rulesmodel.cpp:430 rulesmodel.cpp:435 +#: rulesmodel.cpp:440 rulesmodel.cpp:453 rulesmodel.cpp:467 rulesmodel.cpp:472 +#: rulesmodel.cpp:477 rulesmodel.cpp:482 rulesmodel.cpp:487 rulesmodel.cpp:493 +#: rulesmodel.cpp:502 rulesmodel.cpp:507 rulesmodel.cpp:512 +#, kde-format +msgid "Size & Position" +msgstr "Ölçüsü və Mövqeyi" + +#: rulesmodel.cpp:425 +#, kde-format +msgid "Size" +msgstr "Ölçü" + +#: rulesmodel.cpp:430 +#, kde-format +msgid "Maximized horizontally" +msgstr "Üfüqi tam açma" + +#: rulesmodel.cpp:435 +#, kde-format +msgid "Maximized vertically" +msgstr "Şaquli tam açmaq" + +#: rulesmodel.cpp:440 +#, kde-format +msgid "Virtual Desktop" +msgstr "Virtual İş Masası" + +#: rulesmodel.cpp:453 +#, kde-format +msgid "Activity" +msgstr "İş Otağı" + +#: rulesmodel.cpp:467 +#, kde-format +msgid "Screen" +msgstr "Ekran" + +#: rulesmodel.cpp:472 +#, kde-format +msgid "Fullscreen" +msgstr "Tam ekran" + +#: rulesmodel.cpp:477 +#, kde-format +msgid "Minimized" +msgstr "Yığılmış" + +#: rulesmodel.cpp:482 +#, kde-format +msgid "Shaded" +msgstr "Başlığa yığılmış" + +#: rulesmodel.cpp:487 +#, kde-format +msgid "Initial placement" +msgstr "İlkin yerləşdirmə" + +#: rulesmodel.cpp:493 +#, kde-format +msgid "Ignore requested geometry" +msgstr "Tələb olunan həndəsi quruluşu yox say" + +#: rulesmodel.cpp:495 +#, kde-format +msgid "" +"Windows can ask to appear in a certain position.\n" +"By default this overrides the placement strategy\n" +"what might be nasty if the client abuses the feature\n" +"to unconditionally popup in the middle of your screen." +msgstr "" +"Pəncərə müəyyən bir vəziyyətdə görünməyi tələb edə\n" +"bilər. Standart olaraq bu yerləşdirmə strategiyasını\n" +"inkar edir və bu vəziyyət, tətbiqin, pəncərənin ekranın\n" +"mərkəzində peyda olması imkanından sui istifadə etdiyi\n" +"üçün bezdirici ola bilər." + +#: rulesmodel.cpp:502 +#, kde-format +msgid "Minimum Size" +msgstr "Minimum ölçü" + +#: rulesmodel.cpp:507 +#, kde-format +msgid "Maximum Size" +msgstr "Maksimum ölçü" + +#: rulesmodel.cpp:512 +#, kde-format +msgid "Obey geometry restrictions" +msgstr "Həndəsi məhdudiyyətlərə əməl etmək" + +#: rulesmodel.cpp:514 +#, kde-format +msgid "" +"Eg. terminals or video players can ask to keep a certain aspect ratio\n" +"or only grow by values larger than one\n" +"(eg. by the dimensions of one character).\n" +"This may be pointless and the restriction prevents arbitrary dimensions\n" +"like your complete screen area." +msgstr "" +"Məs., terminallar və video oynadıcılar xüsusi bir ölçü nisbətini saxlamağı " +"tələb edə bilər və ya pəncərənin ölçüsünün\n" +"bir piksel addımı ilə dəyişməsinə imkan verməmək\n" +"(məs., terminal pəncərəsi həmişə sətir və simvolların\n" +"tam sayından ibarət olsun). Bu parametrin aktiv edilməsi\n" +"pəncərə ilə ekran kənarları arasında boşluğun qalmaması\n" +"üçün pəncərənin tam açılmasına mane ola bilər." + +#: rulesmodel.cpp:523 +#, kde-format +msgid "Keep above" +msgstr "Üstündə tutmaq" + +#: rulesmodel.cpp:523 rulesmodel.cpp:528 rulesmodel.cpp:533 rulesmodel.cpp:539 +#: rulesmodel.cpp:545 rulesmodel.cpp:551 +#, kde-format +msgid "Arrangement & Access" +msgstr "Düzülüş və Giriş" + +#: rulesmodel.cpp:528 +#, kde-format +msgid "Keep below" +msgstr "Altında tutmaq" + +#: rulesmodel.cpp:533 +#, kde-format +msgid "Skip taskbar" +msgstr "Tapşırıq panelində göstərməmək" + +#: rulesmodel.cpp:535 +#, kde-format +msgid "Window shall (not) appear in the taskbar." +msgstr "Pəncərə tapşırıq panelində görünməlidir/görülnməməlidir." + +#: rulesmodel.cpp:539 +#, kde-format +msgid "Skip pager" +msgstr "İş Masası dəyişdiricisində göstərməmək" + +#: rulesmodel.cpp:541 +#, kde-format +msgid "Window shall (not) appear in the manager for virtual desktops" +msgstr "Pəncərə İş Masası dəyişdiricisində görünməlidir/görülnməməlidir." + +#: rulesmodel.cpp:545 +#, kde-format +msgid "Skip switcher" +msgstr "Pəncərəni dəyişdirirkən göstərmək" + +#: rulesmodel.cpp:547 +#, kde-format +msgid "Window shall (not) appear in the Alt+Tab list" +msgstr "Pəncərə pəncərə dəyişdiricisində görünməlidir/görülnməməlidir." + +#: rulesmodel.cpp:551 +#, kde-format +msgid "Shortcut" +msgstr "Qısayol" + +#: rulesmodel.cpp:557 +#, kde-format +msgid "No titlebar and frame" +msgstr "Başlıq və çərçivə yoxdur" + +#: rulesmodel.cpp:557 rulesmodel.cpp:562 rulesmodel.cpp:568 rulesmodel.cpp:573 +#: rulesmodel.cpp:578 rulesmodel.cpp:589 rulesmodel.cpp:600 rulesmodel.cpp:608 +#: rulesmodel.cpp:621 rulesmodel.cpp:626 rulesmodel.cpp:632 rulesmodel.cpp:637 +#, kde-format +msgid "Appearance & Fixes" +msgstr "Xarici görünüş və Səhv düzəlişi" + +#: rulesmodel.cpp:562 +#, kde-format +msgid "Titlebar color scheme" +msgstr "Başlıq rəngi sxemi" + +#: rulesmodel.cpp:568 +#, kde-format +msgid "Active opacity" +msgstr "Aktiv pəncərənin şəffaflığı" + +#: rulesmodel.cpp:573 +#, kde-format +msgid "Inactive opacity" +msgstr "Qeyri-aktiv pəncərənin şəffaflığı" + +#: rulesmodel.cpp:578 +#, kde-format +msgid "Focus stealing prevention" +msgstr "Fokus oğurlanmasını əngəlləmək" + +#: rulesmodel.cpp:580 +#, kde-format +msgid "" +"KWin tries to prevent windows from taking the focus\n" +"(\"activate\") while you're working in another window,\n" +"but this may sometimes fail or superact.\n" +"\"None\" will unconditionally allow this window to get the focus while\n" +"\"Extreme\" will completely prevent it from taking the focus." +msgstr "" +"İstifadəçi cari pəncərədə işləyərkən, KWin, fokusun başqa\n" +"pəncərəyə keçməsinə mane olmağa çalışır (\"aktivləşdir\"),\n" +"lakin bu bəzən işləməyə bilər və bəzən də aqressiv işləyə\n" +"bilər. Bu funksiyanın bu pəncərə üçün işləmə səviyyəsini\n" +"tənzimləmək üçün bu ayardan istifadə edin.\n" +"\n" +"\"Heç biri\" bütün hallarda bu pəncərənin fokuslanmasına\n" +"imkan verir. \"Müstəsna\" bu pəncərənin fokuslanmasının\n" +"tam qarşısını alır." + +#: rulesmodel.cpp:589 +#, kde-format +msgid "Focus protection" +msgstr "Fokusun qorunması" + +#: rulesmodel.cpp:591 +#, kde-format +msgid "" +"This controls the focus protection of the currently active window.\n" +"None will always give the focus away,\n" +"Extreme will keep it.\n" +"Otherwise it's interleaved with the stealing prevention\n" +"assigned to the window that wants the focus." +msgstr "" +"Bu parametr pəncərənin fokusunun qorunması \n" +"üstünlüyünü təyin edir.\n" +"\"Heç biri\" həmişə fokusu dəyişməyə imkan verir.\n" +"\"Müstəsna\" fokusu dəyişməyə imkan vermir.\n" +"Əks halda fokusun fokuslanma istəyən pəncərəyə\n" +"keçməsinin qarşısını alır." + +#: rulesmodel.cpp:600 +#, kde-format +msgid "Accept focus" +msgstr "Fokuslaşmanı qəbul etmək" + +#: rulesmodel.cpp:602 +#, kde-format +msgid "" +"Windows may prevent to get the focus (activate) when being clicked.\n" +"On the other hand you might wish to prevent a window\n" +"from getting focused on a mouse click." +msgstr "" +"Pəncərələr, kliklədikdə fokus almağa mane ola bilər.Digər\n" +"tərəfdən bir pəncərənin bir siçan kliki ilə fokuslanmasının\n" +"qarşısını almaq istəyə bilərsiniz." + +#: rulesmodel.cpp:608 +#, kde-format +msgid "Ignore global shortcuts" +msgstr "Qlobal Qısayolları yox saymaq" + +#: rulesmodel.cpp:610 +#, kde-format +msgid "" +"When used, a window will receive\n" +"all keyboard inputs while it is active, including Alt+Tab etc.\n" +"This is especially interesting for emulators or virtual machines.\n" +"\n" +"Be warned:\n" +"you won't be able to Alt+Tab out of the window\n" +"nor use any other global shortcut (such as Alt+F2 to show KRunner)\n" +"while it's active!" +msgstr "" +"İstifadə olunduğu zaman, pəncərə aktiv olarsa\n" +"bütün klaviatura girişlərini qəbul edəcəkdir\n" +"(Alt+Tab və digəriləri daxil). Bu emulyator və\n" +"virtual sistemlər üçün lazım ola bilər.\n" +"\n" +"Xəbərdarlıq:\n" +"Bu parametr aktiv olduğu müddətdə, siz Alt+Tab\n" +"(həmçinin KRunner-i başlatmaq üçün Alt+F2)\n" +"qlobal qısayolundan istifadə edə bilməyəcəksiniz!" + +#: rulesmodel.cpp:621 +#, kde-format +msgid "Closeable" +msgstr "Bağlana bilən" + +#: rulesmodel.cpp:626 +#, kde-format +msgid "Set window type" +msgstr "Pəncərə növünü seçin" + +#: rulesmodel.cpp:632 +#, kde-format +msgid "Desktop file name" +msgstr ".desktop faylı" + +#: rulesmodel.cpp:637 +#, kde-format +msgid "Block compositing" +msgstr "Effektləri əngəlləmək" + +#: rulesmodel.cpp:717 +#, kde-format +msgid "Normal Window" +msgstr "Normal Pəncərə" + +#: rulesmodel.cpp:718 +#, kde-format +msgid "Dialog Window" +msgstr "Dialoq Pəncərəsi" + +#: rulesmodel.cpp:719 +#, kde-format +msgid "Utility Window" +msgstr "Alətlər Pəncərəsi" + +#: rulesmodel.cpp:720 +#, kde-format +msgid "Dock (panel)" +msgstr "Dok Panel" + +#: rulesmodel.cpp:721 +#, kde-format +msgid "Toolbar" +msgstr "Alətlər paneli" + +#: rulesmodel.cpp:722 +#, kde-format +msgid "Torn-Off Menu" +msgstr "Qopan menyu" + +#: rulesmodel.cpp:723 +#, kde-format +msgid "Splash Screen" +msgstr "Açılış ekranı" + +#: rulesmodel.cpp:724 +#, kde-format +msgid "Desktop" +msgstr "İş Masası" + +#. i18n("Unmanaged Window") }, deprecated +#: rulesmodel.cpp:726 +#, kde-format +msgid "Standalone Menubar" +msgstr "Müstəqil menyu" + +#: rulesmodel.cpp:741 +#, kde-format +msgid "All Desktops" +msgstr "Bütün İş Masaları" + +#: rulesmodel.cpp:754 +#, kde-format +msgid "All Activities" +msgstr "Bütün İş Otaqları" + +#: rulesmodel.cpp:775 +#, kde-format +msgid "Default" +msgstr "Standart" + +#: rulesmodel.cpp:776 +#, kde-format +msgid "No Placement" +msgstr "Yerləşmə yoxdur" + +#: rulesmodel.cpp:777 +#, kde-format +msgid "Minimal Overlapping" +msgstr "Minimal örtmə" + +#: rulesmodel.cpp:778 +#, kde-format +msgid "Maximized" +msgstr "Tam açılan" + +#: rulesmodel.cpp:779 +#, kde-format +msgid "Cascaded" +msgstr "Kaskad" + +#: rulesmodel.cpp:780 +#, kde-format +msgid "Centered" +msgstr "Mərkəzdə" + +#: rulesmodel.cpp:781 +#, kde-format +msgid "Random" +msgstr "Təsadüfi" + +#: rulesmodel.cpp:782 +#, kde-format +msgid "In Top-Left Corner" +msgstr "Yuxarı sol küncdə" + +#: rulesmodel.cpp:783 +#, kde-format +msgid "Under Mouse" +msgstr "Kursorun altında" + +#: rulesmodel.cpp:784 +#, kde-format +msgid "On Main Window" +msgstr "Əsas Pəncərədə" + +#: rulesmodel.cpp:792 +#, kde-format +msgid "None" +msgstr "Heç biri" + +#: rulesmodel.cpp:793 +#, kde-format +msgid "Low" +msgstr "Zəif üstünlük" + +#: rulesmodel.cpp:794 +#, kde-format +msgid "Normal" +msgstr "Normal üstünlük" + +#: rulesmodel.cpp:795 +#, kde-format +msgid "High" +msgstr "Yüksək üstünlük" + +#: rulesmodel.cpp:796 +#, kde-format +msgid "Extreme" +msgstr "Müstəsna" \ No newline at end of file diff --git a/po/az/kcm_kwintabbox.po b/po/az/kcm_kwintabbox.po new file mode 100644 index 0000000..193a443 --- /dev/null +++ b/po/az/kcm_kwintabbox.po @@ -0,0 +1,228 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the kwin package. +# +# Xəyyam Qocayev , 2020. +msgid "" +msgstr "" +"Project-Id-Version: kwin\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-08-15 02:23+0200\n" +"PO-Revision-Date: 2020-07-22 12:32+0400\n" +"Last-Translator: Xəyyam Qocayev \n" +"Language-Team: Azerbaijani \n" +"Language: az\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Lokalize 20.04.3\n" + +#: kwintabboxconfigform.cpp:77 +#, kde-format +msgid "KWin" +msgstr "KWin" + +#: layoutpreview.cpp:147 +#, kde-format +msgctxt "An example Desktop Name" +msgid "Desktop 1" +msgstr "İş Masası 1" + +#: main.cpp:65 +#, kde-format +msgid "Main" +msgstr "Əsas" + +#: main.cpp:66 +#, kde-format +msgid "Alternative" +msgstr "Alternativ" + +#: main.cpp:68 +#, kde-format +msgid "Get New Task Switchers..." +msgstr "Yeni Vizual Pəncərə Dəyişdiricilərini Almaq..." + +#: main.cpp:78 +#, kde-format +msgid "" +"Focus policy settings limit the functionality of navigating through windows." +msgstr "" +"Fokus siyasəti parametrləri, pəncərələr üzrə səyahət funksiyasını " +"məhdudlaşdırır." + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_3) +#: main.ui:32 +#, kde-format +msgid "Content" +msgstr "Tərkibi" + +#. i18n: ectx: property (text), widget (QCheckBox, showDesktop) +#: main.ui:41 +#, kde-format +msgid "Include \"Show Desktop\" icon" +msgstr "\"İş Masasını Göstərmək\" ikonunu daxil etmək" + +#. i18n: ectx: property (text), item, widget (KComboBox, switchingModeCombo) +#: main.ui:55 +#, kde-format +msgid "Recently used" +msgstr "Son istifadə olunan" + +#. i18n: ectx: property (text), item, widget (KComboBox, switchingModeCombo) +#: main.ui:60 +#, kde-format +msgid "Stacking order" +msgstr "Yerləşmə sırası" + +#. i18n: ectx: property (text), widget (QCheckBox, oneAppWindow) +#: main.ui:68 +#, kde-format +msgid "Only one window per application" +msgstr "Hər tətbiq üçün bir pəncərə" + +#. i18n: ectx: property (text), widget (QLabel, label_8) +#: main.ui:78 +#, kde-format +msgid "Sort order:" +msgstr "Çeşidləmə qaydası:" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox) +#: main.ui:104 +#, kde-format +msgid "Filter windows by" +msgstr "Pəncərələri buna görə çeşidləmək" + +#. i18n: ectx: property (text), widget (QCheckBox, filterDesktops) +#: main.ui:113 +#, kde-format +msgid "Virtual desktops" +msgstr "Virtual İş Masaları" + +#. i18n: ectx: property (text), widget (QRadioButton, currentDesktop) +#: main.ui:157 +#, kde-format +msgid "Current desktop" +msgstr "Cari İş Masasından" + +#. i18n: ectx: property (text), widget (QRadioButton, otherDesktops) +#: main.ui:164 +#, kde-format +msgid "All other desktops" +msgstr "Bütün digər İş Masalarından" + +#. i18n: ectx: property (text), widget (QCheckBox, filterActivities) +#: main.ui:174 +#, kde-format +msgid "Activities" +msgstr "İş Otaqları" + +#. i18n: ectx: property (text), widget (QRadioButton, currentActivity) +#: main.ui:218 +#, kde-format +msgid "Current activity" +msgstr "Cari İş Otağından" + +#. i18n: ectx: property (text), widget (QRadioButton, otherActivities) +#: main.ui:225 +#, kde-format +msgid "All other activities" +msgstr "Bütün digər İş Otaqlarından" + +#. i18n: ectx: property (text), widget (QCheckBox, filterScreens) +#: main.ui:235 +#, kde-format +msgid "Screens" +msgstr "Ekranlar" + +#. i18n: ectx: property (text), widget (QRadioButton, currentScreen) +#: main.ui:279 +#, kde-format +msgid "Current screen" +msgstr "Cari ekrandan" + +#. i18n: ectx: property (text), widget (QRadioButton, otherScreens) +#: main.ui:286 +#, kde-format +msgid "All other screens" +msgstr "Bütün digər ekranlardan" + +#. i18n: ectx: property (text), widget (QCheckBox, filterMinimization) +#: main.ui:296 +#, kde-format +msgid "Minimization" +msgstr "Minimallaşdırma" + +#. i18n: ectx: property (text), widget (QRadioButton, visibleWindows) +#: main.ui:340 +#, kde-format +msgid "Visible windows" +msgstr "Görünən pəncərələr" + +#. i18n: ectx: property (text), widget (QRadioButton, hiddenWindows) +#: main.ui:347 +#, kde-format +msgid "Hidden windows" +msgstr "Gizli Pəncərələr" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_4) +#: main.ui:386 +#, kde-format +msgid "Shortcuts" +msgstr "Qısayollar" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: main.ui:395 main.ui:438 +#, kde-format +msgid "Forward" +msgstr "İrəli" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: main.ui:418 +#, kde-format +msgid "All windows" +msgstr "Bütün pəncərələr" + +#. i18n: ectx: property (text), widget (QLabel, label_4) +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: main.ui:428 main.ui:448 +#, kde-format +msgid "Reverse" +msgstr "Əks sıralama" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: main.ui:470 +#, kde-format +msgid "Current application" +msgstr "Cari tətbiq pəncərəsi" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#: main.ui:489 +#, kde-format +msgid "Visualization" +msgstr "Vizualizasiya" + +#. i18n: ectx: property (toolTip), widget (QComboBox, effectCombo) +#: main.ui:519 +#, kde-format +msgid "The effect to replace the list window when desktop effects are active." +msgstr "" +"İş Masası effektləri aktiv olduqda istifadə olunan pəncərə dəyişdirmə " +"vizualizasiyası." + +#. i18n: ectx: property (toolTip), widget (QCheckBox, kcfg_HighlightWindows) +#: main.ui:549 +#, kde-format +msgid "" +"The currently selected window will be highlighted by fading out all other " +"windows. This option requires desktop effects to be active." +msgstr "" +"Hal-hazırda seçilmiş pəncərə bütün digər pəncərələrin solması ilə " +"fərqlənəcəkdir. Bu seçim İş Masası effektlərin aktiv olmasını tələb edir." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_HighlightWindows) +#: main.ui:552 +#, kde-format +msgid "Show selected window" +msgstr "Seçilmiş pəncərəni göstərmək" \ No newline at end of file diff --git a/po/az/kcmkwincommon.po b/po/az/kcmkwincommon.po new file mode 100644 index 0000000..b578ac1 --- /dev/null +++ b/po/az/kcmkwincommon.po @@ -0,0 +1,81 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the kwin package. +# +# Xəyyam Qocayev , 2020. +msgid "" +msgstr "" +"Project-Id-Version: kwin\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-08-08 02:22+0200\n" +"PO-Revision-Date: 2020-05-15 00:16+0400\n" +"Last-Translator: Xəyyam Qocayev \n" +"Language-Team: Azerbaijani\n" +"Language: az\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Lokalize 20.04.0\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Xəyyam" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "xxmn77@gmail.com" + +#: effectsmodel.cpp:51 +#, kde-format +msgctxt "Category of Desktop Effects, used as section header" +msgid "Accessibility" +msgstr "Xüsusi imkanlar" + +#: effectsmodel.cpp:52 +#, kde-format +msgctxt "Category of Desktop Effects, used as section header" +msgid "Appearance" +msgstr "Xarici Görünüş" + +#: effectsmodel.cpp:53 +#, kde-format +msgctxt "Category of Desktop Effects, used as section header" +msgid "Focus" +msgstr "Fokus" + +#: effectsmodel.cpp:54 +#, kde-format +msgctxt "Category of Desktop Effects, used as section header" +msgid "Show Desktop Animation" +msgstr "İş Masası animasiyalarını göstərmək" + +#: effectsmodel.cpp:55 +#, kde-format +msgctxt "Category of Desktop Effects, used as section header" +msgid "Tools" +msgstr "Alətlər" + +#: effectsmodel.cpp:56 +#, kde-format +msgctxt "Category of Desktop Effects, used as section header" +msgid "Virtual Desktop Switching Animation" +msgstr "Virtual İş Masalarının çevirilməsi animasiyası" + +#: effectsmodel.cpp:57 +#, kde-format +msgctxt "Category of Desktop Effects, used as section header" +msgid "Window Management" +msgstr "Pəncərələrin İdarə edilməsi" + +#: effectsmodel.cpp:58 +#, kde-format +msgctxt "Category of Desktop Effects, used as section header" +msgid "Window Open/Close Animation" +msgstr "Pəncərənin açılış/bağlanış animasiyası" + +#: effectsmodel.cpp:226 +#, kde-format +msgid "KWin development team" +msgstr "KWin tərtibatşılar Komandası" \ No newline at end of file diff --git a/po/az/kcmkwincompositing.po b/po/az/kcmkwincompositing.po new file mode 100644 index 0000000..641e304 --- /dev/null +++ b/po/az/kcmkwincompositing.po @@ -0,0 +1,235 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the kwin package. +# +# Xəyyam Qocayev , 2020. +msgid "" +msgstr "" +"Project-Id-Version: kwin\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-08-08 02:22+0200\n" +"PO-Revision-Date: 2020-05-20 17:25+0400\n" +"Last-Translator: Xəyyam Qocayev \n" +"Language-Team: Azerbaijani\n" +"Language: az\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Lokalize 20.04.0\n" + +#. i18n: ectx: property (text), widget (KMessageWidget, glCrashedWarning) +#: compositing.ui:32 +#, kde-format +msgid "" +"OpenGL compositing (the default) has crashed KWin in the past.\n" +"This was most likely due to a driver bug.\n" +"If you think that you have meanwhile upgraded to a stable driver,\n" +"you can reset this protection but be aware that this might result in an " +"immediate crash!\n" +"Alternatively, you might want to use the XRender backend instead." +msgstr "" +"Əvvələr OpenGL qrafik effekti (standart) artıq KWin pozulmasına səbəb " +"olmuşdu.\n" +"Çox güman ki, bu sürücülərdəki xəta səbəbindən baş verir.\n" +"Əgər sürücü artıq stabil versiyaya yenilənibsə,\n" +"bu qorumanı sıfırlaya bilərsiniz. Ancaq nəzərə alın ki, bu ani çökməyə səbəb " +"ola bilər!\n" +"Alternativ variant kimi XRender istifadə etmək olar." + +#. i18n: ectx: property (text), widget (KMessageWidget, scaleWarning) +#: compositing.ui:45 +#, kde-format +msgid "" +"Scale method \"Accurate\" is not supported by all hardware and can cause " +"performance regressions and rendering artifacts." +msgstr "" +"\"Dəqiq hamarlama\" miqyas rejimini heç də bütün video kartlar dəstəkləmir " +"və bu sistemin işində yavaşlamalara və görüntüdə pozulmalara səbəb ola bilər." + +#. i18n: ectx: property (text), widget (KMessageWidget, windowThumbnailWarning) +#: compositing.ui:68 +#, kde-format +msgid "" +"Keeping the window thumbnail always interferes with the minimized state of " +"windows. This can result in windows not suspending their work when minimized." +msgstr "" +"Pəncərələrin miniatürlərinin daima yenilənməsi, hətta pəncərələrin büküldüyü " +"halda belə tərkiblərinin alınmasını tələb edir. Bükülü vəziyyətdə belə " +"pəncərələrin görüntü işləmləri öz içini davam etdirir." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Enabled) +#: compositing.ui:80 +#, kde-format +msgid "Enable compositor on startup" +msgstr "Sistemə girişdə qrafik effektləri də başlatmaq" + +#. i18n: ectx: property (text), widget (QLabel, animationSpeedLabel) +#: compositing.ui:87 +#, kde-format +msgid "Animation speed:" +msgstr "Animasiya tezliyi:" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: compositing.ui:124 +#, kde-format +msgid "Very slow" +msgstr "Çox yavaş" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: compositing.ui:144 +#, kde-format +msgid "Instant" +msgstr "Cəld" + +#. i18n: ectx: property (text), widget (QLabel, scaleMethodLabel) +#: compositing.ui:156 +#, kde-format +msgid "Scale method:" +msgstr "Miqyaslama rejimi" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_XRenderSmoothScale) +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glTextureFilter) +#: compositing.ui:166 compositing.ui:180 +#, kde-format +msgid "Crisp" +msgstr "Qıvraq" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_XRenderSmoothScale) +#: compositing.ui:171 +#, kde-format +msgid "Smooth (slower)" +msgstr "Az hamarlanmış" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glTextureFilter) +#: compositing.ui:185 +#, kde-format +msgid "Smooth" +msgstr "Hamarlanmış" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glTextureFilter) +#: compositing.ui:190 +#, kde-format +msgid "Accurate" +msgstr "Dəqiq hamarlanmış" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: compositing.ui:207 +#, kde-format +msgid "Rendering backend:" +msgstr "Görüntü mexanizmi:" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: compositing.ui:224 +#, kde-format +msgid "Tearing prevention (\"vsync\"):" +msgstr "Qırılmaların əngəllənməsi (\"vsync\"):" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glPreferBufferSwap) +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_HiddenPreviews) +#: compositing.ui:232 compositing.ui:268 +#, kde-format +msgid "Never" +msgstr "Heç vaxt" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glPreferBufferSwap) +#: compositing.ui:237 +#, kde-format +msgid "Automatic" +msgstr "Avtomatik" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glPreferBufferSwap) +#: compositing.ui:242 +#, kde-format +msgid "Only when cheap" +msgstr "Ən az itki ilə" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glPreferBufferSwap) +#: compositing.ui:247 +#, kde-format +msgid "Full screen repaints" +msgstr "Tam ekran görüntü" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glPreferBufferSwap) +#: compositing.ui:252 +#, kde-format +msgid "Re-use screen content" +msgstr "Təkrar istifadə" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: compositing.ui:260 +#, kde-format +msgid "Keep window thumbnails:" +msgstr "Pəncərə miniatürləri yaratmaq:" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_HiddenPreviews) +#: compositing.ui:273 +#, kde-format +msgid "Only for Shown Windows" +msgstr "Yalnız görünən pəncərərərlər üçün" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_HiddenPreviews) +#: compositing.ui:278 +#, kde-format +msgid "Always" +msgstr "Həmişə" + +#. i18n: ectx: property (toolTip), widget (QCheckBox, kcfg_WindowsBlockCompositing) +#: compositing.ui:288 +#, kde-format +msgid "" +"Applications can set a hint to block compositing when the window is open.\n" +" This brings performance improvements for e.g. games.\n" +" The setting can be overruled by window-specific rules." +msgstr "" +"Tətbiq açıq pəncərəsində qrafik effektlərin söndürülməsini təklif edə " +"bilər.\n" +"Bu sistemin işini yaxşılaşdıra bilər (məs., oyun zamanı).\n" +"Bu qayda ayrıca pəncə üçün də tətbiq edilə bilər." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_WindowsBlockCompositing) +#: compositing.ui:291 +#, kde-format +msgid "Allow applications to block compositing" +msgstr "Tətbiqə qrafik effektləri söndürməyə icazə vermək" + +#: main.cpp:80 +#, kde-format +msgid "Re-enable OpenGL detection" +msgstr "OpenGL-i təkrara aktiv etmək" + +#: main.cpp:132 +#, kde-format +msgid "" +"\"Only when cheap\" only prevents tearing for full screen changes like a " +"video." +msgstr "" +"\"Ən az itki ilə\" rejimində qırılmanın qarşısı yalnız tam ekran yenilənən " +"zaman alınır, məsələn video göstərilən zaman." + +#: main.cpp:136 +#, kde-format +msgid "\"Full screen repaints\" can cause performance problems." +msgstr "\"Tam ekran görüntü\" rejimi sistemin işini ləngıdə bilər." + +#: main.cpp:140 +#, kde-format +msgid "" +"\"Re-use screen content\" causes severe performance problems on MESA drivers." +msgstr "" +"\"Təkrar istifadə\" rejimi MESA sürücüsünün işi zamanı sistemin işini ciddi " +"ləngidə bilər." + +#: main.cpp:160 +#, kde-format +msgid "OpenGL 3.1" +msgstr "OpenGL 3.1" + +#: main.cpp:161 +#, kde-format +msgid "OpenGL 2.0" +msgstr "OpenGL 2.0" + +#: main.cpp:162 +#, kde-format +msgid "XRender" +msgstr "XRender" \ No newline at end of file diff --git a/po/az/kcmkwinscreenedges.po b/po/az/kcmkwinscreenedges.po new file mode 100644 index 0000000..9f0e372 --- /dev/null +++ b/po/az/kcmkwinscreenedges.po @@ -0,0 +1,237 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the kwin package. +# +# Xəyyam Qocayev , 2020. +msgid "" +msgstr "" +"Project-Id-Version: kwin\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-08-08 02:22+0200\n" +"PO-Revision-Date: 2020-08-12 09:21+0400\n" +"Last-Translator: Kheyyam Gojayev \n" +"Language-Team: Azerbaijan\n" +"Language: az\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Lokalize 20.04.3\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Xəyyam Qocayev" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "xxmn77@gmail.com" + +#: main.cpp:123 touch.cpp:122 +#, kde-format +msgid "No Action" +msgstr "Hərəkətsiz" + +#: main.cpp:124 touch.cpp:123 +#, kde-format +msgid "Show Desktop" +msgstr "İş masasını göstərmək" + +#: main.cpp:125 touch.cpp:124 +#, kde-format +msgid "Lock Screen" +msgstr "Ekranı kilidləmək" + +#: main.cpp:126 touch.cpp:125 +#, kde-format +msgid "Show KRunner" +msgstr "KRunner göstərmək" + +#: main.cpp:127 touch.cpp:126 +#, kde-format +msgid "Activity Manager" +msgstr "Fəaliyyət Meneceri" + +#: main.cpp:128 touch.cpp:127 +#, kde-format +msgid "Application Launcher" +msgstr "Tətbiq Başladıcı" + +#: main.cpp:132 touch.cpp:131 +#, kde-format +msgid "%1 - All Desktops" +msgstr "%1 - Bütün İş masaları" + +#: main.cpp:133 touch.cpp:132 +#, kde-format +msgid "%1 - Current Desktop" +msgstr "%1 - Hazırkı İş masası" + +#: main.cpp:134 touch.cpp:133 +#, kde-format +msgid "%1 - Current Application" +msgstr "%1 - Cari tətbiq" + +#: main.cpp:137 touch.cpp:136 +#, kde-format +msgid "%1 - Cube" +msgstr "%1 - Kub" + +#: main.cpp:138 touch.cpp:137 +#, kde-format +msgid "%1 - Cylinder" +msgstr "%1 - Silindir" + +#: main.cpp:139 touch.cpp:138 +#, kde-format +msgid "%1 - Sphere" +msgstr "%1 - Kürə" + +#: main.cpp:141 touch.cpp:140 +#, kde-format +msgid "Toggle window switching" +msgstr "Pəncərə dəyişmə açarı" + +#: main.cpp:142 touch.cpp:141 +#, kde-format +msgid "Toggle alternative window switching" +msgstr "Əlavə pəncərə dəyişmə açarı" + +#. i18n: ectx: property (text), widget (QLabel, infoLabel) +#: main.ui:23 +#, kde-format +msgid "" +"You can trigger an action by pushing the mouse cursor against the " +"corresponding screen edge or corner." +msgstr "" +"Siçanın kursorunu ekranın kənarına və ya küncünə doğru itərək müəyyən " +"fəaliyyəti həyata keçirə bilərsiniz." + +#. i18n: ectx: property (text), widget (QLabel, quickMaximizeLabel) +#: main.ui:67 +#, kde-format +msgid "&Maximize:" +msgstr "&Böyütmək:" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ElectricBorderMaximize) +#: main.ui:77 +#, kde-format +msgid "Windows dragged to top edge" +msgstr "Pəncərələr üst kənara sürükləndi" + +#. i18n: ectx: property (text), widget (QLabel, quickTileLabel) +#: main.ui:84 +#, kde-format +msgid "&Tile:" +msgstr "&Başlıq:" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ElectricBorderTiling) +#: main.ui:94 +#, kde-format +msgid "Windows dragged to left or right edge" +msgstr "Pəncərələr sol və ya sağ kənarlara sürükləndi" + +#. i18n: ectx: property (text), widget (QLabel, electricBorderCornerRatioLabel) +#: main.ui:101 +#, kde-format +msgid "Trigger &quarter tiling in:" +msgstr "Pəncərənin ekranın dörddə bir hissəsi qədər açılan sahəsi:" + +#. i18n: ectx: property (suffix), widget (QSpinBox, electricBorderCornerRatioSpin) +#: main.ui:116 +#, no-c-format, kde-format +msgid "%" +msgstr "%" + +#. i18n: ectx: property (prefix), widget (QSpinBox, electricBorderCornerRatioSpin) +#: main.ui:119 +#, kde-format +msgid "Outer " +msgstr "Yuxarı və aşağı kənarlar " + +#. i18n: ectx: property (text), widget (QLabel, label_1) +#: main.ui:135 +#, kde-format +msgid "of the screen" +msgstr "ekranın kənarları" + +#. i18n: ectx: property (toolTip), widget (QLabel, desktopSwitchLabel) +#: main.ui:144 +#, kde-format +msgid "" +"Change desktop when the mouse cursor is pushed against the edge of the screen" +msgstr "Kursoru ekranın kənarına toxundurduqda başqa iş masasına keçmək" + +#. i18n: ectx: property (text), widget (QLabel, desktopSwitchLabel) +#: main.ui:147 +#, kde-format +msgid "&Switch desktop on edge:" +msgstr "Kursoru ekranın kənarına hərəkət etdirdikdə iş ma&sasını dəyişmək:" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_ElectricBorders) +#: main.ui:158 +#, kde-format +msgctxt "Switch desktop on edge" +msgid "Disabled" +msgstr "Qeyri-aktiv" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_ElectricBorders) +#: main.ui:163 +#, kde-format +msgid "Only When Moving Windows" +msgstr "Yalnız pəncərələrin yerini dəyişdikdə" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_ElectricBorders) +#: main.ui:168 +#, kde-format +msgid "Always Enabled" +msgstr "Həmişə aktiv" + +#. i18n: ectx: property (toolTip), widget (QLabel, activationDelayLabel) +#: main.ui:176 +#, kde-format +msgid "" +"Amount of time required for the mouse cursor to be pushed against the edge " +"of the screen before the action is triggered" +msgstr "" +"Hərəkətə başlamazdan əvvəl kursorun ekranın kənarına toxunması üçün tələb " +"olunan vaxt müddəti" + +#. i18n: ectx: property (text), widget (QLabel, activationDelayLabel) +#: main.ui:179 +#, kde-format +msgid "Activation &delay:" +msgstr "Aktivləşmə müddəti:" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_ElectricBorderDelay) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_ElectricBorderCooldown) +#: main.ui:189 main.ui:224 +#, kde-format +msgid " ms" +msgstr " ms" + +#. i18n: ectx: property (toolTip), widget (QLabel, triggerCooldownLabel) +#: main.ui:208 +#, kde-format +msgid "" +"Amount of time required after triggering an action until the next trigger " +"can occur" +msgstr "" +"Kursorun ekranın kənarına toxunması ilə müvafiq fəaliyyətin başlaması " +"arasındakı vaxt" + +#. i18n: ectx: property (text), widget (QLabel, triggerCooldownLabel) +#: main.ui:211 +#, kde-format +msgid "&Reactivation delay:" +msgstr "Tək&rar fəaliyyətlər arasıdakı vaxt müddəti:" + +#. i18n: ectx: property (text), widget (QLabel, label_1) +#: touch.ui:17 +#, kde-format +msgid "" +"You can trigger an action by swiping from the screen edge towards the center " +"of the screen." +msgstr "" +"Fəaliyyəti icra etmək üçü ekran üzrə ekranın müvafiq kənarından mərkəzinə " +"doğru sürüşdürə bilərsiniz" \ No newline at end of file diff --git a/po/az/kcmkwm.po b/po/az/kcmkwm.po new file mode 100644 index 0000000..580a2d6 --- /dev/null +++ b/po/az/kcmkwm.po @@ -0,0 +1,1389 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the kwin package. +# +# Xəyyam Qocayev , 2020. +msgid "" +msgstr "" +"Project-Id-Version: kwin\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-08-29 02:26+0200\n" +"PO-Revision-Date: 2020-09-03 00:02+0400\n" +"Last-Translator: Kheyyam Gojayev \n" +"Language-Team: Azerbaijani \n" +"Language: az\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Lokalize 20.08.0\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Xəyyam Qocayev" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "xxmn77@gmail.com" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_1) +#: actions.ui:17 +#, kde-format +msgid "Inactive Inner Window Actions" +msgstr "Qeyri-altiv pəncərənin daxili" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#. i18n: ectx: property (text), widget (QLabel, label_1) +#: actions.ui:26 mouse.ui:177 +#, kde-format +msgid "&Left click:" +msgstr "Siçanın so&l düyməsi:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandWindow1) +#: actions.ui:39 +#, kde-format +msgid "" +"In this row you can customize left click behavior when clicking into an " +"inactive inner window ('inner' means: not titlebar, not frame)." +msgstr "" +"Bu sətirdə siz qeyri-aktiv pəncərənin daxilində (başlıq və çərçivəyə deyil) " +"siçanın sol düyməsinin vurulması zamanı onun davranışını təyin edə " +"bilərsiniz." + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow3) +#: actions.ui:43 actions.ui:83 actions.ui:123 +#, kde-format +msgid "Activate, raise and pass click" +msgstr "Aktivləşdirmık, üstə çıxarmaq və tıklamanı icra etmək" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow3) +#: actions.ui:48 actions.ui:88 actions.ui:128 +#, kde-format +msgid "Activate and pass click" +msgstr "Aktivləşdirmək və tıklamanı icra etmək" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow3) +#: actions.ui:53 actions.ui:93 actions.ui:133 mouse.ui:293 mouse.ui:408 +#: mouse.ui:523 +#, kde-format +msgid "Activate" +msgstr "Aktivləşdirmək" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow3) +#: actions.ui:58 actions.ui:98 actions.ui:138 mouse.ui:283 mouse.ui:398 +#: mouse.ui:513 +#, kde-format +msgid "Activate and raise" +msgstr "Aktivləşdirmək və üstə çıxarmaq" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: actions.ui:66 mouse.ui:200 +#, kde-format +msgid "&Middle click:" +msgstr "Siçanın orta düy&məsi:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandWindow2) +#: actions.ui:79 +#, kde-format +msgid "" +"In this row you can customize middle click behavior when clicking into an " +"inactive inner window ('inner' means: not titlebar, not frame)." +msgstr "" +"Bu sətirdə siz qeyri-aktiv pəncərənin daxilində (başlıq və çərçivəyə deyil) " +"siçanın orta düyməsinin vurulması zamanı onun davranışını təyin edə " +"bilərsiniz." + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: actions.ui:106 mouse.ui:213 +#, kde-format +msgid "&Right click:" +msgstr "Sağ &klik:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandWindow3) +#: actions.ui:119 +#, kde-format +msgid "" +"In this row you can customize right click behavior when clicking into an " +"inactive inner window ('inner' means: not titlebar, not frame)." +msgstr "" +"Bu sətirdə siz qeyri-aktiv pəncərənin daxilində (başlıq və çərçivəyə deyil) " +"siçanın sağ düyməsinin vurulması zamanı onun davranışını təyin edə " +"bilərsiniz." + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: actions.ui:146 mouse.ui:88 +#, kde-format +msgid "Mouse &wheel:" +msgstr "Siçanın diyircə&yi:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandWindowWheel) +#: actions.ui:159 +#, kde-format +msgid "" +"In this row you can customize behavior when scrolling into an inactive inner " +"window ('inner' means: not titlebar, not frame)." +msgstr "" +"Bu sətirdə siz qeyri-aktiv pəncərənin daxilində (başlıq və çərçivəyə deyil) " +"siçanın diyircəyinə vurulması zamanı onun davranışını təyin edə bilərsiniz." + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindowWheel) +#: actions.ui:163 +#, kde-format +msgid "Scroll" +msgstr "Sürüşdürmə" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindowWheel) +#: actions.ui:168 +#, kde-format +msgid "Activate and scroll" +msgstr "Aktivləşdirmək və sürüşdürmək" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindowWheel) +#: actions.ui:173 +#, kde-format +msgid "Activate, raise and scroll" +msgstr "Aktivləşdirmək, üzə çıxartmaq və sürüşdürmək" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#: actions.ui:184 +#, kde-format +msgid "Inner Window, Titlebar and Frame Actions" +msgstr "Pəncərə daxilində, Başlıqda və Çərçivədə" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: actions.ui:195 +#, kde-format +msgid "Mo&difier key:" +msgstr "&Dəyişdirici düymə:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandAllKey) +#: actions.ui:205 +#, kde-format +msgid "" +"Here you select whether holding the Meta key or Alt key will allow you to " +"perform the following actions." +msgstr "" +"Burada Meta düyməsini və ya Alt düyməsini basılı saxlamaqla aşağıdakı " +"hərəkətləri əməlləri icra edə bilərsiniz." + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllKey) +#: actions.ui:209 +#, kde-format +msgid "Meta" +msgstr "Meta" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllKey) +#: actions.ui:214 +#, kde-format +msgid "Alt" +msgstr "Alt" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: actions.ui:236 +#, kde-format +msgid " + " +msgstr " + " + +#. i18n: ectx: property (text), widget (QLabel, label_8) +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: actions.ui:248 mouse.ui:601 +#, kde-format +msgid "L&eft click:" +msgstr "S&ol klik:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandAll1) +#: actions.ui:261 +#, kde-format +msgid "" +"In this row you can customize left click behavior when clicking into the " +"titlebar or the frame." +msgstr "" +"Bu sətirdə başlıq çubuğuna və ya çərçivəyə tıkladıqda sol klik davranışına " +"düzəliş edə bilərsiniz." + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:265 actions.ui:335 actions.ui:405 +#, kde-format +msgid "Move" +msgstr "Köçürmək" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:270 actions.ui:340 actions.ui:410 +#, kde-format +msgid "Activate, raise and move" +msgstr "Aktivləşdirmək, yüksəltmək və köçürmək" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:275 actions.ui:345 actions.ui:415 mouse.ui:246 mouse.ui:308 +#: mouse.ui:361 mouse.ui:423 mouse.ui:476 mouse.ui:538 +#, kde-format +msgid "Toggle raise and lower" +msgstr "Yüksəltmə və Alçaltma keçisi" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:280 actions.ui:350 actions.ui:420 +#, kde-format +msgid "Resize" +msgstr "Ölçüsünü dəyişmək" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:285 actions.ui:355 actions.ui:425 mouse.ui:236 mouse.ui:298 +#: mouse.ui:351 mouse.ui:413 mouse.ui:466 mouse.ui:528 +#, kde-format +msgid "Raise" +msgstr "Yüksəltmək" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:290 actions.ui:360 actions.ui:430 mouse.ui:65 mouse.ui:241 +#: mouse.ui:303 mouse.ui:356 mouse.ui:418 mouse.ui:471 mouse.ui:533 +#, kde-format +msgid "Lower" +msgstr "Alçaltmaq" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:295 actions.ui:365 actions.ui:435 mouse.ui:55 mouse.ui:251 +#: mouse.ui:313 mouse.ui:366 mouse.ui:428 mouse.ui:481 mouse.ui:543 +#, kde-format +msgid "Minimize" +msgstr "Kiçiltmək" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:300 actions.ui:370 actions.ui:440 +#, kde-format +msgid "Decrease opacity" +msgstr "Şəffaflığı azaltmaq" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:305 actions.ui:375 actions.ui:445 +#, kde-format +msgid "Increase opacity" +msgstr "Şəffaflığı artırmaq" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:310 actions.ui:380 actions.ui:450 actions.ui:505 mouse.ui:80 +#: mouse.ui:132 mouse.ui:271 mouse.ui:333 mouse.ui:386 mouse.ui:448 +#: mouse.ui:501 mouse.ui:563 +#, kde-format +msgid "Do nothing" +msgstr "Heç biri" + +#. i18n: ectx: property (text), widget (QLabel, label_8) +#: actions.ui:318 +#, kde-format +msgid "Middle &click:" +msgstr "O&rta düymə:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandAll2) +#: actions.ui:331 +#, kde-format +msgid "" +"In this row you can customize middle click behavior when clicking into the " +"titlebar or the frame." +msgstr "" +"Bu sətirdə siz başlıq çubuğuna və çəçivəyə vurduqda orta düymənin " +"davranışına düzəliş edə bilərsiniz." + +#. i18n: ectx: property (text), widget (QLabel, label_10) +#. i18n: ectx: property (text), widget (QLabel, label_9) +#: actions.ui:388 mouse.ui:671 +#, kde-format +msgid "Right clic&k:" +msgstr "Sağ kli&k:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:401 +#, kde-format +msgid "" +"In this row you can customize right click behavior when clicking into the " +"titlebar or the frame." +msgstr "" +"Bu sətirdə siz siz başlıq çübüğuna və çərçivəyə vurduqda sağ düymənin " +"davranışlarına düzəliş edə bilərsiniz." + +#. i18n: ectx: property (text), widget (QLabel, label_10) +#: actions.ui:458 +#, kde-format +msgid "Mo&use wheel:" +msgstr "Siçanın di&yircəyi:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:471 +#, kde-format +msgid "" +"Here you can customize KDE's behavior when scrolling with the mouse wheel in " +"a window while pressing the modifier key." +msgstr "" +"Burada siz pəncərə daxilində dəyişdirici düyməyə basdıqda siçanın " +"diyircəyini hərəkət etdirərkən KDE-nin davranışına düzəliş edə bilərsiniz" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:475 mouse.ui:102 +#, kde-format +msgid "Raise/lower" +msgstr "Yüksəltmək/Alçaltmaq" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:480 mouse.ui:107 +#, kde-format +msgid "Shade/unshade" +msgstr "Kolgələmək/Kölgələməmək" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:485 mouse.ui:112 +#, kde-format +msgid "Maximize/restore" +msgstr "Böyütmək/Geri qaytarmaq" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:490 mouse.ui:117 +#, kde-format +msgid "Keep above/below" +msgstr "Üstə tutmaq/Altda tutmaq" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:495 mouse.ui:122 +#, kde-format +msgid "Move to previous/next desktop" +msgstr "Əvvəlki/sonrakı iş masasına kğçür" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:500 mouse.ui:127 +#, kde-format +msgid "Change opacity" +msgstr "Şəffaflığı dəyişmək" + +#. i18n: ectx: property (text), widget (QLabel, shadeHoverLabel) +#: advanced.ui:20 +#, kde-format +msgid "Window &unshading:" +msgstr "Pəncərəni başlıqdan aç&maq:" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_ShadeHover) +#: advanced.ui:32 +#, kde-format +msgid "" +"

If this option is enabled, a shaded window will " +"unshade automatically when the mouse pointer has been over the titlebar for " +"some time.

" +msgstr "" +"

Bu seçimdə, başlığa yığılmış pəncərə, kursoru başlıq " +"çubuğu üzərində bir müddət saxladıqda açılacaqdır.

" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ShadeHover) +#: advanced.ui:35 +#, kde-format +msgid "On titlebar hover after:" +msgstr "Başlıq çubuğu üzərində qaldığı müddət:" + +#. i18n: ectx: property (whatsThis), widget (QSpinBox, kcfg_ShadeHoverInterval) +#: advanced.ui:42 +#, kde-format +msgid "" +"Sets the time in milliseconds before the window unshades when the mouse " +"pointer goes over the shaded window." +msgstr "" +"Kursoru üzərində saxladıqda başlıq çubuğuna yığılmış pəncərənin avtomatik " +"açılma müddəti, millisaniyələrlə." + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_ShadeHoverInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_DelayFocusInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_AutoRaiseInterval) +#: advanced.ui:45 focus.ui:85 focus.ui:178 +#, kde-format +msgid " ms" +msgstr " ms" + +#. i18n: ectx: property (text), widget (QLabel, windowPlacementLabel) +#: advanced.ui:66 +#, kde-format +msgid "Window &placement:" +msgstr "Yeni &pəncərənin yerləşdirilməsi:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_Placement) +#: advanced.ui:76 +#, kde-format +msgid "" +"

The placement policy determines where a new window " +"will appear on the desktop.

  • Smart will try to achieve a minimum overlap of windows
  • Maximizing will try to maximize every window to fill the " +"whole screen. It might be useful to selectively affect placement of some " +"windows using the window-specific settings.
  • Cascade will " +"cascade the windows
  • Random will use a random " +"position
  • Centered will place the window " +"centered
  • Zero-cornered will place the window in " +"the top-left corner
  • Under mouse will place the " +"window under the pointer
" +msgstr "" +"

Bu parametr yeni pəncərənin iş masasında yerləşdirilmə " +"qaydasnı təyin etməyə imkan verir.

  • Ən az üst-üstə düşmə pəncərələrin minimum üst-üstə " +"düşməsinə nail olmağa çalışacaq
  • Tam açılanbütün " +"ekranı doldurmaq üçün hər pəncərəni böyütməyə çalışacaqdır. Pəncərənin " +"xüsusi parametrlərini istifadə edərək bəzi pəncərələrin yerləşdirilməsinə " +"seçici şəkildə təsir etmək faydalı ola bilər.
  • ArdıcılPəncərələr ardıcıl düzüləcək
  • TəsadüfiPəncərələr " +"ixtiyari düzüləcək
  • Mərkəzdə Pəncərələr ekranın " +"mərkəszində yerləşəcək
  • Yuxarı sol küncdə Pəncərələr " +"yuxarı sol küncdə yerləşəcək
  • Kursorun altında " +"Pəncərələr kursorun altında yerləşəcək
" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:80 +#, kde-format +msgid "Minimal Overlapping" +msgstr "Ən az üst-üstə düşmə" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:85 +#, kde-format +msgid "Maximized" +msgstr "Tam açılan" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:90 +#, kde-format +msgid "Cascaded" +msgstr "Ardıcıl" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:95 +#, kde-format +msgid "Random" +msgstr "Təsadüfi" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:100 +#, kde-format +msgid "Centered" +msgstr "Mərkəzdə" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:105 +#, kde-format +msgid "In Top-Left Corner" +msgstr "Yuxarı sol küncdə" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:110 +#, kde-format +msgid "Under mouse" +msgstr "Kursorun altında" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_AllowKDEAppsToRememberWindowPositions) +#: advanced.ui:118 +#, kde-format +msgid "" +"When turned on, KDE apps which are able to remember the positions of their " +"windows are allowed to do so. This will override the window placement mode " +"defined above." +msgstr "" +"Açıldıqda, pəncərələrinin yerlərini yadda saxlaya bilən KDE tətbiqetmələrinə " +"icazə verilir. Bu, yuxarıda müəyyənləşdirilmiş pəncərə yerləşdirmə rejimini " +"ləğv edəcəkdir." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AllowKDEAppsToRememberWindowPositions) +#: advanced.ui:121 +#, kde-format +msgid "Allow KDE apps to remember the positions of their own windows" +msgstr "" +"KDE tətbiqlərinə onların öz pəncərələrinin mövqeyini yadda saxlamağa icazə " +"vermək" + +#. i18n: ectx: property (text), widget (QLabel, specialWindowsLabel) +#: advanced.ui:128 +#, kde-format +msgid "&Special windows:" +msgstr "&Xüsusi pəncərələr:" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_HideUtilityWindowsForInactive) +#: advanced.ui:138 +#, kde-format +msgid "" +"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." +msgstr "" +"Bu parametr aktiv edildikdə köməkçi pəncərərələr (alətlər paneli, ayrılmış " +"menyular...) qeyri-aktiv tətbiqlər üçün gizlədiləcəkdir. Qeyd: Bu " +"xüsusiyyətlərin düzgün işləməsi üçün tətbiqlərin, pəncərələri doğru şəkildə " +"quraşdırmaları lazımdır." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_HideUtilityWindowsForInactive) +#: advanced.ui:141 +#, kde-format +msgid "Hide utility windows for inactive applications" +msgstr "Qeyri-aktiv tətbiqlərin köməkçi pəncərələrini gizlətmək." + +#. i18n: ectx: property (text), widget (QLabel, windowFocusPolicyLabel) +#: focus.ui:22 +#, kde-format +msgid "Window &activation policy:" +msgstr "Pəncərələrin &aktişləşdirilməsi qaydaları:" + +#. i18n: ectx: property (whatsThis), widget (QComboBox, windowFocusPolicy) +#: focus.ui:32 +#, kde-format +msgid "With this option you can specify how and when windows will be focused." +msgstr "Pəncərələrin aktişləşdirilməsi qaydaları." + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:36 +#, kde-format +msgctxt "sassa asas" +msgid "Click to focus" +msgstr "Fokuslamaq üçün vurun" + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:41 +#, kde-format +msgid "Click to focus (mouse precedence)" +msgstr "Fokuslamaq üçün vurun (siçanın üstünlüyü)" + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:46 +#, kde-format +msgid "Focus follows mouse" +msgstr "Kursor ilə fokuslamaq" + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:51 +#, kde-format +msgid "Focus follows mouse (mouse precedence)" +msgstr "Kursor ilə fokuslamaq (siçanın üstünlüyü)" + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:56 +#, kde-format +msgid "Focus under mouse" +msgstr "Kursorun altından fokuslamaq" + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:61 +#, kde-format +msgid "Focus strictly under mouse" +msgstr "Kursorun tam altından fokuslamaq" + +#. i18n: ectx: property (text), widget (QLabel, delayFocusOnLabel) +#: focus.ui:69 +#, kde-format +msgid "&Delay focus by:" +msgstr "&Fokuslanma müddəti:" + +#. i18n: ectx: property (whatsThis), widget (QSpinBox, kcfg_DelayFocusInterval) +#: focus.ui:82 +#, kde-format +msgid "" +"This is the delay after which the window the mouse pointer is over will " +"automatically receive focus." +msgstr "" +"Bu, müddət ərzində kursor üzərində dayandıqda pəncərə avtomatik olaraq " +"fokuslanacaqdır." + +#. i18n: ectx: property (text), widget (QLabel, focusStealingLabel) +#: focus.ui:101 +#, kde-format +msgid "Focus &stealing prevention:" +msgstr "Fokusun dəyişdirilməsinin qarşısının alınması &səviyyəsi:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_FocusStealingPreventionLevel) +#: focus.ui:114 +#, kde-format +msgid "" +"

This option specifies how much KWin will try to " +"prevent unwanted focus stealing caused by unexpected activation of new " +"windows. (Note: This feature does not work with the Focus under mouse or Focus strictly under mouse focus policies.)

  • 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.

" +msgstr "" +"

Bu seçim, yeni pəncərələrin gözlənilmədən " +"aktivləşdirilməsi nəticəsində yaranan arzuolunmaz fokus dəyişməsinin " +"qarşısını almağa KWin-in nə iş görəcəyini göstərir. (Qeyd: Bu, Kursorun altından fokuslama və ya Kursorun tam altından fokuslama fokuslama " +"qaydaları ilə işləməyəcək.)

  • Qeyd:" +" Qarşılıqlı təsir söndürülüb və yeni pəncərə həmişə aktivdir.
  • Aşağı: Qarşılıqlı təsir aktivləşdirilib; bəzi pəncərədə " +"əsas mexanizm üçün dəstək olmadıqda və KWin, pəncərəni aktiv etmək və ya " +"etməmək barədə etibarlı qərar verə bilməyəndə aktivləşdiriləcəkdir. Bu " +"parametr tətbiqlərdən asılı olaraq orta səviyyədən daha pis və daha yaxşı " +"nəticələrə səbəb ola bilər.
  • Orta: Qarşılıqlı " +"təsir sktivdir.
  • Yüksək: Yeni pəncərələr, yalnız heç bir " +"pəncərə aktiv deyilsə və ya hazırda aktiv tətbiqə aiddirsə, aktiv olur. " +"Siçan fokus rejimini istifadə etmədikdə bu parametr, o qədər də yararlı " +"deyil.
  • Çox yüksək: Bütün pəncərələr istifadəçi tərəfindən " +"açıq şəkildə aktivləşdirilməlidir.

Fokus dəyişməsinin qarşısı " +"alınan pəncərə diqqət tələb edilən kimi qeyd olunacaq. Məsələn tapşırıq " +"panelindəki həmin pəncərəyə aid element işıqlanacaq. Bunu \"Sistem Ayarları" +"\" menyusundakı bildirişlərin ayarlanması bölümündə dəyişmək olar.

" + +#. i18n: Focus Stealing Prevention Level +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_FocusStealingPreventionLevel) +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_BorderSnapZone) +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_WindowSnapZone) +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_CenterSnapZone) +#: focus.ui:118 moving.ui:53 moving.ui:75 moving.ui:97 +#, kde-format +msgid "None" +msgstr "Heç biri" + +#. i18n: Focus Stealing Prevention Level +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_FocusStealingPreventionLevel) +#: focus.ui:123 +#, kde-format +msgid "Low" +msgstr "Aşağı" + +#. i18n: Focus Stealing Prevention Level +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_FocusStealingPreventionLevel) +#: focus.ui:128 +#, kde-format +msgid "Medium" +msgstr "Orta" + +#. i18n: Focus Stealing Prevention Level +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_FocusStealingPreventionLevel) +#: focus.ui:133 +#, kde-format +msgid "High" +msgstr "Yüksək" + +#. i18n: Focus Stealing Prevention Level +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_FocusStealingPreventionLevel) +#: focus.ui:138 +#, kde-format +msgid "Extreme" +msgstr "Çox yüksək" + +#. i18n: ectx: property (text), widget (QLabel, raisingWindowsLabel) +#: focus.ui:146 +#, kde-format +msgid "Raising windows:" +msgstr "Pəncərələri önə çıxarmaq:" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_ClickRaise) +#: focus.ui:153 +#, kde-format +msgid "" +"When this option is enabled, the active window will be brought to the front " +"when you click somewhere into the window contents. To change it for inactive " +"windows, you need to change the settings in the Actions tab." +msgstr "" +"Bu seçimdə pəncər daxilinə vurduqda aktiv pəncərə ön plana çıxarılacaqdır. " +"Aktiv pəncərə üçünü bunu \"Fəaliyyətlər\" vərəqində ayarlamaq olar." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ClickRaise) +#: focus.ui:156 +#, kde-format +msgid "&Click raises active window" +msgstr "Klik aktiv pən&cərəni önə çıxarır" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_AutoRaise) +#: focus.ui:165 +#, kde-format +msgid "" +"When this option is enabled, a window in the background will automatically " +"come to the front when the mouse pointer has been over it for some time." +msgstr "" +"Bu seçimdə kursor bir müddət üzərində yerləşdiyi halda pəncərə avtomatik önə " +"çıxacaq." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AutoRaise) +#: focus.ui:168 +#, kde-format +msgid "&Raise on hover, delayed by:" +msgstr "Üzə&rində saxlamaqla önə çıxma müddəti:" + +#. i18n: ectx: property (whatsThis), widget (QSpinBox, kcfg_AutoRaiseInterval) +#: focus.ui:175 +#, kde-format +msgid "" +"This is the delay after which the window that the mouse pointer is over will " +"automatically come to the front." +msgstr "" +"Bu kursorun üzərində dayanması ilə pəncərənin önə çıxmasına qədər olan " +"müddətdir." + +#. i18n: ectx: property (text), widget (QLabel, multiscreenBehaviorLabel) +#: focus.ui:196 +#, kde-format +msgid "Multiscreen behavior:" +msgstr "Birdən çox ekranla davranış:" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_ActiveMouseScreen) +#: focus.ui:203 +#, kde-format +msgid "" +"When this option is enabled, the active Xinerama screen (where new windows " +"appear, for example) is the screen containing the mouse pointer. When " +"disabled, the active Xinerama screen is the screen containing the focused " +"window. By default this option is disabled for Click to focus and enabled " +"for other focus policies." +msgstr "" +"Bu seçimdə, aktiv Xinerama ekranı (məsələn, yeni pəncərələrin peyda olduğu " +"yer) siçan kursoru olan ekrandır. Söndürüldükdə, aktiv Xinerama ekranı, " +"fokuslanmış pəncərədən ibarət ekrandır. Standart olaraq bu seçim klikləməklə " +"fokuslamaq üçün qeyri-aktiv edilib və digər fokuslama növü üçün aktivdir." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ActiveMouseScreen) +#: focus.ui:206 +#, kde-format +msgid "Active screen follows &mouse" +msgstr "Aktiv ekran &kursoru izləyir" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_SeparateScreenFocus) +#: focus.ui:213 +#, kde-format +msgid "" +"When this option is enabled, focus operations are limited only to the active " +"Xinerama screen" +msgstr "" +"Bu seçim aktiv edildikdə fokuslama əməliyyatı aktiv Xinerama ekranı ilə " +"məhdudlaşır" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_SeparateScreenFocus) +#: focus.ui:216 +#, kde-format +msgid "&Separate screen focus" +msgstr "Ekran üçün ayrıca foku&s" + +#. i18n: ectx: property (text), widget (QLabel, windowFocusPolicyDescriptionLabel) +#: focus.ui:229 +#, kde-format +msgid "Window activation policy description" +msgstr "Pəncərənin aktivləşdirilməsi qaydalarının təfərrüatları" + +#: main.cpp:73 +#, kde-format +msgid "&Focus" +msgstr "&Fokus" + +#: main.cpp:84 +#, kde-format +msgid "Titlebar A&ctions" +msgstr "Başlıq paneli Fəa&liyyətləri" + +#: main.cpp:91 +#, kde-format +msgid "W&indow Actions" +msgstr "Pəncərə Fəal&iyyətləri" + +#: main.cpp:98 +#, kde-format +msgid "Mo&vement" +msgstr "&Yerdəyişmə" + +#: main.cpp:105 +#, kde-format +msgid "Adva&nced" +msgstr "Əla&və" + +#: main.cpp:110 +#, kde-format +msgid "Window Behavior Configuration Module" +msgstr "Pəncərə Davranışı Tənzimləmə Modulu" + +#: main.cpp:112 +#, kde-format +msgid "(c) 1997 - 2002 KWin and KControl Authors" +msgstr "(c) 1997 - 2002 KWin and KControl Authors" + +#: main.cpp:114 +#, kde-format +msgid "Matthias Ettrich" +msgstr "Matthias Ettrich" + +#: main.cpp:115 +#, kde-format +msgid "Waldo Bastian" +msgstr "Waldo Bastian" + +#: main.cpp:116 +#, kde-format +msgid "Cristian Tibirna" +msgstr "Cristian Tibirna" + +#: main.cpp:117 +#, kde-format +msgid "Matthias Kalle Dalheimer" +msgstr "Matthias Kalle Dalheimer" + +#: main.cpp:118 +#, kde-format +msgid "Daniel Molkentin" +msgstr "Daniel Molkentin" + +#: main.cpp:119 +#, kde-format +msgid "Wynn Wilkes" +msgstr "Wynn Wilkes" + +#: main.cpp:120 +#, kde-format +msgid "Pat Dowler" +msgstr "Pat Dowler" + +#: main.cpp:121 +#, kde-format +msgid "Bernd Wuebben" +msgstr "Bernd Wuebben" + +#: main.cpp:122 +#, kde-format +msgid "Matthias Hoelzer-Kluepfel" +msgstr "Matthias Hoelzer-Kluepfel" + +#: main.cpp:167 +#, kde-format +msgid "" +"

Window Behavior

Here you can customize the way windows behave " +"when being moved, resized or clicked on. You can also specify a focus policy " +"as well as a placement policy for new windows.

Please note that this " +"configuration will not take effect if you do not use KWin as your window " +"manager. If you do use a different window manager, please refer to its " +"documentation for how to customize window behavior.

" +msgstr "" +"

Pəncərə Davranışı

Burada siz, yerdəyişmə, ölçüsünün dəyişməsi və " +"klikləmə zamanı pəncərənin davranışını ayarlaya bilərsiniz. Həmçinin burada " +"fokuslama və yeni pəncərənin yerləşmə qaydalarını təyin edə bilərsiniz.

" +"

Nəzərə alın ki, yalnız KWin pəncərə menecerindən istifadə edildiyi halda " +"bu qayda işləyəcək. Əgər başqa pəncərə meneceri istifadə edirsinizsə, " +"pəncərə davranışının necə ayarlanması haqqında ona uyğun məlumatlara " +"müraciət edin.

" + +#: main.cpp:187 +#, kde-format +msgid "&Titlebar Actions" +msgstr "Başlıq çubuğu Fəaliyyə&tləri" + +#: main.cpp:193 +#, kde-format +msgid "Window Actio&ns" +msgstr "Pə&ncərə Fəaliyyətləri" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_1) +#: mouse.ui:17 +#, kde-format +msgid "Titlebar Actions" +msgstr "Başlıq çubuğu Fəaliyyətləri" + +#. i18n: ectx: property (text), widget (QLabel, label_1) +#: mouse.ui:26 +#, kde-format +msgid "&Double-click:" +msgstr "&İkili klik:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#: mouse.ui:36 +#, kde-format +msgid "Behavior on double click into the titlebar." +msgstr "başlıq çubuğuna ikili kilik davranşları." + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonLeftClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonMiddleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonRightClickCommand) +#: mouse.ui:40 mouse.ui:615 mouse.ui:650 mouse.ui:685 +#, kde-format +msgid "Maximize" +msgstr "Tam açılan" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonLeftClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonMiddleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonRightClickCommand) +#: mouse.ui:45 mouse.ui:620 mouse.ui:655 mouse.ui:690 +#, kde-format +msgid "Vertically maximize" +msgstr "Şaquli tam açılma" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonLeftClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonMiddleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonRightClickCommand) +#: mouse.ui:50 mouse.ui:625 mouse.ui:660 mouse.ui:695 +#, kde-format +msgid "Horizontally maximize" +msgstr "Üfüqi tam açılma" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#: mouse.ui:60 mouse.ui:256 mouse.ui:318 mouse.ui:371 mouse.ui:433 mouse.ui:486 +#: mouse.ui:548 +#, kde-format +msgid "Shade" +msgstr "Balığa yığmaq" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#: mouse.ui:70 mouse.ui:261 mouse.ui:323 mouse.ui:376 mouse.ui:438 mouse.ui:491 +#: mouse.ui:553 +#, kde-format +msgid "Close" +msgstr "Bağlamaq" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#: mouse.ui:75 +#, kde-format +msgid "Show on all desktops" +msgstr "Bütün iş masalarında göstərmək" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandTitlebarWheel) +#: mouse.ui:98 +#, kde-format +msgid "Behavior on mouse wheel scroll over the titlebar." +msgstr "" +"Balıqd çubuğunda siçanın diyircəyinin hərəkəti zamanı davranış." + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#: mouse.ui:143 +#, kde-format +msgid "Titlebar and Frame Actions" +msgstr "Başlıq çubuğu və Çərçivə Fəaliyyətləri" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: mouse.ui:167 +#, kde-format +msgid "Active" +msgstr "Aktiv pəncərə" + +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: mouse.ui:190 +#, kde-format +msgid "Inactive" +msgstr "Qeyri-aktiv pəncərə" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandActiveTitlebar3) +#: mouse.ui:232 mouse.ui:347 mouse.ui:462 +#, kde-format +msgid "" +"Behavior on left click into the titlebar or frame of an active window." +msgstr "" +"Aktiv pəncərə çərçivəsi və başlıq çubuğuna sol klik zamanı " +"davranış." + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#: mouse.ui:266 mouse.ui:328 mouse.ui:381 mouse.ui:443 mouse.ui:496 +#: mouse.ui:558 +#, kde-format +msgid "Show actions menu" +msgstr "Fəaliyyətlər menyusunu göstərmək" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#: mouse.ui:279 mouse.ui:394 mouse.ui:509 +#, kde-format +msgid "" +"Behavior on left click into the titlebar or frame of an " +"inactive window." +msgstr "" +"Qeyri-aktiv pəncərə çərçivəsi və ya başlıq çubuğuna sol " +"klik zamanı davranış." + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#: mouse.ui:288 mouse.ui:403 mouse.ui:518 +#, kde-format +msgid "Activate and lower" +msgstr "Fokuslamaq və arxa plana keşirmək" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_3) +#: mouse.ui:589 +#, kde-format +msgid "Maximize Button Actions" +msgstr "Böyütmə düyməsi" + +#. i18n: ectx: property (whatsThis), widget (QLabel, label_8) +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_MaximizeButtonLeftClickCommand) +#: mouse.ui:598 mouse.ui:611 +#, kde-format +msgid "Behavior on left click onto the maximize button." +msgstr "Böyütmə düyməsinə sol klik zamanı davranış." + +#. i18n: ectx: property (whatsThis), widget (QLabel, label_9) +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_MaximizeButtonMiddleClickCommand) +#: mouse.ui:633 mouse.ui:646 +#, kde-format +msgid "Behavior on middle click onto the maximize button." +msgstr "Böyütmə düyməsinə orta düymə ilə klik zamanı davranış." + +#. i18n: ectx: property (text), widget (QLabel, label_9) +#: mouse.ui:636 +#, kde-format +msgid "Middle c&lick:" +msgstr "Orta düymə k&liki:" + +#. i18n: ectx: property (whatsThis), widget (QLabel, label_10) +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_MaximizeButtonRightClickCommand) +#: mouse.ui:668 mouse.ui:681 +#, kde-format +msgid "Behavior on right click onto the maximize button." +msgstr "Böyütmə süyməsinə sağ düymə ilə kilik zamanı davranış." + +#. i18n: ectx: property (text), widget (QLabel, geometryTipLabel) +#: moving.ui:20 +#, kde-format +msgid "Window &geometry:" +msgstr "Pəncərənin həndəsi göstəriciləri:" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_GeometryTip) +#: moving.ui:30 +#, kde-format +msgid "" +"Enable this option if you want a window's geometry to be displayed while it " +"is being moved or resized. The window position relative to the top-left " +"corner of the screen is displayed together with its size." +msgstr "" +"Bu seçim aktiv edilərsə pəncərənin yerini və ya ölçüsünü dəyişərkən onun " +"həndəsi göstəriciləri görünəcəkdir. Ekranın yuxarı küncünə nisbi olaraq " +"ölçüləri ilə birgə pəncərənin ekranda mövqeyi göstəriləcəkdir." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_GeometryTip) +#: moving.ui:33 +#, kde-format +msgid "Display when moving or resizing" +msgstr "Yerdəyişmə və ya ölçüsü dəyişdirilərkən göstərmək" + +#. i18n: ectx: property (text), widget (QLabel, borderSnapLabel) +#: moving.ui:40 +#, kde-format +msgid "Screen &edge snap zone:" +msgstr "&Kənar yapışma sahəsi:" + +#. i18n: ectx: property (whatsThis), widget (QSpinBox, kcfg_BorderSnapZone) +#: moving.ui:50 +#, kde-format +msgid "" +"Here you can set the snap zone for screen edges, i.e. the 'strength' of the " +"magnetic field which will make windows snap to the border when moved near it." +msgstr "" +"Burada siz kənara yapışma sahəsini təyin edə bilərsiniz. Bu pəncərələrin " +"kənara yapışmasına imkan verən maqnit sahəsinin qüvvəsi kimi başa düşülə " +"bilər." + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_BorderSnapZone) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_WindowSnapZone) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_CenterSnapZone) +#: moving.ui:56 moving.ui:78 moving.ui:100 +#, kde-format +msgid " px" +msgstr " piksel" + +#. i18n: ectx: property (whatsThis), widget (QSpinBox, kcfg_WindowSnapZone) +#: moving.ui:72 +#, kde-format +msgid "" +"Here you can set the snap zone for windows, i.e. the 'strength' of the " +"magnetic field which will make windows snap to each other when they are " +"moved near another window." +msgstr "" +"Burada siz pəncərələr üçün yapışma sahəsini ayarlaya bilərsiniz. Bu " +"pəncərələrin bir-birinə yapışmasına imkan verən maqnit sahəsinin qüvvəsi " +"kimi başa düşülə bilər." + +#. i18n: ectx: property (whatsThis), widget (QSpinBox, kcfg_CenterSnapZone) +#: moving.ui:94 +#, kde-format +msgid "" +"Here you can set the snap zone for the screen center, i.e. the 'strength' of " +"the magnetic field which will make windows snap to the center of the screen " +"when moved near it." +msgstr "" +"Burada siz ekranın mərkəzi üçün yapışma sahəsini təyin edə bilərsiniz. Bu " +"pəncərələrin mərkəzə yapışmasına imkan verən maqnit sahəsinin qüvvəsi kimi " +"başa düşülə bilər." + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_SnapOnlyWhenOverlapping) +#: moving.ui:113 +#, kde-format +msgid "" +"Here you can set that windows will be only snapped if you try to overlap " +"them, i.e. they will not be snapped if the windows comes only near another " +"window or border." +msgstr "" +"Burada üst-üstə yerləşdirməyə cəhd etdiyinizdə pəncərələrin bir-birinə " +"yapışmasını tənzimləyə bilərsiniz. Başqa bir pəncərənin və ya ekranın " +"kənarına yaxınlaşdıqda yapışmayacaqdır." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_SnapOnlyWhenOverlapping) +#: moving.ui:116 +#, kde-format +msgid "Only when overlapping" +msgstr "Yalnız üst-üstə gəldikdə yapışdırmaq" + +#. i18n: ectx: property (text), widget (QLabel, windowSnapLabel) +#: moving.ui:123 +#, kde-format +msgid "&Window snap zone:" +msgstr "&Pəncərənin yapışma sahəsi:" + +#. i18n: ectx: property (text), widget (QLabel, centerSnaplabel) +#: moving.ui:133 +#, kde-format +msgid "&Center snap zone:" +msgstr "&Mərkəzi yapışma sahəsi:" + +#. i18n: ectx: property (text), widget (QLabel, OverlapSnapLabel) +#: moving.ui:143 +#, kde-format +msgid "&Snap windows:" +msgstr "Pən&cərələri yapışdırmaq:" + +#: windows.cpp:87 +#, kde-format +msgid "" +"Click to focus: A window becomes active when you click into it. " +"This behavior is common on other operating systems and likely what you want." +msgstr "" +"Fokus üçün klik:Pəncərə daxilinə və ya başlığına klikləndikdə " +"aktivləşir. Siz ola bilsin ki, bununla başqa əməliyyat sistemlərindən " +"tanışsını." + +#: windows.cpp:91 +#, kde-format +msgid "" +"Click to focus (mouse precedence): Mostly the same as Click to " +"focus. If an active window has to be chosen by the system (eg. because " +"the currently active one was closed) the window under the mouse is the " +"preferred candidate. Unusual, but possible variant of Click to focus." +msgstr "" +"Fokus üçün klik (siçanın üstünlüyü):Bu rejimin oxşarıFokus üçün " +"klik. Əgər sistem aktiv pəncərəni özü seçərsə (məsələn, aktiv pəncərə " +"bağlandıqda) üstünlük kursorun alrındakı pəncərəyə verilir. Bir az qeyri-adi " +"olsa da Fokus üçün klik rejiminə alternativ variant kimi qəbul " +"olunur." + +#: windows.cpp:96 +#, kde-format +msgid "" +"Focus follows mouse: Moving the mouse onto a window will activate " +"it. Eg. windows randomly appearing under the mouse will not gain the focus. " +"Focus stealing prevention takes place as usual. Think as Click " +"to focus just without having to actually click." +msgstr "" +"Fokus siçanı izləyir:Kursoru üzərində hərəkət etdirdikdə pəncərə " +"aktivləşəcək. Təsadüfi kursorun altına düşən pəncərə fokuslanmayacaq, belə " +"ki, Fokusun dəyişməsin qarşısının alınması adi qaydada işləyir. Bu " +"variant Fokus üçün klikoxşayır, ancaq xüsusi klik tələb olunmur." + +#: windows.cpp:100 +#, kde-format +msgid "" +"This is mostly the same as Focus follows mouse. If an active window " +"has to be chosen by the system (eg. because the currently active one was " +"closed) the window under the mouse is the preferred candidate. Choose this, " +"if you want a hover controlled focus." +msgstr "" +"Bu daha çoxFokus siçanı izləyir variantı kimidir. Əgər sistem aktiv " +"pəncərəni özü seçərsə (məs., aktiv pəncərə bağlandıqda), üstünlük kursorun " +"altındakı pəncərəyə verilir. Üstündə saxlayarkən fokuslamaq üçün bu rejimi " +"seçin." + +#: windows.cpp:105 +#, kde-format +msgid "" +"Focus under mouse: The focus always remains on the window under the " +"mouse.
Warning: Focus stealing prevention and " +"the tabbox ('Alt+Tab') contradict the activation policy and will " +"not work. You very likely want to use Focus follows mouse (mouse " +"precedence) instead!" +msgstr "" +"Kursorun altında fokus: Həmişə kursorun altında fokuslanır.
Xəbərdarlıq Fokusun dəyişməsinin qarşısnın alınması və başlıq qutusu ('Alt+Tab') aktivləşdirmə qaydasına ziddir və " +"işlıməz.Bunun əvəzinə siz Fokus siçanı izləyirvariantını işlətmək " +"istəyə bilərsiniz." + +#: windows.cpp:109 +#, kde-format +msgid "" +"Focus strictly under mouse: The focus is always on the window under " +"the mouse (in doubt nowhere) very much like the focus behavior in an " +"unmanaged legacy X11 environment.
Warning: Focus " +"stealing prevention and the tabbox ('Alt+Tab') contradict the " +"activation policy and will not work. You very likely want to use Focus " +"follows mouse (mouse precedence) instead!" +msgstr "" +"Fokus tam krsorun altında: Fokus həmişə kursorun altında (əgər " +"kursorun altı boşdursa fokus ititr) -daha çox öncəki X11 mühitindəki fokusun " +"davranışına oxşayır.
Qeyd: Fokusun dəyişməsinin " +"qarşısının alınmasıvə başlıq qutusu ('Alt+Tab') davranışına " +"ziddir və işləməz. Bunun əvəzinə siz Fokus siçanı izləyir (siçanın " +"üstünlüyü)variantını işlətmək istəyə bilərsiniz." \ No newline at end of file diff --git a/po/az/kwin.po b/po/az/kwin.po new file mode 100644 index 0000000..c6c28a6 --- /dev/null +++ b/po/az/kwin.po @@ -0,0 +1,2585 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the kwin package. +# +# Xəyyam Qocayev , 2020. +msgid "" +msgstr "" +"Project-Id-Version: kwin\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-11-25 08:45+0100\n" +"PO-Revision-Date: 2020-08-05 00:34+0400\n" +"Last-Translator: Kheyyam Gojayev \n" +"Language-Team: Azerbaijan\n" +"Language: az\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Lokalize 20.04.3\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Xəyyam Qocayev" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "xxmn77@gmail.com" + +#: abstract_client.cpp:2729 +#, kde-format +msgctxt "Application is not responding, appended to window title" +msgid "(Not Responding)" +msgstr "(Cavab Yoxdur)" + +#: abstract_wayland_output.cpp:252 +#, kde-format +msgid "unknown" +msgstr "naməlum" + +#: colorcorrection/manager.cpp:58 +#, kde-format +msgctxt "Night Color was disabled" +msgid "Night Color Off" +msgstr "Gecə Rəngi söndürülüb" + +#: colorcorrection/manager.cpp:59 +#, kde-format +msgctxt "Night Color was enabled" +msgid "Night Color On" +msgstr "Gecə Rəngi aktivdir" + +#: colorcorrection/manager.cpp:228 colorcorrection/manager.cpp:231 +#: colorcorrection/manager.cpp:238 +#, kde-format +msgid "Toggle Night Color" +msgstr "Gecə Rəngini aktivləşdirmək/söndürmək" + +#: composite.cpp:926 +#, kde-format +msgid "" +"Desktop effects have been suspended by another application.
You can " +"resume using the '%1' shortcut." +msgstr "" +"Qrafik effektlər başqa tətbiq tərəfindən söndürüldü.
Onları \"%1\" " +"qısayolu vasitəsi ilə bərpa edə bilərsiniz." + +#: debug_console.cpp:65 +#, kde-format +msgid "Timestamp" +msgstr "Vaxt Möhürü" + +#: debug_console.cpp:70 +#, kde-format +msgid "Timestamp (µsec)" +msgstr "Vaxt Möhürü (µsan)" + +#: debug_console.cpp:77 +#, kde-format +msgctxt "A mouse button" +msgid "Left" +msgstr "Sol" + +#: debug_console.cpp:79 +#, kde-format +msgctxt "A mouse button" +msgid "Right" +msgstr "Sağ" + +#: debug_console.cpp:81 +#, kde-format +msgctxt "A mouse button" +msgid "Middle" +msgstr "Orta" + +#: debug_console.cpp:83 +#, kde-format +msgctxt "A mouse button" +msgid "Back" +msgstr "Geriyə" + +#: debug_console.cpp:85 +#, kde-format +msgctxt "A mouse button" +msgid "Forward" +msgstr "İrəli" + +#: debug_console.cpp:87 +#, kde-format +msgctxt "A mouse button" +msgid "Task" +msgstr "Tapşırıq" + +#: debug_console.cpp:89 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 4" +msgstr "Əlavə düymə 4" + +#: debug_console.cpp:91 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 5" +msgstr "Əlavə düymə 5" + +#: debug_console.cpp:93 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 6" +msgstr "Əlavə düymə 6" + +#: debug_console.cpp:95 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 7" +msgstr "Əlavə düymə 7" + +#: debug_console.cpp:97 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 8" +msgstr "Əlavə düymə 8" + +#: debug_console.cpp:99 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 9" +msgstr "Əlavə düymə 9" + +#: debug_console.cpp:101 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 10" +msgstr "Əlavə düymə 10" + +#: debug_console.cpp:103 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 11" +msgstr "Əlavə düymə 11" + +#: debug_console.cpp:105 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 12" +msgstr "Əlavə düymə 12" + +#: debug_console.cpp:107 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 13" +msgstr "Əlavə düymə 13" + +#: debug_console.cpp:109 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 14" +msgstr "Əlavə düymə 14" + +#: debug_console.cpp:111 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 15" +msgstr "Əlavə düymə 15" + +#: debug_console.cpp:113 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 16" +msgstr "Əlavə düymə 16" + +#: debug_console.cpp:115 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 17" +msgstr "Əlavə düymə 17" + +#: debug_console.cpp:117 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 18" +msgstr "Əlavə düymə 18" + +#: debug_console.cpp:119 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 19" +msgstr "Əlavə düymə 19" + +#: debug_console.cpp:121 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 20" +msgstr "Əlavə düymə 20" + +#: debug_console.cpp:123 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 21" +msgstr "Əlavə düymə 1" + +#: debug_console.cpp:125 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 22" +msgstr "Əlavə düymə 22" + +#: debug_console.cpp:127 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 23" +msgstr "Əlavə düymə 23" + +#: debug_console.cpp:129 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 24" +msgstr "Əlavə düymə 24" + +#: debug_console.cpp:138 debug_console.cpp:140 +#, kde-format +msgid "Input Device" +msgstr "Daxiletmə cihazı" + +#: debug_console.cpp:138 +#, kde-format +msgctxt "The input device of the event is not known" +msgid "Unknown" +msgstr "Naməlum" + +#: debug_console.cpp:175 +#, kde-format +msgctxt "A mouse pointer motion event" +msgid "Pointer Motion" +msgstr "Göstəricinin hərəkəti" + +#: debug_console.cpp:182 +#, kde-format +msgctxt "The relative mouse movement" +msgid "Delta" +msgstr "Fərq" + +#: debug_console.cpp:186 +#, kde-format +msgctxt "The relative mouse movement" +msgid "Delta (not accelerated)" +msgstr "Fərq (sürətləndirilməmiş)" + +#: debug_console.cpp:189 +#, kde-format +msgctxt "The global mouse pointer position" +msgid "Global Position" +msgstr "Qlobal Mövqe" + +#: debug_console.cpp:193 +#, kde-format +msgctxt "A mouse pointer button press event" +msgid "Pointer Button Press" +msgstr "Siçanın düyməsinin basılması" + +#: debug_console.cpp:196 debug_console.cpp:204 +#, kde-format +msgctxt "A button in a mouse press/release event" +msgid "Button" +msgstr "Düymə" + +# Bu kod libinput_event_pointer_get_button() библиотеки libinput. --aspotashev funksiyalarından gələn kodlardır +#: debug_console.cpp:197 debug_console.cpp:205 +#, kde-format +msgctxt "A button in a mouse press/release event" +msgid "Native Button code" +msgstr "Doğma Düymə Kodu" + +#: debug_console.cpp:198 debug_console.cpp:206 +#, kde-format +msgctxt "All currently pressed buttons in a mouse press/release event" +msgid "Pressed Buttons" +msgstr "Basılan düymələr" + +#: debug_console.cpp:201 +#, kde-format +msgctxt "A mouse pointer button release event" +msgid "Pointer Button Release" +msgstr "Siçanın Düyməsinin Buraxılması" + +#: debug_console.cpp:221 +#, kde-format +msgctxt "A mouse pointer axis (wheel) event" +msgid "Pointer Axis" +msgstr "Siçanın Diyircəyinin Hərəkəti" + +#: debug_console.cpp:225 +#, kde-format +msgctxt "The orientation of a pointer axis event" +msgid "Orientation" +msgstr "İstiqamət" + +#: debug_console.cpp:226 +#, kde-format +msgctxt "An orientation of a pointer axis event" +msgid "Horizontal" +msgstr "Üfüqi" + +#: debug_console.cpp:227 +#, kde-format +msgctxt "An orientation of a pointer axis event" +msgid "Vertical" +msgstr "Şaquli" + +#: debug_console.cpp:228 +#, kde-format +msgctxt "The angle delta of a pointer axis event" +msgid "Delta" +msgstr "Dönmə bucağı" + +#: debug_console.cpp:243 +#, kde-format +msgctxt "A key press event" +msgid "Key Press" +msgstr "Düyməni Basılması" + +#: debug_console.cpp:246 +#, kde-format +msgctxt "A key release event" +msgid "Key Release" +msgstr "Düymənin Buraxılması" + +#: debug_console.cpp:255 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Shift" +msgstr "Shift" + +#: debug_console.cpp:259 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Control" +msgstr "Nəzarət" + +#: debug_console.cpp:263 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Alt" +msgstr "Alt" + +#: debug_console.cpp:267 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Meta" +msgstr "Meta" + +#: debug_console.cpp:271 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Keypad" +msgstr "Əlavə Klaviatura" + +#: debug_console.cpp:275 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Group-switch" +msgstr "Qrup Dəyişdirici" + +#: debug_console.cpp:281 +#, kde-format +msgctxt "Whether the event is an automatic key repeat" +msgid "Repeat" +msgstr "Təkrar" + +#: debug_console.cpp:285 +#, kde-format +msgctxt "The code as read from the input device" +msgid "Scan code" +msgstr "Skan-kod" + +#: debug_console.cpp:286 +#, kde-format +msgctxt "Key according to Qt" +msgid "Qt::Key code" +msgstr "Qt::Düymə kodu" + +#: debug_console.cpp:288 +#, kde-format +msgctxt "The translated code to an Xkb symbol" +msgid "Xkb symbol" +msgstr "Xkb simvol" + +#: debug_console.cpp:289 +#, kde-format +msgctxt "The translated code interpreted as text" +msgid "Utf8" +msgstr "Utf8" + +#: debug_console.cpp:290 +#, kde-format +msgctxt "The currently active modifiers" +msgid "Modifiers" +msgstr "Dəyişdiricilər" + +#: debug_console.cpp:302 +#, kde-format +msgctxt "A touch down event" +msgid "Touch down" +msgstr "Aşağı toxunma" + +#: debug_console.cpp:304 debug_console.cpp:319 debug_console.cpp:334 +#, kde-format +msgctxt "The id of the touch point in the touch event" +msgid "Point identifier" +msgstr "Nöqtə tanıma" + +#: debug_console.cpp:305 debug_console.cpp:320 +#, kde-format +msgctxt "The global position of the touch point" +msgid "Global position" +msgstr "Qlobal Mövqe" + +#: debug_console.cpp:317 +#, kde-format +msgctxt "A touch motion event" +msgid "Touch Motion" +msgstr "Toxynma Nöqtəsinin Hərəkəti" + +#: debug_console.cpp:332 +#, kde-format +msgctxt "A touch up event" +msgid "Touch Up" +msgstr "Yuxarı toxunmaq" + +#: debug_console.cpp:345 +#, kde-format +msgctxt "A pinch gesture is started" +msgid "Pinch start" +msgstr "Sıxılma başlanğıcı" + +#: debug_console.cpp:347 +#, kde-format +msgctxt "Number of fingers in this pinch gesture" +msgid "Finger count" +msgstr "Barmaq sayı" + +#: debug_console.cpp:358 +#, kde-format +msgctxt "A pinch gesture is updated" +msgid "Pinch update" +msgstr "Sıxılma yenilənməsi" + +#: debug_console.cpp:360 +#, kde-format +msgctxt "Current scale in pinch gesture" +msgid "Scale" +msgstr "Miqyas" + +#: debug_console.cpp:361 +#, kde-format +msgctxt "Current angle in pinch gesture" +msgid "Angle delta" +msgstr "Hərləmə bucağı" + +#: debug_console.cpp:362 +#, kde-format +msgctxt "Current delta in pinch gesture" +msgid "Delta x" +msgstr "x üzrə yerdəyişmə" + +#: debug_console.cpp:363 +#, kde-format +msgctxt "Current delta in pinch gesture" +msgid "Delta y" +msgstr "y üzrə yerdəyişmə" + +#: debug_console.cpp:374 +#, kde-format +msgctxt "A pinch gesture ended" +msgid "Pinch end" +msgstr "Sıxılmanın sonu" + +#: debug_console.cpp:386 +#, kde-format +msgctxt "A pinch gesture got cancelled" +msgid "Pinch cancelled" +msgstr "Sıxılmadan imtina" + +#: debug_console.cpp:398 +#, kde-format +msgctxt "A swipe gesture is started" +msgid "Swipe start" +msgstr "Sürüşdürmə başlanğıcı" + +#: debug_console.cpp:400 +#, kde-format +msgctxt "Number of fingers in this swipe gesture" +msgid "Finger count" +msgstr "Barmaq sayı" + +#: debug_console.cpp:411 +#, kde-format +msgctxt "A swipe gesture is updated" +msgid "Swipe update" +msgstr "Sürüşdürmə yenilənməsi" + +#: debug_console.cpp:413 +#, kde-format +msgctxt "Current delta in swipe gesture" +msgid "Delta x" +msgstr "x üzrə yerdəyişmə" + +#: debug_console.cpp:414 +#, kde-format +msgctxt "Current delta in swipe gesture" +msgid "Delta y" +msgstr "y üzrə yerdəyişmə" + +#: debug_console.cpp:425 +#, kde-format +msgctxt "A swipe gesture ended" +msgid "Swipe end" +msgstr "Sürüşdürmənin sonu" + +#: debug_console.cpp:437 +#, kde-format +msgctxt "A swipe gesture got cancelled" +msgid "Swipe cancelled" +msgstr "Sürüşdürmədəm imtina" + +#: debug_console.cpp:449 +#, kde-format +msgctxt "A hardware switch (e.g. notebook lid) got toggled" +msgid "Switch toggled" +msgstr "Dəyişdirici açarın işə düşməsi" + +#: debug_console.cpp:457 +#, kde-format +msgctxt "Name of a hardware switch" +msgid "Notebook lid" +msgstr "Noutbukun qapağı" + +#: debug_console.cpp:459 +#, kde-format +msgctxt "Name of a hardware switch" +msgid "Tablet mode" +msgstr "Planşet rejimi" + +#: debug_console.cpp:461 +#, kde-format +msgctxt "A hardware switch" +msgid "Switch" +msgstr "Dəyişdirmək" + +#: debug_console.cpp:465 +#, kde-format +msgctxt "The hardware switch got turned off" +msgid "Off" +msgstr "Söndürülüb" + +#: debug_console.cpp:468 +#, kde-format +msgctxt "The hardware switch got turned on" +msgid "On" +msgstr "Aktivdir" + +#: debug_console.cpp:473 +#, kde-format +msgctxt "State of a hardware switch (on/off)" +msgid "State" +msgstr "Vəziyyət" + +#: debug_console.cpp:488 +#, kde-format +msgid "Tablet Tool" +msgstr "Planşet Aləti" + +#: debug_console.cpp:489 +#, kde-format +msgid "EventType" +msgstr "Hadisənin Növü" + +#: debug_console.cpp:490 debug_console.cpp:537 debug_console.cpp:549 +#, kde-format +msgid "Position" +msgstr "Mövqe" + +#: debug_console.cpp:492 +#, kde-format +msgid "Tilt" +msgstr "Meyillki" + +#: debug_console.cpp:494 +#, kde-format +msgid "Rotation" +msgstr "Dönmə" + +#: debug_console.cpp:495 +#, kde-format +msgid "Pressure" +msgstr "Basılma təzyiqi" + +#: debug_console.cpp:496 +#, kde-format +msgid "Buttons" +msgstr "Düymələr" + +#. i18n: ectx: property (title), widget (QGroupBox, modifiersBox) +#: debug_console.cpp:497 debug_console.ui:356 +#, kde-format +msgid "Modifiers" +msgstr "Dəyişdiricilər" + +#: debug_console.cpp:510 +#, kde-format +msgid "Tablet Tool Button" +msgstr "Planşet Aləti Düyməsi" + +#: debug_console.cpp:511 debug_console.cpp:526 +#, kde-format +msgid "Pressed Buttons" +msgstr "Basılan düymələr" + +#: debug_console.cpp:525 +#, kde-format +msgid "Tablet Pad Button" +msgstr "Planşet Düymələri" + +#: debug_console.cpp:535 +#, kde-format +msgid "Tablet Pad Strip" +msgstr "Planşetin Toxunma Zolağı" + +#: debug_console.cpp:536 debug_console.cpp:548 +#, kde-format +msgid "Number" +msgstr "Say" + +#: debug_console.cpp:538 debug_console.cpp:550 +#, kde-format +msgid "isFinger" +msgstr "isFinger" + +#: debug_console.cpp:547 +#, kde-format +msgid "Tablet Pad Ring" +msgstr "Planşetin Toxunma Halqası" + +#: debug_console.cpp:735 +#, kde-format +msgid "No Mouse Buttons" +msgstr "Siçan Düymələri Yoxdur" + +#: debug_console.cpp:739 +#, kde-format +msgctxt "Mouse Button" +msgid "left" +msgstr "sol" + +#: debug_console.cpp:742 +#, kde-format +msgctxt "Mouse Button" +msgid "right" +msgstr "sağ" + +#: debug_console.cpp:745 +#, kde-format +msgctxt "Mouse Button" +msgid "middle" +msgstr "orta" + +#: debug_console.cpp:748 +#, kde-format +msgctxt "Mouse Button" +msgid "back" +msgstr "geriyə" + +#: debug_console.cpp:751 +#, kde-format +msgctxt "Mouse Button" +msgid "forward" +msgstr "irəli" + +#: debug_console.cpp:754 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 1" +msgstr "əlavə 1" + +#: debug_console.cpp:757 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 2" +msgstr "əlavə 2" + +#: debug_console.cpp:760 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 3" +msgstr "əlavə 3" + +#: debug_console.cpp:763 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 4" +msgstr "əlavə 4" + +#: debug_console.cpp:766 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 5" +msgstr "əlavə 5" + +#: debug_console.cpp:769 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 6" +msgstr "əlavə 6" + +#: debug_console.cpp:772 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 7" +msgstr "əlavə 7" + +#: debug_console.cpp:775 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 8" +msgstr "əlavə 8" + +#: debug_console.cpp:778 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 9" +msgstr "əlavə 9" + +#: debug_console.cpp:781 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 10" +msgstr "əlavə 10" + +#: debug_console.cpp:784 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 11" +msgstr "əlavə 11" + +#: debug_console.cpp:787 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 12" +msgstr "əlavə 12" + +#: debug_console.cpp:790 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 13" +msgstr "əlavə 13" + +#: debug_console.cpp:793 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 14" +msgstr "əlavə 14" + +#: debug_console.cpp:796 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 15" +msgstr "əlavə 15" + +#: debug_console.cpp:799 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 16" +msgstr "əlavə 16" + +#: debug_console.cpp:802 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 17" +msgstr "əlavə 17" + +#: debug_console.cpp:805 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 18" +msgstr "əlavə 18" + +#: debug_console.cpp:808 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 19" +msgstr "əlavə 19" + +#: debug_console.cpp:811 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 20" +msgstr "əlavə 20" + +#: debug_console.cpp:814 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 21" +msgstr "əlavə 21" + +#: debug_console.cpp:817 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 22" +msgstr "əlavə 22" + +#: debug_console.cpp:820 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 23" +msgstr "əlavə 23" + +#: debug_console.cpp:823 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 24" +msgstr "əlavə 24" + +#: debug_console.cpp:826 +#, kde-format +msgctxt "Mouse Button" +msgid "task" +msgstr "tapşırıq" + +#: debug_console.cpp:1176 +#, kde-format +msgid "X11 Client Windows" +msgstr "X11 Müştəri Pəncərələri" + +#: debug_console.cpp:1178 +#, kde-format +msgid "X11 Unmanaged Windows" +msgstr "X11 İdarəedilməyən Pəncərələri" + +#: debug_console.cpp:1180 +#, kde-format +msgid "Wayland Windows" +msgstr "Wayland Pəncərələri" + +#: debug_console.cpp:1182 +#, kde-format +msgid "Internal Windows" +msgstr "Daxili Pəncərələr" + +#. i18n: ectx: property (text), widget (QPushButton, quitButton) +#: debug_console.ui:32 +#, kde-format +msgid "Quit Debug Console" +msgstr "Sazlayıcı Konsolu bağlamaq" + +#. i18n: ectx: attribute (title), widget (QWidget, windows) +#: debug_console.ui:45 +#, kde-format +msgid "Windows" +msgstr "Pəncərələr" + +#. i18n: ectx: attribute (title), widget (QWidget, surfaces) +#: debug_console.ui:59 +#, kde-format +msgid "Surfaces" +msgstr "Səthi" + +#. i18n: ectx: attribute (title), widget (QWidget, input) +#: debug_console.ui:69 +#, kde-format +msgid "Input Events" +msgstr "Daxili Hadisələr" + +#. i18n: ectx: attribute (title), widget (QWidget, inputDevices) +#: debug_console.ui:86 +#, kde-format +msgid "Input Devices" +msgstr "Daxiletmə cihazları" + +#. i18n: ectx: attribute (title), widget (QWidget, tab) +#: debug_console.ui:96 +#, kde-format +msgid "OpenGL" +msgstr "OpenGL" + +#. i18n: ectx: property (text), widget (QLabel, noOpenGLLabel) +#: debug_console.ui:102 +#, kde-format +msgid "No OpenGL compositor running" +msgstr "OpenGL-ə əsaslanan kompozit pəncərə meneceri başladılmayıb" + +#. i18n: ectx: property (title), widget (QGroupBox, driverInfoBox) +#: debug_console.ui:130 +#, kde-format +msgid "OpenGL (ES) driver information" +msgstr "OpenGL (ES) sürücüsü haqqında" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: debug_console.ui:136 +#, kde-format +msgid "Vendor:" +msgstr "Təhcizatçı:" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: debug_console.ui:143 +#, kde-format +msgid "Renderer:" +msgstr "Görüntü işlənməsi modulu:" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: debug_console.ui:150 +#, kde-format +msgid "Version:" +msgstr "Versiya:" + +# Qrafiakada obyektin görünüşünün işlənməsində kölgələmə effektinin yaradılması üçün proqram dili. +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: debug_console.ui:157 +#, kde-format +msgid "Shading Language Version:" +msgstr "Kölgələmə Proqram Dili Versiyası:" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: debug_console.ui:164 +#, kde-format +msgid "Driver:" +msgstr "Sürücü:" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: debug_console.ui:171 +#, kde-format +msgid "GPU class:" +msgstr "Qrafik Prosessorun Seriyası:" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: debug_console.ui:178 +#, kde-format +msgid "OpenGL Version:" +msgstr "OpenGL Versiyası:" + +#. i18n: ectx: property (text), widget (QLabel, label_8) +#: debug_console.ui:185 +#, kde-format +msgid "GLSL Version:" +msgstr "GLSL Versiyası:" + +#. i18n: ectx: property (title), widget (QGroupBox, platformExtensionsBox) +#: debug_console.ui:251 +#, kde-format +msgid "Platform Extensions" +msgstr "Genişləndirilmiş Platformalar" + +#. i18n: ectx: property (title), widget (QGroupBox, glExtensionsBox) +#: debug_console.ui:267 +#, kde-format +msgid "OpenGL (ES) Extensions" +msgstr "OpenGL (ES) Genişlənmələri" + +#. i18n: ectx: attribute (title), widget (QWidget, keyboard) +#: debug_console.ui:288 +#, kde-format +msgid "Keyboard" +msgstr "Klaviatura" + +#. i18n: ectx: property (title), widget (QGroupBox, layoutBox) +#: debug_console.ui:315 +#, kde-format +msgid "Keymap Layouts" +msgstr "Klaviatura Qatları" + +#. i18n: ectx: property (text), widget (QLabel, label_9) +#: debug_console.ui:337 +#, kde-format +msgid "Current Layout:" +msgstr "Cari klaviatura qatı:" + +#. i18n: ectx: property (title), widget (QGroupBox, activeModifiersBox) +#: debug_console.ui:372 +#, kde-format +msgid "Active Modifiers" +msgstr "Aktiv dəyişdirici düymələri" + +#. i18n: ectx: property (title), widget (QGroupBox, ledsBox) +#: debug_console.ui:388 +#, kde-format +msgid "LEDs" +msgstr "LED-lər" + +#. i18n: ectx: property (title), widget (QGroupBox, activeLedsBox) +#: debug_console.ui:404 +#, kde-format +msgid "Active LEDs" +msgstr "Aktiv LED-lər" + +#: helpers/killer/killer.cpp:30 +#, kde-format +msgid "Window Manager" +msgstr "Pəncərə Meneceri" + +#: helpers/killer/killer.cpp:35 +#, kde-format +msgid "PID of the application to terminate" +msgstr "Bağlanacaq tətbiqlərin PİD-i" + +# PİD - Çoxtapşırıqlı Əməliyyat Sistemlərində prosesin özünəməxsus nömrəsi. +#: helpers/killer/killer.cpp:35 +#, kde-format +msgid "pid" +msgstr "pid" + +#: helpers/killer/killer.cpp:37 +#, kde-format +msgid "Hostname on which the application is running" +msgstr "Tətbiqin işlək olduğu host adı" + +#: helpers/killer/killer.cpp:37 +#, kde-format +msgid "hostname" +msgstr "host_adı" + +#: helpers/killer/killer.cpp:39 +#, kde-format +msgid "Caption of the window to be terminated" +msgstr "Bağlanacaq pəncərənin başlığı" + +#: helpers/killer/killer.cpp:39 +#, kde-format +msgid "caption" +msgstr "başlıq" + +#: helpers/killer/killer.cpp:41 +#, kde-format +msgid "Name of the application to be terminated" +msgstr "Bağlanacaq tətbiqin adı" + +#: helpers/killer/killer.cpp:41 +#, kde-format +msgid "name" +msgstr "ad" + +#: helpers/killer/killer.cpp:43 +#, kde-format +msgid "ID of resource belonging to the application" +msgstr "Tətbiqə aid mənbənin İD-si" + +#: helpers/killer/killer.cpp:43 +#, kde-format +msgid "id" +msgstr "id" + +#: helpers/killer/killer.cpp:45 +#, kde-format +msgid "Time of user action causing termination" +msgstr "İstifadəçinin xitam vermə fəaliyyətinin müddəti" + +#: helpers/killer/killer.cpp:45 +#, kde-format +msgid "time" +msgstr "vaxt" + +#: helpers/killer/killer.cpp:47 +#, kde-format +msgid "KWin helper utility" +msgstr "KWin köməkçi vasitəsi" + +#: helpers/killer/killer.cpp:71 +#, kde-format +msgid "This helper utility is not supposed to be called directly." +msgstr "Bu köməkçi vasitə birbaşa çağırıla bilməz" + +#: helpers/killer/killer.cpp:81 +#, kde-format +msgctxt "@info" +msgid "Application \"%1\" is not responding" +msgstr "\"%1\" tətbiqi cavab vermir" + +#: helpers/killer/killer.cpp:83 +#, kde-kuit-format +msgctxt "@info" +msgid "" +"

You tried to close window \"%1\" from application \"%2\" (Process ID: %3) " +"but the application is not responding.

" +msgstr "" +"

Siz \"%2\" tətbiqinddəki \"%1\" pəncərəsini bağlamağa cəhd etdiniz " +"(Prosesin İD-si: %3) lakin bu tətbiq cavab vermir.

" + +#: helpers/killer/killer.cpp:85 +#, kde-kuit-format +msgctxt "@info" +msgid "" +"

You tried to close window \"%1\" from application \"%2\" (Process ID: " +"%3), running on host \"%4\", but the application is not responding.

" +msgstr "" +"

Siz \"%4\" kompyuterində açılan \"%2\" tətbiqinddəki \"%1\" pəncərəsini " +"bağlamağa cəhd etdiniz (Prosesin İD-si: %3) lakin bu tətbiq cavab vermir.

" + +#: helpers/killer/killer.cpp:88 +#, kde-kuit-format +msgctxt "@info" +msgid "" +"

Do you want to terminate this application?

Terminating the " +"application will close all of its child windows. Any unsaved data will be " +"lost.

" +msgstr "" +"

Bu tətbiqi bağlamaq istəyirsiniz?

Bu tətbiqin bağlanması " +"ona aid bütün pəncərələri bağlayacaq. Bu halda saxlanılmamış məlumatlar " +"itiriləcəkdir.

" + +#: helpers/killer/killer.cpp:92 +#, kde-format +msgid "&Terminate Application %1" +msgstr "%1 &Tətbiqibi bağlamaq" + +#: helpers/killer/killer.cpp:93 +#, kde-format +msgid "Wait Longer" +msgstr "Bir az daha gözləmək" + +#: keyboard_layout.cpp:110 keyboard_layout.cpp:111 +#, kde-format +msgctxt "tooltip title" +msgid "Keyboard Layout" +msgstr "Klaviatura qatı" + +#: keyboard_layout.cpp:258 +#, kde-format +msgid "Configure Layouts..." +msgstr "Qatları tənzimləmək..." + +#: killwindow.cpp:33 +#, kde-format +msgid "" +"Select window to force close with left click or enter.\n" +"Escape or right click to cancel." +msgstr "" +"Enter və ya sol kliklə məcburi bağlamaq üçün pəncərəni seçmək.\n" +"Ləğv etmək üçün Escape düyməsinə vurun və ya sağ düyməni klikləyin." + +#: kwinbindings.cpp:38 +#, kde-format +msgid "Window Operations Menu" +msgstr "Pəncərə Əməliyyatı Menyusu" + +#: kwinbindings.cpp:40 +#, kde-format +msgid "Close Window" +msgstr "Pəncərəni Bağlamaq" + +#: kwinbindings.cpp:42 +#, kde-format +msgid "Maximize Window" +msgstr "Pəncərəni Böyütmək" + +#: kwinbindings.cpp:44 +#, kde-format +msgid "Maximize Window Vertically" +msgstr "Pəncərəni Şaquli Böyütmək" + +#: kwinbindings.cpp:46 +#, kde-format +msgid "Maximize Window Horizontally" +msgstr "Pəncərəni Üfüqi Böyütmək" + +#: kwinbindings.cpp:48 +#, kde-format +msgid "Minimize Window" +msgstr "Pəncərəni Yığmaq" + +#: kwinbindings.cpp:50 +#, kde-format +msgid "Shade Window" +msgstr "Pəncərəni Başlığa Yığmaq" + +#: kwinbindings.cpp:52 +#, kde-format +msgid "Move Window" +msgstr "Pəncərənin Yerini Dəyişmək" + +#: kwinbindings.cpp:54 +#, kde-format +msgid "Resize Window" +msgstr "Pəncərənin Ölçüsünü Dəyişmək" + +#: kwinbindings.cpp:56 +#, kde-format +msgid "Raise Window" +msgstr "Pəncərəni Qaldırmaq" + +#: kwinbindings.cpp:58 +#, kde-format +msgid "Lower Window" +msgstr "Pəncərəni Endirmək" + +#: kwinbindings.cpp:60 +#, kde-format +msgid "Toggle Window Raise/Lower" +msgstr "Pəncərəni endirmək/qaldərmaq" + +#: kwinbindings.cpp:62 +#, kde-format +msgid "Make Window Fullscreen" +msgstr "Pəncərəni Tam Ekran Açmaq" + +#: kwinbindings.cpp:64 +#, kde-format +msgid "Hide Window Border" +msgstr "Pəncərə Çərçivəsini Gizlətmək" + +#: kwinbindings.cpp:66 +#, kde-format +msgid "Keep Window Above Others" +msgstr "Pəncərənin Digərilərinin Üzərində Tutmaq" + +#: kwinbindings.cpp:68 +#, kde-format +msgid "Keep Window Below Others" +msgstr "Pəncərəni Digərilərinin Altında Tutmaq" + +#: kwinbindings.cpp:70 +#, kde-format +msgid "Activate Window Demanding Attention" +msgstr "Pəncərəyə Diqqət Yetirilməsini Aktivləşdirmək" + +#: kwinbindings.cpp:72 +#, kde-format +msgid "Setup Window Shortcut" +msgstr "Pəncərə Qısayolunu Qurmaq" + +#: kwinbindings.cpp:74 +#, kde-format +msgid "Pack Window to the Right" +msgstr "Pəncərəni Sağda Qruplaşdırmaq" + +#: kwinbindings.cpp:76 +#, kde-format +msgid "Pack Window to the Left" +msgstr "Pəncərəni Solda Qruplaşdırmaq" + +#: kwinbindings.cpp:78 +#, kde-format +msgid "Pack Window Up" +msgstr "Pəncərəni Yuxarıda Qruplaşdırmaq" + +#: kwinbindings.cpp:80 +#, kde-format +msgid "Pack Window Down" +msgstr "Pəncərəni Aşağıda Qruplaşdırmaq" + +#: kwinbindings.cpp:82 +#, kde-format +msgid "Pack Grow Window Horizontally" +msgstr "Pəncərəni Üfüqi Böyütmək" + +#: kwinbindings.cpp:84 +#, kde-format +msgid "Pack Grow Window Vertically" +msgstr "Pəncərəni Şaquli Böyütmək" + +#: kwinbindings.cpp:86 +#, kde-format +msgid "Pack Shrink Window Horizontally" +msgstr "Pəncərəni Üfüqi Sıxlaşdırmaq" + +#: kwinbindings.cpp:88 +#, kde-format +msgid "Pack Shrink Window Vertically" +msgstr "Pəncərəni Şaquli Sıxlaşdırmaq" + +#: kwinbindings.cpp:90 +#, kde-format +msgid "Quick Tile Window to the Left" +msgstr "Pəncərəni Ekranın Sol Yarısı Qədər Açmaq" + +#: kwinbindings.cpp:92 +#, kde-format +msgid "Quick Tile Window to the Right" +msgstr "Pəncərəni Ekranın Sağ Yarısı Qədər Açmaq" + +#: kwinbindings.cpp:94 +#, kde-format +msgid "Quick Tile Window to the Top" +msgstr "Pəncərəni Ekranın Yuxarı Yarısı Qədər Açmaq" + +#: kwinbindings.cpp:96 +#, kde-format +msgid "Quick Tile Window to the Bottom" +msgstr "Pəncərəni Ekranın Aşağı Yarısı Qədər Açmaq" + +#: kwinbindings.cpp:98 +#, kde-format +msgid "Quick Tile Window to the Top Left" +msgstr "Pəncərəni Ekranın Sol Yuxarı Dörddə Biri Qədər Açmaq" + +#: kwinbindings.cpp:100 +#, kde-format +msgid "Quick Tile Window to the Bottom Left" +msgstr "Pəncərəni Ekranın Sol Aşağı Dörddə Biri Qədər Açmaq" + +#: kwinbindings.cpp:102 +#, kde-format +msgid "Quick Tile Window to the Top Right" +msgstr "Pəncərəni Ekranın Sağ Yuxarı Dörddə Biri Qədər Açmaq" + +#: kwinbindings.cpp:104 +#, kde-format +msgid "Quick Tile Window to the Bottom Right" +msgstr "Pəncərəni Ekranın Sağ Aşağı Dörddə Biri Qədər Açmaq" + +#: kwinbindings.cpp:106 +#, kde-format +msgid "Switch to Window Above" +msgstr "Üstdəki Pəncərəyə Keçmək" + +#: kwinbindings.cpp:108 +#, kde-format +msgid "Switch to Window Below" +msgstr "Altdakı Pəncərəyə Keçmək" + +#: kwinbindings.cpp:110 +#, kde-format +msgid "Switch to Window to the Right" +msgstr "Sağdakı Pəncərəyə Keçmək" + +#: kwinbindings.cpp:112 +#, kde-format +msgid "Switch to Window to the Left" +msgstr "Soldakı Pəncərəyə Keçmək" + +#: kwinbindings.cpp:114 +#, kde-format +msgid "Increase Opacity of Active Window by 5 %" +msgstr "Pəncərənin qeyri-şəffaflığını 5% artırmaq" + +#: kwinbindings.cpp:116 +#, kde-format +msgid "Decrease Opacity of Active Window by 5 %" +msgstr "Pəncərənin qeyri-şəffaflığını 5% azaltmaq" + +#: kwinbindings.cpp:119 +#, kde-format +msgid "Keep Window on All Desktops" +msgstr "Pəncərəni Bütün İş Masalarında Saxlamaq" + +#: kwinbindings.cpp:123 +#, kde-format +msgid "Window to Desktop %1" +msgstr "Pəncərəni %1 İş Masasına köçürmək" + +#: kwinbindings.cpp:125 +#, kde-format +msgid "Window to Next Desktop" +msgstr "Pəncərəni Növbəti İş Masasına köçürmək" + +#: kwinbindings.cpp:126 +#, kde-format +msgid "Window to Previous Desktop" +msgstr "Pəncərəni Əvvəlki İş Masasına köçürmək" + +#: kwinbindings.cpp:127 +#, kde-format +msgid "Window One Desktop to the Right" +msgstr "Pəncərəni Bir İş Masası Sağa köçürmək" + +#: kwinbindings.cpp:128 +#, kde-format +msgid "Window One Desktop to the Left" +msgstr "Pəncərəni Bir İş Masası Sola köçürmək" + +#: kwinbindings.cpp:129 +#, kde-format +msgid "Window One Desktop Up" +msgstr "Pəncərəni Bir İş Masası Yuxarı köçürmək" + +#: kwinbindings.cpp:130 +#, kde-format +msgid "Window One Desktop Down" +msgstr "Pəncərəni Bir İş Masası Aşağı köçürmək" + +#: kwinbindings.cpp:133 +#, kde-format +msgid "Window to Screen %1" +msgstr "Pəncərəni %1 Ekranına köçürmək" + +#: kwinbindings.cpp:135 +#, kde-format +msgid "Window to Next Screen" +msgstr "Pəncərəni Növbəti Ekrana köçürmək" + +#: kwinbindings.cpp:136 +#, kde-format +msgid "Window to Previous Screen" +msgstr "Pəncərəni Əvvəlki Ekrana köçürmək" + +#: kwinbindings.cpp:137 +#, kde-format +msgid "Show Desktop" +msgstr "İş Masasını Göstərmək" + +#: kwinbindings.cpp:140 +#, kde-format +msgid "Switch to Screen %1" +msgstr "%1 ekranına keçid " + +#: kwinbindings.cpp:143 +#, kde-format +msgid "Switch to Next Screen" +msgstr "Növbəti Ekrana Keçid" + +#: kwinbindings.cpp:144 +#, kde-format +msgid "Switch to Previous Screen" +msgstr "Əvvəlki Ekrana Keçid" + +#: kwinbindings.cpp:146 +#, kde-format +msgid "Kill Window" +msgstr "Pəncərəni Ləğv etmək" + +#: kwinbindings.cpp:147 +#, kde-format +msgid "Suspend Compositing" +msgstr "Kompozit Effektləri Dayandırmaq" + +#: kwinbindings.cpp:148 +#, kde-format +msgid "Invert Screen Colors" +msgstr "Ekran Rənglərinin Əks Dəyişimi" + +#: main.cpp:184 main.cpp:214 +#, kde-format +msgid "KDE window manager" +msgstr "KDE pəncərə meneceri" + +#: main.cpp:189 +#, kde-format +msgid "KWin" +msgstr "KWin" + +#: main.cpp:193 +#, kde-format +msgid "(c) 1999-2019, The KDE Developers" +msgstr "(c) 1999-2019, The KDE Developers" + +#: main.cpp:195 +#, kde-format +msgid "Matthias Ettrich" +msgstr "Matthias Ettrich" + +#: main.cpp:196 +#, kde-format +msgid "Cristian Tibirna" +msgstr "Cristian Tibirna" + +#: main.cpp:197 +#, kde-format +msgid "Daniel M. Duley" +msgstr "Daniel M. Duley" + +#: main.cpp:198 +#, kde-format +msgid "Luboš Luňák" +msgstr "Luboš Luňák" + +#: main.cpp:199 +#, kde-format +msgid "Martin Flöser" +msgstr "Martin Flöser" + +#: main.cpp:200 +#, kde-format +msgid "David Edmundson" +msgstr "David Edmundson" + +#: main.cpp:201 +#, kde-format +msgid "Roman Gilg" +msgstr "Roman Gilg" + +#: main.cpp:202 +#, kde-format +msgid "Vlad Zahorodnii" +msgstr "Vlad Zahorodnii" + +#: main.cpp:211 +#, kde-format +msgid "Disable configuration options" +msgstr "Tənzimləmə seçimlərini söndürmək" + +#: main.cpp:212 +#, kde-format +msgid "Indicate that KWin has recently crashed n times" +msgstr "KWin-də neçə dəfə xəta baş verdiyini göstərir" + +#: main_wayland.cpp:459 +#, kde-format +msgid "Start a rootless Xwayland server." +msgstr "Köksüz Xwayland serverini başlatmaq." + +#: main_wayland.cpp:461 +#, kde-format +msgid "" +"Name of the Wayland socket to listen on. If not set \"wayland-0\" is used." +msgstr "" +"Qoşulma üçün Wayland soketinin adı. Standart olaraq \"wayland-0\" istifadə " +"olunur." + +#: main_wayland.cpp:464 +#, kde-format +msgid "Render to framebuffer." +msgstr "Framebuferə vizualizasiya etmək." + +#: main_wayland.cpp:466 +#, kde-format +msgid "The framebuffer device to render to." +msgstr "Vizualizasiya olunacaq framebufer cihazı." + +#: main_wayland.cpp:469 +#, kde-format +msgid "The X11 Display to use in windowed mode on platform X11." +msgstr "X11 platformasında pəncərə rejimində istifadə olunacaq X11 Ekranı." + +#: main_wayland.cpp:472 +#, kde-format +msgid "The Wayland Display to use in windowed mode on platform Wayland." +msgstr "" +"Wayland platformasında pəncərə rejimində istifadə olunacaq Wayland Ekranı." + +#: main_wayland.cpp:474 +#, kde-format +msgid "Render to a virtual framebuffer." +msgstr "Virtual framebuferə vizualizasiya etmək." + +#: main_wayland.cpp:476 +#, kde-format +msgid "The width for windowed mode. Default width is 1024." +msgstr "Pəncərə rejimi halında pəncərənin eni. Standart en - 1024" + +#: main_wayland.cpp:480 +#, kde-format +msgid "The height for windowed mode. Default height is 768." +msgstr "" +"Pəncərə rejimi halında pəncərənin hündürlüyü. Standart hündürlük - 1024" + +#: main_wayland.cpp:485 +#, kde-format +msgid "The scale for windowed mode. Default value is 1." +msgstr "Pəncərə rejimi halında pəncərənin miqyası. Standart miqyas - 1024" + +#: main_wayland.cpp:490 +#, kde-format +msgid "" +"The number of windows to open as outputs in windowed mode. Default value is 1" +msgstr "" +"Pəncərəli rejimdə çıxışlar kimi açılacaq pəncərələrin sayı. Standart dəyər - " +"1" + +#: main_wayland.cpp:520 +#, kde-format +msgid "Use libhybris hwcomposer" +msgstr "libhybris vsitəsi ilə təsvirin işlənməsi" + +#: main_wayland.cpp:526 +#, kde-format +msgid "" +"Enable libinput support for input events processing. Note: never use in a " +"nested session.\t(deprecated)" +msgstr "" +"Giriş əməllərinin işlənməsi üçün libinput dəstəklənməsini aktiv etmək. " +"Diqqət: heç zman iç içə sesiyada istifadə etməyin. \t(köhnəlmiş)" + +#: main_wayland.cpp:529 +#, kde-format +msgid "Render through drm node." +msgstr "DRM vasitəsi ilə təsvirin işlənməsi" + +#: main_wayland.cpp:536 +#, kde-format +msgid "Input method that KWin starts." +msgstr "KWin başladılan daixil olma üsulu." + +#: main_wayland.cpp:541 +#, kde-format +msgid "List all available backends and quit." +msgstr "Bütün mövcud geri izləri siyahıya alın və çıxın" + +#: main_wayland.cpp:545 +#, kde-format +msgid "Starts the session in locked mode." +msgstr "Sesiyanı kilid rejimində başladır." + +#: main_wayland.cpp:549 +#, kde-format +msgid "Starts the session without lock screen support." +msgstr "Sesiyanı ekran kilidli olmadan başladır." + +#: main_wayland.cpp:553 +#, kde-format +msgid "Starts the session without global shortcuts support." +msgstr "Sesiyanı qlobal qısayollar dəstəklənməsi olmadan başladır." + +#: main_wayland.cpp:557 +#, kde-format +msgid "Exit after the session application, which is started by KWin, closed." +msgstr "KWin tərəfindən başladılan sesiya tətbiqi bağlandıqdan sonra çıxmaq." + +#: main_wayland.cpp:562 +#, kde-format +msgid "Applications to start once Wayland and Xwayland server are started" +msgstr "Wayland və Xwayland başladıqdan sonra başladılacaq tətbiqlər" + +#: main_x11.cpp:65 +#, kde-format +msgid "" +"KWin is unstable.\n" +"It seems to have crashed several times in a row.\n" +"You can select another window manager to run:" +msgstr "" +"KWin stabil deyil.\n" +"Görünür ki, ardıcıl olaraq bir neçə dəfə qəzaya uğradı.\n" +"Başlatmaq üçün başqa pəncərə menecerini seçə bilərsiniz:" + +#: main_x11.cpp:224 +#, kde-format +msgid "" +"kwin: unable to claim manager selection, another wm running? (try using --" +"replace)\n" +msgstr "" +"kwin: pəncərə meneceri kimi istifadə etmək mümkün deyil, ehtimal ki, başqa " +"pəncərə meneceri başladılıb (--replace istifadə edərək cəhd edin)\n" + +#: main_x11.cpp:241 +#, kde-format +msgid "kwin: another window manager is running (try using --replace)\n" +msgstr "" +"kwin: başqa pəncərə meneceri başladılıb (--replace istifadə edərək cəhd " +"edin)\n" + +#: main_x11.cpp:437 +#, kde-format +msgid "Replace already-running ICCCM2.0-compliant window manager" +msgstr "Artıq başladılmış, ICCCM2.0-yə uyğun pəncərə menecerini əvəzləmək" + +#: main_x11.cpp:444 +#, kde-format +msgid "Disable KActivities integration." +msgstr "KDE İş Otaqları ilə inteqrasiyanı söndürmək" + +#: plugins/scenes/opengl/scene_opengl.cpp:535 +#, kde-format +msgid "Desktop effects were restarted due to a graphics reset" +msgstr "İş Masası effektləri qrafik sıfırlanması səbəbindən yenidən başladıldı" + +#. i18n: ectx: label, entry (count), group (General) +#: rulebooksettingsbase.kcfg:9 +#, kde-format +msgid "Total rules count" +msgstr "Ümumi qaydaların sayı" + +#. i18n: ectx: label, entry (description), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:10 +#, kde-format +msgid "Rule description" +msgstr "Qaydalar haqqında" + +#. i18n: ectx: label, entry (descriptionLegacy), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:13 +#, kde-format +msgid "Rule description (legacy)" +msgstr "Qaydalar haqqında (köhnəlmiş)" + +#. i18n: ectx: label, entry (DeleteRule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:16 +#, kde-format +msgid "Delete this rule (for use in imports)" +msgstr "Bu qaydanı silmək (idxal etdikdə istifadə etmək)" + +#. i18n: ectx: label, entry (wmclass), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:20 +#, kde-format +msgid "Window class (application)" +msgstr "Pəncərə sinifi (tətbiq)" + +#. i18n: ectx: label, entry (wmclassmatch), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:23 +#, kde-format +msgid "Window class string match type" +msgstr "Pənəcərə sinifi sətrinin uyğunluq növü" + +#. i18n: ectx: label, entry (wmclasscomplete), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:29 +#, kde-format +msgid "Match whole window class" +msgstr "Bütün pəncərə siniflərinə uyğun" + +#. i18n: ectx: label, entry (windowrole), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:34 +#, kde-format +msgid "Window role" +msgstr "Pəncərənin rolu" + +#. i18n: ectx: label, entry (windowrolematch), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:37 +#, kde-format +msgid "Window role string match type" +msgstr "Pəncərə rolu sətrinin uyğunluq növü" + +#. i18n: ectx: label, entry (title), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:44 +#, kde-format +msgid "Window title" +msgstr "Pəncərə başlığı" + +#. i18n: ectx: label, entry (titlematch), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:47 +#, kde-format +msgid "Window title string match type" +msgstr "Pəncərə başlığı sətrinin uyğunluq növü" + +#. i18n: ectx: label, entry (clientmachine), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:54 +#, kde-format +msgid "Machine (hostname)" +msgstr "Sistem (host_adı)" + +#. i18n: ectx: label, entry (clientmachinematch), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:57 +#, kde-format +msgid "Machine string match type" +msgstr "Sistem adı sətrinin uyğunluq növü" + +#. i18n: ectx: label, entry (types), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:64 +#, kde-format +msgid "Window types that match" +msgstr "pəncərə növünə uyğun" + +#. i18n: ectx: label, entry (placement), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:69 +#, kde-format +msgid "Initial placement" +msgstr "İlkin yerləşdirmə" + +#. i18n: ectx: label, entry (placementrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:74 +#, kde-format +msgid "Initial placement rule type" +msgstr "İlkin yerləşdirmə qaydasının növü" + +#. i18n: ectx: label, entry (position), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:79 +#, kde-format +msgid "Window position" +msgstr "Pəncərənin mövqeyi" + +#. i18n: ectx: label, entry (positionrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:83 +#, kde-format +msgid "Window position rule type" +msgstr "Pəncərənin yerləşmə qaydasının növü" + +#. i18n: ectx: label, entry (size), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:90 +#, kde-format +msgid "Window size" +msgstr "Pəncərənin ölçüsü" + +#. i18n: ectx: label, entry (sizerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:93 +#, kde-format +msgid "Window size rule type" +msgstr "Pəncərənin ölçüsü qaydasının növü" + +#. i18n: ectx: label, entry (minsize), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:100 +#, kde-format +msgid "Window minimum size" +msgstr "Pəncərənin minimum olçüsü" + +#. i18n: ectx: label, entry (minsizerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:104 +#, kde-format +msgid "Window minimum size rule type" +msgstr "Pəncərənin ən az ölçüsünün qaydası növü" + +#. i18n: ectx: label, entry (maxsize), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:109 +#, kde-format +msgid "Window maximum size" +msgstr "Pəncərənin maksimum olçüsü" + +#. i18n: ectx: label, entry (maxsizerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:113 +#, kde-format +msgid "Window maximum size rule type" +msgstr "Pəncərənin ən çox olçüsünün qaydasının növü" + +#. i18n: ectx: label, entry (opacityactive), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:118 +#, kde-format +msgid "Active opacity" +msgstr "Aktiv pəncərənin şəffaflığı" + +#. i18n: ectx: label, entry (opacityactiverule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:124 +#, kde-format +msgid "Active opacity rule type" +msgstr "Aktiv pəncərə şəffaflığının qaydasının növü" + +#. i18n: ectx: label, entry (opacityinactive), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:129 +#, kde-format +msgid "Inactive opacity" +msgstr "Qeyri-aktiv pəncərənin şəffaflığı" + +#. i18n: ectx: label, entry (opacityinactiverule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:135 +#, kde-format +msgid "Inactive opacity rule type" +msgstr "Qeyri-aktiv pəncərənin şəffaflığı qaydasının növü" + +#. i18n: ectx: label, entry (ignoregeometry), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:140 +#, kde-format +msgid "Ignore requested geometry" +msgstr "Tələb olunan həndəsi quruluşu yox say" + +#. i18n: ectx: label, entry (ignoregeometryrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:144 +#, kde-format +msgid "Ignore requested geometry rule type" +msgstr "Tələb olunan həndəsi qaydanın növünü yox say" + +#. i18n: ectx: label, entry (desktop), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:151 +#, kde-format +msgid "Desktop number" +msgstr "İş Masasının nömrəsi" + +#. i18n: ectx: label, entry (desktoprule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:155 +#, kde-format +msgid "Desktop number rule type" +msgstr "İş Masasının nömrələmə qaydası növü" + +#. i18n: ectx: label, entry (screen), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:162 +#, kde-format +msgid "Screen number" +msgstr "Ekranın nömrəsi" + +#. i18n: ectx: label, entry (screenrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:166 +#, kde-format +msgid "Screen number rule type" +msgstr "Ekranın nömrələnməsi qaydası növü" + +#. i18n: ectx: label, entry (activity), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:173 +#, kde-format +msgid "Activity" +msgstr "İş Otağı" + +#. i18n: ectx: label, entry (activityrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:176 +#, kde-format +msgid "Activity rule type" +msgstr "İş Otağı qaydasının növü" + +#. i18n: ectx: label, entry (type), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:183 +#, kde-format +msgid "Set window type to" +msgstr "Pəncərə növünü seçin" + +#. i18n: ectx: label, entry (typerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:189 +#, kde-format +msgid "Set window type rule type" +msgstr "Pəncərə növünün dəyişdirilməsi qaydasının növü" + +#. i18n: ectx: label, entry (maximizevert), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:194 +#, kde-format +msgid "Maximized vertically" +msgstr "Şaquli tam açmaq" + +#. i18n: ectx: label, entry (maximizevertrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:198 +#, kde-format +msgid "Maximized vertically rule type" +msgstr "Şaquli tam açma qaydasının növü" + +#. i18n: ectx: label, entry (maximizehoriz), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:205 +#, kde-format +msgid "Maximized horizontally" +msgstr "Üfüqi tam açma" + +#. i18n: ectx: label, entry (maximizehorizrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:209 +#, kde-format +msgid "Maximized horizontally rule type" +msgstr "Üfüqi tam açma qaydasının növü" + +#. i18n: ectx: label, entry (minimize), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:216 +#, kde-format +msgid "Minimized" +msgstr "Yığılmış" + +#. i18n: ectx: label, entry (minimizerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:220 +#, kde-format +msgid "Minimized rule type" +msgstr "Yığilma qaydasının növü" + +#. i18n: ectx: label, entry (shade), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:227 +#, kde-format +msgid "Shaded" +msgstr "Başlığa yığılmış" + +#. i18n: ectx: label, entry (shaderule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:231 +#, kde-format +msgid "Shaded rule type" +msgstr "Başlığa yığılma qaydasının növü" + +#. i18n: ectx: label, entry (skiptaskbar), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:238 +#, kde-format +msgid "Skip taskbar" +msgstr "Tapşırıq panelində göstərməmək" + +#. i18n: ectx: label, entry (skiptaskbarrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:242 +#, kde-format +msgid "Skip taskbar rule type" +msgstr "Tapşırıq panelində göstərməmə qaydasının növü" + +#. i18n: ectx: label, entry (skippager), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:249 +#, kde-format +msgid "Skip pager" +msgstr "İş Masası dəyişdiricisində göstərməmək" + +#. i18n: ectx: label, entry (skippagerrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:253 +#, kde-format +msgid "Skip pager rule type" +msgstr "İş Masası dəyişdiricisində göstərməmək qaydasının növü" + +#. i18n: ectx: label, entry (skipswitcher), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:260 +#, kde-format +msgid "Skip switcher" +msgstr "Pəncərəni dəyişdirirkən göstərmək" + +#. i18n: ectx: label, entry (skipswitcherrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:264 +#, kde-format +msgid "Skip switcher rule type" +msgstr "Pəncərə dəyişdiricisində göstərməmək qaydasının növü" + +#. i18n: ectx: label, entry (above), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:271 +#, kde-format +msgid "Keep above" +msgstr "Digərilərinin üzərində tutmaq" + +#. i18n: ectx: label, entry (aboverule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:275 +#, kde-format +msgid "Keep above rule type" +msgstr "Digərilərinin üzərində tutmaq qaydasının növü" + +#. i18n: ectx: label, entry (below), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:282 +#, kde-format +msgid "Keep below" +msgstr "Arxa planda tutmaq" + +#. i18n: ectx: label, entry (belowrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:286 +#, kde-format +msgid "Keep below rule type" +msgstr "Arxa planda tutmaq qaydasının növü" + +#. i18n: ectx: label, entry (fullscreen), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:293 +#, kde-format +msgid "Fullscreen" +msgstr "Tam ekran" + +#. i18n: ectx: label, entry (fullscreenrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:297 +#, kde-format +msgid "Fullscreen rule type" +msgstr "Tam ekran qaydasının növü" + +#. i18n: ectx: label, entry (noborder), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:304 +#, kde-format +msgid "No titlebar and frame" +msgstr "Başlıqsız və çərçivəsiz" + +#. i18n: ectx: label, entry (noborderrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:308 +#, kde-format +msgid "No titlebar rule type" +msgstr "Başlıqsız pəncərə qaydasının növü" + +#. i18n: ectx: label, entry (decocolor), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:315 +#, kde-format +msgid "Titlebar color and scheme" +msgstr "Başlıq rəngi və sxemi" + +#. i18n: ectx: label, entry (decocolorrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:318 +#, kde-format +msgid "Titlebar color rule type" +msgstr "Başlıq rəngi qaydasının növü" + +#. i18n: ectx: label, entry (blockcompositing), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:323 +#, kde-format +msgid "Block Compositing" +msgstr "Effektləri əngəlləmək" + +#. i18n: ectx: label, entry (blockcompositingrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:327 +#, kde-format +msgid "Block Compositing rule type" +msgstr "Effektlərin əngəllənməsi qaydasının növü" + +#. i18n: ectx: label, entry (fsplevel), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:332 +#, kde-format +msgid "Focus stealing prevention" +msgstr "Fokus oğurlanmasını əngəlləmək" + +#. i18n: ectx: label, entry (fsplevelrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:338 +#, kde-format +msgid "Focus stealing prevention rule type" +msgstr "Fokus oğurlanması əngəllənmə qaydasının növü" + +#. i18n: ectx: label, entry (fpplevel), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:343 +#, kde-format +msgid "Focus protection" +msgstr "Fokusun qorunması" + +#. i18n: ectx: label, entry (fpplevelrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:349 +#, kde-format +msgid "Focus protection rule type" +msgstr "Fokusun qorunması qaydasının növü" + +#. i18n: ectx: label, entry (acceptfocus), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:354 +#, kde-format +msgid "Accept Focus" +msgstr "Fokuslaşmanı qəbul etmək" + +#. i18n: ectx: label, entry (acceptfocusrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:358 +#, kde-format +msgid "Accept Focus rule type" +msgstr "Fokuslaşmanın qəbul edilməsi qaydasının növü" + +#. i18n: ectx: label, entry (closeable), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:363 +#, kde-format +msgid "Closeable" +msgstr "Bağlana bilən" + +#. i18n: ectx: label, entry (closeablerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:367 +#, kde-format +msgid "Closeable rule type" +msgstr "Bağlana bilən pəncərə qaydasının növü" + +#. i18n: ectx: label, entry (autogroup), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:372 +#, kde-format +msgid "Autogroup with identical" +msgstr "Eyni pəncərələrlə qruplaşdırmaq" + +#. i18n: ectx: label, entry (autogrouprule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:376 +#, kde-format +msgid "Autogroup with identical rule type" +msgstr "Eyni pəncərələrlə qruplaşdırma qaydasının növü" + +#. i18n: ectx: label, entry (autogroupfg), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:381 +#, kde-format +msgid "Autogroup in foreground" +msgstr "Ön planda qruplaşdırmaq" + +#. i18n: ectx: label, entry (autogroupfgrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:385 +#, kde-format +msgid "Autogroup in foreground rule type" +msgstr "Ön planda qruplaşdırma qaydasının növü" + +#. i18n: ectx: label, entry (autogroupid), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:390 +#, kde-format +msgid "Autogroup by ID" +msgstr "İD tərəfindən qruplaşdırmaq" + +#. i18n: ectx: label, entry (autogroupidrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:393 +#, kde-format +msgid "Autogroup by ID rule type" +msgstr "İD tərəfindən qruplaşdırma qaydasının növü" + +#. i18n: ectx: label, entry (strictgeometry), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:398 +#, kde-format +msgid "Obey geometry restrictions" +msgstr "Həndəsi məhdudiyyətlərə əməl etmək" + +#. i18n: ectx: label, entry (strictgeometryrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:402 +#, kde-format +msgid "Obey geometry restrictions rule type" +msgstr "Həndəsi məhdudiyyətlərə əməl etmə qaydasının növü" + +#. i18n: ectx: label, entry (shortcut), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:407 +#, kde-format +msgid "Shortcut" +msgstr "Qısayol" + +#. i18n: ectx: label, entry (shortcutrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:410 +#, kde-format +msgid "Shortcut rule type" +msgstr "Qısayol qaydasının növü" + +#. i18n: ectx: label, entry (disableglobalshortcuts), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:417 +#, kde-format +msgid "Ignore global shortcuts" +msgstr "Qlobal Qısayolları yox saymaq" + +#. i18n: ectx: label, entry (disableglobalshortcutsrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:421 +#, kde-format +msgid "Ignore global shortcuts rule type" +msgstr "Qlobal Qısayolları yox sayma qaydasının növü" + +#. i18n: ectx: label, entry (desktopfile), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:426 +#, kde-format +msgid "Desktop file name" +msgstr "İş Masası faylının adı" + +#. i18n: ectx: label, entry (desktopfilerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:429 +#, kde-format +msgid "Desktop file name rule type" +msgstr "İş Masası faylının adlandırma qaydasının növü" + +#: scripting/genericscriptedconfig.cpp:70 +#, kde-format +msgctxt "Error message" +msgid "Plugin does not provide configuration file in expected location" +msgstr "Plasma əlavəsi, gözlənilən yerdə tənzimləmə faylını təqdim etmir" + +#: scripting/scripting.cpp:117 +#, kde-format +msgctxt "Assertion failed in KWin script with given value" +msgid "Assertion failed: %1 is not null" +msgstr "Təsdiq edilmədi: %1, null ilə bərabər deyil" + +#: scripting/scripting.cpp:135 +#, kde-format +msgctxt "Assertion failed in KWin script" +msgid "Assertion failed: argument is null" +msgstr "Təsdiq edilmədi: arqument null ilə bərabərdir" + +#: scripting/scripting.cpp:177 +#, kde-format +msgctxt "Error in KWin Script" +msgid "" +"Invalid number of arguments. At least service, path, interface and method " +"need to be provided" +msgstr "" +"Yalnış sayda arqument. Ən azı, xidmət (service), yol (path), interfeys " +"(interface) və metod(method) təmin edilməlidir" + +#: scripting/scripting.cpp:183 +#, kde-format +msgctxt "Error in KWin Script" +msgid "" +"Invalid type. Service, path, interface and method need to be string values" +msgstr "" +"Səhv növ. Xidmət (service), yol (path), interfeys (interface) və " +"metod(method) arqumentləri sətir dəyərində olmalıdır" + +#: scripting/scriptingutils.cpp:17 +#, kde-format +msgctxt "syntax error in KWin script" +msgid "Invalid number of arguments" +msgstr "Yalnış sayda arqumentər" + +#: scripting/scriptingutils.cpp:30 +#, kde-format +msgctxt "KWin Scripting function received incorrect value for an expected type" +msgid "%1 is not a variant type" +msgstr "%1 variant növü deyil" + +#. i18n: ectx: property (windowTitle), widget (QDialog, ShortcutDialog) +#: shortcutdialog.ui:14 +#, kde-format +msgid "Dialog" +msgstr "Dialoq" + +#. i18n: ectx: property (text), widget (QToolButton, clearButton) +#: shortcutdialog.ui:25 +#, kde-format +msgid "..." +msgstr "..." + +#: tabbox/tabbox.cpp:372 +#, kde-format +msgctxt "Special entry in alt+tab list for minimizing all windows" +msgid "Show Desktop" +msgstr "İş Masasını Göstərmək" + +#: tabbox/tabbox.cpp:521 +#, kde-format +msgid "Walk Through Windows" +msgstr "Pəncərələrdə İrəli Hərəkət" + +#: tabbox/tabbox.cpp:522 +#, kde-format +msgid "Walk Through Windows (Reverse)" +msgstr "Pəncərələrdə Əksinə Hərəkət" + +#: tabbox/tabbox.cpp:523 +#, kde-format +msgid "Walk Through Windows Alternative" +msgstr "Pəncərələrdə Alternativ Hərəkət" + +#: tabbox/tabbox.cpp:524 +#, kde-format +msgid "Walk Through Windows Alternative (Reverse)" +msgstr "Pəncərələrdə Əksinə Alternativ Hərəkət" + +#: tabbox/tabbox.cpp:525 +#, kde-format +msgid "Walk Through Windows of Current Application" +msgstr "Cari tətbiqin pəncərələrində hərəkət" + +#: tabbox/tabbox.cpp:526 +#, kde-format +msgid "Walk Through Windows of Current Application (Reverse)" +msgstr "Cari tətbiqin pəncərələrində əksinə hərəkət" + +#: tabbox/tabbox.cpp:527 +#, kde-format +msgid "Walk Through Windows of Current Application Alternative" +msgstr "Cari tətbiqin pəncərələrində alternativ hərəkət" + +#: tabbox/tabbox.cpp:528 +#, kde-format +msgid "Walk Through Windows of Current Application Alternative (Reverse)" +msgstr "Cari tətbiqin pəncərələrində əksinə alternativ hərəkət" + +#: tabbox/tabbox.cpp:529 +#, kde-format +msgid "Walk Through Desktops" +msgstr "İş Masalarında Hərəkət" + +#: tabbox/tabbox.cpp:530 +#, kde-format +msgid "Walk Through Desktops (Reverse)" +msgstr "İş Masalarında əksinə hərəkət" + +#: tabbox/tabbox.cpp:531 +#, kde-format +msgid "Walk Through Desktop List" +msgstr "İş Masaları siyahısında hərəkət" + +#: tabbox/tabbox.cpp:532 +#, kde-format +msgid "Walk Through Desktop List (Reverse)" +msgstr "İş Masaları siyahısında əksinə hərəkət" + +#: tabbox/tabboxhandler.cpp:272 +#, kde-format +msgid "" +"The Window Switcher installation is broken, resources are missing.\n" +"Contact your distribution about this." +msgstr "" +"Pəncərə dəyişdiricisinin quraşdırılması pozuldu, mənbə yoxdur.\n" +"Bu haqda distribütor tərtibatşısı ilə əlaqə saxlayın." + +#: useractions.cpp:167 +#, kde-format +msgid "" +"You have selected to show a window without its border.\n" +"Without the border, you will not be able to enable the border again using " +"the mouse: use the window operations menu instead, activated using the %1 " +"keyboard shortcut." +msgstr "" +"Pəncərəni çərçivəsiz göstərməyi seçmisiniz.\n" +"Çərçivə olmadıqda onu siçanın köməyi ilə yenidən aktiv edə bilməzsiniz. " +"Bunun üçün %1 qısayol düyməsi ilə açılan pəncərə menyusundan istifadə edin." + +#: useractions.cpp:175 +#, kde-format +msgid "" +"You have selected to show a window in fullscreen mode.\n" +"If the application itself does not have an option to turn the fullscreen " +"mode off you will not be able to disable it again using the mouse: use the " +"window operations menu instead, activated using the %1 keyboard shortcut." +msgstr "" +"Pəncərənin tam ekran rejimində göstərilməsini seçmisiniz.\n" +"Əgər tətbiqin özündə tam ekran rejimini söndürmək funksiyası yoxdursa " +"siçanın köməyi ilə bu rejimi söndürmək mümkün deyil. Bunun üçün %1 qısayol " +"düyməsi ilə açıla bilən pəncərə menyusundan sitifadə edin." + +#: useractions.cpp:240 +#, kde-format +msgid "&Move" +msgstr "&Köçürmək" + +#: useractions.cpp:245 +#, kde-format +msgid "&Resize" +msgstr "&Ölçüsünü dəyişmək" + +#: useractions.cpp:250 +#, kde-format +msgid "Keep &Above Others" +msgstr "Digərilərinin üzərində &tutmaq" + +#: useractions.cpp:256 +#, kde-format +msgid "Keep &Below Others" +msgstr "Arxa &planda tutmaq" + +#: useractions.cpp:262 +#, kde-format +msgid "&Fullscreen" +msgstr "&Tam ekran" + +#: useractions.cpp:268 +#, kde-format +msgid "&Shade" +msgstr "&Başlığa yığmaq" + +#: useractions.cpp:274 +#, kde-format +msgid "&No Border" +msgstr "Çərçivə&siz" + +#: useractions.cpp:282 +#, kde-format +msgid "Set Window Short&cut..." +msgstr "Pən&cərə Qısayolunu Qurmaq..." + +#: useractions.cpp:287 +#, kde-format +msgid "Configure Special &Window Settings..." +msgstr "Xüsusi &pəncərə ayarlarını tənzimləmək..." + +#: useractions.cpp:292 +#, kde-format +msgid "Configure S&pecial Application Settings..." +msgstr "Xüsusi &tətbiq ayarlarını tənzimləmək..." + +#: useractions.cpp:300 +#, kde-format +msgctxt "" +"Entry in context menu of window decoration to open the configuration module " +"of KWin" +msgid "Configure W&indow Manager..." +msgstr "Pəncərə Menecer&ini tənzimləmək..." + +#: useractions.cpp:328 +#, kde-format +msgid "Ma&ximize" +msgstr "&Genişləndirmək" + +#: useractions.cpp:334 +#, kde-format +msgid "Mi&nimize" +msgstr "&Yığmaq" + +#: useractions.cpp:340 +#, kde-format +msgid "&More Actions" +msgstr "&Daha çox fəaliyyətlər" + +#: useractions.cpp:343 +#, kde-format +msgid "&Close" +msgstr "&Bağlamaq" + +#: useractions.cpp:410 +#, kde-format +msgid "&Extensions" +msgstr "&Genişlənmələr" + +#: useractions.cpp:451 +#, kde-format +msgid "&Desktops" +msgstr "İş &Masaları" + +#: useractions.cpp:465 +#, kde-format +msgid "Move to &Desktop" +msgstr "İş Masasına &köşürmək" + +#: useractions.cpp:483 +#, kde-format +msgid "Move to &Screen" +msgstr "&Ekrana köçürmək" + +#: useractions.cpp:499 +#, kde-format +msgid "Show in &Activities" +msgstr "İş &Otaqlarında göstərmək" + +#: useractions.cpp:514 useractions.cpp:559 +#, kde-format +msgid "&All Desktops" +msgstr "Bütün &İş Masaları" + +#: useractions.cpp:542 useractions.cpp:595 +#, kde-format +msgctxt "Create a new desktop and move there the window" +msgid "&New Desktop" +msgstr "&Yeni İş Masası" + +#: useractions.cpp:618 +#, kde-format +msgctxt "" +"@item:inmenu List of all Screens to send a window to. First argument is a " +"number, second the output identifier. E.g. Screen 1 (HDMI1)" +msgid "Screen &%1 (%2)" +msgstr "&%1 Ekranı (%2)" + +#: useractions.cpp:641 +#, kde-format +msgid "&All Activities" +msgstr "&Bütün İş Otaqları" + +#: useractions.cpp:887 +#, kde-format +msgctxt "'%1' is a keyboard shortcut like 'ctrl+w'" +msgid "%1 is already in use" +msgstr "%1 artıq istifadə olunur" + +#: useractions.cpp:889 +#, kde-format +msgctxt "keyboard shortcut '%1' is used by action '%2' in application '%3'" +msgid "%1 is used by %2 in %3" +msgstr "%1 %3-də %2 tərəfindən istifadə olunur" + +#: useractions.cpp:1021 +#, kde-format +msgid "Activate Window (%1)" +msgstr "Aktiv pəncərə (%1)" + +#: useractions.cpp:1163 +#, kde-format +msgid "" +"The window manager is configured to consider the screen with the mouse on it " +"as active one.\n" +"Therefore it is not possible to switch to a screen explicitly." +msgstr "" +"Pəncərə Meneceri, siçanın üzərində olduğu ekranı, aktiv qəbul edəcək şəkildə " +"tənzimlənmişdir.\n" +"Bu səbəblə birmənalı olaraq ekrana keçid mümkün olmayacaq." + +#: virtualdesktops.cpp:698 virtualdesktops.cpp:767 +#, kde-format +msgid "Desktop %1" +msgstr "İş Masası %1" + +#: virtualdesktops.cpp:802 +#, kde-format +msgid "Switch to Next Desktop" +msgstr "Növbəti İŞ Masasına keçid" + +#: virtualdesktops.cpp:804 +#, kde-format +msgid "Switch to Previous Desktop" +msgstr "Əvvəlki İş Masasına keçid" + +#: virtualdesktops.cpp:806 +#, kde-format +msgid "Switch One Desktop to the Right" +msgstr "Bir İş Masası sağa keçmək" + +#: virtualdesktops.cpp:808 +#, kde-format +msgid "Switch One Desktop to the Left" +msgstr "Bir İş Masası sola keçmək" + +#: virtualdesktops.cpp:810 +#, kde-format +msgid "Switch One Desktop Up" +msgstr "Bir İş Masası yuxarı keçmək" + +#: virtualdesktops.cpp:812 +#, kde-format +msgid "Switch One Desktop Down" +msgstr "Bir İş Masası aşağı keçmək" + +#: virtualdesktops.cpp:825 +#, kde-format +msgid "Switch to Desktop %1" +msgstr "%1 İş Masasına keçid" + +#: virtualkeyboard.cpp:84 +#, kde-format +msgid "Virtual Keyboard" +msgstr "Virtual Klaviatura" + +#: virtualkeyboard.cpp:350 +#, kde-format +msgid "Virtual Keyboard: enabled" +msgstr "Virtual Klaviatura aktiv edilib" + +#: virtualkeyboard.cpp:353 +#, kde-format +msgid "Virtual Keyboard: disabled" +msgstr "Virtual Klaviatura söndürülüb" + +#: virtualkeyboard.cpp:355 +#, kde-format +msgid "Whether to show the virtual keyboard on demand." +msgstr "Virtual klaviaturanı tələbata görə göstərib, göstərməmək" + +#: workspace.cpp:1363 +#, kde-format +msgctxt "Introductory text shown in the support information." +msgid "" +"KWin Support Information:\n" +"The following information should be used when requesting support on e.g. " +"https://forum.kde.org.\n" +"It provides information about the currently running instance, which options " +"are used,\n" +"what OpenGL driver and which effects are running.\n" +"Please post the information provided underneath this introductory text to a " +"paste bin service\n" +"like https://paste.kde.org instead of pasting into support threads.\n" +msgstr "" +"KWin İnformasiya Dəstəyi:\n" +"Aşağıdakı məlumatlar, misal üçün, https://forum.kde.org saytına dəstək " +"sorğusu göndərilən zaman istifadə olunmalıdır.\n" +"Bu , hansı seçimlərin istifadə olunduğu, və hansı OpenGL sürücüsü və \n" +"effektlərin işə salındığı\n" +"cari işlək vəziyyət haqqında məlumatları təqdim edir.\n" +"Lütfən, bu giriş mətninin altındakı məlumatları dəstək mesajları yerinə\n" +"https://paste.kde.org saytına oxşar bir pastebin xidmətinə göndərin.\n" \ No newline at end of file diff --git a/po/az/kwin_clients.po b/po/az/kwin_clients.po new file mode 100644 index 0000000..9cf01d7 --- /dev/null +++ b/po/az/kwin_clients.po @@ -0,0 +1,127 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the kwin package. +# +# Xəyyam Qocayev , 2020. +msgid "" +msgstr "" +"Project-Id-Version: kwin\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-08-08 02:22+0200\n" +"PO-Revision-Date: 2020-06-03 12:35+0400\n" +"Last-Translator: Xəyyam Qocayev \n" +"Language-Team: Azerbaijani\n" +"Language: az\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Lokalize 20.04.1\n" + +#: aurorae/src/aurorae.cpp:683 +#, kde-format +msgctxt "@item:inlistbox Button size:" +msgid "Tiny" +msgstr "Kiçik" + +#: aurorae/src/aurorae.cpp:684 +#, kde-format +msgctxt "@item:inlistbox Button size:" +msgid "Normal" +msgstr "Normal" + +#: aurorae/src/aurorae.cpp:685 +#, kde-format +msgctxt "@item:inlistbox Button size:" +msgid "Large" +msgstr "Geniş" + +#: aurorae/src/aurorae.cpp:686 +#, kde-format +msgctxt "@item:inlistbox Button size:" +msgid "Very Large" +msgstr "Çox geniş" + +#: aurorae/src/aurorae.cpp:687 +#, kde-format +msgctxt "@item:inlistbox Button size:" +msgid "Huge" +msgstr "Nəhəng" + +#: aurorae/src/aurorae.cpp:688 +#, kde-format +msgctxt "@item:inlistbox Button size:" +msgid "Very Huge" +msgstr "Çox nəhəng" + +#: aurorae/src/aurorae.cpp:689 +#, kde-format +msgctxt "@item:inlistbox Button size:" +msgid "Oversized" +msgstr "Həddən artıq böyük" + +#: aurorae/src/aurorae.cpp:692 +#, kde-format +msgid "Button size:" +msgstr "Düymə ölçüsündə" + +#. i18n: ectx: property (windowTitle), widget (QWidget, PlastikConfigDialog) +#: aurorae/themes/plastik/package/contents/ui/config.ui:14 +#, kde-format +msgid "Config Dialog" +msgstr "Tənzimləmələr Dialoqu" + +#. i18n: ectx: property (title), widget (KButtonGroup, titleAlign) +#: aurorae/themes/plastik/package/contents/ui/config.ui:23 +#, kde-format +msgid "Title &Alignment" +msgstr "B&aşlıq düzləndirməsi" + +#. i18n: ectx: property (text), widget (QRadioButton, kcfg_titleAlignLeft) +#: aurorae/themes/plastik/package/contents/ui/config.ui:29 +#, kde-format +msgid "Left" +msgstr "Sol" + +#. i18n: ectx: property (text), widget (QRadioButton, kcfg_titleAlignCenter) +#: aurorae/themes/plastik/package/contents/ui/config.ui:36 +#, kde-format +msgid "Center" +msgstr "Mərkəz" + +#. i18n: ectx: property (text), widget (QRadioButton, kcfg_titleAlignRight) +#: aurorae/themes/plastik/package/contents/ui/config.ui:43 +#, kde-format +msgid "Right" +msgstr "Sağ" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_coloredBorder) +#: aurorae/themes/plastik/package/contents/ui/config.ui:53 +#, kde-format +msgid "" +"Check this option if the window border should be painted in the titlebar " +"color. Otherwise it will be painted in the background color." +msgstr "" +"Pəncərə kənarlarının başlıq çubuğu rəngində olması üçün bu seçimi edin. Əks " +"halda kənarlar üçün fon rəngiistifadə olunacaq." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_coloredBorder) +#: aurorae/themes/plastik/package/contents/ui/config.ui:56 +#, kde-format +msgid "Colored window border" +msgstr "Rənglənmiş pəncərə kənarları" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_animateButtons) +#: aurorae/themes/plastik/package/contents/ui/config.ui:66 +#, kde-format +msgid "" +"Check this option if you want the buttons to fade in when the mouse pointer " +"hovers over them and fade out again when it moves away." +msgstr "" +"Əgər kursoru üzərində hərəkət etdirdikdə düymələrin rənginin dəyişməsini " +"istəyirsinizsə bu seçimi edin." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_animateButtons) +#: aurorae/themes/plastik/package/contents/ui/config.ui:69 +#, kde-format +msgid "Animate buttons" +msgstr "Düymələrin animasiyası" \ No newline at end of file diff --git a/po/az/kwin_effects.po b/po/az/kwin_effects.po new file mode 100644 index 0000000..bbd7ada --- /dev/null +++ b/po/az/kwin_effects.po @@ -0,0 +1,2155 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the kwin package. +# +# Xəyyam Qocayev , 2020. +msgid "" +msgstr "" +"Project-Id-Version: kwin\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-10-23 08:49+0200\n" +"PO-Revision-Date: 2020-07-21 13:05+0400\n" +"Last-Translator: Xəyyam Qocayev \n" +"Language-Team: Azerbaijani \n" +"Language: az\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Lokalize 20.04.3\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Xəyyam Qocayev" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "xxmn77@gmail.com" + +#. i18n: ectx: property (text), widget (QLabel, labelConstantBlurDescription) +#: blur/blur_config.ui:17 +#, kde-format +msgid "Blur strength:" +msgstr "Yayğınlıq səviyyəsi:" + +#. i18n: ectx: property (text), widget (QLabel, labelConstantBlurLight) +#. i18n: ectx: property (text), widget (QLabel, labelConstantNoiseLight) +#: blur/blur_config.ui:42 blur/blur_config.ui:108 +#, kde-format +msgid "Light" +msgstr "Zəif" + +#. i18n: ectx: property (text), widget (QLabel, labelConstantBlurStrong) +#. i18n: ectx: property (text), widget (QLabel, labelConstantNoiseStrong) +#: blur/blur_config.ui:74 blur/blur_config.ui:137 +#, kde-format +msgid "Strong" +msgstr "Güclü" + +#. i18n: ectx: property (text), widget (QLabel, labelConstantNoiseDescription) +#: blur/blur_config.ui:83 +#, kde-format +msgid "Noise strength:" +msgstr "Küy səviyyəsi:" + +#: colorpicker/colorpicker.cpp:107 +#, kde-format +msgid "" +"Select a position for color picking with left click or enter.\n" +"Escape or right click to cancel." +msgstr "" +"Sol kliklə və ya Enter düyməsi ilə rəng seçimi yerini açın.\n" +"İmtina etmək üçün: Escape düyməsi və ya sol klik." + +#: coverswitch/coverswitch.cpp:945 flipswitch/flipswitch.cpp:925 +#, kde-format +msgctxt "Special entry in alt+tab list for minimizing all windows" +msgid "Show Desktop" +msgstr "İş Masasını Göstərmək" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DrawWindowCaptions) +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_WindowTitle) +#: coverswitch/coverswitch_config.ui:17 flipswitch/flipswitch_config.ui:191 +#: presentwindows/presentwindows_config.ui:406 +#, kde-format +msgid "Display window &titles" +msgstr "Pəncərə başlıqlarını gös&tərmək" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: coverswitch/coverswitch_config.ui:29 cube/cube_config.ui:344 +#, kde-format +msgid "Zoom" +msgstr "Miqyas" + +#. i18n: ectx: property (toolTip), widget (QSlider, kcfg_zPosition) +#: coverswitch/coverswitch_config.ui:39 +#, kde-format +msgid "Define how far away the windows should appear" +msgstr "Pəncərənin nə qədər uzaqda görünməsini təyin etmək" + +#. i18n: ectx: property (text), widget (QLabel, label_8) +#: coverswitch/coverswitch_config.ui:66 cube/cube_config.ui:350 +#, kde-format +msgid "Near" +msgstr "Yaxında" + +#. i18n: ectx: property (text), widget (QLabel, label_9) +#: coverswitch/coverswitch_config.ui:86 cube/cube_config.ui:357 +#, kde-format +msgid "Far" +msgstr "Uzaqda" + +#. i18n: ectx: attribute (title), widget (QWidget, tab) +#: coverswitch/coverswitch_config.ui:110 +#, kde-format +msgid "Animation" +msgstr "Animasiya" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AnimateSwitch) +#: coverswitch/coverswitch_config.ui:116 +#, kde-format +msgid "Animate switch" +msgstr "Animasiyalı dəyişim" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AnimateStart) +#: coverswitch/coverswitch_config.ui:123 +#, kde-format +msgid "Animation on tab box open" +msgstr "Pəncərə dəyişdiricinin animasiyalı açılması" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AnimateStop) +#: coverswitch/coverswitch_config.ui:130 +#, kde-format +msgid "Animation on tab box close" +msgstr "Pəncərə dəyişdiricinin animasiyalı bağlanması" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: coverswitch/coverswitch_config.ui:139 magiclamp/magiclamp_config.ui:17 +#, kde-format +msgid "Animation duration:" +msgstr "Animasiya müddəti:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_RotationDuration) +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_Duration) +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_AnimationDuration) +#: coverswitch/coverswitch_config.ui:158 cube/cube_config.ui:149 +#: cubeslide/cubeslide_config.ui:49 magiclamp/magiclamp_config.ui:36 +#, kde-format +msgctxt "Duration of rotation" +msgid "Default" +msgstr "Standart" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_Duration) +#: coverswitch/coverswitch_config.ui:161 glide/glide_config.ui:35 +#: scale/package/contents/ui/config.ui:33 slide/slide_config.ui:35 +#, kde-format +msgid " milliseconds" +msgstr " millisaniyə" + +#. i18n: ectx: attribute (title), widget (QWidget, tab_3) +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Reflection) +#: coverswitch/coverswitch_config.ui:177 coverswitch/coverswitch_config.ui:183 +#, kde-format +msgid "Reflections" +msgstr "Əks təsir" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: coverswitch/coverswitch_config.ui:195 +#, kde-format +msgid "Rear color" +msgstr "Arxadakı rəng" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: coverswitch/coverswitch_config.ui:205 +#, kde-format +msgid "Front color" +msgstr "Öndəki rəng" + +#: cube/cube.cpp:186 cube/cube_config.cpp:60 +#, kde-format +msgid "Desktop Cube" +msgstr "İş Masası kubu" + +#: cube/cube.cpp:194 cube/cube_config.cpp:65 +#, kde-format +msgid "Desktop Cylinder" +msgstr "İş Masası silindiri" + +#: cube/cube.cpp:200 cube/cube_config.cpp:69 +#, kde-format +msgid "Desktop Sphere" +msgstr "İş Masası kürəsi" + +#: cube/cube_config.cpp:49 +#, kde-format +msgctxt "@title:tab Basic Settings" +msgid "Basic" +msgstr "Əsas" + +#: cube/cube_config.cpp:50 +#, kde-format +msgctxt "@title:tab Advanced Settings" +msgid "Advanced" +msgstr "Əlavə" + +#: cube/cube_config.cpp:54 desktopgrid/desktopgrid_config.cpp:52 +#: flipswitch/flipswitch_config.cpp:56 invert/invert_config.cpp:38 +#: lookingglass/lookingglass_config.cpp:58 magnifier/magnifier_config.cpp:58 +#: mouseclick/mouseclick_config.cpp:50 mousemark/mousemark_config.cpp:56 +#: presentwindows/presentwindows_config.cpp:51 +#: showpaint/showpaint_config.cpp:36 +#: thumbnailaside/thumbnailaside_config.cpp:57 +#: trackmouse/trackmouse_config.cpp:54 +#: windowgeometry/windowgeometry_config.cpp:45 zoom/zoom_config.cpp:59 +#, kde-format +msgid "KWin" +msgstr "KWin" + +#. i18n: ectx: attribute (title), widget (QWidget, tab) +#: cube/cube_config.ui:21 +#, kde-format +msgid "Tab 1" +msgstr "Vərəq 1" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_5) +#: cube/cube_config.ui:27 +#, kde-format +msgid "Background" +msgstr "Arxa plan" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: cube/cube_config.ui:33 +#, kde-format +msgid "Background color:" +msgstr "Fon Rəngi:" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: cube/cube_config.ui:56 +#, kde-format +msgid "Wallpaper:" +msgstr "Divar Kağızı:" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_8) +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#: cube/cube_config.ui:82 desktopgrid/desktopgrid_config.ui:207 +#: flipswitch/flipswitch_config.ui:204 +#: presentwindows/presentwindows_config.ui:17 +#, kde-format +msgid "Activation" +msgstr "Aktivləşdirmək" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_7) +#. i18n: ectx: property (title), widget (QGroupBox, groupBox) +#: cube/cube_config.ui:104 desktopgrid/desktopgrid_config.ui:17 +#: flipswitch/flipswitch_config.ui:17 mousemark/mousemark_config.ui:17 +#: presentwindows/presentwindows_config.ui:387 +#: thumbnailaside/thumbnailaside_config.ui:17 +#, kde-format +msgid "Appearance" +msgstr "Xarici görünüş" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DisplayDesktopName) +#: cube/cube_config.ui:110 +#, kde-format +msgid "Display desktop name" +msgstr "İş Masasının adını göstərmək" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Reflection) +#: cube/cube_config.ui:117 +#, kde-format +msgid "Reflection" +msgstr "Əks olunma" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: cube/cube_config.ui:124 cubeslide/cubeslide_config.ui:72 +#, kde-format +msgid "Rotation duration:" +msgstr "Fırlanma müddəti:" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ZOrdering) +#: cube/cube_config.ui:175 +#, kde-format +msgid "Windows hover above cube" +msgstr "Pəncərəni kubun üzərinə qaldırmaq" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_4) +#: cube/cube_config.ui:185 +#, kde-format +msgid "Opacity" +msgstr "Qeyri-şəffaflıq" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_OpacitySpin) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_Opacity) +#: cube/cube_config.ui:225 thumbnailaside/thumbnailaside_config.ui:87 +#, no-c-format, kde-format +msgid " %" +msgstr " %" + +#. i18n: ectx: property (text), widget (QLabel, label_4) +#. i18n: ectx: property (text), widget (QLabel, label_2) +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: cube/cube_config.ui:238 translucency/package/contents/ui/config.ui:156 +#: translucency/package/contents/ui/config.ui:418 +#, kde-format +msgid "Transparent" +msgstr "Şəffaf" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#. i18n: ectx: property (text), widget (QLabel, label_3) +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: cube/cube_config.ui:245 translucency/package/contents/ui/config.ui:121 +#: translucency/package/contents/ui/config.ui:431 +#, kde-format +msgid "Opaque" +msgstr "Qeyri-şəffaf" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_OpacityDesktopOnly) +#: cube/cube_config.ui:255 +#, kde-format +msgid "Do not change opacity of windows" +msgstr "Pəncərələrin qeyri-şəffaflığını dəyişməmək" + +#. i18n: ectx: attribute (title), widget (QWidget, tab_2) +#: cube/cube_config.ui:279 +#, kde-format +msgid "Tab 2" +msgstr "Vərəq 2" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_3) +#: cube/cube_config.ui:285 +#, kde-format +msgid "Caps" +msgstr "Yuxarı və Aşağı sərhəd" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Caps) +#: cube/cube_config.ui:291 +#, kde-format +msgid "Show caps" +msgstr "Yuxarı və Aşağı sərhədləri göstərmək" + +#. i18n: ectx: property (text), widget (QLabel, capColorLabel) +#: cube/cube_config.ui:298 +#, kde-format +msgid "Cap color:" +msgstr "Sərhəddin rəngi:" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_TexturedCaps) +#: cube/cube_config.ui:321 +#, kde-format +msgid "Display image on caps" +msgstr "Sərhədlərdə şəkili göstərmək" + +#. i18n: ectx: property (toolTip), widget (QSlider, kcfg_ZPosition) +#: cube/cube_config.ui:367 +#, kde-format +msgid "Define how far away the object should appear" +msgstr "Elementin nə qədər uzaqda görünməsini təyin etmək" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_9) +#: cube/cube_config.ui:408 +#, kde-format +msgid "Additional Options" +msgstr "Əlavə Seçimlər" + +#. i18n: ectx: property (toolTip), widget (QCheckBox, kcfg_CloseOnMouseRelease) +#: cube/cube_config.ui:415 +#, kde-format +msgid "" +"If enabled the effect will be deactivated after rotating the cube with the " +"mouse,\n" +"otherwise it will remain active" +msgstr "Bu seçim aktiv olarsa,kubu siçanla fırlatdıqda effekt söndürüləcək." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_CloseOnMouseRelease) +#: cube/cube_config.ui:418 +#, kde-format +msgid "Close after mouse dragging" +msgstr "Siçanla fırlatdıqda bağlamaq" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_TabBox) +#: cube/cube_config.ui:425 +#, kde-format +msgid "Use this effect for walking through the desktops" +msgstr "İş Masalarına keçid zamanı bu effekti istifadə etmək" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_InvertKeys) +#: cube/cube_config.ui:432 +#, kde-format +msgid "Invert cursor keys" +msgstr "Siçan düymələrinin tərsi" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_InvertMouse) +#: cube/cube_config.ui:439 +#, kde-format +msgid "Invert mouse" +msgstr "Siçanın tərsi" + +#. i18n: ectx: property (title), widget (QGroupBox, capDeformationGroupBox) +#: cube/cube_config.ui:449 +#, kde-format +msgid "Sphere Cap Deformation" +msgstr "Kürənin deformasiyası" + +#. i18n: ectx: property (text), widget (QLabel, capDeformationSphereLabel) +#: cube/cube_config.ui:471 +#, kde-format +msgid "Sphere" +msgstr "Kürə" + +#. i18n: ectx: property (text), widget (QLabel, capDeformationPlaneLabel) +#: cube/cube_config.ui:478 +#, kde-format +msgid "Plane" +msgstr "Müstəvi" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DontSlideStickyWindows) +#: cubeslide/cubeslide_config.ui:17 +#, kde-format +msgid "Do not animate windows on all desktops" +msgstr "Pəncərə animasiyalarını bütün İş Masalarında söndürmək" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_RingLife) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_RotationDuration) +#: cubeslide/cubeslide_config.ui:52 mouseclick/mouseclick_config.ui:132 +#, kde-format +msgid " msec" +msgstr " msan" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DontSlidePanels) +#: cubeslide/cubeslide_config.ui:65 +#, kde-format +msgid "Do not animate panels" +msgstr "Panellərin animasiyalarını söndürmək" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_UsePagerLayout) +#: cubeslide/cubeslide_config.ui:85 +#, kde-format +msgid "Use pager layout for animation" +msgstr "Animasiya üçün səhifələyici sxemini istifadə etmək" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_UseWindowMoving) +#: cubeslide/cubeslide_config.ui:92 +#, kde-format +msgid "Start animation when moving windows towards screen edges" +msgstr "" +"Pəncərələri ekran kənarlarına doğru hərəkət etdirdikdə animasiyanı başlatmaq" + +#: desktopgrid/desktopgrid.cpp:65 desktopgrid/desktopgrid_config.cpp:57 +#, kde-format +msgid "Show Desktop Grid" +msgstr "İş Masalarını göstərmək" + +#: desktopgrid/desktopgrid_config.cpp:65 +#, kde-format +msgctxt "Desktop name alignment:" +msgid "Disabled" +msgstr "Söndürülüb" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_InRotationEdge) +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_OutRotationEdge) +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: desktopgrid/desktopgrid_config.cpp:66 flipswitch/flipswitch_config.ui:160 +#: glide/glide_config.ui:70 glide/glide_config.ui:168 +#, kde-format +msgid "Top" +msgstr "Yuxarıda" + +#: desktopgrid/desktopgrid_config.cpp:67 +#, kde-format +msgid "Top-Right" +msgstr "Yuxarı sağda" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_InRotationEdge) +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_OutRotationEdge) +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: desktopgrid/desktopgrid_config.cpp:68 flipswitch/flipswitch_config.ui:125 +#: glide/glide_config.ui:75 glide/glide_config.ui:173 +#, kde-format +msgid "Right" +msgstr "Sağda" + +#: desktopgrid/desktopgrid_config.cpp:69 +#, kde-format +msgid "Bottom-Right" +msgstr "Aşağı sağda" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_InRotationEdge) +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_OutRotationEdge) +#. i18n: ectx: property (text), widget (QLabel, label_8) +#: desktopgrid/desktopgrid_config.cpp:70 flipswitch/flipswitch_config.ui:180 +#: glide/glide_config.ui:80 glide/glide_config.ui:178 +#, kde-format +msgid "Bottom" +msgstr "Aşağıda" + +#: desktopgrid/desktopgrid_config.cpp:71 +#, kde-format +msgid "Bottom-Left" +msgstr "Aşağı solda" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_InRotationEdge) +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_OutRotationEdge) +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: desktopgrid/desktopgrid_config.cpp:72 flipswitch/flipswitch_config.ui:105 +#: glide/glide_config.ui:85 glide/glide_config.ui:183 +#, kde-format +msgid "Left" +msgstr "Solda" + +#: desktopgrid/desktopgrid_config.cpp:73 +#, kde-format +msgid "Top-Left" +msgstr "Yuxarı solda" + +#: desktopgrid/desktopgrid_config.cpp:74 +#, kde-format +msgid "Center" +msgstr "Mərkəzdə" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: desktopgrid/desktopgrid_config.ui:23 +#, kde-format +msgid "Zoom &duration:" +msgstr "Miqyaslama mü&ddəti:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_ZoomDuration) +#: desktopgrid/desktopgrid_config.ui:42 +#, kde-format +msgctxt "Duration of zoom" +msgid "Default" +msgstr "Standart" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: desktopgrid/desktopgrid_config.ui:55 +#, kde-format +msgid "Border wid&th:" +msgstr "Çərçivənin e&ni:" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: desktopgrid/desktopgrid_config.ui:84 +#, kde-format +msgid "Desktop &name alignment:" +msgstr "İş Masasının adı:" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: desktopgrid/desktopgrid_config.ui:107 +#, kde-format +msgid "&Layout mode:" +msgstr "Yer&ləşdirmə:" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LayoutMode) +#: desktopgrid/desktopgrid_config.ui:127 +#, kde-format +msgid "Pager" +msgstr "Səhifələyici" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LayoutMode) +#: desktopgrid/desktopgrid_config.ui:132 +#, kde-format +msgid "Automatic" +msgstr "Avtomatik" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LayoutMode) +#: desktopgrid/desktopgrid_config.ui:137 +#, kde-format +msgid "Custom" +msgstr "Fərdi" + +#. i18n: ectx: property (text), widget (QLabel, layoutRowsLabel) +#: desktopgrid/desktopgrid_config.ui:145 +#, kde-format +msgid "N&umber of rows:" +msgstr "Sətirlərin sa&yı:" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_PresentWindows) +#: desktopgrid/desktopgrid_config.ui:190 +#, kde-format +msgid "Use Present Windows effect to layout the windows" +msgstr "" +"Pəncərələri yerləşdirmək üçün mövcud pəncərələr effektini istifadə etmək" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ShowAddRemove) +#: desktopgrid/desktopgrid_config.ui:197 +#, kde-format +msgid "Show buttons to alter count of virtual desktops" +msgstr "Virtual İş Masalarının sayını gösərmək üçün düymələri göstərmək" + +#. i18n: ectx: property (text), widget (QLabel, label_Strength) +#: diminactive/diminactive_config.ui:17 +#, kde-format +msgid "Strength:" +msgstr "Qaralma səviyyəsi:" + +#. i18n: ectx: property (text), widget (QLabel, label_Dim) +#: diminactive/diminactive_config.ui:40 +#, kde-format +msgid "Dim:" +msgstr "Qaraltmaq:" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DimPanels) +#: diminactive/diminactive_config.ui:47 +#, kde-format +msgid "Docks and panels" +msgstr "Dok-lar və Panellər" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DimDesktop) +#: diminactive/diminactive_config.ui:54 +#, kde-format +msgid "Desktop" +msgstr "İş Masası" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DimKeepAbove) +#: diminactive/diminactive_config.ui:61 +#, kde-format +msgid "Keep above windows" +msgstr "Pəncərələrin üzərində tutmaq" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DimByGroup) +#: diminactive/diminactive_config.ui:68 +#, kde-format +msgid "By window group" +msgstr "Pəncərə qrupuna görə" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DimFullScreen) +#: diminactive/diminactive_config.ui:75 +#, kde-format +msgid "Fullscreen windows" +msgstr "Tam ekran açılan pəncərələr" + +#: effect_builtins.cpp:91 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Blur" +msgstr "Yayğınlıq" + +#: effect_builtins.cpp:92 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Blurs the background behind semi-transparent windows" +msgstr "Yarımşəffaf pəncərələrin altındakı fonu yayğınlaşdırmaq" + +#: effect_builtins.cpp:106 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Color Picker" +msgstr "Rəng seçimi" + +#: effect_builtins.cpp:107 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Supports picking a color" +msgstr "İstifadəçiyə ekranda rəngi seçməyə imkan verir" + +#: effect_builtins.cpp:121 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Background contrast" +msgstr "Fon kontrastı" + +#: effect_builtins.cpp:122 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Improve contrast and readability behind semi-transparent windows" +msgstr "Yarımşəffaf pəncərənin altındakı fonun kontrastını yaxşılaşdırır" + +#: effect_builtins.cpp:136 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Cover Switch" +msgstr "Karusel" + +#: effect_builtins.cpp:137 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Display a Cover Flow effect for the alt+tab window switcher" +msgstr "Alt+Tab ilə pəncərələrə keçidi karusel effekti ilə göstərir" + +#: effect_builtins.cpp:151 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Desktop Cube" +msgstr "İş Masası kubu" + +#: effect_builtins.cpp:152 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Display each virtual desktop on a side of a cube" +msgstr "Kubun hər kənarında bir İş Masasını göstərmək" + +#: effect_builtins.cpp:166 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Desktop Cube Animation" +msgstr "Animasiyalı İş Masası kubu" + +#: effect_builtins.cpp:167 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Animate desktop switching with a cube" +msgstr "İş Masalarına keçidi kub fırlanması ilə göstərmək" + +#: effect_builtins.cpp:181 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Desktop Grid" +msgstr "Bütün İş Masaları" + +#: effect_builtins.cpp:182 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Zoom out so all desktops are displayed side-by-side in a grid" +msgstr "Bütün İş Masaların bir ekranda yan yana göstərmək" + +#: effect_builtins.cpp:196 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Dim Inactive" +msgstr "Qeyri-aktiv pəncərələrin tutqunlaşması" + +#: effect_builtins.cpp:197 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Darken inactive windows" +msgstr "Qeyri-aktiv pəncərələri tutqunlaşdırır" + +#: effect_builtins.cpp:211 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Fall Apart" +msgstr "Dağılmaq" + +#: effect_builtins.cpp:212 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Closed windows fall into pieces" +msgstr "Pəncərəni hissələrə parçalayaraq bağlayır" + +#: effect_builtins.cpp:226 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Flip Switch" +msgstr "Səhifələmək" + +#: effect_builtins.cpp:227 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "" +"Flip through windows that are in a stack for the alt+tab window switcher" +msgstr "Alt+Tab ilə pəncərələrə keçidi kitab səhifələmək kimi göstərir" + +#: effect_builtins.cpp:241 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Glide" +msgstr "Süzülmə" + +#: effect_builtins.cpp:242 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Glide windows as they appear or disappear" +msgstr "Pəncərələrin süzülərək açılması və ya bağlanması" + +#: effect_builtins.cpp:256 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Highlight Window" +msgstr "Pəncərəni işıqlandırmaq" + +#: effect_builtins.cpp:257 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Highlight the appropriate window when hovering over taskbar entries" +msgstr "" +"Tapşıqıq paneli üzərində hərəkət edərkən müvadiq pəncərənin işıqlanması" + +#: effect_builtins.cpp:271 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Invert" +msgstr "Neqativ" + +#: effect_builtins.cpp:272 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Inverts the color of the desktop and windows" +msgstr "İş Masası və pəncərənin neqativi" + +#: effect_builtins.cpp:286 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Kscreen" +msgstr "Kscreen" + +#: effect_builtins.cpp:287 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Helper Effect for KScreen" +msgstr "KScreen üşün köməkçi effekti" + +#: effect_builtins.cpp:301 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Looking Glass" +msgstr "Linza" + +#: effect_builtins.cpp:302 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "A screen magnifier that looks like a fisheye lens" +msgstr "Ekrandakı sahəni linza effekti ilə göstərir" + +#: effect_builtins.cpp:316 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Magic Lamp" +msgstr "Sehirli Lampa" + +#: effect_builtins.cpp:317 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Simulate a magic lamp when minimizing windows" +msgstr "Pəncərələri sehirli lampa effekti ilə yığır" + +#: effect_builtins.cpp:331 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Magnifier" +msgstr "Lupa" + +#: effect_builtins.cpp:332 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Magnify the section of the screen that is near the mouse cursor" +msgstr "Kursorun altındakı ekran hissəsini böyüdücü şüşədə göstərir" + +#: effect_builtins.cpp:346 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Mouse Click Animation" +msgstr "Siçan klikinin animasiyası" + +#: effect_builtins.cpp:347 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "" +"Creates an animation whenever a mouse button is clicked. This is useful for " +"screenrecordings/presentations" +msgstr "" +"Hər siçan düyməsini vurduqda animaisya yaradır. Bu adətən ekrandan video " +"yazdıqda və ya təqdimat üçün istifadə olunur." + +#: effect_builtins.cpp:361 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Mouse Mark" +msgstr "Siçanla rəsm" + +#: effect_builtins.cpp:362 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Allows you to draw lines on the desktop" +msgstr "Siçan kursoru ilə ekranda şəkil çəkməyə imkan verir" + +#: effect_builtins.cpp:376 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Present Windows" +msgstr "Bütün Pəncərələr" + +#: effect_builtins.cpp:377 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Zoom out until all opened windows can be displayed side-by-side" +msgstr "Bütün açıq pəncərələrin miniatürünü yan-yana göstərir" + +#: effect_builtins.cpp:391 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Resize Window" +msgstr "Pəncərə ölçüsünün dəyişilməsi" + +#: effect_builtins.cpp:392 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Resizes windows with a fast texture scale instead of updating contents" +msgstr "" +"Pəncərənin ölçüsünü tərkiblərini yeniləmək əvəzinə teksturasını " +"miqyaslamaqla dəyişir" + +#: effect_builtins.cpp:406 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Screen Edge" +msgstr "Ekranın kənarları" + +#: effect_builtins.cpp:407 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Highlights a screen edge when approaching" +msgstr "Ekranın kənarlarına yaxınlaşdıqda onları vurğulayaraq seçir" + +#: effect_builtins.cpp:421 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Screenshot" +msgstr "Ekranın şəkli" + +#: effect_builtins.cpp:422 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Helper effect for screenshot tools" +msgstr "Ekranın şəklini çəkən vasitə üçün köməkçi effekt" + +#: effect_builtins.cpp:436 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Sheet" +msgstr "Vərəq" + +#: effect_builtins.cpp:437 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "" +"Make modal dialogs smoothly fly in and out when they are shown or hidden" +msgstr "Modal pəncərələr rəvan şəkildə uçuşla göstərilir və ya gizlədilir" + +#: effect_builtins.cpp:451 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Show FPS" +msgstr "Saniyədəki kadrlar" + +#: effect_builtins.cpp:452 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Display KWin's performance in the corner of the screen" +msgstr "" +"Ekranın küncündə qrafikanın effektlərin saniyədəki kadrlarının sayını " +"göstərir" + +#: effect_builtins.cpp:466 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Show Paint" +msgstr "Yenilənən sahə" + +#: effect_builtins.cpp:467 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Highlight areas of the desktop that have been recently updated" +msgstr "İş Masasının yenilənən sahəsini işıqlandırır" + +#: effect_builtins.cpp:481 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Slide" +msgstr "Sürüşdürmə" + +#: effect_builtins.cpp:482 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Slide desktops when switching virtual desktops" +msgstr "Virtual İş Masalarını dəyişərkən iş masalarını sürüşdürür" + +#: effect_builtins.cpp:496 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Slide Back" +msgstr "Arxasına sürüşdürmək" + +#: effect_builtins.cpp:497 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Slide back windows when another window is raised" +msgstr "Yeni pəncərə qaldırıldığında digər pəncərə arxaya sürüşür" + +#: effect_builtins.cpp:511 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Sliding popups" +msgstr "Sürüşən pəncə animasiyası" + +#: effect_builtins.cpp:512 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Sliding animation for Plasma popups" +msgstr "Sürüşən pəncərələrin animasiyalı peyda olması" + +#: effect_builtins.cpp:526 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Snap Helper" +msgstr "Yerləşdirmə köməkçisi" + +#: effect_builtins.cpp:527 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Help you locate the center of the screen when moving a window" +msgstr "" +"Pəncərələrin yeri dəyişdirilən zaman sizə ekranın mərkəzini təyin etməyə " +"imkan verir" + +#: effect_builtins.cpp:541 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Startup Feedback" +msgstr "İstifadəçi rəyinin başlanğıcı" + +#: effect_builtins.cpp:542 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Helper effect for startup feedback" +msgstr "İstifadəçi rəyinin başlanğıcı üçün yardımçı effekt" + +#: effect_builtins.cpp:556 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Thumbnail Aside" +msgstr "Yandakı miniatür pəncərəsi" + +#: effect_builtins.cpp:557 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Display window thumbnails on the edge of the screen" +msgstr "Ekranın kənarında miniatür pəncərəsini göstərir" + +#: effect_builtins.cpp:571 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Touch Points" +msgstr "Toxunma nöqtələri" + +#: effect_builtins.cpp:572 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Visualize touch points" +msgstr "Ekranda toxunan nöqtələri canlandırır" + +#: effect_builtins.cpp:586 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Track Mouse" +msgstr "Kursorun izi" + +#: effect_builtins.cpp:587 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Display a mouse cursor locating effect when activated" +msgstr "Ekranda kursorun yerini göstərir" + +#: effect_builtins.cpp:601 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Window Geometry" +msgstr "Pəncərənin həndəsi yeri" + +#: effect_builtins.cpp:602 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Display window geometries on move/resize" +msgstr "" +"Pəncərənin yeri və ya ölçüsü dəyişdirildiyində ekranda onun mövqeyinin və " +"ölçüsünün həndəsi göstəriciləri." + +#: effect_builtins.cpp:616 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Wobbly Windows" +msgstr "Titrək pəncərələr" + +#: effect_builtins.cpp:617 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Deform windows while they are moving" +msgstr "Hərəkət etdirildiyində pəncərələr dartılır/yığılır" + +#: effect_builtins.cpp:631 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Zoom" +msgstr "Miqyas" + +#: effect_builtins.cpp:632 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Magnify the entire desktop" +msgstr "İş Masasının miqyasının dəyişdirilməsi" + +#: flipswitch/flipswitch.cpp:48 flipswitch/flipswitch_config.cpp:50 +#, kde-format +msgid "Toggle Flip Switch (Current desktop)" +msgstr "Səhifələmə açarı (Cari İş Masası üç.)" + +#: flipswitch/flipswitch.cpp:55 flipswitch/flipswitch_config.cpp:53 +#, kde-format +msgid "Toggle Flip Switch (All desktops)" +msgstr "Səhifələmə açarı (Bütün İş Masaları üç.)" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: flipswitch/flipswitch_config.ui:23 +#, kde-format +msgid "Flip animation duration:" +msgstr "Səhifələmə animasiyasının müddəti:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_Duration) +#: flipswitch/flipswitch_config.ui:42 +#, kde-format +msgctxt "Duration of flip animation" +msgid "Default" +msgstr "Standart" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: flipswitch/flipswitch_config.ui:55 +#, kde-format +msgid "Angle:" +msgstr "Bucaq:" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_Angle) +#: flipswitch/flipswitch_config.ui:71 +#, kde-format +msgid " °" +msgstr " °" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: flipswitch/flipswitch_config.ui:81 +#, kde-format +msgid "Horizontal position of front:" +msgstr "Pəncərəni üfüqi movqeyi:" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: flipswitch/flipswitch_config.ui:136 +#, kde-format +msgid "Vertical position of front:" +msgstr "Pəncərəni şaquli movqeyi:" + +#. i18n: ectx: property (text), widget (QLabel, label_Duration) +#: glide/glide_config.ui:19 scale/package/contents/ui/config.ui:17 +#: slide/slide_config.ui:19 +#, kde-format +msgid "Duration:" +msgstr "Müddət:" + +#. i18n: Duration of the slide animation. +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_Duration) +#: glide/glide_config.ui:32 scale/package/contents/ui/config.ui:30 +#: slide/slide_config.ui:32 +#, kde-format +msgid "Default" +msgstr "Standart" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_InAnimation) +#: glide/glide_config.ui:50 +#, kde-format +msgid "Window Open Animation" +msgstr "Pəncərənin açılma animasiyası" + +#. i18n: ectx: property (text), widget (QLabel, label_InRotationEdge) +#. i18n: ectx: property (text), widget (QLabel, label_OutRotationEdge) +#: glide/glide_config.ui:56 glide/glide_config.ui:154 +#, kde-format +msgid "Rotation edge:" +msgstr "Dönmə kənarı:" + +#. i18n: ectx: property (text), widget (QLabel, label_InRotationAngle) +#. i18n: ectx: property (text), widget (QLabel, label_OutRotationAngle) +#: glide/glide_config.ui:93 glide/glide_config.ui:191 +#, kde-format +msgid "Rotation angle:" +msgstr "Dönmə bucağı:" + +#. i18n: ectx: property (text), widget (QLabel, label_InDistance) +#. i18n: ectx: property (text), widget (QLabel, label_OutDistance) +#: glide/glide_config.ui:119 glide/glide_config.ui:198 +#, kde-format +msgid "Distance:" +msgstr "Məsafə:" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_OutAnimation) +#: glide/glide_config.ui:148 +#, kde-format +msgid "Window Close Animation" +msgstr "Pəncərənin bağlanma animasiyası" + +#: invert/invert.cpp:34 invert/invert_config.cpp:41 +#, kde-format +msgid "Toggle Invert Effect" +msgstr "Naqativ effekt açarı" + +#: invert/invert.cpp:42 invert/invert_config.cpp:47 +#, kde-format +msgid "Toggle Invert Effect on Window" +msgstr "Pəncərədəki neqativ effekt açarı" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_FadeToBlack) +#: login/package/contents/ui/config.ui:17 +#, kde-format +msgid "Fade to black (fullscreen splash screens only)" +msgstr "qaralmaqla yox olma (yalnız tamekranlı salamlama pəncərəsi üçün)" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: lookingglass/lookingglass_config.ui:24 +#, kde-format +msgid "&Radius:" +msgstr "&Radius:" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_AnimationDuration) +#: magiclamp/magiclamp_config.ui:39 +#, kde-format +msgid "milliseconds" +msgstr "millisaniyə" + +#. i18n: ectx: property (title), widget (QGroupBox, groupSize) +#: magnifier/magnifier_config.ui:17 +#, kde-format +msgid "Size" +msgstr "Ölçü" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: magnifier/magnifier_config.ui:23 +#, kde-format +msgid "&Width:" +msgstr "&Eni:" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_Width) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_Height) +#: magnifier/magnifier_config.ui:42 magnifier/magnifier_config.ui:74 +#, kde-format +msgid " px" +msgstr " piksel" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: magnifier/magnifier_config.ui:55 +#, kde-format +msgid "&Height:" +msgstr "&Hündürlük:" + +#: mouseclick/mouseclick.cpp:40 mouseclick/mouseclick_config.cpp:53 +#, kde-format +msgid "Toggle Mouse Click Effect" +msgstr "Siçan kliki effektinin açarı" + +#: mouseclick/mouseclick.cpp:48 +#, kde-format +msgctxt "Left mouse button" +msgid "Left" +msgstr "Sol" + +#: mouseclick/mouseclick.cpp:49 +#, kde-format +msgctxt "Middle mouse button" +msgid "Middle" +msgstr "Orta" + +#: mouseclick/mouseclick.cpp:50 +#, kde-format +msgctxt "Right mouse button" +msgid "Right" +msgstr "Sağ" + +#: mouseclick/mouseclick.h:63 +#, kde-format +msgid "↓" +msgstr "↓" + +#: mouseclick/mouseclick.h:64 +#, kde-format +msgid "↑" +msgstr "↑" + +#. i18n: ectx: attribute (title), widget (QWidget, basic_tab) +#: mouseclick/mouseclick_config.ui:21 +#, kde-format +msgid "Basic Settings" +msgstr "Əsas Ayarlar" + +#. i18n: ectx: property (text), widget (QLabel, button1_label) +#: mouseclick/mouseclick_config.ui:37 +#, kde-format +msgid "Left Mouse Button Color:" +msgstr "Sol siçan düyməsinin rəngi:" + +#. i18n: ectx: property (text), widget (QLabel, button2_label) +#: mouseclick/mouseclick_config.ui:50 +#, kde-format +msgid "Middle Mouse Button Color:" +msgstr "Orta siçan düyməsinin rəngi:" + +#. i18n: ectx: property (text), widget (QLabel, button3_label) +#: mouseclick/mouseclick_config.ui:70 +#, kde-format +msgid "Right Mouse Button Color:" +msgstr "Sağ siçan düyməsinin rəngi:" + +#. i18n: ectx: attribute (title), widget (QWidget, advanced_tab) +#: mouseclick/mouseclick_config.ui:91 +#, kde-format +msgid "Advanced Settings" +msgstr "Əlavə Ayarlar" + +#. i18n: ectx: property (title), widget (QGroupBox, rings) +#: mouseclick/mouseclick_config.ui:97 +#, kde-format +msgid "Rings" +msgstr "Halqa" + +#. i18n: ectx: property (text), widget (QLabel, ring_line_width_label) +#: mouseclick/mouseclick_config.ui:103 +#, kde-format +msgid "Line Width:" +msgstr "Cizginin eni:" + +#. i18n: ectx: property (suffix), widget (QDoubleSpinBox, kcfg_LineWidth) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_RingSize) +#: mouseclick/mouseclick_config.ui:119 mouseclick/mouseclick_config.ui:171 +#: mousemark/mousemark_config.cpp:45 +#, kde-format +msgid " pixel" +msgid_plural " pixels" +msgstr[0] " piksel" +msgstr[1] " piksellər" + +#. i18n: ectx: property (text), widget (QLabel, ring_duration_label) +#: mouseclick/mouseclick_config.ui:145 +#, kde-format +msgid "Ring Duration:" +msgstr "Müddəti:" + +#. i18n: ectx: property (text), widget (QLabel, ring_radius_label) +#: mouseclick/mouseclick_config.ui:155 +#, kde-format +msgid "Ring Radius:" +msgstr "Radiusu:" + +#. i18n: ectx: property (text), widget (QLabel, ring_count_label) +#: mouseclick/mouseclick_config.ui:184 +#, kde-format +msgid "Ring Count:" +msgstr "Sayı:" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox) +#. i18n: ectx: property (title), widget (QGroupBox, font) +#: mouseclick/mouseclick_config.ui:210 showfps/showfps_config.ui:17 +#, kde-format +msgid "Text" +msgstr "Mətn" + +#. i18n: ectx: property (text), widget (QLabel, font_label) +#: mouseclick/mouseclick_config.ui:216 +#, kde-format +msgid "Font:" +msgstr "Şrift:" + +#. i18n: ectx: property (text), widget (QLabel, showtext_label) +#: mouseclick/mouseclick_config.ui:233 +#, kde-format +msgid "Show Text:" +msgstr "Mətni Göstər:" + +#: mousemark/mousemark.cpp:41 +#, kde-format +msgid "Clear All Mouse Marks" +msgstr "Siçanın bütün nişanlarını silmək" + +#: mousemark/mousemark.cpp:48 mousemark/mousemark_config.cpp:65 +#, kde-format +msgid "Clear Last Mouse Mark" +msgstr "Siçanın sonuncu nişanını silmək" + +#: mousemark/mousemark_config.cpp:59 +#, kde-format +msgid "Clear Mouse Marks" +msgstr "Siçanın nişanlarını silmək" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: mousemark/mousemark_config.ui:23 +#, kde-format +msgid "Wid&th:" +msgstr "&Eni:" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: mousemark/mousemark_config.ui:36 +#, kde-format +msgid "&Color:" +msgstr "&Rəng:" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: mousemark/mousemark_config.ui:78 +#, kde-format +msgid "Draw with the mouse by holding Shift+Meta keys and moving the mouse." +msgstr "Shift+Meta düymələrini sıxaraq siçanı hətəkət etdirməklə rəsm çəkmək." + +#: presentwindows/presentwindows.cpp:66 +#: presentwindows/presentwindows_config.cpp:62 +#, kde-format +msgid "Toggle Present Windows (Current desktop)" +msgstr "Cari İş Masasının bütün pəncərələrini göstərmək" + +#: presentwindows/presentwindows.cpp:75 +#: presentwindows/presentwindows_config.cpp:56 +#, kde-format +msgid "Toggle Present Windows (All desktops)" +msgstr "Bütün İş Masalarının bütün pəncərələrini göstərmək" + +#: presentwindows/presentwindows.cpp:85 +#: presentwindows/presentwindows_config.cpp:68 +#, kde-format +msgid "Toggle Present Windows (Window class)" +msgstr "Tək sinifə aid pəncərəni göstərmək" + +#: presentwindows/presentwindows.cpp:1666 +#, kde-format +msgid "" +"Filter:\n" +"%1" +msgstr "" +"Süzgəc:\n" +"%1" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_3) +#: presentwindows/presentwindows_config.ui:39 +#, kde-format +msgid "Natural Layout Settings" +msgstr "Təbii düzülüş ayarları" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_FillGaps) +#: presentwindows/presentwindows_config.ui:45 +#, kde-format +msgid "Fill &gaps" +msgstr "Boşluqları &doldurmaq" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: presentwindows/presentwindows_config.ui:65 +#, kde-format +msgid "Faster" +msgstr "Tez" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: presentwindows/presentwindows_config.ui:112 +#, kde-format +msgid "Nicer" +msgstr "Gözəl" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_4) +#: presentwindows/presentwindows_config.ui:122 +#, kde-format +msgid "Windows" +msgstr "Pəncərələr" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#. i18n: ectx: property (text), widget (QLabel, label_8) +#: presentwindows/presentwindows_config.ui:128 +#: presentwindows/presentwindows_config.ui:282 +#, kde-format +msgid "Left button:" +msgstr "Sol düymə:" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonDesktop) +#: presentwindows/presentwindows_config.ui:139 +#: presentwindows/presentwindows_config.ui:183 +#: presentwindows/presentwindows_config.ui:232 +#: presentwindows/presentwindows_config.ui:293 +#: presentwindows/presentwindows_config.ui:327 +#: presentwindows/presentwindows_config.ui:361 +#, kde-format +msgid "No action" +msgstr "Fəaliyyət yoxdur" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonDesktop) +#: presentwindows/presentwindows_config.ui:144 +#: presentwindows/presentwindows_config.ui:188 +#: presentwindows/presentwindows_config.ui:237 +#: presentwindows/presentwindows_config.ui:298 +#: presentwindows/presentwindows_config.ui:332 +#: presentwindows/presentwindows_config.ui:366 +#, kde-format +msgid "Activate window" +msgstr "Pəncərəni aktivləşdirmək" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonDesktop) +#: presentwindows/presentwindows_config.ui:149 +#: presentwindows/presentwindows_config.ui:193 +#: presentwindows/presentwindows_config.ui:242 +#: presentwindows/presentwindows_config.ui:303 +#: presentwindows/presentwindows_config.ui:337 +#: presentwindows/presentwindows_config.ui:371 +#, kde-format +msgid "End effect" +msgstr "Effektləri dayandırmaq" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonWindow) +#: presentwindows/presentwindows_config.ui:154 +#: presentwindows/presentwindows_config.ui:198 +#: presentwindows/presentwindows_config.ui:247 +#, kde-format +msgid "Bring window to current desktop" +msgstr "Pəncərəni cari İş Masasına köçürmək" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonWindow) +#: presentwindows/presentwindows_config.ui:159 +#: presentwindows/presentwindows_config.ui:203 +#: presentwindows/presentwindows_config.ui:252 +#, kde-format +msgid "Send window to all desktops" +msgstr "Pəncərəni bütün İş Masalarına göndərmək" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonWindow) +#: presentwindows/presentwindows_config.ui:164 +#: presentwindows/presentwindows_config.ui:208 +#: presentwindows/presentwindows_config.ui:257 +#, kde-format +msgid "(Un-)Minimize window" +msgstr "Pəncərəni yığmaq/açmaq" + +#. i18n: ectx: property (text), widget (QLabel, label_4) +#. i18n: ectx: property (text), widget (QLabel, label_9) +#: presentwindows/presentwindows_config.ui:172 +#: presentwindows/presentwindows_config.ui:316 +#, kde-format +msgid "Middle button:" +msgstr "Orta düymə:" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonWindow) +#: presentwindows/presentwindows_config.ui:213 +#: presentwindows/presentwindows_config.ui:262 +#, kde-format +msgid "Close window" +msgstr "Pəncərəni Bağlamaq" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#. i18n: ectx: property (text), widget (QLabel, label_10) +#: presentwindows/presentwindows_config.ui:221 +#: presentwindows/presentwindows_config.ui:350 +#, kde-format +msgid "Right button:" +msgstr "Sağ düymə:" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_5) +#: presentwindows/presentwindows_config.ui:273 +#, kde-format +msgctxt "@title:group actions when clicking on desktop" +msgid "Desktop" +msgstr "İş Masası" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonDesktop) +#: presentwindows/presentwindows_config.ui:308 +#: presentwindows/presentwindows_config.ui:342 +#: presentwindows/presentwindows_config.ui:376 +#, kde-format +msgid "Show desktop" +msgstr "İş Masasını Göstərmək" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: presentwindows/presentwindows_config.ui:393 +#, kde-format +msgid "Layout mode:" +msgstr "Yerləşdirmə rejimi:" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DrawWindowIcons) +#: presentwindows/presentwindows_config.ui:413 +#, kde-format +msgid "Display window &icons" +msgstr "Pəncərən&in ikonlarını göstərmək" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_IgnoreMinimized) +#: presentwindows/presentwindows_config.ui:420 +#, kde-format +msgid "Ignore &minimized windows" +msgstr "Yığılmış pəncərələri yox saymaq" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ShowPanel) +#: presentwindows/presentwindows_config.ui:427 +#, kde-format +msgid "Show &panels" +msgstr "&Panelləri göstərmək" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LayoutMode) +#: presentwindows/presentwindows_config.ui:441 +#, kde-format +msgid "Natural" +msgstr "Təbii" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LayoutMode) +#: presentwindows/presentwindows_config.ui:446 +#, kde-format +msgid "Regular Grid" +msgstr "Düzənli Şəbəkə" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LayoutMode) +#: presentwindows/presentwindows_config.ui:451 +#, kde-format +msgid "Flexible Grid" +msgstr "Düzənsiz Şəbəkə" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AllowClosingWindows) +#: presentwindows/presentwindows_config.ui:459 +#, kde-format +msgid "Provide buttons to close the windows" +msgstr "Pəncərənin \"bağlamaq\" düyməsini göstərmək" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_TextureScale) +#: resize/resize_config.ui:17 +#, kde-format +msgid "Scale window" +msgstr "Pəncərənin miqyası" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Outline) +#: resize/resize_config.ui:24 +#, kde-format +msgid "Show outline" +msgstr "Konturu göstərmək" + +#. i18n: ectx: property (text), widget (QLabel, label_InScale) +#: scale/package/contents/ui/config.ui:46 +#, kde-format +msgid "Window open scale:" +msgstr "Açılan pəncərənin ilkin miqyası:" + +#. i18n: ectx: property (text), widget (QLabel, label_OutScale) +#: scale/package/contents/ui/config.ui:53 +#, kde-format +msgid "Window close scale:" +msgstr "Bağlanan pəncərənin son miqyası:" + +#: screenshot/screenshot.cpp:440 +#, kde-format +msgctxt "Notification caption that a screenshot got saved to file" +msgid "Screenshot" +msgstr "Ekranın şəkli" + +#: screenshot/screenshot.cpp:441 +#, kde-format +msgctxt "Notification with path to screenshot file" +msgid "Screenshot saved to %1" +msgstr "Ekran şəkli %1-də saxlanıldı" + +#: screenshot/screenshot.cpp:578 +#, kde-format +msgid "" +"Select window to screen shot with left click or enter.\n" +"Escape or right click to cancel." +msgstr "" +"Sol klik və ya enter düyməsi ilə şəkili çəkiləcək pəncərəni seçmək.\n" +"Bundan imtina etmək üçün Escape və ya sağ klik." + +#: screenshot/screenshot.cpp:581 +#, kde-format +msgid "" +"Create screen shot with left click or enter.\n" +"Escape or right click to cancel." +msgstr "" +"Sol klik və ya enter düyməsi ilə ekranın şəklini çəkmək.\n" +"Bundan imtina etmək üçün Escape və ya sol klik." + +#: showfps/showfps.cpp:54 +#, kde-format +msgid "This effect is not a benchmark" +msgstr "bu effekt - sürət yoxlaması deyil" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: showfps/showfps_config.ui:23 +#, kde-format +msgid "Text position:" +msgstr "Mətn Yeri:" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TextPosition) +#: showfps/showfps_config.ui:43 +#, kde-format +msgid "Inside Graph" +msgstr "Qrafikanın tərkibində" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TextPosition) +#: showfps/showfps_config.ui:48 +#, kde-format +msgid "Nowhere" +msgstr "Heç bir yerdə" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TextPosition) +#: showfps/showfps_config.ui:53 +#, kde-format +msgid "Top Left" +msgstr "Yuxarı solda" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TextPosition) +#: showfps/showfps_config.ui:58 +#, kde-format +msgid "Top Right" +msgstr "Yuxarı sağda" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TextPosition) +#: showfps/showfps_config.ui:63 +#, kde-format +msgid "Bottom Left" +msgstr "Aşağı solda" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TextPosition) +#: showfps/showfps_config.ui:68 +#, kde-format +msgid "Bottom Right" +msgstr "Aşağı sağda" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: showfps/showfps_config.ui:76 +#, kde-format +msgid "Text font:" +msgstr "Mətnin şrifti:" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: showfps/showfps_config.ui:96 +#, kde-format +msgid "Text color:" +msgstr "Mətnin rəngi:" + +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: showfps/showfps_config.ui:119 +#, kde-format +msgid "Text alpha:" +msgstr "Mətnin yarımşəffaflığı:" + +#: showpaint/showpaint.cpp:42 showpaint/showpaint_config.cpp:41 +#, kde-format +msgid "Toggle Show Paint" +msgstr "Görüntülənənin işıqlanma açarı" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_Gaps) +#: slide/slide_config.ui:50 +#, kde-format +msgid "Gap between desktops" +msgstr "İş Masaları arasındakı boşluqlar" + +#. i18n: ectx: property (text), widget (QLabel, label_HorizontalGap) +#: slide/slide_config.ui:56 +#, kde-format +msgid "Horizontal:" +msgstr "Üfüqi:" + +#. i18n: ectx: property (text), widget (QLabel, label_VerticalGap) +#: slide/slide_config.ui:79 +#, kde-format +msgid "Vertical:" +msgstr "Şaquli:" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_SlideDocks) +#: slide/slide_config.ui:105 +#, kde-format +msgid "Slide docks" +msgstr "Panelləri sürüşdürmək" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_SlideBackground) +#: slide/slide_config.ui:112 +#, kde-format +msgid "Slide desktop background" +msgstr "İş Masaları fonlarını sürüşdürmək" + +#: thumbnailaside/thumbnailaside.cpp:29 +#: thumbnailaside/thumbnailaside_config.cpp:62 +#, kde-format +msgid "Toggle Thumbnail for Current Window" +msgstr "Cari İç Masasının miniatürlərini göstərmək/gizlətmək" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: thumbnailaside/thumbnailaside_config.ui:23 +#, kde-format +msgid "Maximum &width:" +msgstr "Maksimum &en:" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: thumbnailaside/thumbnailaside_config.ui:36 +#, kde-format +msgid "&Spacing:" +msgstr "Abza&s:" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_Spacing) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_MaxWidth) +#: thumbnailaside/thumbnailaside_config.ui:55 +#: thumbnailaside/thumbnailaside_config.ui:106 +#, kde-format +msgid " pixels" +msgstr " piksellər" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: thumbnailaside/thumbnailaside_config.ui:68 +#, kde-format +msgid "&Opacity:" +msgstr "&Qeyri-şəffaflıq:" + +#: trackmouse/trackmouse.cpp:50 trackmouse/trackmouse_config.cpp:59 +#, kde-format +msgid "Track mouse" +msgstr "Kursorun izi" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: trackmouse/trackmouse_config.ui:26 +#, kde-format +msgid "Trigger effect with:" +msgstr "Aktivləşmə:" + +#. i18n: ectx: property (text), widget (QLabel, label_KeyboardShortcut) +#: trackmouse/trackmouse_config.ui:33 +#, kde-format +msgid "Keyboard shortcut:" +msgstr "Klaviatura Qısayolları: " + +#. i18n: ectx: property (text), widget (QLabel, label_ModifierKeys) +#: trackmouse/trackmouse_config.ui:43 +#, kde-format +msgid "Modifier keys:" +msgstr "Dəyişdirici düymələr:" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Alt) +#: trackmouse/trackmouse_config.ui:65 +#, kde-format +msgid "Alt" +msgstr "Alt" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Control) +#: trackmouse/trackmouse_config.ui:72 +#, kde-format +msgid "Ctrl" +msgstr "Ctrl" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Shift) +#: trackmouse/trackmouse_config.ui:79 +#, kde-format +msgid "Shift" +msgstr "Shift" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Meta) +#: trackmouse/trackmouse_config.ui:86 +#, kde-format +msgid "Meta" +msgstr "Meta" + +#. i18n: ectx: property (windowTitle), widget (QWidget, KWin::TranslucencyEffectConfigForm) +#: translucency/package/contents/ui/config.ui:14 +#, kde-format +msgid "Translucency" +msgstr "Yarımşəffaflıq" + +#. i18n: ectx: property (title), widget (QGroupBox, m_opacityGroupBox) +#: translucency/package/contents/ui/config.ui:20 +#, kde-format +msgid "General Translucency Settings" +msgstr "Yarımşəffaflığın əsas ayarları" + +#. i18n: ectx: property (text), widget (QLabel, comboboxpopup_label) +#: translucency/package/contents/ui/config.ui:64 +#, kde-format +msgid "Combobox popups:" +msgstr "Sürüşərək açılan siyahılar:" + +#. i18n: ectx: property (text), widget (QLabel, dialogs_label) +#: translucency/package/contents/ui/config.ui:137 +#, kde-format +msgid "Dialogs:" +msgstr "Dialoqlar:" + +#. i18n: ectx: property (text), widget (QLabel, menus_label) +#: translucency/package/contents/ui/config.ui:188 +#, kde-format +msgid "Menus:" +msgstr "Menyular:" + +#. i18n: ectx: property (text), widget (QLabel, moveresize_label) +#: translucency/package/contents/ui/config.ui:207 +#, kde-format +msgid "Moving windows:" +msgstr "Yeri dəyişdirilən pəncərələr:" + +#. i18n: ectx: property (text), widget (QLabel, inactive_label) +#: translucency/package/contents/ui/config.ui:226 +#, kde-format +msgid "Inactive windows:" +msgstr "Qeyri-aktiv pəncərələr" + +#. i18n: ectx: property (title), widget (QGroupBox, kcfg_IndividualMenuConfig) +#: translucency/package/contents/ui/config.ui:267 +#, kde-format +msgid "Set menu translucency independently" +msgstr "Menyu üçün başqa yarımşəffaflıq təyin etmək" + +#. i18n: ectx: property (text), widget (QLabel, dropdownmenus_label) +#: translucency/package/contents/ui/config.ui:285 +#, kde-format +msgid "Dropdown menus:" +msgstr "Sürüşərək açılan menyular:" + +#. i18n: ectx: property (text), widget (QLabel, popupmenus_label) +#: translucency/package/contents/ui/config.ui:329 +#, kde-format +msgid "Popup menus:" +msgstr "Açılan menyular:" + +#. i18n: ectx: property (text), widget (QLabel, tornoffmenus_label) +#: translucency/package/contents/ui/config.ui:367 +#, kde-format +msgid "Torn-off menus:" +msgstr "Qopan menyular:" + +#: windowgeometry/windowgeometry.cpp:43 +#, kde-format +msgid "Toggle window geometry display (effect only)" +msgstr "Pəncərənin həndəsi gəstəricilərini göstərmək/gizlətmək (yalnız effekt)" + +#: windowgeometry/windowgeometry_config.cpp:47 +#, kde-format +msgid "Toggle KWin composited geometry display" +msgstr "Pəncərənin həndəsi gəstəricilərini göstərmək/gizlətmək" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Move) +#: windowgeometry/windowgeometry_config.ui:17 +#, kde-format +msgid "Display for moving windows" +msgstr "Pəncərələrin yerini dəyişərkən göstərmək" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Resize) +#: windowgeometry/windowgeometry_config.ui:24 +#, kde-format +msgid "Display for resizing windows" +msgstr "Pəncərənin ölçüsünü dəyişərkən göstərmək" + +#. i18n: ectx: property (title), widget (QGroupBox, advancedGroup) +#: wobblywindows/wobblywindows_config.ui:20 +#, kde-format +msgid "Advanced" +msgstr "Əlavə" + +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: wobblywindows/wobblywindows_config.ui:26 +#, kde-format +msgid "&Stiffness:" +msgstr "Ək&s təsir:" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: wobblywindows/wobblywindows_config.ui:68 +#, kde-format +msgid "Dra&g:" +msgstr "Daşımaq:" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: wobblywindows/wobblywindows_config.ui:81 +#, kde-format +msgid "&Move factor:" +msgstr "Yerdəyiş&mə:" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_MoveWobble) +#: wobblywindows/wobblywindows_config.ui:155 +#, kde-format +msgid "Wo&bble when moving" +msgstr "Hərəkət etdirdikdə titrəmə" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ResizeWobble) +#: wobblywindows/wobblywindows_config.ui:162 +#, kde-format +msgid "Wobble when &resizing" +msgstr "Ölçüsünü dəyişdirdikdə titrəmə" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AdvancedMode) +#: wobblywindows/wobblywindows_config.ui:182 +#, kde-format +msgid "Enable &advanced mode" +msgstr "Əl&avə rejimləri aktiv etmək" + +#. i18n: ectx: property (title), widget (QGroupBox, basicGroup) +#: wobblywindows/wobblywindows_config.ui:192 +#, kde-format +msgid "&Wobbliness" +msgstr "Titrəmə &genişliyi" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: wobblywindows/wobblywindows_config.ui:201 +#, kde-format +msgid "Less" +msgstr "Zəif" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: wobblywindows/wobblywindows_config.ui:224 +#, kde-format +msgid "More" +msgstr "Çox" + +#: zoom/zoom.cpp:74 +#, kde-format +msgid "Move Zoomed Area to Left" +msgstr "Böyümüş sahəni sola hərəkət etdirmək" + +#: zoom/zoom.cpp:82 +#, kde-format +msgid "Move Zoomed Area to Right" +msgstr "Böyümüş sahəni sağa hərəkət etdirmək" + +#: zoom/zoom.cpp:90 +#, kde-format +msgid "Move Zoomed Area Upwards" +msgstr "Böyümüş sahəni yuxarı hərəkət etdirmək" + +#: zoom/zoom.cpp:98 +#, kde-format +msgid "Move Zoomed Area Downwards" +msgstr "Böyümüş sahəni aşağı hərəkət etdirmək" + +#: zoom/zoom.cpp:107 zoom/zoom_config.cpp:109 +#, kde-format +msgid "Move Mouse to Focus" +msgstr "Kursoru daxiletmə fokusuna köçürmək" + +#: zoom/zoom.cpp:115 zoom/zoom_config.cpp:116 +#, kde-format +msgid "Move Mouse to Center" +msgstr "Kursoru mərkəzə köçürmək" + +#: zoom/zoom_config.cpp:81 +#, kde-format +msgid "Move Left" +msgstr "Sola keçirmək" + +#: zoom/zoom_config.cpp:88 +#, kde-format +msgid "Move Right" +msgstr "Sağa keçirmək" + +#: zoom/zoom_config.cpp:95 +#, kde-format +msgid "Move Up" +msgstr "Yuxarı keçirmək" + +#: zoom/zoom_config.cpp:102 +#, kde-format +msgid "Move Down" +msgstr "Aşağı keçirmək" + +#. i18n: ectx: property (whatsThis), widget (QLabel, label) +#. i18n: ectx: property (whatsThis), widget (QDoubleSpinBox, kcfg_ZoomFactor) +#: zoom/zoom_config.ui:25 zoom/zoom_config.ui:41 +#, kde-format +msgid "On zoom-in and zoom-out change the zoom by the defined zoom-factor." +msgstr "Miqyaz böyütmə və kiçiltməni bilinən miqyas əmsalına görə dəyişmək" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: zoom/zoom_config.ui:28 +#, kde-format +msgid "Zoom Factor:" +msgstr "Miqyaz Əmsalı:" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_EnableFocusTracking) +#: zoom/zoom_config.ui:66 +#, kde-format +msgid "" +"Enable tracking of the focused location. This needs QAccessible to be " +"enabled per application (\"export QT_LINUX_ACCESSIBILITY_ALWAYS_ON=1\")." +msgstr "" +"Fokusun yerini izləməyi aktiv etmək. Hər tətbiqdə QAccessible aktiv " +"olmalıdır (\"export QT_LINUX_ACCESSIBILITY_ALWAYS_ON=1\")." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_EnableFocusTracking) +#: zoom/zoom_config.ui:69 +#, kde-format +msgid "Enable Focus Tracking" +msgstr "Fokus izləməni aktiv etmək" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_EnableTextCaretTracking) +#: zoom/zoom_config.ui:76 +#, kde-format +msgid "" +"Enable tracking of the text cursor. This needs QAccessible to be enabled per " +"application (\"export QT_LINUX_ACCESSIBILITY_ALWAYS_ON=1\")." +msgstr "" +"Mətn kursorunun izlənməsini aktiv etmək. Hər tətbiqdə QAccessible aktiv " +"olmalıdır (\"export QT_LINUX_ACCESSIBILITY_ALWAYS_ON=1\")." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_EnableTextCaretTracking) +#: zoom/zoom_config.ui:79 +#, kde-format +msgid "Enable Text Cursor Tracking" +msgstr "Mətn kursorunun izlənməsini aktiv etmək" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: zoom/zoom_config.ui:86 +#, kde-format +msgid "Mouse Pointer:" +msgstr "Siçanın kursoru:" + +#. i18n: ectx: property (whatsThis), widget (QComboBox, kcfg_MousePointer) +#: zoom/zoom_config.ui:99 +#, kde-format +msgid "Visibility of the mouse-pointer." +msgstr "Siçanın kursorunun göstərilməsi." + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_MousePointer) +#: zoom/zoom_config.ui:103 +#, kde-format +msgid "Scale" +msgstr "Miqyas" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_MousePointer) +#: zoom/zoom_config.ui:108 +#, kde-format +msgid "Keep" +msgstr "Saxlamaq" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_MousePointer) +#: zoom/zoom_config.ui:113 +#, kde-format +msgid "Hide" +msgstr "Gizlətmək" + +#. i18n: ectx: property (whatsThis), widget (QComboBox, kcfg_MouseTracking) +#: zoom/zoom_config.ui:121 +#, kde-format +msgid "Track moving of the mouse." +msgstr "Kursorun yerdəyişməsini izləmək." + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_MouseTracking) +#: zoom/zoom_config.ui:125 +#, kde-format +msgid "Proportional" +msgstr "Tənasüblü" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_MouseTracking) +#: zoom/zoom_config.ui:130 +#, kde-format +msgid "Centered" +msgstr "Mərkəzləşmiş" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_MouseTracking) +#: zoom/zoom_config.ui:135 +#, kde-format +msgid "Push" +msgstr "İrəliləyən" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_MouseTracking) +#: zoom/zoom_config.ui:140 +#, kde-format +msgid "Disabled" +msgstr "Söndürülüb" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: zoom/zoom_config.ui:148 +#, kde-format +msgid "Mouse Tracking:" +msgstr "Siçanın izlənməsi:" \ No newline at end of file diff --git a/po/az/kwin_scripting.po b/po/az/kwin_scripting.po new file mode 100644 index 0000000..bd61bc2 --- /dev/null +++ b/po/az/kwin_scripting.po @@ -0,0 +1,117 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the kwin package. +# +# Xəyyam Qocayev , 2020. +msgid "" +msgstr "" +"Project-Id-Version: kwin\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-08-08 02:22+0200\n" +"PO-Revision-Date: 2020-07-22 16:39+0400\n" +"Last-Translator: Xəyyam Qocayev \n" +"Language-Team: Azerbaijani \n" +"Language: az\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Lokalize 20.04.3\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Xəyyam Qocayev" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "xxmn77@gmail.com" + +#: genericscriptedconfig.cpp:70 +#, kde-format +msgctxt "Error message" +msgid "Plugin does not provide configuration file in expected location" +msgstr "Plasma əlavəsi, gözlənilən yerdə tənzimləmə faylını təqdim etmir" + +#: scripting.cpp:117 +#, kde-format +msgctxt "Assertion failed in KWin script with given value" +msgid "Assertion failed: %1 is not null" +msgstr "Təsdiq edilmədi: %1, null ilə bərabər deyil" + +#: scripting.cpp:135 +#, kde-format +msgctxt "Assertion failed in KWin script" +msgid "Assertion failed: argument is null" +msgstr "Təsdiq edilmədi: arqument null ilə bərabərdir" + +#: scripting.cpp:177 +#, kde-format +msgctxt "Error in KWin Script" +msgid "" +"Invalid number of arguments. At least service, path, interface and method " +"need to be provided" +msgstr "" +"Yalnış sayda arqument. Ən azı, xidmət (service), yol (path), interfeys " +"(interface) və metod(method) təmin edilməlidir" + +#: scripting.cpp:183 +#, kde-format +msgctxt "Error in KWin Script" +msgid "" +"Invalid type. Service, path, interface and method need to be string values" +msgstr "" +"Səhv növ. Xidmət (service), yol (path), interfeys (interface) və " +"metod(method) arqumentləri sətir dəyərində olmalıdır" + +#: scriptingutils.cpp:17 +#, kde-format +msgctxt "syntax error in KWin script" +msgid "Invalid number of arguments" +msgstr "Yalnış sayda arqumentər" + +#: scriptingutils.cpp:30 +#, kde-format +msgctxt "KWin Scripting function received incorrect value for an expected type" +msgid "%1 is not a variant type" +msgstr "%1 variant növü deyil" + +#: scriptingutils.h:40 +#, kde-format +msgctxt "KWin Scripting function received incorrect value for an expected type" +msgid "%1 is not of required type" +msgstr "%1 tələb olunan növ deyil" + +#: scriptingutils.h:147 +#, kde-format +msgctxt "KWin Scripting error thrown due to incorrect argument" +msgid "Second argument to registerScreenEdge needs to be a callback" +msgstr "" +"registerScreenEdge üçün ikinci arqumenti əks çağırış funksiyası olmalıdır" + +#: scriptingutils.h:202 +#, kde-format +msgctxt "KWin Scripting error thrown due to incorrect argument" +msgid "Second argument to registerTouchScreenEdge needs to be a callback" +msgstr "" +"registerTouchScreenEdge üçün ikinci arqumenti əks çağırış funksiyası " +"olmalıdır" + +#: scriptingutils.h:239 +#, kde-format +msgctxt "KWin Scripting error thrown due to incorrect argument" +msgid "Argument for registerUserActionsMenu needs to be a callback" +msgstr "" +"registerUserActionsMenu üçün arqument geri çağırış funksiyası olmalıdır" + +#: scriptingutils.h:294 +#, kde-format +msgctxt "Assertion failed in KWin script with given value" +msgid "Assertion failed: %1" +msgstr "Uğursuz təsdiqlənmə: %1" + +#: scriptingutils.h:305 +#, kde-format +msgctxt "Assertion failed in KWin script with expected value and actual value" +msgid "Assertion failed: Expected %1, got %2" +msgstr "Uğursuz təsdiqlənmə: %1 gözlənildiyi halda %2 alındı" \ No newline at end of file diff --git a/po/az/kwin_scripts.po b/po/az/kwin_scripts.po new file mode 100644 index 0000000..bb8e9ec --- /dev/null +++ b/po/az/kwin_scripts.po @@ -0,0 +1,61 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the kwin package. +# +# Xəyyam Qocayev , 2020. +msgid "" +msgstr "" +"Project-Id-Version: kwin\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2019-05-20 03:25+0200\n" +"PO-Revision-Date: 2020-06-03 12:38+0400\n" +"Last-Translator: Xəyyam Qocayev \n" +"Language-Team: Azerbaijani\n" +"Language: az\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Lokalize 20.04.1\n" + +#. i18n: ectx: property (windowTitle), widget (QWidget, KWin::VideoWallConfigForm) +#: videowall/contents/ui/config.ui:14 +#, kde-format +msgid "Video Wall" +msgstr "Video Divar Kağızı" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ApplyTo) +#: videowall/contents/ui/config.ui:20 +#, kde-format +msgid "Apply to" +msgstr "Buna tıtbiq etmək" + +#. i18n: ectx: property (text), widget (QLineEdit, kcfg_Whitelist) +#: videowall/contents/ui/config.ui:32 +#, kde-format +msgid "vlc, xv, vdpau, smplayer, dragon, xine, ffplay" +msgstr "vlc, xv, vdpau, smplayer, dragon, xine, ffplay" + +#. i18n: ectx: property (placeholderText), widget (QLineEdit, kcfg_Whitelist) +#. i18n: ectx: property (placeholderText), widget (QLineEdit, kcfg_Blacklist) +#: videowall/contents/ui/config.ui:35 videowall/contents/ui/config.ui:66 +#, kde-format +msgid "Comma separated list of window classes" +msgstr "Pəncərə sinifləri siyahısını vergüllə ayırmaq" + +#. i18n: ectx: property (text), widget (QLabel, applyLabel) +#: videowall/contents/ui/config.ui:45 +#, kde-format +msgid "All" +msgstr "Bütün" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Ignore) +#: videowall/contents/ui/config.ui:54 +#, kde-format +msgid "Ignore" +msgstr "İnkar etmək" + +#. i18n: ectx: property (text), widget (QLabel, ignoreLabel) +#: videowall/contents/ui/config.ui:73 +#, kde-format +msgid "None" +msgstr "Heç biri" \ No newline at end of file diff --git a/po/be/kcm_kwin_virtualdesktops.po b/po/be/kcm_kwin_virtualdesktops.po new file mode 100644 index 0000000..65bfe1f --- /dev/null +++ b/po/be/kcm_kwin_virtualdesktops.po @@ -0,0 +1,133 @@ +# translation of kcm_kwindesktop.po to Belarusian +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Darafei Praliaskouski , 2007. +# Darafei Praliaskouski , 2008. +msgid "" +msgstr "" +"Project-Id-Version: kcm_kwindesktop\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-08-08 02:22+0200\n" +"PO-Revision-Date: 2008-02-29 12:53+0200\n" +"Last-Translator: Darafei Praliaskouski \n" +"Language-Team: Belarusian \n" +"Language: be\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: KAider 0.1\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "" + +#: desktopsmodel.cpp:455 +#, kde-format +msgid "There was an error connecting to the compositor." +msgstr "" + +#: desktopsmodel.cpp:657 +#, kde-format +msgid "There was an error saving the settings to the compositor." +msgstr "" + +#: desktopsmodel.cpp:660 +#, kde-format +msgid "There was an error requesting information from the compositor." +msgstr "" + +#: package/contents/ui/main.qml:18 +#, kde-format +msgid "" +"This module lets you configure the navigation, number and layout of virtual " +"desktops." +msgstr "" + +#: package/contents/ui/main.qml:68 +#, kde-format +msgctxt "@info:tooltip" +msgid "Rename" +msgstr "" + +#: package/contents/ui/main.qml:78 +#, kde-format +msgctxt "@info:tooltip" +msgid "Remove" +msgstr "" + +#: package/contents/ui/main.qml:104 +#, kde-format +msgid "" +"Virtual desktops have been changed outside this settings application. Saving " +"now will overwrite the changes." +msgstr "" + +#: package/contents/ui/main.qml:118 +#, kde-format +msgid "Row %1" +msgstr "" + +#: package/contents/ui/main.qml:131 +#, kde-format +msgctxt "@action:button" +msgid "Add" +msgstr "" + +#: package/contents/ui/main.qml:134 +#, fuzzy, kde-format +#| msgid "Desktop %1" +msgid "New Desktop" +msgstr "Працоўны стол %1" + +#: package/contents/ui/main.qml:148 +#, kde-format +msgid "1 Row" +msgid_plural "%1 Rows" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +#: package/contents/ui/main.qml:160 +#, kde-format +msgid "Options:" +msgstr "" + +#: package/contents/ui/main.qml:162 +#, kde-format +msgid "Navigation wraps around" +msgstr "" + +#: package/contents/ui/main.qml:177 +#, kde-format +msgid "Show animation when switching:" +msgstr "" + +#: package/contents/ui/main.qml:220 +#, kde-format +msgid "Show on-screen display when switching:" +msgstr "" + +#: package/contents/ui/main.qml:238 +#, kde-format +msgid "%1 ms" +msgstr "" + +#: package/contents/ui/main.qml:256 +#, kde-format +msgid "Show desktop layout indicators" +msgstr "" + +#: virtualdesktops.cpp:30 +#, fuzzy, kde-format +#| msgid "Desktop %1" +msgid "Virtual Desktops" +msgstr "Працоўны стол %1" \ No newline at end of file diff --git a/po/be/kcm_kwindecoration.po b/po/be/kcm_kwindecoration.po new file mode 100644 index 0000000..8c3d79b --- /dev/null +++ b/po/be/kcm_kwindecoration.po @@ -0,0 +1,231 @@ +# translation of kcmkwindecoration.po to Belarusian +# +# Darafei Praliaskouski , 2006. +# Darafei Praliaskouski , 2007. +# Darafei Praliaskouski , 2008. +msgid "" +msgstr "" +"Project-Id-Version: kcmkwindecoration\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-11-17 08:20+0100\n" +"PO-Revision-Date: 2008-02-29 13:05+0200\n" +"Last-Translator: Darafei Praliaskouski \n" +"Language-Team: Belarusian \n" +"Language: be\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: KAider 0.1\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Дарафей Праляскоўскі" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "symbol@akeeri.tk" + +#: declarative-plugin/buttonsmodel.cpp:54 +#, kde-format +msgid "Menu" +msgstr "Меню" + +#: declarative-plugin/buttonsmodel.cpp:56 +#, kde-format +msgid "Application menu" +msgstr "" + +#: declarative-plugin/buttonsmodel.cpp:58 +#, fuzzy, kde-format +#| msgid "On All Desktops" +msgid "On all desktops" +msgstr "На ўсіх працоўных сталах" + +#: declarative-plugin/buttonsmodel.cpp:60 +#, kde-format +msgid "Minimize" +msgstr "Згарнуць" + +#: declarative-plugin/buttonsmodel.cpp:62 +#, kde-format +msgid "Maximize" +msgstr "Найбольшыць" + +#: declarative-plugin/buttonsmodel.cpp:64 +#, kde-format +msgid "Close" +msgstr "Закрыць" + +#: declarative-plugin/buttonsmodel.cpp:66 +#, kde-format +msgid "Context help" +msgstr "" + +#: declarative-plugin/buttonsmodel.cpp:68 +#, kde-format +msgid "Shade" +msgstr "Зацяніць" + +#: declarative-plugin/buttonsmodel.cpp:70 +#, fuzzy, kde-format +#| msgid "Keep Below Others" +msgid "Keep below" +msgstr "Трымаць ніжэй астатніх" + +#: declarative-plugin/buttonsmodel.cpp:72 +#, fuzzy, kde-format +#| msgid "Keep Above Others" +msgid "Keep above" +msgstr "Трымаць вышэй астатніх" + +#: kcm.cpp:50 +#, fuzzy, kde-format +#| msgid "&Window Decoration" +msgid "Window Decorations" +msgstr "Дэкарацыі вокнаў" + +#: kcm.cpp:54 +#, kde-format +msgid "Valerio Pilo" +msgstr "" + +#: kcm.cpp:55 +#, kde-format +msgid "Author" +msgstr "" + +#: kcm.cpp:104 +#, fuzzy, kde-format +#| msgid "&Window Decoration" +msgid "Download New Window Decorations" +msgstr "Дэкарацыі вокнаў" + +#: package/contents/ui/Buttons.qml:73 +#, kde-format +msgid "Titlebar" +msgstr "" + +#: package/contents/ui/Buttons.qml:214 +#, kde-format +msgid "Drop button here to remove it" +msgstr "" + +#: package/contents/ui/Buttons.qml:232 +#, kde-format +msgid "Drag buttons between here and the titlebar" +msgstr "" + +#: package/contents/ui/main.qml:15 +#, kde-format +msgid "This module lets you configure the window decorations." +msgstr "" + +#: package/contents/ui/main.qml:49 +#, kde-format +msgctxt "tab label" +msgid "Theme" +msgstr "" + +#: package/contents/ui/main.qml:53 +#, fuzzy, kde-format +#| msgid "Buttons" +msgctxt "tab label" +msgid "Titlebar Buttons" +msgstr "Кнопкі" + +#: package/contents/ui/main.qml:78 +#, kde-format +msgctxt "checkbox label" +msgid "Use theme's default window border size" +msgstr "" + +#: package/contents/ui/main.qml:109 +#, fuzzy, kde-format +#| msgid "&Window Decoration" +msgctxt "button text" +msgid "Get New Window Decorations..." +msgstr "Дэкарацыі вокнаў" + +#: package/contents/ui/main.qml:126 +#, kde-format +msgctxt "checkbox label" +msgid "Close windows by double clicking the menu button" +msgstr "" + +#: package/contents/ui/main.qml:139 +#, kde-format +msgctxt "popup tip" +msgid "" +"Close by double clicking: Keep the window's Menu button pressed until it " +"appears." +msgstr "" + +#: package/contents/ui/main.qml:146 +#, fuzzy, kde-format +#| msgid "&Show window button tooltips" +msgctxt "checkbox label" +msgid "Show titlebar button tooltips" +msgstr "Паказваць падказкі кнопак акна" + +#: package/contents/ui/Themes.qml:89 +#, kde-format +msgid "Edit %1 Theme" +msgstr "" + +#: utils.cpp:26 +#, fuzzy, kde-format +#| msgid "B&order size:" +msgid "No Borders" +msgstr "Памер рамкі:" + +#: utils.cpp:27 +#, fuzzy, kde-format +#| msgid "B&order size:" +msgid "No Side Borders" +msgstr "Памер рамкі:" + +#: utils.cpp:28 +#, fuzzy, kde-format +#| msgid "Tiny" +msgid "Tiny" +msgstr "Малюсенькі" + +#: utils.cpp:29 +#, fuzzy, kde-format +#| msgid "Normal" +msgid "Normal" +msgstr "Звычайны" + +#: utils.cpp:30 +#, fuzzy, kde-format +#| msgid "Large" +msgid "Large" +msgstr "Вялікі" + +#: utils.cpp:31 +#, fuzzy, kde-format +#| msgid "Very Large" +msgid "Very Large" +msgstr "Вельмі вялікі" + +#: utils.cpp:32 +#, fuzzy, kde-format +#| msgid "Huge" +msgid "Huge" +msgstr "Звышвялікі" + +#: utils.cpp:33 +#, fuzzy, kde-format +#| msgid "Very Huge" +msgid "Very Huge" +msgstr "Зусім-зусім вялікі" + +#: utils.cpp:34 +#, fuzzy, kde-format +#| msgid "Oversized" +msgid "Oversized" +msgstr "Занадта вялікі" \ No newline at end of file diff --git a/po/be/kcm_kwinrules.po b/po/be/kcm_kwinrules.po new file mode 100644 index 0000000..49ba319 --- /dev/null +++ b/po/be/kcm_kwinrules.po @@ -0,0 +1,851 @@ +# translation of kcmkwinrules.po to Belarusian +# +# Darafei Praliaskouski , 2006. +# Ihar Hrachyshka , 2006. +# Darafei Praliaskouski , 2007. +msgid "" +msgstr "" +"Project-Id-Version: kcmkwinrules\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-11-03 08:14+0100\n" +"PO-Revision-Date: 2007-10-28 14:23+0200\n" +"Last-Translator: Darafei Praliaskouski \n" +"Language-Team: Belarusian \n" +"Language: be\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: KBabel 1.11.4\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#, fuzzy, kde-format +#| msgid "Your names" +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Дарафей Праляскоўскі" + +#, fuzzy, kde-format +#| msgid "Your emails" +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "komzpa@licei2.com" + +#: kcmrules.cpp:28 +#, fuzzy, kde-format +#| msgid "Window &role:" +msgid "Window Rules" +msgstr "&Роля вакна:" + +#: kcmrules.cpp:32 +#, kde-format +msgid "Ismael Asensio" +msgstr "" + +#: kcmrules.cpp:33 +#, kde-format +msgid "Author" +msgstr "" + +#: kcmrules.cpp:37 +#, kde-format +msgid "" +"

Window-specific Settings

Here you can customize window settings " +"specifically only for some windows.

Please note that this " +"configuration will not take effect if you do not use KWin as your window " +"manager. If you do use a different window manager, please refer to its " +"documentation for how to customize window behavior.

" +msgstr "" + +#: main.cpp:91 +#, kde-format +msgid "Application settings for %1" +msgstr "Настаўленні праграмы для %1" + +#: main.cpp:111 rulesmodel.cpp:216 +#, kde-format +msgid "Window settings for %1" +msgstr "Уласцівасці акна для %1" + +#: main.cpp:163 +#, fuzzy, kde-format +#| msgid "Edit Window-Specific Settings" +msgctxt "Window caption for the application wide rules dialog" +msgid "Edit Application-Specific Settings" +msgstr "Змяніць настаўленні для асобных вокнаў" + +#: main.cpp:197 +#, kde-format +msgid "KWin" +msgstr "KWin" + +#: main.cpp:204 +#, kde-format +msgid "KWin helper utility" +msgstr "Дапаможная утыліта KWin" + +#: main.cpp:205 +#, kde-format +msgid "KWin id of the window for special window settings." +msgstr "" + +#: main.cpp:206 +#, kde-format +msgid "Whether the settings should affect all windows of the application." +msgstr "" + +#: main.cpp:215 +#, kde-format +msgid "This helper utility is not supposed to be called directly." +msgstr "Гэтую дапаможную праграму не трэба выклікаць уручную." + +#: optionsmodel.cpp:145 +#, kde-format +msgid "Unimportant" +msgstr "" + +#: optionsmodel.cpp:146 +#, kde-format +msgid "Exact Match" +msgstr "Дакладнае супадзенне" + +#: optionsmodel.cpp:147 +#, kde-format +msgid "Substring Match" +msgstr "Супадзенне падрадку" + +#: optionsmodel.cpp:148 +#, kde-format +msgid "Regular Expression" +msgstr "Рэгулярны выраз" + +#: optionsmodel.cpp:153 +#, fuzzy, kde-format +msgid "Do Not Affect" +msgstr "Не запісваць" + +#: optionsmodel.cpp:154 +#, kde-format +msgid "" +"The window property will not be affected and therefore the default handling " +"for it will be used.\n" +"Specifying this will block more generic window settings from taking effect." +msgstr "" + +#: optionsmodel.cpp:157 +#, fuzzy, kde-format +msgid "Apply Initially" +msgstr "&Ужыць налады" + +#: optionsmodel.cpp:158 +#, kde-format +msgid "" +"The window property will be only set to the given value after the window is " +"created.\n" +"No further changes will be affected." +msgstr "" + +#: optionsmodel.cpp:161 +#, kde-format +msgid "Remember" +msgstr "Запомніць" + +#: optionsmodel.cpp:162 +#, kde-format +msgid "" +"The value of the window property will be remembered and, every time the " +"window is created, the last remembered value will be applied." +msgstr "" + +#: optionsmodel.cpp:165 +#, kde-format +msgid "Force" +msgstr "Прымусова" + +#: optionsmodel.cpp:166 +#, kde-format +msgid "The window property will be always forced to the given value." +msgstr "" + +#: optionsmodel.cpp:168 +#, kde-format +msgid "Apply Now" +msgstr "Ужыць зараз" + +#: optionsmodel.cpp:169 +#, kde-format +msgid "" +"The window property will be set to the given value immediately and will not " +"be affected later\n" +"(this action will be deleted afterwards)." +msgstr "" + +#: optionsmodel.cpp:172 +#, fuzzy, kde-format +msgid "Force Temporarily" +msgstr "Зачыніць" + +#: optionsmodel.cpp:173 +#, kde-format +msgid "" +"The window property will be forced to the given value until it is hidden\n" +"(this action will be deleted after the window is hidden)." +msgstr "" + +#: package/contents/ui/FileDialogLoader.qml:14 +#, kde-format +msgid "Select File" +msgstr "" + +#: package/contents/ui/FileDialogLoader.qml:26 +#, kde-format +msgid "KWin Rules (*.kwinrule)" +msgstr "" + +#: package/contents/ui/OptionsComboBox.qml:32 +#, kde-format +msgid "None selected" +msgstr "" + +#: package/contents/ui/OptionsComboBox.qml:37 +#, kde-format +msgid "All selected" +msgstr "" + +#: package/contents/ui/OptionsComboBox.qml:39 +#, kde-format +msgid "%1 selected" +msgid_plural "%1 selected" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +#: package/contents/ui/RulesEditor.qml:48 +#: package/contents/ui/RulesEditor.qml:67 +#, kde-format +msgid "Add Properties..." +msgstr "" + +#: package/contents/ui/RulesEditor.qml:67 +#, kde-format +msgid "Close" +msgstr "" + +#: package/contents/ui/RulesEditor.qml:80 +#, fuzzy, kde-format +msgid "Detect Window Properties" +msgstr "Ствараецца вакно ўласьцівасьцяў." + +#: package/contents/ui/RulesEditor.qml:93 +#, kde-format +msgid "Instantly" +msgstr "" + +#: package/contents/ui/RulesEditor.qml:94 +#, kde-format +msgid "After %1 second" +msgid_plural "After %1 seconds" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +#: package/contents/ui/RulesEditor.qml:113 +#, fuzzy, kde-format +msgid "Select properties" +msgstr "Ствараецца вакно ўласьцівасьцяў." + +#: package/contents/ui/RulesEditor.qml:205 +#: package/contents/ui/ValueEditor.qml:53 +#, kde-format +msgid "Yes" +msgstr "" + +#: package/contents/ui/RulesEditor.qml:205 +#: package/contents/ui/ValueEditor.qml:59 +#, fuzzy, kde-format +#| msgid "None" +msgid "No" +msgstr "Няма" + +#: package/contents/ui/RulesEditor.qml:207 +#: package/contents/ui/ValueEditor.qml:127 +#: package/contents/ui/ValueEditor.qml:134 +#, kde-format +msgid "%1 %" +msgstr "" + +#: package/contents/ui/RulesEditor.qml:209 +#, kde-format +msgctxt "Coordinates (x, y)" +msgid "(%1, %2)" +msgstr "" + +#: package/contents/ui/RulesEditor.qml:211 +#, kde-format +msgctxt "Size (width, height)" +msgid "(%1, %2)" +msgstr "" + +#: package/contents/ui/RulesList.qml:61 +#, kde-format +msgid "No rules for specific windows are currently set" +msgstr "" + +#: package/contents/ui/RulesList.qml:69 +#, kde-format +msgid "Select the rules to export" +msgstr "" + +#: package/contents/ui/RulesList.qml:73 +#, kde-format +msgid "Unselect All" +msgstr "" + +#: package/contents/ui/RulesList.qml:73 +#, kde-format +msgid "Select All" +msgstr "" + +#: package/contents/ui/RulesList.qml:87 +#, kde-format +msgid "Save Rules" +msgstr "" + +#: package/contents/ui/RulesList.qml:98 +#, fuzzy, kde-format +#| msgid "&New..." +msgid "Add New..." +msgstr "&Новы..." + +#: package/contents/ui/RulesList.qml:109 +#, kde-format +msgid "Import..." +msgstr "" + +#: package/contents/ui/RulesList.qml:117 +#, kde-format +msgid "Cancel Export" +msgstr "" + +#: package/contents/ui/RulesList.qml:117 +#, fuzzy, kde-format +#| msgid "Edit..." +msgid "Export..." +msgstr "Змяніць..." + +#: package/contents/ui/RulesList.qml:198 +#, kde-format +msgid "Edit" +msgstr "Змяніць" + +#: package/contents/ui/RulesList.qml:207 +#, kde-format +msgid "Delete" +msgstr "Выдаліць" + +#: package/contents/ui/RulesList.qml:220 +#, kde-format +msgid "Import Rules" +msgstr "" + +#: package/contents/ui/RulesList.qml:232 +#, kde-format +msgid "Export Rules" +msgstr "" + +#: package/contents/ui/ValueEditor.qml:162 +#, kde-format +msgctxt "(x, y) coordinates separator in size/position" +msgid "x" +msgstr "" + +#: rulesdialog.cpp:28 +#, kde-format +msgid "Edit Window-Specific Settings" +msgstr "Змяніць настаўленні для асобных вокнаў" + +#: rulesmodel.cpp:219 +#, kde-format +msgid "Settings for %1" +msgstr "Настаўленні для %1" + +#: rulesmodel.cpp:222 +#, fuzzy, kde-format +#| msgid "Window settings for %1" +msgid "New window settings" +msgstr "Уласцівасці акна для %1" + +#: rulesmodel.cpp:236 +#, kde-format +msgid "" +"You have specified the window class as unimportant.\n" +"This means the settings will possibly apply to windows from all " +"applications. If you really want to create a generic setting, it is " +"recommended you at least limit the window types to avoid special window " +"types." +msgstr "" + +#: rulesmodel.cpp:366 +#, fuzzy, kde-format +#| msgid "De&scription:" +msgid "Description" +msgstr "Апі&санне:" + +#: rulesmodel.cpp:366 rulesmodel.cpp:374 rulesmodel.cpp:382 rulesmodel.cpp:389 +#: rulesmodel.cpp:395 rulesmodel.cpp:403 rulesmodel.cpp:408 rulesmodel.cpp:414 +#, fuzzy, kde-format +#| msgid "&Window" +msgid "Window matching" +msgstr "&Вакно" + +#: rulesmodel.cpp:374 +#, fuzzy, kde-format +#| msgid "Window &class (application type):" +msgid "Window class (application)" +msgstr "&Клас вакна (тып праграмы):" + +#: rulesmodel.cpp:382 +#, fuzzy, kde-format +#| msgid "Match also window &title" +msgid "Match whole window class" +msgstr "Пошук таксама па назве акна" + +#: rulesmodel.cpp:389 +#, fuzzy, kde-format +#| msgid "Match also window &title" +msgid "Whole window class" +msgstr "Пошук таксама па назве акна" + +#: rulesmodel.cpp:395 +#, fuzzy, kde-format +#| msgid "Window &types:" +msgid "Window types" +msgstr "Тыпы вокнаў:" + +#: rulesmodel.cpp:403 +#, fuzzy, kde-format +#| msgid "Window &role:" +msgid "Window role" +msgstr "&Роля вакна:" + +#: rulesmodel.cpp:408 +#, fuzzy, kde-format +#| msgid "Window t&itle:" +msgid "Window title" +msgstr "Загаловак акна:" + +#: rulesmodel.cpp:414 +#, fuzzy, kde-format +msgid "Machine (hostname)" +msgstr "Друкуе назву вузла.\n" + +#: rulesmodel.cpp:420 +#, fuzzy, kde-format +msgid "Position" +msgstr "Становішча" + +#: rulesmodel.cpp:420 rulesmodel.cpp:425 rulesmodel.cpp:430 rulesmodel.cpp:435 +#: rulesmodel.cpp:440 rulesmodel.cpp:453 rulesmodel.cpp:467 rulesmodel.cpp:472 +#: rulesmodel.cpp:477 rulesmodel.cpp:482 rulesmodel.cpp:487 rulesmodel.cpp:493 +#: rulesmodel.cpp:502 rulesmodel.cpp:507 rulesmodel.cpp:512 +#, fuzzy, kde-format +msgid "Size & Position" +msgstr "Становішча" + +#: rulesmodel.cpp:425 +#, fuzzy, kde-format +#| msgid "&Size" +msgid "Size" +msgstr "&Памер" + +#: rulesmodel.cpp:430 +#, fuzzy, kde-format +#| msgid "Maximized &horizontally" +msgid "Maximized horizontally" +msgstr "Разгарнуць гарызантальна" + +#: rulesmodel.cpp:435 +#, fuzzy, kde-format +#| msgid "Maximized &vertically" +msgid "Maximized vertically" +msgstr "Разгарнуць вертыкальна" + +#: rulesmodel.cpp:440 +#, fuzzy, kde-format +#| msgid "All Desktops" +msgid "Virtual Desktop" +msgstr "Усе працоўныя сталы" + +#: rulesmodel.cpp:453 +#, kde-format +msgid "Activity" +msgstr "" + +#: rulesmodel.cpp:467 +#, fuzzy, kde-format +#| msgid "Splash Screen" +msgid "Screen" +msgstr "Застаўка загрузкі" + +#: rulesmodel.cpp:472 +#, fuzzy, kde-format +#| msgid "&Fullscreen" +msgid "Fullscreen" +msgstr "&Поўнаэкранны рэжым" + +#: rulesmodel.cpp:477 +#, fuzzy, kde-format +#| msgid "M&aximum size" +msgid "Minimized" +msgstr "Най&большы памер" + +#: rulesmodel.cpp:482 +#, kde-format +msgid "Shaded" +msgstr "" + +#: rulesmodel.cpp:487 +#, kde-format +msgid "Initial placement" +msgstr "" + +#: rulesmodel.cpp:493 +#, fuzzy, kde-format +msgid "Ignore requested geometry" +msgstr "GenericName=Інтэрактыўная геаметрыя" + +#: rulesmodel.cpp:495 +#, kde-format +msgid "" +"Windows can ask to appear in a certain position.\n" +"By default this overrides the placement strategy\n" +"what might be nasty if the client abuses the feature\n" +"to unconditionally popup in the middle of your screen." +msgstr "" + +#: rulesmodel.cpp:502 +#, fuzzy, kde-format +#| msgid "M&inimum size" +msgid "Minimum Size" +msgstr "&Найменьшы памер" + +#: rulesmodel.cpp:507 +#, fuzzy, kde-format +#| msgid "M&aximum size" +msgid "Maximum Size" +msgstr "Най&большы памер" + +#: rulesmodel.cpp:512 +#, kde-format +msgid "Obey geometry restrictions" +msgstr "" + +#: rulesmodel.cpp:514 +#, kde-format +msgid "" +"Eg. terminals or video players can ask to keep a certain aspect ratio\n" +"or only grow by values larger than one\n" +"(eg. by the dimensions of one character).\n" +"This may be pointless and the restriction prevents arbitrary dimensions\n" +"like your complete screen area." +msgstr "" + +#: rulesmodel.cpp:523 +#, fuzzy, kde-format +#| msgid "Keep &above" +msgid "Keep above" +msgstr "Трымаць вышэй астатніх" + +#: rulesmodel.cpp:523 rulesmodel.cpp:528 rulesmodel.cpp:533 rulesmodel.cpp:539 +#: rulesmodel.cpp:545 rulesmodel.cpp:551 +#, kde-format +msgid "Arrangement & Access" +msgstr "" + +#: rulesmodel.cpp:528 +#, fuzzy, kde-format +#| msgid "Keep &below" +msgid "Keep below" +msgstr "Трымаць ніжэй астатніх" + +#: rulesmodel.cpp:533 +#, fuzzy, kde-format +#| msgid "Skip &taskbar" +msgid "Skip taskbar" +msgstr "Мінуць панель задач" + +#: rulesmodel.cpp:535 +#, kde-format +msgid "Window shall (not) appear in the taskbar." +msgstr "" + +#: rulesmodel.cpp:539 +#, fuzzy, kde-format +#| msgid "Skip &taskbar" +msgid "Skip pager" +msgstr "Мінуць панель задач" + +#: rulesmodel.cpp:541 +#, kde-format +msgid "Window shall (not) appear in the manager for virtual desktops" +msgstr "" + +#: rulesmodel.cpp:545 +#, kde-format +msgid "Skip switcher" +msgstr "" + +#: rulesmodel.cpp:547 +#, kde-format +msgid "Window shall (not) appear in the Alt+Tab list" +msgstr "" + +#: rulesmodel.cpp:551 +#, kde-format +msgid "Shortcut" +msgstr "Скарот" + +#: rulesmodel.cpp:557 +#, kde-format +msgid "No titlebar and frame" +msgstr "" + +#: rulesmodel.cpp:557 rulesmodel.cpp:562 rulesmodel.cpp:568 rulesmodel.cpp:573 +#: rulesmodel.cpp:578 rulesmodel.cpp:589 rulesmodel.cpp:600 rulesmodel.cpp:608 +#: rulesmodel.cpp:621 rulesmodel.cpp:626 rulesmodel.cpp:632 rulesmodel.cpp:637 +#, kde-format +msgid "Appearance & Fixes" +msgstr "" + +#: rulesmodel.cpp:562 +#, kde-format +msgid "Titlebar color scheme" +msgstr "" + +#: rulesmodel.cpp:568 +#, kde-format +msgid "Active opacity" +msgstr "" + +#: rulesmodel.cpp:573 +#, kde-format +msgid "Inactive opacity" +msgstr "" + +#: rulesmodel.cpp:578 +#, fuzzy, kde-format +msgid "Focus stealing prevention" +msgstr "Засяроджаньне на вехнім узроўні" + +#: rulesmodel.cpp:580 +#, kde-format +msgid "" +"KWin tries to prevent windows from taking the focus\n" +"(\"activate\") while you're working in another window,\n" +"but this may sometimes fail or superact.\n" +"\"None\" will unconditionally allow this window to get the focus while\n" +"\"Extreme\" will completely prevent it from taking the focus." +msgstr "" + +#: rulesmodel.cpp:589 +#, fuzzy, kde-format +msgid "Focus protection" +msgstr "Засяроджаньне на вехнім узроўні" + +#: rulesmodel.cpp:591 +#, kde-format +msgid "" +"This controls the focus protection of the currently active window.\n" +"None will always give the focus away,\n" +"Extreme will keep it.\n" +"Otherwise it's interleaved with the stealing prevention\n" +"assigned to the window that wants the focus." +msgstr "" + +#: rulesmodel.cpp:600 +#, fuzzy, kde-format +msgid "Accept focus" +msgstr "Можа засяроджвацца" + +#: rulesmodel.cpp:602 +#, kde-format +msgid "" +"Windows may prevent to get the focus (activate) when being clicked.\n" +"On the other hand you might wish to prevent a window\n" +"from getting focused on a mouse click." +msgstr "" + +#: rulesmodel.cpp:608 +#, fuzzy, kde-format +#| msgid "Block global shortcuts" +msgid "Ignore global shortcuts" +msgstr "Блакаваць глабальныя скароты" + +#: rulesmodel.cpp:610 +#, kde-format +msgid "" +"When used, a window will receive\n" +"all keyboard inputs while it is active, including Alt+Tab etc.\n" +"This is especially interesting for emulators or virtual machines.\n" +"\n" +"Be warned:\n" +"you won't be able to Alt+Tab out of the window\n" +"nor use any other global shortcut (such as Alt+F2 to show KRunner)\n" +"while it's active!" +msgstr "" + +#: rulesmodel.cpp:621 +#, kde-format +msgid "Closeable" +msgstr "" + +#: rulesmodel.cpp:626 +#, fuzzy, kde-format +#| msgid "Window &type" +msgid "Set window type" +msgstr "Тып акна" + +#: rulesmodel.cpp:632 +#, kde-format +msgid "Desktop file name" +msgstr "" + +#: rulesmodel.cpp:637 +#, kde-format +msgid "Block compositing" +msgstr "" + +#: rulesmodel.cpp:717 +#, kde-format +msgid "Normal Window" +msgstr "Звычайнае акно" + +#: rulesmodel.cpp:718 +#, kde-format +msgid "Dialog Window" +msgstr "Дыялогавае вакно" + +#: rulesmodel.cpp:719 +#, kde-format +msgid "Utility Window" +msgstr "Дапаможнае акно" + +#: rulesmodel.cpp:720 +#, kde-format +msgid "Dock (panel)" +msgstr "Док (панель)" + +#: rulesmodel.cpp:721 +#, kde-format +msgid "Toolbar" +msgstr "Панель начыння" + +#: rulesmodel.cpp:722 +#, kde-format +msgid "Torn-Off Menu" +msgstr "Адчэпленае меню" + +#: rulesmodel.cpp:723 +#, kde-format +msgid "Splash Screen" +msgstr "Застаўка загрузкі" + +#: rulesmodel.cpp:724 +#, kde-format +msgid "Desktop" +msgstr "Працоўны стол" + +#. i18n("Unmanaged Window") }, deprecated +#: rulesmodel.cpp:726 +#, kde-format +msgid "Standalone Menubar" +msgstr "Асобнае меню" + +#: rulesmodel.cpp:741 +#, kde-format +msgid "All Desktops" +msgstr "Усе працоўныя сталы" + +#: rulesmodel.cpp:754 +#, kde-format +msgid "All Activities" +msgstr "" + +#: rulesmodel.cpp:775 +#, kde-format +msgid "Default" +msgstr "Прадвызначаны" + +#: rulesmodel.cpp:776 +#, kde-format +msgid "No Placement" +msgstr "" + +#: rulesmodel.cpp:777 +#, kde-format +msgid "Minimal Overlapping" +msgstr "" + +#: rulesmodel.cpp:778 +#, fuzzy, kde-format +#| msgid "M&aximum size" +msgid "Maximized" +msgstr "Най&большы памер" + +#: rulesmodel.cpp:779 +#, fuzzy, kde-format +#| msgid "Cascade" +msgid "Cascaded" +msgstr "Каскад" + +#: rulesmodel.cpp:780 +#, kde-format +msgid "Centered" +msgstr "Пасярэдзіне" + +#: rulesmodel.cpp:781 +#, kde-format +msgid "Random" +msgstr "Выпадковыя" + +#: rulesmodel.cpp:782 +#, fuzzy, kde-format +#| msgid "Top-Left Corner" +msgid "In Top-Left Corner" +msgstr "Верхні левы кут" + +#: rulesmodel.cpp:783 +#, kde-format +msgid "Under Mouse" +msgstr "Пад мышшу" + +#: rulesmodel.cpp:784 +#, kde-format +msgid "On Main Window" +msgstr "На галоўным акне" + +#: rulesmodel.cpp:792 +#, fuzzy, kde-format +#| msgid "None" +msgid "None" +msgstr "Няма" + +#: rulesmodel.cpp:793 +#, kde-format +msgid "Low" +msgstr "Нізкі" + +#: rulesmodel.cpp:794 +#, kde-format +msgid "Normal" +msgstr "Звычайны" + +#: rulesmodel.cpp:795 +#, kde-format +msgid "High" +msgstr "Высокі" + +#: rulesmodel.cpp:796 +#, kde-format +msgid "Extreme" +msgstr "Экстрымальна" \ No newline at end of file diff --git a/po/be/kcmkwincompositing.po b/po/be/kcmkwincompositing.po new file mode 100644 index 0000000..6a8a7a7 --- /dev/null +++ b/po/be/kcmkwincompositing.po @@ -0,0 +1,220 @@ +# translation of kcmkwincompositing.po to Belarusian +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Darafei Praliaskouski , 2007. +msgid "" +msgstr "" +"Project-Id-Version: kcmkwincompositing\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-08-08 02:22+0200\n" +"PO-Revision-Date: 2007-08-27 12:19+0300\n" +"Last-Translator: Darafei Praliaskouski \n" +"Language-Team: Belarusian \n" +"Language: be\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: KBabel 1.11.4\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#. i18n: ectx: property (text), widget (KMessageWidget, glCrashedWarning) +#: compositing.ui:32 +#, kde-format +msgid "" +"OpenGL compositing (the default) has crashed KWin in the past.\n" +"This was most likely due to a driver bug.\n" +"If you think that you have meanwhile upgraded to a stable driver,\n" +"you can reset this protection but be aware that this might result in an " +"immediate crash!\n" +"Alternatively, you might want to use the XRender backend instead." +msgstr "" + +#. i18n: ectx: property (text), widget (KMessageWidget, scaleWarning) +#: compositing.ui:45 +#, kde-format +msgid "" +"Scale method \"Accurate\" is not supported by all hardware and can cause " +"performance regressions and rendering artifacts." +msgstr "" + +#. i18n: ectx: property (text), widget (KMessageWidget, windowThumbnailWarning) +#: compositing.ui:68 +#, kde-format +msgid "" +"Keeping the window thumbnail always interferes with the minimized state of " +"windows. This can result in windows not suspending their work when minimized." +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Enabled) +#: compositing.ui:80 +#, fuzzy, kde-format +#| msgid "Enable desktop effects" +msgid "Enable compositor on startup" +msgstr "Уключыць эфекты працоўнага стала" + +#. i18n: ectx: property (text), widget (QLabel, animationSpeedLabel) +#: compositing.ui:87 +#, kde-format +msgid "Animation speed:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: compositing.ui:124 +#, kde-format +msgid "Very slow" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: compositing.ui:144 +#, kde-format +msgid "Instant" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, scaleMethodLabel) +#: compositing.ui:156 +#, kde-format +msgid "Scale method:" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_XRenderSmoothScale) +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glTextureFilter) +#: compositing.ui:166 compositing.ui:180 +#, kde-format +msgid "Crisp" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_XRenderSmoothScale) +#: compositing.ui:171 +#, kde-format +msgid "Smooth (slower)" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glTextureFilter) +#: compositing.ui:185 +#, kde-format +msgid "Smooth" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glTextureFilter) +#: compositing.ui:190 +#, kde-format +msgid "Accurate" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: compositing.ui:207 +#, kde-format +msgid "Rendering backend:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: compositing.ui:224 +#, kde-format +msgid "Tearing prevention (\"vsync\"):" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glPreferBufferSwap) +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_HiddenPreviews) +#: compositing.ui:232 compositing.ui:268 +#, kde-format +msgid "Never" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glPreferBufferSwap) +#: compositing.ui:237 +#, kde-format +msgid "Automatic" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glPreferBufferSwap) +#: compositing.ui:242 +#, kde-format +msgid "Only when cheap" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glPreferBufferSwap) +#: compositing.ui:247 +#, kde-format +msgid "Full screen repaints" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glPreferBufferSwap) +#: compositing.ui:252 +#, kde-format +msgid "Re-use screen content" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: compositing.ui:260 +#, kde-format +msgid "Keep window thumbnails:" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_HiddenPreviews) +#: compositing.ui:273 +#, kde-format +msgid "Only for Shown Windows" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_HiddenPreviews) +#: compositing.ui:278 +#, kde-format +msgid "Always" +msgstr "" + +#. i18n: ectx: property (toolTip), widget (QCheckBox, kcfg_WindowsBlockCompositing) +#: compositing.ui:288 +#, kde-format +msgid "" +"Applications can set a hint to block compositing when the window is open.\n" +" This brings performance improvements for e.g. games.\n" +" The setting can be overruled by window-specific rules." +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_WindowsBlockCompositing) +#: compositing.ui:291 +#, kde-format +msgid "Allow applications to block compositing" +msgstr "" + +#: main.cpp:80 +#, kde-format +msgid "Re-enable OpenGL detection" +msgstr "" + +#: main.cpp:132 +#, kde-format +msgid "" +"\"Only when cheap\" only prevents tearing for full screen changes like a " +"video." +msgstr "" + +#: main.cpp:136 +#, kde-format +msgid "\"Full screen repaints\" can cause performance problems." +msgstr "" + +#: main.cpp:140 +#, kde-format +msgid "" +"\"Re-use screen content\" causes severe performance problems on MESA drivers." +msgstr "" + +#: main.cpp:160 +#, fuzzy, kde-format +#| msgid "OpenGL" +msgid "OpenGL 3.1" +msgstr "OpenGL" + +#: main.cpp:161 +#, fuzzy, kde-format +#| msgid "OpenGL" +msgid "OpenGL 2.0" +msgstr "OpenGL" + +#: main.cpp:162 +#, kde-format +msgid "XRender" +msgstr "XRender" \ No newline at end of file diff --git a/po/be/kcmkwm.po b/po/be/kcmkwm.po new file mode 100644 index 0000000..a102dba --- /dev/null +++ b/po/be/kcmkwm.po @@ -0,0 +1,1275 @@ +# translation of kcmkwm.po to Belarusian +# +# Darafei Praliaskouski , 2006. +# Darafei Praliaskouski , 2007. +msgid "" +msgstr "" +"Project-Id-Version: kcmkwm\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-08-29 02:26+0200\n" +"PO-Revision-Date: 2007-08-26 21:22+0300\n" +"Last-Translator: Darafei Praliaskouski \n" +"Language-Team: Belarusian \n" +"Language: be\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: KBabel 1.11.4\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Дарафей Праляскоўскі" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "komzpa@licei2.com" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_1) +#: actions.ui:17 +#, fuzzy, kde-format +#| msgid "Window Actio&ns" +msgid "Inactive Inner Window Actions" +msgstr "Днеянні во&кнаў" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#. i18n: ectx: property (text), widget (QLabel, label_1) +#: actions.ui:26 mouse.ui:177 +#, fuzzy, kde-format +#| msgid "&Titlebar double-click:" +msgid "&Left click:" +msgstr "Падвоеная пстрычка па загалоўку:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandWindow1) +#: actions.ui:39 +#, kde-format +msgid "" +"In this row you can customize left click behavior when clicking into an " +"inactive inner window ('inner' means: not titlebar, not frame)." +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow3) +#: actions.ui:43 actions.ui:83 actions.ui:123 +#, fuzzy, kde-format +#| msgid "Activate" +msgid "Activate, raise and pass click" +msgstr "Актывізаваць" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow3) +#: actions.ui:48 actions.ui:88 actions.ui:128 +#, fuzzy, kde-format +#| msgid "Activate" +msgid "Activate and pass click" +msgstr "Актывізаваць" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow3) +#: actions.ui:53 actions.ui:93 actions.ui:133 mouse.ui:293 mouse.ui:408 +#: mouse.ui:523 +#, kde-format +msgid "Activate" +msgstr "Актывізаваць" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow3) +#: actions.ui:58 actions.ui:98 actions.ui:138 mouse.ui:283 mouse.ui:398 +#: mouse.ui:513 +#, fuzzy, kde-format +#| msgid "Activate" +msgid "Activate and raise" +msgstr "Актывізаваць" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: actions.ui:66 mouse.ui:200 +#, fuzzy, kde-format +#| msgid "&Titlebar double-click:" +msgid "&Middle click:" +msgstr "Падвоеная пстрычка па загалоўку:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandWindow2) +#: actions.ui:79 +#, kde-format +msgid "" +"In this row you can customize middle click behavior when clicking into an " +"inactive inner window ('inner' means: not titlebar, not frame)." +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: actions.ui:106 mouse.ui:213 +#, kde-format +msgid "&Right click:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandWindow3) +#: actions.ui:119 +#, kde-format +msgid "" +"In this row you can customize right click behavior when clicking into an " +"inactive inner window ('inner' means: not titlebar, not frame)." +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: actions.ui:146 mouse.ui:88 +#, kde-format +msgid "Mouse &wheel:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandWindowWheel) +#: actions.ui:159 +#, kde-format +msgid "" +"In this row you can customize behavior when scrolling into an inactive inner " +"window ('inner' means: not titlebar, not frame)." +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindowWheel) +#: actions.ui:163 +#, kde-format +msgid "Scroll" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindowWheel) +#: actions.ui:168 +#, fuzzy, kde-format +#| msgid "Activate" +msgid "Activate and scroll" +msgstr "Актывізаваць" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindowWheel) +#: actions.ui:173 +#, fuzzy, kde-format +#| msgid "Activate" +msgid "Activate, raise and scroll" +msgstr "Актывізаваць" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#: actions.ui:184 +#, kde-format +msgid "Inner Window, Titlebar and Frame Actions" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: actions.ui:195 +#, kde-format +msgid "Mo&difier key:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandAllKey) +#: actions.ui:205 +#, kde-format +msgid "" +"Here you select whether holding the Meta key or Alt key will allow you to " +"perform the following actions." +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllKey) +#: actions.ui:209 +#, kde-format +msgid "Meta" +msgstr "Meta" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllKey) +#: actions.ui:214 +#, kde-format +msgid "Alt" +msgstr "Alt" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: actions.ui:236 +#, kde-format +msgid " + " +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_8) +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: actions.ui:248 mouse.ui:601 +#, fuzzy, kde-format +#| msgid "&Titlebar double-click:" +msgid "L&eft click:" +msgstr "Падвоеная пстрычка па загалоўку:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandAll1) +#: actions.ui:261 +#, kde-format +msgid "" +"In this row you can customize left click behavior when clicking into the " +"titlebar or the frame." +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:265 actions.ui:335 actions.ui:405 +#, kde-format +msgid "Move" +msgstr "Перанесці" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:270 actions.ui:340 actions.ui:410 +#, fuzzy, kde-format +#| msgid "Activate" +msgid "Activate, raise and move" +msgstr "Актывізаваць" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:275 actions.ui:345 actions.ui:415 mouse.ui:246 mouse.ui:308 +#: mouse.ui:361 mouse.ui:423 mouse.ui:476 mouse.ui:538 +#, kde-format +msgid "Toggle raise and lower" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:280 actions.ui:350 actions.ui:420 +#, kde-format +msgid "Resize" +msgstr "Змяніць памер" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:285 actions.ui:355 actions.ui:425 mouse.ui:236 mouse.ui:298 +#: mouse.ui:351 mouse.ui:413 mouse.ui:466 mouse.ui:528 +#, kde-format +msgid "Raise" +msgstr "Падняць" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:290 actions.ui:360 actions.ui:430 mouse.ui:65 mouse.ui:241 +#: mouse.ui:303 mouse.ui:356 mouse.ui:418 mouse.ui:471 mouse.ui:533 +#, kde-format +msgid "Lower" +msgstr "Пасунуць ніжэй" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:295 actions.ui:365 actions.ui:435 mouse.ui:55 mouse.ui:251 +#: mouse.ui:313 mouse.ui:366 mouse.ui:428 mouse.ui:481 mouse.ui:543 +#, kde-format +msgid "Minimize" +msgstr "Найменшыць" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:300 actions.ui:370 actions.ui:440 +#, fuzzy, kde-format +#| msgid "Change Opacity" +msgid "Decrease opacity" +msgstr "Змяніць празрыстасць" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:305 actions.ui:375 actions.ui:445 +#, fuzzy, kde-format +#| msgid "Change Opacity" +msgid "Increase opacity" +msgstr "Змяніць празрыстасць" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:310 actions.ui:380 actions.ui:450 actions.ui:505 mouse.ui:80 +#: mouse.ui:132 mouse.ui:271 mouse.ui:333 mouse.ui:386 mouse.ui:448 +#: mouse.ui:501 mouse.ui:563 +#, fuzzy, kde-format +#| msgid "Nothing" +msgid "Do nothing" +msgstr "Нічога" + +#. i18n: ectx: property (text), widget (QLabel, label_8) +#: actions.ui:318 +#, fuzzy, kde-format +#| msgid "Middle button:" +msgid "Middle &click:" +msgstr "Сярэдняя кнопка:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandAll2) +#: actions.ui:331 +#, kde-format +msgid "" +"In this row you can customize middle click behavior when clicking into the " +"titlebar or the frame." +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_10) +#. i18n: ectx: property (text), widget (QLabel, label_9) +#: actions.ui:388 mouse.ui:671 +#, kde-format +msgid "Right clic&k:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:401 +#, kde-format +msgid "" +"In this row you can customize right click behavior when clicking into the " +"titlebar or the frame." +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_10) +#: actions.ui:458 +#, kde-format +msgid "Mo&use wheel:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:471 +#, kde-format +msgid "" +"Here you can customize KDE's behavior when scrolling with the mouse wheel in " +"a window while pressing the modifier key." +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:475 mouse.ui:102 +#, fuzzy, kde-format +#| msgid "Lower" +msgid "Raise/lower" +msgstr "Пасунуць ніжэй" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:480 mouse.ui:107 +#, kde-format +msgid "Shade/unshade" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:485 mouse.ui:112 +#, fuzzy, kde-format +#| msgid "Maximize" +msgid "Maximize/restore" +msgstr "Найбольшыць" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:490 mouse.ui:117 +#, kde-format +msgid "Keep above/below" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:495 mouse.ui:122 +#, fuzzy, kde-format +#| msgid "Move to Previous/Next Desktop" +msgid "Move to previous/next desktop" +msgstr "Перамясціць на папярэдні/наступны працоўны стол" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:500 mouse.ui:127 +#, fuzzy, kde-format +#| msgid "Change Opacity" +msgid "Change opacity" +msgstr "Змяніць празрыстасць" + +#. i18n: ectx: property (text), widget (QLabel, shadeHoverLabel) +#: advanced.ui:20 +#, fuzzy, kde-format +#| msgid "Window Actio&ns" +msgid "Window &unshading:" +msgstr "Днеянні во&кнаў" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_ShadeHover) +#: advanced.ui:32 +#, kde-format +msgid "" +"

If this option is enabled, a shaded window will " +"unshade automatically when the mouse pointer has been over the titlebar for " +"some time.

" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ShadeHover) +#: advanced.ui:35 +#, kde-format +msgid "On titlebar hover after:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QSpinBox, kcfg_ShadeHoverInterval) +#: advanced.ui:42 +#, kde-format +msgid "" +"Sets the time in milliseconds before the window unshades when the mouse " +"pointer goes over the shaded window." +msgstr "" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_ShadeHoverInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_DelayFocusInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_AutoRaiseInterval) +#: advanced.ui:45 focus.ui:85 focus.ui:178 +#, fuzzy, kde-format +#| msgid " msec" +msgid " ms" +msgstr " мс" + +#. i18n: ectx: property (text), widget (QLabel, windowPlacementLabel) +#: advanced.ui:66 +#, kde-format +msgid "Window &placement:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_Placement) +#: advanced.ui:76 +#, kde-format +msgid "" +"

The placement policy determines where a new window " +"will appear on the desktop.

  • Smart will try to achieve a minimum overlap of windows
  • Maximizing will try to maximize every window to fill the " +"whole screen. It might be useful to selectively affect placement of some " +"windows using the window-specific settings.
  • Cascade will " +"cascade the windows
  • Random will use a random " +"position
  • Centered will place the window " +"centered
  • Zero-cornered will place the window in " +"the top-left corner
  • Under mouse will place the " +"window under the pointer
" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:80 +#, kde-format +msgid "Minimal Overlapping" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:85 +#, fuzzy, kde-format +#| msgid "Maximize" +msgid "Maximized" +msgstr "Найбольшыць" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:90 +#, fuzzy, kde-format +#| msgid "Cascade" +msgid "Cascaded" +msgstr "Каскад" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:95 +#, kde-format +msgid "Random" +msgstr "Выпадковыя" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:100 +#, kde-format +msgid "Centered" +msgstr "Пасярэдзіне" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:105 +#, kde-format +msgid "In Top-Left Corner" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:110 +#, kde-format +msgid "Under mouse" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_AllowKDEAppsToRememberWindowPositions) +#: advanced.ui:118 +#, kde-format +msgid "" +"When turned on, KDE apps which are able to remember the positions of their " +"windows are allowed to do so. This will override the window placement mode " +"defined above." +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AllowKDEAppsToRememberWindowPositions) +#: advanced.ui:121 +#, kde-format +msgid "Allow KDE apps to remember the positions of their own windows" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, specialWindowsLabel) +#: advanced.ui:128 +#, fuzzy, kde-format +#| msgid "Windows" +msgid "&Special windows:" +msgstr "Вокны" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_HideUtilityWindowsForInactive) +#: advanced.ui:138 +#, kde-format +msgid "" +"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." +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_HideUtilityWindowsForInactive) +#: advanced.ui:141 +#, kde-format +msgid "Hide utility windows for inactive applications" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, windowFocusPolicyLabel) +#: focus.ui:22 +#, kde-format +msgid "Window &activation policy:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QComboBox, windowFocusPolicy) +#: focus.ui:32 +#, kde-format +msgid "With this option you can specify how and when windows will be focused." +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:36 +#, kde-format +msgctxt "sassa asas" +msgid "Click to focus" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:41 +#, kde-format +msgid "Click to focus (mouse precedence)" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:46 +#, kde-format +msgid "Focus follows mouse" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:51 +#, kde-format +msgid "Focus follows mouse (mouse precedence)" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:56 +#, kde-format +msgid "Focus under mouse" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:61 +#, kde-format +msgid "Focus strictly under mouse" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, delayFocusOnLabel) +#: focus.ui:69 +#, kde-format +msgid "&Delay focus by:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QSpinBox, kcfg_DelayFocusInterval) +#: focus.ui:82 +#, kde-format +msgid "" +"This is the delay after which the window the mouse pointer is over will " +"automatically receive focus." +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, focusStealingLabel) +#: focus.ui:101 +#, kde-format +msgid "Focus &stealing prevention:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_FocusStealingPreventionLevel) +#: focus.ui:114 +#, kde-format +msgid "" +"

This option specifies how much KWin will try to " +"prevent unwanted focus stealing caused by unexpected activation of new " +"windows. (Note: This feature does not work with the Focus under mouse or Focus strictly under mouse focus policies.)

  • 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.

" +msgstr "" + +#. i18n: Focus Stealing Prevention Level +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_FocusStealingPreventionLevel) +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_BorderSnapZone) +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_WindowSnapZone) +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_CenterSnapZone) +#: focus.ui:118 moving.ui:53 moving.ui:75 moving.ui:97 +#, fuzzy, kde-format +#| msgctxt "Focus Stealing Prevention Level" +#| msgid "None" +msgid "None" +msgstr "Няма" + +#. i18n: Focus Stealing Prevention Level +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_FocusStealingPreventionLevel) +#: focus.ui:123 +#, fuzzy, kde-format +#| msgctxt "Focus Stealing Prevention Level" +#| msgid "Low" +msgid "Low" +msgstr "Нізкі" + +#. i18n: Focus Stealing Prevention Level +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_FocusStealingPreventionLevel) +#: focus.ui:128 +#, kde-format +msgid "Medium" +msgstr "" + +#. i18n: Focus Stealing Prevention Level +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_FocusStealingPreventionLevel) +#: focus.ui:133 +#, fuzzy, kde-format +#| msgctxt "Focus Stealing Prevention Level" +#| msgid "High" +msgid "High" +msgstr "Высокі" + +#. i18n: Focus Stealing Prevention Level +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_FocusStealingPreventionLevel) +#: focus.ui:138 +#, fuzzy, kde-format +#| msgctxt "Focus Stealing Prevention Level" +#| msgid "Extreme" +msgid "Extreme" +msgstr "Экстрымальна" + +#. i18n: ectx: property (text), widget (QLabel, raisingWindowsLabel) +#: focus.ui:146 +#, fuzzy, kde-format +#| msgid "Windows" +msgid "Raising windows:" +msgstr "Вокны" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_ClickRaise) +#: focus.ui:153 +#, kde-format +msgid "" +"When this option is enabled, the active window will be brought to the front " +"when you click somewhere into the window contents. To change it for inactive " +"windows, you need to change the settings in the Actions tab." +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ClickRaise) +#: focus.ui:156 +#, kde-format +msgid "&Click raises active window" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_AutoRaise) +#: focus.ui:165 +#, kde-format +msgid "" +"When this option is enabled, a window in the background will automatically " +"come to the front when the mouse pointer has been over it for some time." +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AutoRaise) +#: focus.ui:168 +#, kde-format +msgid "&Raise on hover, delayed by:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QSpinBox, kcfg_AutoRaiseInterval) +#: focus.ui:175 +#, kde-format +msgid "" +"This is the delay after which the window that the mouse pointer is over will " +"automatically come to the front." +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, multiscreenBehaviorLabel) +#: focus.ui:196 +#, kde-format +msgid "Multiscreen behavior:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_ActiveMouseScreen) +#: focus.ui:203 +#, kde-format +msgid "" +"When this option is enabled, the active Xinerama screen (where new windows " +"appear, for example) is the screen containing the mouse pointer. When " +"disabled, the active Xinerama screen is the screen containing the focused " +"window. By default this option is disabled for Click to focus and enabled " +"for other focus policies." +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ActiveMouseScreen) +#: focus.ui:206 +#, kde-format +msgid "Active screen follows &mouse" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_SeparateScreenFocus) +#: focus.ui:213 +#, kde-format +msgid "" +"When this option is enabled, focus operations are limited only to the active " +"Xinerama screen" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_SeparateScreenFocus) +#: focus.ui:216 +#, kde-format +msgid "&Separate screen focus" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, windowFocusPolicyDescriptionLabel) +#: focus.ui:229 +#, kde-format +msgid "Window activation policy description" +msgstr "" + +#: main.cpp:73 +#, kde-format +msgid "&Focus" +msgstr "Фокус" + +#: main.cpp:84 +#, fuzzy, kde-format +#| msgid "&Titlebar Actions" +msgid "Titlebar A&ctions" +msgstr "Дзеянні радку загалоўку" + +#: main.cpp:91 +#, fuzzy, kde-format +#| msgid "Window Actio&ns" +msgid "W&indow Actions" +msgstr "Днеянні во&кнаў" + +#: main.cpp:98 +#, kde-format +msgid "Mo&vement" +msgstr "" + +#: main.cpp:105 +#, fuzzy, kde-format +#| msgid "Ad&vanced" +msgid "Adva&nced" +msgstr "Ад&мысловыя" + +#: main.cpp:110 +#, kde-format +msgid "Window Behavior Configuration Module" +msgstr "Модуль настаўлення паводзін вокнаў" + +#: main.cpp:112 +#, kde-format +msgid "(c) 1997 - 2002 KWin and KControl Authors" +msgstr "(c) 1997 - 2002 Аўтары KWin і KControl" + +#: main.cpp:114 +#, kde-format +msgid "Matthias Ettrich" +msgstr "Матыяс Эттрых" + +#: main.cpp:115 +#, kde-format +msgid "Waldo Bastian" +msgstr "Вальдо Басціян" + +#: main.cpp:116 +#, kde-format +msgid "Cristian Tibirna" +msgstr "Крысціян Цібірна" + +#: main.cpp:117 +#, kde-format +msgid "Matthias Kalle Dalheimer" +msgstr "Маттыяс Калле Дальхеймер" + +#: main.cpp:118 +#, kde-format +msgid "Daniel Molkentin" +msgstr "Даніель Молькенцін" + +#: main.cpp:119 +#, kde-format +msgid "Wynn Wilkes" +msgstr "Вінн Вількс" + +#: main.cpp:120 +#, kde-format +msgid "Pat Dowler" +msgstr "Пэт Доўлер" + +#: main.cpp:121 +#, kde-format +msgid "Bernd Wuebben" +msgstr "Бернд Уэббен" + +#: main.cpp:122 +#, kde-format +msgid "Matthias Hoelzer-Kluepfel" +msgstr "Матыяс Хёльцэр-Клюпфэл" + +#: main.cpp:167 +#, kde-format +msgid "" +"

Window Behavior

Here you can customize the way windows behave " +"when being moved, resized or clicked on. You can also specify a focus policy " +"as well as a placement policy for new windows.

Please note that this " +"configuration will not take effect if you do not use KWin as your window " +"manager. If you do use a different window manager, please refer to its " +"documentation for how to customize window behavior.

" +msgstr "" + +#: main.cpp:187 +#, kde-format +msgid "&Titlebar Actions" +msgstr "Дзеянні радку загалоўку" + +#: main.cpp:193 +#, kde-format +msgid "Window Actio&ns" +msgstr "Днеянні во&кнаў" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_1) +#: mouse.ui:17 +#, fuzzy, kde-format +#| msgid "&Titlebar Actions" +msgid "Titlebar Actions" +msgstr "Дзеянні радку загалоўку" + +#. i18n: ectx: property (text), widget (QLabel, label_1) +#: mouse.ui:26 +#, fuzzy, kde-format +#| msgid "&Titlebar double-click:" +msgid "&Double-click:" +msgstr "Падвоеная пстрычка па загалоўку:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#: mouse.ui:36 +#, kde-format +msgid "Behavior on double click into the titlebar." +msgstr "" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonLeftClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonMiddleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonRightClickCommand) +#: mouse.ui:40 mouse.ui:615 mouse.ui:650 mouse.ui:685 +#, fuzzy, kde-format +#| msgid "Maximize" +msgid "Maximize" +msgstr "Найбольшыць" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonLeftClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonMiddleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonRightClickCommand) +#: mouse.ui:45 mouse.ui:620 mouse.ui:655 mouse.ui:690 +#, kde-format +msgid "Vertically maximize" +msgstr "" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonLeftClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonMiddleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonRightClickCommand) +#: mouse.ui:50 mouse.ui:625 mouse.ui:660 mouse.ui:695 +#, kde-format +msgid "Horizontally maximize" +msgstr "" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#: mouse.ui:60 mouse.ui:256 mouse.ui:318 mouse.ui:371 mouse.ui:433 mouse.ui:486 +#: mouse.ui:548 +#, kde-format +msgid "Shade" +msgstr "Зацяніць" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#: mouse.ui:70 mouse.ui:261 mouse.ui:323 mouse.ui:376 mouse.ui:438 mouse.ui:491 +#: mouse.ui:553 +#, kde-format +msgid "Close" +msgstr "" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#: mouse.ui:75 +#, fuzzy, kde-format +#| msgid "On All Desktops" +msgid "Show on all desktops" +msgstr "На ўсіх працоўных сталах" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandTitlebarWheel) +#: mouse.ui:98 +#, kde-format +msgid "Behavior on mouse wheel scroll over the titlebar." +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#: mouse.ui:143 +#, fuzzy, kde-format +#| msgid "&Titlebar Actions" +msgid "Titlebar and Frame Actions" +msgstr "Дзеянні радку загалоўку" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: mouse.ui:167 +#, kde-format +msgid "Active" +msgstr "Актыўная" + +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: mouse.ui:190 +#, fuzzy, kde-format +#| msgid "Active" +msgid "Inactive" +msgstr "Актыўная" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandActiveTitlebar3) +#: mouse.ui:232 mouse.ui:347 mouse.ui:462 +#, kde-format +msgid "" +"Behavior on left click into the titlebar or frame of an active window." +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#: mouse.ui:266 mouse.ui:328 mouse.ui:381 mouse.ui:443 mouse.ui:496 +#: mouse.ui:558 +#, fuzzy, kde-format +#| msgid "Operations Menu" +msgid "Show actions menu" +msgstr "Меню аперацый" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#: mouse.ui:279 mouse.ui:394 mouse.ui:509 +#, kde-format +msgid "" +"Behavior on left click into the titlebar or frame of an " +"inactive window." +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#: mouse.ui:288 mouse.ui:403 mouse.ui:518 +#, fuzzy, kde-format +#| msgid "Activate" +msgid "Activate and lower" +msgstr "Актывізаваць" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_3) +#: mouse.ui:589 +#, fuzzy, kde-format +#| msgid "Maximize" +msgid "Maximize Button Actions" +msgstr "Найбольшыць" + +#. i18n: ectx: property (whatsThis), widget (QLabel, label_8) +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_MaximizeButtonLeftClickCommand) +#: mouse.ui:598 mouse.ui:611 +#, kde-format +msgid "Behavior on left click onto the maximize button." +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QLabel, label_9) +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_MaximizeButtonMiddleClickCommand) +#: mouse.ui:633 mouse.ui:646 +#, kde-format +msgid "Behavior on middle click onto the maximize button." +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_9) +#: mouse.ui:636 +#, fuzzy, kde-format +#| msgid "Middle button:" +msgid "Middle c&lick:" +msgstr "Сярэдняя кнопка:" + +#. i18n: ectx: property (whatsThis), widget (QLabel, label_10) +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_MaximizeButtonRightClickCommand) +#: mouse.ui:668 mouse.ui:681 +#, kde-format +msgid "Behavior on right click onto the maximize button." +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, geometryTipLabel) +#: moving.ui:20 +#, kde-format +msgid "Window &geometry:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_GeometryTip) +#: moving.ui:30 +#, kde-format +msgid "" +"Enable this option if you want a window's geometry to be displayed while it " +"is being moved or resized. The window position relative to the top-left " +"corner of the screen is displayed together with its size." +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_GeometryTip) +#: moving.ui:33 +#, kde-format +msgid "Display when moving or resizing" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, borderSnapLabel) +#: moving.ui:40 +#, kde-format +msgid "Screen &edge snap zone:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QSpinBox, kcfg_BorderSnapZone) +#: moving.ui:50 +#, kde-format +msgid "" +"Here you can set the snap zone for screen edges, i.e. the 'strength' of the " +"magnetic field which will make windows snap to the border when moved near it." +msgstr "" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_BorderSnapZone) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_WindowSnapZone) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_CenterSnapZone) +#: moving.ui:56 moving.ui:78 moving.ui:100 +#, kde-format +msgid " px" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QSpinBox, kcfg_WindowSnapZone) +#: moving.ui:72 +#, kde-format +msgid "" +"Here you can set the snap zone for windows, i.e. the 'strength' of the " +"magnetic field which will make windows snap to each other when they are " +"moved near another window." +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QSpinBox, kcfg_CenterSnapZone) +#: moving.ui:94 +#, kde-format +msgid "" +"Here you can set the snap zone for the screen center, i.e. the 'strength' of " +"the magnetic field which will make windows snap to the center of the screen " +"when moved near it." +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_SnapOnlyWhenOverlapping) +#: moving.ui:113 +#, kde-format +msgid "" +"Here you can set that windows will be only snapped if you try to overlap " +"them, i.e. they will not be snapped if the windows comes only near another " +"window or border." +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_SnapOnlyWhenOverlapping) +#: moving.ui:116 +#, kde-format +msgid "Only when overlapping" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, windowSnapLabel) +#: moving.ui:123 +#, kde-format +msgid "&Window snap zone:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, centerSnaplabel) +#: moving.ui:133 +#, kde-format +msgid "&Center snap zone:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, OverlapSnapLabel) +#: moving.ui:143 +#, fuzzy, kde-format +#| msgid "Windows" +msgid "&Snap windows:" +msgstr "Вокны" + +#: windows.cpp:87 +#, kde-format +msgid "" +"Click to focus: A window becomes active when you click into it. " +"This behavior is common on other operating systems and likely what you want." +msgstr "" + +#: windows.cpp:91 +#, kde-format +msgid "" +"Click to focus (mouse precedence): Mostly the same as Click to " +"focus. If an active window has to be chosen by the system (eg. because " +"the currently active one was closed) the window under the mouse is the " +"preferred candidate. Unusual, but possible variant of Click to focus." +msgstr "" + +#: windows.cpp:96 +#, kde-format +msgid "" +"Focus follows mouse: Moving the mouse onto a window will activate " +"it. Eg. windows randomly appearing under the mouse will not gain the focus. " +"Focus stealing prevention takes place as usual. Think as Click " +"to focus just without having to actually click." +msgstr "" + +#: windows.cpp:100 +#, kde-format +msgid "" +"This is mostly the same as Focus follows mouse. If an active window " +"has to be chosen by the system (eg. because the currently active one was " +"closed) the window under the mouse is the preferred candidate. Choose this, " +"if you want a hover controlled focus." +msgstr "" + +#: windows.cpp:105 +#, kde-format +msgid "" +"Focus under mouse: The focus always remains on the window under the " +"mouse.
Warning: Focus stealing prevention and " +"the tabbox ('Alt+Tab') contradict the activation policy and will " +"not work. You very likely want to use Focus follows mouse (mouse " +"precedence) instead!" +msgstr "" + +#: windows.cpp:109 +#, kde-format +msgid "" +"Focus strictly under mouse: The focus is always on the window under " +"the mouse (in doubt nowhere) very much like the focus behavior in an " +"unmanaged legacy X11 environment.
Warning: Focus " +"stealing prevention and the tabbox ('Alt+Tab') contradict the " +"activation policy and will not work. You very likely want to use Focus " +"follows mouse (mouse precedence) instead!" +msgstr "" \ No newline at end of file diff --git a/po/be/kwin.po b/po/be/kwin.po new file mode 100644 index 0000000..5a39a36 --- /dev/null +++ b/po/be/kwin.po @@ -0,0 +1,2622 @@ +# translation of kwin.po to Belarusian +# +# Ihar Hrachyshka , 2006. +# Darafei Praliaskouski , 2006. +# Symbol , 2006. +# Darafei Praliaskouski , 2007. +msgid "" +msgstr "" +"Project-Id-Version: kwin\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-11-25 08:45+0100\n" +"PO-Revision-Date: 2007-12-09 18:32+0200\n" +"Last-Translator: Darafei Praliaskouski \n" +"Language-Team: Belarusian \n" +"Language: be\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: KBabel 1.11.4\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Ігар Грачышка,Дарафей Праляскоўскі" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "ihar.hrachyshka@gmail.com,symbol@akeeri.tk" + +#: abstract_client.cpp:2729 +#, kde-format +msgctxt "Application is not responding, appended to window title" +msgid "(Not Responding)" +msgstr "" + +#: abstract_wayland_output.cpp:252 +#, kde-format +msgid "unknown" +msgstr "" + +#: colorcorrection/manager.cpp:58 +#, kde-format +msgctxt "Night Color was disabled" +msgid "Night Color Off" +msgstr "" + +#: colorcorrection/manager.cpp:59 +#, kde-format +msgctxt "Night Color was enabled" +msgid "Night Color On" +msgstr "" + +#: colorcorrection/manager.cpp:228 colorcorrection/manager.cpp:231 +#: colorcorrection/manager.cpp:238 +#, kde-format +msgid "Toggle Night Color" +msgstr "" + +#: composite.cpp:926 +#, kde-format +msgid "" +"Desktop effects have been suspended by another application.
You can " +"resume using the '%1' shortcut." +msgstr "" + +#: debug_console.cpp:65 +#, kde-format +msgid "Timestamp" +msgstr "" + +#: debug_console.cpp:70 +#, kde-format +msgid "Timestamp (µsec)" +msgstr "" + +#: debug_console.cpp:77 +#, fuzzy, kde-format +#| msgid "Top-left" +msgctxt "A mouse button" +msgid "Left" +msgstr "Верхні ніжні" + +#: debug_console.cpp:79 +#, fuzzy, kde-format +#| msgid "Top-right" +msgctxt "A mouse button" +msgid "Right" +msgstr "Правы верхні" + +#: debug_console.cpp:81 +#, kde-format +msgctxt "A mouse button" +msgid "Middle" +msgstr "" + +#: debug_console.cpp:83 +#, kde-format +msgctxt "A mouse button" +msgid "Back" +msgstr "" + +#: debug_console.cpp:85 +#, kde-format +msgctxt "A mouse button" +msgid "Forward" +msgstr "" + +#: debug_console.cpp:87 +#, kde-format +msgctxt "A mouse button" +msgid "Task" +msgstr "" + +#: debug_console.cpp:89 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 4" +msgstr "" + +#: debug_console.cpp:91 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 5" +msgstr "" + +#: debug_console.cpp:93 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 6" +msgstr "" + +#: debug_console.cpp:95 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 7" +msgstr "" + +#: debug_console.cpp:97 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 8" +msgstr "" + +#: debug_console.cpp:99 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 9" +msgstr "" + +#: debug_console.cpp:101 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 10" +msgstr "" + +#: debug_console.cpp:103 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 11" +msgstr "" + +#: debug_console.cpp:105 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 12" +msgstr "" + +#: debug_console.cpp:107 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 13" +msgstr "" + +#: debug_console.cpp:109 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 14" +msgstr "" + +#: debug_console.cpp:111 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 15" +msgstr "" + +#: debug_console.cpp:113 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 16" +msgstr "" + +#: debug_console.cpp:115 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 17" +msgstr "" + +#: debug_console.cpp:117 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 18" +msgstr "" + +#: debug_console.cpp:119 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 19" +msgstr "" + +#: debug_console.cpp:121 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 20" +msgstr "" + +#: debug_console.cpp:123 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 21" +msgstr "" + +#: debug_console.cpp:125 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 22" +msgstr "" + +#: debug_console.cpp:127 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 23" +msgstr "" + +#: debug_console.cpp:129 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 24" +msgstr "" + +#: debug_console.cpp:138 debug_console.cpp:140 +#, kde-format +msgid "Input Device" +msgstr "" + +#: debug_console.cpp:138 +#, kde-format +msgctxt "The input device of the event is not known" +msgid "Unknown" +msgstr "" + +#: debug_console.cpp:175 +#, kde-format +msgctxt "A mouse pointer motion event" +msgid "Pointer Motion" +msgstr "" + +#: debug_console.cpp:182 +#, kde-format +msgctxt "The relative mouse movement" +msgid "Delta" +msgstr "" + +#: debug_console.cpp:186 +#, kde-format +msgctxt "The relative mouse movement" +msgid "Delta (not accelerated)" +msgstr "" + +#: debug_console.cpp:189 +#, kde-format +msgctxt "The global mouse pointer position" +msgid "Global Position" +msgstr "" + +#: debug_console.cpp:193 +#, kde-format +msgctxt "A mouse pointer button press event" +msgid "Pointer Button Press" +msgstr "" + +#: debug_console.cpp:196 debug_console.cpp:204 +#, kde-format +msgctxt "A button in a mouse press/release event" +msgid "Button" +msgstr "" + +#: debug_console.cpp:197 debug_console.cpp:205 +#, kde-format +msgctxt "A button in a mouse press/release event" +msgid "Native Button code" +msgstr "" + +#: debug_console.cpp:198 debug_console.cpp:206 +#, kde-format +msgctxt "All currently pressed buttons in a mouse press/release event" +msgid "Pressed Buttons" +msgstr "" + +#: debug_console.cpp:201 +#, kde-format +msgctxt "A mouse pointer button release event" +msgid "Pointer Button Release" +msgstr "" + +#: debug_console.cpp:221 +#, kde-format +msgctxt "A mouse pointer axis (wheel) event" +msgid "Pointer Axis" +msgstr "" + +#: debug_console.cpp:225 +#, kde-format +msgctxt "The orientation of a pointer axis event" +msgid "Orientation" +msgstr "" + +#: debug_console.cpp:226 +#, kde-format +msgctxt "An orientation of a pointer axis event" +msgid "Horizontal" +msgstr "" + +#: debug_console.cpp:227 +#, kde-format +msgctxt "An orientation of a pointer axis event" +msgid "Vertical" +msgstr "" + +#: debug_console.cpp:228 +#, kde-format +msgctxt "The angle delta of a pointer axis event" +msgid "Delta" +msgstr "" + +#: debug_console.cpp:243 +#, kde-format +msgctxt "A key press event" +msgid "Key Press" +msgstr "" + +#: debug_console.cpp:246 +#, kde-format +msgctxt "A key release event" +msgid "Key Release" +msgstr "" + +#: debug_console.cpp:255 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Shift" +msgstr "" + +#: debug_console.cpp:259 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Control" +msgstr "" + +#: debug_console.cpp:263 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Alt" +msgstr "" + +#: debug_console.cpp:267 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Meta" +msgstr "" + +#: debug_console.cpp:271 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Keypad" +msgstr "" + +#: debug_console.cpp:275 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Group-switch" +msgstr "" + +#: debug_console.cpp:281 +#, kde-format +msgctxt "Whether the event is an automatic key repeat" +msgid "Repeat" +msgstr "" + +#: debug_console.cpp:285 +#, kde-format +msgctxt "The code as read from the input device" +msgid "Scan code" +msgstr "" + +#: debug_console.cpp:286 +#, kde-format +msgctxt "Key according to Qt" +msgid "Qt::Key code" +msgstr "" + +#: debug_console.cpp:288 +#, kde-format +msgctxt "The translated code to an Xkb symbol" +msgid "Xkb symbol" +msgstr "" + +#: debug_console.cpp:289 +#, kde-format +msgctxt "The translated code interpreted as text" +msgid "Utf8" +msgstr "" + +#: debug_console.cpp:290 +#, kde-format +msgctxt "The currently active modifiers" +msgid "Modifiers" +msgstr "" + +#: debug_console.cpp:302 +#, kde-format +msgctxt "A touch down event" +msgid "Touch down" +msgstr "" + +#: debug_console.cpp:304 debug_console.cpp:319 debug_console.cpp:334 +#, kde-format +msgctxt "The id of the touch point in the touch event" +msgid "Point identifier" +msgstr "" + +#: debug_console.cpp:305 debug_console.cpp:320 +#, kde-format +msgctxt "The global position of the touch point" +msgid "Global position" +msgstr "" + +#: debug_console.cpp:317 +#, kde-format +msgctxt "A touch motion event" +msgid "Touch Motion" +msgstr "" + +#: debug_console.cpp:332 +#, kde-format +msgctxt "A touch up event" +msgid "Touch Up" +msgstr "" + +#: debug_console.cpp:345 +#, kde-format +msgctxt "A pinch gesture is started" +msgid "Pinch start" +msgstr "" + +#: debug_console.cpp:347 +#, kde-format +msgctxt "Number of fingers in this pinch gesture" +msgid "Finger count" +msgstr "" + +#: debug_console.cpp:358 +#, kde-format +msgctxt "A pinch gesture is updated" +msgid "Pinch update" +msgstr "" + +#: debug_console.cpp:360 +#, kde-format +msgctxt "Current scale in pinch gesture" +msgid "Scale" +msgstr "" + +#: debug_console.cpp:361 +#, kde-format +msgctxt "Current angle in pinch gesture" +msgid "Angle delta" +msgstr "" + +#: debug_console.cpp:362 +#, kde-format +msgctxt "Current delta in pinch gesture" +msgid "Delta x" +msgstr "" + +#: debug_console.cpp:363 +#, kde-format +msgctxt "Current delta in pinch gesture" +msgid "Delta y" +msgstr "" + +#: debug_console.cpp:374 +#, kde-format +msgctxt "A pinch gesture ended" +msgid "Pinch end" +msgstr "" + +#: debug_console.cpp:386 +#, kde-format +msgctxt "A pinch gesture got cancelled" +msgid "Pinch cancelled" +msgstr "" + +#: debug_console.cpp:398 +#, kde-format +msgctxt "A swipe gesture is started" +msgid "Swipe start" +msgstr "" + +#: debug_console.cpp:400 +#, kde-format +msgctxt "Number of fingers in this swipe gesture" +msgid "Finger count" +msgstr "" + +#: debug_console.cpp:411 +#, kde-format +msgctxt "A swipe gesture is updated" +msgid "Swipe update" +msgstr "" + +#: debug_console.cpp:413 +#, kde-format +msgctxt "Current delta in swipe gesture" +msgid "Delta x" +msgstr "" + +#: debug_console.cpp:414 +#, kde-format +msgctxt "Current delta in swipe gesture" +msgid "Delta y" +msgstr "" + +#: debug_console.cpp:425 +#, kde-format +msgctxt "A swipe gesture ended" +msgid "Swipe end" +msgstr "" + +#: debug_console.cpp:437 +#, kde-format +msgctxt "A swipe gesture got cancelled" +msgid "Swipe cancelled" +msgstr "" + +#: debug_console.cpp:449 +#, fuzzy, kde-format +#| msgid "Switch to Screen 0" +msgctxt "A hardware switch (e.g. notebook lid) got toggled" +msgid "Switch toggled" +msgstr "Пераключыцца на экран 0" + +#: debug_console.cpp:457 +#, kde-format +msgctxt "Name of a hardware switch" +msgid "Notebook lid" +msgstr "" + +#: debug_console.cpp:459 +#, kde-format +msgctxt "Name of a hardware switch" +msgid "Tablet mode" +msgstr "" + +#: debug_console.cpp:461 +#, fuzzy, kde-format +#| msgid "Switch to Screen 0" +msgctxt "A hardware switch" +msgid "Switch" +msgstr "Пераключыцца на экран 0" + +#: debug_console.cpp:465 +#, kde-format +msgctxt "The hardware switch got turned off" +msgid "Off" +msgstr "" + +#: debug_console.cpp:468 +#, kde-format +msgctxt "The hardware switch got turned on" +msgid "On" +msgstr "" + +#: debug_console.cpp:473 +#, kde-format +msgctxt "State of a hardware switch (on/off)" +msgid "State" +msgstr "" + +#: debug_console.cpp:488 +#, kde-format +msgid "Tablet Tool" +msgstr "" + +#: debug_console.cpp:489 +#, kde-format +msgid "EventType" +msgstr "" + +#: debug_console.cpp:490 debug_console.cpp:537 debug_console.cpp:549 +#, kde-format +msgid "Position" +msgstr "" + +#: debug_console.cpp:492 +#, kde-format +msgid "Tilt" +msgstr "" + +#: debug_console.cpp:494 +#, kde-format +msgid "Rotation" +msgstr "" + +#: debug_console.cpp:495 +#, kde-format +msgid "Pressure" +msgstr "" + +#: debug_console.cpp:496 +#, fuzzy, kde-format +#| msgid "Mouse Emulation" +msgid "Buttons" +msgstr "Эмуляцыя мышы" + +#. i18n: ectx: property (title), widget (QGroupBox, modifiersBox) +#: debug_console.cpp:497 debug_console.ui:356 +#, kde-format +msgid "Modifiers" +msgstr "" + +#: debug_console.cpp:510 +#, kde-format +msgid "Tablet Tool Button" +msgstr "" + +#: debug_console.cpp:511 debug_console.cpp:526 +#, fuzzy, kde-format +#| msgid "Mouse Emulation" +msgid "Pressed Buttons" +msgstr "Эмуляцыя мышы" + +#: debug_console.cpp:525 +#, kde-format +msgid "Tablet Pad Button" +msgstr "" + +#: debug_console.cpp:535 +#, kde-format +msgid "Tablet Pad Strip" +msgstr "" + +#: debug_console.cpp:536 debug_console.cpp:548 +#, kde-format +msgid "Number" +msgstr "" + +#: debug_console.cpp:538 debug_console.cpp:550 +#, kde-format +msgid "isFinger" +msgstr "" + +#: debug_console.cpp:547 +#, kde-format +msgid "Tablet Pad Ring" +msgstr "" + +#: debug_console.cpp:735 +#, fuzzy, kde-format +#| msgid "Mouse Emulation" +msgid "No Mouse Buttons" +msgstr "Эмуляцыя мышы" + +#: debug_console.cpp:739 +#, kde-format +msgctxt "Mouse Button" +msgid "left" +msgstr "" + +#: debug_console.cpp:742 +#, fuzzy, kde-format +#| msgid "Top-right" +msgctxt "Mouse Button" +msgid "right" +msgstr "Правы верхні" + +#: debug_console.cpp:745 +#, kde-format +msgctxt "Mouse Button" +msgid "middle" +msgstr "" + +#: debug_console.cpp:748 +#, kde-format +msgctxt "Mouse Button" +msgid "back" +msgstr "" + +#: debug_console.cpp:751 +#, kde-format +msgctxt "Mouse Button" +msgid "forward" +msgstr "" + +#: debug_console.cpp:754 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 1" +msgstr "" + +#: debug_console.cpp:757 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 2" +msgstr "" + +#: debug_console.cpp:760 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 3" +msgstr "" + +#: debug_console.cpp:763 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 4" +msgstr "" + +#: debug_console.cpp:766 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 5" +msgstr "" + +#: debug_console.cpp:769 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 6" +msgstr "" + +#: debug_console.cpp:772 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 7" +msgstr "" + +#: debug_console.cpp:775 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 8" +msgstr "" + +#: debug_console.cpp:778 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 9" +msgstr "" + +#: debug_console.cpp:781 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 10" +msgstr "" + +#: debug_console.cpp:784 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 11" +msgstr "" + +#: debug_console.cpp:787 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 12" +msgstr "" + +#: debug_console.cpp:790 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 13" +msgstr "" + +#: debug_console.cpp:793 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 14" +msgstr "" + +#: debug_console.cpp:796 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 15" +msgstr "" + +#: debug_console.cpp:799 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 16" +msgstr "" + +#: debug_console.cpp:802 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 17" +msgstr "" + +#: debug_console.cpp:805 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 18" +msgstr "" + +#: debug_console.cpp:808 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 19" +msgstr "" + +#: debug_console.cpp:811 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 20" +msgstr "" + +#: debug_console.cpp:814 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 21" +msgstr "" + +#: debug_console.cpp:817 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 22" +msgstr "" + +#: debug_console.cpp:820 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 23" +msgstr "" + +#: debug_console.cpp:823 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 24" +msgstr "" + +#: debug_console.cpp:826 +#, kde-format +msgctxt "Mouse Button" +msgid "task" +msgstr "" + +#: debug_console.cpp:1176 +#, fuzzy, kde-format +#| msgid "Close Window" +msgid "X11 Client Windows" +msgstr "Закрыць акно" + +#: debug_console.cpp:1178 +#, kde-format +msgid "X11 Unmanaged Windows" +msgstr "" + +#: debug_console.cpp:1180 +#, fuzzy, kde-format +#| msgid "Shade Window" +msgid "Wayland Windows" +msgstr "Зацяніць вакно" + +#: debug_console.cpp:1182 +#, fuzzy, kde-format +#| msgid "Raise Window" +msgid "Internal Windows" +msgstr "Падняць вакно вышэй" + +#. i18n: ectx: property (text), widget (QPushButton, quitButton) +#: debug_console.ui:32 +#, kde-format +msgid "Quit Debug Console" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, windows) +#: debug_console.ui:45 +#, kde-format +msgid "Windows" +msgstr "Вокны" + +#. i18n: ectx: attribute (title), widget (QWidget, surfaces) +#: debug_console.ui:59 +#, kde-format +msgid "Surfaces" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, input) +#: debug_console.ui:69 +#, kde-format +msgid "Input Events" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, inputDevices) +#: debug_console.ui:86 +#, kde-format +msgid "Input Devices" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, tab) +#: debug_console.ui:96 +#, kde-format +msgid "OpenGL" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, noOpenGLLabel) +#: debug_console.ui:102 +#, kde-format +msgid "No OpenGL compositor running" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, driverInfoBox) +#: debug_console.ui:130 +#, kde-format +msgid "OpenGL (ES) driver information" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: debug_console.ui:136 +#, kde-format +msgid "Vendor:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: debug_console.ui:143 +#, kde-format +msgid "Renderer:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: debug_console.ui:150 +#, kde-format +msgid "Version:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: debug_console.ui:157 +#, kde-format +msgid "Shading Language Version:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: debug_console.ui:164 +#, kde-format +msgid "Driver:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: debug_console.ui:171 +#, kde-format +msgid "GPU class:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: debug_console.ui:178 +#, kde-format +msgid "OpenGL Version:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_8) +#: debug_console.ui:185 +#, kde-format +msgid "GLSL Version:" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, platformExtensionsBox) +#: debug_console.ui:251 +#, kde-format +msgid "Platform Extensions" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, glExtensionsBox) +#: debug_console.ui:267 +#, kde-format +msgid "OpenGL (ES) Extensions" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, keyboard) +#: debug_console.ui:288 +#, kde-format +msgid "Keyboard" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, layoutBox) +#: debug_console.ui:315 +#, kde-format +msgid "Keymap Layouts" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_9) +#: debug_console.ui:337 +#, fuzzy, kde-format +#| msgid "Configur&e Window Behavior..." +msgid "Current Layout:" +msgstr "Наладзіць &паводзіны акна..." + +#. i18n: ectx: property (title), widget (QGroupBox, activeModifiersBox) +#: debug_console.ui:372 +#, kde-format +msgid "Active Modifiers" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, ledsBox) +#: debug_console.ui:388 +#, kde-format +msgid "LEDs" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, activeLedsBox) +#: debug_console.ui:404 +#, kde-format +msgid "Active LEDs" +msgstr "" + +#: helpers/killer/killer.cpp:30 +#, fuzzy, kde-format +#| msgid "KDE window manager" +msgid "Window Manager" +msgstr "Кіраўнік вокнаў KDE" + +#: helpers/killer/killer.cpp:35 +#, fuzzy, kde-format +#| msgid "Caption of the window to be terminated." +msgid "PID of the application to terminate" +msgstr "Загаловак акна, якое трэба забіць." + +#: helpers/killer/killer.cpp:35 +#, kde-format +msgid "pid" +msgstr "" + +#: helpers/killer/killer.cpp:37 +#, kde-format +msgid "Hostname on which the application is running" +msgstr "" + +#: helpers/killer/killer.cpp:37 +#, kde-format +msgid "hostname" +msgstr "" + +#: helpers/killer/killer.cpp:39 +#, fuzzy, kde-format +#| msgid "Caption of the window to be terminated." +msgid "Caption of the window to be terminated" +msgstr "Загаловак акна, якое трэба забіць." + +#: helpers/killer/killer.cpp:39 +#, kde-format +msgid "caption" +msgstr "" + +#: helpers/killer/killer.cpp:41 +#, fuzzy, kde-format +#| msgid "Caption of the window to be terminated." +msgid "Name of the application to be terminated" +msgstr "Загаловак акна, якое трэба забіць." + +#: helpers/killer/killer.cpp:41 +#, kde-format +msgid "name" +msgstr "" + +#: helpers/killer/killer.cpp:43 +#, kde-format +msgid "ID of resource belonging to the application" +msgstr "" + +#: helpers/killer/killer.cpp:43 +#, kde-format +msgid "id" +msgstr "" + +#: helpers/killer/killer.cpp:45 +#, kde-format +msgid "Time of user action causing termination" +msgstr "" + +#: helpers/killer/killer.cpp:45 +#, kde-format +msgid "time" +msgstr "" + +#: helpers/killer/killer.cpp:47 +#, kde-format +msgid "KWin helper utility" +msgstr "Дапаможная утыліта KWin" + +#: helpers/killer/killer.cpp:71 +#, kde-format +msgid "This helper utility is not supposed to be called directly." +msgstr "Гэтую дапаможную праграму не трэба выклікаць уручную." + +#: helpers/killer/killer.cpp:81 +#, kde-format +msgctxt "@info" +msgid "Application \"%1\" is not responding" +msgstr "" + +#: helpers/killer/killer.cpp:83 +#, kde-kuit-format +msgctxt "@info" +msgid "" +"

You tried to close window \"%1\" from application \"%2\" (Process ID: %3) " +"but the application is not responding.

" +msgstr "" + +#: helpers/killer/killer.cpp:85 +#, kde-kuit-format +msgctxt "@info" +msgid "" +"

You tried to close window \"%1\" from application \"%2\" (Process ID: " +"%3), running on host \"%4\", but the application is not responding.

" +msgstr "" + +#: helpers/killer/killer.cpp:88 +#, kde-kuit-format +msgctxt "@info" +msgid "" +"

Do you want to terminate this application?

Terminating the " +"application will close all of its child windows. Any unsaved data will be " +"lost.

" +msgstr "" + +#: helpers/killer/killer.cpp:92 +#, kde-format +msgid "&Terminate Application %1" +msgstr "" + +#: helpers/killer/killer.cpp:93 +#, kde-format +msgid "Wait Longer" +msgstr "" + +#: keyboard_layout.cpp:110 keyboard_layout.cpp:111 +#, kde-format +msgctxt "tooltip title" +msgid "Keyboard Layout" +msgstr "" + +#: keyboard_layout.cpp:258 +#, fuzzy, kde-format +#| msgid "Configur&e Window Behavior..." +msgid "Configure Layouts..." +msgstr "Наладзіць &паводзіны акна..." + +#: killwindow.cpp:33 +#, kde-format +msgid "" +"Select window to force close with left click or enter.\n" +"Escape or right click to cancel." +msgstr "" + +#: kwinbindings.cpp:38 +#, kde-format +msgid "Window Operations Menu" +msgstr "Меню дзеянняў з вокнамі" + +#: kwinbindings.cpp:40 +#, kde-format +msgid "Close Window" +msgstr "Закрыць акно" + +#: kwinbindings.cpp:42 +#, kde-format +msgid "Maximize Window" +msgstr "Найбольшыць вакно" + +#: kwinbindings.cpp:44 +#, kde-format +msgid "Maximize Window Vertically" +msgstr "Найбольшыць вакно па вертыкалі" + +#: kwinbindings.cpp:46 +#, kde-format +msgid "Maximize Window Horizontally" +msgstr "Найбольшыць вакно па гарызанталі" + +#: kwinbindings.cpp:48 +#, kde-format +msgid "Minimize Window" +msgstr "Найменшыць вакно" + +#: kwinbindings.cpp:50 +#, kde-format +msgid "Shade Window" +msgstr "Зацяніць вакно" + +#: kwinbindings.cpp:52 +#, kde-format +msgid "Move Window" +msgstr "Перасунуць вакно" + +#: kwinbindings.cpp:54 +#, kde-format +msgid "Resize Window" +msgstr "Змяніць памер вакна" + +#: kwinbindings.cpp:56 +#, kde-format +msgid "Raise Window" +msgstr "Падняць вакно вышэй" + +#: kwinbindings.cpp:58 +#, kde-format +msgid "Lower Window" +msgstr "Апусціць вакно ніжэй" + +#: kwinbindings.cpp:60 +#, kde-format +msgid "Toggle Window Raise/Lower" +msgstr "Перанос на задні/пярэдні план" + +#: kwinbindings.cpp:62 +#, kde-format +msgid "Make Window Fullscreen" +msgstr "Разгарнуць вакно ў поўнаэкранным рэжыме" + +#: kwinbindings.cpp:64 +#, kde-format +msgid "Hide Window Border" +msgstr "Схаваць межы вакна" + +#: kwinbindings.cpp:66 +#, kde-format +msgid "Keep Window Above Others" +msgstr "Трымаць вакно вышэй астатніх" + +#: kwinbindings.cpp:68 +#, kde-format +msgid "Keep Window Below Others" +msgstr "Трымаць вакно ніжэй астатніх" + +#: kwinbindings.cpp:70 +#, kde-format +msgid "Activate Window Demanding Attention" +msgstr "Актывізаваць вакно, якое патрабуе ўвагі" + +#: kwinbindings.cpp:72 +#, kde-format +msgid "Setup Window Shortcut" +msgstr "Усталяваць скарот для вакна" + +#: kwinbindings.cpp:74 +#, kde-format +msgid "Pack Window to the Right" +msgstr "Згрувапаць вокны ўправа" + +#: kwinbindings.cpp:76 +#, kde-format +msgid "Pack Window to the Left" +msgstr "Згрувапаць вокны ўлева" + +#: kwinbindings.cpp:78 +#, kde-format +msgid "Pack Window Up" +msgstr "Згрувапаць вокны ўверсе" + +#: kwinbindings.cpp:80 +#, kde-format +msgid "Pack Window Down" +msgstr "Згрувапаць вокны ўнізе" + +#: kwinbindings.cpp:82 +#, kde-format +msgid "Pack Grow Window Horizontally" +msgstr "" + +#: kwinbindings.cpp:84 +#, kde-format +msgid "Pack Grow Window Vertically" +msgstr "" + +#: kwinbindings.cpp:86 +#, kde-format +msgid "Pack Shrink Window Horizontally" +msgstr "" + +#: kwinbindings.cpp:88 +#, kde-format +msgid "Pack Shrink Window Vertically" +msgstr "" + +#: kwinbindings.cpp:90 +#, fuzzy, kde-format +#| msgid "Pack Window to the Left" +msgid "Quick Tile Window to the Left" +msgstr "Згрувапаць вокны ўлева" + +#: kwinbindings.cpp:92 +#, fuzzy, kde-format +#| msgid "Pack Window to the Right" +msgid "Quick Tile Window to the Right" +msgstr "Згрувапаць вокны ўправа" + +#: kwinbindings.cpp:94 +#, fuzzy, kde-format +#| msgid "Pack Window to the Left" +msgid "Quick Tile Window to the Top" +msgstr "Згрувапаць вокны ўлева" + +#: kwinbindings.cpp:96 +#, fuzzy, kde-format +#| msgid "Pack Window to the Left" +msgid "Quick Tile Window to the Bottom" +msgstr "Згрувапаць вокны ўлева" + +#: kwinbindings.cpp:98 +#, fuzzy, kde-format +#| msgid "Pack Window to the Left" +msgid "Quick Tile Window to the Top Left" +msgstr "Згрувапаць вокны ўлева" + +#: kwinbindings.cpp:100 +#, fuzzy, kde-format +#| msgid "Pack Window to the Left" +msgid "Quick Tile Window to the Bottom Left" +msgstr "Згрувапаць вокны ўлева" + +#: kwinbindings.cpp:102 +#, fuzzy, kde-format +#| msgid "Pack Window to the Right" +msgid "Quick Tile Window to the Top Right" +msgstr "Згрувапаць вокны ўправа" + +#: kwinbindings.cpp:104 +#, fuzzy, kde-format +#| msgid "Pack Window to the Right" +msgid "Quick Tile Window to the Bottom Right" +msgstr "Згрувапаць вокны ўправа" + +#: kwinbindings.cpp:106 +#, fuzzy, kde-format +#| msgid "Switch to Screen 0" +msgid "Switch to Window Above" +msgstr "Пераключыцца на экран 0" + +#: kwinbindings.cpp:108 +#, fuzzy, kde-format +#| msgid "Switch to Previous Desktop" +msgid "Switch to Window Below" +msgstr "Пераключыцца на папярэдні працоўны стол" + +#: kwinbindings.cpp:110 +#, fuzzy, kde-format +#| msgid "Pack Window to the Right" +msgid "Switch to Window to the Right" +msgstr "Згрувапаць вокны ўправа" + +#: kwinbindings.cpp:112 +#, fuzzy, kde-format +#| msgid "Pack Window to the Left" +msgid "Switch to Window to the Left" +msgstr "Згрувапаць вокны ўлева" + +#: kwinbindings.cpp:114 +#, kde-format +msgid "Increase Opacity of Active Window by 5 %" +msgstr "" + +#: kwinbindings.cpp:116 +#, kde-format +msgid "Decrease Opacity of Active Window by 5 %" +msgstr "" + +#: kwinbindings.cpp:119 +#, kde-format +msgid "Keep Window on All Desktops" +msgstr "Трымаць вакно на ўсіх працоўных сталах" + +#: kwinbindings.cpp:123 +#, fuzzy, kde-format +#| msgid "Window to Desktop 1" +msgid "Window to Desktop %1" +msgstr "Акно на працоўны стол 1" + +#: kwinbindings.cpp:125 +#, kde-format +msgid "Window to Next Desktop" +msgstr "Акно на наступны працоўны стол" + +#: kwinbindings.cpp:126 +#, kde-format +msgid "Window to Previous Desktop" +msgstr "Акно на папярэдні працоўны стол" + +#: kwinbindings.cpp:127 +#, kde-format +msgid "Window One Desktop to the Right" +msgstr "Акно на працоўны стол справа" + +#: kwinbindings.cpp:128 +#, kde-format +msgid "Window One Desktop to the Left" +msgstr "Акно на працоўны стол злева" + +#: kwinbindings.cpp:129 +#, kde-format +msgid "Window One Desktop Up" +msgstr "Акно на вышэйшы працоўны стол" + +#: kwinbindings.cpp:130 +#, kde-format +msgid "Window One Desktop Down" +msgstr "Акно на ніжэйшы працоўны стол" + +#: kwinbindings.cpp:133 +#, fuzzy, kde-format +#| msgid "Window to Screen 1" +msgid "Window to Screen %1" +msgstr "Акно на экран 1" + +#: kwinbindings.cpp:135 +#, kde-format +msgid "Window to Next Screen" +msgstr "Акно на наступны экран" + +#: kwinbindings.cpp:136 +#, fuzzy, kde-format +#| msgid "Window to Previous Desktop" +msgid "Window to Previous Screen" +msgstr "Акно на папярэдні працоўны стол" + +#: kwinbindings.cpp:137 +#, fuzzy, kde-format +#| msgid "Show Desktop Grid" +msgid "Show Desktop" +msgstr "Паказваць сетку працоўнага стала" + +#: kwinbindings.cpp:140 +#, fuzzy, kde-format +#| msgid "Switch to Screen 1" +msgid "Switch to Screen %1" +msgstr "Пераключыцца на экран 1" + +#: kwinbindings.cpp:143 +#, kde-format +msgid "Switch to Next Screen" +msgstr "Пераключыцца на наступны экран" + +#: kwinbindings.cpp:144 +#, fuzzy, kde-format +#| msgid "Switch to Previous Desktop" +msgid "Switch to Previous Screen" +msgstr "Пераключыцца на папярэдні працоўны стол" + +#: kwinbindings.cpp:146 +#, kde-format +msgid "Kill Window" +msgstr "Забіцьв акно" + +#: kwinbindings.cpp:147 +#, kde-format +msgid "Suspend Compositing" +msgstr "" + +#: kwinbindings.cpp:148 +#, kde-format +msgid "Invert Screen Colors" +msgstr "" + +#: main.cpp:184 main.cpp:214 +#, kde-format +msgid "KDE window manager" +msgstr "Кіраўнік вокнаў KDE" + +#: main.cpp:189 +#, kde-format +msgid "KWin" +msgstr "KWin" + +#: main.cpp:193 +#, fuzzy, kde-format +#| msgid "(c) 1999-2005, The KDE Developers" +msgid "(c) 1999-2019, The KDE Developers" +msgstr "(c) 1999-2005, Распрацоўшчыкі KDE" + +#: main.cpp:195 +#, kde-format +msgid "Matthias Ettrich" +msgstr "Матыяс Эттрых" + +#: main.cpp:196 +#, kde-format +msgid "Cristian Tibirna" +msgstr "Крысціян Цібірна" + +#: main.cpp:197 +#, kde-format +msgid "Daniel M. Duley" +msgstr "Даніэль М. Дулі" + +#: main.cpp:198 +#, kde-format +msgid "Luboš Luňák" +msgstr "Любаш Люнак" + +#: main.cpp:199 +#, kde-format +msgid "Martin Flöser" +msgstr "" + +#: main.cpp:200 +#, kde-format +msgid "David Edmundson" +msgstr "" + +#: main.cpp:201 +#, kde-format +msgid "Roman Gilg" +msgstr "" + +#: main.cpp:202 +#, kde-format +msgid "Vlad Zahorodnii" +msgstr "" + +#: main.cpp:211 +#, kde-format +msgid "Disable configuration options" +msgstr "Выключыць параметры наладкі" + +#: main.cpp:212 +#, kde-format +msgid "Indicate that KWin has recently crashed n times" +msgstr "" + +#: main_wayland.cpp:459 +#, kde-format +msgid "Start a rootless Xwayland server." +msgstr "" + +#: main_wayland.cpp:461 +#, kde-format +msgid "" +"Name of the Wayland socket to listen on. If not set \"wayland-0\" is used." +msgstr "" + +#: main_wayland.cpp:464 +#, kde-format +msgid "Render to framebuffer." +msgstr "" + +#: main_wayland.cpp:466 +#, kde-format +msgid "The framebuffer device to render to." +msgstr "" + +#: main_wayland.cpp:469 +#, kde-format +msgid "The X11 Display to use in windowed mode on platform X11." +msgstr "" + +#: main_wayland.cpp:472 +#, kde-format +msgid "The Wayland Display to use in windowed mode on platform Wayland." +msgstr "" + +#: main_wayland.cpp:474 +#, kde-format +msgid "Render to a virtual framebuffer." +msgstr "" + +#: main_wayland.cpp:476 +#, kde-format +msgid "The width for windowed mode. Default width is 1024." +msgstr "" + +#: main_wayland.cpp:480 +#, kde-format +msgid "The height for windowed mode. Default height is 768." +msgstr "" + +#: main_wayland.cpp:485 +#, kde-format +msgid "The scale for windowed mode. Default value is 1." +msgstr "" + +#: main_wayland.cpp:490 +#, kde-format +msgid "" +"The number of windows to open as outputs in windowed mode. Default value is 1" +msgstr "" + +#: main_wayland.cpp:520 +#, kde-format +msgid "Use libhybris hwcomposer" +msgstr "" + +#: main_wayland.cpp:526 +#, kde-format +msgid "" +"Enable libinput support for input events processing. Note: never use in a " +"nested session.\t(deprecated)" +msgstr "" + +#: main_wayland.cpp:529 +#, kde-format +msgid "Render through drm node." +msgstr "" + +#: main_wayland.cpp:536 +#, kde-format +msgid "Input method that KWin starts." +msgstr "" + +#: main_wayland.cpp:541 +#, kde-format +msgid "List all available backends and quit." +msgstr "" + +#: main_wayland.cpp:545 +#, kde-format +msgid "Starts the session in locked mode." +msgstr "" + +#: main_wayland.cpp:549 +#, kde-format +msgid "Starts the session without lock screen support." +msgstr "" + +#: main_wayland.cpp:553 +#, kde-format +msgid "Starts the session without global shortcuts support." +msgstr "" + +#: main_wayland.cpp:557 +#, kde-format +msgid "Exit after the session application, which is started by KWin, closed." +msgstr "" + +#: main_wayland.cpp:562 +#, kde-format +msgid "Applications to start once Wayland and Xwayland server are started" +msgstr "" + +#: main_x11.cpp:65 +#, kde-format +msgid "" +"KWin is unstable.\n" +"It seems to have crashed several times in a row.\n" +"You can select another window manager to run:" +msgstr "" + +#: main_x11.cpp:224 +#, kde-format +msgid "" +"kwin: unable to claim manager selection, another wm running? (try using --" +"replace)\n" +msgstr "" +"kwin: немагчыма стаць кіраўніком вокнаў, запушчаны іншы wm? (паспрабуйце --" +"replace)\n" + +#: main_x11.cpp:241 +#, fuzzy, kde-format +#| msgid "" +#| "kwin: unable to claim manager selection, another wm running? (try using --" +#| "replace)\n" +msgid "kwin: another window manager is running (try using --replace)\n" +msgstr "" +"kwin: немагчыма стаць кіраўніком вокнаў, запушчаны іншы wm? (паспрабуйце --" +"replace)\n" + +#: main_x11.cpp:437 +#, kde-format +msgid "Replace already-running ICCCM2.0-compliant window manager" +msgstr "Замяніць ужо запушчаны кіраўнік вокнаў, сумяшчальны з ICCCM2.0" + +#: main_x11.cpp:444 +#, kde-format +msgid "Disable KActivities integration." +msgstr "" + +#: plugins/scenes/opengl/scene_opengl.cpp:535 +#, kde-format +msgid "Desktop effects were restarted due to a graphics reset" +msgstr "" + +#. i18n: ectx: label, entry (count), group (General) +#: rulebooksettingsbase.kcfg:9 +#, kde-format +msgid "Total rules count" +msgstr "" + +#. i18n: ectx: label, entry (description), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:10 +#, kde-format +msgid "Rule description" +msgstr "" + +#. i18n: ectx: label, entry (descriptionLegacy), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:13 +#, kde-format +msgid "Rule description (legacy)" +msgstr "" + +#. i18n: ectx: label, entry (DeleteRule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:16 +#, kde-format +msgid "Delete this rule (for use in imports)" +msgstr "" + +#. i18n: ectx: label, entry (wmclass), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:20 +#, kde-format +msgid "Window class (application)" +msgstr "" + +#. i18n: ectx: label, entry (wmclassmatch), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:23 +#, kde-format +msgid "Window class string match type" +msgstr "" + +#. i18n: ectx: label, entry (wmclasscomplete), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:29 +#, kde-format +msgid "Match whole window class" +msgstr "" + +#. i18n: ectx: label, entry (windowrole), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:34 +#, fuzzy, kde-format +#| msgid "Windows" +msgid "Window role" +msgstr "Вокны" + +#. i18n: ectx: label, entry (windowrolematch), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:37 +#, kde-format +msgid "Window role string match type" +msgstr "" + +#. i18n: ectx: label, entry (title), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:44 +#, fuzzy, kde-format +#| msgid "Windows" +msgid "Window title" +msgstr "Вокны" + +#. i18n: ectx: label, entry (titlematch), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:47 +#, kde-format +msgid "Window title string match type" +msgstr "" + +#. i18n: ectx: label, entry (clientmachine), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:54 +#, kde-format +msgid "Machine (hostname)" +msgstr "" + +#. i18n: ectx: label, entry (clientmachinematch), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:57 +#, kde-format +msgid "Machine string match type" +msgstr "" + +#. i18n: ectx: label, entry (types), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:64 +#, kde-format +msgid "Window types that match" +msgstr "" + +#. i18n: ectx: label, entry (placement), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:69 +#, kde-format +msgid "Initial placement" +msgstr "" + +#. i18n: ectx: label, entry (placementrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:74 +#, fuzzy, kde-format +#| msgid "&Fullscreen" +msgid "Initial placement rule type" +msgstr "&Поўнаэкранны рэжым" + +#. i18n: ectx: label, entry (position), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:79 +#, fuzzy, kde-format +#| msgid "Window Operations Menu" +msgid "Window position" +msgstr "Меню дзеянняў з вокнамі" + +#. i18n: ectx: label, entry (positionrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:83 +#, fuzzy, kde-format +#| msgid "Window to Screen 0" +msgid "Window position rule type" +msgstr "Акно на экран 0" + +#. i18n: ectx: label, entry (size), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:90 +#, fuzzy, kde-format +#| msgid "Windows" +msgid "Window size" +msgstr "Вокны" + +#. i18n: ectx: label, entry (sizerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:93 +#, kde-format +msgid "Window size rule type" +msgstr "" + +#. i18n: ectx: label, entry (minsize), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:100 +#, kde-format +msgid "Window minimum size" +msgstr "" + +#. i18n: ectx: label, entry (minsizerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:104 +#, kde-format +msgid "Window minimum size rule type" +msgstr "" + +#. i18n: ectx: label, entry (maxsize), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:109 +#, kde-format +msgid "Window maximum size" +msgstr "" + +#. i18n: ectx: label, entry (maxsizerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:113 +#, kde-format +msgid "Window maximum size rule type" +msgstr "" + +#. i18n: ectx: label, entry (opacityactive), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:118 +#, kde-format +msgid "Active opacity" +msgstr "" + +#. i18n: ectx: label, entry (opacityactiverule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:124 +#, kde-format +msgid "Active opacity rule type" +msgstr "" + +#. i18n: ectx: label, entry (opacityinactive), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:129 +#, kde-format +msgid "Inactive opacity" +msgstr "" + +#. i18n: ectx: label, entry (opacityinactiverule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:135 +#, kde-format +msgid "Inactive opacity rule type" +msgstr "" + +#. i18n: ectx: label, entry (ignoregeometry), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:140 +#, kde-format +msgid "Ignore requested geometry" +msgstr "" + +#. i18n: ectx: label, entry (ignoregeometryrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:144 +#, kde-format +msgid "Ignore requested geometry rule type" +msgstr "" + +#. i18n: ectx: label, entry (desktop), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:151 +#, fuzzy, kde-format +#| msgid "Desktop %1" +msgid "Desktop number" +msgstr "Працоўны стол %1" + +#. i18n: ectx: label, entry (desktoprule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:155 +#, fuzzy, kde-format +#| msgid "Desktop %1" +msgid "Desktop number rule type" +msgstr "Працоўны стол %1" + +#. i18n: ectx: label, entry (screen), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:162 +#, kde-format +msgid "Screen number" +msgstr "" + +#. i18n: ectx: label, entry (screenrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:166 +#, kde-format +msgid "Screen number rule type" +msgstr "" + +#. i18n: ectx: label, entry (activity), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:173 +#, kde-format +msgid "Activity" +msgstr "" + +#. i18n: ectx: label, entry (activityrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:176 +#, kde-format +msgid "Activity rule type" +msgstr "" + +#. i18n: ectx: label, entry (type), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:183 +#, fuzzy, kde-format +#| msgid "Setup Window Shortcut" +msgid "Set window type to" +msgstr "Усталяваць скарот для вакна" + +#. i18n: ectx: label, entry (typerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:189 +#, kde-format +msgid "Set window type rule type" +msgstr "" + +#. i18n: ectx: label, entry (maximizevert), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:194 +#, fuzzy, kde-format +#| msgid "Maximize Window Vertically" +msgid "Maximized vertically" +msgstr "Найбольшыць вакно па вертыкалі" + +#. i18n: ectx: label, entry (maximizevertrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:198 +#, fuzzy, kde-format +#| msgid "Maximize Window Vertically" +msgid "Maximized vertically rule type" +msgstr "Найбольшыць вакно па вертыкалі" + +#. i18n: ectx: label, entry (maximizehoriz), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:205 +#, fuzzy, kde-format +#| msgid "Maximize Window Horizontally" +msgid "Maximized horizontally" +msgstr "Найбольшыць вакно па гарызанталі" + +#. i18n: ectx: label, entry (maximizehorizrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:209 +#, fuzzy, kde-format +#| msgid "Maximize Window Horizontally" +msgid "Maximized horizontally rule type" +msgstr "Найбольшыць вакно па гарызанталі" + +#. i18n: ectx: label, entry (minimize), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:216 +#, fuzzy, kde-format +#| msgid "Minimize" +msgid "Minimized" +msgstr "Найменшыць" + +#. i18n: ectx: label, entry (minimizerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:220 +#, kde-format +msgid "Minimized rule type" +msgstr "" + +#. i18n: ectx: label, entry (shade), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:227 +#, fuzzy, kde-format +#| msgid "Shade" +msgid "Shaded" +msgstr "Зацяніць" + +#. i18n: ectx: label, entry (shaderule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:231 +#, kde-format +msgid "Shaded rule type" +msgstr "" + +#. i18n: ectx: label, entry (skiptaskbar), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:238 +#, kde-format +msgid "Skip taskbar" +msgstr "" + +#. i18n: ectx: label, entry (skiptaskbarrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:242 +#, kde-format +msgid "Skip taskbar rule type" +msgstr "" + +#. i18n: ectx: label, entry (skippager), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:249 +#, kde-format +msgid "Skip pager" +msgstr "" + +#. i18n: ectx: label, entry (skippagerrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:253 +#, kde-format +msgid "Skip pager rule type" +msgstr "" + +#. i18n: ectx: label, entry (skipswitcher), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:260 +#, fuzzy, kde-format +#| msgid "Switch to Screen 0" +msgid "Skip switcher" +msgstr "Пераключыцца на экран 0" + +#. i18n: ectx: label, entry (skipswitcherrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:264 +#, kde-format +msgid "Skip switcher rule type" +msgstr "" + +#. i18n: ectx: label, entry (above), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:271 +#, fuzzy, kde-format +#| msgid "Keep above others" +msgid "Keep above" +msgstr "Трымаць вышэй астатніх" + +#. i18n: ectx: label, entry (aboverule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:275 +#, fuzzy, kde-format +#| msgid "Keep above others" +msgid "Keep above rule type" +msgstr "Трымаць вышэй астатніх" + +#. i18n: ectx: label, entry (below), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:282 +#, fuzzy, kde-format +#| msgid "Keep below others" +msgid "Keep below" +msgstr "Трымаць ніжэй астатніх" + +#. i18n: ectx: label, entry (belowrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:286 +#, fuzzy, kde-format +#| msgid "Keep below others" +msgid "Keep below rule type" +msgstr "Трымаць ніжэй астатніх" + +#. i18n: ectx: label, entry (fullscreen), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:293 +#, fuzzy, kde-format +#| msgid "&Fullscreen" +msgid "Fullscreen" +msgstr "&Поўнаэкранны рэжым" + +#. i18n: ectx: label, entry (fullscreenrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:297 +#, fuzzy, kde-format +#| msgid "&Fullscreen" +msgid "Fullscreen rule type" +msgstr "&Поўнаэкранны рэжым" + +#. i18n: ectx: label, entry (noborder), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:304 +#, kde-format +msgid "No titlebar and frame" +msgstr "" + +#. i18n: ectx: label, entry (noborderrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:308 +#, kde-format +msgid "No titlebar rule type" +msgstr "" + +#. i18n: ectx: label, entry (decocolor), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:315 +#, kde-format +msgid "Titlebar color and scheme" +msgstr "" + +#. i18n: ectx: label, entry (decocolorrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:318 +#, kde-format +msgid "Titlebar color rule type" +msgstr "" + +#. i18n: ectx: label, entry (blockcompositing), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:323 +#, kde-format +msgid "Block Compositing" +msgstr "" + +#. i18n: ectx: label, entry (blockcompositingrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:327 +#, kde-format +msgid "Block Compositing rule type" +msgstr "" + +#. i18n: ectx: label, entry (fsplevel), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:332 +#, kde-format +msgid "Focus stealing prevention" +msgstr "" + +#. i18n: ectx: label, entry (fsplevelrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:338 +#, kde-format +msgid "Focus stealing prevention rule type" +msgstr "" + +#. i18n: ectx: label, entry (fpplevel), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:343 +#, kde-format +msgid "Focus protection" +msgstr "" + +#. i18n: ectx: label, entry (fpplevelrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:349 +#, kde-format +msgid "Focus protection rule type" +msgstr "" + +#. i18n: ectx: label, entry (acceptfocus), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:354 +#, kde-format +msgid "Accept Focus" +msgstr "" + +#. i18n: ectx: label, entry (acceptfocusrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:358 +#, kde-format +msgid "Accept Focus rule type" +msgstr "" + +#. i18n: ectx: label, entry (closeable), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:363 +#, fuzzy, kde-format +#| msgid "Close" +msgid "Closeable" +msgstr "Закрыць" + +#. i18n: ectx: label, entry (closeablerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:367 +#, kde-format +msgid "Closeable rule type" +msgstr "" + +#. i18n: ectx: label, entry (autogroup), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:372 +#, kde-format +msgid "Autogroup with identical" +msgstr "" + +#. i18n: ectx: label, entry (autogrouprule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:376 +#, kde-format +msgid "Autogroup with identical rule type" +msgstr "" + +#. i18n: ectx: label, entry (autogroupfg), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:381 +#, kde-format +msgid "Autogroup in foreground" +msgstr "" + +#. i18n: ectx: label, entry (autogroupfgrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:385 +#, kde-format +msgid "Autogroup in foreground rule type" +msgstr "" + +#. i18n: ectx: label, entry (autogroupid), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:390 +#, kde-format +msgid "Autogroup by ID" +msgstr "" + +#. i18n: ectx: label, entry (autogroupidrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:393 +#, kde-format +msgid "Autogroup by ID rule type" +msgstr "" + +#. i18n: ectx: label, entry (strictgeometry), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:398 +#, kde-format +msgid "Obey geometry restrictions" +msgstr "" + +#. i18n: ectx: label, entry (strictgeometryrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:402 +#, kde-format +msgid "Obey geometry restrictions rule type" +msgstr "" + +#. i18n: ectx: label, entry (shortcut), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:407 +#, kde-format +msgid "Shortcut" +msgstr "" + +#. i18n: ectx: label, entry (shortcutrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:410 +#, kde-format +msgid "Shortcut rule type" +msgstr "" + +#. i18n: ectx: label, entry (disableglobalshortcuts), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:417 +#, fuzzy, kde-format +#| msgid "Block Global Shortcuts" +msgid "Ignore global shortcuts" +msgstr "Блакаваць глабальныя скароты" + +#. i18n: ectx: label, entry (disableglobalshortcutsrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:421 +#, kde-format +msgid "Ignore global shortcuts rule type" +msgstr "" + +#. i18n: ectx: label, entry (desktopfile), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:426 +#, fuzzy, kde-format +#| msgid "Desktop %1" +msgid "Desktop file name" +msgstr "Працоўны стол %1" + +#. i18n: ectx: label, entry (desktopfilerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:429 +#, kde-format +msgid "Desktop file name rule type" +msgstr "" + +#: scripting/genericscriptedconfig.cpp:70 +#, kde-format +msgctxt "Error message" +msgid "Plugin does not provide configuration file in expected location" +msgstr "" + +#: scripting/scripting.cpp:117 +#, kde-format +msgctxt "Assertion failed in KWin script with given value" +msgid "Assertion failed: %1 is not null" +msgstr "" + +#: scripting/scripting.cpp:135 +#, kde-format +msgctxt "Assertion failed in KWin script" +msgid "Assertion failed: argument is null" +msgstr "" + +#: scripting/scripting.cpp:177 +#, kde-format +msgctxt "Error in KWin Script" +msgid "" +"Invalid number of arguments. At least service, path, interface and method " +"need to be provided" +msgstr "" + +#: scripting/scripting.cpp:183 +#, kde-format +msgctxt "Error in KWin Script" +msgid "" +"Invalid type. Service, path, interface and method need to be string values" +msgstr "" + +#: scripting/scriptingutils.cpp:17 +#, kde-format +msgctxt "syntax error in KWin script" +msgid "Invalid number of arguments" +msgstr "" + +#: scripting/scriptingutils.cpp:30 +#, kde-format +msgctxt "KWin Scripting function received incorrect value for an expected type" +msgid "%1 is not a variant type" +msgstr "" + +#. i18n: ectx: property (windowTitle), widget (QDialog, ShortcutDialog) +#: shortcutdialog.ui:14 +#, kde-format +msgid "Dialog" +msgstr "" + +#. i18n: ectx: property (text), widget (QToolButton, clearButton) +#: shortcutdialog.ui:25 +#, kde-format +msgid "..." +msgstr "" + +#: tabbox/tabbox.cpp:372 +#, fuzzy, kde-format +#| msgid "Show Desktop Grid" +msgctxt "Special entry in alt+tab list for minimizing all windows" +msgid "Show Desktop" +msgstr "Паказваць сетку працоўнага стала" + +#: tabbox/tabbox.cpp:521 +#, kde-format +msgid "Walk Through Windows" +msgstr "Пераход па вокнах" + +#: tabbox/tabbox.cpp:522 +#, kde-format +msgid "Walk Through Windows (Reverse)" +msgstr "Адваротны пераход па вокнах" + +#: tabbox/tabbox.cpp:523 +#, fuzzy, kde-format +#| msgid "Walk Through Windows (Reverse)" +msgid "Walk Through Windows Alternative" +msgstr "Адваротны пераход па вокнах" + +#: tabbox/tabbox.cpp:524 +#, fuzzy, kde-format +#| msgid "Walk Through Windows (Reverse)" +msgid "Walk Through Windows Alternative (Reverse)" +msgstr "Адваротны пераход па вокнах" + +#: tabbox/tabbox.cpp:525 +#, fuzzy, kde-format +#| msgid "Walk Through Windows (Reverse)" +msgid "Walk Through Windows of Current Application" +msgstr "Адваротны пераход па вокнах" + +#: tabbox/tabbox.cpp:526 +#, fuzzy, kde-format +#| msgid "Walk Through Windows (Reverse)" +msgid "Walk Through Windows of Current Application (Reverse)" +msgstr "Адваротны пераход па вокнах" + +#: tabbox/tabbox.cpp:527 +#, fuzzy, kde-format +#| msgid "Walk Through Windows (Reverse)" +msgid "Walk Through Windows of Current Application Alternative" +msgstr "Адваротны пераход па вокнах" + +#: tabbox/tabbox.cpp:528 +#, fuzzy, kde-format +#| msgid "Walk Through Windows (Reverse)" +msgid "Walk Through Windows of Current Application Alternative (Reverse)" +msgstr "Адваротны пераход па вокнах" + +#: tabbox/tabbox.cpp:529 +#, kde-format +msgid "Walk Through Desktops" +msgstr "Пераход па працоўных сталах" + +#: tabbox/tabbox.cpp:530 +#, kde-format +msgid "Walk Through Desktops (Reverse)" +msgstr "Адваротны пераход па працоўных сталах" + +#: tabbox/tabbox.cpp:531 +#, kde-format +msgid "Walk Through Desktop List" +msgstr "Пераход па спісе працоўных сталоў" + +#: tabbox/tabbox.cpp:532 +#, kde-format +msgid "Walk Through Desktop List (Reverse)" +msgstr "Адваротны пераход па спісе працоўных сталоў" + +#: tabbox/tabboxhandler.cpp:272 +#, kde-format +msgid "" +"The Window Switcher installation is broken, resources are missing.\n" +"Contact your distribution about this." +msgstr "" + +#: useractions.cpp:167 +#, kde-format +msgid "" +"You have selected to show a window without its border.\n" +"Without the border, you will not be able to enable the border again using " +"the mouse: use the window operations menu instead, activated using the %1 " +"keyboard shortcut." +msgstr "" + +#: useractions.cpp:175 +#, kde-format +msgid "" +"You have selected to show a window in fullscreen mode.\n" +"If the application itself does not have an option to turn the fullscreen " +"mode off you will not be able to disable it again using the mouse: use the " +"window operations menu instead, activated using the %1 keyboard shortcut." +msgstr "" + +#: useractions.cpp:240 +#, kde-format +msgid "&Move" +msgstr "&Перасунуць" + +#: useractions.cpp:245 +#, fuzzy, kde-format +#| msgid "Re&size" +msgid "&Resize" +msgstr "Змяніць &памер" + +#: useractions.cpp:250 +#, kde-format +msgid "Keep &Above Others" +msgstr "Трымаць &вышэй астатніх" + +#: useractions.cpp:256 +#, kde-format +msgid "Keep &Below Others" +msgstr "Трымаць &ніжэй астатніх" + +#: useractions.cpp:262 +#, kde-format +msgid "&Fullscreen" +msgstr "&Поўнаэкранны рэжым" + +#: useractions.cpp:268 +#, fuzzy, kde-format +#| msgid "Shade" +msgid "&Shade" +msgstr "Зацяніць" + +#: useractions.cpp:274 +#, kde-format +msgid "&No Border" +msgstr "&Без межаў" + +#: useractions.cpp:282 +#, fuzzy, kde-format +#| msgid "Window &Shortcut..." +msgid "Set Window Short&cut..." +msgstr "Скарот &акна..." + +#: useractions.cpp:287 +#, fuzzy, kde-format +#| msgid "&Special Window Settings..." +msgid "Configure Special &Window Settings..." +msgstr "&Спецыяльныя наладкі акна..." + +#: useractions.cpp:292 +#, fuzzy, kde-format +#| msgid "&Special Application Settings..." +msgid "Configure S&pecial Application Settings..." +msgstr "&Спецыяльныя наладкі праграмы..." + +#: useractions.cpp:300 +#, fuzzy, kde-format +#| msgid "KDE window manager" +msgctxt "" +"Entry in context menu of window decoration to open the configuration module " +"of KWin" +msgid "Configure W&indow Manager..." +msgstr "Кіраўнік вокнаў KDE" + +#: useractions.cpp:328 +#, kde-format +msgid "Ma&ximize" +msgstr "Най&большыць" + +#: useractions.cpp:334 +#, kde-format +msgid "Mi&nimize" +msgstr "Най&меншыць" + +#: useractions.cpp:340 +#, kde-format +msgid "&More Actions" +msgstr "" + +#: useractions.cpp:343 +#, kde-format +msgid "&Close" +msgstr "За&крыць" + +#: useractions.cpp:410 +#, kde-format +msgid "&Extensions" +msgstr "" + +#: useractions.cpp:451 +#, fuzzy, kde-format +#| msgid "&All Desktops" +msgid "&Desktops" +msgstr "&Усе працоўныя сталы" + +#: useractions.cpp:465 +#, fuzzy, kde-format +#| msgid "To &Desktop" +msgid "Move to &Desktop" +msgstr "На &працоўны стол" + +#: useractions.cpp:483 +#, fuzzy, kde-format +#| msgid "To &Desktop" +msgid "Move to &Screen" +msgstr "На &працоўны стол" + +#: useractions.cpp:499 +#, kde-format +msgid "Show in &Activities" +msgstr "" + +#: useractions.cpp:514 useractions.cpp:559 +#, kde-format +msgid "&All Desktops" +msgstr "&Усе працоўныя сталы" + +#: useractions.cpp:542 useractions.cpp:595 +#, fuzzy, kde-format +#| msgid "Show Desktop Grid" +msgctxt "Create a new desktop and move there the window" +msgid "&New Desktop" +msgstr "Паказваць сетку працоўнага стала" + +#: useractions.cpp:618 +#, fuzzy, kde-format +#| msgid "Window to Screen 1" +msgctxt "" +"@item:inmenu List of all Screens to send a window to. First argument is a " +"number, second the output identifier. E.g. Screen 1 (HDMI1)" +msgid "Screen &%1 (%2)" +msgstr "Акно на экран 1" + +#: useractions.cpp:641 +#, kde-format +msgid "&All Activities" +msgstr "" + +#: useractions.cpp:887 +#, kde-format +msgctxt "'%1' is a keyboard shortcut like 'ctrl+w'" +msgid "%1 is already in use" +msgstr "" + +#: useractions.cpp:889 +#, kde-format +msgctxt "keyboard shortcut '%1' is used by action '%2' in application '%3'" +msgid "%1 is used by %2 in %3" +msgstr "" + +#: useractions.cpp:1021 +#, kde-format +msgid "Activate Window (%1)" +msgstr "" + +#: useractions.cpp:1163 +#, kde-format +msgid "" +"The window manager is configured to consider the screen with the mouse on it " +"as active one.\n" +"Therefore it is not possible to switch to a screen explicitly." +msgstr "" + +#: virtualdesktops.cpp:698 virtualdesktops.cpp:767 +#, kde-format +msgid "Desktop %1" +msgstr "Працоўны стол %1" + +#: virtualdesktops.cpp:802 +#, kde-format +msgid "Switch to Next Desktop" +msgstr "Пераключыцца на наступны працоўны стол" + +#: virtualdesktops.cpp:804 +#, kde-format +msgid "Switch to Previous Desktop" +msgstr "Пераключыцца на папярэдні працоўны стол" + +#: virtualdesktops.cpp:806 +#, kde-format +msgid "Switch One Desktop to the Right" +msgstr "Пераключыцца на працоўны стол справа" + +#: virtualdesktops.cpp:808 +#, kde-format +msgid "Switch One Desktop to the Left" +msgstr "Пераключыцца на працоўны стол злева" + +#: virtualdesktops.cpp:810 +#, kde-format +msgid "Switch One Desktop Up" +msgstr "Пераключыцца на вышэйшы працоўны стол" + +#: virtualdesktops.cpp:812 +#, kde-format +msgid "Switch One Desktop Down" +msgstr "Пераключыцца на ніжэйшы працоўны стол" + +#: virtualdesktops.cpp:825 +#, fuzzy, kde-format +#| msgid "Switch to Desktop 1" +msgid "Switch to Desktop %1" +msgstr "Пераключыцца на працоўны стол 1" + +#: virtualkeyboard.cpp:84 +#, kde-format +msgid "Virtual Keyboard" +msgstr "" + +#: virtualkeyboard.cpp:350 +#, kde-format +msgid "Virtual Keyboard: enabled" +msgstr "" + +#: virtualkeyboard.cpp:353 +#, kde-format +msgid "Virtual Keyboard: disabled" +msgstr "" + +#: virtualkeyboard.cpp:355 +#, kde-format +msgid "Whether to show the virtual keyboard on demand." +msgstr "" + +#: workspace.cpp:1363 +#, kde-format +msgctxt "Introductory text shown in the support information." +msgid "" +"KWin Support Information:\n" +"The following information should be used when requesting support on e.g. " +"https://forum.kde.org.\n" +"It provides information about the currently running instance, which options " +"are used,\n" +"what OpenGL driver and which effects are running.\n" +"Please post the information provided underneath this introductory text to a " +"paste bin service\n" +"like https://paste.kde.org instead of pasting into support threads.\n" +msgstr "" \ No newline at end of file diff --git a/po/be/kwin_clients.po b/po/be/kwin_clients.po new file mode 100644 index 0000000..72aa9b4 --- /dev/null +++ b/po/be/kwin_clients.po @@ -0,0 +1,131 @@ +# translation of kwin_clients.po to Belarusian +# +# Darafei Praliaskouski , 2006. +# Darafei Praliaskouski , 2007. +# Darafei Praliaskouski , 2008. +msgid "" +msgstr "" +"Project-Id-Version: kwin_clients\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-08-08 02:22+0200\n" +"PO-Revision-Date: 2008-02-29 13:09+0200\n" +"Last-Translator: Darafei Praliaskouski \n" +"Language-Team: Belarusian \n" +"Language: be\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: KAider 0.1\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#: aurorae/src/aurorae.cpp:683 +#, kde-format +msgctxt "@item:inlistbox Button size:" +msgid "Tiny" +msgstr "" + +#: aurorae/src/aurorae.cpp:684 +#, kde-format +msgctxt "@item:inlistbox Button size:" +msgid "Normal" +msgstr "" + +#: aurorae/src/aurorae.cpp:685 +#, fuzzy, kde-format +#| msgid "Large" +msgctxt "@item:inlistbox Button size:" +msgid "Large" +msgstr "Вялікі" + +#: aurorae/src/aurorae.cpp:686 +#, fuzzy, kde-format +#| msgid "Large" +msgctxt "@item:inlistbox Button size:" +msgid "Very Large" +msgstr "Вялікі" + +#: aurorae/src/aurorae.cpp:687 +#, fuzzy, kde-format +#| msgid "Large" +msgctxt "@item:inlistbox Button size:" +msgid "Huge" +msgstr "Вялікі" + +#: aurorae/src/aurorae.cpp:688 +#, fuzzy, kde-format +#| msgid "Large" +msgctxt "@item:inlistbox Button size:" +msgid "Very Huge" +msgstr "Вялікі" + +#: aurorae/src/aurorae.cpp:689 +#, fuzzy, kde-format +#| msgid "Resize" +msgctxt "@item:inlistbox Button size:" +msgid "Oversized" +msgstr "Змяніць памер" + +#: aurorae/src/aurorae.cpp:692 +#, kde-format +msgid "Button size:" +msgstr "" + +#. i18n: ectx: property (windowTitle), widget (QWidget, PlastikConfigDialog) +#: aurorae/themes/plastik/package/contents/ui/config.ui:14 +#, kde-format +msgid "Config Dialog" +msgstr "Дыялог настаўленняў" + +#. i18n: ectx: property (title), widget (KButtonGroup, titleAlign) +#: aurorae/themes/plastik/package/contents/ui/config.ui:23 +#, kde-format +msgid "Title &Alignment" +msgstr "Месцазнаходжанне загалоўку" + +#. i18n: ectx: property (text), widget (QRadioButton, kcfg_titleAlignLeft) +#: aurorae/themes/plastik/package/contents/ui/config.ui:29 +#, kde-format +msgid "Left" +msgstr "Леваруч" + +#. i18n: ectx: property (text), widget (QRadioButton, kcfg_titleAlignCenter) +#: aurorae/themes/plastik/package/contents/ui/config.ui:36 +#, kde-format +msgid "Center" +msgstr "Пасярэдзіне" + +#. i18n: ectx: property (text), widget (QRadioButton, kcfg_titleAlignRight) +#: aurorae/themes/plastik/package/contents/ui/config.ui:43 +#, kde-format +msgid "Right" +msgstr "Праваруч" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_coloredBorder) +#: aurorae/themes/plastik/package/contents/ui/config.ui:53 +#, kde-format +msgid "" +"Check this option if the window border should be painted in the titlebar " +"color. Otherwise it will be painted in the background color." +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_coloredBorder) +#: aurorae/themes/plastik/package/contents/ui/config.ui:56 +#, fuzzy, kde-format +#| msgid "Colored Window Border" +msgid "Colored window border" +msgstr "Каляровая рамка акна" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_animateButtons) +#: aurorae/themes/plastik/package/contents/ui/config.ui:66 +#, kde-format +msgid "" +"Check this option if you want the buttons to fade in when the mouse pointer " +"hovers over them and fade out again when it moves away." +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_animateButtons) +#: aurorae/themes/plastik/package/contents/ui/config.ui:69 +#, kde-format +msgid "Animate buttons" +msgstr "Анімаваць кнопкі" \ No newline at end of file diff --git a/po/be/kwin_effects.po b/po/be/kwin_effects.po new file mode 100644 index 0000000..4d6ddd2 --- /dev/null +++ b/po/be/kwin_effects.po @@ -0,0 +1,2181 @@ +# translation of kwin_effects.po to Belarusian +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Darafei Praliaskouski , 2007. +msgid "" +msgstr "" +"Project-Id-Version: kwin_effects\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-10-23 08:49+0200\n" +"PO-Revision-Date: 2007-12-09 18:29+0200\n" +"Last-Translator: Darafei Praliaskouski \n" +"Language-Team: Belarusian \n" +"Language: be\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: KBabel 1.11.4\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, labelConstantBlurDescription) +#: blur/blur_config.ui:17 +#, fuzzy, kde-format +#| msgid "&Strength" +msgid "Blur strength:" +msgstr "Сіла" + +#. i18n: ectx: property (text), widget (QLabel, labelConstantBlurLight) +#. i18n: ectx: property (text), widget (QLabel, labelConstantNoiseLight) +#: blur/blur_config.ui:42 blur/blur_config.ui:108 +#, fuzzy, kde-format +#| msgid "Right" +msgid "Light" +msgstr "Права" + +#. i18n: ectx: property (text), widget (QLabel, labelConstantBlurStrong) +#. i18n: ectx: property (text), widget (QLabel, labelConstantNoiseStrong) +#: blur/blur_config.ui:74 blur/blur_config.ui:137 +#, fuzzy, kde-format +#| msgid "&Strength" +msgid "Strong" +msgstr "Сіла" + +#. i18n: ectx: property (text), widget (QLabel, labelConstantNoiseDescription) +#: blur/blur_config.ui:83 +#, fuzzy, kde-format +#| msgid "&Strength" +msgid "Noise strength:" +msgstr "Сіла" + +#: colorpicker/colorpicker.cpp:107 +#, kde-format +msgid "" +"Select a position for color picking with left click or enter.\n" +"Escape or right click to cancel." +msgstr "" + +#: coverswitch/coverswitch.cpp:945 flipswitch/flipswitch.cpp:925 +#, fuzzy, kde-format +#| msgid "Show Desktop Grid" +msgctxt "Special entry in alt+tab list for minimizing all windows" +msgid "Show Desktop" +msgstr "Паказваць сетку працоўнага стала" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DrawWindowCaptions) +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_WindowTitle) +#: coverswitch/coverswitch_config.ui:17 flipswitch/flipswitch_config.ui:191 +#: presentwindows/presentwindows_config.ui:406 +#, kde-format +msgid "Display window &titles" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: coverswitch/coverswitch_config.ui:29 cube/cube_config.ui:344 +#, fuzzy, kde-format +#| msgid "Bottom" +msgid "Zoom" +msgstr "Дол" + +#. i18n: ectx: property (toolTip), widget (QSlider, kcfg_zPosition) +#: coverswitch/coverswitch_config.ui:39 +#, kde-format +msgid "Define how far away the windows should appear" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_8) +#: coverswitch/coverswitch_config.ui:66 cube/cube_config.ui:350 +#, kde-format +msgid "Near" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_9) +#: coverswitch/coverswitch_config.ui:86 cube/cube_config.ui:357 +#, kde-format +msgid "Far" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, tab) +#: coverswitch/coverswitch_config.ui:110 +#, kde-format +msgid "Animation" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AnimateSwitch) +#: coverswitch/coverswitch_config.ui:116 +#, kde-format +msgid "Animate switch" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AnimateStart) +#: coverswitch/coverswitch_config.ui:123 +#, kde-format +msgid "Animation on tab box open" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AnimateStop) +#: coverswitch/coverswitch_config.ui:130 +#, kde-format +msgid "Animation on tab box close" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: coverswitch/coverswitch_config.ui:139 magiclamp/magiclamp_config.ui:17 +#, kde-format +msgid "Animation duration:" +msgstr "" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_RotationDuration) +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_Duration) +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_AnimationDuration) +#: coverswitch/coverswitch_config.ui:158 cube/cube_config.ui:149 +#: cubeslide/cubeslide_config.ui:49 magiclamp/magiclamp_config.ui:36 +#, kde-format +msgctxt "Duration of rotation" +msgid "Default" +msgstr "" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_Duration) +#: coverswitch/coverswitch_config.ui:161 glide/glide_config.ui:35 +#: scale/package/contents/ui/config.ui:33 slide/slide_config.ui:35 +#, kde-format +msgid " milliseconds" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, tab_3) +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Reflection) +#: coverswitch/coverswitch_config.ui:177 coverswitch/coverswitch_config.ui:183 +#, kde-format +msgid "Reflections" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: coverswitch/coverswitch_config.ui:195 +#, fuzzy, kde-format +#| msgid "&Color" +msgid "Rear color" +msgstr "Колер" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: coverswitch/coverswitch_config.ui:205 +#, fuzzy, kde-format +#| msgid "Shadow opacity:" +msgid "Front color" +msgstr "Празрыстасць цені:" + +#: cube/cube.cpp:186 cube/cube_config.cpp:60 +#, kde-format +msgid "Desktop Cube" +msgstr "" + +#: cube/cube.cpp:194 cube/cube_config.cpp:65 +#, fuzzy, kde-format +#| msgid "Show Desktop Grid" +msgid "Desktop Cylinder" +msgstr "Паказваць сетку працоўнага стала" + +#: cube/cube.cpp:200 cube/cube_config.cpp:69 +#, kde-format +msgid "Desktop Sphere" +msgstr "" + +#: cube/cube_config.cpp:49 +#, kde-format +msgctxt "@title:tab Basic Settings" +msgid "Basic" +msgstr "" + +#: cube/cube_config.cpp:50 +#, kde-format +msgctxt "@title:tab Advanced Settings" +msgid "Advanced" +msgstr "" + +#: cube/cube_config.cpp:54 desktopgrid/desktopgrid_config.cpp:52 +#: flipswitch/flipswitch_config.cpp:56 invert/invert_config.cpp:38 +#: lookingglass/lookingglass_config.cpp:58 magnifier/magnifier_config.cpp:58 +#: mouseclick/mouseclick_config.cpp:50 mousemark/mousemark_config.cpp:56 +#: presentwindows/presentwindows_config.cpp:51 +#: showpaint/showpaint_config.cpp:36 +#: thumbnailaside/thumbnailaside_config.cpp:57 +#: trackmouse/trackmouse_config.cpp:54 +#: windowgeometry/windowgeometry_config.cpp:45 zoom/zoom_config.cpp:59 +#, kde-format +msgid "KWin" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, tab) +#: cube/cube_config.ui:21 +#, kde-format +msgid "Tab 1" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_5) +#: cube/cube_config.ui:27 +#, kde-format +msgid "Background" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: cube/cube_config.ui:33 +#, fuzzy, kde-format +#| msgid "Shadow opacity:" +msgid "Background color:" +msgstr "Празрыстасць цені:" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: cube/cube_config.ui:56 +#, kde-format +msgid "Wallpaper:" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_8) +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#: cube/cube_config.ui:82 desktopgrid/desktopgrid_config.ui:207 +#: flipswitch/flipswitch_config.ui:204 +#: presentwindows/presentwindows_config.ui:17 +#, kde-format +msgid "Activation" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_7) +#. i18n: ectx: property (title), widget (QGroupBox, groupBox) +#: cube/cube_config.ui:104 desktopgrid/desktopgrid_config.ui:17 +#: flipswitch/flipswitch_config.ui:17 mousemark/mousemark_config.ui:17 +#: presentwindows/presentwindows_config.ui:387 +#: thumbnailaside/thumbnailaside_config.ui:17 +#, kde-format +msgid "Appearance" +msgstr "Вонкавы выгляд" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DisplayDesktopName) +#: cube/cube_config.ui:110 +#, kde-format +msgid "Display desktop name" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Reflection) +#: cube/cube_config.ui:117 +#, kde-format +msgid "Reflection" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: cube/cube_config.ui:124 cubeslide/cubeslide_config.ui:72 +#, kde-format +msgid "Rotation duration:" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ZOrdering) +#: cube/cube_config.ui:175 +#, kde-format +msgid "Windows hover above cube" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_4) +#: cube/cube_config.ui:185 +#, fuzzy, kde-format +#| msgid "&Opacity" +msgid "Opacity" +msgstr "&Празрыстасць" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_OpacitySpin) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_Opacity) +#: cube/cube_config.ui:225 thumbnailaside/thumbnailaside_config.ui:87 +#, no-c-format, kde-format +msgid " %" +msgstr " %" + +#. i18n: ectx: property (text), widget (QLabel, label_4) +#. i18n: ectx: property (text), widget (QLabel, label_2) +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: cube/cube_config.ui:238 translucency/package/contents/ui/config.ui:156 +#: translucency/package/contents/ui/config.ui:418 +#, kde-format +msgid "Transparent" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#. i18n: ectx: property (text), widget (QLabel, label_3) +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: cube/cube_config.ui:245 translucency/package/contents/ui/config.ui:121 +#: translucency/package/contents/ui/config.ui:431 +#, kde-format +msgid "Opaque" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_OpacityDesktopOnly) +#: cube/cube_config.ui:255 +#, kde-format +msgid "Do not change opacity of windows" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, tab_2) +#: cube/cube_config.ui:279 +#, kde-format +msgid "Tab 2" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_3) +#: cube/cube_config.ui:285 +#, kde-format +msgid "Caps" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Caps) +#: cube/cube_config.ui:291 +#, kde-format +msgid "Show caps" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, capColorLabel) +#: cube/cube_config.ui:298 +#, fuzzy, kde-format +#| msgid "&Color" +msgid "Cap color:" +msgstr "Колер" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_TexturedCaps) +#: cube/cube_config.ui:321 +#, kde-format +msgid "Display image on caps" +msgstr "" + +#. i18n: ectx: property (toolTip), widget (QSlider, kcfg_ZPosition) +#: cube/cube_config.ui:367 +#, kde-format +msgid "Define how far away the object should appear" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_9) +#: cube/cube_config.ui:408 +#, kde-format +msgid "Additional Options" +msgstr "" + +#. i18n: ectx: property (toolTip), widget (QCheckBox, kcfg_CloseOnMouseRelease) +#: cube/cube_config.ui:415 +#, kde-format +msgid "" +"If enabled the effect will be deactivated after rotating the cube with the " +"mouse,\n" +"otherwise it will remain active" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_CloseOnMouseRelease) +#: cube/cube_config.ui:418 +#, kde-format +msgid "Close after mouse dragging" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_TabBox) +#: cube/cube_config.ui:425 +#, kde-format +msgid "Use this effect for walking through the desktops" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_InvertKeys) +#: cube/cube_config.ui:432 +#, kde-format +msgid "Invert cursor keys" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_InvertMouse) +#: cube/cube_config.ui:439 +#, kde-format +msgid "Invert mouse" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, capDeformationGroupBox) +#: cube/cube_config.ui:449 +#, kde-format +msgid "Sphere Cap Deformation" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, capDeformationSphereLabel) +#: cube/cube_config.ui:471 +#, kde-format +msgid "Sphere" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, capDeformationPlaneLabel) +#: cube/cube_config.ui:478 +#, kde-format +msgid "Plane" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DontSlideStickyWindows) +#: cubeslide/cubeslide_config.ui:17 +#, kde-format +msgid "Do not animate windows on all desktops" +msgstr "" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_RingLife) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_RotationDuration) +#: cubeslide/cubeslide_config.ui:52 mouseclick/mouseclick_config.ui:132 +#, kde-format +msgid " msec" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DontSlidePanels) +#: cubeslide/cubeslide_config.ui:65 +#, kde-format +msgid "Do not animate panels" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_UsePagerLayout) +#: cubeslide/cubeslide_config.ui:85 +#, kde-format +msgid "Use pager layout for animation" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_UseWindowMoving) +#: cubeslide/cubeslide_config.ui:92 +#, kde-format +msgid "Start animation when moving windows towards screen edges" +msgstr "" + +#: desktopgrid/desktopgrid.cpp:65 desktopgrid/desktopgrid_config.cpp:57 +#, kde-format +msgid "Show Desktop Grid" +msgstr "Паказваць сетку працоўнага стала" + +#: desktopgrid/desktopgrid_config.cpp:65 +#, kde-format +msgctxt "Desktop name alignment:" +msgid "Disabled" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_InRotationEdge) +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_OutRotationEdge) +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: desktopgrid/desktopgrid_config.cpp:66 flipswitch/flipswitch_config.ui:160 +#: glide/glide_config.ui:70 glide/glide_config.ui:168 +#, kde-format +msgid "Top" +msgstr "Верх" + +#: desktopgrid/desktopgrid_config.cpp:67 +#, fuzzy, kde-format +#| msgid "Top-right" +msgid "Top-Right" +msgstr "Правы верхні" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_InRotationEdge) +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_OutRotationEdge) +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: desktopgrid/desktopgrid_config.cpp:68 flipswitch/flipswitch_config.ui:125 +#: glide/glide_config.ui:75 glide/glide_config.ui:173 +#, kde-format +msgid "Right" +msgstr "Права" + +#: desktopgrid/desktopgrid_config.cpp:69 +#, fuzzy, kde-format +#| msgid "Bottom-right" +msgid "Bottom-Right" +msgstr "Правы ніжні" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_InRotationEdge) +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_OutRotationEdge) +#. i18n: ectx: property (text), widget (QLabel, label_8) +#: desktopgrid/desktopgrid_config.cpp:70 flipswitch/flipswitch_config.ui:180 +#: glide/glide_config.ui:80 glide/glide_config.ui:178 +#, kde-format +msgid "Bottom" +msgstr "Дол" + +#: desktopgrid/desktopgrid_config.cpp:71 +#, fuzzy, kde-format +#| msgid "Bottom-left" +msgid "Bottom-Left" +msgstr "Левы ніжні" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_InRotationEdge) +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_OutRotationEdge) +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: desktopgrid/desktopgrid_config.cpp:72 flipswitch/flipswitch_config.ui:105 +#: glide/glide_config.ui:85 glide/glide_config.ui:183 +#, kde-format +msgid "Left" +msgstr "Лева" + +#: desktopgrid/desktopgrid_config.cpp:73 +#, fuzzy, kde-format +#| msgid "Top-left" +msgid "Top-Left" +msgstr "Верхні левы" + +#: desktopgrid/desktopgrid_config.cpp:74 +#, kde-format +msgid "Center" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: desktopgrid/desktopgrid_config.ui:23 +#, kde-format +msgid "Zoom &duration:" +msgstr "" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_ZoomDuration) +#: desktopgrid/desktopgrid_config.ui:42 +#, kde-format +msgctxt "Duration of zoom" +msgid "Default" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: desktopgrid/desktopgrid_config.ui:55 +#, kde-format +msgid "Border wid&th:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: desktopgrid/desktopgrid_config.ui:84 +#, kde-format +msgid "Desktop &name alignment:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: desktopgrid/desktopgrid_config.ui:107 +#, kde-format +msgid "&Layout mode:" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LayoutMode) +#: desktopgrid/desktopgrid_config.ui:127 +#, kde-format +msgid "Pager" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LayoutMode) +#: desktopgrid/desktopgrid_config.ui:132 +#, kde-format +msgid "Automatic" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LayoutMode) +#: desktopgrid/desktopgrid_config.ui:137 +#, kde-format +msgid "Custom" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, layoutRowsLabel) +#: desktopgrid/desktopgrid_config.ui:145 +#, kde-format +msgid "N&umber of rows:" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_PresentWindows) +#: desktopgrid/desktopgrid_config.ui:190 +#, kde-format +msgid "Use Present Windows effect to layout the windows" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ShowAddRemove) +#: desktopgrid/desktopgrid_config.ui:197 +#, kde-format +msgid "Show buttons to alter count of virtual desktops" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_Strength) +#: diminactive/diminactive_config.ui:17 +#, fuzzy, kde-format +#| msgid "&Strength" +msgid "Strength:" +msgstr "Сіла" + +#. i18n: ectx: property (text), widget (QLabel, label_Dim) +#: diminactive/diminactive_config.ui:40 +#, kde-format +msgid "Dim:" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DimPanels) +#: diminactive/diminactive_config.ui:47 +#, kde-format +msgid "Docks and panels" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DimDesktop) +#: diminactive/diminactive_config.ui:54 +#, fuzzy, kde-format +#| msgid "Show Desktop Grid" +msgid "Desktop" +msgstr "Паказваць сетку працоўнага стала" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DimKeepAbove) +#: diminactive/diminactive_config.ui:61 +#, kde-format +msgid "Keep above windows" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DimByGroup) +#: diminactive/diminactive_config.ui:68 +#, kde-format +msgid "By window group" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DimFullScreen) +#: diminactive/diminactive_config.ui:75 +#, kde-format +msgid "Fullscreen windows" +msgstr "" + +#: effect_builtins.cpp:91 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Blur" +msgstr "" + +#: effect_builtins.cpp:92 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Blurs the background behind semi-transparent windows" +msgstr "" + +#: effect_builtins.cpp:106 +#, fuzzy, kde-format +#| msgid "&Color" +msgctxt "Name of a KWin Effect" +msgid "Color Picker" +msgstr "Колер" + +#: effect_builtins.cpp:107 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Supports picking a color" +msgstr "" + +#: effect_builtins.cpp:121 +#, fuzzy, kde-format +#| msgid "Shadow opacity:" +msgctxt "Name of a KWin Effect" +msgid "Background contrast" +msgstr "Празрыстасць цені:" + +#: effect_builtins.cpp:122 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Improve contrast and readability behind semi-transparent windows" +msgstr "" + +#: effect_builtins.cpp:136 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Cover Switch" +msgstr "" + +#: effect_builtins.cpp:137 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Display a Cover Flow effect for the alt+tab window switcher" +msgstr "" + +#: effect_builtins.cpp:151 +#, fuzzy, kde-format +#| msgid "Show Desktop Grid" +msgctxt "Name of a KWin Effect" +msgid "Desktop Cube" +msgstr "Паказваць сетку працоўнага стала" + +#: effect_builtins.cpp:152 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Display each virtual desktop on a side of a cube" +msgstr "" + +#: effect_builtins.cpp:166 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Desktop Cube Animation" +msgstr "" + +#: effect_builtins.cpp:167 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Animate desktop switching with a cube" +msgstr "" + +#: effect_builtins.cpp:181 +#, fuzzy, kde-format +#| msgid "Show Desktop Grid" +msgctxt "Name of a KWin Effect" +msgid "Desktop Grid" +msgstr "Паказваць сетку працоўнага стала" + +#: effect_builtins.cpp:182 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Zoom out so all desktops are displayed side-by-side in a grid" +msgstr "" + +#: effect_builtins.cpp:196 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Dim Inactive" +msgstr "" + +#: effect_builtins.cpp:197 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Darken inactive windows" +msgstr "" + +#: effect_builtins.cpp:211 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Fall Apart" +msgstr "" + +#: effect_builtins.cpp:212 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Closed windows fall into pieces" +msgstr "" + +#: effect_builtins.cpp:226 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Flip Switch" +msgstr "" + +#: effect_builtins.cpp:227 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "" +"Flip through windows that are in a stack for the alt+tab window switcher" +msgstr "" + +#: effect_builtins.cpp:241 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Glide" +msgstr "" + +#: effect_builtins.cpp:242 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Glide windows as they appear or disappear" +msgstr "" + +#: effect_builtins.cpp:256 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Highlight Window" +msgstr "" + +#: effect_builtins.cpp:257 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Highlight the appropriate window when hovering over taskbar entries" +msgstr "" + +#: effect_builtins.cpp:271 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Invert" +msgstr "" + +#: effect_builtins.cpp:272 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Inverts the color of the desktop and windows" +msgstr "" + +#: effect_builtins.cpp:286 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Kscreen" +msgstr "" + +#: effect_builtins.cpp:287 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Helper Effect for KScreen" +msgstr "" + +#: effect_builtins.cpp:301 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Looking Glass" +msgstr "" + +#: effect_builtins.cpp:302 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "A screen magnifier that looks like a fisheye lens" +msgstr "" + +#: effect_builtins.cpp:316 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Magic Lamp" +msgstr "" + +#: effect_builtins.cpp:317 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Simulate a magic lamp when minimizing windows" +msgstr "" + +#: effect_builtins.cpp:331 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Magnifier" +msgstr "" + +#: effect_builtins.cpp:332 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Magnify the section of the screen that is near the mouse cursor" +msgstr "" + +#: effect_builtins.cpp:346 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Mouse Click Animation" +msgstr "" + +#: effect_builtins.cpp:347 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "" +"Creates an animation whenever a mouse button is clicked. This is useful for " +"screenrecordings/presentations" +msgstr "" + +#: effect_builtins.cpp:361 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Mouse Mark" +msgstr "" + +#: effect_builtins.cpp:362 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Allows you to draw lines on the desktop" +msgstr "" + +#: effect_builtins.cpp:376 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Present Windows" +msgstr "" + +#: effect_builtins.cpp:377 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Zoom out until all opened windows can be displayed side-by-side" +msgstr "" + +#: effect_builtins.cpp:391 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Resize Window" +msgstr "" + +#: effect_builtins.cpp:392 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Resizes windows with a fast texture scale instead of updating contents" +msgstr "" + +#: effect_builtins.cpp:406 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Screen Edge" +msgstr "" + +#: effect_builtins.cpp:407 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Highlights a screen edge when approaching" +msgstr "" + +#: effect_builtins.cpp:421 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Screenshot" +msgstr "" + +#: effect_builtins.cpp:422 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Helper effect for screenshot tools" +msgstr "" + +#: effect_builtins.cpp:436 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Sheet" +msgstr "" + +#: effect_builtins.cpp:437 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "" +"Make modal dialogs smoothly fly in and out when they are shown or hidden" +msgstr "" + +#: effect_builtins.cpp:451 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Show FPS" +msgstr "" + +#: effect_builtins.cpp:452 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Display KWin's performance in the corner of the screen" +msgstr "" + +#: effect_builtins.cpp:466 +#, fuzzy, kde-format +#| msgid "Show Desktop Grid" +msgctxt "Name of a KWin Effect" +msgid "Show Paint" +msgstr "Паказваць сетку працоўнага стала" + +#: effect_builtins.cpp:467 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Highlight areas of the desktop that have been recently updated" +msgstr "" + +#: effect_builtins.cpp:481 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Slide" +msgstr "" + +#: effect_builtins.cpp:482 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Slide desktops when switching virtual desktops" +msgstr "" + +#: effect_builtins.cpp:496 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Slide Back" +msgstr "" + +#: effect_builtins.cpp:497 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Slide back windows when another window is raised" +msgstr "" + +#: effect_builtins.cpp:511 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Sliding popups" +msgstr "" + +#: effect_builtins.cpp:512 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Sliding animation for Plasma popups" +msgstr "" + +#: effect_builtins.cpp:526 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Snap Helper" +msgstr "" + +#: effect_builtins.cpp:527 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Help you locate the center of the screen when moving a window" +msgstr "" + +#: effect_builtins.cpp:541 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Startup Feedback" +msgstr "" + +#: effect_builtins.cpp:542 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Helper effect for startup feedback" +msgstr "" + +#: effect_builtins.cpp:556 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Thumbnail Aside" +msgstr "" + +#: effect_builtins.cpp:557 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Display window thumbnails on the edge of the screen" +msgstr "" + +#: effect_builtins.cpp:571 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Touch Points" +msgstr "" + +#: effect_builtins.cpp:572 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Visualize touch points" +msgstr "" + +#: effect_builtins.cpp:586 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Track Mouse" +msgstr "" + +#: effect_builtins.cpp:587 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Display a mouse cursor locating effect when activated" +msgstr "" + +#: effect_builtins.cpp:601 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Window Geometry" +msgstr "" + +#: effect_builtins.cpp:602 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Display window geometries on move/resize" +msgstr "" + +#: effect_builtins.cpp:616 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Wobbly Windows" +msgstr "" + +#: effect_builtins.cpp:617 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Deform windows while they are moving" +msgstr "" + +#: effect_builtins.cpp:631 +#, fuzzy, kde-format +#| msgid "Bottom" +msgctxt "Name of a KWin Effect" +msgid "Zoom" +msgstr "Дол" + +#: effect_builtins.cpp:632 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Magnify the entire desktop" +msgstr "" + +#: flipswitch/flipswitch.cpp:48 flipswitch/flipswitch_config.cpp:50 +#, kde-format +msgid "Toggle Flip Switch (Current desktop)" +msgstr "" + +#: flipswitch/flipswitch.cpp:55 flipswitch/flipswitch_config.cpp:53 +#, kde-format +msgid "Toggle Flip Switch (All desktops)" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: flipswitch/flipswitch_config.ui:23 +#, kde-format +msgid "Flip animation duration:" +msgstr "" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_Duration) +#: flipswitch/flipswitch_config.ui:42 +#, kde-format +msgctxt "Duration of flip animation" +msgid "Default" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: flipswitch/flipswitch_config.ui:55 +#, kde-format +msgid "Angle:" +msgstr "" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_Angle) +#: flipswitch/flipswitch_config.ui:71 +#, kde-format +msgid " °" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: flipswitch/flipswitch_config.ui:81 +#, kde-format +msgid "Horizontal position of front:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: flipswitch/flipswitch_config.ui:136 +#, kde-format +msgid "Vertical position of front:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_Duration) +#: glide/glide_config.ui:19 scale/package/contents/ui/config.ui:17 +#: slide/slide_config.ui:19 +#, kde-format +msgid "Duration:" +msgstr "" + +#. i18n: Duration of the slide animation. +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_Duration) +#: glide/glide_config.ui:32 scale/package/contents/ui/config.ui:30 +#: slide/slide_config.ui:32 +#, kde-format +msgid "Default" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_InAnimation) +#: glide/glide_config.ui:50 +#, kde-format +msgid "Window Open Animation" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_InRotationEdge) +#. i18n: ectx: property (text), widget (QLabel, label_OutRotationEdge) +#: glide/glide_config.ui:56 glide/glide_config.ui:154 +#, kde-format +msgid "Rotation edge:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_InRotationAngle) +#. i18n: ectx: property (text), widget (QLabel, label_OutRotationAngle) +#: glide/glide_config.ui:93 glide/glide_config.ui:191 +#, kde-format +msgid "Rotation angle:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_InDistance) +#. i18n: ectx: property (text), widget (QLabel, label_OutDistance) +#: glide/glide_config.ui:119 glide/glide_config.ui:198 +#, kde-format +msgid "Distance:" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_OutAnimation) +#: glide/glide_config.ui:148 +#, kde-format +msgid "Window Close Animation" +msgstr "" + +#: invert/invert.cpp:34 invert/invert_config.cpp:41 +#, fuzzy, kde-format +#| msgid "Toggle Invert effect" +msgid "Toggle Invert Effect" +msgstr "Уключыць эфект інвертавання" + +#: invert/invert.cpp:42 invert/invert_config.cpp:47 +#, fuzzy, kde-format +#| msgid "Toggle Invert effect" +msgid "Toggle Invert Effect on Window" +msgstr "Уключыць эфект інвертавання" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_FadeToBlack) +#: login/package/contents/ui/config.ui:17 +#, kde-format +msgid "Fade to black (fullscreen splash screens only)" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: lookingglass/lookingglass_config.ui:24 +#, fuzzy, kde-format +#| msgid "&Radius" +msgid "&Radius:" +msgstr "Радыус" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_AnimationDuration) +#: magiclamp/magiclamp_config.ui:39 +#, kde-format +msgid "milliseconds" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupSize) +#: magnifier/magnifier_config.ui:17 +#, kde-format +msgid "Size" +msgstr "Памер" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: magnifier/magnifier_config.ui:23 +#, fuzzy, kde-format +#| msgid "&Width" +msgid "&Width:" +msgstr "&Шырыня" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_Width) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_Height) +#: magnifier/magnifier_config.ui:42 magnifier/magnifier_config.ui:74 +#, kde-format +msgid " px" +msgstr " пікс" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: magnifier/magnifier_config.ui:55 +#, fuzzy, kde-format +#| msgid "&Height" +msgid "&Height:" +msgstr "&Вышыня" + +#: mouseclick/mouseclick.cpp:40 mouseclick/mouseclick_config.cpp:53 +#, fuzzy, kde-format +#| msgid "Toggle Invert effect" +msgid "Toggle Mouse Click Effect" +msgstr "Уключыць эфект інвертавання" + +#: mouseclick/mouseclick.cpp:48 +#, fuzzy, kde-format +#| msgid "Left" +msgctxt "Left mouse button" +msgid "Left" +msgstr "Лева" + +#: mouseclick/mouseclick.cpp:49 +#, kde-format +msgctxt "Middle mouse button" +msgid "Middle" +msgstr "" + +#: mouseclick/mouseclick.cpp:50 +#, fuzzy, kde-format +#| msgid "Right" +msgctxt "Right mouse button" +msgid "Right" +msgstr "Права" + +#: mouseclick/mouseclick.h:63 +#, kde-format +msgid "↓" +msgstr "" + +#: mouseclick/mouseclick.h:64 +#, kde-format +msgid "↑" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, basic_tab) +#: mouseclick/mouseclick_config.ui:21 +#, kde-format +msgid "Basic Settings" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, button1_label) +#: mouseclick/mouseclick_config.ui:37 +#, kde-format +msgid "Left Mouse Button Color:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, button2_label) +#: mouseclick/mouseclick_config.ui:50 +#, kde-format +msgid "Middle Mouse Button Color:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, button3_label) +#: mouseclick/mouseclick_config.ui:70 +#, kde-format +msgid "Right Mouse Button Color:" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, advanced_tab) +#: mouseclick/mouseclick_config.ui:91 +#, kde-format +msgid "Advanced Settings" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, rings) +#: mouseclick/mouseclick_config.ui:97 +#, kde-format +msgid "Rings" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, ring_line_width_label) +#: mouseclick/mouseclick_config.ui:103 +#, fuzzy, kde-format +#| msgid "&Width" +msgid "Line Width:" +msgstr "&Шырыня" + +#. i18n: ectx: property (suffix), widget (QDoubleSpinBox, kcfg_LineWidth) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_RingSize) +#: mouseclick/mouseclick_config.ui:119 mouseclick/mouseclick_config.ui:171 +#: mousemark/mousemark_config.cpp:45 +#, fuzzy, kde-format +#| msgid " px" +msgid " pixel" +msgid_plural " pixels" +msgstr[0] " пікс" +msgstr[1] " пікс" +msgstr[2] " пікс" + +#. i18n: ectx: property (text), widget (QLabel, ring_duration_label) +#: mouseclick/mouseclick_config.ui:145 +#, kde-format +msgid "Ring Duration:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, ring_radius_label) +#: mouseclick/mouseclick_config.ui:155 +#, fuzzy, kde-format +#| msgid "&Radius" +msgid "Ring Radius:" +msgstr "Радыус" + +#. i18n: ectx: property (text), widget (QLabel, ring_count_label) +#: mouseclick/mouseclick_config.ui:184 +#, kde-format +msgid "Ring Count:" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox) +#. i18n: ectx: property (title), widget (QGroupBox, font) +#: mouseclick/mouseclick_config.ui:210 showfps/showfps_config.ui:17 +#, kde-format +msgid "Text" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, font_label) +#: mouseclick/mouseclick_config.ui:216 +#, kde-format +msgid "Font:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, showtext_label) +#: mouseclick/mouseclick_config.ui:233 +#, fuzzy, kde-format +#| msgid "Show Desktop Grid" +msgid "Show Text:" +msgstr "Паказваць сетку працоўнага стала" + +#: mousemark/mousemark.cpp:41 +#, kde-format +msgid "Clear All Mouse Marks" +msgstr "" + +#: mousemark/mousemark.cpp:48 mousemark/mousemark_config.cpp:65 +#, kde-format +msgid "Clear Last Mouse Mark" +msgstr "" + +#: mousemark/mousemark_config.cpp:59 +#, kde-format +msgid "Clear Mouse Marks" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: mousemark/mousemark_config.ui:23 +#, kde-format +msgid "Wid&th:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: mousemark/mousemark_config.ui:36 +#, fuzzy, kde-format +#| msgid "&Color" +msgid "&Color:" +msgstr "Колер" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: mousemark/mousemark_config.ui:78 +#, kde-format +msgid "Draw with the mouse by holding Shift+Meta keys and moving the mouse." +msgstr "" + +#: presentwindows/presentwindows.cpp:66 +#: presentwindows/presentwindows_config.cpp:62 +#, kde-format +msgid "Toggle Present Windows (Current desktop)" +msgstr "" + +#: presentwindows/presentwindows.cpp:75 +#: presentwindows/presentwindows_config.cpp:56 +#, kde-format +msgid "Toggle Present Windows (All desktops)" +msgstr "" + +#: presentwindows/presentwindows.cpp:85 +#: presentwindows/presentwindows_config.cpp:68 +#, kde-format +msgid "Toggle Present Windows (Window class)" +msgstr "" + +#: presentwindows/presentwindows.cpp:1666 +#, kde-format +msgid "" +"Filter:\n" +"%1" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_3) +#: presentwindows/presentwindows_config.ui:39 +#, kde-format +msgid "Natural Layout Settings" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_FillGaps) +#: presentwindows/presentwindows_config.ui:45 +#, kde-format +msgid "Fill &gaps" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: presentwindows/presentwindows_config.ui:65 +#, kde-format +msgid "Faster" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: presentwindows/presentwindows_config.ui:112 +#, kde-format +msgid "Nicer" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_4) +#: presentwindows/presentwindows_config.ui:122 +#, kde-format +msgid "Windows" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#. i18n: ectx: property (text), widget (QLabel, label_8) +#: presentwindows/presentwindows_config.ui:128 +#: presentwindows/presentwindows_config.ui:282 +#, kde-format +msgid "Left button:" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonDesktop) +#: presentwindows/presentwindows_config.ui:139 +#: presentwindows/presentwindows_config.ui:183 +#: presentwindows/presentwindows_config.ui:232 +#: presentwindows/presentwindows_config.ui:293 +#: presentwindows/presentwindows_config.ui:327 +#: presentwindows/presentwindows_config.ui:361 +#, kde-format +msgid "No action" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonDesktop) +#: presentwindows/presentwindows_config.ui:144 +#: presentwindows/presentwindows_config.ui:188 +#: presentwindows/presentwindows_config.ui:237 +#: presentwindows/presentwindows_config.ui:298 +#: presentwindows/presentwindows_config.ui:332 +#: presentwindows/presentwindows_config.ui:366 +#, kde-format +msgid "Activate window" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonDesktop) +#: presentwindows/presentwindows_config.ui:149 +#: presentwindows/presentwindows_config.ui:193 +#: presentwindows/presentwindows_config.ui:242 +#: presentwindows/presentwindows_config.ui:303 +#: presentwindows/presentwindows_config.ui:337 +#: presentwindows/presentwindows_config.ui:371 +#, kde-format +msgid "End effect" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonWindow) +#: presentwindows/presentwindows_config.ui:154 +#: presentwindows/presentwindows_config.ui:198 +#: presentwindows/presentwindows_config.ui:247 +#, kde-format +msgid "Bring window to current desktop" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonWindow) +#: presentwindows/presentwindows_config.ui:159 +#: presentwindows/presentwindows_config.ui:203 +#: presentwindows/presentwindows_config.ui:252 +#, kde-format +msgid "Send window to all desktops" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonWindow) +#: presentwindows/presentwindows_config.ui:164 +#: presentwindows/presentwindows_config.ui:208 +#: presentwindows/presentwindows_config.ui:257 +#, kde-format +msgid "(Un-)Minimize window" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_4) +#. i18n: ectx: property (text), widget (QLabel, label_9) +#: presentwindows/presentwindows_config.ui:172 +#: presentwindows/presentwindows_config.ui:316 +#, kde-format +msgid "Middle button:" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonWindow) +#: presentwindows/presentwindows_config.ui:213 +#: presentwindows/presentwindows_config.ui:262 +#, kde-format +msgid "Close window" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#. i18n: ectx: property (text), widget (QLabel, label_10) +#: presentwindows/presentwindows_config.ui:221 +#: presentwindows/presentwindows_config.ui:350 +#, kde-format +msgid "Right button:" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_5) +#: presentwindows/presentwindows_config.ui:273 +#, fuzzy, kde-format +#| msgid "Show Desktop Grid" +msgctxt "@title:group actions when clicking on desktop" +msgid "Desktop" +msgstr "Паказваць сетку працоўнага стала" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonDesktop) +#: presentwindows/presentwindows_config.ui:308 +#: presentwindows/presentwindows_config.ui:342 +#: presentwindows/presentwindows_config.ui:376 +#, fuzzy, kde-format +#| msgid "Show Desktop Grid" +msgid "Show desktop" +msgstr "Паказваць сетку працоўнага стала" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: presentwindows/presentwindows_config.ui:393 +#, kde-format +msgid "Layout mode:" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DrawWindowIcons) +#: presentwindows/presentwindows_config.ui:413 +#, kde-format +msgid "Display window &icons" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_IgnoreMinimized) +#: presentwindows/presentwindows_config.ui:420 +#, kde-format +msgid "Ignore &minimized windows" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ShowPanel) +#: presentwindows/presentwindows_config.ui:427 +#, kde-format +msgid "Show &panels" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LayoutMode) +#: presentwindows/presentwindows_config.ui:441 +#, kde-format +msgid "Natural" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LayoutMode) +#: presentwindows/presentwindows_config.ui:446 +#, kde-format +msgid "Regular Grid" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LayoutMode) +#: presentwindows/presentwindows_config.ui:451 +#, kde-format +msgid "Flexible Grid" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AllowClosingWindows) +#: presentwindows/presentwindows_config.ui:459 +#, kde-format +msgid "Provide buttons to close the windows" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_TextureScale) +#: resize/resize_config.ui:17 +#, kde-format +msgid "Scale window" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Outline) +#: resize/resize_config.ui:24 +#, kde-format +msgid "Show outline" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_InScale) +#: scale/package/contents/ui/config.ui:46 +#, kde-format +msgid "Window open scale:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_OutScale) +#: scale/package/contents/ui/config.ui:53 +#, kde-format +msgid "Window close scale:" +msgstr "" + +#: screenshot/screenshot.cpp:440 +#, kde-format +msgctxt "Notification caption that a screenshot got saved to file" +msgid "Screenshot" +msgstr "" + +#: screenshot/screenshot.cpp:441 +#, kde-format +msgctxt "Notification with path to screenshot file" +msgid "Screenshot saved to %1" +msgstr "" + +#: screenshot/screenshot.cpp:578 +#, kde-format +msgid "" +"Select window to screen shot with left click or enter.\n" +"Escape or right click to cancel." +msgstr "" + +#: screenshot/screenshot.cpp:581 +#, kde-format +msgid "" +"Create screen shot with left click or enter.\n" +"Escape or right click to cancel." +msgstr "" + +#: showfps/showfps.cpp:54 +#, kde-format +msgid "This effect is not a benchmark" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: showfps/showfps_config.ui:23 +#, kde-format +msgid "Text position:" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TextPosition) +#: showfps/showfps_config.ui:43 +#, kde-format +msgid "Inside Graph" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TextPosition) +#: showfps/showfps_config.ui:48 +#, kde-format +msgid "Nowhere" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TextPosition) +#: showfps/showfps_config.ui:53 +#, fuzzy, kde-format +#| msgid "Top-left" +msgid "Top Left" +msgstr "Верхні левы" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TextPosition) +#: showfps/showfps_config.ui:58 +#, fuzzy, kde-format +#| msgid "Top-right" +msgid "Top Right" +msgstr "Правы верхні" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TextPosition) +#: showfps/showfps_config.ui:63 +#, fuzzy, kde-format +#| msgid "Bottom-left" +msgid "Bottom Left" +msgstr "Левы ніжні" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TextPosition) +#: showfps/showfps_config.ui:68 +#, fuzzy, kde-format +#| msgid "Bottom-right" +msgid "Bottom Right" +msgstr "Правы ніжні" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: showfps/showfps_config.ui:76 +#, kde-format +msgid "Text font:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: showfps/showfps_config.ui:96 +#, kde-format +msgid "Text color:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: showfps/showfps_config.ui:119 +#, kde-format +msgid "Text alpha:" +msgstr "" + +#: showpaint/showpaint.cpp:42 showpaint/showpaint_config.cpp:41 +#, fuzzy, kde-format +#| msgid "Show Desktop Grid" +msgid "Toggle Show Paint" +msgstr "Паказваць сетку працоўнага стала" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_Gaps) +#: slide/slide_config.ui:50 +#, kde-format +msgid "Gap between desktops" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_HorizontalGap) +#: slide/slide_config.ui:56 +#, kde-format +msgid "Horizontal:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_VerticalGap) +#: slide/slide_config.ui:79 +#, kde-format +msgid "Vertical:" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_SlideDocks) +#: slide/slide_config.ui:105 +#, kde-format +msgid "Slide docks" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_SlideBackground) +#: slide/slide_config.ui:112 +#, kde-format +msgid "Slide desktop background" +msgstr "" + +#: thumbnailaside/thumbnailaside.cpp:29 +#: thumbnailaside/thumbnailaside_config.cpp:62 +#, kde-format +msgid "Toggle Thumbnail for Current Window" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: thumbnailaside/thumbnailaside_config.ui:23 +#, kde-format +msgid "Maximum &width:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: thumbnailaside/thumbnailaside_config.ui:36 +#, kde-format +msgid "&Spacing:" +msgstr "" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_Spacing) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_MaxWidth) +#: thumbnailaside/thumbnailaside_config.ui:55 +#: thumbnailaside/thumbnailaside_config.ui:106 +#, fuzzy, kde-format +#| msgid " px" +msgid " pixels" +msgstr " пікс" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: thumbnailaside/thumbnailaside_config.ui:68 +#, fuzzy, kde-format +#| msgid "&Opacity" +msgid "&Opacity:" +msgstr "&Празрыстасць" + +#: trackmouse/trackmouse.cpp:50 trackmouse/trackmouse_config.cpp:59 +#, kde-format +msgid "Track mouse" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: trackmouse/trackmouse_config.ui:26 +#, kde-format +msgid "Trigger effect with:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_KeyboardShortcut) +#: trackmouse/trackmouse_config.ui:33 +#, kde-format +msgid "Keyboard shortcut:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_ModifierKeys) +#: trackmouse/trackmouse_config.ui:43 +#, kde-format +msgid "Modifier keys:" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Alt) +#: trackmouse/trackmouse_config.ui:65 +#, kde-format +msgid "Alt" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Control) +#: trackmouse/trackmouse_config.ui:72 +#, kde-format +msgid "Ctrl" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Shift) +#: trackmouse/trackmouse_config.ui:79 +#, kde-format +msgid "Shift" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Meta) +#: trackmouse/trackmouse_config.ui:86 +#, kde-format +msgid "Meta" +msgstr "" + +#. i18n: ectx: property (windowTitle), widget (QWidget, KWin::TranslucencyEffectConfigForm) +#: translucency/package/contents/ui/config.ui:14 +#, kde-format +msgid "Translucency" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, m_opacityGroupBox) +#: translucency/package/contents/ui/config.ui:20 +#, kde-format +msgid "General Translucency Settings" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, comboboxpopup_label) +#: translucency/package/contents/ui/config.ui:64 +#, kde-format +msgid "Combobox popups:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, dialogs_label) +#: translucency/package/contents/ui/config.ui:137 +#, kde-format +msgid "Dialogs:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, menus_label) +#: translucency/package/contents/ui/config.ui:188 +#, kde-format +msgid "Menus:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, moveresize_label) +#: translucency/package/contents/ui/config.ui:207 +#, kde-format +msgid "Moving windows:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, inactive_label) +#: translucency/package/contents/ui/config.ui:226 +#, kde-format +msgid "Inactive windows:" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, kcfg_IndividualMenuConfig) +#: translucency/package/contents/ui/config.ui:267 +#, kde-format +msgid "Set menu translucency independently" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, dropdownmenus_label) +#: translucency/package/contents/ui/config.ui:285 +#, kde-format +msgid "Dropdown menus:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, popupmenus_label) +#: translucency/package/contents/ui/config.ui:329 +#, kde-format +msgid "Popup menus:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, tornoffmenus_label) +#: translucency/package/contents/ui/config.ui:367 +#, kde-format +msgid "Torn-off menus:" +msgstr "" + +#: windowgeometry/windowgeometry.cpp:43 +#, kde-format +msgid "Toggle window geometry display (effect only)" +msgstr "" + +#: windowgeometry/windowgeometry_config.cpp:47 +#, kde-format +msgid "Toggle KWin composited geometry display" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Move) +#: windowgeometry/windowgeometry_config.ui:17 +#, kde-format +msgid "Display for moving windows" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Resize) +#: windowgeometry/windowgeometry_config.ui:24 +#, kde-format +msgid "Display for resizing windows" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, advancedGroup) +#: wobblywindows/wobblywindows_config.ui:20 +#, kde-format +msgid "Advanced" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: wobblywindows/wobblywindows_config.ui:26 +#, kde-format +msgid "&Stiffness:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: wobblywindows/wobblywindows_config.ui:68 +#, kde-format +msgid "Dra&g:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: wobblywindows/wobblywindows_config.ui:81 +#, kde-format +msgid "&Move factor:" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_MoveWobble) +#: wobblywindows/wobblywindows_config.ui:155 +#, kde-format +msgid "Wo&bble when moving" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ResizeWobble) +#: wobblywindows/wobblywindows_config.ui:162 +#, kde-format +msgid "Wobble when &resizing" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AdvancedMode) +#: wobblywindows/wobblywindows_config.ui:182 +#, kde-format +msgid "Enable &advanced mode" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, basicGroup) +#: wobblywindows/wobblywindows_config.ui:192 +#, kde-format +msgid "&Wobbliness" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: wobblywindows/wobblywindows_config.ui:201 +#, kde-format +msgid "Less" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: wobblywindows/wobblywindows_config.ui:224 +#, kde-format +msgid "More" +msgstr "" + +#: zoom/zoom.cpp:74 +#, kde-format +msgid "Move Zoomed Area to Left" +msgstr "" + +#: zoom/zoom.cpp:82 +#, kde-format +msgid "Move Zoomed Area to Right" +msgstr "" + +#: zoom/zoom.cpp:90 +#, kde-format +msgid "Move Zoomed Area Upwards" +msgstr "" + +#: zoom/zoom.cpp:98 +#, kde-format +msgid "Move Zoomed Area Downwards" +msgstr "" + +#: zoom/zoom.cpp:107 zoom/zoom_config.cpp:109 +#, kde-format +msgid "Move Mouse to Focus" +msgstr "" + +#: zoom/zoom.cpp:115 zoom/zoom_config.cpp:116 +#, kde-format +msgid "Move Mouse to Center" +msgstr "" + +#: zoom/zoom_config.cpp:81 +#, fuzzy, kde-format +#| msgid "Top-left" +msgid "Move Left" +msgstr "Верхні левы" + +#: zoom/zoom_config.cpp:88 +#, fuzzy, kde-format +#| msgid "Top-right" +msgid "Move Right" +msgstr "Правы верхні" + +#: zoom/zoom_config.cpp:95 +#, kde-format +msgid "Move Up" +msgstr "" + +#: zoom/zoom_config.cpp:102 +#, kde-format +msgid "Move Down" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QLabel, label) +#. i18n: ectx: property (whatsThis), widget (QDoubleSpinBox, kcfg_ZoomFactor) +#: zoom/zoom_config.ui:25 zoom/zoom_config.ui:41 +#, kde-format +msgid "On zoom-in and zoom-out change the zoom by the defined zoom-factor." +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: zoom/zoom_config.ui:28 +#, kde-format +msgid "Zoom Factor:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_EnableFocusTracking) +#: zoom/zoom_config.ui:66 +#, kde-format +msgid "" +"Enable tracking of the focused location. This needs QAccessible to be " +"enabled per application (\"export QT_LINUX_ACCESSIBILITY_ALWAYS_ON=1\")." +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_EnableFocusTracking) +#: zoom/zoom_config.ui:69 +#, kde-format +msgid "Enable Focus Tracking" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_EnableTextCaretTracking) +#: zoom/zoom_config.ui:76 +#, kde-format +msgid "" +"Enable tracking of the text cursor. This needs QAccessible to be enabled per " +"application (\"export QT_LINUX_ACCESSIBILITY_ALWAYS_ON=1\")." +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_EnableTextCaretTracking) +#: zoom/zoom_config.ui:79 +#, kde-format +msgid "Enable Text Cursor Tracking" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: zoom/zoom_config.ui:86 +#, kde-format +msgid "Mouse Pointer:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QComboBox, kcfg_MousePointer) +#: zoom/zoom_config.ui:99 +#, kde-format +msgid "Visibility of the mouse-pointer." +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_MousePointer) +#: zoom/zoom_config.ui:103 +#, kde-format +msgid "Scale" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_MousePointer) +#: zoom/zoom_config.ui:108 +#, kde-format +msgid "Keep" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_MousePointer) +#: zoom/zoom_config.ui:113 +#, kde-format +msgid "Hide" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QComboBox, kcfg_MouseTracking) +#: zoom/zoom_config.ui:121 +#, kde-format +msgid "Track moving of the mouse." +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_MouseTracking) +#: zoom/zoom_config.ui:125 +#, kde-format +msgid "Proportional" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_MouseTracking) +#: zoom/zoom_config.ui:130 +#, kde-format +msgid "Centered" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_MouseTracking) +#: zoom/zoom_config.ui:135 +#, kde-format +msgid "Push" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_MouseTracking) +#: zoom/zoom_config.ui:140 +#, kde-format +msgid "Disabled" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: zoom/zoom_config.ui:148 +#, kde-format +msgid "Mouse Tracking:" +msgstr "" \ No newline at end of file diff --git a/po/be@latin/kwin.po b/po/be@latin/kwin.po new file mode 100644 index 0000000..cc089a4 --- /dev/null +++ b/po/be@latin/kwin.po @@ -0,0 +1,2630 @@ +# translation of kwin.po to Belarusian Latin +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Ihar Hrachyshka , 2008. +msgid "" +msgstr "" +"Project-Id-Version: kwin\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-11-25 08:45+0100\n" +"PO-Revision-Date: 2008-12-27 14:55+0200\n" +"Last-Translator: Ihar Hrachyshka \n" +"Language-Team: Belarusian Latin \n" +"Language: be@latin\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: KBabel 1.11.4\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Ihar Hračyška" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "ihar.hrachyshka@gmail.com" + +#: abstract_client.cpp:2729 +#, kde-format +msgctxt "Application is not responding, appended to window title" +msgid "(Not Responding)" +msgstr "" + +#: abstract_wayland_output.cpp:252 +#, kde-format +msgid "unknown" +msgstr "" + +#: colorcorrection/manager.cpp:58 +#, kde-format +msgctxt "Night Color was disabled" +msgid "Night Color Off" +msgstr "" + +#: colorcorrection/manager.cpp:59 +#, kde-format +msgctxt "Night Color was enabled" +msgid "Night Color On" +msgstr "" + +#: colorcorrection/manager.cpp:228 colorcorrection/manager.cpp:231 +#: colorcorrection/manager.cpp:238 +#, kde-format +msgid "Toggle Night Color" +msgstr "" + +#: composite.cpp:926 +#, kde-format +msgid "" +"Desktop effects have been suspended by another application.
You can " +"resume using the '%1' shortcut." +msgstr "" + +#: debug_console.cpp:65 +#, kde-format +msgid "Timestamp" +msgstr "" + +#: debug_console.cpp:70 +#, kde-format +msgid "Timestamp (µsec)" +msgstr "" + +#: debug_console.cpp:77 +#, fuzzy, kde-format +#| msgid "Top-Left" +msgctxt "A mouse button" +msgid "Left" +msgstr "Uviersie źleva" + +#: debug_console.cpp:79 +#, fuzzy, kde-format +#| msgid "Top-Right" +msgctxt "A mouse button" +msgid "Right" +msgstr "Uviersie sprava" + +#: debug_console.cpp:81 +#, kde-format +msgctxt "A mouse button" +msgid "Middle" +msgstr "" + +#: debug_console.cpp:83 +#, kde-format +msgctxt "A mouse button" +msgid "Back" +msgstr "" + +#: debug_console.cpp:85 +#, kde-format +msgctxt "A mouse button" +msgid "Forward" +msgstr "" + +#: debug_console.cpp:87 +#, kde-format +msgctxt "A mouse button" +msgid "Task" +msgstr "" + +#: debug_console.cpp:89 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 4" +msgstr "" + +#: debug_console.cpp:91 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 5" +msgstr "" + +#: debug_console.cpp:93 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 6" +msgstr "" + +#: debug_console.cpp:95 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 7" +msgstr "" + +#: debug_console.cpp:97 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 8" +msgstr "" + +#: debug_console.cpp:99 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 9" +msgstr "" + +#: debug_console.cpp:101 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 10" +msgstr "" + +#: debug_console.cpp:103 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 11" +msgstr "" + +#: debug_console.cpp:105 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 12" +msgstr "" + +#: debug_console.cpp:107 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 13" +msgstr "" + +#: debug_console.cpp:109 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 14" +msgstr "" + +#: debug_console.cpp:111 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 15" +msgstr "" + +#: debug_console.cpp:113 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 16" +msgstr "" + +#: debug_console.cpp:115 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 17" +msgstr "" + +#: debug_console.cpp:117 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 18" +msgstr "" + +#: debug_console.cpp:119 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 19" +msgstr "" + +#: debug_console.cpp:121 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 20" +msgstr "" + +#: debug_console.cpp:123 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 21" +msgstr "" + +#: debug_console.cpp:125 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 22" +msgstr "" + +#: debug_console.cpp:127 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 23" +msgstr "" + +#: debug_console.cpp:129 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 24" +msgstr "" + +#: debug_console.cpp:138 debug_console.cpp:140 +#, kde-format +msgid "Input Device" +msgstr "" + +#: debug_console.cpp:138 +#, kde-format +msgctxt "The input device of the event is not known" +msgid "Unknown" +msgstr "" + +#: debug_console.cpp:175 +#, kde-format +msgctxt "A mouse pointer motion event" +msgid "Pointer Motion" +msgstr "" + +#: debug_console.cpp:182 +#, kde-format +msgctxt "The relative mouse movement" +msgid "Delta" +msgstr "" + +#: debug_console.cpp:186 +#, kde-format +msgctxt "The relative mouse movement" +msgid "Delta (not accelerated)" +msgstr "" + +#: debug_console.cpp:189 +#, kde-format +msgctxt "The global mouse pointer position" +msgid "Global Position" +msgstr "" + +#: debug_console.cpp:193 +#, kde-format +msgctxt "A mouse pointer button press event" +msgid "Pointer Button Press" +msgstr "" + +#: debug_console.cpp:196 debug_console.cpp:204 +#, kde-format +msgctxt "A button in a mouse press/release event" +msgid "Button" +msgstr "" + +#: debug_console.cpp:197 debug_console.cpp:205 +#, kde-format +msgctxt "A button in a mouse press/release event" +msgid "Native Button code" +msgstr "" + +#: debug_console.cpp:198 debug_console.cpp:206 +#, kde-format +msgctxt "All currently pressed buttons in a mouse press/release event" +msgid "Pressed Buttons" +msgstr "" + +#: debug_console.cpp:201 +#, kde-format +msgctxt "A mouse pointer button release event" +msgid "Pointer Button Release" +msgstr "" + +#: debug_console.cpp:221 +#, kde-format +msgctxt "A mouse pointer axis (wheel) event" +msgid "Pointer Axis" +msgstr "" + +#: debug_console.cpp:225 +#, kde-format +msgctxt "The orientation of a pointer axis event" +msgid "Orientation" +msgstr "" + +#: debug_console.cpp:226 +#, kde-format +msgctxt "An orientation of a pointer axis event" +msgid "Horizontal" +msgstr "" + +#: debug_console.cpp:227 +#, kde-format +msgctxt "An orientation of a pointer axis event" +msgid "Vertical" +msgstr "" + +#: debug_console.cpp:228 +#, kde-format +msgctxt "The angle delta of a pointer axis event" +msgid "Delta" +msgstr "" + +#: debug_console.cpp:243 +#, kde-format +msgctxt "A key press event" +msgid "Key Press" +msgstr "" + +#: debug_console.cpp:246 +#, kde-format +msgctxt "A key release event" +msgid "Key Release" +msgstr "" + +#: debug_console.cpp:255 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Shift" +msgstr "" + +#: debug_console.cpp:259 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Control" +msgstr "" + +#: debug_console.cpp:263 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Alt" +msgstr "" + +#: debug_console.cpp:267 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Meta" +msgstr "" + +#: debug_console.cpp:271 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Keypad" +msgstr "" + +#: debug_console.cpp:275 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Group-switch" +msgstr "" + +#: debug_console.cpp:281 +#, kde-format +msgctxt "Whether the event is an automatic key repeat" +msgid "Repeat" +msgstr "" + +#: debug_console.cpp:285 +#, kde-format +msgctxt "The code as read from the input device" +msgid "Scan code" +msgstr "" + +#: debug_console.cpp:286 +#, kde-format +msgctxt "Key according to Qt" +msgid "Qt::Key code" +msgstr "" + +#: debug_console.cpp:288 +#, kde-format +msgctxt "The translated code to an Xkb symbol" +msgid "Xkb symbol" +msgstr "" + +#: debug_console.cpp:289 +#, kde-format +msgctxt "The translated code interpreted as text" +msgid "Utf8" +msgstr "" + +#: debug_console.cpp:290 +#, kde-format +msgctxt "The currently active modifiers" +msgid "Modifiers" +msgstr "" + +#: debug_console.cpp:302 +#, kde-format +msgctxt "A touch down event" +msgid "Touch down" +msgstr "" + +#: debug_console.cpp:304 debug_console.cpp:319 debug_console.cpp:334 +#, kde-format +msgctxt "The id of the touch point in the touch event" +msgid "Point identifier" +msgstr "" + +#: debug_console.cpp:305 debug_console.cpp:320 +#, kde-format +msgctxt "The global position of the touch point" +msgid "Global position" +msgstr "" + +#: debug_console.cpp:317 +#, kde-format +msgctxt "A touch motion event" +msgid "Touch Motion" +msgstr "" + +#: debug_console.cpp:332 +#, kde-format +msgctxt "A touch up event" +msgid "Touch Up" +msgstr "" + +#: debug_console.cpp:345 +#, kde-format +msgctxt "A pinch gesture is started" +msgid "Pinch start" +msgstr "" + +#: debug_console.cpp:347 +#, kde-format +msgctxt "Number of fingers in this pinch gesture" +msgid "Finger count" +msgstr "" + +#: debug_console.cpp:358 +#, kde-format +msgctxt "A pinch gesture is updated" +msgid "Pinch update" +msgstr "" + +#: debug_console.cpp:360 +#, kde-format +msgctxt "Current scale in pinch gesture" +msgid "Scale" +msgstr "" + +#: debug_console.cpp:361 +#, kde-format +msgctxt "Current angle in pinch gesture" +msgid "Angle delta" +msgstr "" + +#: debug_console.cpp:362 +#, kde-format +msgctxt "Current delta in pinch gesture" +msgid "Delta x" +msgstr "" + +#: debug_console.cpp:363 +#, kde-format +msgctxt "Current delta in pinch gesture" +msgid "Delta y" +msgstr "" + +#: debug_console.cpp:374 +#, kde-format +msgctxt "A pinch gesture ended" +msgid "Pinch end" +msgstr "" + +#: debug_console.cpp:386 +#, kde-format +msgctxt "A pinch gesture got cancelled" +msgid "Pinch cancelled" +msgstr "" + +#: debug_console.cpp:398 +#, kde-format +msgctxt "A swipe gesture is started" +msgid "Swipe start" +msgstr "" + +#: debug_console.cpp:400 +#, kde-format +msgctxt "Number of fingers in this swipe gesture" +msgid "Finger count" +msgstr "" + +#: debug_console.cpp:411 +#, kde-format +msgctxt "A swipe gesture is updated" +msgid "Swipe update" +msgstr "" + +#: debug_console.cpp:413 +#, kde-format +msgctxt "Current delta in swipe gesture" +msgid "Delta x" +msgstr "" + +#: debug_console.cpp:414 +#, kde-format +msgctxt "Current delta in swipe gesture" +msgid "Delta y" +msgstr "" + +#: debug_console.cpp:425 +#, kde-format +msgctxt "A swipe gesture ended" +msgid "Swipe end" +msgstr "" + +#: debug_console.cpp:437 +#, kde-format +msgctxt "A swipe gesture got cancelled" +msgid "Swipe cancelled" +msgstr "" + +#: debug_console.cpp:449 +#, fuzzy, kde-format +#| msgid "Switch to Screen 0" +msgctxt "A hardware switch (e.g. notebook lid) got toggled" +msgid "Switch toggled" +msgstr "Uklučy ekran „0”" + +#: debug_console.cpp:457 +#, kde-format +msgctxt "Name of a hardware switch" +msgid "Notebook lid" +msgstr "" + +#: debug_console.cpp:459 +#, kde-format +msgctxt "Name of a hardware switch" +msgid "Tablet mode" +msgstr "" + +#: debug_console.cpp:461 +#, fuzzy, kde-format +#| msgid "Switch to Screen 0" +msgctxt "A hardware switch" +msgid "Switch" +msgstr "Uklučy ekran „0”" + +#: debug_console.cpp:465 +#, kde-format +msgctxt "The hardware switch got turned off" +msgid "Off" +msgstr "" + +#: debug_console.cpp:468 +#, kde-format +msgctxt "The hardware switch got turned on" +msgid "On" +msgstr "" + +#: debug_console.cpp:473 +#, kde-format +msgctxt "State of a hardware switch (on/off)" +msgid "State" +msgstr "" + +#: debug_console.cpp:488 +#, kde-format +msgid "Tablet Tool" +msgstr "" + +#: debug_console.cpp:489 +#, kde-format +msgid "EventType" +msgstr "" + +#: debug_console.cpp:490 debug_console.cpp:537 debug_console.cpp:549 +#, kde-format +msgid "Position" +msgstr "" + +#: debug_console.cpp:492 +#, kde-format +msgid "Tilt" +msgstr "" + +#: debug_console.cpp:494 +#, kde-format +msgid "Rotation" +msgstr "" + +#: debug_console.cpp:495 +#, kde-format +msgid "Pressure" +msgstr "" + +#: debug_console.cpp:496 +#, fuzzy, kde-format +#| msgid "Mouse Emulation" +msgid "Buttons" +msgstr "Emulacyja myšy" + +#. i18n: ectx: property (title), widget (QGroupBox, modifiersBox) +#: debug_console.cpp:497 debug_console.ui:356 +#, kde-format +msgid "Modifiers" +msgstr "" + +#: debug_console.cpp:510 +#, kde-format +msgid "Tablet Tool Button" +msgstr "" + +#: debug_console.cpp:511 debug_console.cpp:526 +#, fuzzy, kde-format +#| msgid "Mouse Emulation" +msgid "Pressed Buttons" +msgstr "Emulacyja myšy" + +#: debug_console.cpp:525 +#, kde-format +msgid "Tablet Pad Button" +msgstr "" + +#: debug_console.cpp:535 +#, kde-format +msgid "Tablet Pad Strip" +msgstr "" + +#: debug_console.cpp:536 debug_console.cpp:548 +#, kde-format +msgid "Number" +msgstr "" + +#: debug_console.cpp:538 debug_console.cpp:550 +#, kde-format +msgid "isFinger" +msgstr "" + +#: debug_console.cpp:547 +#, kde-format +msgid "Tablet Pad Ring" +msgstr "" + +#: debug_console.cpp:735 +#, fuzzy, kde-format +#| msgid "Mouse Emulation" +msgid "No Mouse Buttons" +msgstr "Emulacyja myšy" + +#: debug_console.cpp:739 +#, kde-format +msgctxt "Mouse Button" +msgid "left" +msgstr "" + +#: debug_console.cpp:742 +#, fuzzy, kde-format +#| msgid "Top-Right" +msgctxt "Mouse Button" +msgid "right" +msgstr "Uviersie sprava" + +#: debug_console.cpp:745 +#, kde-format +msgctxt "Mouse Button" +msgid "middle" +msgstr "" + +#: debug_console.cpp:748 +#, kde-format +msgctxt "Mouse Button" +msgid "back" +msgstr "" + +#: debug_console.cpp:751 +#, kde-format +msgctxt "Mouse Button" +msgid "forward" +msgstr "" + +#: debug_console.cpp:754 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 1" +msgstr "" + +#: debug_console.cpp:757 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 2" +msgstr "" + +#: debug_console.cpp:760 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 3" +msgstr "" + +#: debug_console.cpp:763 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 4" +msgstr "" + +#: debug_console.cpp:766 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 5" +msgstr "" + +#: debug_console.cpp:769 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 6" +msgstr "" + +#: debug_console.cpp:772 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 7" +msgstr "" + +#: debug_console.cpp:775 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 8" +msgstr "" + +#: debug_console.cpp:778 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 9" +msgstr "" + +#: debug_console.cpp:781 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 10" +msgstr "" + +#: debug_console.cpp:784 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 11" +msgstr "" + +#: debug_console.cpp:787 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 12" +msgstr "" + +#: debug_console.cpp:790 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 13" +msgstr "" + +#: debug_console.cpp:793 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 14" +msgstr "" + +#: debug_console.cpp:796 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 15" +msgstr "" + +#: debug_console.cpp:799 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 16" +msgstr "" + +#: debug_console.cpp:802 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 17" +msgstr "" + +#: debug_console.cpp:805 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 18" +msgstr "" + +#: debug_console.cpp:808 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 19" +msgstr "" + +#: debug_console.cpp:811 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 20" +msgstr "" + +#: debug_console.cpp:814 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 21" +msgstr "" + +#: debug_console.cpp:817 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 22" +msgstr "" + +#: debug_console.cpp:820 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 23" +msgstr "" + +#: debug_console.cpp:823 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 24" +msgstr "" + +#: debug_console.cpp:826 +#, kde-format +msgctxt "Mouse Button" +msgid "task" +msgstr "" + +#: debug_console.cpp:1176 +#, fuzzy, kde-format +#| msgid "Close Window" +msgid "X11 Client Windows" +msgstr "Začyni akno" + +#: debug_console.cpp:1178 +#, kde-format +msgid "X11 Unmanaged Windows" +msgstr "" + +#: debug_console.cpp:1180 +#, fuzzy, kde-format +#| msgid "Shade Window" +msgid "Wayland Windows" +msgstr "Zharni akno ŭ zahałovak" + +#: debug_console.cpp:1182 +#, fuzzy, kde-format +#| msgid "Lower Window" +msgid "Internal Windows" +msgstr "Źniž akno" + +#. i18n: ectx: property (text), widget (QPushButton, quitButton) +#: debug_console.ui:32 +#, kde-format +msgid "Quit Debug Console" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, windows) +#: debug_console.ui:45 +#, kde-format +msgid "Windows" +msgstr "Vokny" + +#. i18n: ectx: attribute (title), widget (QWidget, surfaces) +#: debug_console.ui:59 +#, kde-format +msgid "Surfaces" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, input) +#: debug_console.ui:69 +#, kde-format +msgid "Input Events" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, inputDevices) +#: debug_console.ui:86 +#, kde-format +msgid "Input Devices" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, tab) +#: debug_console.ui:96 +#, kde-format +msgid "OpenGL" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, noOpenGLLabel) +#: debug_console.ui:102 +#, kde-format +msgid "No OpenGL compositor running" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, driverInfoBox) +#: debug_console.ui:130 +#, kde-format +msgid "OpenGL (ES) driver information" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: debug_console.ui:136 +#, kde-format +msgid "Vendor:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: debug_console.ui:143 +#, kde-format +msgid "Renderer:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: debug_console.ui:150 +#, kde-format +msgid "Version:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: debug_console.ui:157 +#, kde-format +msgid "Shading Language Version:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: debug_console.ui:164 +#, kde-format +msgid "Driver:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: debug_console.ui:171 +#, kde-format +msgid "GPU class:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: debug_console.ui:178 +#, kde-format +msgid "OpenGL Version:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_8) +#: debug_console.ui:185 +#, kde-format +msgid "GLSL Version:" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, platformExtensionsBox) +#: debug_console.ui:251 +#, kde-format +msgid "Platform Extensions" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, glExtensionsBox) +#: debug_console.ui:267 +#, kde-format +msgid "OpenGL (ES) Extensions" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, keyboard) +#: debug_console.ui:288 +#, kde-format +msgid "Keyboard" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, layoutBox) +#: debug_console.ui:315 +#, kde-format +msgid "Keymap Layouts" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_9) +#: debug_console.ui:337 +#, fuzzy, kde-format +#| msgid "Configur&e Window Behavior..." +msgid "Current Layout:" +msgstr "&Naładź pavodziny akna..." + +#. i18n: ectx: property (title), widget (QGroupBox, activeModifiersBox) +#: debug_console.ui:372 +#, kde-format +msgid "Active Modifiers" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, ledsBox) +#: debug_console.ui:388 +#, kde-format +msgid "LEDs" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, activeLedsBox) +#: debug_console.ui:404 +#, kde-format +msgid "Active LEDs" +msgstr "" + +#: helpers/killer/killer.cpp:30 +#, fuzzy, kde-format +#| msgid "KDE window manager" +msgid "Window Manager" +msgstr "Akońnik dla „KDE”" + +#: helpers/killer/killer.cpp:35 +#, kde-format +msgid "PID of the application to terminate" +msgstr "PID aplikacyi, jakuju treba źniščyć" + +#: helpers/killer/killer.cpp:35 +#, kde-format +msgid "pid" +msgstr "" + +#: helpers/killer/killer.cpp:37 +#, kde-format +msgid "Hostname on which the application is running" +msgstr "Nazva hostu, dzie pracuje aplikacyja" + +#: helpers/killer/killer.cpp:37 +#, kde-format +msgid "hostname" +msgstr "" + +#: helpers/killer/killer.cpp:39 +#, kde-format +msgid "Caption of the window to be terminated" +msgstr "Nazva akna, jakoje treba źniščyć" + +#: helpers/killer/killer.cpp:39 +#, kde-format +msgid "caption" +msgstr "" + +#: helpers/killer/killer.cpp:41 +#, kde-format +msgid "Name of the application to be terminated" +msgstr "Nazva aplikacyi, jakuju treba źniščyć" + +#: helpers/killer/killer.cpp:41 +#, kde-format +msgid "name" +msgstr "" + +#: helpers/killer/killer.cpp:43 +#, kde-format +msgid "ID of resource belonging to the application" +msgstr "Identyfikatar resursu, jaki naležyć aplikacyi" + +#: helpers/killer/killer.cpp:43 +#, kde-format +msgid "id" +msgstr "" + +#: helpers/killer/killer.cpp:45 +#, kde-format +msgid "Time of user action causing termination" +msgstr "Čas aperacyi źniščeńnia, učynienaj karystańnikam" + +#: helpers/killer/killer.cpp:45 +#, kde-format +msgid "time" +msgstr "" + +#: helpers/killer/killer.cpp:47 +#, kde-format +msgid "KWin helper utility" +msgstr "Słužbovaja prahrama dla akońnika „KWin”" + +#: helpers/killer/killer.cpp:71 +#, kde-format +msgid "This helper utility is not supposed to be called directly." +msgstr "Hetuju słužbovuju prahramu nia varta ŭklučać biespasiarednie." + +#: helpers/killer/killer.cpp:81 +#, kde-format +msgctxt "@info" +msgid "Application \"%1\" is not responding" +msgstr "" + +#: helpers/killer/killer.cpp:83 +#, kde-kuit-format +msgctxt "@info" +msgid "" +"

You tried to close window \"%1\" from application \"%2\" (Process ID: %3) " +"but the application is not responding.

" +msgstr "" + +#: helpers/killer/killer.cpp:85 +#, kde-kuit-format +msgctxt "@info" +msgid "" +"

You tried to close window \"%1\" from application \"%2\" (Process ID: " +"%3), running on host \"%4\", but the application is not responding.

" +msgstr "" + +#: helpers/killer/killer.cpp:88 +#, kde-kuit-format +msgctxt "@info" +msgid "" +"

Do you want to terminate this application?

Terminating the " +"application will close all of its child windows. Any unsaved data will be " +"lost.

" +msgstr "" + +#: helpers/killer/killer.cpp:92 +#, kde-format +msgid "&Terminate Application %1" +msgstr "" + +#: helpers/killer/killer.cpp:93 +#, kde-format +msgid "Wait Longer" +msgstr "" + +#: keyboard_layout.cpp:110 keyboard_layout.cpp:111 +#, kde-format +msgctxt "tooltip title" +msgid "Keyboard Layout" +msgstr "" + +#: keyboard_layout.cpp:258 +#, fuzzy, kde-format +#| msgid "Configur&e Window Behavior..." +msgid "Configure Layouts..." +msgstr "&Naładź pavodziny akna..." + +#: killwindow.cpp:33 +#, kde-format +msgid "" +"Select window to force close with left click or enter.\n" +"Escape or right click to cancel." +msgstr "" + +#: kwinbindings.cpp:38 +#, kde-format +msgid "Window Operations Menu" +msgstr "Menu akonnych aperacyjaŭ" + +#: kwinbindings.cpp:40 +#, kde-format +msgid "Close Window" +msgstr "Začyni akno" + +#: kwinbindings.cpp:42 +#, kde-format +msgid "Maximize Window" +msgstr "Zmaksymalizuj akno" + +#: kwinbindings.cpp:44 +#, kde-format +msgid "Maximize Window Vertically" +msgstr "Zmaksymalizuj akno vertykalna" + +#: kwinbindings.cpp:46 +#, kde-format +msgid "Maximize Window Horizontally" +msgstr "Zmaksymalizuj akno haryzantalna" + +#: kwinbindings.cpp:48 +#, kde-format +msgid "Minimize Window" +msgstr "Źminimalizuj akno" + +#: kwinbindings.cpp:50 +#, kde-format +msgid "Shade Window" +msgstr "Zharni akno ŭ zahałovak" + +#: kwinbindings.cpp:52 +#, kde-format +msgid "Move Window" +msgstr "Pierasuń akno" + +#: kwinbindings.cpp:54 +#, kde-format +msgid "Resize Window" +msgstr "Źmiani pamiery akna" + +#: kwinbindings.cpp:56 +#, kde-format +msgid "Raise Window" +msgstr "Uźnimi akno" + +#: kwinbindings.cpp:58 +#, kde-format +msgid "Lower Window" +msgstr "Źniž akno" + +#: kwinbindings.cpp:60 +#, kde-format +msgid "Toggle Window Raise/Lower" +msgstr "Źmiani rovień akna" + +#: kwinbindings.cpp:62 +#, kde-format +msgid "Make Window Fullscreen" +msgstr "Razharni akno na ŭvieś ekran" + +#: kwinbindings.cpp:64 +#, kde-format +msgid "Hide Window Border" +msgstr "Schavaj abrys akna" + +#: kwinbindings.cpp:66 +#, kde-format +msgid "Keep Window Above Others" +msgstr "Uźnimi akno nad inšymi" + +#: kwinbindings.cpp:68 +#, kde-format +msgid "Keep Window Below Others" +msgstr "Źniž akno pad inšyja" + +#: kwinbindings.cpp:70 +#, kde-format +msgid "Activate Window Demanding Attention" +msgstr "Uklučy akno, jakoje vymahaje ŭvahi" + +#: kwinbindings.cpp:72 +#, kde-format +msgid "Setup Window Shortcut" +msgstr "Naładź skarot dla akna" + +#: kwinbindings.cpp:74 +#, kde-format +msgid "Pack Window to the Right" +msgstr "Vykładzi akno sprava" + +#: kwinbindings.cpp:76 +#, kde-format +msgid "Pack Window to the Left" +msgstr "Vykładzi akno źleva" + +#: kwinbindings.cpp:78 +#, kde-format +msgid "Pack Window Up" +msgstr "Vykładzi akno źvierchu" + +#: kwinbindings.cpp:80 +#, kde-format +msgid "Pack Window Down" +msgstr "Vykładzi akno źnizu" + +#: kwinbindings.cpp:82 +#, kde-format +msgid "Pack Grow Window Horizontally" +msgstr "Vykładzi akno, razharnuŭšy haryzantalna" + +#: kwinbindings.cpp:84 +#, kde-format +msgid "Pack Grow Window Vertically" +msgstr "Vykładzi akno, razharnuŭšy vertykalna" + +#: kwinbindings.cpp:86 +#, kde-format +msgid "Pack Shrink Window Horizontally" +msgstr "Vykładzi akno, pryharnuŭšy haryzantalna" + +#: kwinbindings.cpp:88 +#, kde-format +msgid "Pack Shrink Window Vertically" +msgstr "Vykładzi akno, pryharnuŭšy vertykalna" + +#: kwinbindings.cpp:90 +#, fuzzy, kde-format +#| msgid "Pack Window to the Left" +msgid "Quick Tile Window to the Left" +msgstr "Vykładzi akno źleva" + +#: kwinbindings.cpp:92 +#, fuzzy, kde-format +#| msgid "Pack Window to the Right" +msgid "Quick Tile Window to the Right" +msgstr "Vykładzi akno sprava" + +#: kwinbindings.cpp:94 +#, fuzzy, kde-format +#| msgid "Pack Window to the Left" +msgid "Quick Tile Window to the Top" +msgstr "Vykładzi akno źleva" + +#: kwinbindings.cpp:96 +#, fuzzy, kde-format +#| msgid "Pack Window to the Left" +msgid "Quick Tile Window to the Bottom" +msgstr "Vykładzi akno źleva" + +#: kwinbindings.cpp:98 +#, fuzzy, kde-format +#| msgid "Pack Window to the Left" +msgid "Quick Tile Window to the Top Left" +msgstr "Vykładzi akno źleva" + +#: kwinbindings.cpp:100 +#, fuzzy, kde-format +#| msgid "Pack Window to the Left" +msgid "Quick Tile Window to the Bottom Left" +msgstr "Vykładzi akno źleva" + +#: kwinbindings.cpp:102 +#, fuzzy, kde-format +#| msgid "Pack Window to the Right" +msgid "Quick Tile Window to the Top Right" +msgstr "Vykładzi akno sprava" + +#: kwinbindings.cpp:104 +#, fuzzy, kde-format +#| msgid "Pack Window to the Right" +msgid "Quick Tile Window to the Bottom Right" +msgstr "Vykładzi akno sprava" + +#: kwinbindings.cpp:106 +#, fuzzy, kde-format +#| msgid "Switch to Screen 0" +msgid "Switch to Window Above" +msgstr "Uklučy ekran „0”" + +#: kwinbindings.cpp:108 +#, fuzzy, kde-format +#| msgid "Switch to Previous Desktop" +msgid "Switch to Window Below" +msgstr "Uklučy papiaredni stoł" + +#: kwinbindings.cpp:110 +#, fuzzy, kde-format +#| msgid "Pack Window to the Right" +msgid "Switch to Window to the Right" +msgstr "Vykładzi akno sprava" + +#: kwinbindings.cpp:112 +#, fuzzy, kde-format +#| msgid "Pack Window to the Left" +msgid "Switch to Window to the Left" +msgstr "Vykładzi akno źleva" + +#: kwinbindings.cpp:114 +#, kde-format +msgid "Increase Opacity of Active Window by 5 %" +msgstr "" + +#: kwinbindings.cpp:116 +#, kde-format +msgid "Decrease Opacity of Active Window by 5 %" +msgstr "" + +#: kwinbindings.cpp:119 +#, kde-format +msgid "Keep Window on All Desktops" +msgstr "Pakazvaj akno na ŭsich rabočych stałach" + +#: kwinbindings.cpp:123 +#, fuzzy, kde-format +#| msgid "Window to Desktop 1" +msgid "Window to Desktop %1" +msgstr "Akno na stale „1”" + +#: kwinbindings.cpp:125 +#, kde-format +msgid "Window to Next Desktop" +msgstr "Akno na nastupny stoł" + +#: kwinbindings.cpp:126 +#, kde-format +msgid "Window to Previous Desktop" +msgstr "Akno na papieredni stoł" + +#: kwinbindings.cpp:127 +#, kde-format +msgid "Window One Desktop to the Right" +msgstr "Akno na praviejšy stoł" + +#: kwinbindings.cpp:128 +#, kde-format +msgid "Window One Desktop to the Left" +msgstr "Akno na laviejšy stoł" + +#: kwinbindings.cpp:129 +#, kde-format +msgid "Window One Desktop Up" +msgstr "Akno na vyšejšy stoł" + +#: kwinbindings.cpp:130 +#, kde-format +msgid "Window One Desktop Down" +msgstr "Akno na nižejšy stoł" + +#: kwinbindings.cpp:133 +#, fuzzy, kde-format +#| msgid "Window to Screen 1" +msgid "Window to Screen %1" +msgstr "Akno na ekran „1”" + +#: kwinbindings.cpp:135 +#, kde-format +msgid "Window to Next Screen" +msgstr "Akno na nastupny ekran" + +#: kwinbindings.cpp:136 +#, fuzzy, kde-format +#| msgid "Window to Previous Desktop" +msgid "Window to Previous Screen" +msgstr "Akno na papieredni stoł" + +#: kwinbindings.cpp:137 +#, fuzzy, kde-format +#| msgid "Show Desktop Grid" +msgid "Show Desktop" +msgstr "Sietka stała" + +#: kwinbindings.cpp:140 +#, fuzzy, kde-format +#| msgid "Switch to Screen 1" +msgid "Switch to Screen %1" +msgstr "Uklučy ekran „1”" + +#: kwinbindings.cpp:143 +#, kde-format +msgid "Switch to Next Screen" +msgstr "Uklučy nastupny ekran" + +#: kwinbindings.cpp:144 +#, fuzzy, kde-format +#| msgid "Switch to Previous Desktop" +msgid "Switch to Previous Screen" +msgstr "Uklučy papiaredni stoł" + +#: kwinbindings.cpp:146 +#, kde-format +msgid "Kill Window" +msgstr "Zabi akno" + +#: kwinbindings.cpp:147 +#, kde-format +msgid "Suspend Compositing" +msgstr "Ustrymaj kampazycyju" + +#: kwinbindings.cpp:148 +#, kde-format +msgid "Invert Screen Colors" +msgstr "" + +#: main.cpp:184 main.cpp:214 +#, kde-format +msgid "KDE window manager" +msgstr "Akońnik dla „KDE”" + +#: main.cpp:189 +#, kde-format +msgid "KWin" +msgstr "KWin" + +#: main.cpp:193 +#, fuzzy, kde-format +#| msgid "(c) 1999-2008, The KDE Developers" +msgid "(c) 1999-2019, The KDE Developers" +msgstr "@ 1999-2008, raspracoŭniki „KDE”" + +#: main.cpp:195 +#, kde-format +msgid "Matthias Ettrich" +msgstr "Matthias Ettrich" + +#: main.cpp:196 +#, kde-format +msgid "Cristian Tibirna" +msgstr "Cristian Tibirna" + +#: main.cpp:197 +#, kde-format +msgid "Daniel M. Duley" +msgstr "Daniel M. Duley" + +#: main.cpp:198 +#, kde-format +msgid "Luboš Luňák" +msgstr "Luboš Luňák" + +#: main.cpp:199 +#, kde-format +msgid "Martin Flöser" +msgstr "" + +#: main.cpp:200 +#, kde-format +msgid "David Edmundson" +msgstr "" + +#: main.cpp:201 +#, kde-format +msgid "Roman Gilg" +msgstr "" + +#: main.cpp:202 +#, kde-format +msgid "Vlad Zahorodnii" +msgstr "" + +#: main.cpp:211 +#, kde-format +msgid "Disable configuration options" +msgstr "Vyklučy opcyi naładaŭ" + +#: main.cpp:212 +#, kde-format +msgid "Indicate that KWin has recently crashed n times" +msgstr "Adznačaj, kali akońnik „KWin” niadaŭna łamaŭsia „n” razoŭ zapar" + +#: main_wayland.cpp:459 +#, kde-format +msgid "Start a rootless Xwayland server." +msgstr "" + +#: main_wayland.cpp:461 +#, kde-format +msgid "" +"Name of the Wayland socket to listen on. If not set \"wayland-0\" is used." +msgstr "" + +#: main_wayland.cpp:464 +#, kde-format +msgid "Render to framebuffer." +msgstr "" + +#: main_wayland.cpp:466 +#, kde-format +msgid "The framebuffer device to render to." +msgstr "" + +#: main_wayland.cpp:469 +#, kde-format +msgid "The X11 Display to use in windowed mode on platform X11." +msgstr "" + +#: main_wayland.cpp:472 +#, kde-format +msgid "The Wayland Display to use in windowed mode on platform Wayland." +msgstr "" + +#: main_wayland.cpp:474 +#, kde-format +msgid "Render to a virtual framebuffer." +msgstr "" + +#: main_wayland.cpp:476 +#, kde-format +msgid "The width for windowed mode. Default width is 1024." +msgstr "" + +#: main_wayland.cpp:480 +#, kde-format +msgid "The height for windowed mode. Default height is 768." +msgstr "" + +#: main_wayland.cpp:485 +#, kde-format +msgid "The scale for windowed mode. Default value is 1." +msgstr "" + +#: main_wayland.cpp:490 +#, kde-format +msgid "" +"The number of windows to open as outputs in windowed mode. Default value is 1" +msgstr "" + +#: main_wayland.cpp:520 +#, kde-format +msgid "Use libhybris hwcomposer" +msgstr "" + +#: main_wayland.cpp:526 +#, kde-format +msgid "" +"Enable libinput support for input events processing. Note: never use in a " +"nested session.\t(deprecated)" +msgstr "" + +#: main_wayland.cpp:529 +#, kde-format +msgid "Render through drm node." +msgstr "" + +#: main_wayland.cpp:536 +#, kde-format +msgid "Input method that KWin starts." +msgstr "" + +#: main_wayland.cpp:541 +#, kde-format +msgid "List all available backends and quit." +msgstr "" + +#: main_wayland.cpp:545 +#, kde-format +msgid "Starts the session in locked mode." +msgstr "" + +#: main_wayland.cpp:549 +#, kde-format +msgid "Starts the session without lock screen support." +msgstr "" + +#: main_wayland.cpp:553 +#, kde-format +msgid "Starts the session without global shortcuts support." +msgstr "" + +#: main_wayland.cpp:557 +#, kde-format +msgid "Exit after the session application, which is started by KWin, closed." +msgstr "" + +#: main_wayland.cpp:562 +#, kde-format +msgid "Applications to start once Wayland and Xwayland server are started" +msgstr "" + +#: main_x11.cpp:65 +#, kde-format +msgid "" +"KWin is unstable.\n" +"It seems to have crashed several times in a row.\n" +"You can select another window manager to run:" +msgstr "" +"Akońnik „KWin” zusim nie stabilny.\n" +"Jon złamaŭsia niekalki razoŭ zapar.\n" +"Ty možaš vybrać inšaha akońnika sa śpisu:" + +#: main_x11.cpp:224 +#, kde-format +msgid "" +"kwin: unable to claim manager selection, another wm running? (try using --" +"replace)\n" +msgstr "" +"kwin: nie ŭdałosia aznačyć siabie, jak dziejnaha akońnika. Niaŭžo ŭklučany " +"inšy akońnik? Tady pasprabuj opcyju „--replace”)\n" + +#: main_x11.cpp:241 +#, fuzzy, kde-format +#| msgid "" +#| "kwin: unable to claim manager selection, another wm running? (try using --" +#| "replace)\n" +msgid "kwin: another window manager is running (try using --replace)\n" +msgstr "" +"kwin: nie ŭdałosia aznačyć siabie, jak dziejnaha akońnika. Niaŭžo ŭklučany " +"inšy akońnik? Tady pasprabuj opcyju „--replace”)\n" + +#: main_x11.cpp:437 +#, kde-format +msgid "Replace already-running ICCCM2.0-compliant window manager" +msgstr "Zamiani inšaha akońnika, jaki absłuhoŭvaje standart „ICCCM2.0”" + +#: main_x11.cpp:444 +#, kde-format +msgid "Disable KActivities integration." +msgstr "" + +#: plugins/scenes/opengl/scene_opengl.cpp:535 +#, kde-format +msgid "Desktop effects were restarted due to a graphics reset" +msgstr "" + +#. i18n: ectx: label, entry (count), group (General) +#: rulebooksettingsbase.kcfg:9 +#, kde-format +msgid "Total rules count" +msgstr "" + +#. i18n: ectx: label, entry (description), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:10 +#, kde-format +msgid "Rule description" +msgstr "" + +#. i18n: ectx: label, entry (descriptionLegacy), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:13 +#, kde-format +msgid "Rule description (legacy)" +msgstr "" + +#. i18n: ectx: label, entry (DeleteRule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:16 +#, kde-format +msgid "Delete this rule (for use in imports)" +msgstr "" + +#. i18n: ectx: label, entry (wmclass), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:20 +#, kde-format +msgid "Window class (application)" +msgstr "" + +#. i18n: ectx: label, entry (wmclassmatch), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:23 +#, kde-format +msgid "Window class string match type" +msgstr "" + +#. i18n: ectx: label, entry (wmclasscomplete), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:29 +#, kde-format +msgid "Match whole window class" +msgstr "" + +#. i18n: ectx: label, entry (windowrole), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:34 +#, fuzzy, kde-format +#| msgid "Windows" +msgid "Window role" +msgstr "Vokny" + +#. i18n: ectx: label, entry (windowrolematch), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:37 +#, kde-format +msgid "Window role string match type" +msgstr "" + +#. i18n: ectx: label, entry (title), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:44 +#, fuzzy, kde-format +#| msgid "Windows" +msgid "Window title" +msgstr "Vokny" + +#. i18n: ectx: label, entry (titlematch), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:47 +#, kde-format +msgid "Window title string match type" +msgstr "" + +#. i18n: ectx: label, entry (clientmachine), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:54 +#, kde-format +msgid "Machine (hostname)" +msgstr "" + +#. i18n: ectx: label, entry (clientmachinematch), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:57 +#, kde-format +msgid "Machine string match type" +msgstr "" + +#. i18n: ectx: label, entry (types), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:64 +#, kde-format +msgid "Window types that match" +msgstr "" + +#. i18n: ectx: label, entry (placement), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:69 +#, kde-format +msgid "Initial placement" +msgstr "" + +#. i18n: ectx: label, entry (placementrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:74 +#, fuzzy, kde-format +#| msgid "&Fullscreen" +msgid "Initial placement rule type" +msgstr "&Na ŭvieś ekran" + +#. i18n: ectx: label, entry (position), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:79 +#, fuzzy, kde-format +#| msgid "Window Operations Menu" +msgid "Window position" +msgstr "Menu akonnych aperacyjaŭ" + +#. i18n: ectx: label, entry (positionrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:83 +#, fuzzy, kde-format +#| msgid "Window to Screen 0" +msgid "Window position rule type" +msgstr "Akno na ekran „0”" + +#. i18n: ectx: label, entry (size), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:90 +#, fuzzy, kde-format +#| msgid "Windows" +msgid "Window size" +msgstr "Vokny" + +#. i18n: ectx: label, entry (sizerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:93 +#, kde-format +msgid "Window size rule type" +msgstr "" + +#. i18n: ectx: label, entry (minsize), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:100 +#, kde-format +msgid "Window minimum size" +msgstr "" + +#. i18n: ectx: label, entry (minsizerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:104 +#, kde-format +msgid "Window minimum size rule type" +msgstr "" + +#. i18n: ectx: label, entry (maxsize), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:109 +#, kde-format +msgid "Window maximum size" +msgstr "" + +#. i18n: ectx: label, entry (maxsizerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:113 +#, kde-format +msgid "Window maximum size rule type" +msgstr "" + +#. i18n: ectx: label, entry (opacityactive), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:118 +#, kde-format +msgid "Active opacity" +msgstr "" + +#. i18n: ectx: label, entry (opacityactiverule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:124 +#, kde-format +msgid "Active opacity rule type" +msgstr "" + +#. i18n: ectx: label, entry (opacityinactive), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:129 +#, kde-format +msgid "Inactive opacity" +msgstr "" + +#. i18n: ectx: label, entry (opacityinactiverule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:135 +#, kde-format +msgid "Inactive opacity rule type" +msgstr "" + +#. i18n: ectx: label, entry (ignoregeometry), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:140 +#, kde-format +msgid "Ignore requested geometry" +msgstr "" + +#. i18n: ectx: label, entry (ignoregeometryrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:144 +#, kde-format +msgid "Ignore requested geometry rule type" +msgstr "" + +#. i18n: ectx: label, entry (desktop), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:151 +#, fuzzy, kde-format +#| msgid "Desktop Cube" +msgid "Desktop number" +msgstr "Rabočy kub" + +#. i18n: ectx: label, entry (desktoprule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:155 +#, fuzzy, kde-format +#| msgid "Desktop Cube" +msgid "Desktop number rule type" +msgstr "Rabočy kub" + +#. i18n: ectx: label, entry (screen), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:162 +#, kde-format +msgid "Screen number" +msgstr "" + +#. i18n: ectx: label, entry (screenrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:166 +#, kde-format +msgid "Screen number rule type" +msgstr "" + +#. i18n: ectx: label, entry (activity), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:173 +#, kde-format +msgid "Activity" +msgstr "" + +#. i18n: ectx: label, entry (activityrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:176 +#, kde-format +msgid "Activity rule type" +msgstr "" + +#. i18n: ectx: label, entry (type), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:183 +#, fuzzy, kde-format +#| msgid "Setup Window Shortcut" +msgid "Set window type to" +msgstr "Naładź skarot dla akna" + +#. i18n: ectx: label, entry (typerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:189 +#, kde-format +msgid "Set window type rule type" +msgstr "" + +#. i18n: ectx: label, entry (maximizevert), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:194 +#, fuzzy, kde-format +#| msgid "Maximize Window Vertically" +msgid "Maximized vertically" +msgstr "Zmaksymalizuj akno vertykalna" + +#. i18n: ectx: label, entry (maximizevertrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:198 +#, fuzzy, kde-format +#| msgid "Maximize Window Vertically" +msgid "Maximized vertically rule type" +msgstr "Zmaksymalizuj akno vertykalna" + +#. i18n: ectx: label, entry (maximizehoriz), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:205 +#, fuzzy, kde-format +#| msgid "Maximize Window Horizontally" +msgid "Maximized horizontally" +msgstr "Zmaksymalizuj akno haryzantalna" + +#. i18n: ectx: label, entry (maximizehorizrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:209 +#, fuzzy, kde-format +#| msgid "Maximize Window Horizontally" +msgid "Maximized horizontally rule type" +msgstr "Zmaksymalizuj akno haryzantalna" + +#. i18n: ectx: label, entry (minimize), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:216 +#, fuzzy, kde-format +#| msgid "Minimize" +msgid "Minimized" +msgstr "Źminimalizuj" + +#. i18n: ectx: label, entry (minimizerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:220 +#, kde-format +msgid "Minimized rule type" +msgstr "" + +#. i18n: ectx: label, entry (shade), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:227 +#, fuzzy, kde-format +#| msgid "Shade" +msgid "Shaded" +msgstr "Zharni ŭ zahałovak" + +#. i18n: ectx: label, entry (shaderule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:231 +#, kde-format +msgid "Shaded rule type" +msgstr "" + +#. i18n: ectx: label, entry (skiptaskbar), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:238 +#, kde-format +msgid "Skip taskbar" +msgstr "" + +#. i18n: ectx: label, entry (skiptaskbarrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:242 +#, kde-format +msgid "Skip taskbar rule type" +msgstr "" + +#. i18n: ectx: label, entry (skippager), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:249 +#, kde-format +msgid "Skip pager" +msgstr "" + +#. i18n: ectx: label, entry (skippagerrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:253 +#, kde-format +msgid "Skip pager rule type" +msgstr "" + +#. i18n: ectx: label, entry (skipswitcher), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:260 +#, fuzzy, kde-format +#| msgid "Switch to Screen 0" +msgid "Skip switcher" +msgstr "Uklučy ekran „0”" + +#. i18n: ectx: label, entry (skipswitcherrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:264 +#, kde-format +msgid "Skip switcher rule type" +msgstr "" + +#. i18n: ectx: label, entry (above), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:271 +#, fuzzy, kde-format +#| msgid "Keep above others" +msgid "Keep above" +msgstr "Uźnimi nad inšymi" + +#. i18n: ectx: label, entry (aboverule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:275 +#, fuzzy, kde-format +#| msgid "Keep above others" +msgid "Keep above rule type" +msgstr "Uźnimi nad inšymi" + +#. i18n: ectx: label, entry (below), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:282 +#, fuzzy, kde-format +#| msgid "Keep below others" +msgid "Keep below" +msgstr "Źniž pad inšyja" + +#. i18n: ectx: label, entry (belowrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:286 +#, fuzzy, kde-format +#| msgid "Keep below others" +msgid "Keep below rule type" +msgstr "Źniž pad inšyja" + +#. i18n: ectx: label, entry (fullscreen), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:293 +#, fuzzy, kde-format +#| msgid "&Fullscreen" +msgid "Fullscreen" +msgstr "&Na ŭvieś ekran" + +#. i18n: ectx: label, entry (fullscreenrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:297 +#, fuzzy, kde-format +#| msgid "&Fullscreen" +msgid "Fullscreen rule type" +msgstr "&Na ŭvieś ekran" + +#. i18n: ectx: label, entry (noborder), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:304 +#, kde-format +msgid "No titlebar and frame" +msgstr "" + +#. i18n: ectx: label, entry (noborderrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:308 +#, kde-format +msgid "No titlebar rule type" +msgstr "" + +#. i18n: ectx: label, entry (decocolor), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:315 +#, kde-format +msgid "Titlebar color and scheme" +msgstr "" + +#. i18n: ectx: label, entry (decocolorrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:318 +#, kde-format +msgid "Titlebar color rule type" +msgstr "" + +#. i18n: ectx: label, entry (blockcompositing), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:323 +#, fuzzy, kde-format +#| msgid "Suspend Compositing" +msgid "Block Compositing" +msgstr "Ustrymaj kampazycyju" + +#. i18n: ectx: label, entry (blockcompositingrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:327 +#, kde-format +msgid "Block Compositing rule type" +msgstr "" + +#. i18n: ectx: label, entry (fsplevel), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:332 +#, kde-format +msgid "Focus stealing prevention" +msgstr "" + +#. i18n: ectx: label, entry (fsplevelrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:338 +#, kde-format +msgid "Focus stealing prevention rule type" +msgstr "" + +#. i18n: ectx: label, entry (fpplevel), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:343 +#, kde-format +msgid "Focus protection" +msgstr "" + +#. i18n: ectx: label, entry (fpplevelrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:349 +#, kde-format +msgid "Focus protection rule type" +msgstr "" + +#. i18n: ectx: label, entry (acceptfocus), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:354 +#, kde-format +msgid "Accept Focus" +msgstr "" + +#. i18n: ectx: label, entry (acceptfocusrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:358 +#, kde-format +msgid "Accept Focus rule type" +msgstr "" + +#. i18n: ectx: label, entry (closeable), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:363 +#, fuzzy, kde-format +#| msgid "Close" +msgid "Closeable" +msgstr "Začyni" + +#. i18n: ectx: label, entry (closeablerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:367 +#, kde-format +msgid "Closeable rule type" +msgstr "" + +#. i18n: ectx: label, entry (autogroup), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:372 +#, kde-format +msgid "Autogroup with identical" +msgstr "" + +#. i18n: ectx: label, entry (autogrouprule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:376 +#, kde-format +msgid "Autogroup with identical rule type" +msgstr "" + +#. i18n: ectx: label, entry (autogroupfg), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:381 +#, kde-format +msgid "Autogroup in foreground" +msgstr "" + +#. i18n: ectx: label, entry (autogroupfgrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:385 +#, kde-format +msgid "Autogroup in foreground rule type" +msgstr "" + +#. i18n: ectx: label, entry (autogroupid), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:390 +#, kde-format +msgid "Autogroup by ID" +msgstr "" + +#. i18n: ectx: label, entry (autogroupidrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:393 +#, kde-format +msgid "Autogroup by ID rule type" +msgstr "" + +#. i18n: ectx: label, entry (strictgeometry), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:398 +#, kde-format +msgid "Obey geometry restrictions" +msgstr "" + +#. i18n: ectx: label, entry (strictgeometryrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:402 +#, kde-format +msgid "Obey geometry restrictions rule type" +msgstr "" + +#. i18n: ectx: label, entry (shortcut), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:407 +#, kde-format +msgid "Shortcut" +msgstr "" + +#. i18n: ectx: label, entry (shortcutrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:410 +#, kde-format +msgid "Shortcut rule type" +msgstr "" + +#. i18n: ectx: label, entry (disableglobalshortcuts), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:417 +#, fuzzy, kde-format +#| msgid "Block Global Shortcuts" +msgid "Ignore global shortcuts" +msgstr "Zabarani paŭsiudnyja skaroty" + +#. i18n: ectx: label, entry (disableglobalshortcutsrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:421 +#, kde-format +msgid "Ignore global shortcuts rule type" +msgstr "" + +#. i18n: ectx: label, entry (desktopfile), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:426 +#, fuzzy, kde-format +#| msgid "Desktop Cube" +msgid "Desktop file name" +msgstr "Rabočy kub" + +#. i18n: ectx: label, entry (desktopfilerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:429 +#, kde-format +msgid "Desktop file name rule type" +msgstr "" + +#: scripting/genericscriptedconfig.cpp:70 +#, kde-format +msgctxt "Error message" +msgid "Plugin does not provide configuration file in expected location" +msgstr "" + +#: scripting/scripting.cpp:117 +#, kde-format +msgctxt "Assertion failed in KWin script with given value" +msgid "Assertion failed: %1 is not null" +msgstr "" + +#: scripting/scripting.cpp:135 +#, kde-format +msgctxt "Assertion failed in KWin script" +msgid "Assertion failed: argument is null" +msgstr "" + +#: scripting/scripting.cpp:177 +#, kde-format +msgctxt "Error in KWin Script" +msgid "" +"Invalid number of arguments. At least service, path, interface and method " +"need to be provided" +msgstr "" + +#: scripting/scripting.cpp:183 +#, kde-format +msgctxt "Error in KWin Script" +msgid "" +"Invalid type. Service, path, interface and method need to be string values" +msgstr "" + +#: scripting/scriptingutils.cpp:17 +#, kde-format +msgctxt "syntax error in KWin script" +msgid "Invalid number of arguments" +msgstr "" + +#: scripting/scriptingutils.cpp:30 +#, kde-format +msgctxt "KWin Scripting function received incorrect value for an expected type" +msgid "%1 is not a variant type" +msgstr "" + +#. i18n: ectx: property (windowTitle), widget (QDialog, ShortcutDialog) +#: shortcutdialog.ui:14 +#, kde-format +msgid "Dialog" +msgstr "" + +#. i18n: ectx: property (text), widget (QToolButton, clearButton) +#: shortcutdialog.ui:25 +#, kde-format +msgid "..." +msgstr "" + +#: tabbox/tabbox.cpp:372 +#, fuzzy, kde-format +#| msgid "Show Desktop Grid" +msgctxt "Special entry in alt+tab list for minimizing all windows" +msgid "Show Desktop" +msgstr "Sietka stała" + +#: tabbox/tabbox.cpp:521 +#, kde-format +msgid "Walk Through Windows" +msgstr "Pierachod pamiž voknami" + +#: tabbox/tabbox.cpp:522 +#, kde-format +msgid "Walk Through Windows (Reverse)" +msgstr "Pierachod pamiž voknami (advarotny kirunak)" + +#: tabbox/tabbox.cpp:523 +#, fuzzy, kde-format +#| msgid "Walk Through Windows (Reverse)" +msgid "Walk Through Windows Alternative" +msgstr "Pierachod pamiž voknami (advarotny kirunak)" + +#: tabbox/tabbox.cpp:524 +#, fuzzy, kde-format +#| msgid "Walk Through Windows (Reverse)" +msgid "Walk Through Windows Alternative (Reverse)" +msgstr "Pierachod pamiž voknami (advarotny kirunak)" + +#: tabbox/tabbox.cpp:525 +#, fuzzy, kde-format +#| msgid "Walk Through Windows (Reverse)" +msgid "Walk Through Windows of Current Application" +msgstr "Pierachod pamiž voknami (advarotny kirunak)" + +#: tabbox/tabbox.cpp:526 +#, fuzzy, kde-format +#| msgid "Walk Through Windows (Reverse)" +msgid "Walk Through Windows of Current Application (Reverse)" +msgstr "Pierachod pamiž voknami (advarotny kirunak)" + +#: tabbox/tabbox.cpp:527 +#, fuzzy, kde-format +#| msgid "Walk Through Windows (Reverse)" +msgid "Walk Through Windows of Current Application Alternative" +msgstr "Pierachod pamiž voknami (advarotny kirunak)" + +#: tabbox/tabbox.cpp:528 +#, fuzzy, kde-format +#| msgid "Walk Through Windows (Reverse)" +msgid "Walk Through Windows of Current Application Alternative (Reverse)" +msgstr "Pierachod pamiž voknami (advarotny kirunak)" + +#: tabbox/tabbox.cpp:529 +#, kde-format +msgid "Walk Through Desktops" +msgstr "Pierachod pamiž rabočymi stałami" + +#: tabbox/tabbox.cpp:530 +#, kde-format +msgid "Walk Through Desktops (Reverse)" +msgstr "Pierachod pamiž rabočymi stałami (advarotny kirunak)" + +#: tabbox/tabbox.cpp:531 +#, kde-format +msgid "Walk Through Desktop List" +msgstr "Pierachod u śpisie rabočych stałoŭ" + +#: tabbox/tabbox.cpp:532 +#, kde-format +msgid "Walk Through Desktop List (Reverse)" +msgstr "Pierachod u śpisie rabočych stałoŭ (advarotny kirunak)" + +#: tabbox/tabboxhandler.cpp:272 +#, kde-format +msgid "" +"The Window Switcher installation is broken, resources are missing.\n" +"Contact your distribution about this." +msgstr "" + +#: useractions.cpp:167 +#, kde-format +msgid "" +"You have selected to show a window without its border.\n" +"Without the border, you will not be able to enable the border again using " +"the mouse: use the window operations menu instead, activated using the %1 " +"keyboard shortcut." +msgstr "" +"Ty zahadaŭ pakazvać akno biez abrysaŭ.\n" +"Biez abrysu ty nia zmožaš jaho viarnuć myššu. Zamiest hetaha karystajsia " +"menu aperacyjaŭ akna, jakoje ŭklučajecca klavijaturnym skarotam „%1”." + +#: useractions.cpp:175 +#, kde-format +msgid "" +"You have selected to show a window in fullscreen mode.\n" +"If the application itself does not have an option to turn the fullscreen " +"mode off you will not be able to disable it again using the mouse: use the " +"window operations menu instead, activated using the %1 keyboard shortcut." +msgstr "" +"Ty zahadaŭ pakazvać akno na ŭvieś ekran.\n" +"Kali ŭ samoj aplikacyi niama peŭnaj mahčymaści viarnuć zvykły sposab " +"prahladu akna, tady ty nia zmožaš zrabić heta myššu. Zamiest hetaha " +"karystajsia menu aperacyjaŭ akna, jakoje ŭklučajecca klavijaturnym skarotam " +"„%1”." + +#: useractions.cpp:240 +#, kde-format +msgid "&Move" +msgstr "&Pierasuń" + +#: useractions.cpp:245 +#, fuzzy, kde-format +#| msgid "Re&size" +msgid "&Resize" +msgstr "&Źmiani pamiery" + +#: useractions.cpp:250 +#, kde-format +msgid "Keep &Above Others" +msgstr "&Uźnimi nad inšymi" + +#: useractions.cpp:256 +#, kde-format +msgid "Keep &Below Others" +msgstr "&Źniž pad inšyja" + +#: useractions.cpp:262 +#, kde-format +msgid "&Fullscreen" +msgstr "&Na ŭvieś ekran" + +#: useractions.cpp:268 +#, fuzzy, kde-format +#| msgid "Shade" +msgid "&Shade" +msgstr "Zharni ŭ zahałovak" + +#: useractions.cpp:274 +#, kde-format +msgid "&No Border" +msgstr "&Biez abrysu" + +#: useractions.cpp:282 +#, fuzzy, kde-format +#| msgid "Window &Shortcut..." +msgid "Set Window Short&cut..." +msgstr "&Skarot dla akna..." + +#: useractions.cpp:287 +#, fuzzy, kde-format +#| msgid "&Special Window Settings..." +msgid "Configure Special &Window Settings..." +msgstr "&Asablivyja nałady akna..." + +#: useractions.cpp:292 +#, fuzzy, kde-format +#| msgid "&Special Application Settings..." +msgid "Configure S&pecial Application Settings..." +msgstr "&Asablivyja nałady aplikacyi..." + +#: useractions.cpp:300 +#, fuzzy, kde-format +#| msgid "KDE window manager" +msgctxt "" +"Entry in context menu of window decoration to open the configuration module " +"of KWin" +msgid "Configure W&indow Manager..." +msgstr "Akońnik dla „KDE”" + +#: useractions.cpp:328 +#, kde-format +msgid "Ma&ximize" +msgstr "Zma&ksymalizuj" + +#: useractions.cpp:334 +#, kde-format +msgid "Mi&nimize" +msgstr "Ź&minimalizuj" + +#: useractions.cpp:340 +#, kde-format +msgid "&More Actions" +msgstr "" + +#: useractions.cpp:343 +#, kde-format +msgid "&Close" +msgstr "&Začyni" + +#: useractions.cpp:410 +#, kde-format +msgid "&Extensions" +msgstr "" + +#: useractions.cpp:451 +#, fuzzy, kde-format +#| msgid "&All Desktops" +msgid "&Desktops" +msgstr "&Na ŭsich stałach" + +#: useractions.cpp:465 +#, fuzzy, kde-format +#| msgid "To &Desktop" +msgid "Move to &Desktop" +msgstr "Na &stoł" + +#: useractions.cpp:483 +#, fuzzy, kde-format +#| msgid "To &Desktop" +msgid "Move to &Screen" +msgstr "Na &stoł" + +#: useractions.cpp:499 +#, kde-format +msgid "Show in &Activities" +msgstr "" + +#: useractions.cpp:514 useractions.cpp:559 +#, kde-format +msgid "&All Desktops" +msgstr "&Na ŭsich stałach" + +#: useractions.cpp:542 useractions.cpp:595 +#, fuzzy, kde-format +#| msgid "Show Desktop Grid" +msgctxt "Create a new desktop and move there the window" +msgid "&New Desktop" +msgstr "Sietka stała" + +#: useractions.cpp:618 +#, fuzzy, kde-format +#| msgid "Window to Screen 1" +msgctxt "" +"@item:inmenu List of all Screens to send a window to. First argument is a " +"number, second the output identifier. E.g. Screen 1 (HDMI1)" +msgid "Screen &%1 (%2)" +msgstr "Akno na ekran „1”" + +#: useractions.cpp:641 +#, kde-format +msgid "&All Activities" +msgstr "" + +#: useractions.cpp:887 +#, kde-format +msgctxt "'%1' is a keyboard shortcut like 'ctrl+w'" +msgid "%1 is already in use" +msgstr "" + +#: useractions.cpp:889 +#, kde-format +msgctxt "keyboard shortcut '%1' is used by action '%2' in application '%3'" +msgid "%1 is used by %2 in %3" +msgstr "" + +#: useractions.cpp:1021 +#, kde-format +msgid "Activate Window (%1)" +msgstr "Uklučy akno (%1)" + +#: useractions.cpp:1163 +#, kde-format +msgid "" +"The window manager is configured to consider the screen with the mouse on it " +"as active one.\n" +"Therefore it is not possible to switch to a screen explicitly." +msgstr "" + +#: virtualdesktops.cpp:698 virtualdesktops.cpp:767 +#, kde-format +msgid "Desktop %1" +msgstr "Stoł %1" + +#: virtualdesktops.cpp:802 +#, kde-format +msgid "Switch to Next Desktop" +msgstr "Uklučy nastupny stoł" + +#: virtualdesktops.cpp:804 +#, kde-format +msgid "Switch to Previous Desktop" +msgstr "Uklučy papiaredni stoł" + +#: virtualdesktops.cpp:806 +#, kde-format +msgid "Switch One Desktop to the Right" +msgstr "Uklučy praviejšy stoł" + +#: virtualdesktops.cpp:808 +#, kde-format +msgid "Switch One Desktop to the Left" +msgstr "Uklučy laviejšy stoł" + +#: virtualdesktops.cpp:810 +#, kde-format +msgid "Switch One Desktop Up" +msgstr "Uklučy vyšejšy stoł" + +#: virtualdesktops.cpp:812 +#, kde-format +msgid "Switch One Desktop Down" +msgstr "Uklučy nižejšy stoł" + +#: virtualdesktops.cpp:825 +#, fuzzy, kde-format +#| msgid "Switch to Desktop 1" +msgid "Switch to Desktop %1" +msgstr "Uklučy stoł „1”" + +#: virtualkeyboard.cpp:84 +#, kde-format +msgid "Virtual Keyboard" +msgstr "" + +#: virtualkeyboard.cpp:350 +#, kde-format +msgid "Virtual Keyboard: enabled" +msgstr "" + +#: virtualkeyboard.cpp:353 +#, kde-format +msgid "Virtual Keyboard: disabled" +msgstr "" + +#: virtualkeyboard.cpp:355 +#, kde-format +msgid "Whether to show the virtual keyboard on demand." +msgstr "" + +#: workspace.cpp:1363 +#, kde-format +msgctxt "Introductory text shown in the support information." +msgid "" +"KWin Support Information:\n" +"The following information should be used when requesting support on e.g. " +"https://forum.kde.org.\n" +"It provides information about the currently running instance, which options " +"are used,\n" +"what OpenGL driver and which effects are running.\n" +"Please post the information provided underneath this introductory text to a " +"paste bin service\n" +"like https://paste.kde.org instead of pasting into support threads.\n" +msgstr "" \ No newline at end of file diff --git a/po/bg/kcm_kwin_virtualdesktops.po b/po/bg/kcm_kwin_virtualdesktops.po new file mode 100644 index 0000000..f6cf838 --- /dev/null +++ b/po/bg/kcm_kwin_virtualdesktops.po @@ -0,0 +1,131 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Zlatko Popov , 2008. +# Yasen Pramatarov , 2009, 2010. +msgid "" +msgstr "" +"Project-Id-Version: kcm_kwindesktop\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-08-08 02:22+0200\n" +"PO-Revision-Date: 2010-06-26 14:14+0300\n" +"Last-Translator: Yasen Pramatarov \n" +"Language-Team: Bulgarian \n" +"Language: bg\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: KBabel 1.11.4\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 "yasen@lindeas.com,zlatkopopov@fsa-bg.org" + +#: desktopsmodel.cpp:455 +#, kde-format +msgid "There was an error connecting to the compositor." +msgstr "" + +#: desktopsmodel.cpp:657 +#, kde-format +msgid "There was an error saving the settings to the compositor." +msgstr "" + +#: desktopsmodel.cpp:660 +#, kde-format +msgid "There was an error requesting information from the compositor." +msgstr "" + +#: package/contents/ui/main.qml:18 +#, kde-format +msgid "" +"This module lets you configure the navigation, number and layout of virtual " +"desktops." +msgstr "" + +#: package/contents/ui/main.qml:68 +#, kde-format +msgctxt "@info:tooltip" +msgid "Rename" +msgstr "" + +#: package/contents/ui/main.qml:78 +#, kde-format +msgctxt "@info:tooltip" +msgid "Remove" +msgstr "" + +#: package/contents/ui/main.qml:104 +#, kde-format +msgid "" +"Virtual desktops have been changed outside this settings application. Saving " +"now will overwrite the changes." +msgstr "" + +#: package/contents/ui/main.qml:118 +#, kde-format +msgid "Row %1" +msgstr "" + +#: package/contents/ui/main.qml:131 +#, kde-format +msgctxt "@action:button" +msgid "Add" +msgstr "" + +#: package/contents/ui/main.qml:134 +#, fuzzy, kde-format +#| msgid "Desktops" +msgid "New Desktop" +msgstr "Работни плотове" + +#: package/contents/ui/main.qml:148 +#, kde-format +msgid "1 Row" +msgid_plural "%1 Rows" +msgstr[0] "" +msgstr[1] "" + +#: package/contents/ui/main.qml:160 +#, kde-format +msgid "Options:" +msgstr "" + +#: package/contents/ui/main.qml:162 +#, fuzzy, kde-format +#| msgid "Desktop navigation wraps around" +msgid "Navigation wraps around" +msgstr "Кръгова навигация през плотовете" + +#: package/contents/ui/main.qml:177 +#, kde-format +msgid "Show animation when switching:" +msgstr "" + +#: package/contents/ui/main.qml:220 +#, kde-format +msgid "Show on-screen display when switching:" +msgstr "" + +#: package/contents/ui/main.qml:238 +#, kde-format +msgid "%1 ms" +msgstr "" + +#: package/contents/ui/main.qml:256 +#, kde-format +msgid "Show desktop layout indicators" +msgstr "" + +#: virtualdesktops.cpp:30 +#, fuzzy, kde-format +#| msgid "Desktops" +msgid "Virtual Desktops" +msgstr "Работни плотове" \ No newline at end of file diff --git a/po/bg/kcm_kwindecoration.po b/po/bg/kcm_kwindecoration.po new file mode 100644 index 0000000..8d460a8 --- /dev/null +++ b/po/bg/kcm_kwindecoration.po @@ -0,0 +1,237 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Zlatko Popov , 2006, 2007, 2008. +# Yasen Pramatarov , 2009, 2010. +msgid "" +msgstr "" +"Project-Id-Version: kcmkwindecoration\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-11-17 08:20+0100\n" +"PO-Revision-Date: 2010-10-05 23:36+0300\n" +"Last-Translator: Yasen Pramatarov \n" +"Language-Team: Bulgarian \n" +"Language: bg\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: KBabel 1.11.4\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 "radnev@yahoo.com" + +#: declarative-plugin/buttonsmodel.cpp:54 +#, kde-format +msgid "Menu" +msgstr "Меню" + +#: declarative-plugin/buttonsmodel.cpp:56 +#, kde-format +msgid "Application menu" +msgstr "" + +#: declarative-plugin/buttonsmodel.cpp:58 +#, fuzzy, kde-format +#| msgid "On All Desktops" +msgid "On all desktops" +msgstr "Към всички работни плотове" + +#: declarative-plugin/buttonsmodel.cpp:60 +#, kde-format +msgid "Minimize" +msgstr "Минимизиране" + +#: declarative-plugin/buttonsmodel.cpp:62 +#, kde-format +msgid "Maximize" +msgstr "Максимизиране" + +#: declarative-plugin/buttonsmodel.cpp:64 +#, kde-format +msgid "Close" +msgstr "Затваряне" + +#: declarative-plugin/buttonsmodel.cpp:66 +#, kde-format +msgid "Context help" +msgstr "" + +#: declarative-plugin/buttonsmodel.cpp:68 +#, kde-format +msgid "Shade" +msgstr "Сгъване" + +#: declarative-plugin/buttonsmodel.cpp:70 +#, fuzzy, kde-format +#| msgid "Keep Below Others" +msgid "Keep below" +msgstr "На заден план" + +#: declarative-plugin/buttonsmodel.cpp:72 +#, fuzzy, kde-format +#| msgid "Keep Above Others" +msgid "Keep above" +msgstr "На преден план" + +#: kcm.cpp:50 +#, fuzzy, kde-format +#| msgid "Get New Decorations..." +msgid "Window Decorations" +msgstr "Взимане на нови декорации..." + +#: kcm.cpp:54 +#, kde-format +msgid "Valerio Pilo" +msgstr "" + +#: kcm.cpp:55 +#, kde-format +msgid "Author" +msgstr "" + +#: kcm.cpp:104 +#, fuzzy, kde-format +#| msgid "Get New Decorations..." +msgid "Download New Window Decorations" +msgstr "Взимане на нови декорации..." + +#: package/contents/ui/Buttons.qml:73 +#, kde-format +msgid "Titlebar" +msgstr "" + +#: package/contents/ui/Buttons.qml:214 +#, kde-format +msgid "Drop button here to remove it" +msgstr "" + +#: package/contents/ui/Buttons.qml:232 +#, kde-format +msgid "Drag buttons between here and the titlebar" +msgstr "" + +#: package/contents/ui/main.qml:15 +#, kde-format +msgid "This module lets you configure the window decorations." +msgstr "" + +#: package/contents/ui/main.qml:49 +#, kde-format +msgctxt "tab label" +msgid "Theme" +msgstr "" + +#: package/contents/ui/main.qml:53 +#, fuzzy, kde-format +#| msgid "Buttons" +msgctxt "tab label" +msgid "Titlebar Buttons" +msgstr "Бутони" + +#: package/contents/ui/main.qml:78 +#, kde-format +msgctxt "checkbox label" +msgid "Use theme's default window border size" +msgstr "" + +#: package/contents/ui/main.qml:109 +#, fuzzy, kde-format +#| msgid "Get New Decorations..." +msgctxt "button text" +msgid "Get New Window Decorations..." +msgstr "Взимане на нови декорации..." + +#: package/contents/ui/main.qml:126 +#, kde-format +msgctxt "checkbox label" +msgid "Close windows by double clicking the menu button" +msgstr "" + +#: package/contents/ui/main.qml:139 +#, kde-format +msgctxt "popup tip" +msgid "" +"Close by double clicking: Keep the window's Menu button pressed until it " +"appears." +msgstr "" + +#: package/contents/ui/main.qml:146 +#, fuzzy, kde-format +#| msgid "&Show window button tooltips" +msgctxt "checkbox label" +msgid "Show titlebar button tooltips" +msgstr "Пок&азване на подсказките за бутоните на прозореца" + +#: package/contents/ui/Themes.qml:89 +#, kde-format +msgid "Edit %1 Theme" +msgstr "" + +#: utils.cpp:26 +#, fuzzy, kde-format +#| msgid "Border size:" +msgid "No Borders" +msgstr "Размер на рамката:" + +#: utils.cpp:27 +#, fuzzy, kde-format +#| msgid "Border size:" +msgid "No Side Borders" +msgstr "Размер на рамката:" + +#: utils.cpp:28 +#, fuzzy, kde-format +#| msgctxt "@item:inlistbox Border size:" +#| msgid "Tiny" +msgid "Tiny" +msgstr "Малък" + +#: utils.cpp:29 +#, fuzzy, kde-format +#| msgctxt "@item:inlistbox Border size:" +#| msgid "Normal" +msgid "Normal" +msgstr "Нормален" + +#: utils.cpp:30 +#, fuzzy, kde-format +#| msgctxt "@item:inlistbox Border size:" +#| msgid "Large" +msgid "Large" +msgstr "Голям" + +#: utils.cpp:31 +#, fuzzy, kde-format +#| msgctxt "@item:inlistbox Border size:" +#| msgid "Very Large" +msgid "Very Large" +msgstr "Много голям" + +#: utils.cpp:32 +#, fuzzy, kde-format +#| msgctxt "@item:inlistbox Border size:" +#| msgid "Huge" +msgid "Huge" +msgstr "Огромен" + +#: utils.cpp:33 +#, fuzzy, kde-format +#| msgctxt "@item:inlistbox Border size:" +#| msgid "Very Huge" +msgid "Very Huge" +msgstr "Много огромен" + +#: utils.cpp:34 +#, fuzzy, kde-format +#| msgctxt "@item:inlistbox Border size:" +#| msgid "Oversized" +msgid "Oversized" +msgstr "Свръх огромен" \ No newline at end of file diff --git a/po/bg/kcm_kwinrules.po b/po/bg/kcm_kwinrules.po new file mode 100644 index 0000000..f31604d --- /dev/null +++ b/po/bg/kcm_kwinrules.po @@ -0,0 +1,874 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Zlatko Popov , 2006, 2007, 2008. +# Yasen Pramatarov , 2011. +msgid "" +msgstr "" +"Project-Id-Version: kcmkwinrules\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-11-03 08:14+0100\n" +"PO-Revision-Date: 2011-07-23 20:59+0300\n" +"Last-Translator: Yasen Pramatarov \n" +"Language-Team: Bulgarian \n" +"Language: bg\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 1.2\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 "radnev@yahoo.com" + +#: kcmrules.cpp:28 +#, fuzzy, kde-format +#| msgid "Window &role:" +msgid "Window Rules" +msgstr "Рол&я на прозорец:" + +#: kcmrules.cpp:32 +#, kde-format +msgid "Ismael Asensio" +msgstr "" + +#: kcmrules.cpp:33 +#, kde-format +msgid "Author" +msgstr "" + +#: kcmrules.cpp:37 +#, kde-format +msgid "" +"

Window-specific Settings

Here you can customize window settings " +"specifically only for some windows.

Please note that this " +"configuration will not take effect if you do not use KWin as your window " +"manager. If you do use a different window manager, please refer to its " +"documentation for how to customize window behavior.

" +msgstr "" +"

Настройки на прозорците

От тук може да настроите поведението на " +"различните типове прозорци.

" + +#: main.cpp:91 +#, kde-format +msgid "Application settings for %1" +msgstr "Настройки за %1" + +#: main.cpp:111 rulesmodel.cpp:216 +#, kde-format +msgid "Window settings for %1" +msgstr "Настройки за %1" + +#: main.cpp:163 +#, fuzzy, kde-format +#| msgid "Edit Window-Specific Settings" +msgctxt "Window caption for the application wide rules dialog" +msgid "Edit Application-Specific Settings" +msgstr "Редактиране специфичните настройки на прозорците" + +#: main.cpp:197 +#, kde-format +msgid "KWin" +msgstr "KWin" + +#: main.cpp:204 +#, kde-format +msgid "KWin helper utility" +msgstr "Помощно средство за KWin" + +#: main.cpp:205 +#, fuzzy, kde-format +#| msgid "WId of the window for special window settings." +msgid "KWin id of the window for special window settings." +msgstr "WId на прозореца за специалните настройки." + +#: main.cpp:206 +#, kde-format +msgid "Whether the settings should affect all windows of the application." +msgstr "Дали настройките да важат за всички прозорци на приложението." + +#: main.cpp:215 +#, kde-format +msgid "This helper utility is not supposed to be called directly." +msgstr "This helper utility is not supposed to be called directly." + +#: optionsmodel.cpp:145 +#, kde-format +msgid "Unimportant" +msgstr "Маловажно" + +#: optionsmodel.cpp:146 +#, kde-format +msgid "Exact Match" +msgstr "Пълно съвпадение" + +#: optionsmodel.cpp:147 +#, kde-format +msgid "Substring Match" +msgstr "Частично съвпадение" + +#: optionsmodel.cpp:148 +#, kde-format +msgid "Regular Expression" +msgstr "Регулярен израз" + +#: optionsmodel.cpp:153 +#, kde-format +msgid "Do Not Affect" +msgstr "Без прилагане" + +#: optionsmodel.cpp:154 +#, kde-format +msgid "" +"The window property will not be affected and therefore the default handling " +"for it will be used.\n" +"Specifying this will block more generic window settings from taking effect." +msgstr "" + +#: optionsmodel.cpp:157 +#, kde-format +msgid "Apply Initially" +msgstr "Първоначално прилагане" + +#: optionsmodel.cpp:158 +#, kde-format +msgid "" +"The window property will be only set to the given value after the window is " +"created.\n" +"No further changes will be affected." +msgstr "" + +#: optionsmodel.cpp:161 +#, kde-format +msgid "Remember" +msgstr "Запомняне" + +#: optionsmodel.cpp:162 +#, kde-format +msgid "" +"The value of the window property will be remembered and, every time the " +"window is created, the last remembered value will be applied." +msgstr "" + +#: optionsmodel.cpp:165 +#, kde-format +msgid "Force" +msgstr "Прилагане" + +#: optionsmodel.cpp:166 +#, kde-format +msgid "The window property will be always forced to the given value." +msgstr "" + +#: optionsmodel.cpp:168 +#, kde-format +msgid "Apply Now" +msgstr "Прилагане сега" + +#: optionsmodel.cpp:169 +#, kde-format +msgid "" +"The window property will be set to the given value immediately and will not " +"be affected later\n" +"(this action will be deleted afterwards)." +msgstr "" + +#: optionsmodel.cpp:172 +#, kde-format +msgid "Force Temporarily" +msgstr "Временно прилагане" + +#: optionsmodel.cpp:173 +#, kde-format +msgid "" +"The window property will be forced to the given value until it is hidden\n" +"(this action will be deleted after the window is hidden)." +msgstr "" + +#: package/contents/ui/FileDialogLoader.qml:14 +#, kde-format +msgid "Select File" +msgstr "" + +#: package/contents/ui/FileDialogLoader.qml:26 +#, kde-format +msgid "KWin Rules (*.kwinrule)" +msgstr "" + +#: package/contents/ui/OptionsComboBox.qml:32 +#, kde-format +msgid "None selected" +msgstr "" + +#: package/contents/ui/OptionsComboBox.qml:37 +#, kde-format +msgid "All selected" +msgstr "" + +#: package/contents/ui/OptionsComboBox.qml:39 +#, kde-format +msgid "%1 selected" +msgid_plural "%1 selected" +msgstr[0] "" +msgstr[1] "" + +#: package/contents/ui/RulesEditor.qml:48 +#: package/contents/ui/RulesEditor.qml:67 +#, kde-format +msgid "Add Properties..." +msgstr "" + +#: package/contents/ui/RulesEditor.qml:67 +#, fuzzy, kde-format +#| msgid "&Closeable" +msgid "Close" +msgstr "Р&азрешено затваряне" + +#: package/contents/ui/RulesEditor.qml:80 +#, fuzzy, kde-format +#| msgid "&Detect Window Properties" +msgid "Detect Window Properties" +msgstr "&Информация за прозореца" + +#: package/contents/ui/RulesEditor.qml:93 +#, kde-format +msgid "Instantly" +msgstr "" + +#: package/contents/ui/RulesEditor.qml:94 +#, kde-format +msgid "After %1 second" +msgid_plural "After %1 seconds" +msgstr[0] "" +msgstr[1] "" + +#: package/contents/ui/RulesEditor.qml:113 +#, fuzzy, kde-format +#| msgid "&Detect Window Properties" +msgid "Select properties" +msgstr "&Информация за прозореца" + +#: package/contents/ui/RulesEditor.qml:205 +#: package/contents/ui/ValueEditor.qml:53 +#, kde-format +msgid "Yes" +msgstr "" + +#: package/contents/ui/RulesEditor.qml:205 +#: package/contents/ui/ValueEditor.qml:59 +#, fuzzy, kde-format +#| msgctxt "no focus stealing prevention" +#| msgid "None" +msgid "No" +msgstr "Без" + +#: package/contents/ui/RulesEditor.qml:207 +#: package/contents/ui/ValueEditor.qml:127 +#: package/contents/ui/ValueEditor.qml:134 +#, kde-format +msgid "%1 %" +msgstr "" + +#: package/contents/ui/RulesEditor.qml:209 +#, kde-format +msgctxt "Coordinates (x, y)" +msgid "(%1, %2)" +msgstr "" + +#: package/contents/ui/RulesEditor.qml:211 +#, kde-format +msgctxt "Size (width, height)" +msgid "(%1, %2)" +msgstr "" + +#: package/contents/ui/RulesList.qml:61 +#, kde-format +msgid "No rules for specific windows are currently set" +msgstr "" + +#: package/contents/ui/RulesList.qml:69 +#, kde-format +msgid "Select the rules to export" +msgstr "" + +#: package/contents/ui/RulesList.qml:73 +#, kde-format +msgid "Unselect All" +msgstr "" + +#: package/contents/ui/RulesList.qml:73 +#, kde-format +msgid "Select All" +msgstr "" + +#: package/contents/ui/RulesList.qml:87 +#, kde-format +msgid "Save Rules" +msgstr "" + +#: package/contents/ui/RulesList.qml:98 +#, fuzzy, kde-format +#| msgid "&New..." +msgid "Add New..." +msgstr "До&бавяне..." + +#: package/contents/ui/RulesList.qml:109 +#, fuzzy, kde-format +#| msgid "&Import" +msgid "Import..." +msgstr "&Внасяне" + +#: package/contents/ui/RulesList.qml:117 +#, fuzzy, kde-format +#| msgid "&Export" +msgid "Cancel Export" +msgstr "&Изнасяне" + +#: package/contents/ui/RulesList.qml:117 +#, fuzzy, kde-format +#| msgid "&Export" +msgid "Export..." +msgstr "&Изнасяне" + +#: package/contents/ui/RulesList.qml:198 +#, kde-format +msgid "Edit" +msgstr "Редактиране" + +#: package/contents/ui/RulesList.qml:207 +#, kde-format +msgid "Delete" +msgstr "Изтриване" + +#: package/contents/ui/RulesList.qml:220 +#, kde-format +msgid "Import Rules" +msgstr "Правила за внасяне" + +#: package/contents/ui/RulesList.qml:232 +#, fuzzy, kde-format +#| msgid "Export Rule" +msgid "Export Rules" +msgstr "Правило за изнасяне" + +#: package/contents/ui/ValueEditor.qml:162 +#, kde-format +msgctxt "(x, y) coordinates separator in size/position" +msgid "x" +msgstr "" + +#: rulesdialog.cpp:28 +#, kde-format +msgid "Edit Window-Specific Settings" +msgstr "Редактиране специфичните настройки на прозорците" + +#: rulesmodel.cpp:219 +#, kde-format +msgid "Settings for %1" +msgstr "Настройки за %1" + +#: rulesmodel.cpp:222 +#, fuzzy, kde-format +#| msgid "Window settings for %1" +msgid "New window settings" +msgstr "Настройки за %1" + +#: rulesmodel.cpp:236 +#, kde-format +msgid "" +"You have specified the window class as unimportant.\n" +"This means the settings will possibly apply to windows from all " +"applications. If you really want to create a generic setting, it is " +"recommended you at least limit the window types to avoid special window " +"types." +msgstr "" +"Зададохте класа на прозореца като маловажен.\n" +"Това означава, че настройките ще са валидни за всички прозорци от всички " +"програми. Ако искате да създадете общи настройки за всички програми, по-" +"добре е да ограничите типа на прозореца, за да бъдат избегнати специалните " +"типове прозорци." + +#: rulesmodel.cpp:366 +#, fuzzy, kde-format +#| msgid "De&scription:" +msgid "Description" +msgstr "Оп&исание:" + +#: rulesmodel.cpp:366 rulesmodel.cpp:374 rulesmodel.cpp:382 rulesmodel.cpp:389 +#: rulesmodel.cpp:395 rulesmodel.cpp:403 rulesmodel.cpp:408 rulesmodel.cpp:414 +#, fuzzy, kde-format +#| msgid "&Window" +msgid "Window matching" +msgstr "Прозоре&ц" + +#: rulesmodel.cpp:374 +#, fuzzy, kde-format +#| msgid "Window &class (application type):" +msgid "Window class (application)" +msgstr "Кл&ас прозорец (тип програма):" + +#: rulesmodel.cpp:382 +#, fuzzy, kde-format +#| msgid "Match w&hole window class" +msgid "Match whole window class" +msgstr "П&ълно съвпадение на класа на прозореца" + +#: rulesmodel.cpp:389 +#, fuzzy, kde-format +#| msgid "Match w&hole window class" +msgid "Whole window class" +msgstr "П&ълно съвпадение на класа на прозореца" + +#: rulesmodel.cpp:395 +#, fuzzy, kde-format +#| msgid "Window &types:" +msgid "Window types" +msgstr "&Типове прозорци:" + +#: rulesmodel.cpp:403 +#, fuzzy, kde-format +#| msgid "Window &role:" +msgid "Window role" +msgstr "Рол&я на прозорец:" + +#: rulesmodel.cpp:408 +#, fuzzy, kde-format +#| msgid "Window t&itle:" +msgid "Window title" +msgstr "&Заглавие:" + +#: rulesmodel.cpp:414 +#, fuzzy, kde-format +#| msgid "&Machine (hostname):" +msgid "Machine (hostname)" +msgstr "&Машина (хост):" + +#: rulesmodel.cpp:420 +#, fuzzy, kde-format +#| msgid "&Position" +msgid "Position" +msgstr "Позици&я" + +#: rulesmodel.cpp:420 rulesmodel.cpp:425 rulesmodel.cpp:430 rulesmodel.cpp:435 +#: rulesmodel.cpp:440 rulesmodel.cpp:453 rulesmodel.cpp:467 rulesmodel.cpp:472 +#: rulesmodel.cpp:477 rulesmodel.cpp:482 rulesmodel.cpp:487 rulesmodel.cpp:493 +#: rulesmodel.cpp:502 rulesmodel.cpp:507 rulesmodel.cpp:512 +#, fuzzy, kde-format +#| msgid "&Position" +msgid "Size & Position" +msgstr "Позици&я" + +#: rulesmodel.cpp:425 +#, fuzzy, kde-format +#| msgid "&Size" +msgid "Size" +msgstr "Ра&змер" + +#: rulesmodel.cpp:430 +#, fuzzy, kde-format +#| msgid "Maximized &horizontally" +msgid "Maximized horizontally" +msgstr "&Хоризонтално максимизиран" + +#: rulesmodel.cpp:435 +#, fuzzy, kde-format +#| msgid "Maximized &vertically" +msgid "Maximized vertically" +msgstr "&Вертикално максимизиран" + +#: rulesmodel.cpp:440 +#, fuzzy, kde-format +#| msgid "All Desktops" +msgid "Virtual Desktop" +msgstr "Всички работни плотове" + +#: rulesmodel.cpp:453 +#, fuzzy, kde-format +#| msgid "A&ctive opacity" +msgid "Activity" +msgstr "&Активна непрозрачност" + +#: rulesmodel.cpp:467 +#, fuzzy, kde-format +#| msgid "Splash Screen" +msgid "Screen" +msgstr "Начален екран" + +#: rulesmodel.cpp:472 +#, fuzzy, kde-format +#| msgid "&Fullscreen" +msgid "Fullscreen" +msgstr "Пълен &екран" + +#: rulesmodel.cpp:477 +#, fuzzy, kde-format +#| msgid "M&inimized" +msgid "Minimized" +msgstr "&Минимизиран" + +#: rulesmodel.cpp:482 +#, fuzzy, kde-format +#| msgid "Sh&aded" +msgid "Shaded" +msgstr "&Сгънат" + +#: rulesmodel.cpp:487 +#, fuzzy, kde-format +#| msgid "Initial p&lacement" +msgid "Initial placement" +msgstr "Начално разполо&жение" + +#: rulesmodel.cpp:493 +#, fuzzy, kde-format +#| msgid "Ignore requested &geometry" +msgid "Ignore requested geometry" +msgstr "&Игнориране на заявената позиция и размер" + +#: rulesmodel.cpp:495 +#, kde-format +msgid "" +"Windows can ask to appear in a certain position.\n" +"By default this overrides the placement strategy\n" +"what might be nasty if the client abuses the feature\n" +"to unconditionally popup in the middle of your screen." +msgstr "" + +#: rulesmodel.cpp:502 +#, fuzzy, kde-format +#| msgid "M&inimum size" +msgid "Minimum Size" +msgstr "&Минимален размер" + +#: rulesmodel.cpp:507 +#, fuzzy, kde-format +#| msgid "M&aximum size" +msgid "Maximum Size" +msgstr "Мак&симален размер" + +#: rulesmodel.cpp:512 +#, kde-format +msgid "Obey geometry restrictions" +msgstr "" + +#: rulesmodel.cpp:514 +#, kde-format +msgid "" +"Eg. terminals or video players can ask to keep a certain aspect ratio\n" +"or only grow by values larger than one\n" +"(eg. by the dimensions of one character).\n" +"This may be pointless and the restriction prevents arbitrary dimensions\n" +"like your complete screen area." +msgstr "" + +#: rulesmodel.cpp:523 +#, fuzzy, kde-format +#| msgid "Keep &above" +msgid "Keep above" +msgstr "На пр&еден план" + +#: rulesmodel.cpp:523 rulesmodel.cpp:528 rulesmodel.cpp:533 rulesmodel.cpp:539 +#: rulesmodel.cpp:545 rulesmodel.cpp:551 +#, kde-format +msgid "Arrangement & Access" +msgstr "" + +#: rulesmodel.cpp:528 +#, fuzzy, kde-format +#| msgid "Keep &below" +msgid "Keep below" +msgstr "На &заден план" + +#: rulesmodel.cpp:533 +#, fuzzy, kde-format +#| msgid "Skip &taskbar" +msgid "Skip taskbar" +msgstr "Пропускане на &системния панел" + +#: rulesmodel.cpp:535 +#, kde-format +msgid "Window shall (not) appear in the taskbar." +msgstr "" + +#: rulesmodel.cpp:539 +#, fuzzy, kde-format +#| msgid "Skip pa&ger" +msgid "Skip pager" +msgstr "Пропускане на пе&йджъра" + +#: rulesmodel.cpp:541 +#, kde-format +msgid "Window shall (not) appear in the manager for virtual desktops" +msgstr "" + +#: rulesmodel.cpp:545 +#, kde-format +msgid "Skip switcher" +msgstr "" + +#: rulesmodel.cpp:547 +#, kde-format +msgid "Window shall (not) appear in the Alt+Tab list" +msgstr "" + +#: rulesmodel.cpp:551 +#, kde-format +msgid "Shortcut" +msgstr "Бърз клавиш" + +#: rulesmodel.cpp:557 +#, kde-format +msgid "No titlebar and frame" +msgstr "" + +#: rulesmodel.cpp:557 rulesmodel.cpp:562 rulesmodel.cpp:568 rulesmodel.cpp:573 +#: rulesmodel.cpp:578 rulesmodel.cpp:589 rulesmodel.cpp:600 rulesmodel.cpp:608 +#: rulesmodel.cpp:621 rulesmodel.cpp:626 rulesmodel.cpp:632 rulesmodel.cpp:637 +#, kde-format +msgid "Appearance & Fixes" +msgstr "" + +#: rulesmodel.cpp:562 +#, kde-format +msgid "Titlebar color scheme" +msgstr "" + +#: rulesmodel.cpp:568 +#, fuzzy, kde-format +#| msgid "A&ctive opacity" +msgid "Active opacity" +msgstr "&Активна непрозрачност" + +#: rulesmodel.cpp:573 +#, fuzzy, kde-format +#| msgid "I&nactive opacity" +msgid "Inactive opacity" +msgstr "&Неактивна непрозрачност" + +#: rulesmodel.cpp:578 +#, fuzzy, kde-format +#| msgid "&Focus stealing prevention" +msgid "Focus stealing prevention" +msgstr "Пр&едотвратяване открадването на фокуса" + +#: rulesmodel.cpp:580 +#, kde-format +msgid "" +"KWin tries to prevent windows from taking the focus\n" +"(\"activate\") while you're working in another window,\n" +"but this may sometimes fail or superact.\n" +"\"None\" will unconditionally allow this window to get the focus while\n" +"\"Extreme\" will completely prevent it from taking the focus." +msgstr "" + +#: rulesmodel.cpp:589 +#, fuzzy, kde-format +#| msgid "&Focus stealing prevention" +msgid "Focus protection" +msgstr "Пр&едотвратяване открадването на фокуса" + +#: rulesmodel.cpp:591 +#, kde-format +msgid "" +"This controls the focus protection of the currently active window.\n" +"None will always give the focus away,\n" +"Extreme will keep it.\n" +"Otherwise it's interleaved with the stealing prevention\n" +"assigned to the window that wants the focus." +msgstr "" + +#: rulesmodel.cpp:600 +#, fuzzy, kde-format +#| msgid "Accept &focus" +msgid "Accept focus" +msgstr "Приемане на &фокуса" + +#: rulesmodel.cpp:602 +#, kde-format +msgid "" +"Windows may prevent to get the focus (activate) when being clicked.\n" +"On the other hand you might wish to prevent a window\n" +"from getting focused on a mouse click." +msgstr "" + +#: rulesmodel.cpp:608 +#, kde-format +msgid "Ignore global shortcuts" +msgstr "Пренебрегване на общите бързи клавиши" + +#: rulesmodel.cpp:610 +#, kde-format +msgid "" +"When used, a window will receive\n" +"all keyboard inputs while it is active, including Alt+Tab etc.\n" +"This is especially interesting for emulators or virtual machines.\n" +"\n" +"Be warned:\n" +"you won't be able to Alt+Tab out of the window\n" +"nor use any other global shortcut (such as Alt+F2 to show KRunner)\n" +"while it's active!" +msgstr "" + +#: rulesmodel.cpp:621 +#, fuzzy, kde-format +#| msgid "&Closeable" +msgid "Closeable" +msgstr "Р&азрешено затваряне" + +#: rulesmodel.cpp:626 +#, fuzzy, kde-format +#| msgid "Window &type" +msgid "Set window type" +msgstr "&Вид прозорец" + +#: rulesmodel.cpp:632 +#, kde-format +msgid "Desktop file name" +msgstr "" + +#: rulesmodel.cpp:637 +#, kde-format +msgid "Block compositing" +msgstr "" + +#: rulesmodel.cpp:717 +#, kde-format +msgid "Normal Window" +msgstr "Обикновен прозорец" + +#: rulesmodel.cpp:718 +#, kde-format +msgid "Dialog Window" +msgstr "Диалогов прозорец" + +#: rulesmodel.cpp:719 +#, kde-format +msgid "Utility Window" +msgstr "Прозорец с инструменти" + +#: rulesmodel.cpp:720 +#, kde-format +msgid "Dock (panel)" +msgstr "Системен панел" + +#: rulesmodel.cpp:721 +#, kde-format +msgid "Toolbar" +msgstr "Лента с инструменти" + +#: rulesmodel.cpp:722 +#, kde-format +msgid "Torn-Off Menu" +msgstr "Откачено меню" + +#: rulesmodel.cpp:723 +#, kde-format +msgid "Splash Screen" +msgstr "Начален екран" + +#: rulesmodel.cpp:724 +#, kde-format +msgid "Desktop" +msgstr "Работен плот" + +#. i18n("Unmanaged Window") }, deprecated +#: rulesmodel.cpp:726 +#, kde-format +msgid "Standalone Menubar" +msgstr "Независимо меню" + +#: rulesmodel.cpp:741 +#, kde-format +msgid "All Desktops" +msgstr "Всички работни плотове" + +#: rulesmodel.cpp:754 +#, kde-format +msgid "All Activities" +msgstr "" + +#: rulesmodel.cpp:775 +#, kde-format +msgid "Default" +msgstr "По подразбиране" + +#: rulesmodel.cpp:776 +#, kde-format +msgid "No Placement" +msgstr "Без разположение" + +#: rulesmodel.cpp:777 +#, kde-format +msgid "Minimal Overlapping" +msgstr "" + +#: rulesmodel.cpp:778 +#, fuzzy, kde-format +#| msgid "Maximizing" +msgid "Maximized" +msgstr "Максимизирано" + +#: rulesmodel.cpp:779 +#, fuzzy, kde-format +#| msgid "Cascade" +msgid "Cascaded" +msgstr "Каскадно" + +#: rulesmodel.cpp:780 +#, kde-format +msgid "Centered" +msgstr "Центрирано" + +#: rulesmodel.cpp:781 +#, kde-format +msgid "Random" +msgstr "Случайно" + +#: rulesmodel.cpp:782 +#, fuzzy, kde-format +#| msgid "Top-Left Corner" +msgid "In Top-Left Corner" +msgstr "В горния десен ъгъл" + +#: rulesmodel.cpp:783 +#, kde-format +msgid "Under Mouse" +msgstr "Под мишката" + +#: rulesmodel.cpp:784 +#, kde-format +msgid "On Main Window" +msgstr "В главния прозорец" + +#: rulesmodel.cpp:792 +#, fuzzy, kde-format +#| msgctxt "no focus stealing prevention" +#| msgid "None" +msgid "None" +msgstr "Без" + +#: rulesmodel.cpp:793 +#, kde-format +msgid "Low" +msgstr "Слабо" + +#: rulesmodel.cpp:794 +#, kde-format +msgid "Normal" +msgstr "Нормално" + +#: rulesmodel.cpp:795 +#, kde-format +msgid "High" +msgstr "Силно" + +#: rulesmodel.cpp:796 +#, kde-format +msgid "Extreme" +msgstr "Крайно" \ No newline at end of file diff --git a/po/bg/kcm_kwintabbox.po b/po/bg/kcm_kwintabbox.po new file mode 100644 index 0000000..96a3de0 --- /dev/null +++ b/po/bg/kcm_kwintabbox.po @@ -0,0 +1,235 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Yasen Pramatarov , 2009, 2011. +msgid "" +msgstr "" +"Project-Id-Version: kcm_kwintabbox\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-08-15 02:23+0200\n" +"PO-Revision-Date: 2011-04-29 22:11+0300\n" +"Last-Translator: Yasen Pramatarov \n" +"Language-Team: Bulgarian \n" +"Language: bg\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 1.0\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#: kwintabboxconfigform.cpp:77 +#, kde-format +msgid "KWin" +msgstr "" + +#: layoutpreview.cpp:147 +#, fuzzy, kde-format +#| msgid "All Desktops" +msgctxt "An example Desktop Name" +msgid "Desktop 1" +msgstr "Всички работни плотове" + +#: main.cpp:65 +#, kde-format +msgid "Main" +msgstr "Основни" + +#: main.cpp:66 +#, kde-format +msgid "Alternative" +msgstr "Алтернативни" + +#: main.cpp:68 +#, kde-format +msgid "Get New Task Switchers..." +msgstr "" + +#: main.cpp:78 +#, kde-format +msgid "" +"Focus policy settings limit the functionality of navigating through windows." +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_3) +#: main.ui:32 +#, kde-format +msgid "Content" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, showDesktop) +#: main.ui:41 +#, fuzzy, kde-format +#| msgid "Include desktop" +msgid "Include \"Show Desktop\" icon" +msgstr "Включване на работния плот" + +#. i18n: ectx: property (text), item, widget (KComboBox, switchingModeCombo) +#: main.ui:55 +#, kde-format +msgid "Recently used" +msgstr "Скорошно използвани" + +#. i18n: ectx: property (text), item, widget (KComboBox, switchingModeCombo) +#: main.ui:60 +#, kde-format +msgid "Stacking order" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, oneAppWindow) +#: main.ui:68 +#, kde-format +msgid "Only one window per application" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_8) +#: main.ui:78 +#, kde-format +msgid "Sort order:" +msgstr "Подреждане:" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox) +#: main.ui:104 +#, fuzzy, kde-format +#| msgid "List windows:" +msgid "Filter windows by" +msgstr "Списък на прозорците:" + +#. i18n: ectx: property (text), widget (QCheckBox, filterDesktops) +#: main.ui:113 +#, fuzzy, kde-format +#| msgid "All Desktops" +msgid "Virtual desktops" +msgstr "Всички работни плотове" + +#. i18n: ectx: property (text), widget (QRadioButton, currentDesktop) +#: main.ui:157 +#, fuzzy, kde-format +#| msgid "Current Desktop" +msgid "Current desktop" +msgstr "Текущ работен плот" + +#. i18n: ectx: property (text), widget (QRadioButton, otherDesktops) +#: main.ui:164 +#, fuzzy, kde-format +#| msgid "All Desktops" +msgid "All other desktops" +msgstr "Всички работни плотове" + +#. i18n: ectx: property (text), widget (QCheckBox, filterActivities) +#: main.ui:174 +#, kde-format +msgid "Activities" +msgstr "" + +#. i18n: ectx: property (text), widget (QRadioButton, currentActivity) +#: main.ui:218 +#, kde-format +msgid "Current activity" +msgstr "" + +#. i18n: ectx: property (text), widget (QRadioButton, otherActivities) +#: main.ui:225 +#, kde-format +msgid "All other activities" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, filterScreens) +#: main.ui:235 +#, kde-format +msgid "Screens" +msgstr "" + +#. i18n: ectx: property (text), widget (QRadioButton, currentScreen) +#: main.ui:279 +#, fuzzy, kde-format +#| msgid "Current Desktop" +msgid "Current screen" +msgstr "Текущ работен плот" + +#. i18n: ectx: property (text), widget (QRadioButton, otherScreens) +#: main.ui:286 +#, kde-format +msgid "All other screens" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, filterMinimization) +#: main.ui:296 +#, fuzzy, kde-format +#| msgid "Minimum Size" +msgid "Minimization" +msgstr "Минимален размер" + +#. i18n: ectx: property (text), widget (QRadioButton, visibleWindows) +#: main.ui:340 +#, fuzzy, kde-format +#| msgid "List windows:" +msgid "Visible windows" +msgstr "Списък на прозорците:" + +#. i18n: ectx: property (text), widget (QRadioButton, hiddenWindows) +#: main.ui:347 +#, fuzzy, kde-format +#| msgid "List windows:" +msgid "Hidden windows" +msgstr "Списък на прозорците:" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_4) +#: main.ui:386 +#, kde-format +msgid "Shortcuts" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: main.ui:395 main.ui:438 +#, kde-format +msgid "Forward" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: main.ui:418 +#, fuzzy, kde-format +#| msgid "List windows:" +msgid "All windows" +msgstr "Списък на прозорците:" + +#. i18n: ectx: property (text), widget (QLabel, label_4) +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: main.ui:428 main.ui:448 +#, kde-format +msgid "Reverse" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: main.ui:470 +#, kde-format +msgid "Current application" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#: main.ui:489 +#, fuzzy, kde-format +#| msgid "Navigation" +msgid "Visualization" +msgstr "Навигация" + +#. i18n: ectx: property (toolTip), widget (QComboBox, effectCombo) +#: main.ui:519 +#, kde-format +msgid "The effect to replace the list window when desktop effects are active." +msgstr "" + +#. i18n: ectx: property (toolTip), widget (QCheckBox, kcfg_HighlightWindows) +#: main.ui:549 +#, kde-format +msgid "" +"The currently selected window will be highlighted by fading out all other " +"windows. This option requires desktop effects to be active." +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_HighlightWindows) +#: main.ui:552 +#, fuzzy, kde-format +#| msgid "Show outline of selected window" +msgid "Show selected window" +msgstr "Показване на рамка около избраиня прозорец" \ No newline at end of file diff --git a/po/bg/kcmkwincompositing.po b/po/bg/kcmkwincompositing.po new file mode 100644 index 0000000..09d9e4e --- /dev/null +++ b/po/bg/kcmkwincompositing.po @@ -0,0 +1,231 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Yasen Pramatarov , 2009, 2011. +msgid "" +msgstr "" +"Project-Id-Version: kcmkwincompositing\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-08-08 02:22+0200\n" +"PO-Revision-Date: 2011-07-23 15:14+0300\n" +"Last-Translator: Yasen Pramatarov \n" +"Language-Team: Bulgarian \n" +"Language: bg\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 1.2\n" + +#. i18n: ectx: property (text), widget (KMessageWidget, glCrashedWarning) +#: compositing.ui:32 +#, kde-format +msgid "" +"OpenGL compositing (the default) has crashed KWin in the past.\n" +"This was most likely due to a driver bug.\n" +"If you think that you have meanwhile upgraded to a stable driver,\n" +"you can reset this protection but be aware that this might result in an " +"immediate crash!\n" +"Alternatively, you might want to use the XRender backend instead." +msgstr "" + +#. i18n: ectx: property (text), widget (KMessageWidget, scaleWarning) +#: compositing.ui:45 +#, kde-format +msgid "" +"Scale method \"Accurate\" is not supported by all hardware and can cause " +"performance regressions and rendering artifacts." +msgstr "" + +#. i18n: ectx: property (text), widget (KMessageWidget, windowThumbnailWarning) +#: compositing.ui:68 +#, kde-format +msgid "" +"Keeping the window thumbnail always interferes with the minimized state of " +"windows. This can result in windows not suspending their work when minimized." +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Enabled) +#: compositing.ui:80 +#, fuzzy, kde-format +#| msgctxt "@option:check" +#| msgid "Enable desktop effects at startup" +msgid "Enable compositor on startup" +msgstr "Включване на настолните ефекти при зареждане" + +#. i18n: ectx: property (text), widget (QLabel, animationSpeedLabel) +#: compositing.ui:87 +#, fuzzy, kde-format +#| msgid "Animation speed:" +msgid "Animation speed:" +msgstr "Скорост на анимацията:" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: compositing.ui:124 +#, fuzzy, kde-format +#| msgid "Very Slow" +msgid "Very slow" +msgstr "Много бавно" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: compositing.ui:144 +#, kde-format +msgid "Instant" +msgstr "Моментално" + +#. i18n: ectx: property (text), widget (QLabel, scaleMethodLabel) +#: compositing.ui:156 +#, fuzzy, kde-format +#| msgid "Scale method:" +msgid "Scale method:" +msgstr "Метод на мащабиране:" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_XRenderSmoothScale) +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glTextureFilter) +#: compositing.ui:166 compositing.ui:180 +#, kde-format +msgid "Crisp" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_XRenderSmoothScale) +#: compositing.ui:171 +#, kde-format +msgid "Smooth (slower)" +msgstr "Плавно (бавно)" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glTextureFilter) +#: compositing.ui:185 +#, kde-format +msgid "Smooth" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glTextureFilter) +#: compositing.ui:190 +#, kde-format +msgid "Accurate" +msgstr "Точно" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: compositing.ui:207 +#, kde-format +msgid "Rendering backend:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: compositing.ui:224 +#, kde-format +msgid "Tearing prevention (\"vsync\"):" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glPreferBufferSwap) +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_HiddenPreviews) +#: compositing.ui:232 compositing.ui:268 +#, fuzzy, kde-format +#| msgctxt "" +#| "Windows are unmapped as they are requested. This can lead to not having " +#| "updated thumbnials for windows on other desktops." +#| msgid "Never" +msgid "Never" +msgstr "Никога" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glPreferBufferSwap) +#: compositing.ui:237 +#, kde-format +msgid "Automatic" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glPreferBufferSwap) +#: compositing.ui:242 +#, kde-format +msgid "Only when cheap" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glPreferBufferSwap) +#: compositing.ui:247 +#, kde-format +msgid "Full screen repaints" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_glPreferBufferSwap) +#: compositing.ui:252 +#, kde-format +msgid "Re-use screen content" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: compositing.ui:260 +#, fuzzy, kde-format +#| msgid "Keep window thumbnails:" +msgid "Keep window thumbnails:" +msgstr "Запазване миниатюри на прозорците:" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_HiddenPreviews) +#: compositing.ui:273 +#, fuzzy, kde-format +#| msgctxt "" +#| "Windows are not unmapped if the window is somewhere visible on any of the " +#| "virtual desktops." +#| msgid "Only for Shown Windows" +msgid "Only for Shown Windows" +msgstr "Само за показваните прозорци" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_HiddenPreviews) +#: compositing.ui:278 +#, kde-format +msgid "Always" +msgstr "" + +#. i18n: ectx: property (toolTip), widget (QCheckBox, kcfg_WindowsBlockCompositing) +#: compositing.ui:288 +#, kde-format +msgid "" +"Applications can set a hint to block compositing when the window is open.\n" +" This brings performance improvements for e.g. games.\n" +" The setting can be overruled by window-specific rules." +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_WindowsBlockCompositing) +#: compositing.ui:291 +#, kde-format +msgid "Allow applications to block compositing" +msgstr "" + +#: main.cpp:80 +#, kde-format +msgid "Re-enable OpenGL detection" +msgstr "" + +#: main.cpp:132 +#, kde-format +msgid "" +"\"Only when cheap\" only prevents tearing for full screen changes like a " +"video." +msgstr "" + +#: main.cpp:136 +#, kde-format +msgid "\"Full screen repaints\" can cause performance problems." +msgstr "" + +#: main.cpp:140 +#, kde-format +msgid "" +"\"Re-use screen content\" causes severe performance problems on MESA drivers." +msgstr "" + +#: main.cpp:160 +#, fuzzy, kde-format +#| msgid "OpenGL" +msgid "OpenGL 3.1" +msgstr "OpenGL" + +#: main.cpp:161 +#, fuzzy, kde-format +#| msgid "OpenGL" +msgid "OpenGL 2.0" +msgstr "OpenGL" + +#: main.cpp:162 +#, kde-format +msgid "XRender" +msgstr "XRender" \ No newline at end of file diff --git a/po/bg/kcmkwinscreenedges.po b/po/bg/kcmkwinscreenedges.po new file mode 100644 index 0000000..19594e5 --- /dev/null +++ b/po/bg/kcmkwinscreenedges.po @@ -0,0 +1,234 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Yasen Pramatarov , 2009, 2010. +msgid "" +msgstr "" +"Project-Id-Version: kcmkwinscreenedges\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-08-08 02:22+0200\n" +"PO-Revision-Date: 2010-06-24 11:31+0300\n" +"Last-Translator: Yasen Pramatarov \n" +"Language-Team: Bulgarian \n" +"Language: bg\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: KBabel 1.11.4\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 "" + +#: main.cpp:123 touch.cpp:122 +#, kde-format +msgid "No Action" +msgstr "Без действие" + +#: main.cpp:124 touch.cpp:123 +#, kde-format +msgid "Show Desktop" +msgstr "Показване на работния плот" + +#: main.cpp:125 touch.cpp:124 +#, kde-format +msgid "Lock Screen" +msgstr "Заключване на екрана" + +#: main.cpp:126 touch.cpp:125 +#, kde-format +msgid "Show KRunner" +msgstr "" + +#: main.cpp:127 touch.cpp:126 +#, kde-format +msgid "Activity Manager" +msgstr "" + +#: main.cpp:128 touch.cpp:127 +#, kde-format +msgid "Application Launcher" +msgstr "" + +#: main.cpp:132 touch.cpp:131 +#, fuzzy, kde-format +#| msgid "All Desktops" +msgid "%1 - All Desktops" +msgstr "Всички работни плотове" + +#: main.cpp:133 touch.cpp:132 +#, fuzzy, kde-format +#| msgid "Current Desktop" +msgid "%1 - Current Desktop" +msgstr "Текущ работен плот" + +#: main.cpp:134 touch.cpp:133 +#, kde-format +msgid "%1 - Current Application" +msgstr "" + +#: main.cpp:137 touch.cpp:136 +#, fuzzy, kde-format +#| msgid "Cube" +msgid "%1 - Cube" +msgstr "Куб" + +#: main.cpp:138 touch.cpp:137 +#, fuzzy, kde-format +#| msgid "Cylinder" +msgid "%1 - Cylinder" +msgstr "Цилиндър" + +#: main.cpp:139 touch.cpp:138 +#, fuzzy, kde-format +#| msgid "Sphere" +msgid "%1 - Sphere" +msgstr "Сфера" + +#: main.cpp:141 touch.cpp:140 +#, kde-format +msgid "Toggle window switching" +msgstr "" + +#: main.cpp:142 touch.cpp:141 +#, kde-format +msgid "Toggle alternative window switching" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, infoLabel) +#: main.ui:23 +#, kde-format +msgid "" +"You can trigger an action by pushing the mouse cursor against the " +"corresponding screen edge or corner." +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, quickMaximizeLabel) +#: main.ui:67 +#, kde-format +msgid "&Maximize:" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ElectricBorderMaximize) +#: main.ui:77 +#, kde-format +msgid "Windows dragged to top edge" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, quickTileLabel) +#: main.ui:84 +#, kde-format +msgid "&Tile:" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ElectricBorderTiling) +#: main.ui:94 +#, kde-format +msgid "Windows dragged to left or right edge" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, electricBorderCornerRatioLabel) +#: main.ui:101 +#, kde-format +msgid "Trigger &quarter tiling in:" +msgstr "" + +#. i18n: ectx: property (suffix), widget (QSpinBox, electricBorderCornerRatioSpin) +#: main.ui:116 +#, no-c-format, kde-format +msgid "%" +msgstr "" + +#. i18n: ectx: property (prefix), widget (QSpinBox, electricBorderCornerRatioSpin) +#: main.ui:119 +#, kde-format +msgid "Outer " +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_1) +#: main.ui:135 +#, kde-format +msgid "of the screen" +msgstr "" + +#. i18n: ectx: property (toolTip), widget (QLabel, desktopSwitchLabel) +#: main.ui:144 +#, kde-format +msgid "" +"Change desktop when the mouse cursor is pushed against the edge of the screen" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, desktopSwitchLabel) +#: main.ui:147 +#, kde-format +msgid "&Switch desktop on edge:" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_ElectricBorders) +#: main.ui:158 +#, kde-format +msgctxt "Switch desktop on edge" +msgid "Disabled" +msgstr "Изключено" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_ElectricBorders) +#: main.ui:163 +#, kde-format +msgid "Only When Moving Windows" +msgstr "Само при преместване на прозорците" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_ElectricBorders) +#: main.ui:168 +#, kde-format +msgid "Always Enabled" +msgstr "Винаги включено" + +#. i18n: ectx: property (toolTip), widget (QLabel, activationDelayLabel) +#: main.ui:176 +#, kde-format +msgid "" +"Amount of time required for the mouse cursor to be pushed against the edge " +"of the screen before the action is triggered" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, activationDelayLabel) +#: main.ui:179 +#, kde-format +msgid "Activation &delay:" +msgstr "" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_ElectricBorderDelay) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_ElectricBorderCooldown) +#: main.ui:189 main.ui:224 +#, kde-format +msgid " ms" +msgstr " мсек" + +#. i18n: ectx: property (toolTip), widget (QLabel, triggerCooldownLabel) +#: main.ui:208 +#, kde-format +msgid "" +"Amount of time required after triggering an action until the next trigger " +"can occur" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, triggerCooldownLabel) +#: main.ui:211 +#, kde-format +msgid "&Reactivation delay:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_1) +#: touch.ui:17 +#, kde-format +msgid "" +"You can trigger an action by swiping from the screen edge towards the center " +"of the screen." +msgstr "" \ No newline at end of file diff --git a/po/bg/kcmkwm.po b/po/bg/kcmkwm.po new file mode 100644 index 0000000..693833b --- /dev/null +++ b/po/bg/kcmkwm.po @@ -0,0 +1,1425 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Zlatko Popov , 2006, 2007, 2008. +# Yasen Pramatarov , 2009. +msgid "" +msgstr "" +"Project-Id-Version: kcmkwm\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-08-29 02:26+0200\n" +"PO-Revision-Date: 2009-06-21 21:01+0300\n" +"Last-Translator: Yasen Pramatarov \n" +"Language-Team: Bulgarian \n" +"Language: bg\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: KBabel 1.11.4\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 "radnev@yahoo.com" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_1) +#: actions.ui:17 +#, fuzzy, kde-format +#| msgid "Inactive Inner Window" +msgid "Inactive Inner Window Actions" +msgstr "Щракване вътре в неактивен прозорец" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#. i18n: ectx: property (text), widget (QLabel, label_1) +#: actions.ui:26 mouse.ui:177 +#, fuzzy, kde-format +#| msgid "&Titlebar double-click:" +msgid "&Left click:" +msgstr "Дво&йно щракване на заглавието:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandWindow1) +#: actions.ui:39 +#, kde-format +msgid "" +"In this row you can customize left click behavior when clicking into an " +"inactive inner window ('inner' means: not titlebar, not frame)." +msgstr "" +"Поведение на неактивен прозорец при щракване с левия бутон на мишката вътре " +"в него (не върху заглавието или рамката му)." + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow3) +#: actions.ui:43 actions.ui:83 actions.ui:123 +#, fuzzy, kde-format +#| msgid "Activate, Raise & Pass Click" +msgid "Activate, raise and pass click" +msgstr "Активиране, извеждане напред и предаване на щракването" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow3) +#: actions.ui:48 actions.ui:88 actions.ui:128 +#, fuzzy, kde-format +#| msgid "Activate & Pass Click" +msgid "Activate and pass click" +msgstr "Активиране и предаване на щракването" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow3) +#: actions.ui:53 actions.ui:93 actions.ui:133 mouse.ui:293 mouse.ui:408 +#: mouse.ui:523 +#, kde-format +msgid "Activate" +msgstr "Активиране" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow3) +#: actions.ui:58 actions.ui:98 actions.ui:138 mouse.ui:283 mouse.ui:398 +#: mouse.ui:513 +#, fuzzy, kde-format +#| msgid "Activate & Raise" +msgid "Activate and raise" +msgstr "Активиране и извеждане напред" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: actions.ui:66 mouse.ui:200 +#, fuzzy, kde-format +#| msgid "&Titlebar double-click:" +msgid "&Middle click:" +msgstr "Дво&йно щракване на заглавието:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandWindow2) +#: actions.ui:79 +#, kde-format +msgid "" +"In this row you can customize middle click behavior when clicking into an " +"inactive inner window ('inner' means: not titlebar, not frame)." +msgstr "" +"Поведение на неактивен прозорец при щракване със средния бутон на мишката " +"вътре в него (не върху заглавието или рамката му)." + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: actions.ui:106 mouse.ui:213 +#, kde-format +msgid "&Right click:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandWindow3) +#: actions.ui:119 +#, kde-format +msgid "" +"In this row you can customize right click behavior when clicking into an " +"inactive inner window ('inner' means: not titlebar, not frame)." +msgstr "" +"Поведение на неактивен прозорец при щракване с десния бутон на мишката вътре " +"в него (не върху заглавието или рамката му)." + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: actions.ui:146 mouse.ui:88 +#, fuzzy, kde-format +#| msgid "Mouse wheel:" +msgid "Mouse &wheel:" +msgstr "Колелце на мишката:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandWindowWheel) +#: actions.ui:159 +#, kde-format +msgid "" +"In this row you can customize behavior when scrolling into an inactive inner " +"window ('inner' means: not titlebar, not frame)." +msgstr "" +"Поведение на неактивен прозорец при действие с колелцето на мишката вътре в " +"него (не върху заглавието или рамката му)." + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindowWheel) +#: actions.ui:163 +#, kde-format +msgid "Scroll" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindowWheel) +#: actions.ui:168 +#, fuzzy, kde-format +#| msgid "Activate & Lower" +msgid "Activate and scroll" +msgstr "Активиране и извеждане назад" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindowWheel) +#: actions.ui:173 +#, fuzzy, kde-format +#| msgid "Activate, Raise and Move" +msgid "Activate, raise and scroll" +msgstr "Активиране, извеждане напред и преместване" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#: actions.ui:184 +#, fuzzy, kde-format +#| msgid "Inner Window, Titlebar && Frame" +msgid "Inner Window, Titlebar and Frame Actions" +msgstr "Щракване върху прозорец с натиснат клавиш за модификация" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: actions.ui:195 +#, fuzzy, kde-format +#| msgid "Modifier key:" +msgid "Mo&difier key:" +msgstr "Клавиш за модификация:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandAllKey) +#: actions.ui:205 +#, kde-format +msgid "" +"Here you select whether holding the Meta key or Alt key will allow you to " +"perform the following actions." +msgstr "" +"Избор на клавиша за модификация. От тук може да укажете дали да се ползва " +"клавиша Alt, Win или Meta." + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllKey) +#: actions.ui:209 +#, kde-format +msgid "Meta" +msgstr "Meta" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllKey) +#: actions.ui:214 +#, kde-format +msgid "Alt" +msgstr "Alt" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: actions.ui:236 +#, kde-format +msgid " + " +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_8) +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: actions.ui:248 mouse.ui:601 +#, fuzzy, kde-format +#| msgid "&Titlebar double-click:" +msgid "L&eft click:" +msgstr "Дво&йно щракване на заглавието:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandAll1) +#: actions.ui:261 +#, kde-format +msgid "" +"In this row you can customize left click behavior when clicking into the " +"titlebar or the frame." +msgstr "" +"Поведение на прозорец при щракване с левия бутон на мишката върху заглавието " +"или рамката му." + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:265 actions.ui:335 actions.ui:405 +#, kde-format +msgid "Move" +msgstr "Преместване" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:270 actions.ui:340 actions.ui:410 +#, fuzzy, kde-format +#| msgid "Activate, Raise and Move" +msgid "Activate, raise and move" +msgstr "Активиране, извеждане напред и преместване" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:275 actions.ui:345 actions.ui:415 mouse.ui:246 mouse.ui:308 +#: mouse.ui:361 mouse.ui:423 mouse.ui:476 mouse.ui:538 +#, fuzzy, kde-format +#| msgid "Toggle Raise & Lower" +msgid "Toggle raise and lower" +msgstr "Извеждане напред/назад" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:280 actions.ui:350 actions.ui:420 +#, kde-format +msgid "Resize" +msgstr "Промяна на размера" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:285 actions.ui:355 actions.ui:425 mouse.ui:236 mouse.ui:298 +#: mouse.ui:351 mouse.ui:413 mouse.ui:466 mouse.ui:528 +#, kde-format +msgid "Raise" +msgstr "Извеждане напред" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:290 actions.ui:360 actions.ui:430 mouse.ui:65 mouse.ui:241 +#: mouse.ui:303 mouse.ui:356 mouse.ui:418 mouse.ui:471 mouse.ui:533 +#, kde-format +msgid "Lower" +msgstr "Извеждане назад" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:295 actions.ui:365 actions.ui:435 mouse.ui:55 mouse.ui:251 +#: mouse.ui:313 mouse.ui:366 mouse.ui:428 mouse.ui:481 mouse.ui:543 +#, kde-format +msgid "Minimize" +msgstr "Минимизиране" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:300 actions.ui:370 actions.ui:440 +#, fuzzy, kde-format +#| msgid "Change Opacity" +msgid "Decrease opacity" +msgstr "Промяна на непрозрачност" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:305 actions.ui:375 actions.ui:445 +#, fuzzy, kde-format +#| msgid "Change Opacity" +msgid "Increase opacity" +msgstr "Промяна на непрозрачност" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:310 actions.ui:380 actions.ui:450 actions.ui:505 mouse.ui:80 +#: mouse.ui:132 mouse.ui:271 mouse.ui:333 mouse.ui:386 mouse.ui:448 +#: mouse.ui:501 mouse.ui:563 +#, fuzzy, kde-format +#| msgid "Nothing" +msgid "Do nothing" +msgstr "Нищо (без операция)" + +#. i18n: ectx: property (text), widget (QLabel, label_8) +#: actions.ui:318 +#, fuzzy, kde-format +#| msgid "Middle button:" +msgid "Middle &click:" +msgstr "Среден бутон:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandAll2) +#: actions.ui:331 +#, kde-format +msgid "" +"In this row you can customize middle click behavior when clicking into the " +"titlebar or the frame." +msgstr "" +"Поведение на прозорец при щракване със средния бутон на мишката върху " +"заглавието или рамката му." + +#. i18n: ectx: property (text), widget (QLabel, label_10) +#. i18n: ectx: property (text), widget (QLabel, label_9) +#: actions.ui:388 mouse.ui:671 +#, kde-format +msgid "Right clic&k:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:401 +#, kde-format +msgid "" +"In this row you can customize right click behavior when clicking into the " +"titlebar or the frame." +msgstr "" +"Поведение на прозорец при щракване с десния бутон на мишката върху " +"заглавието или рамката му." + +#. i18n: ectx: property (text), widget (QLabel, label_10) +#: actions.ui:458 +#, fuzzy, kde-format +#| msgid "Mouse wheel:" +msgid "Mo&use wheel:" +msgstr "Колелце на мишката:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:471 +#, fuzzy, kde-format +#| msgid "" +#| "Here you can customize KDE's behavior when scrolling with the mouse " +#| "wheel in a window while pressing the modifier key." +msgid "" +"Here you can customize KDE's behavior when scrolling with the mouse wheel in " +"a window while pressing the modifier key." +msgstr "" +"Поведение на прозорец при превъртане на колелцето на мишката вътре в него " +"(вътре, върху рамката или заглавието) заедно с натиснат клавиш за " +"модификация от клавиатурата." + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:475 mouse.ui:102 +#, fuzzy, kde-format +#| msgid "Raise/Lower" +msgid "Raise/lower" +msgstr "Извеждане напред/назад" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:480 mouse.ui:107 +#, fuzzy, kde-format +#| msgid "Shade/Unshade" +msgid "Shade/unshade" +msgstr "Сгъване/разгъване" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:485 mouse.ui:112 +#, fuzzy, kde-format +#| msgid "Maximize/Restore" +msgid "Maximize/restore" +msgstr "Максимизиране/възстановяване" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:490 mouse.ui:117 +#, fuzzy, kde-format +#| msgid "Keep Above/Below" +msgid "Keep above/below" +msgstr "На преден/заден план" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:495 mouse.ui:122 +#, fuzzy, kde-format +#| msgid "Move to Previous/Next Desktop" +msgid "Move to previous/next desktop" +msgstr "Преместване към предишен/следващ плот" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:500 mouse.ui:127 +#, fuzzy, kde-format +#| msgid "Change Opacity" +msgid "Change opacity" +msgstr "Промяна на непрозрачност" + +#. i18n: ectx: property (text), widget (QLabel, shadeHoverLabel) +#: advanced.ui:20 +#, fuzzy, kde-format +#| msgid "Window Actio&ns" +msgid "Window &unshading:" +msgstr "Действия за &прозореца" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_ShadeHover) +#: advanced.ui:32 +#, fuzzy, kde-format +#| msgid "" +#| "If Shade Hover is enabled, a shaded window will un-shade automatically " +#| "when the mouse pointer has been over the title bar for some time." +msgid "" +"

If this option is enabled, a shaded window will " +"unshade automatically when the mouse pointer has been over the titlebar for " +"some time.

" +msgstr "" +"Автоматично разгъване на сгънат прозорец, ако показалецът на мишката бъде " +"задържан върху заглавието на прозореца за определено време." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ShadeHover) +#: advanced.ui:35 +#, kde-format +msgid "On titlebar hover after:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QSpinBox, kcfg_ShadeHoverInterval) +#: advanced.ui:42 +#, kde-format +msgid "" +"Sets the time in milliseconds before the window unshades when the mouse " +"pointer goes over the shaded window." +msgstr "" +"Автоматично разгъване на сгънат прозорец, ако показалецът на мишката бъде " +"задържан върху заглавието на прозореца за определено време." + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_ShadeHoverInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_DelayFocusInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_AutoRaiseInterval) +#: advanced.ui:45 focus.ui:85 focus.ui:178 +#, kde-format +msgid " ms" +msgstr " мсек" + +#. i18n: ectx: property (text), widget (QLabel, windowPlacementLabel) +#: advanced.ui:66 +#, fuzzy, kde-format +#| msgid "&Placement:" +msgid "Window &placement:" +msgstr "Разполо&жение:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_Placement) +#: advanced.ui:76 +#, kde-format +msgid "" +"

The placement policy determines where a new window " +"will appear on the desktop.

  • Smart will try to achieve a minimum overlap of windows
  • Maximizing will try to maximize every window to fill the " +"whole screen. It might be useful to selectively affect placement of some " +"windows using the window-specific settings.
  • Cascade will " +"cascade the windows
  • Random will use a random " +"position
  • Centered will place the window " +"centered
  • Zero-cornered will place the window in " +"the top-left corner
  • Under mouse will place the " +"window under the pointer
" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:80 +#, fuzzy, kde-format +#| msgid "Snap windows onl&y when overlapping" +msgid "Minimal Overlapping" +msgstr "Пр&илепване на прозорците само когато се препокриват" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:85 +#, fuzzy, kde-format +#| msgid "Maximize" +msgid "Maximized" +msgstr "Максимизиране" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:90 +#, fuzzy, kde-format +#| msgid "Cascade" +msgid "Cascaded" +msgstr "Каскадно" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:95 +#, kde-format +msgid "Random" +msgstr "Случайно" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:100 +#, kde-format +msgid "Centered" +msgstr "Центрирано" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:105 +#, kde-format +msgid "In Top-Left Corner" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:110 +#, fuzzy, kde-format +#| msgid "Focus Under Mouse" +msgid "Under mouse" +msgstr "Фокусът е под мишката" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_AllowKDEAppsToRememberWindowPositions) +#: advanced.ui:118 +#, kde-format +msgid "" +"When turned on, KDE apps which are able to remember the positions of their " +"windows are allowed to do so. This will override the window placement mode " +"defined above." +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AllowKDEAppsToRememberWindowPositions) +#: advanced.ui:121 +#, kde-format +msgid "Allow KDE apps to remember the positions of their own windows" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, specialWindowsLabel) +#: advanced.ui:128 +#, fuzzy, kde-format +#| msgid "Windows" +msgid "&Special windows:" +msgstr "Прозорци" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_HideUtilityWindowsForInactive) +#: advanced.ui:138 +#, kde-format +msgid "" +"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." +msgstr "" +"Ако отметката е включена, помощните прозорци на неактивните програми, като " +"ленти за задачи, откачащи се менюта и др., ще бъдат скривани, когато " +"програмата (главния прозорец) не е активен. Когато програмата стане активна, " +"те ще се показват обратно." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_HideUtilityWindowsForInactive) +#: advanced.ui:141 +#, kde-format +msgid "Hide utility windows for inactive applications" +msgstr "Скриване на помощните прозорци на неактивните програми" + +#. i18n: ectx: property (text), widget (QLabel, windowFocusPolicyLabel) +#: focus.ui:22 +#, kde-format +msgid "Window &activation policy:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QComboBox, windowFocusPolicy) +#: focus.ui:32 +#, kde-format +msgid "With this option you can specify how and when windows will be focused." +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:36 +#, fuzzy, kde-format +#| msgid "Click to Focus" +msgctxt "sassa asas" +msgid "Click to focus" +msgstr "Щракване за получаване на фокус" + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:41 +#, kde-format +msgid "Click to focus (mouse precedence)" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:46 +#, fuzzy, kde-format +#| msgid "Focus Follows Mouse" +msgid "Focus follows mouse" +msgstr "Фокусът следва мишката" + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:51 +#, kde-format +msgid "Focus follows mouse (mouse precedence)" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:56 +#, fuzzy, kde-format +#| msgid "Focus Under Mouse" +msgid "Focus under mouse" +msgstr "Фокусът е под мишката" + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:61 +#, fuzzy, kde-format +#| msgid "Focus Strictly Under Mouse" +msgid "Focus strictly under mouse" +msgstr "Фокусът е строго под мишката" + +#. i18n: ectx: property (text), widget (QLabel, delayFocusOnLabel) +#: focus.ui:69 +#, fuzzy, kde-format +#| msgid "Delay focus by:" +msgid "&Delay focus by:" +msgstr "Изчакване за фокус:" + +#. i18n: ectx: property (whatsThis), widget (QSpinBox, kcfg_DelayFocusInterval) +#: focus.ui:82 +#, kde-format +msgid "" +"This is the delay after which the window the mouse pointer is over will " +"automatically receive focus." +msgstr "" +"Пауза, след която прозорец, който се намира на заден план, автоматично ще се " +"изведе напред." + +#. i18n: ectx: property (text), widget (QLabel, focusStealingLabel) +#: focus.ui:101 +#, fuzzy, kde-format +#| msgid "Focus stealing prevention level:" +msgid "Focus &stealing prevention:" +msgstr "Ниво на предотвратяване на открадването на фокуса:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_FocusStealingPreventionLevel) +#: focus.ui:114 +#, fuzzy, kde-format +#| msgid "" +#| "

This option specifies how much KWin will try to prevent unwanted focus " +#| "stealing caused by unexpected activation of new windows. (Note: This " +#| "feature does not work with the Focus Under Mouse or Focus Strictly Under " +#| "Mouse focus policies.)

  • 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 normal level, depending on the applications.
  • Normal: 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.

" +msgid "" +"

This option specifies how much KWin will try to " +"prevent unwanted focus stealing caused by unexpected activation of new " +"windows. (Note: This feature does not work with the Focus under mouse or Focus strictly under mouse focus policies.)

  • 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.

" +msgstr "" +"При отваряне на нов прозорец, често фокусът се прехвърля на него. Т. е. " +"новият прозорец става активен. В някои случаи това е дразнещо и нежелано. От " +"тук може да установите степента на предотвратяване на нежеланото прехвърляне " +"на фокуса. Налични са следните възможности:
  • Без: Без " +"предотвратяване. Новият прозорец винаги се активира.
  • Слабо: " +"Предотвратяването е включено, като почти всички прозорци се активират. Ако " +"някой прозорец се опита да получи фокуса и не поддържа механизма за " +"определяне на това правило, фокусът му се прехвърля.
  • Нормално: Фокусът се прехвърля само на прозорците, които го искат и поддържат този " +"механизъм.
  • Силно: Новият прозорец се активира, само ако няма " +"друг активен прозорец или принадлежи към текущо активния прозорец.
  • " +"
  • Крайно: Всички прозорци трябва да бъдат активирани изрично от " +"потребителя.
" + +#. i18n: Focus Stealing Prevention Level +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_FocusStealingPreventionLevel) +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_BorderSnapZone) +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_WindowSnapZone) +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_CenterSnapZone) +#: focus.ui:118 moving.ui:53 moving.ui:75 moving.ui:97 +#, fuzzy, kde-format +#| msgctxt "Focus Stealing Prevention Level" +#| msgid "None" +msgid "None" +msgstr "Без" + +#. i18n: Focus Stealing Prevention Level +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_FocusStealingPreventionLevel) +#: focus.ui:123 +#, fuzzy, kde-format +#| msgctxt "Focus Stealing Prevention Level" +#| msgid "Low" +msgid "Low" +msgstr "Ниско" + +#. i18n: Focus Stealing Prevention Level +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_FocusStealingPreventionLevel) +#: focus.ui:128 +#, kde-format +msgid "Medium" +msgstr "" + +#. i18n: Focus Stealing Prevention Level +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_FocusStealingPreventionLevel) +#: focus.ui:133 +#, fuzzy, kde-format +#| msgctxt "Focus Stealing Prevention Level" +#| msgid "High" +msgid "High" +msgstr "Високо" + +#. i18n: Focus Stealing Prevention Level +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_FocusStealingPreventionLevel) +#: focus.ui:138 +#, fuzzy, kde-format +#| msgctxt "Focus Stealing Prevention Level" +#| msgid "Extreme" +msgid "Extreme" +msgstr "Екстремално" + +#. i18n: ectx: property (text), widget (QLabel, raisingWindowsLabel) +#: focus.ui:146 +#, fuzzy, kde-format +#| msgid "Windows" +msgid "Raising windows:" +msgstr "Прозорци" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_ClickRaise) +#: focus.ui:153 +#, kde-format +msgid "" +"When this option is enabled, the active window will be brought to the front " +"when you click somewhere into the window contents. To change it for inactive " +"windows, you need to change the settings in the Actions tab." +msgstr "" +"Ако е включена тази отметка, прозорец, който се намира на заден план, ще се " +"изведе на преден план само след щракване с бутон на мишката в него." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ClickRaise) +#: focus.ui:156 +#, fuzzy, kde-format +#| msgid "C&lick raises active window" +msgid "&Click raises active window" +msgstr "&Извеждане напред при щракване" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_AutoRaise) +#: focus.ui:165 +#, kde-format +msgid "" +"When this option is enabled, a window in the background will automatically " +"come to the front when the mouse pointer has been over it for some time." +msgstr "" +"Ако е включена тази отметка, прозорец, който се намира на заден план, " +"автоматично ще се изведе на преден план след известно време (времето " +"зададено в полето \"Пауза\")." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AutoRaise) +#: focus.ui:168 +#, kde-format +msgid "&Raise on hover, delayed by:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QSpinBox, kcfg_AutoRaiseInterval) +#: focus.ui:175 +#, kde-format +msgid "" +"This is the delay after which the window that the mouse pointer is over will " +"automatically come to the front." +msgstr "" +"Пауза, след която прозорец, който се намира на заден план, автоматично ще се " +"изведе напред." + +#. i18n: ectx: property (text), widget (QLabel, multiscreenBehaviorLabel) +#: focus.ui:196 +#, kde-format +msgid "Multiscreen behavior:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_ActiveMouseScreen) +#: focus.ui:203 +#, fuzzy, kde-format +#| msgid "" +#| "When this option is enabled, the active Xinerama screen (where new " +#| "windows appear, for example) is the screen containing the mouse pointer. " +#| "When disabled, the active Xinerama screen is the screen containing the " +#| "focused window. By default this option is disabled for Click to focus and " +#| "enabled for other focus policies." +msgid "" +"When this option is enabled, the active Xinerama screen (where new windows " +"appear, for example) is the screen containing the mouse pointer. When " +"disabled, the active Xinerama screen is the screen containing the focused " +"window. By default this option is disabled for Click to focus and enabled " +"for other focus policies." +msgstr "" +"Ако е включена тази отметка, Xinerama (където се появяват нови прозорци) е " +"екрана с показалеца. Ако е изключено екранът ще бъде този с фокусирания " +"прозорец. По подразбиране тази опция е изключена за \"Щракнете за фокус\" и " +"включена за другите правила." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ActiveMouseScreen) +#: focus.ui:206 +#, kde-format +msgid "Active screen follows &mouse" +msgstr "Активният екран следва &мишката" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_SeparateScreenFocus) +#: focus.ui:213 +#, kde-format +msgid "" +"When this option is enabled, focus operations are limited only to the active " +"Xinerama screen" +msgstr "" +"Ако е включена тази опция, фокусирането ще бъде ограничено до активния " +"Xinerama екран" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_SeparateScreenFocus) +#: focus.ui:216 +#, fuzzy, kde-format +#| msgid "S&eparate screen focus" +msgid "&Separate screen focus" +msgstr "&Разделяне фокуса на екрана" + +#. i18n: ectx: property (text), widget (QLabel, windowFocusPolicyDescriptionLabel) +#: focus.ui:229 +#, kde-format +msgid "Window activation policy description" +msgstr "" + +#: main.cpp:73 +#, kde-format +msgid "&Focus" +msgstr "&Фокус" + +#: main.cpp:84 +#, fuzzy, kde-format +#| msgid "&Titlebar Actions" +msgid "Titlebar A&ctions" +msgstr "Действия за &заглавието" + +#: main.cpp:91 +#, fuzzy, kde-format +#| msgid "Window Actio&ns" +msgid "W&indow Actions" +msgstr "Действия за &прозореца" + +#: main.cpp:98 +#, fuzzy, kde-format +#| msgid "&Placement:" +msgid "Mo&vement" +msgstr "Разполо&жение:" + +#: main.cpp:105 +#, fuzzy, kde-format +#| msgid "Ad&vanced" +msgid "Adva&nced" +msgstr "Доп&ълнителни" + +#: main.cpp:110 +#, kde-format +msgid "Window Behavior Configuration Module" +msgstr "Модул за настройване поведението на прозорците" + +#: main.cpp:112 +#, kde-format +msgid "(c) 1997 - 2002 KWin and KControl Authors" +msgstr "(c) 1997 - 2002 авторите на KWin и KControl" + +#: main.cpp:114 +#, kde-format +msgid "Matthias Ettrich" +msgstr "Matthias Ettrich" + +#: main.cpp:115 +#, kde-format +msgid "Waldo Bastian" +msgstr "Waldo Bastian" + +#: main.cpp:116 +#, kde-format +msgid "Cristian Tibirna" +msgstr "Cristian Tibirna" + +#: main.cpp:117 +#, kde-format +msgid "Matthias Kalle Dalheimer" +msgstr "Matthias Kalle Dalheimer" + +#: main.cpp:118 +#, kde-format +msgid "Daniel Molkentin" +msgstr "Daniel Molkentin" + +#: main.cpp:119 +#, kde-format +msgid "Wynn Wilkes" +msgstr "Wynn Wilkes" + +#: main.cpp:120 +#, kde-format +msgid "Pat Dowler" +msgstr "Pat Dowler" + +#: main.cpp:121 +#, kde-format +msgid "Bernd Wuebben" +msgstr "Bernd Wuebben" + +#: main.cpp:122 +#, kde-format +msgid "Matthias Hoelzer-Kluepfel" +msgstr "Matthias Hoelzer-Kluepfel" + +#: main.cpp:167 +#, kde-format +msgid "" +"

Window Behavior

Here you can customize the way windows behave " +"when being moved, resized or clicked on. You can also specify a focus policy " +"as well as a placement policy for new windows.

Please note that this " +"configuration will not take effect if you do not use KWin as your window " +"manager. If you do use a different window manager, please refer to its " +"documentation for how to customize window behavior.

" +msgstr "" +"

Поведение на прозорците

От тук може да настроите, поведението на " +"прозорците при преместване, промяна на размера или щракване с мишката.

Имайте предвид, че промените в този модул работят, само ако използвате " +"мениджър на прозорци KWin. Ако използвате друг мениджър на прозорци, то за " +"настройка се обърнете към неговата документация.

" + +#: main.cpp:187 +#, kde-format +msgid "&Titlebar Actions" +msgstr "Действия за &заглавието" + +#: main.cpp:193 +#, kde-format +msgid "Window Actio&ns" +msgstr "Действия за &прозореца" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_1) +#: mouse.ui:17 +#, fuzzy, kde-format +#| msgid "&Titlebar Actions" +msgid "Titlebar Actions" +msgstr "Действия за &заглавието" + +#. i18n: ectx: property (text), widget (QLabel, label_1) +#: mouse.ui:26 +#, fuzzy, kde-format +#| msgid "&Titlebar double-click:" +msgid "&Double-click:" +msgstr "Дво&йно щракване на заглавието:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#: mouse.ui:36 +#, kde-format +msgid "Behavior on double click into the titlebar." +msgstr "Поведение на прозорец при двойно щракване върху заглавието му." + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonLeftClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonMiddleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonRightClickCommand) +#: mouse.ui:40 mouse.ui:615 mouse.ui:650 mouse.ui:685 +#, fuzzy, kde-format +#| msgid "Maximize" +msgid "Maximize" +msgstr "Максимизиране" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonLeftClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonMiddleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonRightClickCommand) +#: mouse.ui:45 mouse.ui:620 mouse.ui:655 mouse.ui:690 +#, kde-format +msgid "Vertically maximize" +msgstr "" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonLeftClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonMiddleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MaximizeButtonRightClickCommand) +#: mouse.ui:50 mouse.ui:625 mouse.ui:660 mouse.ui:695 +#, kde-format +msgid "Horizontally maximize" +msgstr "" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#: mouse.ui:60 mouse.ui:256 mouse.ui:318 mouse.ui:371 mouse.ui:433 mouse.ui:486 +#: mouse.ui:548 +#, kde-format +msgid "Shade" +msgstr "Сгъване към заглавието" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#: mouse.ui:70 mouse.ui:261 mouse.ui:323 mouse.ui:376 mouse.ui:438 mouse.ui:491 +#: mouse.ui:553 +#, kde-format +msgid "Close" +msgstr "" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#: mouse.ui:75 +#, fuzzy, kde-format +#| msgid "On All Desktops" +msgid "Show on all desktops" +msgstr "Преместване към всички работни плотове" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandTitlebarWheel) +#: mouse.ui:98 +#, fuzzy, kde-format +#| msgid "Behavior on double click into the titlebar." +msgid "Behavior on mouse wheel scroll over the titlebar." +msgstr "Поведение на прозорец при двойно щракване върху заглавието му." + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#: mouse.ui:143 +#, fuzzy, kde-format +#| msgid "&Titlebar Actions" +msgid "Titlebar and Frame Actions" +msgstr "Действия за &заглавието" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: mouse.ui:167 +#, kde-format +msgid "Active" +msgstr "Активен прозорец" + +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: mouse.ui:190 +#, kde-format +msgid "Inactive" +msgstr "Неактивен прозорец" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandActiveTitlebar3) +#: mouse.ui:232 mouse.ui:347 mouse.ui:462 +#, kde-format +msgid "" +"Behavior on left click into the titlebar or frame of an active window." +msgstr "" +"Поведение на активен прозорец при щракване с левия бутон на мишката върху " +"заглавието или рамката му." + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#: mouse.ui:266 mouse.ui:328 mouse.ui:381 mouse.ui:443 mouse.ui:496 +#: mouse.ui:558 +#, fuzzy, kde-format +#| msgid "Operations Menu" +msgid "Show actions menu" +msgstr "Показване на менюто" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#: mouse.ui:279 mouse.ui:394 mouse.ui:509 +#, kde-format +msgid "" +"Behavior on left click into the titlebar or frame of an " +"inactive window." +msgstr "" +"Поведение на неактивен прозорец при щракване с левия бутон на мишката върху " +"заглавието или рамката му." + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#: mouse.ui:288 mouse.ui:403 mouse.ui:518 +#, fuzzy, kde-format +#| msgid "Activate & Lower" +msgid "Activate and lower" +msgstr "Активиране и извеждане назад" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_3) +#: mouse.ui:589 +#, fuzzy, kde-format +#| msgid "Maximize Button" +msgid "Maximize Button Actions" +msgstr "Бутон за максимизиране" + +#. i18n: ectx: property (whatsThis), widget (QLabel, label_8) +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_MaximizeButtonLeftClickCommand) +#: mouse.ui:598 mouse.ui:611 +#, kde-format +msgid "Behavior on left click onto the maximize button." +msgstr "" +"Поведение на прозорец при ляво щракване върху бутона му за " +"максимизиране." + +#. i18n: ectx: property (whatsThis), widget (QLabel, label_9) +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_MaximizeButtonMiddleClickCommand) +#: mouse.ui:633 mouse.ui:646 +#, kde-format +msgid "Behavior on middle click onto the maximize button." +msgstr "" +"Поведение на прозорец при средно щракване върху бутона му за " +"максимизиране." + +#. i18n: ectx: property (text), widget (QLabel, label_9) +#: mouse.ui:636 +#, fuzzy, kde-format +#| msgid "Middle button:" +msgid "Middle c&lick:" +msgstr "Среден бутон:" + +#. i18n: ectx: property (whatsThis), widget (QLabel, label_10) +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_MaximizeButtonRightClickCommand) +#: mouse.ui:668 mouse.ui:681 +#, kde-format +msgid "Behavior on right click onto the maximize button." +msgstr "" +"Поведение на прозорец при дясно щракване върху бутона му за " +"максимизиране." + +#. i18n: ectx: property (text), widget (QLabel, geometryTipLabel) +#: moving.ui:20 +#, kde-format +msgid "Window &geometry:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_GeometryTip) +#: moving.ui:30 +#, kde-format +msgid "" +"Enable this option if you want a window's geometry to be displayed while it " +"is being moved or resized. The window position relative to the top-left " +"corner of the screen is displayed together with its size." +msgstr "" +"Показване на координатите спрямо горния ляв ъгъл на екрана, заедно с размера " +"при операциите по преместване или промяна размера на прозорците." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_GeometryTip) +#: moving.ui:33 +#, fuzzy, kde-format +#| msgid "Display window &geometry when moving or resizing" +msgid "Display when moving or resizing" +msgstr "Показва&не на координатите при преместване или промяна на размера" + +#. i18n: ectx: property (text), widget (QLabel, borderSnapLabel) +#: moving.ui:40 +#, fuzzy, kde-format +#| msgid "&Border snap zone:" +msgid "Screen &edge snap zone:" +msgstr "Зона на прилепване към краи&щата на екрана:" + +#. i18n: ectx: property (whatsThis), widget (QSpinBox, kcfg_BorderSnapZone) +#: moving.ui:50 +#, fuzzy, kde-format +#| msgid "" +#| "Here you can set the snap zone for screen borders, i.e. the 'strength' of " +#| "the magnetic field which will make windows snap to the border when moved " +#| "near it." +msgid "" +"Here you can set the snap zone for screen edges, i.e. the 'strength' of the " +"magnetic field which will make windows snap to the border when moved near it." +msgstr "" +"Зона на прилепване на прозорците към краищата на екрана. При преместване на " +"прозореца, когато прозорецът навлезе в зоната, той автоматично ще прилепне " +"към края на екрана. По този начин се улеснява преместването на прозорците и " +"се уплътнява използването на екрана." + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_BorderSnapZone) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_WindowSnapZone) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_CenterSnapZone) +#: moving.ui:56 moving.ui:78 moving.ui:100 +#, fuzzy, kde-format +#| msgid " pixel" +#| msgid_plural " pixels" +msgid " px" +msgstr " пиксел" + +#. i18n: ectx: property (whatsThis), widget (QSpinBox, kcfg_WindowSnapZone) +#: moving.ui:72 +#, kde-format +msgid "" +"Here you can set the snap zone for windows, i.e. the 'strength' of the " +"magnetic field which will make windows snap to each other when they are " +"moved near another window." +msgstr "" +"Зона на прилепване на прозорците към рамките на другите прозорци. При " +"преместване, когато прозорецът навлезе в зоната, той автоматично ще прилепне " +"към края на другия прозорец, без да го препокрива или да оставя празно място " +"между тях. По този начин се улеснява преместването на прозорците и се " +"уплътнява използването на екрана." + +#. i18n: ectx: property (whatsThis), widget (QSpinBox, kcfg_CenterSnapZone) +#: moving.ui:94 +#, fuzzy, kde-format +#| msgid "" +#| "Here you can set the snap zone for screen borders, i.e. the 'strength' of " +#| "the magnetic field which will make windows snap to the border when moved " +#| "near it." +msgid "" +"Here you can set the snap zone for the screen center, i.e. the 'strength' of " +"the magnetic field which will make windows snap to the center of the screen " +"when moved near it." +msgstr "" +"Зона на прилепване на прозорците към краищата на екрана. При преместване на " +"прозореца, когато прозорецът навлезе в зоната, той автоматично ще прилепне " +"към края на екрана. По този начин се улеснява преместването на прозорците и " +"се уплътнява използването на екрана." + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_SnapOnlyWhenOverlapping) +#: moving.ui:113 +#, kde-format +msgid "" +"Here you can set that windows will be only snapped if you try to overlap " +"them, i.e. they will not be snapped if the windows comes only near another " +"window or border." +msgstr "" +"Прилепване на прозорците, само когато се препокриват. Ако няма опасност да " +"се препокрият, няма да се активира прилепването независимо, че прозорецът се " +"намира в зоната." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_SnapOnlyWhenOverlapping) +#: moving.ui:116 +#, fuzzy, kde-format +#| msgid "Snap windows onl&y when overlapping" +msgid "Only when overlapping" +msgstr "Пр&илепване на прозорците само когато се препокриват" + +#. i18n: ectx: property (text), widget (QLabel, windowSnapLabel) +#: moving.ui:123 +#, kde-format +msgid "&Window snap zone:" +msgstr "Зона на прилепване към рамките на прозор&ците:" + +#. i18n: ectx: property (text), widget (QLabel, centerSnaplabel) +#: moving.ui:133 +#, fuzzy, kde-format +#| msgid "&Border snap zone:" +msgid "&Center snap zone:" +msgstr "Зона на прилепване към краи&щата на екрана:" + +#. i18n: ectx: property (text), widget (QLabel, OverlapSnapLabel) +#: moving.ui:143 +#, fuzzy, kde-format +#| msgid "Windows" +msgid "&Snap windows:" +msgstr "Прозорци" + +#: windows.cpp:87 +#, kde-format +msgid "" +"Click to focus: A window becomes active when you click into it. " +"This behavior is common on other operating systems and likely what you want." +msgstr "" + +#: windows.cpp:91 +#, kde-format +msgid "" +"Click to focus (mouse precedence): Mostly the same as Click to " +"focus. If an active window has to be chosen by the system (eg. because " +"the currently active one was closed) the window under the mouse is the " +"preferred candidate. Unusual, but possible variant of Click to focus." +msgstr "" + +#: windows.cpp:96 +#, kde-format +msgid "" +"Focus follows mouse: Moving the mouse onto a window will activate " +"it. Eg. windows randomly appearing under the mouse will not gain the focus. " +"Focus stealing prevention takes place as usual. Think as Click " +"to focus just without having to actually click." +msgstr "" + +#: windows.cpp:100 +#, kde-format +msgid "" +"This is mostly the same as Focus follows mouse. If an active window " +"has to be chosen by the system (eg. because the currently active one was " +"closed) the window under the mouse is the preferred candidate. Choose this, " +"if you want a hover controlled focus." +msgstr "" + +#: windows.cpp:105 +#, kde-format +msgid "" +"Focus under mouse: The focus always remains on the window under the " +"mouse.
Warning: Focus stealing prevention and " +"the tabbox ('Alt+Tab') contradict the activation policy and will " +"not work. You very likely want to use Focus follows mouse (mouse " +"precedence) instead!" +msgstr "" + +#: windows.cpp:109 +#, kde-format +msgid "" +"Focus strictly under mouse: The focus is always on the window under " +"the mouse (in doubt nowhere) very much like the focus behavior in an " +"unmanaged legacy X11 environment.
Warning: Focus " +"stealing prevention and the tabbox ('Alt+Tab') contradict the " +"activation policy and will not work. You very likely want to use Focus " +"follows mouse (mouse precedence) instead!" +msgstr "" \ No newline at end of file diff --git a/po/bg/kwin.po b/po/bg/kwin.po new file mode 100644 index 0000000..a96e944 --- /dev/null +++ b/po/bg/kwin.po @@ -0,0 +1,2628 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Zlatko Popov , 2006, 2007, 2008, 2009. +# Yasen Pramatarov , 2009, 2010. +msgid "" +msgstr "" +"Project-Id-Version: kwin\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-11-25 08:45+0100\n" +"PO-Revision-Date: 2010-12-02 00:25+0200\n" +"Last-Translator: Yasen Pramatarov \n" +"Language-Team: Bulgarian \n" +"Language: bg\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: KBabel 1.11.4\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 "radnev@yahoo.com" + +#: abstract_client.cpp:2729 +#, kde-format +msgctxt "Application is not responding, appended to window title" +msgid "(Not Responding)" +msgstr "" + +#: abstract_wayland_output.cpp:252 +#, kde-format +msgid "unknown" +msgstr "" + +#: colorcorrection/manager.cpp:58 +#, kde-format +msgctxt "Night Color was disabled" +msgid "Night Color Off" +msgstr "" + +#: colorcorrection/manager.cpp:59 +#, kde-format +msgctxt "Night Color was enabled" +msgid "Night Color On" +msgstr "" + +#: colorcorrection/manager.cpp:228 colorcorrection/manager.cpp:231 +#: colorcorrection/manager.cpp:238 +#, kde-format +msgid "Toggle Night Color" +msgstr "" + +#: composite.cpp:926 +#, fuzzy, kde-format +#| msgid "" +#| "Compositing has been suspended by another application.
You can resume " +#| "using the '%1' shortcut." +msgid "" +"Desktop effects have been suspended by another application.
You can " +"resume using the '%1' shortcut." +msgstr "" +"Композирането е спряно от друга програма.
Можете да го продължите с " +"краткия клавиш \"%1\"." + +#: debug_console.cpp:65 +#, kde-format +msgid "Timestamp" +msgstr "" + +#: debug_console.cpp:70 +#, kde-format +msgid "Timestamp (µsec)" +msgstr "" + +#: debug_console.cpp:77 +#, kde-format +msgctxt "A mouse button" +msgid "Left" +msgstr "" + +#: debug_console.cpp:79 +#, kde-format +msgctxt "A mouse button" +msgid "Right" +msgstr "" + +#: debug_console.cpp:81 +#, kde-format +msgctxt "A mouse button" +msgid "Middle" +msgstr "" + +#: debug_console.cpp:83 +#, kde-format +msgctxt "A mouse button" +msgid "Back" +msgstr "" + +#: debug_console.cpp:85 +#, kde-format +msgctxt "A mouse button" +msgid "Forward" +msgstr "" + +#: debug_console.cpp:87 +#, kde-format +msgctxt "A mouse button" +msgid "Task" +msgstr "" + +#: debug_console.cpp:89 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 4" +msgstr "" + +#: debug_console.cpp:91 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 5" +msgstr "" + +#: debug_console.cpp:93 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 6" +msgstr "" + +#: debug_console.cpp:95 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 7" +msgstr "" + +#: debug_console.cpp:97 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 8" +msgstr "" + +#: debug_console.cpp:99 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 9" +msgstr "" + +#: debug_console.cpp:101 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 10" +msgstr "" + +#: debug_console.cpp:103 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 11" +msgstr "" + +#: debug_console.cpp:105 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 12" +msgstr "" + +#: debug_console.cpp:107 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 13" +msgstr "" + +#: debug_console.cpp:109 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 14" +msgstr "" + +#: debug_console.cpp:111 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 15" +msgstr "" + +#: debug_console.cpp:113 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 16" +msgstr "" + +#: debug_console.cpp:115 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 17" +msgstr "" + +#: debug_console.cpp:117 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 18" +msgstr "" + +#: debug_console.cpp:119 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 19" +msgstr "" + +#: debug_console.cpp:121 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 20" +msgstr "" + +#: debug_console.cpp:123 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 21" +msgstr "" + +#: debug_console.cpp:125 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 22" +msgstr "" + +#: debug_console.cpp:127 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 23" +msgstr "" + +#: debug_console.cpp:129 +#, kde-format +msgctxt "A mouse button" +msgid "Extra Button 24" +msgstr "" + +#: debug_console.cpp:138 debug_console.cpp:140 +#, kde-format +msgid "Input Device" +msgstr "" + +#: debug_console.cpp:138 +#, kde-format +msgctxt "The input device of the event is not known" +msgid "Unknown" +msgstr "" + +#: debug_console.cpp:175 +#, kde-format +msgctxt "A mouse pointer motion event" +msgid "Pointer Motion" +msgstr "" + +#: debug_console.cpp:182 +#, kde-format +msgctxt "The relative mouse movement" +msgid "Delta" +msgstr "" + +#: debug_console.cpp:186 +#, kde-format +msgctxt "The relative mouse movement" +msgid "Delta (not accelerated)" +msgstr "" + +#: debug_console.cpp:189 +#, kde-format +msgctxt "The global mouse pointer position" +msgid "Global Position" +msgstr "" + +#: debug_console.cpp:193 +#, kde-format +msgctxt "A mouse pointer button press event" +msgid "Pointer Button Press" +msgstr "" + +#: debug_console.cpp:196 debug_console.cpp:204 +#, kde-format +msgctxt "A button in a mouse press/release event" +msgid "Button" +msgstr "" + +#: debug_console.cpp:197 debug_console.cpp:205 +#, kde-format +msgctxt "A button in a mouse press/release event" +msgid "Native Button code" +msgstr "" + +#: debug_console.cpp:198 debug_console.cpp:206 +#, kde-format +msgctxt "All currently pressed buttons in a mouse press/release event" +msgid "Pressed Buttons" +msgstr "" + +#: debug_console.cpp:201 +#, kde-format +msgctxt "A mouse pointer button release event" +msgid "Pointer Button Release" +msgstr "" + +#: debug_console.cpp:221 +#, kde-format +msgctxt "A mouse pointer axis (wheel) event" +msgid "Pointer Axis" +msgstr "" + +#: debug_console.cpp:225 +#, kde-format +msgctxt "The orientation of a pointer axis event" +msgid "Orientation" +msgstr "" + +#: debug_console.cpp:226 +#, kde-format +msgctxt "An orientation of a pointer axis event" +msgid "Horizontal" +msgstr "" + +#: debug_console.cpp:227 +#, kde-format +msgctxt "An orientation of a pointer axis event" +msgid "Vertical" +msgstr "" + +#: debug_console.cpp:228 +#, kde-format +msgctxt "The angle delta of a pointer axis event" +msgid "Delta" +msgstr "" + +#: debug_console.cpp:243 +#, kde-format +msgctxt "A key press event" +msgid "Key Press" +msgstr "" + +#: debug_console.cpp:246 +#, kde-format +msgctxt "A key release event" +msgid "Key Release" +msgstr "" + +#: debug_console.cpp:255 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Shift" +msgstr "" + +#: debug_console.cpp:259 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Control" +msgstr "" + +#: debug_console.cpp:263 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Alt" +msgstr "" + +#: debug_console.cpp:267 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Meta" +msgstr "" + +#: debug_console.cpp:271 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Keypad" +msgstr "" + +#: debug_console.cpp:275 +#, kde-format +msgctxt "A keyboard modifier" +msgid "Group-switch" +msgstr "" + +#: debug_console.cpp:281 +#, kde-format +msgctxt "Whether the event is an automatic key repeat" +msgid "Repeat" +msgstr "" + +#: debug_console.cpp:285 +#, kde-format +msgctxt "The code as read from the input device" +msgid "Scan code" +msgstr "" + +#: debug_console.cpp:286 +#, kde-format +msgctxt "Key according to Qt" +msgid "Qt::Key code" +msgstr "" + +#: debug_console.cpp:288 +#, kde-format +msgctxt "The translated code to an Xkb symbol" +msgid "Xkb symbol" +msgstr "" + +#: debug_console.cpp:289 +#, kde-format +msgctxt "The translated code interpreted as text" +msgid "Utf8" +msgstr "" + +#: debug_console.cpp:290 +#, kde-format +msgctxt "The currently active modifiers" +msgid "Modifiers" +msgstr "" + +#: debug_console.cpp:302 +#, kde-format +msgctxt "A touch down event" +msgid "Touch down" +msgstr "" + +#: debug_console.cpp:304 debug_console.cpp:319 debug_console.cpp:334 +#, kde-format +msgctxt "The id of the touch point in the touch event" +msgid "Point identifier" +msgstr "" + +#: debug_console.cpp:305 debug_console.cpp:320 +#, kde-format +msgctxt "The global position of the touch point" +msgid "Global position" +msgstr "" + +#: debug_console.cpp:317 +#, kde-format +msgctxt "A touch motion event" +msgid "Touch Motion" +msgstr "" + +#: debug_console.cpp:332 +#, kde-format +msgctxt "A touch up event" +msgid "Touch Up" +msgstr "" + +#: debug_console.cpp:345 +#, kde-format +msgctxt "A pinch gesture is started" +msgid "Pinch start" +msgstr "" + +#: debug_console.cpp:347 +#, kde-format +msgctxt "Number of fingers in this pinch gesture" +msgid "Finger count" +msgstr "" + +#: debug_console.cpp:358 +#, kde-format +msgctxt "A pinch gesture is updated" +msgid "Pinch update" +msgstr "" + +#: debug_console.cpp:360 +#, kde-format +msgctxt "Current scale in pinch gesture" +msgid "Scale" +msgstr "" + +#: debug_console.cpp:361 +#, kde-format +msgctxt "Current angle in pinch gesture" +msgid "Angle delta" +msgstr "" + +#: debug_console.cpp:362 +#, kde-format +msgctxt "Current delta in pinch gesture" +msgid "Delta x" +msgstr "" + +#: debug_console.cpp:363 +#, kde-format +msgctxt "Current delta in pinch gesture" +msgid "Delta y" +msgstr "" + +#: debug_console.cpp:374 +#, kde-format +msgctxt "A pinch gesture ended" +msgid "Pinch end" +msgstr "" + +#: debug_console.cpp:386 +#, kde-format +msgctxt "A pinch gesture got cancelled" +msgid "Pinch cancelled" +msgstr "" + +#: debug_console.cpp:398 +#, kde-format +msgctxt "A swipe gesture is started" +msgid "Swipe start" +msgstr "" + +#: debug_console.cpp:400 +#, kde-format +msgctxt "Number of fingers in this swipe gesture" +msgid "Finger count" +msgstr "" + +#: debug_console.cpp:411 +#, kde-format +msgctxt "A swipe gesture is updated" +msgid "Swipe update" +msgstr "" + +#: debug_console.cpp:413 +#, kde-format +msgctxt "Current delta in swipe gesture" +msgid "Delta x" +msgstr "" + +#: debug_console.cpp:414 +#, kde-format +msgctxt "Current delta in swipe gesture" +msgid "Delta y" +msgstr "" + +#: debug_console.cpp:425 +#, kde-format +msgctxt "A swipe gesture ended" +msgid "Swipe end" +msgstr "" + +#: debug_console.cpp:437 +#, kde-format +msgctxt "A swipe gesture got cancelled" +msgid "Swipe cancelled" +msgstr "" + +#: debug_console.cpp:449 +#, fuzzy, kde-format +#| msgid "Switch to Window Tab" +msgctxt "A hardware switch (e.g. notebook lid) got toggled" +msgid "Switch toggled" +msgstr "Превключване към подпрозорец" + +#: debug_console.cpp:457 +#, kde-format +msgctxt "Name of a hardware switch" +msgid "Notebook lid" +msgstr "" + +#: debug_console.cpp:459 +#, kde-format +msgctxt "Name of a hardware switch" +msgid "Tablet mode" +msgstr "" + +#: debug_console.cpp:461 +#, fuzzy, kde-format +#| msgid "Switch to Window Tab" +msgctxt "A hardware switch" +msgid "Switch" +msgstr "Превключване към подпрозорец" + +#: debug_console.cpp:465 +#, kde-format +msgctxt "The hardware switch got turned off" +msgid "Off" +msgstr "" + +#: debug_console.cpp:468 +#, kde-format +msgctxt "The hardware switch got turned on" +msgid "On" +msgstr "" + +#: debug_console.cpp:473 +#, kde-format +msgctxt "State of a hardware switch (on/off)" +msgid "State" +msgstr "" + +#: debug_console.cpp:488 +#, kde-format +msgid "Tablet Tool" +msgstr "" + +#: debug_console.cpp:489 +#, kde-format +msgid "EventType" +msgstr "" + +#: debug_console.cpp:490 debug_console.cpp:537 debug_console.cpp:549 +#, kde-format +msgid "Position" +msgstr "" + +#: debug_console.cpp:492 +#, kde-format +msgid "Tilt" +msgstr "" + +#: debug_console.cpp:494 +#, kde-format +msgid "Rotation" +msgstr "" + +#: debug_console.cpp:495 +#, kde-format +msgid "Pressure" +msgstr "" + +#: debug_console.cpp:496 +#, fuzzy, kde-format +#| msgid "Mouse Emulation" +msgid "Buttons" +msgstr "Емулация на мишка" + +#. i18n: ectx: property (title), widget (QGroupBox, modifiersBox) +#: debug_console.cpp:497 debug_console.ui:356 +#, kde-format +msgid "Modifiers" +msgstr "" + +#: debug_console.cpp:510 +#, kde-format +msgid "Tablet Tool Button" +msgstr "" + +#: debug_console.cpp:511 debug_console.cpp:526 +#, fuzzy, kde-format +#| msgid "Mouse Emulation" +msgid "Pressed Buttons" +msgstr "Емулация на мишка" + +#: debug_console.cpp:525 +#, kde-format +msgid "Tablet Pad Button" +msgstr "" + +#: debug_console.cpp:535 +#, kde-format +msgid "Tablet Pad Strip" +msgstr "" + +#: debug_console.cpp:536 debug_console.cpp:548 +#, kde-format +msgid "Number" +msgstr "" + +#: debug_console.cpp:538 debug_console.cpp:550 +#, kde-format +msgid "isFinger" +msgstr "" + +#: debug_console.cpp:547 +#, kde-format +msgid "Tablet Pad Ring" +msgstr "" + +#: debug_console.cpp:735 +#, fuzzy, kde-format +#| msgid "Mouse Emulation" +msgid "No Mouse Buttons" +msgstr "Емулация на мишка" + +#: debug_console.cpp:739 +#, kde-format +msgctxt "Mouse Button" +msgid "left" +msgstr "" + +#: debug_console.cpp:742 +#, kde-format +msgctxt "Mouse Button" +msgid "right" +msgstr "" + +#: debug_console.cpp:745 +#, kde-format +msgctxt "Mouse Button" +msgid "middle" +msgstr "" + +#: debug_console.cpp:748 +#, kde-format +msgctxt "Mouse Button" +msgid "back" +msgstr "" + +#: debug_console.cpp:751 +#, kde-format +msgctxt "Mouse Button" +msgid "forward" +msgstr "" + +#: debug_console.cpp:754 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 1" +msgstr "" + +#: debug_console.cpp:757 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 2" +msgstr "" + +#: debug_console.cpp:760 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 3" +msgstr "" + +#: debug_console.cpp:763 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 4" +msgstr "" + +#: debug_console.cpp:766 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 5" +msgstr "" + +#: debug_console.cpp:769 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 6" +msgstr "" + +#: debug_console.cpp:772 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 7" +msgstr "" + +#: debug_console.cpp:775 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 8" +msgstr "" + +#: debug_console.cpp:778 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 9" +msgstr "" + +#: debug_console.cpp:781 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 10" +msgstr "" + +#: debug_console.cpp:784 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 11" +msgstr "" + +#: debug_console.cpp:787 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 12" +msgstr "" + +#: debug_console.cpp:790 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 13" +msgstr "" + +#: debug_console.cpp:793 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 14" +msgstr "" + +#: debug_console.cpp:796 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 15" +msgstr "" + +#: debug_console.cpp:799 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 16" +msgstr "" + +#: debug_console.cpp:802 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 17" +msgstr "" + +#: debug_console.cpp:805 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 18" +msgstr "" + +#: debug_console.cpp:808 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 19" +msgstr "" + +#: debug_console.cpp:811 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 20" +msgstr "" + +#: debug_console.cpp:814 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 21" +msgstr "" + +#: debug_console.cpp:817 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 22" +msgstr "" + +#: debug_console.cpp:820 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 23" +msgstr "" + +#: debug_console.cpp:823 +#, kde-format +msgctxt "Mouse Button" +msgid "extra 24" +msgstr "" + +#: debug_console.cpp:826 +#, kde-format +msgctxt "Mouse Button" +msgid "task" +msgstr "" + +#: debug_console.cpp:1176 +#, fuzzy, kde-format +#| msgid "Close Window" +msgid "X11 Client Windows" +msgstr "Затваряне на прозорец" + +#: debug_console.cpp:1178 +#, kde-format +msgid "X11 Unmanaged Windows" +msgstr "" + +#: debug_console.cpp:1180 +#, fuzzy, kde-format +#| msgid "Shade Window" +msgid "Wayland Windows" +msgstr "Сгъване на прозорец" + +#: debug_console.cpp:1182 +#, fuzzy, kde-format +#| msgid "Lower Window" +msgid "Internal Windows" +msgstr "Понижаване на прозорец" + +#. i18n: ectx: property (text), widget (QPushButton, quitButton) +#: debug_console.ui:32 +#, kde-format +msgid "Quit Debug Console" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, windows) +#: debug_console.ui:45 +#, kde-format +msgid "Windows" +msgstr "Прозорци" + +#. i18n: ectx: attribute (title), widget (QWidget, surfaces) +#: debug_console.ui:59 +#, kde-format +msgid "Surfaces" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, input) +#: debug_console.ui:69 +#, kde-format +msgid "Input Events" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, inputDevices) +#: debug_console.ui:86 +#, kde-format +msgid "Input Devices" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, tab) +#: debug_console.ui:96 +#, kde-format +msgid "OpenGL" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, noOpenGLLabel) +#: debug_console.ui:102 +#, kde-format +msgid "No OpenGL compositor running" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, driverInfoBox) +#: debug_console.ui:130 +#, kde-format +msgid "OpenGL (ES) driver information" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: debug_console.ui:136 +#, kde-format +msgid "Vendor:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: debug_console.ui:143 +#, kde-format +msgid "Renderer:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: debug_console.ui:150 +#, kde-format +msgid "Version:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: debug_console.ui:157 +#, kde-format +msgid "Shading Language Version:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: debug_console.ui:164 +#, kde-format +msgid "Driver:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: debug_console.ui:171 +#, kde-format +msgid "GPU class:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: debug_console.ui:178 +#, kde-format +msgid "OpenGL Version:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_8) +#: debug_console.ui:185 +#, kde-format +msgid "GLSL Version:" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, platformExtensionsBox) +#: debug_console.ui:251 +#, kde-format +msgid "Platform Extensions" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, glExtensionsBox) +#: debug_console.ui:267 +#, kde-format +msgid "OpenGL (ES) Extensions" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, keyboard) +#: debug_console.ui:288 +#, fuzzy, kde-format +#| msgid "Next Layout" +msgid "Keyboard" +msgstr "Следваща подредба" + +#. i18n: ectx: property (title), widget (QGroupBox, layoutBox) +#: debug_console.ui:315 +#, fuzzy, kde-format +#| msgid "Next Layout" +msgid "Keymap Layouts" +msgstr "Следваща подредба" + +#. i18n: ectx: property (text), widget (QLabel, label_9) +#: debug_console.ui:337 +#, fuzzy, kde-format +#| msgid "Configur&e Window Behavior..." +msgid "Current Layout:" +msgstr "&Настройване..." + +#. i18n: ectx: property (title), widget (QGroupBox, activeModifiersBox) +#: debug_console.ui:372 +#, kde-format +msgid "Active Modifiers" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, ledsBox) +#: debug_console.ui:388 +#, kde-format +msgid "LEDs" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, activeLedsBox) +#: debug_console.ui:404 +#, kde-format +msgid "Active LEDs" +msgstr "" + +#: helpers/killer/killer.cpp:30 +#, fuzzy, kde-format +#| msgid "KDE window manager" +msgid "Window Manager" +msgstr "Мениджър на прозорци" + +#: helpers/killer/killer.cpp:35 +#, kde-format +msgid "PID of the application to terminate" +msgstr "Спиране - PID на програма." + +#: helpers/killer/killer.cpp:35 +#, kde-format +msgid "pid" +msgstr "" + +#: helpers/killer/killer.cpp:37 +#, kde-format +msgid "Hostname on which the application is running" +msgstr "Хост, където е стартирана програмата." + +#: helpers/killer/killer.cpp:37 +#, kde-format +msgid "hostname" +msgstr "" + +#: helpers/killer/killer.cpp:39 +#, kde-format +msgid "Caption of the window to be terminated" +msgstr "Заглавие на прозореца, който да бъде прекъснат." + +#: helpers/killer/killer.cpp:39 +#, kde-format +msgid "caption" +msgstr "" + +#: helpers/killer/killer.cpp:41 +#, kde-format +msgid "Name of the application to be terminated" +msgstr "Име на програмата за спиране." + +#: helpers/killer/killer.cpp:41 +#, kde-format +msgid "name" +msgstr "" + +#: helpers/killer/killer.cpp:43 +#, kde-format +msgid "ID of resource belonging to the application" +msgstr "ИД - ресурса на програмата." + +#: helpers/killer/killer.cpp:43 +#, kde-format +msgid "id" +msgstr "" + +#: helpers/killer/killer.cpp:45 +#, kde-format +msgid "Time of user action causing termination" +msgstr "Време, което да предизвика убиване на действието." + +#: helpers/killer/killer.cpp:45 +#, kde-format +msgid "time" +msgstr "" + +#: helpers/killer/killer.cpp:47 +#, kde-format +msgid "KWin helper utility" +msgstr "KWin - помощна програма" + +#: helpers/killer/killer.cpp:71 +#, kde-format +msgid "This helper utility is not supposed to be called directly." +msgstr "Този модул не може да се изпълнява директно." + +#: helpers/killer/killer.cpp:81 +#, kde-format +msgctxt "@info" +msgid "Application \"%1\" is not responding" +msgstr "" + +#: helpers/killer/killer.cpp:83 +#, kde-kuit-format +msgctxt "@info" +msgid "" +"

You tried to close window \"%1\" from application \"%2\" (Process ID: %3) " +"but the application is not responding.

" +msgstr "" + +#: helpers/killer/killer.cpp:85 +#, kde-kuit-format +msgctxt "@info" +msgid "" +"

You tried to close window \"%1\" from application \"%2\" (Process ID: " +"%3), running on host \"%4\", but the application is not responding.

" +msgstr "" + +#: helpers/killer/killer.cpp:88 +#, kde-kuit-format +msgctxt "@info" +msgid "" +"

Do you want to terminate this application?

Terminating the " +"application will close all of its child windows. Any unsaved data will be " +"lost.

" +msgstr "" + +#: helpers/killer/killer.cpp:92 +#, kde-format +msgid "&Terminate Application %1" +msgstr "&Прекъсване на програмата %1" + +#: helpers/killer/killer.cpp:93 +#, kde-format +msgid "Wait Longer" +msgstr "" + +#: keyboard_layout.cpp:110 keyboard_layout.cpp:111 +#, fuzzy, kde-format +#| msgid "Next Layout" +msgctxt "tooltip title" +msgid "Keyboard Layout" +msgstr "Следваща подредба" + +#: keyboard_layout.cpp:258 +#, fuzzy, kde-format +#| msgid "Configur&e Window Behavior..." +msgid "Configure Layouts..." +msgstr "&Настройване..." + +#: killwindow.cpp:33 +#, kde-format +msgid "" +"Select window to force close with left click or enter.\n" +"Escape or right click to cancel." +msgstr "" + +#: kwinbindings.cpp:38 +#, kde-format +msgid "Window Operations Menu" +msgstr "Показване меню на прозорец" + +#: kwinbindings.cpp:40 +#, kde-format +msgid "Close Window" +msgstr "Затваряне на прозорец" + +#: kwinbindings.cpp:42 +#, kde-format +msgid "Maximize Window" +msgstr "Максимизиране на прозорец" + +#: kwinbindings.cpp:44 +#, kde-format +msgid "Maximize Window Vertically" +msgstr "Максимизиране на прозорец вертикално" + +#: kwinbindings.cpp:46 +#, kde-format +msgid "Maximize Window Horizontally" +msgstr "Максимизиране на прозорец хоризонтално" + +#: kwinbindings.cpp:48 +#, kde-format +msgid "Minimize Window" +msgstr "Минимизиране на прозорец" + +#: kwinbindings.cpp:50 +#, kde-format +msgid "Shade Window" +msgstr "Сгъване на прозорец" + +#: kwinbindings.cpp:52 +#, kde-format +msgid "Move Window" +msgstr "Преместване на прозорец" + +#: kwinbindings.cpp:54 +#, kde-format +msgid "Resize Window" +msgstr "Промяна размер на прозорец" + +#: kwinbindings.cpp:56 +#, kde-format +msgid "Raise Window" +msgstr "Повишаване на прозорец" + +#: kwinbindings.cpp:58 +#, kde-format +msgid "Lower Window" +msgstr "Понижаване на прозорец" + +#: kwinbindings.cpp:60 +#, kde-format +msgid "Toggle Window Raise/Lower" +msgstr "Превключване между повишаване/понижаване на прозорец" + +#: kwinbindings.cpp:62 +#, kde-format +msgid "Make Window Fullscreen" +msgstr "Прозорец на цял екран" + +#: kwinbindings.cpp:64 +#, kde-format +msgid "Hide Window Border" +msgstr "Премахване рамката на прозорец" + +#: kwinbindings.cpp:66 +#, kde-format +msgid "Keep Window Above Others" +msgstr "Преместване на прозорец над останалите прозорци" + +#: kwinbindings.cpp:68 +#, kde-format +msgid "Keep Window Below Others" +msgstr "Преместване на прозорец под останалите прозорци" + +#: kwinbindings.cpp:70 +#, kde-format +msgid "Activate Window Demanding Attention" +msgstr "Активиране на прозорец при събитие" + +#: kwinbindings.cpp:72 +#, kde-format +msgid "Setup Window Shortcut" +msgstr "Настройване бързите клавиши на прозореца" + +#: kwinbindings.cpp:74 +#, kde-format +msgid "Pack Window to the Right" +msgstr "Свиване на прозорец надясно" + +#: kwinbindings.cpp:76 +#, kde-format +msgid "Pack Window to the Left" +msgstr "Свиване на прозорец наляво" + +#: kwinbindings.cpp:78 +#, kde-format +msgid "Pack Window Up" +msgstr "Свиване на прозорец нагоре" + +#: kwinbindings.cpp:80 +#, kde-format +msgid "Pack Window Down" +msgstr "Свиване на прозорец надолу" + +#: kwinbindings.cpp:82 +#, kde-format +msgid "Pack Grow Window Horizontally" +msgstr "Увеличаване на прозорец хоризонтално" + +#: kwinbindings.cpp:84 +#, kde-format +msgid "Pack Grow Window Vertically" +msgstr "Увеличаване на прозорец вертикално" + +#: kwinbindings.cpp:86 +#, kde-format +msgid "Pack Shrink Window Horizontally" +msgstr "Намаляване на прозорец хоризонтално" + +#: kwinbindings.cpp:88 +#, kde-format +msgid "Pack Shrink Window Vertically" +msgstr "Намаляване на прозорец вертикално" + +#: kwinbindings.cpp:90 +#, kde-format +msgid "Quick Tile Window to the Left" +msgstr "Подреждане на прозореца отляво" + +#: kwinbindings.cpp:92 +#, kde-format +msgid "Quick Tile Window to the Right" +msgstr "Подреждане на прозореца отдясно" + +#: kwinbindings.cpp:94 +#, fuzzy, kde-format +#| msgid "Quick Tile Window to the Left" +msgid "Quick Tile Window to the Top" +msgstr "Подреждане на прозореца отляво" + +#: kwinbindings.cpp:96 +#, fuzzy, kde-format +#| msgid "Quick Tile Window to the Left" +msgid "Quick Tile Window to the Bottom" +msgstr "Подреждане на прозореца отляво" + +#: kwinbindings.cpp:98 +#, fuzzy, kde-format +#| msgid "Quick Tile Window to the Left" +msgid "Quick Tile Window to the Top Left" +msgstr "Подреждане на прозореца отляво" + +#: kwinbindings.cpp:100 +#, fuzzy, kde-format +#| msgid "Quick Tile Window to the Left" +msgid "Quick Tile Window to the Bottom Left" +msgstr "Подреждане на прозореца отляво" + +#: kwinbindings.cpp:102 +#, fuzzy, kde-format +#| msgid "Quick Tile Window to the Right" +msgid "Quick Tile Window to the Top Right" +msgstr "Подреждане на прозореца отдясно" + +#: kwinbindings.cpp:104 +#, fuzzy, kde-format +#| msgid "Quick Tile Window to the Right" +msgid "Quick Tile Window to the Bottom Right" +msgstr "Подреждане на прозореца отдясно" + +#: kwinbindings.cpp:106 +#, kde-format +msgid "Switch to Window Above" +msgstr "Превключване към прозореца отгоре" + +#: kwinbindings.cpp:108 +#, kde-format +msgid "Switch to Window Below" +msgstr "Превключване към прозореца отдолу" + +#: kwinbindings.cpp:110 +#, kde-format +msgid "Switch to Window to the Right" +msgstr "Превключване към прозореца отдясно" + +#: kwinbindings.cpp:112 +#, kde-format +msgid "Switch to Window to the Left" +msgstr "Превключване към прозореца отляво" + +#: kwinbindings.cpp:114 +#, kde-format +msgid "Increase Opacity of Active Window by 5 %" +msgstr "" + +#: kwinbindings.cpp:116 +#, kde-format +msgid "Decrease Opacity of Active Window by 5 %" +msgstr "" + +#: kwinbindings.cpp:119 +#, kde-format +msgid "Keep Window on All Desktops" +msgstr "Прозорец на всички работни плотове" + +#: kwinbindings.cpp:123 +#, fuzzy, kde-format +#| msgid "Window to Desktop 1" +msgid "Window to Desktop %1" +msgstr "Преместване на прозорец на работен плот 1" + +#: kwinbindings.cpp:125 +#, kde-format +msgid "Window to Next Desktop" +msgstr "Преместване на прозорец на следващия работен плот" + +#: kwinbindings.cpp:126 +#, kde-format +msgid "Window to Previous Desktop" +msgstr "Преместване на прозорец на предишния работен плот" + +#: kwinbindings.cpp:127 +#, kde-format +msgid "Window One Desktop to the Right" +msgstr "Преместване на прозорец на десния работен плот" + +#: kwinbindings.cpp:128 +#, kde-format +msgid "Window One Desktop to the Left" +msgstr "Преместване на прозорец на левия работен плот" + +#: kwinbindings.cpp:129 +#, kde-format +msgid "Window One Desktop Up" +msgstr "Преместване на прозорец на горния работен плот" + +#: kwinbindings.cpp:130 +#, kde-format +msgid "Window One Desktop Down" +msgstr "Преместване на прозорец на долния работен плот" + +#: kwinbindings.cpp:133 +#, fuzzy, kde-format +#| msgid "Window to Screen 1" +msgid "Window to Screen %1" +msgstr "Прозорец на екран 1" + +#: kwinbindings.cpp:135 +#, kde-format +msgid "Window to Next Screen" +msgstr "Преместване на прозорец на следващия екран" + +#: kwinbindings.cpp:136 +#, fuzzy, kde-format +#| msgid "Window to Previous Desktop" +msgid "Window to Previous Screen" +msgstr "Преместване на прозорец на предишния работен плот" + +#: kwinbindings.cpp:137 +#, kde-format +msgid "Show Desktop" +msgstr "Показване на работния плот" + +#: kwinbindings.cpp:140 +#, fuzzy, kde-format +#| msgid "Switch to Screen 1" +msgid "Switch to Screen %1" +msgstr "Превключване на работен плот 1" + +#: kwinbindings.cpp:143 +#, kde-format +msgid "Switch to Next Screen" +msgstr "Превключване към следващия работен плот" + +#: kwinbindings.cpp:144 +#, fuzzy, kde-format +#| msgid "Switch to Previous Desktop" +msgid "Switch to Previous Screen" +msgstr "Превключване към предишния работен плот" + +#: kwinbindings.cpp:146 +#, kde-format +msgid "Kill Window" +msgstr "Убиване на прозорец" + +#: kwinbindings.cpp:147 +#, kde-format +msgid "Suspend Compositing" +msgstr "Спиране на композирането" + +#: kwinbindings.cpp:148 +#, kde-format +msgid "Invert Screen Colors" +msgstr "" + +#: main.cpp:184 main.cpp:214 +#, kde-format +msgid "KDE window manager" +msgstr "Мениджър на прозорци" + +#: main.cpp:189 +#, kde-format +msgid "KWin" +msgstr "KWin" + +#: main.cpp:193 +#, fuzzy, kde-format +#| msgid "(c) 1999-2008, The KDE Developers" +msgid "(c) 1999-2019, The KDE Developers" +msgstr "(c) 1999-2008, екипът на KDE" + +#: main.cpp:195 +#, kde-format +msgid "Matthias Ettrich" +msgstr "Matthias Ettrich" + +#: main.cpp:196 +#, kde-format +msgid "Cristian Tibirna" +msgstr "Cristian Tibirna" + +#: main.cpp:197 +#, kde-format +msgid "Daniel M. Duley" +msgstr "Daniel M. Duley" + +#: main.cpp:198 +#, kde-format +msgid "Luboš Luňák" +msgstr "Luboš Luňák" + +#: main.cpp:199 +#, kde-format +msgid "Martin Flöser" +msgstr "" + +#: main.cpp:200 +#, kde-format +msgid "David Edmundson" +msgstr "" + +#: main.cpp:201 +#, kde-format +msgid "Roman Gilg" +msgstr "" + +#: main.cpp:202 +#, kde-format +msgid "Vlad Zahorodnii" +msgstr "" + +#: main.cpp:211 +#, kde-format +msgid "Disable configuration options" +msgstr "Игнориране на настройките" + +#: main.cpp:212 +#, kde-format +msgid "Indicate that KWin has recently crashed n times" +msgstr "Показва, че KWin е спирала n пъти" + +#: main_wayland.cpp:459 +#, kde-format +msgid "Start a rootless Xwayland server." +msgstr "" + +#: main_wayland.cpp:461 +#, kde-format +msgid "" +"Name of the Wayland socket to listen on. If not set \"wayland-0\" is used." +msgstr "" + +#: main_wayland.cpp:464 +#, kde-format +msgid "Render to framebuffer." +msgstr "" + +#: main_wayland.cpp:466 +#, kde-format +msgid "The framebuffer device to render to." +msgstr "" + +#: main_wayland.cpp:469 +#, kde-format +msgid "The X11 Display to use in windowed mode on platform X11." +msgstr "" + +#: main_wayland.cpp:472 +#, kde-format +msgid "The Wayland Display to use in windowed mode on platform Wayland." +msgstr "" + +#: main_wayland.cpp:474 +#, kde-format +msgid "Render to a virtual framebuffer." +msgstr "" + +#: main_wayland.cpp:476 +#, kde-format +msgid "The width for windowed mode. Default width is 1024." +msgstr "" + +#: main_wayland.cpp:480 +#, kde-format +msgid "The height for windowed mode. Default height is 768." +msgstr "" + +#: main_wayland.cpp:485 +#, kde-format +msgid "The scale for windowed mode. Default value is 1." +msgstr "" + +#: main_wayland.cpp:490 +#, kde-format +msgid "" +"The number of windows to open as outputs in windowed mode. Default value is 1" +msgstr "" + +#: main_wayland.cpp:520 +#, kde-format +msgid "Use libhybris hwcomposer" +msgstr "" + +#: main_wayland.cpp:526 +#, kde-format +msgid "" +"Enable libinput support for input events processing. Note: never use in a " +"nested session.\t(deprecated)" +msgstr "" + +#: main_wayland.cpp:529 +#, kde-format +msgid "Render through drm node." +msgstr "" + +#: main_wayland.cpp:536 +#, kde-format +msgid "Input method that KWin starts." +msgstr "" + +#: main_wayland.cpp:541 +#, kde-format +msgid "List all available backends and quit." +msgstr "" + +#: main_wayland.cpp:545 +#, kde-format +msgid "Starts the session in locked mode." +msgstr "" + +#: main_wayland.cpp:549 +#, kde-format +msgid "Starts the session without lock screen support." +msgstr "" + +#: main_wayland.cpp:553 +#, kde-format +msgid "Starts the session without global shortcuts support." +msgstr "" + +#: main_wayland.cpp:557 +#, kde-format +msgid "Exit after the session application, which is started by KWin, closed." +msgstr "" + +#: main_wayland.cpp:562 +#, kde-format +msgid "Applications to start once Wayland and Xwayland server are started" +msgstr "" + +#: main_x11.cpp:65 +#, kde-format +msgid "" +"KWin is unstable.\n" +"It seems to have crashed several times in a row.\n" +"You can select another window manager to run:" +msgstr "" +"KWin е нестабилен.\n" +"Изглежда е спирала по няколко път на ред.\n" +"Можете да изберете друг мениджър:" + +#: main_x11.cpp:224 +#, kde-format +msgid "" +"kwin: unable to claim manager selection, another wm running? (try using --" +"replace)\n" +msgstr "" +"kwin: Мениджърът на прозорци не може да се регистрира. Най-вероятно има " +"стартиран друг. Пробвайте с параметър --replace.\n" + +#: main_x11.cpp:241 +#, fuzzy, kde-format +#| msgid "" +#| "kwin: unable to claim manager selection, another wm running? (try using --" +#| "replace)\n" +msgid "kwin: another window manager is running (try using --replace)\n" +msgstr "" +"kwin: Мениджърът на прозорци не може да се регистрира. Най-вероятно има " +"стартиран друг. Пробвайте с параметър --replace.\n" + +#: main_x11.cpp:437 +#, kde-format +msgid "Replace already-running ICCCM2.0-compliant window manager" +msgstr "" +"Замяна на вече стартиран мениджър на прозорци съвместим със стандарта " +"ICCCM2.0" + +#: main_x11.cpp:444 +#, kde-format +msgid "Disable KActivities integration." +msgstr "" + +#: plugins/scenes/opengl/scene_opengl.cpp:535 +#, kde-format +msgid "Desktop effects were restarted due to a graphics reset" +msgstr "" + +#. i18n: ectx: label, entry (count), group (General) +#: rulebooksettingsbase.kcfg:9 +#, kde-format +msgid "Total rules count" +msgstr "" + +#. i18n: ectx: label, entry (description), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:10 +#, kde-format +msgid "Rule description" +msgstr "" + +#. i18n: ectx: label, entry (descriptionLegacy), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:13 +#, kde-format +msgid "Rule description (legacy)" +msgstr "" + +#. i18n: ectx: label, entry (DeleteRule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:16 +#, kde-format +msgid "Delete this rule (for use in imports)" +msgstr "" + +#. i18n: ectx: label, entry (wmclass), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:20 +#, kde-format +msgid "Window class (application)" +msgstr "" + +#. i18n: ectx: label, entry (wmclassmatch), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:23 +#, kde-format +msgid "Window class string match type" +msgstr "" + +#. i18n: ectx: label, entry (wmclasscomplete), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:29 +#, kde-format +msgid "Match whole window class" +msgstr "" + +#. i18n: ectx: label, entry (windowrole), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:34 +#, fuzzy, kde-format +#| msgid "Windows" +msgid "Window role" +msgstr "Прозорци" + +#. i18n: ectx: label, entry (windowrolematch), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:37 +#, kde-format +msgid "Window role string match type" +msgstr "" + +#. i18n: ectx: label, entry (title), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:44 +#, fuzzy, kde-format +#| msgid "Windows" +msgid "Window title" +msgstr "Прозорци" + +#. i18n: ectx: label, entry (titlematch), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:47 +#, kde-format +msgid "Window title string match type" +msgstr "" + +#. i18n: ectx: label, entry (clientmachine), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:54 +#, kde-format +msgid "Machine (hostname)" +msgstr "" + +#. i18n: ectx: label, entry (clientmachinematch), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:57 +#, kde-format +msgid "Machine string match type" +msgstr "" + +#. i18n: ectx: label, entry (types), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:64 +#, kde-format +msgid "Window types that match" +msgstr "" + +#. i18n: ectx: label, entry (placement), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:69 +#, kde-format +msgid "Initial placement" +msgstr "" + +#. i18n: ectx: label, entry (placementrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:74 +#, fuzzy, kde-format +#| msgid "&Fullscreen" +msgid "Initial placement rule type" +msgstr "Пълен &екран" + +#. i18n: ectx: label, entry (position), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:79 +#, fuzzy, kde-format +#| msgid "Window Operations Menu" +msgid "Window position" +msgstr "Показване меню на прозорец" + +#. i18n: ectx: label, entry (positionrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:83 +#, fuzzy, kde-format +#| msgid "Window to Screen 0" +msgid "Window position rule type" +msgstr "Прозорец на екран 0" + +#. i18n: ectx: label, entry (size), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:90 +#, fuzzy, kde-format +#| msgid "Windows" +msgid "Window size" +msgstr "Прозорци" + +#. i18n: ectx: label, entry (sizerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:93 +#, kde-format +msgid "Window size rule type" +msgstr "" + +#. i18n: ectx: label, entry (minsize), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:100 +#, kde-format +msgid "Window minimum size" +msgstr "" + +#. i18n: ectx: label, entry (minsizerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:104 +#, kde-format +msgid "Window minimum size rule type" +msgstr "" + +#. i18n: ectx: label, entry (maxsize), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:109 +#, kde-format +msgid "Window maximum size" +msgstr "" + +#. i18n: ectx: label, entry (maxsizerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:113 +#, kde-format +msgid "Window maximum size rule type" +msgstr "" + +#. i18n: ectx: label, entry (opacityactive), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:118 +#, kde-format +msgid "Active opacity" +msgstr "" + +#. i18n: ectx: label, entry (opacityactiverule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:124 +#, kde-format +msgid "Active opacity rule type" +msgstr "" + +#. i18n: ectx: label, entry (opacityinactive), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:129 +#, kde-format +msgid "Inactive opacity" +msgstr "" + +#. i18n: ectx: label, entry (opacityinactiverule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:135 +#, kde-format +msgid "Inactive opacity rule type" +msgstr "" + +#. i18n: ectx: label, entry (ignoregeometry), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:140 +#, kde-format +msgid "Ignore requested geometry" +msgstr "" + +#. i18n: ectx: label, entry (ignoregeometryrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:144 +#, kde-format +msgid "Ignore requested geometry rule type" +msgstr "" + +#. i18n: ectx: label, entry (desktop), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:151 +#, fuzzy, kde-format +#| msgid "Desktop %1" +msgid "Desktop number" +msgstr "Работен плот %1" + +#. i18n: ectx: label, entry (desktoprule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:155 +#, kde-format +msgid "Desktop number rule type" +msgstr "" + +#. i18n: ectx: label, entry (screen), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:162 +#, kde-format +msgid "Screen number" +msgstr "" + +#. i18n: ectx: label, entry (screenrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:166 +#, kde-format +msgid "Screen number rule type" +msgstr "" + +#. i18n: ectx: label, entry (activity), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:173 +#, fuzzy, kde-format +#| msgid "&All Activities" +msgid "Activity" +msgstr "В&сички дейности" + +#. i18n: ectx: label, entry (activityrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:176 +#, kde-format +msgid "Activity rule type" +msgstr "" + +#. i18n: ectx: label, entry (type), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:183 +#, fuzzy, kde-format +#| msgid "Setup Window Shortcut" +msgid "Set window type to" +msgstr "Настройване бързите клавиши на прозореца" + +#. i18n: ectx: label, entry (typerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:189 +#, kde-format +msgid "Set window type rule type" +msgstr "" + +#. i18n: ectx: label, entry (maximizevert), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:194 +#, fuzzy, kde-format +#| msgid "Maximize Window Vertically" +msgid "Maximized vertically" +msgstr "Максимизиране на прозорец вертикално" + +#. i18n: ectx: label, entry (maximizevertrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:198 +#, fuzzy, kde-format +#| msgid "Maximize Window Vertically" +msgid "Maximized vertically rule type" +msgstr "Максимизиране на прозорец вертикално" + +#. i18n: ectx: label, entry (maximizehoriz), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:205 +#, fuzzy, kde-format +#| msgid "Maximize Window Horizontally" +msgid "Maximized horizontally" +msgstr "Максимизиране на прозорец хоризонтално" + +#. i18n: ectx: label, entry (maximizehorizrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:209 +#, fuzzy, kde-format +#| msgid "Maximize Window Horizontally" +msgid "Maximized horizontally rule type" +msgstr "Максимизиране на прозорец хоризонтално" + +#. i18n: ectx: label, entry (minimize), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:216 +#, fuzzy, kde-format +#| msgid "Minimize" +msgid "Minimized" +msgstr "Минимизиране" + +#. i18n: ectx: label, entry (minimizerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:220 +#, kde-format +msgid "Minimized rule type" +msgstr "" + +#. i18n: ectx: label, entry (shade), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:227 +#, fuzzy, kde-format +#| msgid "Shade" +msgid "Shaded" +msgstr "Сгъване" + +#. i18n: ectx: label, entry (shaderule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:231 +#, kde-format +msgid "Shaded rule type" +msgstr "" + +#. i18n: ectx: label, entry (skiptaskbar), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:238 +#, kde-format +msgid "Skip taskbar" +msgstr "" + +#. i18n: ectx: label, entry (skiptaskbarrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:242 +#, kde-format +msgid "Skip taskbar rule type" +msgstr "" + +#. i18n: ectx: label, entry (skippager), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:249 +#, kde-format +msgid "Skip pager" +msgstr "" + +#. i18n: ectx: label, entry (skippagerrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:253 +#, kde-format +msgid "Skip pager rule type" +msgstr "" + +#. i18n: ectx: label, entry (skipswitcher), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:260 +#, fuzzy, kde-format +#| msgid "Switch to Window Tab" +msgid "Skip switcher" +msgstr "Превключване към подпрозорец" + +#. i18n: ectx: label, entry (skipswitcherrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:264 +#, kde-format +msgid "Skip switcher rule type" +msgstr "" + +#. i18n: ectx: label, entry (above), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:271 +#, fuzzy, kde-format +#| msgid "Keep above others" +msgid "Keep above" +msgstr "Запазване на преден план" + +#. i18n: ectx: label, entry (aboverule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:275 +#, fuzzy, kde-format +#| msgid "Keep above others" +msgid "Keep above rule type" +msgstr "Запазване на преден план" + +#. i18n: ectx: label, entry (below), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:282 +#, fuzzy, kde-format +#| msgid "Keep below others" +msgid "Keep below" +msgstr "Запазване на заден план" + +#. i18n: ectx: label, entry (belowrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:286 +#, fuzzy, kde-format +#| msgid "Keep below others" +msgid "Keep below rule type" +msgstr "Запазване на заден план" + +#. i18n: ectx: label, entry (fullscreen), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:293 +#, fuzzy, kde-format +#| msgid "&Fullscreen" +msgid "Fullscreen" +msgstr "Пълен &екран" + +#. i18n: ectx: label, entry (fullscreenrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:297 +#, fuzzy, kde-format +#| msgid "&Fullscreen" +msgid "Fullscreen rule type" +msgstr "Пълен &екран" + +#. i18n: ectx: label, entry (noborder), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:304 +#, kde-format +msgid "No titlebar and frame" +msgstr "" + +#. i18n: ectx: label, entry (noborderrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:308 +#, kde-format +msgid "No titlebar rule type" +msgstr "" + +#. i18n: ectx: label, entry (decocolor), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:315 +#, kde-format +msgid "Titlebar color and scheme" +msgstr "" + +#. i18n: ectx: label, entry (decocolorrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:318 +#, kde-format +msgid "Titlebar color rule type" +msgstr "" + +#. i18n: ectx: label, entry (blockcompositing), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:323 +#, fuzzy, kde-format +#| msgid "Suspend Compositing" +msgid "Block Compositing" +msgstr "Спиране на композирането" + +#. i18n: ectx: label, entry (blockcompositingrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:327 +#, kde-format +msgid "Block Compositing rule type" +msgstr "" + +#. i18n: ectx: label, entry (fsplevel), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:332 +#, kde-format +msgid "Focus stealing prevention" +msgstr "" + +#. i18n: ectx: label, entry (fsplevelrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:338 +#, kde-format +msgid "Focus stealing prevention rule type" +msgstr "" + +#. i18n: ectx: label, entry (fpplevel), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:343 +#, kde-format +msgid "Focus protection" +msgstr "" + +#. i18n: ectx: label, entry (fpplevelrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:349 +#, kde-format +msgid "Focus protection rule type" +msgstr "" + +#. i18n: ectx: label, entry (acceptfocus), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:354 +#, kde-format +msgid "Accept Focus" +msgstr "" + +#. i18n: ectx: label, entry (acceptfocusrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:358 +#, kde-format +msgid "Accept Focus rule type" +msgstr "" + +#. i18n: ectx: label, entry (closeable), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:363 +#, fuzzy, kde-format +#| msgid "Close" +msgid "Closeable" +msgstr "Затваряне" + +#. i18n: ectx: label, entry (closeablerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:367 +#, kde-format +msgid "Closeable rule type" +msgstr "" + +#. i18n: ectx: label, entry (autogroup), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:372 +#, kde-format +msgid "Autogroup with identical" +msgstr "" + +#. i18n: ectx: label, entry (autogrouprule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:376 +#, kde-format +msgid "Autogroup with identical rule type" +msgstr "" + +#. i18n: ectx: label, entry (autogroupfg), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:381 +#, kde-format +msgid "Autogroup in foreground" +msgstr "" + +#. i18n: ectx: label, entry (autogroupfgrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:385 +#, kde-format +msgid "Autogroup in foreground rule type" +msgstr "" + +#. i18n: ectx: label, entry (autogroupid), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:390 +#, kde-format +msgid "Autogroup by ID" +msgstr "" + +#. i18n: ectx: label, entry (autogroupidrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:393 +#, kde-format +msgid "Autogroup by ID rule type" +msgstr "" + +#. i18n: ectx: label, entry (strictgeometry), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:398 +#, kde-format +msgid "Obey geometry restrictions" +msgstr "" + +#. i18n: ectx: label, entry (strictgeometryrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:402 +#, kde-format +msgid "Obey geometry restrictions rule type" +msgstr "" + +#. i18n: ectx: label, entry (shortcut), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:407 +#, kde-format +msgid "Shortcut" +msgstr "" + +#. i18n: ectx: label, entry (shortcutrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:410 +#, kde-format +msgid "Shortcut rule type" +msgstr "" + +#. i18n: ectx: label, entry (disableglobalshortcuts), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:417 +#, fuzzy, kde-format +#| msgid "Block Global Shortcuts" +msgid "Ignore global shortcuts" +msgstr "Блокиране на глобалните бързи клавиши" + +#. i18n: ectx: label, entry (disableglobalshortcutsrule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:421 +#, kde-format +msgid "Ignore global shortcuts rule type" +msgstr "" + +#. i18n: ectx: label, entry (desktopfile), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:426 +#, kde-format +msgid "Desktop file name" +msgstr "" + +#. i18n: ectx: label, entry (desktopfilerule), group ($(ruleDescriptionOrNumber)) +#: rulesettings.kcfg:429 +#, kde-format +msgid "Desktop file name rule type" +msgstr "" + +#: scripting/genericscriptedconfig.cpp:70 +#, kde-format +msgctxt "Error message" +msgid "Plugin does not provide configuration file in expected location" +msgstr "" + +#: scripting/scripting.cpp:117 +#, kde-format +msgctxt "Assertion failed in KWin script with given value" +msgid "Assertion failed: %1 is not null" +msgstr "" + +#: scripting/scripting.cpp:135 +#, kde-format +msgctxt "Assertion failed in KWin script" +msgid "Assertion failed: argument is null" +msgstr "" + +#: scripting/scripting.cpp:177 +#, kde-format +msgctxt "Error in KWin Script" +msgid "" +"Invalid number of arguments. At least service, path, interface and method " +"need to be provided" +msgstr "" + +#: scripting/scripting.cpp:183 +#, kde-format +msgctxt "Error in KWin Script" +msgid "" +"Invalid type. Service, path, interface and method need to be string values" +msgstr "" + +#: scripting/scriptingutils.cpp:17 +#, kde-format +msgctxt "syntax error in KWin script" +msgid "Invalid number of arguments" +msgstr "" + +#: scripting/scriptingutils.cpp:30 +#, kde-format +msgctxt "KWin Scripting function received incorrect value for an expected type" +msgid "%1 is not a variant type" +msgstr "" + +#. i18n: ectx: property (windowTitle), widget (QDialog, ShortcutDialog) +#: shortcutdialog.ui:14 +#, kde-format +msgid "Dialog" +msgstr "" + +#. i18n: ectx: property (text), widget (QToolButton, clearButton) +#: shortcutdialog.ui:25 +#, kde-format +msgid "..." +msgstr "" + +#: tabbox/tabbox.cpp:372 +#, kde-format +msgctxt "Special entry in alt+tab list for minimizing all windows" +msgid "Show Desktop" +msgstr "Показване на работния плот" + +#: tabbox/tabbox.cpp:521 +#, kde-format +msgid "Walk Through Windows" +msgstr "Преминаване през прозорците" + +#: tabbox/tabbox.cpp:522 +#, kde-format +msgid "Walk Through Windows (Reverse)" +msgstr "Преминаване през прозорците (обратно)" + +#: tabbox/tabbox.cpp:523 +#, kde-format +msgid "Walk Through Windows Alternative" +msgstr "Алтернативно преминаване през прозорците" + +#: tabbox/tabbox.cpp:524 +#, kde-format +msgid "Walk Through Windows Alternative (Reverse)" +msgstr "Алтернативно преминаване през прозорците (обратно)" + +#: tabbox/tabbox.cpp:525 +#, fuzzy, kde-format +#| msgid "Walk Through Windows Alternative" +msgid "Walk Through Windows of Current Application" +msgstr "Алтернативно преминаване през прозорците" + +#: tabbox/tabbox.cpp:526 +#, fuzzy, kde-format +#| msgid "Walk Through Windows Alternative (Reverse)" +msgid "Walk Through Windows of Current Application (Reverse)" +msgstr "Алтернативно преминаване през прозорците (обратно)" + +#: tabbox/tabbox.cpp:527 +#, fuzzy, kde-format +#| msgid "Walk Through Windows Alternative" +msgid "Walk Through Windows of Current Application Alternative" +msgstr "Алтернативно преминаване през прозорците" + +#: tabbox/tabbox.cpp:528 +#, fuzzy, kde-format +#| msgid "Walk Through Windows Alternative (Reverse)" +msgid "Walk Through Windows of Current Application Alternative (Reverse)" +msgstr "Алтернативно преминаване през прозорците (обратно)" + +#: tabbox/tabbox.cpp:529 +#, kde-format +msgid "Walk Through Desktops" +msgstr "Преминаване през работните плотове" + +#: tabbox/tabbox.cpp:530 +#, kde-format +msgid "Walk Through Desktops (Reverse)" +msgstr "Преминаване през работните плотове (обратно)" + +#: tabbox/tabbox.cpp:531 +#, kde-format +msgid "Walk Through Desktop List" +msgstr "Преминаване през списъка на работните плотове" + +#: tabbox/tabbox.cpp:532 +#, kde-format +msgid "Walk Through Desktop List (Reverse)" +msgstr "Преминаване през списъка на работните плотове (обратно)" + +#: tabbox/tabboxhandler.cpp:272 +#, kde-format +msgid "" +"The Window Switcher installation is broken, resources are missing.\n" +"Contact your distribution about this." +msgstr "" + +#: useractions.cpp:167 +#, kde-format +msgid "" +"You have selected to show a window without its border.\n" +"Without the border, you will not be able to enable the border again using " +"the mouse: use the window operations menu instead, activated using the %1 " +"keyboard shortcut." +msgstr "" +"Избрахте прозорецът да се показва без рамка.\n" +"\n" +"Рамката на прозореца не може да бъде включена с мишката. Ако искате да " +"включите рамката на прозореца, използвайте системното меню на прозореца, " +"което се вика с клавишната комбинация \"%1\"." + +#: useractions.cpp:175 +#, kde-format +msgid "" +"You have selected to show a window in fullscreen mode.\n" +"If the application itself does not have an option to turn the fullscreen " +"mode off you will not be able to disable it again using the mouse: use the " +"window operations menu instead, activated using the %1 keyboard shortcut." +msgstr "" +"Избрахте прозорецът да се показва на цял екран.\n" +"\n" +"Програмата не може да се върне отново в прозорец с помощта на мишката. Ако " +"искате да изключите режима на цял екран, използвайте системното меню на " +"прозореца, което се вика с клавишната комбинация \"%1\"." + +#: useractions.cpp:240 +#, kde-format +msgid "&Move" +msgstr "&Преместване" + +#: useractions.cpp:245 +#, fuzzy, kde-format +#| msgid "Re&size" +msgid "&Resize" +msgstr "П&ромяна на размера" + +#: useractions.cpp:250 +#, kde-format +msgid "Keep &Above Others" +msgstr "На &преден план" + +#: useractions.cpp:256 +#, kde-format +msgid "Keep &Below Others" +msgstr "На &заден план" + +#: useractions.cpp:262 +#, kde-format +msgid "&Fullscreen" +msgstr "Пълен &екран" + +#: useractions.cpp:268 +#, fuzzy, kde-format +#| msgid "Shade" +msgid "&Shade" +msgstr "Сгъване" + +#: useractions.cpp:274 +#, kde-format +msgid "&No Border" +msgstr "&Без рамка" + +#: useractions.cpp:282 +#, fuzzy, kde-format +#| msgid "Window &Shortcut..." +msgid "Set Window Short&cut..." +msgstr "&Бързи клавиши..." + +#: useractions.cpp:287 +#, fuzzy, kde-format +#| msgid "&Special Window Settings..." +msgid "Configure Special &Window Settings..." +msgstr "П&отребителски..." + +#: useractions.cpp:292 +#, fuzzy, kde-format +#| msgid "&Special Application Settings..." +msgid "Configure S&pecial Application Settings..." +msgstr "П&отребителски..." + +#: useractions.cpp:300 +#, fuzzy, kde-format +#| msgid "KDE window manager" +msgctxt "" +"Entry in context menu of window decoration to open the configuration module " +"of KWin" +msgid "Configure W&indow Manager..." +msgstr "Мениджър на прозорци" + +#: useractions.cpp:328 +#, kde-format +msgid "Ma&ximize" +msgstr "Ма&ксимизиране" + +#: useractions.cpp:334 +#, kde-format +msgid "Mi&nimize" +msgstr "Ми&нимизиране" + +#: useractions.cpp:340 +#, kde-format +msgid "&More Actions" +msgstr "" + +#: useractions.cpp:343 +#, kde-format +msgid "&Close" +msgstr "&Затваряне" + +#: useractions.cpp:410 +#, kde-format +msgid "&Extensions" +msgstr "" + +#: useractions.cpp:451 +#, fuzzy, kde-format +#| msgid "&All Desktops" +msgid "&Desktops" +msgstr "&Всички работни плотове" + +#: useractions.cpp:465 +#, fuzzy, kde-format +#| msgid "To &Desktop" +msgid "Move to &Desktop" +msgstr "Преместване к&ъм работния плот" + +#: useractions.cpp:483 +#, fuzzy, kde-format +#| msgid "To &Desktop" +msgid "Move to &Screen" +msgstr "Преместване к&ъм работния плот" + +#: useractions.cpp:499 +#, fuzzy, kde-format +#| msgid "Ac&tivities" +msgid "Show in &Activities" +msgstr "&Дейности" + +#: useractions.cpp:514 useractions.cpp:559 +#, kde-format +msgid "&All Desktops" +msgstr "&Всички работни плотове" + +#: useractions.cpp:542 useractions.cpp:595 +#, fuzzy, kde-format +#| msgid "Show Desktop" +msgctxt "Create a new desktop and move there the window" +msgid "&New Desktop" +msgstr "Показване на работния плот" + +#: useractions.cpp:618 +#, fuzzy, kde-format +#| msgid "Window to Screen 1" +msgctxt "" +"@item:inmenu List of all Screens to send a window to. First argument is a " +"number, second the output identifier. E.g. Screen 1 (HDMI1)" +msgid "Screen &%1 (%2)" +msgstr "Прозорец на екран 1" + +#: useractions.cpp:641 +#, kde-format +msgid "&All Activities" +msgstr "В&сички дейности" + +#: useractions.cpp:887 +#, kde-format +msgctxt "'%1' is a keyboard shortcut like 'ctrl+w'" +msgid "%1 is already in use" +msgstr "" + +#: useractions.cpp:889 +#, kde-format +msgctxt "keyboard shortcut '%1' is used by action '%2' in application '%3'" +msgid "%1 is used by %2 in %3" +msgstr "" + +#: useractions.cpp:1021 +#, kde-format +msgid "Activate Window (%1)" +msgstr "Активиране на прозорец (%1)" + +#: useractions.cpp:1163 +#, kde-format +msgid "" +"The window manager is configured to consider the screen with the mouse on it " +"as active one.\n" +"Therefore it is not possible to switch to a screen explicitly." +msgstr "" + +#: virtualdesktops.cpp:698 virtualdesktops.cpp:767 +#, kde-format +msgid "Desktop %1" +msgstr "Работен плот %1" + +#: virtualdesktops.cpp:802 +#, kde-format +msgid "Switch to Next Desktop" +msgstr "Превключване към следващия работен плот" + +#: virtualdesktops.cpp:804 +#, kde-format +msgid "Switch to Previous Desktop" +msgstr "Превключване към предишния работен плот" + +#: virtualdesktops.cpp:806 +#, kde-format +msgid "Switch One Desktop to the Right" +msgstr "Превключване един работен плот надясно" + +#: virtualdesktops.cpp:808 +#, kde-format +msgid "Switch One Desktop to the Left" +msgstr "Превключване един работен плот наляво" + +#: virtualdesktops.cpp:810 +#, kde-format +msgid "Switch One Desktop Up" +msgstr "Превключване един работен плот нагоре" + +#: virtualdesktops.cpp:812 +#, kde-format +msgid "Switch One Desktop Down" +msgstr "Превключване един работен плот надолу" + +#: virtualdesktops.cpp:825 +#, fuzzy, kde-format +#| msgid "Switch to Desktop 1" +msgid "Switch to Desktop %1" +msgstr "Превключване на работен плот 1" + +#: virtualkeyboard.cpp:84 +#, kde-format +msgid "Virtual Keyboard" +msgstr "" + +#: virtualkeyboard.cpp:350 +#, kde-format +msgid "Virtual Keyboard: enabled" +msgstr "" + +#: virtualkeyboard.cpp:353 +#, kde-format +msgid "Virtual Keyboard: disabled" +msgstr "" + +#: virtualkeyboard.cpp:355 +#, kde-format +msgid "Whether to show the virtual keyboard on demand." +msgstr "" + +#: workspace.cpp:1363 +#, kde-format +msgctxt "Introductory text shown in the support information." +msgid "" +"KWin Support Information:\n" +"The following information should be used when requesting support on e.g. " +"https://forum.kde.org.\n" +"It provides information about the currently running instance, which options " +"are used,\n" +"what OpenGL driver and which effects are running.\n" +"Please post the information provided underneath this introductory text to a " +"paste bin service\n" +"like https://paste.kde.org instead of pasting into support threads.\n" +msgstr "" \ No newline at end of file diff --git a/po/bg/kwin_clients.po b/po/bg/kwin_clients.po new file mode 100644 index 0000000..0d377d9 --- /dev/null +++ b/po/bg/kwin_clients.po @@ -0,0 +1,141 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Zlatko Popov , 2007, 2008, 2009. +# Yasen Pramatarov , 2010, 2011. +msgid "" +msgstr "" +"Project-Id-Version: kwin_clients\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-08-08 02:22+0200\n" +"PO-Revision-Date: 2011-07-24 16:47+0300\n" +"Last-Translator: Yasen Pramatarov \n" +"Language-Team: Bulgarian \n" +"Language: bg\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 1.2\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#: aurorae/src/aurorae.cpp:683 +#, fuzzy, kde-format +#| msgctxt "@item:inlistbox Border size:" +#| msgid "Tiny" +msgctxt "@item:inlistbox Button size:" +msgid "Tiny" +msgstr "Малък" + +#: aurorae/src/aurorae.cpp:684 +#, fuzzy, kde-format +#| msgctxt "@item:inlistbox Button size:" +#| msgid "Normal" +msgctxt "@item:inlistbox Button size:" +msgid "Normal" +msgstr "Нормален" + +#: aurorae/src/aurorae.cpp:685 +#, kde-format +msgctxt "@item:inlistbox Button size:" +msgid "Large" +msgstr "Голям" + +#: aurorae/src/aurorae.cpp:686 +#, fuzzy, kde-format +#| msgctxt "@item:inlistbox Button size:" +#| msgid "Very Large" +msgctxt "@item:inlistbox Button size:" +msgid "Very Large" +msgstr "Много голям" + +#: aurorae/src/aurorae.cpp:687 +#, fuzzy, kde-format +#| msgctxt "@item:inlistbox Button size:" +#| msgid "Huge" +msgctxt "@item:inlistbox Button size:" +msgid "Huge" +msgstr "Огромен" + +#: aurorae/src/aurorae.cpp:688 +#, fuzzy, kde-format +#| msgctxt "@item:inlistbox Border size:" +#| msgid "Very Huge" +msgctxt "@item:inlistbox Button size:" +msgid "Very Huge" +msgstr "Грамаден" + +#: aurorae/src/aurorae.cpp:689 +#, fuzzy, kde-format +#| msgctxt "@item:inlistbox Border size:" +#| msgid "Oversized" +msgctxt "@item:inlistbox Button size:" +msgid "Oversized" +msgstr "Свръх огромен" + +#: aurorae/src/aurorae.cpp:692 +#, fuzzy, kde-format +#| msgid "Button size:" +msgid "Button size:" +msgstr "Размер на бутоните:" + +#. i18n: ectx: property (windowTitle), widget (QWidget, PlastikConfigDialog) +#: aurorae/themes/plastik/package/contents/ui/config.ui:14 +#, kde-format +msgid "Config Dialog" +msgstr "Прозорец за настройки" + +#. i18n: ectx: property (title), widget (KButtonGroup, titleAlign) +#: aurorae/themes/plastik/package/contents/ui/config.ui:23 +#, kde-format +msgid "Title &Alignment" +msgstr "Подравняване на &заглавието" + +#. i18n: ectx: property (text), widget (QRadioButton, kcfg_titleAlignLeft) +#: aurorae/themes/plastik/package/contents/ui/config.ui:29 +#, kde-format +msgid "Left" +msgstr "Ляво" + +#. i18n: ectx: property (text), widget (QRadioButton, kcfg_titleAlignCenter) +#: aurorae/themes/plastik/package/contents/ui/config.ui:36 +#, kde-format +msgid "Center" +msgstr "Центрирано" + +#. i18n: ectx: property (text), widget (QRadioButton, kcfg_titleAlignRight) +#: aurorae/themes/plastik/package/contents/ui/config.ui:43 +#, kde-format +msgid "Right" +msgstr "Дясно" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_coloredBorder) +#: aurorae/themes/plastik/package/contents/ui/config.ui:53 +#, kde-format +msgid "" +"Check this option if the window border should be painted in the titlebar " +"color. Otherwise it will be painted in the background color." +msgstr "" +"Изчертаване рамките на прозореца с цветовете на заглавието. Ако отметката не " +"е включена, се използват цветовете за фона." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_coloredBorder) +#: aurorae/themes/plastik/package/contents/ui/config.ui:56 +#, kde-format +msgid "Colored window border" +msgstr "Цветна рамка на прозорците" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_animateButtons) +#: aurorae/themes/plastik/package/contents/ui/config.ui:66 +#, kde-format +msgid "" +"Check this option if you want the buttons to fade in when the mouse pointer " +"hovers over them and fade out again when it moves away." +msgstr "" +"Открояване на бутоните, когато показалецът на мишката бъде позициониран над " +"тях." + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_animateButtons) +#: aurorae/themes/plastik/package/contents/ui/config.ui:69 +#, kde-format +msgid "Animate buttons" +msgstr "Анимация за бутоните" \ No newline at end of file diff --git a/po/bg/kwin_effects.po b/po/bg/kwin_effects.po new file mode 100644 index 0000000..a892317 --- /dev/null +++ b/po/bg/kwin_effects.po @@ -0,0 +1,2193 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Yasen Pramatarov , 2009, 2010, 2011. +msgid "" +msgstr "" +"Project-Id-Version: kwin_effects\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-10-23 08:49+0200\n" +"PO-Revision-Date: 2011-07-12 20:30+0300\n" +"Last-Translator: Yasen Pramatarov \n" +"Language-Team: Bulgarian \n" +"Language: bg\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 1.2\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, labelConstantBlurDescription) +#: blur/blur_config.ui:17 +#, fuzzy, kde-format +#| msgid "&Strength:" +msgid "Blur strength:" +msgstr "&Сила:" + +#. i18n: ectx: property (text), widget (QLabel, labelConstantBlurLight) +#. i18n: ectx: property (text), widget (QLabel, labelConstantNoiseLight) +#: blur/blur_config.ui:42 blur/blur_config.ui:108 +#, kde-format +msgid "Light" +msgstr "Слаб" + +#. i18n: ectx: property (text), widget (QLabel, labelConstantBlurStrong) +#. i18n: ectx: property (text), widget (QLabel, labelConstantNoiseStrong) +#: blur/blur_config.ui:74 blur/blur_config.ui:137 +#, kde-format +msgid "Strong" +msgstr "Силен" + +#. i18n: ectx: property (text), widget (QLabel, labelConstantNoiseDescription) +#: blur/blur_config.ui:83 +#, fuzzy, kde-format +#| msgid "&Strength:" +msgid "Noise strength:" +msgstr "&Сила:" + +#: colorpicker/colorpicker.cpp:107 +#, kde-format +msgid "" +"Select a position for color picking with left click or enter.\n" +"Escape or right click to cancel." +msgstr "" + +#: coverswitch/coverswitch.cpp:945 flipswitch/flipswitch.cpp:925 +#, fuzzy, kde-format +#| msgid "Show desktop" +msgctxt "Special entry in alt+tab list for minimizing all windows" +msgid "Show Desktop" +msgstr "Показване на работния плот" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DrawWindowCaptions) +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_WindowTitle) +#: coverswitch/coverswitch_config.ui:17 flipswitch/flipswitch_config.ui:191 +#: presentwindows/presentwindows_config.ui:406 +#, kde-format +msgid "Display window &titles" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: coverswitch/coverswitch_config.ui:29 cube/cube_config.ui:344 +#, kde-format +msgid "Zoom" +msgstr "Мащаб" + +#. i18n: ectx: property (toolTip), widget (QSlider, kcfg_zPosition) +#: coverswitch/coverswitch_config.ui:39 +#, kde-format +msgid "Define how far away the windows should appear" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_8) +#: coverswitch/coverswitch_config.ui:66 cube/cube_config.ui:350 +#, kde-format +msgid "Near" +msgstr "Близко" + +#. i18n: ectx: property (text), widget (QLabel, label_9) +#: coverswitch/coverswitch_config.ui:86 cube/cube_config.ui:357 +#, kde-format +msgid "Far" +msgstr "Далече" + +#. i18n: ectx: attribute (title), widget (QWidget, tab) +#: coverswitch/coverswitch_config.ui:110 +#, kde-format +msgid "Animation" +msgstr "Анимация" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AnimateSwitch) +#: coverswitch/coverswitch_config.ui:116 +#, kde-format +msgid "Animate switch" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AnimateStart) +#: coverswitch/coverswitch_config.ui:123 +#, kde-format +msgid "Animation on tab box open" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AnimateStop) +#: coverswitch/coverswitch_config.ui:130 +#, kde-format +msgid "Animation on tab box close" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: coverswitch/coverswitch_config.ui:139 magiclamp/magiclamp_config.ui:17 +#, kde-format +msgid "Animation duration:" +msgstr "Продължителност на анимацията:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_RotationDuration) +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_Duration) +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_AnimationDuration) +#: coverswitch/coverswitch_config.ui:158 cube/cube_config.ui:149 +#: cubeslide/cubeslide_config.ui:49 magiclamp/magiclamp_config.ui:36 +#, kde-format +msgctxt "Duration of rotation" +msgid "Default" +msgstr "По подразбиране" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_Duration) +#: coverswitch/coverswitch_config.ui:161 glide/glide_config.ui:35 +#: scale/package/contents/ui/config.ui:33 slide/slide_config.ui:35 +#, fuzzy, kde-format +#| msgid " millisecond" +#| msgid_plural " milliseconds" +msgid " milliseconds" +msgstr " милисекунда" + +#. i18n: ectx: attribute (title), widget (QWidget, tab_3) +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Reflection) +#: coverswitch/coverswitch_config.ui:177 coverswitch/coverswitch_config.ui:183 +#, kde-format +msgid "Reflections" +msgstr "Отразяване" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: coverswitch/coverswitch_config.ui:195 +#, kde-format +msgid "Rear color" +msgstr "Цвят на предния план" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: coverswitch/coverswitch_config.ui:205 +#, kde-format +msgid "Front color" +msgstr "Цвят на задния план" + +#: cube/cube.cpp:186 cube/cube_config.cpp:60 +#, kde-format +msgid "Desktop Cube" +msgstr "Кубичен работен плот" + +#: cube/cube.cpp:194 cube/cube_config.cpp:65 +#, kde-format +msgid "Desktop Cylinder" +msgstr "Цилиндричен работен плот" + +#: cube/cube.cpp:200 cube/cube_config.cpp:69 +#, kde-format +msgid "Desktop Sphere" +msgstr "Сферичен работен плот" + +#: cube/cube_config.cpp:49 +#, kde-format +msgctxt "@title:tab Basic Settings" +msgid "Basic" +msgstr "Основни" + +#: cube/cube_config.cpp:50 +#, kde-format +msgctxt "@title:tab Advanced Settings" +msgid "Advanced" +msgstr "Допълнителни" + +#: cube/cube_config.cpp:54 desktopgrid/desktopgrid_config.cpp:52 +#: flipswitch/flipswitch_config.cpp:56 invert/invert_config.cpp:38 +#: lookingglass/lookingglass_config.cpp:58 magnifier/magnifier_config.cpp:58 +#: mouseclick/mouseclick_config.cpp:50 mousemark/mousemark_config.cpp:56 +#: presentwindows/presentwindows_config.cpp:51 +#: showpaint/showpaint_config.cpp:36 +#: thumbnailaside/thumbnailaside_config.cpp:57 +#: trackmouse/trackmouse_config.cpp:54 +#: windowgeometry/windowgeometry_config.cpp:45 zoom/zoom_config.cpp:59 +#, kde-format +msgid "KWin" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, tab) +#: cube/cube_config.ui:21 +#, kde-format +msgid "Tab 1" +msgstr "Страница 1" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_5) +#: cube/cube_config.ui:27 +#, kde-format +msgid "Background" +msgstr "Фон" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: cube/cube_config.ui:33 +#, kde-format +msgid "Background color:" +msgstr "Цвят на фона:" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: cube/cube_config.ui:56 +#, kde-format +msgid "Wallpaper:" +msgstr "Тапет:" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_8) +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#: cube/cube_config.ui:82 desktopgrid/desktopgrid_config.ui:207 +#: flipswitch/flipswitch_config.ui:204 +#: presentwindows/presentwindows_config.ui:17 +#, kde-format +msgid "Activation" +msgstr "Включване" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_7) +#. i18n: ectx: property (title), widget (QGroupBox, groupBox) +#: cube/cube_config.ui:104 desktopgrid/desktopgrid_config.ui:17 +#: flipswitch/flipswitch_config.ui:17 mousemark/mousemark_config.ui:17 +#: presentwindows/presentwindows_config.ui:387 +#: thumbnailaside/thumbnailaside_config.ui:17 +#, kde-format +msgid "Appearance" +msgstr "Външен вид" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DisplayDesktopName) +#: cube/cube_config.ui:110 +#, kde-format +msgid "Display desktop name" +msgstr "Показване име на плота" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Reflection) +#: cube/cube_config.ui:117 +#, kde-format +msgid "Reflection" +msgstr "Отразяване" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: cube/cube_config.ui:124 cubeslide/cubeslide_config.ui:72 +#, kde-format +msgid "Rotation duration:" +msgstr "Време за завъртане:" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ZOrdering) +#: cube/cube_config.ui:175 +#, kde-format +msgid "Windows hover above cube" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_4) +#: cube/cube_config.ui:185 +#, kde-format +msgid "Opacity" +msgstr "Непрозрачност" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_OpacitySpin) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_Opacity) +#: cube/cube_config.ui:225 thumbnailaside/thumbnailaside_config.ui:87 +#, no-c-format, kde-format +msgid " %" +msgstr " %" + +#. i18n: ectx: property (text), widget (QLabel, label_4) +#. i18n: ectx: property (text), widget (QLabel, label_2) +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: cube/cube_config.ui:238 translucency/package/contents/ui/config.ui:156 +#: translucency/package/contents/ui/config.ui:418 +#, kde-format +msgid "Transparent" +msgstr "Прозрачност" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#. i18n: ectx: property (text), widget (QLabel, label_3) +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: cube/cube_config.ui:245 translucency/package/contents/ui/config.ui:121 +#: translucency/package/contents/ui/config.ui:431 +#, kde-format +msgid "Opaque" +msgstr "Непрозрачност" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_OpacityDesktopOnly) +#: cube/cube_config.ui:255 +#, kde-format +msgid "Do not change opacity of windows" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, tab_2) +#: cube/cube_config.ui:279 +#, kde-format +msgid "Tab 2" +msgstr "Страница 2" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_3) +#: cube/cube_config.ui:285 +#, kde-format +msgid "Caps" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Caps) +#: cube/cube_config.ui:291 +#, kde-format +msgid "Show caps" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, capColorLabel) +#: cube/cube_config.ui:298 +#, kde-format +msgid "Cap color:" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_TexturedCaps) +#: cube/cube_config.ui:321 +#, kde-format +msgid "Display image on caps" +msgstr "" + +#. i18n: ectx: property (toolTip), widget (QSlider, kcfg_ZPosition) +#: cube/cube_config.ui:367 +#, kde-format +msgid "Define how far away the object should appear" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_9) +#: cube/cube_config.ui:408 +#, kde-format +msgid "Additional Options" +msgstr "Допълнителни настройки" + +#. i18n: ectx: property (toolTip), widget (QCheckBox, kcfg_CloseOnMouseRelease) +#: cube/cube_config.ui:415 +#, kde-format +msgid "" +"If enabled the effect will be deactivated after rotating the cube with the " +"mouse,\n" +"otherwise it will remain active" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_CloseOnMouseRelease) +#: cube/cube_config.ui:418 +#, kde-format +msgid "Close after mouse dragging" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_TabBox) +#: cube/cube_config.ui:425 +#, kde-format +msgid "Use this effect for walking through the desktops" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_InvertKeys) +#: cube/cube_config.ui:432 +#, kde-format +msgid "Invert cursor keys" +msgstr "Обръщане на клавишите-стрелки" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_InvertMouse) +#: cube/cube_config.ui:439 +#, kde-format +msgid "Invert mouse" +msgstr "Обръщане на мишката" + +#. i18n: ectx: property (title), widget (QGroupBox, capDeformationGroupBox) +#: cube/cube_config.ui:449 +#, kde-format +msgid "Sphere Cap Deformation" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, capDeformationSphereLabel) +#: cube/cube_config.ui:471 +#, kde-format +msgid "Sphere" +msgstr "Сфера" + +#. i18n: ectx: property (text), widget (QLabel, capDeformationPlaneLabel) +#: cube/cube_config.ui:478 +#, kde-format +msgid "Plane" +msgstr "Равнина" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DontSlideStickyWindows) +#: cubeslide/cubeslide_config.ui:17 +#, kde-format +msgid "Do not animate windows on all desktops" +msgstr "" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_RingLife) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_RotationDuration) +#: cubeslide/cubeslide_config.ui:52 mouseclick/mouseclick_config.ui:132 +#, kde-format +msgid " msec" +msgstr " мсек" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DontSlidePanels) +#: cubeslide/cubeslide_config.ui:65 +#, kde-format +msgid "Do not animate panels" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_UsePagerLayout) +#: cubeslide/cubeslide_config.ui:85 +#, kde-format +msgid "Use pager layout for animation" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_UseWindowMoving) +#: cubeslide/cubeslide_config.ui:92 +#, kde-format +msgid "Start animation when moving windows towards screen edges" +msgstr "" + +#: desktopgrid/desktopgrid.cpp:65 desktopgrid/desktopgrid_config.cpp:57 +#, kde-format +msgid "Show Desktop Grid" +msgstr "Показване мрежа на работния плот" + +#: desktopgrid/desktopgrid_config.cpp:65 +#, kde-format +msgctxt "Desktop name alignment:" +msgid "Disabled" +msgstr "Изключено" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_InRotationEdge) +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_OutRotationEdge) +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: desktopgrid/desktopgrid_config.cpp:66 flipswitch/flipswitch_config.ui:160 +#: glide/glide_config.ui:70 glide/glide_config.ui:168 +#, kde-format +msgid "Top" +msgstr "Горе" + +#: desktopgrid/desktopgrid_config.cpp:67 +#, kde-format +msgid "Top-Right" +msgstr "Горе вдясно" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_InRotationEdge) +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_OutRotationEdge) +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: desktopgrid/desktopgrid_config.cpp:68 flipswitch/flipswitch_config.ui:125 +#: glide/glide_config.ui:75 glide/glide_config.ui:173 +#, kde-format +msgid "Right" +msgstr "Дясно" + +#: desktopgrid/desktopgrid_config.cpp:69 +#, kde-format +msgid "Bottom-Right" +msgstr "Долу вдясно" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_InRotationEdge) +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_OutRotationEdge) +#. i18n: ectx: property (text), widget (QLabel, label_8) +#: desktopgrid/desktopgrid_config.cpp:70 flipswitch/flipswitch_config.ui:180 +#: glide/glide_config.ui:80 glide/glide_config.ui:178 +#, kde-format +msgid "Bottom" +msgstr "Долу" + +#: desktopgrid/desktopgrid_config.cpp:71 +#, kde-format +msgid "Bottom-Left" +msgstr "Долу вляво" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_InRotationEdge) +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_OutRotationEdge) +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: desktopgrid/desktopgrid_config.cpp:72 flipswitch/flipswitch_config.ui:105 +#: glide/glide_config.ui:85 glide/glide_config.ui:183 +#, kde-format +msgid "Left" +msgstr "Ляво" + +#: desktopgrid/desktopgrid_config.cpp:73 +#, kde-format +msgid "Top-Left" +msgstr "Горе вляво" + +#: desktopgrid/desktopgrid_config.cpp:74 +#, kde-format +msgid "Center" +msgstr "Център" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: desktopgrid/desktopgrid_config.ui:23 +#, kde-format +msgid "Zoom &duration:" +msgstr "" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_ZoomDuration) +#: desktopgrid/desktopgrid_config.ui:42 +#, kde-format +msgctxt "Duration of zoom" +msgid "Default" +msgstr "По подразбиране" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: desktopgrid/desktopgrid_config.ui:55 +#, fuzzy, kde-format +#| msgid "&Border width:" +msgid "Border wid&th:" +msgstr "Широчина на &рамката:" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: desktopgrid/desktopgrid_config.ui:84 +#, kde-format +msgid "Desktop &name alignment:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: desktopgrid/desktopgrid_config.ui:107 +#, kde-format +msgid "&Layout mode:" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LayoutMode) +#: desktopgrid/desktopgrid_config.ui:127 +#, kde-format +msgid "Pager" +msgstr "Пейджър" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LayoutMode) +#: desktopgrid/desktopgrid_config.ui:132 +#, kde-format +msgid "Automatic" +msgstr "Автоматично" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LayoutMode) +#: desktopgrid/desktopgrid_config.ui:137 +#, kde-format +msgid "Custom" +msgstr "Потребителски" + +#. i18n: ectx: property (text), widget (QLabel, layoutRowsLabel) +#: desktopgrid/desktopgrid_config.ui:145 +#, fuzzy, kde-format +#| msgid "Number of &rows:" +msgid "N&umber of rows:" +msgstr "Брой &редове:" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_PresentWindows) +#: desktopgrid/desktopgrid_config.ui:190 +#, kde-format +msgid "Use Present Windows effect to layout the windows" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ShowAddRemove) +#: desktopgrid/desktopgrid_config.ui:197 +#, kde-format +msgid "Show buttons to alter count of virtual desktops" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_Strength) +#: diminactive/diminactive_config.ui:17 +#, fuzzy, kde-format +#| msgid "&Strength:" +msgid "Strength:" +msgstr "&Сила:" + +#. i18n: ectx: property (text), widget (QLabel, label_Dim) +#: diminactive/diminactive_config.ui:40 +#, kde-format +msgid "Dim:" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DimPanels) +#: diminactive/diminactive_config.ui:47 +#, kde-format +msgid "Docks and panels" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DimDesktop) +#: diminactive/diminactive_config.ui:54 +#, fuzzy, kde-format +#| msgctxt "@title:group actions when clicking on desktop" +#| msgid "Desktop" +msgid "Desktop" +msgstr "Работен плот" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DimKeepAbove) +#: diminactive/diminactive_config.ui:61 +#, fuzzy, kde-format +#| msgid "Inactive windows:" +msgid "Keep above windows" +msgstr "Неактивни прозорци:" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DimByGroup) +#: diminactive/diminactive_config.ui:68 +#, kde-format +msgid "By window group" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DimFullScreen) +#: diminactive/diminactive_config.ui:75 +#, fuzzy, kde-format +#| msgid "Windows" +msgid "Fullscreen windows" +msgstr "Прозорци" + +#: effect_builtins.cpp:91 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Blur" +msgstr "" + +#: effect_builtins.cpp:92 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Blurs the background behind semi-transparent windows" +msgstr "" + +#: effect_builtins.cpp:106 +#, fuzzy, kde-format +#| msgctxt "High saturation" +#| msgid "Colored" +msgctxt "Name of a KWin Effect" +msgid "Color Picker" +msgstr "Цветен" + +#: effect_builtins.cpp:107 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Supports picking a color" +msgstr "" + +#: effect_builtins.cpp:121 +#, fuzzy, kde-format +#| msgid "Background color:" +msgctxt "Name of a KWin Effect" +msgid "Background contrast" +msgstr "Цвят на фона:" + +#: effect_builtins.cpp:122 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Improve contrast and readability behind semi-transparent windows" +msgstr "" + +#: effect_builtins.cpp:136 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Cover Switch" +msgstr "" + +#: effect_builtins.cpp:137 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Display a Cover Flow effect for the alt+tab window switcher" +msgstr "" + +#: effect_builtins.cpp:151 +#, fuzzy, kde-format +#| msgid "Desktop Cube" +msgctxt "Name of a KWin Effect" +msgid "Desktop Cube" +msgstr "Кубичен работен плот" + +#: effect_builtins.cpp:152 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Display each virtual desktop on a side of a cube" +msgstr "" + +#: effect_builtins.cpp:166 +#, fuzzy, kde-format +#| msgid "Desktop Cube" +msgctxt "Name of a KWin Effect" +msgid "Desktop Cube Animation" +msgstr "Кубичен работен плот" + +#: effect_builtins.cpp:167 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Animate desktop switching with a cube" +msgstr "" + +#: effect_builtins.cpp:181 +#, fuzzy, kde-format +#| msgid "Show Desktop Grid" +msgctxt "Name of a KWin Effect" +msgid "Desktop Grid" +msgstr "Показване мрежа на работния плот" + +#: effect_builtins.cpp:182 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Zoom out so all desktops are displayed side-by-side in a grid" +msgstr "" + +#: effect_builtins.cpp:196 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Dim Inactive" +msgstr "" + +#: effect_builtins.cpp:197 +#, fuzzy, kde-format +#| msgid "Inactive windows:" +msgctxt "Comment describing the KWin Effect" +msgid "Darken inactive windows" +msgstr "Неактивни прозорци:" + +#: effect_builtins.cpp:211 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Fall Apart" +msgstr "" + +#: effect_builtins.cpp:212 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Closed windows fall into pieces" +msgstr "" + +#: effect_builtins.cpp:226 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Flip Switch" +msgstr "" + +#: effect_builtins.cpp:227 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "" +"Flip through windows that are in a stack for the alt+tab window switcher" +msgstr "" + +#: effect_builtins.cpp:241 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Glide" +msgstr "" + +#: effect_builtins.cpp:242 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Glide windows as they appear or disappear" +msgstr "" + +#: effect_builtins.cpp:256 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Highlight Window" +msgstr "" + +#: effect_builtins.cpp:257 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Highlight the appropriate window when hovering over taskbar entries" +msgstr "" + +#: effect_builtins.cpp:271 +#, fuzzy, kde-format +#| msgid "Invert mouse" +msgctxt "Name of a KWin Effect" +msgid "Invert" +msgstr "Обръщане на мишката" + +#: effect_builtins.cpp:272 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Inverts the color of the desktop and windows" +msgstr "" + +#: effect_builtins.cpp:286 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Kscreen" +msgstr "" + +#: effect_builtins.cpp:287 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Helper Effect for KScreen" +msgstr "" + +#: effect_builtins.cpp:301 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Looking Glass" +msgstr "" + +#: effect_builtins.cpp:302 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "A screen magnifier that looks like a fisheye lens" +msgstr "" + +#: effect_builtins.cpp:316 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Magic Lamp" +msgstr "" + +#: effect_builtins.cpp:317 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Simulate a magic lamp when minimizing windows" +msgstr "" + +#: effect_builtins.cpp:331 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Magnifier" +msgstr "" + +#: effect_builtins.cpp:332 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Magnify the section of the screen that is near the mouse cursor" +msgstr "" + +#: effect_builtins.cpp:346 +#, fuzzy, kde-format +#| msgid "Animation" +msgctxt "Name of a KWin Effect" +msgid "Mouse Click Animation" +msgstr "Анимация" + +#: effect_builtins.cpp:347 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "" +"Creates an animation whenever a mouse button is clicked. This is useful for " +"screenrecordings/presentations" +msgstr "" + +#: effect_builtins.cpp:361 +#, fuzzy, kde-format +#| msgid "Mouse Tracking:" +msgctxt "Name of a KWin Effect" +msgid "Mouse Mark" +msgstr "Следене на мишката:" + +#: effect_builtins.cpp:362 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Allows you to draw lines on the desktop" +msgstr "" + +#: effect_builtins.cpp:376 +#, fuzzy, kde-format +#| msgid "Windows" +msgctxt "Name of a KWin Effect" +msgid "Present Windows" +msgstr "Прозорци" + +#: effect_builtins.cpp:377 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Zoom out until all opened windows can be displayed side-by-side" +msgstr "" + +#: effect_builtins.cpp:391 +#, fuzzy, kde-format +#| msgid "Close window" +msgctxt "Name of a KWin Effect" +msgid "Resize Window" +msgstr "Затваряне на прозореца" + +#: effect_builtins.cpp:392 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Resizes windows with a fast texture scale instead of updating contents" +msgstr "" + +#: effect_builtins.cpp:406 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Screen Edge" +msgstr "" + +#: effect_builtins.cpp:407 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Highlights a screen edge when approaching" +msgstr "" + +#: effect_builtins.cpp:421 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Screenshot" +msgstr "" + +#: effect_builtins.cpp:422 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Helper effect for screenshot tools" +msgstr "" + +#: effect_builtins.cpp:436 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Sheet" +msgstr "" + +#: effect_builtins.cpp:437 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "" +"Make modal dialogs smoothly fly in and out when they are shown or hidden" +msgstr "" + +#: effect_builtins.cpp:451 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Show FPS" +msgstr "" + +#: effect_builtins.cpp:452 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Display KWin's performance in the corner of the screen" +msgstr "" + +#: effect_builtins.cpp:466 +#, fuzzy, kde-format +#| msgid "Show &panels" +msgctxt "Name of a KWin Effect" +msgid "Show Paint" +msgstr "Показване на па&нели" + +#: effect_builtins.cpp:467 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Highlight areas of the desktop that have been recently updated" +msgstr "" + +#: effect_builtins.cpp:481 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Slide" +msgstr "" + +#: effect_builtins.cpp:482 +#, fuzzy, kde-format +#| msgid "Slide when switching tabs" +msgctxt "Comment describing the KWin Effect" +msgid "Slide desktops when switching virtual desktops" +msgstr "Приплъзване при превключване на подпрозорци" + +#: effect_builtins.cpp:496 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Slide Back" +msgstr "" + +#: effect_builtins.cpp:497 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Slide back windows when another window is raised" +msgstr "" + +#: effect_builtins.cpp:511 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Sliding popups" +msgstr "" + +#: effect_builtins.cpp:512 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Sliding animation for Plasma popups" +msgstr "" + +#: effect_builtins.cpp:526 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Snap Helper" +msgstr "" + +#: effect_builtins.cpp:527 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Help you locate the center of the screen when moving a window" +msgstr "" + +#: effect_builtins.cpp:541 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Startup Feedback" +msgstr "" + +#: effect_builtins.cpp:542 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Helper effect for startup feedback" +msgstr "" + +#: effect_builtins.cpp:556 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Thumbnail Aside" +msgstr "" + +#: effect_builtins.cpp:557 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Display window thumbnails on the edge of the screen" +msgstr "" + +#: effect_builtins.cpp:571 +#, fuzzy, kde-format +#| msgid "Mouse Pointer:" +msgctxt "Name of a KWin Effect" +msgid "Touch Points" +msgstr "Курсор на мишката:" + +#: effect_builtins.cpp:572 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Visualize touch points" +msgstr "" + +#: effect_builtins.cpp:586 +#, fuzzy, kde-format +#| msgid "Track mouse" +msgctxt "Name of a KWin Effect" +msgid "Track Mouse" +msgstr "Проследяване на мишката" + +#: effect_builtins.cpp:587 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Display a mouse cursor locating effect when activated" +msgstr "" + +#: effect_builtins.cpp:601 +#, kde-format +msgctxt "Name of a KWin Effect" +msgid "Window Geometry" +msgstr "" + +#: effect_builtins.cpp:602 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Display window geometries on move/resize" +msgstr "" + +#: effect_builtins.cpp:616 +#, fuzzy, kde-format +#| msgid "Windows" +msgctxt "Name of a KWin Effect" +msgid "Wobbly Windows" +msgstr "Прозорци" + +#: effect_builtins.cpp:617 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Deform windows while they are moving" +msgstr "" + +#: effect_builtins.cpp:631 +#, fuzzy, kde-format +#| msgid "Zoom" +msgctxt "Name of a KWin Effect" +msgid "Zoom" +msgstr "Мащаб" + +#: effect_builtins.cpp:632 +#, kde-format +msgctxt "Comment describing the KWin Effect" +msgid "Magnify the entire desktop" +msgstr "" + +#: flipswitch/flipswitch.cpp:48 flipswitch/flipswitch_config.cpp:50 +#, kde-format +msgid "Toggle Flip Switch (Current desktop)" +msgstr "" + +#: flipswitch/flipswitch.cpp:55 flipswitch/flipswitch_config.cpp:53 +#, kde-format +msgid "Toggle Flip Switch (All desktops)" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: flipswitch/flipswitch_config.ui:23 +#, kde-format +msgid "Flip animation duration:" +msgstr "" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_Duration) +#: flipswitch/flipswitch_config.ui:42 +#, kde-format +msgctxt "Duration of flip animation" +msgid "Default" +msgstr "По подразбиране" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: flipswitch/flipswitch_config.ui:55 +#, kde-format +msgid "Angle:" +msgstr "Ъгъл:" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_Angle) +#: flipswitch/flipswitch_config.ui:71 +#, kde-format +msgid " °" +msgstr " °" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: flipswitch/flipswitch_config.ui:81 +#, kde-format +msgid "Horizontal position of front:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: flipswitch/flipswitch_config.ui:136 +#, kde-format +msgid "Vertical position of front:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_Duration) +#: glide/glide_config.ui:19 scale/package/contents/ui/config.ui:17 +#: slide/slide_config.ui:19 +#, fuzzy, kde-format +#| msgid "Duration" +msgid "Duration:" +msgstr "Продължителност" + +#. i18n: Duration of the slide animation. +#. i18n: ectx: property (specialValueText), widget (QSpinBox, kcfg_Duration) +#: glide/glide_config.ui:32 scale/package/contents/ui/config.ui:30 +#: slide/slide_config.ui:32 +#, fuzzy, kde-format +#| msgctxt "Duration of rotation" +#| msgid "Default" +msgid "Default" +msgstr "По подразбиране" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_InAnimation) +#: glide/glide_config.ui:50 +#, fuzzy, kde-format +#| msgid "Animation" +msgid "Window Open Animation" +msgstr "Анимация" + +#. i18n: ectx: property (text), widget (QLabel, label_InRotationEdge) +#. i18n: ectx: property (text), widget (QLabel, label_OutRotationEdge) +#: glide/glide_config.ui:56 glide/glide_config.ui:154 +#, fuzzy, kde-format +#| msgid "Rotation duration:" +msgid "Rotation edge:" +msgstr "Време за завъртане:" + +#. i18n: ectx: property (text), widget (QLabel, label_InRotationAngle) +#. i18n: ectx: property (text), widget (QLabel, label_OutRotationAngle) +#: glide/glide_config.ui:93 glide/glide_config.ui:191 +#, fuzzy, kde-format +#| msgid "Rotation duration:" +msgid "Rotation angle:" +msgstr "Време за завъртане:" + +#. i18n: ectx: property (text), widget (QLabel, label_InDistance) +#. i18n: ectx: property (text), widget (QLabel, label_OutDistance) +#: glide/glide_config.ui:119 glide/glide_config.ui:198 +#, kde-format +msgid "Distance:" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_OutAnimation) +#: glide/glide_config.ui:148 +#, fuzzy, kde-format +#| msgid "Animation" +msgid "Window Close Animation" +msgstr "Анимация" + +#: invert/invert.cpp:34 invert/invert_config.cpp:41 +#, kde-format +msgid "Toggle Invert Effect" +msgstr "" + +#: invert/invert.cpp:42 invert/invert_config.cpp:47 +#, kde-format +msgid "Toggle Invert Effect on Window" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_FadeToBlack) +#: login/package/contents/ui/config.ui:17 +#, kde-format +msgid "Fade to black (fullscreen splash screens only)" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: lookingglass/lookingglass_config.ui:24 +#, kde-format +msgid "&Radius:" +msgstr "&Радиус:" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_AnimationDuration) +#: magiclamp/magiclamp_config.ui:39 +#, fuzzy, kde-format +#| msgid " millisecond" +#| msgid_plural " milliseconds" +msgid "milliseconds" +msgstr " милисекунда" + +#. i18n: ectx: property (title), widget (QGroupBox, groupSize) +#: magnifier/magnifier_config.ui:17 +#, kde-format +msgid "Size" +msgstr "Размер" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: magnifier/magnifier_config.ui:23 +#, kde-format +msgid "&Width:" +msgstr "&Широчина:" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_Width) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_Height) +#: magnifier/magnifier_config.ui:42 magnifier/magnifier_config.ui:74 +#, kde-format +msgid " px" +msgstr " px" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: magnifier/magnifier_config.ui:55 +#, kde-format +msgid "&Height:" +msgstr "&Височина:" + +#: mouseclick/mouseclick.cpp:40 mouseclick/mouseclick_config.cpp:53 +#, fuzzy, kde-format +#| msgid "Glide Effect:" +msgid "Toggle Mouse Click Effect" +msgstr "Ефект на приплъзване:" + +#: mouseclick/mouseclick.cpp:48 +#, fuzzy, kde-format +#| msgid "Left" +msgctxt "Left mouse button" +msgid "Left" +msgstr "Ляво" + +#: mouseclick/mouseclick.cpp:49 +#, fuzzy, kde-format +#| msgid "Middle button:" +msgctxt "Middle mouse button" +msgid "Middle" +msgstr "Среден бутон:" + +#: mouseclick/mouseclick.cpp:50 +#, fuzzy, kde-format +#| msgid "Right" +msgctxt "Right mouse button" +msgid "Right" +msgstr "Дясно" + +#: mouseclick/mouseclick.h:63 +#, kde-format +msgid "↓" +msgstr "" + +#: mouseclick/mouseclick.h:64 +#, kde-format +msgid "↑" +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, basic_tab) +#: mouseclick/mouseclick_config.ui:21 +#, kde-format +msgid "Basic Settings" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, button1_label) +#: mouseclick/mouseclick_config.ui:37 +#, fuzzy, kde-format +#| msgid "Left button:" +msgid "Left Mouse Button Color:" +msgstr "Ляв бутон:" + +#. i18n: ectx: property (text), widget (QLabel, button2_label) +#: mouseclick/mouseclick_config.ui:50 +#, fuzzy, kde-format +#| msgid "Middle button:" +msgid "Middle Mouse Button Color:" +msgstr "Среден бутон:" + +#. i18n: ectx: property (text), widget (QLabel, button3_label) +#: mouseclick/mouseclick_config.ui:70 +#, fuzzy, kde-format +#| msgid "Right button:" +msgid "Right Mouse Button Color:" +msgstr "Десен бутон:" + +#. i18n: ectx: attribute (title), widget (QWidget, advanced_tab) +#: mouseclick/mouseclick_config.ui:91 +#, fuzzy, kde-format +#| msgid "Advanced" +msgid "Advanced Settings" +msgstr "Допълнителни" + +#. i18n: ectx: property (title), widget (QGroupBox, rings) +#: mouseclick/mouseclick_config.ui:97 +#, kde-format +msgid "Rings" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, ring_line_width_label) +#: mouseclick/mouseclick_config.ui:103 +#, fuzzy, kde-format +#| msgid "&Width:" +msgid "Line Width:" +msgstr "&Широчина:" + +#. i18n: ectx: property (suffix), widget (QDoubleSpinBox, kcfg_LineWidth) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_RingSize) +#: mouseclick/mouseclick_config.ui:119 mouseclick/mouseclick_config.ui:171 +#: mousemark/mousemark_config.cpp:45 +#, kde-format +msgid " pixel" +msgid_plural " pixels" +msgstr[0] " точка" +msgstr[1] " точки" + +#. i18n: ectx: property (text), widget (QLabel, ring_duration_label) +#: mouseclick/mouseclick_config.ui:145 +#, fuzzy, kde-format +#| msgid "Fading duration:" +msgid "Ring Duration:" +msgstr "Продължителност на избледняването:" + +#. i18n: ectx: property (text), widget (QLabel, ring_radius_label) +#: mouseclick/mouseclick_config.ui:155 +#, fuzzy, kde-format +#| msgid "&Radius:" +msgid "Ring Radius:" +msgstr "&Радиус:" + +#. i18n: ectx: property (text), widget (QLabel, ring_count_label) +#: mouseclick/mouseclick_config.ui:184 +#, kde-format +msgid "Ring Count:" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox) +#. i18n: ectx: property (title), widget (QGroupBox, font) +#: mouseclick/mouseclick_config.ui:210 showfps/showfps_config.ui:17 +#, kde-format +msgid "Text" +msgstr "Текст" + +#. i18n: ectx: property (text), widget (QLabel, font_label) +#: mouseclick/mouseclick_config.ui:216 +#, kde-format +msgid "Font:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, showtext_label) +#: mouseclick/mouseclick_config.ui:233 +#, fuzzy, kde-format +#| msgid "Show desktop" +msgid "Show Text:" +msgstr "Показване на работния плот" + +#: mousemark/mousemark.cpp:41 +#, kde-format +msgid "Clear All Mouse Marks" +msgstr "" + +#: mousemark/mousemark.cpp:48 mousemark/mousemark_config.cpp:65 +#, kde-format +msgid "Clear Last Mouse Mark" +msgstr "" + +#: mousemark/mousemark_config.cpp:59 +#, kde-format +msgid "Clear Mouse Marks" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: mousemark/mousemark_config.ui:23 +#, kde-format +msgid "Wid&th:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: mousemark/mousemark_config.ui:36 +#, kde-format +msgid "&Color:" +msgstr "&Цвят:" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: mousemark/mousemark_config.ui:78 +#, kde-format +msgid "Draw with the mouse by holding Shift+Meta keys and moving the mouse." +msgstr "" + +#: presentwindows/presentwindows.cpp:66 +#: presentwindows/presentwindows_config.cpp:62 +#, kde-format +msgid "Toggle Present Windows (Current desktop)" +msgstr "" + +#: presentwindows/presentwindows.cpp:75 +#: presentwindows/presentwindows_config.cpp:56 +#, kde-format +msgid "Toggle Present Windows (All desktops)" +msgstr "" + +#: presentwindows/presentwindows.cpp:85 +#: presentwindows/presentwindows_config.cpp:68 +#, kde-format +msgid "Toggle Present Windows (Window class)" +msgstr "" + +#: presentwindows/presentwindows.cpp:1666 +#, kde-format +msgid "" +"Filter:\n" +"%1" +msgstr "" +"Филтър:\n" +"%1" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_3) +#: presentwindows/presentwindows_config.ui:39 +#, kde-format +msgid "Natural Layout Settings" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_FillGaps) +#: presentwindows/presentwindows_config.ui:45 +#, kde-format +msgid "Fill &gaps" +msgstr "&Запълване на празнини" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: presentwindows/presentwindows_config.ui:65 +#, kde-format +msgid "Faster" +msgstr "По-бързо" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: presentwindows/presentwindows_config.ui:112 +#, kde-format +msgid "Nicer" +msgstr "По-красиво" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_4) +#: presentwindows/presentwindows_config.ui:122 +#, kde-format +msgid "Windows" +msgstr "Прозорци" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#. i18n: ectx: property (text), widget (QLabel, label_8) +#: presentwindows/presentwindows_config.ui:128 +#: presentwindows/presentwindows_config.ui:282 +#, kde-format +msgid "Left button:" +msgstr "Ляв бутон:" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonDesktop) +#: presentwindows/presentwindows_config.ui:139 +#: presentwindows/presentwindows_config.ui:183 +#: presentwindows/presentwindows_config.ui:232 +#: presentwindows/presentwindows_config.ui:293 +#: presentwindows/presentwindows_config.ui:327 +#: presentwindows/presentwindows_config.ui:361 +#, kde-format +msgid "No action" +msgstr "Няма действие" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonDesktop) +#: presentwindows/presentwindows_config.ui:144 +#: presentwindows/presentwindows_config.ui:188 +#: presentwindows/presentwindows_config.ui:237 +#: presentwindows/presentwindows_config.ui:298 +#: presentwindows/presentwindows_config.ui:332 +#: presentwindows/presentwindows_config.ui:366 +#, kde-format +msgid "Activate window" +msgstr "Активиране на прозорец" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonDesktop) +#: presentwindows/presentwindows_config.ui:149 +#: presentwindows/presentwindows_config.ui:193 +#: presentwindows/presentwindows_config.ui:242 +#: presentwindows/presentwindows_config.ui:303 +#: presentwindows/presentwindows_config.ui:337 +#: presentwindows/presentwindows_config.ui:371 +#, kde-format +msgid "End effect" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonWindow) +#: presentwindows/presentwindows_config.ui:154 +#: presentwindows/presentwindows_config.ui:198 +#: presentwindows/presentwindows_config.ui:247 +#, kde-format +msgid "Bring window to current desktop" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonWindow) +#: presentwindows/presentwindows_config.ui:159 +#: presentwindows/presentwindows_config.ui:203 +#: presentwindows/presentwindows_config.ui:252 +#, kde-format +msgid "Send window to all desktops" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonWindow) +#: presentwindows/presentwindows_config.ui:164 +#: presentwindows/presentwindows_config.ui:208 +#: presentwindows/presentwindows_config.ui:257 +#, kde-format +msgid "(Un-)Minimize window" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_4) +#. i18n: ectx: property (text), widget (QLabel, label_9) +#: presentwindows/presentwindows_config.ui:172 +#: presentwindows/presentwindows_config.ui:316 +#, kde-format +msgid "Middle button:" +msgstr "Среден бутон:" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonWindow) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonWindow) +#: presentwindows/presentwindows_config.ui:213 +#: presentwindows/presentwindows_config.ui:262 +#, fuzzy, kde-format +#| msgid "Scale window" +msgid "Close window" +msgstr "Мащабиране на прозорец" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#. i18n: ectx: property (text), widget (QLabel, label_10) +#: presentwindows/presentwindows_config.ui:221 +#: presentwindows/presentwindows_config.ui:350 +#, kde-format +msgid "Right button:" +msgstr "Десен бутон:" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_5) +#: presentwindows/presentwindows_config.ui:273 +#, kde-format +msgctxt "@title:group actions when clicking on desktop" +msgid "Desktop" +msgstr "Работен плот" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LeftButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_MiddleButtonDesktop) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_RightButtonDesktop) +#: presentwindows/presentwindows_config.ui:308 +#: presentwindows/presentwindows_config.ui:342 +#: presentwindows/presentwindows_config.ui:376 +#, kde-format +msgid "Show desktop" +msgstr "Показване на работния плот" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: presentwindows/presentwindows_config.ui:393 +#, kde-format +msgid "Layout mode:" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_DrawWindowIcons) +#: presentwindows/presentwindows_config.ui:413 +#, kde-format +msgid "Display window &icons" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_IgnoreMinimized) +#: presentwindows/presentwindows_config.ui:420 +#, kde-format +msgid "Ignore &minimized windows" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ShowPanel) +#: presentwindows/presentwindows_config.ui:427 +#, kde-format +msgid "Show &panels" +msgstr "Показване на па&нели" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LayoutMode) +#: presentwindows/presentwindows_config.ui:441 +#, kde-format +msgid "Natural" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LayoutMode) +#: presentwindows/presentwindows_config.ui:446 +#, kde-format +msgid "Regular Grid" +msgstr "Обикновена мрежа" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_LayoutMode) +#: presentwindows/presentwindows_config.ui:451 +#, kde-format +msgid "Flexible Grid" +msgstr "Гъвкава мрежа" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AllowClosingWindows) +#: presentwindows/presentwindows_config.ui:459 +#, kde-format +msgid "Provide buttons to close the windows" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_TextureScale) +#: resize/resize_config.ui:17 +#, kde-format +msgid "Scale window" +msgstr "Мащабиране на прозорец" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Outline) +#: resize/resize_config.ui:24 +#, kde-format +msgid "Show outline" +msgstr "Показване на рамка" + +#. i18n: ectx: property (text), widget (QLabel, label_InScale) +#: scale/package/contents/ui/config.ui:46 +#, fuzzy, kde-format +#| msgid "Animation" +msgid "Window open scale:" +msgstr "Анимация" + +#. i18n: ectx: property (text), widget (QLabel, label_OutScale) +#: scale/package/contents/ui/config.ui:53 +#, fuzzy, kde-format +#| msgid "Animation" +msgid "Window close scale:" +msgstr "Анимация" + +#: screenshot/screenshot.cpp:440 +#, kde-format +msgctxt "Notification caption that a screenshot got saved to file" +msgid "Screenshot" +msgstr "" + +#: screenshot/screenshot.cpp:441 +#, kde-format +msgctxt "Notification with path to screenshot file" +msgid "Screenshot saved to %1" +msgstr "" + +#: screenshot/screenshot.cpp:578 +#, kde-format +msgid "" +"Select window to screen shot with left click or enter.\n" +"Escape or right click to cancel." +msgstr "" + +#: screenshot/screenshot.cpp:581 +#, kde-format +msgid "" +"Create screen shot with left click or enter.\n" +"Escape or right click to cancel." +msgstr "" + +#: showfps/showfps.cpp:54 +#, kde-format +msgid "This effect is not a benchmark" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: showfps/showfps_config.ui:23 +#, kde-format +msgid "Text position:" +msgstr "Разположение на текста:" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TextPosition) +#: showfps/showfps_config.ui:43 +#, kde-format +msgid "Inside Graph" +msgstr "Вътре в графиката" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TextPosition) +#: showfps/showfps_config.ui:48 +#, kde-format +msgid "Nowhere" +msgstr "Никъде" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TextPosition) +#: showfps/showfps_config.ui:53 +#, kde-format +msgid "Top Left" +msgstr "Горе вляво" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TextPosition) +#: showfps/showfps_config.ui:58 +#, kde-format +msgid "Top Right" +msgstr "Горе вдясно" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TextPosition) +#: showfps/showfps_config.ui:63 +#, kde-format +msgid "Bottom Left" +msgstr "Долу вляво" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TextPosition) +#: showfps/showfps_config.ui:68 +#, kde-format +msgid "Bottom Right" +msgstr "Долу вдясно" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: showfps/showfps_config.ui:76 +#, kde-format +msgid "Text font:" +msgstr "Шрифт за текста:" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: showfps/showfps_config.ui:96 +#, kde-format +msgid "Text color:" +msgstr "Цвят на текста:" + +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: showfps/showfps_config.ui:119 +#, kde-format +msgid "Text alpha:" +msgstr "Прозрачност на текста:" + +#: showpaint/showpaint.cpp:42 showpaint/showpaint_config.cpp:41 +#, fuzzy, kde-format +#| msgid "Show &panels" +msgid "Toggle Show Paint" +msgstr "Показване на па&нели" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_Gaps) +#: slide/slide_config.ui:50 +#, kde-format +msgid "Gap between desktops" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_HorizontalGap) +#: slide/slide_config.ui:56 +#, fuzzy, kde-format +#| msgid "Horizontal" +msgid "Horizontal:" +msgstr "Хоризонтално" + +#. i18n: ectx: property (text), widget (QLabel, label_VerticalGap) +#: slide/slide_config.ui:79 +#, fuzzy, kde-format +#| msgid "Vertical" +msgid "Vertical:" +msgstr "Вертикално" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_SlideDocks) +#: slide/slide_config.ui:105 +#, kde-format +msgid "Slide docks" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_SlideBackground) +#: slide/slide_config.ui:112 +#, fuzzy, kde-format +#| msgid "Slide when grouping" +msgid "Slide desktop background" +msgstr "Приплъзване при групиране" + +#: thumbnailaside/thumbnailaside.cpp:29 +#: thumbnailaside/thumbnailaside_config.cpp:62 +#, kde-format +msgid "Toggle Thumbnail for Current Window" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: thumbnailaside/thumbnailaside_config.ui:23 +#, kde-format +msgid "Maximum &width:" +msgstr "Максимална &широчина:" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: thumbnailaside/thumbnailaside_config.ui:36 +#, kde-format +msgid "&Spacing:" +msgstr "&Интервал:" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_Spacing) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_MaxWidth) +#: thumbnailaside/thumbnailaside_config.ui:55 +#: thumbnailaside/thumbnailaside_config.ui:106 +#, fuzzy, kde-format +#| msgid " pixel" +#| msgid_plural " pixels" +msgid " pixels" +msgstr " точка" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: thumbnailaside/thumbnailaside_config.ui:68 +#, kde-format +msgid "&Opacity:" +msgstr "&Непрозрачност:" + +#: trackmouse/trackmouse.cpp:50 trackmouse/trackmouse_config.cpp:59 +#, kde-format +msgid "Track mouse" +msgstr "Проследяване на мишката" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: trackmouse/trackmouse_config.ui:26 +#, kde-format +msgid "Trigger effect with:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_KeyboardShortcut) +#: trackmouse/trackmouse_config.ui:33 +#, kde-format +msgid "Keyboard shortcut:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_ModifierKeys) +#: trackmouse/trackmouse_config.ui:43 +#, kde-format +msgid "Modifier keys:" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Alt) +#: trackmouse/trackmouse_config.ui:65 +#, kde-format +msgid "Alt" +msgstr "Alt" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Control) +#: trackmouse/trackmouse_config.ui:72 +#, kde-format +msgid "Ctrl" +msgstr "Ctrl" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Shift) +#: trackmouse/trackmouse_config.ui:79 +#, kde-format +msgid "Shift" +msgstr "Shift" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Meta) +#: trackmouse/trackmouse_config.ui:86 +#, kde-format +msgid "Meta" +msgstr "Meta" + +#. i18n: ectx: property (windowTitle), widget (QWidget, KWin::TranslucencyEffectConfigForm) +#: translucency/package/contents/ui/config.ui:14 +#, kde-format +msgid "Translucency" +msgstr "Прозрачност" + +#. i18n: ectx: property (title), widget (QGroupBox, m_opacityGroupBox) +#: translucency/package/contents/ui/config.ui:20 +#, kde-format +msgid "General Translucency Settings" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, comboboxpopup_label) +#: translucency/package/contents/ui/config.ui:64 +#, kde-format +msgid "Combobox popups:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, dialogs_label) +#: translucency/package/contents/ui/config.ui:137 +#, kde-format +msgid "Dialogs:" +msgstr "Диалогови прозорци:" + +#. i18n: ectx: property (text), widget (QLabel, menus_label) +#: translucency/package/contents/ui/config.ui:188 +#, kde-format +msgid "Menus:" +msgstr "Менюта:" + +#. i18n: ectx: property (text), widget (QLabel, moveresize_label) +#: translucency/package/contents/ui/config.ui:207 +#, kde-format +msgid "Moving windows:" +msgstr "Преместващи се прозорци:" + +#. i18n: ectx: property (text), widget (QLabel, inactive_label) +#: translucency/package/contents/ui/config.ui:226 +#, kde-format +msgid "Inactive windows:" +msgstr "Неактивни прозорци:" + +#. i18n: ectx: property (title), widget (QGroupBox, kcfg_IndividualMenuConfig) +#: translucency/package/contents/ui/config.ui:267 +#, kde-format +msgid "Set menu translucency independently" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, dropdownmenus_label) +#: translucency/package/contents/ui/config.ui:285 +#, kde-format +msgid "Dropdown menus:" +msgstr "Падащи менюта:" + +#. i18n: ectx: property (text), widget (QLabel, popupmenus_label) +#: translucency/package/contents/ui/config.ui:329 +#, kde-format +msgid "Popup menus:" +msgstr "Изскачащи менюта:" + +#. i18n: ectx: property (text), widget (QLabel, tornoffmenus_label) +#: translucency/package/contents/ui/config.ui:367 +#, kde-format +msgid "Torn-off menus:" +msgstr "Отделящи се менюта:" + +#: windowgeometry/windowgeometry.cpp:43 +#, kde-format +msgid "Toggle window geometry display (effect only)" +msgstr "" + +#: windowgeometry/windowgeometry_config.cpp:47 +#, kde-format +msgid "Toggle KWin composited geometry display" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Move) +#: windowgeometry/windowgeometry_config.ui:17 +#, kde-format +msgid "Display for moving windows" +msgstr "Показване при премествани прозорци" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_Resize) +#: windowgeometry/windowgeometry_config.ui:24 +#, kde-format +msgid "Display for resizing windows" +msgstr "Показване при преоразмерявани прозорци" + +#. i18n: ectx: property (title), widget (QGroupBox, advancedGroup) +#: wobblywindows/wobblywindows_config.ui:20 +#, kde-format +msgid "Advanced" +msgstr "Допълнителни" + +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: wobblywindows/wobblywindows_config.ui:26 +#, kde-format +msgid "&Stiffness:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: wobblywindows/wobblywindows_config.ui:68 +#, kde-format +msgid "Dra&g:" +msgstr "Вла&чене:" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: wobblywindows/wobblywindows_config.ui:81 +#, kde-format +msgid "&Move factor:" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_MoveWobble) +#: wobblywindows/wobblywindows_config.ui:155 +#, kde-format +msgid "Wo&bble when moving" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ResizeWobble) +#: wobblywindows/wobblywindows_config.ui:162 +#, kde-format +msgid "Wobble when &resizing" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AdvancedMode) +#: wobblywindows/wobblywindows_config.ui:182 +#, kde-format +msgid "Enable &advanced mode" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, basicGroup) +#: wobblywindows/wobblywindows_config.ui:192 +#, kde-format +msgid "&Wobbliness" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: wobblywindows/wobblywindows_config.ui:201 +#, kde-format +msgid "Less" +msgstr "По-малко" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: wobblywindows/wobblywindows_config.ui:224 +#, kde-format +msgid "More" +msgstr "Повече" + +#: zoom/zoom.cpp:74 +#, kde-format +msgid "Move Zoomed Area to Left" +msgstr "" + +#: zoom/zoom.cpp:82 +#, kde-format +msgid "Move Zoomed Area to Right" +msgstr "" + +#: zoom/zoom.cpp:90 +#, kde-format +msgid "Move Zoomed Area Upwards" +msgstr "" + +#: zoom/zoom.cpp:98 +#, kde-format +msgid "Move Zoomed Area Downwards" +msgstr "" + +#: zoom/zoom.cpp:107 zoom/zoom_config.cpp:109 +#, kde-format +msgid "Move Mouse to Focus" +msgstr "Преместване на мишката във фокуса" + +#: zoom/zoom.cpp:115 zoom/zoom_config.cpp:116 +#, kde-format +msgid "Move Mouse to Center" +msgstr "Преместване на мишката в центъра" + +#: zoom/zoom_config.cpp:81 +#, kde-format +msgid "Move Left" +msgstr "Преместване наляво" + +#: zoom/zoom_config.cpp:88 +#, kde-format +msgid "Move Right" +msgstr "Преместване надясно" + +#: zoom/zoom_config.cpp:95 +#, kde-format +msgid "Move Up" +msgstr "Преместване нагоре" + +#: zoom/zoom_config.cpp:102 +#, kde-format +msgid "Move Down" +msgstr "Преместване надолу" + +#. i18n: ectx: property (whatsThis), widget (QLabel, label) +#. i18n: ectx: property (whatsThis), widget (QDoubleSpinBox, kcfg_ZoomFactor) +#: zoom/zoom_config.ui:25 zoom/zoom_config.ui:41 +#, kde-format +msgid "On zoom-in and zoom-out change the zoom by the defined zoom-factor." +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: zoom/zoom_config.ui:28 +#, kde-format +msgid "Zoom Factor:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_EnableFocusTracking) +#: zoom/zoom_config.ui:66 +#, kde-format +msgid "" +"Enable tracking of the focused location. This needs QAccessible to be " +"enabled per application (\"export QT_LINUX_ACCESSIBILITY_ALWAYS_ON=1\")." +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_EnableFocusTracking) +#: zoom/zoom_config.ui:69 +#, kde-format +msgid "Enable Focus Tracking" +msgstr "Включване следене на фокуса" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_EnableTextCaretTracking) +#: zoom/zoom_config.ui:76 +#, kde-format +msgid "" +"Enable tracking of the text cursor. This needs QAccessible to be enabled per " +"application (\"export QT_LINUX_ACCESSIBILITY_ALWAYS_ON=1\")." +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_EnableTextCaretTracking) +#: zoom/zoom_config.ui:79 +#, fuzzy, kde-format +#| msgid "Enable Focus Tracking" +msgid "Enable Text Cursor Tracking" +msgstr "Включване следене на фокуса" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: zoom/zoom_config.ui:86 +#, kde-format +msgid "Mouse Pointer:" +msgstr "Курсор на мишката:" + +#. i18n: ectx: property (whatsThis), widget (QComboBox, kcfg_MousePointer) +#: zoom/zoom_config.ui:99 +#, kde-format +msgid "Visibility of the mouse-pointer." +msgstr "Видимост на курсора на мишката" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_MousePointer) +#: zoom/zoom_config.ui:103 +#, kde-format +msgid "Scale" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_MousePointer) +#: zoom/zoom_config.ui:108 +#, kde-format +msgid "Keep" +msgstr "Запазване" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_MousePointer) +#: zoom/zoom_config.ui:113 +#, kde-format +msgid "Hide" +msgstr "Скриване" + +#. i18n: ectx: property (whatsThis), widget (QComboBox, kcfg_MouseTracking) +#: zoom/zoom_config.ui:121 +#, kde-format +msgid "Track moving of the mouse." +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_MouseTracking) +#: zoom/zoom_config.ui:125 +#, kde-format +msgid "Proportional" +msgstr "Пропорционално" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_MouseTracking) +#: zoom/zoom_config.ui:130 +#, kde-format +msgid "Centered" +msgstr "Центрирано" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_MouseTracking) +#: zoom/zoom_config.ui:135 +#, kde-format +msgid "Push" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, kcfg_MouseTracking) +#: zoom/zoom_config.ui:140 +#, kde-format +msgid "Disabled" +msgstr "Изключено" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: zoom/zoom_config.ui:148 +#, kde-format +msgid "Mouse Tracking:" +msgstr "Следене на мишката:" \ No newline at end of file diff --git a/po/bn/kcmkwm.po b/po/bn/kcmkwm.po new file mode 100644 index 0000000..df88427 --- /dev/null +++ b/po/bn/kcmkwm.po @@ -0,0 +1,1378 @@ +# translation of kcmkwm.po to Bengali +# এটি kdebase/kcmkwm.po ফাইলের বাংলা অনুবাদ +# Deepayan Sarkar , 2004, 2005. +# Deepayan Sarkar , 2009, 2010. +msgid "" +msgstr "" +"Project-Id-Version: kcmkwm\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-08-29 02:26+0200\n" +"PO-Revision-Date: 2010-06-01 10:33-0700\n" +"Last-Translator: Deepayan Sarkar \n" +"Language-Team: Bengali \n" +"Language: bn\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 1.0\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 "anirban@bengalinux.org, deepayan.sarkar@gmail.com" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_1) +#: actions.ui:17 +#, fuzzy, kde-format +#| msgid "Inactive Inner Window" +msgid "Inactive Inner Window Actions" +msgstr "নিষ্ক্রিয় ভিতরের উইন্ডো" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#. i18n: ectx: property (text), widget (QLabel, label_1) +#: actions.ui:26 mouse.ui:177 +#, fuzzy, kde-format +#| msgid "&Titlebar double-click:" +msgid "&Left click:" +msgstr "শীর্ষ&কবার দুবার ক্লিক" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandWindow1) +#: actions.ui:39 +#, kde-format +msgid "" +"In this row you can customize left click behavior when clicking into an " +"inactive inner window ('inner' means: not titlebar, not frame)." +msgstr "" +"এই সারিতে নিষ্ক্রিয় ভিতরের উইন্ডোর শীর্ষকবারে বা ফ্রেমে মাউসের বাম বোতাম ক্লিক্ " +"করলে কী হবে তা পছন্দমত ঠিক করতে পারবেন (ভিতরের মানে: শীর্ষকবার বা কাঠামো নয়)।" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow3) +#: actions.ui:43 actions.ui:83 actions.ui:123 +#, fuzzy, kde-format +#| msgid "Activate, Raise & Pass Click" +msgid "Activate, raise and pass click" +msgstr "সক্রিয় কর, তুলে ধর এবং ক্লিকটি পাঠাও" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow3) +#: actions.ui:48 actions.ui:88 actions.ui:128 +#, fuzzy, kde-format +#| msgid "Activate & Pass Click" +msgid "Activate and pass click" +msgstr "সক্রিয় কর এবং ক্লিকটি পাঠাও" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow3) +#: actions.ui:53 actions.ui:93 actions.ui:133 mouse.ui:293 mouse.ui:408 +#: mouse.ui:523 +#, kde-format +msgid "Activate" +msgstr "সক্রিয় কর" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindow3) +#: actions.ui:58 actions.ui:98 actions.ui:138 mouse.ui:283 mouse.ui:398 +#: mouse.ui:513 +#, fuzzy, kde-format +#| msgid "Activate & Raise" +msgid "Activate and raise" +msgstr "সক্রিয় কর এবং তুলে ধর" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: actions.ui:66 mouse.ui:200 +#, fuzzy, kde-format +#| msgid "&Titlebar double-click:" +msgid "&Middle click:" +msgstr "শীর্ষ&কবার দুবার ক্লিক" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandWindow2) +#: actions.ui:79 +#, kde-format +msgid "" +"In this row you can customize middle click behavior when clicking into an " +"inactive inner window ('inner' means: not titlebar, not frame)." +msgstr "" +"এই সারিতে নিষ্ক্রিয় ভিতরের উইন্ডোর শীর্ষকবারে বা ফ্রেমে মাউসের মধ্য বোতাম ক্লিক্ " +"করলে কী হবে তা পছন্দমত ঠিক করতে পারবেন (ভিতরের মানে: শীর্ষকবার বা কাঠামো নয়)।" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: actions.ui:106 mouse.ui:213 +#, kde-format +msgid "&Right click:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandWindow3) +#: actions.ui:119 +#, kde-format +msgid "" +"In this row you can customize right click behavior when clicking into an " +"inactive inner window ('inner' means: not titlebar, not frame)." +msgstr "" +"এই সারিতে নিষ্ক্রিয় ভিতরের উইন্ডোর শীর্ষকবারে বা ফ্রেমে মাউসের ডান বোতাম ক্লিক্ " +"করলে কী হবে তা পছন্দমত ঠিক করতে পারবেন (ভিতরের মানে: শীর্ষকবার বা কাঠামো নয়)।" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#. i18n: ectx: property (text), widget (QLabel, label_4) +#: actions.ui:146 mouse.ui:88 +#, fuzzy, kde-format +#| msgid "Mouse wheel:" +msgid "Mouse &wheel:" +msgstr "মাউস হুইল:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandWindowWheel) +#: actions.ui:159 +#, fuzzy, kde-format +#| msgid "" +#| "In this row you can customize left click behavior when clicking into an " +#| "inactive inner window ('inner' means: not titlebar, not frame)." +msgid "" +"In this row you can customize behavior when scrolling into an inactive inner " +"window ('inner' means: not titlebar, not frame)." +msgstr "" +"এই সারিতে নিষ্ক্রিয় ভিতরের উইন্ডোর শীর্ষকবারে বা ফ্রেমে মাউসের বাম বোতাম ক্লিক্ " +"করলে কী হবে তা পছন্দমত ঠিক করতে পারবেন (ভিতরের মানে: শীর্ষকবার বা কাঠামো নয়)।" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindowWheel) +#: actions.ui:163 +#, kde-format +msgid "Scroll" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindowWheel) +#: actions.ui:168 +#, fuzzy, kde-format +#| msgid "Activate & Lower" +msgid "Activate and scroll" +msgstr "সক্রিয় কর এবং নামাও" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandWindowWheel) +#: actions.ui:173 +#, fuzzy, kde-format +#| msgid "Activate, Raise and Move" +msgid "Activate, raise and scroll" +msgstr "সক্রিয় কর, তুলে ধর এবং সরাও" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#: actions.ui:184 +#, fuzzy, kde-format +#| msgid "Inner Window, Titlebar && Frame" +msgid "Inner Window, Titlebar and Frame Actions" +msgstr "ভিতরের উইন্ডো, শীর্ষকবার এবং কাঠামো" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: actions.ui:195 +#, fuzzy, kde-format +#| msgid "Modifier key:" +msgid "Mo&difier key:" +msgstr "পরিবর্তক কী(key):" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandAllKey) +#: actions.ui:205 +#, kde-format +msgid "" +"Here you select whether holding the Meta key or Alt key will allow you to " +"perform the following actions." +msgstr "" +"এখানে নিম্নবর্ণিত কাজগুলি করতে মেটা(Meta) না অল্ট(Alt) কী টিপে রাখতে হবে তা বেছে " +"নিন।" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllKey) +#: actions.ui:209 +#, kde-format +msgid "Meta" +msgstr "মেটা (Meta)" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllKey) +#: actions.ui:214 +#, kde-format +msgid "Alt" +msgstr "অল্ট (Alt)" + +#. i18n: ectx: property (text), widget (QLabel, label_6) +#: actions.ui:236 +#, kde-format +msgid " + " +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_8) +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: actions.ui:248 mouse.ui:601 +#, fuzzy, kde-format +#| msgid "&Titlebar double-click:" +msgid "L&eft click:" +msgstr "শীর্ষ&কবার দুবার ক্লিক" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandAll1) +#: actions.ui:261 +#, kde-format +msgid "" +"In this row you can customize left click behavior when clicking into the " +"titlebar or the frame." +msgstr "" +"এখানে উইন্ডোর শীর্ষকবারে বা ফ্রেমে বাঁ বোতাম ক্লিক্ করলে কী হবে তা পছন্দমত ঠিক করতে " +"পারবেন।" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:265 actions.ui:335 actions.ui:405 +#, kde-format +msgid "Move" +msgstr "সরাও" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:270 actions.ui:340 actions.ui:410 +#, fuzzy, kde-format +#| msgid "Activate, Raise and Move" +msgid "Activate, raise and move" +msgstr "সক্রিয় কর, তুলে ধর এবং সরাও" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:275 actions.ui:345 actions.ui:415 mouse.ui:246 mouse.ui:308 +#: mouse.ui:361 mouse.ui:423 mouse.ui:476 mouse.ui:538 +#, fuzzy, kde-format +#| msgid "Toggle Raise & Lower" +msgid "Toggle raise and lower" +msgstr "ওঠানো বা নামানোর মধ্যে পরিবর্তন কর" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:280 actions.ui:350 actions.ui:420 +#, kde-format +msgid "Resize" +msgstr "আকার পরিবর্তন কর" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:285 actions.ui:355 actions.ui:425 mouse.ui:236 mouse.ui:298 +#: mouse.ui:351 mouse.ui:413 mouse.ui:466 mouse.ui:528 +#, kde-format +msgid "Raise" +msgstr "তুলে ধর" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:290 actions.ui:360 actions.ui:430 mouse.ui:65 mouse.ui:241 +#: mouse.ui:303 mouse.ui:356 mouse.ui:418 mouse.ui:471 mouse.ui:533 +#, kde-format +msgid "Lower" +msgstr "নিচে নামাও" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:295 actions.ui:365 actions.ui:435 mouse.ui:55 mouse.ui:251 +#: mouse.ui:313 mouse.ui:366 mouse.ui:428 mouse.ui:481 mouse.ui:543 +#, kde-format +msgid "Minimize" +msgstr "ছোট কর" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:300 actions.ui:370 actions.ui:440 +#, fuzzy, kde-format +#| msgid "Change Opacity" +msgid "Decrease opacity" +msgstr "স্বচ্ছতা বদলাও" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:305 actions.ui:375 actions.ui:445 +#, fuzzy, kde-format +#| msgid "Change Opacity" +msgid "Increase opacity" +msgstr "স্বচ্ছতা বদলাও" + +#. i18n: @item:inlistbox behavior on double click +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_TitlebarDoubleClickCommand) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandActiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandInactiveTitlebar3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll1) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll2) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAll3) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:310 actions.ui:380 actions.ui:450 actions.ui:505 mouse.ui:80 +#: mouse.ui:132 mouse.ui:271 mouse.ui:333 mouse.ui:386 mouse.ui:448 +#: mouse.ui:501 mouse.ui:563 +#, fuzzy, kde-format +#| msgid "Nothing" +msgid "Do nothing" +msgstr "কিছু নয়" + +#. i18n: ectx: property (text), widget (QLabel, label_8) +#: actions.ui:318 +#, fuzzy, kde-format +#| msgid "Middle button:" +msgid "Middle &click:" +msgstr "মাঝের বোতাম:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandAll2) +#: actions.ui:331 +#, kde-format +msgid "" +"In this row you can customize middle click behavior when clicking into the " +"titlebar or the frame." +msgstr "" +"এখানে উইন্ডোর শীর্ষকবারে বা ফ্রেমে মাঝের বোতাম ক্লিক্ করলে কী হবে তা পছন্দমত ঠিক " +"করতে পারবেন।" + +#. i18n: ectx: property (text), widget (QLabel, label_10) +#. i18n: ectx: property (text), widget (QLabel, label_9) +#: actions.ui:388 mouse.ui:671 +#, kde-format +msgid "Right clic&k:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandAll3) +#: actions.ui:401 +#, kde-format +msgid "" +"In this row you can customize right click behavior when clicking into the " +"titlebar or the frame." +msgstr "" +"এখানে উইন্ডোর শীর্ষকবারে বা ফ্রেমে ডান বোতাম ক্লিক্ করলে কী হবে তা পছন্দমত ঠিক " +"করতে পারবেন।" + +#. i18n: ectx: property (text), widget (QLabel, label_10) +#: actions.ui:458 +#, fuzzy, kde-format +#| msgid "Mouse wheel:" +msgid "Mo&use wheel:" +msgstr "মাউস হুইল:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:471 +#, fuzzy, kde-format +msgid "" +"Here you can customize KDE's behavior when scrolling with the mouse wheel in " +"a window while pressing the modifier key." +msgstr "" +"এখানে আপনি একটি পরিবর্তক কী টিপে রেখে উইন্ডোর কোনো জায়গায় ক্লিক করলে কে-ডি-ই কি " +"ব্যবহার করবে তা পছন্দমত ঠিক করতে পারবেন" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:475 mouse.ui:102 +#, fuzzy, kde-format +#| msgid "Toggle Raise & Lower" +msgid "Raise/lower" +msgstr "ওঠানো বা নামানোর মধ্যে পরিবর্তন কর" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:480 mouse.ui:107 +#, kde-format +msgid "Shade/unshade" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:485 mouse.ui:112 +#, fuzzy, kde-format +#| msgid "Maximize/Restore" +msgid "Maximize/restore" +msgstr "পুরো বড় করো/পূর্বাবস্থায় ফেরাও" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:490 mouse.ui:117 +#, fuzzy, kde-format +#| msgid "Keep Above/Below" +msgid "Keep above/below" +msgstr "উপরে/নীচে রাখো" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:495 mouse.ui:122 +#, fuzzy, kde-format +#| msgid "Move to Previous/Next Desktop" +msgid "Move to previous/next desktop" +msgstr "পূর্ববর্তী/পরবর্তী ডেস্কটপে সরাও" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandTitlebarWheel) +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_CommandAllWheel) +#: actions.ui:500 mouse.ui:127 +#, fuzzy, kde-format +#| msgid "Change Opacity" +msgid "Change opacity" +msgstr "স্বচ্ছতা বদলাও" + +#. i18n: ectx: property (text), widget (QLabel, shadeHoverLabel) +#: advanced.ui:20 +#, fuzzy, kde-format +#| msgid "Window Tabbing" +msgid "Window &unshading:" +msgstr "উইণ্ডো ট্যাবিং" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_ShadeHover) +#: advanced.ui:32 +#, fuzzy, kde-format +#| msgid "" +#| "If Shade Hover is enabled, a shaded window will un-shade automatically " +#| "when the mouse pointer has been over the title bar for some time." +msgid "" +"

If this option is enabled, a shaded window will " +"unshade automatically when the mouse pointer has been over the titlebar for " +"some time.

" +msgstr "" +"শেড হোভার কার্যকরী হলে একটি ছায়াবৃত উইন্ডোর শীর্ষকবারের ওপর মাউসের নির্দেশকটি " +"কিছুক্ষন রাখলেই সেটি আপনা থেকে ছড়িয়ে যাবে" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_ShadeHover) +#: advanced.ui:35 +#, kde-format +msgid "On titlebar hover after:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QSpinBox, kcfg_ShadeHoverInterval) +#: advanced.ui:42 +#, kde-format +msgid "" +"Sets the time in milliseconds before the window unshades when the mouse " +"pointer goes over the shaded window." +msgstr "" +"মাউসের নির্দেশকটি কতক্ষন ছায়াবৃত উইন্ডোর শীর্ষকবারের ওপর রাখলে সেটি আপনা থেকে " +"ছড়িয়ে যাবে সেই সময়টিমিলিসেকেন্ডে ঠিক করা যাবে।" + +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_ShadeHoverInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_DelayFocusInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, kcfg_AutoRaiseInterval) +#: advanced.ui:45 focus.ui:85 focus.ui:178 +#, kde-format +msgid " ms" +msgstr " মাইক্রোসেকেন্ড" + +#. i18n: ectx: property (text), widget (QLabel, windowPlacementLabel) +#: advanced.ui:66 +#, fuzzy, kde-format +#| msgid "&Placement:" +msgid "Window &placement:" +msgstr "সংস্থাপ&ন" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_Placement) +#: advanced.ui:76 +#, kde-format +msgid "" +"

The placement policy determines where a new window " +"will appear on the desktop.

  • Smart will try to achieve a minimum overlap of windows
  • Maximizing will try to maximize every window to fill the " +"whole screen. It might be useful to selectively affect placement of some " +"windows using the window-specific settings.
  • Cascade will " +"cascade the windows
  • Random will use a random " +"position
  • Centered will place the window " +"centered
  • Zero-cornered will place the window in " +"the top-left corner
  • Under mouse will place the " +"window under the pointer
" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:80 +#, fuzzy, kde-format +#| msgid "Snap windows onl&y when overlapping" +msgid "Minimal Overlapping" +msgstr "কেব&লমাত্র উপরিপাতন হলে উইন্ডোগুলি আটকে দাও" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:85 +#, fuzzy, kde-format +#| msgctxt "@item:inlistbox behavior on double click" +#| msgid "Maximize" +msgid "Maximized" +msgstr "পুরো বড় কর" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:90 +#, fuzzy, kde-format +#| msgid "Cascade" +msgid "Cascaded" +msgstr "গুচ্ছাকার" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:95 +#, kde-format +msgid "Random" +msgstr "এলোমেলো" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:100 +#, kde-format +msgid "Centered" +msgstr "কেন্দ্রীয়" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:105 +#, kde-format +msgid "In Top-Left Corner" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, kcfg_Placement) +#: advanced.ui:110 +#, fuzzy, kde-format +#| msgid "Focus Under Mouse" +msgid "Under mouse" +msgstr "মাউসের নিচে ফোকাস" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_AllowKDEAppsToRememberWindowPositions) +#: advanced.ui:118 +#, kde-format +msgid "" +"When turned on, KDE apps which are able to remember the positions of their " +"windows are allowed to do so. This will override the window placement mode " +"defined above." +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_AllowKDEAppsToRememberWindowPositions) +#: advanced.ui:121 +#, kde-format +msgid "Allow KDE apps to remember the positions of their own windows" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, specialWindowsLabel) +#: advanced.ui:128 +#, fuzzy, kde-format +#| msgid "Windows" +msgid "&Special windows:" +msgstr "উইন্ডোসমূহ" + +#. i18n: ectx: property (whatsThis), widget (QCheckBox, kcfg_HideUtilityWindowsForInactive) +#: advanced.ui:138 +#, kde-format +msgid "" +"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." +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, kcfg_HideUtilityWindowsForInactive) +#: advanced.ui:141 +#, kde-format +msgid "Hide utility windows for inactive applications" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, windowFocusPolicyLabel) +#: focus.ui:22 +#, kde-format +msgid "Window &activation policy:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QComboBox, windowFocusPolicy) +#: focus.ui:32 +#, fuzzy, kde-format +#| msgid "" +#| "Enable this option if you want an animation shown when windows are " +#| "minimized or restored." +msgid "With this option you can specify how and when windows will be focused." +msgstr "" +"যদি আপনি উইন্ডো য়খন ছোট করা হবে বা আগের আকার করা হবে তখন একটি চলদৃশ্য " +"(অ্যানিমেশন)দেখতে চান, তাহলে এই অপশনটি কার্যকর করুন।" + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:36 +#, fuzzy, kde-format +#| msgid "Click to Focus" +msgctxt "sassa asas" +msgid "Click to focus" +msgstr "ক্লিক করলে ফোকাস" + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:41 +#, kde-format +msgid "Click to focus (mouse precedence)" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:46 +#, fuzzy, kde-format +#| msgid "Focus Follows Mouse" +msgid "Focus follows mouse" +msgstr "মাউস অনুসারী ফোকাস" + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:51 +#, kde-format +msgid "Focus follows mouse (mouse precedence)" +msgstr "" + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:56 +#, fuzzy, kde-format +#| msgid "Focus Under Mouse" +msgid "Focus under mouse" +msgstr "মাউসের নিচে ফোকাস" + +#. i18n: ectx: property (text), item, widget (QComboBox, windowFocusPolicy) +#: focus.ui:61 +#, fuzzy, kde-format +#| msgid "Focus Strictly Under Mouse" +msgid "Focus strictly under mouse" +msgstr "মাউসের ঠিক নিচে ফোকাস" + +#. i18n: ectx: property (text), widget (QLabel, delayFocusOnLabel) +#: focus.ui:69 +#, kde-format +msgid "&Delay focus by:" +msgstr "" + +#. i18n: ectx: property (whatsThis), widget (QSpinBox, kcfg_DelayFocusInterval) +#: focus.ui:82 +#, kde-format +msgid "" +"This is the delay after which the window the mouse pointer is over will " +"automatically receive focus." +msgstr "" +"এই সময় দেরীর পর যে উইন্ডোর ওপর মাউস নির্দেশক আছে সেটি আপনা থেকে উপরে উঠে আসবে।" + +#. i18n: ectx: property (text), widget (QLabel, focusStealingLabel) +#: focus.ui:101 +#, fuzzy, kde-format +#| msgid "Focus stealing prevention level:" +msgid "Focus &stealing prevention:" +msgstr "ফোকাস হরণ প্রতিরোধ স্তর:" + +#. i18n: ectx: property (whatsThis), widget (KComboBox, kcfg_FocusStealingPreventionLevel) +#: focus.ui:114 +#, fuzzy, kde-format +msgid "" +"

This option specifies how much KWin will try to " +"prevent unwanted focus stealing caused by unexpected activation of new " +"windows. (Note: This feature does not work with the Focus under mouse or Focus strictly under mouse focus policies.)

  • 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.

" +msgstr "" +"অপ্রত্যাশিতভাবে কোন অযাচিত উইন্ডো সক্রিয় হওয়ার ফলে ফোকাস হরণ প্রতিরোধের জন্য কে-" +"উইন কতটা চেষ্টা করবেতা এইখানে স্থির করতে পারবেন।

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 dmwxTz&`RZ)FU;{~t0EMR))J literal 0 HcmV?d00001 diff --git a/doc/windowspecific/kopete-chat-attribute.png b/doc/windowspecific/kopete-chat-attribute.png new file mode 100644 index 0000000000000000000000000000000000000000..4e59f2ebe8c866b43571c01c0f1a0ebbf6f2bff6 GIT binary patch literal 50398 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 z z9_USY`br$Am*AaUK=@DiP-H(GKaO^K^6S4>BjJ7`zaXVA91qjK@t@$!-Q{(D%A*<_ zynlS!)Yp%RB7g=X)PORktn_Q+Yw1Be5I75zccLTVJ69p}zF$ge`S%}q(Vq$`7}Wk= zZ?$>=FFDN9%ld1Pe?Fvz=jV69uQt?@H1#(JoN;({?fa$ZsS8P-{DJ`y8Yk@!9y;JU z6>q+a#CN|of#8cdRkt2%tEwgGSiG}Im#%w{ZwSpm|-)T z0H%LZU0bC7s=A{1vJU^BL*7y3S_I`@IuHu3{#UE_vEAjF9aAb!Pe)X~XGFv&JgX1S zLto>kD8aq)jY-^PlX{9#r@JeYeZ-X($Sp#hDz{i(#dOcScM~+U@_HMlECaZVl1)BQ zLZIY=ZP4SOkVa}gB+qeL^vQYdyZoYMhaR& z1f*9!+Y*}l++IlHQ*LIRI_=UCH8CwcDQkCHG@1OEIGX*&_Kr)-v#YkE`UW!fr3Kv0 z5nnn_=Vxofosaccri2H0=x=A+e(2KFtk-+fwxdL^uSzIw`(35o*b2-U;o4X-iaU2M zuGHFAeBM}VN40t?=gmr-tX;2NUBjwY%!qg)@fB?ICfsZ7^_guwLm%pm)FV*u|E84N zN|AE6|0`0i1?{o_Sjtsl9%fdD(CKimRviR;4~Od*)?~K4v)zGosdd}WiE22rvmdVf zX7E$(RO2|nPb&Fv6QLVWbl+X+*~H05MBz|4BLz1Kyswed+8(-zG@L-0%i6t`+8@Z* z5l?0AuRq4d(X@!d*n`<}Xr708Qa8@h=b%o9(}&T=BrvJAo^%5t)=qTE)QlPG9y0#Pp2!y1eD`QS0^0_#~s0W9bq?SZYRYgkBjN(?b>+KoX;{G zYbmkmB$b<<;zBF=rp6h-DGoA)AewY2Z6cJd%g+{vJ(}NHD-ZR0D?D$vF*tNc_TY$J zHGNS+mD%Y}TMGAINYAXdRIav~77l2-;-?zdh`i>k{P_KkKKb+F>6^qW-B361VY9aX zAOh;-nrtKcK5X&fwPm()dWyR{AJb&|kxMbtQix8jMvOjz`F=-{`~Cdz={0k8_;nZ! zW+-7(&)uecd*PM@hxK}+@{Q!Ycl49j$Cl-Y3&+>-?u&e>Q_IVJ#Q1Rss$Cjjf{1H&rhWdNkVXg8%RyAH?rS~E!^h`azdRX zm<|-eJj^PMr_9KN?R$OEo3Ct|$dVO5a7Q0M?(H;e*=WqI%7=wdM1y9K1c#aO6*VJ5 z-SVttev1zk9|o06qHg`dt6!(@terHhG!#g zx>>F?ev%Vb!+a;_RgAV%{llHRqHXV4<9~CsOP)Os$J(n$JefR*XxUMM#g!aP7QvfF z7dO94;kFrld@p)xCJS?qmvZOp)jgQXu3&POBsr(IL8*cV%{q2sW z(-K7Mh>2(vRfKO~ug!I2*5fXmp>Eg|%f`UNmf1(t7H4_79Rhgn7P+i1x=Jz!MWJe< zx<1;bV9;iI5MEorx8_4qwgD7nXhO#jd;n|i)5{7^Ajs9z3*u4lNE<8EwT`+g%L^V@ zUL?;Ulw}heRX|t$g>m_J_^6{62v6>L32^>cCUuxLw4ayazn3^GV)F&r#9;9wVp47| z!xP$BeRx^RCOL!O2D3!y>QBpz<+u&^S90yp>9tuA|B$dhc!dW8PGGe>OPHu-f9Bzo zE#r6M^9<>zf!@&{}$1r$)w1-*~_;5~AT62*nHJ)7FW3Djdl>rx+zP-W$$H(X= z^WrP1JsURPNqk4nj+>!tr&&2j8$8|toh4QAfN*}HIC8t-UQ_88NDsH7!EtjF|DqO- zb)g|m>mo{MUd4`7C96C^(3$N=yeiK1ub32 z$W+=0V-jz}<7f_{10?XOVP5Xw38} zTVj{GOvKa~saAEi__Iv;rHgb@LY#*iuDI%oPCtN03kn?^VMhub%x^*QuQ3f&Z+B@a zM)5P8#pOCiGgr<%3=?Ex?^b>fC;JC5@5$aLvR+(Ho2m}2Mc$fh#PBu%<~;&K6I`hm z3HOMD|BG6l@DH_I2Z11T1uNkqkvXH?uWgX(~e(*u*{Fjj;n3FNRx>+ zb%zdz%$#Ooe?%6DfeZA8&=l?Xcozs^bpo;Gsjh*qy3n0k@RF|6w(rSLpb{t}52jiUFg%w0T`U*%7BU7C+gjM0UW!f$)9`|>NUonrSb4(@t`+`8Y* zu8b6F3|n;hT131E)1?=8^9dHu*aFN*iT#@YUY>&jNfY>)>=L8Y%TVZabm^#TGU1-C zZBm)ysk3(^l02G_(B?T3x40MV z8)YtNK_u@La|c|U><0m@h|Ajp(Wq@rlnX}^=Gj!32XH-XyZ6%HO5SX`5Q9SG1KZK) zT}%)6-bt%_@*asFw0JgNl!Z^NPdZj8Fhgilck2Clke(iX9}%j^xQ2C8Z4xXhJfYUW zctj6lDUCQc_QV}lp4X;nm(CM+nD3PPe@}lO_lpZF_TZ|?mn0bw=J6Nu36;z#Pb*`U z?70Ww*vWklwN3YxKenYhQzfVi1U$Y)8CS8gLUgvaPJI1 ztktnygQgQi#OJ@k$<@FDZ>@O2kHKmCvWuEew*Y0-4ika81((%iVu)+#Bvyk{(&Piu z{Jd6#mrhMfvObWW<~}|Ffvq~?te1h3%Gx4%MqX#1nb(&1#vYziOXd}?5HCTZ5!lt=#2R_hzbxiVBTxIZH4hdt3x z2Zg)Q&tqI4D9@K^UibRoXaV=Sb&4i@B1sr~{2Kfe$$SpSH`bKwy6-=tWIojVPRR%~ zHqIB*PZQZ0yQ2CSpCFi(}qdN<66AZp5K zM^(lO3<`Ez%dQPDZ%b%~F>>Iz_a;cB7e|<%$BLiq%bxr)r~SoO z;K>ul&-PVo2xg0pqc9U^q;;u6oy*V^TbTdS?q24yFc~RUxa-jlY_C{;v%~5+^Q;1e1+q8CH{v@ z8HJ~jxPM6eKRU4gkoeU(Oa-z=x+q40Xn5LkU}Pw_BLm;<4Hq#fz`$5zX;g!?|IE-R zm{@84v=M_n08Vj0K>%FKtUhHsd8%Ynl}X*nS5FGC7ut|0g<-h}5#lYVcr~afga*~QjnR5IG(vX1PIDflG9Ug4Qr;cKN==#w0oT14Dad0?kE1AD@ zF;Q?o!C!=mGGJ-^ZXWy_PGgq}`G?4F^=Oyn`JxC+mwXyDzRKS1@Uk|cRBhTpfhZla zm2{%89AI3)AqfPi1_1d;TCvoe28P)>5UrOffF4d|0;iyni;s%q)8N-U-hhG4fut4w zWD~F%q=e<~BnMjnQ!)&F-t9eEmj+y3|2U;yI^eu|ou=XA*9AI1SQ8Aq{pp!MSbCZA zZ$W%e1eerNU1e#>s^x1)vDeq{0h@>EHOr&?Yz7I{gI_6I42I+NI6Gs1$TV%-Xv!%) zSm0_JFRS3*iS^{an~#xOx#2xBo~EDU_W^$SL_y!(Nu8|E3pY4^t?Asg1UmDPP6m2L z)_6vBNbxjF0F$7ui%h(d>Dy^C0a78{9NxCakQgZY(ak$M;6b zac4mZ9dq9YzCk_Vl|}DyY3(?-jSB|h9E|nWVcn*F&*u-Uyxp9j0jk)<<;S-QvTqNb zDM9=8F5eiRZU4^dNafb9WWEeL+pti>atf#C*kMO16sGA>kD$jn5N}`0H8H@~(eA`M zk8j~pSEtk{MhPsPxVr0P%1UC-w1Q604rBPO^2MH{nva89fw08Ress|4eUMc6A2Jn* z^qjKMQp&HMxY={3^dmUh3IS&NI=^C{uev3sahFjXKYyiYLI)nA%O5>0opST$*oW#{ zgMtc^hvpr7il%!sxe*u@W4sNh~Y z$((XPw%3Bt^BW zw_SC@d0;kX9=$ZZG8U&W17?xH6{nD2+G*RexSZ$%+bk=&Q^!?A(pA}+y$i&S0ViH< zmvgS6j@;Oe(Kj7uF*AP{6#m3ZEME}*bgj_2AeWw5XJ^c9qsJ1JBNw-km>}>y+j06k ziknET3ebM1%(Px3wT7K)_S_n+HboE?<1z2gTHfaD$RkX<;zrLbMo)h~r`SC`T~;LN zh--Iv@uc!g1V2W>^}hT=Bjc-0EpzrvW)OD5pSn9(*TU&eFlpyR+UQG1yCJTE9TV+L zqgkH@r2LdqO$ zeF>6EIJ_YD_DcEa@sga^J>gdOD3&lN?CdoKZ1JNYh7$hx$yr80d`<8?AP&lAe1c2O z5bZP5_4R4q9g4u&>R>`~vtxkzP5zyoDF2$b>v~6ADpIN?Pr5ahWlwzUBYd%iW_ONTMmK}{Pp%S&XJ zzarfy-o!QWv*tzyp+3x9wCka z$1egV_WIp4&70h3z$O6N3H=9Q|8oKrHG<*{e5C;Q00ub_Gon zEyC89f=X|Jj0XAb`c;M1aNQ0pV`WV*#8P3tgV{m+BEcW9cb3)J2Uy4ENKmAygF#{S#+v0`;<>F zsC0?3=gs0_H$!mFrv#JCuOo3*{FTF9ThUEIxX8Eiz;ZsxNyULie5z5FaqN3c!(y1B zTG!cGS?SGMSW=^03gu9O2Qu?x26oc5HNAJ!|9|NQ#;dBPsEscWftp1*K;uNw)AwUB z-PZzdb+Fb^$XHU$dMM!Zm+<<7pybzj9a$ zev4s?EYu+Q-pKonnhLY!SkDH}{A3_|6TfyN3@~=Trx9be1@&ff-Zk`iiKJR3E zuSlSgZ6t2J{T)OII~LPTHJ#?$qvz6%M8HeiR80>zM_h!$oP_y%w`V7s5>Y3QTKjqd zCq8+fDJ*KlqAmMtb5LUwv(3T$Urzk4yfrw7$i~p0$OYSzRzIKMMpQJsk#FBWESdXs zzNY`ty)=o9kHUBqHcCcf-93pCQYv4sBRcf)SUkRCXrT(|)(6z>k*5qgteYwpVjv^C zwD!JK*X`3<{5`!^_beoT@(K?H(#m~3Q31-)=hK{!LeF$CQdN=;c$dnBrvgPnz?Ral z_{tC%$m(sGjH%o|`ucsC9WIcpglFX>Bu>AB^k80*5sX5pcWYZN4Rf*)+p2EAGnH-_v(_6Z(c zc3^1P!`srEA|PO^D_sg94J%3>7o#>U=@bqmJKEhA><$lUWii)Bz!VMp?$!-2j_(=I zt;nIhgn&jI0TrnSF$*A~Ym_}R)+EF4FW6sDtR@iH2{g{cn=2)m`#Mei3c!~DkJLs) zIhJpwuC$Y*4OCj>J%j?muLd+Ks~*&ou2;S@zE^tW?aPs)-4y1)>p>U`bxoTvJ}k)O z8wwxQVPlRCl+C4$M?H-LZWh^6K~~wbTb3Q64=mSI4>B8AzkBMQC_fsqxi!D{QRVz8 zp5d+aGFar=Maa&ITgp0&;82}lpbJqSd^E5!^xxqHuulSp-D`%-OdMsj;^K#$IeUsE zdEN<>aom-1nTj3m1Pio~)d=)7bzl;ULjH9C?2U@g`DomdO2esIY z#GagPt{bx^&qG$YZM#-grBg+)>HYUnTyJC?5}l9|JF-Eis;U-Vyj9Y?y9=@d;!sbg z_yCYf*Q?Zf6x5k8x;MDu@`3#9itcRjXp#N=K)&WfrpnJf=+ioy1|5|MIiwZYh9x|2 ze2P%mf2N4=Tnc=B+Y{|@{3Sm>!vhp22aQH>2Mb_M{+*NK$MN*PIyvAJCr7Z$-|_^~ zR{Bx9HtL_CHl7*$y--V{AokksT`PzGE=J|0SQUOor-7AYFO8I-ASFCqc^P1Ik*k!^ zgc7)>fbhQ|3YeS4DU3qAlzOngy1%5#0Ow4=+}93fkAjB4>gr!{?zUa4$M44fe46T? z#(x1`%93EWC?*PM6N9DD5|n>HsLK%5)v>n`wLzGhdnxC=_>#$DXmE%AQXE_>c4UIo5)EfR zPowsyfcguxv{1p4f-c~Qb`+2kC+~wZbADi%YeaUFYTo z^N+t_%Jckqm;t2PX~u)#-RCLr(ik%R_l&`>FKpz*e&gFzazD)u=huMrbDWu<37MbZ zO<^xov>^j6$Y8i|nO+b+1nS{|@{+^LX7k{>P5g<&Pm0_aSR%5e5TZ*iVw zsO{n50sW%B^HazLFZLGrn8FMTv#=%#8qt#(~`kgv(drlz0uRtC+n7D}QkaSd6y) z0^P<7-Q#0_f!$Ld{GA2YuG;LZD9*hrq4@bh0K=zyL1D)0!{*AQBZf$F@5GFSN>-gX z4`I;7tE_c8Xs*A(#p7z!jSd}d9ycEuS2!q*pUzct-@g(B7wA^-06B!6rb^(4-%9G7 zwv8yk+J@KKT7*9>s4RRI?Pk}q1G)Nk7W{i`T>c^*-xt^U#Q^t=aUNAvG}~WU-hH_& zMP^-H<+qjT!(S(8oUE>%QTbF~w}LX(RK|C({M5kh zC>r>9dC#=1xzSbqpCb6^FA-eHSmQxKvv(Xmt5Y6xtIX8T|M(a`CJCKksuwvudma+Y zItF@57Dj=FDhTWd0ZA!a-E!5&LXS#UqcfufIrz3HUU)=(!%SMT8;ykHk1A#@A3b=t z@cTv4DdkT(F2HVNO9Dt71P0cMq0J%D+>MSXwe95%m|UvplK5Nune~Wxv^8bEUIW%Q z@xsrZTgoxz7xv>q_UlerzHsI4dY>8B)s)~~Q|8;&Ta(T9$Q|CQZPd5DN%$Km~avp$k$|)M_BRC$2!&Z z-yK!Se-5}V=vc!T|NT^WRn^=%QT&7V(9+VkjjYzYvXh}*+wU!oGSc+Oh^7DyYT>wd z8h48GXFj{6*p+CwT*}0J4+jYpB!nd)vtov8pcqHLReYRQq`sy%{$pEb8<@ASl`Llw z%dLJTx&Z z<2J_e7PTJ@ibYx1Jk>k#FV1vrsJ5N*DBivcQ6e6+Ps1K`QFoh(CeW#vYE0?VuT11i zq9MIFj*0U}re!=xJMWV2DvfBLwFtcf%2jn8D5$8L>m}b3%A=!K)ZhfZb{W~XAUAG6 z*5N&Pkh;l=oFj*X%$;;ZJh$c=XfQ&d$~kazPrO}QKl3w(o)oW#$}6J_mD_V4`m46d zm!5ZvM{Z~!tonUtNe4k6lS%f{L%MhOPXoYy zq6a0JQhe?-2uyvx*et4TXw)&P;AuetuDh~(NifBZA+_4;g<-16-> z%pS(b3(IN>jNkdHK$}LFauoHt=kQyPM2e-tD;xt6Jd3aD$Dw<&OUAm{&C}UDO_AF_ zl4d=xe@XGT+;)>qxpFxV7_hhCZ@7vXv(_@~4tV(uO^jbi6kY>?dOfy8XN84!x1mWv z;0RL#-VHD4)$Q~9$Xf>-IW2xT$q6%=n6;(M(Ds@eeFQOZn1cJXb3}CWqW5IO2>(bU zciw0*cV2O4M^aPN=%}Y+l45M7vlY9Cb9Y*;3j2urBDJh16b%V8^OX1!jMS4B)!+YBq@1 z`6olT08!yp-MMRVJ=Pn0xET*G(7UmUzTi5J zp;qWf3Qp1w~C+j)wETkmg8IOF4b2tX8l<(!-`T!ROn)drytc86Z8{ zgQLgYv*@JuUUX5#2QaC<-(~j=O7s?nIKVuV7XW!LR~mXspm;QZ5-1L!0b9x9qC64; z9bfl?nfGp66&z1d?J5u^l(}wAC5|Dwk&$M1Hfp?EiZa4JrVX6~vskQTEfYVOpZ+{Y z>`J*V6m^o4AdZA@e2ouLls4R*F8>4)XxC&WumvCTjyP21>Zn({s@S+RsI4D5INZBh7-p#*@D^wwL(=fXZBo2-=fN38b->KqW`KLF@&-`NH zCc7U6ro5fyA~0+I$<;>b5!SQUnfCZ%ZKJSor&izt3chM7NEzXDF_^Ug9 zzi`eRA2R&rydU(BEx3AY0v6$qbyN~5iX>PgXf#`k+v($oM&W+KCH`RPllThRRk$%F zqYc)auIDn=vs3YpeeYE=|Fw0rHe^WNX8avg?LoS@P(sg?#ehlPqiIv+PbVOSg4KTC z}iVVF9p{(2C1y|Yu%gRtYjqq+=W4{0+Y@2h95xn7gz`ejkK5iQgcCd zShAb}MQo<54eXDPI&}(IDC&8oNXSsRqKL#=aZBqv_f?1#^lKPhX3Ln6yTXvLMa>YiSbFs4MiY0E7T9|hE3n5lxo}yT z=`*<%YdNBLpOwPIk6$7=`l?7%wzjfKxb{RNl4m{8C*FwV#!j{dHHCfN8f(Kmvo>6R zfDh-E+8^Y1=1fHpAs@G8UD~#6SwHZcCke#$!4MG~c~mn}pvwJ@XTtRudT2yI1$LA_ z7!oYWNynq-M?2BTrV~XQ>qMAdQ^#9Wvatl+H8+@yfgMaj9`|CI>on>8A~BCvDwFbb z!8ki_61Covk^oAyHo#ckA}-029DZ;n1-HKVmaKXNEk`Rt~`%n+afhv0hQC^xhL3oyimo^uM17;fw@!UbTHx? zU>=o#vKmU7?i2-M&S2(dvx+2*u0TcY4zkZIEu-oRZVy{a%UrNFg~VhW`Vl$2N^?b8 zYsB?c)WrDvkOHda2qs9cBZdWZrqR=9vSUhmi$SDrJ-`dN?knBqH6UA&&Q#LdgIE?| zmy;aQ+DD?6aDUoV>9Ah9C6qKP%o9%uuT#LU@J-U%yLkegBrBcNQWQ89E3mWQ&AO^~ z?gny>Qy*ly-f#Mww#)tSt@Srp%o;knamAn*?kX1d0wnqkR5m_xDch&zTlMe{T70R7<3OQS zGj81>BR;3q`^g*?7YLh?0@_dqE(Y#^tL^Xk3SkwCzl+;^I)@ zbnngaI#2_b=;b#Uv|AJ=+v^f!3^d&=5H_$^ZkX!F?0lM$vH_E2OmTj1O0#EoC?#T? z>N_LoI#T?y$fJfZ?LnF~d;h}RTaJA~6}dfAKRT8;>6o?MW3Mp2d3UVJ+|PE5P}b8o z4tV0BMl(OqTQ)|aHSNi{6<@v1^^bCTfXb7V;&IbceT$_{MF{ zE}YAf{0djGXB}7bIfo=4dzGB)R%Yl?db3Gf6_DI#*0aXgT6X>#<5bUNS1B^MQD4MUT5#X)a{6sji!EW_M2j_=z&wU;Ck2O)f4w?kqU>K-wsOelqMC&MTt3ules5#1C7NzwIdQh z_7gPfD`2^qwul8;z`oS{XPqSPPB{+- ztVTYkzjfOC5ZWW777q`9;}gW*?N6wESbRi8+H;l!OWd5FL^ADv^l0p(uC=SZ#t{rZ zdgoI_f^3Q{V`o8Fn*miKaAj|<&GF3YT&HBcuOxtR~F3NKBzM|Piy{qKnlcwL+W+MjJ)=t2&Rc{%2ctbi}z?!d+#38rCYtY68EfqFqLBCRFyoLF-tJer>2gEbU_ zp>S2j6*DGUv|rj|1FhKZQ1Faf?!~+ zNojq-`QT?HTk_Cg4@+k6&>Kg;vsSpH#g!K!)ewfpWr?1&WY-|u;6SQ?SX~5>qE1@_ z1On7YiP51@^#p1;os!yU9|i>QJckjYrN*0yfHM1Zww#e>$8+JX1sz#0`W-Ku#XSnq zX~KgUI)P|R6CXsuwwrOzK+M12g$IiE;4~VlIAK0_u zJ{K3>-Ln^DB)nr3{TOpS^884MD{da?0>aWhO0BwEQc@4~u(X*+#Dop|>|kec3#t?~ za;v`zGYJaryvVtXY)^#oitj#COIpBQ&4jIyz1Wimze>f`)(kg{D)Vl`qDNl;&_`dQ zaYnYwTb<#Q?F;~>pX!{Mv{!pds;Q zog5*I9R*Jz+hyI2a0?g3cULO9HNKY{Z{OP%KP!vB(qmsXn_i_L$mW>%ae%2YTProB zmc#m-Hj;;fQ#!YDTgDk#J`~dd2>Q7L7X)|%A|h?>K_#i(#vZGY*1Bvu zR-RK2u$2yBT}k_9hK3A3X}=)*?_L?2 zlPS-8%WH@7>--B$cr#3?n7{HO$JCC(6X}HAJP6P9Oqf*8f**xDTVVBETB*I%Sr@HnamSY+_I`6UmPgt0gbFzG_Tq`Q3EI_E9u zPYj%7fi2^?T>A!l>a$N0`ajubd@#}{>w8y9vZiN0T({)4POENGOcgvdY`mQYctR3E>PVPlzX{-J-ESKMu6kUaP=DVa$+5s8vX4w zBQrbu2uxqs_RYlfw0Ni_e?|oQ+oH5vP^d#Fuh9z6(oY@jP;Z`9mGRfBr6Rc#Yu&vS z-#XIt-@}cV436&t%;`^6CbmUoq~}Iu7;46`k^+N0QszABAqO}{`t9PTy>1$-npctH zJ*tqi`XR>kM{zUUgA5 zAUSMMlMn4=7Dt~c!vcCd4y`ymaGj%F{WOz7bO&TQvNFJV}6jhALkP*4Sg><>X0F$#g8Hctu z^PPSRUB8mo&ZUSh&aOMDA(ndT(I>F*$2I2j5LPBf^qYMmz*;xm=&i?7z1!OJTcTzH zIJh}2lkzircU#zW!0Jva-?Wx-e02si8H=njmVO@=9Q&SUDMpb7Z8G729ki~Wk!Ll4 z>*FNH&2%F+Hw*Afj0qYit#AtVH}O}D%|u^(q+CHHI5M2(Pqop%I=j^Vv)X9%n_t#m z&O%?unb^(*5Xkel?+ zUD99vf4ZdG0lR@5dl=3oKwAR>Hx!`gfIk|G;uE^^KQ8fFg9%uoFLO@z$jet)A+l^m z-vmD!1_iqQ>&C(DJlCM%R}O8l+AQ%JLPQ(+vQmQoqw0^k|CKW7%|3AZBNyzW^}$mb zG&*epA0^XB#wZ)mUaSFWT_L-#$9*Qpb(mtHEe8xf`HMgV7Ib%f5-Xm+W>`=t_feNEsF(NZLjG^=RS>vyhxLlmQ1pdNb`HU2JoI4=a%VAd5O}>kmy)$ zbI>EVN!fS4Ec{5GV_XbWNQXNP^KS*(O3~kr7v#QuWFvQog+@VA-U&dniH^R>AM)$wG^CH`ve(7~^l{yBF`kJKDv#UJrNWN&W3v9`i4 z1b6r3SkBk_`DF^No|FMog+tKz3?9VP)vw?DT2Ah#W;Rcw5nw3DO>hJy%z2U z5oUU7(nL$MK6+%^|6visS}N#7_uo2qe}_^ly(;S{R-WGKvB66>U3p`|hq8nVBsZsnR+Z zQ5zyj>XDe~y_lpc_k~#MGtUl&8Z0FOHyuXSt&kaxm0!uI2VI*bFz*f{M~x%KS{(cR zs7cOvd2)4H0(3mizT}+7R2RRt%Y2@**^5>V(M{Z}GV+4^3(!U@Uc;lk^hX`EE{Int zmL0%$f;wI-s3pLELZ{M1t5x1w7i;S(Ux|Urft#?>T?YhRHu0X&RO1j7c$*uuM@MSL zYv8tPCx(%d2yd~sR9JTcq4k=|?1ESnqRY<(j;%Dl^DOz(GWpT@qczc7NJ*-_pJ;dU29>X>Klc!;N8#@f*AMmOhsU7SWgT+u-sUe;TWWo zY`DJ9`Rg<&v4Ux@G6A6Yu=RNU+u9uLW^XVZixU>xLa_3E0TR5*FBzXD$k4`xjf*2BMBqiq6>xFqbG0j>P}1ydoX z*u8#u;)NI^!oo!H*EGas`@MyiK7iX?9yk^wZ$A@zcuXyes>bIF9Qy+Yfnn|zsffnq zO`8s4l-o~v*21*`w~?L}UTolkWJ6ulg;0np8u80`#ATO^I|71)ToXO$gyAhOM_UDc zTR{$vF|_b zadQF#ckYY!19??(<}W*wFL^H7#zbX^{DeJbw2_W}h7yq(~=` zhz)CB>->)Pl}p#EW8AhAN3>4lJ%xYbVf2cF$1~Pjd7NSb4Vf`w@5H-BDPn1IL2pME z6`#t*Q%g^p8V+M+doXi#7dZ0*y{{tzB4;MtI{()@p8Y$WZf1-Xp^!MiLorvnZkX|e zX{MG~M@1EOe#t?#UUAIfli=9t`e?9s9A+Yd#90oN&b&d6J+gVN(BzrC%{8!@Il{j? zMpemmgQ;+DPycO);-w2z@zm1^u@AGI2d3CeN3bClz*>J$2LXj@7$lwPGzDQ4MeQ>) zQ*3^6eu+Lr&;B!=Wki*YAyN0o^ff^&bRDjW&x6;JFw!od?791Z^er@CsnX!&gSTLC zlqo=T2i7EI!57AO_+a(gz+au8TAUrf~9q5 zv^JG8&__^B%eHnkh!ky&HEIMK8=>uL1+fz|#~MQlOimr_$bHkJn$_*QhbaRE$440K z1IJ{r=bEw#tpj5-Jo@VGwfAauxz5j%JwNjzt#Pz``$Y61=CeY+O z%W<_ut@@c1q*bW~amD^o_Kbfo0m`~OB1e9M;Br3#y(1KoKJex#v@#jJ>)}Mu5!6JW-i-JP0nD2in^xB0=c0V1Uzn6JQ6@r9_IFbSU<`Sy% z$hlx%05*eZDJma;loTYl3nNwk#sK=eoW}P#>R$|?!>*Y@T0cQ1BIqt%6mNbYP1Jds zX4>Ze*V>oBQ@wS6pRP)iSt0XrOc}~h$ei&Yk|9gk&Zn zPNw8WW^oK9^F06ekM4bX?!C|ZKKFh9K0X}I{_VZ?+QZpvueHA4$XPw(Bd}|D*K~~K z1mMs-dBAwxbrAmWGs2eMu2b;$QoyTvAjaf}2??9Jzn6~PC(kZx2ld<1={ydaC8$4H zD@dAHfD3>}b#wp1LDXLDqVvP{4Zinfb|Mk>vCp>P|J9f1*4)QTCzTMK%QYl*e%JmSjY4_ez7nQ@BC_pzq1I*zTu&{CHe;h*1E<1O zj%&8mybW!#%f3)Id{|5p4h995Ju1m|r0jD4Q%|!O-TGbT9@AM87lk%hzG^47=Zn1N zH_q6-{t$Ni-?AeCQuaTxBiEY$z<$1U{qJqa)NtSLzo+NjrscBuGt!ko-l#`uc`oo7 zX5Wgxnv)|g+s!iB>LDp#@-(=m07DKzvMFwqcDI-KXVIf;Oz zAR^+PI0g3^i=@OkXNPx*ElX-_X3m4}lcSs=tyE#FL+6@Kp1dB@G~VEt=f15SDL1Ej zNv(gu3Iq!Z>TuLpHZ5AWESus39tkaeHlKXW3VU5*9a3ifm~aEw$?)LBb@(RLYTLorZV3x`C+@V{o0ggK zjk8(i$(Jk(p$faH^SsRcFp3`Jy{zGSD)pS7(eKVJm3M)keYg6lcv+eG z;ee$&HaJMw6Hw-uaI#A~`_I$lJ#3WRD8}A`Pb*A}EVmMvA3T6DSqa!;EJ)2Hlmd%P z7A-d(@O9c1cis}CY4Tjop>wK9S5Pg5{^%kEVjs8dxvu%?DoIpF#6g{q60oiz+uD* za~L6IcR7rP$`WQ~Y^0)}Sigcg7F3Tq9&9S^~DT*{5G}c3}TGKp5io&_u#cek0qJ z89Zr1_x%OxwG04Cx&!h#15xq50a#Y1sdrCy-1%HPE_1_t;&EyjM6Ku;I%3*6wQ2+%HxST`7sEw(;uXn^uWjY*D zTrMJCbiCZ2q|Q;lxmf?q>moq6s!A^FIRv&;e7hw;lmK4j$rlpHyV;rd?o^XCECK%| z!YR*Bu-7#->~p?KG-=p!q(b|Nn8E7TavjJq$@pJZE7((w)SvFw{iG zXfvR~M!A@9l9E69h9&!)GGO)mTvc^Zb+z`WXuN{StqqVn_(z6ih=YB;Jx}$*wAQw{cv1~IiFe&obXWH^8n4CV7R;=ir92jJJI;V zd<6jXqWKMx#RF)M#M>r~_jEVxpRja~-o_O1<2Yj8m4PHxI}jI<`pD8vaRn_}0OHTj zPYzQ}uW`dnTN3Po1ygd_D4oqkbU-!!UlG0<+VqZ(gVx+5(4tmH1ZNx_)i60IuD2;} zb!^6ssIId?w7UC?pL$3aI4C-0=}-s}o5OVVEB_kc8&vv~^Jg@s2LO$!Uar_2*s{VN zsgF{X-;52}NYq9NZ3f~kEjb&;K-S#mJ!!@&E!SU@=h-ao3tK_;SKwH#v3RCxc{@^x zyV{8pw%lB}3xc)*#j{3VrZFnMK_LcbQ{hR~AB4rZ|=Yzz!oJ^sbF8=w9I9VX9Nx zRghp*)3iAUK%(R~G&e4|`rD~O%2mAU{}Fq-%QQ6hrIzQxrb80{-`LY*>j50J35YyS zPK6dM5pvL^=gWGDOI>}_S`;=fDq667vS$~1`fMHd2)pqr{}9N7NOH7;;^DSnMh1o+T&HKU5snAmKWM+&Y48=i20493Jq1hDXOv{Q1c9w3iI?;y=> zEsgU4uPPGeHSoQ>{YIrT{su>e!N(j)G5i7SkvouseK>!C{^jRT;Ga>a-6}ipe2yb6Gm2b8FFWcm z9e%>j66#w*#kV|aYk5R^JJf-BltX31APOXqC-;*A)mF|L)q<{TtUNoqW-g zze_Uwog#P79({iRJ%8~pM7rs4s53BEbq}gTg$p}4h;--&ZnP`7Ur>a4Ti2?83LX~P zY1+B?!@varu{*!xP5WMPn&XW#-t(=37}~V2JNO@-)Y)|Yp|RM0dS^IL_CJa_`(?H6 z2Qc*UeZUlMQAjtB+5)_Iu*>yBa$I6Iw(gaUSK(8EZlP9JNvhf~=5d2PplCIP#U%zU z(YSNLHHzR9K?(`30Q?GRy|`Ncl^`kE^hHJ?Ho_FQN_|e|JdZY+Zz#xj1(mbVaMlG? z8y>EtiBWt7q4tj}s`le;u+wgjf{drNQ0^A)aE1vKtT&6N@@>gY2uJC0HZ+9&$NMf zVXbC=L|y$6AzR0&Oz!H0INxffMI0wyT-pED-`@-uwN0naV93@#M$88`DJ}!$MaMBy6e%frL#KR&+tGG+j98koP+nHi>!ZaVi|vIzBzPvLc}T zH%v&dix~H3PTnavvVW(6fWm3jPPPIJqX)NiIwNu4bH$b};=_3bHYuOMxs}|+|Kv_R z3}_U4K&B1^eA-<&xuk>BJso))043Hot<63Wv zyn0o%;3g_kgaZDR-GBsS!)9igi?8_`z?s{_un2EAP))v1|RDK)oo3X38F0N+`j zzz?+JENc-s?u$=ieEMHzy*ox9a?Om^`@p#*V~lu5%u~`vPQ~N&$;=)tOxcWf`}q(&?wYEQ;NeMS#So z1q9^PG*>vJ?PjbNNeqer9;e01G;l`^kdpy5XiKsDCdv9m8$f7w#ACmZS8c%nLT+Cc zH7l3=HX7*07EV|5EKAM|Cs~^rOJATV$;-L}0Qdj_@yf-grS*U(Z*pGWK4l@}huK+^ zeZ81%-`Lqkr=jg=3@Vu_aF;GOn)DaCTmv-+!-nNpmG3=zz+U^=)9I=7_zy0lQGgFV z{kkH=w}90rjJLef2e6E`fcuf3tWSS|J?j%Jp<$F~Z5w`PsYwSce>>cF*B!sBxrXCT z4Lgp-Ty?LyoCa+@x+ESj8@(NP-gBf@2{P$A>v~6C)?A ztsfW}k)fDeH0Q>w>f=kjYcb!<}?lAlG_E)3F4O~5MHYRv(!&ezDAoq3?#@R_4nZEUI zlV}(!-rME3uywzVn(sB>{O)i2yuDR5g(BLNqo@uP9U>SD11}O3hW#*D`%D3|P&Zlgd#1owje_E4I?|_Bkub-ni6vfo z6oAQx4REyscVNr`WP~QmAw$#kV8$4Vg9C`5W|Td7fESQ|Ybs{}Qly(@ae zVu&daT<8>PLNe`pk(#eIHkjF1RUC-l=4V-$K+MfCIJ40cIJ;YbyWP}K)$XD1_7lC< zLM`h={_#c=J-U`M*lkewl@$as;iu6l83guKz?jLv2U9$xrkLSn3nWdf~&m_>A~yskaf z!Mu>VZeD#gFNbut?!@|Ax$klS1pFSO44NmzLHK@249!YZ;I-w^jsCUbmA~R47)zeyhCe`1PAFP=zzH|f$emBOLx7!Tc@Z&D%>Gb{>j72U}pZ@U4 zM$t>}KfS`Si%hOjw}ra+^)hV@3I?Fo{J79*m_HnDZh%pBEKi$>jA~TKtn>j00G5#y zcS!SI_bfIE=oW&(T8}3qF01u&M@xBivB52kw&8~zU$UA{4XnAan9UHpUJV;e&WOV zcgtiqNwJ&r?_6!Y`LVvvEbCw79(_)D7HONF$o=-UF6HIN*wMxy7#CD$vD7}#Om3B0 z$Ji|Q;W*2xJogJYzPL*mWiY4v)vtVXnprjzDbO4qE6?yu4#Sn0r>WR+-?&50c| zzzeMaHSOa>k9~FG42VWWOh7U>x>#Mjz{WqYKzazW5DiW8#Xm|iCnvWwXvPyx{6?=s!b(T8R)$dh~S`#1oA}(6HBY?-HbE$b8Fu%>_a54ec(!BSz z$ze?!OjpP;dx-VwOKq!LkvVCX<L-ZBs#>%}_Wd%q7auQ$%2)ZPVc6M`r4-8ZG2g!_ z%b6`(u67d~AQ_8a)kE%cLWKzy3RKFj_%PD4d?#yZFu}>Ln5?= zmiUHT)^lC3{Fpr6%>>bHyq{ejKd?SE+)I{OI3Khv^>@O{nHZC5Vf+b zo+bd@q4JuWx0-D4u<2%={^j-}X-&TBaJBc+sWp+<{%tPF0iVA@sc&{fo{&@9moE3gh0ku{$LIx5U6lcrTb~ReTYYI0BNiGG(2QK8sY0W?ra19f$Rot z`=g4D;iivd8>cYr)>_KHEB*YP5OvU+CG&FFLz=GJF2SDY@^Jx!242Ie+zYXrKThs#_aw-Kn@l=x0Gqk4wWMhGAkzlyL9JzyrdZ@HEf|ufd{9 z)xY`%%(HOJT(;si|}$~KN2^Eiq?IK$HsR!K?!F9^i7z{^~vK>6(8)1c^Ks^PZ5d{?14O9viJ~pR}SL*PO*$zf0!l`(7(BT;my67G; zzYn~cfft4{(i6A!8Wxe7NI@_`fXA!2A-cUe1ov3BMH(pu`u<~{?y8fPSpI;A2rC2w zFEAvbZ?~%Jnx-p8b_m2Tgo+$5CDb!mH}D!jmhwQL$J9g*SOLoOojvaG>Ow(z;iQlI zKo5b&w7^@B0tpo=s08Xg1!sd9rl2;1hS#lsW0}g*U06z5c>a6^`$rmR_+`v71cJ5l z7qH{dA`rR$bf813~is2ZY>`4ZnvB8fZyVQ$d56?n zPcrE0d2a}hRGRwr4tU)((h$KchsA@pLFIXrH1P?%WDX+yDV1H9OpLd`tgy>DY<;bd z$lDGCP5F7x4f|l_>@*v+Q^ffrNXuH#&U{yO(X_00>~T2+!driZU8dQq(3Fs33{O#I zSFmkqrL4+9UG?w|KurwZ`(>XEe`s((DTS-eFZ|pcH0STcEFAqxrFI$nT2BFc z25QL>DlMtd7+71>dN7qs-=zQe!%g=->2j{57hnb>4%$g^O?DC(~DKC&+sYI4$uN?e9(L#_(nv>!IvlVO@hXW7?%f3*BgBFou4T)f> z2tW_P|KtcM0`a5s-_~2wx}}c}NL(A%>OuS8|CAC<4LnQz|TkAJQQH7zBO#yZd4jnd3`G<);Zi|mZf z*B)~TuQRrfca9==;39jwjR%*LqA(2(|Y1)!g>h znJ3nyOWMAK)@yMZD&&-`oDcn}*NJtIR?c@mPVT^I`P~k4&G-rx~d**{Pags=QOWo^GMs^T&_=-PdnMo zOgu_0U%7iCs24W_IHUsu1L9^}Vn$o-}k3@J5 z287=gwQwKpEwzzPGV1QA;c9rERT|eLT?TMx#m`!9#Vll0DXP0-&8olSkHwLq+cH-V zN$!e5(r{(Xy236eRsG^&YBYpSDm`$s_yf~md%Mcuj)iJh?jTFyYlqR3x}0%i;rvXm z>}Pz7f&}>F_#;mUnc6nvYS^@kP)gD5fe|A6Jum1A^S@Be?leg?&HHMjCvYB@j;T!) z>ctU~6ROvxT1nHa3di-&X4)SR8~*?<;rNC5<0>`-iJv+WC|TAuoAF*7H|xK-lP12Y zR6l=6ck+0sM)5Uimv4fl8~Zu(R!haj&*>zi)O?QT#bb`fV|F`mOV5-HZQye`BqW57 zQ!2G!;qE2Hcz3G(Z67;r`-(sy(end6u?NXR&xGC!KO#1-G#jh&L}ZX0Os4^j#aPUh z2IHQdGo7d2gK&F{#f=w249k@Im>iLWyz2~GEsQe*xabsFZ*f$2ZKv)lb@T0^cDnUB ziKFdYk!w8^$^0DRP+H|}{-CwF4x8ciID3gYA1ZeFJK_;$*0mYKJVHe7gBq@z#aSk3 zYk25Y+4NOoNfgP&mwl8*7!_4jM4vsn_L?IwF!VlD?AKUgf3ZdALJ3GcQRVLLsR_>h zp2{!AJ2>&tj9U$AS_G5-1ZR$sO6ij#(`Tp|S?5Et-31S~HdSPORtmF1Pz-Hpc{G;e zBw-YzB`=PI1lKbO&;k?q>-<ms`>r~X>tKG5Yx-zS^LyV~_bYx9P6Mih{BU}< zRE~K?^CwFI!&0v-TD&6hAB=SIUyr4VTy4Wp&Xs zA-vAZ&=7Uk`4vMu^f^m(JQ=)4yhL?Dm$O7qs@;ExH0G7yRBfL_$C`;J)8D13TzG}M zr6EhS?0lStu@&}$R=zWgFiT)N@~k^=SG5n&;3Ypn_tFMu6Zc;+;-$CpKEp_VBKTTP zDZs&aiJSP!ienA9C&QX*YOy^r*C7H`T#i8XmfyZal)PXjX%C39EfXox+FNqT*7Qk^ zT}+L8dy75=+Tdi|2(Qjjv;bcShX7Bi4RKzdG&Ib)-6mUE`CEH88i4I*nzcB7ND!>| zi6!}DF0hs_hq3jhrLkpJnCTZep{t%hLyeH`KL3m|%D3w;$vCF?Zq7MOF+11$XuKdp z+^Qw7c7)tm?$x|ccn^kZ@35lBuLKF>Q{_$%#QgHFLlq9AMnxDkJs+i*=mD^OjGSLj zsVB!@%^RdI9duv$u#{X3rX&MJJc-4-q*&A7%`q2?OZ;y_G{)-zdqlzpyN!tUp^ksC>moBvuDxdtq(VmyOS=NV5oln*QS!F zDjRiIoKD{Ntv75Zkwja0()$fd+Y%860A?Z@5sFnmL_j| z9=mQ62TbkJ`TLz&(r{%AxN$3I94_1ihhwaft@k-sx=`7x_UuVQ}dVM5Gk%TD){hEU@=ouS7=p_;;lZc9z{CGHE*g( zfw&~7KRn1odr8nrZ9a{pZmr{mDhH>$#_>zCP%@n}pAKq9I+`)OjzSOVd1(t(`U0G} zfk~GzV#Z5LEkRH8Q$Q87@nI9`9gEeqUrWnE3#s7b$RZg3w#g`RKly_4{B=T#-O4~+myiYO{!{^KK@s7f*`O1Y zEHM+5aN1sc=v|KSm(O!CU%nMh4)&E^lVTCRUPX&d%-rWvetA^APl^+As{#ip=VwMa zq4PBs;RjiyeKUN^3?1#>_erS^OjmB$DpagX<8AW5sc4YcVbf-LyN<4mk@J;ypU^Y| zZu5Y2FQ^2VYhBH(x%S;MXdPS0l=VS{cqm}-RwXXXeFz^k+F5ky%z*!rC<3x=Q3L3(Q3Kl|l+ z$B3UVC{SYNGkYEyU+ks1E6}#H5D413WA)_B(~ifNu6^*+No>a|@#RV~OzX361p+~a z_OIQLEXk%<^Mk;@^LpnnHi1 zV|c?IOxfF;=VLx))vChAdc)USt*`was(F1>o-TFkY)1_H*z!bS>B*~Ou2&M9T=;vA ztqLob<7hBrqK)YLLHa6L^aJlE97Z>|wBl@KC$XY*8^hYtfJ?o3I z`{reCVtHKhS;j{bHu|WDKo-&W2fpQQnGn74L0d0;7O`>I?_Y$Q5e2F4V|m`|$B12| zB}32C{V&G*y*@Qf@fY*OzVG|6Wbh=QqKp4{N_yYjo~$*u*Z*CLdf+m zzx5hfa>n$I*m=L**t$KQn*A_%pt`sIl$~AO)_{#z_l5<}hFQzvP;%12!7s*Y^IG0q z)(=iKMwPDaYb0AtT^E#X5_~cKXo;d~i2dYqXVn|l;{E2WKIFokW|3A$huYyTD4iSjWu6-dWI^IUyo3b79r(4C=%NJyrHxFWiv*UkjVyW0=$BMBbGx6}=RwGRZjDAT^YHMc%${ zVB*lsx45R0y2mB}R*jW0Nw(-pOi#eUjo1lE8c0V}qkbMmchuE}?zRD~X-wLI@gf!6 zG=G(jp5b8}59?Rs#*H-N=Y)<9Y`Ed1c>alDLz)5K@0M5#mk_L1!HLGEPJUuv%<+!e z{to9>uI^qiBT~{iND9*3T|>hV zXYs!Ov(Mh=+SfVf)A7r54bD7k=2^eE@B4SJ2zAw0MEH;J(a_L{6y#-JqoH9?prN5h z;Nbv&8L=%|LqiKFRFH*g!O(Zd`O~pAAUI1^U6l?qQ?>tUrfUQ@esNYOYCkkn5 zP`FO$R{rw)+?JN)gpP)$H$ACMA?FGOzw!Xo<^h~jN7{@` z%>|hr;!hu?4Q(+oFhD{052?v4es}vk_76#AGFz#cThy`!u7M+C`aYp#xwb6Q2=V4*_#RcpDOHS7*r>LQkVF7mqiT*oJ6WA^t>!Dg0F2 z8Xmzfe9PcLkQ_F4f1=1AU#bwSW#t6AWj?dPUMH*im&ZerM%dVVP3DI0qHE?NK%Z$_ z+7>x8Y7UZd<8l;yy|b}oSruPG(^fxeUDELIRY}=yQ&+YKqiaMc%5xbuznmhCNPA{^ zCO!V#@6iU_(r$IJQgia|iEa9oSE+JV)t~R#9CpDkRg8kPlrvV`d=rQ_`ZH?W9x$-e zn#Q>jdKDxtCEhBX1^;@t_F}<2MI}CA`yuU2{0EyTaSZU;iy<=<*~(&NVh$N`qrsRB zF^%|^iU~2Ck#{5^cHh{=FRS5Za)q^DW~01sXQDhyR?a|Ih)?|4U>WPoix&m9S7K8Y zmR}9{pyCchq4Cbk{a#a*=Zh8I2~@vdh?i@E`+TGdW?esl|G}1KigRj1B51yv*Z}9wX%V%5rT}3rA zIs2qo$UaH#Oms6$E&DrcgU2NQz|2Cqr$=`oSDeMze#$g1dio5`#!y2m9gm5x zT#8KIQHi}$%Z|6^*#=YmlBoJ@=r~_r-aV9>Tsp;W0nhsxu=ZAW{!?$aU-9AGu20xcv+g}MBFp}` zuNnQiA$7CzG(J&c3Wv@?F|?VdId|od_`%Ja@J=_(&V2F zq!dF9CVdG??#Z(by9v7G#=lYL2Wtl{ck32sdg7fY_=V3M{4gC!fS;N5lMnJzx$EbY zE#^o`NZ(t-cRv#k{#0uIK(&$@Wmm!Kr!}P-ns1>BgWOuD3@#jG8R_}Eu0CSPDl{u4 zb3aw~X9L)w!wSzh80VW+z0?F97fOArsGe#D{JmUD)D~&_td$g(^}<^BHXbMqz1ds` z@r$7`kMEko;*u7_hkEh{`~2tP$VuCbVk{oR^S;@p|A@;Je!7=sM3$8xr|&SmEEe2d zP;^u?htmE*6seE^GOKi5>;buRfzmFMH~J~D(@B*MZex}~OgujGe#v>DIlJJ=F5)L-Q3tuiNw9z2kcJYVwJ%{I;$Oxu_38cLK# zM`@qi|QpGHQR#9^dk?Nj8t4sd}N#N`aaLy}ei=InxA3 zr5bDUIg}9+HovDNzaYeEX?wo4wo|M1gPub*QeOQuEoAM9Sw*na<#cJokGEsqmXbai zoG22yl%6ljz3E5apJ<4VYpln$gZDPBcyA3VA?AbQ>#n9nvF zo8^bIQ@j5uxrVqpqNu_(o$BTn-G(4m*KlI2J0AYy#2bRL+rng>rX&l2HH@bAmq+Dr$W89K9k+uF!jGwaU7xD4e zr&I7Fj&b0$*v#?U7PY=&$#rSjceiFIn)w&rRO(9u$>4gVnes8)+H25I1J1}`}Of6c?{deD`Y!1D)i^|8eOvzb9VY(6P+DZQ?IqJ z*~R)Ni+!7zz=Z}5yr$?gr8x%EM8#S{j4JG5>+O~S2o|3BSuf9@uPCa%xu$;={#CwQ zWMka**2-#CiPqit6v%kB#C9&5t<58L_QE;8r^6dKUv}nzaD4y%TsRU`6d|WvZu%rW z5g}S}-6kp#w4@ZDpvfG>W5O5nwU?P-UoWL}ivH`63C(!+K*z3%sYsyf)~7UkkIwN? zf#(WlYg+cYV030K#0y#Ul|O4SGagfkmmKzSnN2Xcai=M>xpI`t4@r0$81dpMd&NUc z3zyT4ByCRWw%3N@Mkqb2rB@sh;uAnXwX4X&q|<*~LW|S+bO&g$5zHE2yNv`bSp}8I zxI#?rQgfxgn9+&5C;Kt0`k%Qw82+mE}QECifjLHTyC`cqqE5syzHlR~+7#y5oZ z@@z(ut1MQRdT&l9%f7ijacGLHzw1GgI`uM#3i;VUkt?0*O6fDAe(AZJ+tt5K0&dnW zHrXq~w@3^5Ny!^UN0IsMB;uAWro0v1gD-AUKg)w6RkeIrfpYfcW~IY?%xE*{mD<#r zO4ggGA?`9Q6XxFIqP@pk)!#no_%BwC8a&!hbG?e7LjnjtpG3117|#)Vx2nw(&)4 z6U?(gzpznq3i5UfZ9*~;Nktc%`hP2&o^R0)h27m5VXrgz*~F+&MTYSdx&~f>%e z&H2Nu%&t3?R(~o#KZEJ%DGTX^aB?iBo0Sd2@2^7HLJ=D4G|L-?b;fjqbd`EI!*KE? z#4Y$bA3sRd!-QQz1XS^gC4^FwrVBkT*xT3s)5A9iZ*$miO3>$2u&sioL718k`!9!^ zvO8kzO0hG<32S?2Y*p_&xFUdKKY5Vhso2z+lIEw2y&9p@)d+k!48)N9e0r>5XJ>6k=;&vmF|wov znmr-V8+FC{-Nrt7y70kwJIAC6r-gcad~?#jI#`se;zq6f=wBTn>jF6p*CD zF_a+W2R`RySkTP?Sz`JC5&QvY9&Wl`Wqh_m&)$t|F!JY_q52_Yv%&M=t+VsTS2AAd z7Lz#2LOkZA$C5#90H7$cLX);g6#pr}Y)L7vteAWg%Od;K%YLRh?yKT$QYC~?KV&Vz zNGtG0CDBL^shhDfMMg^a9Y(L5P5u^;{1FXwea$(a^B4(dgOH z(4gpOC-m$O9#o^DMJwp4cUFHzL$hG`_doFP(WKPT(EhW)G0?yORsDDQ@b3=)S^l%p zf0qBe-w!VmMM4|x1O~1{O!MM-GZgK^=gCIPoeLavG`RWS^`|o2|7`i;^M{E8i9*Zb ziLcr_f?q?s*LBVYe@RAu@j*Flol)Amge|tsJq6Fs8Nm%cN*!Wjw9RFKx$1W24?fd2 zpEjuZWz$-vDkP&d_}n0G{)~zL)ix4eHFO``l2}-SP=BrUK&WP3vHDGXn`wP4=KE<+ zl0qgl&VzrW|89NdVk2&CwwfBd1tsRWPg(y&VEdPD`NF476hbX~-7|aFO7c&b)Q#)?6hZ-j5cSIN^Dq&^ZP41{V`10ZKO2G&ZPL*w`_rBxsO&x;d zvB-Mw_3P7oKCv}`Iq%`)JDC}_q=&Wdccna_F!byys%Bg^J37%XbB%f7I9eBBwot%1 z(Rc_$9#J}c{0t_ekoF~jLaB%3Hujs0Itrf}X8mM!+zXAlog6gsD)V?_afeHtudo?( zV?AVY(p~Ml_UQJwPp6ESb2)SNsblA+i{tMfNoBqT+m5TLptem{W5Q<}_ERLNh8o|a-y9gLa@4IS_z{FU|%%7Xr)rC9L zRdEgaFwqb@Rr}G0$KUxkgxQKW)Udj)OIWx|U0zJ|&&m?6-r&X=-UZ6=g&GpzkYYmC zkG>uyO%d~Y_V+cM(-}6X&gmtQJLcu61!J!`PUkt85B09FaWlD995m=Oy>uLyCIVq$ z8wVuedE9j9W6UU;VN&a;(_?WbL=TAY{ z`6NSn)dQZD^W>9em)jqd#Yi8#oVDAxgb;HF|MFRxnvjp5FLvmm8o7!po~S|bg=W{i z7Z~jg7d8jIGaHYd+mK4!UHsN`lC5!E*t+|6{3=cIL?ojI3mRAn0a8E`ahy`#PUlG| z+?{=Jy7@Q9U(f7nB|PvM9Z?^Oq~-E#YzNz)%ig5Xx2xRzVD@R=z=b{~8=)w@FK_gT zT0m4vDLr$mhvEbc!A2K-rMT0QxsdH{r`na{Wn~MSTFxbMD81o>2*Jy82Sl0UZt!o3 zx1GKx12mo2%#;-N7OlJq5n>d(uR7^J*9lc3MBR7Rx>YlmW9ZL|%&Vti0ulg$%Whe0 zW+0Qsi#VPZnep7;4~?PO$rkfbz35sW+?VV1Q_Xx9aRH+16&MGvVxsMxk1>yJ!7Hb`K+ zk#$u9%RqkXLE{@NMU6o`ToRF^EXFSFW0ZPBUh3#x1{&9k{c(q+2+mST2Lu zSeW67KbP+BId=BdIv~9R9y7CB^=F?)xJbC}${IrXB<=W$`Ry=W90{92_?>biopRAu z<IljH=8Woua|CMA1a1 zT3oW`3j>nQJ=0R}&{m`kY0=x8^^l$0;@O{D^Ivm8%}hJL4yNwu_Cl5tzYaaq>9*$e zPY|@g#xC}aHeIbUo53H$u?(lVXF5z`Oe4F~0Dxl|Kq94`yvxUkro7L-UC>v?qD>s|!k;{9i295Fl{sepW;Sw>K0I|6wga};;~mwd zqzJO3r5%)^GsIoWgq<_m1@qXV;|M*MY3RNio|~?X{KYO5Px%b7(|bvZBT~y93?cZw(`sr@Fut z5PrJe2!Z7fq?Jh`YR?XX@kMZ9xV)*Pm=P`;AhhnNuWvh3k@3D*(TsV7<*yKB;tvgE z)fY+IV2nTX^byS5t|W)9&XT-t=D! z2><>S0CMh^e>eEw_W5^%;QwVGP^6^zfAAE~(s|b@JY#=BQrFpOzB&IXu;sNEcGn7< z+T3J>rXdg@`;wOaF50%Er_#$EPIotsSY6*L$>)MO{>u1tZQJXzl}0qUm4DN^^0`|Q zN@=k4iD*V`A~h%2o01w}mywdMfG{+1$d1hsyN4{4C?5mQ>+SUCPLHR7w&R{;H|i1= zOT$@Rx$Zc(Ki_oM>LGnWG_Gp53*a#*%17@02rgudijA2jnmi>vZ)v;bIZS@q`w~sV-&U&M5V&_hd4F3{ z1>K0d)00dU8RF!WbYU{;m9|fvMvqBoh&g|6RjvQpuKsIy*(+dp^Kxsyr;_}3a(qwJ z50z3RU^MpTw2|I4V6gCMnPU*^xTo|za=X2>5OHEpDbw2+C zc6p6Ak7ZCcH*-6xzDKgtIHXavQVa#`@CJI4v%#Q8ExWu?&-hF>w^rykktwlk{`R&?`{-L8i0MgZM@EHGU>v&ac3|35AL}ksZ@jDhU7x3)g*Tg9{EF8~vK)A)#|F2dt4++`he8%-7=c5yV!oq3 zO1)a&+C{d6_Uj%}O}xKefk96MMuBPs1I_?v58+4AgOBe7AJcwJ6tx9%K*<{%b z_`B}lPur(c(ye#t=j&0TlwA6r=_RMrR>MmmsD~?T2>7iyK=_2j-LpE#>TMzXmlalX zp)g@z)Jtft&(UtZ4{EZZv5_4e|KeM7Cq(j(Eo`^ zn)dt6uRyKhcKgH2!!-^7g(Zsstg@tIi`h~Yyoh?eI+#9~oMvaTn(gJ$%H{ksI*45v zL%3jYH-J%ZA6+>1#Z9#Gt*88H{$vx+4KK6qI+6KmYZI)8?FlP%Gk)M? z1=NNcULQqwB-+xkeLy*o?pWA3&OqH_QS9vm->!uHuwS8UWjAM5)`FB|>d#r!*-&Ma zboGI8AS2;z!5&$^7zm|No!|K(Vl||E(L9WVBL=meBJ~EmdH#*ogIS((B(%T2^IQ42 z1Zp(CzbvAnz$0kxu;x94DV7=Y9KtR8FVsT*+y;dq>-4D20{LFzrKR$c2A?w}e4F}i zQ11NLj~|!8w7n95{bap(dFV{BdeF|Lwn^lUBe{4#kzJ;SOR%f{v-r`~+LlzT7a;ZVDw;y52_jx7i1L{rC5N9CK8J z!zqz)Ii)640{~9TEN5aNjIrnn;lUKVI3?*pX6^aBF1>_KpM1^+xE!PDNTCo6-4~qx zol#zBV8VFj&cnEAO)OYj0f=CGdG<)Q?V#x&q zXw^5>?5r{5Cs8{#^KDwC>O-C5y7s9-2ybT;c}Nnx)Thcit(88?F8uE;Wj4{8NN`P>ejJ-2!VuGY3llt!a75865q5UpUcC1(Cf z(gW!m-lbJvsmMS6ss_ITY$I9Vc-^O4nIW zip$E5XMOlSTzu<;OBZ`p0^0#2Uh=NHh?VkW&MpX#kQ0;^PD(&ku`Sf`TQQ5Wk>%9v z5BSATFT#W*NzQkTj3v#aBFQU(6M>WB4&AX}^Nb`(oaD+31U!lV5W+7(Nx}9dhe8!;8n+m~*CPaC8fB1-=vnE)_J9XXNlUaCn&Rt zs)@!#g2f&}gv%_CYaR!Koxdh>z4J?}Yx`(c(c?nT_gCjnI9IHhJfC+XZjc0*J-KH; zbL!yG8~b8+F`U)3)X60jo}dz~Wj3;Tu@Y}TpgTP5i`pGDSP)O{P}r#x6@-BE@#h>r zxVmlp`0j5mCr&oAfs(hS`;b?i$pczGv^cz#=*Eq}Yp_4bgEDE;cur{Z%d;yQ6io|u zC;B-q0|H>_hf}v+w!vx{vzGaaq{OB(W4g$dkO7- z>5{7s#e=A@NaEJYKj8x-DDqhTxTt%i6JAbI{6^=}q)M%kWR)tFx+Qh7-ZaE<|MuXa zG#AWHaqk)y1$#b0tMNek9#5()+F;UFUSr0jy?V_QSL~1YSbJpDa(@^8KftL&LAm+t zw5$9?bL0=ov+$i9TmWW~QBW*OQyk*R&9BSf5Y8uDKE>`j)L0SbIgq^g;@A_n_T-I9 z#9Q@Z_|+o?NyH4NS4Kf~P77>n0jK_bm+iEvwt|=F+k|7*(gI``oy!{KbYpTRsx?Ef z;e4M;gt$@e6RP2fkGKPHB@|v7PcO)rCrd3|YputFFxem*TQhb$7REC&CVUyyF z;%Ek5elkwE?AP0{h@T_S`|27}4PcnaM#^}L(U{MI*j;(w9tU28v$3kQ$(4HR z^l(DNqrdwJF~D%yQ{_qtIjbFMF(U2tn{bPl_{>j)yQAj02+5t-N6>kWu!x07jZ8yxbK#%?!&ib%nk z?EAhHKX2u|RR+q3i5s^`FKyF&nFd=A#ce^zr$A$dG_*e<<2j`yu#HJk z+@WKg#BgTFQlD-)s?w;@m*w@VpzVU695Sj&Dvcy#J6{-7zxgV1I1visWK#OO4EEK3 zBa1|^J)>h)2Kc9I6N>1qXsdWDdC)6#Gek209hT5@rJpzilN1ay$yMVaDhB-bNoFe5 z(|xz2r|eUn0J*f9+p1@F!q`1Wm{EDRXr|lbG0g^F1{xVemy8v&q!pL=p+*Vvqk50+ zBa5v0qQ@`Od2vDoU(frtG`8yQADyoGkW41K+;h2t>)ONP0&Jvf4eYuTvn zV}>;vR0bAOp#pS@Dc#KDWjO{4((hwX+8`FWnyo(}JJHNK&rnlv$kqja#Gk?AlBHbAN&Q2XP1>ixVe}7W0vEQjXSKjYApM0;r++w9q{kiP z))`Rnjj*f%aJ?y&{!Hz{6u_*G08^yt@PRLaeuw2F)FLS=7_Uu3KV@P_Guy|}@H4E+ zNbM)@(UljgyaUv`Fx+5urMffVjf~I0yx}ZoK!A(-&p9j+No~^C!F7Xu!?GHV+FfeC z8}|xse0Az1qQz@xqR9~Ogs!He=OS0P_)V{Oqp-_K{fg7Sf~9@MKYfY_WFB{N@t?h+ z7IHe}+{*~5RVe8!&E^fm2vQr#ef;X3p6^1m86!ZcNS{ejq}H{XCgey_*S3T_Kg|9} zF0~LW%Uc(WyZlI9whhpqWh7VFRHC9o4s=W2^gXeC^V?C33r6;i>bw!gUiY98geTBQ z9{e0HL=O-C5&yVXEZzphvW`Y5A^69>vBkAC6sZmNuY1>_(GHpI;f z6a4t9t-0wE_Z=0g(U1)u=*6^Q?rH4LOc#PotZg$qmQ_Bx%5>fxy^CRc_A-0EqEjR2 zPtTX{{FB{L7I9JC(#<#rEv6{Z|EmN1k4Er62=)Kq)&E7z;pudZmq!_~W!laFY->l8G<3z1DX!ZU@uxQ!}b z?B@4OsjYFMY9q@4o@Pl*Yn;b-xZVdahAE(P0T|)5jo0E^u0c6rk;j`1i-j(Tm!*(^ zYzZEfVUzKP&llX?@ z3zKnAIliIUBG|V>NR^-9>5XYtS^PfIF4KK!wTd$GJ^|(7G03)Yb&CQJ_kM(OI0b}O z!_yg$D|53o&Iu{55Wmczt4E1n2TnrzzCC+WV_vN$1~^yB)~}_eobcWF1dErTj}v<~ zN=>I0m-26QcUEGG-xSj_GQs%S*F!wv&sO`{+_eCjW@%BF44P1eKDSMmEsH>P>J^O@ z9rTsn9V1+G@n!&-?m}W79AHuxw~Gkkd$%cHBNL*zA6NZ-_=oH1n;Jkl@#SM?=N+X$ zi~2N1``n&c`}x#Mb@k0iX(OwOL)OD4b%*x=FPs0M`8;auGM5Qaa#g6^am@vh5kvd5 zQb;2r_p{_;Y}bq627ud9ez~Op_MQbwD@QM7Sy1JnIDS}`gb!^#^%=tlFa``8jBO}6 zCdzjh?0b>oR`RB5kRWddTZkiH)db=dtFas1`^5zji&u$Ze9z(k4E)NRU93)(uyX{{ z04xl#LiUhf+xUBlVn#i`*@wEkM>b1)qTz%RREqiJFBrsRvGtnIclN~wqYj~57i62h zjtjRzhPdZ(f^I?TvKN1Vz(KBk{QLn>ChE$>QK8y`q5I2goKSW9x#mO%)1(-)LJaoq z)RB2D&gvD&YjRSKjwd-Z*nQ-mBiUzSMhxn&F~hD?0Uv*Q^6^d+ys;Qc~W7*MN2E$||NFIq|zOmir$jzN>( zV~tUgdEa7o`AFw^ov^um8?ovxkZ=A_t*{*Z#$NL7DD$YxrwG z=P|$A;5BdadZ}o|V^>bFE$rdpZf9&5o3g!_!;ANA1m*~^TsV^$U--jLhIzbp>fMgN z@Qanq*`3#yMxcBBh<6Arj^)R+70Y8%3aiG&sbi{eJTlhbuXvd*8oGm}7}#2Mwo6Wa z&(Pn&{pwDr1s&Qvn1$%sJfp+|Nxeqe&r~v>bxf~5Po?UVX2-)OrzVBMUj^DR%R(D; zZeF$-fcwfC{Z`*zAc4*{bs*|}&CD3YOKi^KTjSVK$eh*fXUpu_=YjQSqdVxly-b`e z&z!ol^PfJXGT(d?{OY<=-;JscP}PxXuVwyW-MMIlkIFpS(Mh3!X3X_q-Ro3jBHz1s z8vI#gGFML|Bo`SVLh&`>Q6;?H;KXKwfZIoQ^J@?yL!~|eu z?Go+MnH#r3ugrXbNm9+qY6-8d0-^(H#)Key-4@j5n`jqUhriNxJS^NK;MV0SzbmWt zQMBD!k>npZSOf3YG;<)QV8$cEVL+0syv+Xa7_ms^y~sggxXo<;hP$JCz<}?X^}#YU znoh&qq0h7RB0XwhFhrv>Bao^qfnyIu$e$eV2eFuj7-L1OJww~5ba^9;!8Dji9?T_g z^;R+)L=e-7PJ*?B(@E}N>Oc8P^`$IKt32z0r>|53$HY71M_8CE4FB{f2+7Bvy)@25 z(NBH+RE0gZ16C^C+DI%p-zSQf0&xoD~|bE4zK5px#M`+f=oA0O_KIZI8=?IT@nj_ z4Eadcad#VvmhuS*;33t~KiL&jq%=|ir4beXd2^&X1FP!}O9l`nC3kW^S4;rPwl8&u zVdWbK+koJrGz&ImRYBq>S+7YG8Q*?F|9YXl3K6p*tFP1@Ym_02?S=0jN0G)BYD3Wvkjc~_C(XzO?; z(|-47a$-I_DngoPd3q~Svfxm9>t#U02LaI|DUH+|ORu_Bsv*l)5Nv}r|N-)cw!(scx z<5rJOAEZUS;{4wVH(grV2-po${78mz1FJ>}{hwF|qFq!q{Y8iCgXMK~@H*^vxxkMu z7uiA+sp)ufpMA8Zrmpe-g$xBq&{Q_=h8 z$2G|d|9?*GzHa>A(*M7YkC@rbO_V7KoH9#Z=a)6r9~IwYCLz0hcpbv=!F=JYFm}tk zyWGVBgT|ZV;!}=)(!D4Hf&!)l2yzUHhGmENf+y^-$EN_^o4PF;b(kF88|;hoj21GO zo_U@)((HDASmYSAo!1yjZT7=^&pSl~c40l#Mh$iF_a^}+9Lw2ATW23mb_Zht)dAAp zGJ^uG`%``Z!Z$V|szg`~rX4&gsC|MBMecc>U1Ns|j!uV%n42-Z{OX~GLqgYOD0k+* zleE;Wmfl@cj_JD9TOGWBXk~JoyltZY2#cw!W>d~y4~;vTSRzOzdMwgiQ~=D#E`G?X ztAogeL5OJxF3gHf*LLJSUn1o1CW zt#{nk1=#uwr8o?t>#Y977npNRG`iy!dA`OM8{J03w*}DhS+$t$7(qy*7+}(<`$d$_ zTp9%(=DE_ZH6y<}QrweI{gALs@ZhOCAlsY@2w#!Vq#tI6M*tn{rSY)J(>P${^i1f} zD{ES~kVRijR+-UPM}JyZ*IM6^@yAlHp&gX*5iBLNJ}BVB8ry+pcNUu z>W5Vuaz(kX)!qW`o!l6#Vl9p9B9VG_5FUa#^Us)M|%jVj$y-YBF0~slvAYdGm7Lmjuzzkvd z;_Jx>9j>vM_^1hOD}6ceCX~-q`I0+_2MBxZ5}gb_YzqmG_SG*FG3xE~$2=CR3(0#g zu=(U*XcmU*u&AAr<*n9SCYPb4CW(#w!%cEhCM(~eOwl-c5YF`mFo^O}%Mqu;!dNhB zb|spTN^^_$dAC6)yt^%=NMek$9)|xlI-cWejaU5?jfp`pyNI%BH}-{vKYfq#7UwupaWXJEAjxgn!Blw zh|gwB9DM_VFFz)^=6%Gy8^O^TfV2Rn6aOik=ohraN{er+Z%?KOL{-OB0kHfRaGUJR zIhnpYmdy={wV}?2!+cC86oWTgM z(6k;5@yH-H0&|RaAL0fz143ZRe36l`NA0G>#_BDD9(%QFwN|W)Y8mEIFapnov=hgi zXjrxk*C92hn)w+UQ+G6iKTt~Xsfad^-B{Q&Fy`^K)hkN7g6(QP#vDBSXl($u!oZC) z*%cj`m-=SzjlS9*j8IZgjA`bjKO+<>&v=Hi2j&6~n0{MhM7uA3B<=d;IC~9P{AvWv z$dA>*I1vN%otaKQ=N+%rrPWIE%@Z}fR?Nu9orlo{a zYPXxl1$Oo?0487*U}g_rSRrHcmoV#nexKGySk@p+2ROJwDo~D19%U^ zwNx{1x9Ab`@9d^a`@~}DgtxnYnb%!-d2sSt*D;1LCzLjW1wkoiveA@WOhZ8qSBQLY zy%4Jv;Xa)k3gA~qnv4jr%q$f(ZuSGL2>d({gKXu;&r-aL{$a%vj-B}U(Hx=tb)!Qj zjjK(YsrP zkwq1~Y)&6m!ZFIyUh(J;B+}bVq~f_U^{`cnMTP&NCkYgc%VnfDSIYC;90_u9zG3cm zqs9}87K+Hg<0BMego|trH_*-F9SOB>fG(X? zr3J&|qtRu0mKIabRqdx6pEkz9MOJRePx{{uSj?PgQl?DoqVg88@2SH>%w8IcE zk`*wrTMZbso}EWxg#ca zU_afSRIW8gkOKH3nJFlu^ZhpVZ>sxpbX z!^HK{t&C&Y*z$$c9ULO+EkKJHc9^R9ySD1+0t{tiAv=J;@atrJ&uH&w_x<>HAMi<* zp%AyiCV+)?>5&;Ah}>2s6-{_(+m2h@lAYB`u$`~8Br(qC-FqJBdUf6dZiYMu<}3qp z3L(e&qx00}dh4{Hu!h=$d6dP;-y$Qn=ADBXV1j>r%LYKW=CmNgq(|;)P606_bmFn1 zIe)DNU9uX^YwWBY+g+|60(Q(|W6^;ey1+NEomfeaok1hrQXRiqpu$i5l%uX8{SuvV z3$CX-uXRr;GbGp$4M38utQq=-dDU8|m-yeiM1@+NQjseW_zRNGo8bp7KC?@wkxuRc}xec9U2*c*t2rnvVcVWO&1`%@V0N<<9Bq05^2{@XE0V_f9F3^mye&DU*wH_VTv!`%aHeC9nOYQnL@Yrg;=&XRG97qzas=?C67 zCu8|)0aqlUBthYU0f6CWq1n5MJ{koGPWw5M$te4Pj+e3S7Hq1Z8;8lC4=XMAK5Zns zq|%GLUrqHnzYX#Mz}-3LtTomnzp4QdE<*up|KuGZXS|740Y*cz`4oI@TUU~IZdv_u zvc9_ml84Ayr)#fgID|`Aa5J_{LC-eE4aDUUB?eAqk0Dm&WT`IBjQ?(Ukb|h_rHAn? zdS|RrRlI8po)W5w{w+}P6f+h`^N3x#`D6*r4dC~KL@l7CW7z`+yD>ezrOLc!-IG=# zI!aOsQoYj~z)%6g#ukB;JR`4P;~O7u1_;CLspe&HsNH4{A0P)@{v#FvA=}Y=2T;h8 zy86(%jfWyu(#m>YIX5a-O8S`)fUi-EYJBj`k4(4Vvy9pUUcLUudwO~T{N2GfSt6da zG7)unF9R-%>_xNRIwppR9fqco(g`@`+09fIi~8(iIAG=v2$Nc}Mw87``O}Su7FBPv z96e`cgM=VO zt!AXOl+=Mj75{{cqGY{Sr)2%!F|N+_7EDS};!4H!rkBo|khIL^R+tS;0ehTWH~$cm z?2))(-*}DWNd)1<=))=|l%D&Wy!HJ?-*#SmCdgq~MOKd-ow(Y%=^%ORP^vgj+UIb{ z4xst}GJcpgZWUWcT*d|g9)vg!G3}^wU7d^wzCka$08*Gy@xu2lW%l;$G=!9(q_s3a z>^Y2HS(qCL*gr(wFYq2`Xg2EL1@E4URQfT~Nczp3F8Q_3z!&rGu3|Ttcc%zhD9(ig?VeN=a2Kz_Aw0NUg z*;{9CVPd?KJSI)CG%_E-$!7fuIn5FH_&PVMbf%4azs!k5kCP%loBGbW)g-E4`>Uv@ z*1{KrXF^@?vz{HW4?;+0_aN4{i{1Elu_{sdk>rGY0Tu(8;e*OMYpKKe95wjghwLfph&>^7bZIkK%S3xi*A(M zhQF3+CH&j{r(pp-3hXtX-(}8EKQ*ptcO|crWC5N#ATTFnu)O5FrZvY_N1|c?HGucJ z#(ysB6=0$qq9Yr(2>@Ve(f94J(7AUT{ZMX#YU7bST0g&x zK0X|y1EEQIBBzRRiC*_N5rlvIfMJ(xmG_HSZ8S+xk4w8~-HD3&$8h;(5F$0IRMelS z{VSkT$0B)JaHH*9u(2%`&s)8Cw=-M5z%k@~yL>`7u#x5L=j*|a){2ay~O z|M2o~o2vC?47ObrY_z#Tve{gR8zr&B;_jKY_gU6M}q-9tIu$wLc1FTnkm7{y}83(`@&m1|GTPYV03@|$g)J8Jy*($zu|;ea`c;>?YN!2_sK(3e@9^44|4+kH=MdLyCi_X5Km=G$2yAIM(6M!a=D zaJaqRX)&J3zQI)~_DCzVntNI3XEh-e>5bTLvN}AB&yKfE9G-fvpCpi`!A z{YXIVJey9`EOKki2^`!WaIlF?RL-~BD)b0qEd)R*%Q5H4bT)-Vm$T1--Sm$bo8ezU zLS+IwLOAPVfbUN}=&13c;L>YVPxC8OQ?t|qlV8*t%ABiL>7AuuH#gr9=ee3Awqj&W z+!I_Gzh^VW+VOu>_SR8RwSE66-O?!?!k|cZcS#IjfrP*SA|R!7*U+6h#E>FLcQ->z zr*ukp!`XP>@9$aXeb!m){P=6wTs!94n|*!a8$yxS_%ugNeCrtM?~*IGh0y1puh>pi zz(gg<&y@T!+7S)Jo9=GfA)GRbtco++j9DHy+e>~eoK-}qh(s-Z?E6n z3luJ72d0Rc3O0ir_VT^_gV0x@9#y+@Hi{A7Hg^LXuL&-%eMI%& zh|$HWUl>S@S*yH#TTVA@&TzDf1)aPs=a)9*C;m>P*HngxY-hrxo*2o&xBkP)=f4Qt=3R3vfDhy#lB81*wTusU4aDWbA|i<-Mq`lta&um(=WI5l5Vmgo!_Xp(!{yO0)e+*KR(%hlV{9F}EiBTjd*# zRh@kf>WUkm)`wRXxJl)Wy|B?y9abi2uh<4c9nY@2ON^7XJ~nw@T=eioLZjAQ7kFNZ zdvDC6?q8<5pV@o2AdWdIt!JPSl>W>IkJg;WKe`9u_@ABqb}x6|M-Klf-Y3p2X`*la zo_h<|w1e@js#Ht>iW~+|R3u3K9V6HLh9d>1@%MlX9*Zkk$%Z%)Y%Nn7>{N0L?FlHt zYNoLE;qG_w97v{_8MyB60!2x>hfP{;``gOq66k78!uMZv&Ha2MWx*QM)bnwA%WSp% zD-T`oBjGOGq@rZK({C>L38nq1%~(ep`>9LJ-Q4&y0n#iApbNYZEDKOufFBP+^dY$1 z(ETiCN1%(zolg)BI8MdhxqcQipEpU_`Fgo`>@W9XC01HQWMNd#uGxD|zYyY_OX8B? zAgGVx%39CIx0BoQ6tE}hZ(Cn8aGgYZhH|xdKno*Api9mpN_S> zVz zia8I_@5z`tL+6!!-By>>k24*tEUPs`7xzW-E89^q7({yYj3At18Yx_e2rykH?Ouaw z2*POw)31#$s`ut1#P>^_+Rd*Y=NsUKQVNbF8v85AGY-Y6R?UXYcOX`h5ntachY+gd zkJ53+FYz((t@q2}nBNoU0Dv(npYkzC*iiN(S`ZmiVe5tYPF6;%!MYBeN9Bzk@ z%w{&zU-%%qG;c9{osJjh8PU%m8Y4YeNUck7#(t|UNJh)%63lWcttt)CD2ej5vz4>-EanHZ-lAsAdTDQs$7?UD0KdtcpItNS!Mg>B6?XG-Gm|getvsa{WW73NFM98k)owQF#mfy{W zb{*jObneY#_#z&pR(8xRa7qQq2bU#94g8zj-$sv}a|8ge5a;CE>+b-Ya5 zLyU9w7<$H_;@|~sd_5e1V!%*m-ps23-O-5yM;pXCYISSEmJgoP(X(5u`_b61IwTW+ zV-VG-igbL{5GRm}*OlD1EtenP-p~*yS&AD?BqG=50-AQdlqYT>&C`5_j+%;L{`7ZBHLl}qzc8`<;T_m8}$e-riZYX4%sEqSDd6g0gCes|W z6TN2;X^hfY_L||gLqrRXRNcs8$ORW7l3?fZarl}$;Llq~{ucvLEZTN)vIG+}hwY zY%)n)FDm-u-RJ+3Zy0wi4(E?fYpc#30kqf^pXOlgpfvl}Wv5R<@BLXu2HJ}WHT)OP zNMA*Q57-bY6tOnSIDLu5yy#JMFIaS+Fy;i=slJ9US~Nj^f)tSMvIK6~;j-|HMC`yZ zr5=88Q}Cm`dvZj8EF5@y0#Fzy@ZnQD@XOy%e>Vf4u>Wo*1%Sjq1O7AgVZVoM{~Q7M zvi}YMhNAql)Biat+~xe*D0jAJD7WiQQ-$%_eU|I_rKW-W!!Sx<$I;ymi}JlcxW{|9 z(CoBXuY3O<(X%uuj$GXQhm$D0Vk3o6Gmw~hCN_)y_-7li*L69`()FdzI+;LX6_xJ# zO=q8(*PW5n8233ndG;8J6uZYq05w+@GsTsgo*TxEH=e)Ak?wPN^EF*5a{N>SIaT$Q zh$P4b!Yk^`2P7Kt@R@ITs2^cI+%{ji@_`$L7pRkU=Xs!INdr~;>Z^sNd5fvWln0M0t)pP1v@nCgJs?o%%hBS!eeaq)d(Jy#jY zH34nNy;^9|><=Ih%cr%yAT8@92=4OsfYNj|eQ3)0pd<;-;OGnAc-)2JXMc91u3_VPJ|IQz<_fyg`KTS7{?%%g}YnlU#^ItyT6-S;{cKfoR$@Zq{ z<_@a#@@sbG+1{3zU9juP6m`Lt(1&CI#dqyn0Yw3FKV_LW9oD(s2IU*(8Mm+p$C%QD z$MnmDP0-$KBIms>ws*AUh|3+|-yJ)@D3f}D(JtYIW`k`&I6Zy1FG6VR zG*LoT3Luy(X3)KP@Ao5mm3;SirL2>vRLrhI?#F<7bSE%jITg%vlzOUkm;XZtd+qT= z<^9dezCzvR7{uHxZdR8@M_P30vzRvqjsOC`_bT!R1fyE7V<+j?Q`A>Wu8_m4T`_lh z6iLT{9KN37cglBV)zi&Wjb|wamSMMsU3UhRGwX-8Uvt1OTlq*S(*U@5Q^AaHzI|r! zB;yHjr ztv0-_jB3qO^A0==>y7zTx2b8vAE`zTugn}JSRr8Yk3~oy=iMC)lQfeK13797`3=wP zHRQ^bQXZ5k(PYD0j1-`P!@=(tMzVMsfTc_1TFeUs{+~4SY0|J9U0-a4vAY>s14-5^ zmA~D=J{}E208l#()?XTBs-|JaLN^V(S;C^EA5_h(2LQc`=kNOaUsma2-dZ6XS1jtF zx2Roq02Q&XYv{X-T&5_tV~$K)m+ zx>YA1BISLVLoQ8w{zy8}e=(Qg?L%ptV-6XPX`Fe61U;q^J74-lIA z6;IWL-jpK$5ig*L2S4P07Z}_fZWvLDYaWJ%8WF0z?+?jl38b*RX&dzUu)e%jK%4+? z@tQ5qT=qH4;$m{PRPuEVy0dQ)W(`MP0K^Lh-h))xp(fN2JmSY9;c(C0^o2JB^0m>A zij3oJFy*obzSQT6TK-WBH@`5Dcj2Xz-trF)8sa1zwkPa=-i;+ILuzAWiYGs1(qS*^ zWRO&A|C(_cPr+>#k0z0je$k`5Dz#q3mSoN}F0U3#VW$#DD*Zz^ByXpjijW(|mL8{T z3wa*!j?f<*z2@^BaaH0x-A+h$#_vwemQW^2_I&b#LHR!1sO)jn4 zZH9Hlpjx$meY57g$9#k%oc!WV5s%{fl79vFD&tKNG9y&2%MTGhM6~5OG97J0Gu~z+=5Dc<%hp7TyE&m;O0`*i9QENZ48x~Wol@mRYg%u zM4T1m=gy91eWY%@CWGXOW>=9~fZtIjAx<@Hx%l1NyXYU>Tky|QuWT%i=|c zo?<>B%?{9#%p?{2(=qk_voNmo1XIl6O09fvAQ1qtW+aDgm2@bDGJ}2WJFu1Sg&)KC zJx#tNGT$#c?TfWc2Tx({aqm$Ye5{$cBq~}e+_4C*;+_G@Pq2;dUYb_44~w)|#t1%< zjkgkhz6$B%;e>w}6aU>5djiV)mHW1zvv@%rAKQ zjidKiU$lB^%TdU-UW6FtJnEKKpSi>zjIhJnI~W$9vC|VC0{}Z7cD^%Y=ytHO4E%@E z{woYFeJ2aEU1&@{aeI;Phn0H07R|`ZgxZ=yfiqb&{7n(uUS}#EH2CLWL&nFI_#(?L zJop|I{=r1dBg&y*yOMBF8UkAS9!;c#>JZOTcfBj$o0orVo*i4koKE=`+V_l-H=J;2 zMfd|-uW_VsJfL5~Il$(!zOCz2e@3IR%T%g#+EYw3sKidK2gQ}&y=A3oTKh#em{h53 zJ4s|3%Gb@!YmOjfTjuF4oo}51`{zZ}iD>++08o09ZW8;bh1g`TY`!Vvc?|Age5Ldr z%u3m*(G@dAY&D$OZ|ALr3MI4UEuxlopznM>&ig1_ofi?zo&Iv1ePd>}ZbGsgcN(*+ z?KYZ((HsSf?1-6~_#4>n&aa74sv*7X**0YyxWlMA`UZLWF(gzSf(HpOX-fK%Q-M0H z>A8^$mL;P9Bnt;g1M$ zh`SuU)Q$i^^P#iIn_f;DhFtMPrWI`ff-Bp^tD&xi)hv2FA^8VX|8K1QU$FeYaQlD3 z^8bMGi`x*EdUVvhU^3 zwj3$?mvd@wjCl-{AK?4TFCaL>EK&++yZmNc>z{28yKLy*#k&SZvQf5y&-NQt@Z0?abktlx8}_xLuD0f*75#E5xv`ec50tLZv@`>J z@i#r2H$Q6twcZjCu3fM9ZYMQBkkkUS2Nj>(YNM`r7FU z$<9+4xEL9StY+5C~WXP=T43r2Cj~x%&e0q-Vl-bA=e>hjJ$#P@(HDLVvXf zuqFarx-1Vb{^tEfV=g(FbrT?kx5+=~Jxs!zXT*4odi9kYJ0r`UPgfH zb3jA4vUEMZEu?lT`Uq7qY3R4x>2bI{Q2(p36sX6Qbcwb;Al8XxH#BqlO)ZJeQ{AmT z@`jpQ@n3NM%`M{FHz5Ba8DKt2N-2}JUjea|o9%w)!Exvlq{`Ls&+UhPE?RK9+Huu1 zA6H%V?SE4|`@xG&BvFv+lmsP*f~ zgr)^#QjC3EF2*y_Te$S0-#mYXe%2^soClp9FB%iB|$NQZtu? zRg(`$@;Ey2tIV}cdto&`h(~$EC&9pPalX|mJ{LR4y_ANKYeOePL^ebR5&F$ZmaUK#Y*!4&-qgKsZ7&r$@l|_^d(v4A-I?D1F)kS8J<1U%I|OzKO?18q zICllI4%4)4e>e5%*cBN7F&8UgCW@jQuua>*p@x0ueW@1VD~{v3@x|>W1e7fDh_r!x0{Oy~``X)LIFoW<`r&4`Ip;irwI$o0_V1 zav5Htg^18xo+eAt&mRL~@BBFc0$igs_1#XXA-RLFl?dc^J;02BG1(w5{sJO!se&(J z1^~CCv1xtKD8N29;*v>6yS`#+(Jla3o(56bSan%@IaEkO0gCbh@fyGE3=$VJF2M*# z>ORtR`(o1-HcTsI|1mpm3sBtw`=+XTeqYj~BV{SWWWfEfOrOtVmGZ$cQ*qzHL!V|m zqZ;DWKME^^1HxOcYmPbLX1c42fj3~|3tLEvt?&GisxyFuhHu3+Vl$f2k7%eo+Ninz zMbs=FNMqFKMWO=Q+y*LXWe#~jGnnaDJ1>SCSte(G-lJSCHTAtII;5pWen|=Hq~^$CcLIfP z2EwOBD9JTb=t#wnnBe;blQO>Vx8#qpX(ujJWNN{m82kvkqDtecEEGDC&GL!?=8BF- zF!bxb5fgnK`uK~)+Pb@W5}mwUlmE`NGsAFn99a3JUHqb7Mo!sRJ^GARnDqM+F|7$r z59sFj`j2lp8W8`};To_I=8G?E8_wM|7->Q^`Cwt58Mv>ih5iNvb$N@&l&0DNepnF} z!K%B~s!Xy(7%Ob`gigF3xkh5lhX8deGTuo&?vf!!mpz+`>WLTM(I(lC6(|JB8ds7S zk4mK*IC4_J7lE`uL+IR*>-jt?gknb$@lwJ34Fjn#`1*03O;WG}IVYokOut_Dh&MH1 zr_t-eGf9gt`;YQjUMK`1AE`Sr-uyF{2*FUbVUitOryw6>F`rvs@n}zmI3-q+Zhjn%01*rqlN!U{5^^>|>+GP@p`9iW38k)HH-!#zz;xH?FrdlyLHnU z7qgU4e7cQdU`0cQ;0Wgh;_Enw#X8URsYRr6KlyhS-_m32Av8)70eOR^CAKg?T#i1P z=_IH>4Z@E+t<=l;hl5NNr2IQ3x8wU;B7q>fc3D!H38t#{vPU=?t%4`=8%`%k#u;-a zRH#g3Tj`saZ1U;x_`!NSFiIn)GQ=g}VE{Jz5Dz&PiUwWYrzq#`$lo4kqdk-?yexs5 zVdDO$hocmEFm{URvX++M(ntR1MA*}l!FDEuI56x}h)OL}Up6+n7M-K~>oYD>VnQ4Q zkuH_Jop6ydo4+r+I zj@X?0Ss2hJVBvhfbQZ0ag@ry%$%xH)KttvGHh}5w@1wTKJos(I2d2ZYtkk`ge*CV7 zB=l%D8DR5I#FVHVmL9V}cXVCoAkxtiIizBJ7}#1Cz$%Vx2aZQBw4g z05+q%h_DnMX^O8;Syn~EH)7mf8uAr!A4-#vOo%2+J(6NXsxrS!e6*b;O>bLpFmjAD zQA#vWB)Hn3o5LGVCwFK)^^6J!4+CEg@3B1djm8xLY>Qz=#%nBMD@-Ir=b4+KzKQPModwH2)<~TU&<%A zn*6KBE>!}mqhvmo&-jjKJ%VS0u#iw}BGs+w<{m9RjabwJ#%pRy)1M;2WQ7$H6H9UY z0dl?~_CFgrpoZrrLv_Oo={n2-4qd zVXYkJCBgX^`uc;`*V;F-`C;2r!(DGh8Ei zLuo#tj(xd&g+0u-Wj3Kg^K6=Ep3MTu8oa)Q#B`J?I4I^~TL$G#VUG0s!uC@=02T~B z;L@VbCc%{BPx;j&jTJ1NLSZ#XMT?Mux$e)EM;Qqm(A}*;8uxv4zX8ja!XSEHM-zip zr^j2lX;UBn2AD{T{h~hml8k5$?MU~{Tjteh7Pwvi^l$GPK<`1qq-1AgpjZ#*7*28^ z!8zAUN(DzpFj3=dm8oDrqKAOz&|#KR)%wIDNrI6I!fN)2(TWhy6|?y&7LV~9>_sM) z1%TJ@C@sO!4xLB=k21z)`f%|~Q|{?;%FrQ7KW6GWl7+e+vw<6}&?To5GbA^hEG=MfKCO+tn+1!o8|T&wps z!;V=MII~5814XB_c^wp_6!x9|qr;iE6*IHbh-@~O@N0PN9nS|IWbv0ZpZD-c zOXXhU^*0RzcZ?#Nxw3)Jn`7hGuq5x12G?uJ9PG|k=4K3DQ9zJgPhC30VI&3e#kg-V z35t1xm^k`~OuN2Ol{+>JdEcEHIj+q-IzAYVi%(a&WqsZs)3CB0C3VjWFd61Em`)&A zzuvxYuF0{u;WqoG=3jTBI3RPU?1hZ%5}k$XZTRB<@Eih;!X1%k@Tn3@D;4sc&C)B7x^=kShr{6alG)NyMRLeVpUi3T9D`<1>%UrPnY$ zRY7#*;$4d>6&in%@X%9m$)}xL2*V$0r7ckBRr2Ny!j5U}2mLy8fuNw=FV||L;*F`( zWu6u-c2WP5pD--lsR!!wh$DtkK5&fh{DfAxL)>jAV*G@fzkjtzf8|`jp&w1BT8qzC z+@sm;7GqGn#Q5?de#Dkp$a*HWRF)=yTX;4nox1wdx53|TuBIQ;r#tm)NK@V30U~&_ zmc<4K_XBFL*og19IVd5T-=HWq{AN6zrQX;zpgu}&1C$S$MGY{Y9sF$$6pMKPb*hlE zcxOze4Ip$Hph&w`yc1&K31bpN3~Wa57?FIpnNhvUtcc1;0Zpv6A?EI{qL04H@}|&F zqLxAS?l1Eboe9c3ZqHKB`+&WmSjra$-pS=v?5&ZXco%6abNPJpA97b{YZ8LU55uP^1b91lcO4bsVdYIvq5 zOdHwTxX=UnsaGy}&<_|QMB~@jNfPb)E@|4h%?CKm7C^BBMC!R67nP8d*smNVBb{Q%m^OSn2;HQo4Zxy94~ zSj`^ren`~ZL7q`vo#>7-8K&JqJ<9{uP}|~CO+XaC*q87;YY>|MSoz14kNucYGw;)( zS682V$~%Bkn!K|So{YOxPZp234e-Rj4DKjf;X5+3UU*etgOg6?dNpp*LU=}V?K|@1 z=x~A0zv(V%o zWl}PHJ4XHO0$|@B1Q~CdWhmvm_ZHq1uZUTb;%70It<3|b2{#2E1x~Qm(ZbLTf->gG z)x}nMUJW#I^>W|OS}i?+gbuYUeB(m@XpUlJph}k6vmf8Wc9Q;DiTzZLLDo}|`9Qgyw4IA4*KPdKT)8}iGlMBa z?#DRm<3h%Cpsw{c(Jm0#bch}3-VDG82mc)g$1`^Pm~+;Cyy+CAiAS}&Vs==rrQPC6 zHKvpE2jw9XllgI+BG9h2d5gU}JMJ9C;;^JOhak+6c1n@amEuvU=3;y;CAuAI-Z~R z`GOnM`lFnsLn)(9FjeDdkGhsJEgF)VbiJ*K1pP|0-qfGd9W3~BL?=X>TaPYfx!Ug# zc#57r8y4;y;(oouyc&22*zCx};X5Xmj4f$h#q99!5Lq4|n(yP{SnPBQh((QN1Hl@o zqDo4q0&1g1&Pl*RO^o7I0el&g*_IKCegSagBGr`z>K!<%$b#h}Z8`^&1L$NSag84k z_*xh)9TN=Nk<^0Ne_g7bQKM?F@&w9{p-c(mD3JQt&U1DV zICN4xfVXIF4X`-~p*Bf6xaPb*4E!FktH(ik$W-fPOWL9FtG) zqBXk>b-Yq)Cw`qxmOe` zX`$~9W9HM5)-pOug}VqjP;_nlBpRDWstmMkh}F=jT0?ZrI;!ud=H4Am07?qayfRd+ zimk!9=Yw(&X1%P6uRz{OOWmxgboI_%Hm!v!mCsPs&GfbXdcO-aibigttxweJ?x_BR z#9e|aDmv^#4Q(Y7uQZ0@nWUNSDx{H3FY?p1|Gel)pM(a`;zB)xAL}q=1==!q8CKGx z3VNCXjy{uR=Sj}C-hC9&rp3%+6%!3U(9aC)U|3HVw@F{GWUi7{k;dc3cy(nF2X8oW z-M^D71A$mEaJ$k=B2$yOYi5DM%lj{1BAY&W^`VcL*oW7?$)D$id~iAA6UuItcZ0fv zmcm&$knSlSNWT{8*I>;(nTdEkn&bG*bgUoZ0Vshry7j3@4!QuNC_pi zze74#>9C9(0J^bYPgq+p5!AyKAe(8E;!gr;x9*N;Cy3MFbt$yJIsLMo{bKN}X6|Zr zk3icH2mZbR5~vU5ZI2{eRt*9xqYg@{f$V}}0{pDesdNf#GmXx?cm)IS$j?=Di92*P zHzX^}Hke4GsjP42Z=u$-iW;9p>UIfuKW`?v!=}*65|^w=SePQ|WAS2d9{)Vb2taxN z-JUGCXr!raDqkyBVu>Mb->yqz zby597Tt?WDK`|zRb0vIy@a^;th~G3bb%s`z_!YFOT*wDu`m4lxoh-6pdTi`j=Ldy} zO&bQKw^FO}G?tVQyMXV~?-`)kWeV_ajbvFd0o`y07M=<55LGHJji`;DPb?}RVSa4n z)x7Kz%~WcOL5pAwVcut_RN<{a1h4xfqE0PMvY-tMU5frEAcq@$1Hy2Z?BZBR_ii!|*$g)?i`E z5Y+fHeOmr%^-0iEIEg>rh%I1q|JgGk)%t`-n8UG(OJ%BVGC+c=E{O$Lakg$htDalBy)z4AlcL0-~ld&u6m04pcV>ncT(jQ#26NUH%H#+R&1GJZ@(@3iqqyO+JA9a=w)&^+@XM66J;qKU zl-|$FcsjT^!4exntM&gf&W$*)huTr*mU|y>-_a#tj(oUjrcD?Tia~uspBdYH-Q4sO z;@2-TTKr{z3`aFLK5Vswk4O@8PRF+J@kz|jlKXq=zl2j;jdJ<_5Kj60&|c3dx|92$X{}0Bwd|&#G zKsOQS(W#$hrE@YZ-n^$cME=_uXIS-byPf~E(|KsF z^PhG)Ky#gc+v$W+i`k#v$&c-Y>mw!YM|5BG>+kIBrPdq+opC&F@A&4M?h)u7muJl8 z8yeG>_vq+IZ}y`Ucb6Ra4R`E`2;MCBHV!9$OP8|WD3LS?KD2RL;G;)p^o3O+zvgB zHiJ-qDmuC}$&dNojHz7dPaqH)y0lWkJWcS`=yROaAU|0!60HXKL}|B&9N2S|vu@Ha zbIM8v`B%eJp{vX@mpGK#rF@ygGr2TUm)qCCHQ0ks6n76X9tQg_d!~^h9s$RX`=P%d zF_ZP>-y?pm=&elqE~%f@VtY1ND;CQU-fmP44k+im<3sW-$@ujot_`8Ur$63H;gOo$@RDg-N&52eK)-fiE|A~U5hmNX zECsA#tFiXx(qIjcK~}2CsFyxVb-D|zi+EOJ?tU1p%f8}+mxQ`L`cBbz4&4b&!@VU) zuLeh}{rZahbaLW32U*VAE7JN8oT-K_vocE{4o(B^^^tMvsWQ{z3t{TgFB_6qftvaI z+xy;a!z6hzv4_Y`I}*V_Fv-e1s*1@JS3aGXE5^3aev8=h3XO)cF*9f^DfOMt&-$~{ zhOK??@Q|LZaduJ~d}w=MCW)`^6@{uxB=VQ1U)SvzSZ1qDn{0XIY|r~=lk{x-Uc^TP z>!TJ-4hq}<=nK{6@oud_mFZ0*xFs;-`kdp1gZ{h;rd5#N@cpIQz;{B*$CYn6iLoE2 zzU2?{(o3B%1z)bGUQgB7Bqf0Z6o2Mf;=JD9rSJHL47XOt9M?>*5B7wv@Q90ct(v_5 zkX$JwSY5Vqg@KXs{>Jf!G?FEoLl>l)R15~5S^SOPeY9BpW9)5fbkEKbuncXMrrr3z z!$4`kHTRwCdspjngKE}yXAP|1Z4wixlmxG6`MR1P4l-)``{e6`I4hqy!Edw1@e;-(JabSAlpx!`zB^@pnDbraxOebe zKq!LNcVGPE^@{+nZa#q{Xi7~TeUU#FP%F(o55+;hC0Gm0jNKdY3{rQEe=}fNoL|6} zpV9m>k22JNpF&*>4cg5yrJ02TB;?C@IEb2%jDqM5c5?2Do3F9EHBwJSgK~vFZd2=~ zC_byJbKh81*YKuijM-*??Wd>;G?wNO?F#Uto87p_z9p!An8ZN`^3ZO*&WpxQw93wi z00-2s?=%~9x2hnfEWPAWXZM!S*AY5DfO~FN*51}@`3i+YgJ8LD+CDTR z0Q~E5;8Zq$oxc(IY1-JtN=pnHaIKRUtE7hBY{?T0{j-|9GOiV(5(Vc3cmqVCD`8j) z{kb-vR3)#b%cYHJCZ^WE|0=J)f%UEBT9_j;Z$_+5c){Esm42<{Y}E3-@2u#I(6mC6 zx3DUGV#F5epR#+ToX@RnUpa~o+vI+6s*`H2mOK|cIQ-P!>qIKP@mInHJ86YKGouJ9 zP(&+5^QkiHhZpRf;6m%Bg*(yElJAcY{e!04oF^E_K&#zfAHEcFt+Z?E(#1{trF-3u za=|Y)E-bZ_iFqV>I?>=+1<=aTIy}Ab>9LV6hi}hcS5#DT0Z(Dg-}yUQOAY&p<&ymx zHm2$iBDv>M%0LVYl3<`w>=fm3i-=qEGnMM^ld)lUdPf$(DzZG${qnv7e)2vliEt;>101~*$*>PqfO5N{?@bDwvL#%0_ z%dt}^y#ur785!T>bDw6LHec;25#tnlu#le6lD{NZGrzez=cy`MPF=*B>3!O%Rb+5Y zP`=;D0EcVge$4ywn1z(QU|{1O^OoS=Y`&v0e$mqFArw-16) zqii%Q@A9Z5R7kb5G?0W8iSYYsxo=MM5KYD-Axs6=jSRxTmJM5}lLiclOYM{LyWION zZm2AE&|_dRUmXQTsxM_cGLZ4TrY8k@>76`3wOK~8sZjuL(xGD#k|Lw}bi}3QG)2+^ zhcg&n!@{Yiks;zmLQ|O3Hd4g@6-4cy31-6as9=;KdWxhhzW4S|A0hrsbOwMY7BimE zec`ygq`{xD3(NTB&e`Id0RRa9Oq(9f4ZB}K--8m&{xA1_Grr=d< zoMP)VVC#DYTen1)+rC(O&C;mZ-OOqUG5W}fJ$)Pg)5Cc;3*{Y|+40BEch$g1(!oLH zfeN;Doe2S`^w6O2zYx(!sA?MalH2x!=DH zV5zAJ-rU^zE<8xwDat0xo137g3974m`}-^F>#Px>8hIqAZW6s~4lo$3qqS9LXK%0T z?97RG@WO!~g>fodSXa1JaE&Qj$u8f^9q%#{2o+ z@BW|vJq{1|X3gGf?Y-8T>x}D~5G4gEEDRD1I5;>g8ENr%aBv7E{IWh zfPbK}qhpN>Bt9nblRYXnMfOb~N`w0~#zKpIJW0}zg66DPS68=!QZH{5l%UU_o&5u) zBonbdSMjk!;(P0=-a0bUhdS+N_Fj>%6O%~{dm4lSy+V5svK}*ZP$PdGZ8Pqip;UAx zh6LA*gDTjKf|MWMsXuiQAX|JP$wzr*GI;PS(Umi)EF4{!twH-U~`Q9_TGwj@| zdBcV;_k~lkKh@9CrR{ry2EcoctpW&OwKFpdbl%#W%+1%mvriAM@yN!ft1C~t7F*R+ zfk7jgRHO&#ao6Zr#s3zD9*~Iu`@yfM=1|SZxUJ~qC2^BOP%)7^NgVy#Y-0jz%o+ln zZ4v01o{^i{ZdIucCcdSwyAD!Od;~3+TjZ`6Bb;D`A$EO!L)oLh=LG-E!$dKy%DBeq zT*=5BbyZ%_!W0JC;fR#Lw%;r)DS(!y|00ezy}$S~!OYH5%IJE1Lf~{eQSh=_lh0@(3UVSO9`r2@d@^NjTaM#r36V}qPjAdO-L&HVzJR%Jv z7c`|~HTKtoyuO8XAG+E8_Tm>@aV;rD${uOUrG{ePX8rTyf|&qPj6h-S=AB_b$@l!} zSRT6=*z0A*=~taYiZPRA;=8q|LzeW`9ZjyMEX{70R&PXok~hmPF{4+9LeWJUceKOH z@d+l*%If8B!Z|RnR>SzKXz{+9esYM=c7K!e(9&-3L8;({^(B14{;y;9KL@1ZcS8ny zY2y~t6|LntDcEGp)hw5-ym4RE^zFz9^}{8a$PvK_F?IeA%h8|&3pyqn)8t#?c$ zZ4RExh_3u8lFs5DyV=*BitbBdV_mS$Vo*%&4aNAT5S2_nymZ{fvLCLI334{TA=3qnB zx2!6h({up3-I)+K=w5lswBD{usP)6d!jQx~$51&#^(~xT0!O^6(%r$IEa_$$QE0B0 zSP)4b_?fyypcuj%*jyM?N%riLBsisC{Fqrol+zpn1{;g=MA zy0WxJ`0JaL^t-KgYg`j4%~Y#`&tYBD%iFEpL+`y@maB5pg7M$6XCLR8DTdWuwHX{g zvcIa8c6U5M$CHpyU!ib+FoT3G#>g7stBmury@m3t)P8$rjz{RU_wCk#H|V?`tLM4t z(0p#%cW|q%k7U6SgWPJ(6nEW zG2(DlyQXAMr}Ju=;WL5qI-}BQxPWEcHh;(>#=QO_i*y(^J?YuEfNBy^{oXbC!bitBD!8 zQC~@uiuB%H7qN#+b(zb%W>uxnXDIKXK3Mh9zsYu7y01DrDQvD0-OkG zlX@T2g09TXNXlv3jFL74gLGzPTqb?U8L^ew zS$!)$F8t!1mfHRM531(*#SIE(_)Dt!Hel#?eGXX8ssy4?`8Vw z)oQ5^)U>?P!^%`1p=&9->x}HZt+$kzyZ~3Il-#HNUV|R(G%z?aHFQ&z#QY$m?O8cj zQxHP^+o8+GuqZ~dF}qOi-OfdtaA-k#boDnfR{Tp99v9S*5&nlELOt=!N11OCjbHW& z<=+nV&WejFF94%AXQpc|MfoBeYqUXbC;Gzp8r(jy)_l-lD`w?kljNe{9ZY=M_l{>u zuN(7o&*y+JWBSqX10*FmqrBifNm^=F$c)R6WUcgqYYEqrz3V_<1Yo`5mb9D7YcT*| zA&gH@K<`ToZq z3f#UcJ8JwLIsWo^_I~tOW{fl*4m$fQ2&%sRod8_&XjB`C4u|9e2S?A5QfRuU%YuOg zF+he`*I}c=v57Il!DYa~`6IxAFyOqoz;maOaB%qCZZbn z5BRsg;Pq=MRkeBC<8o+xFz-`gVNvFeLIVdcLrS92=|>ko_^!1_AhIW1ZO!$=BxC-NF&0+f={t`Pqxo^Dmbdqahqbre?02l=m&! z#R#|B6Fk&nNEslSuAaq^3vwY*)Tk619Bbif&)>6)GH%!uIW0b-KT;EQ@OL336gg*u zdrPzI751iIELOcLd3j#H<-a7)J<3lYU;xV$?7S9&7E}o{zD{P*ZI)@gpfvElQIU%x zB#c-0S31(XgZXFXmiSz2$zqE@Y7b@=d0mj3uy&EqMeaY&R}F@y4Kr6U{g$O6c_Zk z$GAOx7H0Yw6FZ=n&s1K^DWvy(Gm;OY-fj#AhG%4TtCc zkR}lyCnO}VTKXy#`~Dr93Y<@Ce|)kp)!?{Gp;KoUD@L2Csb^<~lG192h{`1rE9jBl zT$mf_vzxJIGVQ$NP31`&86H9__y+&v0xSIuOd+-f7A6 zMV3mt6SnWVj7lc~|k{d+XO5mGe61mwJv3V-@>q%=E6L?#Q!l`eT@UPQ8R zP8O4mL$^-)&9*<*n($`WoFC;fb06pr4RXKp-lK$M0_y}HsQC;I7p~W%{_>Y?fzVat z^(QM&pVTI2KP_H2D_?Tn^0p@H1-D{>N_GJ;y%ux!xh)o*yyahLIZum`N+I9^pZ03k z-T$h03N9Me$7hKECKezcO=0OlJFVYxbn&d~kU+@AeQNwUvzC-@>z945YvjOXC->HC zWF!vCjQN=bM7?%v0hR|*>Mxfo99@ZmVc))M*m*d&xLy1qADH%e?dE)K*jf-ydPd3P zigD``CE?z3_iNQ*Pov95C583x=0odV!8a)=YK)&CP11t+h=fv}kB7f(a6P4<6c%9b z7oih^5(n{Ne{%dKB?+??gD?9v&Z*DW!;p8if(?f{z2-P9LNSjYGZWL%#{}dtbpp3@9&}-0zR+yemGKe}8s{5Uu2Bdr5<6 zbSiAIq*ueu&eyYfxV(pwX8X(S?v9f<>HTV7@b%iM6V{c~^9&JUIFxAB?-?Te(wJyq zL)t^H<4r{3BrPaNyf`wIZ={06P7KN=AAKP2Zw{1MAl%|cSGA63eWzi+xy3_P0$5F8`(i$Nzj9IG~ z;(z7IP$J;|s3Vg63!9Q=&EasALo!)}>4Ho^@~0*F|9ka4GMxvwt&Y`q0sb8mozH4H zcAT5+5s!E?;OdpmS621*QXHHJ`lf1H`*#nxLisU40(FhZQ;>rLA(7~1L?qwHWtMhh zoA{V5((wVcVx8TBJG-y3{v!GZ!X*+Km%I}b6G3~87mEv|S-e!1nxBo3*ii1MYk#3+ zV5o=GRkdE#pcu(YDlHGS72%RIbXAz{)_g3PVXNhqM2kY{L1AN34)#CXq!co7JMW@g*7#}l#Ygmh7)vC1lm$QIQc1bMhXtT9^xI*-smc5Ovsl;}tjXGxW7 z-&uW711c8>pA;b&62CgX97ts?ag)u)h+5QBpBVkVgyOn8%fAFI!7YfSRECGZE7W1VM#YSeHdNt=5&Fa0I zaEH`EaVV!`qd?(RCA^s&CcGZr*AnKIcsTv%^`A81ZMxOtd}e0pulpoimOj*s5$WmH z>OUr}=^!8hXQI8v0`dk>kjdM#p+e4ZESzWc2<}nl9i1HRXFLNsR9>{}NWY|-58u62 zf|CiOk1kg9n_%mUb7uFxhE0Ekm|kN-xQERp39|H@DS9K?*k~ZHMgrDVFiTvVw}b3$ z$o=i{=EGJ;cn$i$G$|xUol+5o;)8>6?AS7oAedzA2&^EnlDEFT&<8w4{fn4Q8p_$uVo@3pq`IKABNitIn7tc^SyS%CYXDCgj~-_KOXPBL&FG9 z#MuCIt$O2@f8$kTKk~=mMRi7HONjC5syju3TkBR~7cw`Di}F?$k6St4UThRL3PVDs zzzt`h3H#=d(Is&JuZw+vDhaxDql2s0O;r{7Y{QCf0EXhw_1bxg!E2*Rl9hUINa`zH z@7shgz4LWbzT-W%3nxn&_Ap>^w-R65{E3f_fyd<18hEeUE}pwFA19%d0Q0$Cb#?Cd zbART^A+uEL;c|TjJEX%ScK*C)PYI?LjY+vL7X*u;QTs=ygPsmZ66%hQiTr=h6JEQR zT_XiNop<#llc8f!`-8-!m4K^2ne1qxgD?NxBIf&aJ1DIsy>~G4=|Z$)T;#v^o>7GA zHs8jT(mu6r%O{?bzGl*s`e)3msK<@g_AdKhQ&!vn2oLmX9Alj$LW7xoYPo30fNvx3ZQH%-cL(?wTE(CoQcR{q1Os zgZ;BRmw@YC*C)l)%|9OTs}D=u6FU1h5zWz8r=?znuYGA~-71n^TN-~Z)vaChh#%`; zxZbx4NIr}bezdEfb1K&{qY!dk{gCZwL&TzU;gRjQebT}=36tHcpN4de8Atwpmu_>T;5W8RiuDC{UY9dXkbuG`%U=t3jG*f`J`N?D>v9= ztHU~V+o{dYI~o(HCpa2RI1&TnkycZw9-eL!KMr+bH!bHE+%B2-+|65;wtI3-;$?!r z{F!`)xIc%V`_>8A*JU~@0$y&1f4HsG>0>xe|AezCh@%^>!qOCfm4Jrzn)XPzty9KL zepk}hA97n5YCX*@6ElbLNT+5c-dm>i@-N+H)@WwqrrQy&4=Y9n&w1^K-8D*;LyJBX zwV2P+OprhB#5={-m`7P_l>Tm;FrDsthHzoM*s#E9HQ7I$5>3RaSO0kbiNKmsqgZ$A z-G^$2%L8Fd*yYB?t%sV~q{D*ixjnb_5<0BmVsAG9&9}g8$knPhcy#ny{in_$>`wli z58`#Pf=>N*C4#AFEvfdPF1}RPt+R)7ym*Q+;kZh{*u z=rx*C_4dGI0TtAWQ}&`e5bv4>?3Ey zk+$;oo2;wu58R`<9wzv%=p(HTa|*&yOOyEnn%5p+{>csIsk-At{Aoh5;1M~caK%S= zB#G0>{lSFi&l%Zxj0fL$b6d>PK-y6vlGav^_IHuF7uNDd=988lk0cMqzL|SI`bkAW zgAJZ13!0jR7YXo*oECLuVigQ0{7N;D$$TJNqe}0lS`LK^00MWK`3p}wJDUD z$tWlMye51cx$mWRh+}~P$H}}-?O}~+p1eD{&7NzA6N~q#JcOd2sF#c;awTb7v3bMd zD{M&MsXG&YF)3r_K!v{ayk7#m{t(G`qn%|v0g@Vf78Wy90ghm`cNj&XIHt)vE z16GxH^npiS9>=fdoRQf=A?Wx7D9k46mk_Jj9OJ%ezVu)I6=USAT^v!?2BGzT`!)fs;}-Y1*~K;ol{ReIfEJf+WxD;16xt`GQEyzS zVye|6{k;bRJ&@oa!JKgwF2g)d6VBkkPySZBmk88;2_3geSyr??1TIt-FHu(gcnwJv z{US(nhmr@ZbGuGyDR`{ejdC8V46>$6vY#R?GD`G>)jMBbg5%5_=BC?5`^&`=kF5g; zO(eWbus@g$rTB-RK}Qyw%j`?*xLX_w^;w88jV~Kg$rtszV4_nvDo7?>SJPdmvHN>l z4#^rUO!`1dqd_NhZqNz!nNcc^7?!r)M^%7k1rog`Z@l7H55KtB$ZMbb%Su3r#*k zW@PmN%-b3)as%7_RjkJUY!vRAYEbI4(NX;tV9(1&{kXj65EgC;d4HR@cJ(8yTfgi3p&Jg!n zK^)=O)7%IW6hX4ZqTAB%;&Ri8$C>qTAnfcfD#3`W!0t!x!V2Of%rx9{9Bdo#6`_It z79%*Gr!(FThFU#j=1`+%J2Wb+Xohj^XPuVIe;%+idfNoQ)wAoD|Ki}zeI(Ug#<)1# zM}W8{0JM6F2VVPa>Q1QNr761ht|ua*{QmqvZ~!qF3HciR9tBIn9keA$h9oM=4PidsvxV|7-IFMof+l=kVMPB?s^%dmWl0ZDx9 z`BacoX5ENxi)+NKB`@jI+A1fuQ-Nw3O76W{3T5Y)#Kq|tt(LAM)H2I?Bna`v=@}d~qR@bo%}=9z=u4SaO8DUOSWy2(V&#_3u44)@>MFG} zJDZL6Kut;`I`y&w!+SP9qE(bPf$cOfpRTuBqO=t}fz;jG#}t^u1_nu)J#T$?I-U<$ zR7b+v@RV@*-6`7%ze**VG_g9r9K0Wa97ki4@O=do2pYv!M3b=dO9;nly?(iH8WCPf z!+B4e*4I}urNsw3LARe)V$cL=!hbKG!mS4#xs}-vAcSe7pKwlYS=5XiYl)EAc!BKP zVNICbscCtV+hM#y4x8GxX`f1XQRgTr{?aK3BT{>iz*g?hx%pn`#*O9CS>iD2eSfMw;RZ;#hMi;xXpHbtZ;zw_@Iq%9a-EBr=$qTYXP1w@zVbcm3< z^EDqHurmHg-TvqydWgV{PwVuToc$MdJK~a9OijGYiZz_+pSTF#-ST^4S{N8M)ELfw zG}iXno+#FRcL~0R+C4}&)f&E1){%D=vRFkglxfBb(_18E($@CBH4Tl;j-<98S^H_1 zyOLX$6=pY)v)6m#PmMVrM%A5QSnA3Rdo0T*d7xs4L?T+_ZakFeq`d+7NY|~DuP+Hv z)8`Np@+Qe7rg%yfB_*OP+tW!afUA#sn(4omHSKd_`1&V^kFu|cCp0v%$!cykEdFQq zyBC~7nH{FVk#$QIYVOrc8HZs*TD6S@9L94+RatoF!E9!xN3vi3cqK^Vi=in~ODk#e zKOT*4{|qL}vO=nU!AK~vkJcrH>+36~h=ymTuGHF&;gUGonK0{w@8jg{`+mH??A`7r zb$YM5>Y)z_#To|#o3428TsdV{kCd~{>nUSSevnK(zoRn6!eysnjLfngCnH1b=ao@Rx`^K*rKsBgUYs)=^iME&xu^PBwlVpyPywf11$UHOG) zn-jCDPC0diIlV7n3i3EekhC>|neq3cnwlxNvW+Uv&qS3+S6%=shzBkY6%!$y&@lb>_p~U#=t2fLuIlmJRrFnnW zErg+ZP8_|ND&%&+9D2gE8;%}(Uk(7Lax_!Z=FP5I^$T^$Yn$2dAxs{c5G5P{WW`v< zr`bx8C7M8FIR`{5erQ?!%rt%@0HMONmqUBv817N4=4Wy~oLypdsHiyP$DAy#7v$1O z2wG^2{!!>uOJyIob*I;Lfy+(*SK0n%5eScP+`c0VQ%GgGJ zB+7)GsY+lnQ<5_!!1H~1GEQFPG(o2!HtJV7JMk^~c%d+cA2|5*r-UrF06o&@Zxd|8 z_mhG*fVHp)-Ymmsh%s6EX*Ib%&^!u$9ogMbY$$g6K4?n$dwPe)MSOId1#L(1-hEMD!=IerV`8 zKM>|&n(VGlSfS+Q5{xs+g+bMlB8Okmf}Gkl}rm}JYhQ`#ATt*?%+arrYv!l zr}P!`f-570Am^h5p@PdYlvtggDQiAee6??YYIv*I^`{cdl|rJ zxrNMWU`MG`&SED9n_VxI;h8&djHUZtP>FXqx=v+Twf1`*y*(@?MAX5qe5=$SQ~7-~B~6U{pR-b@lvWTpK1yPBOeds?d_5fH`7bn+Uv`_06y?9-I7zN|apIj`^tRxFOH?R~V7kJ)&vCG|R6#Yhet-VtU4fF5> z#=}u!%CYrB6x)DUfZkbw?N3#PM?6J1L)Ec}4S6udtOy12E%rKPuFfCNdIcoJQ_11( zmZm#K%7Ek8+IHPPbI!JVwxb=xI#*&e%yMVgV2{+^|MT2p3Z+SQR<7WWg~>Q z)-8T%v|c@Rfui7cO1IJcqD|s9>s?)75EL+G{zUk5l~QfVr!uRTKD%YKyHK0>@bEKp zmutPwAgkw_DQ45PUEhhCh1Wk_>L)AosPX?CDwl;y>dwex;WBq1Ym^XBF*0>S z3RXn-G0jOiHV2~$t66kw#)x>5T0g(AT%K4L{X!M_#Pl3ZGjcHqq-KEEr4~H~^7K5_ z?3o`3#=n;#sM}m^+gi`hNcj|~nYUa@)C2l-W2x6jgt{1@;`tk*9vDpCv(Qji10*>x zQt_XHnR54u58QxZd3LXRkAkcWm>v3rYvi%18-Uco7n-X!(W~{?qR@L;Z1(deIHxR1 zD8Cfy&|{%j7tWLgh(b~q4E>EC4re%V_ql9DMwKKKfz~WiCGGFB98G7E;8mQ=5-Ftbxe-1VP^k~DQ z&?4yI*=5YVGn-_jj0wLg&&Gph63Huo*XuulsM{M~gg5f=W%+wemBAAeI_hA&4PJV{7F*Bs?=l)bcSPu$@AKUetv&kRR4_db{{z` z{#s0{b)orCx5-^Xxx|J%Jm657XKy^w9cow5`mXz7~A$&K*Ldn|9*I1#yWBtb|m^z2_a>KT;PlU?()N^u@xv3 zjdB)P+1;KhsAEcP?P2%rvZ&N|y4xMJ~heh06<7@=x;+o6Ual zFk+`3*nEJCBxG5F*ZxAlV?}gjHp}IcfV#|h$wFIa=Uub_ADbo5?a|~iyy)jq$*Tkr zfS4RV#H3SM9Dn*oaxm3++2s6rp~RjiFuhmUeQ;K!7oHXAc^fp;oxZK#rEB{*hXQ{E z>r{QY119GkMp=ryMHAO%>vx1)k8WW-+c2bl2)d}Yn(CWf7Y22-*=T8^Mu{_iQrI~c zez;?Hr-*8sJ?Hgwo9-=#9MixQ7!g>p0_Oz~U91CS9@{(Jukx1CGh(((E}8-i+%5SX zjriBqsm71kmuFrGP^7YzA3vE-tij}Oz|@gPT~x-PAZJ=M>oOe0XRA>{^-}~VjZc(Q z)F2g}LhfVrkSre_*tlsKSoB~tJo-KFyDR*L%KiXWz1`hFX~PfA8P_0>c}&d@ic#iMfu*GY{r1;Dh7YqG8Nccv~xm%hYAl2k^K71w%bHLIaD;vj?o6DRYApU=-<;w z78?7Uq}(^DmM8DrgvCP)L|q>}k10mskNT3gQs$KF`&9c?W?A@trIewMHbxV|B)z{V zW2c=U8h4rNmbNn@8nsO`Lp*{APD+P)U`y{ z3d(8#{}&0}FZwFTmR7MX(#Qr~@F)(I#Q~4&RlLDK4LRgtHT8t5CyKGWW`kefeW=^o z!vK72yev`vQPvHiGyGE(j?r7v&M>NSZIpCM(!2m#wp9UM(O%z=k{4@7z4ncqlFS z{c>6($5=;i=5%wP-!Xia6b;ubPRZP#De13UI>OxTp%ad-l6&jnELO+TGtZdwoRbsn zSP4GPJg02x_C)vZb3~`7FXVb`847~I7yYRM5jEU{oPW45s)h4^Bmy3z%v$=`3j*j1 zF-c;ZP(2A){-(>x*?WJ4PyL+}RYdG8XV?(oK@?Q$4O?&X@nj9y#IFbRH9sQHB|3=ym zN%;nLD?-ZnYS?9Zc;@Twtkc1;BOFd&1}$tpN;vIk>F4jVkdcuDsGIJQyTyF69P!0b zip#V3E4(GNA3!RvfpeG3#wrs1uh2StqXhWO@29Js_*hSV z%d5}afJ4`Mf7PerRU#FY=0Ad`ODxC|YiUO@9oa;rr(Uolh~;*%xe~+p$K5^ZE;?UP zzY*c@2BQ`gtyqiOseEaY(MyKWK?aCtv?Zy}O#>M`JM0V!2K?0Sss3;6h+dkk&XG7Q z0F&;~Xz)9Fd#5NBucywnxvf^KdVgDxab(f+`;lCw`u_Ypa@c)2U@$jN`M`8d^_c5c zJR9AXtS{Z%uvlzCSu{7v=Y&rt?kJbMf(d>(E6DQ|sR^ zLrMg112r5-Rf#q@=h0#vNbTm!l-ELc{kVN+@8ZJTE-CFsoH^^73F5d)N%A()K2806 zR|padz6A1qr6qwKFyj%eZ6KqR4<~wj>~gcm8^|?2%ZSjiIjK4(@eu{b0zT@IM9q}|;J-kM z$6rjz--o7ppf&*RIrs$+BSDMng6mX`5qF`}I}CyGKX4dkIas6{<}R--@Gx9lxU3fr zNVv__40SL=J#AAXl4?#^OUENSkmA64RbK*j7re?!+o4~&68t@A=z-LgROxo^cNlphOoii>LA|hLGsuU}PVY`wsx5 zB_mKfd*^6Tu)*AF%8`i6zzgS5A zeu0a-o{@Gp`2}fTF5cs!#iY08Kc(k-($rdrumDXPP?!Rd2{kp4F4blZE({nGMtO!9 zK%MRGL?b#{?`CbsB-Z|u&~i2is{i(yC9TzEH?l~%7S{-)N5E}NwI&QX!(Vh$ucxi}4~ba=wCpvMK7FCFBlv}oeuK9$d&1r4dC z(QN3_0`b!@^dKX}>nZ`rw{bcXCr~mavny<-YDcBGT?E#P)+K_L69RY=n1`DI&by>r z=A02Q?c5~p3Pz@ae5E?lS{+aI5Two3v; z8q6t1o0sV=fvwmF1pGUSc%I`^pj@MZ6`-1en0UN!RX+0w_+W@)_&caf z;sznu$+LO4nE-&93?2G|#m2+iMvtx9-p_LmW$ct3Ld;~HSx11P_EdYk+TmzI&|f5L zdLB&HfwS(W-;~85b3xf_lB7-WmF}6aydeeZsUa1H1=n&+sBN?KM8B>!X_Bm2T%@@v<-HkYNhB^dJf z$=%Nfgq3{QM==f#IsZ*B43{MfFv^Q&3=ED3f8LjEX7EjM5e|lntj!UnTk(TIbgTr5yDk~6Xy`&1^NR|{hbt{#pB49-Pu$`OkWeyP&9?XgeY5@^Be}E8OgZ%~2b~Wvc zfmHdO^)g_u2+f>}JQ#G)JG1ZIefw+1_YU4{`y$Z4O{CC%1pLL9di8-I#0>1zsBMq5J3uks%fOL53aaa@aL#$=`X)n82|p-0o5z32)j7tdqc?nqSWl?(^hl$NuU1 zLYZ!%Q~ftT*Pm8HV$PBLvwJ^q`h=5D2f(HQ4$xhDn@gBHH=l>e>+1mS4pbc1D{FqcEmUiJ;2{ef?z^G?6XbH;;3ujR=5 zt4bsZnWy1IfvNgYZMIcGuaHANTA;-`MN|D|%-0Z5#w<>-5)520z!(f0D`)^l`!sXW z>6$>?EqYg3;&%NWNFWWh^}rcz_d7Xg<7z=;V?S<@YW0v`b74P)mXZbCP=EIDd*l044a!H*V0 zt`XY%DDnxd{#yex$+s6vKVoXPXHJO@!mQ2fDz{k2xMY*H@WE5o^Cy=H_Vy!zKz4QU z?jcCH$A$09PP@~+2PxFGY3V5#aXU{-YczTqZO-Yf`2|IVCXWzTZq);PBGBB#YL91& zn3JV*!aPpwQ8#_D$pJ>$=&+@s0oYu%hUa`!4LS@pxY^-Jr_^>!2Zwurtw(ah@3ca* zmc!c8*)#`boN$SMsnuugy~MT#!zXC*0(?EM2LSnObzIMEXJT_@m7a{uuee}pz@JLV(-PUGWr|ft=>hv&37RMdBvm#~X$vfK;G{ z?&Yc9AKvkNPUCSvd7~h_!Xp{K>w0ol?{LnckM?8Am|a`o5x%0 z)vJSgJXXd&UADFszuUV82vQ?p(REyNSuO-lS2&K@5AeK=CIgZn`^31TYPm*sp9cKaInTXdKY(A{IO;wabVce^#+#fDrnd2Np)MSN+p<5mdV20j(n`^O?MxgQ zS`e`{PGuM5q@IyvbDnP-Dy^csa=*1-Jp6yJH>Y|t@Vm1OjKj#bjXC)Hf}9Yt>P ziGmy`&0MZ8y8y`kHf@z&>ovJw_xvaeW|47E!jXI4NNT4N4U6xnUbSO0IjcZz4z|lO z7L+y>YEZE|-(1TNcx{%5SwlRypXkH!7KuD5Cj$YWmG0y%SRLx7Y7tN|rM4HOS?jVTHf( zIDr}{{6GWJc5kPEeQ}nXx-^3$CfmY=dk5e&?UO{GvBaNZp)Z{Nan3&2M!u3l)~H|>kP)#$}fOgh1Ep6%Pncd z{M)4-=18VE(?(4@&6U9V&oWo|c7#JwYM&~D;-ryjdbQL+mg=JrAu)&$rp-=&wg0_ls&_ln<1JRwYM#tuY`VTc*9yAZ zw5@wP55=;Us5Ri}qqw<(|hp8bF6)R#l=tLd3v<>rtjZ?ga9XJ1@d^})>ilZgAyBxC(fCl za^5351_sqJ!69)bk&leD-?}P4_TTq`JMMBn(xPpG;Zu9zKjE!Eh|XcP!bBov@L2_u zARv%93>f@DlwtrJA0Yv?8DkW+cjL&&sTArk!q9@xe+f=7vHj*60>hq&zNV9 zm1PzFR;cXy5Yq84q^AYT*-np!~pHdfK@o<^eF7Mbx+VecK0Z}mDCw)#coI_p)8JTKO9OdkqK|V z8p^vxrUbUfP9RVQEBjstZV7MWlnN=1BqnkU3|6?l&+v4nG|n^OQ+3j4E-H+FPGZN^H-F7<|!-pbH@SQZ$*wNtK-10 zKwIVN$pt;%MUe4B|4a#=y6yCHzTb@?6=STZs3d)!p?^w`LssZ%HY!ID@82V#hTVA5!>q))`q}^5qqpFM6Fjw%%RvOV%$Q9?p1`iEb4& ztwuCBUz=q;2iJxj761ip*FsJgcM{*Go#0NdId5Nq&@!BX8KTF7n3oLbY2+Ir=lz+t z00se6*E^jx{E0)ayP0%fp7tF5H-R`-cgWqC%bI#g^*th2po zbU?_;Yqc7;A|p_P73E|nmOMYBBp$u=QhA^AE_YbflHWnCd;xD9U`u2aa{%Hf!BuC| zNn%0R#qN){S{-KEX<@2ST^}bZKKQEuMk={1@BlGFXnIZWx-!C)2UM7dm(*+NSt3D1|0`&a#6W=<>O8Lbv{EiREJ zewt7<yUP@b*ktAK54doHVTd{K4+=J=B!4=ts3xsT~pPDIM6H+d|z|&~10?bJq5} z!~qA&$IM&FsysKbuQ_Bml!C)13tJcE3*))3=EyCR9-M;rOHQUNyTZmCq97dbhhw#t zS>a9xGkIT8Fh}k7xj!c~xZVens1%8~T_{L$VY4Q6HiSKL1nu)Vsmz?(P2`Q}0Y6^a zK7N6HO`W!Ii=9tg`TP6>P~uQmjSb=O*dQN6nJ+DL$$JW`7(Tp{_wJ`P@8a+0D_$+; zcAl=MCMl8e>ZX6XlPhwZH!309$khXibHoFU?|Na-?^sE-@69b8U2A^Ca>iG0@^bGH zd^Db&X|fFZ!>^xTlB7CNO_X*co8qulHzTb^kpWpIG&g7e#-pRBt zwAUU9JQ;^xi zC$sHYIHyf- z`c7HuE&5e*a}3d5N?gCGocy4i@yl$nSpyZDTw>;j6ESK(jFE_6DPcT-eHHYwfKpM+ zhsMv{rrF_^r8lgl#r>Qwv>)`k=dDg}5EpSQwb>bM_NbWXkL&i+{`Y0QPdfmLSp{nx zWZqu{oFzNVR~Yl5FV{Cp{j_(4T1xj%f%0MxTIjgE>HvkVq|hMoJ@taOL6hr!uGvHh z2a4L|CxGZg3!Cr1O-+>*X5>Jkc`4S;N!LN1VS|rxqrZ8)vc}NcX{9;%A&b@65``Y@ z7Z_17ta_v%mgaeI`IaP*(@N;JKh;y8jC%O&+c|ji?goVM=&%X=3W|H(WDG@^D@yqj z>%f~~;agN4pQh=cVwHK8`MVYOZnzRraXs z`-jyz&;aT2n;rE;OQJlxE%?CDNm(SdoEjzcRDTRwbdhdW33( z28HadIOZWi97RHQ_#Ms4XMd8wT*&;h!&jE!hhrzrU4@^7cP}va@?g=XjCQ}Zbv?lw zzR>=2muCLtX0Dgg%CG{@h>%G(4~XIgc|1T)jE7dxMc8uxj&tsSklb7~B%bF)b~4yqv86zH zn5Nh3km0PoARwZ4RztoV&R~{I`8hT1DF;+-h5|*}lAaH@U;-E`?wSwZW@Oc1($5YH zj=o|lc#B}%$f(IRmzq>Fz{?!A#eB;vyEOB_9SvfWrfgXkOOTWFl#+lM}Bxoq&6Q6<6%sxRn)?+!*7e{$~WG#p6} zs#JlfG+~Cu?GDpiZG;4E1`}a_=QxL#@7W7`!28LGaOpqGX$WL|qv2PR!IXeNJLV`y z%n3envf)Oz(d%W?-C-jzt9xF$<>wv_&3qq64kW-H5slwuDtbrM&S<>qJT^+^_?>D^ zDu#iZ9uDR*;_sf2<#M*{%tW-iwhuCSDBMwH!cPmB2oOJ?p(PL`!Nm<>5lA)3BH_yd zwf*kh8H≀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 literal 0 HcmV?d00001 diff --git a/doc/windowspecific/kwin-detect-window.png b/doc/windowspecific/kwin-detect-window.png new file mode 100644 index 0000000000000000000000000000000000000000..b1efe5a87e618d5f3a09ebdb5773d8030add8a68 GIT binary patch literal 27744 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 j1&KYrhK2QU57$38JB@b0c5H(*OSex)z-R literal 0 HcmV?d00001 diff --git a/doc/windowspecific/kwin-rules-main-n-akregator.png b/doc/windowspecific/kwin-rules-main-n-akregator.png new file mode 100644 index 0000000000000000000000000000000000000000..509af2b7553376ffe32af290e51b6ce00fc3a2d3 GIT binary patch literal 49262 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 literal 0 HcmV?d00001 diff --git a/doc/windowspecific/kwin-window-attributes.png b/doc/windowspecific/kwin-window-attributes.png new file mode 100644 index 0000000000000000000000000000000000000000..4bc0fe3068bc5591a779562ac87af542c81caccf GIT binary patch literal 61505 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

C!HzjxY+)nO9!V_nQ&@bg#>R|SN{3YIPgtvi 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